embedded-interfaces
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.
Features
- ๐๏ธ Bit-packed data structures via the
interface_objects!
macro for ergonomic zero-cost packed struct definitions - ๐งฎ Customizable bit patterns for non-contiguous field layouts
- ๐งฌ Flexible enum definitions supporting ranges, wildcards, and value capture
- ๐ Protocol abstraction of register-based and command-based access models on any supported bus
- ๐งต Supports both sync and async, simultaneously if needed
- ๐งช Physical quantities and units can directly be specified in struct/register definitions
Quick Start
This 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.
- Commands are a more abstract interface which allows downstream drivers to represent arbitrary combinations of sending data, reading data and waiting.
- Registers are data that can be read from or written to the device via a register pointer or address. The standard codecs that describe how this address and data needs to be communicated to the device are covered by this crate.
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.
Bit-packed structs
The interface_objects!
macro can be used to define bit-packed structs and
registers. It automatically generates:
- Packed structs with exact memory layout control
- Unpacked variants for easier access and manipulation
- Accessor methods that don't require full unpacking/packing
- Unit conversion functions when physical units are specified
- Debug format as in the image below to debug struct layout issues
Basic Struct Definition
Define a simple bit-packed struct with automatic field packing:
use interface_objects;
interface_objects!
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).
Defaults, field size control and reserved fields
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 interface_objects;
interface_objects!
Custom ranges and bit Patterns
Fields can be comprised of arbitrary bits without order, even of non-contiguous bit layouts.
use interface_objects;
interface_objects!
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 interface_objects;
interface_objects!
Flexible enums
You can define enums with a specific bit-width and powerful value patterns:
use interface_objects;
interface_objects!
Nested Structures
Bit-packed structures can be nested into other structures. The resulting bit-ranges will be fully resolved at compile time.
use interface_objects;
interface_objects!
Arrays
Arrays and nested arrays of primitive types work similarly, but arrays of structs or enums are currently not supported:
use interface_objects;
interface_objects!
Physical Units Integration
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 interface_objects;
use ;
use pascal;
use degree_celsius;
interface_objects!
Registers
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 interface_objects;
use TwoByteRegAddrCodec;
type UnsupportedSpiCodec = UnsupportedCodec;
interface_objects!
Register defaults
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 interface_objects;
use TwoByteRegAddrCodec;
type UnsupportedSpiCodec = UnsupportedCodec;
interface_objects!