From 714cf29d99fc7aad159fffd675c377b14c6bfb4e Mon Sep 17 00:00:00 2001 From: "Earle F. Philhower, III" Date: Wed, 10 Sep 2025 17:37:07 -0700 Subject: [PATCH 1/2] Add I2S slave output mode Uses an external BCLK and LRCLK to control timing of the I2S data output. Useful for synchronizing to an external device or for higher precision clocking than the internal PLL can provide. --- docs/i2s.rst | 6 ++ libraries/I2S/keywords.txt | 1 + libraries/I2S/src/I2S.cpp | 35 ++++++++-- libraries/I2S/src/I2S.h | 2 + libraries/I2S/src/pio_i2s.pio | 90 ++++++++++++++++++++++++-- libraries/I2S/src/pio_i2s.pio.h | 111 +++++++++++++++++++++++++++++++- 6 files changed, 231 insertions(+), 14 deletions(-) diff --git a/docs/i2s.rst b/docs/i2s.rst index bec76dd55..d73ef85e1 100644 --- a/docs/i2s.rst +++ b/docs/i2s.rst @@ -36,6 +36,12 @@ Creates a bi-directional I2S input and output port. Needs to be connected up to the desired pins (see below) and started before any input or output can happen. +bool setSlave() +~~~~~~~~~~~~~~~ +Enables slave mode. BCLK and LRCLK are inputs and used to control the +timing of the DOUT output. Only normal I2S output mode is supported in +slave mode. + bool setBCLK(pin_size_t pin) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Sets the BCLK pin of the I2S device. The LRCLK/word clock will be ``pin + 1`` diff --git a/libraries/I2S/keywords.txt b/libraries/I2S/keywords.txt index 44f2e8d6e..5579b56ee 100644 --- a/libraries/I2S/keywords.txt +++ b/libraries/I2S/keywords.txt @@ -28,6 +28,7 @@ setTDMChannels KEYWORD2 swapClocks KEYWORD2 setMCLKmult KEYWORD2 setSysClk KEYWORD2 +setSlave KEYWORD2 read8 KEYWORD2 read16 KEYWORD2 diff --git a/libraries/I2S/src/I2S.cpp b/libraries/I2S/src/I2S.cpp index fdba6dc88..545d08d32 100644 --- a/libraries/I2S/src/I2S.cpp +++ b/libraries/I2S/src/I2S.cpp @@ -65,6 +65,7 @@ I2S::I2S(PinMode direction, pin_size_t bclk, pin_size_t data, pin_size_t mclk, p _tdmChannels = 8; _swapClocks = false; _multMCLK = 256; + _isSlave = false; } I2S::~I2S() { @@ -87,6 +88,14 @@ bool I2S::setMCLK(pin_size_t pin) { return true; } +bool I2S::setSlave() { + if (_running) { + return false; + } + _isSlave = true; + return true; +} + bool I2S::setDATA(pin_size_t pin) { if (_running || (pin >= __GPIOCNT) || (_isOutput && _isInput)) { return false; @@ -136,12 +145,16 @@ bool I2S::setBuffers(size_t buffers, size_t bufferWords, int32_t silenceSample) bool I2S::setFrequency(int newFreq) { _freq = newFreq; if (_running) { - if (_MCLKenabled) { - int bitClk = _freq * _bps * (_isTDM ? (double)_tdmChannels : 2.0) /* channels */ * (_isInput && _isOutput ? 4.0 : 2.0) /* edges per clock */; - pio_sm_set_clkdiv_int_frac(_pio, _sm, clock_get_hz(clk_sys) / bitClk, 0); + if (_isSlave) { + pio_sm_set_clkdiv_int_frac(_pio, _sm, 1, 0); } else { - float bitClk = _freq * _bps * (_isTDM ? (double)_tdmChannels : 2.0) /* channels */ * (_isInput && _isOutput ? 4.0 : 2.0) /* edges per clock */; - pio_sm_set_clkdiv(_pio, _sm, (float)clock_get_hz(clk_sys) / bitClk); + if (_MCLKenabled) { + int bitClk = _freq * _bps * (_isTDM ? (double)_tdmChannels : 2.0) /* channels */ * (_isInput && _isOutput ? 4.0 : 2.0) /* edges per clock */; + pio_sm_set_clkdiv_int_frac(_pio, _sm, clock_get_hz(clk_sys) / bitClk, 0); + } else { + float bitClk = _freq * _bps * (_isTDM ? (double)_tdmChannels : 2.0) /* channels */ * (_isInput && _isOutput ? 4.0 : 2.0) /* edges per clock */; + pio_sm_set_clkdiv(_pio, _sm, (float)clock_get_hz(clk_sys) / bitClk); + } } } return true; @@ -255,7 +268,11 @@ bool I2S::begin() { _isHolding = 0; int off = 0; if (!_swapClocks) { - _i2s = new PIOProgram(_isOutput ? (_isInput ? (_isTDM ? &pio_tdm_inout_program : &pio_i2s_inout_program) : (_isTDM ? &pio_tdm_out_program : (_isLSBJ ? &pio_lsbj_out_program : &pio_i2s_out_program))) : &pio_i2s_in_program); + if (!_isSlave) { + _i2s = new PIOProgram(_isOutput ? (_isInput ? (_isTDM ? &pio_tdm_inout_program : &pio_i2s_inout_program) : (_isTDM ? &pio_tdm_out_program : (_isLSBJ ? &pio_lsbj_out_program : &pio_i2s_out_program))) : &pio_i2s_in_program); + } else { + _i2s = new PIOProgram(_bps > 16 ? &pio_i2s_out_slave_32_program : &pio_i2s_out_slave_16_program); + } } else { _i2s = new PIOProgram(_isOutput ? (_isInput ? (_isTDM ? &pio_tdm_inout_swap_program : &pio_i2s_inout_swap_program) : (_isTDM ? &pio_tdm_out_swap_program : (_isLSBJ ? &pio_lsbj_out_swap_program : &pio_i2s_out_swap_program))) : &pio_i2s_in_swap_program); } @@ -288,7 +305,11 @@ bool I2S::begin() { } else if (_isLSBJ) { pio_lsbj_out_program_init(_pio, _sm, off, _pinDOUT, _pinBCLK, _bps, _swapClocks); } else { - pio_i2s_out_program_init(_pio, _sm, off, _pinDOUT, _pinBCLK, _bps, _swapClocks); + if (_isSlave) { + pio_i2s_out_slave_program_init(_pio, _sm, off, _pinDOUT, _pinBCLK, _bps, _swapClocks); + } else { + pio_i2s_out_program_init(_pio, _sm, off, _pinDOUT, _pinBCLK, _bps, _swapClocks); + } } } else { pio_i2s_in_program_init(_pio, _sm, off, _pinDIN, _pinBCLK, _bps, _swapClocks); diff --git a/libraries/I2S/src/I2S.h b/libraries/I2S/src/I2S.h index dad45faca..1b184700d 100644 --- a/libraries/I2S/src/I2S.h +++ b/libraries/I2S/src/I2S.h @@ -34,6 +34,7 @@ class I2S : public Stream, public AudioOutputBase { bool setDOUT(pin_size_t pin); bool setDIN(pin_size_t pin); bool setMCLK(pin_size_t pin); + bool setSlave(); virtual bool setBitsPerSample(int bps) override; virtual bool setBuffers(size_t buffers, size_t bufferWords, int32_t silenceSample = 0) override; virtual bool setFrequency(int newFreq) override; @@ -155,6 +156,7 @@ class I2S : public Stream, public AudioOutputBase { bool _isOutput; bool _swapClocks; bool _MCLKenabled; + bool _isSlave; bool _running; diff --git a/libraries/I2S/src/pio_i2s.pio b/libraries/I2S/src/pio_i2s.pio index 38f71d2b3..954f3f23d 100644 --- a/libraries/I2S/src/pio_i2s.pio +++ b/libraries/I2S/src/pio_i2s.pio @@ -50,6 +50,61 @@ right1: out pins, 1 side 0b00 ; Last bit of right also has WCLK change ; Loop back to beginning... +.program pio_i2s_out_slave_16 +; IN 0 = bclk +; IN 1 = lrclk, also EXECCTRL_JMP_PIN +; OUT 0 = DOUT + +waitfirstbit: + wait 0 pin 0 + wait 1 pin 0 + jmp pin waitfirstbit ; LRCLK1 == right channel, so if high keep waiting + +.wrap_target + pull +sendleft: + wait 0 pin 0 ; Falling edge start sending + out pins, 1 ; Shift out a bit + wait 1 pin 0 ; Receiver should grab data here + jmp pin sendright ; LRCLK == 1 => right channel + jmp sendleft + +sendright: + wait 0 pin 0 ; Falling edge start sending + out pins, 1 ; Shift out a bit + wait 1 pin 0 ; Receiver should grab data here + jmp pin sendright ; LRCLK == 1 => right channel +.wrap + +.program pio_i2s_out_slave_32 +; IN 0 = bclk +; IN 1 = lrclk, also EXECCTRL_JMP_PIN +; OUT 0 = DOUT + +waitfirstbit: + wait 0 pin 0 + wait 1 pin 0 + jmp pin waitfirstbit ; LRCLK1 == right channel, so if high keep waiting + +.wrap_target + pull +sendleft: + wait 0 pin 0 ; Falling edge start sending + out pins, 1 ; Shift out a bit + wait 1 pin 0 ; Receiver should grab data here + jmp pin sendright ; LRCLK == 1 => right channel + jmp sendleft + + pull +sendright: + wait 0 pin 0 ; Falling edge start sending + out pins, 1 ; Shift out a bit + wait 1 pin 0 ; Receiver should grab data here + jmp pin sendright ; LRCLK == 1 => right channel +.wrap + + + .program pio_i2s_out_swap .side_set 2 ; 0 = wclk, 1=bclk @@ -61,7 +116,7 @@ right1: mov x, y side 0b10 left1: out pins, 1 side 0b00 - jmp x--, left1 side 0b10 + jmp x--, left1 side 0b10 out pins, 1 side 0b01 ; Last bit of left has WCLK change per I2S spec mov x, y side 0b11 @@ -308,9 +363,6 @@ static inline void pio_i2s_out_program_init(PIO pio, uint sm, uint offset, uint pio_sm_init(pio, sm, offset, &sm_config); - //uint pin_mask = (1u << data_pin) | (3u << clock_pin_base); - //pio_sm_set_pindirs_with_mask(pio, sm, pin_mask, pin_mask); - //pio_sm_set_pins(pio, sm, 0); // clear pins pio_sm_set_consecutive_pindirs(pio, sm, data_pin, 1, true); pio_sm_set_consecutive_pindirs(pio, sm, clock_pin_base, 2, true); pio_sm_set_set_pins(pio, sm, data_pin, 1); @@ -319,6 +371,36 @@ static inline void pio_i2s_out_program_init(PIO pio, uint sm, uint offset, uint pio_sm_exec(pio, sm, pio_encode_set(pio_y, bits - 2)); } + +static inline void pio_i2s_out_slave_program_init(PIO pio, uint sm, uint offset, uint data_pin, uint clock_pin_base, uint bits, bool swap) { + pio_gpio_init(pio, data_pin); + pio_gpio_init(pio, clock_pin_base); + pio_gpio_init(pio, clock_pin_base + 1); + + // 16-bits does a pull every L+R frame. 24/32 bits do a pull every L or R side + pio_sm_config sm_config = bits > 16 ? pio_i2s_out_slave_32_program_get_default_config(offset) : pio_i2s_out_slave_16_program_get_default_config(offset); //TBD swap ? pio_i2s_out_swap_program_get_default_config(offset) : pio_i2s_out_program_get_default_config(offset); + + sm_config_set_out_pins(&sm_config, data_pin, 1); + + sm_config_set_in_pins(&sm_config, clock_pin_base); + sm_config_set_in_pin_count(&sm_config, 2); // BLCK and LRCLK + sm_config_set_jmp_pin(&sm_config, clock_pin_base + 1); + + sm_config_set_out_shift(&sm_config, false, false, (bits <= 16) ? 2 * bits : bits); + sm_config_set_fifo_join(&sm_config, PIO_FIFO_JOIN_TX); + + pio_sm_init(pio, sm, offset, &sm_config); + + pio_sm_set_consecutive_pindirs(pio, sm, data_pin, 1, true); + pio_sm_set_consecutive_pindirs(pio, sm, clock_pin_base, 2, false); + pio_sm_set_out_pins(pio, sm, data_pin, 1); + pio_sm_set_in_pins(pio, sm, clock_pin_base); + + pio_sm_exec(pio, sm, pio_encode_set(pio_y, bits - 2)); +} + + + static inline void pio_tdm_out_program_init(PIO pio, uint sm, uint offset, uint data_pin, uint clock_pin_base, uint bits, bool swap, uint channels) { pio_gpio_init(pio, data_pin); pio_gpio_init(pio, clock_pin_base); diff --git a/libraries/I2S/src/pio_i2s.pio.h b/libraries/I2S/src/pio_i2s.pio.h index 0a9d2d33e..f909e9ee4 100644 --- a/libraries/I2S/src/pio_i2s.pio.h +++ b/libraries/I2S/src/pio_i2s.pio.h @@ -81,6 +81,95 @@ static inline pio_sm_config pio_i2s_out_program_get_default_config(uint offset) } #endif +// -------------------- // +// pio_i2s_out_slave_16 // +// -------------------- // + +#define pio_i2s_out_slave_16_wrap_target 3 +#define pio_i2s_out_slave_16_wrap 12 +#define pio_i2s_out_slave_16_pio_version 0 + +static const uint16_t pio_i2s_out_slave_16_program_instructions[] = { + 0x2020, // 0: wait 0 pin, 0 + 0x20a0, // 1: wait 1 pin, 0 + 0x00c0, // 2: jmp pin, 0 + // .wrap_target + 0x80a0, // 3: pull block + 0x2020, // 4: wait 0 pin, 0 + 0x6001, // 5: out pins, 1 + 0x20a0, // 6: wait 1 pin, 0 + 0x00c9, // 7: jmp pin, 9 + 0x0004, // 8: jmp 4 + 0x2020, // 9: wait 0 pin, 0 + 0x6001, // 10: out pins, 1 + 0x20a0, // 11: wait 1 pin, 0 + 0x00c9, // 12: jmp pin, 9 + // .wrap +}; + +#if !PICO_NO_HARDWARE +static const struct pio_program pio_i2s_out_slave_16_program = { + .instructions = pio_i2s_out_slave_16_program_instructions, + .length = 13, + .origin = -1, + .pio_version = pio_i2s_out_slave_16_pio_version, +#if PICO_PIO_VERSION > 0 + .used_gpio_ranges = 0x0 +#endif +}; + +static inline pio_sm_config pio_i2s_out_slave_16_program_get_default_config(uint offset) { + pio_sm_config c = pio_get_default_sm_config(); + sm_config_set_wrap(&c, offset + pio_i2s_out_slave_16_wrap_target, offset + pio_i2s_out_slave_16_wrap); + return c; +} +#endif + +// -------------------- // +// pio_i2s_out_slave_32 // +// -------------------- // + +#define pio_i2s_out_slave_32_wrap_target 3 +#define pio_i2s_out_slave_32_wrap 13 +#define pio_i2s_out_slave_32_pio_version 0 + +static const uint16_t pio_i2s_out_slave_32_program_instructions[] = { + 0x2020, // 0: wait 0 pin, 0 + 0x20a0, // 1: wait 1 pin, 0 + 0x00c0, // 2: jmp pin, 0 + // .wrap_target + 0x80a0, // 3: pull block + 0x2020, // 4: wait 0 pin, 0 + 0x6001, // 5: out pins, 1 + 0x20a0, // 6: wait 1 pin, 0 + 0x00ca, // 7: jmp pin, 10 + 0x0004, // 8: jmp 4 + 0x80a0, // 9: pull block + 0x2020, // 10: wait 0 pin, 0 + 0x6001, // 11: out pins, 1 + 0x20a0, // 12: wait 1 pin, 0 + 0x00ca, // 13: jmp pin, 10 + // .wrap +}; + +#if !PICO_NO_HARDWARE +static const struct pio_program pio_i2s_out_slave_32_program = { + .instructions = pio_i2s_out_slave_32_program_instructions, + .length = 14, + .origin = -1, + .pio_version = pio_i2s_out_slave_32_pio_version, +#if PICO_PIO_VERSION > 0 + .used_gpio_ranges = 0x0 +#endif +}; + +static inline pio_sm_config pio_i2s_out_slave_32_program_get_default_config(uint offset) { + pio_sm_config c = pio_get_default_sm_config(); + sm_config_set_wrap(&c, offset + pio_i2s_out_slave_32_wrap_target, offset + pio_i2s_out_slave_32_wrap); + return c; +} +#endif + // ---------------- // // pio_i2s_out_swap // // ---------------- // @@ -535,15 +624,31 @@ static inline void pio_i2s_out_program_init(PIO pio, uint sm, uint offset, uint sm_config_set_out_shift(&sm_config, false, true, (bits <= 16) ? 2 * bits : bits); sm_config_set_fifo_join(&sm_config, PIO_FIFO_JOIN_TX); pio_sm_init(pio, sm, offset, &sm_config); - //uint pin_mask = (1u << data_pin) | (3u << clock_pin_base); - //pio_sm_set_pindirs_with_mask(pio, sm, pin_mask, pin_mask); - //pio_sm_set_pins(pio, sm, 0); // clear pins pio_sm_set_consecutive_pindirs(pio, sm, data_pin, 1, true); pio_sm_set_consecutive_pindirs(pio, sm, clock_pin_base, 2, true); pio_sm_set_set_pins(pio, sm, data_pin, 1); pio_sm_set_set_pins(pio, sm, clock_pin_base, 2); pio_sm_exec(pio, sm, pio_encode_set(pio_y, bits - 2)); } +static inline void pio_i2s_out_slave_program_init(PIO pio, uint sm, uint offset, uint data_pin, uint clock_pin_base, uint bits, bool swap) { + pio_gpio_init(pio, data_pin); + pio_gpio_init(pio, clock_pin_base); + pio_gpio_init(pio, clock_pin_base + 1); + // 16-bits does a pull every L+R frame. 24/32 bits do a pull every L or R side + pio_sm_config sm_config = bits > 16 ? pio_i2s_out_slave_32_program_get_default_config(offset) : pio_i2s_out_slave_16_program_get_default_config(offset); //TBD swap ? pio_i2s_out_swap_program_get_default_config(offset) : pio_i2s_out_program_get_default_config(offset); + sm_config_set_out_pins(&sm_config, data_pin, 1); + sm_config_set_in_pins(&sm_config, clock_pin_base); + sm_config_set_in_pin_count(&sm_config, 2); // BLCK and LRCLK + sm_config_set_jmp_pin(&sm_config, clock_pin_base + 1); + sm_config_set_out_shift(&sm_config, false, false, (bits <= 16) ? 2 * bits : bits); + sm_config_set_fifo_join(&sm_config, PIO_FIFO_JOIN_TX); + pio_sm_init(pio, sm, offset, &sm_config); + pio_sm_set_consecutive_pindirs(pio, sm, data_pin, 1, true); + pio_sm_set_consecutive_pindirs(pio, sm, clock_pin_base, 2, false); + pio_sm_set_out_pins(pio, sm, data_pin, 1); + pio_sm_set_in_pins(pio, sm, clock_pin_base); + pio_sm_exec(pio, sm, pio_encode_set(pio_y, bits - 2)); +} static inline void pio_tdm_out_program_init(PIO pio, uint sm, uint offset, uint data_pin, uint clock_pin_base, uint bits, bool swap, uint channels) { pio_gpio_init(pio, data_pin); pio_gpio_init(pio, clock_pin_base); From 230f82d72034ff822579f6936442d9f086c3ca64 Mon Sep 17 00:00:00 2001 From: "Earle F. Philhower, III" Date: Thu, 11 Sep 2025 08:18:15 -0700 Subject: [PATCH 2/2] On DMA underflow, ensure 0s continue to be sent Don't block on a PULL, just send the X register out (will be set to 0 by the setup code). --- libraries/I2S/src/pio_i2s.pio | 8 ++++---- libraries/I2S/src/pio_i2s.pio.h | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/libraries/I2S/src/pio_i2s.pio b/libraries/I2S/src/pio_i2s.pio index 954f3f23d..f0e82ee73 100644 --- a/libraries/I2S/src/pio_i2s.pio +++ b/libraries/I2S/src/pio_i2s.pio @@ -61,7 +61,7 @@ waitfirstbit: jmp pin waitfirstbit ; LRCLK1 == right channel, so if high keep waiting .wrap_target - pull + pull noblock sendleft: wait 0 pin 0 ; Falling edge start sending out pins, 1 ; Shift out a bit @@ -87,7 +87,7 @@ waitfirstbit: jmp pin waitfirstbit ; LRCLK1 == right channel, so if high keep waiting .wrap_target - pull + pull noblock sendleft: wait 0 pin 0 ; Falling edge start sending out pins, 1 ; Shift out a bit @@ -95,7 +95,7 @@ sendleft: jmp pin sendright ; LRCLK == 1 => right channel jmp sendleft - pull + pull noblock sendright: wait 0 pin 0 ; Falling edge start sending out pins, 1 ; Shift out a bit @@ -396,7 +396,7 @@ static inline void pio_i2s_out_slave_program_init(PIO pio, uint sm, uint offset, pio_sm_set_out_pins(pio, sm, data_pin, 1); pio_sm_set_in_pins(pio, sm, clock_pin_base); - pio_sm_exec(pio, sm, pio_encode_set(pio_y, bits - 2)); + pio_sm_exec(pio, sm, pio_encode_set(pio_x, 0)); } diff --git a/libraries/I2S/src/pio_i2s.pio.h b/libraries/I2S/src/pio_i2s.pio.h index f909e9ee4..a1a750ffe 100644 --- a/libraries/I2S/src/pio_i2s.pio.h +++ b/libraries/I2S/src/pio_i2s.pio.h @@ -94,7 +94,7 @@ static const uint16_t pio_i2s_out_slave_16_program_instructions[] = { 0x20a0, // 1: wait 1 pin, 0 0x00c0, // 2: jmp pin, 0 // .wrap_target - 0x80a0, // 3: pull block + 0x8080, // 3: pull noblock 0x2020, // 4: wait 0 pin, 0 0x6001, // 5: out pins, 1 0x20a0, // 6: wait 1 pin, 0 @@ -138,13 +138,13 @@ static const uint16_t pio_i2s_out_slave_32_program_instructions[] = { 0x20a0, // 1: wait 1 pin, 0 0x00c0, // 2: jmp pin, 0 // .wrap_target - 0x80a0, // 3: pull block + 0x8080, // 3: pull noblock 0x2020, // 4: wait 0 pin, 0 0x6001, // 5: out pins, 1 0x20a0, // 6: wait 1 pin, 0 0x00ca, // 7: jmp pin, 10 0x0004, // 8: jmp 4 - 0x80a0, // 9: pull block + 0x8080, // 9: pull noblock 0x2020, // 10: wait 0 pin, 0 0x6001, // 11: out pins, 1 0x20a0, // 12: wait 1 pin, 0 @@ -647,7 +647,7 @@ static inline void pio_i2s_out_slave_program_init(PIO pio, uint sm, uint offset, pio_sm_set_consecutive_pindirs(pio, sm, clock_pin_base, 2, false); pio_sm_set_out_pins(pio, sm, data_pin, 1); pio_sm_set_in_pins(pio, sm, clock_pin_base); - pio_sm_exec(pio, sm, pio_encode_set(pio_y, bits - 2)); + pio_sm_exec(pio, sm, pio_encode_set(pio_x, 0)); } static inline void pio_tdm_out_program_init(PIO pio, uint sm, uint offset, uint data_pin, uint clock_pin_base, uint bits, bool swap, uint channels) { pio_gpio_init(pio, data_pin);