跳转至

QEMU 训练营 2026 专业阶段总结

主要贡献者


背景介绍

工作当中偶尔用到 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 总开关

  1. configure 支持 --enable-rust--disable-rust,并支持 --rust-target-triple
  2. meson.build 通过 have_rust = add_languages('rust', ...) 启用 Rust,并检查 rustc >= 1.83.0bindgen >= 0.60.0
  3. 成功后会在配置头里设置 CONFIG_HAVE_RUST,后续 Kconfig 可用 depends on HAVE_RUST

关键位置: - configure - meson.build

2.2 Kconfig 依赖链

  1. GEVICO_G233hw/riscv/Kconfigselect I2C_GPIOselect SPI_GPIO
  2. I2C_GPIOhw/i2c/Kconfig 里:
  3. depends on HAVE_RUST
  4. select I2C
  5. select AT24C
  6. select X_I2C_RUST
  7. SPI_GPIOhw/ssi/Kconfig 里:
  8. depends on HAVE_RUST
  9. select SSI
  10. 当前代码为 select X_I2C_RUST(即 SPI 也复用同一个 Rust crate 开关)

关键位置: - hw/riscv/Kconfig - hw/i2c/Kconfig - hw/ssi/Kconfig

2.3 Rust crate 注入链接

  1. rust/hw/gevico/meson.build 构建静态 Rust 库 gevico_i2c_gpio(其中同时包含 I2C 与 SPI Rust 逻辑)。
  2. 该库通过 rust_devices_ss.add(when: 'CONFIG_X_I2C_RUST', ...) 注入目标链接集合。
  3. 顶层 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 都采用同样模式:

  1. 定义 QOM 类型名
  2. TYPE_I2C_GPIO = "i2c-gpio"
  3. TYPE_SPI_GPIO = "spi-gpio"
  4. OBJECT_DECLARE_SIMPLE_TYPE(...) 声明对象类型。
  5. TypeInfo 指定 .parent = TYPE_SYS_BUS_DEVICE
  6. type_init(...) 调用 type_register_static(...) 完成注册。
  7. realize 阶段调用 Rust FFI 创建后端状态:
  8. i2c_gpio_rust_new
  9. spi_gpio_rust_new
  10. MemoryRegionOps.read/write 直接转发给 Rust:
  11. *_rust_read
  12. *_rust_write
  13. reset/finalize 也转发给 Rust:
  14. *_rust_reset
  15. *_rust_free

关键位置: - hw/i2c/i2c-gpio.c - hw/ssi/spi-gpio.c

3.2 Rust 侧 QOM 实现(I2C)

I2C 设备是完整 Rust QOM 对象:

  1. 结构体定义:
  2. #[derive(qom::Object, hwcore::Device)]
  3. 首字段 parent_obj: ParentField<SysBusDevice>
  4. 类型声明:
  5. unsafe impl ObjectType for I2CGpioState { TYPE_NAME = c"i2c-gpio" }
  6. 继承关系:
  7. qom_isa!(I2CGpioState : SysBusDevice, DeviceState, Object)
  8. 生命周期钩子:
  9. impl ObjectImpl 中设置 INSTANCE_INITINSTANCE_POST_INITCLASS_INIT
  10. 设备接口:
  11. impl DeviceImpl
  12. impl ResettablePhasesImpl(实现 HOLD
  13. impl SysBusDeviceImpl
  14. MMIO:
  15. MemoryRegionOpsBuilder 绑定 read/write 到 Rust 成员函数

关键位置: - rust/hw/gevico/src/lib.rs

3.3 Rust QOM 框架原理(通用)

rust/qom 框架负责把 Rust 类型映射为 QOM TypeInfo

  1. ObjectImpl trait 提供 TYPE_INFO 常量。
  2. TYPE_INFO 自动填充:
  3. name/parent
  4. instance_init/instance_post_init/finalize
  5. class_init
  6. rust_instance_initrust_class_init 等 C ABI 函数作为桥接入口。

关键位置: - rust/qom/src/qom.rs

4. 从建立 g233 到设备生效

4.1 g233 机器创建并挂载设备

g233.c 里:

  1. 内存映射定义:
  2. I2C: 0x10013000
  3. SPI: 0x10019000
  4. 机器初始化时:
  5. qdev_new("i2c-gpio") + sysbus_realize_and_unref
  6. sysbus_mmio_map(..., VIRT_I2C0.base)
  7. qdev_new("spi-gpio") + sysbus_mmio_map(..., VIRT_SPI0.base)

关键位置: - hw/riscv/g233.c

4.2 设备树(FDT)节点导出

g233.c 还会创建 FDT 节点:

  1. /soc/i2c@10013000
  2. compatible: gevico,i2c-gpio
  3. /soc/spi@10019000
  4. compatible: gevico,spi-gpio

关键位置: - hw/riscv/g233.c

5. I2C 链路:从总线驱动到 EEPROM 读写

5.1 I2C 控制器寄存器语义

Rust I2C 设备实现了寄存器:

  1. CTRL(0x00)EN/START/STOP/RW
  2. STATUS(0x04)BUSY/ACK/DONE
  3. ADDR(0x08):7-bit 地址
  4. DATA(0x0C):数据字节
  5. PRESCALE(0x10):分频

5.2 总线与从设备模型

  1. 总线:I2CBusrust/hw/i2c/src/lib.rs
  2. start_transfer
  3. send
  4. recv
  5. end_transfer
  6. 从设备:At24c02(在 rust/hw/gevico/src/lib.rs
  7. 256 字节存储
  8. 页大小 8 字节
  9. 首字节作为 EEPROM 内部地址指针

5.3 I2C 写流程(主机到从机)

  1. 客户端写 ADDR = 0x50
  2. CTRL = EN|START,触发 I2CBus::start_transfer(addr, false)
  3. DATA = mem_addr,再写 CTRL = EN,触发 send(mem_addr)
  4. DATA = payload,再写 CTRL = EN,触发 send(payload)
  5. CTRL = EN|STOP,触发 end_transfer

状态位由 set_done_ack 更新:DONE 必置,ACK 取决于从机响应。

5.4 I2C 读流程(含 repeated START)

  1. START+write 先写入 EEPROM 内部地址
  2. START+readRW=1)重新寻址
  3. CTRL = EN|RW 触发 recv(),返回到 DATA
  4. STOP 结束传输

6. SPI 链路:寄存器到 Flash 状态机

SPI 在当前实现中是“C QOM 壳 + Rust FFI 状态机”:

  1. C 层 MMIO 转发到 Rust:spi_gpio_rust_read/write
  2. Rust 侧 SPIGpioRust 维护寄存器与 flash:
  3. CR1/SR/DR/CS
  4. wel/wip
  5. flash[256]
  6. 命令状态机:
  7. WREN(0x06)
  8. RDSR(0x05)
  9. READ(0x03)
  10. WRITE(0x02)

关键位置: - hw/ssi/spi-gpio.c - rust/hw/gevico/src/lib.rs

7. QTest 如何覆盖“寄存器读写 -> 总线事务”

7.1 已有 I2C 用例

  1. 寄存器复位与配置:
  2. test-i2c-gpio-init.c
  3. 位级流程(START/STOP/ACK):
  4. test-i2c-gpio-bitbang.c
  5. EEPROM 读写与顺序读:
  6. test-i2c-eeprom-rw.c
  7. 页边界行为:
  8. test-i2c-eeprom-page.c

7.2 已有 SPI 用例

  1. 寄存器初始化:
  2. test-spi-rust-init.c
  3. 传输流程:
  4. test-spi-rust-transfer.c
  5. Flash 命令与读写:
  6. test-spi-rust-flash.c

7.3 测试如何启动 g233

这些测试都通过类似入口启动:

QTestState *qts = qtest_init("-machine g233 -m 2G");

即先创建 g233 机器,再直接通过 MMIO 地址读写控制器寄存器,完成端到端验证。

关键位置: - tests/gevico/qtest/*.c

8. 新增同类 Rust 设备的推荐模板

新增一个“Rust 后端 + C QOM 壳”的设备,可按下面最小闭环:

  1. Kconfig
  2. 新增 config XXX
  3. depends on HAVE_RUST
  4. select X_..._RUST
  5. C 壳
  6. 定义 TYPE_XXX
  7. TypeInfo + type_init
  8. MemoryRegionOps 转发 Rust FFI
  9. Rust crate
  10. 导出 xxx_rust_new/free/reset/read/write
  11. 如需完整 QOM 能力,按 I2CGpioState 模式实现 ObjectImpl/DeviceImpl
  12. 机器接线
  13. qdev_new("xxx")
  14. sysbus_realize_and_unref
  15. sysbus_mmio_map
  16. create_fdt_xxx
  17. qtest
  18. 至少覆盖:reset 值、寄存器读写、关键协议流程、异常路径

9. 当前实现的一个注意点

SPI_GPIOhw/ssi/Kconfig 中当前选择的是 X_I2C_RUST,说明 I2C/SPI Rust 逻辑共同放在 rust/hw/gevico crate 里由同一个配置开关驱动。后续如果希望 SPI 独立开关,可以改为单独 X_SPI_RUST 并拆分 crate。

总结

QEMU 还是挺复杂的,QOM 的引入使代码结构变简单了,但是需要一些额外的学习成本。以后还是要多学习它的一些底层架构。