3. RPMsg初始化流程:从设备树解析到rpmsg_bus注册的完整链路
RPMsg的初始化流程,说白了就是回答一个问题:系统怎么知道有两个核要通信?它们用什么方式通信?
我刚开始接触RPMsg时,以为它就是个简单的消息传递机制,配几个参数就能跑。结果第一次调板子,rpmsg_bus根本没注册成功,连设备树节点都没解析到。后来我才明白,这整个链路从设备树解析、platform设备创建、virtio驱动匹配,到rpmsg_bus注册,每一步都可能出问题。
这一章,我就带你走一遍完整的初始化流程。你跟着我的思路,把每个环节都搞清楚,以后遇到初始化失败的问题,心里就有底了。
3.1 设备树中的RPMsg节点
一切从设备树开始。RPMsg的通信双方,在设备树里通常表现为一个remoteproc节点和一个virtio节点。我习惯把设备树看作系统的「硬件描述语言」,它告诉内核:这里有个远程处理器,它用这种方式和我通信。
一个典型的RPMsg设备树节点长这样:
// 远程处理器节点
remoteproc0: remoteproc@30000000 {
compatible = "ti,omap4-remoteproc";
reg = <0x30000000 0x100000>;
interrupts = <GIC_SPI 80 IRQ_TYPE_LEVEL_HIGH>;
firmware = "rproc-m4-fw.elf";
// 关键:vring和vdev的描述
vring0: vring@30010000 {
compatible = "virtio,mmio";
reg = <0x30010000 0x1000>;
interrupts = <GIC_SPI 81 IRQ_TYPE_LEVEL_HIGH>;
};
vring1: vring@30011000 {
compatible = "virtio,mmio";
reg = <0x30011000 0x1000>;
interrupts = <GIC_SPI 82 IRQ_TYPE_LEVEL_HIGH>;
};
};
3.2 从设备树到platform设备
设备树被内核解析后,每个compatible匹配的节点都会生成一个platform_device。这个过程是内核的设备树子系统自动完成的,但有几个关键点你要注意:
- compatible属性必须匹配:驱动里的of_match_table必须包含设备树中的compatible字符串
- reg和interrupts资源:会被自动转换为platform设备的IORESOURCE_MEM和IORESOURCE_IRQ
- 子节点处理:vring这样的子节点,通常由父节点的驱动手动解析
我举个例子,remoteproc驱动在probe时,会调用of_platform_populate()来创建子节点的platform设备:
static int omap_remoteproc_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node;
int ret;
// 解析remoteproc私有数据
ret = rproc_of_parse_firmware(dev, np, "firmware");
if (ret)
return ret;
// 创建子节点(vring等)的platform设备
ret = of_platform_populate(np, NULL, NULL, dev);
if (ret) {
dev_err(dev, "failed to populate child nodes\n");
return ret;
}
// ... 后续初始化
}
3.3 virtio设备注册与vring初始化
当vring子节点被创建为platform设备后,virtio-mmio驱动会匹配并probe。这一步是RPMsg初始化的核心——它建立了通信的物理通道。
virtio-mmio驱动的probe函数大致流程如下:
- 映射寄存器:将reg地址映射到内核虚拟地址空间
- 注册中断:申请设备树中指定的中断号
- 初始化virtio设备:设置virtio设备的特征位、操作函数等
- 注册virtio设备:调用register_virtio_device()将其注册到virtio总线
这里有个关键点:vring的地址和大小必须在主核和从核之间达成一致。我习惯在设备树中明确指定vring的物理地址,而不是让驱动动态分配。原因很简单——远程处理器不知道动态分配到了哪里。
static int virtio_mmio_probe(struct platform_device *pdev)
{
struct virtio_mmio_device *vm_dev;
struct resource *mem;
int ret;
vm_dev = devm_kzalloc(&pdev->dev, sizeof(*vm_dev), GFP_KERNEL);
if (!vm_dev)
return -ENOMEM;
// 映射寄存器空间
mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
vm_dev->base = devm_ioremap_resource(&pdev->dev, mem);
if (IS_ERR(vm_dev->base))
return PTR_ERR(vm_dev->base);
// 申请中断
vm_dev->irq = platform_get_irq(pdev, 0);
ret = devm_request_irq(&pdev->dev, vm_dev->irq,
virtio_mmio_interrupt, 0,
dev_name(&pdev->dev), vm_dev);
if (ret)
return ret;
// 初始化virtio设备
vm_dev->vdev.id.device = readl(vm_dev->base + VIRTIO_MMIO_DEVICE_ID);
vm_dev->vdev.id.vendor = readl(vm_dev->base + VIRTIO_MMIO_VENDOR_ID);
vm_dev->vdev.config = &virtio_mmio_config_ops;
// 注册virtio设备
ret = register_virtio_device(&vm_dev->vdev);
if (ret)
return ret;
platform_set_drvdata(pdev, vm_dev);
return 0;
}
3.4 rpmsg_bus的注册时机
好了,现在virtio设备已经注册到virtio总线上了。但rpmsg_bus呢?它什么时候注册的?
答案是:rpmsg_bus在系统初始化早期就注册了,但它真正「激活」是在virtio设备匹配到rpmsg驱动之后。
我画个流程图,你一看就明白:
从流程图可以看出,rpmsg_bus的注册其实分两步:
| 阶段 | 触发时机 | 主要工作 |
|---|---|---|
| 总线注册 | 系统初始化(postcore_initcall) | 注册rpmsg_bus类型,初始化总线属性 |
| 设备匹配 | virtio设备注册后 | rpmsg驱动probe,创建rpmsg设备 |
3.5 rpmsg驱动probe:最后的拼图
当virtio设备注册到virtio总线后,内核会遍历virtio总线上的驱动,找到compatible的驱动进行匹配。对于RPMsg来说,这个驱动就是rpmsg_virtio.c中的驱动。
rpmsg驱动的probe函数做了几件重要的事:
- 创建rpmsg设备:调用rpmsg_create_virtio_device()
- 初始化vring:分配vring缓冲区,设置通知机制
- 注册rpmsg通道:创建默认的rpmsg通道,供上层使用
- 启动远程处理器:调用rproc_boot()启动从核
这里有个细节:rpmsg驱动probe时,远程处理器可能还没启动。所以vring的初始化需要和远程处理器协商好——谁分配缓冲区,谁负责通知。我见过一种做法是主核分配所有vring缓冲区,然后把物理地址通过共享内存告诉从核。
static int rpmsg_virtio_probe(struct virtio_device *vdev)
{
struct virtproc_info *vrp;
int ret;
vrp = kzalloc(sizeof(*vrp), GFP_KERNEL);
if (!vrp)
return -ENOMEM;
// 初始化vring
ret = rpmsg_init_vring(vrp, vdev);
if (ret) {
dev_err(&vdev->dev, "failed to init vring\n");
goto free_vrp;
}
// 创建rpmsg设备
ret = rpmsg_register_device(vrp, vdev);
if (ret) {
dev_err(&vdev->dev, "failed to register rpmsg device\n");
goto free_vring;
}
// 启动远程处理器
ret = rproc_boot(vrp->rproc);
if (ret) {
dev_err(&vdev->dev, "failed to boot remote processor\n");
goto unregister_device;
}
dev_info(&vdev->dev, "rpmsg virtio device initialized\n");
return 0;
unregister_device:
rpmsg_unregister_device(vrp);
free_vring:
rpmsg_deinit_vring(vrp);
free_vrp:
kfree(vrp);
return ret;
}
3.6 常见初始化失败场景
说了这么多理论,来点实战的。我总结了几种最常见的初始化失败场景,你遇到了可以直接对照排查:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| dmesg没有remoteproc相关打印 | 设备树节点没被解析 | 检查compatible是否匹配,检查设备树编译是否正确 |
| virtio-mmio probe失败 | reg地址冲突或中断号被占用 | cat /proc/iomem 查看地址使用情况 |
| vring初始化失败 | 共享内存地址不一致或大小不足 | 核对设备树和固件中的vring地址 |
| rpmsg设备注册成功但无法通信 | 远程处理器没启动或固件有问题 | 检查rproc_boot返回值,查看从核启动日志 |
嗯,这里要注意一点:不要只看错误打印,也要看成功打印。有时候驱动probe成功了,但后续的通信初始化失败了,错误信息可能被掩盖。我习惯在dmesg里搜"rpmsg"、"virtio"、"remoteproc"这几个关键词,把所有相关日志都过一遍。
好了,RPMsg的初始化流程就讲到这里。从设备树解析到rpmsg_bus注册,每一步都有它的意义。你把这些环节都吃透了,以后遇到初始化问题,就能像老中医一样——望闻问切,对症下药。