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
This commit is contained in:
Adolfo Reyna
2026-07-17 09:51:08 -04:00
parent e7fb5fb38f
commit 12035f2f39
22 changed files with 2362 additions and 0 deletions
@@ -0,0 +1,178 @@
# 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.