198. Mini-project: an RTC clock with an alarm and rate correction
The third device is a clock: time keeps running from a battery without main power, there is an alarm, and the rate is trimmed to the particular crystal. The RTC of the F103 is simpler than in later series: it is just a 32-bit seconds counter. The calendar, an alarm for "every day at 7:30" and rate correction are the job of the program.
As in sections 196 and 197, the logic (clock.c) is separated from the platform (plat_f103.c):
the calendar was run on 20000 random moments and compared with the Python calendar,
the correction on crystal errors from −60 to +150 ppm. The files are in _verify/proj198/,
checked by _verify/proj198_check.py.
Terms used in the documentation
Term Meaning backup domain, VBAT the battery-backed power domain, the battery pin LSE (32.768 kHz) an external watch crystal RTC prescaler (PRL), counter (CNT), alarm (ALR) the RTC prescaler, counter and alarm RTC calibration (CAL) rate correction by skipping pulses backup data register (BKP_DRx) a register that survives a reset ppm (parts per million) one millionth: 1 ppm ≈ 0.0864 s per day
198.1 What the device does
| Pin | Purpose |
|---|---|
| PC14, PC15 | 32768 Hz crystal |
| VBAT | a CR2032 battery — keeps the RTC and the BKP registers alive without main power |
| PA9, PA10 | USART1 console, 115200 baud |
| PC13 | the board LED — the alarm has gone off |
Commands: get — the time; set 2026-09-21 12:34:56 — set it; alarm 07:30 —
an alarm every day; trim +37.0 — enter the measured crystal error in ppm.
198.2 Time is a single number
The RTC counter is 32 bits of seconds. Counting from 2000-01-01 00:00:00 (a Saturday) gives
headroom until 2136-02-07 06:28:15; counting from 1970, it would run out in 2106. Converting
the number to a date and back is done by the functions cal_from_secs and cal_to_secs: division by 86400,
stepping through the years with leap years taken into account, stepping through the months.
The check: 20000 random moments from 2000 to 2135, plus the second after "February 28 23:59:59" for every year — not a single mismatch with the Python calendar, neither in the date nor in the day of the week; the reverse conversion returns the same number.
Why not store the date in registers field by field? Then you would have to handle the carries yourself: 59 seconds → a minute, December 31 → a year. With a single number the counter does the carries, and arithmetic like "8 hours from now" or "how much time has passed" is just a subtraction.
198.3 RTC registers: write through configuration mode
The RTC lives in its own domain and is clocked from the crystal, not from the bus. That is why writes to its registers go through a "gateway":
- wait for
RTOFF = 1— the previous write has finished; CNF = 1— configuration mode;- write
PRL,CNTorALR(each is two 16-bit halves); CNF = 0— the write goes into the RTC domain;- wait for
RTOFF = 1again.
After a processor reset, until the bus clock has synchronised with the RTC, you must not
read: clear RSF and wait until the block sets it by itself. Before all of this,
enable writes to the backup domain: PWR_CR.DBP = 1.
First power-on, or a dead battery? The firmware puts the marker 0xA5A5 into BKP_DR1.
The BKP registers are powered from VBAT and survive a reset. If there is no marker, the domain is reset,
the crystal is started, the prescaler is set to 32767 (32768 / (32767 + 1) = 1 Hz) and
the counter to 0. If the marker is there, the time is running, and nothing may be touched.
Reading 32 bits from two halves. Between reading CNTH and CNTL the counter may
pass from 0xFFFF to 0x10000. So the upper half is read twice: if they do not
match, read again.
198.4 Rate correction: slowing down only
A watch crystal is not accurate: ±20 ppm is common, and that is 1.7 seconds a day, or
almost a minute a month. The F103 has a correction: CAL = how many crystal pulses out of
every 2²⁰ to throw away. The step is 10⁶ / 2²⁰ = 0.954 ppm, the limit is 127 steps,
121 ppm. But only in one direction: you can throw a pulse away, but you cannot add one.
The trick: change the prescaler. A prescaler of 32766 instead of 32767 speeds the clock up by
30.5 ppm; then CAL slows it down to the exact rate. For a crystal running fast by more
than 121 ppm, the other way round — 32768. The program takes the prescaler closest to the standard one
for which CAL gets within half a step:
| Crystal | Uncorrected | PRL | CAL | Residual | Per month |
|---|---|---|---|---|---|
| +150 ppm | +13.0 s/day | 32768 | 125 | +0.26 ppm | +0.66 s |
| +130 ppm | +11.2 s/day | 32768 | 104 | +0.29 ppm | +0.74 s |
| +90 ppm | +7.8 s/day | 32767 | 94 | +0.35 ppm | +0.90 s |
| +37 ppm | +3.2 s/day | 32767 | 39 | −0.19 ppm | −0.50 s |
| +5 ppm | +0.4 s/day | 32767 | 5 | +0.23 ppm | +0.60 s |
| −10 ppm | −0.9 s/day | 32766 | 22 | −0.46 ppm | −1.20 s |
| −25 ppm | −2.2 s/day | 32766 | 6 | −0.20 ppm | −0.53 s |
| −40 ppm | −3.5 s/day | 32765 | 22 | +0.06 ppm | +0.14 s |
| −60 ppm | −5.2 s/day | 32765 | 1 | +0.08 ppm | +0.21 s |
How to measure the crystal error. The CCO bit in BKP_RTCCR outputs the crystal frequency
divided by 64 (512 Hz) on the TAMPER pin (PC13; the LED is not needed there while you measure),
and you measure it with a frequency counter. The frequency counter's reference must be an order of magnitude
more accurate than what is measured: for ±20 ppm, no worse than ±2 ppm. The frequency counter from section 197
does not give that: its 8 MHz crystal is itself off by ±10…30 ppm (section 174), so the instrument will show
the difference between two crystals, and a "correction" based on it can make the rate worse. What works is an instrument
with a temperature-compensated oscillator, or the pulse-per-second output of a satellite navigation
receiver. The second method needs no instruments: set the time,
and a week later compare it with an accurate clock; 1 second in 7 days is 1.65 ppm.
The correction stays in the backup domain: PRL in the RTC, CAL in BKP_RTCCR.
198.5 The alarm: a single comparison
The ALR register is compared with the counter; when they match, the ALRF flag is set. An alarm "every
day at 7:30" is the nearest 7:30 moment strictly after the current one; after it goes off,
the firmware sets the next one — a day ahead. The run:
| Now | Alarm | Goes off |
|---|---|---|
| 2026-09-21 07:29:59 | 07:30 | in 1 s |
| 2026-09-21 07:29:59 | 07:29 | 2026-09-22 07:29:00 — tomorrow |
| 2026-09-21 07:29:59 | 00:00 | 2026-09-22 00:00:00 |
| 2028-02-28 23:00:00 | 06:00 | 2028-02-29 06:00:00 — the leap day |
The alarm is also a number in the RTC domain, so it survives a processor reset. You can check the flag in an interrupt (EXTI line 17) — then the alarm wakes the processor even from Stop mode (section 10).
198.6 Console and firmware
| Command | Reply |
|---|---|
get |
2026-09-21 07:29:59 пн |
set 2026-09-21 12:34:56 |
ok 2026-09-21 12:34:56 пн |
set 2027-02-29 00:00:00 |
? дата: … — 2027 is not a leap year |
alarm 07:30 |
будильник 2026-09-22 07:30:00 вт |
trim +37.0 |
PRL=32767 CAL=039 |
trim +250.0 |
? вне диапазона коррекции |
Built with arm-none-eabi-gcc -Os for Cortex-M3: 5356 bytes of Flash, no variables
in RAM (everything is on the stack). Of that, 2220 bytes (41 %) are software
double arithmetic from the compiler library used to choose the correction: addition and subtraction,
multiplication and division, comparison.
198.7 What to improve
- Choosing the correction in integers: the error in hundredths of a ppm, comparison
in
int32_t— a couple of kilobytes less. - Sleep between events: Stop mode with wake-up by the alarm and by USART reception — the clock runs from a battery for months.
- Time zone and daylight saving time — keep UTC in the firmware, and the zone in a separate BKP register.
- Accurate time from the network (SNTP, section 156.3) — through a network module, since the BluePill has no network of its own; once a day, and the drift between synchronisations gives you the crystal error as a bonus.
198.8 Pitfalls
The time is reset at every reflash. The marker in BKP_DR1 is not checked:
the firmware configures the RTC from scratch every time.
A write to the RTC "does not go through". You did not wait for RTOFF, did not set CNF, or
did not enable DBP — the domain registers are write-protected.
Once every 18 hours the time jumps by 65536 seconds. The upper and lower halves of the counter were read on different sides of a carry.
The clock runs slow, and CAL does not help. CAL only slows it down. Decrease the prescaler.
The alarm went off only once. After it went off, nobody set it again.
The LED on PC13 is dim or disturbs the crystal. The PC13–PC15 pins are powered from the RTC domain and are weak: no faster than 2 MHz at 30 pF and not a current source. Connect the LED only from the supply to the pin and light it with a low level (as on the BluePill and in the firmware).
The clock runs from the battery, but after a battery change it starts from zero. Change the battery while the main power is on — then the domain does not lose power.
198.9 Summary
- Time is a 32-bit counter of seconds since 2000, good until 2136; the calendar is done in software, checked on 20000 moments.
- Writing to the RTC goes through
RTOFFandCNF; reading 32 bits re-checks the upper half. CALonly slows down (up to 121 ppm in steps of 0.954); for a clock that runs slow, decrease the prescaler: 32766 gives +30.5 ppm. The residual is less than 0.5 ppm, about a second a month.- The alarm is the nearest moment strictly after the current one; after it goes off, set it again.
- The BluePill firmware is 5356 bytes, of which 2220 are
doublein choosing the correction.
Next: section 198а "Mini-project: a battery-powered sensor — a year on one CR2032" — the clock runs from the mains; now a device that lives on a battery.