| Crates.io | embedded-interfaces |
| lib.rs | embedded-interfaces |
| version | 0.10.1 |
| created_at | 2025-08-13 11:10:48.452386+00 |
| updated_at | 2025-08-22 10:16:31.324528+00 |
| description | Traits for common embedded interfaces and procedural macros for effortless definition of registers and commands for embedded device drivers |
| homepage | https://github.com/oddlama/embedded-devices |
| repository | https://github.com/oddlama/embedded-devices |
| max_upload_size | |
| id | 1793551 |
| size | 175,519 |
A comprehensive framework for building type-safe and ergonomic embedded device
drivers. Includes traits for communication protocols, codecs, and abstractions
for register and command based devices. Fully no_std compatible, and suitable
for both sync and async.
interface_objects! macro for ergonomic zero-cost packed struct definitionsThis crate provides two main abstractions, the RegisterInterface and
CommandInterface. By default, we provide implementations for any I2C or SPI
bus from embedded-hal or embedded-hal-async. These interfaces can be used
to respectively read and write registers or execute arbitrary commands.
Finally, a macro called interface_objects! is provided by this crate to simplify the
definition of registers and data for commands which often have very specific
bit-packed layouts. It will be explained in the following section.
The interface_objects! macro can be used to define bit-packed structs and
registers. It automatically generates:
Define a simple bit-packed struct with automatic field packing:
use embedded_interfaces::codegen::interface_objects;
interface_objects! {
/// A basic struct
struct BasicStruct(size = 2) {
/// Some documentation for the field
field1: u8,
/// Another field
field2: u8,
}
}
This defines two types BasicStruct([u8; 2]) and BasicStructUnpacked, where
the former directly wraps the underlying data array while the latter contains
the actual unpacked fields.
The structs can be converted into one another by using .pack() and
.unpack(). The packed representation also gets specific field accessor
functions like .read_field1() or .write_field1(123).
Doc-comments will be transferred to the actual types while automatically adding generated information like the resulting bit range or default value (if given).
You can directly associate defaults to each field and control the desired size in bits. By eliding the field name with an underscore, the field will become a reserved field that is ignored in packing or unpacking operations. No accessors will be generated for it.
use embedded_interfaces::codegen::interface_objects;
interface_objects! {
struct BitFields(size = 1) {
flag1: bool = true, // 1-bit bool
flag2: bool = false, // 1-bit bool
counter: u8{4} = 0b1010, // 4-bit field
_: u8{2}, // 2-bit reserved field
}
}
Fields can be comprised of arbitrary bits without order, even of non-contiguous bit layouts.
use embedded_interfaces::codegen::interface_objects;
interface_objects! {
struct Interleaved(size = 2) {
// Interleaved bits: even positions for one field, odd for another
field1: u8[0,2,4,6,8,10,12,14] = 0x55,
field2: u8[1,3,5,7,9,11,13,15] = 0xAA,
}
}
Fields without explicit bit ranges will automatically use the next N bits after the highest previously used bit. The macro will ensure that all bits are used exactly once at compile time.
use embedded_interfaces::codegen::interface_objects;
interface_objects! {
struct Ranges(size = 2) {
f3: u8[4..12] // 8 specific bits
f1: u8[0..4] // The first 4 bits
f2: u8{4} // The next 4 bits after the highest used until now (so 12,13,14,15)
}
}
You can define enums with a specific bit-width and powerful value patterns:
use embedded_interfaces::codegen::interface_objects;
interface_objects! {
// Underlying type must be some unsigned type, and exact bit width must be given
enum Status: u8{4} {
0x0 Off, // Single value
1..=3 Low(u8), // Value range, captures the specific value
4..8 Medium, // Exclusive range, doesn't capture the value. Packing will use lowest associated value.
8|10|12..16 High, // Specific values
_ Invalid(u8), // Wildcard catch-all
}
struct Device(size = 1) {
mode: Status = Status::Off, // Enums can be used directly in structs, the size is automatically known.
_: u8{4}, // Reserved bits
}
}
Bit-packed structures can be nested into other structures. The resulting bit-ranges will be fully resolved at compile time.
use embedded_interfaces::codegen::interface_objects;
interface_objects! {
struct Point(size = 2) {
x: u8 = 10,
y: u8 = 20,
}
struct Rectangle(size = 4) {
top_left: PointUnpacked = PointUnpacked { x: 1, y: 2 },
bottom_right: PointUnpacked = PointUnpacked { x: 3, y: 4 },
}
}
Arrays and nested arrays of primitive types work similarly, but arrays of structs or enums are currently not supported:
use embedded_interfaces::codegen::interface_objects;
interface_objects! {
struct DataPacket(size = 5) {
data: [u8; 4] = [0x01, 0x02, 0x03, 0x04],
flags: [bool; 8] = [true, false, true, false, true, false, true, false],
}
}
You can directly associate physical quantities from the uom crate with any
raw_* fields. The smallest representable value must be provided as a rational
a / b to allow the macro to convert between the raw and typed representation
automatically. For complex fields the conversion functions can also be given
directly.
New accessors without the raw_ prefix will also be generated to allow
convenient access, for example through a .read_temperature() or
.write_temperature() function.
use embedded_interfaces::codegen::interface_objects;
use uom::si::f64::{Pressure, ThermodynamicTemperature};
use uom::si::pressure::pascal;
use uom::si::thermodynamic_temperature::degree_celsius;
interface_objects! {
struct Units(size = 4) {
raw_temperature: u16 = 75 => {
quantity: ThermodynamicTemperature,
unit: degree_celsius,
lsb: 1f64 / 128f64,
},
raw_pressure: u16 = 12 => {
quantity: Pressure,
unit: pascal,
from_raw: |x| (x as f64 + 7.0) * 31.0,
into_raw: |x| (x / 31.0 - 7.0) as u16,
},
}
}
Registers are structs with some meta information, such as an address and codecs
that specify how this register has to be interfaced with. By replacing struct
with register, the macro will automatically implement the register specific
traits so they can be used with register based devices.
Apart from size, registers require some additional attributes to be set:
addr The register address
mode One of "r", "w" or "rw", specifying whether the register can
be read from, written to or both.
codec_error The error type which the codec may produce, usually () for
simple codecs.
i2c_codec The codec which is responsible to interface this register over
I2C. Often determines the address size in bytes. If I2C is not supported, set
this to UnsupportedCodec.
spi_codec The codec which is responsible to interface this register over
SPI. Often determines how the address header is structured. If SPI is not
supported, set this to UnsupportedCodec.
use embedded_interfaces::codegen::interface_objects;
use embedded_interfaces::registers::i2c::codecs::TwoByteRegAddrCodec;
type UnsupportedSpiCodec = embedded_interfaces::registers::spi::codecs::unsupported_codec::UnsupportedCodec<()>;
interface_objects! {
register ExampleRegister(addr = 0x1234, mode = r, size = 2, codec_error = (), i2c_codec = TwoByteRegAddrCodec, spi_codec = UnsupportedSpiCodec) {
value: u8,
flags: [bool; 8],
}
}
Often you will find yourself defining multiple registers for a device, each time repeating
the codec settings. The register_defaults block allows you to define this once in the beginning:
use embedded_interfaces::codegen::interface_objects;
use embedded_interfaces::registers::i2c::codecs::TwoByteRegAddrCodec;
type UnsupportedSpiCodec = embedded_interfaces::registers::spi::codecs::unsupported_codec::UnsupportedCodec<()>;
interface_objects! {
register_defaults {
codec_error = (),
i2c_codec = TwoByteRegAddrCodec,
spi_codec = UnsupportedSpiCodec,
}
register ExampleRegister(addr = 0x1234, mode = r, size = 2) {
value: u8,
flags: [bool; 8],
}
}