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 框架的核心关系,我画的时候特意把数据流和回调路径标清楚了:
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,够用。