STM32 RTC Tutorial: Calendar Time, Backup Registers and Alarm Interrupts

Many projects need to know the actual time of day: a data logger that timestamps readings, a plant watering system that runs at 6 AM, or a device that wakes from sleep once an hour. Counting milliseconds with HAL_GetTick drifts and resets to zero on every reset. The Real-Time Clock (RTC) peripheral keeps hours, minutes, seconds and the calendar in hardware, can keep running from a coin cell when the main supply is off, and can raise alarm interrupts at chosen times.

This tutorial uses the RTC of the STM32F401 on a Nucleo-F401RE. You will configure it with the 32.768 kHz crystal, set and read the time, keep the time across resets using a backup register, and use Alarm A to run code every ten seconds.

How the STM32 RTC Keeps Time

The RTC needs a slow, accurate clock. Three sources are possible: the LSE (external 32.768 kHz crystal), the LSI (internal RC oscillator of roughly 32 kHz) or the HSE divided down. The LSE is the right choice for timekeeping, because the LSI can be off by several percent, which is minutes per day. The 32.768 kHz frequency is chosen because it is 2 to the power 15, so binary dividers turn it into exactly 1 Hz.

ck_spre (1 Hz) = RTCCLK / ((PREDIV_A + 1) x (PREDIV_S + 1))

With LSE = 32768 Hz:
PREDIV_A (asynchronous) = 127
PREDIV_S (synchronous)  = 255
32768 / (128 x 256) = 1 Hz

The F4 RTC stores time and date in BCD registers and has a set of backup registers (RTC_BKP_DR0 to DR19 on the F401) that keep their contents as long as the backup domain has power. The backup domain is powered from VDD normally, and from the VBAT pin when VDD is removed. On a Nucleo, VBAT is tied to VDD, so the RTC survives a reset but not a power-off unless you modify the board.

Hardware Notes

ItemNucleo-F401REBlue Pill
32.768 kHz crystalX2, fitted on recent board revisions (MB1136 C-02 and later)Fitted, on PC14 and PC15
VBAT pinConnected to VDD on the boardAvailable on the header for a coin cell
Alarm interruptRTC_Alarm_IRQn through EXTI line 17RTC_Alarm_IRQn through EXTI line 17

If your Nucleo is an older revision without X2, select LSI as the RTC clock instead; the code works the same, but expect noticeable drift. For a battery-backed design on your own PCB, connect a CR2032 to VBAT, and remember that PC13 to PC15 are then also in the backup domain with limited drive strength.

CubeMX Configuration

  1. System Core, RCC: set Low Speed Clock (LSE) to Crystal/Ceramic Resonator.
  2. Timers, RTC: tick Activate Clock Source and Activate Calendar. Set Alarm A to Internal Alarm A.
  3. In the RTC parameters set Hour Format 24, Asynchronous Predivider 127, Synchronous Predivider 255. Enter an initial time and date; CubeMX writes them into MX_RTC_Init().
  4. Clock Configuration tab: set the RTC Clock Mux to LSE.
  5. NVIC Settings of the RTC: enable RTC alarms A and B interrupt through EXTI line 17.
  6. Keep USART2 for printing, and generate the code.

Keeping Time Across Resets and Using Alarm A

CubeMX sets the initial time in MX_RTC_Init() every time the board resets, which would throw away the running time. The fix is a marker value in a backup register: if the marker is present, the RTC is already running and MX_RTC_Init() returns early. CubeMX provides a USER CODE section named Check_RTC_BKUP in exactly the right place for this.

main.c (Nucleo-F401RE): RTC with backup-register check and a re-armed 10-second alarm
/* Inside MX_RTC_Init(), generated by CubeMX, after HAL_RTC_Init(): */
  /* USER CODE BEGIN Check_RTC_BKUP */
  if (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1) == 0x32F2)
  {
    return;   /* RTC already set and running: keep the current time */
  }
  /* USER CODE END Check_RTC_BKUP */
  /* ...CubeMX code that sets the initial time and date follows... */

/* ------------------------------------------------------------------ */

/* USER CODE BEGIN Includes */
#include <stdio.h>
/* USER CODE END Includes */

/* USER CODE BEGIN PV */
volatile uint8_t alarm_fired = 0;
/* USER CODE END PV */

/* USER CODE BEGIN 0 */
int _write(int file, char *ptr, int len)
{
  (void)file;
  HAL_UART_Transmit(&huart2, (uint8_t *)ptr, (uint16_t)len, HAL_MAX_DELAY);
  return len;
}

/* Arm Alarm A to match only the seconds field, 'delay' seconds from now */
static void alarm_in_seconds(uint8_t delay)
{
  RTC_TimeTypeDef now;
  RTC_DateTypeDef date;
  RTC_AlarmTypeDef a = { 0 };

  HAL_RTC_GetTime(&hrtc, &now, RTC_FORMAT_BIN);
  HAL_RTC_GetDate(&hrtc, &date, RTC_FORMAT_BIN);   /* must follow GetTime */

  a.AlarmTime.Seconds  = (uint8_t)((now.Seconds + delay) % 60U);
  a.AlarmMask          = RTC_ALARMMASK_DATEWEEKDAY | RTC_ALARMMASK_HOURS |
                         RTC_ALARMMASK_MINUTES;       /* compare seconds only */
  a.AlarmSubSecondMask = RTC_ALARMSUBSECONDMASK_ALL;
  a.AlarmDateWeekDaySel = RTC_ALARMDATEWEEKDAYSEL_DATE;
  a.AlarmDateWeekDay   = 1;
  a.Alarm              = RTC_ALARM_A;
  HAL_RTC_SetAlarm_IT(&hrtc, &a, RTC_FORMAT_BIN);
}

/* Called from RTC_Alarm_IRQHandler() through HAL_RTC_AlarmIRQHandler() */
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)
{
  (void)hrtc;
  alarm_fired = 1;
}
/* USER CODE END 0 */

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART2_UART_Init();
  MX_RTC_Init();

  /* USER CODE BEGIN 2 */
  if (HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1) != 0x32F2)
  {
    HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0x32F2);  /* mark as set */
    printf("\r\nRTC was not set: using the CubeMX start time\r\n");
  }
  else
  {
    printf("\r\nRTC already running: time kept across reset\r\n");
  }
  alarm_in_seconds(10);
  uint8_t last_sec = 0xFF;
  /* USER CODE END 2 */

  while (1)
  {
    /* USER CODE BEGIN 3 */
    RTC_TimeTypeDef t;
    RTC_DateTypeDef d;
    HAL_RTC_GetTime(&hrtc, &t, RTC_FORMAT_BIN);
    HAL_RTC_GetDate(&hrtc, &d, RTC_FORMAT_BIN);

    if (t.Seconds != last_sec)      /* print once per second */
    {
      last_sec = t.Seconds;
      printf("20%02u-%02u-%02u %02u:%02u:%02u\r\n",
             d.Year, d.Month, d.Date, t.Hours, t.Minutes, t.Seconds);
    }

    if (alarm_fired)
    {
      alarm_fired = 0;
      HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
      printf("  Alarm A: 10 seconds passed\r\n");
      alarm_in_seconds(10);         /* re-arm for the next one */
    }
    HAL_Delay(50);
    /* USER CODE END 3 */
  }
}

Important Details

  • Read the date after the time. On the F4, reading the time register locks the shadow date register until the date is read. If you call only HAL_RTC_GetTime, the next readings stay frozen.
  • RTC_FORMAT_BIN vs RTC_FORMAT_BCD: BIN lets the HAL convert to normal numbers. Use BCD only if you want the raw register format.
  • Alarm masks: each masked field is ignored in the comparison. Masking date, hours and minutes means the alarm fires whenever the seconds match, once per minute unless you re-arm it.
  • Year field: the RTC stores 0 to 99, so the code prints a leading 20. Leap years are handled by the hardware within that range.
  • Backup register value: 0x32F2 is just a marker used in ST's own examples; any value that is unlikely to appear by chance works.

To set the clock from the PC, read a string such as T143000 over UART, fill an RTC_TimeTypeDef and call HAL_RTC_SetTime. For better accuracy, the F4 RTC also supports smooth digital calibration with HAL_RTCEx_SetSmoothCalib, which adjusts the rate in small steps after you measure the drift over a few days.

Troubleshooting

  • Code hangs in SystemClock_Config or HAL_RTC_Init: the LSE crystal is missing or not starting. Try LSI to confirm the rest works.
  • Time resets to the CubeMX value after every reset: the Check_RTC_BKUP code is missing or the marker was never written.
  • Seconds never change: you call HAL_RTC_GetTime without HAL_RTC_GetDate.
  • Alarm never fires: the RTC alarm interrupt is not enabled in the NVIC, or the callback name is misspelled.
  • Time lost at power-off on a Nucleo: expected, because VBAT is tied to VDD on that board.

Summary

You clocked the RTC from the 32.768 kHz LSE with prescalers 127 and 255, read time and date in the correct order, preserved the time across resets with a backup register, and used Alarm A with masks to run code on schedule. The RTC is also a key wake-up source for low-power modes, which is the subject of the next tutorial.