Skip to main content

rivet_bsp_support/
pl022.rs

1//! PL022 SSP/SPI driver, this workspace's reference async peripheral
2//! driver: `embedded_hal::spi::SpiBus` (polling, safe from a
3//! preemptive task) and `embedded_hal_async::spi::SpiBus` (RX-ready
4//! completion via [`rivet::sync::Signal`], driven by a real hardware
5//! interrupt) on the same struct — same "sync trait for the preemptive
6//! tier, async trait for the cooperative tier" split `RivetDelay`
7//! resolves for `DelayNs`, just for a peripheral whose async completion
8//! is a genuine ISR instead of a timer.
9//!
10//! Not board-agnostic the way `ns16550`/`serial` are: PL022 is the SPI
11//! block on lm3s6965evb and mps2-an385 specifically (both QEMU-modeled,
12//! both support `CR1.LBM` loopback — TX FIFO copied straight to RX FIFO
13//! in the emulator, no external device needed, which is what makes it a
14//! real, deterministic, CI-testable interrupt path). A board owns the
15//! base address and IRQ number; this module owns the register protocol.
16//!
17//! Two QEMU model quirks shape the async design (confirmed against this
18//! machine's QEMU 8.2.2 PL022 model directly, not just the datasheet):
19//! - `RXIM` only asserts once the RX FIFO holds >= 4 bytes — shorter
20//!   transfers never raise it under emulation. Callers doing real async
21//!   transfers should use >= 4-byte chunks; this driver still completes
22//!   shorter reads/writes correctly on real PL022 hardware, where RTIM
23//!   (never modelled in QEMU) covers the short case.
24//! - `TXIM` is essentially always asserted (transfers are instantaneous
25//!   in the model), so enabling it without immediately masking it is an
26//!   interrupt storm. This driver only ever unmasks `RXIM`.
27
28use core::convert::Infallible;
29
30use rivet::sync::Signal;
31
32const CR0: usize = 0x000;
33const CR1: usize = 0x004;
34const DR: usize = 0x008;
35const SR: usize = 0x00C;
36const CPSR: usize = 0x010;
37const IMSC: usize = 0x014;
38
39const SR_TNF: u32 = 1 << 1; // TX FIFO not full
40const SR_RNE: u32 = 1 << 2; // RX FIFO not empty
41const SR_BSY: u32 = 1 << 4; // controller busy
42
43const CR1_LBM: u32 = 1 << 0; // loopback: TX FIFO feeds RX FIFO directly
44const CR1_SSE: u32 = 1 << 1; // synchronous serial enable
45
46const IMSC_RXIM: u32 = 1 << 2;
47
48/// Real PL022 hardware FIFO depth (ARM PL022 TRM) — the unit this
49/// driver's async path completes in per hardware round trip; longer
50/// buffers are chunked automatically.
51const FIFO_DEPTH: usize = 8;
52
53/// One physical PL022 controller: a fixed register base and the
54/// [`Signal`] its interrupt handler completes.
55///
56/// Construct via [`pl022_instance!`], not `new` directly at a call site
57/// that doesn't also declare the matching ISR — the macro is what
58/// guarantees the `Signal` a given base's ISR touches is the same one
59/// this handle reads. A hand-written `fn isr<const BASE: usize>()` with
60/// an inline `static Signal` would *not* get a separate static per
61/// const-generic instantiation; every base address's ISR would silently
62/// share one `Signal`.
63pub struct Pl022 {
64    base: usize,
65    sig: &'static Signal,
66}
67
68impl Pl022 {
69    /// # Safety
70    /// `base` must be a real, exclusively-owned PL022 register block,
71    /// and `sig` must be the exact [`Signal`] the ISR registered for
72    /// this instance (see [`pl022_instance!`]) calls
73    /// [`Signal::signal`] on.
74    pub const unsafe fn new(base: usize, sig: &'static Signal) -> Self {
75        Self { base, sig }
76    }
77
78    fn reg(&self, offset: usize) -> *mut u32 {
79        (self.base + offset) as *mut u32
80    }
81
82    /// Bring the controller up: master mode (`CR1.MS` = 0), 8-bit
83    /// Motorola SPI frames, optionally looped back internally. `CPSR`/
84    /// `CR0.SCR` set the bit rate (`SSPCLK / (CPSR * (1 + SCR))`); the
85    /// fixed divisor here is untuned since QEMU's model transfers
86    /// instantaneously regardless — a real-hardware board picks its own.
87    pub fn init(&self, loopback: bool) {
88        // SAFETY: fixed PL022 registers, exclusively owned per the
89        // constructor's contract.
90        unsafe {
91            core::ptr::write_volatile(self.reg(CR1), 0); // SSE=0 while configuring
92            core::ptr::write_volatile(self.reg(CPSR), 2); // even divisor >= 2, per the TRM
93            core::ptr::write_volatile(self.reg(CR0), 0x07); // DSS=0b0111 -> 8-bit data size
94            let mut cr1 = CR1_SSE;
95            if loopback {
96                cr1 |= CR1_LBM;
97            }
98            core::ptr::write_volatile(self.reg(CR1), cr1);
99        }
100    }
101
102    fn tx_byte(&self, b: u8) {
103        // SAFETY: see `init`.
104        unsafe {
105            while core::ptr::read_volatile(self.reg(SR)) & SR_TNF == 0 {
106                core::hint::spin_loop();
107            }
108            core::ptr::write_volatile(self.reg(DR), b as u32);
109        }
110    }
111
112    fn rx_byte_poll(&self) -> u8 {
113        // SAFETY: see `init`.
114        unsafe {
115            while core::ptr::read_volatile(self.reg(SR)) & SR_RNE == 0 {
116                core::hint::spin_loop();
117            }
118            core::ptr::read_volatile(self.reg(DR)) as u8
119        }
120    }
121
122    fn transfer_word_sync(&self, tx: u8) -> u8 {
123        self.tx_byte(tx);
124        self.rx_byte_poll()
125    }
126
127    /// One hardware round trip, up to [`FIFO_DEPTH`] bytes: unmask
128    /// `RXIM`, push `tx`, await the real interrupt, then drain exactly
129    /// `rx.len()` bytes. The ISR ([`isr_ack`]) only masks `RXIM` and
130    /// signals — it doesn't drain the FIFO itself, so the awaiting task
131    /// does that here, after `wait()` returns. Keeping the ISR body to
132    /// "mask + signal" is deliberate: see `docs/driver-authoring.md`'s
133    /// guidance on keeping ISR bodies minimal.
134    async fn transfer_chunk_async(&self, tx: &[u8], rx: &mut [u8]) {
135        debug_assert!(tx.len() <= FIFO_DEPTH && rx.len() <= FIFO_DEPTH);
136        self.sig.reset();
137        // SAFETY: see `init`.
138        unsafe {
139            core::ptr::write_volatile(self.reg(IMSC), IMSC_RXIM);
140        }
141        for &b in tx {
142            self.tx_byte(b);
143        }
144        self.sig.wait().await;
145        for slot in rx.iter_mut() {
146            *slot = self.rx_byte_poll();
147        }
148    }
149
150    /// Async transfer of `read`/`write` of possibly different lengths
151    /// (`embedded_hal_async::spi::SpiBus::transfer` semantics: write
152    /// past `read`'s length is sent and its response discarded; read
153    /// past `write`'s length sends `0`), chunked into
154    /// [`FIFO_DEPTH`]-sized hardware round trips.
155    async fn transfer_async(&self, read: &mut [u8], write: &[u8]) {
156        let n = read.len().max(write.len());
157        let mut i = 0;
158        while i < n {
159            let end = (i + FIFO_DEPTH).min(n);
160            let chunk_len = end - i;
161            let mut tx_buf = [0u8; FIFO_DEPTH];
162            for (k, slot) in tx_buf[..chunk_len].iter_mut().enumerate() {
163                *slot = write.get(i + k).copied().unwrap_or(0);
164            }
165            let mut rx_buf = [0u8; FIFO_DEPTH];
166            self.transfer_chunk_async(&tx_buf[..chunk_len], &mut rx_buf[..chunk_len])
167                .await;
168            for (k, &b) in rx_buf[..chunk_len].iter().enumerate() {
169                if let Some(slot) = read.get_mut(i + k) {
170                    *slot = b;
171                }
172            }
173            i = end;
174        }
175    }
176}
177
178/// Shared ISR body for every PL022 instance, called from the `fn()`
179/// [`pl022_instance!`] generates. Masks `RXIM` (level-triggered — it
180/// stays asserted until the FIFO drains below threshold, so masking
181/// rather than draining here is what stops it re-firing before the
182/// awaiting task gets a chance to run) and hands off via `Signal`.
183pub fn isr_ack(base: usize, sig: &Signal) {
184    // SAFETY: `base` is a real PL022 register block passed by the
185    // `pl022_instance!` caller, who owns it exclusively.
186    unsafe {
187        core::ptr::write_volatile((base + IMSC) as *mut u32, 0);
188    }
189    sig.signal();
190}
191
192/// Declares a `static Signal` and a named `fn()` ISR bound to it, for
193/// one physical PL022 instance's completion interrupt. See [`Pl022::new`]
194/// for why this can't just be a generic function with an inline static.
195///
196/// ```ignore
197/// rivet_bsp_support::pl022_instance!(SPI0_SIG, spi0_isr, base = 0x4000_8000);
198/// let spi0 = unsafe { rivet_bsp_support::pl022::Pl022::new(0x4000_8000, &SPI0_SIG) };
199/// rivet::irq::register(IRQ_SPI0, spi0_isr).unwrap();
200/// rivet::irq::enable(IRQ_SPI0);
201/// ```
202#[macro_export]
203macro_rules! pl022_instance {
204    ($sig_name:ident, $isr_name:ident, base = $base:expr) => {
205        static $sig_name: ::rivet::sync::Signal = ::rivet::sync::Signal::new();
206
207        fn $isr_name() {
208            $crate::pl022::isr_ack($base, &$sig_name);
209        }
210    };
211}
212
213impl embedded_hal::spi::ErrorType for Pl022 {
214    type Error = Infallible;
215}
216
217impl embedded_hal::spi::SpiBus<u8> for Pl022 {
218    fn read(&mut self, words: &mut [u8]) -> Result<(), Infallible> {
219        for w in words.iter_mut() {
220            *w = self.transfer_word_sync(0);
221        }
222        Ok(())
223    }
224
225    fn write(&mut self, words: &[u8]) -> Result<(), Infallible> {
226        for &w in words {
227            self.transfer_word_sync(w);
228        }
229        Ok(())
230    }
231
232    fn transfer(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), Infallible> {
233        let n = read.len().max(write.len());
234        for i in 0..n {
235            let tx = write.get(i).copied().unwrap_or(0);
236            let rx = self.transfer_word_sync(tx);
237            if let Some(slot) = read.get_mut(i) {
238                *slot = rx;
239            }
240        }
241        Ok(())
242    }
243
244    fn transfer_in_place(&mut self, words: &mut [u8]) -> Result<(), Infallible> {
245        for w in words.iter_mut() {
246            *w = self.transfer_word_sync(*w);
247        }
248        Ok(())
249    }
250
251    fn flush(&mut self) -> Result<(), Infallible> {
252        // SAFETY: see `init`.
253        unsafe {
254            while core::ptr::read_volatile(self.reg(SR)) & SR_BSY != 0 {
255                core::hint::spin_loop();
256            }
257        }
258        Ok(())
259    }
260}
261
262// `embedded_hal_async::spi::ErrorType` is the same trait re-exported from
263// `embedded_hal::spi` (see that crate's spi.rs) — one `ErrorType` impl
264// above covers both the sync and async `SpiBus` impls below.
265
266impl embedded_hal_async::spi::SpiBus<u8> for Pl022 {
267    async fn read(&mut self, words: &mut [u8]) -> Result<(), Infallible> {
268        self.transfer_async(words, &[]).await;
269        Ok(())
270    }
271
272    async fn write(&mut self, words: &[u8]) -> Result<(), Infallible> {
273        self.transfer_async(&mut [], words).await;
274        Ok(())
275    }
276
277    async fn transfer(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), Infallible> {
278        self.transfer_async(read, write).await;
279        Ok(())
280    }
281
282    async fn transfer_in_place(&mut self, words: &mut [u8]) -> Result<(), Infallible> {
283        let n = words.len();
284        let mut i = 0;
285        while i < n {
286            let end = (i + FIFO_DEPTH).min(n);
287            let chunk_len = end - i;
288            let mut tx_buf = [0u8; FIFO_DEPTH];
289            tx_buf[..chunk_len].copy_from_slice(&words[i..end]);
290            let mut rx_buf = [0u8; FIFO_DEPTH];
291            self.transfer_chunk_async(&tx_buf[..chunk_len], &mut rx_buf[..chunk_len])
292                .await;
293            words[i..end].copy_from_slice(&rx_buf[..chunk_len]);
294            i = end;
295        }
296        Ok(())
297    }
298
299    async fn flush(&mut self) -> Result<(), Infallible> {
300        // SAFETY: see `init`. Not routed through `Signal`/an interrupt —
301        // `BSY` is a fast, bounded poll (bus-idle check), not a
302        // multi-byte transfer completion.
303        unsafe {
304            while core::ptr::read_volatile(self.reg(SR)) & SR_BSY != 0 {
305                core::hint::spin_loop();
306            }
307        }
308        Ok(())
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315
316    // Compile-only: proves `Pl022` is usable through generic
317    // embedded-hal/-async code, not just directly, for both trait
318    // families on the same struct.
319    #[allow(dead_code)]
320    fn generic_sync<S: embedded_hal::spi::SpiBus<u8>>(s: &mut S, buf: &mut [u8]) {
321        let _ = s.transfer_in_place(buf);
322    }
323
324    #[allow(dead_code)]
325    async fn generic_async<S: embedded_hal_async::spi::SpiBus<u8>>(s: &mut S, buf: &mut [u8]) {
326        let _ = s.transfer_in_place(buf).await;
327    }
328
329    #[test]
330    fn type_checks() {
331        let _ = generic_sync::<Pl022>;
332        let _ = generic_async::<Pl022>;
333    }
334}