From 62ff0194f4b7413b17dbc69813ec205638248aa7 Mon Sep 17 00:00:00 2001 From: Adrian Wowk Date: Fri, 18 Jul 2025 19:19:27 -0500 Subject: [PATCH] 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(), );