"I'm not proud of being a congenital pain in the ass. But I will take money for it."

Adding a Real Time Clock to a Raspberry Pi

Sun 07 May 2023 | -- (permalink)

For Reasons, I recently needed to install unbound on a Raspberry Pi running 64-bit Debian (not Raspian). For the most part this was trivial: just download the appropriate Debian Raspberry Pi image, follow the instructions, and Shazam, you have something that feels frighteningly like a Debian server running on a Raspberry Pi.

...until you reboot it, and the clock comes up very wrong, because the Raspberry Pi does not include a Real Time Clock (RTC) chip. Which is fine, you can set the time using NTP...except that you're running DNSSEC, which gets tetchy when signatures are six months in the future, so the DNS names of your NTP servers don't resolve, so you can't set your clock, so DNSSEC doesn't work, so ....

There are several ways to break this loop, but the cleanest one is just to add an RTC to the Pi. For a few bucks extra, you can add one good enough to use the Pi as an NTP server (for a few bucks more than that, you can give the Pi a GPS receiver and make it a stratum one NTP server, but I didn't feel a need to go there just yet).

There are a number of RTC add-ons for the Pi. I picked the Adafruit "PiRTC Precise DS3231 Real Time Clock for Raspberry Pi" for US$14.95. This sits on the GPIO pins of a Raspberry Pi, and is self-contained (no other wiring) other than a battery clip for a CR1220 battery. One can buy cheaper RTC packages for the Pi (including a cheaper one from Adafruit) but at these prices the difference between a cheap RTC and an expensive RTC gets lost in the shipping cost.

The PiRTC is a nice small board, and the assembled package fits inside a normal Raspberry Pi case (at least, it fit in the stock CanaKit case for my Pi4B).

The instructions for adding the PiRTC are, sadly, a bit of out date, and are for Raspian, not Debian, so here are the things I had to figure out for myself:

  1. For some reason nobody actually documents which GPIO pins the PiRTC should sit on. It's the ones closest to the corner of the Pi board (the drilled out hole for the corner post is a hint).

  2. For reasons I don't really understand, fiddling around with devicetree (.dtb) files to enable the driver didn't work for me on Debian (nor, apparently, does it work on Ubuntu). Fortunately, there's a simple solution to this that works well enough.

  3. Your Pi may have multiple I2C buses, in fact it probably does. For reasons unknown, none of the documentation I found guessed correctly about which I2C bus my PiRTC would turn out to be on (everything I found said it would be on bus 0 or bus 1, it turned out to be on bus 3).

Installation procedure, assuming you've already installed Debian on the Pi:

  1. Power off the Pi. Insert the CR1220 battery into the PiRTC (the battery clip is marked with a + to show you which way is up), seat the PiRTC on the left end (closest to the corner) of the Pi's GPIO pins, and power the Pi back up.

  2. Add i2c-dev to /etc/modules so that the I2C driver will be loaded automatically. You may also want to add it manually at this point by running modprobe i2c-dev.

  3. You may also need to add dtparam=i2c_arm=on to /boot/firmware/config.txt. I say "may" because the normal kernel upgrade process (which rewrites config.txt) seems to have subsequently removed that line and nothing seems to mind, but if you have trouble finding I2C devices, try adding this.

  4. Install chrony and i2c-tools if you haven't already done so. chrony displaces the NTP client built into systemd; if you'd prefer to use the systemd code, knock yourself out, but you're on your own there, I wanted an NTP server.

  5. Run i2cdetect -l to find out how many I2C buses you have.

    $ sudo i2cdetect -l
    i2c-3   i2c         bcm2835 (i2c@7e804000)              I2C adapter
    i2c-1   i2c         Broadcom STB :                      I2C adapter
    i2c-2   i2c         bcm2835 (i2c@7e205000)              I2C adapter
    i2c-0   i2c         Broadcom STB :                      I2C adapter
  6. Probe I2C buses with i2cdetect -y $bus until you find the bus with your PiRTC. In my case it was I2C bus 3:

    $ sudo i2cdetect -y  3
         0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
    00:                         -- -- -- -- -- -- -- --
    10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    60: -- -- -- -- -- -- -- -- UU -- -- -- -- -- -- --
    70: -- -- -- -- -- -- -- --

    That UU sitting at bus address 0x68 is the PiRTC. The UU indicates that the kernel driver is up and using that port; if the kernel driver isn't up yet, it may show up as 68. If you see a lot of other ports available, be at least little suspicious: on the Pi where I tested this, I saw almost every port available on buses 0 and 1, no ports available on bus 2, and only port 0x68 available on bus 3.

  7. Now that you know what I2C bus and port the RTC is on, you need a very small kludge to poke the kernel into noticing the I2C device, loading the correct kernel module, and creating /dev/rtc0. On Raspian this is handled via the device tree, but for some reason that doesn't work on Debian (or Ubuntu, as documented by Will Hughes). To work around this, one just needs a systemd unit file which does the needful before chrony starts up:

    Description=Enable DS3231 I2C RTC
    ExecStart=/bin/sh -c "echo ds1307 0x68 >/sys/class/i2c-adapter/i2c-3/new_device && exec /sbin/hwclock -s"

    Adjust the I2C bus number (i2c-3 here, for bus 3) as needed. The echo voodoo tells the kernel to probe for the RTC the hwclock command sets Linux's system clock from the RTC, and the Before constraint requests that all this happen before chronyd starts up.

    Install this unit file as /etc/systemd/system/ds3231.service and enable the service by running:

    $ systemctl enable ds3231
  8. Reboot. If all goes well, the system will come up and find the RTC, which you can check by looking at dmesg output and by looking for /dev/rtc0.

Since you haven't previously initialized the RTC, you will have to do something manual to break the DNSSEC / NTP circular dependency for this one last boot cycle (set the clock manually using the date command, point /etc/resolv.conf at some other name server for long enough for chrony to set the clock, whatever), after which you may want to run hwclock -w to initialize the RTC, but from this point on it should be self-maintaining, with chrony keeping the clock up to date and regularly syncing its idea of the current time into the RTC.