时间同步¶
时间同步对于无线网络设备至关重要。它确保设备之间的时间一致性,从而使得数据包的时间戳、日志记录和其他时间相关的功能能够正确工作。在本项目中时间同步我们分两个部分来展开,第一部分是与互联网的时间同步,第二部分是设备之间的时间同步。
在讨论开始之前,我们先来看一下代码。
time.hpp
#pragma once
#include <Arduino.h>
#include "config.hpp"
#include "nodestate.hpp"
/*
* Time synchronization header
*
* Provides:
* - NTP synchronization function
* - RF-based online check and RTT estimation
* - Stores RF latency (RTT/2) and online status for each node
*/
/* === RF Time Synchronization Configuration === */
#define RF_PING_PAYLOAD "PING"
#define RF_PONG_PAYLOAD "PONG"
#define RF_TIME_SYNC_HEADER "TIME_SYNC"
#define RF_TIME_ACK_HEADER "TIME_ACK"
#define RF_RESPONSE_WAIT_MS 300 // 300 ms wait per reply
#define RF_RETRY_PER_CYCLE 5 // Retry 5 times per send
extern int32_t node_rf_latency[NUM_NODES + 1];
extern bool node_online[NUM_NODES + 1];
bool sync_time_ntp();
bool sync_check_rf_online();
bool rf_time_sync();
time.cpp
#include "timesync.hpp"
#include "config.hpp"
#include "time.hpp"
#include "rf.hpp"
#include <WiFiUdp.h>
#include <NTPClient.h>
WiFiUDP ntpUDP;
// NTPClient timeClient(ntpUDP, "pool.ntp.org", 28800, 60000);
NTPClient timeClient(ntpUDP, "asia.pool.ntp.org", 28800, 60000);
int32_t node_rf_latency[NUM_NODES + 1] = {0};
bool node_online[NUM_NODES + 1] = {false};
bool sync_time_ntp()
{
timeClient.begin();
if (!timeClient.update())
{
Serial.println("[COMMUNICATION] <NTP> Failed to get NTP time.");
return false;
}
uint64_t epoch = timeClient.getEpochTime();
uint16_t current_millis = millis();
Time.set_time_epoch(epoch);
Time.last_update_epoch = epoch;
Time.last_update_ms = current_millis % 1000 + (epoch * 1000); // Convert to ms, keeping current millis for ms part
Time.mcu_base_ms = current_millis;
Time.mcu_time_ms = current_millis;
Time.delta_ms = 0;
Serial.print("[COMMUNICATION] <NTP> Synchronized UNIX epoch: ");
Serial.println(epoch);
Serial.print("[COMMUNICATION] <NTP> Current time: ");
Time.print();
return true;
}
bool sync_check_rf_online()
{
#ifdef GATEWAY
Serial.println("[COMMUNICATION] <SYNC> Master checking nodes...");
for (uint8_t id = 1; id <= NUM_NODES; ++id)
{
bool success = false;
uint32_t rtt = 0;
for (uint8_t attempt = 0; attempt < 3; ++attempt)
{
// Construct PING message
RFMessage msg;
msg.from_id = NODE_ID;
msg.to_id = id;
strncpy(msg.payload, RF_PING_PAYLOAD, sizeof(msg.payload) - 1);
msg.payload[sizeof(msg.payload) - 1] = '\0';
// Stop listening before sending
rf_stop_listening();
bool sent = rf_send(id, msg, false); // No ACK requested
rf_start_listening();
// If send failed, retry
if (!sent)
{
Serial.print(" - Node ");
Serial.print(id);
Serial.println(": SEND FAILED");
delay(100);
continue;
}
// Record send timestamp immediately after successful transmission
uint32_t t_send = millis();
Serial.print(" - PING to Node ");
Serial.print(id);
Serial.print(": sent at ");
Serial.print(t_send);
Serial.println(" ms");
// Attempt to receive response within timeout
RFMessage response;
bool received = rf_receive(response, RF_RESPONSE_WAIT_MS);
// Record receive timestamp after receive attempt
uint32_t t_recv = millis();
Serial.print(" - PING to Node ");
Serial.print(id);
Serial.print(": received at ");
Serial.print(t_recv);
Serial.println(" ms");
// Validate response: correct sender, receiver, and payload prefix
if (received &&
response.from_id == id &&
response.to_id == NODE_ID &&
strncmp(response.payload, RF_PONG_PAYLOAD, 4) == 0)
{
rtt = t_recv - t_send;
success = true;
break;
}
delay(100); // Cooldown between attempts
}
if (success)
{
node_online[id] = true;
node_rf_latency[id] = rtt / 2;
Serial.print(" - Node ");
Serial.print(id);
Serial.print(": ONLINE | RTT = ");
Serial.print(rtt);
Serial.print(" ms | latency ≈ ");
Serial.print(node_rf_latency[id]);
Serial.println(" ms");
}
else
{
node_online[id] = false;
Serial.print(" - Node ");
Serial.print(id);
Serial.println(": OFFLINE");
}
delay(200); // Prevent RF congestion
}
// Determine if any node was successfully contacted
bool any_online = false;
for (uint8_t id = 1; id <= NUM_NODES; ++id)
{
if (node_online[id])
{
any_online = true;
break;
}
}
// Compute the average latency across online nodes, and assign it to the offline nodes
int32_t total_latency = 0;
int32_t online_count = 0;
float average_latency = 0.0f;
for (uint8_t id = 1; id <= NUM_NODES; ++id)
{
if (node_online[id])
{
total_latency += node_rf_latency[id];
online_count++;
}
}
if (online_count > 0)
{
average_latency = static_cast<float>(total_latency) / online_count;
}
else
{
average_latency = 0.0f; // No nodes online, set to 0
}
// Assign average latency to offline nodes
for (uint8_t id = 1; id <= NUM_NODES; ++id)
{
if (!node_online[id])
{
node_rf_latency[id] = static_cast<int32_t>(average_latency);
}
}
// Update status flag
return any_online;
#else // LEAF NODE
Serial.println("[COMMUNICATION] <SYNC> Leaf waiting for PING...");
while (true)
{
RFMessage msg;
bool received = rf_receive(msg, 500);
if (!received)
continue;
// Ensure message is intended for this node and is a valid PING
if (msg.to_id != NODE_ID || strncmp(msg.payload, RF_PING_PAYLOAD, 4) != 0)
continue;
Serial.print("[COMMUNICATION] <SYNC> Received PING from Node ");
Serial.println(msg.from_id);
// Construct and send PONG response
RFMessage response;
response.from_id = NODE_ID;
response.to_id = msg.from_id;
strncpy(response.payload, RF_PONG_PAYLOAD, sizeof(response.payload) - 1);
response.payload[sizeof(response.payload) - 1] = '\0';
rf_stop_listening();
rf_send(msg.from_id, response, false); // No ACK
rf_start_listening();
Serial.println("[COMMUNICATION] <SYNC> PONG sent. Leaf sync complete.");
break;
}
return true;
#endif
}
bool rf_time_sync()
{
#ifdef GATEWAY
Serial.println("[COMMUNICATION] <SYNC> Starting time sync broadcast to all nodes...");
for (uint8_t id = 1; id <= NUM_NODES; ++id)
{
// Estimate master time just before sending to each node
uint32_t master_time = static_cast<uint32_t>(Time.estimate_time_ms());
RFMessage msg;
msg.from_id = NODE_ID;
msg.to_id = id;
snprintf(msg.payload, sizeof(msg.payload), "%s %lu %d", RF_TIME_SYNC_HEADER, master_time, node_rf_latency[id]);
Serial.print("[COMMUNICATION] <SYNC> Sending time to Node ");
Serial.print(id);
Serial.print(": ");
Serial.println(msg.payload);
rf_stop_listening();
bool sent = rf_send(id, msg, false);
rf_start_listening();
if (!sent)
{
Serial.print("[COMMUNICATION] <SYNC> Failed to send to Node ");
Serial.println(id);
continue;
}
RFMessage ack;
bool received = rf_receive(ack, RF_RESPONSE_WAIT_MS);
if (received &&
ack.from_id == id &&
ack.to_id == NODE_ID &&
strncmp(ack.payload, RF_TIME_ACK_HEADER, strlen(RF_TIME_ACK_HEADER)) == 0)
{
uint32_t synced_time = 0;
sscanf(ack.payload + strlen(RF_TIME_ACK_HEADER) + 1, "%lu", &synced_time);
Serial.print("[COMMUNICATION] <SYNC> Node ");
Serial.print(id);
Serial.print(" ACK received. Synced time = ");
Serial.println(synced_time);
}
else
{
Serial.print("[COMMUNICATION] <SYNC> No ACK from Node ");
Serial.println(id);
}
delay(100); // avoid RF congestion
}
node_status.node_flags.time_rf_synced = true;
return true;
#else // LEAF NODE
Serial.println("[COMMUNICATION] <SYNC> Leaf waiting for TIME_SYNC...");
uint32_t master_time = 0;
int latency = 0;
uint32_t adjusted_time = 0;
while (true)
{
RFMessage msg;
bool received = rf_receive(msg, 1000);
if (!received)
continue;
if (msg.to_id != NODE_ID || strncmp(msg.payload, RF_TIME_SYNC_HEADER, strlen(RF_TIME_SYNC_HEADER)) != 0)
continue;
// Parse time and latency from payload
sscanf(msg.payload + strlen(RF_TIME_SYNC_HEADER) + 1, "%lu %d", &master_time, &latency);
adjusted_time = master_time + latency;
// Update local time
Time.set_time_ms(adjusted_time);
Serial.print("[COMMUNICATION] <SYNC> Time adjusted to ");
Serial.println(adjusted_time);
// Send ACK with adjusted time
RFMessage ack;
ack.from_id = NODE_ID;
ack.to_id = msg.from_id;
snprintf(ack.payload, sizeof(ack.payload), "%s %lu", RF_TIME_ACK_HEADER, adjusted_time);
rf_stop_listening();
rf_send(msg.from_id, ack, false);
rf_start_listening();
Serial.println("[COMMUNICATION] <SYNC> Time sync ACK sent.");
break;
}
node_status.node_flags.time_rf_synced = (adjusted_time > 0);
return node_status.node_flags.time_rf_synced;
#endif
}
本项目中的时间同步分为两类:
1. 与互联网进行时间同步(使用 NTP 协议)
2. 设备之间进行本地时间同步(基于 RF 通信和 RTT 计算)
一、与互联网的时间同步 - Network Time Protocol (NTP)¶
NTP(网络时间协议)用于同步设备与互联网上标准时间服务器的时间。其基本原理如下:
- 本地设备发送时间请求,并记录本地发送时间
t1
- NTP 服务器接收请求,记录接收时间
t2
- NTP 服务器返回当前时间
t3
- 本地设备接收响应,并记录接收时间
t4
通过这些时间戳,本地设备可以估算网络延迟,并计算出精确的服务器时间以调整本地系统时间。
本项目中使用的相关函数是:
sync_time_ntp()
¶
该函数通过 NTPClient
库从互联网获取当前的 Unix 时间戳。其核心逻辑包括:
- 初始化 NTP 客户端并连接服务器
- 获取当前 UTC 时间(Unix timestamp)
- 将其转换并更新全局时间变量
Time
- 同时记录当前的运行时间(如
millis()
),用于后续以毫秒精度推算当前时间
这种方式适用于设备刚启动时或需要校准的场景,缺点是依赖网络,并且精度受限(Arduino 平台通常精度只能到秒级)。
二、本地设备之间的时间同步 - 基于 RTT 和 RF 通信¶
由于 NTP 在嵌入式平台上精度有限,且依赖网络,因此我们为本地无线传感器节点设计了基于无线电(RF)通信的高精度同步机制。其主要思路如下:
- 由主节点(Gateway)作为时间基准
- 子节点(Leaf)通过 RF 通信接收主节点发送的基准时间
- 同步过程中计算 RTT(Round Trip Time)来估算通信延迟
时间同步分为两个步骤:
1. 节点在线状态检测与 RTT 计算¶
主节点向每个子节点发送 PING
消息,子节点收到后立即回复 PONG
消息。主节点通过记录:
t_send
:发送PING
的时间t_recv
:收到PONG
的时间
可计算出往返时间:
从而估算一半的单向延迟(RF latency):
主节点维护两个数组:
node_rf_latency[]
:记录每个子节点的通信延迟node_online[]
:记录每个子节点的在线状态(是否有响应)
如果某些子节点未响应,系统会使用在线节点的平均延迟作为替代值填充。
2. 基于 RTT 和主节点时间的同步¶
在获取所有节点的通信延迟后,主节点发送 TIME_SYNC
消息给各个子节点,消息内容包含:
- 当前主节点时间
- 与该子节点的 RF 延迟
子节点收到后进行如下操作:
- 拆解消息,提取主节点时间和延迟
- 将主节点时间加上延迟,得到本地应设定的同步时间
- 将该同步时间覆盖本地时间
- 回复一条
ACK
消息,带上新同步时间作为确认
这样子节点的时间就与主节点同步了,并可在毫秒精度范围内保持一致性。
总结¶
同步方式 | 特点 | 优点 | 缺点 |
---|---|---|---|
NTP同步 | 通过网络获取UTC时间 | 无需本地基准设备,通用性强 | 精度有限(秒级),依赖网络 |
本地RF同步 | 节点间无线通信+RTT校准 | 精度高(毫秒级),适用于局域无线网 | 需实现通信机制和节点协调 |
为了兼顾精度与实用性,本项目设计中采用先通过 NTP 初始化时间,然后通过 RF 实现子节点的高精度同步。
串口输出示例¶
以下是一个典型的串口输出示例,展示了时间同步过程中的关键步骤: