2. Linux Mailbox 框架:框架分层设计、核心数据结构、API 接口概述

好,咱们进入正题。上一章聊了 Mailbox 是什么、能干什么,这一章我带你看看 Linux 内核里到底是怎么组织这套机制的。

说白了,Linux Mailbox 框架就是一套标准化的「收发室」管理方案。它把硬件差异藏起来,给驱动开发者提供统一的接口。你想想看,不同 SoC 的 Mailbox 硬件千奇百怪,有的用寄存器轮询,有的走硬件中断,如果没有框架层抽象,每个芯片厂都得从头造轮子——那调试起来可就真成灾难了。

2.1 框架分层设计

Linux Mailbox 框架分三层,我习惯叫它「三明治结构」:

  • 底层(硬件驱动层):直接操作寄存器,处理中断,发送/接收原始数据。
  • 中间层(核心框架层):提供通用数据结构、注册/注销接口、通道管理、超时控制。
  • 上层(客户端驱动层):调用框架 API 发送消息、注册回调、处理响应。

我在项目中遇到过好几次这样的问题:底层驱动已经写好了,中断也能触发,但上层就是收不到数据。查到最后,发现是中间层的通道状态没更新——框架层的那个标志位没置起来。嗯,这种坑踩过一次你就记住了。

核心要点: 框架层是「胶水」,它不关心你发的是什么数据,只负责把数据从 A 点搬到 B 点,并通知双方「货到了」。

Linux Mailbox 框架分层结构 上层:客户端驱动层 mbox_client_txdone / mbox_client_rx_callback 注册通道 → 发送消息 → 接收响应 中间层:核心框架层 mbox_controller_register / mbox_send_message 通道管理 · 超时控制 · 状态机维护 底层:硬件驱动层 操作寄存器 · 中断处理 · 原始数据收发 platform_driver_register · 硬件初始化 数据流:客户端 → 框架层 → 硬件驱动 → 硬件 → 硬件驱动 → 框架层 → 客户端

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 消息发送的完整流程

我习惯用「打电话」来类比这个过程:

  1. 拨号:客户端调 mbox_send_message(),相当于拿起话筒拨号。
  2. 占线检查:框架检查通道是否空闲(active_req == NULL)。如果忙,返回 -EBUSY。
  3. 挂载请求:框架把消息指针赋给 active_req,然后调底层驱动的 send_data。
  4. 硬件发送:底层驱动写寄存器,触发硬件传输。
  5. 等待挂断:客户端可以等 tx_complete 完成,也可以不等(看业务需求)。
  6. 挂断通知:硬件发送完成,触发中断或轮询到状态变化,底层调 mbox_chan_txdone() 通知框架。
  7. 清理:框架清空 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。