Merge pull request #4210 from mcaveniathor/pio_i2s_rx

Add PioI2sIn, PioI2sInProgram, and example binary
This commit is contained in:
Dario Nieuwenhuis 2025-09-05 19:08:06 +00:00 committed by GitHub
commit 46bf0c71cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 176 additions and 4 deletions

View File

@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<!-- next-header -->
## Unreleased - ReleaseDate
- Add PIO SPI
- Add PIO I2S input
## 0.8.0 - 2025-08-26
## 0.7.1 - 2025-08-26

View File

@ -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);

View File

@ -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<PIO0>;
});
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);
}
}