diff --git a/examples/rp/src/bin/pio_i2s.rs b/examples/rp/src/bin/pio_i2s.rs new file mode 100644 index 000000000..cf60e5b30 --- /dev/null +++ b/examples/rp/src/bin/pio_i2s.rs @@ -0,0 +1,125 @@ +//! This example shows generating audio and sending it to a connected i2s DAC using the PIO +//! module of the RP2040. +//! +//! Connect the i2s DAC as follows: +//! bclk : GPIO 18 +//! lrc : GPIO 19 +//! din : GPIO 20 +//! Then hold down the boot select button to trigger a rising triangle waveform. + +#![no_std] +#![no_main] + +use core::mem; + +use embassy_executor::Spawner; +use embassy_rp::peripherals::PIO0; +use embassy_rp::pio::{Config, FifoJoin, InterruptHandler, Pio, ShiftConfig, ShiftDirection}; +use embassy_rp::{bind_interrupts, Peripheral}; +use fixed::traits::ToFixed; +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; + +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + let mut p = embassy_rp::init(Default::default()); + + // Setup pio state machine for i2s output + let mut pio = Pio::new(p.PIO0, Irqs); + + #[rustfmt::skip] + let pio_program = pio_proc::pio_asm!( + ".side_set 2", + " set x, 14 side 0b01", // side 0bWB - W = Word Clock, B = Bit Clock + "left_data:", + " out pins, 1 side 0b00", + " jmp x-- left_data side 0b01", + " out pins 1 side 0b10", + " set x, 14 side 0b11", + "right_data:", + " out pins 1 side 0b10", + " jmp x-- right_data side 0b11", + " out pins 1 side 0b00", + ); + + let bit_clock_pin = p.PIN_18; + let left_right_clock_pin = p.PIN_19; + let data_pin = p.PIN_20; + + let data_pin = pio.common.make_pio_pin(data_pin); + let bit_clock_pin = pio.common.make_pio_pin(bit_clock_pin); + let left_right_clock_pin = pio.common.make_pio_pin(left_right_clock_pin); + + let cfg = { + let mut cfg = Config::default(); + cfg.use_program( + &pio.common.load_program(&pio_program.program), + &[&bit_clock_pin, &left_right_clock_pin], + ); + cfg.set_out_pins(&[&data_pin]); + const BIT_DEPTH: u32 = 16; + const CHANNELS: u32 = 2; + let clock_frequency = SAMPLE_RATE * BIT_DEPTH * CHANNELS; + cfg.clock_divider = (125_000_000. / clock_frequency as f64 / 2.).to_fixed(); + cfg.shift_out = 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::TxOnly; + cfg + }; + pio.sm0.set_config(&cfg); + pio.sm0.set_pin_dirs( + embassy_rp::pio::Direction::Out, + &[&data_pin, &left_right_clock_pin, &bit_clock_pin], + ); + + // create two audio buffers (back and front) which will take turns being + // filled with new audio data and being sent to 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); + + // start pio state machine + pio.sm0.set_enable(true); + let tx = pio.sm0.tx(); + let mut dma_ref = p.DMA_CH0.into_ref(); + + let mut fade_value: i32 = 0; + let mut phase: i32 = 0; + + loop { + // trigger transfer of front buffer data to the pio fifo + // but don't await the returned future, yet + let dma_future = tx.dma_push(dma_ref.reborrow(), front_buffer); + + // fade in audio when bootsel is pressed + let fade_target = if p.BOOTSEL.is_pressed() { i32::MAX } else { 0 }; + + // fill back buffer with fresh audio samples before awaiting the dma future + for s in back_buffer.iter_mut() { + // exponential approach of fade_value => fade_target + fade_value += (fade_target - fade_value) >> 14; + // generate triangle wave with amplitude and frequency based on fade value + phase = (phase + (fade_value >> 22)) & 0xffff; + let triangle_sample = (phase as i16 as i32).abs() - 16384; + let sample = (triangle_sample * (fade_value >> 15)) >> 16; + // duplicate mono sample into lower and upper half of dma word + *s = (sample as u16 as u32) * 0x10001; + } + + // 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; + mem::swap(&mut back_buffer, &mut front_buffer); + } +}