From 676f9da58360627699736c79a4e24ef20a2b9f87 Mon Sep 17 00:00:00 2001 From: Adrian Wowk Date: Wed, 2 Jul 2025 20:31:45 -0500 Subject: [PATCH 1/8] rp: add new pio dma apis This commit adds StateMachineRx::dma_pull_repeated and StateMachineTx::dma_push_repeated which allow you to discard reads or send dummy writes to the state machine using the DMA hardware --- embassy-rp/src/pio/mod.rs | 67 ++++++++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/embassy-rp/src/pio/mod.rs b/embassy-rp/src/pio/mod.rs index 0d8a94776..f46e664e4 100644 --- a/embassy-rp/src/pio/mod.rs +++ b/embassy-rp/src/pio/mod.rs @@ -12,7 +12,7 @@ use fixed::types::extra::U8; use fixed::FixedU32; use pio::{Program, SideSet, Wrap}; -use crate::dma::{Channel, Transfer, Word}; +use crate::dma::{self, Channel, Transfer, Word}; use crate::gpio::{self, AnyPin, Drive, Level, Pull, SealedPin, SlewRate}; use crate::interrupt::typelevel::{Binding, Handler, Interrupt}; use crate::relocate::RelocatedProgram; @@ -281,6 +281,18 @@ impl<'l, PIO: Instance> Pin<'l, PIO> { }); } + /// Configure the output logic inversion of this pin. + #[inline] + pub fn set_output_inversion(&mut self, invert: bool) { + self.pin.gpio().ctrl().modify(|w| { + w.set_outover(if invert { + pac::io::vals::Outover::INVERT + } else { + pac::io::vals::Outover::NORMAL + }) + }); + } + /// Set the pin's input sync bypass. pub fn set_input_sync_bypass(&mut self, bypass: bool) { let mask = 1 << self.pin(); @@ -360,6 +372,10 @@ impl<'d, PIO: Instance, const SM: usize> StateMachineRx<'d, PIO, SM> { FifoInFuture::new(self) } + fn dreq() -> crate::pac::dma::vals::TreqSel { + crate::pac::dma::vals::TreqSel::from(PIO::PIO_NO * 8 + SM as u8 + 4) + } + /// Prepare DMA transfer from RX FIFO. pub fn dma_pull<'a, C: Channel, W: Word>( &'a mut self, @@ -367,7 +383,6 @@ impl<'d, PIO: Instance, const SM: usize> StateMachineRx<'d, PIO, SM> { data: &'a mut [W], bswap: bool, ) -> Transfer<'a, C> { - let pio_no = PIO::PIO_NO; let p = ch.regs(); p.write_addr().write_value(data.as_ptr() as u32); p.read_addr().write_value(PIO::PIO.rxf(SM).as_ptr() as u32); @@ -377,8 +392,7 @@ impl<'d, PIO: Instance, const SM: usize> StateMachineRx<'d, PIO, SM> { p.trans_count().write(|w| w.set_count(data.len() as u32)); compiler_fence(Ordering::SeqCst); p.ctrl_trig().write(|w| { - // Set RX DREQ for this statemachine - w.set_treq_sel(crate::pac::dma::vals::TreqSel::from(pio_no * 8 + SM as u8 + 4)); + w.set_treq_sel(Self::dreq()); w.set_data_size(W::size()); w.set_chain_to(ch.number()); w.set_incr_read(false); @@ -389,6 +403,36 @@ impl<'d, PIO: Instance, const SM: usize> StateMachineRx<'d, PIO, SM> { compiler_fence(Ordering::SeqCst); Transfer::new(ch) } + + /// Prepare a repeated DMA transfer from RX FIFO. + pub fn dma_pull_repeated<'a, C: Channel, W: Word>(&'a mut self, ch: Peri<'a, C>, len: usize) -> Transfer<'a, C> { + // This is the read version of dma::write_repeated. This allows us to + // discard reads from the RX FIFO through DMA. + + // static mut so it gets allocated in RAM + static mut DUMMY: u32 = 0; + + let p = ch.regs(); + p.write_addr().write_value(core::ptr::addr_of_mut!(DUMMY) as u32); + p.read_addr().write_value(PIO::PIO.rxf(SM).as_ptr() as u32); + + #[cfg(feature = "rp2040")] + p.trans_count().write(|w| *w = len as u32); + #[cfg(feature = "_rp235x")] + p.trans_count().write(|w| w.set_count(len as u32)); + + compiler_fence(Ordering::SeqCst); + p.ctrl_trig().write(|w| { + w.set_treq_sel(Self::dreq()); + w.set_data_size(W::size()); + w.set_chain_to(ch.number()); + w.set_incr_read(false); + w.set_incr_write(false); + w.set_en(true); + }); + compiler_fence(Ordering::SeqCst); + Transfer::new(ch) + } } /// Type representing a state machine TX FIFO. @@ -412,7 +456,7 @@ impl<'d, PIO: Instance, const SM: usize> StateMachineTx<'d, PIO, SM> { (PIO::PIO.flevel().read().0 >> (SM * 8)) as u8 & 0x0f } - /// Check state machine has stalled on empty TX FIFO. + /// Check if state machine has stalled on empty TX FIFO. pub fn stalled(&self) -> bool { let fdebug = PIO::PIO.fdebug(); let ret = fdebug.read().txstall() & (1 << SM) != 0; @@ -451,6 +495,10 @@ impl<'d, PIO: Instance, const SM: usize> StateMachineTx<'d, PIO, SM> { FifoOutFuture::new(self, value) } + fn dreq() -> crate::pac::dma::vals::TreqSel { + crate::pac::dma::vals::TreqSel::from(PIO::PIO_NO * 8 + SM as u8) + } + /// Prepare a DMA transfer to TX FIFO. pub fn dma_push<'a, C: Channel, W: Word>( &'a mut self, @@ -458,7 +506,6 @@ impl<'d, PIO: Instance, const SM: usize> StateMachineTx<'d, PIO, SM> { data: &'a [W], bswap: bool, ) -> Transfer<'a, C> { - let pio_no = PIO::PIO_NO; let p = ch.regs(); p.read_addr().write_value(data.as_ptr() as u32); p.write_addr().write_value(PIO::PIO.txf(SM).as_ptr() as u32); @@ -468,8 +515,7 @@ impl<'d, PIO: Instance, const SM: usize> StateMachineTx<'d, PIO, SM> { p.trans_count().write(|w| w.set_count(data.len() as u32)); compiler_fence(Ordering::SeqCst); p.ctrl_trig().write(|w| { - // Set TX DREQ for this statemachine - w.set_treq_sel(crate::pac::dma::vals::TreqSel::from(pio_no * 8 + SM as u8)); + w.set_treq_sel(Self::dreq()); w.set_data_size(W::size()); w.set_chain_to(ch.number()); w.set_incr_read(true); @@ -480,6 +526,11 @@ impl<'d, PIO: Instance, const SM: usize> StateMachineTx<'d, PIO, SM> { compiler_fence(Ordering::SeqCst); Transfer::new(ch) } + + /// Prepare a repeated DMA transfer to TX FIFO. + pub fn dma_push_repeated<'a, C: Channel, W: Word>(&'a mut self, ch: Peri<'a, C>, len: usize) -> Transfer<'a, C> { + unsafe { dma::write_repeated(ch, PIO::PIO.txf(SM).as_ptr(), len, Self::dreq()) } + } } /// A type representing a single PIO state machine. From 4cac3ac1d24d6b651d79bfca8401824c28f5102c Mon Sep 17 00:00:00 2001 From: Adrian Wowk Date: Wed, 2 Jul 2025 20:35:18 -0500 Subject: [PATCH 2/8] 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. --- embassy-rp/src/pio_programs/mod.rs | 1 + embassy-rp/src/pio_programs/spi.rs | 433 +++++++++++++++++++++++++++++ 2 files changed, 434 insertions(+) create mode 100644 embassy-rp/src/pio_programs/spi.rs 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; pub mod onewire; pub mod pwm; pub mod rotary_encoder; +pub mod spi; pub mod stepper; pub mod uart; pub 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 @@ +//! PIO backed SPi drivers + +use core::marker::PhantomData; + +use embassy_futures::join::join; +use embassy_hal_internal::Peri; +use embedded_hal_02::spi::{Phase, Polarity}; +use fixed::{traits::ToFixed, types::extra::U8}; + +use crate::{ + clocks::clk_sys_freq, + dma::{AnyChannel, Channel}, + gpio::Level, + pio::{Common, Direction, Instance, LoadedProgram, PioPin, ShiftDirection, StateMachine}, + spi::{Async, Blocking, Mode}, +}; + +/// This struct represents a uart tx program loaded into pio instruction memory. +pub struct PioSpiProgram<'d, PIO: crate::pio::Instance> { + prg: LoadedProgram<'d, PIO>, +} + +impl<'d, PIO: crate::pio::Instance> PioSpiProgram<'d, PIO> { + /// Load the spi program into the given pio + pub fn new(common: &mut crate::pio::Common<'d, PIO>, phase: Phase) -> Self { + // These PIO programs are taken straight from the datasheet (3.6.1 in + // RP2040 datasheet, 11.6.1 in RP2350 datasheet) + + // Pin assignments: + // - SCK is side-set pin 0 + // - MOSI is OUT pin 0 + // - MISO is IN pin 0 + // + // Autopush and autopull must be enabled, and the serial frame size is set by + // configuring the push/pull threshold. Shift left/right is fine, but you must + // justify the data yourself. This is done most conveniently for frame sizes of + // 8 or 16 bits by using the narrow store replication and narrow load byte + // picking behaviour of RP2040's IO fabric. + + let prg = match phase { + Phase::CaptureOnFirstTransition => { + let prg = pio::pio_asm!( + r#" + .side_set 1 + + ; Clock phase = 0: data is captured on the leading edge of each SCK pulse, and + ; transitions on the trailing edge, or some time before the first leading edge. + + out pins, 1 side 0 [1] ; Stall here on empty (sideset proceeds even if + in pins, 1 side 1 [1] ; instruction stalls, so we stall with SCK low) + "# + ); + + common.load_program(&prg.program) + } + Phase::CaptureOnSecondTransition => { + let prg = pio::pio_asm!( + r#" + .side_set 1 + + ; Clock phase = 1: data transitions on the leading edge of each SCK pulse, and + ; is captured on the trailing edge. + + out x, 1 side 0 ; Stall here on empty (keep SCK deasserted) + mov pins, x side 1 [1] ; Output data, assert SCK (mov pins uses OUT mapping) + in pins, 1 side 0 ; Input data, deassert SCK + "# + ); + + common.load_program(&prg.program) + } + }; + + Self { prg } + } +} + +/// PIO SPI errors. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum Error { + // No errors for now +} + +/// PIO based Spi driver. +/// +/// This driver is less flexible than the hardware backed one. Configuration can +/// not be changed at runtime. +pub struct Spi<'d, PIO: Instance, const SM: usize, M: Mode> { + sm: StateMachine<'d, PIO, SM>, + tx_dma: Option>, + rx_dma: Option>, + phantom: PhantomData, +} + +/// PIO SPI configuration. +#[non_exhaustive] +#[derive(Clone)] +pub struct Config { + /// Frequency (Hz). + pub frequency: u32, + /// Polarity. + pub polarity: Polarity, +} + +impl Default for Config { + fn default() -> Self { + Self { + frequency: 1_000_000, + polarity: Polarity::IdleLow, + } + } +} + +impl<'d, PIO: Instance, const SM: usize, M: Mode> Spi<'d, PIO, SM, M> { + #[allow(clippy::too_many_arguments)] + fn new_inner( + pio: &mut Common<'d, PIO>, + mut sm: StateMachine<'d, PIO, SM>, + clk_pin: Peri<'d, impl PioPin>, + mosi_pin: Peri<'d, impl PioPin>, + miso_pin: Peri<'d, impl PioPin>, + tx_dma: Option>, + rx_dma: Option>, + program: &PioSpiProgram<'d, PIO>, + config: Config, + ) -> Self { + let mut clk_pin = pio.make_pio_pin(clk_pin); + let mosi_pin = pio.make_pio_pin(mosi_pin); + let miso_pin = pio.make_pio_pin(miso_pin); + + if let Polarity::IdleHigh = config.polarity { + clk_pin.set_output_inversion(true); + } else { + clk_pin.set_output_inversion(false); + } + + sm.set_pins(Level::Low, &[&clk_pin, &mosi_pin]); + sm.set_pin_dirs(Direction::Out, &[&clk_pin, &mosi_pin]); + sm.set_pin_dirs(Direction::In, &[&miso_pin]); + + let mut cfg = crate::pio::Config::default(); + + cfg.use_program(&program.prg, &[&clk_pin]); + cfg.set_out_pins(&[&mosi_pin]); + cfg.set_in_pins(&[&miso_pin]); + + cfg.shift_in.auto_fill = true; + cfg.shift_in.direction = ShiftDirection::Left; + cfg.shift_in.threshold = 8; + + cfg.shift_out.auto_fill = true; + cfg.shift_out.direction = ShiftDirection::Left; + cfg.shift_out.threshold = 8; + + let sys_freq = clk_sys_freq().to_fixed::>(); + let target_freq = (config.frequency * 4).to_fixed::>(); + cfg.clock_divider = (sys_freq / target_freq).to_fixed(); + + sm.set_config(&cfg); + sm.set_enable(true); + + Self { + sm, + tx_dma, + rx_dma, + phantom: PhantomData, + } + } + + fn blocking_read_u8(&mut self) -> Result { + while self.sm.rx().empty() {} + let value = self.sm.rx().pull() as u8; + + Ok(value) + } + + fn blocking_write_u8(&mut self, v: u8) -> Result<(), Error> { + let value = u32::from_be_bytes([v, 0, 0, 0]); + + while !self.sm.tx().try_push(value) {} + + // need to clear here for flush to work correctly + self.sm.tx().stalled(); + + Ok(()) + } + + /// Read data from SPI blocking execution until done. + pub fn blocking_read(&mut self, data: &mut [u8]) -> Result<(), Error> { + for v in data { + self.blocking_write_u8(0)?; + *v = self.blocking_read_u8()?; + } + self.flush()?; + Ok(()) + } + + /// Write data to SPI blocking execution until done. + pub fn blocking_write(&mut self, data: &[u8]) -> Result<(), Error> { + for v in data { + self.blocking_write_u8(*v)?; + let _ = self.blocking_read_u8()?; + } + self.flush()?; + Ok(()) + } + + /// Transfer data to SPI blocking execution until done. + pub fn blocking_transfer(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), Error> { + let len = read.len().max(write.len()); + for i in 0..len { + let wb = write.get(i).copied().unwrap_or(0); + self.blocking_write_u8(wb)?; + + let rb = self.blocking_read_u8()?; + if let Some(r) = read.get_mut(i) { + *r = rb; + } + } + self.flush()?; + Ok(()) + } + + /// Transfer data in place to SPI blocking execution until done. + pub fn blocking_transfer_in_place(&mut self, data: &mut [u8]) -> Result<(), Error> { + for v in data { + self.blocking_write_u8(*v)?; + *v = self.blocking_read_u8()?; + } + self.flush()?; + Ok(()) + } + + /// Block execution until SPI is done. + pub fn flush(&mut self) -> Result<(), Error> { + // Wait for all words in the FIFO to have been pulled by the SM + while !self.sm.tx().empty() {} + + // Wait for last value to be written out to the wire + while !self.sm.tx().stalled() {} + + Ok(()) + } +} + +impl<'d, PIO: Instance, const SM: usize> Spi<'d, PIO, SM, Blocking> { + /// Create an SPI driver in blocking mode. + pub fn new_blocking( + pio: &mut Common<'d, PIO>, + sm: StateMachine<'d, PIO, SM>, + clk: Peri<'d, impl PioPin>, + mosi: Peri<'d, impl PioPin>, + miso: Peri<'d, impl PioPin>, + program: &PioSpiProgram<'d, PIO>, + config: Config, + ) -> Self { + Self::new_inner(pio, sm, clk, mosi, miso, None, None, program, config) + } +} + +impl<'d, PIO: Instance, const SM: usize> Spi<'d, PIO, SM, Async> { + /// Create an SPI driver in async mode supporting DMA operations. + #[allow(clippy::too_many_arguments)] + pub fn new( + pio: &mut Common<'d, PIO>, + sm: StateMachine<'d, PIO, SM>, + clk: Peri<'d, impl PioPin>, + mosi: Peri<'d, impl PioPin>, + miso: Peri<'d, impl PioPin>, + tx_dma: Peri<'d, impl Channel>, + rx_dma: Peri<'d, impl Channel>, + program: &PioSpiProgram<'d, PIO>, + config: Config, + ) -> Self { + Self::new_inner( + pio, + sm, + clk, + mosi, + miso, + Some(tx_dma.into()), + Some(rx_dma.into()), + program, + config, + ) + } + + /// Read data from SPI using DMA. + pub async fn read(&mut self, buffer: &mut [u8]) -> Result<(), Error> { + let (rx, tx) = self.sm.rx_tx(); + + let len = buffer.len(); + + let rx_ch = self.rx_dma.as_mut().unwrap().reborrow(); + let rx_transfer = rx.dma_pull(rx_ch, buffer, false); + + let tx_ch = self.tx_dma.as_mut().unwrap().reborrow(); + let tx_transfer = tx.dma_push_repeated::<_, u8>(tx_ch, len); + + join(tx_transfer, rx_transfer).await; + + Ok(()) + } + + /// Write data to SPI using DMA. + pub async fn write(&mut self, buffer: &[u8]) -> Result<(), Error> { + let (rx, tx) = self.sm.rx_tx(); + + let rx_ch = self.rx_dma.as_mut().unwrap().reborrow(); + let rx_transfer = rx.dma_pull_repeated::<_, u8>(rx_ch, buffer.len()); + + let tx_ch = self.tx_dma.as_mut().unwrap().reborrow(); + let tx_transfer = tx.dma_push(tx_ch, buffer, false); + + join(tx_transfer, rx_transfer).await; + + Ok(()) + } + + /// Transfer data to SPI using DMA. + pub async fn transfer(&mut self, rx_buffer: &mut [u8], tx_buffer: &[u8]) -> Result<(), Error> { + self.transfer_inner(rx_buffer, tx_buffer).await + } + + /// Transfer data in place to SPI using DMA. + pub async fn transfer_in_place(&mut self, words: &mut [u8]) -> Result<(), Error> { + self.transfer_inner(words, words).await + } + + async fn transfer_inner(&mut self, rx_buffer: *mut [u8], tx_buffer: *const [u8]) -> Result<(), Error> { + let (rx, tx) = self.sm.rx_tx(); + + let mut rx_ch = self.rx_dma.as_mut().unwrap().reborrow(); + let rx_transfer = async { + rx.dma_pull(rx_ch.reborrow(), unsafe { &mut *rx_buffer }, false).await; + + if tx_buffer.len() > rx_buffer.len() { + let read_bytes_len = tx_buffer.len() - rx_buffer.len(); + + rx.dma_pull_repeated::<_, u8>(rx_ch, read_bytes_len).await; + } + }; + + let mut tx_ch = self.tx_dma.as_mut().unwrap().reborrow(); + let tx_transfer = async { + tx.dma_push(tx_ch.reborrow(), unsafe { &*tx_buffer }, false).await; + + if rx_buffer.len() > tx_buffer.len() { + let write_bytes_len = rx_buffer.len() - tx_buffer.len(); + + tx.dma_push_repeated::<_, u8>(tx_ch, write_bytes_len).await; + } + }; + + join(tx_transfer, rx_transfer).await; + + Ok(()) + } +} + +// ==================== + +impl<'d, PIO: Instance, const SM: usize, M: Mode> embedded_hal_02::blocking::spi::Transfer for Spi<'d, PIO, SM, M> { + type Error = Error; + fn transfer<'w>(&mut self, words: &'w mut [u8]) -> Result<&'w [u8], Self::Error> { + self.blocking_transfer_in_place(words)?; + Ok(words) + } +} + +impl<'d, PIO: Instance, const SM: usize, M: Mode> embedded_hal_02::blocking::spi::Write for Spi<'d, PIO, SM, M> { + type Error = Error; + + fn write(&mut self, words: &[u8]) -> Result<(), Self::Error> { + self.blocking_write(words) + } +} + +impl embedded_hal_1::spi::Error for Error { + fn kind(&self) -> embedded_hal_1::spi::ErrorKind { + match *self {} + } +} + +impl<'d, PIO: Instance, const SM: usize, M: Mode> embedded_hal_1::spi::ErrorType for Spi<'d, PIO, SM, M> { + type Error = Error; +} + +impl<'d, PIO: Instance, const SM: usize, M: Mode> embedded_hal_1::spi::SpiBus for Spi<'d, PIO, SM, M> { + fn flush(&mut self) -> Result<(), Self::Error> { + Ok(()) + } + + fn read(&mut self, words: &mut [u8]) -> Result<(), Self::Error> { + self.blocking_transfer(words, &[]) + } + + fn write(&mut self, words: &[u8]) -> Result<(), Self::Error> { + self.blocking_write(words) + } + + fn transfer(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), Self::Error> { + self.blocking_transfer(read, write) + } + + fn transfer_in_place(&mut self, words: &mut [u8]) -> Result<(), Self::Error> { + self.blocking_transfer_in_place(words) + } +} + +impl<'d, PIO: Instance, const SM: usize> embedded_hal_async::spi::SpiBus for Spi<'d, PIO, SM, Async> { + async fn flush(&mut self) -> Result<(), Self::Error> { + Ok(()) + } + + async fn write(&mut self, words: &[u8]) -> Result<(), Self::Error> { + self.write(words).await + } + + async fn read(&mut self, words: &mut [u8]) -> Result<(), Self::Error> { + self.read(words).await + } + + async fn transfer(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), Self::Error> { + self.transfer(read, write).await + } + + async fn transfer_in_place(&mut self, words: &mut [u8]) -> Result<(), Self::Error> { + self.transfer_in_place(words).await + } +} From 236662c748eab43a701bf4f43570b191ec2c1158 Mon Sep 17 00:00:00 2001 From: Adrian Wowk Date: Wed, 2 Jul 2025 20:53:26 -0500 Subject: [PATCH 3/8] rp: add pio spi examples --- examples/rp/src/bin/pio_spi.rs | 63 +++++++++++++++++++++++++++ examples/rp/src/bin/pio_spi_async.rs | 65 ++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 examples/rp/src/bin/pio_spi.rs create mode 100644 examples/rp/src/bin/pio_spi_async.rs diff --git a/examples/rp/src/bin/pio_spi.rs b/examples/rp/src/bin/pio_spi.rs new file mode 100644 index 000000000..1fbe235e5 --- /dev/null +++ b/examples/rp/src/bin/pio_spi.rs @@ -0,0 +1,63 @@ +//! This example shows how to use a PIO state machine as an additional SPI +//! (Serial Peripheral Interface) on the RP2040 chip. No specific hardware is +//! specified in this example. +//! +//! If you connect pin 6 and 7 you should get the same data back. + +#![no_std] +#![no_main] + +use defmt::*; +use embassy_executor::Spawner; +use embassy_rp::{ + bind_interrupts, + peripherals::PIO0, + pio, + pio_programs::spi::{Config, PioSpiProgram, Spi}, + spi::Phase, +}; +use embassy_time::Timer; +use {defmt_rtt as _, panic_probe as _}; + +bind_interrupts!(struct Irqs { + PIO0_IRQ_0 => pio::InterruptHandler; +}); + +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + let p = embassy_rp::init(Default::default()); + info!("Hello World!"); + + // These pins are routed to differnet hardware SPI peripherals, but we can + // use them together regardless + let mosi = p.PIN_6; // SPI0 SCLK + let miso = p.PIN_7; // SPI0 MOSI + let clk = p.PIN_8; // SPI1 MISO + + let pio::Pio { mut common, sm0, .. } = pio::Pio::new(p.PIO0, Irqs); + + // The PIO program must be configured with the clock phase + let program = PioSpiProgram::new(&mut common, Phase::CaptureOnFirstTransition); + + // Construct an SPI driver backed by a PIO state machine + let mut spi = Spi::new_blocking( + &mut common, + sm0, + clk, + mosi, + miso, + &program, + // Only the frequency and polarity are set here + Config::default(), + ); + + loop { + let tx_buf = [1_u8, 2, 3, 4, 5, 6]; + let mut rx_buf = [0_u8; 6]; + + spi.blocking_transfer(&mut rx_buf, &tx_buf).unwrap(); + info!("{:?}", rx_buf); + + Timer::after_secs(1).await; + } +} diff --git a/examples/rp/src/bin/pio_spi_async.rs b/examples/rp/src/bin/pio_spi_async.rs new file mode 100644 index 000000000..5abf6fc71 --- /dev/null +++ b/examples/rp/src/bin/pio_spi_async.rs @@ -0,0 +1,65 @@ +//! This example shows how to use a PIO state machine as an additional SPI +//! (Serial Peripheral Interface) on the RP2040 chip. No specific hardware is +//! specified in this example. +//! +//! If you connect pin 6 and 7 you should get the same data back. + +#![no_std] +#![no_main] + +use defmt::*; +use embassy_executor::Spawner; +use embassy_rp::{ + bind_interrupts, + peripherals::PIO0, + pio, + pio_programs::spi::{Config, PioSpiProgram, Spi}, + spi::Phase, +}; +use embassy_time::Timer; +use {defmt_rtt as _, panic_probe as _}; + +bind_interrupts!(struct Irqs { + PIO0_IRQ_0 => pio::InterruptHandler; +}); + +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + let p = embassy_rp::init(Default::default()); + info!("Hello World!"); + + // These pins are routed to differnet hardware SPI peripherals, but we can + // use them together regardless + let mosi = p.PIN_6; // SPI0 SCLK + let miso = p.PIN_7; // SPI0 MOSI + let clk = p.PIN_8; // SPI1 MISO + + let pio::Pio { mut common, sm0, .. } = pio::Pio::new(p.PIO0, Irqs); + + // The PIO program must be configured with the clock phase + let program = PioSpiProgram::new(&mut common, Phase::CaptureOnFirstTransition); + + // Construct an SPI driver backed by a PIO state machine + let mut spi = Spi::new( + &mut common, + sm0, + clk, + mosi, + miso, + p.DMA_CH0, + p.DMA_CH1, + &program, + // Only the frequency and polarity are set here + Config::default(), + ); + + loop { + let tx_buf = [1_u8, 2, 3, 4, 5, 6]; + let mut rx_buf = [0_u8; 6]; + + spi.transfer(&mut rx_buf, &tx_buf).await.unwrap(); + info!("{:?}", rx_buf); + + Timer::after_secs(1).await; + } +} From 83b42e0db620b7fc2364763b452b346b711e8d9f Mon Sep 17 00:00:00 2001 From: Adrian Wowk Date: Wed, 2 Jul 2025 21:18:44 -0500 Subject: [PATCH 4/8] style: cleanup with rustfmt --- embassy-rp/src/pio_programs/spi.rs | 21 ++++++++++----------- examples/rp/src/bin/pio_spi.rs | 11 ++++------- examples/rp/src/bin/pio_spi_async.rs | 13 +++++-------- 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/embassy-rp/src/pio_programs/spi.rs b/embassy-rp/src/pio_programs/spi.rs index 27d401fc9..a1b36c1c8 100644 --- a/embassy-rp/src/pio_programs/spi.rs +++ b/embassy-rp/src/pio_programs/spi.rs @@ -5,24 +5,23 @@ use core::marker::PhantomData; use embassy_futures::join::join; use embassy_hal_internal::Peri; use embedded_hal_02::spi::{Phase, Polarity}; -use fixed::{traits::ToFixed, types::extra::U8}; +use fixed::traits::ToFixed; +use fixed::types::extra::U8; -use crate::{ - clocks::clk_sys_freq, - dma::{AnyChannel, Channel}, - gpio::Level, - pio::{Common, Direction, Instance, LoadedProgram, PioPin, ShiftDirection, StateMachine}, - spi::{Async, Blocking, Mode}, -}; +use crate::clocks::clk_sys_freq; +use crate::dma::{AnyChannel, Channel}; +use crate::gpio::Level; +use crate::pio::{Common, Direction, Instance, LoadedProgram, PioPin, ShiftDirection, StateMachine}; +use crate::spi::{Async, Blocking, Mode}; /// This struct represents a uart tx program loaded into pio instruction memory. -pub struct PioSpiProgram<'d, PIO: crate::pio::Instance> { +pub struct PioSpiProgram<'d, PIO: Instance> { prg: LoadedProgram<'d, PIO>, } -impl<'d, PIO: crate::pio::Instance> PioSpiProgram<'d, PIO> { +impl<'d, PIO: Instance> PioSpiProgram<'d, PIO> { /// Load the spi program into the given pio - pub fn new(common: &mut crate::pio::Common<'d, PIO>, phase: Phase) -> Self { + pub fn new(common: &mut Common<'d, PIO>, phase: Phase) -> Self { // These PIO programs are taken straight from the datasheet (3.6.1 in // RP2040 datasheet, 11.6.1 in RP2350 datasheet) diff --git a/examples/rp/src/bin/pio_spi.rs b/examples/rp/src/bin/pio_spi.rs index 1fbe235e5..0164e4c81 100644 --- a/examples/rp/src/bin/pio_spi.rs +++ b/examples/rp/src/bin/pio_spi.rs @@ -9,13 +9,10 @@ use defmt::*; use embassy_executor::Spawner; -use embassy_rp::{ - bind_interrupts, - peripherals::PIO0, - pio, - pio_programs::spi::{Config, PioSpiProgram, Spi}, - spi::Phase, -}; +use embassy_rp::peripherals::PIO0; +use embassy_rp::pio_programs::spi::{Config, PioSpiProgram, Spi}; +use embassy_rp::spi::Phase; +use embassy_rp::{bind_interrupts, pio}; use embassy_time::Timer; use {defmt_rtt as _, panic_probe as _}; diff --git a/examples/rp/src/bin/pio_spi_async.rs b/examples/rp/src/bin/pio_spi_async.rs index 5abf6fc71..1dbdff609 100644 --- a/examples/rp/src/bin/pio_spi_async.rs +++ b/examples/rp/src/bin/pio_spi_async.rs @@ -9,13 +9,10 @@ use defmt::*; use embassy_executor::Spawner; -use embassy_rp::{ - bind_interrupts, - peripherals::PIO0, - pio, - pio_programs::spi::{Config, PioSpiProgram, Spi}, - spi::Phase, -}; +use embassy_rp::peripherals::PIO0; +use embassy_rp::pio_programs::spi::{Config, PioSpiProgram, Spi}; +use embassy_rp::spi::Phase; +use embassy_rp::{bind_interrupts, pio}; use embassy_time::Timer; use {defmt_rtt as _, panic_probe as _}; @@ -32,7 +29,7 @@ async fn main(_spawner: Spawner) { // use them together regardless let mosi = p.PIN_6; // SPI0 SCLK let miso = p.PIN_7; // SPI0 MOSI - let clk = p.PIN_8; // SPI1 MISO + let clk = p.PIN_8; // SPI1 MISO let pio::Pio { mut common, sm0, .. } = pio::Pio::new(p.PIO0, Irqs); From 62ff0194f4b7413b17dbc69813ec205638248aa7 Mon Sep 17 00:00:00 2001 From: Adrian Wowk Date: Fri, 18 Jul 2025 19:19:27 -0500 Subject: [PATCH 5/8] rp: add pio spi runtime reconfiguration --- embassy-rp/src/pio_programs/spi.rs | 119 ++++++++++++++++++--------- examples/rp/src/bin/pio_spi.rs | 20 +---- examples/rp/src/bin/pio_spi_async.rs | 10 +-- 3 files changed, 87 insertions(+), 62 deletions(-) diff --git a/embassy-rp/src/pio_programs/spi.rs b/embassy-rp/src/pio_programs/spi.rs index a1b36c1c8..6b97cd0f3 100644 --- a/embassy-rp/src/pio_programs/spi.rs +++ b/embassy-rp/src/pio_programs/spi.rs @@ -11,12 +11,13 @@ use fixed::types::extra::U8; use crate::clocks::clk_sys_freq; use crate::dma::{AnyChannel, Channel}; use crate::gpio::Level; -use crate::pio::{Common, Direction, Instance, LoadedProgram, PioPin, ShiftDirection, StateMachine}; -use crate::spi::{Async, Blocking, Mode}; +use crate::pio::{Common, Direction, Instance, LoadedProgram, Pin, PioPin, ShiftDirection, StateMachine}; +use crate::spi::{Async, Blocking, Config, Mode}; -/// This struct represents a uart tx program loaded into pio instruction memory. -pub struct PioSpiProgram<'d, PIO: Instance> { +/// This struct represents an SPI program loaded into pio instruction memory. +struct PioSpiProgram<'d, PIO: Instance> { prg: LoadedProgram<'d, PIO>, + phase: Phase, } impl<'d, PIO: Instance> PioSpiProgram<'d, PIO> { @@ -30,11 +31,11 @@ impl<'d, PIO: Instance> PioSpiProgram<'d, PIO> { // - MOSI is OUT pin 0 // - MISO is IN pin 0 // - // Autopush and autopull must be enabled, and the serial frame size is set by + // Auto-push and auto-pull must be enabled, and the serial frame size is set by // configuring the push/pull threshold. Shift left/right is fine, but you must // justify the data yourself. This is done most conveniently for frame sizes of // 8 or 16 bits by using the narrow store replication and narrow load byte - // picking behaviour of RP2040's IO fabric. + // picking behavior of RP2040's IO fabric. let prg = match phase { Phase::CaptureOnFirstTransition => { @@ -60,9 +61,9 @@ impl<'d, PIO: Instance> PioSpiProgram<'d, PIO> { ; Clock phase = 1: data transitions on the leading edge of each SCK pulse, and ; is captured on the trailing edge. - out x, 1 side 0 ; Stall here on empty (keep SCK deasserted) + out x, 1 side 0 ; Stall here on empty (keep SCK de-asserted) mov pins, x side 1 [1] ; Output data, assert SCK (mov pins uses OUT mapping) - in pins, 1 side 0 ; Input data, deassert SCK + in pins, 1 side 0 ; Input data, de-assert SCK "# ); @@ -70,7 +71,7 @@ impl<'d, PIO: Instance> PioSpiProgram<'d, PIO> { } }; - Self { prg } + Self { prg, phase } } } @@ -83,35 +84,19 @@ pub enum Error { } /// PIO based Spi driver. -/// -/// This driver is less flexible than the hardware backed one. Configuration can -/// not be changed at runtime. +/// Unlike other PIO programs, the PIO SPI driver owns and holds a reference to +/// the PIO memory it uses. This is so that it can be reconfigured at runtime if +/// desired. pub struct Spi<'d, PIO: Instance, const SM: usize, M: Mode> { sm: StateMachine<'d, PIO, SM>, + cfg: crate::pio::Config<'d, PIO>, + program: Option>, + clk_pin: Pin<'d, PIO>, tx_dma: Option>, rx_dma: Option>, phantom: PhantomData, } -/// PIO SPI configuration. -#[non_exhaustive] -#[derive(Clone)] -pub struct Config { - /// Frequency (Hz). - pub frequency: u32, - /// Polarity. - pub polarity: Polarity, -} - -impl Default for Config { - fn default() -> Self { - Self { - frequency: 1_000_000, - polarity: Polarity::IdleLow, - } - } -} - impl<'d, PIO: Instance, const SM: usize, M: Mode> Spi<'d, PIO, SM, M> { #[allow(clippy::too_many_arguments)] fn new_inner( @@ -122,9 +107,10 @@ impl<'d, PIO: Instance, const SM: usize, M: Mode> Spi<'d, PIO, SM, M> { miso_pin: Peri<'d, impl PioPin>, tx_dma: Option>, rx_dma: Option>, - program: &PioSpiProgram<'d, PIO>, config: Config, ) -> Self { + let program = PioSpiProgram::new(pio, config.phase); + let mut clk_pin = pio.make_pio_pin(clk_pin); let mosi_pin = pio.make_pio_pin(mosi_pin); let miso_pin = pio.make_pio_pin(miso_pin); @@ -153,15 +139,16 @@ impl<'d, PIO: Instance, const SM: usize, M: Mode> Spi<'d, PIO, SM, M> { cfg.shift_out.direction = ShiftDirection::Left; cfg.shift_out.threshold = 8; - let sys_freq = clk_sys_freq().to_fixed::>(); - let target_freq = (config.frequency * 4).to_fixed::>(); - cfg.clock_divider = (sys_freq / target_freq).to_fixed(); + cfg.clock_divider = calculate_clock_divider(config.frequency); sm.set_config(&cfg); sm.set_enable(true); Self { sm, + program: Some(program), + cfg, + clk_pin, tx_dma, rx_dma, phantom: PhantomData, @@ -242,6 +229,63 @@ impl<'d, PIO: Instance, const SM: usize, M: Mode> Spi<'d, PIO, SM, M> { Ok(()) } + + /// Set SPI frequency. + pub fn set_frequency(&mut self, freq: u32) { + self.sm.set_enable(false); + + let divider = calculate_clock_divider(freq); + + // save into the config for later but dont use sm.set_config() since + // that operation is relatively more expensive than just setting the + // clock divider + self.cfg.clock_divider = divider; + self.sm.set_clock_divider(divider); + + self.sm.set_enable(true); + } + + /// Set SPI config. + /// + /// This operation will panic if the PIO program needs to be reloaded and + /// there is insufficient room. This is unlikely since the programs for each + /// phase only differ in size by a single instruction. + pub fn set_config(&mut self, pio: &mut Common<'d, PIO>, config: &Config) { + self.sm.set_enable(false); + + self.cfg.clock_divider = calculate_clock_divider(config.frequency); + + if let Polarity::IdleHigh = config.polarity { + self.clk_pin.set_output_inversion(true); + } else { + self.clk_pin.set_output_inversion(false); + } + + if self.program.as_ref().unwrap().phase != config.phase { + let old_program = self.program.take().unwrap(); + + // SAFETY: the state machine is disabled while this happens + unsafe { pio.free_instr(old_program.prg.used_memory) }; + + let new_program = PioSpiProgram::new(pio, config.phase); + + self.cfg.use_program(&new_program.prg, &[&self.clk_pin]); + self.program = Some(new_program); + } + + self.sm.set_config(&self.cfg); + self.sm.restart(); + + self.sm.set_enable(true); + } +} + +fn calculate_clock_divider(frequency_hz: u32) -> fixed::FixedU32 { + // we multiply by 4 since each clock period is equal to 4 instructions + + let sys_freq = clk_sys_freq().to_fixed::>(); + let target_freq = (frequency_hz * 4).to_fixed::>(); + (sys_freq / target_freq).to_fixed() } impl<'d, PIO: Instance, const SM: usize> Spi<'d, PIO, SM, Blocking> { @@ -252,10 +296,9 @@ impl<'d, PIO: Instance, const SM: usize> Spi<'d, PIO, SM, Blocking> { clk: Peri<'d, impl PioPin>, mosi: Peri<'d, impl PioPin>, miso: Peri<'d, impl PioPin>, - program: &PioSpiProgram<'d, PIO>, config: Config, ) -> Self { - Self::new_inner(pio, sm, clk, mosi, miso, None, None, program, config) + Self::new_inner(pio, sm, clk, mosi, miso, None, None, config) } } @@ -270,7 +313,6 @@ impl<'d, PIO: Instance, const SM: usize> Spi<'d, PIO, SM, Async> { miso: Peri<'d, impl PioPin>, tx_dma: Peri<'d, impl Channel>, rx_dma: Peri<'d, impl Channel>, - program: &PioSpiProgram<'d, PIO>, config: Config, ) -> Self { Self::new_inner( @@ -281,7 +323,6 @@ impl<'d, PIO: Instance, const SM: usize> Spi<'d, PIO, SM, Async> { miso, Some(tx_dma.into()), Some(rx_dma.into()), - program, config, ) } diff --git a/examples/rp/src/bin/pio_spi.rs b/examples/rp/src/bin/pio_spi.rs index 0164e4c81..4218327ec 100644 --- a/examples/rp/src/bin/pio_spi.rs +++ b/examples/rp/src/bin/pio_spi.rs @@ -10,8 +10,8 @@ use defmt::*; use embassy_executor::Spawner; use embassy_rp::peripherals::PIO0; -use embassy_rp::pio_programs::spi::{Config, PioSpiProgram, Spi}; -use embassy_rp::spi::Phase; +use embassy_rp::pio_programs::spi::Spi; +use embassy_rp::spi::Config; use embassy_rp::{bind_interrupts, pio}; use embassy_time::Timer; use {defmt_rtt as _, panic_probe as _}; @@ -25,7 +25,7 @@ async fn main(_spawner: Spawner) { let p = embassy_rp::init(Default::default()); info!("Hello World!"); - // These pins are routed to differnet hardware SPI peripherals, but we can + // These pins are routed to different hardware SPI peripherals, but we can // use them together regardless let mosi = p.PIN_6; // SPI0 SCLK let miso = p.PIN_7; // SPI0 MOSI @@ -33,20 +33,8 @@ async fn main(_spawner: Spawner) { let pio::Pio { mut common, sm0, .. } = pio::Pio::new(p.PIO0, Irqs); - // The PIO program must be configured with the clock phase - let program = PioSpiProgram::new(&mut common, Phase::CaptureOnFirstTransition); - // Construct an SPI driver backed by a PIO state machine - let mut spi = Spi::new_blocking( - &mut common, - sm0, - clk, - mosi, - miso, - &program, - // Only the frequency and polarity are set here - Config::default(), - ); + let mut spi = Spi::new_blocking(&mut common, sm0, clk, mosi, miso, Config::default()); loop { let tx_buf = [1_u8, 2, 3, 4, 5, 6]; diff --git a/examples/rp/src/bin/pio_spi_async.rs b/examples/rp/src/bin/pio_spi_async.rs index 1dbdff609..74a2dd11b 100644 --- a/examples/rp/src/bin/pio_spi_async.rs +++ b/examples/rp/src/bin/pio_spi_async.rs @@ -10,8 +10,8 @@ use defmt::*; use embassy_executor::Spawner; use embassy_rp::peripherals::PIO0; -use embassy_rp::pio_programs::spi::{Config, PioSpiProgram, Spi}; -use embassy_rp::spi::Phase; +use embassy_rp::pio_programs::spi::Spi; +use embassy_rp::spi::Config; use embassy_rp::{bind_interrupts, pio}; use embassy_time::Timer; use {defmt_rtt as _, panic_probe as _}; @@ -25,7 +25,7 @@ async fn main(_spawner: Spawner) { let p = embassy_rp::init(Default::default()); info!("Hello World!"); - // These pins are routed to differnet hardware SPI peripherals, but we can + // These pins are routed to different hardware SPI peripherals, but we can // use them together regardless let mosi = p.PIN_6; // SPI0 SCLK let miso = p.PIN_7; // SPI0 MOSI @@ -33,9 +33,6 @@ async fn main(_spawner: Spawner) { let pio::Pio { mut common, sm0, .. } = pio::Pio::new(p.PIO0, Irqs); - // The PIO program must be configured with the clock phase - let program = PioSpiProgram::new(&mut common, Phase::CaptureOnFirstTransition); - // Construct an SPI driver backed by a PIO state machine let mut spi = Spi::new( &mut common, @@ -46,7 +43,6 @@ async fn main(_spawner: Spawner) { p.DMA_CH0, p.DMA_CH1, &program, - // Only the frequency and polarity are set here Config::default(), ); From 451625ff559661c0cc30ca8a70dd0ccee79ba07b Mon Sep 17 00:00:00 2001 From: Adrian Wowk Date: Fri, 18 Jul 2025 19:23:49 -0500 Subject: [PATCH 6/8] rp: fix pio spi async example --- examples/rp/src/bin/pio_spi_async.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/rp/src/bin/pio_spi_async.rs b/examples/rp/src/bin/pio_spi_async.rs index 74a2dd11b..18b57d26e 100644 --- a/examples/rp/src/bin/pio_spi_async.rs +++ b/examples/rp/src/bin/pio_spi_async.rs @@ -42,7 +42,6 @@ async fn main(_spawner: Spawner) { miso, p.DMA_CH0, p.DMA_CH1, - &program, Config::default(), ); From 51373065752693c1e49d8d9df7c4452d0f62b7f3 Mon Sep 17 00:00:00 2001 From: Adrian Wowk Date: Wed, 20 Aug 2025 13:40:14 -0500 Subject: [PATCH 7/8] rp: move pio pin configs after set_config This is needed for the program to work correctly on rp235xb when using the higher pin numbers. --- embassy-rp/src/pio_programs/spi.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/embassy-rp/src/pio_programs/spi.rs b/embassy-rp/src/pio_programs/spi.rs index 6b97cd0f3..b10fc6628 100644 --- a/embassy-rp/src/pio_programs/spi.rs +++ b/embassy-rp/src/pio_programs/spi.rs @@ -121,10 +121,6 @@ impl<'d, PIO: Instance, const SM: usize, M: Mode> Spi<'d, PIO, SM, M> { clk_pin.set_output_inversion(false); } - sm.set_pins(Level::Low, &[&clk_pin, &mosi_pin]); - sm.set_pin_dirs(Direction::Out, &[&clk_pin, &mosi_pin]); - sm.set_pin_dirs(Direction::In, &[&miso_pin]); - let mut cfg = crate::pio::Config::default(); cfg.use_program(&program.prg, &[&clk_pin]); @@ -142,6 +138,11 @@ impl<'d, PIO: Instance, const SM: usize, M: Mode> Spi<'d, PIO, SM, M> { cfg.clock_divider = calculate_clock_divider(config.frequency); sm.set_config(&cfg); + + sm.set_pins(Level::Low, &[&clk_pin, &mosi_pin]); + sm.set_pin_dirs(Direction::Out, &[&clk_pin, &mosi_pin]); + sm.set_pin_dirs(Direction::In, &[&miso_pin]); + sm.set_enable(true); Self { From 815ba8aa7507d0aa27dfb3f3823793e3567719f8 Mon Sep 17 00:00:00 2001 From: Adrian Wowk Date: Wed, 20 Aug 2025 13:44:56 -0500 Subject: [PATCH 8/8] rp: read pio gpiobase in set_pins and set_pin_dirs --- embassy-rp/src/pio/mod.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/embassy-rp/src/pio/mod.rs b/embassy-rp/src/pio/mod.rs index f46e664e4..5f554dfe3 100644 --- a/embassy-rp/src/pio/mod.rs +++ b/embassy-rp/src/pio/mod.rs @@ -977,13 +977,27 @@ impl<'d, PIO: Instance + 'd, const SM: usize> StateMachine<'d, PIO, SM> { self.set_enable(enabled); } + #[cfg(feature = "rp2040")] + fn pin_base() -> u8 { + 0 + } + + #[cfg(feature = "_rp235x")] + fn pin_base() -> u8 { + if PIO::PIO.gpiobase().read().gpiobase() { + 16 + } else { + 0 + } + } + /// Sets pin directions. This pauses the current state machine to run `SET` commands /// and temporarily unsets the `OUT_STICKY` bit. pub fn set_pin_dirs(&mut self, dir: Direction, pins: &[&Pin<'d, PIO>]) { self.with_paused(|sm| { for pin in pins { Self::this_sm().pinctrl().write(|w| { - w.set_set_base(pin.pin()); + w.set_set_base(pin.pin() - Self::pin_base()); w.set_set_count(1); }); // SET PINDIRS, (dir) @@ -998,7 +1012,7 @@ impl<'d, PIO: Instance + 'd, const SM: usize> StateMachine<'d, PIO, SM> { self.with_paused(|sm| { for pin in pins { Self::this_sm().pinctrl().write(|w| { - w.set_set_base(pin.pin()); + w.set_set_base(pin.pin() - Self::pin_base()); w.set_set_count(1); }); // SET PINS, (dir) @@ -1361,6 +1375,7 @@ impl<'d, PIO: Instance> Pio<'d, PIO> { PIO::state().users.store(5, Ordering::Release); PIO::state().used_pins.store(0, Ordering::Release); PIO::Interrupt::unpend(); + unsafe { PIO::Interrupt::enable() }; Self { common: Common {