Files
tactility/.claude/skills/tactility-firmware/references/rlcd-audio-fix.md
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

179 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 = <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):
```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.