Skip to content

Commit

Permalink
docs: add comprehensive documentation for I2C/SPI modules
Browse files Browse the repository at this point in the history
- Add thorough documentation to I2C/SPI module functions
- Update README with complete example showing register and command usage
- Include both blocking and async examples with type inference
- Document command parameters and response handling
  • Loading branch information
BroderickCarlin committed Dec 9, 2024
1 parent 65670e4 commit a537271
Show file tree
Hide file tree
Showing 3 changed files with 376 additions and 33 deletions.
172 changes: 145 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,51 +81,169 @@ impl FromByteArray for MyRegister {
impl ReadableRegister for MyRegister {}
```

### Readable Registers

A register in which values can be retrieved from, or read from, is represented as any type that implements the `ReadableRegister` trait. This trait is very little more than just a marker trait, but it represents a type that is both a `Register` and that can be created from a byte array through the `FromByteArray` trait. The bulk of the work in writing a type that can be read from a register will be in implementing the `FromByteArray` trait.
### Complete Example

A type that implements the `ReadableRegister` trait can then be used with provided utility methods such as those provided by the `i2c` or `spi` modules.
Here's a complete example showing how to use registers and commands with both I2C and SPI devices:

### Writable Registers
```rust
use regiface::{
register, Command, FromByteArray, ReadableRegister, ToByteArray, WritableRegister,
NoParameters,
};

// Temperature register that can be read
#[register(0x00)]
#[derive(ReadableRegister)]
struct Temperature {
celsius: f32,
}

A register in which values can be written to is represented as any type that implements the `WritableRegister` trait. This trait is very little more than just a marker trait, but it represents a type that is both a `Register` and that can be serialized into a byte array through the `ToByteArray` trait. The bulk of the work in writing a type that can be written to a register will be in implementing the `ToByteArray` trait.
impl FromByteArray for Temperature {
type Array = [u8; 4];
type Error = &'static str;

A type that implements the `WritableRegister` trait can then be used with provided utility methods such as those provided by the `i2c` or `spi` modules.
fn from_bytes(bytes: Self::Array) -> Result<Self, Self::Error> {
Ok(Self {
celsius: f32::from_be_bytes(bytes),
})
}
}

### Commands
// Configuration register that can be written
#[register(0x01)]
#[derive(WritableRegister)]
struct Configuration {
sample_rate: u16,
enabled: bool,
}

A command represents an invokable action with optional parameters and response. Commands are implemented using the `Command` trait, which specifies both the command parameters and expected response type. For commands or responses without parameters, the `NoParameters` type can be used.
impl ToByteArray for Configuration {
type Array = [u8; 3];
type Error = &'static str;

```rust
use regiface::{Command, ToByteArray, FromByteArray, NoParameters};
fn to_bytes(&self) -> Result<Self::Array, Self::Error> {
let mut bytes = [0u8; 3];
bytes[0..2].copy_from_slice(&self.sample_rate.to_be_bytes());
bytes[2] = self.enabled as u8;
Ok(bytes)
}
}

struct GetTemperature;
// Command to perform calibration
struct Calibrate;

impl Command for GetTemperature {
type IdType = u8;
type CommandParameters = NoParameters;
type ResponseParameters = Temperature;
#[derive(Default)]
struct CalibrationParams {
reference_temp: f32,
}

fn id() -> Self::IdType {
0x42
}
impl ToByteArray for CalibrationParams {
type Array = [u8; 4];
type Error = &'static str;

fn invoking_parameters(self) -> Self::CommandParameters {
NoParameters::default()
fn to_bytes(&self) -> Result<Self::Array, Self::Error> {
Ok(self.reference_temp.to_be_bytes())
}
}

struct Temperature {
celsius: f32
struct CalibrationResponse {
offset: f32,
}

impl FromByteArray for Temperature {
type Error = core::convert::Infallible;
impl FromByteArray for CalibrationResponse {
type Array = [u8; 4];
type Error = &'static str;

fn from_bytes(bytes: Self::Array) -> Result<Self, Self::Error> {
let celsius = f32::from_be_bytes(bytes);
Ok(Self { celsius })
Ok(Self {
offset: f32::from_be_bytes(bytes),
})
}
}

impl Command for Calibrate {
type CommandParameters = CalibrationParams;
type ResponseParameters = CalibrationResponse;

fn id() -> regiface::id::RegisterId {
0xF0.into()
}

fn invoking_parameters(&self) -> Self::CommandParameters {
CalibrationParams {
reference_temp: 25.0,
}
}
}

// Using with I2C (async)
use embedded_hal_async::i2c::I2c;

async fn use_i2c_device<D: I2c<u8>>(i2c: &mut D) {
const DEVICE_ADDR: u8 = 0x48;

// Read temperature (type inference)
let temp: Temperature = regiface::i2c::r#async::read_register(i2c, DEVICE_ADDR)
.await
.unwrap();
println!("Temperature: {}°C", temp.celsius);

// Write configuration
let config = Configuration {
sample_rate: 100,
enabled: true,
};
regiface::i2c::r#async::write_register(i2c, DEVICE_ADDR, config)
.await
.unwrap();

// Execute calibration command
let result: CalibrationResponse = regiface::i2c::r#async::invoke_command(
i2c,
DEVICE_ADDR,
Calibrate,
)
.await
.unwrap();
println!("Calibration offset: {}", result.offset);
}

// Using with SPI (blocking)
use embedded_hal::spi::SpiDevice;

fn use_spi_device<D: SpiDevice>(spi: &mut D) {
// Read temperature (type inference)
let temp: Temperature = regiface::spi::blocking::read_register(spi).unwrap();
println!("Temperature: {}°C", temp.celsius);

// Write configuration
let config = Configuration {
sample_rate: 100,
enabled: true,
};
regiface::spi::blocking::write_register(spi, config).unwrap();

// Execute calibration command
let result: CalibrationResponse =
regiface::spi::blocking::invoke_command(spi, Calibrate).unwrap();
println!("Calibration offset: {}", result.offset);
}
```

### Readable Registers

A register in which values can be retrieved from, or read from, is represented as any type that implements the `ReadableRegister` trait. This trait is very little more than just a marker trait, but it represents a type that is both a `Register` and that can be created from a byte array through the `FromByteArray` trait. The bulk of the work in writing a type that can be read from a register will be in implementing the `FromByteArray` trait.

A type that implements the `ReadableRegister` trait can then be used with provided utility methods such as those provided by the `i2c` or `spi` modules.

### Writable Registers

A register in which values can be written to is represented as any type that implements the `WritableRegister` trait. This trait is very little more than just a marker trait, but it represents a type that is both a `Register` and that can be serialized into a byte array through the `ToByteArray` trait. The bulk of the work in writing a type that can be written to a register will be in implementing the `ToByteArray` trait.

A type that implements the `WritableRegister` trait can then be used with provided utility methods such as those provided by the `i2c` or `spi` modules.

### Commands

A command represents an invokable action with optional parameters and response. Commands are implemented using the `Command` trait, which specifies both the command parameters and expected response type. For commands or responses without parameters, the `NoParameters` type can be used.

The command's parameters must implement `ToByteArray` for serialization, and its response type must implement `FromByteArray` for deserialization. The command itself specifies an ID that uniquely identifies it to the device.
118 changes: 117 additions & 1 deletion regiface/src/i2c.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! A collection of utility functions for interfacing with registers across an I2C bus
//!
//! Provided are both blocking and async variants of all functions
//! This module provides both blocking and async variants of register read/write operations
//! and command invocation for I2C devices. All operations handle device addressing and
//! proper byte serialization/deserialization of register values.
use crate::{
byte_array::ByteArray as _,
Expand All @@ -12,6 +14,28 @@ use crate::{
pub mod r#async {
use super::*;

/// Read a register value from an I2C device.
///
/// This function performs a write-read I2C transaction, first sending the register ID
/// then reading the register value. The received bytes are deserialized into the
/// specified register type.
///
/// # Parameters
/// * `device` - The I2C device to communicate with
/// * `device_addr` - The I2C address of the target device
///
/// # Errors
/// * `ReadRegisterError::BusError` - Communication with the device failed
/// * `ReadRegisterError::DeserializationError` - Failed to convert received bytes into register value
///
/// # Example
/// ```no_run
/// # use embedded_hal_async::i2c::I2c;
/// # use regiface::i2c;
/// async fn read_temp<D: I2c<u8>>(device: &mut D) {
/// let temp: TemperatureRegister = i2c::r#async::read_register(device, 0x48).await.unwrap();
/// }
/// ```
pub async fn read_register<D, A, R>(
device: &mut D,
device_addr: A,
Expand All @@ -34,6 +58,30 @@ pub mod r#async {
R::from_bytes(buf).map_err(ReadRegisterError::DeserializationError)
}

/// Write a register value to an I2C device.
///
/// This function performs a write I2C transaction, sending both the register ID
/// and the serialized register value. The operation is atomic, using the device's
/// transaction capability to ensure both writes occur without interruption.
///
/// # Parameters
/// * `device` - The I2C device to communicate with
/// * `device_addr` - The I2C address of the target device
/// * `register` - The register value to write
///
/// # Errors
/// * `WriteRegisterError::BusError` - Communication with the device failed
/// * `WriteRegisterError::SerializationError` - Failed to convert register value to bytes
///
/// # Example
/// ```no_run
/// # use embedded_hal_async::i2c::I2c;
/// # use regiface::i2c;
/// async fn configure<D: I2c<u8>>(device: &mut D) {
/// let config = ConfigRegister::new(/* ... */);
/// i2c::r#async::write_register(device, 0x48, config).await.unwrap();
/// }
/// ```
pub async fn write_register<D, A, R>(
device: &mut D,
device_addr: A,
Expand Down Expand Up @@ -63,6 +111,34 @@ pub mod r#async {
.map_err(WriteRegisterError::BusError)
}

/// Invoke a command on an I2C device and receive its response.
///
/// This function performs a complete command transaction:
/// 1. Sends the command ID
/// 2. Sends the serialized command parameters
/// 3. Reads the command response
///
/// The entire operation is atomic, using the device's transaction capability to
/// ensure all steps occur without interruption.
///
/// # Parameters
/// * `device` - The I2C device to communicate with
/// * `device_addr` - The I2C address of the target device
/// * `cmd` - The command to invoke
///
/// # Errors
/// * `CommandError::BusError` - Communication with the device failed
/// * `CommandError::SerializationError` - Failed to convert command parameters to bytes
/// * `CommandError::DeserializationError` - Failed to convert received bytes into response parameters
///
/// # Example
/// ```no_run
/// # use embedded_hal_async::i2c::I2c;
/// # use regiface::i2c;
/// async fn perform_self_test<D: I2c<u8>>(device: &mut D) {
/// let result: SelfTestResponse = i2c::r#async::invoke_command(device, 0x48, SelfTestCommand).await.unwrap();
/// }
/// ```
#[allow(clippy::type_complexity)]
pub async fn invoke_command<D, A, C>(
device: &mut D,
Expand Down Expand Up @@ -109,6 +185,19 @@ pub mod r#async {
pub mod blocking {
use super::*;

/// Read a register value from an I2C device.
///
/// Blocking variant of [`read_register`](crate::i2c::r#async::read_register).
/// See the async function documentation for detailed behavior description.
///
/// # Example
/// ```no_run
/// # use embedded_hal::i2c::I2c;
/// # use regiface::i2c;
/// fn read_temp<D: I2c<u8>>(device: &mut D) {
/// let temp: TemperatureRegister = i2c::blocking::read_register(device, 0x48).unwrap();
/// }
/// ```
pub fn read_register<D, A, R>(
device: &mut D,
device_addr: A,
Expand All @@ -130,6 +219,20 @@ pub mod blocking {
R::from_bytes(buf).map_err(ReadRegisterError::DeserializationError)
}

/// Write a register value to an I2C device.
///
/// Blocking variant of [`write_register`](crate::i2c::r#async::write_register).
/// See the async function documentation for detailed behavior description.
///
/// # Example
/// ```no_run
/// # use embedded_hal::i2c::I2c;
/// # use regiface::i2c;
/// fn configure<D: I2c<u8>>(device: &mut D) {
/// let config = ConfigRegister::new(/* ... */);
/// i2c::blocking::write_register(device, 0x48, config).unwrap();
/// }
/// ```
pub fn write_register<D, A, R>(
device: &mut D,
device_addr: A,
Expand Down Expand Up @@ -158,6 +261,19 @@ pub mod blocking {
.map_err(WriteRegisterError::BusError)
}

/// Invoke a command on an I2C device and receive its response.
///
/// Blocking variant of [`invoke_command`](crate::i2c::r#async::invoke_command).
/// See the async function documentation for detailed behavior description.
///
/// # Example
/// ```no_run
/// # use embedded_hal::i2c::I2c;
/// # use regiface::i2c;
/// fn perform_self_test<D: I2c<u8>>(device: &mut D) {
/// let result: SelfTestResponse = i2c::blocking::invoke_command(device, 0x48, SelfTestCommand).unwrap();
/// }
/// ```
#[allow(clippy::type_complexity)]
pub fn invoke_command<D, A, C>(
device: &mut D,
Expand Down
Loading

0 comments on commit a537271

Please sign in to comment.