Board
Waveshare ESP32-S3-Touch-AMOLED-1.64
Hardware Description
sh8601-driven display
16MB flash + 8MB PSRAM
IDE Name
idf.py
Operating System
Linux
Description
When using the recommended PSRAM canvas pattern (buff_spiram = true + trans_size for DMA staging), large buffer sizes cause intermittent SPI failures:
E (7379) lcd_panel.io.spi: panel_io_spi_tx_color(395): spi transmit (queue) color failed
E (7379) sh8601: panel_sh8601_draw_bitmap(280): send color data failed
Root Cause
In esp_lvgl_port_disp.c, the lvgl_port_flush_io_ready_callback ISR calls lv_disp_flush_ready() after every SPI transaction completion:
static bool lvgl_port_flush_io_ready_callback(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx)
{
// ...
lv_disp_flush_ready(disp_drv); // Called on every chunk!
if (disp_ctx->trans_size && disp_ctx->trans_sem) {
xSemaphoreGiveFromISR(disp_ctx->trans_sem, &taskAwake);
}
// ...
}
When using trans_size chunking, a single LVGL flush is split into multiple SPI transactions. Calling lv_disp_flush_ready() after the first chunk signals LVGL that the draw buffer is free. With double buffering enabled, LVGL may immediately begin rendering to buf2 and initiate a new flush while buf1's remaining chunks are still being transmitted.
This causes SPI queue contention or overflow, especially noticeable with larger PSRAM buffers that require many chunks.
To Reproduce
const lvgl_port_display_cfg_t disp_cfg = {
.io_handle = io_handle,
.panel_handle = panel_handle,
.buffer_size = LCD_H_RES * LCD_V_RES / 4, // Large PSRAM buffer
.trans_size = LCD_H_RES * 10, // Small DMA transfer buffer
.double_buffer = true,
.hres = 280,
.vres = 456,
.flags = {
.buff_spiram = true,
.buff_dma = false,
.swap_bytes = true,
},
};
Trigger any UI update (button press, etc.) - fails with SPI transmit error.
Reducing buffer_size to ~15KB or less works around the issue by reducing the number of chunks per flush.
Expected Behavior
lv_disp_flush_ready() should only be called after the final chunk of a flush operation completes, not after every chunk.
Suggested Fix
Track remaining chunks and only signal flush completion on the last one:
static bool lvgl_port_flush_io_ready_callback(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx)
{
BaseType_t taskAwake = pdFALSE;
lv_disp_drv_t *disp_drv = (lv_disp_drv_t *)user_ctx;
lvgl_port_display_ctx_t *disp_ctx = disp_drv->user_data;
if (disp_ctx->trans_size && disp_ctx->trans_sem) {
xSemaphoreGiveFromISR(disp_ctx->trans_sem, &taskAwake);
// Don't call lv_disp_flush_ready here - let flush_callback call it after last chunk
} else {
// Non-chunked path: safe to signal immediately
lv_disp_flush_ready(disp_drv);
}
return (taskAwake == pdTRUE);
}
And in lvgl_port_flush_callback, after the chunking loop completes:
} else {
// ... existing chunking loop ...
// Signal flush complete after all chunks sent
lv_disp_flush_ready(drv);
}
Environment
- ESP-IDF: v5.5.2
- esp_lvgl_port: v2.x
- Hardware: ESP32-S3 with QSPI display (SH8601), 8MB PSRAM
- LVGL: v9.x
Workaround
Keep buffer_size small enough that chunking results in only 2-3 transactions (~10-15KB for typical display widths).
Sketch
Create a new ESP-IDF project with these files:
<details>
<summary><b>main/idf_component.yml</b></summary>
dependencies:
espressif/esp_lcd_sh8601: "*"
lvgl/lvgl: "^9"
espressif/esp_lvgl_port: "^2"
</details>
<details>
<summary><b>sdkconfig.defaults</b></summary>
CONFIG_IDF_TARGET="esp32s3"
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_OCT=y
CONFIG_SPIRAM_SPEED_80M=y
CONFIG_SPIRAM_USE_MALLOC=y
CONFIG_LV_FONT_MONTSERRAT_20=y
CONFIG_LV_COLOR_DEPTH_16=y
</details>
<details>
<summary><b>main/main.c</b></summary>
/**
* Minimal reproduction: esp_lvgl_port PSRAM chunking bug
* Hardware: ESP32-S3 + SH8601 QSPI display + 8MB PSRAM
*
* Watch serial output - error appears on timer-triggered redraw
*/
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "driver/spi_master.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_sh8601.h"
#include "lvgl.h"
#include "esp_lvgl_port.h"
static const char *TAG = "repro";
#define LCD_H_RES 280
#define LCD_V_RES 456
// BUG: Large buffer causes SPI failure
#define BUFFER_SIZE (LCD_H_RES * LCD_V_RES / 4)
// WORKAROUND: Small buffer works
// #define BUFFER_SIZE (LCD_H_RES * 25)
#define TRANS_SIZE (LCD_H_RES * 10)
static esp_lcd_panel_io_handle_t io_handle;
static esp_lcd_panel_handle_t panel_handle;
static lv_obj_t *btn;
static bool btn_state = false;
static const sh8601_lcd_init_cmd_t lcd_init_cmds[] = {
{0x11, (uint8_t []){0x00}, 0, 80},
{0x29, (uint8_t []){0x00}, 0, 10},
{0x51, (uint8_t []){0xFF}, 1, 0},
};
static void timer_cb(lv_timer_t *t) {
btn_state = !btn_state;
ESP_LOGI(TAG, "Timer fired -> color change triggers bug");
lv_obj_set_style_bg_color(btn,
lv_color_hex(btn_state ? 0x00AA00 : 0x444444), 0);
}
void app_main(void) {
// SPI bus
spi_bus_config_t bus_cfg = SH8601_PANEL_BUS_QSPI_CONFIG(10,11,12,13,14, LCD_H_RES*LCD_V_RES*2);
spi_bus_initialize(SPI2_HOST, &bus_cfg, SPI_DMA_CH_AUTO);
// Panel IO
esp_lcd_panel_io_spi_config_t io_cfg = SH8601_PANEL_IO_QSPI_CONFIG(9, NULL, NULL);
esp_lcd_new_panel_io_spi(SPI2_HOST, &io_cfg, &io_handle);
// Panel
sh8601_vendor_config_t vendor_cfg = {
.init_cmds = lcd_init_cmds, .init_cmds_size = 3,
.flags = { .use_qspi_interface = 1 },
};
esp_lcd_panel_dev_config_t panel_cfg = {
.reset_gpio_num = 21, .bits_per_pixel = 16, .vendor_config = &vendor_cfg,
};
esp_lcd_new_panel_sh8601(io_handle, &panel_cfg, &panel_handle);
esp_lcd_panel_reset(panel_handle);
esp_lcd_panel_init(panel_handle);
esp_lcd_panel_set_gap(panel_handle, 20, 0);
// LVGL
lvgl_port_cfg_t lvgl_cfg = ESP_LVGL_PORT_INIT_CONFIG();
lvgl_port_init(&lvgl_cfg);
ESP_LOGI(TAG, "buffer=%d trans=%d chunks=%d", BUFFER_SIZE, TRANS_SIZE, BUFFER_SIZE/TRANS_SIZE);
lvgl_port_display_cfg_t disp_cfg = {
.io_handle = io_handle, .panel_handle = panel_handle,
.buffer_size = BUFFER_SIZE, .trans_size = TRANS_SIZE,
.double_buffer = true, .hres = LCD_H_RES, .vres = LCD_V_RES,
.flags = { .buff_spiram = true, .buff_dma = false, .swap_bytes = true },
};
lvgl_port_add_disp(&disp_cfg);
// UI
lvgl_port_lock(0);
btn = lv_obj_create(lv_screen_active());
lv_obj_set_size(btn, 160, 160);
lv_obj_center(btn);
lv_obj_set_style_bg_color(btn, lv_color_hex(0x444444), 0);
lv_obj_set_style_radius(btn, 80, 0);
// Timer triggers redraw every 2 seconds
lv_timer_create(timer_cb, 2000, NULL);
lvgl_port_unlock();
ESP_LOGI(TAG, "Waiting for timer to trigger bug...");
while(1) vTaskDelay(1000);
}
</details>
Build and flash, then tap the button to trigger the error.
Other Steps to Reproduce
I have checked existing issues, README.md and ESP32 Forum
Board
Waveshare ESP32-S3-Touch-AMOLED-1.64
Hardware Description
sh8601-driven display
16MB flash + 8MB PSRAM
IDE Name
idf.py
Operating System
Linux
Description
When using the recommended PSRAM canvas pattern (
buff_spiram = true+trans_sizefor DMA staging), large buffer sizes cause intermittent SPI failures:Root Cause
In
esp_lvgl_port_disp.c, thelvgl_port_flush_io_ready_callbackISR callslv_disp_flush_ready()after every SPI transaction completion:When using trans_size chunking, a single LVGL flush is split into multiple SPI transactions. Calling
lv_disp_flush_ready()after the first chunk signals LVGL that the draw buffer is free. With double buffering enabled, LVGL may immediately begin rendering to buf2 and initiate a new flush while buf1's remaining chunks are still being transmitted.This causes SPI queue contention or overflow, especially noticeable with larger PSRAM buffers that require many chunks.
To Reproduce
Trigger any UI update (button press, etc.) - fails with SPI transmit error.
Reducing
buffer_sizeto ~15KB or less works around the issue by reducing the number of chunks per flush.Expected Behavior
lv_disp_flush_ready()should only be called after the final chunk of a flush operation completes, not after every chunk.Suggested Fix
Track remaining chunks and only signal flush completion on the last one:
And in
lvgl_port_flush_callback, after the chunking loop completes:Environment
Workaround
Keep
buffer_sizesmall enough that chunking results in only 2-3 transactions (~10-15KB for typical display widths).Sketch
Other Steps to Reproduce
I have checked existing issues, README.md and ESP32 Forum