Back to Technology

STM32 Part 11: RTC & Calendar

March 31, 2026 Wasil Zafar 24 min read

The STM32 Real-Time Clock is more than just a timekeeping peripheral — it's your always-on backup domain, tamper detection engine, and autonomous wakeup scheduler. Master it fully.

Table of Contents

  1. STM32 RTC Architecture
  2. Reading Date & Time
  3. RTC Alarms
  4. Backup Registers
  5. RTC Tamper Detection
  6. Smooth Calibration
  7. Exercises
  8. RTC Configuration Canvas
  9. Conclusion & Next Steps
Series Overview: This is Part 11 of our 18-part STM32 Unleashed series. Building on the low-power foundations of Part 10, we now master the RTC — the one peripheral that never truly sleeps, running on VBAT even when everything else is powered off.

STM32 Unleashed: HAL Driver Development

Your 18-step learning path • Currently on Step 11
1
Architecture & CubeMX Setup
STM32 family, clock tree, HAL vs LL, CubeMX workflow, first project
Completed
2
GPIO & Button Debounce
GPIO modes, pull-up/down, EXTI, software debounce, HAL_GPIO_ReadPin
Completed
3
UART Communication
Polling, interrupt, DMA modes, printf retargeting, ring buffers
Completed
4
Timers, PWM & Input Capture
TIM basics, PWM generation, input capture, encoder mode
Completed
5
ADC & DAC
Single/continuous conversion, DMA, injected channels, DAC waveforms
Completed
6
SPI Protocol
SPI master/slave, full-duplex, DMA transfers, sensor drivers
Completed
7
I2C Protocol
I2C master, 7/10-bit addressing, DMA, multi-master, error handling
Completed
8
DMA & Memory Efficiency
DMA streams, circular mode, memory-to-memory, zero-copy patterns
Completed
9
Interrupt Management & NVIC
Priority grouping, preemption, ISR design, HAL callbacks, latency
Completed
10
Low-Power Modes
Sleep, Stop, Standby modes, RTC wakeup, LP UART, power profiling
Completed
11
RTC & Calendar
RTC configuration, alarms, backup registers, calendar subseconds
You Are Here
12
CAN Bus
FDCAN/bxCAN, filters, message frames, error handling, automotive use
13
USB CDC Virtual COM Port
USB FS/HS, CDC class, virtual serial, control transfers, descriptors
14
FreeRTOS Integration
Tasks, queues, semaphores, mutexes, CMSIS-RTOS2 wrapper, stack sizing
15
Bootloader Development
Custom IAP bootloader, UART/USB DFU, flash programming, jump-to-app
16
External Storage: SD & QSPI Flash
FATFS on SD card, QSPI NOR flash, memory-mapped execution, wear levelling
17
Ethernet & TCP/IP Stack
LwIP integration, DHCP, TCP server, HTTP, MQTT, Ethernet DMA descriptors
18
Production Readiness
Watchdog, HardFault handler, flash option bytes, code signing, CI/CD

STM32 RTC Architecture

The STM32 Real-Time Clock (RTC) is architecturally separated from the rest of the microcontroller. It lives in the backup power domain, clocked from a dedicated source (LSE or LSI), and powered from VBAT — meaning it continues counting seconds even when the main VDD supply is off. This makes it the backbone of any design that requires persistent timekeeping, periodic autonomous wakeup, or tamper-evident logging.

RTC Backup Domain

The backup domain is isolated from the rest of the MCU by write protection. Before you can configure any RTC register, you must:

  1. Enable the Power Control clock: __HAL_RCC_PWR_CLK_ENABLE()
  2. Disable backup domain write protection: HAL_PWR_EnableBkUpAccess()
  3. Enable the RTC clock source (LSE or LSI) through RCC_OscInitTypeDef
  4. Select the RTC clock source: RCC_PeriphCLKInitTypeDef.RTCClockSelection
  5. Enable the RTC clock: __HAL_RCC_RTC_ENABLE()

The write protection is designed to prevent accidental overwrite of the running calendar, but it also means that code which skips the unlock sequence will silently fail to update RTC registers — one of the more frustrating debugging experiences on STM32.

Clock Sources and Prescalers

The RTC clock source determines both the accuracy of the calendar and the minimum achievable current in low-power modes:

RTC Clock Source Accuracy Power Available in Standby? Startup Time
LSE (32.768 kHz crystal) ±20–100 ppm (crystal dependent) ~0.8 µA Yes (VBAT powered) 100 ms – 2 s (crystal oscillator startup)
LSI (internal ~32 kHz) ±30% at room temp (factory calibration ±15%) ~0.5 µA Yes (internal, no crystal needed) ~40 µs
HSE/32 Matches HSE crystal (typically ±10–50 ppm) HSE must remain on (~300 µA+) No (HSE stops in Stop/Standby) <1 ms (if HSE already running)

Prescaler formula: The RTC generates the 1 Hz calendar tick by dividing its input clock through two cascaded prescalers:

f_CK_SPRE = f_RTCCLK / ((PREDIV_A + 1) × (PREDIV_S + 1))

For the standard 32.768 kHz LSE: PREDIV_A = 127 (7-bit async), PREDIV_S = 255 (15-bit sync). So: 32768 / (128 × 256) = 32768 / 32768 = 1 Hz exactly. The synchronous prescaler output (ck_apre at PREDIV_S+1 = 256 Hz) also drives the sub-second counter (SSR), giving millisecond-resolution timestamps.

HAL_RTC_Init and Calendar Initialisation

/* Full RTC initialisation with LSE, set date 2026-01-15, time 14:30:00 */
#include "stm32l4xx_hal.h"

RTC_HandleTypeDef hrtc;

HAL_StatusTypeDef rtc_init_and_set_datetime(void)
{
    HAL_StatusTypeDef status;

    /* Step 1: Enable Power and Backup domain access */
    __HAL_RCC_PWR_CLK_ENABLE();
    HAL_PWR_EnableBkUpAccess();

    /* Step 2: Enable LSE oscillator */
    RCC_OscInitTypeDef osc = {0};
    osc.OscillatorType = RCC_OSCILLATORTYPE_LSE;
    osc.LSEState       = RCC_LSE_ON;
    osc.PLL.PLLState   = RCC_PLL_NONE;
    status = HAL_RCC_OscConfig(&osc);
    if (status != HAL_OK) return status;

    /* Step 3: Select LSE as RTC clock source */
    RCC_PeriphCLKInitTypeDef pclk = {0};
    pclk.PeriphClockSelection = RCC_PERIPHCLK_RTC;
    pclk.RTCClockSelection    = RCC_RTCCLKSOURCE_LSE;
    status = HAL_RCCEx_PeriphCLKConfig(&pclk);
    if (status != HAL_OK) return status;

    /* Step 4: Enable RTC clock */
    __HAL_RCC_RTC_ENABLE();

    /* Step 5: Initialise RTC — configure prescalers for 1 Hz calendar tick */
    hrtc.Instance            = RTC;
    hrtc.Init.HourFormat     = RTC_HOURFORMAT_24;
    hrtc.Init.AsynchPrediv   = 127;   /* PREDIV_A: 32768/(128) = 256 Hz  */
    hrtc.Init.SynchPrediv    = 255;   /* PREDIV_S: 256/256   = 1 Hz     */
    hrtc.Init.OutPut         = RTC_OUTPUT_DISABLE;
    hrtc.Init.OutPutRemap    = RTC_OUTPUT_REMAP_NONE;
    hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
    hrtc.Init.OutPutType     = RTC_OUTPUT_TYPE_OPENDRAIN;
    status = HAL_RTC_Init(&hrtc);
    if (status != HAL_OK) return status;

    /* Step 6: Set the calendar date — 2026-01-15 Thursday */
    RTC_DateTypeDef date = {0};
    date.Year    = 26;                  /* Last 2 digits of year */
    date.Month   = RTC_MONTH_JANUARY;
    date.Date    = 15;
    date.WeekDay = RTC_WEEKDAY_THURSDAY;
    status = HAL_RTC_SetDate(&hrtc, &date, RTC_FORMAT_BIN);
    if (status != HAL_OK) return status;

    /* Step 7: Set the time — 14:30:00 */
    RTC_TimeTypeDef time = {0};
    time.Hours   = 14;
    time.Minutes = 30;
    time.Seconds = 0;
    time.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
    time.StoreOperation = RTC_STOREOPERATION_RESET;
    return HAL_RTC_SetTime(&hrtc, &time, RTC_FORMAT_BIN);
}

Reading Date & Time

Reading the RTC is not as straightforward as reading a simple register. The STM32 RTC uses shadow registers that are periodically updated from the internal BCD counter. This shadow register mechanism prevents you from reading a time that spans a carry boundary — without it, you might read 23:59:59 for the minutes/seconds but 00:00 for hours if a midnight transition happened between the two reads.

The Shadow Register Locking Rule

The HAL enforces a critical read order: always call HAL_RTC_GetTime() before HAL_RTC_GetDate(). Here is why: reading the RTC_TR register (time) locks the shadow registers from updating. Reading RTC_DR (date) releases the lock. If you read date first without first reading time, the shadows will keep updating between your two reads and you can get an inconsistent pair. The HAL enforces this by returning HAL_ERROR if you call GetDate without a prior GetTime.

Sub-Second Counter (SSR)

The SSR (Sub-Second Register) counts down from PREDIV_S to 0, then wraps back and increments the seconds counter. The millisecond value from SSR is therefore: ms = (PREDIV_S - SSR) * 1000 / (PREDIV_S + 1). For PREDIV_S = 255: ms = (255 - SSR) * 1000 / 256 ≈ (255 - SSR) * 3.906 ms per LSB. This gives ~4 ms resolution — suitable for event logging but not for high-frequency measurement. If you need sub-millisecond resolution, use a hardware timer (TIM) alongside the RTC second tick.

/* Read RTC time and date, format as ISO 8601, transmit over UART */
/* CRITICAL: Always GetTime before GetDate to maintain shadow lock  */
#include "stm32l4xx_hal.h"
#include <stdio.h>

extern RTC_HandleTypeDef hrtc;
extern UART_HandleTypeDef huart2;

void rtc_print_timestamp(void)
{
    RTC_TimeTypeDef sTime = {0};
    RTC_DateTypeDef sDate = {0};

    /* Step 1: Read time FIRST — this locks shadow registers */
    if (HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK)
    {
        HAL_UART_Transmit(&huart2, (uint8_t *)"RTC time read error\r\n", 21, 100);
        return;
    }

    /* Step 2: Read date SECOND — this releases the shadow lock  */
    if (HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN) != HAL_OK)
    {
        HAL_UART_Transmit(&huart2, (uint8_t *)"RTC date read error\r\n", 21, 100);
        return;
    }

    /* Step 3: Compute milliseconds from SSR (sub-second register)  */
    /* SSR counts DOWN: ms = (PREDIV_S - SSR) * 1000 / (PREDIV_S+1) */
    uint32_t prediv_s = hrtc.Init.SynchPrediv;    /* 255 for LSE setup  */
    uint32_t ms = (prediv_s - sTime.SubSeconds) * 1000U / (prediv_s + 1U);

    /* Step 4: Format as ISO 8601: YYYY-MM-DDTHH:MM:SS.mmm */
    char buf[32];
    int len = snprintf(buf, sizeof(buf),
                       "20%02d-%02d-%02dT%02d:%02d:%02d.%03lu\r\n",
                       (int)sDate.Year,
                       (int)sDate.Month,
                       (int)sDate.Date,
                       (int)sTime.Hours,
                       (int)sTime.Minutes,
                       (int)sTime.Seconds,
                       ms);

    HAL_UART_Transmit(&huart2, (uint8_t *)buf, (uint16_t)len, 200);
}
BCD vs Binary Format: The RTC hardware stores time in BCD (Binary Coded Decimal) — each decimal digit occupies a nibble. HAL abstracts this when you use RTC_FORMAT_BIN in your Get/Set calls. If you read the raw RTC_TR register directly (e.g. for speed in an ISR), you must manually decode the BCD: hours = ((RTC->TR & 0x300000) >> 20) * 10 + ((RTC->TR & 0xF0000) >> 16). Using RTC_FORMAT_BIN with HAL avoids this and is recommended for all application code.

RTC Alarms

The STM32 RTC provides two independent alarm channels, Alarm A and Alarm B, each capable of triggering an interrupt or an output signal at a configurable calendar match point. The flexibility comes from the alarm mask register, which lets you specify which fields of the current time must match the alarm fields for the alarm to fire.

Alarm Mask Configuration

The alarm comparator always compares all four fields (date/weekday, hours, minutes, seconds). The mask register selects which comparisons actually count:

Mask Setting Fields Compared Alarm Period Typical Use Case
RTC_ALARMMASK_ALL None (all masked) Every second Periodic 1-second tick
RTC_ALARMMASK_DATEWEEKDAY | HOURS | MINUTES Seconds only Every minute at SS match Alarm at :30 of every minute
RTC_ALARMMASK_DATEWEEKDAY | HOURS Minutes + Seconds Every hour at MM:SS match Hourly data log at :00:00
RTC_ALARMMASK_DATEWEEKDAY Hours + Minutes + Seconds Every day at HH:MM:SS match Daily wakeup at 08:00:00
RTC_ALARMMASK_NONE All fields including date Once at exact date/time One-shot scheduled event

Alarm Interrupt Callback

Alarm A uses the RTC_Alarm_IRQn interrupt, shared with Alarm B. The HAL dispatches to HAL_RTC_AlarmAEventCallback() or HAL_RTCEx_AlarmBEventCallback() depending on which alarm fired. You must clear the alarm interrupt flag within the callback (HAL does this automatically when called from HAL_RTC_AlarmIRQHandler()).

/* Set Alarm A to fire at :30 seconds of every minute (mask all except seconds) */
#include "stm32l4xx_hal.h"

extern RTC_HandleTypeDef hrtc;

HAL_StatusTypeDef rtc_set_alarm_every_minute_at_30s(void)
{
    RTC_AlarmTypeDef alarm = {0};

    /* Alarm time: seconds = 30, all other fields irrelevant due to mask */
    alarm.AlarmTime.Hours   = 0;
    alarm.AlarmTime.Minutes = 0;
    alarm.AlarmTime.Seconds = 30;
    alarm.AlarmTime.SubSeconds = 0;
    alarm.AlarmTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
    alarm.AlarmTime.StoreOperation = RTC_STOREOPERATION_RESET;

    /* Mask everything except seconds: alarm fires at :30 of every minute */
    alarm.AlarmMask  = RTC_ALARMMASK_DATEWEEKDAY |
                       RTC_ALARMMASK_HOURS        |
                       RTC_ALARMMASK_MINUTES;

    /* Sub-second mask: compare all SSR bits (exact sub-second match disabled) */
    alarm.AlarmSubSecondMask = RTC_ALARMSUBSECONDMASK_ALL;

    /* Use date (not weekday) comparison — irrelevant since date is masked */
    alarm.AlarmDateWeekDaySel = RTC_ALARMDATEWEEKDAYSEL_DATE;
    alarm.AlarmDateWeekDay    = 1;

    /* Assign to Alarm A */
    alarm.Alarm = RTC_ALARM_A;

    /* Register alarm with interrupt enabled */
    if (HAL_RTC_SetAlarm_IT(&hrtc, &alarm, RTC_FORMAT_BIN) != HAL_OK)
        return HAL_ERROR;

    /* Enable the RTC alarm interrupt in NVIC */
    HAL_NVIC_SetPriority(RTC_Alarm_IRQn, 5, 0);
    HAL_NVIC_EnableIRQ(RTC_Alarm_IRQn);

    return HAL_OK;
}

/* Alarm A callback — called from HAL_RTC_AlarmIRQHandler in stm32l4xx_it.c */
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc_cb)
{
    UNUSED(hrtc_cb);
    /* Called at :30 seconds of every minute                          */
    /* Perform periodic task: read sensor, update display, etc.       */
    perform_periodic_task();
}

Backup Registers

The STM32 RTC includes 20 (STM32F4) to 32 (STM32H7) 32-bit backup registers (BKP_DRx) that reside in the VBAT-powered backup domain. They are not flash — no erase/write cycle limits — and they persist through:

  • VDD power-off (as long as VBAT is supplied from a coin cell or supercap)
  • System reset (NRST pin) that does not reset the backup domain
  • Standby mode wakeup
  • Watchdog resets (on most families)

They are cleared on a backup domain reset, which occurs when VBAT is also removed, when the BDRST bit is set in RCC_BDCR, or when a tamper event occurs (if tamper-clear is enabled).

Practical uses of backup registers:

  • Boot counter: Increment on every cold start — detect crash loops (if count increments faster than expected, the watchdog is firing).
  • Dirty-shutdown flag: Write a magic word on entry to a critical section; clear on clean exit. If the magic word is present on boot, the previous run crashed mid-operation.
  • Last known-good firmware version: Store the version of the firmware that last ran successfully. Used by bootloaders for automatic rollback.
  • Sensor calibration offset: Write factory calibration values once at manufacturing. Survives power cycles without flash writes.
  • Standby state serialisation: Store small data structures (last sensor reading, configuration flags) before entering Standby. Restore on wakeup without an external EEPROM.
/* Boot counter in BKP_DR0 + dirty-shutdown detection via BKP_DR1 magic word */
#include "stm32l4xx_hal.h"

extern RTC_HandleTypeDef hrtc;

#define DIRTY_MAGIC   0xCAFEBABEU
#define CLEAN_MAGIC   0x600DC0DEU
#define BKP_BOOT_CNT  RTC_BKP_DR0
#define BKP_DIRTY     RTC_BKP_DR1
#define BKP_FW_VER    RTC_BKP_DR2
#define FIRMWARE_VER  0x00010203U    /* Major.Minor.Patch = 1.2.3 */

void bkp_on_boot_check(void)
{
    /* Enable backup domain write access (required for BKUPRead/Write) */
    __HAL_RCC_PWR_CLK_ENABLE();
    HAL_PWR_EnableBkUpAccess();

    uint32_t boot_count  = HAL_RTCEx_BKUPRead(&hrtc, BKP_BOOT_CNT);
    uint32_t dirty_magic = HAL_RTCEx_BKUPRead(&hrtc, BKP_DIRTY);

    /* Detect dirty shutdown from previous run */
    if (dirty_magic == DIRTY_MAGIC)
    {
        /* Previous run did not shut down cleanly — log and handle */
        log_warn("Dirty shutdown detected! Boot count was %lu", boot_count);
        /* Could trigger a self-test, re-calibrate, or enter safe mode */
    }

    /* Increment boot counter */
    HAL_RTCEx_BKUPWrite(&hrtc, BKP_BOOT_CNT, boot_count + 1U);

    /* Store current firmware version */
    HAL_RTCEx_BKUPWrite(&hrtc, BKP_FW_VER, FIRMWARE_VER);

    /* Mark this boot as "dirty" — clear at graceful shutdown */
    HAL_RTCEx_BKUPWrite(&hrtc, BKP_DIRTY, DIRTY_MAGIC);

    log_info("Boot #%lu, firmware 0x%08lX", boot_count + 1U, FIRMWARE_VER);
}

/* Call at graceful shutdown / before entering Standby */
void bkp_mark_clean_shutdown(void)
{
    HAL_RTCEx_BKUPWrite(&hrtc, BKP_DIRTY, CLEAN_MAGIC);
}

RTC Tamper Detection

The STM32 RTC tamper peripheral monitors one or two TAMP pins for unauthorised physical access events. When a tamper event is detected, the hardware can:

  • Generate an interrupt to wake the CPU (even from Stop or Standby)
  • Record the exact timestamp in the tamper timestamp registers (RTC_TSTR / RTC_TSDR)
  • Automatically erase all backup registers, destroying any sensitive keys or calibration data stored in the VBAT domain

The tamper feature is particularly useful for:

  • Enclosure intrusion detection: Connect a microswitch on the TAMP1 pin wired to GND when the case is closed. Opening the enclosure pulls TAMP1 high (via pull-up resistor), triggering the tamper event and erasing cryptographic keys from backup registers.
  • Anti-rollback for secure bootloaders: Store the minimum acceptable firmware version in a backup register. A tamper event resets it, forcing full factory re-provisioning.
  • Production audit logging: Record the first power-on timestamp in BKP registers. Subsequent tamper events log additional timestamps for factory analysis.
/* Configure TAMP1 on falling edge; callback erases BKP registers + logs time */
#include "stm32l4xx_hal.h"

extern RTC_HandleTypeDef hrtc;
volatile uint8_t tamper_event_flag = 0;

HAL_StatusTypeDef rtc_configure_tamper(void)
{
    RTC_TamperTypeDef tamper_cfg = {0};

    /* TAMP1 pin, falling edge (pin is pulled high, contact to GND triggers) */
    tamper_cfg.Tamper                  = RTC_TAMPER_1;
    tamper_cfg.Trigger                 = RTC_TAMPERTRIGGER_FALLINGEDGE;
    tamper_cfg.NoErase                 = RTC_TAMPER_ERASE_BACKUP_ENABLE; /* auto-erase BKP */
    tamper_cfg.MaskFlag                = RTC_TAMPERMASK_FLAG_DISABLE;
    tamper_cfg.Filter                  = RTC_TAMPERFILTER_2SAMPLE;       /* glitch filter */
    tamper_cfg.SamplingFrequency       = RTC_TAMPERSAMPLINGFREQ_RTCCLK_DIV256;
    tamper_cfg.PrechargeDuration       = RTC_TAMPERPRECHARGEDURATION_1RTCCLK;
    tamper_cfg.TamperPullUp            = RTC_TAMPER_PULLUP_ENABLE;
    tamper_cfg.TimeStampOnTamperDetection = RTC_TIMESTAMPONTAMPERDETECTION_ENABLE;

    if (HAL_RTCEx_SetTamper_IT(&hrtc, &tamper_cfg) != HAL_OK)
        return HAL_ERROR;

    /* Enable TAMP/Stamp/CSS interrupt in NVIC */
    HAL_NVIC_SetPriority(TAMP_STAMP_IRQn, 3, 0);
    HAL_NVIC_EnableIRQ(TAMP_STAMP_IRQn);

    return HAL_OK;
}

/* Tamper event callback — BKP registers already erased by hardware */
void HAL_RTCEx_Tamper1EventCallback(RTC_HandleTypeDef *hrtc_cb)
{
    tamper_event_flag = 1;

    /* Read the tamper timestamp to know when intrusion occurred */
    RTC_TimeTypeDef ts_time = {0};
    RTC_DateTypeDef ts_date = {0};
    HAL_RTCEx_GetTimeStamp(hrtc_cb, &ts_time, &ts_date, RTC_FORMAT_BIN);

    /* Log tamper event — BKP regs already cleared by hardware */
    /* Note: backup registers are now erased — do NOT attempt to read them */
    log_security_event("TAMPER at 20%02d-%02d-%02dT%02d:%02d:%02d",
                       ts_date.Year, ts_date.Month,  ts_date.Date,
                       ts_time.Hours, ts_time.Minutes, ts_time.Seconds);

    /* Optionally enter a locked state requiring factory re-provisioning */
}

RTC Smooth Calibration

No 32.768 kHz crystal oscillates at exactly the correct frequency. Manufacturing tolerances, PCB parasitics (load capacitance on XCIN/XCOUT pins), and temperature all shift the oscillation frequency, producing clock drift that accumulates over days and weeks. The STM32 RTC calibration register (CALR) compensates for this by inserting or removing clock pulses over a 220 RTC clock cycle window (~32 seconds for LSE).

Calibration range: The CALR register can add up to +488.5 ppm (by removing pulses in positive calibration mode — RTC_SMOOTHCALIB_PLUSPULSES_SET) or subtract up to -488.5 ppm (removing pulses in PLUSPULSES_RESET mode). Fine steps of approximately 0.954 ppm per bit. Total range: approximately ±488 ppm, covering the vast majority of LSE crystals and most temperature drift scenarios.

Measuring drift: The most reliable method is comparison against a GPS PPS (Pulse Per Second) signal or an NTP-synchronised time source. Connect the GPS PPS to a GPIO with timestamp input capture (using the RTC timestamp input or a TIM input capture). Over 24 hours, the drift in seconds translates directly to ppm: drift_ppm = (drift_seconds / 86400) × 106.

/* Apply smooth calibration based on measured drift in ppm */
/* Positive ppm = clock runs FAST (subtract pulses to slow it down) */
/* Negative ppm = clock runs SLOW (add pulses to speed it up)       */
#include "stm32l4xx_hal.h"

extern RTC_HandleTypeDef hrtc;

/* ppm_drift: positive = fast (add pulses), negative = slow (remove pulses) */
HAL_StatusTypeDef rtc_apply_calibration(int32_t ppm_drift)
{
    /* Each CALMS step is 0.9537 ppm — round to nearest integer  */
    /* Maximum CALMS value is 511 (9-bit register)               */
    const int32_t PPM_PER_STEP_NUM = 9537;   /* 0.9537 * 10000   */
    const int32_t PPM_PER_STEP_DEN = 10000;

    uint32_t cal_mode;
    int32_t  cal_value;

    if (ppm_drift >= 0)
    {
        /* Clock is FAST: use PLUSPULSES_RESET to remove pulses (slow down) */
        /* Effective correction: -CALMS * 0.9537 ppm                        */
        cal_mode  = RTC_SMOOTHCALIB_PLUSPULSES_RESET;
        cal_value = (ppm_drift * PPM_PER_STEP_DEN) / PPM_PER_STEP_NUM;
    }
    else
    {
        /* Clock is SLOW: use PLUSPULSES_SET to add 512 pulses per window   */
        /* then subtract CALMS, giving net positive correction              */
        /* Effective correction: +(512 - CALMS) * 0.9537 ppm               */
        cal_mode  = RTC_SMOOTHCALIB_PLUSPULSES_SET;
        int32_t net_pulses = 512 + (ppm_drift * PPM_PER_STEP_DEN) / PPM_PER_STEP_NUM;
        cal_value = (net_pulses < 0) ? 0 : (net_pulses > 511) ? 511 : net_pulses;
        cal_value = 512 - cal_value;
    }

    /* Clamp to valid range [0, 511] */
    if (cal_value < 0)   cal_value = 0;
    if (cal_value > 511) cal_value = 511;

    /* Apply calibration — takes effect at the next 2^20 cycle boundary */
    return HAL_RTCEx_SetSmoothCalib(&hrtc,
                                     RTC_SMOOTHCALIB_PERIOD_32SEC,
                                     (uint32_t)cal_mode,
                                     (uint32_t)cal_value);
}
Temperature Compensation: A crystal's frequency-temperature curve is parabolic (for AT-cut crystals used in most 32.768 kHz modules). If your device operates across a wide temperature range (−20°C to +85°C), a static calibration value will be optimal only at the turnover temperature (~25°C). For precision timekeeping across temperature, read an on-chip or external temperature sensor and look up the correction in a calibration table stored in flash — this technique, called DTCXO (Digitally Temperature-Compensated Xtal Oscillator), is used in professional RTCs and can achieve <1 ppm accuracy.

Exercises

Beginner Exercise 1: Basic RTC Calendar and UART Output

Configure the RTC with LSE (if available on your board) or LSI as a fallback. Set a specific date and time — use today's date and the current time. Print the formatted timestamp (YYYY-MM-DDTHH:MM:SS) over UART every second using the RTC wakeup timer (not a busy-wait delay). After exactly 5 minutes, compare the reported RTC time to your computer's system clock. If using LSI, you should observe visible drift (10–60 seconds over 24 hours is normal). If using LSE with a crystal, drift should be <1 second over 5 minutes.

Success criteria: Stable UART output updating once per second. Correct date roll-over when tested across midnight. LSE drift <0.5 seconds over 5 minutes; LSI drift <5 seconds.

Intermediate Exercise 2: Standby Datalogger with Alarm Ring Buffer

Implement a wake-on-alarm datalogger using Standby mode. On each wakeup: read the current RTC timestamp plus one sensor value (temperature or ADC reading). Store the pair in a "ring buffer" implemented using 20 backup registers — 10 entries of 2 registers each (timestamp index + value). Set Alarm A to fire 5 minutes later (mask all but hours/minutes/seconds). Enter Standby. On the next boot, detect the Standby flag, reconstruct the ring buffer from backup registers, and print all logged entries over UART before re-entering the loop.

Success criteria: After 50 minutes (10 wakeup cycles), all 10 entries visible over UART. SRAM contents are NOT used — only backup registers. Verify by pulling VDD and re-applying — data must survive.

Advanced Exercise 3: NTP-Synchronised Clock with Drift Calibration

Build an NTP-synchronised clock on an STM32L4 board connected to a host PC via UART. The PC-side script (Python, any platform) queries an NTP server and sends the Unix timestamp over UART in a simple binary or ASCII protocol. The STM32 receives the timestamp, converts it to RTC date/time, and sets the RTC. Measure the drift over 24 hours by sending periodic timestamps from the STM32 and comparing them to the PC's NTP time. Calculate the drift in ppm. Apply HAL_RTCEx_SetSmoothCalib() with the computed calibration value. Measure drift for another 24 hours — target improvement to <1 second/day (<11.6 ppm).

Success criteria: Before calibration: measurable drift visible within 1 hour with LSI, or within 12 hours with LSE. After calibration: drift <1 second over a 24-hour window for LSE; <60 seconds for LSI (LSI is fundamentally limited and cannot achieve 1 s/day with calibration alone — this is a teaching point about clock source selection).

RTC Configuration Canvas

Use this canvas to document your STM32 RTC design — clock source selection, alarm patterns, backup register allocation, tamper configuration, and calibration offset. Export for design review or hand-off documentation.

Conclusion & Next Steps

The STM32 RTC is deceptively rich in capability — far beyond simple timekeeping. In this article we have covered every major feature:

  • RTC architecture: The backup domain powered by VBAT, the write-protection unlock sequence, and the relationship between clock source (LSE vs LSI vs HSE/32), prescalers, and calendar accuracy.
  • Prescaler configuration: The formula f_CK_SPRE = f_RTCCLK / ((PREDIV_A+1)×(PREDIV_S+1)) — standard values of 127/255 for 32.768 kHz LSE giving exactly 1 Hz, with the synchronous prescaler also generating the sub-second (SSR) counter at 256 Hz.
  • Reading time safely: The shadow register locking rule — always HAL_RTC_GetTime() before HAL_RTC_GetDate() — and computing milliseconds from the SSR counter for high-resolution timestamps.
  • RTC alarms: Alarm A and Alarm B with flexible mask configurations ranging from every second to a one-shot exact date/time match. Weekly schedulers using RTC_ALARMDATEWEEKDAYSEL_WEEKDAY.
  • Backup registers: 20–32 × 32-bit VBAT-powered registers surviving power-off and Standby. Practical uses: boot counter, dirty-shutdown detection, firmware version tracking, and Standby state serialisation.
  • Tamper detection: TAMP pins with configurable edge/filter, automatic backup register erase, and timestamp recording — the foundation of tamper-evident logging for security-sensitive devices.
  • Smooth calibration: Up to ±488 ppm correction via the CALR register. Measuring drift against GPS PPS or NTP and applying HAL_RTCEx_SetSmoothCalib() can reduce clock error to <1 second/day.

Next in the Series

In Part 12: CAN Bus, we'll move from the internal backup domain to the automotive world — configuring bxCAN and FDCAN controllers on STM32, defining message filters, transmitting standard and extended frames, handling error states (error-active, error-passive, bus-off), and building a simple CANopen-inspired message dispatcher that handles multiple node IDs on the same bus.

Technology