# RLCD Audio Freeze Fix — GPIO5 Conflict + Correct Pinmap (2026-07-11) ## Symptom When `i2s0` node enabled in `waveshare-esp32-s3-rlcd.dts`, screen freezes / white / stuck on frozen image. Background logic continues (WiFi AP up, buttons generate events, serial alive). No Guru Meditation. ## Root Cause **GPIO5 Mux Conflict** - `Devices/waveshare-esp32-s3-rlcd/Source/devices/Display.cpp` uses `GPIO_NUM_5` as ST7305 DC pin (SPI command/data). - Old DTS used Hosyond board's I2S map: `bclk=5, ws=7, data-out=8, data-in=6, mclk=4`. - `bclk=5` collides with display DC=5. When `esp32_i2s` driver starts and calls `i2s_new_channel` / `i2s_channel_init_std_mode`, it muxes GPIO5 to I2S BCLK output. Display DC line corrupted → SPI commands misinterpreted → display frozen. ## Correct RLCD Pinmap (from working MicroPython `board_config.py`) Source: `/Users/adolforeyna/Projects/MicroPython/test1/Screen/lib/board_config.py` lines 150-181 WAVESHARE_RLCD profile, verified working with mics + playback. ``` MCLK = 16 @ 12.288MHz (via I2S peripheral MCLK output, or PWM in CircuitPython) BCLK = 9 WS = 45 TX = 8 (I2S data-out to ES8311 DAC / speaker) RX = 10 (I2S data-in from ES7210 mic array) AMP = 46 active HIGH (1=enable) I2C = SDA 13, SCL 14 Codec DAC = ES8311 @ 0x18 speaker Codec ADC = ES7210 @ 0x40 4-ch mic array (stereo 16kHz in standard I2S mode, not TDM in MP) ``` Hosyond map (WRONG for RLCD): ``` MCLK 4 @6.144MHz, BCLK 5, WS 7, TX 8, RX 6, AMP 1 active LOW ``` ### DTS Fix ```dts i2s0 { compatible = "espressif,esp32-i2s"; port = ; pin-bclk = <&gpio0 9 GPIO_FLAG_NONE>; pin-ws = <&gpio0 45 GPIO_FLAG_NONE>; pin-data-out = <&gpio0 8 GPIO_FLAG_NONE>; pin-data-in = <&gpio0 10 GPIO_FLAG_NONE>; pin-mclk = <&gpio0 16 GPIO_FLAG_NONE>; }; ``` ## Codec Init Sequences (from `lib/audio_util.py` working) ### ES8311 DAC @0x18 (speaker) Exact MicroPython sequence (16kHz SR, MCLK 12.288MHz): - Soft reset: `0x00=0x1F` delay 10ms `0x00=0x00` delay 10ms - Clock: `0x01=0x3F, 0x02=0x48 (pre_div=3 for 12.288MHz, not 0x00), 0x03=0x10, 0x04=0x10, 0x05=0x00, 0x06=0x03, 0x07=0x00, 0x08=0xFF` - Format: `0x09=0x0C, 0x0A=0x0C` (16-bit standard I2S) - Power: `0x0D=0x01, 0x0E=0x02, 0x12=0x00, 0x13=0x10, 0x1C=0x6A, 0x37=0x08, 0x32=0xBF (0dB), 0x31=0x00 unmute, 0x00=0x80 power-on` - Important: StickS3 minimal sequence `0x02=0x00` is WRONG for RLCD 12.288MHz MCLK, use `0x48`. ### ES7210 MIC @0x40 Ported from `audio_util.py::ES7210.init()` / ESPHome `es7210.cpp` coefs for 12.288MHz/16kHz: - Reset: `0x00=0xFF` 20ms `0x00=0x32` 20ms `0x01=0x3F` (clock off during config) - HPF: `0x09=0x30, 0x0A=0x30, 0x23=0x2A, 0x22=0x0A, 0x20=0x0A, 0x21=0x2A` - Mode: `0x08 &= ~0x01`, analog power `0x40=0xC3, 0x41=0x70, 0x42=0x70` - I2S fmt: `0x11=0x60 (16-bit I2S, TDM off), 0x12=0x00` - Clock: `0x02=0xC3 (doubler+div3), 0x07=0x20 (OSR32), 0x04=0x03, 0x05=0x00 (lrck div 768)` - Clear MIC select bits `0x43..0x46 &= ~0x10`, power down bias `0x4B=0xFF, 0x4C=0xFF` - Enable ADC12 clocks `0x01 &= ~0x0B`, bias on `0x4B=0x00`, MIC1/2 SELMIC+gain 30dB `0x43=0x10|0x0A, 0x44=0x10|0x0A` - Low power `0x47=0x08,0x48=0x08,0x49=0x08,0x4A=0x08`, DLL off `0x06=0x04`, SM `0x00=0x71` 20ms `0x00=0x41` 100ms Implementation note: Use `i2c_controller_write_register_array` per register pair (not bulk) for ES7210 because sequence needs read-modify-write for some regs (0x08, 0x01, 0x43, 0x44). Probe with `has_device_at_address` 200ms first, non-fatal log if absent. ## Amp GPIO46 Handling - RLCD: Active HIGH (Hosyond GPIO1 active LOW) - Working MP toggles per playback: `on_val = 1 if active_level==1 else 0` - Previous Tactility code: HIGH at boot (pop/noise) - Fix 2026-07-11: Keep LOW during codec init (avoid pop/power dip), then HIGH after `audio_ok` (any codec probed) so MCP `play_tone/play_wav/play_mp3` works. Exposed `extern C void waveshare_rlcd_speaker_amp_set(bool)` for future gating in `McpSystem.cpp::begin_audio/end_audio` (begin=HIGH, end=LOW). - Future: MCP should call amp helper around `i2s_controller_write` / `i2s_controller_set_config` like MP does. ## Build & Flash Verification Hermes env pitfall: `PYTHONPATH` contains 3.11 pydantic_core → breaks IDF python 3.9/3.10. Must unset. Working wrapper (used for this fix): ```bash /bin/bash --noprofile -c ' unset PYTHONPATH; unset PYTHONHOME; unset VIRTUAL_ENV export IDF_PATH=/Users/adolforeyna/esp/esp-idf export IDF_PYTHON_ENV_PATH=/Users/adolforeyna/.espressif/python_env/idf5.3_py3.10_env export PATH=$IDF_PYTHON_ENV_PATH/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin eval $($IDF_PATH/tools/idf_tools.py export --format=shell 2>/dev/null) export ESP_IDF_VERSION=5.3 cd firmware idf.py build idf.py -p /dev/cu.usbmodem101 flash ' ``` Boot log after fix (serial 115200): - `esp32_i2s: start i2s0` at 1102 - `esp32_i2c_master: start i2c0` GPIO13/14 - `WaveshareRLCD: ES8311 DAC @0x18 initialized` 1466ms - `WaveshareRLCD: ES7210 mic ADC @0x40 initialized` 1631ms - `Speaker amp GPIO46 enabled` - `Hal: ST7305 started` 2400ms (after I2S, display survives) - No Guru Meditation, buttons continue Binary: `Tactility.bin 0x2b8030 32% free` (16MB flash) ## Follow-up Bugs (MP3 Play/Pops/Robot/VoiceRecorder) — 2026-07-12 Session ### Symptom 2: MP3 fails `i2s_alloc_dma_desc: allocate DMA buffer failed` → `ESP_ERR_NO_MEM` - Log: `Mp3Player: Starting MP3 playback ... size: 31652` → `esp32_i2s: Configuring I2S pins: MCLK=16 BCLK=9 WS=45 ...` → `i2s_common: i2s_alloc_dma_desc(436): allocate DMA buffer failed` → `Failed to set config: 9` → `reset i2s0`. - Root: `Platforms/platform-esp32/source/drivers/esp32_i2s.cpp` used default `I2S_CHANNEL_DEFAULT_CONFIG` → `dma_desc=6 dma_frame=240` → ~5.7KB/ch ×2 TX+RX = 11.4KB internal DMA RAM. ST7305 full_refresh + PSRAM + SDMMC 1-bit + WiFi consumes internal DMA heap → OOM. - Fix: ```cpp i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(port, MASTER); chan_cfg.dma_desc_num = 6; chan_cfg.dma_frame_num = 160; // ~3.8KB/ch, was 11.4KB i2s_new_channel(&chan_cfg, &tx, &rx); ``` Plus RX fallback: if RX init fails, free RX handle and keep TX-only for playback (Mp3Player doesn't need mic). Previously driver created both even for playback-only, doubling usage. TDM path `set_rx_tdm_config` also reduced from `512/8` → `120/4`. ### Symptom 3: Robot / Metallic Sound - Cause: Wrong MCLK multiple. RLCD uses fixed 12.288MHz MCLK (PWM in MP). ESP-IDF default `I2S_STD_CLK_DEFAULT_CONFIG(rate)` sets `mclk_multiple=256` always → for 16kHz expects MCLK=4.096MHz, but board provides 12.288MHz → WS generated from wrong MCLK → pitch shift / robot. - Fix in `get_esp32_std_config`: ```cpp if (rate==16000) mclk_mult=I2S_MCLK_MULTIPLE_768; // 16k*768=12.288MHz exact else if (rate==32000) 384; // 32k*384=12.288M else if (rate==48000) 256; // 48k*256=12.288M else if (rate==24000) 512; etc. // 44.1k family stays 256 → 11.2896MHz, slight pitch shift acceptable (no 12.288 multiple) std_cfg->clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(rate); std_cfg->clk_cfg.mclk_multiple = mclk_mult; ``` Also ES8311 as I2S slave tracks external BCLK/WS, so exact divider not critical if slave. ### Symptom 4: Pops and Repetitions - Pop at start: ES8311 reclock hook `waveshare_rlcd_on_i2s_rate_change` re-wrote `0x06/0x07/0x08` dividers during I2S reconfigure while amp ON → glitch. Plus boot amp ON with random DMA. - Fix: For 16kHz (majority of RLCD files, e.g. `faith-mysterious-garden/page001.mp3` is 16k mono 31652 bytes) skip reclock entirely — boot init already programs perfect 16k config. Hook now: ```cpp if (sample_rate==16000 || sample_rate==0) return ERROR_NONE; // no I2C write → no pop // else only ensure unmuted 0x31=0, 0x32=0xBF, 0x00=0x80 ``` - Repetition: DMA too small 4x120 = 480 frames = 30ms @16k mono. Mp3Player decodes 1152 samples/frame (~72ms) + does `taskYIELD()` → gap → driver repeats last buffer (auto_clear false). Fixed by increasing DMA to 6x160, removing `taskYIELD()`, increasing Mp3Player task stack 4096→6144 prio 5→6, and reducing UART log spam that stole CPU. ### Symptom 5: Log Flood Causing Jitter - `TactilityCore/Source/file/File.cpp:getLock()` logged `File lock function not set!` every `listDirectory()` call. `Tactility` does `listDir /sdcard` every second → 1 Hz warning flood on UART ISR → I2S task jitter → small noises. - Also `listDir start/stop` INFO logs and `esp32_i2s: Configuring I2S pins` INFO on every play → UART contention. - Fixes: ```cpp static bool warned=false; if(!warned){ LOGGER.warn(...); warned=true; } // once-only LOGGER.debug("listDir start") // was INFO LOG_D(TAG, "Configuring I2S pins...") // was LOG_I ``` Verification: after fix serial 12s log shows only WiFi scan, no `File lock` flood. Audible noise floor dropped. ### Symptom 6: VoiceRecorder Buggy (Mic-like Low Pitch) - Working MP `audio_util.py` record uses `I2S.STEREO` (2 mics) even for mono file, then extracts left channel: ```python i2s = I2S(1, sck=..., ws=..., sd=..., mode=I2S.RX, rate=16000, bits=16, format=I2S.STEREO) # stereo->mono: for i in 0..bytes_read step 4: mono[j:j+2]=buffer[i:i+2] ``` - VoiceRecorder old code requested `channel_right = I2S_CHANNEL_NONE` (MONO slot mode). ES7210 still outputs 2 slots → driver expects 1 slot but codec sends 2 → channel misalignment → low pitch / mic-like. - Fix: Request STEREO for RX (`channel_right=1`) and downmix in loop in-place backward copy to avoid overwrite: ```c // read AUDIO_BUF_SIZE stereo (4096), mono_bytes = bytes_read/2 for (int src=bytes_read-4, dst=mono_bytes-2; src>=0; src-=4, dst-=2) buf[dst]=buf[src], buf[dst+1]=buf[src+1]; fwrite(buf,1,mono_bytes,f); ``` File saved as 16k mono WAV (header 1 ch, byterate 32000) with correct pitch. ## Checklist When Adding Audio to New Board 1. Check display pins (DC, CS, RST) vs I2S pins in `board_config.py` MP working reference — never reuse DC as BCLK. 2. Verify MCLK frequency: RLCD 12.288MHz, Hosyond 6.144MHz — affects `0x02` divider (0x48 vs 0x00). For ESP-IDF I2S STD, set `mclk_multiple = MCLK / sample_rate` (768 for 16k@12.288M). 3. Probe both codecs `0x18` + `0x40` with `has_device_at_address`, non-fatal. 4. Amp active level from `board_config.py` `audio_amp_active_level` — 1=HIGH for RLCD/46, 0=LOW for Hosyond/1. 5. If ES7210 present, recording is **stereo** standard I2S (2 mics) per working MP `I2S.STEREO` → downmix left to mono file. Don't request mono slot mode. 6. DMA budget: ST7305 full_refresh + PSRAM + SDMMC consumes internal DMA. Default 6x240 (11.4KB) OOMs. Use 6x160 (~7.6KB) + RX fallback to TX-only on playback OOM. For TDM path also reduce from 8x512. 7. Logging: Make file-lock warning once-only, `listDirectory` DEBUG, I2S pin logs DEBUG — UART flood causes audio jitter. 8. Mp3Player task: stack 6144, prio 6, no `taskYIELD()` between MP3 frames — decode loop mustn't starve I2S DMA. 9. ES8311 reclock: Avoid I2C writes during I2S start for common rate (16k). Boot init already perfect — skip hook for 16k to avoid pop.