驱动初始化流程: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需要的各种信息读出来。
我个人习惯把设备树解析分成三步:
- 获取设备节点:通常probe函数会直接拿到struct platform_device指针,里面就有dev.of_node。
- 读取reg属性:获取寄存器基地址和范围。
- 读取中断属性:获取中断号、中断触发类型。
- 读取自定义属性:比如通道数量、时钟频率、电源域等。
这里我贴一段典型的解析代码,大家感受一下:
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驱动初始化流程,大家可以对照着看。
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驱动的底子就打好了。后面收发消息、中断处理,都是在这个基础上搭起来的。