I²C 驱动练习 - 简单版
我们将不会编写整个驱动程序,只会做第一步:驱动程序编写的 hello world
,即读取传感器的设备 ID。这个版本被标记为简单,因为我们解释了代码片段,你只需将它们复制粘贴到正确的位置即可。如果你缺少 Rust 或嵌入式领域的经验,或者如果你觉得困难版本太难,请使用此版本。两个版本使用的是相同的文件。
i2c-driver/src/icm42670p.rs
是一个非常基础的 I²C IMU 传感器驱动的填空版本。任务是补全这个文件,使得运行 main.rs
可以记录驱动的设备 ID。
i2c-driver/src/icm42670p_solution.rs
提供本练习的解答。如果要运行它,需要更改 main.rs
和 lib.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 设备有两个可能的地址——
0x68
和0x69
。 我们通过向设备上的AP_AD0
引脚施加0V
或3.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)) } }
✅ 实现一个公有方法来读取地址为 0x75
的 WhoAmI
寄存器。使用上面的 read_register()
方法。
✅ 可选:实现更多方法来向驱动程序添加功能。在文档中查阅相应寄存器及其地址。💡 一些点子:
- 启用陀螺仪传感器或加速度计
- 启动测量
- 读取测得数据
🔎 有关外设寄存器的一般信息
寄存器可以有不同的含义,本质上,它们是一个可以存储值的位置。
在这个特定的上下文中,我们使用的是一个外部设备(因为它是一个传感器,即使与主控芯片在同一块 PCB 上)。它可通过 I2C 寻址,我们在读取和写入其寄存器的地址。每个地址都标识了唯一的一个位置,其中包含了一些信息。在这种情况下,我们想要的是包含当前温度的位置的地址。
如果你想尝试从这个传感器获取其他有趣的数据,可以在第 14 节中找到 ICM-42670 的寄存器表。
Simulation
This project is available for simulation through two methods: