1. Today’s topic

Today we examine four different concepts of time:

text
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 rules

And also:

text
SNTP/NTP synchronization
instant step and smooth adjustment
PPS
clock offset
clock drift
holdover
timestamp quality
wraparound
ESP32, STM32, camera, and HIL-bench synchronization

The 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:

text
phase=GREEN
mono_us=128493820
utc=2026-07-18T06:42:31.438217Z
time_quality=SYNCED
sequence=817

If 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:

text
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:

text
frame_sync_capture_ticks
frame_sequence
mono_us
exposure_mode
IR pulse duration
optional UTC

For HIL on Raspberry Pi, distinguish:

text
DUT local monotonic time
Raspberry Pi monotonic time
UTC Raspberry Pi
UTC DUT

3. Theory

2.3.1 3.1. Monotonic time and wall clock solve different problems

Monotonic time should:

text
- never go backwards;
- advance at a consistent scale;
- not depend on NTP;
- not depend on timezone;
- be used for every interval.

Examples:

c
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:

text
duration / timeout / debounce / watchdog:
  monotonic
calendar / telemetry / logs:
  UTC

2.3.2 3.2. Create a timestamp at the event source

Poor approach:

text
GPIO edge
→ queue
→ phase task
→ MQTT task
→ timestamp added at publish

Correct approach:

text
GPIO edge:
  hardware capture timestamp
phase confirmed:
  monotonic timestamp of confirmation
MQTT publish:
  forwards the existing timestamp

An event may store two times:

c
typedef struct {
    uint64_t observed_mono_us;
    uint64_t confirmed_mono_us;
} phase_timing_t;

2.3.3 3.3. Timestamp structure

c
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:

text
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 reboot

2.3.4 3.4. Time quality

text
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

c
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:

text
utc_us = anchor_utc_us + (mono_us - anchor_mono_us)

Accounting for drift:

text
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_us

rate_ppb:

text
+20 000 ppb = +20 ppm
-10 000 ppb = -10 ppm

2.3.6 3.6. Offset and drift

Offset is the clock’s current position error. Drift is its rate error. Example at 20 ppm:

text
20 microseconds of error per second
1.2 milliseconds per minute
72 milliseconds per hour
1.728 seconds per day

2.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:

text
first sync after boot:
  step if UTC has not been used yet
small subsequent error:
  smooth
very large error:
  fault event + controlled step

Timeout 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.

text
PPS pin
→ timer Input Capture
→ capture_ticks
→ PPS event
→ time discipline task

4. Common mistakes

text
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:

markdown
# 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:

c
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:

c
typedef struct {
    uint32_t sequence;
    phase_t phase;
    uint8_t mask;
    event_timestamp_t observed;
    event_timestamp_t confirmed;
} phase_event_t;

CLI command:

text
time status

Example output:

text
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=yes

6. 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

text
hardware event
→ hardware capture / monotonic timestamp
→ event sequence + boot_id
→ time_service
→ optional UTC mapping + quality
→ queue / MQTT / CAN / log

Exercise

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.