Skip to main content

ove/async_runtime/
spi.rs

1// Copyright (C) 2026 Kamil Lulko <kamil.lulko@gmail.com>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4//
5// This file is part of oveRTOS.
6
7//! Async SPI bus wrapper.
8//!
9//! Submits transfers via the C `ove_spi_transfer_async` API; the completion
10//! callback wakes a registered `AtomicWaker`. Per-peripheral state in the
11//! C storage struct enforces single-in-flight transfers — concurrent
12//! submissions return [`Error::BusBusy`].
13//!
14//! With the `embedded-hal-async` Cargo feature the wrapper additionally
15//! implements `embedded_hal_async::spi::SpiBus<u8>`, so any
16//! `embedded-hal-async`-aware sensor driver works on top of it.
17
18use ::core::cell::UnsafeCell;
19use ::core::ffi::c_void;
20use ::core::future::poll_fn;
21use ::core::sync::atomic::{AtomicI32, Ordering};
22use ::core::task::Poll;
23
24use embassy_sync::waitqueue::AtomicWaker;
25
26use crate::bindings;
27use crate::error::{Error, Result};
28
29/// Per-transfer slot — wakes the caller and stores the completion result.
30/// `UnsafeCell` rather than `Mutex` because we use atomics for the
31/// state field, and `AtomicWaker` is itself interior-mut-safe.
32pub struct DmaSlot {
33    waker: AtomicWaker,
34    result: AtomicI32,
35}
36
37impl DmaSlot {
38    /// Sentinel marking a transfer still in flight. `i32::MIN` is well
39    /// outside the negative-OVE-error range (which is -1 to -20-ish).
40    pub const PENDING: i32 = i32::MIN;
41
42    pub const fn new() -> Self {
43        Self {
44            waker: AtomicWaker::new(),
45            result: AtomicI32::new(Self::PENDING),
46        }
47    }
48
49    pub fn reset(&self) {
50        self.result.store(Self::PENDING, Ordering::Release);
51    }
52
53    pub fn result_store(&self, value: i32) {
54        self.result.store(value, Ordering::Release);
55    }
56
57    pub fn result_load(&self) -> i32 {
58        self.result.load(Ordering::Acquire)
59    }
60
61    pub fn register(&self, w: &::core::task::Waker) {
62        self.waker.register(w);
63    }
64
65    pub fn wake(&self) {
66        self.waker.wake();
67    }
68}
69
70unsafe extern "C" fn dma_complete_cb(result: ::core::ffi::c_int, user_data: *mut c_void) {
71    // SAFETY: user_data is a `*const DmaSlot` we passed in below; the
72    // slot is owned by the future and outlives the callback because the
73    // future awaits before dropping it.
74    let slot = unsafe { &*(user_data as *const DmaSlot) };
75    slot.result_store(result as i32);
76    slot.wake();
77}
78
79/// Async SPI bus master.
80pub struct AsyncSpi {
81    spi: bindings::ove_spi_t,
82    slot: UnsafeCell<DmaSlot>,
83}
84
85// SAFETY: the slot's atomic fields are interior-mutability-safe, and the
86// C-side enforces single-in-flight via the async_busy flag. Multiple
87// awaits on the same handle would race on the C-side and return
88// OVE_ERR_BUS_BUSY rather than corrupting state.
89unsafe impl Send for AsyncSpi {}
90unsafe impl Sync for AsyncSpi {}
91
92impl AsyncSpi {
93    /// Wrap an existing `ove_spi_t` handle for async use.
94    ///
95    /// # Safety
96    /// `handle` must be a valid SPI handle. The caller must not use the
97    /// same handle through a sync [`crate::spi::Spi`] wrapper while async
98    /// transfers are in flight.
99    #[inline]
100    pub const unsafe fn from_handle(handle: bindings::ove_spi_t) -> Self {
101        Self {
102            spi: handle,
103            slot: UnsafeCell::new(DmaSlot::new()),
104        }
105    }
106
107    /// Submit an async full-duplex transfer.
108    ///
109    /// `tx` and `rx` lengths must match the actual transfer size. The
110    /// future is `'a`-bound to the buffers so they can't be dropped
111    /// while in flight.
112    pub async fn transfer<'a>(
113        &'a self,
114        cs: Option<&'a bindings::ove_spi_cs>,
115        tx: &'a [u8],
116        rx: &'a mut [u8],
117    ) -> Result<()> {
118        let len = tx.len().max(rx.len());
119        if len == 0 {
120            return Ok(());
121        }
122        let cs_ptr = cs.map_or(::core::ptr::null(), |c| c as *const _);
123        let tx_ptr = if tx.is_empty() {
124            ::core::ptr::null()
125        } else {
126            tx.as_ptr().cast()
127        };
128        let rx_ptr = if rx.is_empty() {
129            ::core::ptr::null_mut()
130        } else {
131            rx.as_mut_ptr().cast()
132        };
133
134        // SAFETY: slot is owned by us and lives for 'a (until the future
135        // is dropped). The C side stops accessing it once the callback
136        // fires, and the .await below blocks until that happens.
137        let slot: &DmaSlot = unsafe { &*self.slot.get() };
138        slot.reset();
139
140        let rc = unsafe {
141            bindings::ove_spi_transfer_async(
142                self.spi,
143                cs_ptr,
144                tx_ptr,
145                rx_ptr,
146                len,
147                Some(dma_complete_cb),
148                slot as *const _ as *mut c_void,
149            )
150        };
151        Error::from_code(rc)?;
152
153        poll_fn(|cx| {
154            slot.register(cx.waker());
155            let r = slot.result_load();
156            if r == DmaSlot::PENDING {
157                Poll::Pending
158            } else {
159                Poll::Ready(Error::from_code(r))
160            }
161        })
162        .await
163    }
164
165    /// Async write-only transfer.
166    pub async fn write<'a>(
167        &'a self,
168        cs: Option<&'a bindings::ove_spi_cs>,
169        data: &'a [u8],
170    ) -> Result<()> {
171        let mut empty = [];
172        self.transfer(cs, data, &mut empty).await
173    }
174
175    /// Async read-only transfer.
176    pub async fn read<'a>(
177        &'a self,
178        cs: Option<&'a bindings::ove_spi_cs>,
179        buf: &'a mut [u8],
180    ) -> Result<()> {
181        let empty: &[u8] = &[];
182        self.transfer(cs, empty, buf).await
183    }
184}
185
186#[cfg(feature = "embedded-hal-async")]
187mod hal_async_impl {
188    use super::AsyncSpi;
189    use crate::error::Error;
190    use embedded_hal::spi::ErrorType;
191    use embedded_hal_async::spi::SpiBus;
192
193    impl ErrorType for AsyncSpi {
194        type Error = Error;
195    }
196
197    impl SpiBus<u8> for AsyncSpi {
198        async fn read(&mut self, words: &mut [u8]) -> Result<(), Self::Error> {
199            AsyncSpi::read(self, None, words).await
200        }
201
202        async fn write(&mut self, words: &[u8]) -> Result<(), Self::Error> {
203            AsyncSpi::write(self, None, words).await
204        }
205
206        async fn transfer(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), Self::Error> {
207            // embedded_hal_async::spi::SpiBus::transfer allows different
208            // tx/rx lengths; we pick the max and zero-extend the shorter
209            // side via the underlying ove_spi_transfer_async semantics.
210            AsyncSpi::transfer(self, None, write, read).await
211        }
212
213        async fn transfer_in_place(&mut self, words: &mut [u8]) -> Result<(), Self::Error> {
214            // ove_spi_transfer_async doesn't natively support tx==rx;
215            // copy through a stack buffer when small, otherwise iterate.
216            let mut buf = [0u8; 64];
217            let mut remaining = &mut words[..];
218            while !remaining.is_empty() {
219                let chunk = remaining.len().min(buf.len());
220                buf[..chunk].copy_from_slice(&remaining[..chunk]);
221                AsyncSpi::transfer(self, None, &buf[..chunk], &mut remaining[..chunk]).await?;
222                let split = ::core::mem::take(&mut remaining);
223                remaining = &mut split[chunk..];
224            }
225            Ok(())
226        }
227
228        async fn flush(&mut self) -> Result<(), Self::Error> {
229            Ok(())
230        }
231    }
232}