Skip to content

Commit

Permalink
Merge pull request #593 from rust-embedded/atomicdevice
Browse files Browse the repository at this point in the history
bus: Adding AtomicDevice for I2C and SPI bus sharing in multiple interrupt contexts
  • Loading branch information
eldruin committed Apr 23, 2024
2 parents 51ad126 + e4b8c77 commit 1d33458
Show file tree
Hide file tree
Showing 8 changed files with 343 additions and 1 deletion.
3 changes: 2 additions & 1 deletion embedded-hal-bus/CHANGELOG.md
Expand Up @@ -7,7 +7,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

No unreleased changes
### Added
- Added a new `AtomicDevice` for I2C and SPI to enable bus sharing across multiple contexts.

## [v0.1.0] - 2023-12-28

Expand Down
1 change: 1 addition & 0 deletions embedded-hal-bus/Cargo.toml
Expand Up @@ -24,6 +24,7 @@ embedded-hal = { version = "1.0.0", path = "../embedded-hal" }
embedded-hal-async = { version = "1.0.0", path = "../embedded-hal-async", optional = true }
critical-section = { version = "1.0" }
defmt-03 = { package = "defmt", version = "0.3", optional = true }
portable-atomic = {version = "1", default-features = false}

[package.metadata.docs.rs]
features = ["std", "async"]
Expand Down
179 changes: 179 additions & 0 deletions embedded-hal-bus/src/i2c/atomic.rs
@@ -0,0 +1,179 @@
use embedded_hal::i2c::{Error, ErrorKind, ErrorType, I2c};

use crate::util::AtomicCell;

/// Atomics-based shared bus [`I2c`] implementation.
///
/// Sharing is implemented with a [`AtomicDevice`], which consists of an `UnsafeCell` and an `AtomicBool` "locked" flag.
/// This means it has low overhead, like [`RefCellDevice`](crate::i2c::RefCellDevice). Aditionally, it is `Send`,
/// which allows sharing a single bus across multiple threads (interrupt priority level), like [`CriticalSectionDevice`](crate::i2c::CriticalSectionDevice),
/// while not using critical sections and therefore impacting real-time performance less.
///
/// The downside is using a simple `AtomicBool` for locking doesn't prevent two threads from trying to lock it at once.
/// For example, the main thread can be doing an I2C transaction, and an interrupt fires and tries to do another. In this
/// case, a `Busy` error is returned that must be handled somehow, usually dropping the data or trying again later.
///
/// Note that retrying in a loop on `Busy` errors usually leads to deadlocks. In the above example, it'll prevent the
/// interrupt handler from returning, which will starve the main thread and prevent it from releasing the lock. If
/// this is an issue for your use case, you most likely should use [`CriticalSectionDevice`](crate::i2c::CriticalSectionDevice) instead.
///
/// This primitive is particularly well-suited for applications that have external arbitration
/// rules that prevent `Busy` errors in the first place, such as the RTIC framework.
///
/// # Examples
///
/// Assuming there is a pressure sensor with address `0x42` on the same bus as a temperature sensor
/// with address `0x20`; [`AtomicDevice`] can be used to give access to both of these sensors
/// from a single `i2c` instance.
///
/// ```
/// use embedded_hal_bus::i2c;
/// use embedded_hal_bus::util::AtomicCell;
/// # use embedded_hal::i2c::{self as hali2c, SevenBitAddress, TenBitAddress, I2c, Operation, ErrorKind};
/// # pub struct Sensor<I2C> {
/// # i2c: I2C,
/// # address: u8,
/// # }
/// # impl<I2C: I2c> Sensor<I2C> {
/// # pub fn new(i2c: I2C, address: u8) -> Self {
/// # Self { i2c, address }
/// # }
/// # }
/// # type PressureSensor<I2C> = Sensor<I2C>;
/// # type TemperatureSensor<I2C> = Sensor<I2C>;
/// # pub struct I2c0;
/// # #[derive(Debug, Copy, Clone, Eq, PartialEq)]
/// # pub enum Error { }
/// # impl hali2c::Error for Error {
/// # fn kind(&self) -> hali2c::ErrorKind {
/// # ErrorKind::Other
/// # }
/// # }
/// # impl hali2c::ErrorType for I2c0 {
/// # type Error = Error;
/// # }
/// # impl I2c<SevenBitAddress> for I2c0 {
/// # fn transaction(&mut self, address: u8, operations: &mut [Operation<'_>]) -> Result<(), Self::Error> {
/// # Ok(())
/// # }
/// # }
/// # struct Hal;
/// # impl Hal {
/// # fn i2c(&self) -> I2c0 {
/// # I2c0
/// # }
/// # }
/// # let hal = Hal;
///
/// let i2c = hal.i2c();
/// let i2c_cell = AtomicCell::new(i2c);
/// let mut temperature_sensor = TemperatureSensor::new(
/// i2c::AtomicDevice::new(&i2c_cell),
/// 0x20,
/// );
/// let mut pressure_sensor = PressureSensor::new(
/// i2c::AtomicDevice::new(&i2c_cell),
/// 0x42,
/// );
/// ```
pub struct AtomicDevice<'a, T> {
bus: &'a AtomicCell<T>,
}

#[derive(Debug, Copy, Clone)]
/// Wrapper type for errors originating from the atomically-checked I2C bus manager.
pub enum AtomicError<T: Error> {
/// This error is returned if the I2C bus was already in use when an operation was attempted,
/// which indicates that the driver requirements are not being met with regard to
/// synchronization.
Busy,

/// An I2C-related error occurred, and the internal error should be inspected.
Other(T),
}

impl<T: Error> Error for AtomicError<T> {
fn kind(&self) -> ErrorKind {
match self {
AtomicError::Other(e) => e.kind(),
_ => ErrorKind::Other,
}
}
}

unsafe impl<'a, T> Send for AtomicDevice<'a, T> {}

impl<'a, T> AtomicDevice<'a, T>
where
T: I2c,
{
/// Create a new `AtomicDevice`.
#[inline]
pub fn new(bus: &'a AtomicCell<T>) -> Self {
Self { bus }
}

fn lock<R, F>(&self, f: F) -> Result<R, AtomicError<T::Error>>
where
F: FnOnce(&mut T) -> Result<R, <T as ErrorType>::Error>,
{
self.bus
.busy
.compare_exchange(
false,
true,
core::sync::atomic::Ordering::SeqCst,
core::sync::atomic::Ordering::SeqCst,
)
.map_err(|_| AtomicError::<T::Error>::Busy)?;

let result = f(unsafe { &mut *self.bus.bus.get() });

self.bus
.busy
.store(false, core::sync::atomic::Ordering::SeqCst);

result.map_err(AtomicError::Other)
}
}

impl<'a, T> ErrorType for AtomicDevice<'a, T>
where
T: I2c,
{
type Error = AtomicError<T::Error>;
}

impl<'a, T> I2c for AtomicDevice<'a, T>
where
T: I2c,
{
#[inline]
fn read(&mut self, address: u8, read: &mut [u8]) -> Result<(), Self::Error> {
self.lock(|bus| bus.read(address, read))
}

#[inline]
fn write(&mut self, address: u8, write: &[u8]) -> Result<(), Self::Error> {
self.lock(|bus| bus.write(address, write))
}

#[inline]
fn write_read(
&mut self,
address: u8,
write: &[u8],
read: &mut [u8],
) -> Result<(), Self::Error> {
self.lock(|bus| bus.write_read(address, write, read))
}

#[inline]
fn transaction(
&mut self,
address: u8,
operations: &mut [embedded_hal::i2c::Operation<'_>],
) -> Result<(), Self::Error> {
self.lock(|bus| bus.transaction(address, operations))
}
}
2 changes: 2 additions & 0 deletions embedded-hal-bus/src/i2c/mod.rs
Expand Up @@ -8,3 +8,5 @@ mod mutex;
pub use mutex::*;
mod critical_section;
pub use self::critical_section::*;
mod atomic;
pub use atomic::*;
1 change: 1 addition & 0 deletions embedded-hal-bus/src/lib.rs
Expand Up @@ -18,3 +18,4 @@ use defmt_03 as defmt;

pub mod i2c;
pub mod spi;
pub mod util;
131 changes: 131 additions & 0 deletions embedded-hal-bus/src/spi/atomic.rs
@@ -0,0 +1,131 @@
use embedded_hal::delay::DelayNs;
use embedded_hal::digital::OutputPin;
use embedded_hal::spi::{Error, ErrorKind, ErrorType, Operation, SpiBus, SpiDevice};

use super::DeviceError;
use crate::spi::shared::transaction;
use crate::util::AtomicCell;

/// Atomics-based shared bus [`SpiDevice`] implementation.
///
/// This allows for sharing an [`SpiBus`], obtaining multiple [`SpiDevice`] instances,
/// each with its own `CS` pin.
///
/// Sharing is implemented with a [`AtomicDevice`], which consists of an `UnsafeCell` and an `AtomicBool` "locked" flag.
/// This means it has low overhead, like [`RefCellDevice`](crate::spi::RefCellDevice). Aditionally, it is `Send`,
/// which allows sharing a single bus across multiple threads (interrupt priority level), like [`CriticalSectionDevice`](crate::spi::CriticalSectionDevice),
/// while not using critical sections and therefore impacting real-time performance less.
///
/// The downside is using a simple `AtomicBool` for locking doesn't prevent two threads from trying to lock it at once.
/// For example, the main thread can be doing a SPI transaction, and an interrupt fires and tries to do another. In this
/// case, a `Busy` error is returned that must be handled somehow, usually dropping the data or trying again later.
///
/// Note that retrying in a loop on `Busy` errors usually leads to deadlocks. In the above example, it'll prevent the
/// interrupt handler from returning, which will starve the main thread and prevent it from releasing the lock. If
/// this is an issue for your use case, you most likely should use [`CriticalSectionDevice`](crate::spi::CriticalSectionDevice) instead.
///
/// This primitive is particularly well-suited for applications that have external arbitration
/// rules that prevent `Busy` errors in the first place, such as the RTIC framework.
pub struct AtomicDevice<'a, BUS, CS, D> {
bus: &'a AtomicCell<BUS>,
cs: CS,
delay: D,
}

#[derive(Debug, Copy, Clone)]
/// Wrapper type for errors returned by [`AtomicDevice`].
pub enum AtomicError<T: Error> {
/// This error is returned if the SPI bus was already in use when an operation was attempted,
/// which indicates that the driver requirements are not being met with regard to
/// synchronization.
Busy,

/// An SPI-related error occurred, and the internal error should be inspected.
Other(T),
}

impl<'a, BUS, CS, D> AtomicDevice<'a, BUS, CS, D> {
/// Create a new [`AtomicDevice`].
#[inline]
pub fn new(bus: &'a AtomicCell<BUS>, cs: CS, delay: D) -> Self {
Self { bus, cs, delay }
}
}

impl<'a, BUS, CS> AtomicDevice<'a, BUS, CS, super::NoDelay>
where
BUS: ErrorType,
CS: OutputPin,
{
/// Create a new [`AtomicDevice`] without support for in-transaction delays.
///
/// **Warning**: The returned instance *technically* doesn't comply with the `SpiDevice`
/// contract, which mandates delay support. It is relatively rare for drivers to use
/// in-transaction delays, so you might still want to use this method because it's more practical.
///
/// Note that a future version of the driver might start using delays, causing your
/// code to panic. This wouldn't be considered a breaking change from the driver side, because
/// drivers are allowed to assume `SpiDevice` implementations comply with the contract.
/// If you feel this risk outweighs the convenience of having `cargo` automatically upgrade
/// the driver crate, you might want to pin the driver's version.
///
/// # Panics
///
/// The returned device will panic if you try to execute a transaction
/// that contains any operations of type [`Operation::DelayNs`].
#[inline]
pub fn new_no_delay(bus: &'a AtomicCell<BUS>, cs: CS) -> Self {
Self {
bus,
cs,
delay: super::NoDelay,
}
}
}

impl<T: Error> Error for AtomicError<T> {
fn kind(&self) -> ErrorKind {
match self {
AtomicError::Other(e) => e.kind(),
_ => ErrorKind::Other,
}
}
}

impl<'a, BUS, CS, D> ErrorType for AtomicDevice<'a, BUS, CS, D>
where
BUS: ErrorType,
CS: OutputPin,
{
type Error = AtomicError<DeviceError<BUS::Error, CS::Error>>;
}

impl<'a, Word: Copy + 'static, BUS, CS, D> SpiDevice<Word> for AtomicDevice<'a, BUS, CS, D>
where
BUS: SpiBus<Word>,
CS: OutputPin,
D: DelayNs,
{
#[inline]
fn transaction(&mut self, operations: &mut [Operation<'_, Word>]) -> Result<(), Self::Error> {
self.bus
.busy
.compare_exchange(
false,
true,
core::sync::atomic::Ordering::SeqCst,
core::sync::atomic::Ordering::SeqCst,
)
.map_err(|_| AtomicError::Busy)?;

let bus = unsafe { &mut *self.bus.bus.get() };

let result = transaction(operations, bus, &mut self.delay, &mut self.cs);

self.bus
.busy
.store(false, core::sync::atomic::Ordering::SeqCst);

result.map_err(AtomicError::Other)
}
}
2 changes: 2 additions & 0 deletions embedded-hal-bus/src/spi/mod.rs
Expand Up @@ -11,8 +11,10 @@ pub use refcell::*;
mod mutex;
#[cfg(feature = "std")]
pub use mutex::*;
mod atomic;
mod critical_section;
mod shared;
pub use atomic::*;

pub use self::critical_section::*;

Expand Down
25 changes: 25 additions & 0 deletions embedded-hal-bus/src/util.rs
@@ -0,0 +1,25 @@
//! Utilities shared by all bus types.

use core::cell::UnsafeCell;

/// Cell type used by [`spi::AtomicDevice`](crate::spi::AtomicDevice) and [`i2c::AtomicDevice`](crate::i2c::AtomicDevice).
///
/// To use `AtomicDevice`, you must wrap the bus with this struct, and then
/// construct multiple `AtomicDevice` instances with references to it.
pub struct AtomicCell<BUS> {
pub(crate) bus: UnsafeCell<BUS>,
pub(crate) busy: portable_atomic::AtomicBool,
}

unsafe impl<BUS: Send> Send for AtomicCell<BUS> {}
unsafe impl<BUS: Send> Sync for AtomicCell<BUS> {}

impl<BUS> AtomicCell<BUS> {
/// Create a new `AtomicCell`
pub fn new(bus: BUS) -> Self {
Self {
bus: UnsafeCell::new(bus),
busy: portable_atomic::AtomicBool::from(false),
}
}
}

0 comments on commit 1d33458

Please sign in to comment.