跳转至

RUST 实验 - GPIO I2C 控制器(QTest)

主要贡献者

完整实现代码可参考仓库(qemu-camp-2026-exper-ovwxxwvo)


📁 项目主要目录结构

./rust/hw/i2c/  
├── i2c_core             // i2c通用核心实现  
│   └── src  
│       ├── core.rs  
│       └── lib.rs  
├── i2c_slave            // i2c各种从机外设实现  
│   └── src  
│       ├── at24c02.rs  
│       └── lib.rs  
├── gpio_i2c             // i2c具体控制器实现  
│   └── src  
│       ├── bindings.rs  
│       ├── registers.rs  
│       ├── device.rs  
│       └── lib.rs  
  • 项目文件结构的设计是为了减少QEMU的C代码对RUST代码的多次调用。
  • 主控设备实现的crate(gpio_i2c)是唯一提供给QEMU调用的接口,每个主控都是独立的crate。
  • 主控总线和从机特性的实现的crate(i2c_core)仅在RUST内部供主控和从机的实现使用。
  • 从机外设的实现将在crate(i2c_slave)以mod形式存在,每个从机外设都为独立的mod。

🛠️ 项目构建所需修改文件

./hw/i2c/Kconfig                    // I2C驱动配置,新增GPIO_I2C编译项,关联Rust实现驱动  
./hw/riscv/Kconfig                  // RISC-V主板配置,GEVICO_G233平台新增GPIO_I2C外设依赖  

./rust/Cargo.toml                   // 顶层workspace添加相应crate  

./rust/hw/i2c/Kconfig               // 添加子项  
./rust/hw/i2c/meson.build           // 添加子项  

./rust/hw/i2c/gpio_i2c/meson.build  // 注意添加bindgen相关  
./rust/hw/i2c/gpio_i2c/Cargo.toml   // 注意依赖相对路径层级  
  • ./rust/Cargo.toml顶层workspace添加相应的crate
    这样lsp才能生效,必要时cargo clean
  • ./rust/hw/i2c/gpio_i2c/meson.build文件中_gpio_i2c_rs需要添加
    {'.': _gpio_i2c_bindings_inc_rs},

🛠️ 项目混合编程对接文件

./rust/hw/i2c/gpio_i2c/wrapper.h        // 专供bindgen解析的适配头,修复工具解析报错并引入标准C头文件  
./rust/hw/i2c/gpio_i2c/src/bindings.rs  // 导入bindgen生成的QEMU的C接口绑定代码,Rust通过其调用C侧接口  

./include/hw/i2c/gpio_i2c.h             // 声明C接口函数符号,供C代码调用Rust实现  
./rust/hw/i2c/gpio_i2c/src/lib.rs       // 导出Rust功能实现,对接C声明接口函数符号  

./include/hw/riscv/g233.h               // 声明项目相关内存映射枚举  
./hw/riscv/g233.c                       // 调用控制器create实现  
  • gpio_i2c_creategpio-i2c这两个符号在C和RUST中是对应的。
  • 主要涉及文件./include/hw/i2c/gpio_i2c.h./rust/hw/i2c/gpio_i2c/src/lib.rs

🧩 GPIO_I2C主控的极简实现框架

// QEMU硬件抽象层  
pub struct GPIOI2CRegisters {}  // 寄存器,存放设备所有寄存器  
pub struct GPIOI2CState {}      // QOM设备模型,存放设备运行状态  
pub struct GPIOI2CClass {}      // QOM设备类,存放设备固定属性  

// 业务特性,强制设备挂载系统总线并规定设备唯一ID  
trait GPIOI2CImpl: SysBusDeviceImpl + IsA<GPIOI2CState> {}  

// QEMU框架适配层  
impl GPIOI2CClass {}                           // 保存设备ID,调用父类初始化  
unsafe impl ObjectType for GPIOI2CState {}     // 绑定实例类对象,设备命名供QEMU使用  
impl GPIOI2CImpl for GPIOI2CState {}           // 定义设备ID  
impl ObjectImpl for GPIOI2CState {}            // 绑定设备完整生命周期函数  
impl DeviceImpl for GPIOI2CState {}            // 启用设备,创建设备硬件资源  
impl ResettablePhasesImpl for GPIOI2CState {}  // 复位回调,虚拟机复位时恢复硬件初始状态  
impl SysBusDeviceImpl for GPIOI2CState {}      // 挂载设备到系统总线,绑定MMIO访问入口  

// QEMU业务接口层  
impl GPIOI2CRegisters {}                       // 实现寄存器读写及复位逻辑  
impl GPIOI2CState {}                           // 实现数据收发工具函数  

// 导出设备创建函数  
pub unsafe extern "C" fn gpio_i2c_create()     // 创建实例化设备,供QEMU的C代码调用  
  • GPIO_I2C主控设备为极简实现,未实现中断逻辑和复位逻辑。(仿pl011)
  • 寄存器的地址偏移和特定寄存器结构体在同crate的registers.rs中定义。
  • I2C总线在i2c_core的crate中的core.rs中定义。
  • I2C从机在i2c_slave的crate中进行不同外设的定义。

🔗 GPIO_I2C主控的业务函数调用链条

gpio_i2c_create  
  └> GPIOI2CState::new  
  |    └> GPIOI2CState::init  
  |         └> MemoryRegion::init_io - GPIOI2C_OPS  
  |         └> GPIOI2CRegisters - Default::default()  
  |         └> I2C_Bus::new  
  └> GPIOI2CState::sysbus_realize  
       └> GPIOI2CState::realize -> I2CBus::attach -> AT24C02Slave::new  
qtest_readl  -> GPIOI2C_OPS.read  -> GPIOI2CState::read  -> GPIOI2CRegisters::read  

qtest_writel -> GPIOI2C_OPS.write -> GPIOI2CState::write -> GPIOI2CRegisters::write  
  ┌-----------------------------------------------------------┘  
  └> I2C_Bus::transfer_read  -> AT24C02Slave::recv  
  └> I2C_Bus::transfer_write -> AT24C02Slave::send  

  • GPIOI2CState::init初始化由GPIOI2CState::new构造间接调用。
  • GPIOI2CState::realize实体化由GPIOI2CState::sysbus_realize实现间接调用。
  • I2CBus分别在GPIOI2CStateinitrealize中进行创建和从机挂载。
  • I2C主从数据传输仅由写控制寄存器触发,写入其余寄存器和读取所有寄存器不触发总线通信。

🔄 I2C协议数据流转

             sys-bus                      i2c-bus  
 risc-v --<----------->-- i2c-master --<----------->-- i2c-slave  
processor                 controller                   peripheral  
   /-SDA-<->- start+[7addr+1rw](tx)+n|ack(rx)+[8data](tx)+n|ack(rx)+stop -<->-SDA-\  
I2C device                                                                    I2C controller  
   \-SCL----- ------            ...12345678...123456789...         ----- -----SCL-/  

  • I2C协议,两线一时钟(SCL)一数据(SDA),单线进行数据收发。
  • 启停信号由主控写控制寄存器触发,不作保存。
  • 应答信号由字节接收方发起,应答判定结果保存在状态寄存器。
  • 7位地址+1位读写标记,构成8位保存在地址寄存器,由主控发起。
  • 8位数据,保存在数据寄存器。

🧩 I2C_BUS的实现框架

pub struct I2CBus {  
    devices: Vec<Box<dyn I2CSlave>>,  // 挂载在总线上的从机列表  
    current_addr: Option<u8>,         // 当前正在通信的从机地址  
    is_recv: bool                     // 本次传输是主机读或主机写的标记  
}  

impl I2CBus {  
    pub fn new             // 创建I2C总线,初始化设备列表及当前寻址地址  
    pub fn device_count    // 统计I2C总线上挂载的从设备数量  
    pub fn is_busy         // 判断总线是否正在传输  

    pub fn attach          // 挂载I2C从机到I2C总线  
    pub fn start_transfer  // 返回开始信号,保存地址,确定主机读写  
    pub fn end_transfer    // 返回结束信号,设置地址为空  
    pub fn send            // 发送数据,调用从机发送函数  
    pub fn recv            // 接收数据,调用从机接收函数  
    pub fn transfer_write  // 封装完整写流程:起始写传输+连续写入字节+结束传输  
    pub fn transfer_read   // 封装完整读流程:起始读传输+连续读取字节+结束传输  
}  
- I2C_Bus的实现是在RUST实验一核心代码上稍作修改完成。

🧩 I2C_SLAVE的实现框架

pub struct AT24C02Slave {  
    pub addr: u8,          // 从机设备地址  
    pub regs: [u8; 256],   // EEPROM存储数组  
    pub pointer: u8,       // 存储读写指针  
    pub first_byte: bool,  // 标记首字节  
}  

impl I2CSlave for AT24C02Slave {  
    fn address  // 获取设备地址  
    fn event    // 响应总线事件  
    fn send     // 发送主机数据,并设置存储读写指针  
    fn recv     // 接收主机数据,并设置存储读写指针  
}  
- AT24C02_Slave的实现是在RUST实验一测试代码上稍作修改完成。


📦 GPIO_I2C主控的寄存器写根据I2C协议的实现

impl GPIOI2CRegisters {  

    pub(self) fn read(&mut self, offset: RegisterOffset) -> u32 {  
    }  

    pub(self) fn write(&mut self, offset: RegisterOffset, value: u32, device: &GPIOI2CState) -> bool {  
        use RegisterOffset::*;  
        match offset {  
            ADDR     => self.addr     = Addr::from(value),  
            DATA     => self.data     = value,  
            CTRL     => {  
                let mut i2c_bus = device.i2c_bus.borrow_mut();  
                let mut ctrl = Ctrl::from(value);  
                match (ctrl.en(), ctrl.start(), ctrl.stop(), ctrl.rw()) {  

            // 使能+起始信号:发起传输,更新总线及寄存器状态  
                    (true, true, false, _) => {  
                        let addr = u32::from(self.addr) as u8;  
                        let ret  = i2c_bus.start_transfer(addr, ctrl.rw());  
                        self.status.set_busy(i2c_bus.is_busy());  
                        self.status.set_ack(ret == 0);  
                        self.status.set_done(true);  
                    },  

            // 使能+停止信号:结束传输,更新总线及寄存器状态  
                    (true, false, true, false) => {  
                        i2c_bus.end_transfer();  
                        self.status.set_busy(i2c_bus.is_busy());  
                        self.status.set_done(true);  
                    },  

            // 使能+无起止+写模式:读寄存器发送数据,更新总线及寄存器状态  
                    (true, false, false, false) => {  
                        let data = self.data as u8;  
                        let ret  = i2c_bus.send(data);  
                        self.status.set_busy(i2c_bus.is_busy());  
                        self.status.set_ack(ret == 0);  
                        self.status.set_done(true);  
                    },  

            // 使能+无起止+读模式:接收数据写寄存器,更新总线及寄存器状态  
                    (true, false, false, true) => {  
                        let data = i2c_bus.recv();  
                        self.data = data as u32;  
                        self.status.set_busy(i2c_bus.is_busy());  
                        self.status.set_done(true);  
                    },  

            // 无效控制组合,不执行任何操作  
                    _ => {},  
                };  
                ctrl.set_start(false);  
                ctrl.set_stop(false);  
                self.ctrl = ctrl  
            },  
            STATUS   => self.status   = Status::from(value),  
            PRESCALE => self.prescale = value,  
        }  
        false  
    }  

}  
- I2C主从数据传输由写控制寄存器触发,需实现5个逻辑分支:
- 使能+起始信号:发起传输,更新总线及寄存器状态
- 使能+停止信号:结束传输,更新总线及寄存器状态
- 使能+无起止+写模式:读寄存器发送数据,更新总线及寄存器状态
- 使能+无起止+读模式:接收数据写寄存器,更新总线及寄存器状态
- 无效控制组合,不执行任何操作


📝 总结

  • 文中目录结构、文件清单、代码框架均可直接复用,更换协议就能移植其他外设。
  • RUST和C的多语言混合编程工程搭建繁琐,可在着手实现具体逻辑前修改涉及文件。
  • 了解QEMU设备的实现框架,理清函数调用链路,关键业务代码是实现主控寄存器基于协议的读写逻辑。
  • 项目未用到GDB调试,有待进一步学习,不懂知识由豆包协助推进。