3. Mailbox 驱动框架:Linux 内核 Mailbox 子系统架构、API 接口介绍、设备树绑定

好,我们进入第三章。这一章,咱们把 Linux 内核的 Mailbox 子系统拆开看看。说白了,就是搞清楚内核到底提供了哪些“基础设施”给我们用。

你想想看,一个 SoC 里可能有多个核,或者多个处理器。它们之间要通信,最直接的方式就是 Mailbox。内核为了不让每个驱动工程师都从头造轮子,就抽象出了这个子系统。我当年第一次接触它的时候,也觉得挺绕,但摸清楚套路后,其实就那么几件事。

3.1 子系统架构:分层与角色

Mailbox 子系统在 Linux 内核里,分了三层。我习惯把它想象成一个“邮局”模型:

  • Controller(邮局总局):负责管理硬件 Mailbox 通道。它知道怎么发消息、怎么收中断。
  • Channel(邮局窗口):一个具体的通信通道。每个通道对应一对发送/接收寄存器。
  • Client(寄信人/收信人):使用 Mailbox 通信的驱动程序,比如 RPMsg 或者某个 IPC 驱动。

这三层的关系,我画了张图,你一看就明白:

Linux Mailbox 子系统分层架构 Client 层(使用者) RPMsg 驱动 | IPC 驱动 | 用户自定义协议驱动 调用 mbox_send_message() / mbox_request_channel() API 接口层(抽象) mbox_controller_register / mbox_send_message / 回调注册 Controller 层(硬件抽象) struct mbox_controller | struct mbox_chan_ops 实现 send_data() / startup() / shutdown() / last_tx_done() 处理硬件寄存器读写、中断服务 核心原则:Client 不直接操作硬件寄存器,全部通过 Controller 层转发

嗯,这里要注意。很多新手容易犯一个错:在 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 就简单多了。你作为使用者,只需要:

  1. 填充 struct mbox_client,注册 rx_callback。
  2. 调用 mbox_request_channel() 获取通道。
  3. 调用 mbox_send_message() 发送数据。
  4. 用完后 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 函数里,大概做这几件事:

  1. 解析设备树,获取寄存器、中断、时钟等资源。
  2. 初始化硬件(复位、使能时钟)。
  3. 填充 struct mbox_controller,设置 ops、chans、num_chans。
  4. 调用 mbox_controller_register() 注册到内核。

注册成功后,内核会把 Controller 加入全局链表。当有 Client 设备 probe 时,内核通过设备树匹配到对应的 Controller,然后调用 mbox_request_channel() 来分配通道。

这个过程中,startup 回调会被调用。我建议你在 startup 里做通道级别的初始化,比如使能中断、清空 FIFO。别在 probe 里一股脑全做了,因为通道可能被动态打开和关闭。