3. Mailbox 驱动框架:Linux 内核 Mailbox 子系统架构、API 接口介绍、设备树绑定
好,我们进入第三章。这一章,咱们把 Linux 内核的 Mailbox 子系统拆开看看。说白了,就是搞清楚内核到底提供了哪些“基础设施”给我们用。
你想想看,一个 SoC 里可能有多个核,或者多个处理器。它们之间要通信,最直接的方式就是 Mailbox。内核为了不让每个驱动工程师都从头造轮子,就抽象出了这个子系统。我当年第一次接触它的时候,也觉得挺绕,但摸清楚套路后,其实就那么几件事。
3.1 子系统架构:分层与角色
Mailbox 子系统在 Linux 内核里,分了三层。我习惯把它想象成一个“邮局”模型:
- Controller(邮局总局):负责管理硬件 Mailbox 通道。它知道怎么发消息、怎么收中断。
- Channel(邮局窗口):一个具体的通信通道。每个通道对应一对发送/接收寄存器。
- Client(寄信人/收信人):使用 Mailbox 通信的驱动程序,比如 RPMsg 或者某个 IPC 驱动。
这三层的关系,我画了张图,你一看就明白:
嗯,这里要注意。很多新手容易犯一个错:在 Client 驱动里直接去读写 Mailbox 的硬件寄存器。千万别这么干。内核的 Mailbox 框架就是让你隔离硬件细节的。你只管调用 API,剩下的交给 Controller。
3.2 核心数据结构与 API 接口
搞懂架构后,我们来看看代码里具体怎么玩。核心就三个结构体:
| 结构体 | 作用 | 关键字段 |
|---|---|---|
struct mbox_controller |
描述一个 Mailbox 控制器 | dev, ops, chans, num_chans, txdone_poll, txdone_irq |
struct mbox_chan |
描述一个通信通道 | mbox, cl, txdone_method, msg_count, msg_free |
struct mbox_client |
描述一个使用者 | dev, tx_block, tx_tout, knows_txdone, rx_callback, tx_prepare, tx_done |
我个人习惯,写 Controller 驱动时,最先关注的是 struct mbox_chan_ops。这个 ops 是你必须实现的函数表:
struct mbox_chan_ops {
int (*send_data)(struct mbox_chan *chan, void *data);
int (*startup)(struct mbox_chan *chan);
void (*shutdown)(struct mbox_chan *chan);
bool (*last_tx_done)(struct mbox_chan *chan);
bool (*peek_data)(struct mbox_chan *chan);
};
这里有个坑,我踩过。关于 txdone 的机制,有两种模式:
- Polling 模式:设置
txdone_poll = true,内核会定时调用你的last_tx_done()来查发送是否完成。 - IRQ 模式:设置
txdone_irq = true,硬件发送完成后触发中断,你在中断里调用mbox_chan_txdone()通知框架。
txdone_poll 和 txdone_irq 都设成了 true。结果内核两个机制都跑,导致发送完成回调被调了两次,Client 端的数据指针被释放了两次,直接 kernel panic。记住,这两个只能选一个。
Client 端的 API 就简单多了。你作为使用者,只需要:
- 填充
struct mbox_client,注册rx_callback。 - 调用
mbox_request_channel()获取通道。 - 调用
mbox_send_message()发送数据。 - 用完后
mbox_free_channel()释放。
代码骨架大概是这样:
static void my_rx_callback(struct mbox_client *cl, void *mssg)
{
// 收到对方发来的消息
pr_info("收到消息: %s\n", (char *)mssg);
}
static struct mbox_client my_client = {
.dev = &pdev->dev,
.tx_block = true,
.tx_tout = 1000,
.knows_txdone = false,
.rx_callback = my_rx_callback,
};
// 请求通道
chan = mbox_request_channel(&my_client, 0);
if (IS_ERR(chan)) {
// 处理错误
}
// 发送消息
char *msg = "Hello from CPU0";
ret = mbox_send_message(chan, msg);
if (ret < 0) {
dev_err(&pdev->dev, "发送失败\n");
}
tx_block 设为 true 时,mbox_send_message() 会阻塞直到发送完成或超时。如果你在中断上下文调用,记得设成 false,否则会睡死。
3.3 设备树绑定:怎么告诉内核你的硬件
设备树,说白了就是硬件的“身份证”。内核通过设备树才知道你的 Mailbox 控制器在哪里、有几个通道、中断号是多少。
一个典型的 Mailbox 控制器节点长这样:
mailbox: mailbox@30000000 {
compatible = "vendor,my-mailbox";
reg = <0x30000000 0x1000>;
interrupts = <0 42 4>;
#mbox-cells = <1>;
clocks = <&clk 5>;
};
关键点解释一下:
#mbox-cells = <1>:表示每个通道引用需要 1 个 cell 参数,通常是通道 ID。reg:寄存器基地址和大小。interrupts:中断号,一般用 SPI。
然后,使用方(Client)的设备树节点里,通过 mboxes 属性引用:
coprocessor: remoteproc@20000000 {
compatible = "vendor,my-rproc";
reg = <0x20000000 0x10000>;
mboxes = <&mailbox 0>, <&mailbox 1>;
mbox-names = "tx", "rx";
};
这里 mboxes 的第一个 cell 是 0,对应通道 0;第二个 cell 是 1,对应通道 1。内核解析时,会调用 of_mbox_index_translate() 来把设备树里的通道号映射到实际的 struct mbox_chan。
#mbox-cells 必须和你的 Controller 驱动里的 of_xlate 回调一致。否则内核解析通道时会报错,返回 -ENODEV。
我记得有一次调试,Client 端一直拿不到通道。查了半天,发现设备树里 #mbox-cells = <2>,但我的 of_xlate 只解析了一个参数。这种低级错误,排查起来最费时间。
3.4 注册流程:从 probe 到通道就绪
最后,我们串一下整个注册流程。Controller 驱动的 probe 函数里,大概做这几件事:
- 解析设备树,获取寄存器、中断、时钟等资源。
- 初始化硬件(复位、使能时钟)。
- 填充
struct mbox_controller,设置ops、chans、num_chans。 - 调用
mbox_controller_register()注册到内核。
注册成功后,内核会把 Controller 加入全局链表。当有 Client 设备 probe 时,内核通过设备树匹配到对应的 Controller,然后调用 mbox_request_channel() 来分配通道。
这个过程中,startup 回调会被调用。我建议你在 startup 里做通道级别的初始化,比如使能中断、清空 FIFO。别在 probe 里一股脑全做了,因为通道可能被动态打开和关闭。