aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdrian Wowk <[email protected]>2025-07-02 20:35:18 -0500
committerDario Nieuwenhuis <[email protected]>2025-09-05 20:35:48 +0200
commit4cac3ac1d24d6b651d79bfca8401824c28f5102c (patch)
tree28d05c46d27cb1e8e7a85b1ad728c8af68bfdc9e
parent676f9da58360627699736c79a4e24ef20a2b9f87 (diff)
rp: add new pio spi program
This commit adds a new PIO program which implements SPI. This allows you to drive more than 2 SPI buses by using PIO state machines as additional duplex SPI interfaces. The driver supports both blocking and async modes of operation and exclusively uses the DMA for async IO.
-rw-r--r--embassy-rp/src/pio_programs/mod.rs1
-rw-r--r--embassy-rp/src/pio_programs/spi.rs433
2 files changed, 434 insertions, 0 deletions
diff --git a/embassy-rp/src/pio_programs/mod.rs b/embassy-rp/src/pio_programs/mod.rs
index 8eac328b3..d05ba3884 100644
--- a/embassy-rp/src/pio_programs/mod.rs
+++ b/embassy-rp/src/pio_programs/mod.rs
@@ -6,6 +6,7 @@ pub mod i2s;
6pub mod onewire; 6pub mod onewire;
7pub mod pwm; 7pub mod pwm;
8pub mod rotary_encoder; 8pub mod rotary_encoder;
9pub mod spi;
9pub mod stepper; 10pub mod stepper;
10pub mod uart; 11pub mod uart;
11pub mod ws2812; 12pub mod ws2812;
diff --git a/embassy-rp/src/pio_programs/spi.rs b/embassy-rp/src/pio_programs/spi.rs
new file mode 100644
index 000000000..27d401fc9
--- /dev/null
+++ b/embassy-rp/src/pio_programs/spi.rs
@@ -0,0 +1,433 @@
1//! PIO backed SPi drivers
2
3use core::marker::PhantomData;
4
5use embassy_futures::join::join;
6use embassy_hal_internal::Peri;
7use embedded_hal_02::spi::{Phase, Polarity};
8use fixed::{traits::ToFixed, types::extra::U8};
9
10use crate::{
11 clocks::clk_sys_freq,
12 dma::{AnyChannel, Channel},
13 gpio::Level,
14 pio::{Common, Direction, Instance, LoadedProgram, PioPin, ShiftDirection, StateMachine},
15 spi::{Async, Blocking, Mode},
16};
17
18/// This struct represents a uart tx program loaded into pio instruction memory.
19pub struct PioSpiProgram<'d, PIO: crate::pio::Instance> {
20 prg: LoadedProgram<'d, PIO>,
21}
22
23impl<'d, PIO: crate::pio::Instance> PioSpiProgram<'d, PIO> {
24 /// Load the spi program into the given pio
25 pub fn new(common: &mut crate::pio::Common<'d, PIO>, phase: Phase) -> Self {
26 // These PIO programs are taken straight from the datasheet (3.6.1 in
27 // RP2040 datasheet, 11.6.1 in RP2350 datasheet)
28
29 // Pin assignments:
30 // - SCK is side-set pin 0
31 // - MOSI is OUT pin 0
32 // - MISO is IN pin 0
33 //
34 // Autopush and autopull must be enabled, and the serial frame size is set by
35 // configuring the push/pull threshold. Shift left/right is fine, but you must
36 // justify the data yourself. This is done most conveniently for frame sizes of
37 // 8 or 16 bits by using the narrow store replication and narrow load byte
38 // picking behaviour of RP2040's IO fabric.
39
40 let prg = match phase {
41 Phase::CaptureOnFirstTransition => {
42 let prg = pio::pio_asm!(
43 r#"
44 .side_set 1
45
46 ; Clock phase = 0: data is captured on the leading edge of each SCK pulse, and
47 ; transitions on the trailing edge, or some time before the first leading edge.
48
49 out pins, 1 side 0 [1] ; Stall here on empty (sideset proceeds even if
50 in pins, 1 side 1 [1] ; instruction stalls, so we stall with SCK low)
51 "#
52 );
53
54 common.load_program(&prg.program)
55 }
56 Phase::CaptureOnSecondTransition => {
57 let prg = pio::pio_asm!(
58 r#"
59 .side_set 1
60
61 ; Clock phase = 1: data transitions on the leading edge of each SCK pulse, and
62 ; is captured on the trailing edge.
63
64 out x, 1 side 0 ; Stall here on empty (keep SCK deasserted)
65 mov pins, x side 1 [1] ; Output data, assert SCK (mov pins uses OUT mapping)
66 in pins, 1 side 0 ; Input data, deassert SCK
67 "#
68 );
69
70 common.load_program(&prg.program)
71 }
72 };
73
74 Self { prg }
75 }
76}
77
78/// PIO SPI errors.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80#[cfg_attr(feature = "defmt", derive(defmt::Format))]
81#[non_exhaustive]
82pub enum Error {
83 // No errors for now
84}
85
86/// PIO based Spi driver.
87///
88/// This driver is less flexible than the hardware backed one. Configuration can
89/// not be changed at runtime.
90pub struct Spi<'d, PIO: Instance, const SM: usize, M: Mode> {
91 sm: StateMachine<'d, PIO, SM>,
92 tx_dma: Option<Peri<'d, AnyChannel>>,
93 rx_dma: Option<Peri<'d, AnyChannel>>,
94 phantom: PhantomData<M>,
95}
96
97/// PIO SPI configuration.
98#[non_exhaustive]
99#[derive(Clone)]
100pub struct Config {
101 /// Frequency (Hz).
102 pub frequency: u32,
103 /// Polarity.
104 pub polarity: Polarity,
105}
106
107impl Default for Config {
108 fn default() -> Self {
109 Self {
110 frequency: 1_000_000,
111 polarity: Polarity::IdleLow,
112 }
113 }
114}
115
116impl<'d, PIO: Instance, const SM: usize, M: Mode> Spi<'d, PIO, SM, M> {
117 #[allow(clippy::too_many_arguments)]
118 fn new_inner(
119 pio: &mut Common<'d, PIO>,
120 mut sm: StateMachine<'d, PIO, SM>,
121 clk_pin: Peri<'d, impl PioPin>,
122 mosi_pin: Peri<'d, impl PioPin>,
123 miso_pin: Peri<'d, impl PioPin>,
124 tx_dma: Option<Peri<'d, AnyChannel>>,
125 rx_dma: Option<Peri<'d, AnyChannel>>,
126 program: &PioSpiProgram<'d, PIO>,
127 config: Config,
128 ) -> Self {
129 let mut clk_pin = pio.make_pio_pin(clk_pin);
130 let mosi_pin = pio.make_pio_pin(mosi_pin);
131 let miso_pin = pio.make_pio_pin(miso_pin);
132
133 if let Polarity::IdleHigh = config.polarity {
134 clk_pin.set_output_inversion(true);
135 } else {
136 clk_pin.set_output_inversion(false);
137 }
138
139 sm.set_pins(Level::Low, &[&clk_pin, &mosi_pin]);
140 sm.set_pin_dirs(Direction::Out, &[&clk_pin, &mosi_pin]);
141 sm.set_pin_dirs(Direction::In, &[&miso_pin]);
142
143 let mut cfg = crate::pio::Config::default();
144
145 cfg.use_program(&program.prg, &[&clk_pin]);
146 cfg.set_out_pins(&[&mosi_pin]);
147 cfg.set_in_pins(&[&miso_pin]);
148
149 cfg.shift_in.auto_fill = true;
150 cfg.shift_in.direction = ShiftDirection::Left;
151 cfg.shift_in.threshold = 8;
152
153 cfg.shift_out.auto_fill = true;
154 cfg.shift_out.direction = ShiftDirection::Left;
155 cfg.shift_out.threshold = 8;
156
157 let sys_freq = clk_sys_freq().to_fixed::<fixed::FixedU64<U8>>();
158 let target_freq = (config.frequency * 4).to_fixed::<fixed::FixedU64<U8>>();
159 cfg.clock_divider = (sys_freq / target_freq).to_fixed();
160
161 sm.set_config(&cfg);
162 sm.set_enable(true);
163
164 Self {
165 sm,
166 tx_dma,
167 rx_dma,
168 phantom: PhantomData,
169 }
170 }
171
172 fn blocking_read_u8(&mut self) -> Result<u8, Error> {
173 while self.sm.rx().empty() {}
174 let value = self.sm.rx().pull() as u8;
175
176 Ok(value)
177 }
178
179 fn blocking_write_u8(&mut self, v: u8) -> Result<(), Error> {
180 let value = u32::from_be_bytes([v, 0, 0, 0]);
181
182 while !self.sm.tx().try_push(value) {}
183
184 // need to clear here for flush to work correctly
185 self.sm.tx().stalled();
186
187 Ok(())
188 }
189
190 /// Read data from SPI blocking execution until done.
191 pub fn blocking_read(&mut self, data: &mut [u8]) -> Result<(), Error> {
192 for v in data {
193 self.blocking_write_u8(0)?;
194 *v = self.blocking_read_u8()?;
195 }
196 self.flush()?;
197 Ok(())
198 }
199
200 /// Write data to SPI blocking execution until done.
201 pub fn blocking_write(&mut self, data: &[u8]) -> Result<(), Error> {
202 for v in data {
203 self.blocking_write_u8(*v)?;
204 let _ = self.blocking_read_u8()?;
205 }
206 self.flush()?;
207 Ok(())
208 }
209
210 /// Transfer data to SPI blocking execution until done.
211 pub fn blocking_transfer(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), Error> {
212 let len = read.len().max(write.len());
213 for i in 0..len {
214 let wb = write.get(i).copied().unwrap_or(0);
215 self.blocking_write_u8(wb)?;
216
217 let rb = self.blocking_read_u8()?;
218 if let Some(r) = read.get_mut(i) {
219 *r = rb;
220 }
221 }
222 self.flush()?;
223 Ok(())
224 }
225
226 /// Transfer data in place to SPI blocking execution until done.
227 pub fn blocking_transfer_in_place(&mut self, data: &mut [u8]) -> Result<(), Error> {
228 for v in data {
229 self.blocking_write_u8(*v)?;
230 *v = self.blocking_read_u8()?;
231 }
232 self.flush()?;
233 Ok(())
234 }
235
236 /// Block execution until SPI is done.
237 pub fn flush(&mut self) -> Result<(), Error> {
238 // Wait for all words in the FIFO to have been pulled by the SM
239 while !self.sm.tx().empty() {}
240
241 // Wait for last value to be written out to the wire
242 while !self.sm.tx().stalled() {}
243
244 Ok(())
245 }
246}
247
248impl<'d, PIO: Instance, const SM: usize> Spi<'d, PIO, SM, Blocking> {
249 /// Create an SPI driver in blocking mode.
250 pub fn new_blocking(
251 pio: &mut Common<'d, PIO>,
252 sm: StateMachine<'d, PIO, SM>,
253 clk: Peri<'d, impl PioPin>,
254 mosi: Peri<'d, impl PioPin>,
255 miso: Peri<'d, impl PioPin>,
256 program: &PioSpiProgram<'d, PIO>,
257 config: Config,
258 ) -> Self {
259 Self::new_inner(pio, sm, clk, mosi, miso, None, None, program, config)
260 }
261}
262
263impl<'d, PIO: Instance, const SM: usize> Spi<'d, PIO, SM, Async> {
264 /// Create an SPI driver in async mode supporting DMA operations.
265 #[allow(clippy::too_many_arguments)]
266 pub fn new(
267 pio: &mut Common<'d, PIO>,
268 sm: StateMachine<'d, PIO, SM>,
269 clk: Peri<'d, impl PioPin>,
270 mosi: Peri<'d, impl PioPin>,
271 miso: Peri<'d, impl PioPin>,
272 tx_dma: Peri<'d, impl Channel>,
273 rx_dma: Peri<'d, impl Channel>,
274 program: &PioSpiProgram<'d, PIO>,
275 config: Config,
276 ) -> Self {
277 Self::new_inner(
278 pio,
279 sm,
280 clk,
281 mosi,
282 miso,
283 Some(tx_dma.into()),
284 Some(rx_dma.into()),
285 program,
286 config,
287 )
288 }
289
290 /// Read data from SPI using DMA.
291 pub async fn read(&mut self, buffer: &mut [u8]) -> Result<(), Error> {
292 let (rx, tx) = self.sm.rx_tx();
293
294 let len = buffer.len();
295
296 let rx_ch = self.rx_dma.as_mut().unwrap().reborrow();
297 let rx_transfer = rx.dma_pull(rx_ch, buffer, false);
298
299 let tx_ch = self.tx_dma.as_mut().unwrap().reborrow();
300 let tx_transfer = tx.dma_push_repeated::<_, u8>(tx_ch, len);
301
302 join(tx_transfer, rx_transfer).await;
303
304 Ok(())
305 }
306
307 /// Write data to SPI using DMA.
308 pub async fn write(&mut self, buffer: &[u8]) -> Result<(), Error> {
309 let (rx, tx) = self.sm.rx_tx();
310
311 let rx_ch = self.rx_dma.as_mut().unwrap().reborrow();
312 let rx_transfer = rx.dma_pull_repeated::<_, u8>(rx_ch, buffer.len());
313
314 let tx_ch = self.tx_dma.as_mut().unwrap().reborrow();
315 let tx_transfer = tx.dma_push(tx_ch, buffer, false);
316
317 join(tx_transfer, rx_transfer).await;
318
319 Ok(())
320 }
321
322 /// Transfer data to SPI using DMA.
323 pub async fn transfer(&mut self, rx_buffer: &mut [u8], tx_buffer: &[u8]) -> Result<(), Error> {
324 self.transfer_inner(rx_buffer, tx_buffer).await
325 }
326
327 /// Transfer data in place to SPI using DMA.
328 pub async fn transfer_in_place(&mut self, words: &mut [u8]) -> Result<(), Error> {
329 self.transfer_inner(words, words).await
330 }
331
332 async fn transfer_inner(&mut self, rx_buffer: *mut [u8], tx_buffer: *const [u8]) -> Result<(), Error> {
333 let (rx, tx) = self.sm.rx_tx();
334
335 let mut rx_ch = self.rx_dma.as_mut().unwrap().reborrow();
336 let rx_transfer = async {
337 rx.dma_pull(rx_ch.reborrow(), unsafe { &mut *rx_buffer }, false).await;
338
339 if tx_buffer.len() > rx_buffer.len() {
340 let read_bytes_len = tx_buffer.len() - rx_buffer.len();
341
342 rx.dma_pull_repeated::<_, u8>(rx_ch, read_bytes_len).await;
343 }
344 };
345
346 let mut tx_ch = self.tx_dma.as_mut().unwrap().reborrow();
347 let tx_transfer = async {
348 tx.dma_push(tx_ch.reborrow(), unsafe { &*tx_buffer }, false).await;
349
350 if rx_buffer.len() > tx_buffer.len() {
351 let write_bytes_len = rx_buffer.len() - tx_buffer.len();
352
353 tx.dma_push_repeated::<_, u8>(tx_ch, write_bytes_len).await;
354 }
355 };
356
357 join(tx_transfer, rx_transfer).await;
358
359 Ok(())
360 }
361}
362
363// ====================
364
365impl<'d, PIO: Instance, const SM: usize, M: Mode> embedded_hal_02::blocking::spi::Transfer<u8> for Spi<'d, PIO, SM, M> {
366 type Error = Error;
367 fn transfer<'w>(&mut self, words: &'w mut [u8]) -> Result<&'w [u8], Self::Error> {
368 self.blocking_transfer_in_place(words)?;
369 Ok(words)
370 }
371}
372
373impl<'d, PIO: Instance, const SM: usize, M: Mode> embedded_hal_02::blocking::spi::Write<u8> for Spi<'d, PIO, SM, M> {
374 type Error = Error;
375
376 fn write(&mut self, words: &[u8]) -> Result<(), Self::Error> {
377 self.blocking_write(words)
378 }
379}
380
381impl embedded_hal_1::spi::Error for Error {
382 fn kind(&self) -> embedded_hal_1::spi::ErrorKind {
383 match *self {}
384 }
385}
386
387impl<'d, PIO: Instance, const SM: usize, M: Mode> embedded_hal_1::spi::ErrorType for Spi<'d, PIO, SM, M> {
388 type Error = Error;
389}
390
391impl<'d, PIO: Instance, const SM: usize, M: Mode> embedded_hal_1::spi::SpiBus<u8> for Spi<'d, PIO, SM, M> {
392 fn flush(&mut self) -> Result<(), Self::Error> {
393 Ok(())
394 }
395
396 fn read(&mut self, words: &mut [u8]) -> Result<(), Self::Error> {
397 self.blocking_transfer(words, &[])
398 }
399
400 fn write(&mut self, words: &[u8]) -> Result<(), Self::Error> {
401 self.blocking_write(words)
402 }
403
404 fn transfer(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), Self::Error> {
405 self.blocking_transfer(read, write)
406 }
407
408 fn transfer_in_place(&mut self, words: &mut [u8]) -> Result<(), Self::Error> {
409 self.blocking_transfer_in_place(words)
410 }
411}
412
413impl<'d, PIO: Instance, const SM: usize> embedded_hal_async::spi::SpiBus<u8> for Spi<'d, PIO, SM, Async> {
414 async fn flush(&mut self) -> Result<(), Self::Error> {
415 Ok(())
416 }
417
418 async fn write(&mut self, words: &[u8]) -> Result<(), Self::Error> {
419 self.write(words).await
420 }
421
422 async fn read(&mut self, words: &mut [u8]) -> Result<(), Self::Error> {
423 self.read(words).await
424 }
425
426 async fn transfer(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), Self::Error> {
427 self.transfer(read, write).await
428 }
429
430 async fn transfer_in_place(&mut self, words: &mut [u8]) -> Result<(), Self::Error> {
431 self.transfer_in_place(words).await
432 }
433}