驱动初始化流程:platform driver模型、设备树解析、硬件资源映射

好,咱们进入第三章。这一章我打算聊聊驱动初始化那点事。说白了,就是你的Mailbox驱动从被内核加载,到真正能干活,中间到底经历了什么。

很多刚接触Mailbox的工程师,上来就盯着中断处理、消息收发这些“热闹”的地方看。但我个人的习惯是,先看初始化。初始化要是没搞对,后面全是白搭。你想想看,寄存器地址都映射错了,你发消息发给谁?

3.1 platform driver模型:驱动和设备是怎么“配对”的

Mailbox驱动在Linux里,通常是以platform driver的形式存在的。为什么?因为Mailbox控制器一般都在SoC内部,挂载在AMBA总线上,但Linux内核会把它们抽象成platform设备。

驱动和设备是怎么“看对眼”的?靠的是of_match_table。内核在启动时,会遍历设备树中所有的节点,然后拿着节点的compatible属性,去匹配驱动的of_match_table。匹配上了,就调用驱动的probe函数。

核心流程:

  • 内核启动,解析设备树,生成platform_device
  • 驱动注册时,platform_driver_register() 将驱动加入内核
  • 内核遍历已注册的设备,匹配compatible属性
  • 匹配成功 → 调用 probe()
  • 匹配失败 → 驱动“睡大觉”,等设备热插拔

我曾经在一个项目里遇到过,设备树里compatible写的是"arm,mhu",驱动里写的是"arm, mhu"——多了一个空格。就这一个空格,驱动死活不干活。排查了整整一天。嗯,从那以后,我每次写compatible都会反复检查空格。

3.2 设备树解析:从DTS到驱动能用的数据

设备树解析,是probe函数里的第一道硬菜。你要从设备树节点里,把Mailbox需要的各种信息读出来。

我个人习惯把设备树解析分成三步:

  1. 获取设备节点:通常probe函数会直接拿到struct platform_device指针,里面就有dev.of_node。
  2. 读取reg属性:获取寄存器基地址和范围。
  3. 读取中断属性:获取中断号、中断触发类型。
  4. 读取自定义属性:比如通道数量、时钟频率、电源域等。

这里我贴一段典型的解析代码,大家感受一下:

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

    mbox = devm_kzalloc(dev, sizeof(*mbox), GFP_KERNEL);
    if (!mbox)
        return -ENOMEM;

    // 1. 获取寄存器资源
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res) {
        dev_err(dev, "failed to get MEM resource\n");
        return -ENODEV;
    }

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

    // 3. 获取中断号
    mbox->irq = platform_get_irq(pdev, 0);
    if (mbox->irq < 0) {
        dev_err(dev, "failed to get IRQ\n");
        return mbox->irq;
    }

    // 4. 读取自定义属性:通道数量
    ret = device_property_read_u32(dev, "mbox-channels", &mbox->num_chans);
    if (ret) {
        dev_warn(dev, "use default channel count: 1\n");
        mbox->num_chans = 1;
    }

    // ... 后续初始化
    return 0;
}

小技巧: 我建议你多用 device_property_read_* 系列函数,而不是直接操作of_*函数。前者兼容了设备树和ACPI,代码更通用。当然,如果你确定只用设备树,of_*也没问题。

3.3 硬件资源映射:把物理地址变成虚拟地址

解析完设备树,拿到了物理地址,接下来就是映射。这一步很关键,因为CPU不能直接访问物理地址,必须通过MMU转换成虚拟地址。

Mailbox驱动里,最常用的映射函数是devm_ioremap_resource。它比老式的ioremap_nocache多了资源管理和错误检查。

函数 特点 推荐场景
ioremap 基础映射,需手动管理 不推荐,容易漏掉释放
devm_ioremap 设备管理生命周期,自动释放 简单场景
devm_ioremap_resource 带资源检查和冲突检测 强烈推荐
of_iomap 直接从设备树节点映射 非platform驱动场景

为什么我推荐devm_ioremap_resource?因为它会帮你检查资源是否已经被其他驱动占用了。我在一个项目里遇到过,两个驱动不小心用了同一段寄存器地址,结果一个写数据,另一个读到的全是乱码。用devm_ioremap_resource,这种问题在probe阶段就能暴露出来。

注意: Mailbox寄存器通常要求强序访问。映射完成后,记得检查一下寄存器地址是否被正确映射。我习惯在probe里加一段验证代码:读一个已知值的寄存器,比如版本号寄存器,确认读到的值符合预期。这一步能帮你快速发现映射问题。

3.4 整体初始化流程图

说了这么多,咱们用一张图把整个流程串起来。这张图是我自己画的一个标准Mailbox驱动初始化流程,大家可以对照着看。

Mailbox驱动初始化流程图 内核启动 解析设备树,生成platform_device platform_driver_register 匹配 compatible 属性 调用 probe() 函数 解析 reg / interrupts / 自定义属性 devm_ioremap_resource 映射 关键检查点 1. compatible 是否匹配 2. reg 资源是否冲突 3. 中断号是否有效 常见失败原因 • 设备树节点缺失 • 地址范围重叠

3.5 避坑指南:我踩过的几个坑

最后,分享几个我在实际项目中遇到的初始化问题,希望能帮大家少走弯路。

坑1:设备树中reg属性写错了地址或长度

我曾经在一个项目里,设备树中reg写的是0x4000_0000 0x1000,但实际Mailbox寄存器范围是0x4000_0000到0x4000_0FFF。长度写成了0x1000,但实际只需要0x100就够了。虽然没出大问题,但浪费了映射空间。更严重的是,如果长度写小了,访问寄存器时就会触发page fault。

坑2:中断号配置错误

Mailbox通常有多个中断源(发送完成、接收到达、错误等)。设备树里interrupts属性如果顺序写错了,驱动里拿到的中断号就会张冠李戴。我建议你在probe里把中断号打印出来,和芯片手册对照一下。

调试小技巧: 在probe函数里多加几个dev_dbg()打印。比如打印映射后的虚拟地址、中断号、通道数量。这样驱动加载时,通过dmesg就能看到关键信息。我习惯在probe成功和失败的地方都加打印,方便定位问题。

嗯,关于驱动初始化,今天就聊这么多。核心就是三件事:配对、解析、映射。把这三步走稳了,Mailbox驱动的底子就打好了。后面收发消息、中断处理,都是在这个基础上搭起来的。