From d4603212f8fa642831b73bab9874b6f367fbc862 Mon Sep 17 00:00:00 2001 From: Chen Jichang Date: Fri, 28 Aug 2026 18:20:18 +0800 Subject: [PATCH] feat(lcd): add MIPI-to-HDMI example (lt9611) --- .../esp32p4/include/hal/mipi_dsi_brg_ll.h | 5 +- .../esp32p4/include/hal/mipi_dsi_host_ll.h | 29 +-- .../esp_hal_lcd/include/hal/mipi_dsi_hal.h | 3 +- .../esp_hal_lcd/include/hal/mipi_dsi_types.h | 13 ++ components/esp_hal_lcd/mipi_dsi_hal.c | 27 ++- components/esp_lcd/dsi/esp_lcd_panel_dpi.c | 26 ++- .../esp_lcd/dsi/include/esp_lcd_mipi_dsi.h | 4 +- .../api-reference/peripherals/lcd/index.rst | 1 + .../api-reference/peripherals/lcd/index.rst | 1 + examples/peripherals/.build-test-rules.yml | 9 + .../lcd/dsi_hdmi_bridge/CMakeLists.txt | 10 + .../peripherals/lcd/dsi_hdmi_bridge/README.md | 83 ++++++++ .../lcd/dsi_hdmi_bridge/main/CMakeLists.txt | 3 + .../main/dsi_hdmi_bridge_example_main.c | 104 ++++++++++ .../dsi_hdmi_bridge/main/idf_component.yml | 5 + .../lcd/dsi_hdmi_bridge/main/lvgl_demo_ui.c | 182 ++++++++++++++++++ .../lcd/dsi_hdmi_bridge/main/lvgl_port.c | 125 ++++++++++++ .../lcd/dsi_hdmi_bridge/main/lvgl_port.h | 26 +++ .../lcd/dsi_hdmi_bridge/sdkconfig.defaults | 8 + .../sdkconfig.defaults.esp32p4 | 3 + 20 files changed, 635 insertions(+), 32 deletions(-) create mode 100644 examples/peripherals/lcd/dsi_hdmi_bridge/CMakeLists.txt create mode 100644 examples/peripherals/lcd/dsi_hdmi_bridge/README.md create mode 100644 examples/peripherals/lcd/dsi_hdmi_bridge/main/CMakeLists.txt create mode 100644 examples/peripherals/lcd/dsi_hdmi_bridge/main/dsi_hdmi_bridge_example_main.c create mode 100644 examples/peripherals/lcd/dsi_hdmi_bridge/main/idf_component.yml create mode 100644 examples/peripherals/lcd/dsi_hdmi_bridge/main/lvgl_demo_ui.c create mode 100644 examples/peripherals/lcd/dsi_hdmi_bridge/main/lvgl_port.c create mode 100644 examples/peripherals/lcd/dsi_hdmi_bridge/main/lvgl_port.h create mode 100644 examples/peripherals/lcd/dsi_hdmi_bridge/sdkconfig.defaults create mode 100644 examples/peripherals/lcd/dsi_hdmi_bridge/sdkconfig.defaults.esp32p4 diff --git a/components/esp_hal_lcd/esp32p4/include/hal/mipi_dsi_brg_ll.h b/components/esp_hal_lcd/esp32p4/include/hal/mipi_dsi_brg_ll.h index 1fffcb7e4a9..f063b9fb63c 100644 --- a/components/esp_hal_lcd/esp32p4/include/hal/mipi_dsi_brg_ll.h +++ b/components/esp_hal_lcd/esp32p4/include/hal/mipi_dsi_brg_ll.h @@ -9,6 +9,7 @@ #include #include #include "hal/assert.h" +#include "soc/mipi_dsi_bridge_reg.h" #include "soc/mipi_dsi_bridge_struct.h" #include "hal/mipi_dsi_types.h" #include "hal/lcd_types.h" @@ -16,6 +17,8 @@ #define MIPI_DSI_LL_GET_BRG(bus_id) (bus_id == 0 ? &MIPI_DSI_BRIDGE : NULL) +#define MIPI_DSI_BRG_HTOTAL_MAX DSI_BRG_HTOTAL_V + #define MIPI_DSI_BRG_LL_EVENT_UNDERRUN (1 << 0) #if HAL_CONFIG(CHIP_SUPPORT_MIN_REV) >= 300 #define MIPI_DSI_BRG_LL_EVENT_VSYNC (1 << 1) @@ -212,7 +215,7 @@ static inline void mipi_dsi_brg_ll_set_horizontal_timing(dsi_brg_dev_t *dev, uin * @brief Set the under run discard count for the bridge controller * * @param dev Pointer to the DSI bridge controller register base address - * @param under_run_discard_count Under run discard count + * @param under_run_discard_count Trigger underrun interrupt when underrun occurs and line_cnt is less than this field. */ static inline void mipi_dsi_brg_ll_set_underrun_discard_count(dsi_brg_dev_t *dev, uint32_t under_run_discard_count) { diff --git a/components/esp_hal_lcd/esp32p4/include/hal/mipi_dsi_host_ll.h b/components/esp_hal_lcd/esp32p4/include/hal/mipi_dsi_host_ll.h index e038cff427c..6d0051a3df1 100644 --- a/components/esp_hal_lcd/esp32p4/include/hal/mipi_dsi_host_ll.h +++ b/components/esp_hal_lcd/esp32p4/include/hal/mipi_dsi_host_ll.h @@ -51,15 +51,6 @@ typedef enum { MIPI_DSI_LL_COLOR_CODE_24BIT = 5, // 24-bit } mipi_dsi_ll_color_coding_t; -/** - * @brief MIPI DSI Video mode burst type - */ -typedef enum { - MIPI_DSI_LL_VIDEO_NON_BURST_WITH_SYNC_PULSES, // Non-burst mode with sync pulses - MIPI_DSI_LL_VIDEO_NON_BURST_WITH_SYNC_EVENTS, // Non-burst mode with sync events - MIPI_DSI_LL_VIDEO_BURST_WITH_SYNC_PULSES, // Burst mode with sync pulses -} mipi_dsi_ll_video_burst_type_t; - /** * @brief Set the DSI Host controller power state * @@ -284,11 +275,25 @@ static inline void mipi_dsi_host_ll_dpi_enable_lp_command(dsi_host_dev_t *dev, b * @brief Set MIPI DSI video burst type * * @param dev Pointer to the DSI Host controller register base address - * @param mode Video mode type + * @param type Video burst type */ -static inline void mipi_dsi_host_ll_dpi_set_video_burst_type(dsi_host_dev_t *dev, mipi_dsi_ll_video_burst_type_t type) +static inline void mipi_dsi_host_ll_dpi_set_video_burst_type(dsi_host_dev_t *dev, mipi_dsi_video_burst_type_t type) { - dev->vid_mode_cfg.vid_mode_type = type; + // Public enum order is not the DSI Host VID_MODE_TYPE encoding. + switch (type) { + case MIPI_DSI_VIDEO_NON_BURST_WITH_SYNC_PULSES: + dev->vid_mode_cfg.vid_mode_type = 0; + break; + case MIPI_DSI_VIDEO_NON_BURST_WITH_SYNC_EVENTS: + dev->vid_mode_cfg.vid_mode_type = 1; + break; + case MIPI_DSI_VIDEO_BURST_WITH_SYNC_PULSES: + dev->vid_mode_cfg.vid_mode_type = 2; + break; + default: + HAL_ASSERT(false); + break; + } } /** diff --git a/components/esp_hal_lcd/include/hal/mipi_dsi_hal.h b/components/esp_hal_lcd/include/hal/mipi_dsi_hal.h index 6ea714d622c..eb2340cab55 100644 --- a/components/esp_hal_lcd/include/hal/mipi_dsi_hal.h +++ b/components/esp_hal_lcd/include/hal/mipi_dsi_hal.h @@ -154,8 +154,9 @@ bool mipi_dsi_hal_host_gen_read_short_packet(mipi_dsi_hal_context_t *hal, uint8_ * @param hbp Horizontal Back Porch * @param active_width Active Width * @param hfp Horizontal Front Porch + * @return True if the compensated timing is valid, otherwise false */ -void mipi_dsi_hal_host_dpi_set_horizontal_timing(mipi_dsi_hal_context_t *hal, uint32_t hsw, uint32_t hbp, uint32_t active_width, uint32_t hfp); +bool mipi_dsi_hal_host_dpi_set_horizontal_timing(mipi_dsi_hal_context_t *hal, uint32_t hsw, uint32_t hbp, uint32_t active_width, uint32_t hfp); /** * @brief Set vertical timing parameters for DPI diff --git a/components/esp_hal_lcd/include/hal/mipi_dsi_types.h b/components/esp_hal_lcd/include/hal/mipi_dsi_types.h index 3d5078bd4b8..0594a37a498 100644 --- a/components/esp_hal_lcd/include/hal/mipi_dsi_types.h +++ b/components/esp_hal_lcd/include/hal/mipi_dsi_types.h @@ -58,6 +58,19 @@ typedef enum { MIPI_DSI_PATTERN_BER_VERTICAL, /*!< Vertical Bit Error Rate(BER) pattern */ } mipi_dsi_pattern_type_t; +/** + * @brief MIPI DSI DPI video burst type + * + * Selects how pixel packets are scheduled in DPI video mode: + * burst vs non-burst, and sync pulses vs sync events. + */ +typedef enum { + MIPI_DSI_VIDEO_BURST_WITH_SYNC_PULSES, /*!< Burst mode with sync pulses */ + MIPI_DSI_VIDEO_NON_BURST_WITH_SYNC_PULSES, /*!< Non-burst mode with sync pulses */ + MIPI_DSI_VIDEO_NON_BURST_WITH_SYNC_EVENTS, /*!< Non-burst mode with sync events */ + MIPI_DSI_VIDEO_BURST_TYPE_MAX, /*!< Number of burst types, used for parameter check */ +} mipi_dsi_video_burst_type_t; + #if SOC_MIPI_DSI_SUPPORTED /** diff --git a/components/esp_hal_lcd/mipi_dsi_hal.c b/components/esp_hal_lcd/mipi_dsi_hal.c index e6f80226138..08d2b01fbcd 100644 --- a/components/esp_hal_lcd/mipi_dsi_hal.c +++ b/components/esp_hal_lcd/mipi_dsi_hal.c @@ -256,26 +256,39 @@ bool mipi_dsi_hal_host_gen_read_dcs_command(mipi_dsi_hal_context_t *hal, uint8_t return mipi_dsi_hal_host_gen_read_short_packet(hal, vc, MIPI_DSI_DT_DCS_READ_0, header_data, ret_param, param_buf_size); } -void mipi_dsi_hal_host_dpi_set_horizontal_timing(mipi_dsi_hal_context_t *hal, uint32_t hsw, uint32_t hbp, uint32_t active_width, uint32_t hfp) +bool mipi_dsi_hal_host_dpi_set_horizontal_timing(mipi_dsi_hal_context_t *hal, uint32_t hsw, uint32_t hbp, uint32_t active_width, uint32_t hfp) { // set the horizontal timing based on user's expectation uint32_t htotal = hsw + hbp + active_width + hfp; + int compensation = (int)roundf(hal->real_dpi_clock_freq_mhz / hal->expect_dpi_clock_freq_mhz * htotal) - (int)htotal; + int compensated_hfp = (int)hfp + compensation; + int compensated_htotal = (int)htotal + compensation; + if (compensated_hfp <= 0 || compensated_htotal > MIPI_DSI_BRG_HTOTAL_MAX) { + HAL_LOGE(TAG, "invalid compensated timing: hfp=%d, htotal=%d", compensated_hfp, compensated_htotal); + return false; + } + if (fabsf(hal->real_dpi_clock_freq_mhz - hal->expect_dpi_clock_freq_mhz) > 0.01f) { + HAL_LOGW(TAG, "DPI clock adjusted from %.2f MHz to %.2f MHz, compensated hfp=%d, compensated htotal=%d", + hal->expect_dpi_clock_freq_mhz, hal->real_dpi_clock_freq_mhz, + compensated_hfp, compensated_htotal); + } + float dpi2lane_clk_ratio = hal->lane_bit_rate_mbps / hal->expect_dpi_clock_freq_mhz / 8.0f; uint32_t host_hsw = (uint32_t)roundf(hsw * dpi2lane_clk_ratio); uint32_t host_hbp = (uint32_t)roundf(hbp * dpi2lane_clk_ratio); uint32_t host_act = (uint32_t)roundf(active_width * dpi2lane_clk_ratio); uint32_t host_hfp = (uint32_t)roundf(hfp * dpi2lane_clk_ratio); uint32_t host_htotal = (uint32_t)roundf(htotal * dpi2lane_clk_ratio); - int compensation = (int)host_htotal - (int)(host_hsw + host_hbp + host_act + host_hfp); - mipi_dsi_host_ll_dpi_set_horizontal_timing(hal->host, host_hsw, host_hbp, host_act + compensation, host_hfp); + int host_compensation = (int)host_htotal - (int)(host_hsw + host_hbp + host_act + host_hfp); + mipi_dsi_host_ll_dpi_set_horizontal_timing(hal->host, host_hsw, host_hbp, host_act + host_compensation, host_hfp); HAL_LOGD(TAG, "host video timing: hsw=%" PRIu32 ", hbp=%" PRIu32 ", act=%" PRIu32 ", hfp=%" PRIu32, - host_hsw, host_hbp, host_act + compensation, host_hfp); + host_hsw, host_hbp, host_act + host_compensation, host_hfp); // compensate the video timing to achieve the same refresh rate as expected - compensation = (int)roundf(hal->real_dpi_clock_freq_mhz / hal->expect_dpi_clock_freq_mhz * htotal) - (int)htotal; - mipi_dsi_brg_ll_set_horizontal_timing(hal->bridge, hsw, hbp, active_width, hfp + compensation); + mipi_dsi_brg_ll_set_horizontal_timing(hal->bridge, hsw, hbp, active_width, compensated_hfp); HAL_LOGD(TAG, "brg video timing: hsw=%" PRIu32 ", hbp=%" PRIu32 ", act=%" PRIu32 ", hfp=%" PRIu32, - hsw, hbp, active_width, hfp + compensation); + hsw, hbp, active_width, (uint32_t)compensated_hfp); + return true; } void mipi_dsi_hal_host_dpi_set_vertical_timing(mipi_dsi_hal_context_t *hal, uint32_t vsw, uint32_t vbp, uint32_t active_height, uint32_t vfp) diff --git a/components/esp_lcd/dsi/esp_lcd_panel_dpi.c b/components/esp_lcd/dsi/esp_lcd_panel_dpi.c index f1d236a221b..599e90e269d 100644 --- a/components/esp_lcd/dsi/esp_lcd_panel_dpi.c +++ b/components/esp_lcd/dsi/esp_lcd_panel_dpi.c @@ -189,6 +189,7 @@ esp_err_t esp_lcd_new_panel_dpi(esp_lcd_dsi_bus_handle_t bus, const esp_lcd_dpi_ ESP_RETURN_ON_FALSE(bus && panel_config && ret_panel, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); ESP_RETURN_ON_FALSE(panel_config->virtual_channel < 4, ESP_ERR_INVALID_ARG, TAG, "invalid virtual channel %d", panel_config->virtual_channel); ESP_RETURN_ON_FALSE(panel_config->dpi_clock_freq_mhz > 0, ESP_ERR_INVALID_ARG, TAG, "invalid DPI clock frequency %.2f", panel_config->dpi_clock_freq_mhz); + ESP_RETURN_ON_FALSE(panel_config->video_burst_type < MIPI_DSI_VIDEO_BURST_TYPE_MAX, ESP_ERR_INVALID_ARG, TAG, "invalid DPI video burst type"); size_t num_fbs = panel_config->num_fbs; // if the user doesn't specify the number of frame buffers, then fallback to use one frame buffer @@ -308,27 +309,32 @@ esp_err_t esp_lcd_new_panel_dpi(esp_lcd_dsi_bus_handle_t bus, const esp_lcd_dpi_ // commands are transmitted in low-power mode mipi_dsi_host_ll_dpi_enable_lp_command(hal->host, true); } - // after sending a frame, the DSI device should return an ack - mipi_dsi_host_ll_dpi_enable_frame_ack(hal->host, true); - // using the burst mode because it's energy-efficient - mipi_dsi_host_ll_dpi_set_video_burst_type(hal->host, MIPI_DSI_LL_VIDEO_BURST_WITH_SYNC_PULSES); - // configure the size of the active lin period, measured in pixels + // Some bridge devices don't return a frame acknowledgement. + mipi_dsi_host_ll_dpi_enable_frame_ack(hal->host, !panel_config->flags.disable_frame_ack); + mipi_dsi_host_ll_dpi_set_video_burst_type(hal->host, panel_config->video_burst_type); + // configure the size of the active line period, measured in pixels mipi_dsi_host_ll_dpi_set_video_packet_pixel_num(hal->host, panel_config->video_timing.h_size); // disable multi-packets mipi_dsi_host_ll_dpi_set_trunks_num(hal->host, 0); // disable "null packets" mipi_dsi_host_ll_dpi_set_null_packet_size(hal->host, 0); // set horizontal and vertical timing configuration - mipi_dsi_hal_host_dpi_set_horizontal_timing(hal, panel_config->video_timing.hsync_pulse_width, - panel_config->video_timing.hsync_back_porch, - panel_config->video_timing.h_size, - panel_config->video_timing.hsync_front_porch); + ESP_GOTO_ON_FALSE(mipi_dsi_hal_host_dpi_set_horizontal_timing(hal, panel_config->video_timing.hsync_pulse_width, + panel_config->video_timing.hsync_back_porch, + panel_config->video_timing.h_size, + panel_config->video_timing.hsync_front_porch), + ESP_ERR_INVALID_ARG, err, TAG, "invalid compensated horizontal timing"); mipi_dsi_hal_host_dpi_set_vertical_timing(hal, panel_config->video_timing.vsync_pulse_width, panel_config->video_timing.vsync_back_porch, panel_config->video_timing.v_size, panel_config->video_timing.vsync_front_porch); mipi_dsi_brg_ll_set_num_pixel_bits(hal->bridge, panel_config->video_timing.h_size * panel_config->video_timing.v_size * bits_per_pixel); - mipi_dsi_brg_ll_set_underrun_discard_count(hal->bridge, panel_config->video_timing.h_size); + // Report underruns for the whole frame: IRQ when underrun occurs and line_cnt < vtotal. + mipi_dsi_brg_ll_set_underrun_discard_count(hal->bridge, + panel_config->video_timing.vsync_pulse_width + + panel_config->video_timing.vsync_back_porch + + panel_config->video_timing.v_size + + panel_config->video_timing.vsync_front_porch); // set the in/out color formats in the DSI bridge mipi_dsi_brg_ll_set_input_color_format(hal->bridge, in_color_format); mipi_dsi_brg_ll_set_output_color_format(hal->bridge, out_color_format, 0); diff --git a/components/esp_lcd/dsi/include/esp_lcd_mipi_dsi.h b/components/esp_lcd/dsi/include/esp_lcd_mipi_dsi.h index 455c50b92d2..58839d4d2f5 100644 --- a/components/esp_lcd/dsi/include/esp_lcd_mipi_dsi.h +++ b/components/esp_lcd/dsi/include/esp_lcd_mipi_dsi.h @@ -94,10 +94,12 @@ typedef struct { which is the format that the panel can accept */ uint8_t num_fbs; /*!< Number of screen-sized frame buffers that allocated by the driver By default (set to either 0 or 1) only one frame buffer will be created */ + mipi_dsi_video_burst_type_t video_burst_type; /*!< DPI video burst type */ esp_lcd_video_timing_t video_timing; /*!< Video timing */ /// Extra configuration flags for MIPI DSI DPI panel struct extra_dpi_panel_flags { - uint32_t disable_lp: 1;/*!< Disable low-power for DPI */ + uint32_t disable_lp: 1; /*!< Disable low-power for DPI */ + uint32_t disable_frame_ack: 1; /*!< Disable the frame acknowledgement request */ } flags; /*!< Extra configuration flags */ } esp_lcd_dpi_panel_config_t; diff --git a/docs/en/api-reference/peripherals/lcd/index.rst b/docs/en/api-reference/peripherals/lcd/index.rst index f5703c163de..61ae4b132f1 100644 --- a/docs/en/api-reference/peripherals/lcd/index.rst +++ b/docs/en/api-reference/peripherals/lcd/index.rst @@ -95,6 +95,7 @@ Application Example :SOC_LCD_RGB_SUPPORTED: * :example:`peripherals/lcd/rgb_panel` demonstrates how to install an RGB panel driver, display a scatter chart on the screen based on the LVGL library. :SOC_I2C_SUPPORTED: * :example:`peripherals/lcd/i2c_oled` demonstrates how to use the SSD1306 panel driver from the `esp_lcd` component to facilitate the porting of LVGL library and display a scrolling text on the OLED screen. :SOC_MIPI_DSI_SUPPORTED: * :example:`peripherals/lcd/mipi_dsi` demonstrates the general process of installing a MIPI DSI LCD driver, and displays a LVGL widget on the screen. + :SOC_MIPI_DSI_SUPPORTED: * :example:`peripherals/lcd/dsi_hdmi_bridge` demonstrates how to drive an HDMI display through a MIPI DSI to HDMI bridge and show a full-screen LVGL UI. :SOC_PARLIO_LCD_SUPPORTED: * :example:`peripherals/lcd/parlio_simulate` demonstrates how to use Parallel IO peripheral to drive an SPI or I80 Interfaced LCD. diff --git a/docs/zh_CN/api-reference/peripherals/lcd/index.rst b/docs/zh_CN/api-reference/peripherals/lcd/index.rst index 8120d42570d..26139419734 100644 --- a/docs/zh_CN/api-reference/peripherals/lcd/index.rst +++ b/docs/zh_CN/api-reference/peripherals/lcd/index.rst @@ -95,6 +95,7 @@ LCD 数据面板操作 :SOC_LCD_RGB_SUPPORTED: * :example:`peripherals/lcd/rgb_panel` 展示了如何安装 RGB 面板驱动程序,并基于 LVGL 库在屏幕上显示散点图。 :SOC_I2C_SUPPORTED: * :example:`peripherals/lcd/i2c_oled` 演示了如何使用 `esp_lcd` 组件中的 SSD1306 面板驱动来简化移植 LVGL 库,并在 OLED 屏幕上显示滚动文本。 :SOC_MIPI_DSI_SUPPORTED: * :example:`peripherals/lcd/mipi_dsi` 演示了如何安装 MIPI DSI LCD 驱动程序,并在屏幕上显示一个 LVGL 小部件。 + :SOC_MIPI_DSI_SUPPORTED: * :example:`peripherals/lcd/dsi_hdmi_bridge` 演示了如何通过 MIPI DSI 转 HDMI 桥接器驱动 HDMI 显示器,并在大屏上显示全屏 LVGL 界面。 :SOC_PARLIO_LCD_SUPPORTED: * :example:`peripherals/lcd/parlio_simulate` 演示了如何使用 Parallel IO 外设驱动 SPI 或 I80 接口的屏幕。 diff --git a/examples/peripherals/.build-test-rules.yml b/examples/peripherals/.build-test-rules.yml index 565c3171c17..44d6beff0f2 100644 --- a/examples/peripherals/.build-test-rules.yml +++ b/examples/peripherals/.build-test-rules.yml @@ -317,6 +317,15 @@ examples/peripherals/jpeg/jpeg_encode: - esp_driver_jpeg - soc +examples/peripherals/lcd/dsi_hdmi_bridge: + disable: + - if: SOC_MIPI_DSI_SUPPORTED != 1 + depends_components: + - esp_driver_dma + - esp_hal_lcd + - esp_lcd + - soc + examples/peripherals/lcd/i2c_oled: disable: - if: SOC_I2C_SUPPORTED != 1 diff --git a/examples/peripherals/lcd/dsi_hdmi_bridge/CMakeLists.txt b/examples/peripherals/lcd/dsi_hdmi_bridge/CMakeLists.txt new file mode 100644 index 00000000000..7e10c6c63c4 --- /dev/null +++ b/examples/peripherals/lcd/dsi_hdmi_bridge/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +idf_build_set_property(MINIMAL_BUILD ON) +project(dsi_hdmi_bridge) + +# TODO IDF-15784: remove +# lvgl upstream sets locals without reading them; silence -Wunused-but-set-variable +idf_component_get_property(lvgl_lib lvgl__lvgl COMPONENT_LIB) +target_compile_options(${lvgl_lib} PRIVATE -Wno-unused-but-set-variable) diff --git a/examples/peripherals/lcd/dsi_hdmi_bridge/README.md b/examples/peripherals/lcd/dsi_hdmi_bridge/README.md new file mode 100644 index 00000000000..68430ebd7b4 --- /dev/null +++ b/examples/peripherals/lcd/dsi_hdmi_bridge/README.md @@ -0,0 +1,83 @@ +| Supported Targets | ESP32-P4 | +| ----------------- | -------- | + +# MIPI DSI to HDMI Bridge Example + +This example demonstrates how to drive an HDMI display from the MIPI DSI peripheral through an external DSI-to-HDMI bridge. + +The example uses the [LT9611](https://components.espressif.com/components/espressif/esp_lcd_lt9611) at 1920x1080@60 Hz in RGB565. It displays a full-screen LVGL dashboard using DMA2D and partial draw buffers in internal RAM. + +## How to use the example + +### Hardware Required + +* An ESP32-P4 development board +* An LT9611 DSI-to-HDMI bridge +* An HDMI display and cable +* A USB cable for power supply and programming + +### Hardware Connection + +The connection between the ESP board, the LT9611 bridge, and the HDMI display is as follows: + +```text + ESP Board LT9611 Bridge HDMI Display ++-----------------------+ +-------------------+ +-------------------+ +| 3V3 +------+ VCC | | | +| | | | | | +| DSI_CLK_P +------+ DSI_CLK_P | | | +| DSI_CLK_N +------+ DSI_CLK_N | | | +| DSI_DAT0_P +------+ DSI_DAT0_P | | | +| DSI_DAT0_N +------+ DSI_DAT0_N | | | +| DSI_DAT1_P +------+ DSI_DAT1_P | | | +| DSI_DAT1_N +------+ DSI_DAT1_N | | | +| | | HDMI OUT+----------+HDMI IN | +| SDA +------+ SDA | | | +| SCL +------+ SCL | | | +| Reset +------+ Reset (optional) | | | +| | | | | | ++-----------------------+ +-------------------+ +-------------------+ +``` + +Before building, update `EXAMPLE_PIN_NUM_LCD_RST`, `EXAMPLE_I2C_NUM`, `EXAMPLE_PIN_NUM_SDA`, and `EXAMPLE_PIN_NUM_SCL` in [dsi_hdmi_bridge_example_main.c](main/dsi_hdmi_bridge_example_main.c) according to the board wiring. + +The example powers `VDD_MIPI_DPHY` from LDO channel 3 at 2500 mV. Update `EXAMPLE_MIPI_DSI_PHY_PWR_LDO_CHAN` and `EXAMPLE_MIPI_DSI_PHY_PWR_LDO_VOLTAGE_MV` in the source according to the board power design. + +### Build and Flash + +Run the following command to build, flash, and monitor the example: + +```text +idf.py -p PORT build flash monitor +``` + +The first build can take additional time while the Component Manager resolves the external bridge and LVGL dependencies. + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html) for complete instructions. + +### Example Output + +```text +I (...) dsi_hdmi_bridge: MIPI D-PHY powered by LDO channel 3 at 2500 mV +I (...) dsi_hdmi_bridge: Initialize LVGL +I (...) dsi_hdmi_bridge: Starting LVGL task +I (...) dsi_hdmi_bridge: Displaying LVGL UI at 1920x1080 +``` + +## Troubleshooting + +If the log shows: + +```text +E lcd.dsi: can't fetch data from external memory fast enough, underrun happens +``` + +the DSI bridge cannot fetch the framebuffer from PSRAM fast enough. Reduce PSRAM traffic, for example: + +* Disable PSRAM XIP (this example already does so for 1080p60 RGB565) +* Keep LVGL draw buffers in internal RAM instead of PSRAM +* Avoid other large PSRAM allocations or DMA while the display is running + +For technical questions, please open an [issue](https://github.com/espressif/esp-idf/issues). diff --git a/examples/peripherals/lcd/dsi_hdmi_bridge/main/CMakeLists.txt b/examples/peripherals/lcd/dsi_hdmi_bridge/main/CMakeLists.txt new file mode 100644 index 00000000000..a4ab301637e --- /dev/null +++ b/examples/peripherals/lcd/dsi_hdmi_bridge/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "dsi_hdmi_bridge_example_main.c" "lvgl_port.c" "lvgl_demo_ui.c" + INCLUDE_DIRS "." + PRIV_REQUIRES esp_driver_i2c esp_lcd esp_timer) diff --git a/examples/peripherals/lcd/dsi_hdmi_bridge/main/dsi_hdmi_bridge_example_main.c b/examples/peripherals/lcd/dsi_hdmi_bridge/main/dsi_hdmi_bridge_example_main.c new file mode 100644 index 00000000000..8fb42d1d44b --- /dev/null +++ b/examples/peripherals/lcd/dsi_hdmi_bridge/main/dsi_hdmi_bridge_example_main.c @@ -0,0 +1,104 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#include "driver/i2c_master.h" +#include "esp_err.h" +#include "esp_lcd_lt9611.h" +#include "esp_lcd_mipi_dsi.h" +#include "esp_lcd_panel_ops.h" +#include "esp_ldo_regulator.h" +#include "esp_log.h" +#include "lvgl_port.h" + +static const char *TAG = "dsi_hdmi_bridge"; + +// The "VDD_MIPI_DPHY" should be supplied with 2.5V, it can source from the internal LDO regulator or from external LDO chip +#define EXAMPLE_MIPI_DSI_PHY_PWR_LDO_CHAN 3 // LDO_VO3 is connected to VDD_MIPI_DPHY +#define EXAMPLE_MIPI_DSI_PHY_PWR_LDO_VOLTAGE_MV 2500 +#define EXAMPLE_MIPI_DSI_LANE_NUM 2 +#define EXAMPLE_PIN_NUM_LCD_RST -1 + +// Update these values according to the board I2C wiring. +#define EXAMPLE_I2C_PORT_NUM 0 +#define EXAMPLE_PIN_NUM_SDA 7 +#define EXAMPLE_PIN_NUM_SCL 8 + +static void example_enable_mipi_dphy_power(void) +{ + const esp_ldo_channel_config_t ldo_config = { + .chan_id = EXAMPLE_MIPI_DSI_PHY_PWR_LDO_CHAN, + .voltage_mv = EXAMPLE_MIPI_DSI_PHY_PWR_LDO_VOLTAGE_MV, + }; + esp_ldo_channel_handle_t ldo = NULL; + ESP_ERROR_CHECK(esp_ldo_acquire_channel(&ldo_config, &ldo)); + ESP_LOGI(TAG, "MIPI D-PHY powered by LDO channel %d at %d mV", + EXAMPLE_MIPI_DSI_PHY_PWR_LDO_CHAN, + EXAMPLE_MIPI_DSI_PHY_PWR_LDO_VOLTAGE_MV); +} + +static i2c_master_bus_handle_t create_i2c_bus(void) +{ + const i2c_master_bus_config_t bus_config = { + .clk_source = I2C_CLK_SRC_DEFAULT, + .glitch_ignore_cnt = 7, + .i2c_port = EXAMPLE_I2C_PORT_NUM, + .sda_io_num = EXAMPLE_PIN_NUM_SDA, + .scl_io_num = EXAMPLE_PIN_NUM_SCL, + .flags.enable_internal_pullup = true, + }; + i2c_master_bus_handle_t bus = NULL; + ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &bus)); + return bus; +} + +static esp_lcd_dsi_bus_handle_t create_dsi_bus(void) +{ + const esp_lcd_dsi_bus_config_t bus_config = { + .bus_id = 0, + .num_data_lanes = EXAMPLE_MIPI_DSI_LANE_NUM, + .lane_bit_rate_mbps = LT9611_1920x1080_60HZ_RGB16_LANE_BITRATE_MBPS, + }; + esp_lcd_dsi_bus_handle_t bus = NULL; + ESP_ERROR_CHECK(esp_lcd_new_dsi_bus(&bus_config, &bus)); + return bus; +} + +static esp_lcd_panel_handle_t create_lt9611_panel(i2c_master_bus_handle_t i2c_bus, + esp_lcd_dsi_bus_handle_t dsi_bus) +{ + const esp_lcd_dpi_panel_config_t dpi_config = LT9611_1920x1080_60HZ_RGB16_DPI_PANEL_CONFIG_WITH_FBS(1); + const lt9611_vendor_config_t vendor_config = { + .mipi_config = { + .dsi_bus = dsi_bus, + .dpi_config = &dpi_config, + .lane_num = EXAMPLE_MIPI_DSI_LANE_NUM, + }, + }; + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = EXAMPLE_PIN_NUM_LCD_RST, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, + .bits_per_pixel = 16, + .flags.reset_active_high = true, + .vendor_config = (void *) &vendor_config, + }; + esp_lcd_panel_handle_t panel = NULL; + ESP_ERROR_CHECK(esp_lcd_new_panel_lt9611(i2c_bus, &panel_config, &panel)); + return panel; +} + +void app_main(void) +{ + example_enable_mipi_dphy_power(); + i2c_master_bus_handle_t i2c_bus = create_i2c_bus(); + esp_lcd_dsi_bus_handle_t dsi_bus = create_dsi_bus(); + esp_lcd_panel_handle_t panel = create_lt9611_panel(i2c_bus, dsi_bus); + + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel)); + ESP_ERROR_CHECK(esp_lcd_dpi_panel_enable_dma2d(panel)); + // Draw the first LVGL frame into the framebuffer before starting DPI scanout. + example_lvgl_init(panel); + ESP_ERROR_CHECK(esp_lcd_panel_init(panel)); +} diff --git a/examples/peripherals/lcd/dsi_hdmi_bridge/main/idf_component.yml b/examples/peripherals/lcd/dsi_hdmi_bridge/main/idf_component.yml new file mode 100644 index 00000000000..4aa9a80846e --- /dev/null +++ b/examples/peripherals/lcd/dsi_hdmi_bridge/main/idf_component.yml @@ -0,0 +1,5 @@ +description: MIPI DSI to HDMI bridge example + +dependencies: + lvgl/lvgl: "9.5.0" + esp_lcd_lt9611: "^0.1.0" diff --git a/examples/peripherals/lcd/dsi_hdmi_bridge/main/lvgl_demo_ui.c b/examples/peripherals/lcd/dsi_hdmi_bridge/main/lvgl_demo_ui.c new file mode 100644 index 00000000000..d8c797d890a --- /dev/null +++ b/examples/peripherals/lcd/dsi_hdmi_bridge/main/lvgl_demo_ui.c @@ -0,0 +1,182 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#include "lvgl.h" + +#define EXAMPLE_COLOR_FORMAT "RGB565" + +static void strip_default_obj_style(lv_obj_t *obj) +{ + lv_obj_remove_flag(obj, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(obj, 0, 0); + lv_obj_set_style_pad_all(obj, 0, 0); + lv_obj_set_style_radius(obj, 0, 0); + lv_obj_set_style_shadow_width(obj, 0, 0); +} + +static void gauge_anim_cb(void *var, int32_t value) +{ + lv_obj_t *arc = var; + lv_arc_set_value(arc, value); + lv_label_set_text_fmt((lv_obj_t *)lv_obj_get_user_data(arc), "%" LV_PRId32 "%%", value); +} + +static void create_header(lv_obj_t *parent, int32_t hor_res, int32_t ver_res, const lv_font_t *font) +{ + lv_obj_t *header = lv_obj_create(parent); + strip_default_obj_style(header); + lv_obj_set_size(header, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_bg_opa(header, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(header, lv_color_hex(0x151A22), 0); + lv_obj_set_style_text_color(header, lv_color_white(), 0); + lv_obj_set_style_pad_hor(header, ver_res / 24, 0); + lv_obj_set_style_pad_ver(header, ver_res / 36, 0); + + static int32_t cols[] = {LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST}; + static int32_t rows[] = {LV_GRID_CONTENT, LV_GRID_TEMPLATE_LAST}; + lv_obj_set_grid_dsc_array(header, cols, rows); + + lv_obj_t *left = lv_label_create(header); + lv_obj_set_style_text_font(left, font, 0); + lv_obj_set_style_text_color(left, lv_color_white(), 0); + lv_label_set_text(left, "MIPI DSI to HDMI"); + lv_obj_set_grid_cell(left, LV_GRID_ALIGN_START, 0, 1, LV_GRID_ALIGN_CENTER, 0, 1); + + lv_obj_t *right = lv_label_create(header); + lv_obj_set_style_text_font(right, font, 0); + lv_obj_set_style_text_color(right, lv_color_white(), 0); + lv_label_set_text_fmt(right, "%" LV_PRId32 "x%" LV_PRId32 " %s", hor_res, ver_res, EXAMPLE_COLOR_FORMAT); + lv_obj_set_grid_cell(right, LV_GRID_ALIGN_END, 2, 1, LV_GRID_ALIGN_CENTER, 0, 1); +} + +static lv_obj_t *create_gauge_card(lv_obj_t *parent, const char *name, lv_color_t color, + int32_t gauge_size, int32_t arc_width, + const lv_font_t *font_value, const lv_font_t *font_caption) +{ + lv_obj_t *card = lv_obj_create(parent); + strip_default_obj_style(card); + lv_obj_set_flex_grow(card, 1); + lv_obj_set_height(card, LV_PCT(100)); + lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(card, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_bg_opa(card, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(card, lv_color_hex(0x171E28), 0); + lv_obj_set_style_radius(card, 16, 0); + lv_obj_set_style_pad_all(card, 16, 0); + lv_obj_set_style_pad_row(card, 12, 0); + + lv_obj_t *arc = lv_arc_create(card); + lv_obj_set_size(arc, gauge_size, gauge_size); + lv_arc_set_rotation(arc, 270); + lv_arc_set_bg_angles(arc, 0, 360); + lv_arc_set_range(arc, 0, 100); + lv_arc_set_value(arc, 0); + lv_obj_remove_style(arc, NULL, LV_PART_KNOB); + lv_obj_remove_flag(arc, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_style_arc_width(arc, arc_width, LV_PART_MAIN); + lv_obj_set_style_arc_width(arc, arc_width, LV_PART_INDICATOR); + lv_obj_set_style_arc_color(arc, lv_color_hex(0x2A3140), LV_PART_MAIN); + lv_obj_set_style_arc_color(arc, color, LV_PART_INDICATOR); + lv_obj_set_style_arc_rounded(arc, true, LV_PART_MAIN); + lv_obj_set_style_arc_rounded(arc, true, LV_PART_INDICATOR); + + lv_obj_t *value = lv_label_create(arc); + lv_obj_set_style_text_font(value, font_value, 0); + lv_obj_set_style_text_color(value, lv_color_white(), 0); + lv_label_set_text(value, "0%"); + lv_obj_center(value); + lv_obj_set_user_data(arc, value); + + lv_obj_t *caption = lv_label_create(card); + lv_obj_set_style_text_font(caption, font_caption, 0); + lv_obj_set_style_text_color(caption, lv_color_hex(0xA8B0C0), 0); + lv_label_set_text(caption, name); + + return arc; +} + +static void create_color_bars(lv_obj_t *parent, int32_t height) +{ + static const uint32_t colors[] = { + 0xFFFFFF, 0xFFFF00, 0x00FFFF, 0x00FF00, + 0xFF00FF, 0xFF0000, 0x0000FF, 0x3C3C3C, + }; + + lv_obj_t *row = lv_obj_create(parent); + strip_default_obj_style(row); + lv_obj_set_size(row, LV_PCT(100), height); + lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); + lv_obj_set_style_pad_column(row, 0, 0); + + for (size_t i = 0; i < sizeof(colors) / sizeof(colors[0]); i++) { + lv_obj_t *bar = lv_obj_create(row); + strip_default_obj_style(bar); + lv_obj_set_flex_grow(bar, 1); + lv_obj_set_height(bar, LV_PCT(100)); + lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, 0); + lv_obj_set_style_bg_color(bar, lv_color_hex(colors[i]), 0); + } +} + +void example_lvgl_demo_ui(lv_display_t *disp) +{ + const int32_t hor_res = lv_display_get_horizontal_resolution(disp); + const int32_t ver_res = lv_display_get_vertical_resolution(disp); + const bool is_1080p = (ver_res >= 1000); + const lv_font_t *font_header = is_1080p ? &lv_font_montserrat_48 : &lv_font_montserrat_28; + const lv_font_t *font_value = &lv_font_montserrat_48; + const lv_font_t *font_caption = is_1080p ? &lv_font_montserrat_28 : &lv_font_montserrat_16; + const int32_t pad = is_1080p ? 32 : 20; + const int32_t gauge_size = ver_res * 42 / 100; + const int32_t arc_width = is_1080p ? 24 : 16; + + lv_theme_default_init(disp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED), + true, font_caption); + + lv_obj_t *scr = lv_display_get_screen_active(disp); + lv_obj_remove_flag(scr, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(scr, lv_color_hex(0x0B0E12), 0); + lv_obj_set_style_text_color(scr, lv_color_white(), 0); + lv_obj_set_style_border_width(scr, 0, 0); + lv_obj_set_style_radius(scr, 0, 0); + lv_obj_set_style_pad_all(scr, 0, 0); + lv_obj_set_style_pad_row(scr, 0, 0); + lv_obj_set_flex_flow(scr, LV_FLEX_FLOW_COLUMN); + + create_header(scr, hor_res, ver_res, font_header); + + lv_obj_t *gauges = lv_obj_create(scr); + strip_default_obj_style(gauges); + lv_obj_set_width(gauges, LV_PCT(100)); + lv_obj_set_flex_grow(gauges, 1); + lv_obj_set_flex_flow(gauges, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(gauges, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_all(gauges, pad, 0); + lv_obj_set_style_pad_column(gauges, pad, 0); + + static const char *names[] = {"Video", "Render", "Memory"}; + static const lv_palette_t palette[] = {LV_PALETTE_BLUE, LV_PALETTE_RED, LV_PALETTE_GREEN}; + static const uint32_t durations[] = {4100, 2600, 2800}; + static const uint32_t playbacks[] = {2700, 3200, 1800}; + + lv_anim_t animation; + lv_anim_init(&animation); + lv_anim_set_values(&animation, 20, 100); + lv_anim_set_repeat_count(&animation, LV_ANIM_REPEAT_INFINITE); + lv_anim_set_exec_cb(&animation, gauge_anim_cb); + + for (int i = 0; i < 3; i++) { + lv_obj_t *arc = create_gauge_card(gauges, names[i], lv_palette_main(palette[i]), + gauge_size, arc_width, font_value, font_caption); + lv_anim_set_var(&animation, arc); + lv_anim_set_duration(&animation, durations[i]); + lv_anim_set_playback_duration(&animation, playbacks[i]); + lv_anim_start(&animation); + } + + create_color_bars(scr, ver_res * 12 / 100); +} diff --git a/examples/peripherals/lcd/dsi_hdmi_bridge/main/lvgl_port.c b/examples/peripherals/lcd/dsi_hdmi_bridge/main/lvgl_port.c new file mode 100644 index 00000000000..725001a92a0 --- /dev/null +++ b/examples/peripherals/lcd/dsi_hdmi_bridge/main/lvgl_port.c @@ -0,0 +1,125 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#include +#include +#include +#include +#include "sdkconfig.h" +#include "esp_heap_caps.h" +#include "esp_lcd_mipi_dsi.h" +#include "esp_lcd_panel_ops.h" +#include "esp_log.h" +#include "esp_macros.h" +#include "esp_timer.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "lvgl.h" +#include "lvgl_port.h" + +static const char *TAG = "dsi_hdmi_bridge"; + +#define EXAMPLE_H_RES 1920 +#define EXAMPLE_V_RES 1080 +#define EXAMPLE_BYTES_PER_PIXEL 2 +#define EXAMPLE_LVGL_COLOR_FORMAT LV_COLOR_FORMAT_RGB565 +#define EXAMPLE_LVGL_DRAW_BUF_LINES 20 +#define EXAMPLE_LVGL_TICK_PERIOD_MS 2 +#define EXAMPLE_LVGL_TASK_STACK_SIZE (8 * 1024) +#define EXAMPLE_LVGL_TASK_PRIORITY 2 +#define EXAMPLE_LVGL_TASK_MAX_DELAY_MS 500 +#define EXAMPLE_LVGL_TASK_MIN_DELAY_MS (1000 / CONFIG_FREERTOS_HZ) + +// LVGL APIs are called from multiple tasks, so serialize access with a lock. +static _lock_t s_lvgl_api_lock; + +extern void example_lvgl_demo_ui(lv_display_t *disp); + +static void example_rounder_flush_area_cb(lv_event_t *event) +{ + // DMA2D / MSPI may require 16-byte aligned windows (flash encryption, PSRAM ECC, etc.). + lv_area_t *area = lv_event_get_invalidated_area(event); + area->x1 = ESP_ALIGN_DOWN(area->x1, 16); + area->x2 = ESP_ALIGN_UP(area->x2 + 1, 16) - 1; +} + +static void example_lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) +{ + esp_lcd_panel_handle_t panel = lv_display_get_user_data(disp); + // LVGL coordinates are inclusive, while draw_bitmap uses an exclusive end. + ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(panel, area->x1, area->y1, area->x2 + 1, area->y2 + 1, px_map)); +} + +static bool example_notify_lvgl_flush_ready(esp_lcd_panel_handle_t panel, + esp_lcd_dpi_panel_event_data_t *edata, + void *user_ctx) +{ + lv_display_flush_ready((lv_display_t *)user_ctx); + return false; +} + +static void example_increase_lvgl_tick(void *arg) +{ + lv_tick_inc(EXAMPLE_LVGL_TICK_PERIOD_MS); +} + +static void example_lvgl_port_task(void *arg) +{ + ESP_LOGI(TAG, "Starting LVGL task"); + while (true) { + _lock_acquire(&s_lvgl_api_lock); + uint32_t time_till_next_ms = lv_timer_handler(); + _lock_release(&s_lvgl_api_lock); + time_till_next_ms = MAX(time_till_next_ms, EXAMPLE_LVGL_TASK_MIN_DELAY_MS); + time_till_next_ms = MIN(time_till_next_ms, EXAMPLE_LVGL_TASK_MAX_DELAY_MS); + usleep(1000 * time_till_next_ms); + } +} + +void example_lvgl_init(esp_lcd_panel_handle_t panel) +{ + ESP_LOGI(TAG, "Initialize LVGL"); + lv_init(); + lv_display_t *display = lv_display_create(EXAMPLE_H_RES, EXAMPLE_V_RES); + assert(display); + lv_display_set_user_data(display, panel); + lv_display_set_color_format(display, EXAMPLE_LVGL_COLOR_FORMAT); + + const size_t draw_buffer_size = + EXAMPLE_H_RES * EXAMPLE_LVGL_DRAW_BUF_LINES * EXAMPLE_BYTES_PER_PIXEL; + // Keep the small 20-line partial buffers in internal RAM to reduce PSRAM + // contention with the continuous framebuffer scanout. + void *buf1 = heap_caps_aligned_calloc(16, 1, draw_buffer_size, + MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + void *buf2 = heap_caps_aligned_calloc(16, 1, draw_buffer_size, + MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + assert(buf1 && buf2); + lv_display_set_buffers(display, buf1, buf2, draw_buffer_size, LV_DISPLAY_RENDER_MODE_PARTIAL); + lv_display_set_flush_cb(display, example_lvgl_flush_cb); + lv_display_add_event_cb(display, example_rounder_flush_area_cb, LV_EVENT_INVALIDATE_AREA, NULL); + + const esp_lcd_dpi_panel_event_callbacks_t callbacks = { + .on_color_trans_done = example_notify_lvgl_flush_ready, + }; + ESP_ERROR_CHECK(esp_lcd_dpi_panel_register_event_callbacks(panel, &callbacks, display)); + + const esp_timer_create_args_t tick_timer_args = { + .callback = example_increase_lvgl_tick, + .name = "lvgl_tick", + }; + esp_timer_handle_t tick_timer = NULL; + ESP_ERROR_CHECK(esp_timer_create(&tick_timer_args, &tick_timer)); + ESP_ERROR_CHECK(esp_timer_start_periodic(tick_timer, EXAMPLE_LVGL_TICK_PERIOD_MS * 1000)); + + _lock_acquire(&s_lvgl_api_lock); + example_lvgl_demo_ui(display); + _lock_release(&s_lvgl_api_lock); + BaseType_t task_created = xTaskCreate(example_lvgl_port_task, "LVGL", + EXAMPLE_LVGL_TASK_STACK_SIZE, NULL, + EXAMPLE_LVGL_TASK_PRIORITY, NULL); + assert(task_created == pdPASS); + ESP_LOGI(TAG, "Displaying LVGL UI at %dx%d", EXAMPLE_H_RES, EXAMPLE_V_RES); +} diff --git a/examples/peripherals/lcd/dsi_hdmi_bridge/main/lvgl_port.h b/examples/peripherals/lcd/dsi_hdmi_bridge/main/lvgl_port.h new file mode 100644 index 00000000000..bb38ea81c0a --- /dev/null +++ b/examples/peripherals/lcd/dsi_hdmi_bridge/main/lvgl_port.h @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#pragma once + +#include "esp_lcd_panel_ops.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Initialize LVGL on top of the HDMI DPI panel + * + * Creates the LVGL display, flush path, tick timer and LVGL task, then starts the demo UI. + * + * @param panel LCD panel handle returned from the HDMI bridge driver + */ +void example_lvgl_init(esp_lcd_panel_handle_t panel); + +#ifdef __cplusplus +} +#endif diff --git a/examples/peripherals/lcd/dsi_hdmi_bridge/sdkconfig.defaults b/examples/peripherals/lcd/dsi_hdmi_bridge/sdkconfig.defaults new file mode 100644 index 00000000000..bd9e9a93550 --- /dev/null +++ b/examples/peripherals/lcd/dsi_hdmi_bridge/sdkconfig.defaults @@ -0,0 +1,8 @@ +CONFIG_LV_CONF_SKIP=y +CONFIG_LV_COLOR_DEPTH_16=y +CONFIG_LV_FONT_MONTSERRAT_16=y +CONFIG_LV_FONT_MONTSERRAT_28=y +CONFIG_LV_FONT_MONTSERRAT_48=y +CONFIG_LV_USE_OBSERVER=y +CONFIG_LV_USE_SYSMON=y +CONFIG_LV_USE_PERF_MONITOR=y diff --git a/examples/peripherals/lcd/dsi_hdmi_bridge/sdkconfig.defaults.esp32p4 b/examples/peripherals/lcd/dsi_hdmi_bridge/sdkconfig.defaults.esp32p4 new file mode 100644 index 00000000000..8c4cbca6136 --- /dev/null +++ b/examples/peripherals/lcd/dsi_hdmi_bridge/sdkconfig.defaults.esp32p4 @@ -0,0 +1,3 @@ +CONFIG_SPIRAM=y +CONFIG_SPIRAM_MODE_HEX=y +CONFIG_SPIRAM_SPEED_250M=y