WiFi 时间
摘要
一篇关于一个已废弃项目的详细记录,该项目旨在构建一个“假的”GPS模块,通过WiFi使用NTP和ESP8266获取时间,目标达到亚毫秒精度,但最终因硬件限制未能实现。
暂无内容
查看缓存全文
缓存时间: 2026/06/05 05:05
# Wifi Time - mitxela.com
来源: https://mitxela.com/projects/wifi_time
这里我将描述一个废弃的项目:构建一个“伪造”的GPS模块,通过WiFi获取时间,用作早期版本精准时钟中GPS分线板的直接替代品。我的错误在于过于雄心勃勃——试图在WiFi环境下,使用普通NTP和慢轮询间隔,仅靠ESP8266的内置振荡器实现亚毫秒级精度,这几乎是不可能完成的目标。我始终感觉成功近在咫尺,但最终不了了之,什么都没发出来。
距离上次碰这个项目已经快六年了。鉴于Mk IV版时钟已经配备了更好的GPS模块(以及用于更好或更佳位置天线的SMA连接器),我大概永远不会完成它了。不过前些天我写关于我那可笑的NTP代理虚拟精准时钟 (https://mitxela.com/projects/vpc) 时,想起了这些早期的实验,觉得或许值得分享一下。
现在,我将把叙述权短暂交给年轻时的自己,来看看2020年7月我写下的部分记录。
如果你想打造一台精准时钟,GPS无疑是最佳选择。没有其他方案能如此轻松地获得如此出色的结果。即便是最便宜的GPS模块,定时精度也能达到几十纳秒。
但在室内,GPS信号接收并不理想。尤其是在采用铝箔隔热层的新建住宅中,GPS信号有时根本无法稳定接收。
网络时间协议(NTP)的精度比GPS模块差了好几个数量级,但在许多建筑中,获取WiFi信号比获取GPS信号更容易。
我对构建带WiFi功能的精准时钟毫无兴趣。把它接入互联网会使其变成另一种完全不同的时钟。我的下一版精准时钟不太可能加入WiFi功能。但是……“伪造GPS”模块确实有某种吸引力——它可以直接插入精准时钟,将其转换为一个WiFi NTP时钟。
上面提到的例子是用Arduino环境配合ESP8266“核心”构建的。我安装它进行尝试,必须承认,虽然我讨厌它的许多方面(臃肿,掩盖重要细节),但上手轻松这一点却难以反驳。上一次(也是唯一一次)尝试ESP8266项目时,我手动安装了SDK和工具链,花了我整整大半天时间。尽管这次是重新开始,但我决定尽可能继续使用Arduino环境。
Arduino和ESP8266有几十种NTP实现,但看到它们的代码令人沮丧。自带的“NTP库”通过循环调用`delay(10)`并计数迭代次数来测量时间。所以即使其他部分完美,结果最多也只能量化为10ms的增量。我必须对可能达到的精度水平保持现实——几毫秒可能已是最好结果——但我们不妨按照标准正确实现,并尽我们所能做到最好。
另一个与新手友好环境相关的问题是,许多教程和示例都是盲目复制粘贴彼此的代码片段。那个示例NTP实现莫名其妙地用垃圾数据填充了NTP头部的某些部分,而许多其他示例和实现都不加解释地复制了这段代码。
实际上,只需一个非零字节即可构成一个有效的NTP请求。第一个字节需要表明我们是客户端,并可选择指定NTP版本。其余47个字节可以留零,它们会被服务器填充。
### NTP时间戳
互联网上有许多指南解释NTP的工作原理。本质上,我们发送一个请求并等待其响应,测量所花费的时间。响应中包含服务器设置的时间戳。我们将请求往返时间的一半加上去,就能得到一个准确的时间戳。如果网络路由对称,精度可以远高于1毫秒。
NTP时间戳为8字节。前4字节是从1900年1月1日开始的秒数。与Unix时间戳非常相似,只是令人厌烦地不同。它是一个无符号数,所以会在2036年溢出,比Unix时间戳的溢出早两年。后4字节是一个小数额,因此完整的8字节构成了一个定点数。
示例NTP代码手动将其转换为日期和时间字符串,但标准的`time.h`内容已默认包含,所以我们只需加上一个偏移量得到Unix时间戳,然后调用`gmtime`和`strftime`即可。
### 字节序
NTP包中的时间戳采用网络字节序,即大端序。ESP8266默认是小端序(处理器技术上支持两者,但我怀疑在Arduino环境中能否切换)。当我发现gcc的一个相对较新的特性`scalar_storage_order("big-endian")`时,我愉快地告别了用于字节序转换的无尽移位。给结构体添加这个属性将强制其采用该字节序,无论平台如何。所需的任何移位指令都会自动生成。
遗憾的是,这在这里完全不奏效。Arduino是C++而非C,g++不支持这个扩展。更糟的是,所有警告默认被抑制,所以它甚至不会告诉你它忽略了这个属性。
***
现在我们回到这个2026年的常规作者身上,我将尽力回忆那些有趣的关键细节。
### 脉冲测量
最重要的部分是能够测量我们的输出。我们很幸运手头有真正的GPS模块,并且在我们工作的容差范围内,它的PPS输出可以视为绝对基准。关键在于给它装上非常好的天线,使其不会失去定位。
我们将在ESP8266上生成自己的PPS输出,然后与GPS输出进行比较。进行此操作的最佳工具是逻辑分析仪。幸运的是,我投资了这个世界上最便宜的逻辑分析仪:
廉价USB逻辑分析仪
这个8通道、24MHz的USB逻辑分析仪大约花费了5英镑,它们竟然厚颜无耻地在上面印了“Saleae”,还用了令人愉悦的Comic Sans字体。但对于我们所需的工作来说,它已经绰绰有余了。
该设备上的芯片组是Cypress FX2,有趣的是,固件在上电时通过USB加载到RAM中。开源的`fx2lafw`固件可以由sigrok为我们加载。
我们希望以最高分辨率采样,但需要持续采样数分钟甚至数小时,这将产生海量数据。更好的方法是每秒只存储两个脉冲之间的偏移量。Sigrok支持许多协议解码器,我知道编写自己的解码器并不太难,但一个名为“Jitter”的现有解码器就能完成我们所需的所有工作。我们指示它将GPS PPS视为时钟,将ESP8266 PPS视为信号。
命令类似这样:
```
sigrok-cli -d fx2lafw --config samplerate=200kHz --samples 800M -C D0,D1,D2,D3,D4 -P jitter:clk=D0:sig=D4 -B jitter
```
这可以通过管道(或tee)输出到一个文件,得到一系列偏移量,然后粘贴到电子表格中。命令行替代方案是gnuplot,它古老而古怪,但由于我重复执行了几十次,将其自动化是合理的。显然,当时我正经历一个perl阶段,因为我写了一个小脚本放在它们之间,像这样:
```
#!/usr/bin/perl
$| = 1;
while (<>) {
chomp;
print $i++ . "\t";
while ($_ > 0.5) {$_ -= 1.0;}
print $_ . "\n";
}
```
这还可以补偿抖动值在恰好错误时刻启动时可能出现的整秒回绕。为了实时查看图表,我的gnuplot文件如下所示(根据需要可选地固定y范围):
```
plot "plot.dat" using 1:2 with lines
#set yrange [0:0.004]
pause 1
reread
```
我希望当时做了更详细的笔记,但显然我的重点是让脉冲定时正确,而不是记录如何绘制图表。我记得我最终的设置是一个单一命令,它开始记录偏移量,同时实时绘制它们,这使得开发过程比原本稍微不那么痛苦。但仍然需要等待大约15分钟才能看到任何更改的效果。
### 硬件设置
实验是在这块相当肮脏的旧面包板上进行的。我倾向于认为当时它更干净一些,是在后续存储过程中积累了污垢。
肮脏的面包板上的ESP8266
飞线桶形插孔上有一个稳压器。图中未显示用于重新编程的FTDI线缆、用于对比的GPS模块以及逻辑分析仪。
面包板特写
我确实也在洞洞板上做了一个原型,但这对于开发和测量其性能来说用处不大。
洞洞板上的原型
跳线用于将其进入编程模式,这恰好使用与NMEA相同的TX/RX引脚。
### 驯服与NTP礼仪
轮询NTP给我们一个瞬时的时间估计。它假设往返大致对称。由于WiFi的糟糕不可预测性,我们需要进行大量估计来平均掉抖动。数据包可能在任一个方向或两个方向上引入随机延迟。
一旦我们有了一些估计,我们需要“驯服”我们自己的振荡器,以便在两次轮询之间维持时间。我常想知道NTP的礼仪是什么,因为它是一个未经身份验证的UDP包,以及为了不被视为滋扰,你需要以多高的频率轮询这个免费服务。默认的NTP安装很少以低于64秒的间隔进行轮询,通常它们每小时或每几小时才轮询一次。当我(当时)计划生产一块电路板并分发给他人时,我尤其担心,即使数量很少。
我的一般计划是开始每16秒轮询一次,然后随着估计值相互吻合而逐渐降低频率。
ESP8266的板载振荡器并不特别精确。驯服它比简单地轮询、判断我们走快或走慢然后调整要复杂得多。为了排除异常值,正确平均,并希望随着轮询频率降低而退避,我们需要保存估计值和调整的历史记录,但这些时间值是以一个正在变化的振荡器为参考的。这是一个相当复杂的控制环路问题。
与其驯服主振荡器,我们可以假装它是稳定的,然后驯服从它派生的次级定时器。主振荡器仍会随温度漂移,但希望漂移足够慢,不会对我们的估计造成严重破坏。
### 首批图表
也许通过图表来讲述故事更好。第一次捕获的结果如下:
第一张图
在这张及后续图表中,横轴为秒,1000大约对应16分钟。纵轴也为秒,显示PPS脉冲之间的偏移量。在这第一张图中,我们只是每10秒轮询一次NTP,并据此尝试输出PPS。
显而易见,存在大量异常值,且偏向正值。我当时住的公寓WiFi争用极其严重,有几十个重叠的网络。我对WiFi的物理层不太熟悉,但也许数据包碰撞的概率在一个方向上大于另一个方向。如果初始出站数据包因信道繁忙而无法发送,它将被延迟,但一旦信道空闲,它(可能)在接下来的大约一秒内保持空闲,这意味着返回时碰撞的可能性较小。不过这只是我的推测。
再次测量时,一些异常值超过了15毫秒。
第二张图
除了异常值外,平均值似乎稳定在大约3毫秒的偏差上,这当时是个谜。
通过跟踪我们的估计并丢弃超出阈值的结果来排除异常值后,我们的结果仍然相当不令人满意:
第三张图
注意纵轴刻度已更改。两次轮询之间线条的斜率与之前相同,只是更明显了。它代表了我们未校准振荡器的漂移,而我们尚未对其进行驯服。
许多次采集之后,我有时能得到像这样好的图表:
第四张图
即使未驯服,结果也几乎在一毫秒内吻合,这太棒了。系统偏差当时仍然是个谜。想想可能是什么原因……
### 环路调谐
如果我们的振荡器驯服工作良好,每次轮询之间的线条斜率应趋于完全水平。然后我们可以降低轮询频率,依靠调谐后的振荡器运行。最初的尝试带来了一些有趣的结果。
第五张图
我们的校正过度,系统变得不稳定。作为我们强制稳定性的部分尝试,我们将每次轮询的结果与之前的估计进行平均,限制单个数据点的影响。
调整控制环路的参数甚至可以几乎被称为有趣,如果不需要再等16分钟才能检查其效果的话。
第六张图
### 系统偏差与池
在整个测量过程中偏移量保持一致,但如果改天再次测量则不同。令人尴尬的是,我很晚才意识到数字收敛到几毫秒偏差的原因:NTP报告的时间有误。NTP“池”是报告时间的服务器集合,免费提供。这些服务器大部分由志愿者运行。当你请求`pool.ntp.org`(或其子域)时,它返回一个地理上相近的、属于该池的服务器的IP地址。大多数服务器不是“stratum-1”服务器,即它们本身也只是通过NTP与其他服务器同步。
我们在开始轮询之前进行DNS查找(否则会破坏定时),然后保留服务器IP地址可能几个小时甚至更长时间。通过一些日志记录,我们可以将性能与IP地址关联起来,并且变得明显的是,偶尔返回的一个IP地址(实际上属于Cloudflare)上的结果要好得多。
第七张图
将`time.cloudflare.com`设置为首选时间服务器后,性能立即大幅提升。显然Cloudflare有预算让他们的时间服务器保持准确,不过话说回来,一个标准的GPS模块也并不贵。我想绝大多数使用NTP的人并不在乎那几毫秒。
### 另一个环境
对我的结果的第二大改进来自在另一个不同的WiFi网络上运行实验。在朋友家和黑客空间尝试后,发现我家的网络抖动比大多数地方都大得多。但是,它*必须*能在我的家庭网络上运行,否则就不值得折腾。在一天中的某些时段,它似乎工作得好得多,所以我尝试进行了一些更长时间的测量。如果我们的控制环路工作良好,我们应该能够平均掉这些差异。
第八张图,4000个数据点
4000个点大约对应66分钟。左侧的引导段是上电序列,此时振荡器偏差很大。规划中的下一步可能是将我们的估计值存储到EEPROM中,这样初始上电时能更接近目标。
由于横轴刻度改变了,斜率看起来被放大了,但这实际上是一个非常棒的结果,让我相信目标近在眼前。上面图表中的大部分点与GPS参考的偏差在半毫秒以内。我们所要做的就是让控制环路正确,这样我们就可以降低轮询频率。
第九张图
看着这些图表很容易……
相似文章
PTP墙钟不实用且过于精确
使用树莓派和LED矩阵显示器构建PTP墙钟的指南,演示通过精确时间协议实现精确时间同步。
Wi-Wi:1纳秒级无线时间同步
Wi-Wi STAMP是日本NICT开发的无线时间同步协议和硬件,使用900 MHz频段实现纳秒级同步和毫米级精度,已在NAB 2026上展示。
时间黑客:利用音频谐波欺骗原子钟
本文探讨了消费级原子钟如何通过音频谐波信号被欺骗的漏洞,并详细说明了由于电离层传播问题,在美国东海岸接收 NIST 的 WWVB 无线电信号所面临的技术挑战。
使用WiFi感知人类 – Ruview
关于使用WiFi信号感知人类存在、运动和生命体征的技术综述
基于Jetson Orin NX SUPER 16GB打造了一款完全离线运行的行李箱机器人。使用Gemma 4 E4B模型,缓存TTFT约200毫秒,搭载30多个传感器,无WiFi/蓝牙/蜂窝网络连接。它有自己的想法。
一位开发者使用Jetson Orin NX和Gemma 4 E4B模型打造了一款名为Sparky的完全离线行李箱机器人,实现了约200毫秒的缓存TTFT和14-15 tok/s的处理速度,通过30多个传感器以自然语言形式将数据输入提示词,全程无网络连接。