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
小提示: 我刚开始写 client 驱动时,总以为 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> 是最常见的。

避坑指南: 我曾经在 DTS 里把 #mbox-cells 写成了 0,结果 client 驱动调用 mbox_request_channel() 时一直返回 -ENODEV。查了两天才发现是 DTS 配错了。记住:#mbox-cells 必须和 controller 驱动里的 of_xlate 回调匹配。

3.5 整体框架图:一张图看懂三层结构

下面这张 SVG 图展示了 Mailbox 框架的核心数据流。我画的时候特意把 API 层放在中间,因为它是连接 client 和 controller 的桥梁。

Linux Mailbox 驱动框架三层结构 Linux Mailbox 驱动框架 Client 层(驱动使用者) mbox_request_channel() → mbox_send_message() → rx_callback 示例:RPMsg 驱动、用户态通信驱动 Mailbox API 层(内核抽象) struct mbox_controller / struct mbox_client / struct mbox_chan Controller 层(硬件驱动) send_data() → startup() → shutdown() → last_tx_done() 操作硬件寄存器:FIFO、中断、状态位 DTS 配置:匹配 compatible、reg、interrupts 数据流向 Client → API → 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 状态和中断计数。这招在定位丢包问题时特别管用。