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}