1. 为什么要在 QEMU 上搭一块 RISC-V AI 芯片的实验台
做 RISC-V AI 芯片验证这行,最头疼的从来不是写 RTL,而是“流片之前怎么把软件栈跑通”。一块真实的硅片从 tapeout 到回片要几个月,中间软件团队不能干等着。这时候 QEMU 就是救命稻草——它能在一台普通 x86 笔记本上虚拟出一套完整的 RISC-V 系统,让固件、内核、驱动、运行时提前跑起来。但问题在于,通用 QEMU 只模拟标准外设,而 AI 芯片往往挂着一堆自定义的加速器、DMA 引擎、PCIe 端点设备,这些在默认的qemu-system-riscv64里根本不存在。
所以这个项目的核心目标很明确:用 AGENT 辅助的方式,从零搭建一个能承载 RISC-V AI 芯片验证的 QEMU 实验台。所谓“实验台”,不是简单跑个qemu-system-riscv64 -M virt就完事,而是要构建一套可复现、可扩展、能挂载自定义 PCIe 设备、能跑 AI 推理负载的虚拟平台。AGENT 在这里扮演的角色,是帮你自动化处理那些重复性极高的环境配置、设备树生成、启动脚本编排、日志分析工作,让你把精力集中在芯片逻辑本身。
这套东西适合谁?三类人最需要:一是 RISC-V 芯片前端设计工程师,需要在流片前验证驱动和固件;二是 AI 加速器软件栈开发者,需要在不依赖硬件的情况下调试 runtime 和编译器;三是系统架构师,想快速评估 PCIe 拓扑、内存映射、中断路由对整体性能的影响。哪怕你只是对 RISC-V 和 QEMU 感兴趣,跟着走一遍也能把整个启动链路摸清楚。
我踩过的坑先放前面:很多人一上来就想着改 QEMU 源码加自定义设备,结果编译一次二十分钟,调试全靠 printf,效率极低。更合理的路径是先用标准 virt 机器把 RISC-V Linux 跑通,再逐步引入 PCIe 设备模型,最后才考虑自定义加速器。AGENT 的价值就在于帮你把“跑通”这一步压缩到最短,把“改设备”这一步的重复劳动自动化。
2. 实验台的整体设计与关键选型思路
2.1 为什么选 QEMU 而不是其他模拟器
RISC-V 生态里模拟器不少,Spike、Renode、QEMU 各有侧重。Spike 是官方 ISA 模拟器,指令级精度高,但外设模型极其简陋,连 PCIe 都没有;Renode 擅长多节点系统仿真,但 RISC-V AI 芯片常用的 PCIe 端点设备支持不够成熟。QEMU 的优势在于:它有一套完整的 PCIe 子系统实现,支持 PCIe 枚举、BAR 空间映射、MSI/MSI-X 中断、甚至 PCIe 热插拔。这些恰好是 AI 芯片作为 PCIe 端点设备时最需要验证的功能。
另一个关键因素是 QEMU 的virt机器已经内置了 RISC-V 的 AIA(Advanced Interrupt Architecture)支持,能模拟 IMSIC 和 APLIC,这对多核 AI 芯片的中断路由验证至关重要。你不需要从零写中断控制器,只需要在设备树里正确描述就行。
2.2 AGENT 在环境搭建中的具体分工
这里说的 AGENT 不是某个特定产品,而是一套自动化代理思路。我把它拆成三个角色:
- 环境探测 AGENT:自动检测宿主机架构、QEMU 版本、交叉编译工具链是否齐全,缺什么补什么。
- 配置生成 AGENT:根据目标芯片的 PCIe 拓扑、内存映射、中断号分配,自动生成 QEMU 命令行参数和设备树片段。
- 日志分析 AGENT:启动失败时自动抓取 QEMU 的
-d日志、内核 dmesg、PCIe 枚举记录,定位是 BAR 冲突还是中断没连上。
这套分工的好处是:你改一个芯片参数,AGENT 重新生成整套配置,不用手动改十几个文件。实测下来,从修改参数到重新启动验证,时间从半小时压缩到三分钟以内。
2.3 实验台的分层架构
整个实验台分四层,从下到上依次是:
| 层级 | 组件 | 作用 |
|---|---|---|
| 宿主层 | x86_64 Linux + KVM | 提供硬件加速,QEMU 跑得接近原生速度 |
| 模拟层 | QEMU RISC-V virt + 自定义 PCIe 设备 | 虚拟出 CPU、内存、PCIe 总线、AI 加速器 |
| 固件层 | OpenSBI + U-Boot | 初始化硬件,传递设备树,引导内核 |
| 系统层 | RISC-V Linux + 驱动 + AI Runtime | 跑真实负载,验证端到端功能 |
这个分层的关键在于:每一层都可以独立替换和调试。比如你怀疑是 OpenSBI 的中断初始化有问题,可以单独换一个版本重新跑,不用动上面的内核和驱动。
3. 核心细节解析与实操要点
3.1 QEMU 版本选择与编译参数
QEMU 对 RISC-V 的支持在 7.0 之后才比较完善,特别是 PCIe 相关的特性。我建议直接用 8.2 或更新的版本,原因有三:一是 8.x 对 AIA 的支持更完整,二是 PCIe 热插拔在 8.x 上稳定很多,三是 8.x 的virt机器默认支持更多 CPU 扩展。
编译时这几个参数必须开:
./configure \ --target-list=riscv64-softmmu \ --enable-kvm \ --enable-debug \ --enable-trace-backends=log \ --disable-werror--enable-debug不是让你去调 QEMU 本身,而是让-d日志更详细,PCIe 枚举过程能打出每个设备的 vendor ID、device ID、BAR 分配情况。--enable-trace-backends=log配合-trace参数可以追踪特定事件,比如pci_config_read、pci_config_write,排查枚举失败时非常有用。
注意:不要用发行版自带的 QEMU,版本往往太老,PCIe 热插拔和 AIA 支持都不全。自己编译一次,后面省心很多。
3.2 设备树的关键节点设计
设备树是 QEMU 和 Guest 内核之间的契约。对于 AI 芯片实验台,设备树里必须准确描述三类信息:
第一类是PCIe 主机桥。RISC-V virt 机器默认的 PCIe 控制器是pcie-host-generic,它的reg属性要覆盖 ECAM 配置空间和 MMIO 窗口。ECAM 大小决定了能挂多少设备,一般给 256MB 足够。MMIO 窗口要留足,AI 芯片的 BAR 空间往往很大,我通常给 1GB 起步。
第二类是中断路由。PCIe 设备的 INTx 中断通过interrupt-map映射到 PLIC 或 APLIC。如果芯片用 MSI/MSI-X,则需要在msi-parent里指向 IMSIC。这里最容易出错的是中断号冲突,建议在设备树里用注释标清楚每个设备占用的中断号。
第三类是DMA 一致性。AI 芯片通常需要访问系统内存做数据搬运,设备树里的dma-coherent属性必须和硬件设计一致。如果硬件不支持缓存一致性,就要在驱动里手动做 cache flush,否则数据对不上。
3.3 PCIe 设备模型的参数计算
假设我们要模拟一块 AI 加速卡,它的 PCIe 配置空间需要这几个关键字段:
- Vendor ID / Device ID:用自定义值,避免和真实设备冲突。我一般用
0x1234 / 0x5678这种测试专用值。 - Class Code:AI 加速器通常归为
0x020000(网络控制器)或0x0B4000(协处理器),具体看驱动怎么匹配。 - BAR 大小:假设芯片有 64KB 寄存器空间和 4GB 显存窗口,那么 BAR0 给 64KB,BAR2 给 4GB(64 位 BAR)。计算 BAR 大小时注意,QEMU 要求是 2 的幂次,且不能小于 4KB。
QEMU 命令行里这样写:
-device pcie-root-port,id=rp0,slot=1 \ -device my-ai-accel,bus=rp0,addr=0,\ vendor-id=0x1234,device-id=0x5678,\ bar0-size=0x10000,bar2-size=0x100000000pcie-root-port是必须的,因为 AI 芯片通常挂在根端口下面,而不是直接挂根总线。这样更接近真实拓扑,也方便测试热插拔。
3.4 启动脚本的自动化编排
手动敲 QEMU 命令很容易漏参数,我习惯写一个run.sh,把变量抽出来:
QEMU=qemu-system-riscv64 KERNEL=Image INITRD=rootfs.cpio.gz BIOS=fw_jump.bin MEM=8G SMP=4 $QEMU -M virt,aia=aplic-imsic \ -cpu rv64,x-v=true,vlen=256 \ -smp $SMP -m $MEM \ -bios $BIOS \ -kernel $KERNEL \ -initrd $INITRD \ -append "console=ttyS0 root=/dev/ram" \ -nographic \ -device pcie-root-port,id=rp0,slot=1 \ -device my-ai-accel,bus=rp0 \ -d guest_errors,pci_config \ -D qemu.logaia=aplic-imsic开启高级中断架构,vlen=256给向量扩展留空间,AI 负载经常用得上。-d pci_config会把每次配置空间读写打到qemu.log,枚举失败时直接搜这个文件。
4. 实操过程与核心环节实现
4.1 宿主机环境准备
先确认宿主机是 x86_64 Linux,内核 5.15 以上,KVM 模块已加载。用lsmod | grep kvm检查,如果没有就modprobe kvm_intel或kvm_amd。交叉编译工具链用riscv64-linux-gnu-前缀的版本,Ubuntu 下直接apt install gcc-riscv64-linux-gnu。
QEMU 编译安装到/opt/qemu-riscv,避免污染系统路径。编译时用-j$(nproc)加速,实测 16 核机器大约 8 分钟编完。
4.2 构建最小根文件系统
用 BusyBox 构建 initramfs 是最快的方式:
mkdir -p rootfs/{bin,sbin,etc,proc,sys,dev} cd rootfs cp /usr/riscv64-linux-gnu/bin/busybox bin/ ln -s busybox bin/sh cat > init << 'EOF' #!/bin/sh mount -t proc none /proc mount -t sysfs none /sys mount -t devtmpfs none /dev echo "RISC-V AI Lab Ready" exec /bin/sh EOF chmod +x init find . | cpio -o -H newc | gzip > ../rootfs.cpio.gz这个 initramfs 只有几 MB,启动到 shell 只要两秒。后面要跑 AI 负载时,再把 runtime 和模型文件塞进去。
4.3 编译 OpenSBI 和内核
OpenSBI 用fw_jump.bin就够,编译时指定PLATFORM=generic。内核配置里必须开这几个选项:
CONFIG_PCI=y CONFIG_PCI_HOST_GENERIC=y CONFIG_PCIE_ECAM_GENERIC=y CONFIG_PCI_MSI=y CONFIG_RISCV_IMSIC=y CONFIG_AI_ACCEL=m最后一项是自定义 AI 加速器驱动,先编成模块,方便反复加载卸载调试。
4.4 启动验证与 PCIe 枚举检查
启动后第一件事是lspci -vv,看 AI 设备有没有被枚举到。正常输出应该能看到1234:5678这个设备,BAR 地址已经分配好。如果没看到,检查qemu.log里的pci_config记录,看是配置空间读返回全 F,还是 BAR 分配失败。
第二件事是cat /proc/interrupts,确认 MSI 中断已经注册。AI 设备的中断号应该出现在列表里,且计数会随着驱动加载增加。
第三件事是dmesg | grep -i pcie,看内核有没有报 BAR 冲突或链路训练失败。QEMU 模拟的 PCIe 链路不会真的训练,但内核的枚举逻辑会走一遍,任何资源分配问题都会在这里暴露。
4.5 跑一个最小 AI 推理负载
在 initramfs 里放一个简单的矩阵乘法程序,通过/dev/ai-accel提交任务。驱动收到 ioctl 后,通过 DMA 把数据搬到设备 BAR 空间,触发加速器计算,完成后发 MSI 中断。整个过程在 QEMU 里跑一遍,能验证从用户态到设备模型的完整链路。
实测下来,一个 256x256 的矩阵乘法在 QEMU 里大约 200ms 完成,比真实硬件慢,但功能验证足够了。
5. 常见问题与排查技巧实录
5.1 PCIe 设备枚举失败
最常见的原因是 BAR 大小没对齐。QEMU 要求 BAR 大小是 2 的幂,且不能小于 4KB。如果你给bar0-size=0x18000,QEMU 会直接报错退出。改成0x20000就好了。
另一个原因是pcie-root-port的slot参数冲突。每个根端口占一个 slot,设备挂在根端口下面用addr区分。如果两个设备用了同一个addr,枚举会卡住。
5.2 中断收不到
先确认msi-parent指向正确。RISC-V virt 机器里 IMSIC 的 phandle 通常是固定的,但如果你改了设备树,可能对不上。用dtc -I dtb -O dts反编译 QEMU 生成的设备树,搜msi-parent看指向哪个节点。
如果用的是 INTx 中断,检查interrupt-map里的中断号有没有和别的设备冲突。PLIC 的中断号是全局分配的,冲突了内核会报irq 15: nobody cared。
5.3 内核启动卡在 PCIe 枚举
这种情况通常是 ECAM 窗口太小。默认pcie-host-generic的 ECAM 是 256MB,能挂 32 个设备。如果你模拟的芯片有多个功能(multi-function),每个功能占一个设备号,很容易超。把 ECAM 调到 512MB 或 1GB。
还有一种可能是 MMIO 窗口和内存重叠。检查 QEMU 命令行里的-m和 PCIe MMIO 基地址,确保不冲突。RISC-V virt 的 MMIO 通常从0x30000000开始,内存从0x80000000开始,一般不会撞,但自定义机器类型时要留意。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| lspci 看不到设备 | BAR 大小非法 | 检查 qemu.log 里的配置空间读 |
| 驱动加载失败 | Vendor/Device ID 不匹配 | 对比驱动里的 id_table |
| 中断计数不增加 | MSI 未使能 | 检查 PCI_COMMAND 寄存器的 bit 10 |
| DMA 数据错乱 | 缓存不一致 | 确认 dma-coherent 属性 |
| 启动卡死 | ECAM 窗口不足 | 增大 ECAM 或减少设备数 |
5.5 独家避坑技巧
第一,永远保留一份能跑通的最小配置。每次改参数前先备份run.sh和设备树,出问题了能快速回滚。我习惯用 git 管理这些配置文件,每次改动都有记录。
第二,QEMU 的-d日志要分级开启。平时只开guest_errors,排查 PCIe 问题时再加pci_config,否则日志文件几分钟就上 GB。
第三,设备树里的中断号用宏定义。不要直接写数字,用#define AI_ACCEL_IRQ 15这种,改的时候一处生效,避免漏改。
第四,AGENT 生成的配置要人工复核一遍。自动化工具再聪明也可能理解错你的意图,特别是 PCIe 拓扑这种强约束的东西,生成后一定要对着芯片手册核对一遍。
6. 实验台的扩展方向与个人体会
这套实验台跑通之后,扩展空间很大。往小了说,可以加更多 PCIe 设备模拟多卡场景,验证 PCIe Switch 的枚举和均衡;往大了说,可以把 QEMU 的设备模型换成 SystemC 或 Verilator 协同仿真,让 RTL 和软件栈在同一个环境里联调。
我个人在实际操作中的体会是:QEMU 实验台的价值不在于跑得多快,而在于把“软件能不能跑”这个问题从流片后提前到流片前。很多驱动 bug、设备树错误、中断路由问题,在 QEMU 里暴露出来,改起来成本几乎为零。等真实芯片回来,软件栈已经基本就绪,剩下的就是性能调优。
最后分享一个小技巧:如果你要模拟的 AI 芯片有多个 PCIe 功能,比如一个计算功能加一个管理功能,用multi-function设备模型比挂两个独立设备更接近真实。QEMU 里通过addr的低三位区分功能号,配置空间里Header Type的 bit 7 要置 1。这个细节很多文档不写,但实际芯片经常这么设计,提前在 QEMU 里验证能省不少事。