Reading STM32F1 real-time clock

11.04.2012 20:22

VESNA is using STM32F1 family of ARM Cortex M3 microcontrollers from ST Microelectronics. These chips have a real-time clock peripheral built-in that can be used to keep track of time and date. In VESNA it uses an external 32.768 kHz tuning-fork quartz oscillator and is running even when the CPU has been power-down to conserve power.

The clock can be used in a number of ways: it can trigger periodic (e.g. system tick) and non-periodic (e.g. alarm) interrupts or you can simply read its value when you need a timestamp in your code. The latter use might appear to be the simplest, but can be especially problematic as the peripheral stores time in no less than 4 16-bit registers spread out over 16 bytes of address space. They can not be read atomically which can lead to subtle race condition bugs where the clock appears to be wrong for duration of one tick. I recently spent quite some time debugging such a bug and would like to share my findings (for best experience, open up reference manual at chapter 18: Real-time clock).

RTC keeps time in two internal registers: the prescaler RTC_DIV counts down periods of the RTC oscillator. Once it reaches zero it is reset and the counter RTC_CNT register gets incremented. These two registers aren't directly accessible - instead each of them has two 16-bit shadow registers on a CPU-accessible bus APB1 that get periodically updated with fresh values synchronously to the CPU bus clock. These are called RTC_DIVH, RTC_DIVL, RTC_CNTH and RTC_CNTL in the documentation.

VESNA uses what is likely the most common configuration: the prescaler is set so that it wraps around each 32768 cycles, making RTC_CNT count seconds while RTC_DIV can be used to keep fractional seconds with around 30 μs precision.

There are two important things to watch out:

  • As mentioned before, you can't read the four values atomically. This means that between reading say RTC_CNTH and RTC_DIVL the values might have changed. In the best case this means you get a value off by one RTC tick. In the worst case, lower registers just overflowed into a RTC_CNTH increment and the value you read is off by 18 hours.
  • RTC_CNT only gets incremented one clock tick after RTV_DIV gets reset.

First, you might be tempted to make the four bus reads atomic by synchronizing the reads with the shadow register update. There is a RTC_CRL_RSF registers synchronized flag that gets set by hardware each time the shadow registers are updated. I have tried this by thinking that if I read the values immediately after it gets set the values won't change for another RTC clock period (which should be plenty, considering RTC runs on the order of 10 kHz and the CPU on the order of 10 MHz). This however does not work reliably for some reason - the documentation only says that this works for the first update of the register anyway. Such synchronization also slows down the clock read-out function and even makes its run time unpredictable.

The second point is actually documented in the datasheet if you look carefully at the timing diagram in the real-time clock chapter. But it is easy to overlook and I wasted more than one day thinking that observing that behavior is due to some problem in my code. It also makes detecting counter overflow somewhat more complicated.

In the end, I went with code like this:

uint16_t divl1 = RTC_DIVL;
uint16_t cnth1 = RTC_CNTH;
uint16_t cntl1 = RTC_CNTL;

uint16_t divl2 = RTC_DIVL;
uint16_t cnth2 = RTC_CNTH;
uint16_t cntl2 = RTC_CNTL;

uint16_t divl, cnth, cntl;

if(cntl1 != cntl2) {
	/* overflow occurred between reads of cntl, hence it
	 * couldn't have occurred before the first read. */
	divl = divl1;
	cnth = cnth1;
	cntl = cntl1;
} else {
	/* no overflow between reads of cntl, hence the
	 * values between the reads are correct */
	divl = divl2;
	cnth = cnth2;
	cntl = cntl2;
}

/* CNT is incremented one RTCCLK tick after the DIV counter
 * gets reset to 32767, so to correct for that increment 
 * the seconds count if DIV just got reset */
uint32_t sec = (((uint32_t)cnth) << 16 | ((uint32_t)cntl));
if(divl == 32767) sec++;

/*
 *        1000000                   15625
 * usec = ------- * (32767 - div) = ----- * (32767 - div)
 *         32768                     512
 */

uint32_t usec = 15625 * (32767 - ((uint32_t)divl)) / 512;

This code makes two assumptions: that RTC_CNTH is unused (i.e. prescaler divides the oscillator frequency by less than 65536) and that the CPU is fast enough to read the four registers in less than one increment of the counter registers. Note that the latter one can be affected by interrupt service routines, so if you have a slow CPU clock, fast running RTC and/or long-running ISRs it might be necessary to disable interrupts while reading the RTC registers.

A version of this function that would work with any prescaler setting would make a nice addition to libopencm3, but I have yet to come up with one elegant enough to warrant a patch.

Note that currently both libopencm3 with rtc_get_counter_val() and rtc_get_prescale_div_val() and STM's FWLIB with RTC_GetCounter() and RTC_GetDivider() get this wrong. Also they don't support getting both values in a consistent way. There is a discussion about this issue on STM32 forums and the solution given there is functionally identical to mine (though I don't like the potential for a goto-induced infinite loop).

Posted by Tomaž | Categories: Digital

Comments

Just to let you know that I used your code in VGA demo project. See https://github.com/abelykh0/VGA-demo-on-bluepill.

Posted by Andrey Belykh

Add a new comment


(No HTML tags allowed. Separate paragraphs with a blank line.)