Using the Maxim DS3231 I2C real-time clock in C on the Raspberry Pi Pico

Chip logo

The Maxim DS3231 I2C real-time clock is a reasonably accurate, inexpensive device, that is easy to interface to the Raspberry Pi Pico. In fact, because its I2C protocol is so straightforward, it can almost be used as an object lesson in I2C programming on the Pico. This article is about programming the device in C; Python support is available elsewhere.

I'm only giving snippets of C source code in this article; full source code is in my GitHub repository.

Note:
In order to reduce complexity, this article only deals with the basic date, time, and temperature features of the DS3231. The device also supports alarms, and has sophisticated calibration procedures which I only touch on.

About the DS3231

The DS3231 is a battery-backed real-time clock with an I2C interface. Typically the battery is a 3V CR2032 cell, but the device has a power supply range of 2.3 to 5.5 volts. Power will be taken from whichever supply -- the battery or external power -- has the higher voltage. So if the IC is powered from the Pico's 3.3V output, the battery voltage must be lower than that.

The IC itself is only available in surface-mount format, but modules with ordinary solder pins are available from on-line sellers for a few pounds. Some modules include a battery holder for a specific type of cell. Some also include a low-capacity flash memory device; if provided, this will probably use a different I2C interface to that of the clock.

The I2C address of the DS3231 is 0x68, and this cannot be changed.

The DS3231 has nineteen registers -- seven for the time and date, nine for setting alarms, two for temperature, and one control register. Although it can be configured to operate in different modes, the default operation will generally be fine when connected to a microcontroller.

As well as date and time, the DS3231 exposes a temperature measurement. It measures the temperature for calibrating its internal oscillator, rather for general applications, and the temperature of the IC is likely to be at least a little higher than ambient.

Setting up the I2C

As with any I2C device, we need to set up the relevant GPIO pins. There are only two -- data (SDA) and clock (SCL).

#include <hardware/i2c.h>
#include <hardware/gpio.h>
...
i2c_init (i2c0, 100000);
gpio_set_function (sda_gpio, GPIO_FUNC_I2C);
gpio_set_function (scl_gpio, GPIO_FUNC_I2C);
gpio_pull_up (sda_gpio);
gpio_pull_up (scl_gpio);

The baud rate of 100000 is a safe value to use, but the device may run faster with tidy wiring and a stable power supply. Note that I2C is an open-collector system, that is, signalling is performed by connecting the data and clock lines to ground, rather than applying actual voltages. This means that the data and clock pins need to be "pulled up" somewhere with a resistor. The I2C device does not have pull-up resistors of its own, but pull-ups can be applied in the Pico.

The DS3231 registers

Using the DS3231 amounts to reading and writing its registers. For basic date and time operation we only need the first seven, R0-R6. To read the temperature we need registers R17 and R18.

Registers can be read and written individually or in bulk. In general, data is read or written until the controller sends an I2C stop.

To read a single register, we write the address without a stop, and then read the value, with a stop. So the code looks like this:

uint8_t reg=<reg_num>;
uint8_t result;
i2c_write_blocking (I2C_PORT, DS3231_ADDRESS, &reg, 1,  true);
i2c_read_blocking  (I2C_PORT, DS3231_ADDRESS, &result, 1, false);

To write a single register, we send the register number and the byte of data, with a stop at the end:

uint8_t b[2] = {<reg_num>, <value>};
i2c_write_blocking (I2C_PORT, DS3231_ADDRESS, &b, 2,  false);

To read a group of registers, we send the register number without a stop, and then read as many bytes as required. To read all the time and date registers (R0 to R6), however, there's no need even to send an address, because 0 is assumed:

uint8_t values[7];
i2c_read_blocking  (I2C_PORT, DS3231_ADDRESS, &values, 7, false);

Some of the date-time registers contain additional data, which the controller needs to deal with. For example, bit 6 of register 2, which contains the "hour" value, indicates whether the clock is in 24-hour or 12-hour timing mode. This bit needs to be filtered out of the hours value.

Register data format

All the date/time register values are in binary-coded decimal. That is, each byte is treated as two four-bit groups, each containing a decimal number. This is inefficient -- The FAT32 filesystem, for example, squeezes an entire date/time into four bytes, while the DS3231 requires seven bytes with its BCD format. The use of BCD makes decoding for display purposes very easy, but it's not a natural format for computation. Happily, conversion from ordinary binary to BCD is simple and fast, even on a microcontroller.

The format of the temperature register is a little different. R17 stores the integer part of the temperature in Celsius, whilst the top two bits of R18 store a number between 0 and 3. This number represents a number of quarter-degree steps. So the overall temperature is given by

 R18 + R19 * 0.25

Date/time register values

The first seven registers, R0-R6, hold the year, month, day, day-of-week, hour, minute, and second values. Note that the year is a two-digit BCD number, so we have to make an assumption about the century. This is not problematic at present, and won't be until 2099. This makes interpretation of the registers easy -- for the year 2022 we just store "22".

It's a bit of an oddity that the DS3231 has separate 'day' (of month) and day-of-week registers. However, because the year is only a two-digit value, in principle the same day-of-month can be associated with different days-of-week (in different centuries). In addition, when the IC is used in simple clock designs, it makes design a lot easier if the IC can supply a day-of-week value without calculation.

In a microcontroller application, it does no harm if the day-of-week register is ignored completely, and neither read nor written.

Calibration and crystal ageing

I won't dwell on these points in this introductory article, but designers should be aware that the DS3231 can be calibrated. This is done by switching different capacitors in parallel with the crystal. It's expected that, as the crystal ages, its oscillation frequency will drift a little. If the application is required to keep to-the-second precision over months, it will need to be calibrated by adjusting the oscillator frequency.

Sample source code

So that's it -- DS3231 interfacing is pretty straightforward. However, it's somewhat difficult to explain in the absract. A complete application example is available in my GitHub repository.

Closing remarks

Reading the date/time repeatedly from the I2C clock IC is inefficient, both in I2C bandwidth and CPU usage. If the date/time is only read once every few seconds, that's probably not a problem. However, if it's done hundreds of times a second, that's a different matter. Each read will take a few milliseconds.

A better approach, perhaps, is to synchronize the I2C real-time clock to some internal, system clock when the Pico starts up. Thereafter, the application might resynchronize at intervals of, say, an hour. The Pico has a built in clock whose data format is oddly similar to that of the DS3231.