第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通常有多个通道,每个通道可以独立收发数据。通道初始化要做什么?

  1. 分配通道数据结构——每个通道有自己的状态、数据缓冲区、等待队列
  2. 初始化同步机制——spinlock或mutex,防止并发访问
  3. 初始化等待队列——用于阻塞式读取
  4. 设置通道回调——如果有上层协议,注册回调函数

代码示例:

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 整体初始化流程图

下面这张图把整个初始化流程串起来了:

Mailbox驱动初始化流程图 Mailbox驱动初始化流程图 probe函数入口 设备树解析 资源申请(ioremap) 中断注册(request_irq) 通道初始化 注册设备(完成) 关键检查点 1. compatible是否匹配 2. reg地址段数量 3. 中断号是否有效 4. 通道数是否合理 常见失败原因 • 设备树属性缺失 • 中断号冲突 • 内存申请失败

4.6 初始化过程中的常见问题

我把这些年遇到的初始化问题整理了一下:

问题现象 可能原因 排查方法
probe没被调用 compatible不匹配 检查设备树和驱动里的字符串是否一致
ioremap失败 地址段冲突或越界 查看/proc/iomem确认地址范围
中断注册失败 中断号无效或已被占用 cat /proc/interrupts查看中断使用情况
通道初始化后无法通信 通道数据结构未正确初始化 检查锁、等待队列、回调函数指针

重要提醒:初始化顺序不能乱。先解析设备树,再申请资源,然后注册中断,最后初始化通道。我曾经试过先注册中断再ioremap,结果中断触发时寄存器还没映射,直接访问非法地址。