4. 驱动初始化流程:probe函数详解、设备树解析、中断注册、通道初始化

驱动初始化,说白了就是让内核认识你的硬件,并把它准备好干活。probe函数是这一切的起点。我刚开始写Mailbox驱动时,总觉得probe就是“注册一下、分配内存、完事”。后来踩了几个坑才明白——probe里每一步的顺序、每个资源的检查,都直接决定了驱动能不能稳定跑起来。

4.1 probe函数:驱动的“入场券”

probe函数什么时候被调用?当设备树中描述的硬件设备和驱动匹配成功时,内核就会调用你的probe。它的原型长这样:

static int mailbox_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct mailbox_device *mdev;
    struct resource *res;
    int ret;

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

    // 2. 映射寄存器地址
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    mdev->base = devm_ioremap_resource(dev, res);
    if (IS_ERR(mdev->base))
        return PTR_ERR(mdev->base);

    // 3. 解析设备树
    ret = mailbox_parse_dt(mdev, pdev);
    if (ret)
        return ret;

    // 4. 注册中断
    ret = mailbox_request_irq(mdev, pdev);
    if (ret)
        return ret;

    // 5. 初始化通道
    ret = mailbox_channels_init(mdev);
    if (ret)
        return ret;

    // 6. 注册Mailbox框架
    ret = devm_mailbox_controller_register(dev, &mdev->mbox);
    if (ret)
        return ret;

    dev_info(dev, "Mailbox driver initialized successfully\n");
    return 0;
}

嗯,这里要注意:每一步都可能失败。我见过不少驱动,probe里一口气做完所有事,失败时也不回滚——结果系统里留下半残的设备,后面一操作就panic。

核心原则:probe函数必须支持“干净地失败”。用devm_系列API可以省去手动释放资源的麻烦,但中断、通道状态这些还是得自己管。

4.2 设备树解析:让驱动认识硬件

设备树是驱动和硬件之间的“翻译官”。我个人习惯在probe一开始就把设备树信息全部解析出来,存到私有结构体里。这样后面所有函数都能直接访问,不用到处翻设备树节点。

常见的解析内容包括:

设备树属性 解析函数 说明
reg platform_get_resource 获取寄存器基地址和范围
interrupts platform_get_irq 获取中断号
mbox-cells of_property_read_u32 获取通道数量
clocks devm_clk_get 获取时钟源
mbox-channels of_property_count_u32_elems 自定义通道配置

举个例子,设备树里可能会这样描述:

mailbox: mailbox@f0000000 {
    compatible = "vendor,mailbox-v1";
    reg = <0x0 0xf0000000 0x0 0x1000>;
    interrupts = <0 42 4>;
    #mbox-cells = <1>;
    clocks = <&clkc 15>;
    mbox-channels = <0 1 2 3>;
};

解析时,我建议每个属性都检查返回值。曾经有个项目,设备树里漏写了clocks属性,驱动没检查直接往下走——结果时钟没使能,寄存器读写全返回0,排查了两天才找到原因。

避坑指南:解析设备树时,用of_property_read_bool检查可选属性是否存在,用dev_err打印缺失信息。这样调试时一眼就能看出是设备树配错了。

4.3 中断注册:别让CPU空转

Mailbox的核心是异步通信,中断就是那个“通知你收信”的信号。注册中断时,我一般这样写:

static int mailbox_request_irq(struct mailbox_device *mdev,
                               struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    int irq, ret;

    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    ret = devm_request_irq(dev, irq, mailbox_isr,
                           IRQF_TRIGGER_RISING | IRQF_SHARED,
                           "mailbox", mdev);
    if (ret) {
        dev_err(dev, "Failed to request IRQ %d\n", irq);
        return ret;
    }

    mdev->irq = irq;
    return 0;
}

这里有几个关键点:

  • 中断触发方式:Mailbox硬件通常支持边沿触发或电平触发。我建议用IRQF_TRIGGER_RISING(上升沿),配合硬件清中断机制,不容易丢中断。
  • 共享中断:有些SoC里多个Mailbox共用一根中断线,必须加IRQF_SHARED标志。否则中断注册会失败。
  • 中断处理函数:要快!我一般在ISR里只做两件事——读状态寄存器、唤醒等待队列。具体的数据拷贝放到工作队列或tasklet里做。

注意:中断处理函数里绝对不能调用mailbox_send_data这类可能引起递归调用的函数。我曾经这么干过,结果中断风暴直接把系统打挂了。

4.4 通道初始化:给每个“信箱”编号

通道就是Mailbox里的独立通信链路。初始化时,需要为每个通道分配资源、设置状态、绑定回调函数。我习惯用一个循环搞定:

static int mailbox_channels_init(struct mailbox_device *mdev)
{
    int i;

    for (i = 0; i < mdev->num_chans; i++) {
        struct mailbox_chan *chan = &mdev->chans[i];

        // 初始化自旋锁
        spin_lock_init(&chan->lock);

        // 初始化等待队列
        init_waitqueue_head(&chan->wait);

        // 设置通道状态为空闲
        chan->state = CHAN_STATE_IDLE;

        // 分配发送/接收缓冲区
        chan->tx_buf = devm_kzalloc(mdev->dev, mdev->msg_size, GFP_KERNEL);
        chan->rx_buf = devm_kzalloc(mdev->dev, mdev->msg_size, GFP_KERNEL);
        if (!chan->tx_buf || !chan->rx_buf)
            return -ENOMEM;

        // 注册到Mailbox框架
        mdev->mbox.chans[i] = chan;
    }

    return 0;
}

通道状态机一般包括:

  • IDLE:空闲,可以发送
  • PENDING:消息已提交,等待对方确认
  • BUSY:正在传输中
  • ERROR:传输出错

为什么要维护状态?想想看——如果上层应用连续发送两条消息,而硬件一次只能处理一条,没有状态管理就会导致数据覆盖。我在一个项目里就遇到过这个问题,最后加了个PENDING状态和超时机制才解决。

4.5 整体流程一览

下面这张图展示了probe函数的完整流程:

Mailbox驱动probe函数初始化流程 probe函数入口 分配私有数据结构 映射寄存器地址 解析设备树属性 注册中断处理函数 初始化所有通道 注册Mailbox框架 关键检查点 • 每一步都检查返回值 • 失败时回滚已分配资源 • 中断ISR要轻量快速 • 通道状态机必须完整 • 设备树属性不能遗漏

这张图把probe的7个步骤串起来了。每个步骤之间都有箭头,表示严格的顺序依赖。比如,你必须在映射寄存器之后才能解析设备树中的寄存器相关属性,必须在注册中断之后才能初始化通道(因为通道初始化可能涉及中断使能)。

我的经验:调试probe函数时,在每一步后面加dev_dbg打印。这样通过dmesg就能看到驱动停在了哪一步。我曾经靠这个办法,五分钟就定位到一个设备树地址写错的问题。

好了,probe函数的四个核心部分——设备树解析、中断注册、通道初始化、整体流程——都讲完了。记住一个原则:probe要像搭积木一样,一块一块搭,每搭一块都检查稳不稳。这样你的Mailbox驱动才能在各种硬件平台上稳定运行。