From afc18bd1afa89e19405cff2a2703a7db308ed802 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Thu, 6 Aug 2026 10:02:41 -0500 Subject: [PATCH 01/13] fix(esp32-p4): Skip MIPI-DSI DCS reads during panel init (watchdog hang) On the Waveshare ESP32-P4-NANO/ETH the 10.1" ILI9881C panel does not answer MIPI-DSI DCS reads (bus turn-around), and the ESP-IDF DSI HAL busy-waits on the read with no timeout. The optional panel-ID read in Ili9881::initialize() therefore hung panel bring-up and tripped the task watchdog (IDLE0 starvation) instead of showing the display. Stop passing a read_command to the panel driver in both BSPs so it skips the ID read and initializes with writes only. Reported on hardware (esp32-p4-nano + 10.1" ILI9881C). Builds for esp32p4 on ESP-IDF 6.0. Co-Authored-By: Claude Opus 4.8 (1M context) --- components/esp32-p4-eth/src/video.cpp | 6 +++++- components/esp32-p4-nano/src/video.cpp | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/components/esp32-p4-eth/src/video.cpp b/components/esp32-p4-eth/src/video.cpp index 569b986f3..d57681476 100644 --- a/components/esp32-p4-eth/src/video.cpp +++ b/components/esp32-p4-eth/src/video.cpp @@ -132,7 +132,11 @@ bool Esp32P4Eth::initialize_lcd() { espp::display_drivers::Config display_config{ .panel_io = nullptr, .write_command = std::bind_front(&Esp32P4Eth::dsi_write_command, this), - .read_command = std::bind_front(&Esp32P4Eth::dsi_read_command, this), + // NOTE: the Waveshare ESP32-P4 panels do not reliably support MIPI-DSI DCS + // reads (bus turn-around); the ESP-IDF HAL busy-waits on the read, which + // hangs panel init and trips the task watchdog. Do not provide a + // read_command so the driver skips the optional panel-ID read. + .read_command = nullptr, .lcd_send_lines = nullptr, .reset_pin = GPIO_NUM_NC, .data_command_pin = GPIO_NUM_NC, diff --git a/components/esp32-p4-nano/src/video.cpp b/components/esp32-p4-nano/src/video.cpp index 1a8c94de6..d711b8ddf 100644 --- a/components/esp32-p4-nano/src/video.cpp +++ b/components/esp32-p4-nano/src/video.cpp @@ -132,7 +132,11 @@ bool Esp32P4Nano::initialize_lcd() { espp::display_drivers::Config display_config{ .panel_io = nullptr, .write_command = std::bind_front(&Esp32P4Nano::dsi_write_command, this), - .read_command = std::bind_front(&Esp32P4Nano::dsi_read_command, this), + // NOTE: the Waveshare ESP32-P4 panels do not reliably support MIPI-DSI DCS + // reads (bus turn-around); the ESP-IDF HAL busy-waits on the read, which + // hangs panel init and trips the task watchdog. Do not provide a + // read_command so the driver skips the optional panel-ID read. + .read_command = nullptr, .lcd_send_lines = nullptr, .reset_pin = GPIO_NUM_NC, .data_command_pin = GPIO_NUM_NC, From 2315d4b7316e5bed98f8f62d9d0c9f66b64a0689 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Thu, 6 Aug 2026 10:36:02 -0500 Subject: [PATCH 02/13] fix(esp32-p4): Per-panel MIPI-DSI lane bit rate (1500 for ILI9881C) Co-Authored-By: Claude Opus 4.8 (1M context) --- components/esp32-p4-eth/include/esp32-p4-eth.hpp | 10 +++++----- components/esp32-p4-eth/src/video.cpp | 8 +++++--- components/esp32-p4-nano/include/esp32-p4-nano.hpp | 10 +++++----- components/esp32-p4-nano/src/video.cpp | 8 +++++--- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/components/esp32-p4-eth/include/esp32-p4-eth.hpp b/components/esp32-p4-eth/include/esp32-p4-eth.hpp index 359cb962a..da69a5efe 100644 --- a/components/esp32-p4-eth/include/esp32-p4-eth.hpp +++ b/components/esp32-p4-eth/include/esp32-p4-eth.hpp @@ -476,17 +476,18 @@ class Esp32P4Eth : public BaseComponent { size_t width; size_t height; int dpi_clock_freq_mhz; + int lane_bitrate_mbps; gpio_num_t backlight_io; gpio_num_t reset_io; int hsync_pulse_width, hsync_back_porch, hsync_front_porch; int vsync_pulse_width, vsync_back_porch, vsync_front_porch; }; // EK79007 7" 1024x600 (reset over DSI, no backlight GPIO) - static constexpr PanelParams EK79007_PARAMS{1024, 600, 52, GPIO_NUM_NC, GPIO_NUM_NC, 10, - 160, 160, 1, 23, 12}; + static constexpr PanelParams EK79007_PARAMS{1024, 600, 52, 900, GPIO_NUM_NC, GPIO_NUM_NC, + 10, 160, 160, 1, 23, 12}; // ILI9881C 10.1" 800x1280 (hsync: pulse=40, back=140, front=40; reset over DSI) - static constexpr PanelParams ILI9881C_PARAMS{800, 1280, 80, GPIO_NUM_NC, GPIO_NUM_NC, 40, - 140, 40, 4, 16, 16}; + static constexpr PanelParams ILI9881C_PARAMS{800, 1280, 80, 1500, GPIO_NUM_NC, GPIO_NUM_NC, + 40, 140, 40, 4, 16, 16}; #if CONFIG_ESP32_P4_ETH_DISPLAY_EK79007 static constexpr DisplayController default_controller_ = DisplayController::EK79007; @@ -508,7 +509,6 @@ class Esp32P4Eth : public BaseComponent { static constexpr int mipi_dsi_lanes = 2; // DSI HS lane bit rate (Mbps/lane). 900 Mbps matches Espressif's official // panel bus configs; using the wrong rate mis-packs the pixel bits on the link. - static constexpr int mipi_dsi_lane_bitrate_mbps = 900; static constexpr int mipi_dsi_phy_ldo_channel = 3; // on-chip LDO_VO3 -> VDD_MIPI_DPHY static constexpr int mipi_dsi_phy_ldo_voltage_mv = 2500; diff --git a/components/esp32-p4-eth/src/video.cpp b/components/esp32-p4-eth/src/video.cpp index d57681476..7d3b60e9b 100644 --- a/components/esp32-p4-eth/src/video.cpp +++ b/components/esp32-p4-eth/src/video.cpp @@ -28,6 +28,9 @@ namespace espp { bool Esp32P4Eth::initialize_lcd() { logger_.info("Initializing LCD (MIPI-DSI)"); + // Select the panel params first: the DSI lane bit rate is per-panel. + apply_panel_params(default_controller_); + esp_err_t ret = ESP_OK; // Enable the MIPI DSI PHY power LDO (on-chip LDO_VO3 -> VDD_MIPI_DPHY, @@ -51,12 +54,12 @@ bool Esp32P4Eth::initialize_lcd() { // Create the MIPI DSI bus (also initializes the DSI PHY) if (lcd_handles_.mipi_dsi_bus == nullptr) { logger_.info("Creating MIPI DSI bus ({} lanes, {} Mbps/lane)", mipi_dsi_lanes, - mipi_dsi_lane_bitrate_mbps); + panel_params_.lane_bitrate_mbps); esp_lcd_dsi_bus_config_t bus_config = {}; bus_config.bus_id = 0; bus_config.num_data_lanes = mipi_dsi_lanes; bus_config.phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT; - bus_config.lane_bit_rate_mbps = mipi_dsi_lane_bitrate_mbps; + bus_config.lane_bit_rate_mbps = panel_params_.lane_bitrate_mbps; ret = esp_lcd_new_dsi_bus(&bus_config, &lcd_handles_.mipi_dsi_bus); if (ret != ESP_OK) { logger_.error("New DSI bus init failed: {}", esp_err_to_name(ret)); @@ -80,7 +83,6 @@ bool Esp32P4Eth::initialize_lcd() { // Select the panel (Kconfig-driven) and apply its parameters (geometry, DPI // timing). - apply_panel_params(default_controller_); logger_.info("Using display panel: {} ({}x{})", get_display_controller_name(), display_width_, display_height_); diff --git a/components/esp32-p4-nano/include/esp32-p4-nano.hpp b/components/esp32-p4-nano/include/esp32-p4-nano.hpp index 875948d36..f395d0cb0 100644 --- a/components/esp32-p4-nano/include/esp32-p4-nano.hpp +++ b/components/esp32-p4-nano/include/esp32-p4-nano.hpp @@ -477,17 +477,18 @@ class Esp32P4Nano : public BaseComponent { size_t width; size_t height; int dpi_clock_freq_mhz; + int lane_bitrate_mbps; gpio_num_t backlight_io; gpio_num_t reset_io; int hsync_pulse_width, hsync_back_porch, hsync_front_porch; int vsync_pulse_width, vsync_back_porch, vsync_front_porch; }; // EK79007 7" 1024x600 (reset over DSI, no backlight GPIO) - static constexpr PanelParams EK79007_PARAMS{1024, 600, 52, GPIO_NUM_NC, GPIO_NUM_NC, 10, - 160, 160, 1, 23, 12}; + static constexpr PanelParams EK79007_PARAMS{1024, 600, 52, 900, GPIO_NUM_NC, GPIO_NUM_NC, + 10, 160, 160, 1, 23, 12}; // ILI9881C 10.1" 800x1280 (hsync: pulse=40, back=140, front=40; reset over DSI) - static constexpr PanelParams ILI9881C_PARAMS{800, 1280, 80, GPIO_NUM_NC, GPIO_NUM_NC, 40, - 140, 40, 4, 16, 16}; + static constexpr PanelParams ILI9881C_PARAMS{800, 1280, 80, 1500, GPIO_NUM_NC, GPIO_NUM_NC, + 40, 140, 40, 4, 16, 16}; #if CONFIG_ESP32_P4_NANO_DISPLAY_EK79007 static constexpr DisplayController default_controller_ = DisplayController::EK79007; @@ -509,7 +510,6 @@ class Esp32P4Nano : public BaseComponent { static constexpr int mipi_dsi_lanes = 2; // DSI HS lane bit rate (Mbps/lane). 900 Mbps matches Espressif's official // panel bus configs; using the wrong rate mis-packs the pixel bits on the link. - static constexpr int mipi_dsi_lane_bitrate_mbps = 900; static constexpr int mipi_dsi_phy_ldo_channel = 3; // on-chip LDO_VO3 -> VDD_MIPI_DPHY static constexpr int mipi_dsi_phy_ldo_voltage_mv = 2500; diff --git a/components/esp32-p4-nano/src/video.cpp b/components/esp32-p4-nano/src/video.cpp index d711b8ddf..848f785ad 100644 --- a/components/esp32-p4-nano/src/video.cpp +++ b/components/esp32-p4-nano/src/video.cpp @@ -28,6 +28,9 @@ namespace espp { bool Esp32P4Nano::initialize_lcd() { logger_.info("Initializing LCD (MIPI-DSI)"); + // Select the panel params first: the DSI lane bit rate is per-panel. + apply_panel_params(default_controller_); + esp_err_t ret = ESP_OK; // Enable the MIPI DSI PHY power LDO (on-chip LDO_VO3 -> VDD_MIPI_DPHY, @@ -51,12 +54,12 @@ bool Esp32P4Nano::initialize_lcd() { // Create the MIPI DSI bus (also initializes the DSI PHY) if (lcd_handles_.mipi_dsi_bus == nullptr) { logger_.info("Creating MIPI DSI bus ({} lanes, {} Mbps/lane)", mipi_dsi_lanes, - mipi_dsi_lane_bitrate_mbps); + panel_params_.lane_bitrate_mbps); esp_lcd_dsi_bus_config_t bus_config = {}; bus_config.bus_id = 0; bus_config.num_data_lanes = mipi_dsi_lanes; bus_config.phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT; - bus_config.lane_bit_rate_mbps = mipi_dsi_lane_bitrate_mbps; + bus_config.lane_bit_rate_mbps = panel_params_.lane_bitrate_mbps; ret = esp_lcd_new_dsi_bus(&bus_config, &lcd_handles_.mipi_dsi_bus); if (ret != ESP_OK) { logger_.error("New DSI bus init failed: {}", esp_err_to_name(ret)); @@ -80,7 +83,6 @@ bool Esp32P4Nano::initialize_lcd() { // Select the panel (Kconfig-driven) and apply its parameters (geometry, DPI // timing). - apply_panel_params(default_controller_); logger_.info("Using display panel: {} ({}x{})", get_display_controller_name(), display_width_, display_height_); From c02d27a014a84b6f1836eee22a52f06cfaaed0d9 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Thu, 6 Aug 2026 10:55:14 -0500 Subject: [PATCH 03/13] fix(esp32-p4): Send panel vendor init before creating the DPI panel (DSI FIFO hang) initialize_lcd() created the DPI video panel (esp_lcd_new_panel_dpi) BEFORE sending the panel controller's vendor init sequence over DBI. esp_lcd_new_panel_dpi() starts the HS video stream immediately, and while it is running the DSI cannot drain the low-power command FIFO. A short init (EK79007) fits the FIFO, but ILI9881C's 202 commands overflow it, so the HAL busy-waits on a full FIFO and the task watchdog fires during Ili9881::initialize(). Reorder so the vendor init runs while the DSI is still in command mode, then create the DPI panel and start video. Applies to both esp32-p4-nano and esp32-p4-eth. Builds for esp32p4 on ESP-IDF 6.0. Hardware test pending (esp32-p4-eth + ILI9881C). Co-Authored-By: Claude Opus 4.8 (1M context) --- components/esp32-p4-eth/src/video.cpp | 76 ++++++++++++++------------ components/esp32-p4-nano/src/video.cpp | 76 ++++++++++++++------------ 2 files changed, 80 insertions(+), 72 deletions(-) diff --git a/components/esp32-p4-eth/src/video.cpp b/components/esp32-p4-eth/src/video.cpp index 7d3b60e9b..669a1ab87 100644 --- a/components/esp32-p4-eth/src/video.cpp +++ b/components/esp32-p4-eth/src/video.cpp @@ -93,42 +93,6 @@ bool Esp32P4Eth::initialize_lcd() { // and the display fully initializes and shows pixels without any backlight code. brightness(100.0f); - // Create the DPI (video) panel with the configured panel's timing. - if (lcd_handles_.panel == nullptr) { - esp_lcd_dpi_panel_config_t dpi_cfg{}; - memset(&dpi_cfg, 0, sizeof(dpi_cfg)); - dpi_cfg.virtual_channel = 0; - dpi_cfg.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT; - dpi_cfg.dpi_clock_freq_mhz = panel_params_.dpi_clock_freq_mhz; -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) - dpi_cfg.in_color_format = LCD_COLOR_FMT_RGB565; - dpi_cfg.out_color_format = LCD_COLOR_FMT_RGB565; -#else - dpi_cfg.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; - dpi_cfg.flags.use_dma2d = true; -#endif - dpi_cfg.num_fbs = 1; - dpi_cfg.video_timing.h_size = display_width_; - dpi_cfg.video_timing.v_size = display_height_; - dpi_cfg.video_timing.hsync_pulse_width = panel_params_.hsync_pulse_width; - dpi_cfg.video_timing.hsync_back_porch = panel_params_.hsync_back_porch; - dpi_cfg.video_timing.hsync_front_porch = panel_params_.hsync_front_porch; - dpi_cfg.video_timing.vsync_pulse_width = panel_params_.vsync_pulse_width; - dpi_cfg.video_timing.vsync_back_porch = panel_params_.vsync_back_porch; - dpi_cfg.video_timing.vsync_front_porch = panel_params_.vsync_front_porch; - logger_.info("Creating DPI panel ({}x{} @ {} MHz)", dpi_cfg.video_timing.h_size, - dpi_cfg.video_timing.v_size, dpi_cfg.dpi_clock_freq_mhz); - ret = esp_lcd_new_panel_dpi(lcd_handles_.mipi_dsi_bus, &dpi_cfg, &lcd_handles_.panel); - if (ret != ESP_OK) { - logger_.error("Failed to create MIPI DSI DPI panel: {}", esp_err_to_name(ret)); - return false; - } - // NOTE: deliberately do NOT enable DMA2D for the DPI panel. DMA2D is a - // color-processing engine, not a plain copy: routing the LVGL flush - // (esp_lcd_panel_draw_bitmap) through it corrupts the RGB565 channel order, - // while the plain CPU copy path renders correctly. - } - // Send the panel controller's vendor init sequence over DBI (command mode), // before starting the DPI video stream. espp::display_drivers::Config display_config{ @@ -170,6 +134,46 @@ bool Esp32P4Eth::initialize_lcd() { return false; } + // Create the DPI (video) panel with the configured panel's timing. This must + // come AFTER the vendor init sequence above: esp_lcd_new_panel_dpi() starts the + // HS video stream, and once it is running the DSI cannot drain the low-power + // command FIFO, so a long init sequence (e.g. ILI9881C's 202 commands) would + // overflow it and hang. + if (lcd_handles_.panel == nullptr) { + esp_lcd_dpi_panel_config_t dpi_cfg{}; + memset(&dpi_cfg, 0, sizeof(dpi_cfg)); + dpi_cfg.virtual_channel = 0; + dpi_cfg.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT; + dpi_cfg.dpi_clock_freq_mhz = panel_params_.dpi_clock_freq_mhz; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + dpi_cfg.in_color_format = LCD_COLOR_FMT_RGB565; + dpi_cfg.out_color_format = LCD_COLOR_FMT_RGB565; +#else + dpi_cfg.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; + dpi_cfg.flags.use_dma2d = true; +#endif + dpi_cfg.num_fbs = 1; + dpi_cfg.video_timing.h_size = display_width_; + dpi_cfg.video_timing.v_size = display_height_; + dpi_cfg.video_timing.hsync_pulse_width = panel_params_.hsync_pulse_width; + dpi_cfg.video_timing.hsync_back_porch = panel_params_.hsync_back_porch; + dpi_cfg.video_timing.hsync_front_porch = panel_params_.hsync_front_porch; + dpi_cfg.video_timing.vsync_pulse_width = panel_params_.vsync_pulse_width; + dpi_cfg.video_timing.vsync_back_porch = panel_params_.vsync_back_porch; + dpi_cfg.video_timing.vsync_front_porch = panel_params_.vsync_front_porch; + logger_.info("Creating DPI panel ({}x{} @ {} MHz)", dpi_cfg.video_timing.h_size, + dpi_cfg.video_timing.v_size, dpi_cfg.dpi_clock_freq_mhz); + ret = esp_lcd_new_panel_dpi(lcd_handles_.mipi_dsi_bus, &dpi_cfg, &lcd_handles_.panel); + if (ret != ESP_OK) { + logger_.error("Failed to create MIPI DSI DPI panel: {}", esp_err_to_name(ret)); + return false; + } + // NOTE: deliberately do NOT enable DMA2D for the DPI panel. DMA2D is a + // color-processing engine, not a plain copy: routing the LVGL flush + // (esp_lcd_panel_draw_bitmap) through it corrupts the RGB565 channel order, + // while the plain CPU copy path renders correctly. + } + // Low-level panel init (starts the DPI video stream) ret = lcd_handles_.panel->init(lcd_handles_.panel); if (ret != ESP_OK) { diff --git a/components/esp32-p4-nano/src/video.cpp b/components/esp32-p4-nano/src/video.cpp index 848f785ad..ec7dfc5cb 100644 --- a/components/esp32-p4-nano/src/video.cpp +++ b/components/esp32-p4-nano/src/video.cpp @@ -93,42 +93,6 @@ bool Esp32P4Nano::initialize_lcd() { // and the display fully initializes and shows pixels without any backlight code. brightness(100.0f); - // Create the DPI (video) panel with the configured panel's timing. - if (lcd_handles_.panel == nullptr) { - esp_lcd_dpi_panel_config_t dpi_cfg{}; - memset(&dpi_cfg, 0, sizeof(dpi_cfg)); - dpi_cfg.virtual_channel = 0; - dpi_cfg.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT; - dpi_cfg.dpi_clock_freq_mhz = panel_params_.dpi_clock_freq_mhz; -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) - dpi_cfg.in_color_format = LCD_COLOR_FMT_RGB565; - dpi_cfg.out_color_format = LCD_COLOR_FMT_RGB565; -#else - dpi_cfg.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; - dpi_cfg.flags.use_dma2d = true; -#endif - dpi_cfg.num_fbs = 1; - dpi_cfg.video_timing.h_size = display_width_; - dpi_cfg.video_timing.v_size = display_height_; - dpi_cfg.video_timing.hsync_pulse_width = panel_params_.hsync_pulse_width; - dpi_cfg.video_timing.hsync_back_porch = panel_params_.hsync_back_porch; - dpi_cfg.video_timing.hsync_front_porch = panel_params_.hsync_front_porch; - dpi_cfg.video_timing.vsync_pulse_width = panel_params_.vsync_pulse_width; - dpi_cfg.video_timing.vsync_back_porch = panel_params_.vsync_back_porch; - dpi_cfg.video_timing.vsync_front_porch = panel_params_.vsync_front_porch; - logger_.info("Creating DPI panel ({}x{} @ {} MHz)", dpi_cfg.video_timing.h_size, - dpi_cfg.video_timing.v_size, dpi_cfg.dpi_clock_freq_mhz); - ret = esp_lcd_new_panel_dpi(lcd_handles_.mipi_dsi_bus, &dpi_cfg, &lcd_handles_.panel); - if (ret != ESP_OK) { - logger_.error("Failed to create MIPI DSI DPI panel: {}", esp_err_to_name(ret)); - return false; - } - // NOTE: deliberately do NOT enable DMA2D for the DPI panel. DMA2D is a - // color-processing engine, not a plain copy: routing the LVGL flush - // (esp_lcd_panel_draw_bitmap) through it corrupts the RGB565 channel order, - // while the plain CPU copy path renders correctly. - } - // Send the panel controller's vendor init sequence over DBI (command mode), // before starting the DPI video stream. espp::display_drivers::Config display_config{ @@ -170,6 +134,46 @@ bool Esp32P4Nano::initialize_lcd() { return false; } + // Create the DPI (video) panel with the configured panel's timing. This must + // come AFTER the vendor init sequence above: esp_lcd_new_panel_dpi() starts the + // HS video stream, and once it is running the DSI cannot drain the low-power + // command FIFO, so a long init sequence (e.g. ILI9881C's 202 commands) would + // overflow it and hang. + if (lcd_handles_.panel == nullptr) { + esp_lcd_dpi_panel_config_t dpi_cfg{}; + memset(&dpi_cfg, 0, sizeof(dpi_cfg)); + dpi_cfg.virtual_channel = 0; + dpi_cfg.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT; + dpi_cfg.dpi_clock_freq_mhz = panel_params_.dpi_clock_freq_mhz; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + dpi_cfg.in_color_format = LCD_COLOR_FMT_RGB565; + dpi_cfg.out_color_format = LCD_COLOR_FMT_RGB565; +#else + dpi_cfg.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; + dpi_cfg.flags.use_dma2d = true; +#endif + dpi_cfg.num_fbs = 1; + dpi_cfg.video_timing.h_size = display_width_; + dpi_cfg.video_timing.v_size = display_height_; + dpi_cfg.video_timing.hsync_pulse_width = panel_params_.hsync_pulse_width; + dpi_cfg.video_timing.hsync_back_porch = panel_params_.hsync_back_porch; + dpi_cfg.video_timing.hsync_front_porch = panel_params_.hsync_front_porch; + dpi_cfg.video_timing.vsync_pulse_width = panel_params_.vsync_pulse_width; + dpi_cfg.video_timing.vsync_back_porch = panel_params_.vsync_back_porch; + dpi_cfg.video_timing.vsync_front_porch = panel_params_.vsync_front_porch; + logger_.info("Creating DPI panel ({}x{} @ {} MHz)", dpi_cfg.video_timing.h_size, + dpi_cfg.video_timing.v_size, dpi_cfg.dpi_clock_freq_mhz); + ret = esp_lcd_new_panel_dpi(lcd_handles_.mipi_dsi_bus, &dpi_cfg, &lcd_handles_.panel); + if (ret != ESP_OK) { + logger_.error("Failed to create MIPI DSI DPI panel: {}", esp_err_to_name(ret)); + return false; + } + // NOTE: deliberately do NOT enable DMA2D for the DPI panel. DMA2D is a + // color-processing engine, not a plain copy: routing the LVGL flush + // (esp_lcd_panel_draw_bitmap) through it corrupts the RGB565 channel order, + // while the plain CPU copy path renders correctly. + } + // Low-level panel init (starts the DPI video stream) ret = lcd_handles_.panel->init(lcd_handles_.panel); if (ret != ESP_OK) { From 36235f9a04f964c3d482e548daee6bfcd7734d51 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Thu, 6 Aug 2026 11:19:48 -0500 Subject: [PATCH 04/13] fix(esp32-p4): Drive the Waveshare 10.1" JD9365 panel with the vendor component Co-Authored-By: Claude Opus 4.8 (1M context) --- components/esp32-p4-eth/Kconfig.projbuild | 20 +- components/esp32-p4-eth/idf_component.yml | 3 + .../esp32-p4-eth/include/esp32-p4-eth.hpp | 39 ++- components/esp32-p4-eth/src/video.cpp | 287 ++++++++++++------ components/esp32-p4-nano/Kconfig.projbuild | 20 +- components/esp32-p4-nano/idf_component.yml | 3 + .../esp32-p4-nano/include/esp32-p4-nano.hpp | 39 ++- components/esp32-p4-nano/src/video.cpp | 287 ++++++++++++------ 8 files changed, 492 insertions(+), 206 deletions(-) diff --git a/components/esp32-p4-eth/Kconfig.projbuild b/components/esp32-p4-eth/Kconfig.projbuild index f3706ce36..f73890a51 100644 --- a/components/esp32-p4-eth/Kconfig.projbuild +++ b/components/esp32-p4-eth/Kconfig.projbuild @@ -2,18 +2,30 @@ menu "ESP32-P4-ETH Configuration" choice ESP32_P4_ETH_DISPLAY prompt "MIPI-DSI display panel" - default ESP32_P4_ETH_DISPLAY_ILI9881C + default ESP32_P4_ETH_DISPLAY_JD9365_10_1 help Select the MIPI-DSI panel attached to the Waveshare ESP32-P4-ETH. The BSP does not auto-detect the panel (runtime DSI-ID probing hangs the boot watchdog on some panels), so set this to the panel you have. + config ESP32_P4_ETH_DISPLAY_JD9365_10_1 + bool "10.1\" 800x1280 (JD9365) - the panel Waveshare sells for this board" + help + The Waveshare 10.1-inch 800x1280 DSI panel, which uses a JD9365 + controller. Driven by Waveshare's esp_lcd_jd9365 managed component + (vendor init + DPI panel). Reset is handled over DSI (no reset GPIO). + The backlight is driven by an on-board I2C controller (addr 0x45), so + there is no backlight GPIO; brightness() writes the controller. + config ESP32_P4_ETH_DISPLAY_ILI9881C bool "ILI9881C (10.1-inch, 800x1280)" help - The 10.1-inch 800x1280 ILI9881C panel. Reset is handled over DSI (no - reset GPIO). The backlight is driven by an on-board I2C controller, so - there is no backlight GPIO. + A 10.1-inch 800x1280 ILI9881C panel. EXPERIMENTAL on this board: the + panel Waveshare sells for this board is a JD9365, and the ILI9881C + init sequence wedges the DSI link on it (task-watchdog hang). Only + select this if you have actually attached an ILI9881C panel. Reset is + handled over DSI (no reset GPIO). The backlight is driven by an + on-board I2C controller, so there is no backlight GPIO. config ESP32_P4_ETH_DISPLAY_EK79007 bool "EK79007 (7-inch, 1024x600)" diff --git a/components/esp32-p4-eth/idf_component.yml b/components/esp32-p4-eth/idf_component.yml index e3edb9c0c..2c1f7084c 100644 --- a/components/esp32-p4-eth/idf_component.yml +++ b/components/esp32-p4-eth/idf_component.yml @@ -31,6 +31,9 @@ dependencies: espp/gt911: ">=1.0" espp/input_drivers: ">=1.0" espp/interrupt: ">=1.0" + # The Waveshare 10.1" 800x1280 panel for this board is a JD9365, driven by + # Waveshare's managed panel component (vendor init + DPI panel creation). + waveshare/esp_lcd_jd9365: "^2.0.0" # MIPI-CSI camera pipeline: esp_video provides the V4L2 capture framework # (CSI + ISP) and esp_cam_sensor provides the OV5647 sensor driver. espressif/esp_video: ">=2.0" diff --git a/components/esp32-p4-eth/include/esp32-p4-eth.hpp b/components/esp32-p4-eth/include/esp32-p4-eth.hpp index da69a5efe..549b0b253 100644 --- a/components/esp32-p4-eth/include/esp32-p4-eth.hpp +++ b/components/esp32-p4-eth/include/esp32-p4-eth.hpp @@ -40,8 +40,8 @@ namespace espp { /// /// This class provides a singleton interface to the board's peripherals: /// - 10/100 Ethernet via the ESP32-P4 internal EMAC and an IP101GRI RMII PHY. -/// - MIPI-DSI display (ILI9881C 10.1" or EK79007 7", selected via Kconfig) with -/// a GT911 capacitive-touch controller. +/// - MIPI-DSI display (JD9365 10.1" by default, or ILI9881C 10.1" / EK79007 7", +/// selected via Kconfig) with a GT911 capacitive-touch controller. /// - MIPI-CSI camera (esp_video / V4L2 capture pipeline; OV5647 by default). /// - microSD / TF card over 4-bit SDMMC. /// - ES8311 audio codec (+ NS4150B amplifier) for speaker output and microphone @@ -140,7 +140,7 @@ class Esp32P4Eth : public BaseComponent { using touch_callback_t = std::function; /// Enum for the display controller type (selected via Kconfig) - enum class DisplayController { UNKNOWN, EK79007, ILI9881C }; + enum class DisplayController { UNKNOWN, EK79007, ILI9881C, JD9365 }; /// Default touch INT GPIO used by initialize_touch(). GPIO_NUM_NC means the /// GT911 is polled; if interrupt-driven touch is enabled via Kconfig this is @@ -174,6 +174,8 @@ class Esp32P4Eth : public BaseComponent { return "EK79007"; case DisplayController::ILI9881C: return "ILI9881C"; + case DisplayController::JD9365: + return "JD9365"; default: return "Unknown"; } @@ -233,8 +235,10 @@ class Esp32P4Eth : public BaseComponent { /// Set the display brightness /// \param brightness The brightness as a percentage (0-100) - /// \note The ESP32-P4-ETH has no backlight GPIO; brightness is stored but not - /// applied to hardware (see the source for details). + /// \note The ESP32-P4-ETH has no backlight GPIO. On the 10.1" JD9365 panel + /// the backlight is driven by an on-board I2C controller (addr 0x45) + /// and this call writes it; on other panels the value is stored but + /// not applied to hardware (see the source for details). void brightness(float brightness); /// Get the display brightness @@ -488,16 +492,26 @@ class Esp32P4Eth : public BaseComponent { // ILI9881C 10.1" 800x1280 (hsync: pulse=40, back=140, front=40; reset over DSI) static constexpr PanelParams ILI9881C_PARAMS{800, 1280, 80, 1500, GPIO_NUM_NC, GPIO_NUM_NC, 40, 140, 40, 4, 16, 16}; + // JD9365 10.1" 800x1280 (the panel Waveshare sells for this board; reset over + // DSI, no backlight GPIO). NOTE: on the JD9365 path the DPI timing actually + // used comes from the esp_lcd_jd9365 component's + // JD9365_800_1280_PANEL_60HZ_DPI_CONFIG macro; these timing fields are + // informational. The lane bit rate (1500 Mbps) IS used for the DSI bus. + static constexpr PanelParams JD9365_PARAMS{800, 1280, 80, 1500, GPIO_NUM_NC, GPIO_NUM_NC, + 20, 20, 40, 4, 10, 30}; #if CONFIG_ESP32_P4_ETH_DISPLAY_EK79007 static constexpr DisplayController default_controller_ = DisplayController::EK79007; -#else +#elif CONFIG_ESP32_P4_ETH_DISPLAY_ILI9881C static constexpr DisplayController default_controller_ = DisplayController::ILI9881C; +#else + static constexpr DisplayController default_controller_ = DisplayController::JD9365; #endif // Runtime display geometry, set from the configured panel. - PanelParams panel_params_{default_controller_ == DisplayController::ILI9881C ? ILI9881C_PARAMS - : EK79007_PARAMS}; + PanelParams panel_params_{default_controller_ == DisplayController::ILI9881C ? ILI9881C_PARAMS + : default_controller_ == DisplayController::EK79007 ? EK79007_PARAMS + : JD9365_PARAMS}; size_t display_width_{panel_params_.width}; size_t display_height_{panel_params_.height}; @@ -638,9 +652,14 @@ class Esp32P4Eth : public BaseComponent { ///////////////////////////////////////////////////////////////////////////// // Display state (MIPI-DSI). NOTE: there is no backlight GPIO / espp::Led on - // this board; the backlight is driven by an on-board I2C controller, so the - // stored brightness is best-effort only (see src/video.cpp). + // this board; the backlight is driven by an on-board I2C controller. On the + // 10.1" JD9365 panel brightness() writes that controller (addr 0x45, reg + // 0x86); on other panels the stored brightness is best-effort only (see + // src/video.cpp). ///////////////////////////////////////////////////////////////////////////// + // On-board I2C backlight controller (10.1" JD9365 panel) + static constexpr uint8_t backlight_i2c_address = 0x45; + std::shared_ptr> backlight_i2c_device_; std::atomic brightness_{100.0f}; std::shared_ptr> display_; std::shared_ptr display_driver_{static_cast(nullptr)}; diff --git a/components/esp32-p4-eth/src/video.cpp b/components/esp32-p4-eth/src/video.cpp index 669a1ab87..14d015f3d 100644 --- a/components/esp32-p4-eth/src/video.cpp +++ b/components/esp32-p4-eth/src/video.cpp @@ -21,6 +21,10 @@ #include #include +// Waveshare's managed panel component for the JD9365 (the controller on the +// 10.1" 800x1280 panel Waveshare sells for this board). +#include "esp_lcd_jd9365.h" + using namespace std::chrono_literals; namespace espp { @@ -87,98 +91,166 @@ bool Esp32P4Eth::initialize_lcd() { display_height_); // NOTE: The ESP32-P4-ETH has no backlight GPIO. The backlight is driven by an - // on-board I2C controller (addr ~0x45/0x95, chip/protocol not yet identified); - // brightness control is a TODO pending hardware identification. The panel powers - // up with the backlight on, so no espp::Led / PWM backlight is instantiated here - // and the display fully initializes and shows pixels without any backlight code. + // on-board I2C controller (addr 0x45). On the 10.1" JD9365 panel brightness() + // writes that controller (reg 0x86); on the other panels the panel powers up + // with the backlight on and brightness() only stores the value. No espp::Led / + // PWM backlight is instantiated here. brightness(100.0f); - // Send the panel controller's vendor init sequence over DBI (command mode), - // before starting the DPI video stream. - espp::display_drivers::Config display_config{ - .panel_io = nullptr, - .write_command = std::bind_front(&Esp32P4Eth::dsi_write_command, this), - // NOTE: the Waveshare ESP32-P4 panels do not reliably support MIPI-DSI DCS - // reads (bus turn-around); the ESP-IDF HAL busy-waits on the read, which - // hangs panel init and trips the task watchdog. Do not provide a - // read_command so the driver skips the optional panel-ID read. - .read_command = nullptr, - .lcd_send_lines = nullptr, - .reset_pin = GPIO_NUM_NC, - .data_command_pin = GPIO_NUM_NC, - .reset_value = false, - .invert_colors = invert_colors, - .swap_color_order = swap_color_order, - .offset_x = 0, - .offset_y = 0, - .swap_xy = swap_xy, - .mirror_x = mirror_x, - .mirror_y = mirror_y, - .mirror_portrait = false, - }; - - display_driver_.reset(); - if (display_controller_ == DisplayController::ILI9881C) { - auto driver = std::make_shared(display_config); - if (driver->initialize()) { - display_driver_ = std::move(driver); + if (display_controller_ == DisplayController::JD9365) { + // The Waveshare 10.1" 800x1280 panel is a JD9365, driven by Waveshare's + // esp_lcd_jd9365 managed component. The component sends the vendor init + // sequence over the DBI IO and creates/starts the DPI (video) panel + // internally: the handle it returns IS the real DPI panel handle (it + // patches the DPI panel's vtable and enables DMA2D itself), so + // esp_lcd_panel_draw_bitmap() and the DPI event callbacks below work on it + // directly. Do NOT create a DPI panel or run an espp display driver here. + if (lcd_handles_.panel == nullptr) { + // Values from the component's JD9365_800_1280_PANEL_60HZ_DPI_CONFIG macro; + // spelled out field-by-field because the macro's C designated-initializer + // ordering is not valid C++. + esp_lcd_dpi_panel_config_t dpi_config{}; + dpi_config.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT; + dpi_config.dpi_clock_freq_mhz = 80; + dpi_config.virtual_channel = 0; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + dpi_config.in_color_format = LCD_COLOR_FMT_RGB565; + dpi_config.out_color_format = LCD_COLOR_FMT_RGB565; +#else + dpi_config.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; + dpi_config.flags.use_dma2d = true; +#endif + dpi_config.num_fbs = 1; + dpi_config.video_timing.h_size = 800; + dpi_config.video_timing.v_size = 1280; + dpi_config.video_timing.hsync_back_porch = 20; + dpi_config.video_timing.hsync_pulse_width = 20; + dpi_config.video_timing.hsync_front_porch = 40; + dpi_config.video_timing.vsync_back_porch = 10; + dpi_config.video_timing.vsync_pulse_width = 4; + dpi_config.video_timing.vsync_front_porch = 30; + jd9365_vendor_config_t vendor_config{}; + vendor_config.init_cmds = nullptr; // use the component's default init sequence + vendor_config.init_cmds_size = 0; + vendor_config.mipi_config.dsi_bus = lcd_handles_.mipi_dsi_bus; + vendor_config.mipi_config.dpi_config = &dpi_config; + vendor_config.mipi_config.lane_num = mipi_dsi_lanes; + esp_lcd_panel_dev_config_t lcd_dev_config{}; + lcd_dev_config.reset_gpio_num = GPIO_NUM_NC; // reset over DSI, no reset GPIO + lcd_dev_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; + lcd_dev_config.bits_per_pixel = 16; + lcd_dev_config.vendor_config = &vendor_config; + logger_.info("Creating JD9365 panel ({}x{} @ {} MHz DPI)", display_width_, display_height_, + dpi_config.dpi_clock_freq_mhz); + ret = esp_lcd_new_panel_jd9365(lcd_handles_.io, &lcd_dev_config, &lcd_handles_.panel); + if (ret != ESP_OK) { + logger_.error("Failed to create JD9365 panel: {}", esp_err_to_name(ret)); + return false; + } + ret = esp_lcd_panel_reset(lcd_handles_.panel); + if (ret != ESP_OK) { + logger_.error("JD9365 panel reset failed: {}", esp_err_to_name(ret)); + return false; + } + // Sends the vendor init over DBI, then starts the DPI video stream. + ret = esp_lcd_panel_init(lcd_handles_.panel); + if (ret != ESP_OK) { + logger_.error("JD9365 panel init failed: {}", esp_err_to_name(ret)); + return false; + } } + // The managed component owns the panel init on this path; there is no espp + // display driver. + display_driver_.reset(); } else { - auto driver = std::make_shared(display_config); - if (driver->initialize()) { - display_driver_ = std::move(driver); + // espp-driver path (ILI9881C / EK79007): send the panel controller's vendor + // init sequence over DBI (command mode), before starting the DPI video + // stream. + espp::display_drivers::Config display_config{ + .panel_io = nullptr, + .write_command = std::bind_front(&Esp32P4Eth::dsi_write_command, this), + // NOTE: the Waveshare ESP32-P4 panels do not reliably support MIPI-DSI DCS + // reads (bus turn-around); the ESP-IDF HAL busy-waits on the read, which + // hangs panel init and trips the task watchdog. Do not provide a + // read_command so the driver skips the optional panel-ID read. + .read_command = nullptr, + .lcd_send_lines = nullptr, + .reset_pin = GPIO_NUM_NC, + .data_command_pin = GPIO_NUM_NC, + .reset_value = false, + .invert_colors = invert_colors, + .swap_color_order = swap_color_order, + .offset_x = 0, + .offset_y = 0, + .swap_xy = swap_xy, + .mirror_x = mirror_x, + .mirror_y = mirror_y, + .mirror_portrait = false, + }; + + display_driver_.reset(); + if (display_controller_ == DisplayController::ILI9881C) { + auto driver = std::make_shared(display_config); + if (driver->initialize()) { + display_driver_ = std::move(driver); + } + } else { + auto driver = std::make_shared(display_config); + if (driver->initialize()) { + display_driver_ = std::move(driver); + } + } + if (!display_driver_) { + logger_.error("Failed to initialize {} display controller", get_display_controller_name()); + return false; } - } - if (!display_driver_) { - logger_.error("Failed to initialize {} display controller", get_display_controller_name()); - return false; - } - // Create the DPI (video) panel with the configured panel's timing. This must - // come AFTER the vendor init sequence above: esp_lcd_new_panel_dpi() starts the - // HS video stream, and once it is running the DSI cannot drain the low-power - // command FIFO, so a long init sequence (e.g. ILI9881C's 202 commands) would - // overflow it and hang. - if (lcd_handles_.panel == nullptr) { - esp_lcd_dpi_panel_config_t dpi_cfg{}; - memset(&dpi_cfg, 0, sizeof(dpi_cfg)); - dpi_cfg.virtual_channel = 0; - dpi_cfg.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT; - dpi_cfg.dpi_clock_freq_mhz = panel_params_.dpi_clock_freq_mhz; + // Create the DPI (video) panel with the configured panel's timing. This must + // come AFTER the vendor init sequence above: esp_lcd_new_panel_dpi() starts the + // HS video stream, and once it is running the DSI cannot drain the low-power + // command FIFO, so a long init sequence (e.g. ILI9881C's 202 commands) would + // overflow it and hang. + if (lcd_handles_.panel == nullptr) { + esp_lcd_dpi_panel_config_t dpi_cfg{}; + memset(&dpi_cfg, 0, sizeof(dpi_cfg)); + dpi_cfg.virtual_channel = 0; + dpi_cfg.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT; + dpi_cfg.dpi_clock_freq_mhz = panel_params_.dpi_clock_freq_mhz; #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) - dpi_cfg.in_color_format = LCD_COLOR_FMT_RGB565; - dpi_cfg.out_color_format = LCD_COLOR_FMT_RGB565; + dpi_cfg.in_color_format = LCD_COLOR_FMT_RGB565; + dpi_cfg.out_color_format = LCD_COLOR_FMT_RGB565; #else - dpi_cfg.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; - dpi_cfg.flags.use_dma2d = true; + dpi_cfg.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; + dpi_cfg.flags.use_dma2d = true; #endif - dpi_cfg.num_fbs = 1; - dpi_cfg.video_timing.h_size = display_width_; - dpi_cfg.video_timing.v_size = display_height_; - dpi_cfg.video_timing.hsync_pulse_width = panel_params_.hsync_pulse_width; - dpi_cfg.video_timing.hsync_back_porch = panel_params_.hsync_back_porch; - dpi_cfg.video_timing.hsync_front_porch = panel_params_.hsync_front_porch; - dpi_cfg.video_timing.vsync_pulse_width = panel_params_.vsync_pulse_width; - dpi_cfg.video_timing.vsync_back_porch = panel_params_.vsync_back_porch; - dpi_cfg.video_timing.vsync_front_porch = panel_params_.vsync_front_porch; - logger_.info("Creating DPI panel ({}x{} @ {} MHz)", dpi_cfg.video_timing.h_size, - dpi_cfg.video_timing.v_size, dpi_cfg.dpi_clock_freq_mhz); - ret = esp_lcd_new_panel_dpi(lcd_handles_.mipi_dsi_bus, &dpi_cfg, &lcd_handles_.panel); + dpi_cfg.num_fbs = 1; + dpi_cfg.video_timing.h_size = display_width_; + dpi_cfg.video_timing.v_size = display_height_; + dpi_cfg.video_timing.hsync_pulse_width = panel_params_.hsync_pulse_width; + dpi_cfg.video_timing.hsync_back_porch = panel_params_.hsync_back_porch; + dpi_cfg.video_timing.hsync_front_porch = panel_params_.hsync_front_porch; + dpi_cfg.video_timing.vsync_pulse_width = panel_params_.vsync_pulse_width; + dpi_cfg.video_timing.vsync_back_porch = panel_params_.vsync_back_porch; + dpi_cfg.video_timing.vsync_front_porch = panel_params_.vsync_front_porch; + logger_.info("Creating DPI panel ({}x{} @ {} MHz)", dpi_cfg.video_timing.h_size, + dpi_cfg.video_timing.v_size, dpi_cfg.dpi_clock_freq_mhz); + ret = esp_lcd_new_panel_dpi(lcd_handles_.mipi_dsi_bus, &dpi_cfg, &lcd_handles_.panel); + if (ret != ESP_OK) { + logger_.error("Failed to create MIPI DSI DPI panel: {}", esp_err_to_name(ret)); + return false; + } + // NOTE: deliberately do NOT enable DMA2D for the DPI panel. DMA2D is a + // color-processing engine, not a plain copy: routing the LVGL flush + // (esp_lcd_panel_draw_bitmap) through it corrupts the RGB565 channel order, + // while the plain CPU copy path renders correctly. + } + + // Low-level panel init (starts the DPI video stream) + ret = lcd_handles_.panel->init(lcd_handles_.panel); if (ret != ESP_OK) { - logger_.error("Failed to create MIPI DSI DPI panel: {}", esp_err_to_name(ret)); + logger_.error("Low-level panel init failed: {}", esp_err_to_name(ret)); return false; } - // NOTE: deliberately do NOT enable DMA2D for the DPI panel. DMA2D is a - // color-processing engine, not a plain copy: routing the LVGL flush - // (esp_lcd_panel_draw_bitmap) through it corrupts the RGB565 channel order, - // while the plain CPU copy path renders correctly. - } - - // Low-level panel init (starts the DPI video stream) - ret = lcd_handles_.panel->init(lcd_handles_.panel); - if (ret != ESP_OK) { - logger_.error("Low-level panel init failed: {}", esp_err_to_name(ret)); - return false; } // Note: the raw MIPI-DSI DPI panel does not implement disp_on_off (the panel @@ -203,8 +275,17 @@ bool Esp32P4Eth::initialize_lcd() { void Esp32P4Eth::apply_panel_params(DisplayController controller) { display_controller_ = (controller == DisplayController::UNKNOWN) ? default_controller_ : controller; - panel_params_ = - (display_controller_ == DisplayController::ILI9881C) ? ILI9881C_PARAMS : EK79007_PARAMS; + switch (display_controller_) { + case DisplayController::JD9365: + panel_params_ = JD9365_PARAMS; + break; + case DisplayController::ILI9881C: + panel_params_ = ILI9881C_PARAMS; + break; + default: + panel_params_ = EK79007_PARAMS; + break; + } display_width_ = panel_params_.width; display_height_ = panel_params_.height; } @@ -297,15 +378,43 @@ void Esp32P4Eth::write_lcd_lines(int xs, int ys, int xe, int ye, const uint8_t * void Esp32P4Eth::brightness(float brightness) { // The ESP32-P4-ETH has NO backlight GPIO. The backlight is driven by an - // on-board I2C controller (addr ~0x45/0x95, chip/protocol not yet identified); - // brightness control is a TODO pending hardware identification. The panel powers - // up with the backlight on. For now this is best-effort: store the requested - // value so brightness() reads back what was set, and log it. + // on-board I2C controller at address 0x45. On the 10.1" JD9365 panel the + // brightness register is 0x86 (value 0-255); the "A"-series panels use 0x96 + // instead. For panels other than the JD9365 the chip/protocol has not been + // verified, so this remains best-effort: store the requested value so + // brightness() reads back what was set, and log it. brightness = std::clamp(brightness, 0.0f, 100.0f); brightness_ = brightness; - logger_.debug("brightness({}) requested; no backlight GPIO on this board (on-board I2C " - "controller not yet identified), value stored only", - brightness); + if (display_controller_ != DisplayController::JD9365) { + logger_.debug("brightness({}) requested; no backlight GPIO on this board (on-board I2C " + "backlight protocol not verified for this panel), value stored only", + brightness); + return; + } + // Lazily create the backlight I2C device on the internal bus. + if (!backlight_i2c_device_) { + std::error_code ec; + backlight_i2c_device_ = internal_i2c_.add_device( + { + .device_address = backlight_i2c_address, + .timeout_ms = static_cast(internal_i2c_.config().timeout_ms), + .scl_speed_hz = internal_i2c_.config().clk_speed, + .log_level = espp::Logger::Verbosity::WARN, + }, + ec); + if (!backlight_i2c_device_) { + logger_.error("Could not initialize backlight I2C device (0x{:02X}): {}", + backlight_i2c_address, ec.message()); + return; + } + } + // Register 0x86 is the brightness register for the 10.1" JD9365 panel (the + // "A"-series panels use 0x96), value 0-255. + const uint8_t data[2] = {0x86, static_cast(255.0f * brightness / 100.0f)}; + std::error_code ec; + if (!backlight_i2c_device_->write(data, sizeof(data), ec)) { + logger_.error("Failed to write backlight brightness: {}", ec.message()); + } } float Esp32P4Eth::brightness() const { return brightness_.load(); } diff --git a/components/esp32-p4-nano/Kconfig.projbuild b/components/esp32-p4-nano/Kconfig.projbuild index 1d896f437..488e97e66 100644 --- a/components/esp32-p4-nano/Kconfig.projbuild +++ b/components/esp32-p4-nano/Kconfig.projbuild @@ -2,18 +2,30 @@ menu "ESP32-P4-NANO Configuration" choice ESP32_P4_NANO_DISPLAY prompt "MIPI-DSI display panel" - default ESP32_P4_NANO_DISPLAY_ILI9881C + default ESP32_P4_NANO_DISPLAY_JD9365_10_1 help Select the MIPI-DSI panel attached to the Waveshare ESP32-P4-NANO. The BSP does not auto-detect the panel (runtime DSI-ID probing hangs the boot watchdog on some panels), so set this to the panel you have. + config ESP32_P4_NANO_DISPLAY_JD9365_10_1 + bool "10.1\" 800x1280 (JD9365) - the panel Waveshare sells for this board" + help + The Waveshare 10.1-inch 800x1280 DSI panel, which uses a JD9365 + controller. Driven by Waveshare's esp_lcd_jd9365 managed component + (vendor init + DPI panel). Reset is handled over DSI (no reset GPIO). + The backlight is driven by an on-board I2C controller (addr 0x45), so + there is no backlight GPIO; brightness() writes the controller. + config ESP32_P4_NANO_DISPLAY_ILI9881C bool "ILI9881C (10.1-inch, 800x1280)" help - The 10.1-inch 800x1280 ILI9881C panel. Reset is handled over DSI (no - reset GPIO). The backlight is driven by an on-board I2C controller, so - there is no backlight GPIO. + A 10.1-inch 800x1280 ILI9881C panel. EXPERIMENTAL on this board: the + panel Waveshare sells for this board is a JD9365, and the ILI9881C + init sequence wedges the DSI link on it (task-watchdog hang). Only + select this if you have actually attached an ILI9881C panel. Reset is + handled over DSI (no reset GPIO). The backlight is driven by an + on-board I2C controller, so there is no backlight GPIO. config ESP32_P4_NANO_DISPLAY_EK79007 bool "EK79007 (7-inch, 1024x600)" diff --git a/components/esp32-p4-nano/idf_component.yml b/components/esp32-p4-nano/idf_component.yml index 991b4ba6d..fb5286679 100644 --- a/components/esp32-p4-nano/idf_component.yml +++ b/components/esp32-p4-nano/idf_component.yml @@ -31,6 +31,9 @@ dependencies: espp/gt911: ">=1.0" espp/input_drivers: ">=1.0" espp/interrupt: ">=1.0" + # The Waveshare 10.1" 800x1280 panel for this board is a JD9365, driven by + # Waveshare's managed panel component (vendor init + DPI panel creation). + waveshare/esp_lcd_jd9365: "^2.0.0" # MIPI-CSI camera pipeline: esp_video provides the V4L2 capture framework # (CSI + ISP) and esp_cam_sensor provides the OV5647 sensor driver. espressif/esp_video: ">=2.0" diff --git a/components/esp32-p4-nano/include/esp32-p4-nano.hpp b/components/esp32-p4-nano/include/esp32-p4-nano.hpp index f395d0cb0..f6a9c51d8 100644 --- a/components/esp32-p4-nano/include/esp32-p4-nano.hpp +++ b/components/esp32-p4-nano/include/esp32-p4-nano.hpp @@ -40,8 +40,8 @@ namespace espp { /// /// This class provides a singleton interface to the board's peripherals: /// - 10/100 Ethernet via the ESP32-P4 internal EMAC and an IP101GRI RMII PHY. -/// - MIPI-DSI display (ILI9881C 10.1" or EK79007 7", selected via Kconfig) with -/// a GT911 capacitive-touch controller. +/// - MIPI-DSI display (JD9365 10.1" by default, or ILI9881C 10.1" / EK79007 7", +/// selected via Kconfig) with a GT911 capacitive-touch controller. /// - MIPI-CSI camera (esp_video / V4L2 capture pipeline; OV5647 by default). /// - microSD / TF card over 4-bit SDMMC. /// - ES8311 audio codec (+ NS4150B amplifier) for speaker output and microphone @@ -141,7 +141,7 @@ class Esp32P4Nano : public BaseComponent { using touch_callback_t = std::function; /// Enum for the display controller type (selected via Kconfig) - enum class DisplayController { UNKNOWN, EK79007, ILI9881C }; + enum class DisplayController { UNKNOWN, EK79007, ILI9881C, JD9365 }; /// Default touch INT GPIO used by initialize_touch(). GPIO_NUM_NC means the /// GT911 is polled; if interrupt-driven touch is enabled via Kconfig this is @@ -175,6 +175,8 @@ class Esp32P4Nano : public BaseComponent { return "EK79007"; case DisplayController::ILI9881C: return "ILI9881C"; + case DisplayController::JD9365: + return "JD9365"; default: return "Unknown"; } @@ -234,8 +236,10 @@ class Esp32P4Nano : public BaseComponent { /// Set the display brightness /// \param brightness The brightness as a percentage (0-100) - /// \note The ESP32-P4-NANO has no backlight GPIO; brightness is stored but not - /// applied to hardware (see the source for details). + /// \note The ESP32-P4-NANO has no backlight GPIO. On the 10.1" JD9365 panel + /// the backlight is driven by an on-board I2C controller (addr 0x45) + /// and this call writes it; on other panels the value is stored but + /// not applied to hardware (see the source for details). void brightness(float brightness); /// Get the display brightness @@ -489,16 +493,26 @@ class Esp32P4Nano : public BaseComponent { // ILI9881C 10.1" 800x1280 (hsync: pulse=40, back=140, front=40; reset over DSI) static constexpr PanelParams ILI9881C_PARAMS{800, 1280, 80, 1500, GPIO_NUM_NC, GPIO_NUM_NC, 40, 140, 40, 4, 16, 16}; + // JD9365 10.1" 800x1280 (the panel Waveshare sells for this board; reset over + // DSI, no backlight GPIO). NOTE: on the JD9365 path the DPI timing actually + // used comes from the esp_lcd_jd9365 component's + // JD9365_800_1280_PANEL_60HZ_DPI_CONFIG macro; these timing fields are + // informational. The lane bit rate (1500 Mbps) IS used for the DSI bus. + static constexpr PanelParams JD9365_PARAMS{800, 1280, 80, 1500, GPIO_NUM_NC, GPIO_NUM_NC, + 20, 20, 40, 4, 10, 30}; #if CONFIG_ESP32_P4_NANO_DISPLAY_EK79007 static constexpr DisplayController default_controller_ = DisplayController::EK79007; -#else +#elif CONFIG_ESP32_P4_NANO_DISPLAY_ILI9881C static constexpr DisplayController default_controller_ = DisplayController::ILI9881C; +#else + static constexpr DisplayController default_controller_ = DisplayController::JD9365; #endif // Runtime display geometry, set from the configured panel. - PanelParams panel_params_{default_controller_ == DisplayController::ILI9881C ? ILI9881C_PARAMS - : EK79007_PARAMS}; + PanelParams panel_params_{default_controller_ == DisplayController::ILI9881C ? ILI9881C_PARAMS + : default_controller_ == DisplayController::EK79007 ? EK79007_PARAMS + : JD9365_PARAMS}; size_t display_width_{panel_params_.width}; size_t display_height_{panel_params_.height}; @@ -639,9 +653,14 @@ class Esp32P4Nano : public BaseComponent { ///////////////////////////////////////////////////////////////////////////// // Display state (MIPI-DSI). NOTE: there is no backlight GPIO / espp::Led on - // this board; the backlight is driven by an on-board I2C controller, so the - // stored brightness is best-effort only (see src/video.cpp). + // this board; the backlight is driven by an on-board I2C controller. On the + // 10.1" JD9365 panel brightness() writes that controller (addr 0x45, reg + // 0x86); on other panels the stored brightness is best-effort only (see + // src/video.cpp). ///////////////////////////////////////////////////////////////////////////// + // On-board I2C backlight controller (10.1" JD9365 panel) + static constexpr uint8_t backlight_i2c_address = 0x45; + std::shared_ptr> backlight_i2c_device_; std::atomic brightness_{100.0f}; std::shared_ptr> display_; std::shared_ptr display_driver_{static_cast(nullptr)}; diff --git a/components/esp32-p4-nano/src/video.cpp b/components/esp32-p4-nano/src/video.cpp index ec7dfc5cb..cab3d63b6 100644 --- a/components/esp32-p4-nano/src/video.cpp +++ b/components/esp32-p4-nano/src/video.cpp @@ -21,6 +21,10 @@ #include #include +// Waveshare's managed panel component for the JD9365 (the controller on the +// 10.1" 800x1280 panel Waveshare sells for this board). +#include "esp_lcd_jd9365.h" + using namespace std::chrono_literals; namespace espp { @@ -87,98 +91,166 @@ bool Esp32P4Nano::initialize_lcd() { display_height_); // NOTE: The ESP32-P4-NANO has no backlight GPIO. The backlight is driven by an - // on-board I2C controller (addr ~0x45/0x95, chip/protocol not yet identified); - // brightness control is a TODO pending hardware identification. The panel powers - // up with the backlight on, so no espp::Led / PWM backlight is instantiated here - // and the display fully initializes and shows pixels without any backlight code. + // on-board I2C controller (addr 0x45). On the 10.1" JD9365 panel brightness() + // writes that controller (reg 0x86); on the other panels the panel powers up + // with the backlight on and brightness() only stores the value. No espp::Led / + // PWM backlight is instantiated here. brightness(100.0f); - // Send the panel controller's vendor init sequence over DBI (command mode), - // before starting the DPI video stream. - espp::display_drivers::Config display_config{ - .panel_io = nullptr, - .write_command = std::bind_front(&Esp32P4Nano::dsi_write_command, this), - // NOTE: the Waveshare ESP32-P4 panels do not reliably support MIPI-DSI DCS - // reads (bus turn-around); the ESP-IDF HAL busy-waits on the read, which - // hangs panel init and trips the task watchdog. Do not provide a - // read_command so the driver skips the optional panel-ID read. - .read_command = nullptr, - .lcd_send_lines = nullptr, - .reset_pin = GPIO_NUM_NC, - .data_command_pin = GPIO_NUM_NC, - .reset_value = false, - .invert_colors = invert_colors, - .swap_color_order = swap_color_order, - .offset_x = 0, - .offset_y = 0, - .swap_xy = swap_xy, - .mirror_x = mirror_x, - .mirror_y = mirror_y, - .mirror_portrait = false, - }; - - display_driver_.reset(); - if (display_controller_ == DisplayController::ILI9881C) { - auto driver = std::make_shared(display_config); - if (driver->initialize()) { - display_driver_ = std::move(driver); + if (display_controller_ == DisplayController::JD9365) { + // The Waveshare 10.1" 800x1280 panel is a JD9365, driven by Waveshare's + // esp_lcd_jd9365 managed component. The component sends the vendor init + // sequence over the DBI IO and creates/starts the DPI (video) panel + // internally: the handle it returns IS the real DPI panel handle (it + // patches the DPI panel's vtable and enables DMA2D itself), so + // esp_lcd_panel_draw_bitmap() and the DPI event callbacks below work on it + // directly. Do NOT create a DPI panel or run an espp display driver here. + if (lcd_handles_.panel == nullptr) { + // Values from the component's JD9365_800_1280_PANEL_60HZ_DPI_CONFIG macro; + // spelled out field-by-field because the macro's C designated-initializer + // ordering is not valid C++. + esp_lcd_dpi_panel_config_t dpi_config{}; + dpi_config.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT; + dpi_config.dpi_clock_freq_mhz = 80; + dpi_config.virtual_channel = 0; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + dpi_config.in_color_format = LCD_COLOR_FMT_RGB565; + dpi_config.out_color_format = LCD_COLOR_FMT_RGB565; +#else + dpi_config.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; + dpi_config.flags.use_dma2d = true; +#endif + dpi_config.num_fbs = 1; + dpi_config.video_timing.h_size = 800; + dpi_config.video_timing.v_size = 1280; + dpi_config.video_timing.hsync_back_porch = 20; + dpi_config.video_timing.hsync_pulse_width = 20; + dpi_config.video_timing.hsync_front_porch = 40; + dpi_config.video_timing.vsync_back_porch = 10; + dpi_config.video_timing.vsync_pulse_width = 4; + dpi_config.video_timing.vsync_front_porch = 30; + jd9365_vendor_config_t vendor_config{}; + vendor_config.init_cmds = nullptr; // use the component's default init sequence + vendor_config.init_cmds_size = 0; + vendor_config.mipi_config.dsi_bus = lcd_handles_.mipi_dsi_bus; + vendor_config.mipi_config.dpi_config = &dpi_config; + vendor_config.mipi_config.lane_num = mipi_dsi_lanes; + esp_lcd_panel_dev_config_t lcd_dev_config{}; + lcd_dev_config.reset_gpio_num = GPIO_NUM_NC; // reset over DSI, no reset GPIO + lcd_dev_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; + lcd_dev_config.bits_per_pixel = 16; + lcd_dev_config.vendor_config = &vendor_config; + logger_.info("Creating JD9365 panel ({}x{} @ {} MHz DPI)", display_width_, display_height_, + dpi_config.dpi_clock_freq_mhz); + ret = esp_lcd_new_panel_jd9365(lcd_handles_.io, &lcd_dev_config, &lcd_handles_.panel); + if (ret != ESP_OK) { + logger_.error("Failed to create JD9365 panel: {}", esp_err_to_name(ret)); + return false; + } + ret = esp_lcd_panel_reset(lcd_handles_.panel); + if (ret != ESP_OK) { + logger_.error("JD9365 panel reset failed: {}", esp_err_to_name(ret)); + return false; + } + // Sends the vendor init over DBI, then starts the DPI video stream. + ret = esp_lcd_panel_init(lcd_handles_.panel); + if (ret != ESP_OK) { + logger_.error("JD9365 panel init failed: {}", esp_err_to_name(ret)); + return false; + } } + // The managed component owns the panel init on this path; there is no espp + // display driver. + display_driver_.reset(); } else { - auto driver = std::make_shared(display_config); - if (driver->initialize()) { - display_driver_ = std::move(driver); + // espp-driver path (ILI9881C / EK79007): send the panel controller's vendor + // init sequence over DBI (command mode), before starting the DPI video + // stream. + espp::display_drivers::Config display_config{ + .panel_io = nullptr, + .write_command = std::bind_front(&Esp32P4Nano::dsi_write_command, this), + // NOTE: the Waveshare ESP32-P4 panels do not reliably support MIPI-DSI DCS + // reads (bus turn-around); the ESP-IDF HAL busy-waits on the read, which + // hangs panel init and trips the task watchdog. Do not provide a + // read_command so the driver skips the optional panel-ID read. + .read_command = nullptr, + .lcd_send_lines = nullptr, + .reset_pin = GPIO_NUM_NC, + .data_command_pin = GPIO_NUM_NC, + .reset_value = false, + .invert_colors = invert_colors, + .swap_color_order = swap_color_order, + .offset_x = 0, + .offset_y = 0, + .swap_xy = swap_xy, + .mirror_x = mirror_x, + .mirror_y = mirror_y, + .mirror_portrait = false, + }; + + display_driver_.reset(); + if (display_controller_ == DisplayController::ILI9881C) { + auto driver = std::make_shared(display_config); + if (driver->initialize()) { + display_driver_ = std::move(driver); + } + } else { + auto driver = std::make_shared(display_config); + if (driver->initialize()) { + display_driver_ = std::move(driver); + } + } + if (!display_driver_) { + logger_.error("Failed to initialize {} display controller", get_display_controller_name()); + return false; } - } - if (!display_driver_) { - logger_.error("Failed to initialize {} display controller", get_display_controller_name()); - return false; - } - // Create the DPI (video) panel with the configured panel's timing. This must - // come AFTER the vendor init sequence above: esp_lcd_new_panel_dpi() starts the - // HS video stream, and once it is running the DSI cannot drain the low-power - // command FIFO, so a long init sequence (e.g. ILI9881C's 202 commands) would - // overflow it and hang. - if (lcd_handles_.panel == nullptr) { - esp_lcd_dpi_panel_config_t dpi_cfg{}; - memset(&dpi_cfg, 0, sizeof(dpi_cfg)); - dpi_cfg.virtual_channel = 0; - dpi_cfg.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT; - dpi_cfg.dpi_clock_freq_mhz = panel_params_.dpi_clock_freq_mhz; + // Create the DPI (video) panel with the configured panel's timing. This must + // come AFTER the vendor init sequence above: esp_lcd_new_panel_dpi() starts the + // HS video stream, and once it is running the DSI cannot drain the low-power + // command FIFO, so a long init sequence (e.g. ILI9881C's 202 commands) would + // overflow it and hang. + if (lcd_handles_.panel == nullptr) { + esp_lcd_dpi_panel_config_t dpi_cfg{}; + memset(&dpi_cfg, 0, sizeof(dpi_cfg)); + dpi_cfg.virtual_channel = 0; + dpi_cfg.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT; + dpi_cfg.dpi_clock_freq_mhz = panel_params_.dpi_clock_freq_mhz; #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) - dpi_cfg.in_color_format = LCD_COLOR_FMT_RGB565; - dpi_cfg.out_color_format = LCD_COLOR_FMT_RGB565; + dpi_cfg.in_color_format = LCD_COLOR_FMT_RGB565; + dpi_cfg.out_color_format = LCD_COLOR_FMT_RGB565; #else - dpi_cfg.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; - dpi_cfg.flags.use_dma2d = true; + dpi_cfg.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; + dpi_cfg.flags.use_dma2d = true; #endif - dpi_cfg.num_fbs = 1; - dpi_cfg.video_timing.h_size = display_width_; - dpi_cfg.video_timing.v_size = display_height_; - dpi_cfg.video_timing.hsync_pulse_width = panel_params_.hsync_pulse_width; - dpi_cfg.video_timing.hsync_back_porch = panel_params_.hsync_back_porch; - dpi_cfg.video_timing.hsync_front_porch = panel_params_.hsync_front_porch; - dpi_cfg.video_timing.vsync_pulse_width = panel_params_.vsync_pulse_width; - dpi_cfg.video_timing.vsync_back_porch = panel_params_.vsync_back_porch; - dpi_cfg.video_timing.vsync_front_porch = panel_params_.vsync_front_porch; - logger_.info("Creating DPI panel ({}x{} @ {} MHz)", dpi_cfg.video_timing.h_size, - dpi_cfg.video_timing.v_size, dpi_cfg.dpi_clock_freq_mhz); - ret = esp_lcd_new_panel_dpi(lcd_handles_.mipi_dsi_bus, &dpi_cfg, &lcd_handles_.panel); + dpi_cfg.num_fbs = 1; + dpi_cfg.video_timing.h_size = display_width_; + dpi_cfg.video_timing.v_size = display_height_; + dpi_cfg.video_timing.hsync_pulse_width = panel_params_.hsync_pulse_width; + dpi_cfg.video_timing.hsync_back_porch = panel_params_.hsync_back_porch; + dpi_cfg.video_timing.hsync_front_porch = panel_params_.hsync_front_porch; + dpi_cfg.video_timing.vsync_pulse_width = panel_params_.vsync_pulse_width; + dpi_cfg.video_timing.vsync_back_porch = panel_params_.vsync_back_porch; + dpi_cfg.video_timing.vsync_front_porch = panel_params_.vsync_front_porch; + logger_.info("Creating DPI panel ({}x{} @ {} MHz)", dpi_cfg.video_timing.h_size, + dpi_cfg.video_timing.v_size, dpi_cfg.dpi_clock_freq_mhz); + ret = esp_lcd_new_panel_dpi(lcd_handles_.mipi_dsi_bus, &dpi_cfg, &lcd_handles_.panel); + if (ret != ESP_OK) { + logger_.error("Failed to create MIPI DSI DPI panel: {}", esp_err_to_name(ret)); + return false; + } + // NOTE: deliberately do NOT enable DMA2D for the DPI panel. DMA2D is a + // color-processing engine, not a plain copy: routing the LVGL flush + // (esp_lcd_panel_draw_bitmap) through it corrupts the RGB565 channel order, + // while the plain CPU copy path renders correctly. + } + + // Low-level panel init (starts the DPI video stream) + ret = lcd_handles_.panel->init(lcd_handles_.panel); if (ret != ESP_OK) { - logger_.error("Failed to create MIPI DSI DPI panel: {}", esp_err_to_name(ret)); + logger_.error("Low-level panel init failed: {}", esp_err_to_name(ret)); return false; } - // NOTE: deliberately do NOT enable DMA2D for the DPI panel. DMA2D is a - // color-processing engine, not a plain copy: routing the LVGL flush - // (esp_lcd_panel_draw_bitmap) through it corrupts the RGB565 channel order, - // while the plain CPU copy path renders correctly. - } - - // Low-level panel init (starts the DPI video stream) - ret = lcd_handles_.panel->init(lcd_handles_.panel); - if (ret != ESP_OK) { - logger_.error("Low-level panel init failed: {}", esp_err_to_name(ret)); - return false; } // Note: the raw MIPI-DSI DPI panel does not implement disp_on_off (the panel @@ -203,8 +275,17 @@ bool Esp32P4Nano::initialize_lcd() { void Esp32P4Nano::apply_panel_params(DisplayController controller) { display_controller_ = (controller == DisplayController::UNKNOWN) ? default_controller_ : controller; - panel_params_ = - (display_controller_ == DisplayController::ILI9881C) ? ILI9881C_PARAMS : EK79007_PARAMS; + switch (display_controller_) { + case DisplayController::JD9365: + panel_params_ = JD9365_PARAMS; + break; + case DisplayController::ILI9881C: + panel_params_ = ILI9881C_PARAMS; + break; + default: + panel_params_ = EK79007_PARAMS; + break; + } display_width_ = panel_params_.width; display_height_ = panel_params_.height; } @@ -297,15 +378,43 @@ void Esp32P4Nano::write_lcd_lines(int xs, int ys, int xe, int ye, const uint8_t void Esp32P4Nano::brightness(float brightness) { // The ESP32-P4-NANO has NO backlight GPIO. The backlight is driven by an - // on-board I2C controller (addr ~0x45/0x95, chip/protocol not yet identified); - // brightness control is a TODO pending hardware identification. The panel powers - // up with the backlight on. For now this is best-effort: store the requested - // value so brightness() reads back what was set, and log it. + // on-board I2C controller at address 0x45. On the 10.1" JD9365 panel the + // brightness register is 0x86 (value 0-255); the "A"-series panels use 0x96 + // instead. For panels other than the JD9365 the chip/protocol has not been + // verified, so this remains best-effort: store the requested value so + // brightness() reads back what was set, and log it. brightness = std::clamp(brightness, 0.0f, 100.0f); brightness_ = brightness; - logger_.debug("brightness({}) requested; no backlight GPIO on this board (on-board I2C " - "controller not yet identified), value stored only", - brightness); + if (display_controller_ != DisplayController::JD9365) { + logger_.debug("brightness({}) requested; no backlight GPIO on this board (on-board I2C " + "backlight protocol not verified for this panel), value stored only", + brightness); + return; + } + // Lazily create the backlight I2C device on the internal bus. + if (!backlight_i2c_device_) { + std::error_code ec; + backlight_i2c_device_ = internal_i2c_.add_device( + { + .device_address = backlight_i2c_address, + .timeout_ms = static_cast(internal_i2c_.config().timeout_ms), + .scl_speed_hz = internal_i2c_.config().clk_speed, + .log_level = espp::Logger::Verbosity::WARN, + }, + ec); + if (!backlight_i2c_device_) { + logger_.error("Could not initialize backlight I2C device (0x{:02X}): {}", + backlight_i2c_address, ec.message()); + return; + } + } + // Register 0x86 is the brightness register for the 10.1" JD9365 panel (the + // "A"-series panels use 0x96), value 0-255. + const uint8_t data[2] = {0x86, static_cast(255.0f * brightness / 100.0f)}; + std::error_code ec; + if (!backlight_i2c_device_->write(data, sizeof(data), ec)) { + logger_.error("Failed to write backlight brightness: {}", ec.message()); + } } float Esp32P4Nano::brightness() const { return brightness_.load(); } From f48212dade793256eb6ef739e8a185d620a613d6 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Thu, 6 Aug 2026 11:51:26 -0500 Subject: [PATCH 05/13] fix(esp32-p4): Espp-native JD9365 driver; single I2C owner fixes touch/audio/backlight The waveshare/esp_lcd_jd9365 component brought the display up but internally creates its own I2C bus on hardcoded SDA=7/SCL=8 (I2C_NUM_1) for the panel's power controller at 0x45 - and never deletes it (the cleanup is commented out in its source). That second master on the BSP's own I2C pins broke every subsequent transaction on the BSP bus: GT911 probe timeouts, ES8311 and backlight writes failing with ESP_ERR_INVALID_RESPONSE. Replace the component with an espp-native driver so exactly one I2C owner exists: - New espp::Jd9365 (display_drivers/jd9365.hpp), modeled on espp::Ili9881, with the component's active 200-entry vendor init table converted verbatim (attributed, Apache-2.0), the same no-GPIO reset (DCS SWRESET + 120 ms), and the same DCS order (user page, MADCTL, COLMOD, 2-lane config, table, SLPOUT/DISPON). No DCS reads. - BSPs run the panel power-on sequence over their own internal_i2c_ before DSI init (0x45: 0x95=0x11, 0x95=0x17, 0x96=0x00, 100 ms, 0x96=0xFF, 1 s), then the espp-driver path with the identical DPI timing; on IDF >= 6.0 the JD9365 path also enables DMA2D like the vendor component did. - brightness() now writes the correct backlight register 0x96 (0-255); 0x86 is used by other panel revisions in Waveshare's BSP. - Drop the waveshare/esp_lcd_jd9365 dependency (and transitively i2c_bus). Applies to both esp32-p4-nano and esp32-p4-eth; builds green for esp32p4 on ESP-IDF 6.0. Display verified on hardware with the vendor flow; this preserves that flow byte-for-byte while unbreaking I2C. Hardware re-test pending. Co-Authored-By: Claude Opus 4.8 (1M context) --- components/display_drivers/include/jd9365.hpp | 351 ++++++++++++++++++ components/esp32-p4-eth/Kconfig.projbuild | 4 +- components/esp32-p4-eth/idf_component.yml | 3 - .../esp32-p4-eth/include/esp32-p4-eth.hpp | 9 +- components/esp32-p4-eth/src/video.cpp | 298 ++++++++------- components/esp32-p4-nano/Kconfig.projbuild | 4 +- components/esp32-p4-nano/idf_component.yml | 3 - .../esp32-p4-nano/include/esp32-p4-nano.hpp | 9 +- components/esp32-p4-nano/src/video.cpp | 298 ++++++++------- doc/Doxyfile | 1 + 10 files changed, 654 insertions(+), 326 deletions(-) create mode 100644 components/display_drivers/include/jd9365.hpp diff --git a/components/display_drivers/include/jd9365.hpp b/components/display_drivers/include/jd9365.hpp new file mode 100644 index 000000000..1e8af842f --- /dev/null +++ b/components/display_drivers/include/jd9365.hpp @@ -0,0 +1,351 @@ +#pragma once + +#include + +#include "display_drivers.hpp" + +namespace espp { +/** + * @brief Display driver for the JD9365 MIPI-DSI display controller. + * + * The JD9365 (e.g. the Waveshare 10.1" 800x1280 DSI panel used with the + * ESP32-P4-ETH / ESP32-P4-NANO boards) is configured over the MIPI-DSI DBI + * (command) channel and then driven by the DPI video stream. This follows the + * same interface as the other espp display drivers and relies on a lower-level + * transport to execute write_command. + * + * The vendor initialization sequence (the page-selected register writes below) + * is taken verbatim from the default init table + * (`vendor_specific_init_default[]`) of Waveshare's esp_lcd_jd9365 v2.0.0 + * managed component (Apache-2.0, Copyright 2024 Espressif Systems (Shanghai) + * CO LTD), as is the command ordering around it (software reset; user page / + * MADCTL / COLMOD / DSI lane config; vendor table ending in Sleep-Out + + * Display-On). + */ +class Jd9365 : public display_drivers::MipiDbiDisplayDriver { + // JD9365 MADCTL mirror bits (gate scan / source scan direction) + static constexpr uint8_t GS_BIT = 1 << 0; ///< Gate scan direction -> mirror x + static constexpr uint8_t SS_BIT = 1 << 1; ///< Source scan direction -> mirror y + +public: + enum class Command : uint8_t { + nop = 0x00, ///< No Operation + swreset = 0x01, ///< Software Reset + sleep_in = 0x10, ///< Sleep In + sleep_out = 0x11, ///< Sleep Out + invert_off = 0x20, ///< Display Inversion Off + invert_on = 0x21, ///< Display Inversion On + display_off = 0x28, ///< Display Off + display_on = 0x29, ///< Display On + caset = 0x2A, ///< Column Address Set + raset = 0x2B, ///< Row Address Set + ramwr = 0x2C, ///< Memory Write + madctl = 0x36, ///< Memory Data Access Control + colmod = 0x3A, ///< Pixel Format Set + dsi_int0 = 0x80, ///< DSI interface config (lane count), on the user page + page_select = 0xE0, ///< Command page select (0x00 = user page) + }; + + /// Values for the dsi_int0 (0x80) DSI lane-count command + static constexpr uint8_t DSI_1_LANE = 0x00; + static constexpr uint8_t DSI_2_LANE = 0x01; + static constexpr uint8_t DSI_3_LANE = 0x10; + static constexpr uint8_t DSI_4_LANE = 0x11; + + /// Value for page_select (0xE0) selecting the user command page + static constexpr uint8_t PAGE_USER = 0x00; + + explicit Jd9365(const display_drivers::Config &config) + : MipiDbiDisplayDriver(config, + {.column_address_command = static_cast(Command::caset), + .row_address_command = static_cast(Command::raset), + .memory_write_command = static_cast(Command::ramwr)}) {} + + bool initialize() override { + using namespace std::chrono_literals; + display_drivers::init_pins(config_.reset_pin, config_.data_command_pin, config_.reset_value); + + // Neither supported board routes a panel reset GPIO, so do what the vendor + // component's panel_jd9365_reset() does in that case: a DCS software reset + // followed by a 120 ms delay. + if (config_.reset_pin == GPIO_NUM_NC) { + write_command(static_cast(Command::swreset), {}, 0); + std::this_thread::sleep_for(120ms); + } + + auto madctl = make_madctl(DisplayRotation::LANDSCAPE); + + uint8_t colmod = 0x55; + switch (config_.bits_per_pixel) { + case 16: // RGB565 + colmod = 0x55; + break; + case 18: // RGB666 + colmod = 0x66; + break; + case 24: // RGB888 + colmod = 0x77; + break; + default: + break; + } + + auto init_commands = std::to_array>({ + // Select the user command page, then MADCTL / COLMOD / DSI lane count + // (2 data lanes), in the same order the vendor component's + // panel_jd9365_init() sends them before the vendor init table. + {static_cast(Command::page_select), {PAGE_USER}, 0}, + {static_cast(Command::madctl), {madctl}, 0}, + {static_cast(Command::colmod), {colmod}, 0}, + {static_cast(Command::dsi_int0), {DSI_2_LANE}, 0}, + + // Vendor init table (verbatim from Waveshare's esp_lcd_jd9365 v2.0.0 + // vendor_specific_init_default[], default 800x1280 10.1" panel table). + {0xE0, {0x00}, 0}, + {0xE1, {0x93}, 0}, + {0xE2, {0x65}, 0}, + {0xE3, {0xF8}, 0}, + {0x80, {0x01}, 0}, + + // Select command page 1 + {0xE0, {0x01}, 0}, + {0x00, {0x00}, 0}, + {0x01, {0x38}, 0}, + {0x03, {0x10}, 0}, + {0x04, {0x38}, 0}, + + {0x0C, {0x74}, 0}, + + {0x17, {0x00}, 0}, + {0x18, {0xAF}, 0}, + {0x19, {0x00}, 0}, + {0x1A, {0x00}, 0}, + {0x1B, {0xAF}, 0}, + {0x1C, {0x00}, 0}, + + {0x35, {0x26}, 0}, + + {0x37, {0x09}, 0}, + + {0x38, {0x04}, 0}, + {0x39, {0x00}, 0}, + {0x3A, {0x01}, 0}, + {0x3C, {0x78}, 0}, + {0x3D, {0xFF}, 0}, + {0x3E, {0xFF}, 0}, + {0x3F, {0x7F}, 0}, + + {0x40, {0x06}, 0}, + {0x41, {0xA0}, 0}, + {0x42, {0x81}, 0}, + {0x43, {0x1E}, 0}, + {0x44, {0x0D}, 0}, + {0x45, {0x28}, 0}, + // SKIP: //{0x4A, (uint8_t[]){0x35}, 1, 0},//bist + + {0x55, {0x02}, 0}, + {0x57, {0x69}, 0}, + {0x59, {0x0A}, 0}, + {0x5A, {0x2A}, 0}, + {0x5B, {0x17}, 0}, + + {0x5D, {0x7F}, 0}, + {0x5E, {0x6A}, 0}, + {0x5F, {0x5B}, 0}, + {0x60, {0x4F}, 0}, + {0x61, {0x4A}, 0}, + {0x62, {0x3D}, 0}, + {0x63, {0x41}, 0}, + {0x64, {0x2A}, 0}, + {0x65, {0x44}, 0}, + {0x66, {0x43}, 0}, + {0x67, {0x44}, 0}, + {0x68, {0x62}, 0}, + {0x69, {0x52}, 0}, + {0x6A, {0x59}, 0}, + {0x6B, {0x4C}, 0}, + {0x6C, {0x48}, 0}, + {0x6D, {0x3A}, 0}, + {0x6E, {0x26}, 0}, + {0x6F, {0x00}, 0}, + {0x70, {0x7F}, 0}, + {0x71, {0x6A}, 0}, + {0x72, {0x5B}, 0}, + {0x73, {0x4F}, 0}, + {0x74, {0x4A}, 0}, + {0x75, {0x3D}, 0}, + {0x76, {0x41}, 0}, + {0x77, {0x2A}, 0}, + {0x78, {0x44}, 0}, + {0x79, {0x43}, 0}, + {0x7A, {0x44}, 0}, + {0x7B, {0x62}, 0}, + {0x7C, {0x52}, 0}, + {0x7D, {0x59}, 0}, + {0x7E, {0x4C}, 0}, + {0x7F, {0x48}, 0}, + {0x80, {0x3A}, 0}, + {0x81, {0x26}, 0}, + {0x82, {0x00}, 0}, + + // Select command page 2 + {0xE0, {0x02}, 0}, + {0x00, {0x42}, 0}, + {0x01, {0x42}, 0}, + {0x02, {0x40}, 0}, + {0x03, {0x40}, 0}, + {0x04, {0x5E}, 0}, + {0x05, {0x5E}, 0}, + {0x06, {0x5F}, 0}, + {0x07, {0x5F}, 0}, + {0x08, {0x5F}, 0}, + {0x09, {0x57}, 0}, + {0x0A, {0x57}, 0}, + {0x0B, {0x77}, 0}, + {0x0C, {0x77}, 0}, + {0x0D, {0x47}, 0}, + {0x0E, {0x47}, 0}, + {0x0F, {0x45}, 0}, + {0x10, {0x45}, 0}, + {0x11, {0x4B}, 0}, + {0x12, {0x4B}, 0}, + {0x13, {0x49}, 0}, + {0x14, {0x49}, 0}, + {0x15, {0x5F}, 0}, + + {0x16, {0x41}, 0}, + {0x17, {0x41}, 0}, + {0x18, {0x40}, 0}, + {0x19, {0x40}, 0}, + {0x1A, {0x5E}, 0}, + {0x1B, {0x5E}, 0}, + {0x1C, {0x5F}, 0}, + {0x1D, {0x5F}, 0}, + {0x1E, {0x5F}, 0}, + {0x1F, {0x57}, 0}, + {0x20, {0x57}, 0}, + {0x21, {0x77}, 0}, + {0x22, {0x77}, 0}, + {0x23, {0x46}, 0}, + {0x24, {0x46}, 0}, + {0x25, {0x44}, 0}, + {0x26, {0x44}, 0}, + {0x27, {0x4A}, 0}, + {0x28, {0x4A}, 0}, + {0x29, {0x48}, 0}, + {0x2A, {0x48}, 0}, + {0x2B, {0x5F}, 0}, + + {0x2C, {0x01}, 0}, + {0x2D, {0x01}, 0}, + {0x2E, {0x00}, 0}, + {0x2F, {0x00}, 0}, + {0x30, {0x1F}, 0}, + {0x31, {0x1F}, 0}, + {0x32, {0x1E}, 0}, + {0x33, {0x1E}, 0}, + {0x34, {0x1F}, 0}, + {0x35, {0x17}, 0}, + {0x36, {0x17}, 0}, + {0x37, {0x37}, 0}, + {0x38, {0x37}, 0}, + {0x39, {0x08}, 0}, + {0x3A, {0x08}, 0}, + {0x3B, {0x0A}, 0}, + {0x3C, {0x0A}, 0}, + {0x3D, {0x04}, 0}, + {0x3E, {0x04}, 0}, + {0x3F, {0x06}, 0}, + {0x40, {0x06}, 0}, + {0x41, {0x1F}, 0}, + + {0x42, {0x02}, 0}, + {0x43, {0x02}, 0}, + {0x44, {0x00}, 0}, + {0x45, {0x00}, 0}, + {0x46, {0x1F}, 0}, + {0x47, {0x1F}, 0}, + {0x48, {0x1E}, 0}, + {0x49, {0x1E}, 0}, + {0x4A, {0x1F}, 0}, + {0x4B, {0x17}, 0}, + {0x4C, {0x17}, 0}, + {0x4D, {0x37}, 0}, + {0x4E, {0x37}, 0}, + {0x4F, {0x09}, 0}, + {0x50, {0x09}, 0}, + {0x51, {0x0B}, 0}, + {0x52, {0x0B}, 0}, + {0x53, {0x05}, 0}, + {0x54, {0x05}, 0}, + {0x55, {0x07}, 0}, + {0x56, {0x07}, 0}, + {0x57, {0x1F}, 0}, + + {0x58, {0x40}, 0}, + {0x5B, {0x30}, 0}, + {0x5C, {0x00}, 0}, + {0x5D, {0x34}, 0}, + {0x5E, {0x05}, 0}, + {0x5F, {0x02}, 0}, + {0x63, {0x00}, 0}, + {0x64, {0x6A}, 0}, + {0x67, {0x73}, 0}, + {0x68, {0x07}, 0}, + {0x69, {0x08}, 0}, + {0x6A, {0x6A}, 0}, + {0x6B, {0x08}, 0}, + + {0x6C, {0x00}, 0}, + {0x6D, {0x00}, 0}, + {0x6E, {0x00}, 0}, + {0x6F, {0x88}, 0}, + + {0x75, {0xFF}, 0}, + {0x77, {0xDD}, 0}, + {0x78, {0x2C}, 0}, + {0x79, {0x15}, 0}, + {0x7A, {0x17}, 0}, + {0x7D, {0x14}, 0}, + {0x7E, {0x82}, 0}, + + // Select command page 4 + {0xE0, {0x04}, 0}, + {0x00, {0x0E}, 0}, + {0x02, {0xB3}, 0}, + {0x09, {0x61}, 0}, + {0x0E, {0x48}, 0}, + {0x37, {0x58}, 0}, + {0x2B, {0x0F}, 0}, + + // Select command page 0 (user page) + {0xE0, {0x00}, 0}, + + {0xE6, {0x02}, 0}, + {0xE7, {0x0C}, 0}, + + {static_cast(Command::sleep_out), {0x00}, 120}, // Sleep-Out (+120 ms) + + {static_cast(Command::display_on), {0x00}, 20}, // Display-On (+20 ms) + }); + + send_commands(init_commands); + return true; + } + + void set_rotation(const DisplayRotation &rotation) override { + Controller::set_rotation(rotation); + auto data = std::array{make_madctl(rotation)}; + auto page_user = std::array{PAGE_USER}; + std::scoped_lock lock(io_mutex_); + write_command(static_cast(Command::page_select), page_user, 0); + write_command(static_cast(Command::madctl), data, 0); + } + +private: + uint8_t make_madctl(DisplayRotation rotation) const { + auto value = display_drivers::make_madctl_base(config_, LCD_CMD_BGR_BIT, GS_BIT, SS_BIT, 0); + return display_drivers::apply_standard_rotation(value, config_, rotation, GS_BIT, SS_BIT, 0); + } +}; +} // namespace espp diff --git a/components/esp32-p4-eth/Kconfig.projbuild b/components/esp32-p4-eth/Kconfig.projbuild index f73890a51..559c9c4f7 100644 --- a/components/esp32-p4-eth/Kconfig.projbuild +++ b/components/esp32-p4-eth/Kconfig.projbuild @@ -12,8 +12,8 @@ menu "ESP32-P4-ETH Configuration" bool "10.1\" 800x1280 (JD9365) - the panel Waveshare sells for this board" help The Waveshare 10.1-inch 800x1280 DSI panel, which uses a JD9365 - controller. Driven by Waveshare's esp_lcd_jd9365 managed component - (vendor init + DPI panel). Reset is handled over DSI (no reset GPIO). + controller, driven by the espp::Jd9365 display driver (vendor init + over DSI/DBI + DPI panel). Reset is handled over DSI (no reset GPIO). The backlight is driven by an on-board I2C controller (addr 0x45), so there is no backlight GPIO; brightness() writes the controller. diff --git a/components/esp32-p4-eth/idf_component.yml b/components/esp32-p4-eth/idf_component.yml index 2c1f7084c..e3edb9c0c 100644 --- a/components/esp32-p4-eth/idf_component.yml +++ b/components/esp32-p4-eth/idf_component.yml @@ -31,9 +31,6 @@ dependencies: espp/gt911: ">=1.0" espp/input_drivers: ">=1.0" espp/interrupt: ">=1.0" - # The Waveshare 10.1" 800x1280 panel for this board is a JD9365, driven by - # Waveshare's managed panel component (vendor init + DPI panel creation). - waveshare/esp_lcd_jd9365: "^2.0.0" # MIPI-CSI camera pipeline: esp_video provides the V4L2 capture framework # (CSI + ISP) and esp_cam_sensor provides the OV5647 sensor driver. espressif/esp_video: ">=2.0" diff --git a/components/esp32-p4-eth/include/esp32-p4-eth.hpp b/components/esp32-p4-eth/include/esp32-p4-eth.hpp index 549b0b253..c48f3504f 100644 --- a/components/esp32-p4-eth/include/esp32-p4-eth.hpp +++ b/components/esp32-p4-eth/include/esp32-p4-eth.hpp @@ -32,6 +32,7 @@ #include "i2c.hpp" #include "ili9881.hpp" #include "interrupt.hpp" +#include "jd9365.hpp" #include "task.hpp" #include "touchpad_input.hpp" @@ -493,10 +494,8 @@ class Esp32P4Eth : public BaseComponent { static constexpr PanelParams ILI9881C_PARAMS{800, 1280, 80, 1500, GPIO_NUM_NC, GPIO_NUM_NC, 40, 140, 40, 4, 16, 16}; // JD9365 10.1" 800x1280 (the panel Waveshare sells for this board; reset over - // DSI, no backlight GPIO). NOTE: on the JD9365 path the DPI timing actually - // used comes from the esp_lcd_jd9365 component's - // JD9365_800_1280_PANEL_60HZ_DPI_CONFIG macro; these timing fields are - // informational. The lane bit rate (1500 Mbps) IS used for the DSI bus. + // DSI, no backlight GPIO). Timing matches Waveshare's + // JD9365_800_1280_PANEL_60HZ_DPI_CONFIG vendor timing. static constexpr PanelParams JD9365_PARAMS{800, 1280, 80, 1500, GPIO_NUM_NC, GPIO_NUM_NC, 20, 20, 40, 4, 10, 30}; @@ -654,7 +653,7 @@ class Esp32P4Eth : public BaseComponent { // Display state (MIPI-DSI). NOTE: there is no backlight GPIO / espp::Led on // this board; the backlight is driven by an on-board I2C controller. On the // 10.1" JD9365 panel brightness() writes that controller (addr 0x45, reg - // 0x86); on other panels the stored brightness is best-effort only (see + // 0x96); on other panels the stored brightness is best-effort only (see // src/video.cpp). ///////////////////////////////////////////////////////////////////////////// // On-board I2C backlight controller (10.1" JD9365 panel) diff --git a/components/esp32-p4-eth/src/video.cpp b/components/esp32-p4-eth/src/video.cpp index 14d015f3d..796e0de30 100644 --- a/components/esp32-p4-eth/src/video.cpp +++ b/components/esp32-p4-eth/src/video.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -21,10 +22,6 @@ #include #include -// Waveshare's managed panel component for the JD9365 (the controller on the -// 10.1" 800x1280 panel Waveshare sells for this board). -#include "esp_lcd_jd9365.h" - using namespace std::chrono_literals; namespace espp { @@ -55,6 +52,47 @@ bool Esp32P4Eth::initialize_lcd() { // the panel is reset over DSI during its vendor init sequence, so there is no // hardware GPIO reset step here (unlike the ESP32-P4-Function-EV-Board). + // The 10.1" JD9365 panel is powered/reset and backlit by an on-board I2C + // controller (addr 0x45) on the BSP's internal I2C bus: register 0x95 is the + // panel power/reset control and 0x96 the backlight level (0-255) on this + // panel's controller. This power-on sequence (values and delays) matches + // Waveshare's vendor panel component and must run before any DSI traffic + // so the panel is powered and out of reset when the vendor init sequence is + // sent. + if (display_controller_ == DisplayController::JD9365) { + // Lazily create the backlight/panel-power I2C device on the internal bus + // (shared with brightness()). + if (!backlight_i2c_device_) { + std::error_code ec; + backlight_i2c_device_ = internal_i2c_.add_device( + { + .device_address = backlight_i2c_address, + .timeout_ms = static_cast(internal_i2c_.config().timeout_ms), + .scl_speed_hz = internal_i2c_.config().clk_speed, + .log_level = espp::Logger::Verbosity::WARN, + }, + ec); + if (!backlight_i2c_device_) { + logger_.error("Could not initialize panel power/backlight I2C device (0x{:02X}): {}", + backlight_i2c_address, ec.message()); + return false; + } + } + auto write_panel_reg = [this](uint8_t reg, uint8_t value) { + const uint8_t data[2] = {reg, value}; + std::error_code ec; + if (!backlight_i2c_device_->write(data, sizeof(data), ec)) { + logger_.error("Failed to write panel power controller reg 0x{:02X}: {}", reg, ec.message()); + } + }; + write_panel_reg(0x95, 0x11); // panel power/reset control + write_panel_reg(0x95, 0x17); // panel power/reset control + write_panel_reg(0x96, 0x00); // backlight off while powering up + std::this_thread::sleep_for(100ms); + write_panel_reg(0x96, 0xFF); // backlight full on + std::this_thread::sleep_for(1000ms); + } + // Create the MIPI DSI bus (also initializes the DSI PHY) if (lcd_handles_.mipi_dsi_bus == nullptr) { logger_.info("Creating MIPI DSI bus ({} lanes, {} Mbps/lane)", mipi_dsi_lanes, @@ -92,165 +130,118 @@ bool Esp32P4Eth::initialize_lcd() { // NOTE: The ESP32-P4-ETH has no backlight GPIO. The backlight is driven by an // on-board I2C controller (addr 0x45). On the 10.1" JD9365 panel brightness() - // writes that controller (reg 0x86); on the other panels the panel powers up + // writes that controller (reg 0x96); on the other panels the panel powers up // with the backlight on and brightness() only stores the value. No espp::Led / // PWM backlight is instantiated here. brightness(100.0f); + // espp-driver path (JD9365 / ILI9881C / EK79007): send the panel + // controller's vendor init sequence over DBI (command mode), before starting + // the DPI video stream. + espp::display_drivers::Config display_config{ + .panel_io = nullptr, + .write_command = std::bind_front(&Esp32P4Eth::dsi_write_command, this), + // NOTE: the Waveshare ESP32-P4 panels do not reliably support MIPI-DSI DCS + // reads (bus turn-around); the ESP-IDF HAL busy-waits on the read, which + // hangs panel init and trips the task watchdog. Do not provide a + // read_command so the driver skips the optional panel-ID read. + .read_command = nullptr, + .lcd_send_lines = nullptr, + .reset_pin = GPIO_NUM_NC, + .data_command_pin = GPIO_NUM_NC, + .reset_value = false, + .invert_colors = invert_colors, + .swap_color_order = swap_color_order, + .offset_x = 0, + .offset_y = 0, + .swap_xy = swap_xy, + .mirror_x = mirror_x, + .mirror_y = mirror_y, + .mirror_portrait = false, + }; + + display_driver_.reset(); if (display_controller_ == DisplayController::JD9365) { - // The Waveshare 10.1" 800x1280 panel is a JD9365, driven by Waveshare's - // esp_lcd_jd9365 managed component. The component sends the vendor init - // sequence over the DBI IO and creates/starts the DPI (video) panel - // internally: the handle it returns IS the real DPI panel handle (it - // patches the DPI panel's vtable and enables DMA2D itself), so - // esp_lcd_panel_draw_bitmap() and the DPI event callbacks below work on it - // directly. Do NOT create a DPI panel or run an espp display driver here. - if (lcd_handles_.panel == nullptr) { - // Values from the component's JD9365_800_1280_PANEL_60HZ_DPI_CONFIG macro; - // spelled out field-by-field because the macro's C designated-initializer - // ordering is not valid C++. - esp_lcd_dpi_panel_config_t dpi_config{}; - dpi_config.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT; - dpi_config.dpi_clock_freq_mhz = 80; - dpi_config.virtual_channel = 0; -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) - dpi_config.in_color_format = LCD_COLOR_FMT_RGB565; - dpi_config.out_color_format = LCD_COLOR_FMT_RGB565; -#else - dpi_config.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; - dpi_config.flags.use_dma2d = true; -#endif - dpi_config.num_fbs = 1; - dpi_config.video_timing.h_size = 800; - dpi_config.video_timing.v_size = 1280; - dpi_config.video_timing.hsync_back_porch = 20; - dpi_config.video_timing.hsync_pulse_width = 20; - dpi_config.video_timing.hsync_front_porch = 40; - dpi_config.video_timing.vsync_back_porch = 10; - dpi_config.video_timing.vsync_pulse_width = 4; - dpi_config.video_timing.vsync_front_porch = 30; - jd9365_vendor_config_t vendor_config{}; - vendor_config.init_cmds = nullptr; // use the component's default init sequence - vendor_config.init_cmds_size = 0; - vendor_config.mipi_config.dsi_bus = lcd_handles_.mipi_dsi_bus; - vendor_config.mipi_config.dpi_config = &dpi_config; - vendor_config.mipi_config.lane_num = mipi_dsi_lanes; - esp_lcd_panel_dev_config_t lcd_dev_config{}; - lcd_dev_config.reset_gpio_num = GPIO_NUM_NC; // reset over DSI, no reset GPIO - lcd_dev_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; - lcd_dev_config.bits_per_pixel = 16; - lcd_dev_config.vendor_config = &vendor_config; - logger_.info("Creating JD9365 panel ({}x{} @ {} MHz DPI)", display_width_, display_height_, - dpi_config.dpi_clock_freq_mhz); - ret = esp_lcd_new_panel_jd9365(lcd_handles_.io, &lcd_dev_config, &lcd_handles_.panel); - if (ret != ESP_OK) { - logger_.error("Failed to create JD9365 panel: {}", esp_err_to_name(ret)); - return false; - } - ret = esp_lcd_panel_reset(lcd_handles_.panel); - if (ret != ESP_OK) { - logger_.error("JD9365 panel reset failed: {}", esp_err_to_name(ret)); - return false; - } - // Sends the vendor init over DBI, then starts the DPI video stream. - ret = esp_lcd_panel_init(lcd_handles_.panel); - if (ret != ESP_OK) { - logger_.error("JD9365 panel init failed: {}", esp_err_to_name(ret)); - return false; - } + // The Waveshare 10.1" 800x1280 panel is a JD9365. espp::Jd9365 performs + // the DCS software reset and sends the vendor init sequence (taken from + // Waveshare's vendor panel component; see jd9365.hpp) over the DBI IO. + auto driver = std::make_shared(display_config); + if (driver->initialize()) { + display_driver_ = std::move(driver); } - // The managed component owns the panel init on this path; there is no espp - // display driver. - display_driver_.reset(); - } else { - // espp-driver path (ILI9881C / EK79007): send the panel controller's vendor - // init sequence over DBI (command mode), before starting the DPI video - // stream. - espp::display_drivers::Config display_config{ - .panel_io = nullptr, - .write_command = std::bind_front(&Esp32P4Eth::dsi_write_command, this), - // NOTE: the Waveshare ESP32-P4 panels do not reliably support MIPI-DSI DCS - // reads (bus turn-around); the ESP-IDF HAL busy-waits on the read, which - // hangs panel init and trips the task watchdog. Do not provide a - // read_command so the driver skips the optional panel-ID read. - .read_command = nullptr, - .lcd_send_lines = nullptr, - .reset_pin = GPIO_NUM_NC, - .data_command_pin = GPIO_NUM_NC, - .reset_value = false, - .invert_colors = invert_colors, - .swap_color_order = swap_color_order, - .offset_x = 0, - .offset_y = 0, - .swap_xy = swap_xy, - .mirror_x = mirror_x, - .mirror_y = mirror_y, - .mirror_portrait = false, - }; - - display_driver_.reset(); - if (display_controller_ == DisplayController::ILI9881C) { - auto driver = std::make_shared(display_config); - if (driver->initialize()) { - display_driver_ = std::move(driver); - } - } else { - auto driver = std::make_shared(display_config); - if (driver->initialize()) { - display_driver_ = std::move(driver); - } + } else if (display_controller_ == DisplayController::ILI9881C) { + auto driver = std::make_shared(display_config); + if (driver->initialize()) { + display_driver_ = std::move(driver); } - if (!display_driver_) { - logger_.error("Failed to initialize {} display controller", get_display_controller_name()); - return false; + } else { + auto driver = std::make_shared(display_config); + if (driver->initialize()) { + display_driver_ = std::move(driver); } + } + if (!display_driver_) { + logger_.error("Failed to initialize {} display controller", get_display_controller_name()); + return false; + } - // Create the DPI (video) panel with the configured panel's timing. This must - // come AFTER the vendor init sequence above: esp_lcd_new_panel_dpi() starts the - // HS video stream, and once it is running the DSI cannot drain the low-power - // command FIFO, so a long init sequence (e.g. ILI9881C's 202 commands) would - // overflow it and hang. - if (lcd_handles_.panel == nullptr) { - esp_lcd_dpi_panel_config_t dpi_cfg{}; - memset(&dpi_cfg, 0, sizeof(dpi_cfg)); - dpi_cfg.virtual_channel = 0; - dpi_cfg.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT; - dpi_cfg.dpi_clock_freq_mhz = panel_params_.dpi_clock_freq_mhz; + // Create the DPI (video) panel with the configured panel's timing. This must + // come AFTER the vendor init sequence above: esp_lcd_new_panel_dpi() starts the + // HS video stream, and once it is running the DSI cannot drain the low-power + // command FIFO, so a long init sequence (e.g. ILI9881C's 202 commands) would + // overflow it and hang. + if (lcd_handles_.panel == nullptr) { + esp_lcd_dpi_panel_config_t dpi_cfg{}; + memset(&dpi_cfg, 0, sizeof(dpi_cfg)); + dpi_cfg.virtual_channel = 0; + dpi_cfg.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT; + dpi_cfg.dpi_clock_freq_mhz = panel_params_.dpi_clock_freq_mhz; #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) - dpi_cfg.in_color_format = LCD_COLOR_FMT_RGB565; - dpi_cfg.out_color_format = LCD_COLOR_FMT_RGB565; + dpi_cfg.in_color_format = LCD_COLOR_FMT_RGB565; + dpi_cfg.out_color_format = LCD_COLOR_FMT_RGB565; #else - dpi_cfg.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; - dpi_cfg.flags.use_dma2d = true; + dpi_cfg.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; + dpi_cfg.flags.use_dma2d = true; #endif - dpi_cfg.num_fbs = 1; - dpi_cfg.video_timing.h_size = display_width_; - dpi_cfg.video_timing.v_size = display_height_; - dpi_cfg.video_timing.hsync_pulse_width = panel_params_.hsync_pulse_width; - dpi_cfg.video_timing.hsync_back_porch = panel_params_.hsync_back_porch; - dpi_cfg.video_timing.hsync_front_porch = panel_params_.hsync_front_porch; - dpi_cfg.video_timing.vsync_pulse_width = panel_params_.vsync_pulse_width; - dpi_cfg.video_timing.vsync_back_porch = panel_params_.vsync_back_porch; - dpi_cfg.video_timing.vsync_front_porch = panel_params_.vsync_front_porch; - logger_.info("Creating DPI panel ({}x{} @ {} MHz)", dpi_cfg.video_timing.h_size, - dpi_cfg.video_timing.v_size, dpi_cfg.dpi_clock_freq_mhz); - ret = esp_lcd_new_panel_dpi(lcd_handles_.mipi_dsi_bus, &dpi_cfg, &lcd_handles_.panel); + dpi_cfg.num_fbs = 1; + dpi_cfg.video_timing.h_size = display_width_; + dpi_cfg.video_timing.v_size = display_height_; + dpi_cfg.video_timing.hsync_pulse_width = panel_params_.hsync_pulse_width; + dpi_cfg.video_timing.hsync_back_porch = panel_params_.hsync_back_porch; + dpi_cfg.video_timing.hsync_front_porch = panel_params_.hsync_front_porch; + dpi_cfg.video_timing.vsync_pulse_width = panel_params_.vsync_pulse_width; + dpi_cfg.video_timing.vsync_back_porch = panel_params_.vsync_back_porch; + dpi_cfg.video_timing.vsync_front_porch = panel_params_.vsync_front_porch; + logger_.info("Creating DPI panel ({}x{} @ {} MHz)", dpi_cfg.video_timing.h_size, + dpi_cfg.video_timing.v_size, dpi_cfg.dpi_clock_freq_mhz); + ret = esp_lcd_new_panel_dpi(lcd_handles_.mipi_dsi_bus, &dpi_cfg, &lcd_handles_.panel); + if (ret != ESP_OK) { + logger_.error("Failed to create MIPI DSI DPI panel: {}", esp_err_to_name(ret)); + return false; + } + // NOTE: for the ILI9881C / EK79007 panels we deliberately do NOT enable + // DMA2D for the DPI panel. DMA2D is a color-processing engine, not a plain + // copy: routing the LVGL flush (esp_lcd_panel_draw_bitmap) through it + // corrupts the RGB565 channel order on those panels, while the plain CPU + // copy path renders correctly. The JD9365 panel renders correctly WITH + // DMA2D (and Waveshare's vendor panel component enables it), so keep it + // enabled on that path. +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + if (display_controller_ == DisplayController::JD9365) { + ret = esp_lcd_dpi_panel_enable_dma2d(lcd_handles_.panel); if (ret != ESP_OK) { - logger_.error("Failed to create MIPI DSI DPI panel: {}", esp_err_to_name(ret)); + logger_.error("Failed to enable DMA2D for the DPI panel: {}", esp_err_to_name(ret)); return false; } - // NOTE: deliberately do NOT enable DMA2D for the DPI panel. DMA2D is a - // color-processing engine, not a plain copy: routing the LVGL flush - // (esp_lcd_panel_draw_bitmap) through it corrupts the RGB565 channel order, - // while the plain CPU copy path renders correctly. } +#endif + } - // Low-level panel init (starts the DPI video stream) - ret = lcd_handles_.panel->init(lcd_handles_.panel); - if (ret != ESP_OK) { - logger_.error("Low-level panel init failed: {}", esp_err_to_name(ret)); - return false; - } + // Low-level panel init (starts the DPI video stream) + ret = lcd_handles_.panel->init(lcd_handles_.panel); + if (ret != ESP_OK) { + logger_.error("Low-level panel init failed: {}", esp_err_to_name(ret)); + return false; } // Note: the raw MIPI-DSI DPI panel does not implement disp_on_off (the panel @@ -379,10 +370,10 @@ void Esp32P4Eth::write_lcd_lines(int xs, int ys, int xe, int ye, const uint8_t * void Esp32P4Eth::brightness(float brightness) { // The ESP32-P4-ETH has NO backlight GPIO. The backlight is driven by an // on-board I2C controller at address 0x45. On the 10.1" JD9365 panel the - // brightness register is 0x86 (value 0-255); the "A"-series panels use 0x96 - // instead. For panels other than the JD9365 the chip/protocol has not been - // verified, so this remains best-effort: store the requested value so - // brightness() reads back what was set, and log it. + // brightness register is 0x96 (value 0-255); Waveshare's own BSP writes 0x86 + // instead for some panel revisions. For panels other than the JD9365 the + // chip/protocol has not been verified, so this remains best-effort: store + // the requested value so brightness() reads back what was set, and log it. brightness = std::clamp(brightness, 0.0f, 100.0f); brightness_ = brightness; if (display_controller_ != DisplayController::JD9365) { @@ -408,9 +399,10 @@ void Esp32P4Eth::brightness(float brightness) { return; } } - // Register 0x86 is the brightness register for the 10.1" JD9365 panel (the - // "A"-series panels use 0x96), value 0-255. - const uint8_t data[2] = {0x86, static_cast(255.0f * brightness / 100.0f)}; + // Register 0x96 is the brightness register (0-255) on the 10.1" JD9365 + // panel's power/backlight controller (Waveshare's BSP uses 0x86 for some + // panel revisions). + const uint8_t data[2] = {0x96, static_cast(255.0f * brightness / 100.0f)}; std::error_code ec; if (!backlight_i2c_device_->write(data, sizeof(data), ec)) { logger_.error("Failed to write backlight brightness: {}", ec.message()); diff --git a/components/esp32-p4-nano/Kconfig.projbuild b/components/esp32-p4-nano/Kconfig.projbuild index 488e97e66..ae60e3381 100644 --- a/components/esp32-p4-nano/Kconfig.projbuild +++ b/components/esp32-p4-nano/Kconfig.projbuild @@ -12,8 +12,8 @@ menu "ESP32-P4-NANO Configuration" bool "10.1\" 800x1280 (JD9365) - the panel Waveshare sells for this board" help The Waveshare 10.1-inch 800x1280 DSI panel, which uses a JD9365 - controller. Driven by Waveshare's esp_lcd_jd9365 managed component - (vendor init + DPI panel). Reset is handled over DSI (no reset GPIO). + controller, driven by the espp::Jd9365 display driver (vendor init + over DSI/DBI + DPI panel). Reset is handled over DSI (no reset GPIO). The backlight is driven by an on-board I2C controller (addr 0x45), so there is no backlight GPIO; brightness() writes the controller. diff --git a/components/esp32-p4-nano/idf_component.yml b/components/esp32-p4-nano/idf_component.yml index fb5286679..991b4ba6d 100644 --- a/components/esp32-p4-nano/idf_component.yml +++ b/components/esp32-p4-nano/idf_component.yml @@ -31,9 +31,6 @@ dependencies: espp/gt911: ">=1.0" espp/input_drivers: ">=1.0" espp/interrupt: ">=1.0" - # The Waveshare 10.1" 800x1280 panel for this board is a JD9365, driven by - # Waveshare's managed panel component (vendor init + DPI panel creation). - waveshare/esp_lcd_jd9365: "^2.0.0" # MIPI-CSI camera pipeline: esp_video provides the V4L2 capture framework # (CSI + ISP) and esp_cam_sensor provides the OV5647 sensor driver. espressif/esp_video: ">=2.0" diff --git a/components/esp32-p4-nano/include/esp32-p4-nano.hpp b/components/esp32-p4-nano/include/esp32-p4-nano.hpp index f6a9c51d8..6e54c68e7 100644 --- a/components/esp32-p4-nano/include/esp32-p4-nano.hpp +++ b/components/esp32-p4-nano/include/esp32-p4-nano.hpp @@ -32,6 +32,7 @@ #include "i2c.hpp" #include "ili9881.hpp" #include "interrupt.hpp" +#include "jd9365.hpp" #include "task.hpp" #include "touchpad_input.hpp" @@ -494,10 +495,8 @@ class Esp32P4Nano : public BaseComponent { static constexpr PanelParams ILI9881C_PARAMS{800, 1280, 80, 1500, GPIO_NUM_NC, GPIO_NUM_NC, 40, 140, 40, 4, 16, 16}; // JD9365 10.1" 800x1280 (the panel Waveshare sells for this board; reset over - // DSI, no backlight GPIO). NOTE: on the JD9365 path the DPI timing actually - // used comes from the esp_lcd_jd9365 component's - // JD9365_800_1280_PANEL_60HZ_DPI_CONFIG macro; these timing fields are - // informational. The lane bit rate (1500 Mbps) IS used for the DSI bus. + // DSI, no backlight GPIO). Timing matches Waveshare's + // JD9365_800_1280_PANEL_60HZ_DPI_CONFIG vendor timing. static constexpr PanelParams JD9365_PARAMS{800, 1280, 80, 1500, GPIO_NUM_NC, GPIO_NUM_NC, 20, 20, 40, 4, 10, 30}; @@ -655,7 +654,7 @@ class Esp32P4Nano : public BaseComponent { // Display state (MIPI-DSI). NOTE: there is no backlight GPIO / espp::Led on // this board; the backlight is driven by an on-board I2C controller. On the // 10.1" JD9365 panel brightness() writes that controller (addr 0x45, reg - // 0x86); on other panels the stored brightness is best-effort only (see + // 0x96); on other panels the stored brightness is best-effort only (see // src/video.cpp). ///////////////////////////////////////////////////////////////////////////// // On-board I2C backlight controller (10.1" JD9365 panel) diff --git a/components/esp32-p4-nano/src/video.cpp b/components/esp32-p4-nano/src/video.cpp index cab3d63b6..bf9f45ef5 100644 --- a/components/esp32-p4-nano/src/video.cpp +++ b/components/esp32-p4-nano/src/video.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -21,10 +22,6 @@ #include #include -// Waveshare's managed panel component for the JD9365 (the controller on the -// 10.1" 800x1280 panel Waveshare sells for this board). -#include "esp_lcd_jd9365.h" - using namespace std::chrono_literals; namespace espp { @@ -55,6 +52,47 @@ bool Esp32P4Nano::initialize_lcd() { // the panel is reset over DSI during its vendor init sequence, so there is no // hardware GPIO reset step here (unlike the ESP32-P4-Function-EV-Board). + // The 10.1" JD9365 panel is powered/reset and backlit by an on-board I2C + // controller (addr 0x45) on the BSP's internal I2C bus: register 0x95 is the + // panel power/reset control and 0x96 the backlight level (0-255) on this + // panel's controller. This power-on sequence (values and delays) matches + // Waveshare's vendor panel component and must run before any DSI traffic + // so the panel is powered and out of reset when the vendor init sequence is + // sent. + if (display_controller_ == DisplayController::JD9365) { + // Lazily create the backlight/panel-power I2C device on the internal bus + // (shared with brightness()). + if (!backlight_i2c_device_) { + std::error_code ec; + backlight_i2c_device_ = internal_i2c_.add_device( + { + .device_address = backlight_i2c_address, + .timeout_ms = static_cast(internal_i2c_.config().timeout_ms), + .scl_speed_hz = internal_i2c_.config().clk_speed, + .log_level = espp::Logger::Verbosity::WARN, + }, + ec); + if (!backlight_i2c_device_) { + logger_.error("Could not initialize panel power/backlight I2C device (0x{:02X}): {}", + backlight_i2c_address, ec.message()); + return false; + } + } + auto write_panel_reg = [this](uint8_t reg, uint8_t value) { + const uint8_t data[2] = {reg, value}; + std::error_code ec; + if (!backlight_i2c_device_->write(data, sizeof(data), ec)) { + logger_.error("Failed to write panel power controller reg 0x{:02X}: {}", reg, ec.message()); + } + }; + write_panel_reg(0x95, 0x11); // panel power/reset control + write_panel_reg(0x95, 0x17); // panel power/reset control + write_panel_reg(0x96, 0x00); // backlight off while powering up + std::this_thread::sleep_for(100ms); + write_panel_reg(0x96, 0xFF); // backlight full on + std::this_thread::sleep_for(1000ms); + } + // Create the MIPI DSI bus (also initializes the DSI PHY) if (lcd_handles_.mipi_dsi_bus == nullptr) { logger_.info("Creating MIPI DSI bus ({} lanes, {} Mbps/lane)", mipi_dsi_lanes, @@ -92,165 +130,118 @@ bool Esp32P4Nano::initialize_lcd() { // NOTE: The ESP32-P4-NANO has no backlight GPIO. The backlight is driven by an // on-board I2C controller (addr 0x45). On the 10.1" JD9365 panel brightness() - // writes that controller (reg 0x86); on the other panels the panel powers up + // writes that controller (reg 0x96); on the other panels the panel powers up // with the backlight on and brightness() only stores the value. No espp::Led / // PWM backlight is instantiated here. brightness(100.0f); + // espp-driver path (JD9365 / ILI9881C / EK79007): send the panel + // controller's vendor init sequence over DBI (command mode), before starting + // the DPI video stream. + espp::display_drivers::Config display_config{ + .panel_io = nullptr, + .write_command = std::bind_front(&Esp32P4Nano::dsi_write_command, this), + // NOTE: the Waveshare ESP32-P4 panels do not reliably support MIPI-DSI DCS + // reads (bus turn-around); the ESP-IDF HAL busy-waits on the read, which + // hangs panel init and trips the task watchdog. Do not provide a + // read_command so the driver skips the optional panel-ID read. + .read_command = nullptr, + .lcd_send_lines = nullptr, + .reset_pin = GPIO_NUM_NC, + .data_command_pin = GPIO_NUM_NC, + .reset_value = false, + .invert_colors = invert_colors, + .swap_color_order = swap_color_order, + .offset_x = 0, + .offset_y = 0, + .swap_xy = swap_xy, + .mirror_x = mirror_x, + .mirror_y = mirror_y, + .mirror_portrait = false, + }; + + display_driver_.reset(); if (display_controller_ == DisplayController::JD9365) { - // The Waveshare 10.1" 800x1280 panel is a JD9365, driven by Waveshare's - // esp_lcd_jd9365 managed component. The component sends the vendor init - // sequence over the DBI IO and creates/starts the DPI (video) panel - // internally: the handle it returns IS the real DPI panel handle (it - // patches the DPI panel's vtable and enables DMA2D itself), so - // esp_lcd_panel_draw_bitmap() and the DPI event callbacks below work on it - // directly. Do NOT create a DPI panel or run an espp display driver here. - if (lcd_handles_.panel == nullptr) { - // Values from the component's JD9365_800_1280_PANEL_60HZ_DPI_CONFIG macro; - // spelled out field-by-field because the macro's C designated-initializer - // ordering is not valid C++. - esp_lcd_dpi_panel_config_t dpi_config{}; - dpi_config.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT; - dpi_config.dpi_clock_freq_mhz = 80; - dpi_config.virtual_channel = 0; -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) - dpi_config.in_color_format = LCD_COLOR_FMT_RGB565; - dpi_config.out_color_format = LCD_COLOR_FMT_RGB565; -#else - dpi_config.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; - dpi_config.flags.use_dma2d = true; -#endif - dpi_config.num_fbs = 1; - dpi_config.video_timing.h_size = 800; - dpi_config.video_timing.v_size = 1280; - dpi_config.video_timing.hsync_back_porch = 20; - dpi_config.video_timing.hsync_pulse_width = 20; - dpi_config.video_timing.hsync_front_porch = 40; - dpi_config.video_timing.vsync_back_porch = 10; - dpi_config.video_timing.vsync_pulse_width = 4; - dpi_config.video_timing.vsync_front_porch = 30; - jd9365_vendor_config_t vendor_config{}; - vendor_config.init_cmds = nullptr; // use the component's default init sequence - vendor_config.init_cmds_size = 0; - vendor_config.mipi_config.dsi_bus = lcd_handles_.mipi_dsi_bus; - vendor_config.mipi_config.dpi_config = &dpi_config; - vendor_config.mipi_config.lane_num = mipi_dsi_lanes; - esp_lcd_panel_dev_config_t lcd_dev_config{}; - lcd_dev_config.reset_gpio_num = GPIO_NUM_NC; // reset over DSI, no reset GPIO - lcd_dev_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB; - lcd_dev_config.bits_per_pixel = 16; - lcd_dev_config.vendor_config = &vendor_config; - logger_.info("Creating JD9365 panel ({}x{} @ {} MHz DPI)", display_width_, display_height_, - dpi_config.dpi_clock_freq_mhz); - ret = esp_lcd_new_panel_jd9365(lcd_handles_.io, &lcd_dev_config, &lcd_handles_.panel); - if (ret != ESP_OK) { - logger_.error("Failed to create JD9365 panel: {}", esp_err_to_name(ret)); - return false; - } - ret = esp_lcd_panel_reset(lcd_handles_.panel); - if (ret != ESP_OK) { - logger_.error("JD9365 panel reset failed: {}", esp_err_to_name(ret)); - return false; - } - // Sends the vendor init over DBI, then starts the DPI video stream. - ret = esp_lcd_panel_init(lcd_handles_.panel); - if (ret != ESP_OK) { - logger_.error("JD9365 panel init failed: {}", esp_err_to_name(ret)); - return false; - } + // The Waveshare 10.1" 800x1280 panel is a JD9365. espp::Jd9365 performs + // the DCS software reset and sends the vendor init sequence (taken from + // Waveshare's vendor panel component; see jd9365.hpp) over the DBI IO. + auto driver = std::make_shared(display_config); + if (driver->initialize()) { + display_driver_ = std::move(driver); } - // The managed component owns the panel init on this path; there is no espp - // display driver. - display_driver_.reset(); - } else { - // espp-driver path (ILI9881C / EK79007): send the panel controller's vendor - // init sequence over DBI (command mode), before starting the DPI video - // stream. - espp::display_drivers::Config display_config{ - .panel_io = nullptr, - .write_command = std::bind_front(&Esp32P4Nano::dsi_write_command, this), - // NOTE: the Waveshare ESP32-P4 panels do not reliably support MIPI-DSI DCS - // reads (bus turn-around); the ESP-IDF HAL busy-waits on the read, which - // hangs panel init and trips the task watchdog. Do not provide a - // read_command so the driver skips the optional panel-ID read. - .read_command = nullptr, - .lcd_send_lines = nullptr, - .reset_pin = GPIO_NUM_NC, - .data_command_pin = GPIO_NUM_NC, - .reset_value = false, - .invert_colors = invert_colors, - .swap_color_order = swap_color_order, - .offset_x = 0, - .offset_y = 0, - .swap_xy = swap_xy, - .mirror_x = mirror_x, - .mirror_y = mirror_y, - .mirror_portrait = false, - }; - - display_driver_.reset(); - if (display_controller_ == DisplayController::ILI9881C) { - auto driver = std::make_shared(display_config); - if (driver->initialize()) { - display_driver_ = std::move(driver); - } - } else { - auto driver = std::make_shared(display_config); - if (driver->initialize()) { - display_driver_ = std::move(driver); - } + } else if (display_controller_ == DisplayController::ILI9881C) { + auto driver = std::make_shared(display_config); + if (driver->initialize()) { + display_driver_ = std::move(driver); } - if (!display_driver_) { - logger_.error("Failed to initialize {} display controller", get_display_controller_name()); - return false; + } else { + auto driver = std::make_shared(display_config); + if (driver->initialize()) { + display_driver_ = std::move(driver); } + } + if (!display_driver_) { + logger_.error("Failed to initialize {} display controller", get_display_controller_name()); + return false; + } - // Create the DPI (video) panel with the configured panel's timing. This must - // come AFTER the vendor init sequence above: esp_lcd_new_panel_dpi() starts the - // HS video stream, and once it is running the DSI cannot drain the low-power - // command FIFO, so a long init sequence (e.g. ILI9881C's 202 commands) would - // overflow it and hang. - if (lcd_handles_.panel == nullptr) { - esp_lcd_dpi_panel_config_t dpi_cfg{}; - memset(&dpi_cfg, 0, sizeof(dpi_cfg)); - dpi_cfg.virtual_channel = 0; - dpi_cfg.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT; - dpi_cfg.dpi_clock_freq_mhz = panel_params_.dpi_clock_freq_mhz; + // Create the DPI (video) panel with the configured panel's timing. This must + // come AFTER the vendor init sequence above: esp_lcd_new_panel_dpi() starts the + // HS video stream, and once it is running the DSI cannot drain the low-power + // command FIFO, so a long init sequence (e.g. ILI9881C's 202 commands) would + // overflow it and hang. + if (lcd_handles_.panel == nullptr) { + esp_lcd_dpi_panel_config_t dpi_cfg{}; + memset(&dpi_cfg, 0, sizeof(dpi_cfg)); + dpi_cfg.virtual_channel = 0; + dpi_cfg.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT; + dpi_cfg.dpi_clock_freq_mhz = panel_params_.dpi_clock_freq_mhz; #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) - dpi_cfg.in_color_format = LCD_COLOR_FMT_RGB565; - dpi_cfg.out_color_format = LCD_COLOR_FMT_RGB565; + dpi_cfg.in_color_format = LCD_COLOR_FMT_RGB565; + dpi_cfg.out_color_format = LCD_COLOR_FMT_RGB565; #else - dpi_cfg.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; - dpi_cfg.flags.use_dma2d = true; + dpi_cfg.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; + dpi_cfg.flags.use_dma2d = true; #endif - dpi_cfg.num_fbs = 1; - dpi_cfg.video_timing.h_size = display_width_; - dpi_cfg.video_timing.v_size = display_height_; - dpi_cfg.video_timing.hsync_pulse_width = panel_params_.hsync_pulse_width; - dpi_cfg.video_timing.hsync_back_porch = panel_params_.hsync_back_porch; - dpi_cfg.video_timing.hsync_front_porch = panel_params_.hsync_front_porch; - dpi_cfg.video_timing.vsync_pulse_width = panel_params_.vsync_pulse_width; - dpi_cfg.video_timing.vsync_back_porch = panel_params_.vsync_back_porch; - dpi_cfg.video_timing.vsync_front_porch = panel_params_.vsync_front_porch; - logger_.info("Creating DPI panel ({}x{} @ {} MHz)", dpi_cfg.video_timing.h_size, - dpi_cfg.video_timing.v_size, dpi_cfg.dpi_clock_freq_mhz); - ret = esp_lcd_new_panel_dpi(lcd_handles_.mipi_dsi_bus, &dpi_cfg, &lcd_handles_.panel); + dpi_cfg.num_fbs = 1; + dpi_cfg.video_timing.h_size = display_width_; + dpi_cfg.video_timing.v_size = display_height_; + dpi_cfg.video_timing.hsync_pulse_width = panel_params_.hsync_pulse_width; + dpi_cfg.video_timing.hsync_back_porch = panel_params_.hsync_back_porch; + dpi_cfg.video_timing.hsync_front_porch = panel_params_.hsync_front_porch; + dpi_cfg.video_timing.vsync_pulse_width = panel_params_.vsync_pulse_width; + dpi_cfg.video_timing.vsync_back_porch = panel_params_.vsync_back_porch; + dpi_cfg.video_timing.vsync_front_porch = panel_params_.vsync_front_porch; + logger_.info("Creating DPI panel ({}x{} @ {} MHz)", dpi_cfg.video_timing.h_size, + dpi_cfg.video_timing.v_size, dpi_cfg.dpi_clock_freq_mhz); + ret = esp_lcd_new_panel_dpi(lcd_handles_.mipi_dsi_bus, &dpi_cfg, &lcd_handles_.panel); + if (ret != ESP_OK) { + logger_.error("Failed to create MIPI DSI DPI panel: {}", esp_err_to_name(ret)); + return false; + } + // NOTE: for the ILI9881C / EK79007 panels we deliberately do NOT enable + // DMA2D for the DPI panel. DMA2D is a color-processing engine, not a plain + // copy: routing the LVGL flush (esp_lcd_panel_draw_bitmap) through it + // corrupts the RGB565 channel order on those panels, while the plain CPU + // copy path renders correctly. The JD9365 panel renders correctly WITH + // DMA2D (and Waveshare's vendor panel component enables it), so keep it + // enabled on that path. +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + if (display_controller_ == DisplayController::JD9365) { + ret = esp_lcd_dpi_panel_enable_dma2d(lcd_handles_.panel); if (ret != ESP_OK) { - logger_.error("Failed to create MIPI DSI DPI panel: {}", esp_err_to_name(ret)); + logger_.error("Failed to enable DMA2D for the DPI panel: {}", esp_err_to_name(ret)); return false; } - // NOTE: deliberately do NOT enable DMA2D for the DPI panel. DMA2D is a - // color-processing engine, not a plain copy: routing the LVGL flush - // (esp_lcd_panel_draw_bitmap) through it corrupts the RGB565 channel order, - // while the plain CPU copy path renders correctly. } +#endif + } - // Low-level panel init (starts the DPI video stream) - ret = lcd_handles_.panel->init(lcd_handles_.panel); - if (ret != ESP_OK) { - logger_.error("Low-level panel init failed: {}", esp_err_to_name(ret)); - return false; - } + // Low-level panel init (starts the DPI video stream) + ret = lcd_handles_.panel->init(lcd_handles_.panel); + if (ret != ESP_OK) { + logger_.error("Low-level panel init failed: {}", esp_err_to_name(ret)); + return false; } // Note: the raw MIPI-DSI DPI panel does not implement disp_on_off (the panel @@ -379,10 +370,10 @@ void Esp32P4Nano::write_lcd_lines(int xs, int ys, int xe, int ye, const uint8_t void Esp32P4Nano::brightness(float brightness) { // The ESP32-P4-NANO has NO backlight GPIO. The backlight is driven by an // on-board I2C controller at address 0x45. On the 10.1" JD9365 panel the - // brightness register is 0x86 (value 0-255); the "A"-series panels use 0x96 - // instead. For panels other than the JD9365 the chip/protocol has not been - // verified, so this remains best-effort: store the requested value so - // brightness() reads back what was set, and log it. + // brightness register is 0x96 (value 0-255); Waveshare's own BSP writes 0x86 + // instead for some panel revisions. For panels other than the JD9365 the + // chip/protocol has not been verified, so this remains best-effort: store + // the requested value so brightness() reads back what was set, and log it. brightness = std::clamp(brightness, 0.0f, 100.0f); brightness_ = brightness; if (display_controller_ != DisplayController::JD9365) { @@ -408,9 +399,10 @@ void Esp32P4Nano::brightness(float brightness) { return; } } - // Register 0x86 is the brightness register for the 10.1" JD9365 panel (the - // "A"-series panels use 0x96), value 0-255. - const uint8_t data[2] = {0x86, static_cast(255.0f * brightness / 100.0f)}; + // Register 0x96 is the brightness register (0-255) on the 10.1" JD9365 + // panel's power/backlight controller (Waveshare's BSP uses 0x86 for some + // panel revisions). + const uint8_t data[2] = {0x96, static_cast(255.0f * brightness / 100.0f)}; std::error_code ec; if (!backlight_i2c_device_->write(data, sizeof(data), ec)) { logger_.error("Failed to write backlight brightness: {}", ec.message()); diff --git a/doc/Doxyfile b/doc/Doxyfile index 18773f1b8..c5e775e42 100755 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -254,6 +254,7 @@ INPUT = \ $(PROJECT_PATH)/components/display_drivers/include/ek79007.hpp \ $(PROJECT_PATH)/components/display_drivers/include/ili9341.hpp \ $(PROJECT_PATH)/components/display_drivers/include/ili9881.hpp \ + $(PROJECT_PATH)/components/display_drivers/include/jd9365.hpp \ $(PROJECT_PATH)/components/display_drivers/include/sh8601.hpp \ $(PROJECT_PATH)/components/display_drivers/include/spi_panel_io.hpp \ $(PROJECT_PATH)/components/display_drivers/include/ssd1351.hpp \ From f0c1886ae39995fc863940094e60a1ef0c90b8f3 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Thu, 6 Aug 2026 12:01:35 -0500 Subject: [PATCH 06/13] fix touch inversion --- components/esp32-p4-eth/include/esp32-p4-eth.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/esp32-p4-eth/include/esp32-p4-eth.hpp b/components/esp32-p4-eth/include/esp32-p4-eth.hpp index c48f3504f..e07c9b239 100644 --- a/components/esp32-p4-eth/include/esp32-p4-eth.hpp +++ b/components/esp32-p4-eth/include/esp32-p4-eth.hpp @@ -534,8 +534,8 @@ class Esp32P4Eth : public BaseComponent { static constexpr bool swap_xy = false; // touch -> display coordinate conversion. May need tuning per panel. static constexpr bool touch_swap_xy = false; - static constexpr bool touch_invert_x = true; - static constexpr bool touch_invert_y = true; + static constexpr bool touch_invert_x = false; + static constexpr bool touch_invert_y = false; // Touch (GT911) - interrupt/reset are NOT connected on this board static constexpr uint8_t gt911_default_address = 0x5D; From 03f2044953d3833572c9e5eb42b99764685d4889 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Thu, 6 Aug 2026 12:59:04 -0500 Subject: [PATCH 07/13] match eth kit --- components/esp32-p4-nano/include/esp32-p4-nano.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/esp32-p4-nano/include/esp32-p4-nano.hpp b/components/esp32-p4-nano/include/esp32-p4-nano.hpp index 6e54c68e7..b8f8c550b 100644 --- a/components/esp32-p4-nano/include/esp32-p4-nano.hpp +++ b/components/esp32-p4-nano/include/esp32-p4-nano.hpp @@ -535,8 +535,8 @@ class Esp32P4Nano : public BaseComponent { static constexpr bool swap_xy = false; // touch -> display coordinate conversion. May need tuning per panel. static constexpr bool touch_swap_xy = false; - static constexpr bool touch_invert_x = true; - static constexpr bool touch_invert_y = true; + static constexpr bool touch_invert_x = false; + static constexpr bool touch_invert_y = false; // Touch (GT911) - interrupt/reset are NOT connected on this board static constexpr uint8_t gt911_default_address = 0x5D; From 35e7b75e02862b69d4e2ea8f14ab56869c8accc9 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Thu, 6 Aug 2026 13:05:42 -0500 Subject: [PATCH 08/13] fix(esp32-p4): Feed I2S playback at a constant frame cadence (choppy audio) The audio task drained the TX stream buffer by writing only the currently `available` bytes to I2S each cycle. With a bursty producer that makes the DMA cadence variable and interleaves full frames of silence into continuous program material, which sounds choppy/glitchy - notably worse than the m5stack-tab5, whose playback path (like esp-box and t-deck) always writes a full, frame-aligned buffer with the queued samples zero-padded to buffer_size so the DMA is fed at a constant cadence. Match that proven path in esp32-p4-eth, esp32-p4-nano, and the esp32-p4-function-ev-board (where the pattern was copied from). Builds green for esp32p4 on ESP-IDF 6.0 (eth + nano examples). Co-Authored-By: Claude Opus 4.8 (1M context) --- components/esp32-p4-eth/src/audio.cpp | 7 ++++++- components/esp32-p4-function-ev-board/src/audio.cpp | 7 ++++++- components/esp32-p4-nano/src/audio.cpp | 7 ++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/components/esp32-p4-eth/src/audio.cpp b/components/esp32-p4-eth/src/audio.cpp index 8825aaa35..d9acaefa8 100644 --- a/components/esp32-p4-eth/src/audio.cpp +++ b/components/esp32-p4-eth/src/audio.cpp @@ -301,7 +301,12 @@ bool Esp32P4Eth::audio_task_callback(std::mutex &m, std::condition_variable &cv, i2s_channel_write(audio_tx_handle, tx_buf, buffer_size, NULL, pdMS_TO_TICKS(100)); } else { xStreamBufferReceive(audio_tx_stream, tx_buf, available, 0); - i2s_channel_write(audio_tx_handle, tx_buf, available, NULL, pdMS_TO_TICKS(100)); + // Always write a full, frame-aligned buffer (queued samples zero-padded to + // buffer_size) so the I2S DMA is fed at a constant cadence - matching the + // esp-box / t-deck / m5stack-tab5 playback path. Writing only `available` + // bytes makes the drain cadence variable and interleaves whole frames of + // silence into bursty streams, which sounds choppy/glitchy. + i2s_channel_write(audio_tx_handle, tx_buf, buffer_size, NULL, pdMS_TO_TICKS(100)); } // honor a stop request per the Task contract: check/clear notified under m std::unique_lock lock(m); diff --git a/components/esp32-p4-function-ev-board/src/audio.cpp b/components/esp32-p4-function-ev-board/src/audio.cpp index fa1832704..511082d4f 100644 --- a/components/esp32-p4-function-ev-board/src/audio.cpp +++ b/components/esp32-p4-function-ev-board/src/audio.cpp @@ -273,7 +273,12 @@ bool Esp32P4FunctionEvBoard::audio_task_callback(std::mutex &m, std::condition_v i2s_channel_write(audio_tx_handle, tx_buf, buffer_size, NULL, portMAX_DELAY); } else { xStreamBufferReceive(audio_tx_stream, tx_buf, available, 0); - i2s_channel_write(audio_tx_handle, tx_buf, available, NULL, portMAX_DELAY); + // Always write a full, frame-aligned buffer (queued samples zero-padded to + // buffer_size) so the I2S DMA is fed at a constant cadence - matching the + // esp-box / t-deck / m5stack-tab5 playback path. Writing only `available` + // bytes makes the drain cadence variable and interleaves whole frames of + // silence into bursty streams, which sounds choppy/glitchy. + i2s_channel_write(audio_tx_handle, tx_buf, buffer_size, NULL, portMAX_DELAY); } return false; } diff --git a/components/esp32-p4-nano/src/audio.cpp b/components/esp32-p4-nano/src/audio.cpp index 903cb626d..dd2d4c0e1 100644 --- a/components/esp32-p4-nano/src/audio.cpp +++ b/components/esp32-p4-nano/src/audio.cpp @@ -302,7 +302,12 @@ bool Esp32P4Nano::audio_task_callback(std::mutex &m, std::condition_variable &cv i2s_channel_write(audio_tx_handle, tx_buf, buffer_size, NULL, pdMS_TO_TICKS(100)); } else { xStreamBufferReceive(audio_tx_stream, tx_buf, available, 0); - i2s_channel_write(audio_tx_handle, tx_buf, available, NULL, pdMS_TO_TICKS(100)); + // Always write a full, frame-aligned buffer (queued samples zero-padded to + // buffer_size) so the I2S DMA is fed at a constant cadence - matching the + // esp-box / t-deck / m5stack-tab5 playback path. Writing only `available` + // bytes makes the drain cadence variable and interleaves whole frames of + // silence into bursty streams, which sounds choppy/glitchy. + i2s_channel_write(audio_tx_handle, tx_buf, buffer_size, NULL, pdMS_TO_TICKS(100)); } // honor a stop request per the Task contract: check/clear notified under m std::unique_lock lock(m); From 42cc41608e5415d23622f76df6dd9e316a0dbd24 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Thu, 6 Aug 2026 13:29:00 -0500 Subject: [PATCH 09/13] feat(esp32-p4): Add clear_audio() and restart the click sound on each touch Clicks were queueing: play_audio() appends to a large TX stream buffer (up to ~64 KB), so rapid touches serialized their click sounds behind whatever was still queued, playing long after the touches stopped. Add clear_audio(), which drops any queued-but-unplayed audio (the current ~16 ms DMA frame still finishes), and use it in the examples' touch handler so each new touch restarts the click immediately for responsive UI feedback. Applies to esp32-p4-eth and esp32-p4-nano; builds green on ESP-IDF 6.0. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../esp32-p4-eth/example/main/esp32_p4_eth_example.cpp | 3 +++ components/esp32-p4-eth/include/esp32-p4-eth.hpp | 6 ++++++ components/esp32-p4-eth/src/audio.cpp | 10 ++++++++++ .../example/main/esp32_p4_nano_example.cpp | 3 +++ components/esp32-p4-nano/include/esp32-p4-nano.hpp | 6 ++++++ components/esp32-p4-nano/src/audio.cpp | 10 ++++++++++ 6 files changed, 38 insertions(+) diff --git a/components/esp32-p4-eth/example/main/esp32_p4_eth_example.cpp b/components/esp32-p4-eth/example/main/esp32_p4_eth_example.cpp index 8813dbabf..38aed59f7 100644 --- a/components/esp32-p4-eth/example/main/esp32_p4_eth_example.cpp +++ b/components/esp32-p4-eth/example/main/esp32_p4_eth_example.cpp @@ -140,6 +140,9 @@ extern "C" void app_main(void) { if (td.num_touch_points > 0) { const bool new_touch = (prev_td != td); if (new_touch && !audio_bytes.empty()) { + // restart the click on every new touch (drop any queued tail) so the + // sound tracks touches responsively instead of queueing up + board.clear_audio(); board.play_audio(audio_bytes); // non-blocking, touch-down edge only } if (new_touch && gui.draw_page_active()) { diff --git a/components/esp32-p4-eth/include/esp32-p4-eth.hpp b/components/esp32-p4-eth/include/esp32-p4-eth.hpp index e07c9b239..b606fa80a 100644 --- a/components/esp32-p4-eth/include/esp32-p4-eth.hpp +++ b/components/esp32-p4-eth/include/esp32-p4-eth.hpp @@ -351,6 +351,12 @@ class Esp32P4Eth : public BaseComponent { /// \note Must be called from task context, not from an ISR. size_t play_audio(std::span data); + /// Drop any queued (not yet played) audio so a subsequent play_audio() starts + /// immediately instead of waiting behind previously queued sound. Useful for + /// UI sounds where a new event should restart the sound for maximum + /// responsiveness. + void clear_audio(); + ///////////////////////////////////////////////////////////////////////////// // Microphone ///////////////////////////////////////////////////////////////////////////// diff --git a/components/esp32-p4-eth/src/audio.cpp b/components/esp32-p4-eth/src/audio.cpp index d9acaefa8..1669e1258 100644 --- a/components/esp32-p4-eth/src/audio.cpp +++ b/components/esp32-p4-eth/src/audio.cpp @@ -171,6 +171,16 @@ size_t Esp32P4Eth::play_audio(const uint8_t *data, uint32_t num_bytes) { return xStreamBufferSend(audio_tx_stream, data, sendable, 0); } +void Esp32P4Eth::clear_audio() { + if (!audio_initialized_ || audio_tx_stream == nullptr) { + return; + } + // Drop everything queued but not yet handed to the I2S DMA. The drain task's + // current (at most one) frame still finishes, so this cuts over on the next + // ~16 ms frame boundary. + xStreamBufferReset(audio_tx_stream); +} + size_t Esp32P4Eth::play_audio(std::span data) { return play_audio(data.data(), data.size()); } diff --git a/components/esp32-p4-nano/example/main/esp32_p4_nano_example.cpp b/components/esp32-p4-nano/example/main/esp32_p4_nano_example.cpp index b29eeaf3e..8b4df7b6e 100644 --- a/components/esp32-p4-nano/example/main/esp32_p4_nano_example.cpp +++ b/components/esp32-p4-nano/example/main/esp32_p4_nano_example.cpp @@ -140,6 +140,9 @@ extern "C" void app_main(void) { if (td.num_touch_points > 0) { const bool new_touch = (prev_td != td); if (new_touch && !audio_bytes.empty()) { + // restart the click on every new touch (drop any queued tail) so the + // sound tracks touches responsively instead of queueing up + board.clear_audio(); board.play_audio(audio_bytes); // non-blocking, touch-down edge only } if (new_touch && gui.draw_page_active()) { diff --git a/components/esp32-p4-nano/include/esp32-p4-nano.hpp b/components/esp32-p4-nano/include/esp32-p4-nano.hpp index b8f8c550b..ca03c084f 100644 --- a/components/esp32-p4-nano/include/esp32-p4-nano.hpp +++ b/components/esp32-p4-nano/include/esp32-p4-nano.hpp @@ -352,6 +352,12 @@ class Esp32P4Nano : public BaseComponent { /// \note Must be called from task context, not from an ISR. size_t play_audio(std::span data); + /// Drop any queued (not yet played) audio so a subsequent play_audio() starts + /// immediately instead of waiting behind previously queued sound. Useful for + /// UI sounds where a new event should restart the sound for maximum + /// responsiveness. + void clear_audio(); + ///////////////////////////////////////////////////////////////////////////// // Microphone ///////////////////////////////////////////////////////////////////////////// diff --git a/components/esp32-p4-nano/src/audio.cpp b/components/esp32-p4-nano/src/audio.cpp index dd2d4c0e1..4c9d0da15 100644 --- a/components/esp32-p4-nano/src/audio.cpp +++ b/components/esp32-p4-nano/src/audio.cpp @@ -172,6 +172,16 @@ size_t Esp32P4Nano::play_audio(const uint8_t *data, uint32_t num_bytes) { return xStreamBufferSend(audio_tx_stream, data, sendable, 0); } +void Esp32P4Nano::clear_audio() { + if (!audio_initialized_ || audio_tx_stream == nullptr) { + return; + } + // Drop everything queued but not yet handed to the I2S DMA. The drain task's + // current (at most one) frame still finishes, so this cuts over on the next + // ~16 ms frame boundary. + xStreamBufferReset(audio_tx_stream); +} + size_t Esp32P4Nano::play_audio(std::span data) { return play_audio(data.data(), data.size()); } From 35ac855290f34b0c981218fb6a3aa56e95752aad Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Thu, 6 Aug 2026 13:39:19 -0500 Subject: [PATCH 10/13] fix(esp32-p4): Run the camera SCCB at 100 kHz like Waveshare's demos Waveshare's own working OV5647 demos for these boards run the SCCB at 100 kHz (their Kconfig floor), while we inherited 400 kHz from the tab5. Some OV sensors probe unreliably at higher SCCB rates, and esp_video applies this frequency to the SCCB device even when reusing an external I2C bus handle, so match the vendor's proven value. Verified against the board schematics that the rest of the camera topology is correct as-is: the CSI connector's SCCB is the shared GPIO7/8 bus, the module enable line (CSI_IO0) is pulled high in hardware, and there is no reset/pwdn GPIO to drive. Co-Authored-By: Claude Opus 4.8 (1M context) --- components/esp32-p4-eth/src/camera.cpp | 11 +++++------ components/esp32-p4-nano/src/camera.cpp | 11 +++++------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/components/esp32-p4-eth/src/camera.cpp b/components/esp32-p4-eth/src/camera.cpp index a68271c2f..12f59ff70 100644 --- a/components/esp32-p4-eth/src/camera.cpp +++ b/components/esp32-p4-eth/src/camera.cpp @@ -51,12 +51,11 @@ bool Esp32P4Eth::initialize_camera(const camera_frame_callback_t &callback, esp_video_init_csi_config_t csi_config = {}; csi_config.sccb_config.init_sccb = false; csi_config.sccb_config.i2c_handle = internal_i2c_.native_bus_handle(); - // Run the sensor SCCB at 400 kHz rather than 100 kHz. The SCCB shares the - // internal I2C bus with the touch controller and audio codec, and the ISP's - // auto-exposure writes the sensor over SCCB every frame; at 100 kHz each of - // those writes holds the shared bus ~4x longer than needed. 400 kHz is a safe - // SCCB speed for this sensor and matches the bus's configured clock. - csi_config.sccb_config.freq = 400000; + // Run the sensor SCCB at 100 kHz, matching Waveshare's own camera demos for + // these boards (their Kconfig floor is 100 kHz). Some OV sensors are + // unreliable at higher SCCB rates during probe, and esp_video applies this + // freq to the SCCB device even when reusing an external I2C bus handle. + csi_config.sccb_config.freq = 100000; csi_config.reset_pin = GPIO_NUM_NC; csi_config.pwdn_pin = GPIO_NUM_NC; csi_config.dont_init_ldo = false; diff --git a/components/esp32-p4-nano/src/camera.cpp b/components/esp32-p4-nano/src/camera.cpp index 817868f93..3e5ddbf36 100644 --- a/components/esp32-p4-nano/src/camera.cpp +++ b/components/esp32-p4-nano/src/camera.cpp @@ -51,12 +51,11 @@ bool Esp32P4Nano::initialize_camera(const camera_frame_callback_t &callback, esp_video_init_csi_config_t csi_config = {}; csi_config.sccb_config.init_sccb = false; csi_config.sccb_config.i2c_handle = internal_i2c_.native_bus_handle(); - // Run the sensor SCCB at 400 kHz rather than 100 kHz. The SCCB shares the - // internal I2C bus with the touch controller and audio codec, and the ISP's - // auto-exposure writes the sensor over SCCB every frame; at 100 kHz each of - // those writes holds the shared bus ~4x longer than needed. 400 kHz is a safe - // SCCB speed for this sensor and matches the bus's configured clock. - csi_config.sccb_config.freq = 400000; + // Run the sensor SCCB at 100 kHz, matching Waveshare's own camera demos for + // these boards (their Kconfig floor is 100 kHz). Some OV sensors are + // unreliable at higher SCCB rates during probe, and esp_video applies this + // freq to the SCCB device even when reusing an external I2C bus handle. + csi_config.sccb_config.freq = 100000; csi_config.reset_pin = GPIO_NUM_NC; csi_config.pwdn_pin = GPIO_NUM_NC; csi_config.dont_init_ldo = false; From e1f76a2937280911093c6807520aa722d859bed2 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Thu, 6 Aug 2026 14:12:03 -0500 Subject: [PATCH 11/13] fix(esp32-p4): Example polish - click gating, WAV data-chunk parsing, tab scroll - Gate the touch click sound (and circle draw) to the draw/status page, so touches on the Audio/Camera tabs stay silent like the other BSP examples. - Parse the WAV RIFF chunks properly and play exactly the 'data' payload. The fixed 44-byte strip played the file's trailing metadata chunks (cue/LIST/bext, 686 bytes in click.wav) as audio, causing a pop at the end of every click. - Disable LV_OBJ_FLAG_SCROLLABLE on the individual tab pages so drags inside a page don't rubber-band/scroll (the tabview content swipe was already off). Applies to esp32-p4-eth and esp32-p4-nano examples; builds green on ESP-IDF 6.0. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../example/main/esp32_p4_eth_example.cpp | 36 +++++++++++++++---- components/esp32-p4-eth/example/main/gui.cpp | 6 ++++ .../example/main/esp32_p4_nano_example.cpp | 36 +++++++++++++++---- components/esp32-p4-nano/example/main/gui.cpp | 6 ++++ 4 files changed, 70 insertions(+), 14 deletions(-) diff --git a/components/esp32-p4-eth/example/main/esp32_p4_eth_example.cpp b/components/esp32-p4-eth/example/main/esp32_p4_eth_example.cpp index 38aed59f7..1fc09de63 100644 --- a/components/esp32-p4-eth/example/main/esp32_p4_eth_example.cpp +++ b/components/esp32-p4-eth/example/main/esp32_p4_eth_example.cpp @@ -139,13 +139,15 @@ extern "C" void app_main(void) { touch_y = td.y; if (td.num_touch_points > 0) { const bool new_touch = (prev_td != td); - if (new_touch && !audio_bytes.empty()) { - // restart the click on every new touch (drop any queued tail) so the - // sound tracks touches responsively instead of queueing up - board.clear_audio(); - board.play_audio(audio_bytes); // non-blocking, touch-down edge only - } + // Touch feedback (click + circle) only applies on the draw/status page; + // touches on the other tabs (buttons, sliders) stay silent. if (new_touch && gui.draw_page_active()) { + if (!audio_bytes.empty()) { + // restart the click on every new touch (drop any queued tail) so the + // sound tracks touches responsively instead of queueing up + board.clear_audio(); + board.play_audio(audio_bytes); // non-blocking, touch-down edge only + } gui.draw_circle(td.x, td.y, kCircleRadius); } } @@ -333,7 +335,27 @@ static bool load_audio(size_t &out_size, size_t &out_sample_rate) { } uint32_t sample_rate = 0; std::memcpy(&sample_rate, &audio_bytes[24], sizeof(sample_rate)); - audio_bytes.erase(audio_bytes.begin(), audio_bytes.begin() + 44); + // Walk the RIFF chunks to find the 'data' chunk and keep exactly its payload. + // A fixed 44-byte strip is wrong for files with trailing metadata chunks + // (cue/LIST/bext): those bytes would be played as audio, producing a pop at + // the end of playback. + size_t data_off = 0, data_len = 0; + for (size_t off = 12; off + 8 <= audio_bytes.size();) { + uint32_t chunk_size = 0; + std::memcpy(&chunk_size, &audio_bytes[off + 4], sizeof(chunk_size)); + if (std::memcmp(&audio_bytes[off], "data", 4) == 0) { + data_off = off + 8; + data_len = std::min(chunk_size, audio_bytes.size() - data_off); + break; + } + off += 8 + chunk_size + (chunk_size & 1); // chunks are word-aligned + } + if (data_len == 0) { + audio_bytes.clear(); + return false; + } + audio_bytes.erase(audio_bytes.begin() + data_off + data_len, audio_bytes.end()); + audio_bytes.erase(audio_bytes.begin(), audio_bytes.begin() + data_off); out_size = audio_bytes.size(); out_sample_rate = sample_rate; return true; diff --git a/components/esp32-p4-eth/example/main/gui.cpp b/components/esp32-p4-eth/example/main/gui.cpp index 86dcf64e0..cfee70ae7 100644 --- a/components/esp32-p4-eth/example/main/gui.cpp +++ b/components/esp32-p4-eth/example/main/gui.cpp @@ -30,6 +30,12 @@ void Gui::init_tabview() { status_tab_ = lv_tabview_add_tab(tabview_, "Status"); audio_tab_ = lv_tabview_add_tab(tabview_, "Audio"); camera_tab_ = lv_tabview_add_tab(tabview_, "Camera"); + // The tab pages themselves are scrollable by default; disable that so drags + // inside a page don't rubber-band/scroll the content (matches the other BSP + // example GUIs). + lv_obj_clear_flag(status_tab_, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_clear_flag(audio_tab_, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_clear_flag(camera_tab_, LV_OBJ_FLAG_SCROLLABLE); // switching tabs is done with the tab buttons only: disable swipe // scrolling of the content so drawing on the Status tab cannot accidentally // change pages diff --git a/components/esp32-p4-nano/example/main/esp32_p4_nano_example.cpp b/components/esp32-p4-nano/example/main/esp32_p4_nano_example.cpp index 8b4df7b6e..e2b59960b 100644 --- a/components/esp32-p4-nano/example/main/esp32_p4_nano_example.cpp +++ b/components/esp32-p4-nano/example/main/esp32_p4_nano_example.cpp @@ -139,13 +139,15 @@ extern "C" void app_main(void) { touch_y = td.y; if (td.num_touch_points > 0) { const bool new_touch = (prev_td != td); - if (new_touch && !audio_bytes.empty()) { - // restart the click on every new touch (drop any queued tail) so the - // sound tracks touches responsively instead of queueing up - board.clear_audio(); - board.play_audio(audio_bytes); // non-blocking, touch-down edge only - } + // Touch feedback (click + circle) only applies on the draw/status page; + // touches on the other tabs (buttons, sliders) stay silent. if (new_touch && gui.draw_page_active()) { + if (!audio_bytes.empty()) { + // restart the click on every new touch (drop any queued tail) so the + // sound tracks touches responsively instead of queueing up + board.clear_audio(); + board.play_audio(audio_bytes); // non-blocking, touch-down edge only + } gui.draw_circle(td.x, td.y, kCircleRadius); } } @@ -333,7 +335,27 @@ static bool load_audio(size_t &out_size, size_t &out_sample_rate) { } uint32_t sample_rate = 0; std::memcpy(&sample_rate, &audio_bytes[24], sizeof(sample_rate)); - audio_bytes.erase(audio_bytes.begin(), audio_bytes.begin() + 44); + // Walk the RIFF chunks to find the 'data' chunk and keep exactly its payload. + // A fixed 44-byte strip is wrong for files with trailing metadata chunks + // (cue/LIST/bext): those bytes would be played as audio, producing a pop at + // the end of playback. + size_t data_off = 0, data_len = 0; + for (size_t off = 12; off + 8 <= audio_bytes.size();) { + uint32_t chunk_size = 0; + std::memcpy(&chunk_size, &audio_bytes[off + 4], sizeof(chunk_size)); + if (std::memcmp(&audio_bytes[off], "data", 4) == 0) { + data_off = off + 8; + data_len = std::min(chunk_size, audio_bytes.size() - data_off); + break; + } + off += 8 + chunk_size + (chunk_size & 1); // chunks are word-aligned + } + if (data_len == 0) { + audio_bytes.clear(); + return false; + } + audio_bytes.erase(audio_bytes.begin() + data_off + data_len, audio_bytes.end()); + audio_bytes.erase(audio_bytes.begin(), audio_bytes.begin() + data_off); out_size = audio_bytes.size(); out_sample_rate = sample_rate; return true; diff --git a/components/esp32-p4-nano/example/main/gui.cpp b/components/esp32-p4-nano/example/main/gui.cpp index 57fc9ebea..2ab7a3757 100644 --- a/components/esp32-p4-nano/example/main/gui.cpp +++ b/components/esp32-p4-nano/example/main/gui.cpp @@ -30,6 +30,12 @@ void Gui::init_tabview() { status_tab_ = lv_tabview_add_tab(tabview_, "Status"); audio_tab_ = lv_tabview_add_tab(tabview_, "Audio"); camera_tab_ = lv_tabview_add_tab(tabview_, "Camera"); + // The tab pages themselves are scrollable by default; disable that so drags + // inside a page don't rubber-band/scroll the content (matches the other BSP + // example GUIs). + lv_obj_clear_flag(status_tab_, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_clear_flag(audio_tab_, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_clear_flag(camera_tab_, LV_OBJ_FLAG_SCROLLABLE); // switching tabs is done with the tab buttons only: disable swipe // scrolling of the content so drawing on the Status tab cannot accidentally // change pages From c456f4b4deaa8e7ebe98527b50b69564ca9fbafa Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Thu, 6 Aug 2026 16:21:57 -0500 Subject: [PATCH 12/13] fix(esp32-p4): Decouple touch drawing from LVGL rendering; click on down-edge only Touch drawing produced few points because Gui::draw_circle() (called from the 60 Hz touch poll task) locked the same mutex Gui::update() holds for the whole lv_task_handler() render - tens of ms per frame on the 800x1280 panel with CPU software rotation - so the touch task spent most of each frame blocked and the effective sample rate collapsed. draw_circle() now just queues the point under a small dedicated lock and the GUI update task drains the queue under the LVGL mutex, so touch sampling runs at the full poll rate regardless of render time. Also fire the click sound only on the touch-DOWN edge (prev.num_touch_points == 0) instead of on every reported change: previously each drag movement counted as a "new touch", which both restarted the click continuously while drawing and was the original source of the queued-clicks behavior. Touch is polled (~60 Hz / 16 ms; the GT911 INT line is not wired to the P4 on these boards) - the sparse points were mutex contention, not the poll rate, and not audio (play_audio is a 0-timeout non-blocking enqueue). Applies to esp32-p4-eth and esp32-p4-nano examples; builds green on ESP-IDF 6.0. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../example/main/esp32_p4_eth_example.cpp | 15 +++++++----- components/esp32-p4-eth/example/main/gui.cpp | 23 ++++++++++++++++++- components/esp32-p4-eth/example/main/gui.hpp | 10 ++++++++ .../example/main/esp32_p4_nano_example.cpp | 15 +++++++----- components/esp32-p4-nano/example/main/gui.cpp | 23 ++++++++++++++++++- components/esp32-p4-nano/example/main/gui.hpp | 10 ++++++++ 6 files changed, 82 insertions(+), 14 deletions(-) diff --git a/components/esp32-p4-eth/example/main/esp32_p4_eth_example.cpp b/components/esp32-p4-eth/example/main/esp32_p4_eth_example.cpp index 1fc09de63..e21f6453c 100644 --- a/components/esp32-p4-eth/example/main/esp32_p4_eth_example.cpp +++ b/components/esp32-p4-eth/example/main/esp32_p4_eth_example.cpp @@ -139,16 +139,19 @@ extern "C" void app_main(void) { touch_y = td.y; if (td.num_touch_points > 0) { const bool new_touch = (prev_td != td); + const bool touch_down_edge = (prev_td.num_touch_points == 0); // Touch feedback (click + circle) only applies on the draw/status page; // touches on the other tabs (buttons, sliders) stay silent. - if (new_touch && gui.draw_page_active()) { - if (!audio_bytes.empty()) { - // restart the click on every new touch (drop any queued tail) so the - // sound tracks touches responsively instead of queueing up + if (gui.draw_page_active()) { + // Click only on the touch-DOWN edge (not on every drag movement), and + // restart it (drop any queued tail) so it tracks touches responsively. + if (touch_down_edge && !audio_bytes.empty()) { board.clear_audio(); - board.play_audio(audio_bytes); // non-blocking, touch-down edge only + board.play_audio(audio_bytes); // non-blocking + } + if (new_touch) { + gui.draw_circle(td.x, td.y, kCircleRadius); } - gui.draw_circle(td.x, td.y, kCircleRadius); } } prev_td = td; diff --git a/components/esp32-p4-eth/example/main/gui.cpp b/components/esp32-p4-eth/example/main/gui.cpp index cfee70ae7..90842420c 100644 --- a/components/esp32-p4-eth/example/main/gui.cpp +++ b/components/esp32-p4-eth/example/main/gui.cpp @@ -272,6 +272,17 @@ void Gui::init_circle_layer() { bool Gui::update(std::mutex &m, std::condition_variable &cv) { { std::lock_guard lock(mutex_); + // drain any touch points queued since the last cycle + { + std::vector points; + { + std::lock_guard plock(pending_points_mutex_); + points.swap(pending_points_); + } + for (const auto &c : points) { + draw_circle_pending(c); + } + } lv_task_handler(); // keep the audio volume label in sync with the live BSP state, so the // first press of a volume button doesn't appear to jump from a stale @@ -386,8 +397,18 @@ void Gui::next_rotation() { } void Gui::draw_circle(int x, int y, int radius) { - std::lock_guard lock(mutex_); + // Only queue the point here; the GUI update task drains the queue under the + // LVGL mutex. Taking mutex_ directly would block the caller (the touch poll + // task) for the duration of lv_task_handler() rendering, collapsing the + // touch sample rate. + std::lock_guard lock(pending_points_mutex_); + pending_points_.push_back({.x = x, .y = y, .radius = radius, .visible = true}); +} + +void Gui::draw_circle_pending(const Circle &c) { + // caller holds mutex_ lv_obj_move_foreground(circle_layer_); + int x = c.x, y = c.y, radius = c.radius; Circle previous_circle = circles_[next_circle_index_]; circles_[next_circle_index_] = {.x = x, .y = y, .radius = radius, .visible = true}; next_circle_index_ = (next_circle_index_ + 1) % circles_.size(); diff --git a/components/esp32-p4-eth/example/main/gui.hpp b/components/esp32-p4-eth/example/main/gui.hpp index 864bbda54..f0f535ad6 100644 --- a/components/esp32-p4-eth/example/main/gui.hpp +++ b/components/esp32-p4-eth/example/main/gui.hpp @@ -75,6 +75,10 @@ class Gui { /// @param x The x coordinate (screen space) /// @param y The y coordinate (screen space) /// @param radius The radius of the circle + /// Queue a circle to draw at (x, y). Thread-safe and non-blocking: the point + /// is queued under a small lock and rendered by the GUI update task on its + /// next cycle, so callers (e.g. the touch poll task) never wait on LVGL + /// rendering. void draw_circle(int x, int y, int radius); /// Clear all circles from the screen. Thread-safe. @@ -156,6 +160,7 @@ class Gui { // unlocked implementations, called with the mutex held void clear_circles_impl(); + void draw_circle_pending(const Circle &c); // LVGL objects lv_obj_t *tabview_{nullptr}; @@ -203,6 +208,11 @@ class Gui { .task_config = {.name = "gui", .stack_size_bytes = 12 * 1024}}}; espp::Logger logger_; std::recursive_mutex mutex_; + // Pending draw_circle() points, queued by (fast) producers and drained under + // mutex_ by the GUI update task; keeps the touch poll task from blocking on + // LVGL rendering. + std::mutex pending_points_mutex_; + std::vector pending_points_; // True between init_ui() and deinit_ui(). Guards set_camera_frame() (called // from the camera task) against touching the LVGL tree after teardown. bool ui_ready_{false}; diff --git a/components/esp32-p4-nano/example/main/esp32_p4_nano_example.cpp b/components/esp32-p4-nano/example/main/esp32_p4_nano_example.cpp index e2b59960b..33e204c00 100644 --- a/components/esp32-p4-nano/example/main/esp32_p4_nano_example.cpp +++ b/components/esp32-p4-nano/example/main/esp32_p4_nano_example.cpp @@ -139,16 +139,19 @@ extern "C" void app_main(void) { touch_y = td.y; if (td.num_touch_points > 0) { const bool new_touch = (prev_td != td); + const bool touch_down_edge = (prev_td.num_touch_points == 0); // Touch feedback (click + circle) only applies on the draw/status page; // touches on the other tabs (buttons, sliders) stay silent. - if (new_touch && gui.draw_page_active()) { - if (!audio_bytes.empty()) { - // restart the click on every new touch (drop any queued tail) so the - // sound tracks touches responsively instead of queueing up + if (gui.draw_page_active()) { + // Click only on the touch-DOWN edge (not on every drag movement), and + // restart it (drop any queued tail) so it tracks touches responsively. + if (touch_down_edge && !audio_bytes.empty()) { board.clear_audio(); - board.play_audio(audio_bytes); // non-blocking, touch-down edge only + board.play_audio(audio_bytes); // non-blocking + } + if (new_touch) { + gui.draw_circle(td.x, td.y, kCircleRadius); } - gui.draw_circle(td.x, td.y, kCircleRadius); } } prev_td = td; diff --git a/components/esp32-p4-nano/example/main/gui.cpp b/components/esp32-p4-nano/example/main/gui.cpp index 2ab7a3757..3a4a50021 100644 --- a/components/esp32-p4-nano/example/main/gui.cpp +++ b/components/esp32-p4-nano/example/main/gui.cpp @@ -272,6 +272,17 @@ void Gui::init_circle_layer() { bool Gui::update(std::mutex &m, std::condition_variable &cv) { { std::lock_guard lock(mutex_); + // drain any touch points queued since the last cycle + { + std::vector points; + { + std::lock_guard plock(pending_points_mutex_); + points.swap(pending_points_); + } + for (const auto &c : points) { + draw_circle_pending(c); + } + } lv_task_handler(); // keep the audio volume label in sync with the live BSP state, so the // first press of a volume button doesn't appear to jump from a stale @@ -386,8 +397,18 @@ void Gui::next_rotation() { } void Gui::draw_circle(int x, int y, int radius) { - std::lock_guard lock(mutex_); + // Only queue the point here; the GUI update task drains the queue under the + // LVGL mutex. Taking mutex_ directly would block the caller (the touch poll + // task) for the duration of lv_task_handler() rendering, collapsing the + // touch sample rate. + std::lock_guard lock(pending_points_mutex_); + pending_points_.push_back({.x = x, .y = y, .radius = radius, .visible = true}); +} + +void Gui::draw_circle_pending(const Circle &c) { + // caller holds mutex_ lv_obj_move_foreground(circle_layer_); + int x = c.x, y = c.y, radius = c.radius; Circle previous_circle = circles_[next_circle_index_]; circles_[next_circle_index_] = {.x = x, .y = y, .radius = radius, .visible = true}; next_circle_index_ = (next_circle_index_ + 1) % circles_.size(); diff --git a/components/esp32-p4-nano/example/main/gui.hpp b/components/esp32-p4-nano/example/main/gui.hpp index 36944c786..d90d23eae 100644 --- a/components/esp32-p4-nano/example/main/gui.hpp +++ b/components/esp32-p4-nano/example/main/gui.hpp @@ -75,6 +75,10 @@ class Gui { /// @param x The x coordinate (screen space) /// @param y The y coordinate (screen space) /// @param radius The radius of the circle + /// Queue a circle to draw at (x, y). Thread-safe and non-blocking: the point + /// is queued under a small lock and rendered by the GUI update task on its + /// next cycle, so callers (e.g. the touch poll task) never wait on LVGL + /// rendering. void draw_circle(int x, int y, int radius); /// Clear all circles from the screen. Thread-safe. @@ -156,6 +160,7 @@ class Gui { // unlocked implementations, called with the mutex held void clear_circles_impl(); + void draw_circle_pending(const Circle &c); // LVGL objects lv_obj_t *tabview_{nullptr}; @@ -203,6 +208,11 @@ class Gui { .task_config = {.name = "gui", .stack_size_bytes = 12 * 1024}}}; espp::Logger logger_; std::recursive_mutex mutex_; + // Pending draw_circle() points, queued by (fast) producers and drained under + // mutex_ by the GUI update task; keeps the touch poll task from blocking on + // LVGL rendering. + std::mutex pending_points_mutex_; + std::vector pending_points_; // True between init_ui() and deinit_ui(). Guards set_camera_frame() (called // from the camera task) against touching the LVGL tree after teardown. bool ui_ready_{false}; From 2c684b8fddf842abb991c7e51d1bc8d91565798d Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Thu, 6 Aug 2026 16:52:45 -0500 Subject: [PATCH 13/13] feat(esp32-p4): Retrigger the click while dragging (rate-limited restart) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../example/main/esp32_p4_eth_example.cpp | 17 +++++++++++++---- .../example/main/esp32_p4_nano_example.cpp | 17 +++++++++++++---- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/components/esp32-p4-eth/example/main/esp32_p4_eth_example.cpp b/components/esp32-p4-eth/example/main/esp32_p4_eth_example.cpp index e21f6453c..0daa16277 100644 --- a/components/esp32-p4-eth/example/main/esp32_p4_eth_example.cpp +++ b/components/esp32-p4-eth/example/main/esp32_p4_eth_example.cpp @@ -143,11 +143,20 @@ extern "C" void app_main(void) { // Touch feedback (click + circle) only applies on the draw/status page; // touches on the other tabs (buttons, sliders) stay silent. if (gui.draw_page_active()) { - // Click only on the touch-DOWN edge (not on every drag movement), and - // restart it (drop any queued tail) so it tracks touches responsively. - if (touch_down_edge && !audio_bytes.empty()) { - board.clear_audio(); + // Click feedback: instant on the touch-DOWN edge, and retriggered while + // dragging - each retrigger restarts (clips) the click so drawing gives + // a stream of overlapping-feel clicks. The retrigger interval keeps a + // fast drag from restarting the click every poll (16 ms), which would + // reduce it to a buzz of its first few milliseconds. + static constexpr auto kClickRetriggerInterval = std::chrono::milliseconds(100); + static auto last_click_time = std::chrono::steady_clock::time_point{}; + const auto now = std::chrono::steady_clock::now(); + const bool click_due = + touch_down_edge || (now - last_click_time >= kClickRetriggerInterval); + if (new_touch && click_due && !audio_bytes.empty()) { + board.clear_audio(); // drop any queued tail (restart) board.play_audio(audio_bytes); // non-blocking + last_click_time = now; } if (new_touch) { gui.draw_circle(td.x, td.y, kCircleRadius); diff --git a/components/esp32-p4-nano/example/main/esp32_p4_nano_example.cpp b/components/esp32-p4-nano/example/main/esp32_p4_nano_example.cpp index 33e204c00..eb1de2e5e 100644 --- a/components/esp32-p4-nano/example/main/esp32_p4_nano_example.cpp +++ b/components/esp32-p4-nano/example/main/esp32_p4_nano_example.cpp @@ -143,11 +143,20 @@ extern "C" void app_main(void) { // Touch feedback (click + circle) only applies on the draw/status page; // touches on the other tabs (buttons, sliders) stay silent. if (gui.draw_page_active()) { - // Click only on the touch-DOWN edge (not on every drag movement), and - // restart it (drop any queued tail) so it tracks touches responsively. - if (touch_down_edge && !audio_bytes.empty()) { - board.clear_audio(); + // Click feedback: instant on the touch-DOWN edge, and retriggered while + // dragging - each retrigger restarts (clips) the click so drawing gives + // a stream of overlapping-feel clicks. The retrigger interval keeps a + // fast drag from restarting the click every poll (16 ms), which would + // reduce it to a buzz of its first few milliseconds. + static constexpr auto kClickRetriggerInterval = std::chrono::milliseconds(100); + static auto last_click_time = std::chrono::steady_clock::time_point{}; + const auto now = std::chrono::steady_clock::now(); + const bool click_due = + touch_down_edge || (now - last_click_time >= kClickRetriggerInterval); + if (new_touch && click_due && !audio_bytes.empty()) { + board.clear_audio(); // drop any queued tail (restart) board.play_audio(audio_bytes); // non-blocking + last_click_time = now; } if (new_touch) { gui.draw_circle(td.x, td.y, kCircleRadius);