Merge branch 'feat/add_lt9611_conponent' into 'master'

feat(lcd): add MIPI-to-HDMI example (lt9611)

Closes IDF-15419

See merge request espressif/esp-idf!52175
This commit is contained in:
morris
2026-09-14 10:57:57 +08:00
20 changed files with 635 additions and 32 deletions

View File

@@ -9,6 +9,7 @@
#include <stdbool.h>
#include <stdint.h>
#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)
{

View File

@@ -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;
}
}
/**

View File

@@ -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

View File

@@ -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
/**

View File

@@ -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)

View File

@@ -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);

View File

@@ -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;

View File

@@ -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.

View File

@@ -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 接口的屏幕。

View File

@@ -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

View File

@@ -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)

View File

@@ -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).

View File

@@ -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)

View File

@@ -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));
}

View File

@@ -0,0 +1,5 @@
description: MIPI DSI to HDMI bridge example
dependencies:
lvgl/lvgl: "9.5.0"
esp_lcd_lt9611: "^0.1.0"

View File

@@ -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);
}

View File

@@ -0,0 +1,125 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: CC0-1.0
*/
#include <assert.h>
#include <unistd.h>
#include <sys/lock.h>
#include <sys/param.h>
#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);
}

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,3 @@
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_HEX=y
CONFIG_SPIRAM_SPEED_250M=y