5.9 Watchdog 设备
5.9 Watchdog 设备
📚 本节导读
学习时长:约 30 分钟
难度级别:⭐⭐⭐☆☆
前置知识:C 语言基础、看门狗基本原理、5.1 设备驱动框架
本节讲解 OneOS 的 Watchdog 看门狗设备。看门狗是一种硬件定时器,用于监控系统运行状态:若系统在超时时间内没有"喂狗"(刷新计数器),看门狗会触发系统复位,防止系统死锁或跑飞。
OneOS 看门狗设备把硬件看门狗封装成标准设备,通过 os_device_control + 标准控制命令(OS_DEVICE_CTRL_WDT_*)来设置超时、启动、喂狗、停止。本节会带你:
- 理解看门狗的两类硬件(独立 IWDG / 窗口 WWDG)与工作区别;
- 掌握
os_watchdog_device结构体(parent/ops字段与继承机制)、os_watchdog_ops操作接口与 6 条标准控制命令; - 掌握喂狗周期与超时时间的关系、WWDG 窗口喂狗规则;
- 逐个拆解
os_hw_watchdog_register/os_watchdog_init/os_watchdog_control/os_watchdog_deinit的完整源码; - 掌握看门狗的注册、启动、喂狗流程;
- 理解更换 MCU 时如何适配 Watchdog(HAL 层
drv_wdt.c的os_watchdog_ops契约与超时秒→重载值换算)的移植方法。
说明:本节所有源码均取自项目
drivers/watchdog/watchdog.h、drivers/watchdog/watchdog.c、drivers/hal/nrf/drivers/nrf52/drv_wdt.c,与项目源码逐字一致(含原始英文注释)。
🎯 学习目标
- 理解独立看门狗(IWDG)与窗口看门狗(WWDG)的区别及窗口喂狗规则
- 掌握
os_watchdog_device结构体(parent/ops)与os_watchdog_ops操作接口 - 掌握 6 条标准控制命令(GET/SET_TIMEOUT、GET_TIMELEFT、KEEPALIVE、START、STOP)
- 掌握喂狗周期与超时时间的合理取值
- 逐个拆解
os_hw_watchdog_register/os_watchdog_init/os_watchdog_control/os_watchdog_deinit的完整源码 - 掌握看门狗的注册、启动、喂狗流程
- 理解 Watchdog 的 HAL 层
os_watchdog_ops契约,掌握更换 MCU 时的适配流程与超时换算
一、概述
1.1 Watchdog 解决什么问题
一句话核心:Watchdog 设备解决"防止系统死锁/跑飞后无法自恢复"的可靠性问题。它让你"启动看门狗后定期喂狗,系统一旦卡死超时不喂狗,硬件自动复位系统"。
为什么需要看门狗:
- 嵌入式系统可能因电磁干扰、软件 bug、内存错误而"死机"或"跑飞"。
- 没有看门狗时,系统只能靠人工断电重启。
- 看门狗硬件会在"超时未喂狗"时自动复位系统,实现无人值守的自恢复。
1.2 两类看门狗硬件
| 类型 | 时钟源 | 特点 | 喂狗要求 |
|---|---|---|---|
| 独立看门狗(IWDG) | 独立 RC 振荡器 | 独立于主时钟,主时钟故障仍能工作 | 超时前喂即可,喂得越早越好 |
| 窗口看门狗(WWDG) | 系统时钟 | 依赖主时钟,提供精确时间窗口 | 必须在窗口内喂,过早或过晚都复位 |
关键理解:IWDG 只要"超时前喂狗"就行;WWDG 有上下窗口,喂狗动作必须落在窗口区间内——太早喂(窗口还没打开)或太晚喂(窗口已关闭)都会复位。WWDG 更精确但也更难用。
数值演算 —— WWDG 窗口喂狗:假设窗口看门狗的窗口配置为"下边界 W_min = 40% 计数器、上边界 W_max = 20% 计数器"(计数器递减),即允许喂狗的区间是"计数器从 40% 降到 20%"这一段:
计数器值(递减): 100% 40% 20% 0%
────────────●───────●────●────●───▶ 时间
窗口未开 │ │
└─喂狗合法区间─┘
┌─────────────────────────────────────────┐
│ 过早喂(计数器 >40%)→ 复位 │
│ 窗口内喂(40%≥计数值>20%)→ 正常 │
│ 过晚喂(计数器 ≤20%,即超时)→ 复位 │
└─────────────────────────────────────────┘为什么 WWDG 设计成"过早也复位":这能检测"系统执行过快"的异常。如果程序因为 bug 跳过了关键流程、执行得比预期快,会过早喂狗,WWDG 立即复位——这是 IWDG 做不到的(IWDG 只检测"过慢/卡死")。代价是喂狗代码必须放在任务调度节奏稳定的地方。
1.3 标准控制命令
来源:drivers/watchdog/watchdog.h
#define OS_DEVICE_CTRL_WDT_GET_TIMEOUT (1) /* get timeout(in seconds) */
#define OS_DEVICE_CTRL_WDT_SET_TIMEOUT (2) /* set timeout(in seconds) */
#define OS_DEVICE_CTRL_WDT_GET_TIMELEFT (3) /* get the left time before reboot(in seconds) */
#define OS_DEVICE_CTRL_WDT_KEEPALIVE (4) /* refresh watchdog */
#define OS_DEVICE_CTRL_WDT_START (5) /* start watchdog */
#define OS_DEVICE_CTRL_WDT_STOP (6) /* stop watchdog */关键理解 —— 命令语义:
| 命令宏 | 值 | 作用 |
|---|---|---|
OS_DEVICE_CTRL_WDT_GET_TIMEOUT | 1 | 读取超时时间(秒) |
OS_DEVICE_CTRL_WDT_SET_TIMEOUT | 2 | 设置超时时间(秒) |
OS_DEVICE_CTRL_WDT_GET_TIMELEFT | 3 | 读取距离复位剩余时间(秒) |
OS_DEVICE_CTRL_WDT_KEEPALIVE | 4 | 喂狗(刷新计数器) |
OS_DEVICE_CTRL_WDT_START | 5 | 启动看门狗 |
OS_DEVICE_CTRL_WDT_STOP | 6 | 停止看门狗 |
注意:超时时间单位是秒(不是毫秒)。所有操作都用
os_device_control(dev, 命令, &arg)下发,arg是int/uint32_t类型的时间值。
数值演算 —— 喂狗周期与超时的关系:喂狗周期(T_feed)必须明显小于超时时间(T_timeout),同时留出任务调度的抖动余量。经验法则:T_feed = T_timeout / 3~/ 2。以 T_timeout = 5s 为例:
喂狗周期 T_feed | 选择理由 | 是否安全 |
|---|---|---|
5s / 5 = 1s | 偏快,占用 CPU 稍多 | ✅ 安全(余量 4s) |
5s / 3 ≈ 1.7s | 折中,常用 | ✅ 安全(余量约 3.3s) |
5s / 2 = 2.5s | 接近上限 | ⚠️ 余量仅 2.5s,任务调度抖动可能超时 |
5s | 等于超时 | ❌ 危险,任何一次调度延迟都导致复位 |
为什么不能喂得太勤:喂狗本身有开销(进临界区、写硬件寄存器),太勤浪费 CPU;更重要的是喂狗任务若和主流程耦合,可能掩盖主流程卡死。为什么不能太接近超时:系统有调度抖动、中断延迟,实际喂狗时刻可能晚于设定周期,必须留足够余量。所以
/3~/2是兼顾安全与开销的经验值。
1.4 Watchdog 使用场景
1.4.1 一句话核心
Watchdog 设备适用于所有需要高可靠性的嵌入式产品,在系统启动时开启看门狗,运行中周期性喂狗,确保系统卡死后能自动复位恢复。
1.4.2 典型应用场景
场景一:系统启动开启看门狗 + 喂狗任务
启动:
os_device_open("wdt")
SET_TIMEOUT = 5(秒)
START
运行:
喂狗任务每 2 秒 KEEPALIVE 一次(< 5s 超时)为什么用 Watchdog:设置超时 + 启动后,系统只要在超时内持续喂狗就正常运行,一旦主流程卡死,喂狗任务也停,硬件自动复位。
场景二:窗口看门狗的精确喂狗
WWDG 窗口:必须在窗口 [W_min, W_max] 内喂狗
过早喂 → 复位
窗口内喂 → 正常
过晚喂(超时)→ 复位为什么用 Watchdog:WWDG 能更早发现"系统执行过快/过慢"的异常,适合对任务调度节奏有严格要求的场景。
1.4.3 与"软件看门狗/任务心跳检测"的对比(为什么用硬件看门狗)
"看门狗"思想也可以用软件实现,但可靠性与硬件看门狗有本质区别:
| 维度 | 硬件看门狗(本节) | 软件看门狗 / 任务心跳检测 |
|---|---|---|
| 可靠性 | 独立硬件,主时钟/软件全死也能复位 | 依赖 CPU 仍在跑(若 CPU 死机,软件看门狗自己也停) |
| 兜底能力 | 系统完全跑飞也能复位 | 只能检测"某个任务/线程卡死",CPU 整体死机则失效 |
| 实现 | 只需周期喂狗 | 需要心跳信号 + 监控任务 + 超时判定 |
| 典型用途 | 系统级死锁/跑飞的最后防线 | 检测单个任务超时、业务逻辑异常 |
结论:两者不是二选一,而是叠加——硬件看门狗做"系统级兜底复位",软件看门狗/心跳检测做"业务级异常告警"。产品中常同时用:心跳任务周期喂硬件看门狗,若检测到某个关键任务卡死(软件心跳超时)则记录日志/保存现场。
1.4.4 选型边界表(何时不该用它)
| 场景 | 该用什么 | 为什么 |
|---|---|---|
| 只需要定时触发 | 内核定时器 / HRTIMER(5.7) | 看门狗是"复位保障",不是普通定时器 |
| 不需要自动复位恢复 | 不使用看门狗 | 某些场景下不希望系统自动复位 |
| 需要业务级异常监控 | 软件看门狗 / 任务心跳检测 | 硬件看门狗只复位,无法记录业务异常现场 |
二、核心数据结构
2.1 看门狗设备 os_watchdog_device
来源:drivers/watchdog/watchdog.h
struct os_watchdog_device
{
struct os_device parent; /* inherit from os_device */
const struct os_watchdog_ops *ops; /* Operation function set */
};
typedef struct os_watchdog_device os_watchdog_t;字段详解:
| 字段 | 类型 | 含义 |
|---|---|---|
parent | struct os_device(内嵌在开头) | 标准设备基类,让这个结构体"是一个设备",可被 os_device_register 注册、被 os_device_open 打开 |
ops | const struct os_watchdog_ops * | 看门狗操作函数表(指向 HAL 层提供的 init/control) |
关键理解 —— 为什么 parent(os_device)内嵌在最开头:这与 5.2 GPIO、5.8 RTC 的机制完全一致。parent 是第一个成员,所以"设备指针 struct os_device *dev"和"看门狗指针 os_watchdog_t *wtd"指向同一块内存、数值相等。框架层的 os_watchdog_init 里 wtd = (os_watchdog_t *)dev 只是用更大的视角去解读 dev 指向的内存,从而访问 parent 后面的 ops 字段。这是 OneOS 用"C 语言继承"实现设备多态的标准手法。
关键理解 —— ops 为空时的风险:os_watchdog_init/os_watchdog_control 直接解引用 wtd->ops->init/wtd->ops->control,没有判空。若 HAL 层注册时忘了给 ops 赋值(nrf5_wdg_probe 里 wdg->ops = &ops),一旦设备被打开/操作就会访问非法内存。所以注册看门狗设备前,必须确保 ops 已指向有效的操作表。
为什么用 os_watchdog_t 别名:typedef struct os_watchdog_device os_watchdog_t 是 OneOS 的命名惯例——xxx_t 表示"xxx 对象类型",写代码时用更短的 os_watchdog_t,两者是同一个类型。
2.2 看门狗操作函数表 os_watchdog_ops
来源:drivers/watchdog/watchdog.h
struct os_watchdog_ops
{
os_err_t (*init)(os_watchdog_t *wdt); /* init watchdog */
os_err_t (*control)(os_watchdog_t *wdt, int cmd, void *arg); /* control watchdog */
};字段详解:
| 函数指针 | 说明 | 典型实现(见第四章移植) |
|---|---|---|
init | 初始化看门狗硬件(配置时钟/重载值/中断) | nrf5_wdt_init:nrfx_wdt_init(&config, handler) |
control | 处理控制命令(设超时/启动/喂狗/停止等) | nrf5_wdt_control:switch(cmd) 分发 |
关键理解:control 是唯一的"命令分发"入口,HAL 层根据 cmd 参数执行对应操作。所有 OS_DEVICE_CTRL_WDT_* 命令最终都走到 HAL 的 control。注意:ops 里的两个函数指针 HAL 层都可以选择不提供——os_watchdog_init 里 if (wtd->ops->init) 会先判断 init 是否为空,为空则返回 OS_NOSYS;但 control 必须提供,因为 os_watchdog_control 和 os_watchdog_deinit 直接调用它、不判空。
三、核心源码详解
说明:以下源码均取自
drivers/watchdog/watchdog.c,与项目源码逐字一致(含原始英文注释)。
3.1 os_watchdog_init —— 初始化看门狗
一句话定位:设备打开时调用,把初始化转发给 HAL 层的 ops->init。
完整源码(drivers/watchdog/watchdog.c,与项目一致):
static os_err_t os_watchdog_init(struct os_device *dev)
{
os_watchdog_t *wtd;
OS_ASSERT(dev != OS_NULL);
wtd = (os_watchdog_t *)dev;
if (wtd->ops->init)
{
return (wtd->ops->init(wtd));
}
return (OS_NOSYS);
}分步详解:
第 1 步:类型转换与调用 HAL init(条件 ops->init 存在)
OS_ASSERT(dev != OS_NULL);
wtd = (os_watchdog_t *)dev;
if (wtd->ops->init)
{
return (wtd->ops->init(wtd));
}
return (OS_NOSYS);
}详细解释:dev 就是 os_watchdog_t 结构体(继承 os_device),强转后调用 HAL 层的 ops->init(wtd) 初始化硬件。若 HAL 未提供 init,返回 OS_NOSYS(不支持)。
os_watchdog_init() 条件走向总结:
os_watchdog_init(dev) 条件走向总结:
├── dev == OS_NULL → OS_ASSERT 断言崩溃(应传有效设备指针)
└── dev != OS_NULL → wtd = (os_watchdog_t*)dev
├── wtd->ops->init 非空(HAL 提供 init)
│ └── return wtd->ops->init(wtd)(调 HAL 初始化硬件)
└── wtd->ops->init 为空(HAL 未提供)
└── return OS_NOSYS(不支持)3.2 os_watchdog_control —— 命令分发
一句话定位:所有控制命令(设超时/启动/喂狗/停止等)透传给 HAL 的 ops->control。
完整源码(drivers/watchdog/watchdog.c,与项目一致):
static os_err_t os_watchdog_control(struct os_device *dev, int cmd, void *args)
{
os_watchdog_t *wtd;
OS_ASSERT(dev != OS_NULL);
wtd = (os_watchdog_t *)dev;
return (wtd->ops->control(wtd, cmd, args));
}分步详解:
第 1 步:透传命令给 HAL(无条件执行)
OS_ASSERT(dev != OS_NULL);
wtd = (os_watchdog_t *)dev;
return (wtd->ops->control(wtd, cmd, args));
}详细解释:关键理解:os_device_control(dev, cmd, args) 到达这里后,把 cmd(如 OS_DEVICE_CTRL_WDT_SET_TIMEOUT)和 args 原样透传给 HAL 层的 ops->control。所以应用设置超时/启动/喂狗/停止全都走这条通道。
os_watchdog_control() 条件走向总结:
os_watchdog_control(dev, cmd, args) 条件走向总结:
├── dev == OS_NULL → OS_ASSERT 断言崩溃
└── dev != OS_NULL → wtd = (os_watchdog_t*)dev
└── return wtd->ops->control(wtd, cmd, args) ← 原样透传命令与参数3.3 os_watchdog_deinit —— 关闭看门狗
一句话定位:设备关闭时调用,尝试用 STOP 命令停止看门狗。
完整源码(drivers/watchdog/watchdog.c,与项目一致):
static os_err_t os_watchdog_deinit(struct os_device *dev)
{
os_watchdog_t *wtd;
OS_ASSERT(dev != OS_NULL);
wtd = (os_watchdog_t *)dev;
if (wtd->ops->control(wtd, OS_DEVICE_CTRL_WDT_STOP, OS_NULL) != OS_SUCCESS)
{
os_kprintf(" This watchdog can not be stoped\r\n");
return (OS_FAILURE);
}
return (OS_SUCCESS);
}分步详解:
第 1 步:发 STOP 命令停止看门狗(条件 停止失败)
if (wtd->ops->control(wtd, OS_DEVICE_CTRL_WDT_STOP, OS_NULL) != OS_SUCCESS)
{
os_kprintf(" This watchdog can not be stoped\r\n");
return (OS_FAILURE);
}
return (OS_SUCCESS);
}详细解释:关闭设备时发 OS_DEVICE_CTRL_WDT_STOP 命令停止看门狗。关键理解:很多 MCU 的看门狗一旦启动无法停止,此时 control 返回失败,打印提示并返回 OS_FAILURE。这是看门狗的常见特性。
os_watchdog_deinit() 条件走向总结:
os_watchdog_deinit(dev) 条件走向总结:
├── dev == OS_NULL → OS_ASSERT 断言崩溃
└── dev != OS_NULL → wtd = (os_watchdog_t*)dev
├── wtd->ops->control(STOP) 返回 OS_SUCCESS(能停止)
│ └── return OS_SUCCESS(正常关闭)
└── wtd->ops->control(STOP) 返回非 OS_SUCCESS(停止失败,多数 MCU 启动后不可停)
├── 打印 " This watchdog can not be stoped"
└── return OS_FAILURE3.4 os_hw_watchdog_register —— 注册看门狗设备
一句话定位:HAL 层调用,把看门狗设备注册到设备框架,并绑定 wdt_ops 操作表。
完整源码(drivers/watchdog/watchdog.c,与项目一致):
const static struct os_device_ops wdt_ops = {
.init = os_watchdog_init,
.deinit = os_watchdog_deinit,
.control = os_watchdog_control,
};
os_err_t os_hw_watchdog_register(struct os_watchdog_device *wtd, const char *name, void *data)
{
struct os_device *device;
OS_ASSERT(wtd != OS_NULL);
device = &(wtd->parent);
device->type = OS_DEVICE_TYPE_MISCELLANEOUS;
device->ops = &wdt_ops;
device->user_data = data;
/* register a character device */
return os_device_register(device, name);
}分步详解:
第 1 步:设置设备类型与操作表(无条件执行)
struct os_device *device;
OS_ASSERT(wtd != OS_NULL);
device = &(wtd->parent);
device->type = OS_DEVICE_TYPE_MISCELLANEOUS;
device->ops = &wdt_ops;
device->user_data = data;详细解释:把 wtd->parent(os_device)的类型设为 OS_DEVICE_TYPE_MISCELLANEOUS(杂项设备),操作表设为 wdt_ops(含 init/deinit/control),用户数据 user_data 存 data。
第 2 步:注册设备(无条件执行)
return os_device_register(device, name);
}详细解释:用 name(如 "wdt")注册到设备框架。注册成功后应用可用 os_device_open("wdt") 打开并使用。
os_hw_watchdog_register() 条件走向总结:
os_hw_watchdog_register(wtd, name, data) 条件走向总结:
├── wtd == OS_NULL → OS_ASSERT 断言崩溃(注册的 wtd 必须有效)
└── wtd != OS_NULL
├── device = &wtd->parent
├── device->type = OS_DEVICE_TYPE_MISCELLANEOUS(杂项设备)
├── device->ops = &wdt_ops(init/deinit/control)
├── device->user_data = data
└── return os_device_register(device, name)(注册,成功则返回 OS_SUCCESS)四、完整使用示例
示例:打开看门狗、设置超时、启动并喂狗
#include <watchdog/watchdog.h>
#include <device.h>
#include <os_task.h>
#include <os_stddef.h>
#include <os_util.h>
/* 喂狗任务:每 2 秒喂一次(< 5 秒超时),系统卡死则不复位 */
void wdt_feed_task(void *arg)
{
os_device_t *wdt = (os_device_t *)arg;
while (1)
{
/* 喂狗失败说明看门狗未启动/已停止,需要处理 */
if (os_device_control(wdt, OS_DEVICE_CTRL_WDT_KEEPALIVE, OS_NULL) != OS_SUCCESS)
{
os_kprintf("wdt keepalive failed\r\n");
}
os_task_msleep(2000); /* 每 2s 喂一次(超时 5s,余量 3s) */
}
}
/* 定义喂狗任务控制块与任务栈(静态创建,不占堆) */
OS_TASK_DEFINE(wdt_feed_cb);
OS_TASK_STACK_DEFINE(wdt_feed_stack, 1024);
/* 启动喂狗任务 */
void wdt_feed_task_start(os_device_t *wdt)
{
os_task_id tid;
os_err_t ret;
tid = os_task_create(&wdt_feed_cb,
OS_TASK_STACK_BEGIN_ADDR(wdt_feed_stack),
OS_TASK_STACK_SIZE(wdt_feed_stack),
"wdt_feed", /* 任务名 */
wdt_feed_task, /* 任务入口 */
wdt, /* 入口参数:把 wdt 设备指针传进去 */
20); /* 优先级 20 */
OS_ASSERT(tid != OS_NULL); /* 创建失败(TCB/栈内存不足)则断言 */
ret = os_task_startup(tid);
if (ret != OS_SUCCESS)
{
os_kprintf("wdt feed task startup failed\r\n");
}
}
/* 整体流程:打开 → 设超时 → 启动 → 起喂狗任务 */
void wdt_demo(void)
{
os_device_t *wdt;
os_err_t ret;
uint32_t timeout = 5; /* 超时 5 秒 */
wdt = os_device_open("wdt");
if (wdt == OS_NULL)
{
os_kprintf("open wdt failed\r\n");
return;
}
ret = os_device_control(wdt, OS_DEVICE_CTRL_WDT_SET_TIMEOUT, &timeout);
if (ret != OS_SUCCESS)
{
os_kprintf("set timeout failed\r\n");
return;
}
ret = os_device_control(wdt, OS_DEVICE_CTRL_WDT_START, OS_NULL);
if (ret != OS_SUCCESS)
{
os_kprintf("start wdt failed\r\n");
return;
}
os_kprintf("wdt started, timeout=%d s\r\n", timeout);
wdt_feed_task_start(wdt); /* 启动独立喂狗任务 */
}说明:示例展示 Watchdog 完整用法:
os_device_open("wdt")→SET_TIMEOUT设超时 →START启动 → 用OS_TASK_DEFINE/os_task_create创建独立喂狗任务,周期性KEEPALIVE喂狗。实际效果:设置 5 秒超时并启动后,只要喂狗任务每 2 秒喂一次,系统就正常;若系统卡死导致喂狗任务停止超过 5 秒,硬件自动复位。
注意:
- 所有看门狗操作都通过
os_device_control+OS_DEVICE_CTRL_WDT_*命令完成,没有os_watchdog_start之类的专用函数。- 超时时间单位是秒。喂狗间隔应明显小于超时时间(如设为超时的 1/3~1/2)。
- 喂狗必须放在独立任务里,且喂狗前要确认关键流程已完成(见 1.4.3)——若在主流程里喂狗,主流程卡死但喂狗代码仍被执行,看门狗失去意义。
五、Watchdog 的移植/适配(更换 MCU)
5.1 先认识 HAL 层:谁在真正操作看门狗硬件
回顾 1.3 节,应用层所有操作都通过 os_device_control + OS_DEVICE_CTRL_WDT_* 命令下发,最终到达框架层 os_watchdog_control,再透传给 HAL 层的 ops->control。这个 ops 是谁提供的? 答案是 HAL 层每个厂商的 drv_wdt.c(或 drv_iwdt.c)。
关键认知:drivers/watchdog/watchdog.c 是跨平台统一的框架层,它只负责"注册设备、透传命令、转发 init/STOP",不碰任何硬件寄存器。真正跟芯片相关的是 drivers/hal/<厂商>/drivers/drv_wdt.c——它为这个 MCU 实现"初始化看门狗硬件、设置超时、喂狗、启动/停止"。换 MCU 只需换这份 drv_wdt.c。
项目 drivers/hal/ 下每个 MCU 厂商都有看门狗驱动(drv_wdt.c 或 drv_iwdt.c),它们并列平级,一个工程只编译当前所选 MCU 的那一份:
| 厂商 | 文件 |
|---|---|
| nRF | drivers/hal/nrf/drivers/nrf52/drv_wdt.c |
| 博通集成 | drivers/hal/beken/drivers/drv_wdt.c |
| 高云 | drivers/hal/levetop/drivers/drv_wdt.c |
| 复旦微 | drivers/hal/fm/drivers/15f3/drv_iwdt.c(含 WWDG 版本) |
| 华大 HC | drivers/hal/hdsc/HC32F19x_HAL/sdk/src/hc_wdt.c |
5.2 移植的全景图:只有 HAL 层与 MCU 相关
┌──────────────────────────────────────────────────────────┐
│ 应用层(Application) │
│ os_device_open("wdt") → control(设置超时/启动/喂狗/停止) │
└──────────────────────────────┬───────────────────────────┘
│ os_device_control(dev, cmd, &arg)
▼
┌──────────────────────────────────────────────────────────┐
│ Watchdog 框架层(drivers/watchdog/watchdog.c) │
│ os_hw_watchdog_register / os_watchdog_init/control/ │
│ deinit(换 MCU 不用改) │
└──────────────────────────────┬───────────────────────────┘
│ wdt->ops->init/control
▼
┌──────────────────────────────────────────────────────────┐
│ HAL 层(drivers/hal/<厂商>/drivers/drv_wdt.c) ★需适配★ │
│ 实现 os_watchdog_ops 的 init/control │
│ 操作具体看门狗寄存器(重载值/使能/喂狗) │
└──────────────────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ 硬件(MCU 的 IWDG/WWDG 看门狗外设) │
└──────────────────────────────────────────────────────────┘移植的关键洞察:换 MCU,应用层与框架层一行都不用改,只需在 HAL 层提供 os_watchdog_ops(init + control)并注册为 "wdt" 设备。
5.3 移植需要提供的"接口契约"
HAL 层看门狗驱动要能用,必须实现 os_watchdog_ops 的两个函数,并用 os_hw_watchdog_register 注册。
源码参考——nRF52 的 drv_wdt.c(drivers/hal/nrf/drivers/nrf52/drv_wdt.c,与项目逐字一致):
static void nrf5_wdg_refresh(os_watchdog_t *wdg)
{
os_ubase_t level;
os_spin_lock_irqsave(&gs_device_lock, &level);
nrfx_wdt_channel_feed(m_channel_id);
os_spin_unlock_irqrestore(&gs_device_lock, level);
}
static os_err_t nrf5_wdt_init(os_watchdog_t *wdg)
{
nrfx_wdt_config_t config = NRFX_WDT_DEAFULT_CONFIG;
config.reload_value = 10000; // ms
nrfx_wdt_init(&config, wdt_event_handler);
nrfx_wdt_channel_alloc(&m_channel_id);
return OS_SUCCESS;
}
static os_err_t nrf5_wdt_control(os_watchdog_t *wdg, int cmd, void *arg)
{
switch (cmd)
{
case OS_DEVICE_CTRL_WDT_KEEPALIVE:
nrf5_wdg_refresh(wdg);
return OS_SUCCESS;
case OS_DEVICE_CTRL_WDT_SET_TIMEOUT:
break;
case OS_DEVICE_CTRL_WDT_GET_TIMEOUT:
break;
case OS_DEVICE_CTRL_WDT_START:
nrf5_wdg_start(wdg);
return OS_SUCCESS;
}
return OS_NOSYS;
}
const static struct os_watchdog_ops ops = {
.init = &nrf5_wdt_init,
.control = &nrf5_wdt_control,
};接口契约要点:
| 项目 | 要求 |
|---|---|
ops->init | 初始化看门狗硬件(配置时钟源、重载值、使能、喂狗通道),必须(框架会判空,为空返回 OS_NOSYS) |
ops->control | 处理 KEEPALIVE/START/SET_TIMEOUT/GET_TIMEOUT/GET_TIMELEFT/STOP 命令,必须(框架不判空直接调用) |
| 喂狗线程安全 | 喂狗/启动操作应加 os_spin_lock_irqsave(&gs_device_lock, &level) 保护(见 nrf5_wdg_refresh),因为看门狗寄存器可能被多个上下文访问 |
| 注册 | 用 os_hw_watchdog_register(wdg, dev->name, OS_NULL) 注册为 "wdt" 设备 |
5.4 移植的标准流程(6 步)
第 1 步:确认硬件能力——新 MCU 看门狗类型(IWDG/WWDG)、时钟源、重载值位宽、是否可停止。
第 2 步:定义喂狗通道/句柄——如 nRF 的 nrfx_wdt_channel_id m_channel_id,其它芯片可能是 HAL 句柄。
第 3 步:实现 init——配置看门狗并预留喂狗通道:
static os_err_t my_wdt_init(os_watchdog_t *wdg)
{
/* 配置看门狗时钟源、使能、分配喂狗通道 */
/* 例如 nrf: nrfx_wdt_init(&config, handler) + nrfx_wdt_channel_alloc(&id) */
return OS_SUCCESS;
}第 4 步:实现 control——用 switch(cmd) 处理命令:
static os_err_t my_wdt_control(os_watchdog_t *wdg, int cmd, void *arg)
{
switch (cmd)
{
case OS_DEVICE_CTRL_WDT_KEEPALIVE: /* 喂狗 */
return my_wdt_feed(wdg);
case OS_DEVICE_CTRL_WDT_START: /* 启动 */
return my_wdt_start(wdg);
case OS_DEVICE_CTRL_WDT_SET_TIMEOUT: /* 设超时(arg 指向秒数) */
return my_wdt_set_timeout(wdg, *(uint32_t *)arg);
case OS_DEVICE_CTRL_WDT_GET_TIMELEFT: /* 读剩余时间 */
*(uint32_t *)arg = my_wdt_get_timeleft(wdg);
return OS_SUCCESS;
default:
return OS_NOSYS;
}
}
const static struct os_watchdog_ops ops = {
.init = my_wdt_init,
.control = my_wdt_control,
};第 5 步:实现 probe 并注册——os_hw_watchdog_register 需要 os_watchdog_t 实例(分配 + 填 ops):
static int my_wdg_probe(const os_driver_info_t *drv, const os_device_info_t *dev)
{
os_watchdog_t *wdg = os_calloc(1, sizeof(os_watchdog_t));
OS_ASSERT(wdg);
wdg->ops = &ops;
if (os_hw_watchdog_register(wdg, dev->name, OS_NULL) != OS_SUCCESS)
{
os_kprintf("wdg device register failed.\r\n");
return OS_FAILURE;
}
return OS_SUCCESS;
}
OS_DRIVER_INFO my_wdt_driver = {
.name = "WDG_HandleTypeDef",
.probe = my_wdg_probe,
};
OS_DRIVER_DEFINE(my_wdt_driver, OS_INIT_LEVEL_PRE_DEVICE, OS_INIT_SUBLEVEL_LOW);第 6 步:数值演算——超时"秒"→硬件计数/重载值换算:control 收到的 SET_TIMEOUT 参数是秒,而看门狗硬件需要的是重载值/计数值。换算公式:
重载值 = 超时秒 × 时钟频率 / 预分频假设看门狗时钟 freq = 32768 Hz(如 nRF 的低频晶振),无预分频,设超时 5 秒:
| 计算项 | 代入计算 | 结果 |
|---|---|---|
| 重载值 | 5 × 32768 / 1 | 163840 |
| 检查是否超位宽 | 假设 16 位计数器(最大 65535) | 163840 > 65535 ❌ |
发现问题:5 秒超出了 16 位计数器的范围,说明需要预分频或改用更大的计数位宽。加预分频 prescaler = 128:
| 计算项 | 代入计算 | 结果 |
|---|---|---|
| 实际时钟 | 32768 / 128 | 256 Hz |
| 重载值 | 5 × 256 / 1 | 1280 ✓(< 65535,合法) |
为什么要在 HAL 层做这个换算:因为不同 MCU 的看门狗时钟频率、预分频、位宽都不同,应用层只关心"几秒",具体怎么变成硬件重载值必须由 HAL 层根据芯片参数换算。换算错误会直接导致实际超时时间与设定不符(超时过短频繁复位、过长失去保护)。
5.5 移植检查清单(快速自查)
| 检查项 | 说明 |
|---|---|
ops->control 处理了 KEEPALIVE/START? | 这两个是必备命令,缺失则喂狗/启动失效 |
| 喂狗操作加了临界区保护? | 看门狗寄存器可能被多上下文访问,用 os_spin_lock_irqsave 保护 |
| 超时秒→重载值换算正确? | 结合芯片时钟/预分频/位宽,防止超时时间与实际不符 |
注册名是 "wdt"? | 需与 os_device_open("wdt") 一致,否则应用打开失败 |
| 启动后能否停止? | 多数 MCU 不可停,deinit 的 STOP 会失败,属正常现象 |
📝 本节小结
核心要点回顾
本节围绕 Watchdog 设备展开,核心要点如下:
1. 两类看门狗
| 类型 | 时钟源 | 喂狗要求 |
|---|---|---|
| IWDG 独立看门狗 | 独立 RC | 超时前喂即可 |
| WWDG 窗口看门狗 | 系统时钟 | 必须在窗口内喂,过早过晚都复位 |
2. 标准控制命令
GET/SET_TIMEOUT、GET_TIMELEFT、KEEPALIVE(喂狗)、START、STOP。- 全部通过
os_device_control(dev, cmd, &arg)下发,超时单位秒。
3. 核心函数
| 函数 | 作用 |
|---|---|
os_watchdog_init | 打开时转发到 ops->init |
os_watchdog_control | 透传命令到 ops->control |
os_watchdog_deinit | 关闭时发 STOP |
os_hw_watchdog_register | 注册设备(类型 MISC、ops=wdt_ops) |
4. 关键特性
- 多数 MCU 看门狗启动后无法停止,
deinit的 STOP 会失败。 - 喂狗间隔通常设为超时的 1/3~1/2(如超时 5s,喂狗周期取 1~2.5s)。
5. 关键结构体
os_watchdog_device:内嵌parent(os_device,首成员保证dev与控制块指针数值相等)+ops(操作表指针)。os_watchdog_ops:init(可判空,空则OS_NOSYS)+control(必须提供,框架不判空)。ops为空时解引用会崩溃,HAL 注册前必须赋值。
6. 更换 MCU 的移植要点
- 框架层
watchdog.c与 HAL 层drv_wdt.c解耦,换 MCU 只换drv_wdt.c。 - HAL 契约 = 实现
os_watchdog_ops(init/control)+os_hw_watchdog_register注册为"wdt"。 - 超时"秒"需在 HAL 层换算成硬件重载值(
重载值 = 超时秒 × 时钟频率 / 预分频),注意位宽与预分频。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
用了不存在的 os_watchdog_start 等函数 | 看门狗操作统一用 os_device_control + OS_DEVICE_CTRL_WDT_* 命令,没有专用函数 |
| 超时单位搞错 | 超时时间单位是秒(SET_TIMEOUT),不是毫秒 |
| 喂狗过频或过晚 | IWDG 在超时前喂;WWDG 必须在窗口内喂,过早过晚都复位 |
| 在主流程喂狗 | 若主流程卡死,喂狗代码可能仍被执行,应用独立任务 + 心跳检测喂狗 |
| 启动后无法关闭 | 多数 MCU 看门狗启动后不可停止,开发调试阶段先关掉或延长超时 |
| 忽略复位原因判断 | 复位后应先读复位标志区分是看门狗复位还是上电复位,便于定位问题 |
| 喂狗周期接近超时 | 喂狗周期应为超时的 1/3~1/2,留足调度抖动余量;等于超时则任何延迟都复位 |
ops 未赋值就注册 | os_watchdog_init/control 直接解引用 ops->xxx 不判空,HAL 注册前必须 wdg->ops = &ops |
| 换 MCU 改框架层 | 只改 HAL 层 drv_wdt.c,watchdog.c 框架与应用层不动 |
| 超时秒→重载值换算错 | 重载值 = 超时秒 × 时钟频率 / 预分频,注意计数器位宽,超位宽需加大预分频 |
| 喂狗未加临界区保护 | 喂狗寄存器可能被多上下文访问,用 os_spin_lock_irqsave 保护(见 nrf5_wdg_refresh) |
下一步
掌握了 Watchdog 看门狗设备后,接下来学习 Sensors 传感器设备:
- 5.10 Sensors设备:了解 OneOS 统一传感器框架与加速度计/陀螺仪等常见传感器读取。