4、看门狗驱动开发实战:设备树配置、驱动probe流程、open/release/ioctl实现

好,咱们直接进入正题。看门狗驱动,说白了就是给系统上个“保险”。你系统跑飞了,它帮你复位。但驱动怎么写?设备树怎么配?probe 流程怎么走?这些才是硬骨头。我当年第一次写看门狗驱动时,就踩过不少坑,今天咱们一个一个说清楚。

4.1 设备树配置:让内核认识你的看门狗

设备树,就是硬件的“身份证”。内核启动时,通过它来匹配驱动。看门狗设备树节点,通常长这样:

watchdog: watchdog@f0000000 {
    compatible = "vendor,my-wdt";
    reg = <0x0 0xf0000000 0x0 0x1000>;
    interrupts = <0 42 4>;
    clocks = <&wdt_clk>;
    timeout-sec = <60>;
    status = "okay";
};

这里有几个关键点,我重点说一下:

  • compatible:驱动匹配的关键字符串。我习惯写成 "厂商,设备名" 的格式,比如 "ti,omap-wdt"。注意,这个字符串必须和驱动里的 of_match_table 完全一致,大小写都不能错。我曾经因为一个字母大小写问题,排查了整整半天。
  • reg:寄存器基地址和范围。看门狗通常有一组控制寄存器,你需要知道它们在哪。
  • interrupts:看门狗超时中断。有些看门狗支持“提前通知”功能,超时前先触发一个中断,让系统有机会“喂狗”。这个中断号要跟硬件手册对得上。
  • timeout-sec:默认超时时间,单位秒。这个属性不是必须的,但我建议加上。为什么?因为不同板子对超时时间要求不一样,写在设备树里,方便板级维护人员调整,不用改驱动代码。
小技巧: 设备树里还可以加一些自定义属性,比如 "reset-on-timeout" 表示超时后是否复位。我个人习惯把这类策略性的配置都放在设备树里,驱动只负责解析和执行。这样驱动更通用,维护起来也方便。

4.2 驱动 probe 流程:从匹配到初始化

设备树配好了,内核怎么找到你的驱动?靠的是 platform_driver 结构体。看门狗驱动通常注册为 platform 驱动。probe 函数是驱动的入口,也是你初始化硬件的地方。

一个典型的 probe 流程,我总结为三步:

  1. 解析设备树资源:获取寄存器地址、中断号、时钟等。
  2. 初始化硬件:复位看门狗、设置超时时间、注册中断等。
  3. 注册 misc 设备:让用户空间可以通过 /dev/watchdog 访问。

来看代码:

static int my_wdt_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct my_wdt *wdt;
    struct resource *res;
    int ret;

    // 1. 分配私有数据结构
    wdt = devm_kzalloc(dev, sizeof(*wdt), GFP_KERNEL);
    if (!wdt)
        return -ENOMEM;

    // 2. 获取寄存器基地址
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    wdt->base = devm_ioremap_resource(dev, res);
    if (IS_ERR(wdt->base))
        return PTR_ERR(wdt->base);

    // 3. 获取中断(如果有)
    wdt->irq = platform_get_irq(pdev, 0);
    if (wdt->irq > 0) {
        ret = devm_request_irq(dev, wdt->irq, my_wdt_isr, 0,
                               pdev->name, wdt);
        if (ret)
            return ret;
    }

    // 4. 获取超时时间(设备树或默认值)
    wdt->timeout = WDT_DEFAULT_TIMEOUT;
    device_property_read_u32(dev, "timeout-sec", &wdt->timeout);

    // 5. 初始化硬件(复位看门狗,设置初始超时)
    my_wdt_hw_init(wdt);

    // 6. 注册 misc 设备
    wdt->miscdev.minor = MISC_DYNAMIC_MINOR;
    wdt->miscdev.name = "watchdog";
    wdt->miscdev.fops = &my_wdt_fops;
    wdt->miscdev.parent = dev;
    ret = misc_register(&wdt->miscdev);
    if (ret) {
        dev_err(dev, "cannot register misc device\n");
        return ret;
    }

    platform_set_drvdata(pdev, wdt);
    dev_info(dev, "watchdog probed, timeout=%d sec\n", wdt->timeout);
    return 0;
}

嗯,这里要注意一点:probe 里不要直接启动看门狗。为什么?因为驱动刚加载,用户空间还没准备好。如果一 probe 就启动看门狗,用户空间来不及喂狗,系统就复位了。我建议在 open 时才启动,或者提供一个 ioctl 让用户控制启动时机。

避坑指南: 我曾经在一个项目里,probe 时直接启动了看门狗,结果系统启动到一半就反复复位。排查了好久才发现是看门狗在“捣乱”。从那以后,我坚持“probe 只初始化,不启动”的原则。

4.3 open/release/ioctl 实现:用户空间的接口

驱动写好了,用户空间怎么用?通过文件操作接口。看门狗驱动通常实现三个核心操作:open、release 和 ioctl。

4.3.1 open:启动看门狗

open 函数,说白了就是“开门放狗”。用户空间打开 /dev/watchdog 时,驱动应该启动看门狗计时器。

static int my_wdt_open(struct inode *inode, struct file *file)
{
    struct my_wdt *wdt = container_of(inode->i_cdev,
                                      struct my_wdt, cdev);

    // 确保只有一个进程打开
    if (test_and_set_bit(WDT_FLAG_OPEN, &wdt->flags))
        return -EBUSY;

    file->private_data = wdt;

    // 启动看门狗硬件
    my_wdt_start(wdt);

    // 设置超时时间
    my_wdt_set_timeout(wdt, wdt->timeout);

    return nonseekable_open(inode, file);
}

这里有个细节:互斥访问。看门狗设备通常只允许一个进程打开。我用一个标志位 WDT_FLAG_OPEN 来保证。如果多个进程同时打开,返回 -EBUSY。你想想看,如果两个进程同时喂狗,一个喂一个不喂,系统该听谁的?

4.3.2 release:停止还是继续?

release 函数,是用户空间关闭设备时调用的。但看门狗的 release 有个特殊之处:要不要停止看门狗?

我的建议是:不要停止。为什么?因为用户空间程序可能崩溃了,来不及喂狗。如果 release 时停止看门狗,那看门狗就失去了保护作用。正确的做法是:release 时不清除标志位,让看门狗继续运行。如果用户空间程序正常退出,它应该在退出前通过 ioctl 停止看门狗。

static int my_wdt_release(struct inode *inode, struct file *file)
{
    struct my_wdt *wdt = file->private_data;

    // 不清除 OPEN 标志,看门狗继续运行
    // 除非用户通过 ioctl 明确要求停止

    return 0;
}
重要: 很多新手在这里犯错。release 时停止看门狗,会导致系统失去保护。记住:看门狗一旦启动,除非用户明确要求,否则不要停止。

4.3.3 ioctl:喂狗与配置

ioctl 是看门狗驱动的核心接口。用户空间通过它来喂狗、设置超时时间、获取状态等。Linux 内核定义了标准的看门狗 ioctl 命令,在 linux/watchdog.h 中。

常用的 ioctl 命令:

命令 功能 说明
WDIOC_KEEPALIVE 喂狗 重置看门狗计时器
WDIOC_SETTIMEOUT 设置超时时间 传入 int 类型秒数
WDIOC_GETTIMEOUT 获取当前超时时间 返回 int 类型秒数
WDIOC_GETTIMELEFT 获取剩余时间 返回 int 类型秒数
WDIOC_SETOPTIONS 设置选项 如 WDIOS_DISABLECARD 停止看门狗

来看实现:

static long my_wdt_ioctl(struct file *file, unsigned int cmd,
                         unsigned long arg)
{
    struct my_wdt *wdt = file->private_data;
    int timeout, ret = 0;

    switch (cmd) {
    case WDIOC_KEEPALIVE:
        // 喂狗:重置硬件计时器
        my_wdt_ping(wdt);
        break;

    case WDIOC_SETTIMEOUT:
        // 设置超时时间
        if (get_user(timeout, (int __user *)arg))
            return -EFAULT;
        if (timeout < 1 || timeout > wdt->max_timeout)
            return -EINVAL;
        wdt->timeout = timeout;
        my_wdt_set_timeout(wdt, timeout);
        // 回写实际设置的值(硬件可能有精度限制)
        if (put_user(wdt->timeout, (int __user *)arg))
            return -EFAULT;
        break;

    case WDIOC_GETTIMEOUT:
        // 获取当前超时时间
        if (put_user(wdt->timeout, (int __user *)arg))
            return -EFAULT;
        break;

    case WDIOC_GETTIMELEFT:
        // 获取剩余时间(需要硬件支持)
        timeout = my_wdt_get_timeleft(wdt);
        if (timeout < 0)
            return -EOPNOTSUPP;
        if (put_user(timeout, (int __user *)arg))
            return -EFAULT;
        break;

    case WDIOC_SETOPTIONS:
        // 设置选项,如停止看门狗
        if (get_user(timeout, (int __user *)arg))
            return -EFAULT;
        if (timeout & WDIOS_DISABLECARD) {
            my_wdt_stop(wdt);
            clear_bit(WDT_FLAG_OPEN, &wdt->flags);
        }
        if (timeout & WDIOS_ENABLECARD) {
            my_wdt_start(wdt);
            set_bit(WDT_FLAG_OPEN, &wdt->flags);
        }
        break;

    default:
        return -ENOTTY;
    }

    return ret;
}

这里有个细节:WDIOC_SETTIMEOUT 要回写实际值。为什么?因为硬件可能不支持你设置的精确值。比如你设置 30 秒,硬件只能支持 32 秒。你回写 32 秒,用户空间就知道实际超时时间了。我见过不少驱动忽略这一步,导致用户空间以为超时时间是 30 秒,实际却是 32 秒,调试时一头雾水。

个人经验: 喂狗操作(WDIOC_KEEPALIVE)要尽量快。我在一个项目里,喂狗函数里加了调试打印,结果喂狗本身耗时太长,导致看门狗超时。所以,喂狗函数里不要做任何耗时操作,直接写寄存器就行。

4.4 总结

看门狗驱动开发,核心就三点:

  • 设备树配置:compatible、reg、interrupts、timeout-sec 是关键。
  • probe 流程:解析资源、初始化硬件、注册 misc 设备。记住:probe 时不要启动看门狗。
  • open/release/ioctl:open 时启动,release 时不要停止,ioctl 实现喂狗和配置。

嗯,这些就是看门狗驱动开发的核心内容。下一章,咱们聊聊电源管理驱动中的 regulator 框架,那个也挺有意思的。