Skip to main content

xpanse_api/bus/
spi.rs

1//! SPI bus abstraction.
2//!
3//! Wraps a concrete backend (hardware, PIO, or bit-banged) behind a single
4//! boxed trait object so that drivers can operate on an `SpiBusHandle`
5//! without knowing which backend was selected.
6//!
7//! [`BusAllocator`](crate::bus::allocator::BusAllocator) is the entry point used to
8//! allocate SPI buses at startup.
9
10use alloc::boxed::Box;
11use core::future::Future;
12use core::pin::Pin;
13
14/// Error returned by SPI operations.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, defmt::Format)]
16pub enum SpiError {
17    Overrun,
18    ModeFault,
19    Crc,
20    InvalidFrequency,
21    Other,
22}
23
24impl embedded_hal::spi::Error for SpiError {
25    fn kind(&self) -> embedded_hal::spi::ErrorKind {
26        match self {
27            SpiError::Overrun => embedded_hal::spi::ErrorKind::Overrun,
28            SpiError::ModeFault => embedded_hal::spi::ErrorKind::ModeFault,
29            SpiError::Crc => embedded_hal::spi::ErrorKind::FrameFormat,
30            SpiError::InvalidFrequency => embedded_hal::spi::ErrorKind::Other,
31            SpiError::Other => embedded_hal::spi::ErrorKind::Other,
32        }
33    }
34}
35
36impl From<embassy_rp::spi::Error> for SpiError {
37    fn from(_: embassy_rp::spi::Error) -> Self {
38        SpiError::Other
39    }
40}
41
42impl From<embassy_rp::pio_programs::spi::Error> for SpiError {
43    fn from(_: embassy_rp::pio_programs::spi::Error) -> Self {
44        SpiError::Other
45    }
46}
47
48/// Backend variant of an [`SpiBusHandle`].
49#[derive(Debug, Clone, Copy, PartialEq, Eq, defmt::Format)]
50pub enum SpiBusVersion {
51    /// Hardware SPI peripheral.
52    Hardware,
53    /// PIO-based bit-banged SPI.
54    Pio,
55    /// Pure GPIO bit-bang.
56    BitBang,
57}
58
59/// Async, trait-object-safe SPI bus.
60pub trait DynSpiBus {
61    fn flush<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = Result<(), SpiError>> + 'a>>;
62
63    fn write<'a>(
64        &'a mut self,
65        data: &'a [u8],
66    ) -> Pin<Box<dyn Future<Output = Result<(), SpiError>> + 'a>>;
67
68    fn read<'a>(
69        &'a mut self,
70        data: &'a mut [u8],
71    ) -> Pin<Box<dyn Future<Output = Result<(), SpiError>> + 'a>>;
72
73    fn transfer<'a>(
74        &'a mut self,
75        read: &'a mut [u8],
76        write: &'a [u8],
77    ) -> Pin<Box<dyn Future<Output = Result<(), SpiError>> + 'a>>;
78
79    fn transfer_in_place<'a>(
80        &'a mut self,
81        words: &'a mut [u8],
82    ) -> Pin<Box<dyn Future<Output = Result<(), SpiError>> + 'a>>;
83}
84
85/// Blocking-only SPI bus used by some backends.
86pub trait DynSpiBusBlocking {
87    fn flush_blocking(&mut self) -> Result<(), SpiError>;
88    fn write_blocking(&mut self, data: &[u8]) -> Result<(), SpiError>;
89    fn read_blocking(&mut self, data: &mut [u8]) -> Result<(), SpiError>;
90    fn transfer_blocking(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), SpiError>;
91    fn transfer_in_place_blocking(&mut self, words: &mut [u8]) -> Result<(), SpiError>;
92}
93
94/// Combined async + blocking trait-object SPI bus.
95pub trait DynSpiBusCombined: DynSpiBus + DynSpiBusBlocking + Send {}
96impl<T: DynSpiBus + DynSpiBusBlocking + Send> DynSpiBusCombined for T {}
97
98/// Owned handle to an async SPI bus.
99///
100/// Dropping this handle does not return its startup resources, so keep it alive
101/// for as long as you need SPI access.
102///
103/// # Example
104///
105/// ```ignore
106/// use xpanse_api::bus::spi::{SpiBusHandle, SpiError};
107///
108/// async fn write_read(bus: &mut SpiBusHandle, tx: &[u8], rx: &mut [u8]) -> Result<(), SpiError> {
109///     bus.transfer(rx, tx).await
110/// }
111/// ```
112#[must_use = "dropping a bus handle does not return its startup resources"]
113pub struct SpiBusHandle {
114    inner: Box<dyn DynSpiBusCombined>,
115    version: SpiBusVersion,
116}
117
118impl SpiBusHandle {
119    /// Wraps a boxed backend and records its [`SpiBusVersion`].
120    pub fn new(inner: Box<dyn DynSpiBusCombined>, version: SpiBusVersion) -> Self {
121        Self { inner, version }
122    }
123
124    /// Returns the backend variant.
125    pub fn version(&self) -> SpiBusVersion {
126        self.version
127    }
128
129    /// Flush the bus.
130    pub async fn flush(&mut self) -> Result<(), SpiError> {
131        self.inner.flush().await
132    }
133
134    /// Write bytes to the bus.
135    pub async fn write(&mut self, data: &[u8]) -> Result<(), SpiError> {
136        self.inner.write(data).await
137    }
138
139    /// Read bytes from the bus (write 0xFF to clocks).
140    pub async fn read(&mut self, data: &mut [u8]) -> Result<(), SpiError> {
141        self.inner.read(data).await
142    }
143
144    /// Write `write` and read `read` simultaneously.
145    pub async fn transfer(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), SpiError> {
146        self.inner.transfer(read, write).await
147    }
148
149    /// Transfer in place: `words` is both written and overwritten with read data.
150    pub async fn transfer_in_place(&mut self, words: &mut [u8]) -> Result<(), SpiError> {
151        self.inner.transfer_in_place(words).await
152    }
153
154    /// Flush the bus (blocking).
155    pub fn flush_blocking(&mut self) -> Result<(), SpiError> {
156        self.inner.flush_blocking()
157    }
158
159    /// Write bytes to the bus (blocking).
160    pub fn write_blocking(&mut self, data: &[u8]) -> Result<(), SpiError> {
161        self.inner.write_blocking(data)
162    }
163
164    /// Read bytes from the bus (blocking).
165    pub fn read_blocking(&mut self, data: &mut [u8]) -> Result<(), SpiError> {
166        self.inner.read_blocking(data)
167    }
168
169    /// Write and read simultaneously (blocking).
170    pub fn transfer_blocking(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), SpiError> {
171        self.inner.transfer_blocking(read, write)
172    }
173
174    /// Transfer in place (blocking).
175    pub fn transfer_in_place_blocking(&mut self, words: &mut [u8]) -> Result<(), SpiError> {
176        self.inner.transfer_in_place_blocking(words)
177    }
178}
179
180// ── embedded-hal trait impls on SpiBusHandle ──────────────────────────
181
182impl embedded_hal::spi::ErrorType for SpiBusHandle {
183    type Error = SpiError;
184}
185
186impl embedded_hal::spi::SpiBus<u8> for SpiBusHandle {
187    fn flush(&mut self) -> Result<(), SpiError> {
188        self.flush_blocking()
189    }
190
191    fn read(&mut self, words: &mut [u8]) -> Result<(), SpiError> {
192        self.read_blocking(words)
193    }
194
195    fn write(&mut self, words: &[u8]) -> Result<(), SpiError> {
196        self.write_blocking(words)
197    }
198
199    fn transfer(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), SpiError> {
200        self.transfer_blocking(read, write)
201    }
202
203    fn transfer_in_place(&mut self, words: &mut [u8]) -> Result<(), SpiError> {
204        self.transfer_in_place_blocking(words)
205    }
206}
207
208impl embedded_hal_async::spi::SpiBus<u8> for SpiBusHandle {
209    async fn flush(&mut self) -> Result<(), SpiError> {
210        self.flush().await
211    }
212
213    async fn read(&mut self, words: &mut [u8]) -> Result<(), SpiError> {
214        self.read(words).await
215    }
216
217    async fn write(&mut self, words: &[u8]) -> Result<(), SpiError> {
218        self.write(words).await
219    }
220
221    async fn transfer(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), SpiError> {
222        self.transfer(read, write).await
223    }
224
225    async fn transfer_in_place(&mut self, words: &mut [u8]) -> Result<(), SpiError> {
226        self.inner.transfer_in_place(words).await
227    }
228}
229
230// Blanket impls: anything implementing embedded-hal SpiBus<u8> gets our traits for free
231
232impl<T> DynSpiBusBlocking for T
233where
234    T: embedded_hal::spi::SpiBus<u8>,
235    T::Error: Into<SpiError>,
236    T: Send,
237{
238    fn flush_blocking(&mut self) -> Result<(), SpiError> {
239        self.flush().map_err(|e| e.into())
240    }
241
242    fn write_blocking(&mut self, data: &[u8]) -> Result<(), SpiError> {
243        self.write(data).map_err(|e| e.into())
244    }
245
246    fn read_blocking(&mut self, data: &mut [u8]) -> Result<(), SpiError> {
247        self.read(data).map_err(|e| e.into())
248    }
249
250    fn transfer_blocking(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), SpiError> {
251        self.transfer(read, write).map_err(|e| e.into())
252    }
253
254    fn transfer_in_place_blocking(&mut self, words: &mut [u8]) -> Result<(), SpiError> {
255        self.transfer_in_place(words).map_err(|e| e.into())
256    }
257}
258
259impl<T> DynSpiBus for T
260where
261    T: embedded_hal_async::spi::SpiBus<u8>,
262    T::Error: Into<SpiError>,
263    T: Send,
264{
265    fn flush<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = Result<(), SpiError>> + 'a>> {
266        Box::pin(async move {
267            embedded_hal_async::spi::SpiBus::flush(self)
268                .await
269                .map_err(|e| e.into())
270        })
271    }
272
273    fn write<'a>(
274        &'a mut self,
275        data: &'a [u8],
276    ) -> Pin<Box<dyn Future<Output = Result<(), SpiError>> + 'a>> {
277        Box::pin(async move {
278            embedded_hal_async::spi::SpiBus::write(self, data)
279                .await
280                .map_err(|e| e.into())
281        })
282    }
283
284    fn read<'a>(
285        &'a mut self,
286        data: &'a mut [u8],
287    ) -> Pin<Box<dyn Future<Output = Result<(), SpiError>> + 'a>> {
288        Box::pin(async move {
289            embedded_hal_async::spi::SpiBus::read(self, data)
290                .await
291                .map_err(|e| e.into())
292        })
293    }
294
295    fn transfer<'a>(
296        &'a mut self,
297        read: &'a mut [u8],
298        write: &'a [u8],
299    ) -> Pin<Box<dyn Future<Output = Result<(), SpiError>> + 'a>> {
300        Box::pin(async move {
301            embedded_hal_async::spi::SpiBus::transfer(self, read, write)
302                .await
303                .map_err(|e| e.into())
304        })
305    }
306
307    fn transfer_in_place<'a>(
308        &'a mut self,
309        words: &'a mut [u8],
310    ) -> Pin<Box<dyn Future<Output = Result<(), SpiError>> + 'a>> {
311        Box::pin(async move {
312            embedded_hal_async::spi::SpiBus::transfer_in_place(self, words)
313                .await
314                .map_err(|e| e.into())
315        })
316    }
317}