Mailbox 控制器驱动(二):发送通道与接收通道的注册与中断处理
上一章我们把 Mailbox 控制器驱动的基础框架搭好了,设备树解析、平台驱动注册、硬件寄存器映射都就位了。但说实话,那只是万里长征第一步。真正的核心在于——发送通道(TX)和接收通道(RX)怎么注册?中断怎么处理?这两个搞不定,核间通信就是空中楼阁。
我个人习惯把 TX/RX 通道的注册流程画成一张图,这样脑子里有个全局印象。咱们先看这张 SVG 结构图,再逐层拆代码。
4.1 发送通道(TX)注册流程
TX 通道说白了就是「本核往外发消息的管道」。注册 TX 通道时,核心工作是:分配通道索引、配置硬件 FIFO 或寄存器、挂载发送完成回调。
我在项目中遇到过一个问题:某个平台有 8 个通道,但设备树里只配了 4 个 TX。结果驱动一跑,索引越界直接 panic。所以注册前一定要先数清楚通道总数。
来看一段实际的注册代码(基于某 ARM SoC 的 Mailbox 控制器):
static int imx_mbox_tx_channel_init(struct imx_mbox_priv *priv,
struct device_node *np, int idx)
{
struct imx_mbox_chan *chan;
int ret;
/* 1. 分配通道结构体 */
chan = devm_kzalloc(priv->dev, sizeof(*chan), GFP_KERNEL);
if (!chan)
return -ENOMEM;
/* 2. 设置通道 ID 和方向 */
chan->idx = idx;
chan->dir = MBOX_TX; /* 标记为发送通道 */
/* 3. 映射硬件寄存器(每个通道有独立的 TRIG 和 STAT 寄存器) */
chan->reg_base = priv->reg_base + (idx * 0x10);
chan->trig_reg = chan->reg_base + 0x00; /* 触发发送 */
chan->stat_reg = chan->reg_base + 0x04; /* 状态寄存器 */
/* 4. 设置发送完成回调(由上层 RPMsg 或客户端注册) */
chan->tx_done = NULL; /* 后续由 mbox_client 注册时绑定 */
/* 5. 注册到控制器通道数组 */
priv->chans[idx] = chan;
dev_dbg(priv->dev, "TX channel %d registered at 0x%p\n", idx, chan->reg_base);
return 0;
}
4.2 接收通道(RX)注册流程
RX 通道和 TX 通道的注册流程基本对称,但有一个关键区别——RX 通道必须绑定接收中断。因为数据是对方核主动发过来的,你得靠中断来「被通知」。
你想想看,如果 RX 通道没有中断,难道要轮询吗?那 CPU 占用率直接飙到 100%,还做什么嵌入式系统。
RX 通道注册的核心代码:
static int imx_mbox_rx_channel_init(struct imx_mbox_priv *priv,
struct device_node *np, int idx)
{
struct imx_mbox_chan *chan;
int ret;
chan = devm_kzalloc(priv->dev, sizeof(*chan), GFP_KERNEL);
if (!chan)
return -ENOMEM;
chan->idx = idx;
chan->dir = MBOX_RX; /* 标记为接收通道 */
/* 映射寄存器(与 TX 共用同一组基址,但偏移不同) */
chan->reg_base = priv->reg_base + (idx * 0x10);
chan->data_reg = chan->reg_base + 0x08; /* 数据寄存器 */
chan->ctrl_reg = chan->reg_base + 0x0C; /* 控制寄存器 */
/* 重点:使能接收中断 */
writel(IMX_MBOX_IMR_RIE, chan->ctrl_reg); /* 使能接收中断使能位 */
/* 注册到通道数组 */
priv->chans[idx] = chan;
dev_dbg(priv->dev, "RX channel %d registered, interrupt enabled\n", idx);
return 0;
}
4.3 中断处理机制
中断处理是 Mailbox 驱动的灵魂。Linux 内核的 Mailbox 框架把中断处理分成了两层:
- 控制器层中断处理:由你的驱动实现,负责读取硬件状态寄存器,判断是哪个通道触发了中断,然后调用框架的 API 通知上层。
- 框架层回调:由
mbox_chan_received_data()或mbox_chan_txdone()触发,最终调用到客户端注册的回调函数。
来看一个实际的中断处理函数:
static irqreturn_t imx_mbox_interrupt(int irq, void *dev_id)
{
struct imx_mbox_priv *priv = dev_id;
u32 status;
int i;
/* 1. 读取中断状态寄存器 */
status = readl(priv->reg_base + IMX_MBOX_SR);
/* 2. 遍历所有通道,判断哪个通道触发了中断 */
for (i = 0; i < priv->num_chans; i++) {
struct imx_mbox_chan *chan = priv->chans[i];
if (!chan)
continue;
/* 3. 判断是 TX 完成中断还是 RX 数据到达中断 */
if (status & (1 << (i * 2))) {
/* TX 完成中断 */
if (chan->tx_done)
chan->tx_done(chan->cl, NULL);
/* 清除 TX 中断标志 */
writel(1 << (i * 2), priv->reg_base + IMX_MBOX_ICR);
}
if (status & (1 << (i * 2 + 1))) {
/* RX 数据到达中断 */
u32 data = readl(chan->data_reg);
mbox_chan_received_data(chan, (void *)&data);
/* 清除 RX 中断标志 */
writel(1 << (i * 2 + 1), priv->reg_base + IMX_MBOX_ICR);
}
}
return IRQ_HANDLED;
}
4.4 TX/RX 通道注册与中断的完整流程表
为了让你一目了然,我把 TX 和 RX 通道的注册流程、中断处理、回调关系整理成了一张对比表:
| 项目 | TX 通道 | RX 通道 |
|---|---|---|
| 方向 | 本核 → 对方核 | 对方核 → 本核 |
| 注册时机 | probe() 中解析设备树后 | probe() 中解析设备树后 |
| 硬件资源 | TRIG 寄存器、STAT 寄存器 | DATA 寄存器、CTRL 寄存器 |
| 中断类型 | 发送完成中断(可选) | 数据到达中断(必须) |
| 中断使能 | 按需使能 | 注册时立即使能 |
| 回调函数 | tx_done() |
rx_callback() |
| 框架 API | mbox_chan_txdone() |
mbox_chan_received_data() |
4.5 避坑指南:中断处理中的常见问题
做 Mailbox 驱动,中断处理是翻车率最高的地方。我总结几个亲身踩过的坑:
- 中断标志清除顺序:一定要先读状态,再处理数据,最后清标志。反过来会导致中断丢失。
- 共享中断处理:如果多个 Mailbox 实例共用同一个中断号,必须在中断处理函数开头判断是否是自己的中断,不是则返回
IRQ_NONE。 - 中断上下文中的耗时操作:中断处理函数里不要做
memcpy大数据拷贝或printk大量日志。我曾经在中断里打印调试信息,结果系统直接死锁。 - RX 通道数据读取时序:有些硬件在读取 DATA 寄存器后会自动清除中断标志,这时你就不需要再写 ICR 了。一定要看芯片手册。
dev_dbg,打印中断状态和通道号。配合 trace_printk 可以做到不干扰实时性。我调试多核通信时,经常用这个办法定位中断风暴。
4.6 实际代码整合:probe() 中的通道注册
最后,把 TX/RX 通道注册放到 probe() 函数里,形成一个完整的流程:
static int imx_mbox_probe(struct platform_device *pdev)
{
struct imx_mbox_priv *priv;
struct device_node *np = pdev->dev.of_node;
int num_chans, ret, i;
/* ... 资源获取、时钟使能等基础操作 ... */
/* 1. 从设备树获取通道总数 */
ret = of_property_read_u32(np, "mbox-channels", &num_chans);
if (ret)
num_chans = 4; /* 默认值 */
priv->num_chans = num_chans;
priv->chans = devm_kcalloc(&pdev->dev, num_chans,
sizeof(struct imx_mbox_chan *), GFP_KERNEL);
/* 2. 遍历设备树子节点,注册 TX/RX 通道 */
for_each_child_of_node(np, child) {
u32 idx, dir;
if (of_property_read_u32(child, "reg", &idx))
continue;
if (of_property_read_u32(child, "direction", &dir))
continue;
if (dir == MBOX_TX)
imx_mbox_tx_channel_init(priv, child, idx);
else if (dir == MBOX_RX)
imx_mbox_rx_channel_init(priv, child, idx);
}
/* 3. 注册中断 */
ret = platform_get_irq(pdev, 0);
ret = devm_request_irq(&pdev->dev, ret, imx_mbox_interrupt,
IRQF_SHARED, "imx-mbox", priv);
/* 4. 注册 Mailbox 控制器 */
/* ... 调用 mbox_controller_register() ... */
return 0;
}
嗯,到这里 TX/RX 通道的注册和中断处理就讲完了。核心就三件事:分配通道、配置硬件、挂中断。下一层就是客户端怎么用这些通道发数据了,但那是后面章节的事。先把控制器这层搞扎实,后面才稳。