Skip to content

Shared I2S AudioReactive

piggei edited this page Oct 3, 2026 · 2 revisions

The Waveshare ESP32-S3 RGB Matrix uses a shared physical I2S clock bus for the ES7210 microphone path and the ES8311 speaker path. This page records the problem identified while implementing v0.2.0, the hardware-qualified clock-routing solution, and the rules that future changes must preserve.

Shared hardware

  • MCLK: GPIO12
  • BCLK: GPIO43
  • LRCK/WS: GPIO38
  • ES7210 microphone data into ESP32-S3: GPIO39
  • ESP32-S3 speaker data to ES8311: GPIO21
  • ES8311 I2C: GPIO47/GPIO48, address 0x18
  • PA enable: GPIO11

AudioReactive owns I2S0 RX and, on the tested configuration, is the physical clock master. Buzzer uses I2S1 for TX.

What went wrong

The first Audio backend used I2S1 as a second master at 48 kHz. On this board that conflicts with the clock pins already used by the microphone path and can make AudioReactive report a saturated or otherwise invalid signal at idle.

The first shared-slave redesign removed the second master but still allowed the legacy I2S pin helper to configure the physical BCLK/LRCK pads. That could disturb the existing I2S0 master routes. A later GPIO-matrix-only tap avoided pad reconfiguration but hardware testing showed that I2S1 could remain unclocked unless the GPIO input buffers were explicitly enabled.

Qualified clock-routing implementation

The current implementation uses the following rules when AudioReactive owns the complete shared bus:

  1. AudioReactive remains the only physical MCLK/BCLK/LRCK master and keeps I2S0 RX.
  2. Buzzer installs I2S1 as TX slave.
  3. In shared mode i2s_set_pin() configures only DOUT GPIO21.
  4. GPIO43/GPIO38 keep their I2S0 output routes and additionally have their input buffers enabled.
  5. The GPIO matrix taps BCLK/LRCK internally into the I2S1 slave inputs.
  6. Physical LRCK activity is checked before I2S1/ES8311 bring-up.
  7. Buzzer does not call i2s_set_clk() in shared mode and never controls/uninstalls I2S0.
  8. The global I2C bus is not reinitialized in shared mode; only ES8311 registers are addressed.
  9. Shared teardown disconnects only the internal I2S1 clock taps.

The tested shared stream uses 22.05 kHz and the AudioReactive slot width (32 bit normally, 16 bit when I2S_USE_16BIT_SAMPLES is built).

Hardware qualification

On the tested Waveshare + WLED 17.0.0-devV5 setup, dev-b012 qualified the clock-routing/ownership part of the design with Buzzer configured as Audio (I2S) and AudioReactive enabled:

  • cold boot succeeds;
  • AudioReactive remains stable in silence;
  • Buzzer audio playback works;
  • Buzzer does not intentionally reconfigure the I2S0 RX path.

dev-b014 then increased TX buffering to 8x256 without changing the clock topology. Real-hardware testing showed smooth speaker playback and smooth AudioReactive bars, with more than 20,000 captured writes, err=0, short=0, late=0, and maxGap about 13 ms against about 92.9 ms of DMA coverage. Runtime AudioReactive OFF -> ON -> OFF transitions also passed. The topology and current buffering/scheduling behavior are therefore the v0.2.0 stable non-regression baseline.

Mode transitions

If all shared pins are owned by AudioReactive, Buzzer requests shared mode. If only some are owned, Buzzer treats the bus as being in transition and stays stopped. If none are owned, Buzzer may use standalone I2S1 master mode at 48 kHz / 16 bit.

Pin ownership is only ownership evidence. The shared backend additionally requires physical LRCK activity before it becomes ready.

Diagnostics

A ready shared backend reports a line similar to:

shared-I2S | TX I2S1 slave | rate 22050 Hz | ... | clock tap GPIO-matrix + input-enable | RX I2S0 untouched

/json/state also reports audioMode, audioRate, and audioBits. The qualified /json/info telemetry reports DMA 8x256, DMA coverage, write/error/short-write counters, maximum write duration, maximum producer gap, late-write count, core distribution, and minimum free producer-task stack. v0.2.0 keeps this runtime unchanged.

Useful initialization errors include:

  • shared LRCK inactive
  • shared clock input enable failed
  • shared clock/DMA priming timeout

Future-change guardrails

Do not introduce a change that:

  • controls or uninstalls I2S0 from the Buzzer module;
  • routes MCLK/BCLK/LRCK from Buzzer while shared mode is active;
  • calls i2s_set_clk() for shared I2S1;
  • reinitializes the global I2C bus while AudioReactive is using it;
  • treats PinManager ownership as proof that the external clocks are already running;
  • switches to standalone master while AudioReactive owns only part of the shared bus.

If future WLED/AudioReactive versions change sample rate or slot width, the shared TX format must be updated or negotiated before Buzzer declares itself ready.

Clone this wiki locally