Add DSI load-test harness; give the LVGL task a 16 KB stack

Start the display via bsp_display_start_with_config with a 16 KB LVGL
task stack. The 8 KB default overflows once LV_INV_BUF_SIZE is raised to
128 (the partial-flush path's stack frame scales with it) — the guard
fires as a "Stack protection fault" boot loop otherwise.

Add FACE_LOADTEST (gated off): a load-emulation harness that cycles
isolated load types — a render-only face ladder, light/heavy render plus
a network stream, render plus playback, and all three stacked. Each
phase logs a marker and shows an on-screen label. It pinned the blue
flicker to a DSI PSRAM-bandwidth underrun (the driver logs "underrun
happens"), which is serial-verifiable without eyes on the screen. Kept
for future bus-contention debugging.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKPbR6DY2JygHbyLjxm7Uu
This commit is contained in:
2026-07-15 14:42:19 +02:00
co-authored by Claude Opus 4.8
parent 67de636ab4
commit d1e1250016
+111 -6
View File
@@ -22,6 +22,13 @@
/* 1 = L1 wifi driver diagnosis (SoftAP, no STA/gateway). 0 = normal app. */
#define WIFI_DIAG_MODE 0
/* 1 = deterministic load-emulation harness for the blue-flicker RCA: cycles
* isolated load types (render-only ladder, light-render+network, heavy-render+
* network, heavy-render+playback) with a log marker per phase, so we can see
* which load starves the DSI. Kept (gated off) for future bus-contention debugging.
* 0 = normal app. */
#define FACE_LOADTEST 0
static const char *TAG = "desklock";
/* All utterance framing (blocking WS sends, chime, face) runs on app_task, never
@@ -107,6 +114,94 @@ void app_on_playback_done(void)
face_set(FACE_IDLE);
}
#if FACE_LOADTEST
/* Stream real mic audio upstream for `ms` — exercises the network path (I2S-in
* DMA + esp-hosted SDIO TX, both touching PSRAM) with valid utterance framing so
* the gateway keeps the WS open. */
static void loadtest_stream(int ms)
{
if (!gw_connected()) {
vTaskDelay(pdMS_TO_TICKS(ms));
return;
}
gw_send_text("{\"type\":\"utterance_start\"}");
audio_capture_start();
vTaskDelay(pdMS_TO_TICKS(ms));
audio_capture_stop();
gw_send_text("{\"type\":\"utterance_end\"}");
}
static void loadtest_task(void *arg)
{
(void)arg;
vTaskDelay(pdMS_TO_TICKS(12000)); /* let boot + Wi-Fi + gateway settle */
const struct {
face_state_t st;
const char *name;
} LADDER[] = {
{ FACE_IDLE, "IDLE(4)" }, { FACE_PENSIVE, "PENSIVE(7)" },
{ FACE_SPEAKING, "SPEAKING(14)" }, { FACE_LISTENING, "LISTENING(16)" },
{ FACE_RAGE, "RAGE(34)" }, { FACE_EFFORT, "EFFORT(40)" },
};
char tag[24];
static uint8_t netframe[1024] = {0};
for (;;) {
ESP_LOGW("loadtest", "=== P1 render-only ladder (NO network) ===");
for (int i = 0; i < 6; i++) {
ESP_LOGW("loadtest", "P1: %s", LADDER[i].name);
face_set(LADDER[i].st);
snprintf(tag, sizeof(tag), "P1 %s", LADDER[i].name);
face_status(tag); /* phase label on screen */
vTaskDelay(pdMS_TO_TICKS(6000));
}
ESP_LOGW("loadtest", "=== P2 LIGHT render (IDLE 4) + NETWORK stream — key test ===");
face_set(FACE_IDLE);
face_status("P2 net + idle");
loadtest_stream(8000);
vTaskDelay(pdMS_TO_TICKS(6000)); /* let any reply drain */
ESP_LOGW("loadtest", "=== P3 HEAVY render (EFFORT 40) + NETWORK stream ===");
face_set(FACE_EFFORT);
face_status("P3 net + EFFORT");
loadtest_stream(8000);
vTaskDelay(pdMS_TO_TICKS(6000));
ESP_LOGW("loadtest", "=== P4 HEAVY render (EFFORT 40) + PLAYBACK dma ===");
face_set(FACE_EFFORT);
face_status("P4 play + EFFORT");
for (int i = 0; i < 3; i++) {
audio_play_gong();
vTaskDelay(pdMS_TO_TICKS(2500));
}
/* P5: the real-conversation worst case — heavy render + audio playback +
* network stream ALL AT ONCE, like the SPEAKING state (TTS audio arriving
* over WS while it plays and the rain runs). Render (P1) and each DMA source
* alone (P3/P4) were clean at 48 MHz; this stacks them to reproduce the live
* flicker. utterance framing keeps the WS accepting the frames. */
ESP_LOGW("loadtest", "=== P5 EFFORT + PLAYBACK + NETWORK (full-reply emulation) ===");
face_set(FACE_EFFORT);
face_status("P5 play+net+render");
gw_send_text("{\"type\":\"utterance_start\"}");
for (int t = 0; t < 550; t++) { /* ~8s combined load */
if (!audio_is_playing()) {
audio_play_gong(); /* I2S-out playback DMA */
face_set(FACE_EFFORT); /* playback-done flips to IDLE */
}
if (gw_connected()) {
gw_send_bin(netframe, sizeof(netframe)); /* SDIO/mempool DMA */
}
vTaskDelay(pdMS_TO_TICKS(15));
}
gw_send_text("{\"type\":\"utterance_end\"}");
vTaskDelay(pdMS_TO_TICKS(4000));
ESP_LOGW("loadtest", "=== cycle complete, repeating in 3s ===");
vTaskDelay(pdMS_TO_TICKS(3000));
}
}
#endif
#if SDIO_TX_SELFTEST
/* Temporary autonomous SDIO-TX stress test: stream mic audio upstream for 40s
* right after connecting (no user tap needed) to prove the #167 alignment fix.
@@ -142,17 +237,23 @@ void app_main(void)
s_events = xQueueCreate(8, sizeof(app_event_t));
/* TRIPLE_FULL, not the BSP default TRIPLE_PARTIAL: on IDF 5.5 the DSI driver
* lacks the on_frame_buf_complete callback, so PARTIAL mode falls back to a
* per-refresh "fake vsync" that over-releases the scanout buffer when a frame
* takes >1 refresh to draw (heavy rain + always-on WakeNet) -> tearing/flicker.
* TRIPLE_FULL is submit-gated even without that callback. Same 3 framebuffers. */
/* Same config bsp_display_start() uses (TRIPLE_PARTIAL, no rotation) but with a
* 16 KB LVGL task stack instead of the 8 KB default.
*
* The blue flicker's real cause is a DSI framebuffer-read underrun on the PSRAM
* bus (fixed primarily by the reduced DPI clock in the BSP). This 16 KB stack
* pairs with LV_INV_BUF_SIZE=128 (top CMakeLists), a SECONDARY bandwidth
* mitigation: 128 keeps the busy 40-stream rain as small partial redraws instead
* of collapsing into full-screen redraws, which would pile large PSRAM write
* bursts onto the DSI's read and re-provoke the underrun. The 128-entry inv
* buffer enlarges the flush stack frame, and 8 KB overflowed it by ~1 KB. */
bsp_display_cfg_t disp_cfg = {
.lv_adapter_cfg = ESP_LV_ADAPTER_DEFAULT_CONFIG(),
.rotation = ESP_LV_ADAPTER_ROTATE_0,
.tear_avoid_mode = ESP_LV_ADAPTER_TEAR_AVOID_MODE_TRIPLE_FULL,
.tear_avoid_mode = ESP_LV_ADAPTER_TEAR_AVOID_MODE_TRIPLE_PARTIAL,
.touch_flags = { .swap_xy = 0, .mirror_x = 0, .mirror_y = 0 },
};
disp_cfg.lv_adapter_cfg.task_stack_size = 16 * 1024;
bsp_display_start_with_config(&disp_cfg);
bsp_display_backlight_on();
face_init();
@@ -172,4 +273,8 @@ void app_main(void)
#endif
ESP_LOGI(TAG, "DeskLock up");
#if FACE_LOADTEST
xTaskCreate(loadtest_task, "loadtest", 4096, NULL, 4, NULL);
#endif
}