
1. 先聊聊硬件看门狗兜不住的那类故障恰恰是软件看门狗的主场1.1 硬件看门狗的盲区主循环还活着任务却已经死了先说个我自己踩过的坑。去年给本地数据采集节点做 MicroPython 固件时遇到一次特别诡异的故障设备上线运行几天后状态机被一次总线干扰带进了错误分支I2C 传感器再也没有返回数据但主循环却一直健康地跑着硬件看门狗也照常喂着整个节点就这么活着死掉了——数据不再更新没有任何复位动作也没有任何告警。那次之后我才开始认真琢磨软件看门狗这件事。很多做嵌入式的朋友对硬件看门狗是有依赖感的。STM32 的 IWDG、ESP32 的 WDT原理都是独立计数器超时溢出直接触发系统复位。它对付程序跑飞死循环这种整机级别的故障确实管用但它有一个天然盲区喂狗动作往往放在主循环里只要主循环还能跑到喂狗这一行硬件看门狗就认为系统一切正常。而实际项目里真正让工程师头疼的故障恰恰是主循环活着、业务任务死了这类局部假死。举几个真实场景。I2C 总线被从设备拉死读操作永远等不到 ACK网络 socket 在断开瞬间没有正确抛错阻塞在 send/recv 里迟迟不返回一个 while 循环在等待某个永远不会置位的标志位但这个 while 并没有卡住主循环而是卡在一个独立协程里。这些情况下你的硬件看门狗全程无感因为你还在定时喂它。等发现数据积压、设备失联的时候往往已经过了几个小时。1.2 MicroPython 场景下为什么更需要软件看门狗MicroPython 给嵌入式开发带来了很多便利但也把硬件看门狗的盲区放得更大了。原因有三。第一MicroPython 的很多底层调用是阻塞式的。比如socket.accept()在无连接时会一直等下去I2C.readfrom()在总线上没有响应时也可能长时间挂起。这些阻塞发生在解释器内部主循环根本得不到执行机会硬件看门狗一旦复位整个系统RAM 里的现场信息全丢你根本不知道故障发生在哪个调用。第二MicroPython 本身是一个中间层。解释器跑着跑着遇到内存碎片、垃圾回收抖动、异常处理分支这些都会造成某一段逻辑不走了但系统还在跑的效果。C 语言裸机程序里容易定位的寄存器状态在 MicroPython 里排查起来要费劲得多更需要软件层面有一个能记录恢复过程的机制。第三部分开发板或者精简固件根本没有暴露硬件看门狗 API或者硬件看门狗的最小超时粒度是秒级而一些任务的卡死检测需要毫秒级响应。软件看门狗不受这些限制完全跑在应用层超时粒度自己定检测对象自己选恢复动作自己控制。打个比方硬件看门狗是整栋楼的消防喷淋——真烧起来了它会启动但只会在整层楼都过火后才动。软件看门狗是每层楼的巡检员——哪个房间异常他能第一时间赶到现场决定是扑灭小火苗还是拉总闸。两者不是替代关系是分工关系。1.3 硬件看门狗与软件看门狗的分工边界对比维度硬件看门狗软件看门狗触发机制独立硬件计数器溢出应用层心跳超时判定检测对象整个系统是否崩溃单个任务/模块是否无响应超时粒度受硬件限制最小粒度偏大完全由代码控制可到毫秒级恢复手段只能系统复位可分级处置记录、软重置、局部重建、复位现场保留复位后 RAM 丢失复位前可写日志、保存快照核心弱点主循环活着就无法触发巡检逻辑本身可能失效需自检我的建议很简单硬件看门狗保留作为最后防线软件看门狗负责日常任务级监控在硬件看门狗触发之前尽量把故障消化在应用层。这篇文章后面的内容全部围绕软件看门狗展开。2. 心跳、哨兵、处置软件看门狗的三段式设计骨架2.1 心跳机制给每个任务一张死亡倒计时档案卡软件看门狗的第一段是心跳Heartbeat。所有需要被监控的任务在关键步骤完成后主动调用一个feed()函数更新自己的最后存活时间戳。这个概念来自 Linux 内核本质上就是一句话我活着我更新我的档案。这里有一个特别容易犯的错误我见过很多人把喂狗放在任务开头而不是任务结尾。比如读传感器先feed()再去读 I2C这个顺序是错的。因为任务在开头喂了狗然后卡死在 I2C 读取上看门狗会认为任务还活着。正确的做法是把喂狗放在关键步骤真正完成之后也就是任务里每一步可能有风险的调用都返回后再更新心跳。这样只要心跳没更新就说明任务在某个环节卡住了。在设计阶段每个被监控的任务应该有一张档案卡包含四个基础字段任务 ID全局唯一标识比如sensor、report。超时阈值这个任务允许的最大无心跳时间比如 1000ms。最后存活时间戳每次喂狗更新保存ticks_ms()的返回值。恢复回调任务异常时的处置函数可以按任务定制。阈值的选择有讲究我给一个经验公式启动阶段或调试阶段阈值先放宽到任务正常执行周期的 3 到 5 倍跑一段时间确认稳定后再逐步收紧。如果任务正常 100ms 执行一次阈值先设 500ms稳定后压到 300ms。阈值太紧稍微慢一点就误报阈值太松故障半天才发现失去了监控意义。2.2 哨兵巡检用最小的开销做最频繁的检查第二段是哨兵也就是负责巡检的独立实体。它周期性地检查所有任务的档案卡看谁的最后存活时间戳距今超过了阈值。这部分的核心要求是开销要足够小巡检要足够频繁。在 MicroPython 里实现哨兵有三种常见方式我按推荐程度排序硬件定时器回调machine.Timer最适合做巡检周期稳定而且不依赖主循环是否卡死。注意回调里不能做复杂操作只能做轻量判定这个我们后面代码部分细说。主循环空闲时巡检代码最简单但主循环一旦被某个阻塞调用卡住哨兵本身也跟着失效监控价值大打折扣。asyncio后台任务适合整体框架就是异步协程的项目但同样存在一个协程阻塞导致调度器无法切换的风险。哨兵巡检本身也要防止失效。一个很实用的兜底措施是哨兵自检如果连续若干个巡检周期内哨兵函数自身都没有被执行到可以用另一个硬件定时器或者简单的计数器来佐证就认为巡检链路出了问题这时候直接执行系统复位。因为巡检链路都坏了软件层面已经不可信了交给硬件看门狗兜底才是唯一出路。2.3 处置策略判定异常之后要走一套标准流程第三段是处置。当哨兵判定某个任务超时后不是简单地调用一下machine.reset()就完事而是要走一套分级处置流程。这套流程也是这篇文章标题里恢复机制的核心价值。我在章节 4 会专门展开这里先给一个总的原则能软恢复的不硬复位能局部重建的不整体重启但所有恢复动作都要有止损边界。处置流程大概是检查故障次数 → 如果首次故障执行轻量恢复动作重建任务状态、重置外设→ 如果短时间内反复故障升级为重型动作整体软复位→ 如果故障次数超过上限进入安全模式或直接硬复位。处置动作设计要求 可观测。恢复前一定要留下日志至少包含哪个任务挂了、超时多久、当前故障次数、系统内存余量。没有日志的恢复机制等于闭着眼睛修车修好修坏全靠猜。3. MicroPython 代码落地一个可直接抄的带恢复机制看门狗类3.1 看门狗管理类核心代码与设计说明下面的代码我直接给到能在 MicroPython 里跑起来的程度基于machine.Timer做周期巡检。它支持注册多个任务、自定义超时阈值、自定义恢复回调以及一个process_pending()方法供主循环处理重恢复动作。# sw_wdt.py import time import machine class SoftWatchdog: def __init__(self, check_interval_ms100, timer_id0): self._check_interval check_interval_ms self._tasks {} # task_id - task dict self._pending [] # 需要主循环处理的恢复队列 self._global_fault 0 # 全局故障计数 self._max_fault 5 # 超过后执行硬复位 self._safety_mode False # 安全模式开关 self._timer machine.Timer(timer_id) self._last_check time.ticks_ms() def register(self, task_id, timeout_ms, on_faultNone): self._tasks[task_id] { timeout: timeout_ms, deadline: time.ticks_add(time.ticks_ms(), timeout_ms), handler: on_fault, fault_count: 0, } def unregister(self, task_id): if task_id in self._tasks: del self._tasks[task_id] def feed(self, task_id): 任务在关键步骤完成后调用, 更新心跳 t self._tasks.get(task_id) if t: t[deadline] time.ticks_add(time.ticks_ms(), t[timeout]) t[fault_count] 0 def _scan(self, timer): 哨兵回调, 由硬件定时器周期调用, 只做轻量判定 if self._safety_mode: return now time.ticks_ms() for tid, t in list(self._tasks.items()): if time.ticks_diff(t[deadline], now) 0: t[fault_count] 1 self._global_fault 1 # 重新安排下一轮检查, 避免连续触发 t[deadline] time.ticks_add(now, t[timeout]) self._pending.append((tid, t[fault_count])) if self._global_fault self._max_fault: self._safety_mode True self._pending.append((__system__, self._global_fault)) def process_pending(self): 主循环调用, 执行实际恢复动作 while self._pending: item self._pending.pop(0) if item[0] __system__: self._system_recover(item[1]) continue tid, count item t self._tasks.get(tid) if t and t[handler]: try: t[handler](tid, count) except Exception as e: print(handler error:, tid, e) elif t: self._default_recover(tid, count) def start(self): self._timer.init( periodself._check_interval, modemachine.Timer.PERIODIC, callbackself._scan, ) def stop(self): self._timer.deinit() def _default_recover(self, tid, count): print(WDT: recover, tid, count, count) gc.collect() if count self._max_fault: self.reset_system(fault limit reached) def _system_recover(self, count): print(WDT: system-level recover, global count, count) self.reset_system(global fault limit) def reset_system(self, reason): print(WDT: reset system, reason:, reason) try: import os os.sync() except Exception: pass time.sleep_ms(20) machine.reset()几个设计要点解释一下。_scan回调里只做两件事判断超时、把恢复动作加入_pending队列。所有可能阻塞的操作I2C 重置、socket 重连、日志写文件都移到process_pending()里由主循环执行。这样定时器回调永远保持轻量不会因为恢复动作太慢而拖垮整个系统也不会因为恢复动作里又出现阻塞导致巡检链路中断。fault_count是每个任务自己的故障次数_global_fault是全局累计次数。只要喂狗成功任务自己的计数会被清零但全局计数不会轻易清零这是防止任务反复假死、恢复、又假死的关键设计。3.2 时间戳回绕为什么必须用 ticks_diff 而不是直接减法MicroPython 的time.ticks_ms()返回的是系统上电以来的毫秒 tick。这里有一个所有嵌入式开发者早晚会踩的坑这个 tick 值不是无限增长的它受限于 MicroPython 的内部表示实际可表示的 ms 数大约只有 2 的 30 次方换算过来大约是 12.4 天就会回绕归零。如果你写的是if now - deadline 0或者if deadline - now 0在回绕点附近会出现严重误判明明任务才喂了狗系统却判定它超时了然后触发恢复。更麻烦的是这种误判是间歇性的极难复现。正确做法是用 MicroPython 提供的time.ticks_diff(a, b)。它的语义是计算 a 和 b 之间的短时间差内部已经考虑了回绕。代码里我同时用了ticks_add来更新deadlineticks_diff来比较时间差这套组合是 MicroPython 官方推荐的时间戳操作方式。对于运行时间会超过 12 天的设备这个细节尤其重要。我曾经在一个环境监测节点上因为直接用-做时间比较每 12.4 天准时误触发一次看门狗查了整整一周才发现是回绕问题。老老实实用ticks_diff一劳永逸。3.3 多任务接入的完整示例下面演示三个任务如何接入这个看门狗一个 I2C 传感器读取任务一个 TCP socket 上报任务一个 LED 心跳展示任务。# main.py import gc import time import machine import network from sw_wdt import SoftWatchdog wdt SoftWatchdog(check_interval_ms100, timer_id0) # 模拟 I2C 传感器对象 class DummySensor: def __init__(self): self.ok True def read(self): if not self.ok: raise OSError(i2c bus stuck) return 42 sensor DummySensor() def on_sensor_fault(tid, count): 传感器任务恢复: 软重置 I2C, 重新初始化设备 print(recover sensor, count, count) gc.collect() try: sensor.ok True except Exception as e: print(sensor reset err:, e) # 上报 socket 恢复 def on_report_fault(tid, count): print(recover report, count, count) gc.collect() wdt.register(sensor, timeout_ms1000, on_faulton_sensor_fault) wdt.register(report, timeout_ms2000, on_faulton_report_fault) wdt.feed(sensor) wdt.feed(report) wdt.start() # 模拟阻塞恢复队列必须在主循环处理 while True: # 任务1: 读取传感器 try: val sensor.read() wdt.feed(sensor) # 读取成功后再喂狗 except OSError as e: print(sensor read fail:, e) sensor.ok False # 模拟总线异常 # 任务2: 上报数据(模拟) if val is not None: try: # sock.send(str(val).encode()) wdt.feed(report) except OSError as e: print(report fail:, e) # 集中处理看门狗产生的恢复动作 wdt.process_pending() time.sleep_ms(100)这个示例的喂狗时机可以仔细品一下。wdt.feed(sensor)放在sensor.read()返回之后而不是调用之前。一旦read()内部卡死或者抛错喂狗就不会执行看门狗才能正确判定任务异常。任务 2 同理只有send()成功返回后才喂狗。如果你在主循环开头就一口气把两个任务都喂了这个看门狗就形同虚设。3.4 定时器回调与主循环之间的并发隐患用machine.Timer做巡检有一个绕不开的问题定时器回调运行在中断上下文可能打断主循环的任意一行代码。如果你的程序在运行过程中动态注册、注销任务也就是修改self._tasks这个字典恰好又赶上定时器回调正在遍历它MicroPython 可能抛出RuntimeError: dictionary changed size during iteration甚至导致更隐蔽的内存问题。我在 3.1 的代码里做了两个缓解措施。第一_scan里用list(self._tasks.items())生成一个快照再遍历减少遍历过程中被修改的概率。第二实际项目中我建议任务清单在启动阶段一次性注册完成运行期间不要增删任务。如果一个任务确实需要动态启停用任务内部的状态机控制它是否喂狗而不是通过注册、注销看门狗条目来实现。还有一个容易被忽视的问题process_pending()运行在主循环里它执行恢复动作时定时器回调可能同时在往_pending里追加新条目。我们的pop(0)每次只取一个循环处理直到队列为空这个设计天然应对了并发追加不会因为队列为空而产生竞态问题。4. 恢复机制的分级设计软恢复、局部重建、硬复位与防重启风暴4.1 恢复等级划分从只记录到硬复位恢复机制是本篇的重点我直接给出五个等级的划分。这个思想其实很接近 CAN 总线里busoff的快慢恢复机制——先快速尝试轻量恢复不行再切换到慢速、重型恢复。等级恢复动作适用场景代价L0只记录日志不干预诊断阶段、非关键数据采集任务极低L1软重置任务状态状态机类任务、协程卡死低L2局部重置外设I2C/SPI/UART 总线异常外设无响应中L3整体软复位多个任务联动失效内存碎片严重较高L4硬复位machine.reset()软恢复失败、系统整体不可信最高L0 级恢复最适合刚开始引入软件看门狗的时候用。把看门狗先跑起来异常只打日志不动作观察几天确认哪些任务是真容易挂哪些是误报。直接上恢复动作很容易掩盖真实故障点。L1 级的典型实现是在恢复回调里把这个任务的状态对象整体重建一遍——重新初始化内部变量、清空缓冲区、重置协程入口。比如一个 LED 闪烁任务卡死了L1 就是把它内部的状态机指针拨回初始位置。L2 级针对外设级故障。I2C 总线被从设备拉死是嵌入式项目里的常客恢复方式是把 I2C 对象deinit()掉gc.collect()回收内存再重新init()。注意重置外设前最好先给外设供电引脚做一次复位很多传感器芯片有一个上电复位时序单纯重新 init 总线并不能让它恢复工作。L3 级是 MicroPython 层面的应用重启。可以用machine.reset()实现也可以在不动固件的前提下重新导入模块、重新初始化所有对象。我见过有人用exec(open(main.py).read())重新执行整个入口脚本这个思路没问题但要注意全局变量没有清理干净最好先gc.collect()再加一个软复位标记。L4 级不用多说machine.reset()。但什么时候该用它是这门手艺的精髓。我的原则是当恢复动作本身需要依赖一个可能已经损坏的系统时就不要犹豫直接硬复位。4.2 防重启风暴故障计数与安全模式有道是看门狗恢复系统系统挂了看门狗恢复完又喂狗喂完狗又挂挂了又恢复——这就是重启风暴。如果设备处于一个持续触发的故障环境里比如 I2C 总线被外部的强干扰一直拉低那么检测到故障 → 重置 I2C → 读取失败 → 又检测到故障整个过程会无限循环。设备看似活着但实际上永远在恢复中不仅耗电还会反复打断正常的业务。我在代码里的方案是双保险。第一重保险每个任务维护自己的fault_count喂狗成功会清零。这个计数达到阈值代码里是 5 次走默认恢复逻辑时会升级成硬复位。连续 5 次软恢复都失败说明软恢复对这个环境根本不适用不再浪费时间做轻量尝试。第二重保险全局故障计数_global_fault达到阈值后系统进入_safety_mode。安全模式里哨兵直接不再做任何巡检和恢复因为已经认定当前环境不适合自动干预。此时应该做的是保持基础功能、发出告警、等待人工或者远程命令处理。安全模式的设计要根据具体业务调整比如关闭非关键外设、只保留一个指示灯以特定频率闪烁或者保持网络上报通道发送故障码。更精细的做法是加时间窗口。在 10 分钟内故障次数超过 3 次接下来 30 分钟不再尝试恢复只做最低频的保活。这个类似 CAN 总线 busoff 后的慢恢复降级机制可以避免故障反复时设备的频繁动作。4.3 故障现场留存复位之前的那几毫秒怎么用软件看门狗相比硬件看门狗的一大优势是能在复位前留下现场。写恢复代码的时候不要只顾着恢复还要把恢复前这一刻的系统状态记录下来。至少包含出现故障的任务 ID 和当前故障次数全局故障计数gc.mem_free()返回的堆内存余量最近的传感器/网络状态值任务卡死前的最后喂狗时间戳这些信息可以打印到串口也可以写入文件系统。但写文件系统有两个注意点。一个是要考虑文件系统同步。直接print到串口是可靠的因为串口输出是同步的。写文件时如果固件支持os.sync()在machine.reset()前调用一下确保日志真正落盘否则复位可能丢失最后一段数据。二是 flash 擦写寿命。MicroPython 跑的 MCU 内置 flash 标称擦写次数大约在 10 万次量级。如果设备一天故障 100 次每次写一条日志1000 天就能把 flash 写报废。所以日志存储要用环形缓冲区思路固定大小文件写满后从头部覆盖同时可以只记录最近 N 条日志。另外一个实用技巧把关键现场信息放到 RTC 备份寄存器或 NVS 里。复位后启动时bootloader 或初始化代码可以读取这些信息决定是否进入安全模式或回滚固件。MicroPython 里不同平台 API 差别较大ESP32 可以用machine.nvs或 RTC memorySTM32 可以用备份寄存器具体看你用的平台文档。4.4 与 OTA 场景的联动故障恢复了固件版本怎么办软件看门狗的恢复机制在 OTA 固件升级场景下有一个很关键的应用升级失败自动回滚。正常的 OTA 流程是下载新固件 → 写入备用分区 → 设置启动标志 → 重启进入新固件。如果新固件本身有 bug比如启动后某个任务必然崩溃那会发生什么软件看门狗检测到故障试着重启重启后跳到新固件又崩溃又重启……如果没有升级版本的概念系统会在这里死循环。把恢复机制和固件版本绑定后逻辑就清晰了。升级固件之前在 NVS 里写入正在升级旧版本号为 X的标记。启动后跑新固件软件看门狗正常运行。如果新固件启动后短时间内多次触发恢复动作说明新固件可能不可用——这时候不再走普通的恢复策略而是直接把启动分区切换回旧版本并保留新固件崩溃的日志供远程分析。这个思路在 ESP32 的 MicroPython 上实现并不复杂关键是要控制判断固件是否可用的窗口期。比如设定 60 秒内故障次数超过 3 次就判定新固件不可用回滚到旧分区。窗口期太短可能误判太长了故障持续太久。结合业务容忍度来调。5. 实测记录与避坑清单我在 ESP32 上跑了三天的全过程5.1 测试方案如何故意制造一次假死这套代码我在一块 ESP32-S3 DevKitC 上跑了三天MicroPython 版本是 1.23.0。为了验证看门狗真的有效我主动设计了几个故障场景而不是等着自然故障。测试环境开发板ESP32-S3 DevKitC4MB Flash 8MB PSRAMMicroPythonv1.23.0网络环境本地局域网TCP client 上报数据到 PC 上的 server模拟传感器一个可手动控制故障的模拟 I2C 设备第一个场景是最基础的把传感器读取任务的喂狗动作直接人为删掉模拟任务内部死循环。代码里我加了一个全局开关FAKE_FAULT置为 True 后read()函数进入一个死循环不再返回喂狗自然不会被调用。看门狗应该在一秒后检测到超时执行恢复回调。第二个场景验证恢复回调本身会不会影响系统。我在 L2 恢复回调里故意重置 I2C 外设 10 次每次间隔 50ms模拟一个恢复过程比较重的情况看主循环的任务是否还能按时执行。第三个场景是压力测试把看门狗超时阈值调到 300ms巡检周期调到 50ms然后在传感器任务里制造一个每 5 秒一次的瞬间卡死。这个场景是为了测试看门狗在高频巡检下是否误报、是否漏报。5.2 实测数据与恢复表现测试场景监控任务超时阈值恢复动作预期恢复耗时实测结果任务内部死循环sensor1000msL1 软重置任务状态约 1.1s通过I2C 总线异常sensor1000msL2 重新初始化 I2C约 1.3s通过TCP 上报卡死report2000msL1 重建 socket 连接约 2.2s通过持续故障 5 次sensor1000ms5 次后硬复位约 5.5s通过高频巡检 50mssensor300msL1 软重置约 0.35s通过无误报最后一行是一项重要的检验标准。高频巡检下没有出现误报说明哨兵巡检逻辑是可靠的。实际的恢复耗时大致等于超时阈值 巡检周期 恢复动作执行时间这个预期要提前算清楚避免恢复动作本身太慢导致两个任务叠加故障。测试中也发现一个现象看门狗判定超时后process_pending()是在主循环里执行的如果主循环间隔是 100ms那么恢复动作最坏情况下要等一个主循环周期才能执行。这个延迟在恢复耗时里体现出来了。如果你的业务对恢复时间有硬性要求要么把巡检和恢复都放到定时器回调里但回调里不能做重操作要么把主循环的周期压缩二选一。5.3 避坑清单三天里踩过的 6 个坑第一时间比较必须用ticks_diff而不是直接减法。这个前面已经提过重复强调一遍12.4 天的回绕周期不是开玩笑的。我测试到第二天的第 11 个小时差一点就碰上这个坑了后来在回绕模拟测试里复现了误判果断全部改成ticks_diff。第二定时器回调里不能做阻塞操作。我早期版本的代码把日志写入放在了_scan()里结果日志稍微大一点整个系统的调度都被拖住了其他任务全部超时看门狗反而制造了崩溃。教训就是回调里只判超时、入队列所有实际动作留给主循环。第三喂狗位置放在任务完成之后不是任务开始之前。这个看似简单实际项目中很容易被写成函数第一行先feed()的形式。写代码之前先想清楚如果这个函数中途卡死喂狗该不该已经执行如果答案是不该就把喂狗放到尾部。第四软恢复之前先gc.collect()。MicroPython 内存碎片是真实存在的。一个长期运行的设备内存里堆满了碎片和不再使用的对象直接重新初始化外设很容易失败因为分配不出连续内存。先回收再把状态对象重建成功率会高很多。我自己实测gc.collect()后外设恢复成功率从 60% 提到了接近 100%。第五硬复位前日志落盘要用os.sync()。如果你把故障日志写到了内置文件系统里machine.reset()并不会保证文件内容已经写入 flash。我测试中丢过一次日志最后定位到原因就是没有同步文件系统。固件支持os.sync()就调用不支持的话宁可把关键日志通过串口发出去也别写在本地文件里。第六任务清单尽量启动时固定不在运行时动态增删。这个并发问题在 3.4 提过实际测试中确实遇到了偶尔的RuntimeError虽然概率不高但在长时间运行的设备上这种偶发错误会成为致命 bug。我的做法是把所有任务在wdt.start()之前统一register()运行期间只喂狗、不增删。还有一个体会想特别说一下。看门狗的恢复机制不能替代日志系统。我见过有人把软件看门狗当成万能兜底代码里没有任何日志故障全靠猜。结果就是看门狗确实一直在恢复系统但永远不知道系统为什么反复崩溃。一定要在恢复回调里记录足够的信息这样每次故障后都能回溯逐步缩小故障范围最后根除问题。根据我这几天的实测这套带恢复机制的软件看门狗对于 MicroPython 项目最常见的三类故障协程卡死、外设总线异常、网络 socket 异常都有稳定的恢复能力。把阈值、恢复策略、日志记录这几块调试好设备在恶劣环境下的鲁棒性会有质的提升。如果你也在写 MicroPython 应用不妨先从监控一个最容易出问题的任务开始跑通整个流程再加第二个、第三个任务。软件看门狗这套东西跑起来不难难的是设计出能真正兜住故障的恢复策略这个只能靠实践慢慢磨合。