ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

树莓派Pico RTC与NTP时间同步实战:解决掉电丢时间问题

树莓派Pico RTC与NTP时间同步实战:解决掉电丢时间问题 1. 为什么树莓派 Pico 需要一个 RTC从掉电丢时间说起树莓派 Pico 是我用过的最便宜的开发板之一但便宜并不代表简单。很多刚开始接触 Pico 的朋友都会遇到一个特别尴尬的问题板子跑得好好的一旦断电重启时间就回到 2021 年 1 月 1 日。日志文件里记录的时间全部作废数据采集的排序完全错乱定时任务全部失效。如果你正在做一个需要记录时间戳的项目这个问题几乎绕不开。Pico 用的 RP2040 芯片内部其实集成了一组实时时钟RTC外设底层硬件是有的但它有一个致命的短板——没有独立的备用电池供电引脚。也就是说这颗 RTC 只有在 Pico 上电的时候才能维持运行一旦主电源断开RTC 内部寄存器立即归零。这和你在电脑主板上看到的那种带纽扣电池的 RTC 完全是两回事。所以我们在 MicroPython 环境下做 RTC 开发实际上要做两件事第一掌握内置 RTC 的正确配置和读取方法让系统在运行时拥有准确的时间基准第二通过 NTPNetwork Time Protocol网络时间协议在联网时自动校准时间解决掉电归零的问题。这两个能力组合起来才能让 Pico 变成一个真正有用的、带准确时间的嵌入式设备。这篇文章我想把这两部分内容完整地过一遍。我不会只贴一段能跑的代码就算完事而是把 RTC 在 MicroPython 里的行为机制、NTP 同步的具体实现步骤、时区处理的坑、以及没有网络时的降级方案都详细展开。无论你是刚开始玩 Pico 的新手还是已经做过几个项目的中级玩家这篇文章应该都能给你一些有用的参考。先说清楚我写这篇文章的前提条件你需要一块树莓派 Pico 开发板Pico W 更佳因为它自带 Wi-Fi以及一个烧录好 MicroPython 固件的环境。如果你用的是普通 Pico也没有关系我会把有网络和无网络两种场景的方案都覆盖到。2. Pico 内置 RTC 硬件机制为什么它会归零以及 MicroPython 如何操作它2.1 RP2040 的 RTC 外设到底是个什么东西树莓派 Pico 的核心芯片 RP2040 内部有一个完整的 RTC 模块。它本质上是一组计数器在系统时钟的驱动下以秒为单位累加时间同时维护年、月、日、时、分、秒这些完整的时间字段。从硬件层面来看它和你在 STM32、ESP32 上看到的 RTC 没什么本质区别都遵循 BCD 编码格式来存储时间数据。但问题出在电源设计上。RP2040 的 VBUS 引脚负责主供电3V3_EN 引脚控制内部稳压器。这个芯片没有设计独立的 RTC 电源域也就是说它的 RTC 寄存器和主系统共享同一个电源。一旦主电源断开寄存器内容全部丢失。RTC 模块的计数器和寄存器组既没有掉电保持机制也没有预留外部电池接口。这是芯片设计层面的硬限制不是软件能解决的。我在实际项目里验证过这个行为。用一块普通 Pico 板子跑了一段 MicroPython 脚本先设置好当前时间然后断电等 30 秒再重新上电读取 RTC 时间已经回到了默认值——MicroPython 固件会给出一个编译固件时设定的默认时间通常是 2021 年 1 月 1 日 00:00:00。这个现象符合预期但也说明了一个关键点如果你想做需要长期稳定时间的设备光靠内置 RTC 不够必须引入外部 RTC 芯片或者每次上电后通过 NTP 自动校准。这个话题我会在第 6 部分详细展开。这里的核心机制你要记住Pico 内置 RTC 是易失性的它依赖系统主电源存活。MicroPython 对它的操作本质上就是对一组硬件寄存器的读写没有电池备份掉电即清零。2.2 MicroPython 中 RTC 的核心 API 使用细节MicroPython 的machine模块里提供了RTC类用法非常简洁。初始化一个 RTC 对象from machine import RTC rtc RTC()这里有一个容易忽略的细节machine.RTC的构造函数不需要传任何参数。在 RP2040 平台上它直接映射到底层 RTC 硬件。初始化之后RTC 会立刻以默认时间开始运行。如果你不设置时间读出来的就是固件编译时的默认时间。设置时间的标准方法是传一个元组给datetime()方法rtc.datetime((2025, 3, 15, 6, 10, 30, 0, 0))这个元组共 8 个字段顺序依次是年、月、日、星期、时、分、秒、微秒。注意这里的星期字段MicroPython 用的是 0 表示周一6 表示周日。很多刚接触的朋友在这里踩坑以为和 Linux 里的tm_wday一样是 0 表示周日。读取时间更简单current rtc.datetime() print(current)输出结果也是一个 8 元素元组(2025, 3, 15, 5, 10, 30, 0, 0)我建议在实际项目里把 RTC 的读写封装成一个独立的模块方便统一管理。下面是我常用的写法# rtc_driver.py from machine import RTC class PicoRTC: def __init__(self): self._rtc RTC() def set_time(self, year, month, day, hour, minute, second, weekdayNone, sub_second0): if weekday is None: weekday self._calc_weekday(year, month, day) self._rtc.datetime((year, month, day, weekday, hour, minute, second, sub_second)) def get_time(self): dt self._rtc.datetime() return { year: dt[0], month: dt[1], day: dt[2], weekday: dt[3], hour: dt[4], minute: dt[5], second: dt[6], sub_second: dt[7] } staticmethod def _calc_weekday(year, month, day): # 蔡勒公式计算星期返回0表示周一 if month 3: month 12 year - 1 K year % 100 J year // 100 h (day 13 * (month 1) // 5 K K // 4 J // 4 5 * J) % 7 # h: 0Saturday, 1Sunday, 2Monday, ..., 6Friday return (h 5) % 7这个封装的意义在于你可以在set_time()里省掉手动计算星期的步骤只需要提供年月日时分秒即可。_calc_weekday方法用了蔡勒公式是公历日期转星期的经典算法输入输出已经对齐 MicroPython 的 0周一约定。2.3 RTC 驱动框架的理解其实就是一个寄存器操作层如果你研究过 Linux 内核的 RTC 驱动框架比如drivers/rtc/rtc-rp2040.c就会发现内核把 RTC 分成了两个层面底层驱动负责读写硬件寄存器上层接口通过rtc_class_ops结构体向用户空间暴露read_time、set_time、read_alarm等标准操作。MicroPython 的底层实现思路基本一致也有machine_rtc.c这样的源文件来对接固件层面的machine.RTC类。RTC 模块的内部结构包含一个 6 字节的 RTC 寄存器区域RTC_0到RTC_5存储秒、分钟、小时、日期、月份、年份以及星期。它还有一个 RTC IRQ 中断控制器可以配置闹钟中断和定时中断。MicroPython 里目前没有把闹钟中断完整暴露出来RTC 类只提供了时间读写功能。如果你需要闹钟功能得通过 C 扩展自己封装或者在 MicroPython 里用定时器轮询来实现。从应用开发者的视角来看你不需要关心这些寄存器的具体地址和位域分布但理解这个驱动框架有助于你定位问题。比如你发现 RTC 时间走得不准就大概率不是驱动层代码的问题而是晶振频率偏差导致的时钟漂移。我建议把硬件-驱动-应用三层分开来看问题这样排查效率会高很多。3. 一劳永逸解决时间问题NTP 时间同步的完整实现3.1 NTP 的原理到底是怎么回事Network Time ProtocolNTP是互联网上广泛使用的时间同步协议。它的基本思路非常朴素客户端向服务器发送一个请求报文记录发送时间 t1服务器收到后记录接收时间 t2并在回复报文里填上发送时间 t3客户端收到回复后记录接收时间 t4。通过这四个时间戳就可以计算网络传输延迟和客户端与服务器之间的时钟偏移然后调整本地时钟。NTP 报文格式里最重要的字段包括 leap indicator、version、mode、stratum、poll、precision、root delay、root dispersion、reference ID、reference timestamp、originate timestamp、receive timestamp 和 transmit timestamp。MicroPython 环境里做 NTP 时间同步最常用的方法不是自己解析这些二进制字段而是使用现成的 NTP 客户端库。在 MicroPython 生态里最流行的 NTP 客户端来自micropython-lib的ntptime.py模块。它的核心逻辑是发送一个 SNTPSimple NTP请求到默认的 NTP 服务器默认是pool.ntp.org或阿里云的 NTP 服务器解析响应中的 transmit timestamp然后调用rtc.datetime()设置系统时间。用 SNTP 而不是完整 NTP 的原因很简单嵌入式设备的计算资源和网络条件有限不需要完整的 NTP 复杂算法只需要时间偏移修正这一个基本功能。SNTP 是 NTP 的简化版本精度通常在几十毫秒到几百毫秒之间对于绝大多数嵌入式应用足够了。3.2 在树莓派 Pico 上实现 NTP 同步首先要强调NTP 需要网络连接。如果你用的是 Pico W自带 Wi-Fi直接用内置的network模块连接 Wi-Fi 即可。如果你用的是普通 Pico需要外接一个 ESP8266 或 ENC28J60 之类的网络模块。本文以 Pico W 为基础来演示。先写一个连接 Wi-Fi 的辅助函数import network import time def connect_wifi(ssid, password, timeout15): wlan network.WLAN(network.STA_IF) wlan.active(True) if not wlan.isconnected(): print(Connecting to WiFi...) wlan.connect(ssid, password) start time.time() while not wlan.isconnected(): if time.time() - start timeout: raise RuntimeError(WiFi connection timeout) time.sleep(0.5) print(WiFi connected:, wlan.ifconfig()) return wlanWi-Fi 连接成功后就可以导入ntptime模块进行时间同步了import ntptime from machine import RTC rtc RTC() def sync_time_ntp(): ntptime.settime() current rtc.datetime() print(NTP sync done:, current) return currentntptime.settime()内部做了什么我简单拆解一下。它先创建一个socket连接发送一个 SNTP 请求到服务器 123 端口。服务器响应后它会从响应报文中偏移 40 字节的位置读取 8 字节的 transmit timestamp这是一个 64 位的 NTP 时间戳前 32 位是从 1900 年 1 月 1 日开始的秒数。然后它把 NTP 时间换算成 Unix 时间戳即从 1970 年 1 月 1 日开始的秒数再把 Unix 时间戳分解成年月日时分秒直接写入 RTC 寄存器。这里有一个关键点ntptime.settime()默认设置的时区是 UTC。如果你在中国需要手动加上 8 小时也就是在同步完成之后将本地时间设置为 UTC8。常见的做法是自定义一个时间同步函数或者修改ntptime.py源码中的时间补偿逻辑。3.3 中国时区和夏令时的坑不止加 8 小时这么简单我在网上看到很多教程说只要在ntptime.settime()之后给小时加 8 就行。这个说法基本正确但不够严谨。它隐含了一个前提你只用RTC.datetime()来读取时间字段而且完全不关心跨日、跨月、跨年时的进位。Python 的time模块提供了mktime()和localtime()函数正确处理时区需要用到它们。我的做法是先读取 NTP 同步后的 UTC 时间把它转换成 Unix 时间戳然后加上 8 小时的秒数28800 秒再用time.localtime()转换回本地的年月日时分秒字段最后设置到 RTC 里。这样即使过了午夜 12 点日期也能自动进位。下面这个函数我一直在用实测没有问题import time from machine import RTC rtc RTC() UTC_OFFSET 8 * 3600 # 中国标准时间 UTC8 def sync_time_ntp_cst(): import ntptime ntptime.settime() # 获取当前 UTC 时间的 Unix 时间戳 utc_tuple rtc.datetime() utc_timestamp time.mktime((utc_tuple[0], utc_tuple[1], utc_tuple[2], utc_tuple[4], utc_tuple[5], utc_tuple[6], 0, 0)) local_timestamp utc_timestamp UTC_OFFSET local_tuple time.localtime(local_timestamp) # local_tuple 是 (year, month, day, hour, minute, second, weekday, yearday) weekday local_tuple[6] # 在 MicroPython 里localtime 返回的 weekday 0Monday rtc.datetime((local_tuple[0], local_tuple[1], local_tuple[2], weekday, local_tuple[3], local_tuple[4], local_tuple[5], 0)) print(Time synchronized (CST):, rtc.datetime())这种做法的好处还在于它天然的处理了闰年、大小月、跨年进位等问题。因为它实际上是先把所有的日期时间字段规约为一个单调递增的整数Unix 时间戳然后再从整数反解出人类可读的时间字段。这一点是很多新手容易忽略的直接对小时字段加 8 会在跨天时产生严重错误。关于时区你还需要注意MicroPython 的time.localtime()底层实现是标准的gmtime加上偏移但它不处理夏令时。对于中国这种全年固定 UTC8 的地区完全没问题。但如果你的项目要跑到实行夏令时的地区就需要更复杂的处理逻辑了。3.4 NTP 同步失败的容错处理不能一崩了之实际项目中Wi-Fi 连接不稳定、NTP 服务器无响应、DNS 解析失败都是很常见的情况。如果同步失败就直接抛异常重启后还是错误时间那就违背了引入 NTP 的初衷。我建议把时间同步拆成两步第一步判断是否需要同步比如 RTC 时间早于某个阈值说明上次同步后发生过掉电第二步执行同步并在失败时给出降级方案。def is_rtc_time_valid(min_year2025): rtc RTC() y rtc.datetime()[0] return y min_year这个函数的作用是判断 RTC 时间是否合理。如果你设定的最低合法年份是 2025那么从 2021 年 1 月 1 日这种固件默认时间就会被判定为无效。在这个基础上时间同步逻辑可以这样写def ensure_time_synced(ssid, password, forceFalse): if not force and is_rtc_time_valid(): print(RTC time seems valid, skip NTP sync) return True try: connect_wifi(ssid, password) sync_time_ntp_cst() return True except Exception as e: print(NTP sync failed:, e) return False如果同步失败你可以继续使用 RTC 的当前时间虽然它是错的或者让设备重新进入连接等待状态。在需要精准时间的系统中我通常的做法是在日志里记录时间未同步的告警状态系统继续运行但时间戳标记为不可信。等到网络恢复后再自动触发同步。4. NTP 同步之后RTC 校准、时间漂移与精度实测4.1 为什么同步完了时间还会慢慢跑偏很多朋友有个误解认为只要 NTP 同步一次RTC 就永远准确了。事实不然。我实测过Pico 内置 RTC 在常温下的走时误差大约每天 2 到 10 秒不等具体取决于板卡的晶振质量和工作温度。这个误差来自 32.768kHz 晶振的频率偏差是物理层面的软件无法彻底消除只能通过定期同步来纠正。对于大部分应用场景比如日志打点、数据采集时间戳这个精度已经够用。但如果你要做的设备是自动贩卖机、打卡机、或者任何需要断电后依然长时间准确走时的设备就很有必要考虑外部 RTC 芯片方案。我在第 5 部分会专门讨论。NTP 同步并不是越频繁越好。每次同步会消耗网络流量而且如果设备时钟和服务器时间偏差不大频繁同步没有意义。经验法则是每天同步 2 到 4 次每次同步之间的间隔可以根据你观察到的漂移率来调整。比如你发现设备每天快 5 秒可以每 12 小时同步一次这样时间偏差始终控制在 2.5 秒以内。4.2 测量你的 Pico RTC 实际漂移率漂移率是衡量 RTC 走时精度的关键参数。测量方法很简单先用 NTP 同步到标准时间然后在 24 小时后再次读取 RTC 时间比较与标准时间的差值。把这个差值记录下来就是一天内的绝对漂移量。我做一个示例import time import ntptime from machine import RTC rtc RTC() def measure_drift(): # 假设已经完成NTP同步 t0 time.time() time.sleep(24 * 3600) # 等待24小时实际项目中不用真等这里是示意 rtc_tuple rtc.datetime() t1 time.mktime((rtc_tuple[0], rtc_tuple[1], rtc_tuple[2], rtc_tuple[4], rtc_tuple[5], rtc_tuple[6], 0, 0)) drift_seconds t1 - (t0 24 * 3600) print(Drift in 24 hours: {} seconds.format(drift_seconds))实际操作中你不需要让板子真的在那里干等 24 小时。你可以记录两次 NTP 同步之间的 RTC 走时差值除以经过的小时数得到每小时漂移率。比如 12 小时漂移了 3 秒那每小时漂移就是 0.25 秒平均一天 6 秒。知道了这个数值你就可以设置合理的 NTP 重同步周期。这个数据还有一个用途如果漂移量特别大比如每天超过 30 秒说明你的晶振可能有问题或者板子供电不稳。正常 Pico 板卡应该落在每天 5 到 20 秒这个范围内。如果超出太多先检查电源质量再看是不是温度太高。4.3 温度对 RTC 精度的影响一个隐蔽的变量晶振频率会随着温度变化。RP2040 内部 RTC 使用的 32.768kHz 晶振频率温度系数通常在 -0.04 ppm/°C 左右。这意味着温度每升高 30 度每天可能多漂 1 到 2 秒。如果设备放在户外、机柜或者其他温度波动大的环境里实测漂移率会和室内测试结果有明显差异。对于实际项目我建议你在目标环境温度下做一次漂移测量而不是直接沿用室内数据。这也是为什么很多工业设备在设计时会加温度补偿或者缩短 NTP 同步周期。如果你做的设备要在室外跑把 NTP 同步周期从 24 小时缩短到 6 小时是比较稳妥的选择。5. 断网场景怎么办外部 RTC 芯片与 Pico 的搭配方案5.1 内置 RTC 不够用接一颗 DS3231 是主流做法NTP 解决的是联网时如何自动校准时间的问题但如果你想做一个完全离线的设备或者设备经常长时间断网内置 RTC 的弱点就无法忽视了掉电即清零。这时候解决方案很明确外接一颗带电池备份的 RTC 芯片。行业里最常用的选择是 DS3231精度极高温补晶振年误差通常在 ±2 分钟以内I2C 接口自带涓流充电电路可以直接接充电电池。它还有一个关键优势模块上通常带一个 CR2032 电池座或者 LIR2032 可充电电池断电后 RTC 继续走时数据不会丢失。在 MicroPython 里操作 DS3231 非常简单。machine.I2C配合现成的ds3231.py驱动文件就行。驱动逻辑就是通过 I2C 读写 DS3231 内部寄存器地址 0x00 到 0x06 分别存储秒、分、时、星期、日、月、年寄存器值以 BCD 格式保存。5.2 双 RTC 架构既要有本地时间又要有标准时间我的实际做法是Pico 内置 RTC 作为主计时器DS3231 作为断电保持的备用时间源。上电后程序先检查内置 RTC 时间是否合法。如果不合法说明刚上电内置 RTC 是默认时间就从 DS3231 读取上次保存的时间写入内置 RTC。这样即使完全没有网络系统也能保持正确时间。如果之后能联网再用 NTP 校准内置 RTC并同步写回 DS3231确保备用时间源保持准确。这个双 RTC 架构听起来复杂但代码并不难。下面是一个简化的驱动例子from machine import I2C, Pin, RTC import time class DS3231: def __init__(self, i2c, addr0x68): self.i2c i2c self.addr addr def _bcd_to_dec(self, bcd): return (bcd 4) * 10 (bcd 0x0F) def _dec_to_bcd(self, dec): return ((dec // 10) 4) | (dec % 10) def read_time(self): data self.i2c.readfrom_mem(self.addr, 0x00, 7) second self._bcd_to_dec(data[0] 0x7F) minute self._bcd_to_dec(data[1]) hour self._bcd_to_dec(data[2] 0x3F) day self._bcd_to_dec(data[4]) month self._bcd_to_dec(data[5] 0x1F) year self._bcd_to_dec(data[6]) 2000 return (year, month, day, hour, minute, second) def write_time(self, year, month, day, hour, minute, second): data bytes([ self._dec_to_bcd(second), self._dec_to_bcd(minute), self._dec_to_bcd(hour), 0, # day of week, 这里简化为0 self._dec_to_bcd(day), self._dec_to_bcd(month), self._dec_to_bcd(year - 2000) ]) self.i2c.writeto_mem(self.addr, 0x00, data)使用方式i2c I2C(0, sclPin(5), sdaPin(4), freq400000) ds DS3231(i2c) rtc RTC() def sync_from_ds3231(): t ds.read_time() rtc.datetime((t[0], t[1], t[2], 0, t[3], t[4], t[5], 0)) print(RTC restored from DS3231:, t) def sync_to_ds3231(): t rtc.datetime() ds.write_time(t[0], t[1], t[2], t[4], t[5], t[6]) print(DS3231 updated from RTC:, t[0], t[1], t[2], t[4], t[5], t[6])这里有个细节要注意DS3231 的星期寄存器和 MicroPython RTC 的星期定义不同。DS3231 的星期范围是 1 到 71Sunday7SaturdayMicroPython 是 0 到 60Monday。我上面的驱动把星期字段直接写 0 了如果你的应用需要读取正确的星期得在两个表示方法之间做转换。5.3 全志 H136 RTC 电源切换电路为什么会被讨论搜索热词里出现了全志 h136 rtc电源切换电路说明很多人在做带 RTC 的嵌入式 Linux 板卡时会关心 RTC 的备用电源切换逻辑。虽然全志 H136 是应用处理器和 Pico 不是同一类平台但它背后的设计思路是通用的RTC 需要在主电源掉电时自动切换到纽扣电池或者超级电容供电。在 Pico 的失控场景里虽然没有 H136 那样复杂的电源管理单元但如果你自己设计了外接 RTC 模块的电路电源切换仍然是一个值得思考的问题。DS3231 模块通常自带电池座和切换二极管主电源和电池之间用两个二极管做 OR 连接确保任何一路有电都能供给 DS3231。如果你是自己打板设计记得在 DS3231 的 VCC 引脚前加一个 0.1uF 去耦电容电池正极再加一个 1kΩ 限流电阻对于 LIR2032 可充电电池这个电阻是充电电流限制的一部分。我见过不少 DIY 项目RTC 芯片单独工作都正常一接上主系统就出现时间偶尔跳变的情况最后排查发现是 I2C 上拉电阻没加。DS3231 模块上的 I2C 上拉电阻一般已经有了但如果你直接用裸芯片必须自己在 SCL、SDA 上各加一个 2.2kΩ 到 4.7kΩ 的上拉电阻到 VCC。没有上拉电阻I2C 通信会时好时坏RTC 读取的时间会随机出错。6. 实战案例做一个上电自动校时、断网也能跑的数据记录器6.1 场景设计和系统架构我去年给一个温室环境监测的小项目做过一套时间管理方案典型的资源受限场景一块 Pico W外加 DS3231 模块和几个传感器温湿度、光照。系统要求有三条每隔 5 分钟记录一次传感器数据存到 SD 卡每条数据必须带准确时间戳设备可能放在信号不好的位置Wi-Fi 不是时刻可用意外断电后重新上电能自动恢复正确时间不依赖人工设置。这个需求非常典型。我把时间管理模块拆成了三层最底层是 DS3231断电保持中间层是 Pico 内置 RTC系统主时间源最上层是 NTP 校准联网时自动纠正漂移。时序图大致是上电 → 从 DS3231 恢复时间到内置 RTC → 尝试连接 Wi-Fi → 成功则 NTP 校准并回写 DS3231 → 开始数据采集循环。6.2 完整代码框架从时间初始化到定时任务下面这个示例代码是把之前提到的所有逻辑整合起来可以直接抄下来改成自己的项目import time import network from machine import RTC, I2C, Pin # 假设 DS3231 模块接到 GP4(SDA)、GP5(SCL) i2c I2C(0, sclPin(5), sdaPin(4), freq400000) rtc RTC() ds DS3231(i2c) # 使用前面定义的 DS3231 驱动 def connect_wifi(ssid, password, timeout10): wlan network.WLAN(network.STA_IF) wlan.active(True) if wlan.isconnected(): return True wlan.connect(ssid, password) start time.ticks_ms() while not wlan.isconnected(): if time.ticks_diff(time.ticks_ms(), start) timeout * 1000: return False time.sleep_ms(200) return True def is_rtc_valid(): return rtc.datetime()[0] 2025 def main(): # 1. 优先从 DS3231 恢复时间 try: t ds.read_time() rtc.datetime((t[0], t[1], t[2], 0, t[3], t[4], t[5], 0)) print([init] Time restored from DS3231) except Exception as e: print([init] DS3231 read failed:, e) # 2. 尝试联网同步 ssid your_ssid password your_password if connect_wifi(ssid, password): print([init] WiFi connected, syncing time...) try: sync_time_ntp_cst() # 前面定义的函数 sync_to_ds3231() # 把校准后的时间写回 DS3231 except Exception as e: print([init] NTP sync failed:, e) else: print([init] WiFi unavailable, using local RTC time) # 3. 进入数据采集循环 last_record time.time() while True: now rtc.datetime() timestamp_str {:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}.format( now[0], now[1], now[2], now[4], now[5], now[6]) print([data], timestamp_str, sensor1xx) # 这里写自己的采集和存储逻辑 time.sleep(300) # 5 分钟一次这个逻辑看起来很简单但它在实际项目中能扛住各种意外情况。DS3231 读不到比如电池没电时程序不崩溃直接往下走Wi-Fi 连不上时不阻塞用本地时间继续跑NTP 同步成功后回写 DS3231把备用时间源的精度也校准了。6.3 实测中的意外情况和改进方向我在测试这套系统时遇到过几个值得注意的现象在这里一起说说。第一个是 DS3231 初次上电读出的时间是随机值。因为电池座里没有装电池芯片没有有效的时间基准。如果你从没有电池的 DS3231 模块读时间会得到乱七八糟的数值。解决方法是首次上电时如果检测到 DS3231 时间完全不合法比如年份小于 2020就强制设置一个初始时间并提示用户手动校准。第二个是 NTP 同步后如果立刻回写 DS3231可能导致 DS3231 内部的温度补偿晶振状态发生瞬时跳变。我在快速连续同步时曾观察到 DS3231 时间偶发跳变几秒的问题。稳妥的做法是NTP 同步后等待 1 到 2 秒再写 DS3231。第三个是要注意 Pico 的 I2C 引脚选择。GP4/GP5 是 I2C0 的默认引脚如果你用了别的引脚必须明确指定I2C(0, sclPin(x), sdaPin(y))。很多网友把 DS3231 接到其他引脚却忘记创建一个新的 I2C 实例一直报 I2C 通信错误就是这里出了问题。这套时间方案后来我又移植到了几个不同项目上包括一个户外气象站。那个设备在户外零下的环境里工作了一个月DS3231 走了大约 40 秒。换算一下每天漂移在 1 到 2 秒以内考虑到零下温度对晶振的影响这个精度是完全够用的。7. 进阶思考NTP 时间同步和自动驾驶/工业场景的时间同步有什么区别最后聊一个延伸话题。搜索热词里出现了自动驾驶时间同步gptp时间同步原理这些关键词。我顺便说一下嵌入式里的时间同步其实分好几个层级Pico 用的这种 SNTP 是最简单的一种精度在毫秒级到百毫秒级。而在自动驾驶、工业控制这些场景里毫秒级都不够用它们需要的是 PTPPrecision Time Protocol精确时间协议也就是 IEEE 1588 标准以及汽车领域常用的 gPTPGeneralized Precision Time Protocol通用精确时间协议。PTP 和 NTP 最大的区别在于PTP 在硬件层面对网络报文打时间戳依靠支持硬件时间戳的网卡和交换机在网络上测量时延的精度可以达到微秒甚至亚微秒级别。Pico 的普通以太网/Wi-Fi 方案根本不具备硬件时间戳能力所以它只能做 NTP/SNTP。但不管协议多高级核心思路是一样的测量网络延迟估算时钟偏移调整本地时钟。你在 Pico 上理解透 SNTP 的同步流程将来去接触 PTP 也不会觉得陌生。嵌入式时间同步的本质就是在一个分布式系统里让所有的节点都尽可能地让本地时钟逼近一个统一的时间基准。回到 Pico 这个平台。如果你只是做一个日志记录器或者传感器节点SNTP 加 DS3231 的方案已经非常够用了。如果你想继续深入还可以研究ntptime模块底层的时间戳解析代码自己改造成支持多个 NTP 服务器冗余的版本。我见过一个开源项目在 Pico 上同时配置三个 NTP 服务器取偏移量的中位数作为最终校准值这就是在向 NTP 的完整算法靠拢了。时间管理看起来是一个不显眼的模块但它是所有日志、事件、数据正确性的基础。我在好几个项目里吃过时间不对调试到怀疑人生的亏最后发现源头就是 RTC 没配置好。希望这篇文章能帮你避开我踩过的那些坑。如果有条件建议你在自己手头的项目上把内置 RTC、DS3231、NTP 三种方案都搭一遍跑一轮漂移测试对这个系统的理解会深刻很多。
返回列表