1. Today’s topic
Today we examine four different concepts of time:
monotonic time:
elapsed time within the current boot
UTC / wall clock:
calendar time: date, hours, minutes, seconds
hardware capture time:
precise external edge time relative to a timer
local civil time:
UTC with a time zone and clock-change rulesAnd also:
SNTP/NTP synchronization
instant step and smooth adjustment
PPS
clock offset
clock drift
holdover
timestamp quality
wraparound
ESP32, STM32, camera, and HIL-bench synchronizationThe central idea: timeout, debounce, watchdog, and period measurement use only monotonic time. UTC is needed for logs and correlating events between devices, but it may be adjusted forwards or backwards.
2. Why this matters in your projects
For traffic-light phase detection, a phase-change event should contain both calendar time and a local monotonic timestamp:
phase=GREEN
mono_us=128493820
utc=2026-07-18T06:42:31.438217Z
time_quality=SYNCED
sequence=817If NTP adjusts calendar time backwards, sequence and mono_us still preserve the correct event order. EC25, MQTT, and TCP need two different types of time:
monotonic:
AT command timeout;
reconnect backoff;
registration wait;
modem_task watchdog;
request RTT.
UTC:
telemetry timestamp;
SMS time;
TLS certificate validation;
error log;
event correlation on the server.The camera and HDR illumination need:
frame_sync_capture_ticks
frame_sequence
mono_us
exposure_mode
IR pulse duration
optional UTCFor HIL on Raspberry Pi, distinguish:
DUT local monotonic time
Raspberry Pi monotonic time
UTC Raspberry Pi
UTC DUT3. Theory
2.3.1 3.1. Monotonic time and wall clock solve different problems
Monotonic time should:
- never go backwards;
- advance at a consistent scale;
- not depend on NTP;
- not depend on timezone;
- be used for every interval.Examples:
uint64_t started_us;
uint64_t timeout_us;
uint64_t last_progress_us;
uint64_t debounce_started_us;
if ((now_us - started_us) >= timeout_us) {
timeout();
}UTC answers “What is the current calendar time?”. It may change after NTP, RTC restoration, or manual correction. Therefore UTC must not be used for timeout, debounce, watchdog, or reconnect-backoff. Rule:
duration / timeout / debounce / watchdog:
monotonic
calendar / telemetry / logs:
UTC2.3.2 3.2. Create a timestamp at the event source
Poor approach:
GPIO edge
→ queue
→ phase task
→ MQTT task
→ timestamp added at publishCorrect approach:
GPIO edge:
hardware capture timestamp
phase confirmed:
monotonic timestamp of confirmation
MQTT publish:
forwards the existing timestampAn event may store two times:
typedef struct {
uint64_t observed_mono_us;
uint64_t confirmed_mono_us;
} phase_timing_t;2.3.3 3.3. Timestamp structure
typedef enum {
TIME_QUALITY_UNSYNCED = 0,
TIME_QUALITY_ESTIMATED,
TIME_QUALITY_SYNCED,
TIME_QUALITY_HOLDOVER,
TIME_QUALITY_INVALID,
} time_quality_t;
typedef struct {
uint64_t mono_us;
int64_t utc_us;
bool utc_valid;
time_quality_t quality;
uint32_t sync_generation;
uint32_t boot_id;
} event_timestamp_t;Meaning:
mono_us:
the primary timestamp and event order
utc_us:
mapping to UTC when known
quality:
how trustworthy UTC is
sync_generation:
which synchronization was used
boot_id:
distinguishes identical mono_us after reboot2.3.4 3.4. Time quality
UNSYNCED:
the device just booted; NTP/RTC is not available yet.
ESTIMATED:
time restored from RTC or a saved value.
SYNCED:
a fresh NTP/SNTP/GNSS source was received.
HOLDOVER:
the source was lost, but the device continues keeping time with its local oscillator.
INVALID:
time is clearly incorrect or contradicts other sources.2.3.5 3.5. Relating monotonic time to UTC through an anchor
typedef struct {
uint64_t anchor_mono_us;
int64_t anchor_utc_us;
int32_t rate_ppb;
uint32_t generation;
time_quality_t quality;
bool valid;
} time_anchor_t;Basic model:
utc_us = anchor_utc_us + (mono_us - anchor_mono_us)Accounting for drift:
delta_us = mono_us - anchor_mono_us
correction_us = delta_us × rate_ppb / 1 000 000 000
utc_us = anchor_utc_us + delta_us + correction_usrate_ppb:
+20 000 ppb = +20 ppm
-10 000 ppb = -10 ppm2.3.6 3.6. Offset and drift
Offset is the clock’s current position error. Drift is its rate error. Example at 20 ppm:
20 microseconds of error per second
1.2 milliseconds per minute
72 milliseconds per hour
1.728 seconds per day2.3.7 3.7. Step and smooth correction
Step changes UTC immediately. Smooth/slew gradually speeds up or slows down the system clock. A useful production policy:
first sync after boot:
step if UTC has not been used yet
small subsequent error:
smooth
very large error:
fault event + controlled stepTimeout still remains on the monotonic clock.
2.3.8 3.8. PPS
PPS gives a precise second boundary, but not the second’s number. Absolute UTC requires GNSS/NMEA/UBX, RTC, NTP, or another source of the calendar second.
PPS pin
→ timer Input Capture
→ capture_ticks
→ PPS event
→ time discipline task4. Common mistakes
1. Using UTC for timeout.
2. Adding a timestamp at MQTT publish.
3. Treating any calendar time as synchronized.
4. Not keeping boot_id.
5. Treating PPS as complete UTC.
6. Abruptly correcting the monotonic clock.
7. Ignoring drift in holdover.
8. Mixing ms, us, ticks, and RTOS ticks.
9. Not accounting for wraparound.
10. Applying timezone inside a protocol.
11. Treating NTP as proof of microsecond accuracy.5. Practical assignment for 30-60 minutes
Create TIME_POLICY.md:
# Time architecture policy
1. All timeouts use monotonic time.
2. UTC is never used for duration measurement.
3. Hardware events are timestamped at their source.
4. Every event contains boot_id and sequence.
5. UTC timestamps include time quality.
6. SNTP corrections never change monotonic time.
7. After loss of synchronization, state becomes HOLDOVER.
8. Local timezone is applied only for display.
9. Units are encoded in variable and field names.
10. Wraparound behavior is covered by tests.Add an API:
typedef enum {
TIME_QUALITY_UNSYNCED = 0,
TIME_QUALITY_ESTIMATED,
TIME_QUALITY_SYNCED,
TIME_QUALITY_HOLDOVER,
TIME_QUALITY_INVALID,
} time_quality_t;
typedef struct {
uint64_t mono_us;
int64_t utc_us;
uint32_t boot_id;
uint32_t sync_generation;
time_quality_t quality;
bool utc_valid;
} event_timestamp_t;
void time_service_init(uint32_t boot_id);
uint64_t time_service_mono_us(void);
void time_service_apply_sync(uint64_t mono_us,
int64_t utc_us,
time_quality_t quality);
event_timestamp_t time_service_timestamp(void);
event_timestamp_t time_service_from_mono(uint64_t mono_us);
void time_service_mark_holdover(void);Add to phase_event_t:
typedef struct {
uint32_t sequence;
phase_t phase;
uint8_t mask;
event_timestamp_t observed;
event_timestamp_t confirmed;
} phase_event_t;CLI command:
time statusExample output:
time:
quality=SYNCED
boot_id=42
sync_generation=7
mono_us=1834291203
utc=2026-07-18T06:42:31.438217Z
last_sync_age=1842s
offset_last=-1240us
drift_estimate=+18.4ppm
uncertainty=4200us
source=SNTP
smooth_sync=yes6. Further reading or experiments
- ESP-IDF System Time, ESP Timer, and ESP-NETIF SNTP Service.
- STM32 AN4759 on RTC, subsecond, synchronization, and smooth calibration.
- STM32 AN4013 on timer synchronization, master/slave timers, and TRGO.
Brief recap
hardware event
→ hardware capture / monotonic timestamp
→ event sequence + boot_id
→ time_service
→ optional UTC mapping + quality
→ queue / MQTT / CAN / logExercise
NTP steps UTC backwards while an AT command timeout is running. Describe the clock used for that timeout and the event fields that preserve ordering across correction and reboot.
Self-check criteria: Explain why UTC correction cannot extend or reverse the timeout; distinguish event order within a boot from cross-device calendar correlation.
Show the supplied answer
Use monotonic time for the timeout. Preserve the source’s monotonic timestamp and sequence, add boot_id to distinguish boots, and attach UTC mapping with explicit quality/sync_generation rather than replacing the ordering clock.
Exercise
A device loses its synchronization source after a good sync. Define the reported time quality and how drift affects its uncertainty during holdover.
Self-check criteria: Do not equate a formatted calendar value with verified accuracy. Explain which evidence changes when synchronization returns.
Show the supplied answer
Report HOLDOVER rather than claiming a fresh SYNCED source. Continue local monotonic timing and estimate UTC from the last anchor; let uncertainty reflect elapsed time and oscillator drift, and record the last synchronization age.