I²C 驱动练习 - 简单版

我们将不会编写整个驱动程序,只会做第一步:驱动程序编写的 hello world,即读取传感器的设备 ID。这个版本被标记为简单,因为我们解释了代码片段,你只需将它们复制粘贴到正确的位置即可。如果你缺少 Rust 或嵌入式领域的经验,或者如果你觉得困难版本太难,请使用此版本。两个版本使用的是相同的文件。

i2c-driver/src/icm42670p.rs 是一个非常基础的 I²C IMU 传感器驱动的填空版本。任务是补全这个文件,使得运行 main.rs 可以记录驱动的设备 ID。

i2c-driver/src/icm42670p_solution.rs 提供本练习的解答。如果要运行它,需要更改 main.rslib.rs 中的导入语句。导入语句已经存在,你只需要注释掉当前的导入语句,并取消注释标记为解答的几行。

驱动

传感器实例

要使用外设传感器,首先要获取它的一个实例。传感器被表示成一个结构体,包含其地址和 I²C 总线对象。这是使用 embedded-hal crate 中定义的 trait 来实现的。该结构体是公有的,因为我们需要从这个 crate 外访问它,但它的字段是私有的。


#![allow(unused)]
fn main() {
#[derive(Debug)]
pub struct ICM42670P<I2C> {
    // The concrete I²C device implementation.
    i2c: I2C,

    // Device address
    address: DeviceAddr,
}
}

我们添加一个 impl 块,包含可以在传感器实例上使用的所有方法。它还定义了错误处理。在这个块中,我们还实现了一个实例化方法。(与结构体类似)方法也可以是公有的或私有的。这个方法需要从外部访问,所以它被标记为 pub。请注意,以这种方式编写的传感器实例会获取 I²C 总线的所有权。


#![allow(unused)]
fn main() {
impl<I2C, E> ICM42670P<I2C>
where
    I2C: i2c::WriteRead<Error = E> + i2c::Write<Error = E>,
{
    /// Creates a new instance of the sensor, taking ownership of the i2c peripheral.
    pub fn new(i2c: I2C, address: DeviceAddr) -> Result<Self, E> {
        Ok(Self { i2c, address })
    }
// ...
}

设备地址

  • 设备的地址在代码中可用:

#![allow(unused)]
fn main() {
pub enum DeviceAddr {
    /// 0x68
    AD0 = 0b110_1000,
    /// 0x69
    AD1 = 0b110_1001,
}
}
  • 这个 I²C 设备有两个可能的地址——0x680x69。 我们通过向设备上的 AP_AD0 引脚施加 0V3.3V 来告诉设备我们希望它使用哪一个地址。如果我们施加 0V,它会监听地址 0x68。如果我们施加 3.3V,它会监听地址 0x69。因此,可以将引脚 AD_AD0 视为一位输入,用于设置设备地址的最低位。 数据手册的 9.3 节提供了更多信息

寄存器的表示

传感器的寄存器表示为枚举。每个变体都将寄存器的地址作为值。Register 类型实现了一种提供变体地址的方法。


#![allow(unused)]
fn main() {
#[derive(Clone, Copy)]
pub enum Register {
    WhoAmI = 0x75,
}

impl Register {
    fn address(&self) -> u8 {
        *self as u8
    }
}

}

read_register()write_register()

基于 embedded-hal crate 提供的方法,我们定义了 读取写入 的方法。它们将作为更具体的方法的基础,并作为一个抽象层,用于适配具有 8 位寄存器的传感器。请注意 read_register() 方法是基于 write_read() 方法实现的。其原因在于 I²C 协议的特点:我们首先需要在 I²C 总线上写一个命令来指定我们要读取哪个寄存器。这些辅助方法可以保持私有,因为我们不需要从这个 crate 外访问它们。


#![allow(unused)]
fn main() {
impl<I2C, E> ICM42670P<I2C>
where
    I2C: i2c::WriteRead<Error = E> + i2c::Write<Error = E>,
{
    /// Creates a new instance of the sensor, taking ownership of the i2c peripheral.
    pub fn new(i2c: I2C, address: DeviceAddr) -> Result<Self, E> {
        Ok(Self { i2c, address })
    }
    // ...
    /// Writes into a register
    // This method is not public as it is only needed inside this file.
    #[allow(unused)]
    fn write_register(&mut self, register: Register, value: u8) -> Result<(), E> {
        let byte = value;
        self.i2c
            .write(self.address as u8, &[register.address(), byte])
    }

    /// Reads a register using a `write_read` method.
    // This method is not public as it is only needed inside this file.
    fn read_register(&mut self, register: Register) -> Result<u8, E> {
        let mut data = [0];
        self.i2c
            .write_read(self.address as u8, &[register.address()], &mut data)?;
        Ok(u8::from_le_bytes(data))
    }
}

✅ 实现一个公有方法来读取地址为 0x75WhoAmI 寄存器。使用上面的 read_register() 方法。

✅ 可选:实现更多方法来向驱动程序添加功能。在文档中查阅相应寄存器及其地址。💡 一些点子:

  • 启用陀螺仪传感器或加速度计
  • 启动测量
  • 读取测得数据

🔎 有关外设寄存器的一般信息

寄存器可以有不同的含义,本质上,它们是一个可以存储值的位置

在这个特定的上下文中,我们使用的是一个外部设备(因为它是一个传感器,即使与主控芯片在同一块 PCB 上)。它可通过 I2C 寻址,我们在读取和写入其寄存器的地址。每个地址都标识了唯一的一个位置,其中包含了一些信息。在这种情况下,我们想要的是包含当前温度的位置的地址。

如果你想尝试从这个传感器获取其他有趣的数据,可以在第 14 节中找到 ICM-42670 的寄存器表。

Simulation

This project is available for simulation through two methods:

  • Wokwi projects
  • Wokwi files are also present in the project folder to simulate it with Wokwi VS Code extension:
    1. Press F1, select Wokwi: Select Config File and choose advanced/i2c-driver/wokwi.toml
    2. Build you project
    3. Press F1 again and select Wokwi: Start Simulator