4、Linux内核Mailbox驱动框架:控制器驱动、客户端驱动、DTS配置与注册流程
好,咱们进入第四章。这一章我打算把Linux内核里Mailbox驱动的骨架给你拆开看看。说白了,就是搞清楚三件事:控制器驱动怎么写、客户端驱动怎么挂、设备树怎么配。这三样东西拧在一起,才能让两个核真正聊起来。
我记得刚接触Mailbox框架时,翻内核代码翻得头晕。后来发现,其实内核已经把套路定好了——你照着填就行。今天我就带你走一遍这个套路。
4.1 整体框架:谁管发,谁管收?
先看一张图,把角色理清楚。
从图上你能看到:控制器驱动在底层,直接操作寄存器;客户端驱动在上层,调用API发消息。中间通过内核提供的mbox API连接。RPMsg层是可选的——如果你只想裸用Mailbox,完全可以跳过它。
4.2 Mailbox控制器驱动:管通道的人
控制器驱动是真正跟硬件打交道的部分。它的核心数据结构是 struct mbox_controller。
struct mbox_controller {
struct device *dev;
const struct mbox_chan_ops *ops;
struct mbox_chan *chans;
int num_chans;
bool txdone_poll;
unsigned int txpoll_period;
struct list_head node;
};
这里我重点说几个字段:
- ops:操作函数集,必须实现。包括
send_data、startup、shutdown、last_tx_done等。 - chans:通道数组。每个通道对应一个硬件Mailbox实例。
- txdone_poll:是否用轮询方式检测发送完成。我建议能用中断就别用轮询,除非硬件实在不支持。
关键点:控制器驱动要做的事其实就三件——初始化硬件、提供收发接口、管理中断。别想复杂了。
4.2.1 操作函数集 mbox_chan_ops
这是控制器驱动的灵魂。每个函数都对应一个硬件操作。
| 函数 | 作用 | 我踩过的坑 |
|---|---|---|
send_data |
把数据写入硬件FIFO或寄存器 | 记得检查FIFO是否满,否则丢数据 |
startup |
打开通道,使能中断 | 别忘了清空遗留的中断标志 |
shutdown |
关闭通道,屏蔽中断 | 要等正在发送的报文完成 |
last_tx_done |
轮询模式下检查发送是否完成 | 轮询周期别设太短,浪费CPU |
我曾经在一个项目里,因为 startup 里没清中断标志,导致一打开通道就触发假中断。查了两天才找到原因——嗯,从那以后我写 startup 第一行一定是清中断。
4.3 Mailbox客户端驱动:用通道的人
客户端驱动相对简单。它不需要管硬件细节,只需要向控制器驱动申请一个通道,然后发消息。
核心数据结构是 struct mbox_client:
struct mbox_client {
struct device *dev;
bool tx_block;
unsigned long tx_tout;
bool knows_txdone;
void (*rx_callback)(struct mbox_client *cl, void *mssg);
void (*tx_prepare)(struct mbox_client *cl, void *mssg);
void (*tx_done)(struct mbox_client *cl, void *mssg, int r);
};
这里我提醒你注意几个回调:
- rx_callback:收到消息时触发。这是客户端唯一必须实现的回调。
- tx_prepare:发送前准备数据。如果你需要填充头部或校验,放这里。
- tx_done:发送完成通知。可以用来释放缓冲区或唤醒等待队列。
我的习惯:tx_block 我一般设成 true,配合 tx_tout 设置超时。这样发送时如果通道忙,内核会自动帮你等待,不用自己写重试逻辑。
4.4 DTS设备树配置:把硬件描述给内核
设备树是Linux下描述硬件的主要方式。Mailbox的DTS配置分两部分:控制器节点和客户端节点。
4.4.1 控制器节点示例
mailbox: mailbox@30000000 {
compatible = "vendor,mailbox-ip";
reg = <0x30000000 0x1000>;
interrupts = <0 42 4>;
#mbox-cells = <1>;
clocks = <&clkc 15>;
};
这里 #mbox-cells = <1> 表示客户端引用时需要1个参数,通常是通道编号。
4.4.2 客户端节点示例
rproc: remoteproc@40000000 {
compatible = "vendor,remote-processor";
reg = <0x40000000 0x10000>;
mboxes = <&mailbox 0>, <&mailbox 1>;
mbox-names = "tx", "rx";
};
mboxes 属性指定了使用的Mailbox控制器和通道号。mbox-names 是别名,驱动里可以用 of_parse_phandle 按名字获取。
注意:#mbox-cells 的值必须和控制器驱动里解析的参数个数一致。我曾经配错过,导致内核解析通道号时读到垃圾数据,系统直接panic。
4.5 驱动注册流程:从probe到通道就绪
整个注册流程,我习惯把它分成5步。你照着这个顺序写,基本不会出错。
- 控制器驱动probe:读取DTS,获取寄存器地址、中断号、时钟等资源。
- 初始化通道:分配
mbox_chan数组,每个通道绑定一个硬件实例。 - 注册控制器:调用
mbox_controller_register(),把控制器加入内核的mbox总线。 - 客户端驱动probe:通过
mbox_request_channel()申请通道,传入mbox_client结构体。 - 开始通信:调用
mbox_send_message()发送数据,通过rx_callback接收数据。
这里我贴一段简化的控制器probe代码,帮你理解:
static int my_mbox_probe(struct platform_device *pdev)
{
struct mbox_controller *mbox;
struct resource *res;
mbox = devm_kzalloc(&pdev->dev, sizeof(*mbox), GFP_KERNEL);
// 1. 获取硬件资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
// 2. 初始化通道
mbox->num_chans = 2;
mbox->chans = devm_kcalloc(...);
// 3. 绑定操作函数
mbox->ops = &my_mbox_ops;
// 4. 注册
return mbox_controller_register(mbox);
}
核心流程:控制器先注册,客户端后申请。顺序不能乱。如果客户端先probe,它找不到控制器,会返回 -EPROBE_DEFER,内核后面会重试。
4.6 避坑指南:我踩过的三个坑
做Mailbox驱动这几年,我踩过不少坑。挑三个最典型的跟你说说。
- 坑一:中断上下文不能睡眠。在
rx_callback里,你是在中断上下文。别调用msleep或mutex_lock,否则内核会报scheduling while atomic。用workqueue把耗时操作推出去。 - 坑二:通道引用计数。客户端调用
mbox_request_channel会增加通道的引用计数。如果客户端卸载时没调用mbox_free_channel,通道就永远被占着。我见过一个产品因为这个原因,热插拔几次后所有通道都用完了。 - 坑三:DTS中mbox-cells不匹配。控制器声明
#mbox-cells = <2>,客户端只传了一个参数,内核解析时直接越界。这种错误编译时检查不出来,运行时才崩溃。
调试技巧:打开内核的 CONFIG_MAILBOX 和 CONFIG_DEBUG_MAILBOX,能看到每个通道的申请/释放日志。我排查问题时第一件事就是开这个。