第4章 驱动初始化流程:probe函数解析、中断注册、通道初始化、设备树解析
驱动初始化,说白了就是让内核认识你的硬件。我见过不少工程师,设备树配得花里胡哨,probe函数却写成一团浆糊。结果呢?驱动加载了,硬件没反应。今天咱们就把这四块内容拆开揉碎了讲。
4.1 probe函数——驱动的入口
probe函数什么时候被调用?当设备树里的compatible属性和驱动里的of_match_table匹配上时,内核就会调用你的probe。嗯,这里要注意:匹配成功不代表硬件就绪,你还要在probe里做一系列初始化。
我个人的习惯是,probe函数里只做三件事:
- 解析设备树——拿到寄存器地址、中断号、通道配置
- 申请资源——ioremap、request_irq、分配内存
- 注册设备——misc_register或platform_device_add
别把业务逻辑塞进probe。我曾经见过有人把数据收发也写在probe里,结果驱动加载卡死,整个系统起不来。probe要快,要轻量。
核心原则:probe函数只做初始化,不做业务。业务逻辑放到open/read/write/ioctl里。
4.2 设备树解析——从DTS到内核数据
设备树解析是驱动初始化的第一步。你想想看,内核怎么知道你的Mailbox有几个通道?中断号是多少?全靠设备树。
常用的API就这几个:
| API | 作用 | 返回值 |
|---|---|---|
of_property_read_u32 |
读取32位整数属性 | 0成功,负值失败 |
of_property_read_u32_array |
读取整数数组 | 0成功,负值失败 |
of_iomap |
映射寄存器地址 | 虚拟地址指针 |
irq_of_parse_and_map |
解析中断并映射 | 中断号 |
举个例子,设备树里这样配:
mailbox: mailbox@10000000 {
compatible = "vendor,mailbox-v1";
reg = <0x10000000 0x1000>;
interrupts = <0 55 4>;
channels = <4>;
#mbox-cells = <1>;
};
probe里解析代码:
static int mailbox_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct mailbox_priv *priv;
struct resource *res;
int ret;
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
// 解析寄存器地址
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
priv->base = devm_ioremap_resource(dev, res);
if (IS_ERR(priv->base))
return PTR_ERR(priv->base);
// 解析中断
priv->irq = platform_get_irq(pdev, 0);
if (priv->irq < 0)
return priv->irq;
// 解析通道数
ret = of_property_read_u32(dev->of_node, "channels", &priv->num_chans);
if (ret) {
dev_err(dev, "failed to get channels property\n");
return ret;
}
// 后续初始化...
return 0;
}
避坑:我曾经遇到过一个坑——设备树里reg属性写了两个地址段,但platform_get_resource只取了第一个。结果第二个地址段没映射,访问时直接oops。记得检查resource数量。
4.3 中断注册——别让中断处理成为瓶颈
Mailbox驱动里,中断是核心通信机制。发送方写完数据,触发中断通知接收方。接收方在中断处理函数里读数据、清标志。
中断注册的API:
ret = devm_request_irq(dev, priv->irq, mailbox_isr,
IRQF_TRIGGER_RISING, "mailbox", priv);
if (ret) {
dev_err(dev, "failed to request irq %d\n", priv->irq);
return ret;
}
中断处理函数怎么写?我建议遵循三个原则:
- 快进快出——只做必要的寄存器读写,别做耗时操作
- 区分通道——多个通道共用同一个中断线,要判断哪个通道触发了
- 唤醒等待队列——通知上层有数据来了
举个例子:
static irqreturn_t mailbox_isr(int irq, void *dev_id)
{
struct mailbox_priv *priv = dev_id;
u32 status;
int i;
status = readl(priv->base + MAILBOX_STATUS);
if (!status)
return IRQ_NONE;
for (i = 0; i < priv->num_chans; i++) {
if (status & (1 << i)) {
// 读取该通道数据
priv->chan[i].data = readl(priv->base + MAILBOX_DATA(i));
// 清中断标志
writel(1 << i, priv->base + MAILBOX_CLEAR);
// 唤醒等待队列
wake_up_interruptible(&priv->chan[i].waitq);
}
}
return IRQ_HANDLED;
}
经验之谈:中断处理函数里不要用printk。我曾经在调试时加了一堆printk,结果中断频繁触发时系统直接hang住。用tracepoint或者dev_dbg代替。
4.4 通道初始化——每个通道都是独立的通信管道
Mailbox通常有多个通道,每个通道可以独立收发数据。通道初始化要做什么?
- 分配通道数据结构——每个通道有自己的状态、数据缓冲区、等待队列
- 初始化同步机制——spinlock或mutex,防止并发访问
- 初始化等待队列——用于阻塞式读取
- 设置通道回调——如果有上层协议,注册回调函数
代码示例:
for (i = 0; i < priv->num_chans; i++) {
priv->chan[i].id = i;
priv->chan[i].priv = priv;
spin_lock_init(&priv->chan[i].lock);
init_waitqueue_head(&priv->chan[i].waitq);
priv->chan[i].rx_callback = NULL;
priv->chan[i].tx_done = false;
}
你想想看,如果通道初始化没做好,两个进程同时往一个通道写数据,会发生什么?数据错乱、死锁、甚至内核panic。所以锁的初始化一定要到位。
4.5 整体初始化流程图
下面这张图把整个初始化流程串起来了:
4.6 初始化过程中的常见问题
我把这些年遇到的初始化问题整理了一下:
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| probe没被调用 | compatible不匹配 | 检查设备树和驱动里的字符串是否一致 |
| ioremap失败 | 地址段冲突或越界 | 查看/proc/iomem确认地址范围 |
| 中断注册失败 | 中断号无效或已被占用 | cat /proc/interrupts查看中断使用情况 |
| 通道初始化后无法通信 | 通道数据结构未正确初始化 | 检查锁、等待队列、回调函数指针 |
重要提醒:初始化顺序不能乱。先解析设备树,再申请资源,然后注册中断,最后初始化通道。我曾经试过先注册中断再ioremap,结果中断触发时寄存器还没映射,直接访问非法地址。