3、Linux Mailbox 驱动框架:API、Client、Controller 与 DTS
好,咱们进入正题。Mailbox 驱动框架,说白了就是一套标准化的“收发室”机制。Linux 内核把复杂的硬件 mailbox 抽象成两层:Controller(控制器)和 Client(客户端)。
我个人习惯把 Controller 想象成邮局的分拣中心,它只管把信发出去、收进来,不关心信里写了什么。Client 则是寄信人或者收信人,它只负责把信丢进邮筒,或者从信箱里取信。这两层通过内核提供的 Mailbox API 沟通。
你想想看,如果没有这个框架,每个驱动都得直接操作 mailbox 寄存器,那代码得多乱?内核社区早就替我们想好了。
3.1 Mailbox API:核心接口一览
API 是框架的灵魂。Linux 内核在 include/linux/mailbox_client.h 和 include/linux/mailbox_controller.h 里定义了这些接口。我挑几个最常用的给你拆开讲讲。
| API 函数 | 所属层 | 作用 |
|---|---|---|
mbox_request_channel() |
Client | 申请一个 mailbox 通道,绑定到指定 controller |
mbox_send_message() |
Client | 发送消息(非阻塞,放入队列就返回) |
mbox_client_txdone() |
Client | 通知 controller 发送已完成(用于轮询模式) |
mbox_controller_register() |
Controller | 向内核注册一个 controller 实例 |
mbox_controller_unregister() |
Controller | 注销 controller |
mbox_send_message() 是同步的。其实它是异步的——消息先入队,真正发送由 controller 的 send_data() 回调完成。这个坑我踩过,后来查代码才明白。
3.2 Client 驱动编写:寄信人的视角
写 client 驱动,你只需要关心两件事:申请通道 和 收发消息。内核帮你屏蔽了硬件细节。
来看一个典型的 client 驱动片段:
#include <linux/mailbox_client.h>
static struct mbox_client cl = {
.dev = &pdev->dev,
.tx_done = NULL, // 发送完成回调,NULL 表示用轮询
.rx_callback = my_rx_cb, // 接收回调
.tx_block = true, // 是否阻塞等待发送完成
.tx_tout = 1000, // 超时时间(ms)
};
static int my_probe(struct platform_device *pdev)
{
struct mbox_chan *chan;
chan = mbox_request_channel(&cl, 0); // 申请通道 0
if (IS_ERR(chan))
return PTR_ERR(chan);
// 发送一条消息
mbox_send_message(chan, "Hello from client");
return 0;
}
这里有个关键点:tx_block 和 tx_done 是互斥的。如果你设了 tx_block = true,内核会帮你等发送完成;如果你设了 tx_done 回调,那就用中断方式通知。我建议新手先用阻塞模式,逻辑简单,不容易出 race condition。
tx_tout 设成了 0,结果内核直接 panic 了。这个值必须大于 0,否则超时检测会除零。
3.3 Controller 驱动编写:邮局分拣员的视角
Controller 驱动就复杂多了。你需要实现 struct mbox_controller_ops 里的回调函数:
| 回调函数 | 说明 |
|---|---|
send_data() |
实际写入硬件寄存器,触发发送 |
startup() |
通道初始化,比如使能中断 |
shutdown() |
通道关闭,释放资源 |
last_tx_done() |
查询上次发送是否完成(轮询模式用) |
一个简化的 controller 实现大概长这样:
static int my_send_data(struct mbox_chan *chan, void *data)
{
struct my_mbox *mbox = chan->con_priv;
const char *msg = (const char *)data;
// 把消息写入硬件 FIFO
writel(*(u32 *)msg, mbox->regs + TX_REG);
// 触发发送
writel(1, mbox->regs + TRIGGER_REG);
return 0;
}
static const struct mbox_controller_ops my_ops = {
.send_data = my_send_data,
.startup = my_startup,
.shutdown = my_shutdown,
};
static int my_probe(struct platform_device *pdev)
{
struct mbox_controller *mbox;
mbox = devm_kzalloc(&pdev->dev, sizeof(*mbox), GFP_KERNEL);
mbox->ops = &my_ops;
mbox->num_chans = 1;
mbox->chans = my_chans; // 预分配的通道数组
mbox->dev = &pdev->dev;
return mbox_controller_register(mbox);
}
嗯,这里要注意:chan->con_priv 是 controller 的私有数据指针,你可以用它挂载硬件寄存器基址、spinlock 等。我习惯在 startup() 里分配并赋值,在 shutdown() 里清理。
3.4 DTS 配置:把一切串起来
DTS(Device Tree Source)是 Linux 驱动和硬件之间的“翻译官”。没有它,驱动根本不知道 mailbox 控制器挂在哪、中断号是多少。
一个典型的 mailbox controller 节点:
mailbox@30000000 {
compatible = "vendor,my-mbox";
reg = <0x30000000 0x1000>;
interrupts = <GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH>;
#mbox-cells = <1>; // 表示 client 需要 1 个参数(通道号)
clocks = <&clk IMX_MBOX_CLK>;
};
Client 节点引用它:
my-client {
compatible = "vendor,my-client";
mboxes = <&mailbox 0>; // 使用 mailbox 的通道 0
mbox-names = "tx"; // 给通道起个名字
};
这里 #mbox-cells 决定了 client 在 mboxes 属性里需要提供几个参数。大部分硬件只需要一个通道号,所以 #mbox-cells = <1> 是最常见的。
#mbox-cells 写成了 0,结果 client 驱动调用 mbox_request_channel() 时一直返回 -ENODEV。查了两天才发现是 DTS 配错了。记住:#mbox-cells 必须和 controller 驱动里的 of_xlate 回调匹配。
3.5 整体框架图:一张图看懂三层结构
下面这张 SVG 图展示了 Mailbox 框架的核心数据流。我画的时候特意把 API 层放在中间,因为它是连接 client 和 controller 的桥梁。
3.6 关键数据结构速查
写驱动时,这几个结构体你几乎天天打交道。我整理了一张速查表:
| 结构体 | 定义位置 | 核心字段 |
|---|---|---|
struct mbox_client |
mailbox_client.h | dev, tx_done, rx_callback, tx_block, tx_tout |
struct mbox_controller |
mailbox_controller.h | ops, chans, num_chans, dev, of_xlate |
struct mbox_chan |
mailbox_controller.h | cl, con_priv, msg_count, txdone_method |
mbox_chan 的 con_priv 里挂一个 debugfs 文件,用来实时查看 FIFO 状态和中断计数。这招在定位丢包问题时特别管用。