- 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
11 KiB
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.cppusesGPIO_NUM_5as 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=5collides with display DC=5. Whenesp32_i2sdriver starts and callsi2s_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=0x1Fdelay 10ms0x00=0x00delay 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=0x00is WRONG for RLCD 12.288MHz MCLK, use0x48.
ES7210 MIC @0x40
Ported from audio_util.py::ES7210.init() / ESPHome es7210.cpp coefs for 12.288MHz/16kHz:
- Reset:
0x00=0xFF20ms0x00=0x3220ms0x01=0x3F(clock off during config) - HPF:
0x09=0x30, 0x0A=0x30, 0x23=0x2A, 0x22=0x0A, 0x20=0x0A, 0x21=0x2A - Mode:
0x08 &= ~0x01, analog power0x40=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 bias0x4B=0xFF, 0x4C=0xFF - Enable ADC12 clocks
0x01 &= ~0x0B, bias on0x4B=0x00, MIC1/2 SELMIC+gain 30dB0x43=0x10|0x0A, 0x44=0x10|0x0A - Low power
0x47=0x08,0x48=0x08,0x49=0x08,0x4A=0x08, DLL off0x06=0x04, SM0x00=0x7120ms0x00=0x41100ms
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 MCPplay_tone/play_wav/play_mp3works. Exposedextern C void waveshare_rlcd_speaker_amp_set(bool)for future gating inMcpSystem.cpp::begin_audio/end_audio(begin=HIGH, end=LOW). - Future: MCP should call amp helper around
i2s_controller_write/i2s_controller_set_configlike 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 i2s0at 1102esp32_i2c_master: start i2c0GPIO13/14WaveshareRLCD: ES8311 DAC @0x18 initialized1466msWaveshareRLCD: ES7210 mic ADC @0x40 initialized1631msSpeaker amp GPIO46 enabledHal: ST7305 started2400ms (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.cppused defaultI2S_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:
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
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);set_rx_tdm_configalso reduced from512/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)setsmclk_multiple=256always → 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:Also ES8311 as I2S slave tracks external BCLK/WS, so exact divider not critical if slave.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;
Symptom 4: Pops and Repetitions
- Pop at start: ES8311 reclock hook
waveshare_rlcd_on_i2s_rate_changere-wrote0x06/0x07/0x08dividers 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.mp3is 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, removingtaskYIELD(), 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()loggedFile lock function not set!everylistDirectory()call.TactilitydoeslistDir /sdcardevery second → 1 Hz warning flood on UART ISR → I2S task jitter → small noises.- Also
listDir start/stopINFO logs andesp32_i2s: Configuring I2S pinsINFO on every play → UART contention. - Fixes:
Verification: after fix serial 12s log shows only WiFi scan, no
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_IFile lockflood. Audible noise floor dropped.
Symptom 6: VoiceRecorder Buggy (Mic-like Low Pitch)
- Working MP
audio_util.pyrecord usesI2S.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:File saved as 16k mono WAV (header 1 ch, byterate 32000) with correct pitch.// 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);
Checklist When Adding Audio to New Board
- Check display pins (DC, CS, RST) vs I2S pins in
board_config.pyMP working reference — never reuse DC as BCLK. - Verify MCLK frequency: RLCD 12.288MHz, Hosyond 6.144MHz — affects
0x02divider (0x48 vs 0x00). For ESP-IDF I2S STD, setmclk_multiple = MCLK / sample_rate(768 for 16k@12.288M). - Probe both codecs
0x18+0x40withhas_device_at_address, non-fatal. - Amp active level from
board_config.pyaudio_amp_active_level— 1=HIGH for RLCD/46, 0=LOW for Hosyond/1. - 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. - 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.
- Logging: Make file-lock warning once-only,
listDirectoryDEBUG, I2S pin logs DEBUG — UART flood causes audio jitter. - Mp3Player task: stack 6144, prio 6, no
taskYIELD()between MP3 frames — decode loop mustn't starve I2S DMA. - ES8311 reclock: Avoid I2C writes during I2S start for common rate (16k). Boot init already perfect — skip hook for 16k to avoid pop.