3. RPMsg协议栈架构

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>;
    };
};
我的经验:设备树里的reg地址一定要和远程处理器实际使用的共享内存地址一致。我曾经遇到过地址写错一位,结果vring初始化时直接访问了非法地址,系统直接挂掉。排查了整整一天才发现是设备树地址偏移了0x1000。

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;
    }
    
    // ... 后续初始化
}
注意:of_platform_populate()只创建compatible属性匹配的子节点。如果vring节点没有compatible属性,或者compatible值写错了,子设备根本不会创建。我见过有人把"virtio,mmio"写成了"virtio-mmio",少了个逗号,结果驱动匹配不上。

3.3 virtio设备注册与vring初始化

当vring子节点被创建为platform设备后,virtio-mmio驱动会匹配并probe。这一步是RPMsg初始化的核心——它建立了通信的物理通道。

virtio-mmio驱动的probe函数大致流程如下:

  1. 映射寄存器:将reg地址映射到内核虚拟地址空间
  2. 注册中断:申请设备树中指定的中断号
  3. 初始化virtio设备:设置virtio设备的特征位、操作函数等
  4. 注册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初始化完整流程图 设备树解析 platform_device创建 virtio-mmio驱动probe register_virtio_device() rpmsg驱动匹配probe 关键点 rpmsg_bus在系统初始化 早期已注册,但设备匹配 发生在virtio设备注册后 常见问题 设备树compatible不匹配 vring地址不一致 中断号冲突

从流程图可以看出,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;
}
我的建议:调试初始化问题时,可以在每个关键步骤加dev_info打印。我习惯在设备树解析、platform probe、virtio probe、rpmsg probe四个位置都加打印。这样一旦初始化失败,看dmesg就知道卡在哪一步了。

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注册,每一步都有它的意义。你把这些环节都吃透了,以后遇到初始化问题,就能像老中医一样——望闻问切,对症下药。