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:默认超时时间,单位秒。这个属性不是必须的,但我建议加上。为什么?因为不同板子对超时时间要求不一样,写在设备树里,方便板级维护人员调整,不用改驱动代码。
4.2 驱动 probe 流程:从匹配到初始化
设备树配好了,内核怎么找到你的驱动?靠的是 platform_driver 结构体。看门狗驱动通常注册为 platform 驱动。probe 函数是驱动的入口,也是你初始化硬件的地方。
一个典型的 probe 流程,我总结为三步:
- 解析设备树资源:获取寄存器地址、中断号、时钟等。
- 初始化硬件:复位看门狗、设置超时时间、注册中断等。
- 注册 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 让用户控制启动时机。
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;
}
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 秒,调试时一头雾水。
4.4 总结
看门狗驱动开发,核心就三点:
- 设备树配置:compatible、reg、interrupts、timeout-sec 是关键。
- probe 流程:解析资源、初始化硬件、注册 misc 设备。记住:probe 时不要启动看门狗。
- open/release/ioctl:open 时启动,release 时不要停止,ioctl 实现喂狗和配置。
嗯,这些就是看门狗驱动开发的核心内容。下一章,咱们聊聊电源管理驱动中的 regulator 框架,那个也挺有意思的。