aboutsummaryrefslogtreecommitdiff
path: root/embassy-rp
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 /embassy-rp
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.
Diffstat (limited to 'embassy-rp')
-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}