4、硬件看门狗驱动开发:基于i.MX8平台的看门狗驱动实例,设备树配置

好,咱们今天来聊点硬核的。硬件看门狗驱动开发,说白了就是给系统装一个「心跳监护仪」。系统正常时,你定期去踢它一脚;系统挂了,它就帮你硬重启。我在i.MX8平台上折腾过好几轮,踩过不少坑,今天把经验掰开揉碎了讲给你听。

4.1 为什么非得用硬件看门狗?

你可能会问:软件里搞个定时器不也能复位吗?嗯,这里要注意——软件看门狗一旦CPU跑飞、中断被关、或者内核panic了,它自己都活不了,更别说救你了。硬件看门狗是独立于CPU运行的,它有自己的时钟源,哪怕内核挂了,它照样能触发复位。

我个人习惯,在产品量产阶段,硬件看门狗是必选项。曾经有个项目,客户现场偶发死机,软件团队查了两个月没找到根因。最后我建议加上硬件看门狗,复位日志一抓,发现是某个外设DMA把内存踩了。你看,硬件看门狗不只是保命,还能帮你定位问题。

4.2 i.MX8看门狗硬件特性速览

i.MX8系列集成了两个看门狗模块:WDOG1和WDOG2。我一般用WDOG1做系统级看门狗,WDOG2留给安全域用。核心参数如下:

参数 说明
时钟源 内部低频振荡器(32.768kHz)或外部晶振
超时范围 0.5秒 ~ 128秒(可配置)
复位方式 WARM复位(不重新初始化DDR)或COLD复位
中断支持 超时前可先触发中断,给系统一次「悔过」机会
防误触发 需要写入特定序列(0xB480 + 0x4A80)才能刷新计数器

这里有个关键点:i.MX8的看门狗一旦使能,除非硬件复位,否则无法关闭。所以调试阶段要小心,我曾经在开发板上忘了关看门狗,每次刚进uboot就被复位,折腾了半天才想起来是看门狗在捣乱。

4.3 设备树配置:把硬件信息告诉内核

在Linux里,驱动不直接操作物理地址,而是通过设备树来描述硬件。i.MX8的看门狗设备树节点长这样:

&wdog1: watchdog@30280000 {
    compatible = "fsl,imx8qxp-wdt", "fsl,imx21-wdt";
    reg = <0x0 0x30280000 0x0 0x10000>;
    interrupts = <GIC_SPI 79 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clk IMX8QXP_WDOG1_CLK>;
    clock-names = "wdog";
    fsl,ext-reset-output = <1>;
    timeout-sec = <60>;
    status = "okay";
};

我来逐行解释一下:

  • compatible:驱动匹配的关键字。这里用了两个,内核会先匹配imx8qxp-wdt,如果找不到就回退到imx21-wdt。我建议保留回退兼容,方便内核版本升级。
  • reg:看门狗寄存器的物理基地址和大小。i.MX8QXP的WDOG1基地址是0x30280000,长度64KB。
  • interrupts:中断号79,SPI类型,高电平触发。这个中断用于超时前的「预警」。
  • clocks:时钟源。这里指定了WDOG1_CLK,频率通常是32.768kHz。
  • fsl,ext-reset-output:这个属性很关键。设为1表示看门狗超时后输出外部复位信号,而不是只产生内部中断。量产产品必须设为1。
  • timeout-sec:默认超时时间,单位秒。我习惯设60秒,给系统足够的踢狗缓冲。
⚠️ 避坑指南: 我曾经在设备树里忘了加 fsl,ext-reset-output 属性,结果看门狗超时后只是打印了一条警告日志,系统根本没复位。排查了半天才发现是复位输出没使能。这个属性在i.MX8的参考手册里叫WCR[WDZST],设备树里对应这个属性名。

4.4 驱动核心:踢狗与超时处理

驱动代码其实不复杂,核心就两个函数:imx2_wdt_ping(踢狗)和 imx2_wdt_isr(中断处理)。

先看踢狗函数:

static void imx2_wdt_ping(struct watchdog_device *wdog)
{
    struct imx2_wdt_device *wdev = watchdog_get_drvdata(wdog);
    void __iomem *base = wdev->base;

    /* 写入刷新序列:先写0xB480,再写0x4A80 */
    writel(0xB480, base + IMX2_WDT_WSR);
    writel(0x4A80, base + IMX2_WDT_WSR);
}

为什么是0xB480和0x4A80?这是i.MX8硬件规定的解锁序列,防止软件误操作导致看门狗被意外刷新。你想想看,如果随便写个值就能踢狗,那看门狗还有什么安全性可言?

再看中断处理函数:

static irqreturn_t imx2_wdt_isr(int irq, void *dev_id)
{
    struct watchdog_device *wdog = dev_id;
    struct imx2_wdt_device *wdev = watchdog_get_drvdata(wdog);

    /* 清除中断标志 */
    writel(IMX2_WDT_WICR_WTIC, wdev->base + IMX2_WDT_WICR);

    /* 打印告警,通知用户空间 */
    dev_crit(wdev->dev, "Watchdog timeout! System will reset in %d seconds\n",
             wdog->timeout);

    /* 这里可以触发panic,让内核dump现场 */
    panic("Hardware watchdog triggered");

    return IRQ_HANDLED;
}

这里有个设计取舍:中断来了之后,是让系统直接复位,还是先打印日志再复位?我个人习惯是调用panic(),这样内核会打印oops信息、调用栈、寄存器状态,对事后分析非常有帮助。当然,生产环境可能希望静默复位,那就直接返回,等超时后硬件自动复位。

4.5 用户空间踢狗:守护进程的职责

驱动写好了,谁来踢狗?答案是用户空间的守护进程。Linux提供了/dev/watchdog设备节点,用户程序只需要定期往里面写数据就行:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    int fd = open("/dev/watchdog", O_WRONLY);
    if (fd < 0) {
        perror("open watchdog failed");
        return -1;
    }

    while (1) {
        /* 写入任意字符即可踢狗 */
        write(fd, "K", 1);
        sleep(30);  /* 每30秒踢一次,超时60秒 */
    }

    close(fd);
    return 0;
}

注意sleep时间要小于超时时间的一半。我一般设超时60秒,踢狗间隔20秒,留足余量。曾经有个项目,踢狗线程被高优先级任务抢占,导致看门狗误复位。后来我把踢狗线程优先级提到最高,才解决了问题。

4.6 调试技巧:别让看门狗咬到你

开发阶段,看门狗就是个麻烦精。我分享几个实用技巧:

  • uboot阶段关闭看门狗:在uboot里执行 wdog disable,或者在内核启动参数加 nowatchdog
  • 使用软链接控制开关echo V > /dev/watchdog 可以关闭看门狗(需要驱动支持)。
  • 模拟超时测试:把超时时间设成5秒,然后停止踢狗,观察复位行为是否符合预期。
  • 查看复位原因:i.MX8的SRC寄存器可以读出上次复位源,判断是看门狗复位还是上电复位。
💡 个人经验: 调试时我习惯在驱动里加一个debugfs接口,可以动态开关看门狗、修改超时时间、手动触发复位。这样不用反复编译内核,调试效率高很多。

4.7 总结:硬件看门狗是最后一道防线

硬件看门狗驱动开发,说白了就是三件事:设备树配好、驱动写对、用户空间踢狗。但真正难的是系统层面的配合——中断优先级、任务调度、电源管理,任何一个环节出问题,看门狗都可能误动作或者失效。

我见过太多项目,看门狗驱动写得没问题,但系统在低功耗模式下把看门狗时钟关了,导致复位失效。所以,写完驱动后一定要做完整的压力测试:模拟死锁、模拟内存泄漏、模拟中断风暴,确保看门狗在各种异常场景下都能可靠工作。

嗯,这一章就到这里。下一章咱们聊聊多核架构下的看门狗设计,那个更刺激。