From 1b9226326aafddc0b411cd36f9ace24dfa7b3e92 Mon Sep 17 00:00:00 2001 From: Thomas Faour Date: Sat, 18 Jul 2026 14:06:54 -0400 Subject: [PATCH] Fix hardware-verified bugs: EPD stack overflow and busy-wait spin Two crashes found flashing to real hardware: - epd_display_stream's 4KB SPI chunk buffer was a stack local, but the default main task stack (3584 bytes) is smaller than that alone -- Guru Meditation stack protection fault. Made it static instead, and bumped CONFIG_ESP_MAIN_TASK_STACK_SIZE to 8192 for headroom in the rest of the boot call chain (provisioning -> QR render -> eventually the HTTP fetch cycle all run in this one task). - epd_wait_busy() polled with a 1ms vTaskDelay, which rounds down to 0 FreeRTOS ticks at the default 100Hz tick rate -- so it never actually blocked, tight-spinning the CPU for the panel's real refresh time (15-30+s for a full-color pass) and starving the idle task long enough to trip the 5s task watchdog. Bumped to 20ms, safely >=1 tick regardless of tick rate. Also updates the EPD pin defaults to the board's actual wiring (CLK=20 MOSI=19 CS=18 DC=9 RST=10 BUSY=11), confirmed working on hardware. --- firmware/components/epd7in3e/Kconfig | 22 ++++++++++++++-------- firmware/components/epd7in3e/epd7in3e.c | 15 +++++++++++++-- firmware/sdkconfig.defaults | 7 +++++++ 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/firmware/components/epd7in3e/Kconfig b/firmware/components/epd7in3e/Kconfig index e5785c5..fbaa66c 100644 --- a/firmware/components/epd7in3e/Kconfig +++ b/firmware/components/epd7in3e/Kconfig @@ -2,11 +2,17 @@ menu "E-Paper Display (epd7in3e) Configuration" config EPD_PIN_CLK int "SPI CLK (SCLK) GPIO" - default 18 + default 20 help - Not physically wired yet -- these are sensible ESP32-C6 devkit - defaults (avoiding strapping pins 4/5/8/9/15 and the USB-JTAG - pins 12/13). Override to match your actual wiring. + Defaults match the wiring on the reference build: CLK=20, + MOSI(DIN)=19, CS=18, DC=9, RST=10, BUSY=11. Override to match + your own wiring if different. + + Note: DC uses GPIO9, one of the ESP32-C6's strapping pins + (sampled at reset to help select boot mode). It's only sampled + during power-on/reset, so it's safe to drive as a normal output + once the app is running -- but if flashing/boot ever misbehaves, + check whether the EPD is pulling this line low during reset. config EPD_PIN_MOSI int "SPI MOSI (DIN) GPIO" @@ -14,19 +20,19 @@ menu "E-Paper Display (epd7in3e) Configuration" config EPD_PIN_CS int "SPI CS GPIO" - default 20 + default 18 config EPD_PIN_DC int "Data/Command GPIO" - default 21 + default 9 config EPD_PIN_RST int "Reset GPIO" - default 22 + default 10 config EPD_PIN_BUSY int "Busy GPIO" - default 23 + default 11 config EPD_SPI_CLOCK_HZ int "SPI clock speed (Hz)" diff --git a/firmware/components/epd7in3e/epd7in3e.c b/firmware/components/epd7in3e/epd7in3e.c index b309c5b..9a4f846 100644 --- a/firmware/components/epd7in3e/epd7in3e.c +++ b/firmware/components/epd7in3e/epd7in3e.c @@ -34,8 +34,14 @@ static void epd_delay_ms(uint32_t ms) /* BUSY: LOW = busy, HIGH = idle. */ static void epd_wait_busy(void) { + /* A full refresh on this panel can hold BUSY low for 15-30+ seconds. + * A 1ms poll delay rounds down to 0 FreeRTOS ticks at the default + * 100Hz tick rate, so vTaskDelay() never actually blocks -- confirmed + * on hardware as a tight busy-spin that starves the idle task long + * enough to trip the 5s task watchdog. 20ms is safely >= 1 tick + * regardless of tick rate and still imperceptibly responsive here. */ while (gpio_get_level((gpio_num_t)CONFIG_EPD_PIN_BUSY) == 0) { - epd_delay_ms(1); + epd_delay_ms(20); } } @@ -207,7 +213,12 @@ esp_err_t epd_display_stream(epd_read_fn_t read_fn, void *ctx) gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_DC, 1); gpio_set_level((gpio_num_t)CONFIG_EPD_PIN_CS, 0); - uint8_t chunk[EPD_SPI_CHUNK_SIZE]; + /* Static rather than a stack local: the default main task stack + * (CONFIG_ESP_MAIN_TASK_STACK_SIZE, 3584 bytes) is smaller than this + * 4KB chunk buffer alone, let alone with the rest of the call chain -- + * confirmed on hardware as a stack protection fault/crash. Not + * reentrant, but this driver only ever runs from one task at a time. */ + static uint8_t chunk[EPD_SPI_CHUNK_SIZE]; size_t total = 0; size_t n; esp_err_t err = ESP_OK; diff --git a/firmware/sdkconfig.defaults b/firmware/sdkconfig.defaults index c22b07b..c9fee64 100644 --- a/firmware/sdkconfig.defaults +++ b/firmware/sdkconfig.defaults @@ -7,3 +7,10 @@ CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" + +# The whole boot flow (provisioning, QR rendering, and eventually the HTTP +# fetch cycle) runs in the single default "main" task. The 3584-byte +# default already crashed with a stack protection fault once (a 4KB SPI +# chunk buffer that's since been moved off the stack) -- bumping this gives +# headroom for the rest of that call chain plus the upcoming HTTP client. +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192