
1. 为什么我要用 QEMU 给 RISC-V AI 芯片搭一个实验台做芯片验证这行的人都有一个共识流片之前能把软件栈跑通多少流片之后就少熬多少个通宵。我最近在跟一个 RISC-V 架构的 AI 加速芯片项目芯片还没回片但固件、驱动、推理框架的适配工作不能干等着。手里没有硅片那就得先造一个“数字孪生”出来——QEMU 就是这个场景下最现实的选择。这篇文章要聊的就是怎么用 AGENT 辅助的方式从零把一套 QEMU 的 RISC-V 实验环境拉起来让它能模拟一块带 PCIe 总线的 AI 芯片跑通基本的设备枚举、驱动加载和简单的推理任务流。整套东西的核心关键词是AGENT、QEMU、RISC-V、AI 芯片、PCIe——AGENT 负责把繁琐的环境配置和调试流程自动化QEMU 提供虚拟硬件平台RISC-V 是目标指令集架构AI 芯片是我们最终要模拟的设备形态PCIe 则是这颗芯片和主机通信的生命线。适合谁看如果你是在做 RISC-V 相关固件、驱动、算子库适配的工程师或者你手里有一堆 AI 加速卡的软件栈要提前验证但硬件还没到位再或者你单纯想搞明白 QEMU 怎么模拟一个带 PCIe 设备的 RISC-V 系统这篇内容应该能帮你省掉不少翻文档的时间。我会把每一步的操作意图、参数选择的理由、以及我实际踩过的坑都摊开讲尽量做到你照着做就能复现。需要提前说明的是QEMU 的 RISC-V 系统模拟和 x86 下的用法差别不小尤其是 PCIe 部分很多在 x86 上理所当然的事情在 RISC-V 的 virt 机器上需要额外配置。下面我按实际搭建的顺序从整体设计思路开始拆。2. 整体方案设计与核心思路拆解2.1 为什么选 QEMU 而不是其他模拟方案市面上能模拟 RISC-V 的方案不止 QEMU 一家Spike、Renode、以及一些商业仿真器都能做。但落到“模拟一块带 PCIe 的 AI 芯片”这个具体需求上QEMU 的优势非常明确。Spike 是 RISC-V 官方的 ISA 模拟器指令级精度高但它本质上是个功能模拟器外设模型非常有限PCIe 这种复杂总线它基本不碰。Renode 在嵌入式外设模拟上很强但 PCIe 生态和 AI 加速器相关的模型积累不如 QEMU。商业仿真器精度和功能都够但成本和上手门槛摆在那里做日常软件栈验证不划算。QEMU 的 virt 机器对 RISC-V 的支持已经相当成熟PCIe 主机桥、ECAM 配置空间、MSI/MSI-X 中断这些都有现成实现。更关键的是QEMU 允许你自定义设备模型我可以写一个简化版的 AI 加速器 PCIe 设备挂上去让驱动开发者有东西可以枚举、可以读写 BAR、可以触发中断。这种“够用就好”的模拟精度恰好匹配软件栈早期验证的需求。提示QEMU 模拟的是功能行为不是时序行为。如果你的验证目标涉及 PCIe 链路训练、均衡、信号完整性这些物理层特性QEMU 帮不上忙那需要真实的 FPGA 原型或者硬件仿真加速器。2.2 AGENT 在这个流程里扮演什么角色标题里带了 AGENT我得说清楚它在这里不是噱头。搭建 QEMU RISC-V 环境涉及大量重复性操作下载交叉编译工具链、编译 QEMU、准备 rootfs、写启动脚本、反复调整命令行参数、解析启动日志定位问题。这些活儿如果纯手工做一次两次还行但当你需要频繁重建环境、切换配置组合的时候效率就成问题了。我用的 AGENT 是一个基于脚本编排的自动化助手核心能力是接收自然语言描述的任务拆解成可执行的命令序列执行后解析输出遇到错误自动尝试修复或给出建议。比如我说“帮我编译一个支持 RISC-V virt 和 PCIe 的 QEMU开启 debug 日志”它能自动拉取源码、配置编译选项、处理依赖缺失、编译并验证产物。这比我自己敲一长串 configure 参数再盯着报错要省心得多。但要注意AGENT 不是万能的。它对 QEMU 内部机制的理解有限遇到深层次的配置冲突还是得人来判断。我的经验是把它当成一个“执行力很强但需要明确指令的助手”任务描述越具体它干得越好。2.3 目标实验台的架构设计我最终要搭出来的东西逻辑结构是这样的宿主环境一台 x86_64 的 Linux 开发机负责跑 QEMU 和交叉编译工具链。QEMU 模拟的 RISC-V 系统使用 virt 机器类型包含多个 CPU 核心、内存、一个 PCIe 主机桥。PCIe 设备一个自定义的 AI 加速器设备模型挂在 PCIe 总线上有独立的 BAR 空间和中断。Guest 系统一个精简的 Linux我用的 Buildroot 构建带 RISC-V 交叉编译的驱动和测试程序。通信通道通过 QEMU 的 virtio 串口和网络做 guest 与宿主的数据交换。这个架构的好处是每一层都可控。QEMU 的参数我可以随时调设备模型我可以按需改guest 系统我可以裁剪到最小。对于软件栈验证来说可控性比性能重要得多。3. 环境准备与核心组件搭建3.1 宿主环境的基础依赖我的开发机是 Ubuntu 22.04其他发行版的操作类似包管理器换一下就行。基础依赖这块QEMU 编译需要的东西不少我列一下关键的sudo apt update sudo apt install -y build-essential git ninja-build pkg-config \ libglib2.0-dev lib pixman-1-dev libslirp-dev \ python3-venv python3-pip flex bison \ libfdt-dev zlib1g-dev libcap-ng-dev libattr1-dev这里面有几个包容易被忽略但很关键。libfdt-dev是设备树操作库RISC-V 的 virt 机器靠设备树向 guest 传递硬件信息没有它 QEMU 编译会报错。libslirp-dev提供用户态网络后面 guest 要联网下载东西或者做网络通信测试会用到。libcap-ng-dev和libattr1-dev关系到 QEMU 的权限管理虽然可以关掉但建议装上省得后面折腾。交叉编译工具链我用的是官方预编译的版本直接下载解压就行wget https://github.com/riscv-collab/riscv-gnu-toolchain/releases/download/2024.03.01/riscv64-glibc-ubuntu-22.04-gcc-nightly-2024.03.01-nightly.tar.gz tar -xzf riscv64-glibc-ubuntu-22.04-gcc-nightly-2024.03.01-nightly.tar.gz -C /opt export PATH/opt/riscv/bin:$PATH验证一下riscv64-unknown-linux-gnu-gcc --version能打印出版本信息就说明工具链就位了。这一步 AGENT 可以帮你自动完成下载和解压但 PATH 的设置建议手动确认因为不同 shell 的配置文件加载顺序有时候会坑你。3.2 编译支持 PCIe 的 QEMUQEMU 的编译配置决定了它支持哪些机器类型和设备。RISC-V virt 机器默认就带 PCIe 主机桥但如果你想用自定义设备或者需要更细的调试能力编译时得把相关选项打开。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 内部逻辑或者看崩溃栈的时候有用。--enable-trace-backendslog开启 trace 日志PCIe 配置空间的读写、中断触发这些事件都能记录下来排查问题的时候是救命稻草。--disable-werror是因为新版本 GCC 对老代码的警告比较严格不关掉可能编译失败。编译完成后验证./build/qemu-system-riscv64 --version ./build/qemu-system-riscv64 -machine help | grep virt看到 virt 机器在列表里就说明基础环境 OK 了。注意QEMU 的版本选择有讲究。太老的版本 PCIe 支持不完整太新的版本可能引入未稳定的改动。我实测 v8.2.0 在 RISC-V virt PCIe 这个组合上比较稳建议从这个版本起步。3.3 构建最小化的 RISC-V Guest 系统Guest 系统我用 Buildroot 来构建原因是它能把整个系统裁剪到很小启动快而且配置过程完全脚本化适合反复重建。git clone https://gitlab.com/buildroot.org/buildroot.git cd buildroot git checkout 2024.02 make qemu_riscv64_virt_defconfig make menuconfig在 menuconfig 里需要调整几个地方Target Options里确认架构是 RISCV64ABI 是 lp64d。Toolchain里选择外部工具链指向我们之前装的 riscv64-unknown-linux-gnu。System configuration里把 root 密码设一下方便调试。Kernel里选上 PCIe 相关的支持特别是CONFIG_PCI、CONFIG_PCI_HOST_GENERIC、CONFIG_PCI_MSI。Target packages里加上pciutils后面要用 lspci 验证设备枚举。配置好之后make -j$(nproc)Buildroot 会自动下载内核源码、编译内核、构建 rootfs、打包成镜像。第一次编译时间比较长取决于机器性能半小时到一小时都正常。产物在output/images/下面关键的是Image内核镜像和rootfs.ext2根文件系统。3.4 自定义 AI 加速器 PCIe 设备模型这是整个实验台最有意思的部分。QEMU 自带了很多 PCIe 设备模型网卡、显卡、NVMe 等但没有 AI 加速器。我需要写一个简化版的设备模型让 guest 里的驱动有东西可以操作。设备模型的核心要素Vendor ID 和 Device ID自定义一组值比如 0x1234 和 0x5678驱动靠这个来匹配。BAR 空间至少一个 MMIO BAR用来映射寄存器。我给了 4KB 的空间前 256 字节放控制寄存器后面放数据缓冲区。中断支持 MSI-X这样能模拟真实 AI 芯片的中断行为。DMAAI 芯片通常需要 DMA 搬运数据QEMU 的设备模型可以通过pci_dma_read/write来实现。设备模型的代码结构大致如下这是简化示意实际代码更长typedef struct { PCIDevice parent_obj; MemoryRegion mmio; uint32_t regs[1024]; } AIChipState; static void ai_chip_mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size) { AIChipState *s opaque; s-regs[addr 2] val; if (addr REG_START) { // 触发推理任务实际场景会启动一个后台线程模拟计算 ai_chip_trigger_msix(s); } } static const MemoryRegionOps ai_chip_ops { .write ai_chip_mmio_write, .read ai_chip_mmio_read, .endianness DEVICE_LITTLE_ENDIAN, };把这个设备注册到 QEMU 的 PCIe 总线上启动命令里加上-device ai-chip就能挂上去。驱动开发者看到的就是一个标准的 PCIe 设备可以枚举、可以读写 BAR、可以收中断。提示写设备模型的时候寄存器地址的对齐和访问宽度要严格按 PCIe 规范来。我一开始没注意 64 位寄存器的对齐导致 guest 驱动读出来的值全是错的排查了大半天。4. 实操过程与关键环节实现4.1 启动命令的完整拆解把前面准备的东西串起来启动命令是这样的./build/qemu-system-riscv64 \ -machine virt,pcieon \ -cpu rv64 \ -smp 4 \ -m 4G \ -kernel output/images/Image \ -drive fileoutput/images/rootfs.ext2,formatraw,idhd0 \ -device virtio-blk-pci,drivehd0 \ -device ai-chip \ -nographic \ -append root/dev/vda rw consolettyS0逐项解释-machine virt,pcieon使用 virt 机器并开启 PCIe 支持。有些 QEMU 版本默认就开但显式写上更保险。-cpu rv64指定 CPU 类型。如果需要向量扩展做 AI 算子验证可以换成rv64,vtrue。-smp 44 个 CPU 核心模拟多核场景。-m 4G4GB 内存跑 Linux 和简单推理任务够用了。-kernel和-drive指定内核和根文件系统。-device virtio-blk-pci把磁盘挂到 PCIe 总线上顺便验证 PCIe 存储设备的工作情况。-device ai-chip挂载我们自定义的 AI 加速器。-nographic串口输出到终端方便看启动日志。-append内核启动参数指定根文件系统和控制台。启动后如果一切正常你会看到内核日志滚动最后停在登录提示符。4.2 验证 PCIe 设备枚举进入 guest 系统后第一件事是确认 PCIe 设备被正确枚举lspci -vvv你应该能看到类似这样的输出00:01.0 Ethernet controller: Red Hat, Inc. Virtio network device 00:02.0 SCSI storage controller: Red Hat, Inc. Virtio block device 00:03.0 Unclassified device: Device 1234:5678最后那个1234:5678就是我们的 AI 芯片。lspci -vvv还能看到 BAR 空间的映射地址、MSI-X 的配置、链路状态等信息。如果设备没出现排查顺序是检查 QEMU 启动命令里-device ai-chip有没有写对。检查设备模型的 Vendor/Device ID 有没有和驱动匹配。看 QEMU 的 trace 日志确认配置空间读写是否正常。4.3 驱动加载与寄存器读写测试设备枚举成功后加载驱动insmod ai_chip_drv.ko dmesg | tail -20驱动会做几件事读取 BAR 空间、映射寄存器、注册字符设备、申请 MSI-X 中断。dmesg 里能看到驱动的打印信息确认 BAR 映射的物理地址和长度。然后写一个简单的用户态程序测试寄存器读写int fd open(/dev/aichip0, O_RDWR); uint32_t val 0xdeadbeef; write(fd, val, 4); // 写寄存器 read(fd, val, 4); // 读回来 printf(read back: 0x%x\n, val);如果读回来的值和写进去的一致说明 MMIO 通路是通的。4.4 中断与 DMA 的联合验证AI 芯片的核心工作模式是驱动把任务描述符通过 DMA 写到设备内存然后写寄存器触发任务设备完成后通过 MSI-X 中断通知驱动。在 QEMU 里模拟这个流程驱动分配一块 DMA 缓冲区填充任务描述符。驱动把缓冲区物理地址写到设备的 DMA 地址寄存器。驱动写 START 寄存器。QEMU 设备模型收到写操作启动一个定时器模拟计算延迟。定时器到期后设备模型触发 MSI-X 中断。驱动中断处理程序读取状态寄存器确认任务完成。这个流程跑通就说明 PCIe 设备的基本交互模型在 QEMU 里是成立的。我实测下来从写 START 到收到中断延迟在微秒级对于功能验证完全够用。注意QEMU 的 MSI-X 中断模拟和真实硬件有差异。真实硬件的中断延迟和 CPU 负载、中断亲和性都有关系QEMU 里这些因素被简化了。所以中断相关的性能测试结果不能直接外推到真实芯片。5. 常见问题与排查技巧实录5.1 PCIe 设备枚举失败这是最常见的问题。现象是 lspci 看不到设备或者看到了但 BAR 全是 0。现象可能原因排查方法lspci 完全看不到设备QEMU 命令行没加 -device检查启动命令设备出现但 BAR 为 0设备模型 BAR 注册有问题看 QEMU trace 日志设备出现但驱动不匹配Vendor/Device ID 不对对比驱动里的 ID 表枚举过程中 QEMU 崩溃设备模型内存越界gdb 跟 QEMU 崩溃栈我遇到过一次设备模型 BAR 注册时大小没对齐QEMU 直接把 BAR 映射成了 0 长度guest 读出来全是 0xffffffff。后来把 BAR 大小改成 4KB 对齐就好了。5.2 中断收不到MSI-X 中断收不到通常有几个原因MSI-X 表没初始化驱动需要在设备配置空间里找到 MSI-X capability设置表地址和 PBA 地址然后 enable。漏了任何一步中断都不会来。中断向量没绑定QEMU 设备模型触发中断时要指定正确的 vector 号和驱动申请的要一致。QEMU 的 MSI-X 模拟限制某些 QEMU 版本对 MSI-X 的支持有 bug可以试试退回 MSI 或者 legacy INTx。排查的时候在 QEMU 启动命令里加-trace eventspci_msix_*能看到 MSI-X 相关的 trace 事件非常直观。5.3 DMA 数据不一致DMA 测试时发现设备读到的数据和驱动写的不一样大概率是缓存一致性问题。RISC-V 的 virt 机器默认不模拟缓存所以理论上不存在一致性问题。但如果你的驱动里做了 cache flush 操作而 QEMU 不认这些操作反而可能导致数据错乱。我的经验是在 QEMU 环境里DMA 缓冲区用dma_alloc_coherent分配不要手动做 cache 操作。QEMU 会保证一致性。5.4 启动速度慢Buildroot 构建的 rootfs 如果包太多启动会慢。我一开始把 Python、GCC 都塞进去了启动要一分多钟。后来裁剪到只留必要的驱动和测试工具启动时间降到 10 秒以内。裁剪的原则是guest 里只放验证需要的东西。编译工作都在宿主做guest 只负责运行。5.5 AGENT 辅助的边界用 AGENT 自动化环境搭建确实省事但有几个地方它容易出错版本兼容性判断AGENT 可能会选一个“最新”的版本组合但最新不一定最稳。QEMU、内核、Buildroot 三者的版本搭配需要人工确认。错误日志的深层解读编译报错时 AGENT 能处理简单的依赖缺失但涉及代码逻辑的编译错误它往往给不出有效建议。配置参数的语义理解QEMU 的 configure 选项很多AGENT 有时候会选错。比如把--enable-debug和--enable-debug-tcg搞混。我的做法是让 AGENT 干重复性的活下载、解压、执行命令、收集日志关键的配置决策和错误判断自己来。6. 实验台的扩展方向与个人体会这套环境跑通之后能做的事情比一开始预想的多。我在上面陆续加了几个扩展多设备模拟把 AI 芯片实例化多个挂在同一个 PCIe switch 下面验证驱动对多卡场景的支持。QEMU 的 PCIe switch 模型可以直接用配置上稍微改一下就行。PCIe 热插拔验证QEMU 支持在运行时通过 monitor 命令热插拔 PCIe 设备。我试过在 guest 运行过程中把 AI 芯片拔掉再插上验证驱动的热插拔处理逻辑。这个功能对于做设备管理软件的团队很有价值。性能计数器模拟在设备模型里加一些假的性能计数器寄存器驱动读出来可以做一些简单的性能分析逻辑验证。虽然数据是假的但流程是真的。固件交互验证RISC-V 的启动流程涉及 OpenSBI 固件我在这套环境里也验证了固件对 PCIe 设备的初始化流程确认固件正确配置了 PCIe 主机桥。我个人在实际操作中的体会是QEMU 模拟环境最大的价值不是“替代真实硬件”而是“让软件团队在硬件到位之前就能开始工作”。它逼着你把硬件接口定义清楚把驱动的边界条件想明白。很多在真实硬件上不容易复现的异常场景比如设备突然消失、BAR 读写返回异常值在 QEMU 里反而容易构造和调试。最后分享一个小技巧QEMU 的 monitor 控制台启动时加-monitor telnet:127.0.0.1:1234,server,nowait可以在运行时查看 PCIe 设备的配置空间、内存映射、中断状态。调试驱动的时候一边在 guest 里操作一边在 monitor 里看硬件状态比单纯看 dmesg 高效得多。这个组合我用了大半年基本成了标配。