Bidirectional I2S with PIO
Raspberry provide an example I2S output PIO program. I needed simultaneous input and output, so I adapted their program for that. Code at the end of the post.
The default I2S program has output like this:
- Bit clock low, output data
- Bit clock high, remote codec chip latches data
We want this:
- Bit clock low, we output data, so does remote
- Bit clock high, we latch data, so does remote
To do this we need INs as well as OUTs. We can’t squeeze that into existing cycle count, so we run this PIO program 2x as fast as raspberry’s output-only program would run for the same sample rate. That means our final PIO clock should be 4 * sample_rate * 32 for 16-bit stereo.
We do all the extra work during BCLK=1, so all our outputs just get an extra delay cycle. Nice and easy. As with the original, adjust the set commands as necessary for higher bit depths than 16-bit.
You’ll also need to configure the state machine for input, in the same way as output. Output has autopull @ 32 and shift left, so input wants autopush @ 32 and shift left also. And don’t forget to set up the input pin. By the way, don’t forget you need to manually jump to entry_point. I forgot to do that in my rust code and was very confused when my audio bits were out of wack.
If you’ve never done I2S with PIO, I’d recommend referencing the original file for the setup code and notes on usage.
Also, a note on clocks. Raspberry suggest that “Fractional [PIO clock] divider will probably be needed to get correct bit clock period, but for common sysclk freqs this should still give a constant word select period”. If you are targeting a common sample rate, I think this is not true for the default 150MHz sysclk. At least, 44100Hz and 48000Hz give me fractions even across an entire word or frame. You will need to adjust your sysclk a bit if you really need one of the standard sample rates.
Using a fractional divider would be bad in general for my chip. Right now I have it deriving its clock directly from BCLK, so I definitely don’t want that jittering.
But, I don’t like having clock jitter of any sort around any of my audio code even if on paper it seems like it’s fine (I’ve been fighting this with my GBA project…). Just don’t use a fractional divider unless you’re desperate, your life will be easier for it. Adjust your sysclk based on what sample rates you need, or if you can get away with it, just use a nonstandard sample rate. It’s fine.
Anyway, here’s the code:
;
; Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
; Copyright (c) 2026 Artemis Everfree
;
; Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
; following conditions are met:
;
; 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
; disclaimer.
;
; 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
; disclaimer in the documentation and/or other materials provided with the distribution.
;
; 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
; derived from this software without specific prior written permission.
;
; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
; INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
; DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
; SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
; THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
.program audio_i2s_bidi
.side_set 2
; /--- LRCLK
; |/-- BCLK
bitloop1: ; ||
out pins, 1 side 0b10 [1]
in pins, 1 side 0b11
jmp x-- bitloop1 side 0b11
out pins, 1 side 0b00 [1]
in pins, 1 side 0b01
set x, 14 side 0b01
bitloop0:
out pins, 1 side 0b00 [1]
in pins, 1 side 0b01
jmp x-- bitloop0 side 0b01
out pins, 1 side 0b10 [1]
in pins, 1 side 0b11
public entry_point:
set x, 14 side 0b11
And here’s a little bonus for how to initialize the state machine and jump to entry_point with the rust pio crate / hal.
let i2s_prog = pio::pio_file!("./src/i2s.pio", select_program("audio_i2s_bidi"));
// Save entry_point label so we can jump to it later
let i2s_entry = i2s_prog.public_defines.entry_point;
let i2s_prog = i2s_prog.program;
let (mut pio, sm0, _, _, _) = pac.PIO0.split(&mut pac.RESETS);
let i2s_prog = pio.install(&i2s_prog).unwrap();
let i2s_offset = i2s_prog.offset();
let (mut i2s_pio_sm, mut i2s_rx, mut i2s_tx)
= PIOBuilder::from_installed_program(i2s_prog)
// Output config
.out_pins(11 /* GPIO_11 */, 1)
.side_set_pin_base(14 /* GPIO_14 */)
.autopull(true)
.pull_threshold(32)
.out_shift_direction(ShiftDirection::Left)
// Input config
.in_count(1)
.in_pin_base(10 /* GPIO_10 */)
.autopush(true)
.push_threshold(32)
.in_shift_direction(ShiftDirection::Left)
// Gives a 47261.538461538Hz sample rate with 150MHz SYSCLK.
// Adjust SYSCLK to get something closer to standard if you need.
.clock_divisor_fixed_point(26, 0)
.build(sm0);
// THE PINS FOR IT
let mut pi2s_di = pins
.gpio10
.into_function::<FunctionPio0>();
let mut pi2s_do = pins
.gpio11
.into_function::<FunctionPio0>();
let mut pi2s_fsync = pins
.gpio14
.into_function::<FunctionPio0>();
let mut pi2s_bclk = pins
.gpio15
.into_function::<FunctionPio0>();
i2s_pio_sm.set_pindirs([
(10, PinDir::Input),
(11, PinDir::Output),
(14, PinDir::Output),
(15, PinDir::Output),
]);
// Jump to entry_point label
let entry_addr = i2s_entry as u16 + i2s_offset as u16;
assert!(entry_addr < 0b1_00000);
let instr = pio::Instruction {
operands: pio::InstructionOperands::JMP {
condition: pio::JmpCondition::Always,
address: entry_addr as u8,
},
delay: 0,
// If this is None, instruction encoding panics. just to check
// if you're awake. keep you on your toes. make sure you know
// your installed program is side setting whether you like it
// or not.
side_set: Some(0),
};
i2s_pio_sm.exec_instruction(instr);
// Run the thing
let mut i2s_pio_sm = i2s_pio_sm.start();
// Now you can do stuff with the FIFOs.