Mailbox 控制器驱动(二):发送通道与接收通道的注册与中断处理

上一章我们把 Mailbox 控制器驱动的基础框架搭好了,设备树解析、平台驱动注册、硬件寄存器映射都就位了。但说实话,那只是万里长征第一步。真正的核心在于——发送通道(TX)和接收通道(RX)怎么注册?中断怎么处理?这两个搞不定,核间通信就是空中楼阁。

我个人习惯把 TX/RX 通道的注册流程画成一张图,这样脑子里有个全局印象。咱们先看这张 SVG 结构图,再逐层拆代码。

Mailbox TX/RX 通道注册与中断处理流程 设备树解析通道信息 TX 通道注册 RX 通道注册 分配通道 ID / 配置 FIFO / 设置发送回调 注册到 mailbox_controller.chans[] 分配通道 ID / 配置 FIFO / 设置接收回调 注册到 mailbox_controller.chans[] TX 完成中断处理 RX 数据到达中断处理 调用 tx_done 回调 → 通知客户端 调用 rx_callback 回调 → 通知客户端

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;
}
注意:RX 通道的中断使能必须在注册时完成,否则对方核发数据过来,你这边根本不知道。我曾经在调试时漏了这步,花了整整一个下午才发现是中断没开。

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 通道的注册和中断处理就讲完了。核心就三件事:分配通道、配置硬件、挂中断。下一层就是客户端怎么用这些通道发数据了,但那是后面章节的事。先把控制器这层搞扎实,后面才稳。