
1. 为什么树莓派 Pico 需要 RTC 和 NTP 时间同步树莓派 Pico 这块板子我用了挺长时间最早拿它做数据采集时根本没考虑时间戳这回事后来发现没有准确的时间基准采集到的数据基本没法用。Pico 本身没有板载 RTC 模块断电后时间就丢了每次上电都得重新设置这在很多实际项目里是个硬伤。比如做田间环境监测、冷链运输记录、家庭自动化定时任务设备重启后如果时间不对所有逻辑都会跟着乱套。但有朋友会说Pico W 不是能联网吗联网后用 NTP 拉时间不就行了。这话没错但有个现实问题不是所有项目都有稳定的网络环境。工业现场、户外部署、临时搭建的采集节点网络往往不稳定甚至完全没有。就算网络可用频繁请求 NTP 服务器也会增加功耗对电池供电的设备来说并不划算。更麻烦的是上电到完成 NTP 同步之间存在一个时间空窗期如果设备在这个期间需要执行按时触发的动作比如每天早上六点开闸就会错过时机。所以我的思路一直很明确用外部 RTC 模块作为本地时间基准保证断电后时间不丢失网络可用时再用 NTP 自动校准把误差控制在毫秒级。这套方案做下来Pico 就能拥有一个即使离线也能可靠运行的时间系统。这篇文章我会从硬件选型、MicroPython 驱动编写、NTP 同步原理到实际踩坑把完整实现过程拆开来讲适合正在做物联网采集、定时控制或者对时间精度有要求的嵌入式项目开发者参考。2. 硬件准备RTC 模块选型与接线注意事项2.1 为什么推荐 DS3231 而不是 DS1307市面上常见的 RTC 模块主要是 DS1307 和 DS3231 两种我第一次买的时候贪便宜选了 DS1307结果发现误差大得离谱一天能差十几秒根本不能满足定时任务的需求。后来换到 DS3231情况完全不同温度补偿晶振带来的精度提升非常明显实测一个月下来误差也就一两秒这在小成本项目里已经非常理想了。两者的核心差异在于内置晶振和补偿机制。DS1307 用的普通 32.768kHz 晶振对温度变化很敏感夏天冬天误差漂移明显DS3231 内置温度补偿晶振TCXO会根据环境温度实时修正走时频率所以能在工业温度范围内保持高精度。价格上 DS3231 贵不了几块钱但省去了后续反复校准时间的麻烦。另外要注意区分模块的引脚类型。老款 DS3231 模块通常是直插式引脚新款很多做成了 Grove 或 Gravity 接口接线方式不同别买回来发现配不上现有排线。我的建议是买标准 2.54mm 排针版本兼容性最好杜邦线一插就能用。模块上一般都留了 I2C 接口的四个引脚VCC、GND、SDA、SCL有的还引出了 SQW 方波输出引脚可以用它做定时唤醒中断但基础应用不涉及先忽略。2.2 Pico 与 DS3231 的标准接线表Pico 的 I2C0 默认分配在 GPIO 4 (SDA) 和 GPIO 5 (SCL) 上也有板子用 GPIO 0 和 GPIO 1需要查一下你的 MicroPython 固件配置。实际上 Pico 的 I2C 外设可以映射到多组引脚后面代码里我会演示怎么在 MicroPython 里指定引脚。接线不复杂但有个容易忽略的细节RTC 模块的 VCC 建议接 3.3V因为 Pico 的 I2C 引脚是 3.3V 电平模块如果接 5V逻辑电平不匹配可能导致通信不稳定。DS3231 模块引脚Pico 引脚说明VCC36 (3V3_OUT)供 3.3V 电源GND38 (GND)共地SDAGPIO 4I2C 数据线SCLGPIO 5I2C 时钟线我建议在 SDA 和 SCL 上各加一个 4.7kΩ 上拉电阻到 3.3VMicroPython 的 machine.I2C 默认会启用内部上拉但有些模块板载上拉电阻阻值偏大加上外部上拉更稳。实测发现部分 DS3231 模块板载上拉是 10kΩ在长线连接时信号上升沿变缓偶尔会出现读回 0xFF 的情况补了外部 4.7kΩ 后问题就没再出现过。2.3 电池座和备用电源的处理DS3231 模块上通常有一颗 CR2032 电池座这颗电池负责在主电源断电时维持 RTC 走时。注意区分模块上是有电池还是没电池市面上有些精简版模块把电池座省了一旦断电时间就丢。买的时候看清商品描述优先选带电池和充电电路的版本。模块上如果有充电电路一般是一颗小电阻和二极管意味着它支持对可充电锂电池充电。但如果你装的是 CR2032 不可充电电池充电电路反而会成为安全隐患。我一般装机前先把充电电路对应的零欧电阻拆掉改成纯供电模式这样用不可充电电池也没问题。具体要看模块原理图不同厂家设计有差异动手前先查资料。另外装电池的操作时机也很关键。务必在给 Pico 上电之前装好电池或者确保 DS3231 已经通过 I2C 初始化过否则模块内部寄存器处于未定义状态读出来的时间可能是 1900 年或者乱码。我第一次用的时候就是先上电后插电池结果时间死活不对折腾了半天才想起来是初始化顺序的问题。3. MicroPython 下驱动 DS3231 的核心原理3.1 从 I2C 设备地址到寄存器映射DS3231 在 I2C 总线上的设备地址是 0x687 位地址这个地址是硬件固定的无法更改。因此一条 I2C 总线上最多只能挂一颗 DS3231如果你同时用了其他 I2C 传感器只要地址不冲突就行。Pico 上同时挂 DS3231 和 BME280 的时候BME280 的地址一般是 0x76部分模块是 0x77两个互不干扰。DS3231 内部寄存器按地址顺序排列核心的几个寄存器如下寄存器地址功能数据格式0x00秒BCD 编码0x01分BCD 编码0x02时BCD 编码含 12/24 小时标志0x03星期1-7对应周日到周六0x04日BCD 编码0x05月BCD 编码含世纪位0x06年BCD 编码00-990x0E控制寄存器启用振荡器等寄存器里存的时间并非普通十进制数字而是 BCD 码。也就是说秒数 45 实际上存的是 0x45二进制 01000101而不是十进制的 45二进制 00101101。写驱动的时候必须做 BCD 与十进制的互转直接读写整数会导致时间完全乱掉。这个坑非常典型我见过不少人说“为什么我设置的时间读出来是乱码”十有八九就是 BCD 没处理好。3.2 读写 DS3231 的完整 MicroPython 代码下面这段代码是我在实际项目中使用的 DS3231 驱动兼容 MicroPython 的 machine.I2C 接口。首先要初始化 I2C 总线from machine import Pin, I2C import utime # Pico I2C0 使用 GPIO4 (SDA) 和 GPIO5 (SCL) i2c I2C(0, sclPin(5), sdaPin(4), freq400000) # 扫描 I2C 总线上的设备地址确认模块通信正常 devices i2c.scan() if 0x68 in devices: print(DS3231 已检测到地址: 0x68) else: print(未检测到 DS3231请检查接线)这里有个细节freq400000表示使用 400kHz 高速模式。DS3231 支持最高 400kHz 的 I2C 时钟频率但模块上的上拉电阻和线缆长度会影响实际通信质量。如果出现偶发性通信失败可以降到 100000标准模式试试稳定性会更好。接下来是 DS3231 的核心读写逻辑class DS3231: def __init__(self, i2c, addr0x68): self.i2c i2c self.addr addr # 检测芯片是否可用 if self.addr not in self.i2c.scan(): raise OSError(DS3231 not found) staticmethod def _bcd_to_dec(bcd): # BCD 转十进制: 0x45 - 45 return (bcd // 16 * 10) (bcd % 16) staticmethod def _dec_to_bcd(dec): # 十进制转 BCD: 45 - 0x45 return (dec // 10 * 16) (dec % 10) def read_time(self): # 从 0x00 开始连续读取 7 个寄存器 data self.i2c.readfrom_mem(self.addr, 0x00, 7) sec self._bcd_to_dec(data[0] 0x7F) minute self._bcd_to_dec(data[1] 0x7F) hour self._bcd_to_dec(data[2] 0x3F) weekday self._bcd_to_dec(data[3] 0x07) day self._bcd_to_dec(data[4] 0x3F) month self._bcd_to_dec(data[5] 0x1F) year self._bcd_to_dec(data[6]) 2000 return (year, month, day, hour, minute, sec, weekday) def write_time(self, year, month, day, hour, minute, sec, weekday): # 写入时间到 DS3231year 参数应传入四位年份例如 2024 if year 2000: raise ValueError(Year must be 2000) year_bcd self._dec_to_bcd(year % 100) month_bcd self._dec_to_bcd(month) day_bcd self._dec_to_bcd(day) hour_bcd self._dec_to_bcd(hour) minute_bcd self._dec_to_bcd(minute) sec_bcd self._dec_to_bcd(sec) weekday_bcd self._dec_to_bcd(weekday) # 依次写入 0x00 开始的时间寄存器 self.i2c.writeto_mem(self.addr, 0x00, bytes([ sec_bcd, minute_bcd, hour_bcd, weekday_bcd, day_bcd, month_bcd, year_bcd ]))关于寄存器读取后需要做位掩码处理这里解释一下DS3231 的秒寄存器最高位是 CH时钟停止标志读取时要忽略这个位所以data[0] 0x7F把最高位清零。小时寄存器的 bit6 是 12/24 小时制标志bit5 是 AM/PM 标志所以data[2] 0x3F保留低 6 位即可。如果忽略这些掩码可能读到奇怪的数据比如小时变成 21 点0x21 和 0xA1 在 BCD 层面完全不同。3.3 上电初始化与掉电保持的完整策略有了驱动代码后初始化时序也很关键。我在项目里通常是开机后先读取 RTC 的时间然后判断年份是否合理比如在 2020-2100 范围内。如果年份不在合理范围说明电池没电或者模块第一次使用这时就提示用户手动设置时间或者等待 NTP 同步后写入。rtc DS3231(i2c) year, month, day, hour, minute, sec, weekday rtc.read_time() print(RTC 当前时间:, year, month, day, hour, minute, sec) # 判断时间是否合理 if year 2020 or year 2100: print(RTC 时间无效需要重新设置) # 可以在这里写入编译时固定时间 rtc.write_time(2024, 1, 1, 0, 0, 0, 1) print(已写入初始时间) else: print(RTC 时间有效继续使用)判断逻辑不能只看年份还要检查时分秒是否在合理范围。比如读回小时大于 23 或者分钟大于 59都是异常状态。我在实际项目中会把所有字段都做一次范围校验任何一个字段异常都认为 RTC 数据无效需要重新设置。4. NTP 时间同步原理与 MicroPython 实现4.1 NTP 报文格式和核心时间戳换算逻辑NTPNetwork Time Protocol是网络时间同步的标准协议工作原理简单来说就是客户端向服务器发送一个请求报文服务器收到后回送一个包含当前时间戳的响应报文。通过测量网络往返延迟客户端可以估算出与服务器的时间偏差并校准本地时钟。这个协议里最重要的数据结构是 64 位时间戳其中前 32 位表示从 1900 年 1 月 1 日 0 时 0 分 0 秒开始经过的秒数。为什么从 1900 年开始而不是 1970 年因为 NTP 协议诞生于 1985 年1968 年的 RFC 868 时间协议就已经确定了 1900 作为纪元起点后续版本沿用了这个约定。MicroPython 里的utime.time()返回的是从 1970 年 1 月 1 日 0 时Unix 纪元开始的秒数两者之间相差 2208988800 秒。这个数字要记住换算公式很简单# NTP 时间戳 - Unix 时间戳 unix_timestamp ntp_timestamp - 2208988800 # Unix 时间戳 - NTP 时间戳 ntp_timestamp unix_timestamp 2208988800NTP 时间戳溢出问题同样值得关注。32 位无符号整数最多表示到 2106 年届时候 32 位 NTP 时间戳会回绕到 0。不过考虑到当时时间格式会升级到 128 位这个问题还远着呢。但在代码里最好还是预留一下判断逻辑比如读到的 NTP 时间戳小于某个阈值比如 2000000000大约是 2033 年就认为可能出现了异常。4.2 基于 socket 的 NTP 客户端实现MicroPython 相比 CPython 的优势在于它自带socket模块和network模块可以编写 UDP 客户端。NTP 使用的是 UDP 端口 123客户端发送一个 48 字节的数据包服务器返回同样大小的数据包。完整实现如下import socket import struct import utime def get_ntp_time(serverntp.aliyun.com, timeout5): # 构造 NTP 请求数据包48 字节 # 第一个字节: 0x1B 表示 NTP 版本 3、客户端模式 ntp_packet b\x1b b\x00 * 47 try: # 创建 UDP socket sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(timeout) # 发送请求到 NTP 服务器的 123 端口 sock.sendto(ntp_packet, (server, 123)) # 接收响应 data, _ sock.recvfrom(48) sock.close() except OSError as e: print(NTP 请求失败:, e) return None if len(data) 48: print(NTP 响应数据长度异常) return None # 解析响应数据第 40-43 字节是发送时间戳的整数部分 # NTP 报文结构: 前 4 字节是标志位和版本4-11 是根延迟和根离散 # 12-15 是参考标识16-23 是参考时间戳24-31 是发起时间戳 # 32-39 是接收时间戳40-47 是发送时间戳 ntp_timestamp struct.unpack(!I, data[40:44])[0] # 转换为 Unix 时间戳 unix_timestamp ntp_timestamp - 2208988800 return unix_timestamp这里要特别说明一下struct.unpack(!I, data[40:44])的含义!表示网络字节序大端序I表示无符号 32 位整数。NTP 报文使用大端序传输如果解析时不指定字节序在余下小端序的平台上就会得到完全错误的时间值。我第一版代码就是忘了加!符号导致读出的时间戳跑到了 2100 年一脸懵。请求数据包的第一个字节是 0x1B二进制是 00011011。这个字节的最低 3 位是模式字段001 表示客户端请求中间 3 位是版本号011 表示版本 3最高 2 位是标志位。用版本 3 的好处是兼容性最好几乎所有 NTP 服务器都支持。4.3 时区处理和夏令时的选择获取到的 Unix 时间戳是 UTC 时间而中国大陆地区使用的是北京时间UTC8。转换很简单加上 8 小时的秒数就行def utc_to_local(unix_timestamp): # 东八区 UTC8 return unix_timestamp 8 * 3600但是这里有个隐藏问题如果你在海外部署设备或者设备可能会移动位置硬编码 8 就不合适了。更通用的做法是配置一个时区偏移量允许在运行时通过配置文件修改。MicroPython 没有完整的time.tzset()功能所以基本上都是手动加一个常量偏移。夏令时的问题不建议在嵌入式设备上处理。欧美地区的夏令时规则每年都可能调整政治决策也会影响时间规则把这种不确定性写死在固件里是一种负担。坦白讲在 Pico 这类资源受限的 MCU 上做完整时区数据库是不现实的。更好的做法是设备上报时间戳时统一用 UTC让上位机去处理本地化显示。我在自己的项目里就是这么设计的省了很多麻烦。5. 实操记录把 RTC 和 NTP 结合起来的完整方案5.1 带 Wi-Fi 的自动时间校准流程我的最终目标是实现这样的逻辑上电后先读取 RTC 时间如果时间有效就先用着随后尝试连接 Wi-Fi 并请求 NTP 时间如果 NTP 成功就把校准后的时间写回 RTC如果 Wi-Fi 连接失败就继续用 RTC 的时间。下面是完整代码基于 Pico W 或 Pico 加外置 Wi-Fi 模块import network import socket import struct import utime from machine import Pin, I2C, RTC # ---------- DS3231 驱动代码省略见前文 ---------- def connect_wifi(ssid, password, timeout10): wlan network.WLAN(network.STA_IF) wlan.active(True) if not wlan.isconnected(): print(正在连接 Wi-Fi:, ssid) wlan.connect(ssid, password) start_time utime.time() while not wlan.isconnected(): if utime.time() - start_time timeout: print(Wi-Fi 连接超时) return False utime.sleep_ms(500) print(Wi-Fi 已连接, IP:, wlan.ifconfig()[0]) return True def sync_time_from_ntp(rtc): ntp_time get_ntp_time() if ntp_time is None: return False # 转换为北京时间 local_time ntp_time 8 * 3600 # 拆分为年月日时分秒 year, month, day, hour, minute, sec, weekday, _ utime.localtime(local_time) # 写入 Pico 内部 RTC rtc_pico.datetime((year, month, day, weekday, hour, minute, sec, 0)) # 写入外部 DS3231 rtc_ds.write_time(year, month, day, hour, minute, sec, weekday) print(时间同步成功:, year, month, day, hour, minute, sec) return True # 测试流程 i2c I2C(0, sclPin(5), sdaPin(4), freq400000) rtc_ds DS3231(i2c) rtc_pico RTC() # 步骤1: 读取当前 RTC 时间 year, month, day, hour, minute, sec, weekday rtc_ds.read_time() print(DS3231 时间:, year, month, day, hour, minute, sec) # 步骤2: 尝试连接 Wi-Fi if connect_wifi(YOUR_SSID, YOUR_PASSWORD): # 步骤3: 通过 NTP 同步时间并回写 sync_time_from_ntp(rtc_ds) else: # 步骤4: 无法联网继续使用 DS3231 时间 print(无网络连接使用 RTC 时间)这段流程中需要注意代码的第 8 行调用了get_ntp_time()虽然前文定义了但这里要确认get_ntp_time中是否包含了异常处理。如果没有异常处理Wi-Fi 连接不稳定时会直接抛出异常导致程序崩溃。我在实际项目中会给get_ntp_time加上完善的 try-except-finally 结构确保任何异常下 socket 都被正确关闭。5.2 时间校准策略启动同步加周期校准只在上电时做一次 NTP 同步长时间运行后时间还是会有漂移。DS3231 的漂移量大约每月几秒对于大多数应用来说足够了但如果你的项目对时间精度要求很高可以设计成周期性校时。我的做法是上电后立即做一次时间校准之后每隔 24 小时校时一次校时失败时记录失败次数连续失败 3 次后降低频率改为每 6 小时尝试一次如果 Wi-Fi 一直连不上就继续用 RTC 的本地时间启动 24 小时定时器的方式可以用utime.ticks_ms()循环判断不占用额外线程last_sync utime.ticks_ms() while True: # 正常业务代码 do_something() # 每 24 小时同步一次时间 if utime.ticks_diff(utime.ticks_ms(), last_sync) 24 * 3600 * 1000: if connect_wifi(SSID, PASSWORD): sync_time_from_ntp(rtc_ds) last_sync utime.ticks_ms() utime.sleep_ms(100)用utime.ticks_diff而不是直接比较差值是因为 MicroPython 的ticks_ms()在达到最大整数后会发生回绕直接做减法会得到错误的负值。ticks_diff函数专门处理了回绕问题这也是一个容易踩的坑。5.3 时间戳在数据采集项目中的实战用法光有准确的时间还不够关键是怎么用。我的环境监测项目里每个传感器采集的数据都带上时间戳写入日志格式如下def log_sensor_data(temperature, humidity): year, month, day, hour, minute, sec, weekday, _ utime.localtime() timestamp {:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}.format( year, month, day, hour, minute, sec) log_line f{timestamp}, temp{temperature:.2f}C, hum{humidity:.2f}%\n # 写入 SD 卡或通过串口发送 print(log_line.strip()) # 这里可以添加写文件逻辑在 CSV 日志里用十六进制系统时间戳比如 Linux 常用格式其实也可以但人类可读性太差了。我建议都存成YYYY-MM-DD HH:MM:SS格式兼容性和可读性都更好。如果后续要用上位机做分析把字符串解析成 pandas 的时间戳也很方便。6. 常见问题汇总与排查思路6.1 I2C 通信失败的典型原因问题现象可能原因排查方法i2c.scan() 返回空列表接线错误或模块供电异常检查 VCC/GND/SDA/SCL 是否对应正确用万用表测供电电压读回全是 0xFF上拉电阻缺失或信号干扰在 SDA/SCL 上外加 4.7kΩ 上拉电阻时间偶尔读错I2C 频率过高或线缆过长把 freq 从 400000 降到 100000首次读取年份异常电池没电或未初始化更换电池重新写入正确时间SDA 一直为低电平I2C 总线死锁复位 Pico 电源检查 SDA 是否被外设拉低这里说一个我踩过的坑DS3231 模块的 SDA 和 SCL 引脚在部分模块上顺序是反的。有些模块标的 SDA 实际上是 SCL接反后 i2c.scan() 只能偶尔探测到设备通信极不稳定。拿到模块后最好先用万用表或者逻辑分析仪确认引脚定义别急着焊线。6.2 NTP 同步失败的处理策略NTP 请求超时是最常见的问题原因可能是路由器防火墙封了 UDP 123 端口少见但存在也可能是 NTP 服务器不可达管理员配置了错误的服务器地址。我调试时的思路是先用上位机pingNTP 服务器确认为什么不通换用不同厂商的 NTP 服务器做交叉测试检查 Pico W 的 Wi-Fi 连接状态和 DNS 解析def get_ntp_time_multi(servers): for server in servers: t get_ntp_time(server, timeout3) if t is not None: return t print(服务器 {} 不可用尝试下一个.format(server)) return None # 多个国内常用的 NTP 服务器 ntp_servers [ ntp.aliyun.com, ntp1.aliyun.com, ntp2.aliyun.com, pool.ntp.org ] ntp_time get_ntp_time_multi(ntp_servers)推荐一个想法不在生产环境用pool.ntp.org作为唯一服务器源。这个域名背后是全球的 NTP 服务器集群响应质量良莠不齐国内访问延迟也比较大。阿里云和腾讯云的 NTP 服务器在国内的响应速度通常更好实测 RTT 小于 10ms。6.3 其他容易被忽视的坑Pico 内部 RTC 和外部 DS3231 的时间源要分清。MicroPython 的utime.localtime()默认读取的是 Pico 内部 RTC而不是外部 DS3231。如果你用了 DS3231要么每次调用rtc_ds.read_time()要么把 DS3231 的时间同步到 Pico 内部 RTC之后再统一用utime.localtime()。否则可能出现“我明明把时间写进 DS3231 了程序里输出的还是错误时间”的困惑。3124 年问题或者 2100 年问题。DS3231 的年份寄存器只有两位 BCD 码能表示 00-99加上世纪位就是 2000-2099 年范围。MicroPython 的utime.localtime()返回的年份是四位最高支持到 9999 年但 DS3231 写回时只能支持 2000-2099。如果程序向 DS3231 传入 2100 年写回时会变成 00 年逻辑乱套。需要在写入前做年份校验。多个进程或线程同时访问 I2C 总线。如果你的项目用了_thread线程千万要注意不能让两个线程同时对同一颗 I2C 设备发送数据。I2C 协议本身不支持多主并发访问两个线程交叉操作会导致总线状态混乱。正确的做法是用互斥锁_thread.allocate_lock()保护 I2C 访问。7. 进一步扩展结合 RTC 做定时唤醒和低功耗应用如果你选择的 DS3231 模块引出了 SQW 引脚那就能玩出更多花样。SQW 引脚可以配置为输出 1Hz 到 8.192kHz 的方波也可以配置为闹钟中断输出。配合 Pico 的休眠模式可以实现极低功耗的定时唤醒系统。思路是这样的from machine import Pin, I2C, deepsleep # 配置 DS3231 的 SQW 引脚为中断模式 def set_alarm(i2c, hour, minute): # 配置控制寄存器: 开启闹钟中断禁用方波输出 # 控制寄存器地址 0x0E值 0x05 表示开启闹钟中断 i2c.writeto_mem(0x68, 0x0E, bytes([0x05])) # 配置闹钟 1 寄存器: 地址 0x07 (秒), 0x08 (分), 0x09 (时), 0x0A (星期/日) # 秒寄存器最高位置 1 表示启用闹钟即每秒检查一次 i2c.writeto_mem(0x68, 0x07, bytes([0x80])) # 秒任意 i2c.writeto_mem(0x68, 0x08, bytes([minute])) # 分钟匹配 i2c.writeto_mem(0x68, 0x09, bytes([hour])) # 小时匹配 i2c.writeto_mem(0x68, 0x0A, bytes([0x80])) # 日期任意 # SQW 引脚接 Pico 的 GPIO 16下降沿触发 sqw_pin Pin(16, Pin.IN, Pin.PULL_UP) # 设置闹钟后进入深度睡眠 set_alarm(i2c, 6, 0) # 每天早上六点触发 deepsleep() # 等待唤醒这个模式下Pico 在非唤醒时间几乎不耗电整个系统Pico DS3231 传感器可以在两节 AA 电池下运行几个月。相比一直用定时器唤醒的方案用 RTC 硬件闹钟的好处是唤醒精度不受 MCU 内部时钟偏差影响而且可以精确指定唤醒时刻。不过要注意进入deepsleep()后 Pico 的 MicroPython 程序会重新启动类似复位唤醒后不会恢复到原来的断点。所以程序逻辑要设计成状态机模式开机后先检查是冷启动还是定时唤醒再决定执行什么任务。这个坑让我排查了整整一个下午最后翻 Pico 的数据手册才发现。8. 小结与实际项目体会做嵌入式项目这几年时间同步看似是一个基础功能但在实际部署中往往决定项目成败。RTC 和 NTP 的组合方案几乎是微控制器项目的最佳实践RTC 保证离线场景下时间的连续性NTP 提供在线场景下的自动校准两者配合让系统在复杂网络环境中都能保持时间可靠性。几个我自己的经验感受DS3231 买带电池座的版本哪怕贵一点后面省去换电池布线的时间成本远大于那几块钱差价。NTP 服务器建议至少配置两个备用地址生产环境别指望单点服务永远可靠。用 Pico 的 UTC 时间做存储和传输显示时再转本地时间这样的数据可移植性最好跨省跨国的项目都不怕时区问题。如果你在做数据采集网关或者定时控制设备这套方案直接抄作业就好。代码核心就那几个函数DS3231 的读写、NTP 的请求解析、时间换算。写清楚这三个部分整个时间系统就牢固了。