跳转至

WSN 时间同步 — FTSP

基于 ESP-NOW 的泛洪时间同步协议

FTSP 模块为无线传感器网络(WSN)提供微秒级精度的时间同步。一个节点作为网关(根/主节点),通过 NTP 获取墙钟时间并通过 ESP-NOW 广播。其余节点作为叶子节点(从节点),接收广播并通过线性回归校正本地时钟。

演示:多节点 FTSP 时间同步 —— 所有节点同步循环跑马灯颜色


架构

┌─────────────────────────────────────────────────────────────┐
│                      网关 (root)                            │
│                                                             │
│  NTP (互联网) ──► tiny_ntp_record_sync()                   │
│       │                                                     │
│       ▼                                                     │
│  ftsp_gateway_task() ──► esp_now_send() 广播                │
│       │                  每 FTSP_BEACON_INTERVAL_MS         │
│       │                  (默认 1000 ms = 1 Hz)              │
│       ▼                                                     │
│  信标包 (26 字节扩展格式):                                    │
│    ref_time_us  – 网关本地发送时间 (µs)                      │
│    unix_us      – NTP 世界时间 @ 发送 (µs, Unix 纪元)        │
│    seq          – 滚动序列号                                 │
│    node_id      – 网关标识符                                 │
│    flags        – 世界时间有效、同步序列                      │
│    ntp_quality  – NTP 同步质量 (0-100%)                     │
└───────────────────────┬─────────────────────────────────────┘
                        │ ESP-NOW (广播)
          ┌─────────────┼─────────────┐
          ▼             ▼             ▼
┌──────────────────────────────────────────────────────────────┐
│                    叶子节点 (slave)                           │
│                                                              │
│  node_espnow_recv_cb() ──► ftsp_sniffer_cb()                │
│       │                    (捕获 rx_us,抖动 <1 ms)           │
│       ▼                                                     │
│  环形缓冲区 (FTSP_TABLE_SIZE 对):                             │
│    (local_us, ref_us)                                        │
│       │                                                     │
│       ▼                                                     │
│  最小二乘线性回归:                                            │
│    slope  = Σ(xi − x̄)(yi − ȳ) / Σ(xi − x̄)²                │
│    offset = ȳ − slope · x̄                                   │
│    global_us = slope × local_us + offset                     │
│       │                                                     │
│       ▼                                                     │
│  世界时间 (µs 精度):                                          │
│    world_us = global_us + unix_offset_us                     │
│    unix_offset_us = beacon.unix_us - beacon.ref_time_us      │
│                                                              │
│  直接偏移回退(漂移每信标重置一次):                           │
│    world_us = unix_us_last + (now - local_rx)                │
└──────────────────────────────────────────────────────────────┘

核心概念

节点角色

节点角色在编译时通过 node_config.h 中的 NODE_ROLE 选择:

#define NODE_ROLE_GATEWAY   0
#define NODE_ROLE_LEAFNODE  1

#ifndef NODE_ROLE
#define NODE_ROLE NODE_ROLE_GATEWAY
#endif
  • 网关 (NODEROLEGATEWAY):连接 Enterprise WiFi,通过 NTP 同步,广播 FTSP 信标
  • 叶子节点 (NODEROLELEAFNODE):监听 FTSP 信标,计算时钟校正,提供世界时间

时间源

来源 说明
开机时间 FTSP_TIME_BOOT (0) esp_timer_get_time() — µs 自 MCU 启动,始终可用
世界时间 FTSP_TIME_WORLD (1) Unix 纪元 µs,需网关先完成 NTP 同步,自动回退到开机时间

信标数据包格式

基础信标(16 字节):包含魔法字、序列号、网关参考时间。

扩展信标(26 字节):增加 NTP 世界时间(unix_us,完整 µs 精度)、标志位和 NTP 质量——在世界时间可用时使用。

网关在 esp_now_send() 之前立即捕获 ref_time_us,以最小化发送抖动(通常 < 100 µs)。

时钟漂移补偿

叶子节点维护 (localrx, reftime_us) 对的环形缓冲区。收集到 FTSP_MIN_PAIRS_FOR_SYNC(默认为 4)个有效对后,求解最小二乘线性回归:

slope  = Σ(xi − x̄)(yi − ȳ) / Σ(xi − x̄)²
offset = ȳ − slope · x̄

斜率校正相对晶振频率误差(通常 < 40 ppm)。ftsp_precision 模块通过指数移动平均独立追踪漂移。

直接偏移模式(零回归)

ftsp_get_world_time_direct_us() 函数直接使用最近信标的 (rx_us, unix_us) 对:

world_us = unix_us_last + (esp_timer_get_time() - last_rx_us)

这完全消除了漂移累积——误差在每个新信标到达时归零。信标间的最大误差:晶振漂移_ppm × 信标间隔 ≈ 40 µs(1 Hz 时)。


编译期配置

默认值 说明
FTSP_BEACON_INTERVAL_MS 1000 信标间隔 (ms)。越短→收敛越快,但 RF 负载越高
FTSP_TABLE_SIZE 10 环形缓冲区中的 (本地, 参考) 时间戳对数
FTSP_MIN_PAIRS_FOR_SYNC 4 声明"已同步"所需的最小对数
FTSP_USE_WORLD_TIME 0 0 = 开机时间,1 = 世界时间(需 NTP)
FTSP_FAKE_EPOCH_US 1577836800000000 NTP 不可用时的伪纪元(2020-01-01)
FTSP_GATEWAY_TASK_STACK 2048 网关任务栈大小(字节)
FTSP_GATEWAY_TASK_PRIORITY 6 网关任务优先级

API 参考

网关

esp_err_t ftsp_gateway_start(void);   // 启动信标广播
esp_err_t ftsp_gateway_stop(void);    // 停止信标广播
bool      ftsp_gateway_is_running(void);

叶子节点

esp_err_t ftsp_leafnode_start(void);               // 启动信标接收器
esp_err_t ftsp_leafnode_stop(void);                // 停止信标接收器
bool      ftsp_is_synced(void);                    // 检查 FTSP 是否已同步
esp_err_t ftsp_get_global_time(int64_t *global);   // 获取 FTSP 校正后的全局时间

// 世界时间 API(需网关 NTP)
esp_err_t ftsp_get_world_time_us(uint64_t *world_us);       // FTSP 回归
esp_err_t ftsp_get_world_time_direct_us(uint64_t *world_us); // 直接偏移(无漂移)
esp_err_t ftsp_get_world_time_sec(uint32_t *world_sec);     // 自纪元起的秒数
esp_err_t ftsp_get_world_datetime_us(TinyDateTime_t *dt);   // 日历时间 + µs
esp_err_t ftsp_get_gateway_ntp_info(uint64_t *unix_us, uint8_t *ntp_quality);

同步可视化

esp_err_t ftsp_sync_viz_init(void);                     // 启动可视化任务
ftsp_sync_viz_state_t ftsp_sync_viz_get_state(void);    // 获取当前同步状态
void ftsp_sync_viz_get_marquee_color(uint64_t world_time_us,
                                     uint8_t *r, uint8_t *g, uint8_t *b);
esp_err_t ftsp_sync_viz_gateway_start_sync(void);        // 网关:启动同步序列
bool ftsp_sync_viz_gateway_is_syncing(void);

同步可视化状态

状态 LED 颜色 说明
INIT 关闭 模块已启动,尚未激活
WAITING 白色 网关:等待 NTP;叶子:等待信标
SYNC_SEQ 网关:广播 6 信标同步序列
COLLECTING 叶子:收集同步信标
SYNCED 绿色 时间同步成功
MARQUEE 7 色循环 所有节点同时显示相同颜色

跑马灯颜色

索引  0    1     2      3     4     5     6
颜色  红   橙    黄     绿    青    蓝    紫

颜色由 world_time_sec % 7 决定,确保所有节点同时显示相同颜色——这是同步的可视化证明。


同步序列协议

NTP 同步后,网关以精确时序广播 6 个特殊信标:

信标 时间 (s) 用途
0 t=0 参考 (unixus0, rxus0)
1 t=8 第一时序对
2 t=9 第二时序对
3 t=10 第三时序对
4 t=11 第四时序对
5 t=12 第五时序对

叶子收集全部 6 个,计算每对斜率并取平均,然后进入 SYNCED 状态。网关和叶子都针对相同的跑马灯启动时间last_beacon_unix_sec + HOLD_SEC),确保所有节点上的 LED 颜色同步循环。


与 tiny_toolbox 的集成

FTSP 模块使用 tiny_timeNTP 同步钩子来接收 NTP 同步通知,而无需循环 CMake 依赖:

// 在 tiny_toolbox (tiny_time.c) 中:
//   - SNTP 响应到达时立即触发 s_ntp_sync_hook((uint32_t)tv->tv_sec)

// 在 tiny_measurement (ftsp_gateway.c) 中:
//   - 注册钩子:tiny_ntp_set_sync_hook(ftsp_ntp_record_sync);

这种设计保持了清晰的层级: - tiny_toolbox — 最底层,以函数指针形式提供 tiny_ntp_set_sync_hook() - tiny_measurement — 上层,实现 ftsp_ntp_record_sync() 并注册为钩子