From 241129c569023dc71d0025cdc41bcfe40418e7b8 Mon Sep 17 00:00:00 2001 From: Thor McAvenia Date: Fri, 16 May 2025 00:09:10 -0700 Subject: [PATCH] Add PioI2sIn, PioI2sInProgram, and example binary --- embassy-rp/CHANGELOG.md | 3 + embassy-rp/src/pio_programs/i2s.rs | 96 +++++++++++++++++++++++++-- examples/rp235x/src/bin/pio_i2s_rx.rs | 81 ++++++++++++++++++++++ 3 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 examples/rp235x/src/bin/pio_i2s_rx.rs diff --git a/embassy-rp/CHANGELOG.md b/embassy-rp/CHANGELOG.md index ebdc3e1c8..b50d41dd1 100644 --- a/embassy-rp/CHANGELOG.md +++ b/embassy-rp/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased - ReleaseDate +- Add PIO SPI +- Add PIO I2S input + ## 0.8.0 - 2025-08-26 ## 0.7.1 - 2025-08-26 diff --git a/embassy-rp/src/pio_programs/i2s.rs b/embassy-rp/src/pio_programs/i2s.rs index 7ceed3fa6..2382a3f9f 100644 --- a/embassy-rp/src/pio_programs/i2s.rs +++ b/embassy-rp/src/pio_programs/i2s.rs @@ -1,13 +1,101 @@ -//! Pio backed I2s output +//! Pio backed I2s output and output drivers use fixed::traits::ToFixed; use crate::dma::{AnyChannel, Channel, Transfer}; +use crate::gpio::Pull; use crate::pio::{ Common, Config, Direction, FifoJoin, Instance, LoadedProgram, PioPin, ShiftConfig, ShiftDirection, StateMachine, }; use crate::Peri; +/// This struct represents an i2s receiver & controller driver program +pub struct PioI2sInProgram<'d, PIO: Instance> { + prg: LoadedProgram<'d, PIO>, +} + +impl<'d, PIO: Instance> PioI2sInProgram<'d, PIO> { + /// Load the input program into the given pio + pub fn new(common: &mut Common<'d, PIO>) -> Self { + let prg = pio::pio_asm! { + ".side_set 2", + " set x, 14 side 0b01", + "left_data:", + " in pins, 1 side 0b00", // read one left-channel bit from SD + " jmp x-- left_data side 0b01", + " in pins, 1 side 0b10", // ws changes 1 clock before MSB + " set x, 14 side 0b11", + "right_data:", + " in pins, 1 side 0b10", + " jmp x-- right_data side 0b11", + " in pins, 1 side 0b00" // ws changes 1 clock before ms + }; + let prg = common.load_program(&prg.program); + Self { prg } + } +} + +/// Pio backed I2s input driver +pub struct PioI2sIn<'d, P: Instance, const S: usize> { + dma: Peri<'d, AnyChannel>, + sm: StateMachine<'d, P, S>, +} + +impl<'d, P: Instance, const S: usize> PioI2sIn<'d, P, S> { + /// Configure a state machine to act as both the controller (provider of SCK and WS) and receiver (of SD) for an I2S signal + pub fn new( + common: &mut Common<'d, P>, + mut sm: StateMachine<'d, P, S>, + dma: Peri<'d, impl Channel>, + // Whether or not to use the MCU's internal pull-down resistor, as the + // Pico 2 is known to have problems with the inbuilt pulldowns, many + // opt to just use an external pull down resistor to meet requirements of common + // i2s microphones such as the INMP441 + data_pulldown: bool, + data_pin: Peri<'d, impl PioPin>, + bit_clock_pin: Peri<'d, impl PioPin>, + lr_clock_pin: Peri<'d, impl PioPin>, + sample_rate: u32, + bit_depth: u32, + channels: u32, + program: &PioI2sInProgram<'d, P>, + ) -> Self { + let mut data_pin = common.make_pio_pin(data_pin); + if data_pulldown { + data_pin.set_pull(Pull::Down); + } + let bit_clock_pin = common.make_pio_pin(bit_clock_pin); + let left_right_clock_pin = common.make_pio_pin(lr_clock_pin); + + let cfg = { + let mut cfg = Config::default(); + cfg.use_program(&program.prg, &[&bit_clock_pin, &left_right_clock_pin]); + cfg.set_in_pins(&[&data_pin]); + let clock_frequency = sample_rate * bit_depth * channels; + cfg.clock_divider = (crate::clocks::clk_sys_freq() as f64 / clock_frequency as f64 / 2.).to_fixed(); + cfg.shift_in = ShiftConfig { + threshold: 32, + direction: ShiftDirection::Left, + auto_fill: true, + }; + // join fifos to have twice the time to start the next dma transfer + cfg.fifo_join = FifoJoin::RxOnly; // both control signals are sent via side-setting + cfg + }; + sm.set_config(&cfg); + sm.set_pin_dirs(Direction::In, &[&data_pin]); + sm.set_pin_dirs(Direction::Out, &[&left_right_clock_pin, &bit_clock_pin]); + sm.set_enable(true); + + Self { dma: dma.into(), sm } + } + + /// Return an in-prograss dma transfer future. Awaiting it will guarentee a complete transfer. + pub fn read<'b>(&'b mut self, buff: &'b mut [u32]) -> Transfer<'b, AnyChannel> { + self.sm.rx().dma_pull(self.dma.reborrow(), buff, false) + } +} + /// This struct represents an i2s output driver program /// /// The sample bit-depth is set through scratch register `Y`. @@ -26,12 +114,12 @@ impl<'d, PIO: Instance> PioI2sOutProgram<'d, PIO> { "left_data:", " out pins, 1 side 0b00", " jmp x-- left_data side 0b01", - " out pins 1 side 0b10", + " out pins, 1 side 0b10", " mov x, y side 0b11", "right_data:", - " out pins 1 side 0b10", + " out pins, 1 side 0b10", " jmp x-- right_data side 0b11", - " out pins 1 side 0b00", + " out pins, 1 side 0b00", ); let prg = common.load_program(&prg.program); diff --git a/examples/rp235x/src/bin/pio_i2s_rx.rs b/examples/rp235x/src/bin/pio_i2s_rx.rs new file mode 100644 index 000000000..c3f505b13 --- /dev/null +++ b/examples/rp235x/src/bin/pio_i2s_rx.rs @@ -0,0 +1,81 @@ +//! This example shows receiving audio from a connected I2S microphone (or other audio source) +//! using the PIO module of the RP235x. +//! +//! +//! Connect the i2s microphone as follows: +//! bclk : GPIO 18 +//! lrc : GPIO 19 +//! din : GPIO 20 +//! Then hold down the boot select button to begin receiving audio. Received I2S words will be written to +//! buffers for the left and right channels for use in your application, whether that's storage or +//! further processing +//! +//! Note the const USE_ONBOARD_PULLDOWN is by default set to false, meaning an external +//! pull-down resistor is being used on the data pin if required by the mic being used. + +#![no_std] +#![no_main] +use core::mem; + +use defmt::*; +use embassy_executor::Spawner; +use embassy_rp::bind_interrupts; +use embassy_rp::peripherals::PIO0; +use embassy_rp::pio::{InterruptHandler, Pio}; +use embassy_rp::pio_programs::i2s::{PioI2sIn, PioI2sInProgram}; +use static_cell::StaticCell; +use {defmt_rtt as _, panic_probe as _}; + +bind_interrupts!(struct Irqs { + PIO0_IRQ_0 => InterruptHandler; +}); + +const SAMPLE_RATE: u32 = 48_000; +const BIT_DEPTH: u32 = 16; +const CHANNELS: u32 = 2; +const USE_ONBOARD_PULLDOWN: bool = false; // whether or not to use the onboard pull-down resistor, + // which has documented issues on many RP235x boards +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + let p = embassy_rp::init(Default::default()); + + // Setup pio state machine for i2s input + let Pio { mut common, sm0, .. } = Pio::new(p.PIO0, Irqs); + + let bit_clock_pin = p.PIN_18; + let left_right_clock_pin = p.PIN_19; + let data_pin = p.PIN_20; + + let program = PioI2sInProgram::new(&mut common); + let mut i2s = PioI2sIn::new( + &mut common, + sm0, + p.DMA_CH0, + USE_ONBOARD_PULLDOWN, + data_pin, + bit_clock_pin, + left_right_clock_pin, + SAMPLE_RATE, + BIT_DEPTH, + CHANNELS, + &program, + ); + + // create two audio buffers (back and front) which will take turns being + // filled with new audio data from the PIO fifo using DMA + const BUFFER_SIZE: usize = 960; + static DMA_BUFFER: StaticCell<[u32; BUFFER_SIZE * 2]> = StaticCell::new(); + let dma_buffer = DMA_BUFFER.init_with(|| [0u32; BUFFER_SIZE * 2]); + let (mut back_buffer, mut front_buffer) = dma_buffer.split_at_mut(BUFFER_SIZE); + + loop { + // trigger transfer of front buffer data to the pio fifo + // but don't await the returned future, yet + let dma_future = i2s.read(front_buffer); + // now await the dma future. once the dma finishes, the next buffer needs to be queued + // within DMA_DEPTH / SAMPLE_RATE = 8 / 48000 seconds = 166us + dma_future.await; + info!("Received I2S data word: {:?}", &front_buffer); + mem::swap(&mut back_buffer, &mut front_buffer); + } +}