#register #bit-fields #embedded-devices #derive #sensor

macro no-std embedded-registers-derive

Procedural macro for effortless definitions of registers in embedded device drivers

11 releases

0.9.11 Oct 4, 2024
0.9.10 Sep 26, 2024
0.9.9 Aug 26, 2024
0.9.6 Dec 13, 2023
0.9.3 Nov 7, 2023

#8 in #registers

28 downloads per month
Used in 2 crates (via embedded-registers)

MIT/Apache

18KB
228 lines

This crate provides a procedural macro for effortless definitions of registers in embedded device drivers.

Currently, embedded-registers requires you to use #![feature(generic_arg_infer)].

Attribute macro

Registers are defined by adding #[register(...)] to the definition of a bondrewd bitfield. As a short reminder, bondrewd is another proc macro that allows you to define a bitfield structure, which is very handy when dealing with registers, where multiple values are often tightly packed bit-on-bit.

The register attribute macro supports the following arguments:

address The virtual address associated to the register.
read Add this if the register should be readable
write Add this if the register should be writeable

Adding this attribute to a bondrewd struct Foo will result in two types being defined:

  • Foo will become the register, essentially a byte array with the correct size that provides getter and setter functions for the individual fields.
  • FooBitfield will become the underlying bondrewd bitfield, which may be used to construct a register from scratch, or can be obtained via Foo::read_all if you want to unpack all values.

This has the advantage that reading a register incurs no additional memory and CPU cost to unpack all values of the bitfield. You only pay for the members you actually access.

Simple Example

This simple example defines the DeviceId register of an MCP9808. It has the virtual address 0b111 (0x7), uses big endian byte order with the first member of the struct positioned at the most significant bit, is 2 bytes in size and is read-only:

#![feature(generic_arg_infer)]
use embedded_registers::{register, i2c::{I2cDevice, codecs::OneByteRegAddrCodec}, RegisterInterface, ReadableRegister};

#[register(address = 0b111, mode = "r")]
#[bondrewd(read_from = "msb0", default_endianness = "be", enforce_bytes = 2)]
pub struct DeviceId {
    device_id: u8,
    revision: u8,
}

// The register may now be read from an I2C bus using sync or async operations:
// Create new I2cDevice with bus, address, codec
let mut dev = I2cDevice::new(i2c, 0x24, OneByteRegAddrCodec::default());
let reg = dev.read_register::<DeviceId>().await?;
// sync: let reg = DeviceId::read_i2c_blocking(&mut i2c, address);

Complex Example

A more complex example may involve adding your own Bondrewd-capable enums. We also make sure to annotate the correct fields with #[default] to allow reconstructing the power-up defaults easily. Have a look at this excerpt of the Configuration register from the MCP9808:

#![feature(generic_arg_infer)]
use embedded_registers::{register, i2c::{I2cDevice, codecs::OneByteRegAddrCodec}, RegisterInterface, ReadableRegister, WritableRegister};
use bondrewd::BitfieldEnum;

#[derive(BitfieldEnum, Clone, Default, PartialEq, Eq, Debug, Format)]
#[bondrewd_enum(u8)]
pub enum Hysteresis {
    #[default]
    Deg_0_0C = 0b00,
    Deg_1_5C = 0b01,
    Deg_3_0C = 0b10,
    Deg_6_0C = 0b11,
}

#[derive(BitfieldEnum, Clone, Default, PartialEq, Eq, Debug, Format)]
#[bondrewd_enum(u8)]
pub enum ShutdownMode {
    #[default]
    Continuous = 0,
    Shutdown = 1,
}

#[register(address = 0b001, mode = "rw")]
#[bondrewd(read_from = "msb0", default_endianness = "be", enforce_bytes = 2)]
pub struct Config {
    // padding
    #[bondrewd(bit_length = 5, reserve)]
    #[allow(dead_code)]
    reserved: u8,

    #[bondrewd(enum_primitive = "u8", bit_length = 2)]
    pub hysteresis: Hysteresis,
    #[bondrewd(enum_primitive = "u8", bit_length = 1)]
    pub shutdown_mode: ShutdownMode,

    // ... all 16 bits must be filled
    # #[bondrewd(bit_length = 8, reserve)]
    # #[allow(dead_code)]
    # reserved2: u8,
}

// This now allows us to read and write the register, while only
// unpacking the fields we require:
let mut reg = dev.read_register::<Config>().await?;
info!("previous shutdown mode: {}", reg.read_shutdown_mode());
reg.write_shutdown_mode(ShutdownMode::Shutdown);
dev.write_register(&reg).await?;

// If you want to get the full decoded bitfield, you can use either `read_all`
// or `.into()`. If you need to unpack all fields anyway, this might be
// more convenient as it allows you to access the bitfield members more ergonomically.
//let bf: ConfigBitfield = reg.into();
let mut bf = reg.read_all();
info!("previous shutdown mode: {}", bf.shutdown_mode);
bf.shutdown_mode = ShutdownMode::Shutdown;
reg.write_all(bf);

Dependencies

~1–1.5MB
~31K SLoC