QEMU 训练营 2026 专业阶段总结¶
主要贡献者
- 作者:@eyauwag
背景介绍¶
工作当中偶尔用到 QEMU,一直都觉得它非常强大和神秘。这次有机会近距离去看这个项目,并能动手添加一些代码,内心是非常激动的。由于第一次接触 QEMU 源代码,对他的架构还是挺陌生的,尤其是选择了新的语言 RUST,更加非常惶恐。但是好在有 AI 的加持,一边学习,一边添加代码,终于跑通了测试,内心很激动。
专业阶段¶
测试内容主要是跑通 I2C BUS 的单元测试以及 QEMU QTEST 的集成测试。I2C BUS 的单元测试只需要完成 I2CBUS 的函数,能够实现 I2C BUS 的逻辑即可,对于我来说难度不算太大。但是对于 QTEST 的集成测试,却是难度挺大的。我面对的挑战包括: - 什么是 QTEST - g233.c 实现了什么功能 - 模块是怎么集成到系统里面去的 - RUST 与 C 语言代码是怎么合作的 - QOM 是什么?RUST 语言如何基于 QOM 建模,将 I2C 模型和 SPI 模型加入到 QEMU 的框架里面去 我记录了自己学习的过程,按“如何接入编译系统 -> QOM 建模 -> Rust 侧 QOM 实现 -> 从 g233 建机到 I2C/SPI 寄存器读写”的顺序,梳理完整路径。
1. 整体架构一图¶
flowchart LR
A[configure --enable-rust] --> B[Meson have_rust]
B --> C[Kconfig 打开 GEVICO_G233]
C --> D[select I2C_GPIO/SPI_GPIO]
D --> E[C 侧 QOM 壳: i2c-gpio/spi-gpio]
E --> F[Rust 后端: rust/hw/gevico/src/lib.rs]
F --> G[g233.c qdev_new + mmio_map]
G --> H[Guest/QTest 读写 MMIO 寄存器]
H --> I[I2CBus + AT24C02 / SPI Flash 状态机]
2. 新设备模型如何接入编译系统¶
2.1 Rust 总开关¶
configure支持--enable-rust和--disable-rust,并支持--rust-target-triple。meson.build通过have_rust = add_languages('rust', ...)启用 Rust,并检查rustc >= 1.83.0和bindgen >= 0.60.0。- 成功后会在配置头里设置
CONFIG_HAVE_RUST,后续 Kconfig 可用depends on HAVE_RUST。
关键位置:
- configure
- meson.build
2.2 Kconfig 依赖链¶
GEVICO_G233在hw/riscv/Kconfig里select I2C_GPIO、select SPI_GPIO。I2C_GPIO在hw/i2c/Kconfig里:depends on HAVE_RUSTselect I2Cselect AT24Cselect X_I2C_RUSTSPI_GPIO在hw/ssi/Kconfig里:depends on HAVE_RUSTselect SSI- 当前代码为
select X_I2C_RUST(即 SPI 也复用同一个 Rust crate 开关)
关键位置:
- hw/riscv/Kconfig
- hw/i2c/Kconfig
- hw/ssi/Kconfig
2.3 Rust crate 注入链接¶
rust/hw/gevico/meson.build构建静态 Rust 库gevico_i2c_gpio(其中同时包含 I2C 与 SPI Rust 逻辑)。- 该库通过
rust_devices_ss.add(when: 'CONFIG_X_I2C_RUST', ...)注入目标链接集合。 - 顶层
meson.build会收集rust_devices_ss的 crate 名,生成rust_<target>.rs根文件并链接进qemu-system-riscv64。
关键位置:
- rust/hw/gevico/meson.build
- meson.build
3. QOM 模型如何落地¶
这里分两层: - C 层:QOM 壳,负责类型注册和 MMIO 桥接。 - Rust 层:设备行为与状态机实现。
3.1 C 层 QOM 壳(i2c-gpio / spi-gpio)¶
I2C 与 SPI 都采用同样模式:
- 定义 QOM 类型名
TYPE_I2C_GPIO = "i2c-gpio"TYPE_SPI_GPIO = "spi-gpio"OBJECT_DECLARE_SIMPLE_TYPE(...)声明对象类型。TypeInfo指定.parent = TYPE_SYS_BUS_DEVICE。type_init(...)调用type_register_static(...)完成注册。realize阶段调用 Rust FFI 创建后端状态:i2c_gpio_rust_newspi_gpio_rust_newMemoryRegionOps.read/write直接转发给 Rust:*_rust_read*_rust_write- reset/finalize 也转发给 Rust:
*_rust_reset*_rust_free
关键位置:
- hw/i2c/i2c-gpio.c
- hw/ssi/spi-gpio.c
3.2 Rust 侧 QOM 实现(I2C)¶
I2C 设备是完整 Rust QOM 对象:
- 结构体定义:
#[derive(qom::Object, hwcore::Device)]- 首字段
parent_obj: ParentField<SysBusDevice> - 类型声明:
unsafe impl ObjectType for I2CGpioState { TYPE_NAME = c"i2c-gpio" }- 继承关系:
qom_isa!(I2CGpioState : SysBusDevice, DeviceState, Object)- 生命周期钩子:
impl ObjectImpl中设置INSTANCE_INIT、INSTANCE_POST_INIT、CLASS_INIT- 设备接口:
impl DeviceImplimpl ResettablePhasesImpl(实现HOLD)impl SysBusDeviceImpl- MMIO:
MemoryRegionOpsBuilder绑定read/write到 Rust 成员函数
关键位置:
- rust/hw/gevico/src/lib.rs
3.3 Rust QOM 框架原理(通用)¶
rust/qom 框架负责把 Rust 类型映射为 QOM TypeInfo:
ObjectImpltrait 提供TYPE_INFO常量。TYPE_INFO自动填充:name/parentinstance_init/instance_post_init/finalizeclass_initrust_instance_init、rust_class_init等 C ABI 函数作为桥接入口。
关键位置:
- rust/qom/src/qom.rs
4. 从建立 g233 到设备生效¶
4.1 g233 机器创建并挂载设备¶
在 g233.c 里:
- 内存映射定义:
- I2C:
0x10013000 - SPI:
0x10019000 - 机器初始化时:
qdev_new("i2c-gpio")+sysbus_realize_and_unrefsysbus_mmio_map(..., VIRT_I2C0.base)qdev_new("spi-gpio")+sysbus_mmio_map(..., VIRT_SPI0.base)
关键位置:
- hw/riscv/g233.c
4.2 设备树(FDT)节点导出¶
g233.c 还会创建 FDT 节点:
/soc/i2c@10013000- compatible:
gevico,i2c-gpio /soc/spi@10019000- compatible:
gevico,spi-gpio
关键位置:
- hw/riscv/g233.c
5. I2C 链路:从总线驱动到 EEPROM 读写¶
5.1 I2C 控制器寄存器语义¶
Rust I2C 设备实现了寄存器:
CTRL(0x00):EN/START/STOP/RWSTATUS(0x04):BUSY/ACK/DONEADDR(0x08):7-bit 地址DATA(0x0C):数据字节PRESCALE(0x10):分频
5.2 总线与从设备模型¶
- 总线:
I2CBus(rust/hw/i2c/src/lib.rs) start_transfersendrecvend_transfer- 从设备:
At24c02(在rust/hw/gevico/src/lib.rs) - 256 字节存储
- 页大小 8 字节
- 首字节作为 EEPROM 内部地址指针
5.3 I2C 写流程(主机到从机)¶
- 客户端写
ADDR = 0x50 - 写
CTRL = EN|START,触发I2CBus::start_transfer(addr, false) - 写
DATA = mem_addr,再写CTRL = EN,触发send(mem_addr) - 写
DATA = payload,再写CTRL = EN,触发send(payload) - 写
CTRL = EN|STOP,触发end_transfer
状态位由 set_done_ack 更新:DONE 必置,ACK 取决于从机响应。
5.4 I2C 读流程(含 repeated START)¶
START+write先写入 EEPROM 内部地址START+read(RW=1)重新寻址CTRL = EN|RW触发recv(),返回到DATASTOP结束传输
6. SPI 链路:寄存器到 Flash 状态机¶
SPI 在当前实现中是“C QOM 壳 + Rust FFI 状态机”:
- C 层 MMIO 转发到 Rust:
spi_gpio_rust_read/write - Rust 侧
SPIGpioRust维护寄存器与 flash: CR1/SR/DR/CSwel/wipflash[256]- 命令状态机:
WREN(0x06)RDSR(0x05)READ(0x03)WRITE(0x02)
关键位置:
- hw/ssi/spi-gpio.c
- rust/hw/gevico/src/lib.rs
7. QTest 如何覆盖“寄存器读写 -> 总线事务”¶
7.1 已有 I2C 用例¶
- 寄存器复位与配置:
test-i2c-gpio-init.c- 位级流程(START/STOP/ACK):
test-i2c-gpio-bitbang.c- EEPROM 读写与顺序读:
test-i2c-eeprom-rw.c- 页边界行为:
test-i2c-eeprom-page.c
7.2 已有 SPI 用例¶
- 寄存器初始化:
test-spi-rust-init.c- 传输流程:
test-spi-rust-transfer.c- Flash 命令与读写:
test-spi-rust-flash.c
7.3 测试如何启动 g233¶
这些测试都通过类似入口启动:
即先创建 g233 机器,再直接通过 MMIO 地址读写控制器寄存器,完成端到端验证。
关键位置:
- tests/gevico/qtest/*.c
8. 新增同类 Rust 设备的推荐模板¶
新增一个“Rust 后端 + C QOM 壳”的设备,可按下面最小闭环:
- Kconfig
- 新增
config XXX depends on HAVE_RUSTselect X_..._RUST- C 壳
- 定义
TYPE_XXX TypeInfo + type_initMemoryRegionOps转发 Rust FFI- Rust crate
- 导出
xxx_rust_new/free/reset/read/write - 如需完整 QOM 能力,按
I2CGpioState模式实现ObjectImpl/DeviceImpl - 机器接线
qdev_new("xxx")sysbus_realize_and_unrefsysbus_mmio_mapcreate_fdt_xxx- qtest
- 至少覆盖:reset 值、寄存器读写、关键协议流程、异常路径
9. 当前实现的一个注意点¶
SPI_GPIO 在 hw/ssi/Kconfig 中当前选择的是 X_I2C_RUST,说明 I2C/SPI Rust 逻辑共同放在 rust/hw/gevico crate 里由同一个配置开关驱动。后续如果希望 SPI 独立开关,可以改为单独 X_SPI_RUST 并拆分 crate。
总结¶
QEMU 还是挺复杂的,QOM 的引入使代码结构变简单了,但是需要一些额外的学习成本。以后还是要多学习它的一些底层架构。