Skip to main content

rp2040_i2s/
lib.rs

1#![no_std]
2
3use fugit::HertzU32;
4use rp2040_hal::{
5    gpio::{FunctionNull, Pin, PinId, PullDown, ValidFunction},
6    pio::{
7        InstallError, PIOExt, Rx, StateMachine, StateMachineIndex, Stopped, Tx, UninitStateMachine,
8        ValidStateMachine, PIO,
9    },
10};
11
12// Based on a 48kHz sample rate
13// TODO(bschwind) - Support other sample rates. It's basically
14// sample_rate * 2 (stereo) * 2 (2 clock cycles per bit output) * 32 (32 bits per sample)
15const PIO_CLOCK_HZ: u32 = 6_144_000;
16
17#[derive(Debug)]
18pub enum I2SError {
19    PioInstallationError(InstallError),
20}
21
22pub enum PioClockDivider {
23    Exact { integer: u16, fraction: u8 },
24    FromSystemClock(HertzU32),
25}
26
27impl PioClockDivider {
28    fn pio_divider(&self) -> (u16, u8) {
29        match self {
30            Self::Exact { integer, fraction } => (*integer, *fraction),
31            Self::FromSystemClock(system_clock_hz) => {
32                let hertz = system_clock_hz.to_Hz();
33                let (fraction, integer) = libm::modf(hertz as f64 / PIO_CLOCK_HZ as f64);
34
35                (integer as u16, (fraction * 256.0) as u8)
36            },
37        }
38    }
39}
40
41pub trait SampleReader {
42    fn read(&mut self) -> Option<u32>;
43}
44
45impl<SM: ValidStateMachine> SampleReader for Rx<SM> {
46    fn read(&mut self) -> Option<u32> {
47        self.read()
48    }
49}
50
51pub struct I2SOutput<P: rp2040_hal::pio::PIOExt, SM: rp2040_hal::pio::StateMachineIndex> {
52    state_machine: StateMachine<(P, SM), Stopped>,
53    fifo_rx: Rx<(P, SM)>,
54    fifo_tx: Tx<(P, SM)>,
55}
56
57impl<P: PIOExt, SM: StateMachineIndex> I2SOutput<P, SM> {
58    /// Create an I2S output with a data line, a bit clock, and a left/right word clock.
59    /// The left/right word clock pin MUST consecutively follow the bit clock pin. So if
60    /// the bit clock pin is 7, the word clock pin MUST be 8.
61    pub fn new<DataPin, BitClockPin, LeftRightClockPin>(
62        pio: &mut PIO<P>,
63        clock_divider: PioClockDivider,
64        state_machine: UninitStateMachine<(P, SM)>,
65        data_out_pin: Pin<DataPin, FunctionNull, PullDown>,
66        bit_clock_pin: Pin<BitClockPin, FunctionNull, PullDown>,
67        left_right_clock_pin: Pin<LeftRightClockPin, FunctionNull, PullDown>,
68    ) -> Result<Self, I2SError>
69    where
70        DataPin: PinId + ValidFunction<P::PinFunction>,
71        BitClockPin: PinId + ValidFunction<P::PinFunction>,
72        LeftRightClockPin: PinId + ValidFunction<P::PinFunction>,
73    {
74        let data_out_pin: Pin<_, P::PinFunction, _> = data_out_pin.into_function();
75        let bit_clock_pin: Pin<_, P::PinFunction, _> = bit_clock_pin.into_function();
76        let left_right_clock_pin: Pin<_, P::PinFunction, _> = left_right_clock_pin.into_function();
77
78        let data_pin_id = data_out_pin.id().num;
79        let bit_clock_pin_id = bit_clock_pin.id().num;
80        let left_right_clock_pin_id = left_right_clock_pin.id().num;
81
82        assert_eq!(
83            left_right_clock_pin_id - bit_clock_pin_id,
84            1,
85            "The word clock pin must consecutively follow the bit clock pin"
86        );
87
88        #[rustfmt::skip]
89        let dac_pio_program = pio_proc::pio_asm!(
90            ".side_set 2",
91            "    set x, 30          side 0b01", // side 0bWB - W = Word Clock, B = Bit Clock
92            "left_data:",
93            "    out pins, 1        side 0b00",
94            "    jmp x-- left_data  side 0b01",
95            "    out pins 1         side 0b10",
96            "    set x, 30          side 0b11",
97            "right_data:",
98            "    out pins 1         side 0b10",
99            "    jmp x-- right_data side 0b11",
100            "    out pins 1         side 0b00",
101        );
102
103        let installed =
104            pio.install(&dac_pio_program.program).map_err(I2SError::PioInstallationError)?;
105
106        let (divider_int, divider_fraction) = clock_divider.pio_divider();
107
108        let (mut dac_sm, fifo_rx, fifo_tx) =
109            rp2040_hal::pio::PIOBuilder::from_installed_program(installed)
110                .out_pins(data_pin_id, 1)
111                .side_set_pin_base(bit_clock_pin_id)
112                .out_shift_direction(rp2040_hal::pio::ShiftDirection::Left)
113                .clock_divisor_fixed_point(divider_int, divider_fraction)
114                .buffers(rp2040_hal::pio::Buffers::OnlyTx)
115                .autopull(true)
116                .pull_threshold(32)
117                .build(state_machine);
118
119        dac_sm.set_pindirs([
120            (data_pin_id, rp2040_hal::pio::PinDir::Output),
121            (bit_clock_pin_id, rp2040_hal::pio::PinDir::Output),
122            (left_right_clock_pin_id, rp2040_hal::pio::PinDir::Output),
123        ]);
124
125        Ok(Self { state_machine: dac_sm, fifo_rx, fifo_tx })
126    }
127
128    #[allow(clippy::type_complexity)]
129    pub fn split(self) -> (StateMachine<(P, SM), Stopped>, Rx<(P, SM)>, Tx<(P, SM)>) {
130        (self.state_machine, self.fifo_rx, self.fifo_tx)
131    }
132}
133
134pub struct I2SInput<P: rp2040_hal::pio::PIOExt, SM: rp2040_hal::pio::StateMachineIndex> {
135    state_machine: StateMachine<(P, SM), Stopped>,
136    fifo_rx: Rx<(P, SM)>,
137    fifo_tx: Tx<(P, SM)>,
138}
139
140impl<P: PIOExt, SM: StateMachineIndex> I2SInput<P, SM> {
141    /// Create an I2S input with a data line, a bit clock, and a left/right word clock.
142    /// The left/right word clock pin MUST consecutively follow the bit clock pin. So if
143    /// the bit clock pin is 7, the word clock pin MUST be 8.
144    pub fn new<DataPin, BitClockPin, LeftRightClockPin>(
145        pio: &mut PIO<P>,
146        clock_divider: PioClockDivider,
147        state_machine: UninitStateMachine<(P, SM)>,
148        data_in_pin: Pin<DataPin, FunctionNull, PullDown>,
149        bit_clock_pin: Pin<BitClockPin, FunctionNull, PullDown>,
150        left_right_clock_pin: Pin<LeftRightClockPin, FunctionNull, PullDown>,
151    ) -> Result<Self, I2SError>
152    where
153        DataPin: PinId + ValidFunction<P::PinFunction>,
154        BitClockPin: PinId + ValidFunction<P::PinFunction>,
155        LeftRightClockPin: PinId + ValidFunction<P::PinFunction>,
156    {
157        let data_in_pin: Pin<_, P::PinFunction, _> = data_in_pin.into_function();
158        let bit_clock_pin: Pin<_, P::PinFunction, _> = bit_clock_pin.into_function();
159        let left_right_clock_pin: Pin<_, P::PinFunction, _> = left_right_clock_pin.into_function();
160
161        let data_pin_id = data_in_pin.id().num;
162        let bit_clock_pin_id = bit_clock_pin.id().num;
163        let left_right_clock_pin_id = left_right_clock_pin.id().num;
164
165        assert_eq!(
166            left_right_clock_pin_id - bit_clock_pin_id,
167            1,
168            "The word clock pin must consecutively follow the bit clock pin"
169        );
170
171        #[rustfmt::skip]
172        let mic_pio_program = pio_proc::pio_asm!(
173            ".side_set 2",
174            "    set x, 30          side 0b00", // side 0bWB - W = Word Clock, B = Bit Clock
175            "left_data:",
176            "    in pins, 1         side 0b01",
177            "    jmp x-- left_data  side 0b00",
178            "    in pins, 1         side 0b11",
179            "    set x, 30          side 0b10",
180            "right_data:",
181            "    in pins, 1         side 0b11",
182            "    jmp x-- right_data side 0b10",
183            "    in pins, 1         side 0b01",
184        );
185
186        let installed = pio.install(&mic_pio_program.program).unwrap();
187
188        let (divider_int, divider_fraction) = clock_divider.pio_divider();
189
190        let (mut mic_sm, fifo_rx, fifo_tx) =
191            rp2040_hal::pio::PIOBuilder::from_installed_program(installed)
192                .in_pin_base(data_pin_id)
193                .side_set_pin_base(bit_clock_pin_id)
194                .in_shift_direction(rp2040_hal::pio::ShiftDirection::Left)
195                .clock_divisor_fixed_point(divider_int, divider_fraction)
196                .buffers(rp2040_hal::pio::Buffers::OnlyRx)
197                .autopush(true)
198                .push_threshold(32)
199                .build(state_machine);
200
201        mic_sm.set_pindirs([
202            (data_pin_id, rp2040_hal::pio::PinDir::Input),
203            (bit_clock_pin_id, rp2040_hal::pio::PinDir::Output),
204            (left_right_clock_pin_id, rp2040_hal::pio::PinDir::Output),
205        ]);
206
207        Ok(Self { state_machine: mic_sm, fifo_rx, fifo_tx })
208    }
209
210    /// Create an I2S input which only reads from a data line. This assumes
211    /// the I2S device is clocked by another state machine returned from
212    /// `I2SInput::new`. The state machines must have their clock dividers
213    /// sychronized, and start at the exact same time. The PIO program code
214    /// must also stay in sync in terms of when reads occur, and the number
215    /// of instructions.
216    pub fn new_data_only<DataPin>(
217        pio: &mut PIO<P>,
218        clock_divider: PioClockDivider,
219        state_machine: UninitStateMachine<(P, SM)>,
220        data_in_pin: Pin<DataPin, FunctionNull, PullDown>,
221    ) -> Result<Self, I2SError>
222    where
223        DataPin: PinId + ValidFunction<P::PinFunction>,
224    {
225        let data_in_pin: Pin<_, P::PinFunction, _> = data_in_pin.into_function();
226        let data_pin_id = data_in_pin.id().num;
227
228        #[rustfmt::skip]
229        let mic_pio_program = pio_proc::pio_asm!(
230            "    set x, 30",
231            "left_data:",
232            "    in pins, 1",
233            "    jmp x-- left_data",
234            "    in pins, 1",
235            "    set x, 30",
236            "right_data:",
237            "    in pins, 1",
238            "    jmp x-- right_data",
239            "    in pins, 1",
240        );
241
242        let installed = pio.install(&mic_pio_program.program).unwrap();
243
244        let (divider_int, divider_fraction) = clock_divider.pio_divider();
245
246        let (mut mic_sm, fifo_rx, fifo_tx) =
247            rp2040_hal::pio::PIOBuilder::from_installed_program(installed)
248                .in_pin_base(data_pin_id)
249                .in_shift_direction(rp2040_hal::pio::ShiftDirection::Left)
250                .clock_divisor_fixed_point(divider_int, divider_fraction)
251                .buffers(rp2040_hal::pio::Buffers::OnlyRx)
252                .autopush(true)
253                .push_threshold(32)
254                .build(state_machine);
255
256        mic_sm.set_pindirs([(data_pin_id, rp2040_hal::pio::PinDir::Input)]);
257
258        Ok(Self { state_machine: mic_sm, fifo_rx, fifo_tx })
259    }
260
261    #[allow(clippy::type_complexity)]
262    pub fn split(self) -> (StateMachine<(P, SM), Stopped>, Rx<(P, SM)>, Tx<(P, SM)>) {
263        (self.state_machine, self.fifo_rx, self.fifo_tx)
264    }
265}