I once shipped a batch of data loggers that used the microcontroller’s internal software counter to keep track of time — no dedicated RTC hardware. It seemed fine on the bench. In the field, after a power cycle, every device lost track of what time it was, and years of “timestamped” data became useless. That project is the reason I now consider a Real-Time Clock one of the first components I specify on any design that cares about time. In this article I’ll explain what an RTC actually is, how it differs from a regular system timer, and how it’s implemented and used in real embedded designs.
What an RTC Actually Is
A Real-Time Clock is a dedicated hardware peripheral (either integrated into the microcontroller or as a separate IC) whose only job is to keep accurate track of wall-clock time — seconds, minutes, hours, day, month, year — continuously, even while the rest of the system is powered off, asleep, or being reset.
graph TD
A[32.768kHz Crystal Oscillator] --> B[RTC Counter/Divider Chain]
B --> C[Time/Date Registers]
C --> D[MCU reads current time]
E[Backup Battery / Supercap] -.->|keeps RTC running when main power off| B
F[Alarm/Compare Registers] --> G[Wake-up / Interrupt to MCU]
The key distinguishing feature versus a regular system tick counter (like a SysTick timer) is persistence: an RTC is designed to keep counting through power loss, board resets, and firmware crashes, typically backed by a small coin-cell battery or supercapacitor on a dedicated Vbat pin.
Why a Software Timer Isn’t Enough
A software-based “uptime counter” (incrementing a variable on every SysTick interrupt) only tracks time since the last boot. It:
- Resets to zero on every power cycle or reset
- Drifts significantly if based on the main system clock (which is often tuned for CPU speed, not timekeeping accuracy)
- Provides no calendar context (date, day of week) without extra software
- Can’t wake the system from a deep sleep state where the CPU clock itself is stopped
An RTC solves all four problems by running on its own dedicated low-power oscillator (almost always a 32.768kHz crystal, chosen because it divides down to exactly 1Hz using a simple 15-stage binary divider: 2^15 = 32768) that keeps running independent of the main system clock and CPU state.
Core RTC Functions
1. Timekeeping Across Power Cycles
#include "stm32f4xx_hal.h"
RTC_HandleTypeDef hrtc;
void RTC_SetDateTime(uint8_t hour, uint8_t min, uint8_t sec,
uint8_t date, uint8_t month, uint8_t year)
{
RTC_TimeTypeDef sTime = {0};
RTC_DateTypeDef sDate = {0};
sTime.Hours = hour;
sTime.Minutes = min;
sTime.Seconds = sec;
HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
sDate.Date = date;
sDate.Month = month;
sDate.Year = year;
HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
}
void RTC_GetDateTime(RTC_TimeTypeDef *time, RTC_DateTypeDef *date)
{
HAL_RTC_GetTime(&hrtc, time, RTC_FORMAT_BIN);
HAL_RTC_GetDate(&hrtc, date, RTC_FORMAT_BIN); /* must read date after time on STM32 */
}
Because the RTC has its own backup power domain (Vbat pin, backed by a coin cell), this data survives even a complete main-power loss — critical for devices that get unplugged, battery-swapped, or brownout during normal use.
2. Timestamping Events and Data Logs
Every logged sensor reading, error event, or user action typically gets an RTC-derived timestamp so it can be correlated later:
typedef struct {
uint32_t unix_timestamp;
float sensor_value;
} timestamped_log_t;
uint32_t RTC_To_UnixTimestamp(RTC_DateTypeDef *date, RTC_TimeTypeDef *time)
{
struct tm t = {0};
t.tm_year = date->Year + 100; /* years since 1900, RTC year is offset from 2000 */
t.tm_mon = date->Month - 1;
t.tm_mday = date->Date;
t.tm_hour = time->Hours;
t.tm_min = time->Minutes;
t.tm_sec = time->Seconds;
return (uint32_t)mktime(&t);
}
3. Alarms and Scheduled Wake-Ups
This is one of the most powerful features of an RTC in low-power design: setting an alarm register so the RTC itself generates an interrupt at a specific future time, waking the MCU from its deepest sleep mode without the CPU needing to run at all in the meantime.
sequenceDiagram
participant MCU
participant RTC
MCU->>RTC: Set alarm for 06:00:00
MCU->>MCU: Enter STOP/STANDBY mode (CPU clock stopped)
Note over RTC: RTC keeps running on 32.768kHz oscillator
RTC->>RTC: Current time reaches 06:00:00
RTC->>MCU: Alarm interrupt - wake up
MCU->>MCU: Resume execution, read sensors, transmit data
MCU->>MCU: Re-arm alarm, sleep again
void RTC_SetWakeupAlarm(uint8_t hour, uint8_t min)
{
RTC_AlarmTypeDef sAlarm = {0};
sAlarm.AlarmTime.Hours = hour;
sAlarm.AlarmTime.Minutes = min;
sAlarm.AlarmTime.Seconds = 0;
sAlarm.AlarmMask = RTC_ALARMMASK_DATEWEEKDAY; /* trigger daily, ignore date */
sAlarm.Alarm = RTC_ALARM_A;
HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BIN);
}
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)
{
/* Wakes the system from STOP mode - runs main sensing/comm routine */
perform_scheduled_measurement();
}
This pattern is at the heart of most battery-powered IoT devices — sleep for hours, wake for milliseconds, measure and transmit, sleep again. Without a hardware RTC alarm, the CPU would have to stay awake (or wake periodically on a much less accurate low-power timer) just to check the time, burning far more battery.
RTC Accuracy and Calibration
A basic 32.768kHz crystal has a typical tolerance of ±20ppm, which translates to roughly ±1-2 minutes of drift per month — often too much for applications needing precise long-term timekeeping. Many RTC peripherals include a calibration register that periodically adds or removes clock pulses to compensate for known crystal drift, and temperature-compensated RTCs (TCXO-based) can achieve much tighter accuracy for demanding applications.
void RTC_ApplyCalibration(int16_t calibration_ppm)
{
/* Vendor-specific: e.g., STM32 RTC smooth calibration registers
adjust the effective clock by inserting/removing pulses over
a 32-second (or 220-pulse) cycle to trim long-term drift */
HAL_RTCEx_SetSmoothCalib(&hrtc, RTC_SMOOTHCALIB_PERIOD_32SEC,
RTC_SMOOTHCALIB_PLUSPULSES_RESET,
calibration_ppm);
}
RTC Synchronization with Network Time
For internet-connected devices, I typically use the RTC as the local time authority but periodically synchronize it against a trusted network time source (NTP, or a timestamp from a cloud API response) to correct for long-term crystal drift:
void Sync_RTC_With_NTP(uint32_t ntp_unix_time)
{
struct tm *t = gmtime((time_t *)&ntp_unix_time);
RTC_TimeTypeDef sTime = {
.Hours = t->tm_hour, .Minutes = t->tm_min, .Seconds = t->tm_sec
};
RTC_DateTypeDef sDate = {
.Date = t->tm_mday, .Month = t->tm_mon + 1, .Year = t->tm_year - 100
};
HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
}
Using an External RTC IC
When an MCU either lacks a built-in RTC or the application needs higher accuracy and longer battery backup than the internal peripheral supports, a dedicated external RTC IC connected over I2C (like the DS3231, which includes a temperature-compensated crystal oscillator for much better accuracy) is a common addition.
#define DS3231_ADDR 0x68
int DS3231_ReadTime(uint8_t *hour, uint8_t *min, uint8_t *sec)
{
uint8_t reg = 0x00;
uint8_t data[3];
if (I2C_Write(DS3231_ADDR, ®, 1) != I2C_OK) return -1;
if (I2C_Read(DS3231_ADDR, data, 3) != I2C_OK) return -1;
*sec = ((data[0] >> 4) * 10) + (data[0] & 0x0F); /* BCD decode */
*min = ((data[1] >> 4) * 10) + (data[1] & 0x0F);
*hour = ((data[2] >> 4) * 10) + (data[2] & 0x0F);
return 0;
}
Note the BCD (Binary Coded Decimal) encoding used by most RTC ICs for their time registers — a detail that trips up a lot of engineers new to RTC programming, since the raw register value isn’t a simple binary integer but two 4-bit decimal digits packed into a byte.
The DS3231 and similar parts also expose a temperature sensor (used internally for oscillator compensation but readable externally too) and typically achieve accuracy within about ±2 minutes per year, dramatically better than an uncompensated 32.768kHz crystal directly driving an MCU’s internal RTC.
Handling Timezones and Daylight Saving Time
I make it a firm rule to store all RTC and logged timestamps in UTC internally, converting to local time only at the point of display or user interaction. This avoids an entire category of bugs around daylight saving transitions and timezone changes corrupting stored historical data.
/* Convert UTC RTC time to local display time - conversion logic
lives entirely at the display/UI layer, never touching
the stored/logged timestamp itself */
uint32_t Convert_UTC_To_Local(uint32_t utc_timestamp, int16_t utc_offset_minutes)
{
return utc_timestamp + (utc_offset_minutes * 60);
}
RTC Interrupt Modes Beyond Simple Alarms
Beyond a single alarm, many RTC peripherals support periodic wake-up timers independent of the calendar alarm registers, useful for regular polling intervals that don’t need to align to specific wall-clock times:
/* STM32 RTC periodic wakeup timer - fires every N RTC clock ticks,
independent of the calendar alarm mechanism, useful for
simple "wake every 30 seconds" style periodic sensing */
void RTC_ConfigPeriodicWakeup(uint16_t seconds)
{
HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, seconds - 1,
RTC_WAKEUPCLOCK_CK_SPRE_16BITS);
}
void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc)
{
perform_periodic_measurement();
}
Secure and Tamper-Resistant Timekeeping
In applications like smart metering or access control, the RTC itself can become a target — someone might try to roll the clock backward to bypass a time-based access schedule or manipulate billing data. In these designs I add extra safeguards beyond a standard RTC:
- Monotonic counters (a value that can only increase, often stored in secure OTP/fuse memory as described in the security article of this series) used alongside the RTC to detect if wall-clock time has been rolled backward
- Tamper-detect inputs on RTC ICs that specifically monitor for backup power interruption or enclosure opening, logging a tamper event with a timestamp captured at the moment of detection
- Periodic time synchronization against a trusted external source (a secure NTP server, or a signed timestamp from a cloud service) with logic that rejects a sync request proposing an implausible time jump, which could indicate an attempt to manipulate the device’s sense of time
bool Detect_Suspicious_Time_Jump(uint32_t current_rtc_time, uint32_t proposed_new_time)
{
const uint32_t MAX_PLAUSIBLE_DRIFT_SECONDS = 3600; /* 1 hour */
int32_t delta = (int32_t)(proposed_new_time - current_rtc_time);
if (delta < 0 || (uint32_t)delta > MAX_PLAUSIBLE_DRIFT_SECONDS) {
log_tamper_event(current_rtc_time, proposed_new_time);
return true; /* reject the suspicious time update */
}
return false;
}
Designing the RTC Backup Power Domain
Getting reliable backup power to the RTC is as much a hardware design task as a firmware one. A typical backup circuit uses a coin cell (like a CR2032) or a supercapacitor feeding the RTC’s dedicated Vbat pin through a diode, isolating it from the main system supply so the RTC keeps running even when main power is completely removed.
graph TD
A[Main System Power] -->|normal operation| C{Power Selection}
B[Coin Cell / Supercapacitor] -->|backup only, isolated by diode| C
C --> D[RTC Vbat Pin]
A -->|when present, also trickle-charges| E[Supercapacitor, if used instead of coin cell]
A supercapacitor is often preferred over a coin cell in designs where the device is rarely fully unpowered for long stretches (a supercapacitor recharges automatically from main power and never needs manual replacement), while a coin cell makes more sense for devices that might sit unpowered in storage or shipping for extended periods, since supercapacitors self-discharge faster than a good lithium coin cell over long idle durations.
Choosing Between Internal and External RTC: A Decision Framework
When I’m specifying a design, I weigh internal versus external RTC options against a few concrete criteria rather than defaulting to whichever is more convenient:
| Consideration | Internal RTC | External RTC IC |
|---|---|---|
| Accuracy | Typically ±20ppm uncompensated | Can be ±2ppm with temperature compensation (e.g., DS3231) |
| BOM cost | None (already in MCU) | Additional IC, crystal, backup battery holder |
| Board area | None | Small additional footprint |
| Backup power | Shares MCU’s Vbat domain | Independent, can outlast MCU power entirely |
| Best fit | Cost-sensitive designs with modest accuracy needs | Precision timekeeping, long-term data logging, billing-grade applications |
For most consumer IoT products I default to the internal RTC with periodic network time synchronization to correct drift, reserving an external precision RTC for applications where accuracy matters even without connectivity — a standalone data logger deployed for months without a network connection, for instance, has no way to correct internal RTC drift and benefits enormously from a temperature-compensated external part.
Real-World Applications
- Data loggers — every recorded sample needs an accurate, power-cycle-resistant timestamp
- Battery-powered IoT sensors — RTC alarms enable deep sleep between measurements, extending battery life from days to years
- Access control systems — time-based access schedules (e.g., door unlocked only 9am-5pm) rely on accurate RTC time
- Automotive — trip logging, service interval tracking, and event data recorders use RTC timestamps
- Smart metering — billing periods and time-of-use pricing require accurate, tamper-resistant timekeeping
Reliability Considerations
Backup battery health matters more than people expect — a dead coin cell means the RTC resets to a default date on every power loss, silently corrupting timestamps. I add a firmware check at boot that flags an implausible date (e.g., year 2000) as a sign of RTC backup failure, and in tamper-sensitive applications (like metering), some RTCs include tamper-detect pins that log an event if backup power is interrupted.
bool RTC_LostPower_Check(RTC_DateTypeDef *date)
{
return (date->Year < 24); /* Year < 2024 in this example is implausible; flag it */
}
Frequently Asked Questions
Is an RTC the same as a system timer or SysTick? No. A SysTick timer measures elapsed time since boot using the main system clock and resets on every power cycle. An RTC is a dedicated peripheral with its own oscillator and backup power that maintains actual calendar date/time continuously, even across resets and power loss.
Do all microcontrollers have a built-in RTC? Most modern MCUs (STM32, ESP32, many AVR/PIC parts) include an integrated RTC peripheral. For MCUs without one, or when higher accuracy/battery backup is needed, a dedicated external RTC IC (like the DS3231 or PCF8563) connected over I2C is a very common addition.
Why do RTCs use a 32.768kHz crystal specifically? Because 32,768 is exactly 2^15, a simple 15-stage binary divider chain converts that frequency down to precisely 1Hz, making it the most efficient and common choice for low-power timekeeping oscillators.
Can an RTC wake a microcontroller from its lowest power sleep mode? Yes, this is one of its most important roles — RTC alarm interrupts are specifically designed to be able to wake the CPU from STOP/STANDBY/deep-sleep modes where even the main system clock has been stopped.
Summary
A Real-Time Clock gives an embedded system something a plain software counter never can: a persistent, accurate sense of actual calendar time that survives power loss and lets the device sleep almost completely while still waking up exactly when needed. From timestamping sensor logs to scheduling ultra-low-power wake-ups to enabling time-based access control, the RTC quietly underlies a huge share of what makes embedded products actually useful in the real world — which is exactly why I treat it as a first-class design decision rather than an afterthought.
