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函数的完整流程:
这张图把probe的7个步骤串起来了。每个步骤之间都有箭头,表示严格的顺序依赖。比如,你必须在映射寄存器之后才能解析设备树中的寄存器相关属性,必须在注册中断之后才能初始化通道(因为通道初始化可能涉及中断使能)。
我的经验:调试probe函数时,在每一步后面加dev_dbg打印。这样通过dmesg就能看到驱动停在了哪一步。我曾经靠这个办法,五分钟就定位到一个设备树地址写错的问题。
好了,probe函数的四个核心部分——设备树解析、中断注册、通道初始化、整体流程——都讲完了。记住一个原则:probe要像搭积木一样,一块一块搭,每搭一块都检查稳不稳。这样你的Mailbox驱动才能在各种硬件平台上稳定运行。