1. Тема дня

Сегодня разбираем четыре разных понятия времени:

text
monotonic time:
  сколько времени прошло внутри текущего запуска
UTC / wall clock:
  календарное время: дата, часы, минуты, секунды
hardware capture time:
  точный момент внешнего фронта относительно таймера
local civil time:
  UTC с часовым поясом и правилами перехода времени

А также:

text
SNTP/NTP synchronization
instant step и smooth adjustment
PPS
clock offset
clock drift
holdover
timestamp quality
wraparound
синхронизация ESP32, STM32, камеры и HIL-стенда

Главная мысль: Для timeout, debounce, watchdog и измерения периода используется только monotonic time. UTC нужен для журналов и сопоставления событий между устройствами, но он может быть скорректирован вперёд или назад.

2. Зачем это нужно в твоих проектах

Для детектирования фаз светофора событие изменения фазы должно содержать не только календарное время, но и локальный монотонный timestamp:

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

Если NTP скорректировал календарные часы назад, sequence и mono_us всё равно сохранят правильный порядок событий. Для EC25, MQTT и TCP нужны два разных вида времени:

text
monotonic:
  timeout AT-команды;
  reconnect backoff;
  ожидание регистрации;
  watchdog modem_task;
  RTT запроса.
UTC:
  timestamp телеметрии;
  время SMS;
  TLS certificate validation;
  журнал ошибок;
  сопоставление событий на сервере.

Для камеры и HDR-подсветки нужны:

text
frame_sync_capture_ticks
frame_sequence
mono_us
exposure_mode
IR pulse duration
optional UTC

Для HIL на Raspberry Pi важно различать:

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

3. Теория

2.3.1 3.1. Monotonic time и wall clock решают разные задачи

Monotonic time должен:

text
- никогда не идти назад;
- увеличиваться с постоянной шкалой;
- не зависеть от NTP;
- не зависеть от timezone;
- использоваться для всех интервалов.

Примеры:

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 отвечает на вопрос: “Какое сейчас календарное время?”. Он может измениться после NTP, восстановления RTC или ручной коррекции. Поэтому UTC нельзя использовать для timeout, debounce, watchdog или reconnect-backoff. Правило:

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

2.3.2 3.2. Timestamp должен создаваться у источника события

Плохо:

text
GPIO edge
→ queue
→ phase task
→ MQTT task
→ timestamp добавлен при publish

Правильно:

text
GPIO edge:
  hardware capture timestamp
phase confirmed:
  monotonic timestamp подтверждения
MQTT publish:
  передаёт уже существующий timestamp

Для события можно хранить два времени:

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

2.3.3 3.3. Структура timestamp

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;

Смысл:

text
mono_us:
  основной timestamp и порядок событий
utc_us:
  привязка к UTC, если она известна
quality:
  насколько UTC заслуживает доверия
sync_generation:
  какая синхронизация использована
boot_id:
  отличает одинаковые mono_us после reboot

2.3.4 3.4. Качество времени

text
UNSYNCED:
  устройство только загрузилось, NTP/RTC ещё нет.
ESTIMATED:
  время восстановлено из RTC или сохранённого значения.
SYNCED:
  получен свежий NTP/SNTP/GNSS-источник.
HOLDOVER:
  источник потерян, но устройство продолжает считать время локальным генератором.
INVALID:
  время явно неверно или противоречит другим источникам.

2.3.5 3.5. Связь monotonic и UTC через 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;

Базовая модель:

text
utc_us = anchor_utc_us + (mono_us - anchor_mono_us)

С учётом 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 и drift

Offset — ошибка положения часов прямо сейчас. Drift — ошибка скорости часов. Пример 20 ppm:

text
20 микросекунд ошибки за секунду
1.2 миллисекунды за минуту
72 миллисекунды за час
1.728 секунды за сутки

2.3.7 3.7. Step и smooth correction

Step мгновенно переставляет UTC. Smooth/slew постепенно ускоряет или замедляет системные часы. Для production удобно:

text
первый sync после загрузки:
  step, если UTC ещё не использовался
небольшая последующая ошибка:
  smooth
очень большая ошибка:
  fault event + controlled step

Timeout всё равно остаётся на monotonic clock.

2.3.8 3.8. PPS

PPS даёт точную границу секунды, но не говорит номер секунды. Для absolute UTC нужен GNSS/NMEA/UBX, RTC, NTP или другой источник календарной секунды.

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

4. Типичные ошибки

text
1. Использовать UTC для timeout.
2. Добавлять timestamp при MQTT publish.
3. Считать любое calendar time синхронизированным.
4. Не хранить boot_id.
5. Считать PPS полноценным UTC.
6. Резко исправлять monotonic clock.
7. Игнорировать drift в holdover.
8. Смешивать ms, us, ticks и RTOS ticks.
9. Не учитывать wraparound.
10. Применять timezone внутри протокола.
11. Считать NTP доказательством микросекундной точности.

5. Практическое задание на 30-60 минут

Создай 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.

Добавь 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);

Добавь в 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-команда:

text
time status

Пример вывода:

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. Что почитать или попробовать дальше

  • ESP-IDF System Time, ESP Timer и ESP-NETIF SNTP Service.
  • STM32 AN4759 по RTC, subsecond, synchronization и smooth calibration.
  • STM32 AN4013 по timer synchronization, master/slave timers и TRGO.

Короткий итог

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

Задание

NTP перевёл UTC назад во время AT command timeout. Назовите clock для timeout и поля event, сохраняющие порядок при correction и reboot.

Критерии самопроверки: Объяснить, почему UTC correction не продлевает и не обращает timeout; отделить порядок внутри boot от calendar correlation между устройствами.

Показать ответ автора

Использовать monotonic time для timeout. Сохранить monotonic timestamp источника и sequence, добавить boot_id для различения запусков и прикреплять UTC mapping с явными quality/sync_generation вместо замены ordering clock.

Задание

Устройство потеряло synchronization source после исправной sync. Определите time quality и влияние drift на uncertainty в holdover.

Критерии самопроверки: Не приравнивать formatted calendar value к проверенной accuracy. Объяснить изменение evidence при возврате synchronization.

Показать ответ автора

Сообщать HOLDOVER, а не свежий SYNCED source. Продолжать локальный monotonic timing и оценивать UTC от последнего anchor; uncertainty должна учитывать elapsed time и oscillator drift, а last synchronization age сохраняться.