Files
tactility/.claude/skills/tactility-firmware/references/rlcd-audio-fix.md
T
Adolfo Reyna 12035f2f39 chore: move firmware skills in-repo + AGENTS.md
- Migrates tactility-firmware and tactility-bluetooth from
  ~/.hermes/skills/ into .claude/skills/ for repo co-location
- Adds AGENTS.md with local workstation context, board table,
  board-direct build rule, Hermes PYTHONPATH pitfall, common
  Tactility pitfalls, and in-repo skill index

Refs: personal Gitea mirror
2026-07-17 09:51:08 -04:00

11 KiB
Raw Blame History

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

i2s0 {
  compatible = "espressif,esp32-i2s";
  port = <I2S_NUM_0>;
  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):

/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 failedESP_ERR_NO_MEM

  • Log: Mp3Player: Starting MP3 playback ... size: 31652esp32_i2s: Configuring I2S pins: MCLK=16 BCLK=9 WS=45 ...i2s_common: i2s_alloc_dma_desc(436): allocate DMA buffer failedFailed to set config: 9reset i2s0.
  • Root: Platforms/platform-esp32/source/drivers/esp32_i2s.cpp used default I2S_CHANNEL_DEFAULT_CONFIGdma_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:
    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/8120/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:
    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:
    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:
    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:
    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:
    // 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.