2. Linux Mailbox 框架:框架分层设计、核心数据结构、API 接口概述
好,咱们进入正题。上一章聊了 Mailbox 是什么、能干什么,这一章我带你看看 Linux 内核里到底是怎么组织这套机制的。
说白了,Linux Mailbox 框架就是一套标准化的「收发室」管理方案。它把硬件差异藏起来,给驱动开发者提供统一的接口。你想想看,不同 SoC 的 Mailbox 硬件千奇百怪,有的用寄存器轮询,有的走硬件中断,如果没有框架层抽象,每个芯片厂都得从头造轮子——那调试起来可就真成灾难了。
2.1 框架分层设计
Linux Mailbox 框架分三层,我习惯叫它「三明治结构」:
- 底层(硬件驱动层):直接操作寄存器,处理中断,发送/接收原始数据。
- 中间层(核心框架层):提供通用数据结构、注册/注销接口、通道管理、超时控制。
- 上层(客户端驱动层):调用框架 API 发送消息、注册回调、处理响应。
我在项目中遇到过好几次这样的问题:底层驱动已经写好了,中断也能触发,但上层就是收不到数据。查到最后,发现是中间层的通道状态没更新——框架层的那个标志位没置起来。嗯,这种坑踩过一次你就记住了。
核心要点: 框架层是「胶水」,它不关心你发的是什么数据,只负责把数据从 A 点搬到 B 点,并通知双方「货到了」。
2.2 核心数据结构
框架层里最重要的两个结构体,一个是 mbox_controller,一个是 mbox_chan。我刚开始学的时候老搞混,后来自己画了个图才理清楚。
2.2.1 mbox_controller
这个结构体代表一个 Mailbox 控制器。一个 SoC 里可能有多个控制器,每个控制器管理一组通道。
struct mbox_controller {
struct device *dev;
const struct mbox_chan_ops *ops;
struct mbox_chan *chans;
unsigned int num_chans;
bool txdone_poll;
unsigned int txpoll_period;
struct mbox_controller *next;
};
几个关键字段我解释一下:
ops:操作函数集,底层驱动必须实现send_data、startup、shutdown等回调。chans:通道数组,每个元素对应一个硬件通道。txdone_poll:如果硬件不支持发送完成中断,就靠轮询。我调过的一个项目里,硬件手册说支持中断,结果勘误表里写着「此功能有 bug,请使用轮询」——你说气不气人。
2.2.2 mbox_chan
每个通道对应一对通信端点。结构体长这样:
struct mbox_chan {
struct mbox_controller *mbox;
unsigned int txdone_method;
struct mbox_client *cl;
struct completion tx_complete;
void *active_req;
bool msg_sent;
};
这里有个容易踩的坑:active_req 指针。它指向当前正在发送的请求。如果你的客户端在上一笔还没发完时就又调了 mbox_send_message,框架会直接返回 -EBUSY。我曾经在某个项目中,上层应用用多线程并发发消息,结果频繁报 -EBUSY,查了半天才发现是没做互斥。
注意: txdone_method 有三种取值:IRQ(硬件中断通知)、POLL(定时轮询)、ACK(客户端手动确认)。选错了会导致发送状态永远卡住。
2.3 API 接口概述
框架提供的 API 不多,但每个都有讲究。我按使用场景给你列一下:
| API 函数 | 调用方 | 作用 | 我踩过的坑 |
|---|---|---|---|
mbox_controller_register() |
底层驱动 | 注册控制器到框架 | 必须在通道初始化完成后调用,否则框架遍历通道时会崩溃 |
mbox_request_channel() |
客户端 | 申请一个通信通道 | 返回的通道指针要判空,我见过因为设备树里通道号写错导致返回 NULL 的 |
mbox_send_message() |
客户端 | 发送消息(非阻塞) | 返回值不是发送状态,而是消息长度。要看通道的 tx_complete 是否完成 |
mbox_client_txdone() |
客户端 | 通知框架发送完成(ACK 模式用) | 调用时机不对会导致死锁,建议在发送完成回调里调 |
mbox_free_channel() |
客户端 | 释放通道 | 释放后不能再发消息,否则内核直接 oops |
2.4 消息发送的完整流程
我习惯用「打电话」来类比这个过程:
- 拨号:客户端调
mbox_send_message(),相当于拿起话筒拨号。 - 占线检查:框架检查通道是否空闲(
active_req == NULL)。如果忙,返回-EBUSY。 - 挂载请求:框架把消息指针赋给
active_req,然后调底层驱动的send_data。 - 硬件发送:底层驱动写寄存器,触发硬件传输。
- 等待挂断:客户端可以等
tx_complete完成,也可以不等(看业务需求)。 - 挂断通知:硬件发送完成,触发中断或轮询到状态变化,底层调
mbox_chan_txdone()通知框架。 - 清理:框架清空
active_req,唤醒等待队列。
小技巧: 调试时可以在 mbox_send_message 前后加 dev_dbg 打印 active_req 和 msg_sent 的值。这两个标志位能告诉你通道状态机走到哪一步了。
2.5 设备树绑定
最后提一嘴设备树。Mailbox 框架依赖设备树来描述硬件拓扑。一个典型的节点长这样:
mailbox: mailbox@30000000 {
compatible = "vendor,mailbox";
reg = <0x30000000 0x1000>;
interrupts = <GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH>;
#mbox-cells = <1>;
clocks = <&periph_clk>;
};
cpu_core0: cpu@0 {
...
mboxes = <&mailbox 0>;
mbox-names = "tx";
};
注意 #mbox-cells 这个属性。它表示每个通道需要几个参数。常见的是 1 个参数(通道索引),也有用 2 个的(通道索引 + 传输方向)。我见过一个项目,设备树里写的是 #mbox-cells = <2>,但客户端只传了一个参数,结果框架解析通道号时读到垃圾值,直接访问了越界的通道数组——内核 panic,屏幕上一片 oops 信息。
嗯,设备树这东西,写错一个数字,查错一整天。后面我会专门讲怎么用设备树调试 Mailbox。