#devices #sensor #driver #peripheral

nightly no-std embedded-devices

多种嵌入式传感器和设备的设备驱动实现

2个不稳定版本

0.9.8 2024年8月21日
0.0.1 2023年11月4日

112嵌入式开发

Download history 108/week @ 2024-08-16

每月108 次下载

MIT/Apache

190KB
2.5K SLoC

Crate API

embedded-devices

警告:此存储库目前处于实验状态,因此任何内容都可能随时更改。

欢迎使用嵌入式设备项目!在这里,您可以找到各种嵌入式传感器和设备的驱动程序集合,这些驱动程序都是使用这个框架构建的,该框架有助于构建基于寄存器的设备的驱动程序。这些驱动程序旨在用于async,但也支持通过功能开关同步上下文。我们的目标是提供功能完整的、最新的驱动程序,具有人性化的界面,同时满足高级设备功能和低级寄存器访问的需求。请参考以下列表了解支持的设备。

嵌入式-Rust设备驱动程序的生态系统似乎相当分散,既有高质量的实现,也有几个不完整或过时的驱动程序,其中只有少数具有一流的异步支持。这使得它们难以(甚至不可能)在embassy等嵌入式框架中使用,在我看来,这些框架将受益于能够访问更多现成的异步驱动程序和经常更新的依赖项。

目前,此存储库应作为原型。它展示了传感器和设备驱动程序如何从共同的框架中受益,使得新驱动程序的添加变得容易,以便简化未来的协作工作并解决一些上述问题。我们框架的主要组件是embedded-registers,它为通过I2C/SPI定义和与设备寄存器接口提供了一种便捷的解决方案。

支持的设备

以下列出了所有支持的设备。请访问它们的相应文档链接以获取更多信息和使用示例。

制造商 设备 接口 描述 文档
模拟器件 MAX31865 SPI RTDs、NTCs和PTCs的精度温度转换器 文档
博世 BME280 I2C/SPI 温度、压力和湿度传感器 文档
博世 BMP280 I2C/SPI 温度和压力传感器 文档
博世 BMP390 I2C/SPI 温度和压力传感器 文档
微芯 MCP9808 I2C ±0.5°C(最大)精度的数字温度传感器 文档
德州仪器 TMP117 I2C 根据温度范围,精度为±0.1°C至±0.3°C的温度传感器 文档

架构

驱动程序实现是根据制造商名称和设备名称组织的。每个驱动程序都暴露一个包含设备名称的结构,例如 BME280。结构体的泛型没有限制,因为设备可能需要不同的接口组合或额外的引脚。通常,它们会暴露一个 new_i2c 和/或 new_spi 函数,从接口和地址构建相应的对象。

编解码器

绝大多数设备在 I2C 或 SPI 之上使用类似的“协议”来暴露其寄存器,我们称之为编解码器。对于 I2C 和 SPI,我们提供了一个简单的编解码器实现,应该已经与现有的大多数设备类型兼容。如果设备(或单个寄存器)需要更复杂的编解码器——例如验证 CRC 校验和——它可以为此目的实现自己的编解码器。

基于寄存器的设备

嵌入式寄存器 crate 提供了一种定义寄存器的方法,并提供了一个接口实现,允许通过 I2C 或 SPI 读取和写入这些寄存器。

寄存器设备

寄存器定义

首先,让我们从一个非常简单的寄存器定义开始。我们稍后将为这个寄存器创建一个可以从中读取和写入的设备结构体。因此,为了定义我们的简单寄存器,我们

  1. 使用 #[device_register] 宏(我们稍后将定义这个 MyDevice)指定属于哪个设备,
  2. 使用 #[register] 宏定义起始地址和模式(只读、只写、读写),
  3. 使用 bondrewd(一个通用的位域宏)定义寄存器的内容。

大多数设备支持突发读取/写入操作,您可以从中断地址 A 开始读取,并将自动接收到连续内存区域中的值,直到您停止读取。这意味着您可以定义大小为 size > 1 字节 的寄存器,并将获得预期的内容。

让我们想象我们的 MyDevice 在设备地址 0x42(,0x43) 处有一个 2 字节读写寄存器,它包含两个 u8 值。相应的寄存器可以定义如下

/// Insert explanation of this register from the datasheet.
#[device_register(MyDevice)]
#[register(address = 0x42, mode = "rw")]
#[bondrewd(read_from = "msb0", default_endianness = "be", enforce_bytes = 2)]
pub struct ValueRegister {
    /// Insert explanation of this value from the datasheet.
    pub width: u8,
    /// Insert explanation of this value from the datasheet.
    pub height: u8,
}

这将创建两个名为 ValueRegisterValueRegisterBitfield 的结构体。前者将仅包含一个字节数组 [u8; N] 来存储打包的寄存器内容,后者将包含上面定义的实际成员。您将始终使用打包数据与设备接口,这些数据可以直接传输到总线。

【注意】我发现将用ValueRegister编写的成员放入ValueRegisterBitfield中有点误导。所以这可能在将来改变,但我目前想不出另一种像我们现在所拥有的那样简单易用的设计。问题是我们需要一个结构体用于打包数据,另一个用于解包数据。由于我们通常处理打包数据,并且希望为了性能允许直接对打包数据进行读写操作,名称很快就变得混淆。

访问寄存器

在定义寄存器之后,我们可能可以通过MyDevice来访问它。

// Imagine we already have constructed a device:
let mut dev = MyDevice::new_i2c(i2c_bus /* the i2c bus from your controller */, 0x12 /* address */);
// We can now retrieve the register
let mut reg = dev.read_register::<ValueRegister>().await?;

// Unpack a specific field from the register and print it
println!("{}", reg.read_width());
// If you need all fields (or are not bound to tight resource constraints),
// you can also unpack all fields and access them more conveniently
let data = reg.read_all();
// All bitfields implement Debug and defmt::Format, so you can conveniently
// print the contents
println!("{:?}", data);

// We can also change a single value
reg.write_height(190);
// Or pack a bitfield and replace everything
reg.write_all(data); // same as reg = ValueRegister::new(data);

// Which we can now write back to the device, given that the register is writable.
dev.write_register(reg).await?;

更复杂的寄存器

一个更复杂的寄存器可能包含不仅仅是简单的值。通常会有枚举或位标志,幸运的是,我们也可以用bondrewd来表示。使用bondrewd的属性宏,我们可以指定哪个位对应哪个字段。

#[allow(non_camel_case_types)]
#[derive(BitfieldEnum, Copy, Clone, Default, PartialEq, Eq, Debug, defmt::Format)]
#[bondrewd_enum(u8)]
pub enum TemperatureResolution {
    Deg_0_5C = 0b00,
    Deg_0_25C = 0b01,
    Deg_0_125C = 0b10,
    #[default]
    Deg_0_0625C = 0b11,
}

#[device_register(MyDevice)]
#[register(address = 0x44, mode = "rw")]
#[bondrewd(read_from = "msb0", default_endianness = "be", enforce_bytes = 1)]
pub struct ComplexRegister {
    #[bondrewd(bit_length = 6, reserve)]
    #[allow(dead_code)]
    pub reserved: u8,

    #[bondrewd(enum_primitive = "u8", bit_length = 2)]
    pub temperature_resolution: TemperatureResolution,
}

【注意】实际上,我们可能会将所有寄存器都放在一个方便的registers模块中,而不是命名所有寄存器为*Register,以简化操作并删除后缀。

定义设备

现在我们还需要定义我们的设备,这样我们才能实际使用寄存器。想象一下,我们的简单设备只会通过I2C进行通信。

首先,我们可以通过使用#[device]宏来定义一个结构体。这个结构体存储了我们设备所需的运行时状态,例如通信接口。这个宏只是定义了一个标记特质,我们稍后会需要它来定义寄存器,你现在可以忽略它。

/// Insert description from datasheet.
#[device]
pub struct MyDevice<I: RegisterInterface> {
    /// The interface to communicate with the device
    interface: I,
}

寄存器接口是来自embedded-registers的一个特质,它是一个暴露了特定read_registerwrite_register函数的对象。内部这将是我们稍后可以调用的函数,用于读取或写入特定寄存器。

但在此之前,我们需要为我们的设备添加一个构造函数,以便我们可以从真实的I2C总线和地址创建一个新的设备。《code>embedded-registers存储库还提供了一个名为I2cDevice的结构体,它实现了I2C的RegisterInterface,所以我们只需将其用作我们的接口。

对于不需要额外字段的非常简单的设备,有一个方便的宏simple_device::i2c!,它会为您定义必要的设备。

simple_device::i2c!(MyDevice, MyAddress, SevenBitAddress, OneByteRegAddrCodec);

地址枚举MyAddress应包含设备的所有有效地址,以及一个变体,以便允许指定任意地址,以防用户使用地址转换单元。地址可以是任何可以转换为总线底层(7位或10位寻址)的embedded_hal::i2c::AddressMode的类型。对于真实示例,请参阅mcp9808::Address

第三个参数是SevenBitAddressTenBitAddress,取决于设备的寻址模式。在大多数情况下,设备使用7位地址。

最后一个参数指定用于通过I2C读取或写入寄存器的编解码器。这通常是只是一个简单的前缀寄存器地址的编解码器,但在设备的情况下可能会更复杂。这将是你调整所需协议的位置。

最后,这个宏实际上只是为我们的设备定义了一个构造函数,相当于下面这个 impl 块。我们稍后将会看到为什么我们需要 #[maybe_async_cfg]

#[maybe_async_cfg::maybe(
    idents(hal(sync = "embedded_hal", async = "embedded_hal_async")),
    sync(not(feature = "async")),
    async(feature = "async"),
    keep_self
)]
impl<I> MyDevice<I2cDevice<I, hal::i2c::SevenBitAddress, OneByteRegAddrCodec>>
where
    I: hal::i2c::I2c<hal::i2c::SevenBitAddress> + hal::i2c::ErrorType,
{
    /// Initializes a new device with the given address on the specified bus.
    /// This consumes the I2C bus `I`.
    #[inline]
    pub fn new_i2c(interface: I, address: MyAddress) -> Self {
        Self {
            interface: I2cDevice::new(interface, address.into(), OneByteRegAddrCodec::default()),
        }
    }
}

现在我们有了设备,我们需要能够读写寄存器,并且也可以实现使用这些寄存器的高级功能。最简单的实现就是一个带有 #[device_impl] 属性宏的空块。

#[device_impl]
impl<I: RegisterInterface> MyDevice<I> {
}

还记得一开始创建的标记特质吗?它是通过 #[device] 创建的?现在它派上用场了;我们将存储为接口的 I2cDevice 非常通用,允许我们读取任何我们想要的合法寄存器定义。但由于这个crate中有多个具有合法寄存器的驱动程序,我们希望防止寄存器与无关设备一起使用。

因此,#[device_impl] 宏定义了两个函数 read_registerwrite_register,这些函数内部只是调用类似名称的函数 self.interface,但还需要传递的寄存器类型实现我们的标记特质。寄存器通过 #[device_register] 标注标记特质,因此传递无关寄存器现在会导致编译错误。

用户现在已经能够以基本安全的方式与设备寄存器进行接口交互,当然,不包括我们的类型寄存器表示的事物(如隐式设备状态)。最后一步是公开我们的设备的便利函数。经典的有如 initresetmeasureconfigure 或其他,但原则上你可以写任何东西。

最后,在编写设备函数时,我们总是编写异步代码。maybe_async_cfg 宏将自动从我们的定义中派生出同步代码,并用从 hal 的引用替换为 embedded_halembedded_hal_async。这是必要的,以便我们可以支持异步和同步crate消费者。例如,下面是 init 的一个例子。

#[device_impl]
impl<I: RegisterInterface> MyDevice<I> {
    /// Initialize the sensor by verifying its device id and manufacturer id.
    /// Not mandatory, but recommended.
    pub async fn init(&mut self) -> Result<(), InitError<I::Error>> {
        use self::registers::DeviceIdRevision;
        use self::registers::ManufacturerId;

        let device_id = self.read_register::<DeviceIdRevision>().await.map_err(InitError::Bus)?;
        if device_id.read_device_id() != self::registers::DEVICE_ID_VALID {
            return Err(InitError::InvalidDeviceId);
        }

        let manufacturer_id = self.read_register::<ManufacturerId>().await.map_err(InitError::Bus)?;
        if manufacturer_id.read_manufacturer_id() != self::registers::MANUFACTURER_ID_VALID {
            return Err(InitError::InvalidManufacturerId);
        }

        Ok(())
    }
}

贡献

如果您有任何改进当前架构的建议或想法,请鼓励您通过 Matrix 打开问题或联系。

贡献受到热烈欢迎!请随时建议新功能、实现设备驱动程序或提出一般性的改进建议。

许可证

许可如下

根据您的选择。除非您明确表示否则,您根据Apache-2.0许可证定义的任何有意提交以包含在本软件包中的贡献,应按上述方式双重授权,无需任何额外条款或条件。

依赖关系

约3.5MB
约62K SLoC