# Waveshare ESP32-S3-RLCD-4.2 Initialization Race & Freeze Audit (2026-07-14) ## Overview RLCD board (`waveshare-esp32-s3-rlcd`) suffered random freeze "stuck on weird state" requiring hard reset. Serial log triage via pyserial (15s capture) showed no Guru Meditation, only warnings. Root causes found in 3 layers: I2C driver CONFLICT, ES8311 codec blocking boot, ST7305 reset timing. ## File Map - `Devices/waveshare-esp32-s3-rlcd/waveshare,esp32-s3-rlcd.dts` — devicetree, defines i2c0, spi0, sdmmc0, i2s0, gpio0 - `Devices/waveshare-esp32-s3-rlcd/Source/Configuration.cpp` — `initBoot()` powers amp GPIO46 + ES8311 init, `createDevices()` display + TwoButtonControl(GPIO18 KEY, GPIO0 BOOT) - `Drivers/ST7305/Source/St7305Display.h/.cpp` — SPI display driver, reset pin GPIO41, CS 40, DC 5, SPI2 10MHz, `full_refresh=true`, buffer 120k (400x300), `monochrome=true`, `trans_size=0` - `Tactility/Source/service/displayidle/DisplayIdle.cpp` — idle timer + wake logic (50d86de4 fix: run for all displays) ## Initialization Sequence (from serial log) 1. Bootloader 2nd stage → PSRAM 8MB 120MHz detected 2. `kernel: init` → `module: start waveshare-esp32-s3-rlcd` → `module: start platform-esp32` 3. `driver: add esp32_gpio, esp32_i2c, esp32_i2c_master, esp32_i2s, esp32_sdmmc, ...` — both legacy and NG I2C drivers registered 4. `device: start i2c0` → `esp32_i2c_master: start i2c0` (after fix) vs previously `esp32_i2c: start i2c0` 5. `E (907) i2c: CONFLICT! driver_ng is not allowed to be used with this old driver` — IDF 5.x forbids mixing old `i2c_driver_install` and new `i2c_new_master_bus` on same peripheral. Warning appears even when only one instance uses I2C0, because platform-esp32 registers both driver types globally. Benign after migration but indicates legacy driver still loaded. 6. `gpio: GPIO[13] InputEn:1 OutputEn:1 OpenDrain:1` / GPIO14 — I2C pins 7. `esp32_sdmmc_fs: Mounted /sdcard` — SD 1-bit, CLK 38, CMD 21, D0 39, D3 17 8. `SystemEvents: BootInitHalBegin` → `gpio46 HIGH` amp enable 9. `WaveshareRLCD: ES8311 codec initialized` (after 3770ac89 fix) vs previously missing or failed 10. `Devices: Registering ST7305` → `Hal: ST7305 starting` → `ST7305: Starting SPI panel IO creation` → `Hal: ST7305 started` (2160ms with delays vs 1828ms before) 11. `Tactility: DEBUG: Listing /sdcard` → `NTP: Restoring last known time` 12. `Bluetooth: Auto-enabling BLE` → `BLE host task started` + `GAP procedure...` + `startAdvertising: set_fields failed rc=4` (benign) 13. `ServiceRegistration: Adding Gps/wifi/Development/EspNow/WebServer` → `WebServerService: AP started SSID Tactility-5C7D 192.168.4.1` + `HttpServer Started port 80` 14. `McpSystem: Mono TCP Video stream server started 8081` + Color 8083 15. `LVGL: Starting LVGL task` → `Started ST7305` + `ButtonControl: Start` 16. `DisplayIdle: Monochrome/RLCD display detected: idle timer will run but auto-backlight off is disabled` 17. `Boot: Setup display` / `Boot: No backlight` → `Tactility: Registering internal apps` → `Loader: Start by id Launcher` → `GuiService: Showing Launcher` 18. `WifiBootSplashInit: Auto-enabling WiFi` → `WifiService: Enabled` → `STA_START` — auto-connects if SD has credentials ## Root Causes & Fixes (3770ac89 + 50d86de4) ### 1. I2C CONFLICT — legacy vs NG driver **Before:** ```dts #include i2c0 { compatible = "espressif,esp32-i2c"; port = ; ... } ``` - Platform registers both `esp32_i2c` (old) and `esp32_i2c_master` (new) drivers - When i2c0 used old driver, IDF warns CONFLICT if any component uses `driver_ng` (i2c_master internal) — happens because `esp32_i2c_master` driver also calls NG API - Could stall I2C bus if codec init fails **After (3770ac89):** ```dts #include i2c0 { compatible = "espressif,esp32-i2c-master"; port = ; clock-frequency = <400000>; pin-sda = <&gpio0 13>; pin-scl = <&gpio0 14>; } ``` - Now uses master driver, `esp32_i2c_master: start i2c0` in log, ES8311 initializes - Warning `E i2c: CONFLICT!` still appears because platform still registers legacy driver type globally — upstream should remove legacy registration if all boards migrated, but not fatal after this fix - Commit 3770ac89: 0x2b79c0 32% free ### 2. ES8311 Codec Init Blocking Boot **Before:** ```cpp auto* i2c_bus = device_find_by_name("i2c0"); if (i2c_bus) { error_t e = initCodec(i2c_bus); if (e!=NONE) LOG_E(...); } ``` - If ES8311 at 0x18 not responding (no pullups, power not stable), `i2c_controller_write_register_array` blocks 1s timeout per transaction, could lock bus → freeze **After:** ```cpp vTaskDelay(100ms); // after GPIO46 amp HIGH, power stabilize if (i2c_controller_has_device_at_address(bus, 0x18, 200ms)==NONE) { initCodec(bus); // non-fatal, logs success } else { LOG_W("ES8311 not found at 0x18, skipping"); } ``` - Probe first, non-fatal, 100ms delay for amp power, 200ms probe timeout - Prevents bus lock if codec missing ### 3. ST7305 Reset Race — SPI Too Early **Before:** ```cpp esp_lcd_panel_reset(panelHandle); esp_lcd_panel_init(panelHandle); // no delay ``` **After (Drivers/ST7305/Source/St7305Display.cpp):** ```cpp esp_lcd_panel_reset(panelHandle); vTaskDelay(150ms); // ST7305 datasheet needs >120ms after reset esp_lcd_panel_init(panelHandle); vTaskDelay(50ms); ``` - Prevents freeze on fast boot where SPI transaction sent before panel ready, leaving display stuck on garbage / weird state - Observed `Hal: ST7305 started` time 1828ms → 2160ms (+330ms) ### 4. DisplayIdle Wake Logic Gated by supportsBacklight (50d86de4) **Before (after MCP flash fix):** ```cpp if (supportsBacklight) { if (!timeoutEnabled) restore else { if (inactive>=timeout) activate else if (dimmed) wake } } ``` RLCD `supportsBacklight==false` → entire idle/wake state machine skipped → once `displayDimmed=true`, never wakes → appears locked. **After:** Timeout logic runs for ALL displays, only `setBacklightDuty()` guarded by `supportsBacklight && display != nullptr`. See `references/display-idle.md`. ## Pin Conflicts to Watch - GPIO9: ES3C28P BAT_ADC (ADC1_CH8) vs RLCD I2S BCLK pin 5 (not 9, but ES3C uses GPIO9 free, check DTS `pin-bclk` before reusing) - I2C0 SDA 13 / SCL 14 — shared with nothing else on RLCD, but must be `i2c-master` compatible - SPI0: CS 40, MOSI 12, SCLK 11, DC 5, RST 41 — exclusive - SDMMC: CLK 38, CMD 21, D0 39, D3 17, 1-bit ## Screenshot Lock Contention + Permanent Freeze Related (fixed 6f095081) See `references/mcp-system.md` + `references/display-idle.md` Screenshot Lock Contention + 50d86de4/6f095081 sections. Same root: ST7305 full_refresh heavy SPI copy holds LVGL lock 100ms, `handleApiScreenshot` 100ms timeout too short → `could not acquire LVGL lock` error. Also "screen is still showing the freezed image" even after 3770ac89 init fixes — cause was screensaver heavy animation auto-enter + video stream 5ms hog. Fixes 6f095081 (build 0x2b79e0, same size, flashed): - Screenshot API 100→500ms (WebServerService) + error msg "within 500ms" - ScreenshotTask 50→300ms - Video stream 5ms→30ms streaming, 50ms→100ms idle with comment "RLCD ST7305 SPI is slow and shared" - DisplayIdle: for !supportsBacklight (RLCD) don't auto-enter screensaver, only wake — prevents heavy bouncing balls full_refresh that freezes - Power cycle required to clear ST7305 RAM (RTS reset doesn't clear controller RAM, frozen image persists until USB unplug 3s) - Verification 14 checks PASS hermes-verify-jw_55oxm.py ## Verification - Serial log via pyserial 15-20s: check `CONFLICT` gone or benign, `ES8311 codec initialized`, `Hal: ST7305 started` ~2160ms, `Monochrome/RLCD detected`, no `Guru Meditation`, AP `Tactility-5C7D` up - Ad-hoc script 16 checks PASS (DTS master, probe, delays, wake logic, MCP guard, build artifact) - Build: `waveshare-esp32-s3-rlcd 0x2b79c0 32% free` flashed `/dev/cu.usbmodem101` ## Recommendations 1. For RLCD, default `displayTimeout = 0 (Never)` or `ScreensaverType::None` to avoid heavy bouncing balls full_refresh (120k pixels * SPI 10MHz = ~96ms per frame blocks UI) 2. Upstream: remove legacy `esp32_i2c` driver registration from platform-esp32 if all boards migrated to master, to eliminate CONFLICT warning entirely 3. Consider `trans_size` or partial refresh for ST7305 to reduce LVGL hold time, or lower SPI queue depth from 10 to 3