MicroPython软件看门狗实现:带恢复机制的嵌入式设备防死机方案
2026/9/8 7:47:48 网站建设 项目流程

做了这么多年嵌入式,最怕听到的一句话就是“设备死机了,得派人去现场断电重启”。尤其设备已经在客户那边跑了好几个月,你连个串口都摸不到的时候,那个滋味是真不好受。所以后来我给自己定了个规矩:凡是部署到现场的设备,必须带看门狗,不管是硬件狗还是软件狗,至少得有一个。这篇文章就想跟你聊聊,在没有额外硬件成本的情况下,怎么用 MicroPython 在嵌入式设备上实现一个带恢复机制的软件看门狗,让设备在异常卡死之后能自己回过神来,而不是等着人上门服务。

内容不算复杂,但我会把背后的设计思路、为什么这么写、踩过的坑都讲清楚。无论是用 ESP32、RP2040 还是其他支持 MicroPython 的开发板,这套思路都能直接搬过去用。如果你是刚从 Arduino 或者纯 C 开发转到 MicroPython 的开发方式,那这篇文章正好适合你。

1. 为什么软件看门狗在 MicroPython 里格外重要

1.1 硬件看门狗与软件看门狗的定位差异

在看门狗这个事儿上,很多人的第一反应是“板子上有硬件看门狗芯片或者芯片内部有硬件看门狗定时器,直接用不就完了?”确实,硬件看门狗是最底层的兜底方案,它的本质是一个独立的计数器,由硬件电路或者芯片内部的外设实现,一旦启动后软件必须在规定时间内“喂狗”,否则计数器溢出后直接触发芯片复位。这个过程不依赖 CPU 是否正常执行用户代码,哪怕主程序跑飞了,只要喂狗操作没执行,复位信号照样能产生。

但硬件看门狗有一个非常明显的弱点:它只知道“喂了”还是“没喂”,不知道程序到底卡在了哪里、卡了多久、当前状态是什么。而且很多 MicroPython 开发板,比如一些基于 ESP32 的模组,并没有把硬件看门狗的功能完整暴露出来,或者驱动接口不统一,写起来并不顺手。更重要的是,硬件看门狗一旦复位,程序是冷启动,之前的运行状态、错误信息、现场数据全都丢了,你根本不知道设备为什么死机。

软件看门狗的思路就不一样了。它是在应用层模拟看门狗的机制,用一个后台任务或者定时器中断去监控主循环的运行状态,发现异常后不一定要立刻复位,而是可以先尝试记录现场、保存上下文、做一些清理工作,甚至执行一段“软恢复”流程。这种机制在 MicroPython 这种偏应用层开发的场景里非常实用,因为 MicroPython 本身跑得比 C 慢,内存管理也更容易出幺蛾子,硬件看门狗能兜底,但软件看门狗才是真正能帮你定位问题、优雅恢复的工具。

1.2 MicroPython 环境下程序卡死的常见原因

我接触过的 MicroPython 设备死机案例,大部分都不是 CPU 真的跑飞了,而是下面这几种情况:

  • 阻塞式网络请求。用 socket 或者 urequests 请求一个外部服务,对方迟迟不响应,又没有设置超时时间,程序就挂在 recv 上了。
  • 死循环里出现了异常但没被捕获。MicroPython 的报错信息有时候会被大量输出刷掉,代码里 try-except 覆盖不全,一个异常直接让主循环退出,设备看起来就像“死机”了。
  • 内存碎片化严重。MicroPython 跑久了,频繁创建和销毁对象,内存碎片越来越多,某次 malloc 失败直接抛出 MemoryError,如果没有全局兜底,程序就停了。
  • I2C/SPI 总线挂死。外设没应答,读取函数没有超时机制,总线锁住,后续所有通信全部卡住。

这些问题的共同点是:程序并没有真正“死透”,只是卡在了某个不可等待的环节里。硬件看门狗能救急救命,但救完你什么都不知道。软件看门狗的价值就在于,它能在卡死前的一瞬间把现场信息留下来,然后根据你预设的恢复策略动作。

2. 带恢复机制的软件看门狗整体设计思路

2.1 核心机制:喂狗计数与超时判定

软件看门狗的实现原理非常直白。你可以把它想象成一个“打卡机”:主循环每隔一段时间就去打一次卡,也就是调用一下喂狗函数,喂狗函数会把一个计数器加一。与此同时,后台有一个独立的监控者,它每隔固定时间检查一次计数器的值有没有变化。如果检查时发现计数值跟上一次一样,说明主循环已经很久没有来打过卡了,那就判定程序卡死。

这里有个关键点,监控者不能跑在主循环里,否则主循环卡死了监控者一起卡死,看门狗就没意义了。所以监控者要用 MicroPython 的定时器回调,或者放到一个独立的线程里。我个人更推荐用定时器回调,因为 MicroPython 的线程受限于 GIL,而且线程调度在某些端口上并不稳定,定时器回调在中断上下文执行,只要回调函数里不做太重的操作,不会对主流程造成明显影响。

伪代码逻辑大概是这样的:

feed_counter = 0 last_counter = 0 def feed(): global feed_counter feed_counter += 1 def check(): global last_counter if feed_counter == last_counter: # 超时未喂狗,触发恢复流程 do_recovery() else: last_counter = feed_counter

2.2 分级恢复策略:从软恢复到硬复位

真正的“恢复机制”不是一句“单片机重启”就完事儿的。我习惯把恢复动作分成几个等级,按照严重程度依次执行:

第一级是状态复位。比如某个全局状态机卡在了错误状态,看门狗发现异常后先把状态机重置,把任务队列清空,然后让系统从主循环重新开始,这种恢复方式对连续生产任务影响最小,也快。

第二级是软重启。调用machine.reset()或者sys.exit()后重新执行主程序,相当于让 MicroPython 虚拟机重新跑一遍。这个动作会清掉 Python 层面的所有变量,但不会重新初始化硬件外设。如果设备是电池供电的,软重启比硬复位温和得多。

第三级是硬件复位。直接操作machine.reset()配合一些特殊参数,或者翻转复位引脚。这个动作连 MicroPython 解释器都重新加载了,所有外设寄存器全部恢复默认值,相当于一次冷启动。

在实际项目里,我通常这样配置恢复策略:第一次超时就做软恢复,并且把错误次数记录下来。如果短时间内连续触发了多次软恢复,说明问题比较顽固,再升级到硬复位。如果硬复位之后还是反复重启,那就不要再折腾了,直接把设备挂在一个“安全模式”里,只保留最基本的功能,比如只亮故障灯、只发心跳报文,等人工介入。这种分级设计的核心思想是:不要盲目重启,尽量让设备自己降级运行,实在不行再彻底重启,而且重启不能白重启,得把线索留下来。

2.3 状态保存与现场信息记录

说到线索,这是带恢复机制的软件看门狗和普通看门狗最大的区别。普通看门狗复完了就完了,要查问题还得靠运气复现。软件看门狗可以在判定超时的时候,把当前的关键状态存下来,存到哪里有讲究。

  • 存到文件系统。适合带 Flash 文件系统的开发板,缺点是频繁擦写 Flash 有寿命问题,不能每次死机都写。
  • 存到 RTC 内存。很多芯片的 RTC 域有一块独立的小内存,掉电不丢失,适合存错误计数器、上次错误码这种小数据,关键是启动速度快,不伤 Flash。
  • 通过日志串口输出。适合开发阶段,直接打印错误现场,方便跟调试器配合排查。

我在实际项目里的做法是:看门狗触发恢复之前,先把错误码、当前任务名、喂狗间隔、内存剩余量这些信息写进一个内存缓冲区,然后根据恢复等级决定是否写入 Flash。如果是软恢复,就先不写 Flash,等确认恢复失败再写。这样做能最大程度延长 Flash 寿命,同时保证关键错误信息不会全部丢失。

3. MicroPython 软件看门狗核心代码实现

3.1 基础版:一个可用的最小实现

先看一个最基础但能直接用的实现,目标平台是 ESP32,其他平台改一下定时器和重置函数的调用方式就行:

import machine import time import sys class SoftwareWatchdog: """软件看门狗:主循环喂狗,定时器监控超时""" def __init__(self, timeout_ms=5000, check_interval_ms=500): self.timeout_ms = timeout_ms self.check_interval_ms = check_interval_ms self.feed_counter = 0 self.last_counter = 0 self.last_feed_time = time.ticks_ms() self.recovery_count = 0 self.timer = machine.Timer(1) def start(self): """启动看门狗定时器""" self.timer.init( period=self.check_interval_ms, mode=machine.Timer.PERIODIC, callback=self._watchdog_check ) def feed(self): """主循环调用,喂狗""" self.feed_counter += 1 self.last_feed_time = time.ticks_ms() def _watchdog_check(self, timer): """定时器回调,检查喂狗情况""" if self.feed_counter == self.last_counter: # 计数器没变,说明主循环没有执行到 feed() elapsed = time.ticks_diff(time.ticks_ms(), self.last_feed_time) self._handle_timeout(elapsed) else: self.last_counter = self.feed_counter def _handle_timeout(self, elapsed_ms): """超时处理:默认记录日志并重启""" print("WATCHDOG TIMEOUT, elapsed:", elapsed_ms, "ms") self.recovery_count += 1 # 这里可以扩展保存现场、分级恢复逻辑 machine.reset() def stop(self): """停止看门狗,用于正常休眠前""" self.timer.deinit() # 使用示例 watchdog = SoftwareWatchdog(timeout_ms=5000) def main_loop(): watchdog.start() while True: try: # 实际业务逻辑 time.sleep(0.1) except Exception as e: print("main error:", e) finally: watchdog.feed() main_loop()

这个版本已经能实现基本功能:主循环每 100ms 循环一次,喂狗一次。定时器每 500ms 检查一次喂狗计数是否变化。如果主循环因为任何原因卡住超过 5 秒,定时器回调里就会检测到计数值没变,触发machine.reset()

注意我把超时时间设成 5000ms,检查间隔设成 500ms,意思是主循环最多允许 5 秒不去喂狗,超过这个阈值就会触发恢复。检查间隔决定了检测的精度,如果你希望更快反应,可以把这两个时间同时调小。但别把检查间隔设得太小,比如小于 10ms,因为定时器回调本身会占用 CPU 时间,太频繁会影响主流程。

3.2 进阶版:带状态保存与分级恢复

基础版只能做到“卡死就重启”,这还不够好。我实际部署的版本是这样的:

import machine import time import json import sys class AdvancedWatchdog: """带恢复机制的软件看门狗 采用分级恢复策略: 第一级:软恢复,重置业务状态 第二级:软重启,重新运行主程序 第三级:硬复位,彻底重新初始化 如果连续多次硬复位仍失败,进入安全模式 恢复现场信息会保存到 RTC 内存,开机时可读取上次死机原因。 """ # 错误码定义 ERR_NONE = 0 ERR_MAIN_LOOP_STUCK = 1 ERR_MEMORY_ERROR = 2 ERR_BUS_LOCK = 3 def __init__(self, timeout_ms=8000, check_interval_ms=500, max_reboot=3): self.timeout_ms = timeout_ms self.check_interval_ms = check_interval_ms self.max_reboot = max_reboot self.feed_counter = 0 self.last_counter = 0 self.last_feed_time = time.ticks_ms() self.recovery_count = 0 self.last_error = self.ERR_NONE self.safe_mode = False self.timer = machine.Timer(2) self.rtc = machine.RTC() self._load_rtc_state() def _load_rtc_state(self): """开机时从 RTC 内存读取上次的死机记录""" try: data = self.rtc.memory() if data: state = json.loads(data.decode()) self.recovery_count = state.get("recovery_count", 0) self.last_error = state.get("last_error", self.ERR_NONE) if state.get("safe_mode", False): self.safe_mode = True except Exception: pass def _save_rtc_state(self): """把当前状态写入 RTC 内存""" state = { "recovery_count": self.recovery_count, "last_error": self.last_error, "safe_mode": self.safe_mode, "timestamp": time.time() } try: self.rtc.memory(json.dumps(state).encode()) except Exception: pass def start(self): if not self.safe_mode: self.timer.init( period=self.check_interval_ms, mode=machine.Timer.PERIODIC, callback=self._check ) def feed(self): self.feed_counter += 1 self.last_feed_time = time.ticks_ms() def report_error(self, err_code): """业务代码可以主动上报错误类型,帮助看门狗定位问题""" self.last_error = err_code self._save_rtc_state() def _check(self, timer): if self.feed_counter == self.last_counter: elapsed = time.ticks_diff(time.ticks_ms(), self.last_feed_time) self.last_error = self.ERR_MAIN_LOOP_STUCK self.recovery_count += 1 self._save_rtc_state() self._do_recovery() else: self.last_counter = self.feed_counter def _do_recovery(self): """分级恢复核心逻辑""" print("[watchdog] recovery_count =", self.recovery_count) if self.recovery_count <= 1: # 第一次超时:软恢复,清掉业务状态 self._soft_recovery() elif self.recovery_count <= self.max_reboot: # 多次超时:软重启 self._soft_reset() else: # 重启太多,进入安全模式 self._enter_safe_mode() def _soft_recovery(self): """软恢复:只重置业务状态,不清除 Python 运行时 具体操作取决于项目里的状态机设计""" # 假设有一个全局的业务状态对象 try: import app_state app_state.reset() except Exception: pass print("[watchdog] soft recovery done") def _soft_reset(self): """软重启:重新启动 MicroPython 程序""" print("[watchdog] soft reset") self.timer.deinit() machine.reset() def _enter_safe_mode(self): """进入安全模式:停止主业务,只保留诊断信息""" print("[watchdog] enter safe mode") self.safe_mode = True self.timer.deinit() self._save_rtc_state() # 这里跳转到安全模式代码 self._run_safe_main() def _run_safe_main(self): """安全模式主循环:只闪灯,发诊断数据,不执行业务""" led = machine.Pin(2, machine.Pin.OUT) while True: led.value(not led.value()) time.sleep(1) def get_state(self): return { "recovery_count": self.recovery_count, "last_error": self.last_error, "safe_mode": self.safe_mode }

这个进阶版的核心是分级恢复。第一次超时,先尝试把业务状态重置,期望程序能从主循环开头重新正常执行。如果短时间内连续超时次数增加,说明软恢复没解决问题,就升级到软重启。如果软重启都扛不住,就硬复位。复位次数超过阈值,说明设备在当前环境或当前固件下大概率没法稳定运行了,继续重启只会耗电和刷日志,不如进入安全模式等人工处理。

RTC 内存在这里扮演了“黑匣子”的角色。每次异常都会把 recovery_count 和错误码写进 RTC 内存,下次开机后 watchdog 能读到这些数据,这样一来你就知道设备在无人值守期间发生了多少次恢复动作,而不是两眼一抹黑。

3.3 业务代码里的喂狗策略

有了 watchdog 类,接下来要解决的是“在哪里喂狗”的问题。很多人想都不想就在主循环顶部加一行watchdog.feed(),这其实是有效的,但不是最优的。因为如果主循环里有一个阻塞时间超过 timeout_ms 的调用,比如一个 30 秒的网络请求,看门狗就会误判为死机,发生不必要的重启。

更好的做法是把喂狗点放在关键业务步骤之间,而不是笼统地放在循环开头。比如一个采集数据上传服务器的循环,可以这样组织:

watchdog.start() while True: # 读取传感器数据,假设最多 1 秒 sensor_data = read_sensor() watchdog.feed() # 发送数据到服务器,设置超时时间为 3 秒 send_data(sensor_data, timeout=3000) watchdog.feed() # 本地保存数据,可能涉及 Flash 擦写,耗时不确定 save_to_flash(sensor_data) watchdog.feed()

这样设计有几个好处:第一,喂狗点分布在关键操作之间,任何一个操作卡死,下一个喂狗点都不会到达,看门狗就能在超时时间后触发恢复。第二,对于已知耗时的长任务,可以预估其最大执行时间,并在任务期间不喂狗,任务结束后再喂,这样不会误报。第三,日志里可以看到最后喂狗的位置,配合前面保存的现场信息,排查问题会方便很多。

还要注意一种特殊情况:主循环正常执行,但其中某个分支调用了一个永远不会返回的while True,如果这个分支里没有喂狗,看门狗同样能捕捉到。所以写业务代码的时候,凡是可能出现长时间阻塞的地方,都要意识到“这期间看门狗还在运行”。

3.4 定时器回调中的注意事项

这部分是很多人第一次写 MicroPython 软件看门狗最容易踩的坑。定时器回调里绝对不能做这几件事:

  • 不能做内存分配。MicroPython 在中断上下文中分配内存会导致异常甚至死机。回调里创建列表、字典、字符串拼接都是高危操作。如果需要记录信息,尽量用模块级别的预分配变量。
  • 不能执行耗时操作。回调函数里写文件、定期打印、访问网络,这些操作都可能让中断处理时间过长,影响系统其他中断的响应。
  • 不能调用machine.reset()之外的重型恢复函数。有些操作在中断回调里会因为上下文不可用而失败,比如某些外设库的重新初始化。
  • 不能直接调用 Python 的gc.collect()。垃圾回收在中断上下文里执行非常危险,可能触发不可预期的行为。

回到看门狗本身,回调里最安全的做法就是比较两个整数变量,然后设置一个标志位,真正的恢复动作放在主循环里执行。但这里有个矛盾:如果主循环已经卡死了,主循环里的恢复动作也执行不了。所以我的折中方案是:回调里只做超时判定和计数累加,然后用machine.reset()这种“原子”操作来复位。如果用的是软恢复而不是复位,那软恢复逻辑最好是由定时器回调触发一个高优先级的中断标志,同时确保主循环在醒过来之后能立即处理这个标志。实际操作中,MicroPython 的定时器回调确实能打断主循环的阻塞状态(尤其是time.sleep),所以把恢复动作写到回调里,只要不涉及分配内存,大多数时候是可行的,但为了安全起见,我通常还是尽量让回调简单,把复杂逻辑放到一个专门的处理函数里,并且测试到位。

4. 实操验证与常见问题排查

4.1 如何验证看门狗是否真的有效

代码写完了,必须实测验证,不能想当然。我常用的验证手段有两个:一个是模拟死机,一个是观察恢复日志。

模拟死机的最好办法是写一个测试脚本,让主循环在某次执行时主动进入一个空转死循环,并且不喂狗:

import machine import time from watchdog import AdvancedWatchdog watchdog = AdvancedWatchdog(timeout_ms=3000, check_interval_ms=500) watchdog.start() loop_count = 0 while True: loop_count += 1 print("loop", loop_count) if loop_count == 10: print("simulate stuck...") while True: # 模拟死循环,不喂狗 pass time.sleep(0.2) watchdog.feed()

把 timeout_ms 设成 3000,就是 3 秒内不喂狗就触发恢复。预期现象是串口打印到 loop 10 之后,程序卡住,然后 3 秒后看门狗触发恢复,设备复位,串口重新开始输出 boot 日志。如果能看到这个过程,说明超时检测有效。

观察恢复日志的做法是:在 watchdog 恢复逻辑里打印 recovery_count 和错误码,同时用 RTC 内存保存这些字段。复位后从 RTC 读回来,如果能看到上一次恢复次数是 1、错误码对应 MAIN_LOOP_STUCK,就说明状态保存链路也是通的。实际测试时我会在设备上电后主动打印一次watchdog.get_state(),能把上次的运行状态直接输出到串口,方便确认。

4.2 常见问题速查表

下面这几类问题是我在给不同项目集成软件看门狗时遇到过,并且在社区里也频繁被问到的:

问题现象可能原因解决方案
看门狗没有触发恢复喂狗点太多,主循环虽然卡死但某个阻塞函数内部意外喂了狗梳理喂狗点,确保卡死路径上不会执行 feed()
设备频繁重启,但业务看起来没卡死timeout_ms 设置得太短,主循环某个分支在正常情况下就超过了这个时间把正常情况下的最长耗时统计出来,timeout_ms 设置为它 2~3 倍
定时器回调偶尔报 MemoryError回调里做了字符串拼接或列表操作,触发了内存分配把打印和记录操作移到主循环,回调里只做整数比较
恢复后业务状态混乱,出现重复初始化软恢复没有正确清理全局状态,导致新旧状态混在一起软恢复应调用统一的 reset 入口,而不是靠变量覆盖
RTC 内存读出来是乱码json 编解码异常,或 RTC 内存大小不够检查 RTC 内存容量,减少保存的字段数量,或者改成二进制格式
看门狗在深度睡眠期间误触发进入睡眠前没有停掉看门狗定时器在 sleep 前调用 stop(),醒来后重新 start()
硬复位后外部外设没有重新初始化有些外设(比如 OLED、传感器)在 Python 代码重跑时才初始化,但硬件电平状态没复位硬复位前主动调用外设的 deinit/reset,或者采用硬件复位电路

表中的前三个问题最常见,基本都是设计阶段没想清楚“喂狗周期”和“业务周期”的关系。记住一个原则:喂狗频率必须远高于看门狗超时时间,看门狗超时时间必须远高于正常业务最长阻塞时间。否则就会误报或者漏报。

4.3 调试技巧:让看门狗自己“说”问题

看门狗开发调试的时候,我建议把恢复动作先改成“只记录不重启”,也就是触发超时后先打印断言信息,然后继续运行,而不是立刻复位。这样你能在开发阶段看到完整的问题现场:

def _check(self, timer): if self.feed_counter == self.last_counter: elapsed = time.ticks_diff(time.ticks_ms(), self.last_feed_time) # 开发阶段:只打印,不重启 print("DEBUG: watchdog timeout, elapsed =", elapsed) self.last_counter = self.feed_counter # 重置计数,避免反复触发 else: self.last_counter = self.feed_counter

这种“debug 模式”下,程序卡死时你会在串口看到一条超时打印,但设备不会重启,你就有机会用调试器或者继续观察现场变量。等确认恢复逻辑没问题了,再打开真正的恢复动作。

另外,MicroPython 的time.ticks_ms()返回的是毫秒级 tick,它是会回绕的,所以判断超时要用ticks_diff而不是直接比较大小。我自己一开始也是直接用差值小于某个值来判断,后来设备连续跑了 49 天之后突然出现一次误判,查了半天才发现是 tick 回绕的问题。MicroPython 的ticks_diff就是为这个设计的,老老实实用它,别自己造轮子。

5. 进阶扩展:多任务监控与状态持久化

5.1 多任务场景下的看门狗扩展

如果你的系统里不止一个主循环,比如有一个采集线程、一个网络线程,还有一个 UI 刷新循环,那么一个看门狗实例可能不够用,因为每个循环都可能卡住,而主循环喂狗只能证明主循环还活着,不能证明其他线程没事。

扩展方法并不复杂:给每个监控对象分配一个独立的喂狗计数器,看门狗定时器检查所有计数器。这里给出一个简单的多任务版实现思路:

class TaskMonitor: """单个任务的喂狗记录""" def __init__(self, name, timeout_ms): self.name = name self.timeout_ms = timeout_ms self.feed_counter = 0 self.last_counter = 0 self.last_feed_time = time.ticks_ms() class MultiWatchdog: """支持多个任务的软件看门狗""" def __init__(self, check_interval_ms=200): self.tasks = {} self.timer = machine.Timer(3) self.check_interval_ms = check_interval_ms def register_task(self, name, timeout_ms): self.tasks[name] = TaskMonitor(name, timeout_ms) def feed(self, name): if name in self.tasks: self.tasks[name].feed_counter += 1 self.tasks[name].last_feed_time = time.ticks_ms() def start(self): self.timer.init( period=self.check_interval_ms, mode=machine.Timer.PERIODIC, callback=self._check_all ) def _check_all(self, timer): now = time.ticks_ms() for task in self.tasks.values(): if task.feed_counter == task.last_counter: elapsed = time.ticks_diff(now, task.last_feed_time) if elapsed > task.timeout_ms: print("Task", task.name, "timeout, elapsed", elapsed) # 针对具体任务的恢复策略 machine.reset() return else: task.last_counter = task.feed_counter

本质上就是把单任务看门狗的计数器换成字典,每个键对应一个任务。定时器检查的时候逐个比对计数器和超时时间。这样做的好处是,你能精确知道是哪个任务先卡住了,而不是只知道“某个地方死了”。

实际使用中,每个任务的feed(name)要放在对应任务循环的最尾部,确保任务真的跑到了一轮末尾才喂狗。如果任务中间有 2 秒的网络请求,那这个任务的 timeout_ms 要至少给到 6 秒才稳妥。

5.2 与硬件看门狗叠加使用的“双保险”方案

前面讲了软件看门狗很强,但它有一个天然的弱点:软件看门狗跑在 MicroPython 环境里,如果 MicroPython 解释器自身挂了,比如堆栈溢出、固件 bug、底层驱动崩了,那软件看门狗也跟着失效。所以对可靠性要求比较高的设备,我会选择“硬件看门狗 + 软件看门狗”双保险。

硬件看门狗放在最底层,超时时间可以设得很长,比如 60 秒。它平时基本不会触发,只在软件看门狗彻底失效时兜底。软件看门狗负责精细监控,超时时间设成 5 秒,正常情况都是软件看门狗先动作,完成状态保存和分级恢复。如果软件看门狗自己也卡了,60 秒后硬件看门狗会强行复位整个芯片。

这样设计的核心思想是:能优雅恢复就优雅恢复,不能优雅恢复就强制恢复。双保险配置在 MicroPython 里实现也不难,硬件看门狗在 ESP32 上就是:

import machine # 启动硬件看门狗,60 秒超时 hw_wdt = machine.WDT(timeout=60000) hw_wdt.feed()

然后在主循环里喂硬件看门狗的位置要和喂软件看门狗的错开,最好放在不同的执行点。比如软件看门狗在主循环末尾喂,硬件看门狗在主循环开头喂,这样即使软件喂狗逻辑出现死循环,硬件喂狗也不可能连续执行。

5.3 状态持久化:让每次恢复都有据可查

如果设备出问题后,你要靠串口日志才能知道原因,那在现场运维中是比较被动的,因为现场可能没有人接串口。所以把看门狗状态持久化到本地存储,是带恢复机制看门狗的一个重要能力。

除了前面讲的 RTC 内存,还可以把恢复日志写到文件系统里。但要注意写入频率,每次恢复都写一次 Flash,几百次之后 Flash 就可能挂了。我的做法是:只在恢复等级升级时写日志,比如第一次超时只记内存标志,第二次软重启才写文件,第三次硬复位再写一次。这样即使一个晚上重启了 20 次,实际写入 Flash 的次数也不会太多。

日志格式可以很简单:

{ "recovery_count": 5, "last_error": 1, "safe_mode": false, "uptime_before_reset": 3600, "free_mem_before_reset": 28432 }

uptime_before_resetfree_mem_before_reset这两个字段特别有用,可以帮你判断设备是运行了很久之后才卡死,还是一上电就跑几步就挂。如果是后者,多半是初始化阶段的问题,排查范围一下子缩小很多。

6. 从软件看门狗看 MicroPython 项目的可靠性设计思路

6.1 不要把所有可靠性的宝押在一种机制上

做过三四年嵌入式的人都会明白一个道理:没有任何一种机制是绝对可靠的,处于任何时间点都可能出错,你的程序可能出错,看门狗本身也可能出错。所以设计可靠性方案的时候,永远要想清楚“如果这个兜底机制也失效了,会怎么样”。

软件看门狗解决了“业务卡死”的大部分场景,但它不能解决“MicroPython 解释器崩溃”的问题。硬件看门狗解决了“系统级死机”的问题,但它太粗暴,保存不了一点上下文。所以最可靠的设计是分层:

  • 业务层:代码里做好超时处理、异常捕获、内存控制,尽量让程序不那么容易卡死。
  • 监控层:软件看门狗监控各个任务循环的执行情况,捕获异常状态,执行分级恢复。
  • 硬件层:硬件看门狗兜底,保证系统即使遇到最极端的故障也能恢复。

当然,这已经超出“软件看门狗”本身的范畴了,但我觉得既然做嵌入式,就应该从全局视角来看待可靠性设计。软件看门狗不是银弹,它只是监控层里最重要的一个零件。

6.2 软件看门狗设计的“三要三不要”

如果要给新手总结几条经验,我会说:

要做的:

  • 要让喂狗点覆盖所有关键业务路径,尤其是阻塞操作之后。
  • 要让超时时间有足够冗余,避免正常业务被误杀。
  • 要让恢复动作分层,软恢复优先,硬复位兜底。

不要做的:

  • 不要在定时器回调里做内存分配、文件写入、网络请求。
  • 不要用一个喂狗点监控所有任务,多任务就得多路监控。
  • 不要在正式发布时还留着一堆调试打印,这些打印本身可能成为卡死的诱因。

6.3 一个实际项目中的使用心得

最后分享一个实际的案例。我之前做一个环境监测节点,用的就是 ESP32 + MicroPython,板子放在户外配电箱里,要求至少连续运行 3 个月不用人工维护。刚开始测试的时候,设备经常在运行一两天后就没响应了,排查发现是 4G 模块的信号偶尔很差,导致网络请求阻塞了几十秒,主循环卡在那里一直等。

后来我加了软件看门狗,第一次超时就触发软重启,同时还把网络请求改成非阻塞模式。结果设备稳定运行了一个多月,没有再出现需要人工干预的情况。期间看门狗的 recovery_count 有过几次从 0 变到 1,说明它确实起到了作用,但每次都是软恢复就解决了,没有触发硬复位。

很多人觉得 MicroPython 不适合做严肃的嵌入式产品,因为性能和实时性都跟 C 有差距。但只要你把可靠性机制设计到位,MicroPython 完全能支撑起对稳定性有要求的物联网终端设备。软件看门狗就是这套可靠性机制里性价比最高的一块拼图,写起来不复杂,却能帮你省下大量现场运维的麻烦。当然,光有看门狗还不够,代码本身的健壮性才是根子上的事,看门狗是给你兜底的队友,不是给你擦屁股的保姆。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询