QEMU 搭建 RISC-V AI 芯片虚拟实验台:PCIe 设备建模与 Agent 自动化验证
1. 为什么我要用 QEMU 给 RISC-V AI 芯片搭一个实验台第一次接触 RISC-V AI 芯片验证的人十有八九会被同一个问题卡住手里只有一份 RTL 代码或者一块还没回片的工程样片怎么在流片之前把驱动、固件、PCIe 枚举这些软件栈跑通直接上 FPGA 当然可以但 FPGA 的编译周期动辄几个小时调试手段也远不如软件仿真灵活。我自己的做法是先用 QEMU 拉起一个 RISC-V 虚拟平台把 AI 芯片抽象成一个 PCIe 设备挂上去让操作系统、驱动、Agent 控制逻辑都能在这个虚拟实验台上先跑起来。这个思路的核心价值在于把硬件依赖和软件验证解耦。QEMU 本身支持 RISC-V 指令集模拟也支持 PCIe 总线建模我们可以用 QEMU 的-device参数把一个自定义的 PCIe 端点设备挂到虚拟总线上操作系统启动后能通过标准的 PCIe 枚举流程发现它加载对应的驱动然后进行寄存器读写、DMA 传输等操作。整个过程不需要真实的芯片也不需要 FPGA一台普通的 x86 开发机就能跑。适合谁来参考这篇内容如果你是做 RISC-V 芯片验证的工程师或者正在开发 AI 加速卡的驱动和固件又或者你是一个对 Agent 控制硬件感兴趣的软件开发者这套方法都能帮你省下大量等待硬件的时间。我下面会把整个搭建过程拆开讲包括 QEMU 的编译选项、RISC-V 虚拟平台的配置、PCIe 设备的建模方式、以及怎么用 Agent 来自动化整个验证流程。提示这篇文章假设你已经对 Linux 命令行、交叉编译、设备树有基本了解。如果这些概念你还不熟建议先补一下基础否则后面有些步骤会看得比较吃力。2. 整体方案设计与关键选型考量2.1 为什么选 QEMU 而不是其他模拟器市面上能模拟 RISC-V 的工具不止 QEMU 一个Spike、Renode、以及一些商业仿真器都能做指令级模拟。但 QEMU 有几个不可替代的优势第一它支持完整的系统级模拟包括 MMU、中断控制器、PCIe 总线这对我们要验证的 AI 芯片场景至关重要第二QEMU 的设备模型可以用 C 语言编写也可以直接用 QEMU 提供的-device参数加载预编译的设备灵活性很高第三QEMU 的社区活跃RISC-V 相关的补丁合并速度快遇到问题容易找到参考。Spike 虽然是指令集模拟的黄金参考但它本质上是一个 ISA 模拟器没有完整的外设总线模型跑不了 Linux 内核和 PCIe 枚举。Renode 在嵌入式场景下很好用但对 PCIe 的支持不如 QEMU 成熟。所以如果你的目标是验证一个挂在 PCIe 总线上的 AI 加速器QEMU 是目前最务实的选择。2.2 RISC-V 虚拟平台的目标配置我给自己定的目标是模拟一个双核 RV64GC 处理器主频不限QEMU 不模拟真实时钟频率内存 4GB挂一个 virtio 网卡用于网络通信再挂一个自定义的 PCIe AI 加速器设备。操作系统用 Linux 6.x根文件系统用 Buildroot 构建。这个配置足够跑通驱动加载、PCIe 枚举、DMA 测试这些核心流程。为什么选 RV64GC 而不是 RV32因为 AI 芯片的驱动和固件通常需要 64 位地址空间来处理大块 DMA 缓冲区32 位地址空间在映射几 GB 的模型数据时会非常吃力。另外Linux 内核对 RV64 的支持也比 RV32 更完善。2.3 PCIe 设备建模的策略选择QEMU 里建模 PCIe 设备有三种常见做法第一种是直接用 QEMU 已有的设备模型比如virtio系列改改参数就能用第二种是写一个 QEMU 设备模型插件用 C 语言实现寄存器和 DMA 行为第三种是用 QEMU 的-device pci-testdev或者educator这类教学设备先跑通流程再替换成自定义设备。我建议新手从第三种开始先用pci-testdev把 PCIe 枚举和驱动加载的流程跑通确认操作系统能正确识别设备、分配 BAR 空间、加载驱动。然后再逐步替换成自定义设备。这样每一步都有明确的验证点出了问题也容易定位。注意QEMU 的 PCIe 设备模型默认挂在pcie.0总线上如果你需要模拟 PCIe Switch 或者多级拓扑需要在 QEMU 命令行里显式添加x3130-upstream和xio3130-downstream设备。这个后面会详细讲。3. 环境准备与 QEMU 编译实操3.1 基础工具链安装在开始编译 QEMU 之前先把基础工具链装好。我用的开发机是 Ubuntu 22.04其他发行版的包名可能略有不同但大体思路一致。sudo apt update sudo apt install -y git build-essential ninja-build pkg-config \ libglib2.0-dev libpixman-1-dev libfdt-dev \ gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu \ device-tree-compiler这里有几个包需要特别说明libglib2.0-dev和libpixman-1-dev是 QEMU 的核心依赖缺一个都编译不过libfdt-dev是设备树操作库RISC-V 平台配置离不开它gcc-riscv64-linux-gnu是 RISC-V 交叉编译器用来编译内核和驱动device-tree-compiler用来把.dts编译成.dtb。3.2 QEMU 源码编译与配置QEMU 的编译配置选项很多我一般只开需要的目标架构这样编译速度快很多。下面是完整的编译流程git clone https://gitlab.com/qemu-project/qemu.git cd qemu git checkout v8.2.0 ./configure --target-listriscv64-softmmu \ --enable-debug \ --enable-trace-backendslog \ --disable-werror make -j$(nproc)--target-listriscv64-softmmu表示只编译 RISC-V 64 位系统模拟不编译用户态模拟和其他架构这样能把编译时间从半小时压缩到十分钟左右。--enable-debug打开调试符号方便后面用 GDB 跟踪 QEMU 内部行为。--disable-werror是为了避免一些编译器警告被当成错误导致编译中断。编译完成后在build/目录下会生成qemu-system-riscv64可执行文件。你可以用./qemu-system-riscv64 --version确认版本信息。3.3 验证 QEMU 能否正常启动在编译内核之前先用 QEMU 自带的virt机器类型做个快速验证./qemu-system-riscv64 -M virt -m 2G -nographic \ -bios default如果看到 OpenSBI 的启动日志说明 QEMU 本身工作正常。按CtrlA然后按X可以退出 QEMU。这一步看起来简单但能帮你排除掉很多环境问题比如动态库缺失、权限不足等。实操心得如果你在编译 QEMU 时遇到ERROR: glib-2.56 gthread-2.0 is required这类错误八成是libglib2.0-dev没装或者版本太低。Ubuntu 20.04 自带的 glib 版本可能不够建议升级到 22.04 或者手动编译新版本 glib。4. RISC-V Linux 内核与根文件系统构建4.1 内核配置的关键选项QEMU 的virt机器类型支持很多外设但默认配置不一定包含 PCIe 相关的驱动。我们需要自己编译内核确保以下选项被打开git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git cd linux git checkout v6.6 make ARCHriscv CROSS_COMPILEriscv64-linux-gnu- defconfig然后手动调整配置make ARCHriscv CROSS_COMPILEriscv64-linux-gnu- menuconfig需要确认的选项包括CONFIG_PCIyPCI 总线支持CONFIG_PCI_HOST_GENERICy通用 PCI 主机控制器CONFIG_PCIEPORTBUSyPCIe 端口总线支持CONFIG_PCIE_ECAMyECAM 配置空间访问CONFIG_VIRTIO_PCIyvirtio PCI 设备驱动CONFIG_SERIAL_8250y串口驱动用于控制台输出这些选项在defconfig里大部分已经打开了但CONFIG_PCIEPORTBUS有时候需要手动勾选。如果你后面要验证 PCIe 热插拔功能还需要打开CONFIG_HOTPLUG_PCI_PCIEy。编译内核make ARCHriscv CROSS_COMPILEriscv64-linux-gnu- -j$(nproc)编译完成后内核镜像在arch/riscv/boot/Image设备树在arch/riscv/boot/dts/下面。4.2 用 Buildroot 构建根文件系统根文件系统我用 Buildroot 来构建因为它能自动处理交叉编译和依赖关系比手动搭 BusyBox 省事很多。git clone https://gitlab.com/buildroot.org/buildroot.git cd buildroot git checkout 2023.11 make qemu_riscv64_virt_defconfig make menuconfig在menuconfig里需要调整几个地方把Target packages下面的PCI utilities打开这样系统里会有lspci命令把Networking applications下面的iproute2打开方便后面配置网络如果你要用 Agent 做自动化测试还需要把Python 3和pip打开。make -j$(nproc)Buildroot 编译完成后输出在output/images/目录下包括rootfs.ext2、Image、fw_jump.bin等文件。4.3 启动脚本的编写把内核、设备树、根文件系统放到同一个目录下然后写一个启动脚本#!/bin/bash QEMU./qemu-system-riscv64 KERNEL./Image DTB./qemu-virt.dtb ROOTFS./rootfs.ext2 $QEMU -M virt -m 4G -smp 2 \ -kernel $KERNEL \ -dtb $DTB \ -drive file$ROOTFS,formatraw,idhd0 \ -device virtio-blk-pci,drivehd0 \ -netdev user,idnet0 \ -device virtio-net-pci,netdevnet0 \ -nographic \ -append root/dev/vda rw consolettyS0这个脚本里-smp 2表示双核-drive和-device virtio-blk-pci组合挂了一个 virtio 块设备作为根盘-netdev user和-device virtio-net-pci组合挂了一个用户态网络设备。-append里的root/dev/vda指定根文件系统设备consolettyS0指定串口控制台。启动后如果能看到 Linux 登录提示说明整个 RISC-V 虚拟平台已经跑起来了。你可以用lspci命令查看当前 PCIe 总线上的设备列表应该能看到 virtio 网卡和块设备。注意QEMU 的virt机器类型默认使用fw_jump.bin作为 OpenSBI 固件如果你没有指定-bios参数QEMU 会自动使用内置的 OpenSBI。但如果你要自定义 OpenSBI 版本需要显式指定-bios fw_jump.bin。5. PCIe AI 加速器设备建模与挂载5.1 用 pci-testdev 跑通 PCIe 枚举流程在写自定义设备之前先用 QEMU 自带的pci-testdev验证 PCIe 枚举流程$QEMU -M virt -m 4G -smp 2 \ -kernel $KERNEL \ -dtb $DTB \ -drive file$ROOTFS,formatraw,idhd0 \ -device virtio-blk-pci,drivehd0 \ -device pci-testdev \ -nographic \ -append root/dev/vda rw consolettyS0启动后在系统里执行lspci -vv应该能看到一个Red Hat, Inc. QEMU PCI test device。这个设备的 Vendor ID 是0x1b36Device ID 是0x0005。如果你能看到这个设备并且它的 BAR 空间被正确分配说明 PCIe 枚举流程已经跑通了。5.2 自定义 PCIe 设备模型的编写QEMU 的设备模型用 C 语言编写放在hw/misc/或者hw/pci/目录下。一个最简单的 PCIe 设备模型需要实现以下几个部分第一定义设备的 Vendor ID 和 Device ID。我一般用0x1234作为 Vendor ID0x5678作为 Device ID这样在lspci里容易识别。第二实现realize函数在函数里初始化 PCIe 配置空间、注册 BAR 区域、设置中断引脚。第三实现 MMIO 读写回调当操作系统访问 BAR 空间时QEMU 会调用你注册的回调函数。第四实现 DMA 操作AI 加速器通常需要从主机内存读取模型数据这需要用到 QEMU 的pci_dma_read和pci_dma_write接口。下面是一个简化的设备模型框架#include qemu/osdep.h #include hw/pci/pci.h #include hw/pci/pci_device.h #include qom/object.h #define TYPE_MY_AI_DEVICE my-ai-device OBJECT_DECLARE_SIMPLE_TYPE(MyAIDevice, MY_AI_DEVICE) struct MyAIDevice { PCIDevice parent_obj; MemoryRegion mmio; uint32_t regs[256]; }; static uint64_t my_ai_mmio_read(void *opaque, hwaddr addr, unsigned size) { MyAIDevice *s opaque; return s-regs[addr 2]; } static void my_ai_mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size) { MyAIDevice *s opaque; s-regs[addr 2] val; } static const MemoryRegionOps my_ai_mmio_ops { .read my_ai_mmio_read, .write my_ai_mmio_write, .endianness DEVICE_LITTLE_ENDIAN, }; static void my_ai_realize(PCIDevice *pdev, Error **errp) { MyAIDevice *s MY_AI_DEVICE(pdev); memory_region_init_io(s-mmio, OBJECT(s), my_ai_mmio_ops, s, my-ai-mmio, 0x1000); pci_register_bar(pdev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, s-mmio); } static void my_ai_class_init(ObjectClass *klass, void *data) { PCIDeviceClass *k PCI_DEVICE_CLASS(klass); k-realize my_ai_realize; k-vendor_id 0x1234; k-device_id 0x5678; k-revision 0x01; k-class_id PCI_CLASS_OTHERS; } static const TypeInfo my_ai_info { .name TYPE_MY_AI_DEVICE, .parent TYPE_PCI_DEVICE, .instance_size sizeof(MyAIDevice), .class_init my_ai_class_init, .interfaces (InterfaceInfo[]) { { INTERFACE_CONVENTIONAL_PCI_DEVICE }, { }, }, }; static void my_ai_register_types(void) { type_register_static(my_ai_info); } type_init(my_ai_register_types)把这个文件放到hw/misc/my_ai_device.c然后在hw/misc/meson.build里加上对应的编译条目重新编译 QEMU 就能用了。5.3 挂载自定义设备并验证编译完成后用下面的命令启动 QEMU$QEMU -M virt -m 4G -smp 2 \ -kernel $KERNEL \ -dtb $DTB \ -drive file$ROOTFS,formatraw,idhd0 \ -device virtio-blk-pci,drivehd0 \ -device my-ai-device \ -nographic \ -append root/dev/vda rw consolettyS0启动后在系统里执行lspci -vv应该能看到 Vendor ID 为1234、Device ID 为5678的设备。如果 BAR 空间被正确分配说明设备已经成功挂载到 PCIe 总线上。实操心得QEMU 的设备模型默认不支持 PCIe 热插拔如果你需要验证热插拔功能需要在 QEMU 命令行里加上-device pcie-root-port创建一个根端口然后把设备挂到根端口下面。具体做法是先用-device pcie-root-port,idrp0创建根端口再用-device my-ai-device,busrp0把设备挂上去。6. 用 Agent 自动化验证流程6.1 Agent 在硬件验证中的角色定位Agent 在这里不是指某个具体的软件框架而是一种自动化控制模式用一个主控程序Agent去驱动 QEMU 的启动、登录、执行命令、收集结果然后根据结果决定下一步操作。这比手动敲命令效率高得多尤其是在需要反复跑回归测试的场景下。我用的方案是用 Python 写一个 Agent通过 QEMU 的串口或者 SSH 与虚拟机交互。串口方式更通用因为不需要网络配置但解析输出比较麻烦SSH 方式需要虚拟机里跑 SSH 服务但交互更自然。我一般先用串口跑通基本流程再切换到 SSH 做复杂测试。6.2 串口交互 Agent 的实现Python 的pexpect库是处理串口交互的利器。下面是一个简化的 Agent 框架import pexpect import time class QemuAgent: def __init__(self, qemu_cmd): self.child pexpect.spawn(qemu_cmd, timeout60) self.child.logfile open(qemu_agent.log, wb) def wait_for_login(self): self.child.expect(login:) self.child.sendline(root) self.child.expect(#) def run_cmd(self, cmd): self.child.sendline(cmd) self.child.expect(#) return self.child.before.decode() def check_pcie_device(self, vendor_id, device_id): output self.run_cmd(lspci -n) target f{vendor_id}:{device_id} return target in output def close(self): self.child.sendline(poweroff) self.child.expect(pexpect.EOF)这个 Agent 的核心逻辑是启动 QEMU等待登录提示输入用户名然后执行命令并收集输出。check_pcie_device方法用来验证 PCIe 设备是否被正确识别。6.3 自动化回归测试的编排有了 Agent 框架之后就可以编排回归测试了。我一般把测试用例写成 YAML 文件每个用例包含启动参数、预期结果、超时时间test_cases: - name: PCIe device enumeration qemu_args: -device my-ai-device steps: - cmd: lspci -n expect: 1234:5678 - name: BAR space allocation qemu_args: -device my-ai-device steps: - cmd: lspci -vv | grep Region expect: Region 0 - name: DMA read test qemu_args: -device my-ai-device steps: - cmd: cat /sys/bus/pci/devices/0000:00:01.0/resource expect: 0xAgent 读取 YAML 文件逐条执行测试用例把结果记录到日志里。如果某条用例失败Agent 可以自动保存 QEMU 的完整输出方便事后分析。注意QEMU 的启动时间受宿主机负载影响pexpect的超时时间建议设长一点比如 60 秒。如果超时太短QEMU 还没启动完就报错会浪费很多时间在排查假故障上。7. 常见问题与排查技巧实录7.1 PCIe 设备无法被识别这是最常见的问题表现是lspci里看不到设备。排查思路如下第一确认 QEMU 命令行里设备参数拼写正确。-device my-ai-device和-device my-ai-device,buspcie.0效果一样但如果设备模型有bus属性要求不指定可能会挂载失败。第二检查设备模型的realize函数是否被调用。可以在realize函数里加printf或者qemu_log看 QEMU 启动时有没有输出。第三检查 Vendor ID 和 Device ID 是否和lspci -n的输出匹配。有时候设备被识别了但 ID 不对导致你以为没识别。第四检查内核配置里CONFIG_PCI和CONFIG_PCI_HOST_GENERIC是否打开。如果内核不支持 PCIlspci命令本身就不存在。7.2 BAR 空间分配失败如果lspci -vv里看到设备但 BAR 显示unassigned说明 BAR 空间分配失败。常见原因有两个一是 BAR 大小超过了 QEMU 默认的 PCIe 窗口大小需要在 QEMU 命令行里调整-global virtio-pci-bus-mem-limits参数二是设备模型的pci_register_bar调用参数有误比如 size 设成了 0 或者不是 2 的幂。7.3 DMA 传输数据错误DMA 传输错误通常是因为地址映射不对。QEMU 的pci_dma_read和pci_dma_write接口需要传入正确的AddressSpace指针这个指针可以从PCIDevice结构体的bus_master_as字段获取。如果你传的是address_space_memory在某些配置下会出错。7.4 常见问题速查表问题现象可能原因排查方法lspci无输出内核未编译 PCI 支持检查CONFIG_PCIy设备未出现在lspci设备模型未注册检查type_init和meson.buildBAR 显示unassignedBAR 大小超限调整 QEMU PCIe 窗口参数DMA 数据错误地址空间指针错误使用pci_get_address_spaceQEMU 启动卡住设备realize死循环加日志定位实操心得QEMU 的-d参数可以打开各种调试输出比如-d pci会打印 PCIe 枚举过程-d guest_errors会打印客户机错误。这些日志对排查问题非常有帮助但输出量很大建议重定向到文件后再用grep过滤。8. 后续扩展方向与个人体会这套实验台搭好之后能做的事情还有很多。比如你可以把自定义设备模型换成更接近真实 AI 芯片的模型加上 DMA 引擎、中断控制器、多队列支持也可以用 QEMU 的-trace功能打开 PCIe 事务跟踪分析枚举过程中的每一个配置空间读写还可以把 Agent 和 CI 系统集成每次代码提交后自动跑一遍回归测试。我个人在实际操作中的体会是先用最简单的设备跑通全流程再逐步增加复杂度。我见过太多人一上来就写一个功能完整的设备模型结果卡在某个细节上好几天连 PCIe 枚举都没跑通。先用pci-testdev确认环境没问题再换成自己的设备每一步都有明确的验证点出了问题也容易定位。另外QEMU 的版本选择很重要。RISC-V 相关的补丁合并很快建议用最新的稳定版比如 8.2 或者 9.0。老版本可能缺少某些 PCIe 特性比如 PTM、AER 等后面想扩展的时候会很麻烦。最后再分享一个小技巧如果你需要模拟 PCIe Switch 拓扑QEMU 提供了x3130-upstream和xio3130-downstream两个设备模型可以组合出多级 Switch 结构。具体命令是在 QEMU 启动参数里加上-device x3130-upstream,idup0和-device xio3130-downstream,iddown0,busup0然后把 AI 设备挂到down0下面。这个拓扑对验证 PCIe 枚举顺序和资源分配策略非常有用。