从理论到实战:手把手解析 Pico 多线程看门狗的核心逻辑与实现步骤
多线程软件看门狗的核心思想是通过一个独立的看门狗线程来监控其他工作线程的状态,每个工作线程定期“喂狗”以表明自己仍在正常运行:看门狗线程负责监控全局计数变量,固定时间递增变量,工作线程完成一定操作后将全局计数变量清零,即“喂狗”操作;如果看门狗线程发现全局计数变量超过预定义的上限值(比如 2),则认为主程序线程已经卡死,采取重启设备措施。
这里,需要注意的有两点:
- 首先在
RP2040上运行的 MicroPython 只能同时运行两个线程,因为 RP2040 有两个核心,每个核心只能运行一个线程:如果尝试运行超过两个线程,会导致资源冲突或其他错误,这种限制需要在设计多线程应用时特别注意,尤其是在使用 RP2040 进行复杂任务时。如果需要管理多个任务,可以通过适当的任务调度和协调机制,例如轮询或定时器,来模拟多线程行为,而不实际创建多个线程。 - 所有复位相关操作(例如
machine.reset()或machine.soft_reset())只能在主线程进行,否则会引发如OSError异常等错误,主要原因是由于系统关键操作(如复位)涉及底层硬件和系统状态管理,这些操作必须在主线程中完成以确保状态一致性和正确的资源管理,如果在其他线程调用复位操作,可能会导致状态混乱或资源竞争,从而引发错误;同时,USB 相关操作通常由 TinyUSB 驱动处理,复位操作会触发底层 USB 回调函数的执行,如果在非主线程中执行复位操作,就可能触发 TinyUSB 回调的递归调用,而这些回调函数不允许递归调用。
在非主线程中采用复位操作常见报错如下所示:
>>> FATAL: uncaught exception 20008830
OSError
或:
>>> [Watchdog] Main thread is unresponsive. System should restart!
FATAL: uncaught exception 20008830
OSError:
又或是:
>>> Watchdog: Main thread is unresponsive. System should restart!
FATAL: uncaught exception 20008870
OSError: TinyUSB callback can't recurse
在以下代码中,我们实现了一个基于多线程的软件看门狗定时器用于监控主线程和喂狗线程的运行状态,当喂狗线程未能定期“喂狗”(即未能定期清零计数器)时,看门狗会认为系统出现故障,并尝试重启设备。
以下代码可以在我们提供的资料包中 elegance-devkit v1\Demo\54 WDG_Thread 文件夹找到。
示例代码如下:
# Python env : MicroPython v1.23.0
# -*- coding: utf-8 -*-
# @Time : 2024/8/16 上午11:14
# @Author : 李清水
# @File : main.py
# @Description : WDT看门狗定时器类实验,使用Pico软件多线程实现
# ======================================== 导入相关模块 ========================================
# 导入多线程模块
import _thread
# 导入时间相关模块
import time
# 导入硬件模块
import machine
# 导入const常量
from micropython import const
import micropython
# ======================================== 全局变量 ============================================
# 全局变量,用于看门狗线程监控
watchdog_counter = 0
# 超过此计数值未喂狗,则系统重启
WATCHDOG_MAX_COUNTER = const(2)
# 看门狗检查间隔(单位:秒)
WATCHDOG_CHECK_INTERVAL = const(4)
# 喂狗线程执行间隔(秒)
FEED_INTERVAL = const(1)
# 最大喂狗次数,到达后停止喂狗
FEED_COUNT = const(10)
# ======================================== 功能函数 ============================================
# 分配紧急异常缓冲区(必须位于所有中断代码之前)
micropython.alloc_emergency_exception_buf(100)
# 喂狗线程
def feed_thread() -> None:
"""
喂狗线程,用于定期执行喂狗操作,清零 watchdog 计数器,以防止主线程触发重启。
Args:
None
Returns:
None
Raises:
Exception: 在喂狗过程中出现异常时,打印错误信息,不会影响主线程运行。
"""
# 声明全局变量
global watchdog_counter
# 喂狗次数计数器
feed_times = 0
while True:
try:
# 每隔 FEED_INTERVAL 秒执行一次喂狗操作
time.sleep(FEED_INTERVAL)
# 获取互斥锁
lock.acquire()
# 如果喂狗次数未达到 FEED_COUNT,执行喂狗操作
if feed_times < FEED_COUNT:
# 喂狗:清零 watchdog_counter
watchdog_counter = 0
feed_times += 1
print(f"[Feed] Feed the dog! ({feed_times}/{FEED_COUNT})")
else:
# 喂狗次数达到 FEED_COUNT,停止喂狗
print("[Feed] Stop feeding the dog!")
# 退出循环
break
except Exception as e:
# 捕获并打印异常
print(f"[Feed] Error in feed thread: {e}")
finally:
# 释放互斥锁
if lock.locked():
lock.release()
# ======================================== 自定义类 ============================================
# ======================================== 初始化配置 ==========================================
# 上电延时3s
time.sleep(3)
# 打印调试信息
print("FreakStudio : Implement Watchdog Timer using a multi-threading Test")
# 创建一个LED对象
led = machine.Pin(25, machine.Pin.OUT)
# ======================================== 主程序 ===========================================
# 获取互斥锁,在多个线程访问共享资源时进行保护
lock = _thread.allocate_lock()
# 启动喂狗线程
_thread.start_new_thread(feed_thread, ())
# 主线程:监测 `watchdog_counter` 是否超时
try:
# 每隔 WATCHDOG_CHECK_INTERVAL 秒检查一次 watchdog_counter 是否超时
while True:
try:
# 获取互斥锁
lock.acquire()
# 翻转 LED 灯,指示程序正在运行
led.toggle()
# 模拟主线程执行其他任务
time.sleep(WATCHDOG_CHECK_INTERVAL-1)
print("[Main] Main thread is running, checking watchdog...")
# 判断 watchdog_counter 是否大于 WATCHDOG_MAX_COUNTER
if watchdog_counter > WATCHDOG_MAX_COUNTER:
# 喂狗线程由于故障,无法执行喂狗操作
print("[Watchdog] Feed thread is unresponsive. System should restart!")
# 延时 1 秒,确保调试信息输出
time.sleep(1)
# 释放互斥锁
if lock.locked():
lock.release()
# 尝试重启系统
try:
# 软重启系统:删除所有Python对象并重置Python堆空间
machine.soft_reset()
except Exception as e:
# 捕获并打印异常
print(f"[Error] Failed to restart system: {e}")
# 递增 watchdog_counter 的值
watchdog_counter += 1
except Exception as e:
# 捕获并打印异常
print(f"[Main] Error in main thread: {e}")
finally:
# 释放互斥锁
if lock.locked():
lock.release()
except KeyboardInterrupt:
# 捕获用户中断(如 Ctrl+C)
print("[Main] Program interrupted by user.")
finally:
# 确保程序结束时释放资源
print("[Main] Program ended.")
在以上示例代码中,我们进行了以下工作:
- 看门狗机制:设定一个全局变量
watchdog_counter作为看门狗计数器,主线程定期递增watchdog_counter,而喂狗线程定期将其清零(“喂狗”),如果watchdog_counter超过预设的阈值WATCHDOG_MAX_COUNTER,则认为喂狗线程出现问题,系统需要重启。 - 多线程设计:主线程负责监控
watchdog_counter并执行主要任务(如翻转 LED 灯),喂狗线程负责定期“喂狗”,即清零watchdog_counter,喂狗线程每FEED_INTERVAL (1)秒喂狗,运行FEED_COUNT (10)次后停止喂狗,模拟程序异常。 - 互斥锁保护和系统重启:使用
_thread.allocate_lock()创建一个互斥锁lock,用于保护对全局变量watchdog_counter的访问,避免多线程竞争导致数据不一致;同时如果看门狗检测到喂狗线程失效,主线程会尝试调用machine.soft_reset()重启系统。
程序时序图如下所示:

这里,我们使用 machine.soft_reset() 重启系统:

不同于 machine.reset() 硬重启会重新初始化整个系统,machine.soft_reset() 仅会重新启动 MicroPython 解释器,但不会重新初始化硬件,它将清除 MicroPython 的运行时状态(如变量、对象、堆栈等),并重新执行启动脚本(如 boot.py 和 main.py),同时不会断开 REPL 连接。
烧录代码,远程连接树莓派 Pico,终端输出如下:


可以看到,停止喂狗后,在到达超时时间后,看门狗主线程重启设备,在多任务的环境中,我们可以使用看门狗多线程来快速响应主线程出现问题的情况。

多线程软件看门狗的核心思想是通过一个独立的看门狗线程来监控其他工作线程的状态,每个工作线程定期“喂狗”以表明自己仍在正常运行:看门狗线程负责监控全局计数变量,固定时间递增变量,工作线程完成一定操作后将全局计数变量清零,即“喂狗”操作;如果看门狗线程发现全局计数变量超过预定义的上限值(比如 2),则认为主程序线程已经卡死,采取重启设备措施。
浙公网安备 33010602011771号