ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

QEMU 模拟 RISC-V AI 芯片:PCIe 设备建模与驱动验证实战

QEMU 模拟 RISC-V AI 芯片:PCIe 设备建模与驱动验证实战 1. 为什么我要用 QEMU 给 RISC-V AI 芯片搭一张“实验台”第一次接触 RISC-V AI 芯片验证的人十有八九会卡在同一个地方板子还没到或者板子到了但不敢随便刷固件可驱动、固件、PCIe 枚举这些东西又必须尽早跑起来。我当初也是这个状态手里只有一份芯片手册和一堆待验证的寄存器定义硬件团队还在画 PCB软件这边总不能干等着。于是我把目光投向了 QEMU——用纯软件的方式先把一颗“虚拟的 RISC-V AI 芯片”拉起来让驱动开发、PCIe 枚举逻辑、DMA 通路验证这些工作提前跑通。这套思路的核心就是用QEMU模拟一颗带PCIe总线的RISC-VSoC再挂上一个模拟的 AI 加速器设备形成一个可反复重启、可随意改参数、可断点调试的实验台。它解决的不是“替代真实芯片”的问题而是“在真实芯片可用之前把软件栈的骨架先立起来”的问题。适合谁看做 RISC-V 平台固件、Linux 驱动、PCIe 设备枚举、AI 加速器 runtime 的工程师以及想入门 QEMU 设备建模的开发者。哪怕你之前只会在 QEMU 里跑个 ARM 虚拟机这篇也能带你往前走一大步。我先把结论摆在这儿QEMU 搭 RISC-V AI 芯片实验台难点不在“跑起来”而在“跑得像”。跑起来只要一条命令跑得像则需要你理解 RISC-V 的地址空间布局、PCIe 的配置空间机制、以及 QEMU 设备模型里 MMIO 和 DMA 是怎么被拦截和转发的。后面我会把这些一层层拆开讲包括我踩过的几个坑比如 PCIe 枚举时 BAR 分配对不上、DMA 地址翻译走错窗口、以及中断路由配错导致驱动一直 probe 失败。2. 实验台的整体架构一颗虚拟芯片由哪些部件拼成2.1 从“芯片”到“QEMU 机器”的映射关系真实世界里的一颗 RISC-V AI 芯片通常包含几个关键部分一个或多个 RISC-V 核心可能是 RV64GC也可能带向量扩展、一级/二级缓存、片上互联总线比如 AXI 或 TileLink、内存控制器、以及挂在外设总线上的 PCIe Root Complex。AI 加速器一般作为 PCIe Endpoint 挂在 Root Complex 下面或者直接作为 SoC 内部的一个 MMIO 设备。在 QEMU 里这套结构被拆成两层Machine机器和Device设备。Machine 负责描述 CPU 类型、内存布局、中断控制器、总线拓扑Device 负责描述具体外设的行为。我们要做的就是选一个接近目标芯片的 RISC-V Machine 作为底座然后自己写或改一个 PCIe 设备模型来代表 AI 加速器。我当时的做法是以virt机器为起点因为它自带 PCIe Host Bridgegpio-keys、virtio这些都能挂CPU 选rv64内存给 2GB然后在这个基础上加一个自定义的 PCIe Endpoint。这样既省去了从零写 Machine 的工作量又能保证 PCIe 拓扑是真实可用的。2.2 为什么选virt而不是自己写 Machine有人会问既然要模拟特定芯片为什么不干脆写一个全新的 Machine我的经验是除非你的芯片地址空间和标准平台差异极大否则不要从零写 Machine。原因有三点。第一virt机器已经把 RISC-V 的 CLINT本地中断、PLIC平台级中断控制器、UART、RTC 这些基础设施都实现好了你自己写一遍纯属重复劳动而且很容易在中断号映射上出错。第二virt的 PCIe Host Bridge 是经过大量测试的ECAM 配置空间访问、BAR 分配、MSI 中断这些机制都能直接复用。第三QEMU 的 Machine 定义和 Device 定义是解耦的你完全可以在virt上挂自定义设备不需要动 Machine 本身。当然virt也有它的局限。比如它的 PCIe 总线默认只有一条如果你想模拟多 Root Complex 或者复杂的 PCIe Switch 拓扑就需要额外配置。但对于第一版实验台来说单 Root Complex 加一个 Endpoint 已经足够验证大部分驱动逻辑了。2.3 AI 加速器在实验台里的角色定位这里要明确一点我们在 QEMU 里模拟的 AI 加速器不是要模拟它的算力而是要模拟它的寄存器接口和 DMA 行为。真实芯片上驱动通过 MMIO 读写加速器的控制寄存器通过 DMA 把输入数据搬到加速器内部 SRAM计算完成后再通过中断通知驱动取结果。QEMU 设备模型要做的就是把这些寄存器读写拦截下来把 DMA 请求映射到宿主机的内存把中断注入到 guest 的中断控制器。所以我们的设备模型核心就三件事MMIO 区域注册、DMA 地址空间注册、中断线连接。这三件事做对了驱动就能正常 probe至于加速器内部是“真的在算”还是“假装在算”对软件栈验证来说区别不大。我甚至见过有人用 QEMU 设备模型模拟一个“永远返回固定结果”的加速器专门用来测驱动的错误处理路径效果很好。3. 环境准备从零到能跑 RISC-V 的最小闭环3.1 工具链和 QEMU 版本的选择工欲善其事必先利其器。RISC-V 的交叉编译工具链和 QEMU 版本匹配是个老生常谈的问题。我推荐用QEMU 7.0 以上的版本因为从 7.0 开始RISC-V 的 PCIe 支持明显完善了很多尤其是 ECAM 和 MSI 相关的实现。工具链方面用riscv64-unknown-linux-gnu-或者riscv64-linux-gnu-都行关键是 glibc 版本要和你的 rootfs 对上。安装 QEMU 时编译选项里一定要带上--target-listriscv64-softmmu如果你还想跑 ARM 做对比可以加上aarch64-softmmu。我习惯从源码编译因为发行版自带的 QEMU 往往版本偏旧而且不一定开了 RISC-V 的 PCIe 特性。编译命令大概是这样./configure --target-listriscv64-softmmu --enable-debug --enable-slirp make -j$(nproc)--enable-debug在开发设备模型时非常有用可以让你在 QEMU 内部打日志、下断点。--enable-slirp是为了后面 guest 能上网方便 apt 装包。3.2 构建一个能启动的 RISC-V Linux 镜像实验台要跑驱动就得有一个能启动的 Linux。最省事的办法是用 Buildroot 或 Yocto 生成一个最小 rootfs但如果你只是想快速验证可以直接用现成的发行版镜像。我常用的是 Ubuntu 的 RISC-V 预编译镜像配合virt机器和 OpenSBI 固件。启动命令的核心参数包括-M virt、-cpu rv64、-m 2G、-kernel指定内核、-bios指定 OpenSBI、-drive指定 rootfs。这里有个细节RISC-V 的virt机器默认从0x80000000开始加载 BIOS内核一般加载在0x80200000如果你自己编译内核要确保CONFIG_PHYS_RAM_BASE和这个地址对得上否则启动会直接卡死连串口输出都没有。我第一次搭的时候就在这里卡了半天串口一片空白后来用-d in_asm看指令流才发现内核根本没被加载到正确地址。所以建议先用现成的 OpenSBI 和内核组合跑通再换自己的。3.3 验证 PCIe 总线是否真的存在系统起来之后第一件事不是急着挂设备而是确认 PCIe 总线本身是活的。在 guest 里执行lspci如果能看到Host bridge和几个PCI bridge说明 Root Complex 已经枚举成功了。如果lspci报 “No such file or directory”那多半是内核没开CONFIG_PCI或者CONFIG_PCI_HOST_GENERIC。我还会看一眼/proc/iomem确认 PCIe 的 MMIO 窗口和 ECAM 窗口有没有被正确保留。virt机器上ECAM 通常在0x30000000附近MMIO 窗口在0x40000000往上。这些地址在写设备模型时要用到因为你的 BAR 必须落在这个窗口里否则内核枚举时会直接跳过你的设备。提示如果你在lspci里看不到任何设备先检查 QEMU 启动参数里有没有-device pcie-root-port或者类似的桥设备。virt机器默认只给一个 Root ComplexEndpoint 需要显式挂上去。4. 写一个能被 Linux 认出来的 PCIe AI 加速器模型4.1 PCIe 配置空间的“身份证”怎么填Linux 枚举 PCIe 设备时第一步是读配置空间的 Vendor ID 和 Device ID。如果这两个值不合法比如全0xFFFF内核会认为这个 slot 上没有设备。所以我们的设备模型第一件事就是定义这两个 ID。我一般会用一个还没被官方分配的 Vendor ID比如0x1234Device ID 用0x5678Class Code 设成0x020000代表网络控制器或者0x0B4000代表协处理器具体看你的驱动怎么匹配。配置空间里还有几个关键字段BAR0用来映射控制寄存器BAR1用来映射 DMA 描述符环Interrupt Pin和Interrupt Line决定中断怎么走。如果你要用 MSI/MSI-X还需要在 Capability 链表里加上对应的 Capability 结构。QEMU 提供了pci_config_set_vendor_id、pci_config_set_device_id这些辅助函数用起来很方便。我踩过的一个坑是BAR 的大小必须是对齐的 2 的幂。比如你想映射 4KB 的寄存器空间BAR 的 size 就要设成 4KB而且基地址要 4KB 对齐。如果设成 3KB内核在分配 BAR 时会算出错误的窗口导致 MMIO 访问全部落到非法地址。这个错误在 QEMU 日志里表现为 “invalid access”但不会告诉你具体是 BAR 的问题得自己一点点排查。4.2 MMIO 读写驱动和硬件之间的“对话通道”驱动 probe 的时候会先读一下设备的寄存器确认硬件版本号或者状态位。我们的设备模型要在 MMIO 读回调里返回合理的值。比如偏移0x00返回版本号0x00010000偏移0x04返回状态位0x01表示就绪。写回调则要处理驱动下发的命令比如启动 DMA、清除中断、配置工作模式。这里有个经验不要把所有寄存器都实现成可读可写。真实硬件里有些寄存器是只读的有些是写 1 清除的有些是保留位。如果你的模型对所有写操作都照单全收驱动可能会因为读到不该读的值而进入异常分支。我建议对照芯片手册把每个寄存器的行为都实现清楚哪怕暂时用不到也先留个桩。另外MMIO 访问的宽度也要注意。驱动可能用 32 位访问也可能用 64 位访问。QEMU 的MemoryRegionOps里可以指定valid.min_access_size和valid.max_access_size我一般设成 4 到 8 字节太小或太大的访问直接返回错误这样能尽早暴露驱动里的对齐问题。4.3 DMA 地址翻译别让数据搬错地方DMA 是 AI 加速器模型里最容易出错的部分。驱动会把一段物理地址写到 DMA 描述符寄存器里然后启动传输。QEMU 设备模型拿到这个地址后不能直接当成宿主机的虚拟地址用而要通过address_space_map或者dma_memory_read/write接口把它翻译成 guest 物理地址对应的内存。这里的关键是你要用哪个 AddressSpace。如果设备挂在 PCIe 总线上应该用pci_get_address_space拿到设备自己的地址空间这样 IOMMU 的翻译才能生效。如果你直接用address_space_memory那就绕过了 IOMMU在启用 SMMU 的场景下会出错。我遇到过一个典型问题驱动分配的 DMA buffer 在 guest 物理地址0x80000000但设备模型用错了 AddressSpace结果读到的全是零。后来在 QEMU 里加了一行日志把翻译前后的地址都打出来才发现是 AddressSpace 选错了。这个坑很隐蔽因为不会报错只是数据不对。4.4 中断注入让驱动知道“活干完了”DMA 传输完成后设备要发中断通知驱动。如果是传统 INTx 中断就调用pci_irq_assert拉高中断线如果是 MSI/MSI-X就调用msi_notify发送消息。QEMU 的 PCIe 设备模型里PCIDevice结构体自带irq字段用起来不算复杂但要注意中断的触发时机和清除逻辑。我建议在设备模型里维护一个中断状态寄存器驱动写 1 清除。这样即使中断线有毛刺驱动也能通过读状态寄存器确认事件。另外MSI 的 vector 数量要和配置空间里声明的一致否则内核在申请中断时会失败。我见过有人声明了 8 个 vector但设备模型只实现了 1 个结果驱动 probe 到一半就报 “failed to allocate MSI vectors”。5. 把设备挂上总线QEMU 命令行与设备树的配合5.1 用-device参数把 AI 加速器插到 PCIe 槽位设备模型写好后编译进 QEMU就可以用-device参数挂载了。命令大概长这样qemu-system-riscv64 -M virt -cpu rv64 -m 2G \ -kernel Image -bios fw_jump.bin \ -drive filerootfs.img,formatraw,idhd0 \ -device virtio-blk-device,drivehd0 \ -device my-ai-accel,buspcie.0,addr0x1 \ -nographic这里的buspcie.0表示挂在 Root Complex 的第一条总线上addr0x1是设备号。如果你有多个设备地址不能冲突。启动后在 guest 里lspci -nn应该能看到你的设备Vendor ID 和 Device ID 就是你在配置空间里填的那两个。如果看不到设备先检查 QEMU 启动日志里有没有 “device my-ai-accel not found”那说明设备模型没编译进去或者名字写错了。如果设备出现了但驱动没 probe那就要看dmesg里的报错通常是 BAR 分配失败或者中断申请失败。5.2 设备树里要不要写 PCIe 节点virt机器的设备树是 QEMU 动态生成的PCIe Host Bridge 的节点已经包含在内一般不需要手动改。但如果你的设备有特殊的中断路由需求或者你想在设备树里描述设备的 MMIO 窗口那就需要在 QEMU 的 Machine 代码里修改设备树生成逻辑。我的建议是第一版尽量不改设备树让内核通过标准 PCIe 枚举发现设备。等驱动跑通了再考虑加设备树节点做精细化配置。因为改设备树会引入额外的变量一旦出问题你分不清是设备模型的问题还是设备树的问题。5.3 启动参数里那些容易忽略的细节有几个启动参数很容易被忽略但影响很大。-nographic会把串口重定向到终端方便看内核日志。-s和-S可以开启 GDB stub让你在 QEMU 启动时暂停方便调试设备模型的初始化流程。-d trace:my_ai_accel_*可以打开你自定义的 trace 点观察 MMIO 和 DMA 的调用情况。还有一个坑内存大小和 DMA 窗口的关系。如果你给 guest 2GB 内存但 PCIe 的 MMIO 窗口只有 256MB那驱动分配 DMA buffer 时可能会落到窗口之外导致设备模型访问不到。我一般会把 MMIO 窗口设大一点或者在驱动里限制 DMA mask确保 buffer 落在可访问范围内。6. 实测中暴露的问题与排查链路6.1 驱动 probe 失败从dmesg倒推问题驱动 probe 失败是最常见的问题dmesg里通常会有一行错误比如 “BAR 0: no space” 或者 “IRQ 0: nobody cared”。我的排查顺序是先看lspci -vv确认 BAR 有没有被分配再看/proc/interrupts确认中断有没有注册最后看 QEMU 日志确认 MMIO 访问有没有被正确拦截。有一次我遇到 “BAR 0: no space”查了半天发现是 BAR 的 size 设成了 8KB但 PCIe 窗口只剩 4KB 了。把 size 改成 4KB 后问题解决。这个错误其实 QEMU 在枚举时就会打印警告只是被其他日志淹没了需要仔细翻。6.2 DMA 数据不对地址翻译和缓存一致性DMA 数据不对通常有两个原因地址翻译错误或者缓存一致性没处理好。地址翻译的问题前面说过用错 AddressSpace 会导致读到零。缓存一致性则更隐蔽如果 guest 里驱动没有正确执行 cache flush设备模型读到的可能是旧数据。在 QEMU 里因为内存是共享的缓存一致性问题不像真实硬件那么明显但如果你用了dma_memory_read的attrs参数指定了MEMTXATTRS_UNSPECIFIEDQEMU 会走默认路径一般没问题。我建议在设备模型里加一个校验逻辑比如 DMA 完成后回读一段数据和预期值对比不一致就打印警告。6.3 中断风暴MSI 没清除导致的连锁反应中断风暴是另一个常见问题。如果设备模型在 DMA 完成后一直拉高中断线而驱动又没有及时清除就会导致中断不断触发系统卡死。我的做法是在设备模型里加一个“中断已发送”标志只有驱动写了清除寄存器后才允许下一次中断。MSI 场景下还要注意 vector 的 masking。如果驱动暂时屏蔽了某个 vector设备模型不应该再往那个 vector 发消息。QEMU 的msi_notify会检查 vector 的 mask 状态但前提是你在配置空间里正确实现了 MSI-X Table 的读写回调。7. 几个让实验台更好用的进阶技巧7.1 用 trace 点替代 printf 调试在设备模型里加printf是最直接的调试方式但日志多了会拖慢 QEMU而且很难按条件过滤。QEMU 自带的 trace 框架支持在运行时开关 trace 点性能开销小还能按模式过滤。我一般会给 MMIO 读写、DMA 启动、中断注入这几个关键路径各加一个 trace 点调试时用-d trace:my_ai_accel_mmio_read打开平时关掉。定义 trace 点需要在trace-events文件里加一行然后在代码里用trace_my_ai_accel_mmio_read(addr, size, value)调用。编译时会自动生成对应的头文件用起来很顺手。7.2 用 QEMU monitor 动态查看设备状态QEMU monitor 里有个info pci命令可以列出所有 PCIe 设备的配置空间和 BAR 分配情况。调试时我经常一边在 guest 里跑驱动一边在 monitor 里敲info pci对比两边看到的状态是否一致。如果不一致说明设备模型的配置空间实现有问题。还有一个info mtree命令可以列出所有的 MemoryRegion包括你的设备注册的 MMIO 区域。如果 MMIO 区域没出现在列表里那说明注册失败了驱动访问时会直接落到默认的 unassigned 区域读到全0xFF。7.3 把实验台脚本化一键复现最后一点也是我觉得最重要的一点把整个实验台的启动过程脚本化。包括 QEMU 编译、镜像准备、启动命令、guest 内的驱动加载全部写成一个 shell 脚本或者 Makefile。这样每次改完设备模型一条命令就能重新跑一遍不用手动敲一堆参数。我自己的脚本里还会加上-snapshot参数让 rootfs 的修改不落盘每次启动都是干净状态。配合-s和-S还能在启动时挂 GDB方便调试设备模型的初始化。这套流程跑顺之后验证一个新寄存器或者一个新中断路径从改代码到看到结果五分钟以内就能完成。注意脚本里不要写死路径用变量或者环境变量方便在不同机器上迁移。尤其是 QEMU 的安装路径和工具链的前缀不同环境差异很大。这套 QEMU 实验台我用了大半年从最初的“能启动”到后来的“能跑完整驱动”中间踩的坑基本都写在上面的章节里了。它最大的价值不是替代真实硬件而是让你在硬件到位之前把软件栈里那些和硬件无关的逻辑全部验证一遍。等真实芯片回来你只需要关注那些 QEMU 模拟不了的部分比如时序、功耗、模拟特性效率会高很多。
返回列表