Skip to content

Commit 1d33458

Browse files
authored
Merge pull request #593 from rust-embedded/atomicdevice
bus: Adding AtomicDevice for I2C and SPI bus sharing in multiple interrupt contexts
2 parents 51ad126 + e4b8c77 commit 1d33458

File tree

8 files changed

+343
-1
lines changed

8 files changed

+343
-1
lines changed

embedded-hal-bus/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
77

88
## [Unreleased]
99

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

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

embedded-hal-bus/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ embedded-hal = { version = "1.0.0", path = "../embedded-hal" }
2424
embedded-hal-async = { version = "1.0.0", path = "../embedded-hal-async", optional = true }
2525
critical-section = { version = "1.0" }
2626
defmt-03 = { package = "defmt", version = "0.3", optional = true }
27+
portable-atomic = {version = "1", default-features = false}
2728

2829
[package.metadata.docs.rs]
2930
features = ["std", "async"]

embedded-hal-bus/src/i2c/atomic.rs

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
use embedded_hal::i2c::{Error, ErrorKind, ErrorType, I2c};
2+
3+
use crate::util::AtomicCell;
4+
5+
/// Atomics-based shared bus [`I2c`] implementation.
6+
///
7+
/// Sharing is implemented with a [`AtomicDevice`], which consists of an `UnsafeCell` and an `AtomicBool` "locked" flag.
8+
/// This means it has low overhead, like [`RefCellDevice`](crate::i2c::RefCellDevice). Aditionally, it is `Send`,
9+
/// which allows sharing a single bus across multiple threads (interrupt priority level), like [`CriticalSectionDevice`](crate::i2c::CriticalSectionDevice),
10+
/// while not using critical sections and therefore impacting real-time performance less.
11+
///
12+
/// The downside is using a simple `AtomicBool` for locking doesn't prevent two threads from trying to lock it at once.
13+
/// For example, the main thread can be doing an I2C transaction, and an interrupt fires and tries to do another. In this
14+
/// case, a `Busy` error is returned that must be handled somehow, usually dropping the data or trying again later.
15+
///
16+
/// Note that retrying in a loop on `Busy` errors usually leads to deadlocks. In the above example, it'll prevent the
17+
/// interrupt handler from returning, which will starve the main thread and prevent it from releasing the lock. If
18+
/// this is an issue for your use case, you most likely should use [`CriticalSectionDevice`](crate::i2c::CriticalSectionDevice) instead.
19+
///
20+
/// This primitive is particularly well-suited for applications that have external arbitration
21+
/// rules that prevent `Busy` errors in the first place, such as the RTIC framework.
22+
///
23+
/// # Examples
24+
///
25+
/// Assuming there is a pressure sensor with address `0x42` on the same bus as a temperature sensor
26+
/// with address `0x20`; [`AtomicDevice`] can be used to give access to both of these sensors
27+
/// from a single `i2c` instance.
28+
///
29+
/// ```
30+
/// use embedded_hal_bus::i2c;
31+
/// use embedded_hal_bus::util::AtomicCell;
32+
/// # use embedded_hal::i2c::{self as hali2c, SevenBitAddress, TenBitAddress, I2c, Operation, ErrorKind};
33+
/// # pub struct Sensor<I2C> {
34+
/// # i2c: I2C,
35+
/// # address: u8,
36+
/// # }
37+
/// # impl<I2C: I2c> Sensor<I2C> {
38+
/// # pub fn new(i2c: I2C, address: u8) -> Self {
39+
/// # Self { i2c, address }
40+
/// # }
41+
/// # }
42+
/// # type PressureSensor<I2C> = Sensor<I2C>;
43+
/// # type TemperatureSensor<I2C> = Sensor<I2C>;
44+
/// # pub struct I2c0;
45+
/// # #[derive(Debug, Copy, Clone, Eq, PartialEq)]
46+
/// # pub enum Error { }
47+
/// # impl hali2c::Error for Error {
48+
/// # fn kind(&self) -> hali2c::ErrorKind {
49+
/// # ErrorKind::Other
50+
/// # }
51+
/// # }
52+
/// # impl hali2c::ErrorType for I2c0 {
53+
/// # type Error = Error;
54+
/// # }
55+
/// # impl I2c<SevenBitAddress> for I2c0 {
56+
/// # fn transaction(&mut self, address: u8, operations: &mut [Operation<'_>]) -> Result<(), Self::Error> {
57+
/// # Ok(())
58+
/// # }
59+
/// # }
60+
/// # struct Hal;
61+
/// # impl Hal {
62+
/// # fn i2c(&self) -> I2c0 {
63+
/// # I2c0
64+
/// # }
65+
/// # }
66+
/// # let hal = Hal;
67+
///
68+
/// let i2c = hal.i2c();
69+
/// let i2c_cell = AtomicCell::new(i2c);
70+
/// let mut temperature_sensor = TemperatureSensor::new(
71+
/// i2c::AtomicDevice::new(&i2c_cell),
72+
/// 0x20,
73+
/// );
74+
/// let mut pressure_sensor = PressureSensor::new(
75+
/// i2c::AtomicDevice::new(&i2c_cell),
76+
/// 0x42,
77+
/// );
78+
/// ```
79+
pub struct AtomicDevice<'a, T> {
80+
bus: &'a AtomicCell<T>,
81+
}
82+
83+
#[derive(Debug, Copy, Clone)]
84+
/// Wrapper type for errors originating from the atomically-checked I2C bus manager.
85+
pub enum AtomicError<T: Error> {
86+
/// This error is returned if the I2C bus was already in use when an operation was attempted,
87+
/// which indicates that the driver requirements are not being met with regard to
88+
/// synchronization.
89+
Busy,
90+
91+
/// An I2C-related error occurred, and the internal error should be inspected.
92+
Other(T),
93+
}
94+
95+
impl<T: Error> Error for AtomicError<T> {
96+
fn kind(&self) -> ErrorKind {
97+
match self {
98+
AtomicError::Other(e) => e.kind(),
99+
_ => ErrorKind::Other,
100+
}
101+
}
102+
}
103+
104+
unsafe impl<'a, T> Send for AtomicDevice<'a, T> {}
105+
106+
impl<'a, T> AtomicDevice<'a, T>
107+
where
108+
T: I2c,
109+
{
110+
/// Create a new `AtomicDevice`.
111+
#[inline]
112+
pub fn new(bus: &'a AtomicCell<T>) -> Self {
113+
Self { bus }
114+
}
115+
116+
fn lock<R, F>(&self, f: F) -> Result<R, AtomicError<T::Error>>
117+
where
118+
F: FnOnce(&mut T) -> Result<R, <T as ErrorType>::Error>,
119+
{
120+
self.bus
121+
.busy
122+
.compare_exchange(
123+
false,
124+
true,
125+
core::sync::atomic::Ordering::SeqCst,
126+
core::sync::atomic::Ordering::SeqCst,
127+
)
128+
.map_err(|_| AtomicError::<T::Error>::Busy)?;
129+
130+
let result = f(unsafe { &mut *self.bus.bus.get() });
131+
132+
self.bus
133+
.busy
134+
.store(false, core::sync::atomic::Ordering::SeqCst);
135+
136+
result.map_err(AtomicError::Other)
137+
}
138+
}
139+
140+
impl<'a, T> ErrorType for AtomicDevice<'a, T>
141+
where
142+
T: I2c,
143+
{
144+
type Error = AtomicError<T::Error>;
145+
}
146+
147+
impl<'a, T> I2c for AtomicDevice<'a, T>
148+
where
149+
T: I2c,
150+
{
151+
#[inline]
152+
fn read(&mut self, address: u8, read: &mut [u8]) -> Result<(), Self::Error> {
153+
self.lock(|bus| bus.read(address, read))
154+
}
155+
156+
#[inline]
157+
fn write(&mut self, address: u8, write: &[u8]) -> Result<(), Self::Error> {
158+
self.lock(|bus| bus.write(address, write))
159+
}
160+
161+
#[inline]
162+
fn write_read(
163+
&mut self,
164+
address: u8,
165+
write: &[u8],
166+
read: &mut [u8],
167+
) -> Result<(), Self::Error> {
168+
self.lock(|bus| bus.write_read(address, write, read))
169+
}
170+
171+
#[inline]
172+
fn transaction(
173+
&mut self,
174+
address: u8,
175+
operations: &mut [embedded_hal::i2c::Operation<'_>],
176+
) -> Result<(), Self::Error> {
177+
self.lock(|bus| bus.transaction(address, operations))
178+
}
179+
}

embedded-hal-bus/src/i2c/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ mod mutex;
88
pub use mutex::*;
99
mod critical_section;
1010
pub use self::critical_section::*;
11+
mod atomic;
12+
pub use atomic::*;

embedded-hal-bus/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ use defmt_03 as defmt;
1818

1919
pub mod i2c;
2020
pub mod spi;
21+
pub mod util;

embedded-hal-bus/src/spi/atomic.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
use embedded_hal::delay::DelayNs;
2+
use embedded_hal::digital::OutputPin;
3+
use embedded_hal::spi::{Error, ErrorKind, ErrorType, Operation, SpiBus, SpiDevice};
4+
5+
use super::DeviceError;
6+
use crate::spi::shared::transaction;
7+
use crate::util::AtomicCell;
8+
9+
/// Atomics-based shared bus [`SpiDevice`] implementation.
10+
///
11+
/// This allows for sharing an [`SpiBus`], obtaining multiple [`SpiDevice`] instances,
12+
/// each with its own `CS` pin.
13+
///
14+
/// Sharing is implemented with a [`AtomicDevice`], which consists of an `UnsafeCell` and an `AtomicBool` "locked" flag.
15+
/// This means it has low overhead, like [`RefCellDevice`](crate::spi::RefCellDevice). Aditionally, it is `Send`,
16+
/// which allows sharing a single bus across multiple threads (interrupt priority level), like [`CriticalSectionDevice`](crate::spi::CriticalSectionDevice),
17+
/// while not using critical sections and therefore impacting real-time performance less.
18+
///
19+
/// The downside is using a simple `AtomicBool` for locking doesn't prevent two threads from trying to lock it at once.
20+
/// For example, the main thread can be doing a SPI transaction, and an interrupt fires and tries to do another. In this
21+
/// case, a `Busy` error is returned that must be handled somehow, usually dropping the data or trying again later.
22+
///
23+
/// Note that retrying in a loop on `Busy` errors usually leads to deadlocks. In the above example, it'll prevent the
24+
/// interrupt handler from returning, which will starve the main thread and prevent it from releasing the lock. If
25+
/// this is an issue for your use case, you most likely should use [`CriticalSectionDevice`](crate::spi::CriticalSectionDevice) instead.
26+
///
27+
/// This primitive is particularly well-suited for applications that have external arbitration
28+
/// rules that prevent `Busy` errors in the first place, such as the RTIC framework.
29+
pub struct AtomicDevice<'a, BUS, CS, D> {
30+
bus: &'a AtomicCell<BUS>,
31+
cs: CS,
32+
delay: D,
33+
}
34+
35+
#[derive(Debug, Copy, Clone)]
36+
/// Wrapper type for errors returned by [`AtomicDevice`].
37+
pub enum AtomicError<T: Error> {
38+
/// This error is returned if the SPI bus was already in use when an operation was attempted,
39+
/// which indicates that the driver requirements are not being met with regard to
40+
/// synchronization.
41+
Busy,
42+
43+
/// An SPI-related error occurred, and the internal error should be inspected.
44+
Other(T),
45+
}
46+
47+
impl<'a, BUS, CS, D> AtomicDevice<'a, BUS, CS, D> {
48+
/// Create a new [`AtomicDevice`].
49+
#[inline]
50+
pub fn new(bus: &'a AtomicCell<BUS>, cs: CS, delay: D) -> Self {
51+
Self { bus, cs, delay }
52+
}
53+
}
54+
55+
impl<'a, BUS, CS> AtomicDevice<'a, BUS, CS, super::NoDelay>
56+
where
57+
BUS: ErrorType,
58+
CS: OutputPin,
59+
{
60+
/// Create a new [`AtomicDevice`] without support for in-transaction delays.
61+
///
62+
/// **Warning**: The returned instance *technically* doesn't comply with the `SpiDevice`
63+
/// contract, which mandates delay support. It is relatively rare for drivers to use
64+
/// in-transaction delays, so you might still want to use this method because it's more practical.
65+
///
66+
/// Note that a future version of the driver might start using delays, causing your
67+
/// code to panic. This wouldn't be considered a breaking change from the driver side, because
68+
/// drivers are allowed to assume `SpiDevice` implementations comply with the contract.
69+
/// If you feel this risk outweighs the convenience of having `cargo` automatically upgrade
70+
/// the driver crate, you might want to pin the driver's version.
71+
///
72+
/// # Panics
73+
///
74+
/// The returned device will panic if you try to execute a transaction
75+
/// that contains any operations of type [`Operation::DelayNs`].
76+
#[inline]
77+
pub fn new_no_delay(bus: &'a AtomicCell<BUS>, cs: CS) -> Self {
78+
Self {
79+
bus,
80+
cs,
81+
delay: super::NoDelay,
82+
}
83+
}
84+
}
85+
86+
impl<T: Error> Error for AtomicError<T> {
87+
fn kind(&self) -> ErrorKind {
88+
match self {
89+
AtomicError::Other(e) => e.kind(),
90+
_ => ErrorKind::Other,
91+
}
92+
}
93+
}
94+
95+
impl<'a, BUS, CS, D> ErrorType for AtomicDevice<'a, BUS, CS, D>
96+
where
97+
BUS: ErrorType,
98+
CS: OutputPin,
99+
{
100+
type Error = AtomicError<DeviceError<BUS::Error, CS::Error>>;
101+
}
102+
103+
impl<'a, Word: Copy + 'static, BUS, CS, D> SpiDevice<Word> for AtomicDevice<'a, BUS, CS, D>
104+
where
105+
BUS: SpiBus<Word>,
106+
CS: OutputPin,
107+
D: DelayNs,
108+
{
109+
#[inline]
110+
fn transaction(&mut self, operations: &mut [Operation<'_, Word>]) -> Result<(), Self::Error> {
111+
self.bus
112+
.busy
113+
.compare_exchange(
114+
false,
115+
true,
116+
core::sync::atomic::Ordering::SeqCst,
117+
core::sync::atomic::Ordering::SeqCst,
118+
)
119+
.map_err(|_| AtomicError::Busy)?;
120+
121+
let bus = unsafe { &mut *self.bus.bus.get() };
122+
123+
let result = transaction(operations, bus, &mut self.delay, &mut self.cs);
124+
125+
self.bus
126+
.busy
127+
.store(false, core::sync::atomic::Ordering::SeqCst);
128+
129+
result.map_err(AtomicError::Other)
130+
}
131+
}

embedded-hal-bus/src/spi/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ pub use refcell::*;
1111
mod mutex;
1212
#[cfg(feature = "std")]
1313
pub use mutex::*;
14+
mod atomic;
1415
mod critical_section;
1516
mod shared;
17+
pub use atomic::*;
1618

1719
pub use self::critical_section::*;
1820

embedded-hal-bus/src/util.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//! Utilities shared by all bus types.
2+
3+
use core::cell::UnsafeCell;
4+
5+
/// Cell type used by [`spi::AtomicDevice`](crate::spi::AtomicDevice) and [`i2c::AtomicDevice`](crate::i2c::AtomicDevice).
6+
///
7+
/// To use `AtomicDevice`, you must wrap the bus with this struct, and then
8+
/// construct multiple `AtomicDevice` instances with references to it.
9+
pub struct AtomicCell<BUS> {
10+
pub(crate) bus: UnsafeCell<BUS>,
11+
pub(crate) busy: portable_atomic::AtomicBool,
12+
}
13+
14+
unsafe impl<BUS: Send> Send for AtomicCell<BUS> {}
15+
unsafe impl<BUS: Send> Sync for AtomicCell<BUS> {}
16+
17+
impl<BUS> AtomicCell<BUS> {
18+
/// Create a new `AtomicCell`
19+
pub fn new(bus: BUS) -> Self {
20+
Self {
21+
bus: UnsafeCell::new(bus),
22+
busy: portable_atomic::AtomicBool::from(false),
23+
}
24+
}
25+
}

0 commit comments

Comments
 (0)