1. Mailbox 硬件原理:控制器架构、中断机制、寄存器模型与多核通信基础

各位同学,咱们今天聊 Mailbox 硬件。说实话,很多做 Linux 驱动的人,一上来就怼 RPMsg 协议栈,结果底层 Mailbox 怎么工作的完全不清楚。我早年吃过这个亏——调了三天中断不触发,最后发现是寄存器地址算错了半个字偏移。嗯,咱们从根上讲起。

1.1 为什么需要 Mailbox?

多核处理器里,两个核心要交换数据。你想想看,Core A 和 Core B 没有共享内存吗?有的。但问题是——怎么通知对方“我有数据给你”?

轮询?浪费 CPU。共享变量?没有原子保护。中断?谁来触发?

Mailbox 就是干这个的:一个专用的硬件通道,负责发消息、触发中断、传递状态。它不传大数据,只传“信令”或“短消息”。真正的数据负载,通常放在共享内存里,Mailbox 只传指针或描述符。

核心原则:Mailbox 传的是“门铃”,不是“快递”。数据走共享内存,Mailbox 只负责按门铃。

1.2 Mailbox 控制器架构

一个典型的 Mailbox 控制器,内部长什么样?我画了张图,你看一眼就明白。

Mailbox 控制器内部架构 Core A (发送方) Core B (接收方) 发送寄存器组 TX_DATA[0..3] TX_REQ TX_ACK TX_STATUS 接收寄存器组 RX_DATA[0..3] RX_REQ RX_ACK RX_STATUS 控制逻辑 状态机 仲裁 超时检测 错误处理 中断控制器 IRQ_PENDING | IRQ_MASK | IRQ_CLEAR 总线接口 (AXI/AHB/APB) 说明:发送方写 TX_DATA 和 TX_REQ → 控制逻辑检测到请求 → 通知接收方中断 → 接收方读 RX_DATA 数据流方向:Core A → Mailbox → Core B,每次最多传 4 个 32 位字(128 位)

从图上你能看到几个关键模块:

  • 发送寄存器组:发送方把消息写进去,然后置位请求位。
  • 接收寄存器组:接收方从这里读消息,然后回一个确认。
  • 控制逻辑:状态机、仲裁、超时检测——说白了就是协调两边别打架。
  • 中断控制器:产生中断通知接收方“有消息到了”。
  • 总线接口:挂到系统总线上,CPU 通过 load/store 访问。
我的经验:不同厂家的 Mailbox 实现差异很大。TI 的 Mailbox 有 4 个通道,每个通道 4 个消息槽;Xilinx 的 IPI 只有 1 个 32 位寄存器。调驱动前,一定先看 TRM 里的寄存器描述,别想当然。

1.3 中断机制——核心中的核心

Mailbox 为什么快?因为它用中断代替了轮询。但中断机制设计得好不好,直接影响通信效率。

典型的中断流程是这样的:

  1. 发送方写消息:往 TX_DATA 寄存器写数据,然后写 TX_REQ 寄存器(通常写 1 就触发)。
  2. 硬件自动置位:控制逻辑检测到 TX_REQ,把消息搬到接收寄存器,同时置位 IRQ_PENDING。
  3. 中断控制器向接收方发中断:如果 IRQ_MASK 没屏蔽,中断信号就送到 GIC 或 PLIC。
  4. 接收方 ISR 读消息:从 RX_DATA 读走,然后写 RX_ACK 清除中断。
  5. 发送方收到确认:硬件清除 TX_REQ,表示“消息已被消费”。
注意:我曾经在一个项目里遇到“中断风暴”——发送方连续发消息,接收方 ISR 还没处理完,下一个中断又来了。解决办法是:在 ISR 里一次性读完所有待处理消息,或者用 NAPI 机制做中断合并。

中断相关的寄存器,一般有这几个:

寄存器 方向 作用
IRQ_STATUS / IRQ_PENDING 只读 显示当前哪些中断源 pending
IRQ_MASK / IRQ_ENABLE 读写 屏蔽或使能特定中断源
IRQ_CLEAR / IRQ_ACK 写 写 1 清除对应中断 pending 位
IRQ_RAW 只读 绕过 mask,直接看硬件原始中断状态(调试用)
调试技巧:中断不触发?先读 IRQ_RAW。如果 RAW 有值但 STATUS 没有,说明被 MASK 屏蔽了。如果 RAW 都没有,检查发送方有没有真的写 TX_REQ。

1.4 寄存器模型——你得会“读”芯片手册

每个 Mailbox 控制器的寄存器布局都不一样,但套路差不多。我拿一个典型的 32 位 Mailbox 举例:

// 伪代码:Mailbox 寄存器偏移定义
#define MAILBOX_BASE         0x4000_0000

#define REG_TX_DATA0         0x00   // 发送数据寄存器 0
#define REG_TX_DATA1         0x04   // 发送数据寄存器 1
#define REG_TX_DATA2         0x08   // 发送数据寄存器 2
#define REG_TX_DATA3         0x0C   // 发送数据寄存器 3
#define REG_TX_REQ           0x10   // 发送请求寄存器(写1触发)
#define REG_TX_ACK           0x14   // 发送确认寄存器(对方读走后硬件置位)

#define REG_RX_DATA0         0x20   // 接收数据寄存器 0
#define REG_RX_DATA1         0x24   // 接收数据寄存器 1
#define REG_RX_DATA2         0x28   // 接收数据寄存器 2
#define REG_RX_DATA3         0x2C   // 接收数据寄存器 3
#define REG_RX_REQ           0x30   // 接收请求寄存器(硬件置位)
#define REG_RX_ACK           0x34   // 接收确认寄存器(读完后写1清除)

#define REG_IRQ_STATUS       0x40   // 中断状态
#define REG_IRQ_MASK         0x44   // 中断屏蔽
#define REG_IRQ_CLEAR        0x48   // 中断清除

#define REG_VERSION          0xFC   // 版本号(调试用)

你注意看,发送和接收的寄存器偏移是分开的。为什么?因为发送方和接收方可能是两个不同的地址空间(比如 A 核和 M 核,各自映射不同的物理地址)。

我建议:写驱动前,先把寄存器偏移做成一个结构体,用 ioremap 映射。别用硬编码地址,否则换芯片版本时你会哭的。

1.5 多核通信基础——谁跟谁说话?

Mailbox 最常见的场景是 AMP(非对称多处理):一个 Cortex-A 跑 Linux,一个 Cortex-M 跑 RTOS,或者两个 A 核跑不同 OS。它们之间没有缓存一致性,所以不能直接共享指针。

多核通信的典型模式:

  • 主-从模式:A 核是主,M 核是从。A 核发命令,M 核执行并回复。Mailbox 只传命令 ID 和参数。
  • 对等模式:两个核地位平等,都可以主动发消息。需要两个 Mailbox 通道,或者一个通道双向复用。
  • 广播模式:一个核发消息,多个核同时接收。硬件上需要多个接收寄存器组。

我参与过一个车载项目,A 核发“打开摄像头”命令给 M 核,M 核收到后初始化摄像头,然后通过 Mailbox 回复“就绪”。整个过程就 4 个 32 位字:命令 ID、参数、状态码、校验和。数据量不大,但实时性要求极高——延迟不能超过 100 微秒。

关键点:Mailbox 传的是“控制面”消息,不是“数据面”。真正的视频流走 DMA 到共享内存,Mailbox 只传“开始传”、“传完了”这种信令。

1.6 避坑指南——我踩过的几个坑

做 Mailbox 驱动,有几个坑你八成会遇到:

  • 寄存器对齐问题:有些 Mailbox 要求 64 位对齐访问,你用 readl/writel 可能触发 bus error。我遇到过,查了两天才发现是 AXI 总线对齐问题。
  • 中断丢失:接收方 ISR 里没清中断,导致后续中断无法触发。或者清了中断但消息还没读完——硬件把数据覆盖了。
  • 死锁:发送方等 ACK,接收方等消息,结果两边都在等。需要加超时机制。
  • 字节序:A 核和 M 核的字节序可能不同(A 核小端,M 核可能大端)。Mailbox 寄存器是硬件固定的,但消息内容需要做转换。
我曾经:在一个项目里,Mailbox 通信偶尔卡死。查了三天,发现是发送方连续发了两次消息,但接收方只处理了一次,第二次的消息把第一次的覆盖了。解决方案:发送方加一个“忙检测”,等上次消息被 ACK 后再发下一条。

1.7 小结

Mailbox 硬件其实不复杂,就是一组寄存器加一个中断控制器。但它的设计直接影响多核通信的可靠性和性能。你理解了控制器架构、中断流程、寄存器模型,后面学 RPMsg 协议栈就轻松多了。