3. Mailbox 控制器驱动(一):Mailbox框架数据结构解析与操作集实现

好,我们正式开始啃 Mailbox 控制器驱动。这一章是基础中的基础,说白了就是搞清楚 Linux 内核里那套 mailbox 框架到底长什么样。我刚开始接触这块的时候,也被那一堆结构体绕得头晕。后来发现,其实核心就两个东西:mailbox_controller 和 mbox_chan_ops。搞懂它们,你就掌握了半壁江山。

3.1 mailbox_controller 数据结构深度解析

先看主角——struct mbox_controller。这个结构体描述了一个 Mailbox 控制器硬件,它里面装了什么?我们直接看代码:

struct mbox_controller {
    struct device *dev;                 /* 关联的设备 */
    const char *name;                   /* 控制器名称 */
    unsigned int num_chans;             /* 通道数量 */
    struct mbox_chan *chans;            /* 通道数组 */
    struct mbox_chan_ops *ops;          /* 通道操作函数集 */
    struct list_head node;              /* 用于挂入全局链表 */
    struct mbox_controller *parent;     /* 父控制器(用于分层) */
    bool txdone_poll;                   /* 是否轮询发送完成 */
    unsigned int txpoll_period;         /* 轮询周期(ms) */
    bool txdone_irq;                    /* 是否使用中断通知发送完成 */
    struct mbox_chan *(*of_xlate)(struct mbox_controller *,
                                  const struct of_phandle_args *);
    /* 设备树解析回调 */
};

嗯,这里有几个字段我得重点说说。

3.1.1 核心字段解读

字段 作用 我的经验
dev 指向 platform_device 的 device 结构 几乎所有内核框架都靠它做资源管理
num_chans 硬件支持的通道总数 别写死,从设备树或寄存器读取
chans mbox_chan 数组,每个通道一个 动态分配,别用静态数组
ops 操作函数集,驱动核心 后面详细讲
txdone_poll 轮询模式标志 没有硬件中断时用,但效率低
txdone_irq 中断模式标志 推荐使用,实时性好
of_xlate 设备树到通道的映射回调 这个坑最多,后面细说

关键点:txdone_poll 和 txdone_irq 不能同时为 true。我见过有人两个都设了,结果内核 panic。框架会检查,但最好自己心里有数。

3.2 mbox_chan 通道结构体

每个通道对应一个 struct mbox_chan。它才是真正干活的那个:

struct mbox_chan {
    struct mbox_controller *mbox;   /* 所属控制器 */
    unsigned int idx;               /* 通道索引 */
    void *con_priv;                 /* 控制器私有数据 */
    struct mbox_chan_ops *ops;      /* 通道操作函数 */
    spinlock_t lock;                /* 自旋锁 */
    struct completion tx_complete;  /* 发送完成同步 */
    bool active;                    /* 通道是否激活 */
    struct mbox_chan *cl;          /* 客户端指针 */
    struct list_head node;          /* 链表节点 */
};

这里 con_priv 是个 void 指针,你可以指向任何硬件相关的私有数据。比如寄存器基址、中断号什么的。我个人习惯把整个控制器的硬件上下文塞进去,这样回调函数里拿起来方便。

3.3 mbox_chan_ops 操作集实现

操作集是驱动和硬件之间的桥梁。框架定义了几个回调,你至少要实现 send_data:

struct mbox_chan_ops {
    int (*send_data)(struct mbox_chan *chan, void *data);
    int (*startup)(struct mbox_chan *chan);
    void (*shutdown)(struct mbox_chan *chan);
    bool (*last_tx_done)(struct mbox_chan *chan);
    bool (*peek_data)(struct mbox_chan *chan);
};

3.3.1 send_data —— 核心中的核心

这个函数负责把数据写入硬件寄存器,触发发送。原型很简单:

static int my_mbox_send_data(struct mbox_chan *chan, void *data)
{
    struct my_mbox_priv *priv = chan->con_priv;
    u32 *msg = (u32 *)data;

    /* 写入数据寄存器 */
    writel(msg[0], priv->regs + DATA_REG0);
    writel(msg[1], priv->regs + DATA_REG1);

    /* 触发发送(写控制寄存器) */
    writel(TRIGGER_SEND, priv->regs + CTRL_REG);

    return 0;
}

注意,这里 data 是客户端传过来的消息指针。你不需要关心消息格式,直接写寄存器就行。我在一个项目里遇到过一个问题:硬件要求数据必须 8 字节对齐,但客户端传了个奇地址。后来我在 send_data 里加了 memcpy 到局部变量再写,才搞定。

3.3.2 startup 和 shutdown

这两个函数管理通道的开启和关闭:

static int my_mbox_startup(struct mbox_chan *chan)
{
    struct my_mbox_priv *priv = chan->con_priv;

    /* 使能通道中断 */
    writel(readl(priv->regs + IRQ_EN_REG) | BIT(chan->idx),
           priv->regs + IRQ_EN_REG);

    return 0;
}

static void my_mbox_shutdown(struct mbox_chan *chan)
{
    struct my_mbox_priv *priv = chan->con_priv;

    /* 关闭通道中断 */
    writel(readl(priv->regs + IRQ_EN_REG) & ~BIT(chan->idx),
           priv->regs + IRQ_EN_REG);
}

注意:shutdown 里一定要清理干净。我曾经漏掉了中断状态寄存器,结果下次 startup 时触发了假中断,系统直接卡死。

3.3.3 last_tx_done 和 peek_data

这两个用于轮询模式。如果你用了 txdone_poll,框架会周期调用 last_tx_done 检查发送是否完成:

static bool my_mbox_last_tx_done(struct mbox_chan *chan)
{
    struct my_mbox_priv *priv = chan->con_priv;
    u32 status;

    status = readl(priv->regs + STATUS_REG);
    return (status & TX_DONE_BIT) != 0;
}

peek_data 用于检查是否有新数据到达,通常在接收端使用。如果你只做发送,可以不实现。

3.4 整体结构图

下面这张图展示了 mailbox 框架的核心关系,我画的时候特意把数据流和回调路径标清楚了:

Mailbox 控制器框架核心结构 struct mbox_controller dev: device * num_chans: 4 chans: mbox_chan[] ops: mbox_chan_ops * txdone_poll / txdone_irq of_xlate: 设备树解析 struct mbox_chan[4] chan[0] con_priv: 私有数据 chan[1] con_priv: 私有数据 chan[2] con_priv: 私有数据 chan[3] con_priv: 私有数据 struct mbox_chan_ops send_data() — 写入寄存器,触发发送 startup() — 使能通道中断 shutdown() — 关闭通道中断 last_tx_done() — 轮询发送完成 peek_data() — 检查接收数据 客户端(mbox_client) 通过 mbox_send_message() 调用 触发 send_data 回调 数据流方向:客户端 → mbox_controller → mbox_chan → send_data → 硬件寄存器

3.5 注册与注销

驱动初始化时,你需要分配并填充 mbox_controller,然后调用:

int mbox_controller_register(struct mbox_controller *mbox);

注销时:

void mbox_controller_unregister(struct mbox_controller *mbox);

框架内部会帮你管理通道的生命周期。你只需要保证 chans 数组和 ops 在注册前已经准备好。

小技巧:我习惯在 probe 函数里用 devm_kzalloc 分配 mbox_controller 和 chans 数组,这样不用手动释放。内核资源管理框架会帮你收拾残局。

3.6 避坑指南

  • 通道索引别搞错:of_xlate 回调里解析设备树时,返回的通道索引必须和 chans 数组对应。我见过有人返回了越界索引,内核直接 oops。
  • 中断上下文别睡:send_data 可能在原子上下文调用,千万别用 mutex,用 spinlock。
  • 寄存器操作要加锁:多个通道可能共享同一个控制寄存器,操作前记得拿锁。
  • 轮询周期别设太短:txpoll_period 设 1ms 以下会导致 CPU 占用飙升。我一般设 10ms,够用。