known·good

Notes / m5stackcardputeres8311esp32arduinoaudio

M5Stack Cardputer-ADV plays a loud constant whine over all audio (ES8311, Arduino M5Unified)

· by shaun, written up with Claude

On a Cardputer-ADV with custom Arduino firmware, every sound (test tone or MP3) had a loud constant whine on top. The speaker ran a mono I2S frame, so BCLK was half what M5Unified's fixed ES8311 clock divider expects. Setting spk_cfg.stereo = true fixed it.

Tested onM5Stack Cardputer-ADV (ESP32-S3, ES8311 codec); Arduino via PlatformIO, platform espressif32@6.9.0 (ESP-IDF 4.4); M5Cardputer + M5Unified; ESP8266Audio 1.9.7

Symptoms #

Custom Arduino firmware on an M5Stack Cardputer-ADV (an MP3 player sketch built on M5Cardputer / M5Unified and ESP8266Audio) produced a loud, constant whine over all audio:

  • the same with a generated test tone and with MP3 playback,
  • also when running on battery,
  • at every sample rate tried.

The pitch of the actual audio was correct; only the added tone was wrong. The stock UIFlow firmware plays through the same ES8311 codec cleanly, which showed it was a configuration problem, not hardware.

Root cause #

A clock mismatch between the I2S frame and the codec's clock divider:

  • The speaker was configured mono, so the I2S frame was 16 bits per sample period: BCLK = fs × 16 = 768 kHz at 48 kHz.
  • M5Unified initialises the ES8311 with a fixed clock divider (register 0x02 = 0x18) that assumes BCLK = 1.536 MHz (a 32-bit stereo frame).
  • With BCLK at half the expected rate, the DAC master clock ended up about 2× off, which produced the constant tone. The audio pitch stayed right because LRCK still locked the sample rate.

The fix below confirms the mechanism. Register-level changes did not help: reprogramming the ES8311 oversampling (OSR) and bias settings, and even a full ESP-ADF-style codec init, left the whine in place. It took about a dozen reflashes to find.

Fix #

Configure the speaker as stereo before starting it. That gives a 32-bit frame and BCLK = 1.536 MHz, which matches the divider:

cppauto spk_cfg = M5Cardputer.Speaker.config();
spk_cfg.stereo = true;          // 32-bit I2S frame -> BCLK 1.536 MHz at 48 kHz
M5Cardputer.Speaker.config(spk_cfg);
M5Cardputer.Speaker.begin();

spk_cfg.stereo = true was the whole fix; no codec registers needed changing. The config() read-modify-write lines are the usual M5Unified pattern; the line that matters is setting stereo before Speaker.begin().