Browse Source

lcd: add esp_lcd component

* Support intel 8080 LCD panel IO on ESP32-S3
* Support RGB LCD panel on ESP32-S3
* Support SPI && I2C LCD panel IO on all esp chips
morris 4 năm trước cách đây
mục cha
commit
e10202a608
33 tập tin đã thay đổi với 3856 bổ sung13 xóa
  1. 1 0
      .gitlab/CODEOWNERS
  2. 14 0
      components/esp_lcd/CMakeLists.txt
  3. 20 0
      components/esp_lcd/Kconfig
  4. 6 0
      components/esp_lcd/component.mk
  5. 201 0
      components/esp_lcd/include/esp_lcd_panel_io.h
  6. 126 0
      components/esp_lcd/include/esp_lcd_panel_ops.h
  7. 77 0
      components/esp_lcd/include/esp_lcd_panel_rgb.h
  8. 57 0
      components/esp_lcd/include/esp_lcd_panel_vendor.h
  9. 26 0
      components/esp_lcd/include/esp_lcd_types.h
  10. 126 0
      components/esp_lcd/interface/esp_lcd_panel_interface.h
  11. 66 0
      components/esp_lcd/interface/esp_lcd_panel_io_interface.h
  12. 126 0
      components/esp_lcd/src/esp_lcd_common.c
  13. 70 0
      components/esp_lcd/src/esp_lcd_common.h
  14. 54 0
      components/esp_lcd/src/esp_lcd_panel_commands.h
  15. 29 0
      components/esp_lcd/src/esp_lcd_panel_io.c
  16. 149 0
      components/esp_lcd/src/esp_lcd_panel_io_i2c.c
  17. 518 0
      components/esp_lcd/src/esp_lcd_panel_io_i80.c
  18. 230 0
      components/esp_lcd/src/esp_lcd_panel_io_spi.c
  19. 65 0
      components/esp_lcd/src/esp_lcd_panel_ops.c
  20. 235 0
      components/esp_lcd/src/esp_lcd_panel_ssd1306.c
  21. 263 0
      components/esp_lcd/src/esp_lcd_panel_st7789.c
  22. 443 0
      components/esp_lcd/src/esp_lcd_rgb_panel.c
  23. 3 0
      components/esp_lcd/test/CMakeLists.txt
  24. 7 0
      components/esp_lcd/test/component.mk
  25. 125 0
      components/esp_lcd/test/test_i2c_lcd_panel.c
  26. 355 0
      components/esp_lcd/test/test_i80_lcd_panel.c
  27. 113 0
      components/esp_lcd/test/test_lvgl_port.c
  28. 4 0
      components/esp_lcd/test/test_lvgl_port.h
  29. 164 0
      components/esp_lcd/test/test_rgb_panel.c
  30. 157 0
      components/esp_lcd/test/test_spi_lcd_panel.c
  31. 18 9
      components/hal/esp32s3/include/hal/lcd_ll.h
  32. 6 2
      components/soc/esp32s3/include/soc/soc_caps.h
  33. 2 2
      components/soc/include/soc/lcd_periph.h

+ 1 - 0
.gitlab/CODEOWNERS

@@ -84,6 +84,7 @@
 /components/esp_https_server/         @esp-idf-codeowners/app-utilities
 /components/esp_hw_support/           @esp-idf-codeowners/system
 /components/esp_ipc/                  @esp-idf-codeowners/system
+/components/esp_lcd/                  @esp-idf-codeowners/peripherals
 /components/esp_local_ctrl/           @esp-idf-codeowners/app-utilities
 /components/esp_netif/                @esp-idf-codeowners/network
 /components/esp_pm/                   @esp-idf-codeowners/power-management

+ 14 - 0
components/esp_lcd/CMakeLists.txt

@@ -0,0 +1,14 @@
+set(srcs "src/esp_lcd_common.c"
+         "src/esp_lcd_panel_io.c"
+         "src/esp_lcd_panel_io_i2c.c"
+         "src/esp_lcd_panel_io_spi.c"
+         "src/esp_lcd_panel_io_i80.c"
+         "src/esp_lcd_panel_ssd1306.c"
+         "src/esp_lcd_panel_st7789.c"
+         "src/esp_lcd_panel_ops.c"
+         "src/esp_lcd_rgb_panel.c")
+set(includes "include" "interface")
+
+idf_component_register(SRCS ${srcs}
+                       INCLUDE_DIRS ${includes}
+                       PRIV_INCLUDE_DIRS ${priv_includes})

+ 20 - 0
components/esp_lcd/Kconfig

@@ -0,0 +1,20 @@
+menu "LCD and Touch Panel"
+    menu "LCD Peripheral Configuration"
+        depends on IDF_TARGET_ESP32S3
+        choice LCD_PERIPH_CLK_SRC
+            prompt "Select clock source for LCD peripheral"
+            default LCD_PERIPH_CLK_SRC_XTAL if PM_ENABLE
+            default LCD_PERIPH_CLK_SRC_PLL160M
+            help
+                The peripheral clock is where LCD bus clock derives from.
+                Each clock source has its unique feature, e.g.
+                1. XTAL clock can help LCD work stable when DFS is enabled
+                2. PLL160M can achieve higher pixel clock resolution
+
+            config LCD_PERIPH_CLK_SRC_PLL160M
+                bool "PLL_160M clock"
+            config LCD_PERIPH_CLK_SRC_XTAL
+                bool "XTAL clock"
+        endchoice # LCD_PERIPH_CLK_SRC
+    endmenu
+endmenu

+ 6 - 0
components/esp_lcd/component.mk

@@ -0,0 +1,6 @@
+#
+# Component Makefile
+#
+COMPONENT_ADD_INCLUDEDIRS := include
+COMPONENT_PRIV_INCLUDEDIRS := interface
+COMPONENT_SRCDIRS := src

+ 201 - 0
components/esp_lcd/include/esp_lcd_panel_io.h

@@ -0,0 +1,201 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+#pragma once
+
+#include <stdbool.h>
+#include "esp_err.h"
+#include "esp_lcd_types.h"
+#include "soc/soc_caps.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef void *esp_lcd_spi_bus_handle_t;                       /*!< Type of LCD SPI bus handle */
+typedef void *esp_lcd_i2c_bus_handle_t;                       /*!< Type of LCD I2C bus handle */
+typedef struct esp_lcd_i80_bus_t *esp_lcd_i80_bus_handle_t;   /*!< Type of LCD intel 8080 bus handle */
+
+/**
+ * @brief Transmit LCD command and corresponding parameters
+ *
+ * @note Commands sent by this function are short, so they are sent using polling transactions.
+ *       The function does not return before the command tranfer is completed.
+ *       If any queued transactions sent by `esp_lcd_panel_io_tx_color()` are still pending when this function is called,
+ *       this function will wait until they are finished and the queue is empty before sending the command(s).
+ *
+ * @param[in] io LCD panel IO handle, which is created by other factory API like `esp_lcd_new_panel_io_spi()`
+ * @param[in] lcd_cmd The specific LCD command
+ * @param[in] lcd_cmd_bits Length of LCD command, in bits (e.g. 8 bits or 16 bits)
+ * @param[in] param Buffer that holds the command specific parameters, set to NULL if no parameter is needed for the command
+ * @param[in] param_size Size of `param` in memory, in bytes, set to zero if no parameter is needed for the command
+ * @return
+ *          - ESP_ERR_INVALID_ARG   if parameter is invalid
+ *          - ESP_OK                on success
+ */
+esp_err_t esp_lcd_panel_io_tx_param(esp_lcd_panel_io_handle_t io, int lcd_cmd, int lcd_cmd_bits, const void *param, size_t param_size);
+
+/**
+ * @brief Transmit LCD RGB data
+ *
+ * @note This function will package the command and RGB data into a transaction, and push into a queue.
+ *       The real transmission is performed in the background (DMA+interrupt).
+ *       The caller should take care of the lifecycle of the `color` buffer.
+ *       Recycling of color buffer should be done in the callback `on_color_trans_done()`.
+ *
+ * @param[in] io LCD panel IO handle, which is created by factory API like `esp_lcd_new_panel_io_spi()`
+ * @param[in] lcd_cmd The specific LCD command
+ * @param[in] lcd_cmd_bits Length of LCD command, in bits (e.g. 8 bits or 16 bits)
+ * @param[in] color Buffer that holds the RGB color data
+ * @param[in] color_size Size of `color` in memory, in bytes
+ * @return
+ *          - ESP_ERR_INVALID_ARG   if parameter is invalid
+ *          - ESP_OK                on success
+ */
+esp_err_t esp_lcd_panel_io_tx_color(esp_lcd_panel_io_handle_t io, int lcd_cmd, int lcd_cmd_bits, const void *color, size_t color_size);
+
+/**
+ * @brief Destory LCD panel IO handle (deinitialize panel and free all corresponding resource)
+ *
+ * @param[in] io LCD panel IO handle, which is created by factory API like `esp_lcd_new_panel_io_spi()`
+ * @return
+ *          - ESP_ERR_INVALID_ARG   if parameter is invalid
+ *          - ESP_OK                on success
+ */
+esp_err_t esp_lcd_panel_io_del(esp_lcd_panel_io_handle_t io);
+
+/**
+ * @brief Panel IO configuration structure, for SPI interface
+ */
+typedef struct {
+    int cs_gpio_num; /*!< GPIO used for CS line */
+    int dc_gpio_num; /*!< GPIO used to select the D/C line, set this to -1 if the D/C line not controlled by manually pulling high/low GPIO */
+    int spi_mode;    /*!< Traditional SPI mode (0~3) */
+    unsigned int pclk_hz;    /*!< Frequency of pixel clock */
+    size_t trans_queue_depth; /*!< Size of internal transaction queue */
+    bool (*on_color_trans_done)(esp_lcd_panel_io_handle_t panel_io, void *user_data, void *event_data); /*!< Callback, invoked when color data transfer has finished */
+    void *user_data; /*!< User private data, passed directly to on_trans_frame_done's user_data */
+    struct {
+        unsigned int dc_as_cmd_phase: 1; /*!< D/C line value is encoded into SPI transaction command phase */
+        unsigned int dc_low_on_data: 1;  /*!< If this flag is enabled, DC line = 0 means transfer data, DC line = 1 means transfer command; vice versa */
+    } flags;
+} esp_lcd_panel_io_spi_config_t;
+
+/**
+ * @brief Create LCD panel IO handle, for SPI interface
+ *
+ * @param[in] bus SPI bus handle
+ * @param[in] io_config IO configuration, for SPI interface
+ * @param[out] ret_io Returned IO handle
+ * @return
+ *          - ESP_ERR_INVALID_ARG   if parameter is invalid
+ *          - ESP_ERR_NO_MEM        if out of memory
+ *          - ESP_OK                on success
+ */
+esp_err_t esp_lcd_new_panel_io_spi(esp_lcd_spi_bus_handle_t bus, const esp_lcd_panel_io_spi_config_t *io_config, esp_lcd_panel_io_handle_t *ret_io);
+
+typedef struct {
+    uint32_t dev_addr; /*!< I2C device address */
+    bool (*on_color_trans_done)(esp_lcd_panel_io_handle_t panel_io, void *user_data, void *event_data); /*!< Callback, invoked when color data transfer has finished */
+    void *user_data; /*!< User private data, passed directly to on_trans_frame_done's user_data */
+    size_t control_phase_bytes; /*!< I2C LCD panel will encode control information (e.g. D/C seclection) into control phase, in several bytes */
+    unsigned int dc_bit_offset; /*!< Offset of the D/C selection bit in control phase */
+    struct {
+        unsigned int dc_low_on_data: 1;  /*!< If this flag is enabled, DC line = 0 means transfer data, DC line = 1 means transfer command; vice versa */
+    } flags;
+} esp_lcd_panel_io_i2c_config_t;
+
+/**
+ * @brief Create LCD panel IO handle, for I2C interface
+ *
+ * @param[in] bus I2C bus handle
+ * @param[in] io_config IO configuration, for I2C interface
+ * @param[out] ret_io Returned IO handle
+ * @return
+ *          - ESP_ERR_INVALID_ARG   if parameter is invalid
+ *          - ESP_ERR_NO_MEM        if out of memory
+ *          - ESP_OK                on success
+ */
+esp_err_t esp_lcd_new_panel_io_i2c(esp_lcd_i2c_bus_handle_t bus, const esp_lcd_panel_io_i2c_config_t *io_config, esp_lcd_panel_io_handle_t *ret_io);
+
+#if SOC_LCD_I80_SUPPORTED
+/**
+ * @brief LCD Intel 8080 bus configuration structure
+ */
+typedef struct {
+    int dc_gpio_num; /*!< GPIO used for D/C line */
+    int wr_gpio_num; /*!< GPIO used for WR line */
+    int data_gpio_nums[SOC_LCD_I80_BUS_WIDTH]; /*!< GPIOs used for data lines */
+    size_t data_width;         /*!< Number of data lines, 8 or 16 */
+    size_t max_transfer_bytes; /*!< Maximum transfer size, this determines the length of internal DMA link */
+} esp_lcd_i80_bus_config_t;
+
+/**
+ * @brief Create Intel 8080 bus handle
+ *
+ * @param[in] bus_config Bus configuration
+ * @param[out] ret_bus Returned bus handle
+ * @return
+ *          - ESP_ERR_INVALID_ARG   if parameter is invalid
+ *          - ESP_ERR_NO_MEM        if out of memory
+ *          - ESP_ERR_NOT_FOUND     if no free bus is available
+ *          - ESP_OK                on success
+ */
+esp_err_t esp_lcd_new_i80_bus(const esp_lcd_i80_bus_config_t *bus_config, esp_lcd_i80_bus_handle_t *ret_bus);
+
+/**
+ * @brief Destory Intel 8080 bus handle
+ *
+ * @param[in] bus Intel 8080 bus handle, created by `esp_lcd_new_i80_bus()`
+ * @return
+ *          - ESP_ERR_INVALID_ARG   if parameter is invalid
+ *          - ESP_ERR_INVALID_STATE if there still be some device attached to the bus
+ *          - ESP_OK                on success
+ */
+esp_err_t esp_lcd_del_i80_bus(esp_lcd_i80_bus_handle_t bus);
+
+/**
+ * @brief Panel IO configuration structure, for intel 8080 interface
+ */
+typedef struct {
+    int cs_gpio_num;         /*!< GPIO used for CS line */
+    unsigned int pclk_hz;    /*!< Frequency of pixel clock */
+    size_t trans_queue_depth; /*!< Transaction queue size, larger queue, higher throughput */
+    bool (*on_color_trans_done)(esp_lcd_panel_io_handle_t panel_io, void *user_data, void *event_data); /*!< Callback, invoked when color data was tranferred done */
+    void *user_data; /*!< User private data, passed directly to on_trans_done's user_data */
+    struct {
+        unsigned int dc_idle_level: 1;  /*!< Level of DC line in IDLE phase */
+        unsigned int dc_cmd_level: 1;   /*!< Level of DC line in CMD phase */
+        unsigned int dc_dummy_level: 1; /*!< Level of DC line in DUMMY phase */
+        unsigned int dc_data_level: 1;  /*!< Level of DC line in DATA phase */
+    } dc_levels; /*!< Each i80 device might have its own D/C control logic */
+    struct {
+        unsigned int invert_cs: 1;          /*!< Whether to invert the CS line */
+        unsigned int reverse_color_bits: 1; /*!< Reverse the data bits, D[N:0] -> D[0:N] */
+        unsigned int swap_color_bytes: 1;   /*!< Swap adjacent two color bytes */
+        unsigned int pclk_active_neg: 1;    /*!< The display will write data lines when there's a falling edge on WR signal (a.k.a the PCLK) */
+        unsigned int pclk_idle_low: 1;      /*!< The WR signal (a.k.a the PCLK) stays at low level in IDLE phase */
+    } flags;
+} esp_lcd_panel_io_i80_config_t;
+
+/**
+ * @brief Create LCD panel IO, for Intel 8080 interface
+ *
+ * @param[in] bus Intel 8080 bus handle, created by `esp_lcd_new_i80_bus()`
+ * @param[in] io_config IO configuration, for i80 interface
+ * @param[out] ret_io Returned panel IO handle
+ * @return
+ *          - ESP_ERR_INVALID_ARG   if parameter is invalid
+ *          - ESP_ERR_NOT_SUPPORTED if some configuration can't be satisfied, e.g. pixel clock out of the range
+ *          - ESP_ERR_NO_MEM        if out of memory
+ *          - ESP_OK                on success
+ */
+esp_err_t esp_lcd_new_panel_io_i80(esp_lcd_i80_bus_handle_t bus, const esp_lcd_panel_io_i80_config_t *io_config, esp_lcd_panel_io_handle_t *ret_io);
+
+#endif // SOC_LCD_I80_SUPPORTED
+
+#ifdef __cplusplus
+}
+#endif

+ 126 - 0
components/esp_lcd/include/esp_lcd_panel_ops.h

@@ -0,0 +1,126 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+#pragma once
+
+#include <stdbool.h>
+#include "esp_err.h"
+#include "esp_lcd_types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Reset LCD panel
+ *
+ * @note Panel reset must be called before attempting to initialize the panel using `esp_lcd_panel_init()`.
+ *
+ * @param[in] panel LCD panel handle, which is created by other factory API like `esp_lcd_new_panel_st7789()`
+ * @return
+ *          - ESP_OK on success
+ */
+esp_err_t esp_lcd_panel_reset(esp_lcd_panel_handle_t panel);
+
+/**
+ * @brief Initialize LCD panel
+ *
+ * @note Before calling this function, make sure the LCD panel has finished the `reset` stage by `esp_lcd_panel_reset()`.
+ *
+ * @param[in] panel LCD panel handle, which is created by other factory API like `esp_lcd_new_panel_st7789()`
+ * @return
+ *          - ESP_OK on success
+ */
+esp_err_t esp_lcd_panel_init(esp_lcd_panel_handle_t panel);
+
+/**
+ * @brief Deinitialize the LCD panel
+ *
+ * @param[in] panel LCD panel handle, which is created by other factory API like `esp_lcd_new_panel_st7789()`
+ * @return
+ *          - ESP_OK on success
+ */
+esp_err_t esp_lcd_panel_del(esp_lcd_panel_handle_t panel);
+
+/**
+ * @brief Draw bitmap on LCD panel
+ *
+ * @param[in] panel LCD panel handle, which is created by other factory API like `esp_lcd_new_panel_st7789()`
+ * @param[in] x_start Start index on x-axis (x_start included)
+ * @param[in] y_start Start index on y-axis (y_start included)
+ * @param[in] x_end End index on x-axis (x_end not included)
+ * @param[in] y_end End index on y-axis (y_end not included)
+ * @param[in] color_data RGB color data that will be dumped to the specific window range
+ * @return
+ *          - ESP_OK on success
+ */
+esp_err_t esp_lcd_panel_draw_bitmap(esp_lcd_panel_handle_t panel, int x_start, int y_start, int x_end, int y_end, const void *color_data);
+
+/**
+ * @brief Mirror the LCD panel on specific axis
+ *
+ * @note Combined with `esp_lcd_panel_swap_xy()`, one can realize screen rotation
+ *
+ * @param[in] panel LCD panel handle, which is created by other factory API like `esp_lcd_new_panel_st7789()`
+ * @param[in] mirror_x Whether the panel will be mirrored about the x axis
+ * @param[in] mirror_y Whether the panel will be mirrored about the y axis
+ * @return
+ *          - ESP_OK on success
+ *          - ESP_ERR_NOT_SUPPORTED if this function is not supported by the panel
+ */
+esp_err_t esp_lcd_panel_mirror(esp_lcd_panel_handle_t panel, bool mirror_x, bool mirror_y);
+
+/**
+ * @brief Swap/Exchange x and y axis
+ *
+ * @note Combined with `esp_lcd_panel_mirror()`, one can realize screen rotation
+ *
+ * @param[in] panel LCD panel handle, which is created by other factory API like `esp_lcd_new_panel_st7789()`
+ * @param[in] swap_axes Whether to swap the x and y axis
+ * @return
+ *          - ESP_OK on success
+ *          - ESP_ERR_NOT_SUPPORTED if this function is not supported by the panel
+ */
+esp_err_t esp_lcd_panel_swap_xy(esp_lcd_panel_handle_t panel, bool swap_axes);
+
+/**
+ * @brief Set extra gap in x and y axis
+ *
+ * The gap is the space (in pixels) between the left/top sides of the LCD panel and the first row/column respectively of the actual contents displayed.
+ *
+ * @note Setting a gap is useful when positioning or centering a frame that is smaller than the LCD.
+ *
+ * @param[in] panel LCD panel handle, which is created by other factory API like `esp_lcd_new_panel_st7789()`
+ * @param[in] x_gap Extra gap on x axis, in pixels
+ * @param[in] y_gap Extra gap on y axis, in pixels
+ * @return
+ *          - ESP_OK on success
+ */
+esp_err_t esp_lcd_panel_set_gap(esp_lcd_panel_handle_t panel, int x_gap, int y_gap);
+
+/**
+ * @brief Invert the color (bit-wise invert the color data line)
+ *
+ * @param[in] panel LCD panel handle, which is created by other factory API like `esp_lcd_new_panel_st7789()`
+ * @param[in] invert_color_data Whether to invert the color data
+ * @return
+ *          - ESP_OK on success
+ */
+esp_err_t esp_lcd_panel_invert_color(esp_lcd_panel_handle_t panel, bool invert_color_data);
+
+/**
+ * @brief Turn off the display
+ *
+ * @param[in] panel LCD panel handle, which is created by other factory API like `esp_lcd_new_panel_st7789()`
+ * @param[in] off Whether to turn off the screen
+ * @return
+ *          - ESP_OK on success
+ *          - ESP_ERR_NOT_SUPPORTED if this function is not supported by the panel
+ */
+esp_err_t esp_lcd_panel_disp_off(esp_lcd_panel_handle_t panel, bool off);
+
+#ifdef __cplusplus
+}
+#endif

+ 77 - 0
components/esp_lcd/include/esp_lcd_panel_rgb.h

@@ -0,0 +1,77 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+#pragma once
+
+#include <stdbool.h>
+#include "esp_err.h"
+#include "esp_lcd_types.h"
+#include "soc/soc_caps.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#if SOC_LCD_RGB_SUPPORTED
+/**
+ * @brief LCD RGB timing structure
+ */
+typedef struct {
+    unsigned int pclk_hz;           /*!< Frequency of pixel clock */
+    unsigned int h_res;             /*!< Horizontal resolution, i.e. the number of pixels in a line */
+    unsigned int v_res;             /*!< Vertical resolution, i.e. the number of lines in the frame  */
+    unsigned int hsync_pulse_width; /*!< Horizontal sync width, unit: PCLK period */
+    unsigned int hsync_back_porch;  /*!< Horizontal back porch, number of PCLK between hsync and start of line active data */
+    unsigned int hsync_front_porch; /*!< Horizontal front porch, number of PCLK between the end of active data and the next hsync */
+    unsigned int vsync_pulse_width; /*!< Vertical sync width, unit: number of lines */
+    unsigned int vsync_back_porch;  /*!< Vertical back porch, number of invalid lines between vsync and start of frame */
+    unsigned int vsync_front_porch; /*!< Vertical front porch, number of invalid lines between then end of frame and the next vsync */
+    struct {
+        unsigned int hsync_idle_low: 1;  /*!< The hsync signal is low in IDLE state */
+        unsigned int vsync_idle_low: 1;  /*!< The vsync signal is low in IDLE state */
+        unsigned int de_idle_high: 1;    /*!< The de signal is high in IDLE state */
+        unsigned int pclk_active_neg: 1; /*!< The display will write data lines when there's a falling edge on PCLK */
+        unsigned int pclk_idle_low: 1;   /*!< The PCLK stays at low level in IDLE phase */
+    } flags;
+} esp_lcd_rgb_timing_t;
+
+/**
+ * @brief LCD RGB panel configuration structure
+ */
+typedef struct {
+    esp_lcd_rgb_timing_t timings; /*!< RGB timing parameters */
+    size_t data_width;            /*!< Number of data lines */
+    int hsync_gpio_num;           /*!< GPIO used for HSYNC signal */
+    int vsync_gpio_num;           /*!< GPIO used for VSYNC signal */
+    int de_gpio_num;              /*!< GPIO used for DE signal, set to -1 if it's not used */
+    int pclk_gpio_num;            /*!< GPIO used for PCLK signal */
+    int data_gpio_nums[SOC_LCD_RGB_DATA_WIDTH]; /*!< GPIOs used for data lines */
+    int disp_gpio_num; /*!< GPIO used for display control signal, set to -1 if it's not used */
+    bool (*on_frame_trans_done)(esp_lcd_panel_handle_t panel, void *user_data); /*!< Callback, invoked when one frame buffer has transferred done */
+    void *user_data; /*!< User data which would be passed to on_frame_trans_done's user_data */
+    struct {
+        unsigned int disp_active_low: 1; /*!< If this flag is enabled, a low level of display control signal can turn the screen on; vice versa */
+        unsigned int relax_on_idle: 1;   /*!< If this flag is enabled, the host won't refresh the LCD if nothing changed in host's frame buffer (this is usefull for LCD with built-in GRAM) */
+    } flags;
+} esp_lcd_rgb_panel_config_t;
+
+/**
+ * @brief Create RGB LCD panel
+ *
+ * @param rgb_panel_config RGB panel configuration
+ * @param ret_panel Returned LCD panel handle
+ * @return
+ *          - ESP_ERR_INVALID_ARG   if parameter is invalid
+ *          - ESP_ERR_NO_MEM        if out of memory
+ *          - ESP_ERR_NOT_FOUND     if no free RGB panel is available
+ *          - ESP_OK                on success
+ */
+esp_err_t esp_lcd_new_rgb_panel(const esp_lcd_rgb_panel_config_t *rgb_panel_config, esp_lcd_panel_handle_t *ret_panel);
+
+#endif // SOC_LCD_RGB_SUPPORTED
+
+#ifdef __cplusplus
+}
+#endif

+ 57 - 0
components/esp_lcd/include/esp_lcd_panel_vendor.h

@@ -0,0 +1,57 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+#pragma once
+
+#include <stdbool.h>
+#include "esp_err.h"
+#include "esp_lcd_types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Configuration structure for panel device
+ */
+typedef struct {
+    int reset_gpio_num; /*!< GPIO used to reset the LCD panel, set to -1 if it's not used */
+    esp_lcd_color_space_t color_space; /*!< Set the color space used by the LCD panel */
+    unsigned int bits_per_pixel;       /*!< Color depth, in bpp */
+    struct {
+        unsigned int reset_active_high: 1; /*!< Setting this if the panel reset is high level active */
+    } flags;
+    void *vendor_config; /* vendor specific configuration, optional, left as NULL if not used */
+} esp_lcd_panel_dev_config_t;
+
+/**
+ * @brief Create LCD panel for model ST7789
+ *
+ * @param[in] io LCD panel IO handle
+ * @param[in] panel_dev_config general panel device configuration
+ * @param[out] ret_panel Returned LCD panel handle
+ * @return
+ *          - ESP_ERR_INVALID_ARG   if parameter is invalid
+ *          - ESP_ERR_NO_MEM        if out of memory
+ *          - ESP_OK                on success
+ */
+esp_err_t esp_lcd_new_panel_st7789(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel);
+
+/**
+ * @brief Create LCD panel for model SSD1306
+ *
+ * @param[in] io LCD panel IO handle
+ * @param[in] panel_dev_config general panel device configuration
+ * @param[out] ret_panel Returned LCD panel handle
+ * @return
+ *          - ESP_ERR_INVALID_ARG   if parameter is invalid
+ *          - ESP_ERR_NO_MEM        if out of memory
+ *          - ESP_OK                on success
+ */
+esp_err_t esp_lcd_new_panel_ssd1306(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel);
+
+#ifdef __cplusplus
+}
+#endif

+ 26 - 0
components/esp_lcd/include/esp_lcd_types.h

@@ -0,0 +1,26 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+#pragma once
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct esp_lcd_panel_io_t *esp_lcd_panel_io_handle_t; /*!< Type of LCD panel IO handle */
+typedef struct esp_lcd_panel_t *esp_lcd_panel_handle_t;       /*!< Type of LCD panel handle */
+
+/**
+ * @brief LCD color space type definition
+ */
+typedef enum {
+    ESP_LCD_COLOR_SPACE_RGB,        /*!< Color space: RGB */
+    ESP_LCD_COLOR_SPACE_BGR,        /*!< Color space: BGR */
+    ESP_LCD_COLOR_SPACE_MONOCHROME, /*!< Color space: monochrome */
+} esp_lcd_color_space_t;
+
+#ifdef __cplusplus
+}
+#endif

+ 126 - 0
components/esp_lcd/interface/esp_lcd_panel_interface.h

@@ -0,0 +1,126 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+#pragma once
+
+#include <stdbool.h>
+#include "esp_err.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct esp_lcd_panel_t esp_lcd_panel_t;  /*!< Type of LCD panel */
+
+/**
+ * @brief LCD panel interface
+ */
+struct esp_lcd_panel_t {
+    /**
+     * @brief Reset LCD panel
+     *
+     * @param[in] panel LCD panel handle, which is created by other factory API like `esp_lcd_new_panel_st7789()`
+     * @return
+     *          - ESP_OK on success
+     */
+    esp_err_t (*reset)(esp_lcd_panel_t *panel);
+
+    /**
+     * @brief Initialize LCD panel
+     *
+     * @param[in] panel LCD panel handle, which is created by other factory API like `esp_lcd_new_panel_st7789()`
+     * @return
+     *          - ESP_OK on success
+     */
+    esp_err_t (*init)(esp_lcd_panel_t *panel);
+
+    /**
+     * @brief Destory LCD panel
+     *
+     * @param[in] panel LCD panel handle, which is created by other factory API like `esp_lcd_new_panel_st7789()`
+     * @return
+     *          - ESP_OK on success
+     */
+    esp_err_t (*del)(esp_lcd_panel_t *panel);
+
+    /**
+     * @brief Draw bitmap on LCD panel
+     *
+     * @param[in] panel LCD panel handle, which is created by other factory API like `esp_lcd_new_panel_st7789()`
+     * @param[in] x_start Start index on x-axis (x_start included)
+     * @param[in] y_start Start index on y-axis (y_start included)
+     * @param[in] x_end End index on x-axis (x_end not included)
+     * @param[in] y_end End index on y-axis (y_end not included)
+     * @param[in] color_data RGB color data that will be dumped to the specific window range
+     * @return
+     *          - ESP_OK on success
+     */
+    esp_err_t (*draw_bitmap)(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data);
+
+    /**
+     * @brief Mirror the LCD panel on specific axis
+     *
+     * @note Combine this function with `swap_xy`, one can realize screen rotatation
+     *
+     * @param[in] panel LCD panel handle, which is created by other factory API like `esp_lcd_new_panel_st7789()`
+     * @param[in] x_axis Whether the panel will be mirrored about the x_axis
+     * @param[in] y_axis Whether the panel will be mirrored about the y_axis
+     * @return
+     *          - ESP_OK on success
+     *          - ESP_ERR_NOT_SUPPORTED if this function is not supported by the panel
+     */
+    esp_err_t (*mirror)(esp_lcd_panel_t *panel, bool x_axis, bool y_axis);
+
+    /**
+     * @brief Swap/Exchange x and y axis
+     *
+     * @note Combine this function with `mirror`, one can realize screen rotatation
+     *
+     * @param[in] panel LCD panel handle, which is created by other factory API like `esp_lcd_new_panel_st7789()`
+     * @param[in] swap_axes Whether to swap the x and y axis
+     * @return
+     *          - ESP_OK on success
+     *          - ESP_ERR_NOT_SUPPORTED if this function is not supported by the panel
+     */
+    esp_err_t (*swap_xy)(esp_lcd_panel_t *panel, bool swap_axes);
+
+    /**
+     * @brief Set extra gap in x and y axis
+     *
+     * @note The gap is only used for calculating the real coordinates.
+     *
+     * @param[in] panel LCD panel handle, which is created by other factory API like `esp_lcd_new_panel_st7789()`
+     * @param[in] x_gap Extra gap on x axis, in pixels
+     * @param[in] y_gap Extra gap on y axis, in pixels
+     * @return
+     *          - ESP_OK on success
+     */
+    esp_err_t (*set_gap)(esp_lcd_panel_t *panel, int x_gap, int y_gap);
+
+    /**
+     * @brief Invert the color (bit 1 -> 0 for color data line, and vice versa)
+     *
+     * @param[in] panel LCD panel handle, which is created by other factory API like `esp_lcd_new_panel_st7789()`
+     * @param[in] invert_color_data Whether to invert the color data
+     * @return
+     *          - ESP_OK on success
+     */
+    esp_err_t (*invert_color)(esp_lcd_panel_t *panel, bool invert_color_data);
+
+    /**
+     * @brief Turn off the display
+     *
+     * @param[in] panel LCD panel handle, which is created by other factory API like `esp_lcd_new_panel_st7789()`
+     * @param[in] off Whether to turn off the screen
+     * @return
+     *          - ESP_OK on success
+     *          - ESP_ERR_NOT_SUPPORTED if this function is not supported by the panel
+     */
+    esp_err_t (*disp_off)(esp_lcd_panel_t *panel, bool off);
+};
+
+#ifdef __cplusplus
+}
+#endif

+ 66 - 0
components/esp_lcd/interface/esp_lcd_panel_io_interface.h

@@ -0,0 +1,66 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+#pragma once
+
+#include <stdbool.h>
+#include "esp_err.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct esp_lcd_panel_io_t esp_lcd_panel_io_t; /*!< Type of LCD panel IO */
+
+/**
+ * @brief LCD panel IO interface
+ */
+struct esp_lcd_panel_io_t {
+    /**
+     * @brief Transmit LCD command and corresponding parameters
+     *
+     * @note This is the panel-specific interface called by function `esp_lcd_panel_io_tx_param()`.
+     *
+     * @param[in] io LCD panel IO handle, which is created by other factory API like `esp_lcd_new_panel_io_spi()`
+     * @param[in] lcd_cmd The specific LCD command
+     * @param[in] lcd_cmd_bits Length of LCD command, in bits (e.g. 8 bits or 16 bits)
+     * @param[in] param Buffer that holds the command specific parameters, set to NULL if no parameter is needed for the command
+     * @param[in] param_size Size of `param` in memory, in bytes, set to zero if no parameter is needed for the command
+     * @return
+     *          - ESP_ERR_INVALID_ARG   if parameter is invalid
+     *          - ESP_OK                on success
+     */
+    esp_err_t (*tx_param)(esp_lcd_panel_io_t *io, int lcd_cmd, int lcd_cmd_bits, const void *param, size_t param_size);
+
+    /**
+     * @brief Transmit LCD RGB data
+     *
+     * @note This is the panel-specific interface called by function `esp_lcd_panel_io_tx_color()`.
+     *
+     * @param[in] io LCD panel IO handle, which is created by other factory API like `esp_lcd_new_panel_io_spi()`
+     * @param[in] lcd_cmd The specific LCD command
+     * @param[in] lcd_cmd_bits Length of LCD command, in bits (e.g. 8 bits or 16 bits)
+     * @param[in] color Buffer that holds the RGB color data
+     * @param[in] color_size Size of `color` in memory, in bytes
+     * @return
+     *          - ESP_ERR_INVALID_ARG   if parameter is invalid
+     *          - ESP_OK                on success
+     */
+    esp_err_t (*tx_color)(esp_lcd_panel_io_t *io, int lcd_cmd, int lcd_cmd_bits, const void *color, size_t color_size);
+
+    /**
+     * @brief Destory LCD panel IO handle (deinitialize all and free resource)
+     *
+     * @param[in] io LCD panel IO handle, which is created by other factory API like `esp_lcd_new_panel_io_spi()`
+     * @return
+     *          - ESP_ERR_INVALID_ARG   if parameter is invalid
+     *          - ESP_OK                on success
+     */
+    esp_err_t (*del)(esp_lcd_panel_io_t *io);
+};
+
+#ifdef __cplusplus
+}
+#endif

+ 126 - 0
components/esp_lcd/src/esp_lcd_common.c

@@ -0,0 +1,126 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "freertos/FreeRTOS.h"
+#include "soc/rtc.h" // for querying XTAL clock
+#include "soc/soc_caps.h"
+#if SOC_LCDCAM_SUPPORTED
+#include "esp_lcd_common.h"
+#include "hal/lcd_ll.h"
+#include "hal/lcd_hal.h"
+
+typedef struct esp_lcd_platform_t {
+    portMUX_TYPE spinlock; // spinlock used to protect platform level resources
+    union {
+        void *panels[SOC_LCD_RGB_PANELS]; // array of RGB LCD panel instances
+        void *buses[SOC_LCD_I80_BUSES];   // array of i80 bus instances
+    }; // LCD peripheral can only work under either RGB mode or intel 8080 mode
+} esp_lcd_platform_t;
+
+esp_lcd_platform_t s_lcd_platform = {
+    .spinlock = (portMUX_TYPE)portMUX_INITIALIZER_UNLOCKED,
+    .buses = {} // initially the bus slots and panel slots are empty
+};
+
+int lcd_com_register_device(lcd_com_device_type_t device_type, void *device_obj)
+{
+    int member_id = -1;
+    switch (device_type) {
+    case LCD_COM_DEVICE_TYPE_I80:
+        // search for a bus slot then register to platform
+        for (int i = 0; (i < SOC_LCD_I80_BUSES) && (member_id == -1); i++) {
+            portENTER_CRITICAL(&s_lcd_platform.spinlock);
+            if (!s_lcd_platform.buses[i]) {
+                s_lcd_platform.buses[i] = device_obj;
+                member_id = i;
+            }
+            portEXIT_CRITICAL(&s_lcd_platform.spinlock);
+        }
+        break;
+    case LCD_COM_DEVICE_TYPE_RGB:
+        // search for a panel slot then register to platform
+        for (int i = 0; (i < SOC_LCD_RGB_PANELS) && (member_id == -1); i++) {
+            portENTER_CRITICAL(&s_lcd_platform.spinlock);
+            if (!s_lcd_platform.panels[i]) {
+                s_lcd_platform.panels[i] = device_obj;
+                member_id = i;
+            }
+            portEXIT_CRITICAL(&s_lcd_platform.spinlock);
+        }
+        break;
+    default:
+        break;
+    }
+    return member_id;
+}
+
+void lcd_com_remove_device(lcd_com_device_type_t device_type, int member_id)
+{
+    switch (device_type) {
+    case LCD_COM_DEVICE_TYPE_I80:
+        portENTER_CRITICAL(&s_lcd_platform.spinlock);
+        if (s_lcd_platform.buses[member_id]) {
+            s_lcd_platform.buses[member_id] = NULL;
+        }
+        portEXIT_CRITICAL(&s_lcd_platform.spinlock);
+        break;
+    case LCD_COM_DEVICE_TYPE_RGB:
+        portENTER_CRITICAL(&s_lcd_platform.spinlock);
+        if (s_lcd_platform.panels[member_id]) {
+            s_lcd_platform.panels[member_id] = NULL;
+        }
+        portEXIT_CRITICAL(&s_lcd_platform.spinlock);
+        break;
+    default:
+        break;
+    }
+}
+
+unsigned long lcd_com_select_periph_clock(lcd_hal_context_t *hal)
+{
+    unsigned long resolution_hz = 0;
+    int clock_source = -1;
+#if CONFIG_LCD_PERIPH_CLK_SRC_PLL160M
+    resolution_hz = 160000000 / LCD_PERIPH_CLOCK_PRE_SCALE;
+    clock_source = LCD_LL_CLOCK_SRC_PLL160M;
+#elif CONFIG_LCD_PERIPH_CLK_SRC_XTAL
+    resolution_hz = rtc_clk_xtal_freq_get() * 1000000 / LCD_PERIPH_CLOCK_PRE_SCALE;
+    clock_source = LCD_LL_CLOCK_SRC_XTAL;
+#else
+#error "invalid LCD peripheral clock source"
+#endif
+
+    lcd_ll_set_group_clock_src(hal->dev, clock_source, LCD_PERIPH_CLOCK_PRE_SCALE, 1, 0);
+    return resolution_hz;
+}
+
+void lcd_com_mount_dma_data(dma_descriptor_t *desc_head, const void *buffer, size_t len)
+{
+    size_t prepared_length = 0;
+    uint8_t *data = (uint8_t *)buffer;
+    dma_descriptor_t *desc = desc_head;
+    while (len > DMA_DESCRIPTOR_BUFFER_MAX_SIZE) {
+        desc->dw0.suc_eof = 0; // not the end of the transaction
+        desc->dw0.size = DMA_DESCRIPTOR_BUFFER_MAX_SIZE;
+        desc->dw0.length = DMA_DESCRIPTOR_BUFFER_MAX_SIZE;
+        desc->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA;
+        desc->buffer = &data[prepared_length];
+        desc = desc->next; // move to next descriptor
+        prepared_length += DMA_DESCRIPTOR_BUFFER_MAX_SIZE;
+        len -= DMA_DESCRIPTOR_BUFFER_MAX_SIZE;
+    }
+    if (len) {
+        desc->dw0.suc_eof = 1; // end of the transaction
+        desc->dw0.size = len;
+        desc->dw0.length = len;
+        desc->dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA;
+        desc->buffer = &data[prepared_length];
+        desc = desc->next; // move to next descriptor
+        prepared_length += len;
+    }
+}
+
+#endif // SOC_LCDCAM_SUPPORTED

+ 70 - 0
components/esp_lcd/src/esp_lcd_common.h

@@ -0,0 +1,70 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+#pragma once
+
+#include "soc/soc_caps.h"
+#if SOC_LCDCAM_SUPPORTED
+#include "hal/lcd_hal.h"
+#include "hal/dma_types.h"
+#else
+#error "lcd peripheral is not supported on this chip"
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#if SOC_LCDCAM_SUPPORTED
+
+#define LCD_PERIPH_CLOCK_PRE_SCALE (2) // This is the minimum divider that can be applied to LCD peripheral
+
+typedef enum {
+    LCD_COM_DEVICE_TYPE_I80,
+    LCD_COM_DEVICE_TYPE_RGB
+} lcd_com_device_type_t;
+
+/**
+ * @brief Register a LCD device to platform
+ *
+ * @param device_type Device type, refer to lcd_com_device_type_t
+ * @param device_obj Device object
+ * @return >=0: member_id, <0: no free lcd bus/panel slots
+ */
+int lcd_com_register_device(lcd_com_device_type_t device_type, void *device_obj);
+
+/**
+ * @brief Remove a device from platform
+ *
+ * @param device_type Device type, refer to lcd_com_device_type_t
+ * @param member_id member ID
+ */
+void lcd_com_remove_device(lcd_com_device_type_t device_type, int member_id);
+
+/**
+ * @brief Select clock source and return peripheral clock resolution (in Hz)
+ *
+ * @note The clock source selection is injected by the Kconfig system,
+ *       dynamic switching peripheral clock source is not supported in driver.
+ *
+ * @param hal HAL object
+ * @return Peripheral clock resolution, in Hz
+ */
+unsigned long lcd_com_select_periph_clock(lcd_hal_context_t *hal);
+
+/**
+ * @brief Mount data to DMA descriptors
+ *
+ * @param desc_head Point to the head of DMA descriptor chain
+ * @param buffer Data buffer
+ * @param len Size of the data buffer, in bytes
+ */
+void lcd_com_mount_dma_data(dma_descriptor_t *desc_head, const void *buffer, size_t len);
+
+#endif // SOC_LCDCAM_SUPPORTED
+
+#ifdef __cplusplus
+}
+#endif

+ 54 - 0
components/esp_lcd/src/esp_lcd_panel_commands.h

@@ -0,0 +1,54 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+#pragma once
+
+/* Common LCD panel commands */
+#define LCD_CMD_NOP          0x00 // This command is empty command
+#define LCD_CMD_SWRESET      0x01 // Software reset registers (the built-in frame buffer is not affected)
+#define LCD_CMD_RDDID        0x04 // Read 24-bit display ID
+#define LCD_CMD_RDDST        0x09 // Read display status
+#define LCD_CMD_RDDPM        0x0A // Read display power mode
+#define LCD_CMD_RDD_MADCTL   0x0B // Read display MADCTL
+#define LCD_CMD_RDD_COLMOD   0x0C // Read display pixel format
+#define LCD_CMD_RDDIM        0x0D // Read display image mode
+#define LCD_CMD_RDDSM        0x0E // Read display signal mode
+#define LCD_CMD_RDDSR        0x0F // Read display self-diagnostic result
+#define LCD_CMD_SLPIN        0x10 // Go into sleep mode (DC/DC, oscillator, scanning stopped, but memory keeps content)
+#define LCD_CMD_SLPOUT       0x11 // Exit sleep mode
+#define LCD_CMD_PTLON        0x12 // Turns on partial display mode
+#define LCD_CMD_NORON        0x13 // Turns on normal display mode
+#define LCD_CMD_INVOFF       0x20 // Recover from display inversion mode
+#define LCD_CMD_INVON        0x21 // Go into display inversion mode
+#define LCD_CMD_GAMSET       0x26 // Select Gamma curve for current display
+#define LCD_CMD_DISPOFF      0x28 // Display off (disable frame buffer output)
+#define LCD_CMD_DISPON       0x29 // Display on (enable frame buffer output)
+#define LCD_CMD_CASET        0x2A // Set column address
+#define LCD_CMD_RASET        0x2B // Set row address
+#define LCD_CMD_RAMWR        0x2C // Write frame memory
+#define LCD_CMD_RAMRD        0x2E // Read frame memory
+#define LCD_CMD_PTLAR        0x30 // Define the partial area
+#define LCD_CMD_VSCRDEF      0x33 // Vertical scrolling definition
+#define LCD_CMD_TEOFF        0x34 // Turns of tearing effect
+#define LCD_CMD_TEON         0x35 // Turns on tearing effect
+
+#define LCD_CMD_MADCTL       0x36     // Memory data access control
+#define LCD_CMD_MH_BIT       (1 << 2) // Display data latch order, 0: refresh left to right, 1: refresh right to left
+#define LCD_CMD_BGR_BIT      (1 << 3) // RGB/BGR order, 0: RGB, 1: BGR
+#define LCD_CMD_ML_BIT       (1 << 4) // Line address order, 0: refresh top to bottom, 1: refresh bottom to top
+#define LCD_CMD_MV_BIT       (1 << 5) // Row/Column order, 0: normal mode, 1: reverse mode
+#define LCD_CMD_MX_BIT       (1 << 6) // Column address order, 0: left to right, 1: right to left
+#define LCD_CMD_MY_BIT       (1 << 7) // Row address order, 0: top to bottom, 1: bottom to top
+
+#define LCD_CMD_VSCSAD       0x37 // Vertical scroll start address
+#define LCD_CMD_IDMOFF       0x38 // Recover from IDLE mode
+#define LCD_CMD_IDMON        0x39 // Fall into IDLE mode (8 color depth is displayed)
+#define LCD_CMD_COLMOD       0x3A // Defines the format of RGB picture data, which is to be transferred via the MCU interface
+#define LCD_CMD_RAMWRC       0x3C // Memory write continue
+#define LCD_CMD_RAMRDC       0x3E // Memory read continue
+#define LCD_CMD_STE          0x44 // Set tear scanline, tearing effect output signal when display module reaches line N
+#define LCD_CMD_GDCAN        0x45 // Get scanline
+#define LCD_CMD_WRDISBV      0x51 // Write display brightness
+#define LCD_CMD_RDDISBV      0x52 // Read display brightness value

+ 29 - 0
components/esp_lcd/src/esp_lcd_panel_io.c

@@ -0,0 +1,29 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "esp_check.h"
+#include "esp_lcd_panel_io.h"
+#include "esp_lcd_panel_io_interface.h"
+
+static const char *TAG = "lcd_panel.io";
+
+esp_err_t esp_lcd_panel_io_tx_param(esp_lcd_panel_io_handle_t io, int lcd_cmd, int lcd_cmd_bits, const void *param, size_t param_size)
+{
+    ESP_RETURN_ON_FALSE(io, ESP_ERR_INVALID_ARG, TAG, "invalid panel io handle");
+    return io->tx_param(io, lcd_cmd, lcd_cmd_bits, param, param_size);
+}
+
+esp_err_t esp_lcd_panel_io_tx_color(esp_lcd_panel_io_handle_t io, int lcd_cmd, int lcd_cmd_bits, const void *color, size_t color_size)
+{
+    ESP_RETURN_ON_FALSE(io, ESP_ERR_INVALID_ARG, TAG, "invalid panel io handle");
+    return io->tx_color(io, lcd_cmd, lcd_cmd_bits, color, color_size);
+}
+
+esp_err_t esp_lcd_panel_io_del(esp_lcd_panel_io_handle_t io)
+{
+    ESP_RETURN_ON_FALSE(io, ESP_ERR_INVALID_ARG, TAG, "invalid panel io handle");
+    return io->del(io);
+}

+ 149 - 0
components/esp_lcd/src/esp_lcd_panel_io_i2c.c

@@ -0,0 +1,149 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// #define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
+
+#include <stdlib.h>
+#include <string.h>
+#include <sys/cdefs.h>
+#include "esp_lcd_panel_io_interface.h"
+#include "esp_lcd_panel_io.h"
+#include "driver/i2c.h"
+#include "driver/gpio.h"
+#include "esp_log.h"
+#include "esp_check.h"
+
+static const char *TAG = "lcd_panel.io.i2c";
+
+static esp_err_t panel_io_i2c_del(esp_lcd_panel_io_t *io);
+static esp_err_t panel_io_i2c_tx_param(esp_lcd_panel_io_t *io, int lcd_cmd, int lcd_cmd_bits, const void *param, size_t param_size);
+static esp_err_t panel_io_i2c_tx_color(esp_lcd_panel_io_t *io, int lcd_cmd, int lcd_cmd_bits, const void *color, size_t color_size);
+
+typedef struct {
+    esp_lcd_panel_io_t base; // Base class of generic lcd panel io
+    uint32_t i2c_bus_id;     // I2C bus id, indicating which I2C port
+    uint32_t dev_addr;       // Device address
+    uint32_t control_phase_cmd;  // control byte when transferring command
+    uint32_t control_phase_data; // control byte when transferring data
+    bool (*on_color_trans_done)(esp_lcd_panel_io_handle_t panel_io, void *user_data, void *event_data); // User register's callback, invoked when color data trans done
+    void *user_data;             // User's private data, passed directly to callback on_color_trans_done()
+} lcd_panel_io_i2c_t;
+
+esp_err_t esp_lcd_new_panel_io_i2c(esp_lcd_i2c_bus_handle_t bus, const esp_lcd_panel_io_i2c_config_t *io_config, esp_lcd_panel_io_handle_t *ret_io)
+{
+    esp_err_t ret = ESP_OK;
+    lcd_panel_io_i2c_t *i2c_panel_io = NULL;
+    ESP_GOTO_ON_FALSE(io_config && ret_io, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument");
+    ESP_GOTO_ON_FALSE(io_config->control_phase_bytes * 8 > io_config->dc_bit_offset, ESP_ERR_INVALID_ARG, err, TAG, "D/C bit exceeds control bytes");
+    i2c_panel_io = calloc(1, sizeof(lcd_panel_io_i2c_t));
+    ESP_GOTO_ON_FALSE(i2c_panel_io, ESP_ERR_NO_MEM, err, TAG, "no mem for i2c panel io");
+
+    i2c_panel_io->i2c_bus_id = (uint32_t)bus;
+    i2c_panel_io->on_color_trans_done = io_config->on_color_trans_done;
+    i2c_panel_io->control_phase_data = (!io_config->flags.dc_low_on_data) << (io_config->dc_bit_offset);
+    i2c_panel_io->control_phase_cmd = (io_config->flags.dc_low_on_data) << (io_config->dc_bit_offset);
+    i2c_panel_io->user_data = io_config->user_data;
+    i2c_panel_io->dev_addr = io_config->dev_addr;
+    i2c_panel_io->base.del = panel_io_i2c_del;
+    i2c_panel_io->base.tx_param = panel_io_i2c_tx_param;
+    i2c_panel_io->base.tx_color = panel_io_i2c_tx_color;
+    *ret_io = &(i2c_panel_io->base);
+    ESP_LOGD(TAG, "new i2c lcd panel io @%p", i2c_panel_io);
+
+    return ESP_OK;
+err:
+    return ret;
+}
+
+static esp_err_t panel_io_i2c_del(esp_lcd_panel_io_t *io)
+{
+    esp_err_t ret = ESP_OK;
+    lcd_panel_io_i2c_t *i2c_panel_io = __containerof(io, lcd_panel_io_i2c_t, base);
+
+    ESP_LOGD(TAG, "del lcd panel io spi @%p", i2c_panel_io);
+    free(i2c_panel_io);
+    return ret;
+}
+
+static esp_err_t panel_io_i2c_tx_param(esp_lcd_panel_io_t *io, int lcd_cmd, int lcd_cmd_bits, const void *param, size_t param_size)
+{
+    esp_err_t ret = ESP_OK;
+    lcd_panel_io_i2c_t *i2c_panel_io = __containerof(io, lcd_panel_io_i2c_t, base);
+
+    i2c_cmd_handle_t cmd_link = i2c_cmd_link_create();
+    ESP_GOTO_ON_FALSE(cmd_link, ESP_ERR_NO_MEM, err, TAG, "no mem for i2c cmd link");
+    ESP_GOTO_ON_ERROR(i2c_master_start(cmd_link), err, TAG, "issue start failed"); // start phase
+    ESP_GOTO_ON_ERROR(i2c_master_write_byte(cmd_link, (i2c_panel_io->dev_addr << 1) | I2C_MASTER_WRITE, true), err, TAG, "write address failed"); // address phase
+    ESP_GOTO_ON_ERROR(i2c_master_write_byte(cmd_link, i2c_panel_io->control_phase_cmd, true), err, TAG, "write control command failed"); // control phase
+    switch (lcd_cmd_bits / 8) { // LCD command
+    case 4:
+        ESP_GOTO_ON_ERROR(i2c_master_write_byte(cmd_link, (lcd_cmd >> 24) & 0xFF, true), err, TAG, "write LCD cmd failed"); // fall-through
+    case 3:
+        ESP_GOTO_ON_ERROR(i2c_master_write_byte(cmd_link, (lcd_cmd >> 16) & 0xFF, true), err, TAG, "write LCD cmd failed"); // fall-through
+    case 2:
+        ESP_GOTO_ON_ERROR(i2c_master_write_byte(cmd_link, (lcd_cmd >> 8) & 0xFF, true), err, TAG, "write LCD cmd failed"); // fall-through
+    case 1:
+        ESP_GOTO_ON_ERROR(i2c_master_write_byte(cmd_link, (lcd_cmd >> 0) & 0xFF, true), err, TAG, "write LCD cmd failed"); // fall-through
+    default:
+        break;
+    }
+
+    if (param) {
+        uint8_t *data = (uint8_t *) param; // parameters for that command
+        ESP_GOTO_ON_ERROR(i2c_master_write(cmd_link, data, param_size, true), err, TAG, "write param failed");
+    }
+    ESP_GOTO_ON_ERROR(i2c_master_stop(cmd_link), err, TAG, "issue stop failed"); // stop phase
+
+    ESP_GOTO_ON_ERROR(i2c_master_cmd_begin(i2c_panel_io->i2c_bus_id, cmd_link, portMAX_DELAY), err, TAG, "i2c transaction failed");
+    i2c_cmd_link_delete(cmd_link);
+
+    return ESP_OK;
+err:
+    if (cmd_link) {
+        i2c_cmd_link_delete(cmd_link);
+    }
+    return ret;
+}
+
+static esp_err_t panel_io_i2c_tx_color(esp_lcd_panel_io_t *io, int lcd_cmd, int lcd_cmd_bits, const void *color, size_t color_size)
+{
+    esp_err_t ret = ESP_OK;
+    lcd_panel_io_i2c_t *i2c_panel_io = __containerof(io, lcd_panel_io_i2c_t, base);
+
+    i2c_cmd_handle_t cmd_link = i2c_cmd_link_create();
+    ESP_GOTO_ON_FALSE(cmd_link, ESP_ERR_NO_MEM, err, TAG, "no mem for i2c cmd link");
+    ESP_GOTO_ON_ERROR(i2c_master_start(cmd_link), err, TAG, "issue start failed"); // start phase
+    ESP_GOTO_ON_ERROR(i2c_master_write_byte(cmd_link, (i2c_panel_io->dev_addr << 1) | I2C_MASTER_WRITE, true), err, TAG, "write address failed"); // address phase
+    ESP_GOTO_ON_ERROR(i2c_master_write_byte(cmd_link, i2c_panel_io->control_phase_data, true), err, TAG, "write control data failed"); // control phase
+    switch (lcd_cmd_bits / 8) { // LCD command
+    case 4:
+        ESP_GOTO_ON_ERROR(i2c_master_write_byte(cmd_link, (lcd_cmd >> 24) & 0xFF, true), err, TAG, "write LCD cmd failed"); // fall-through
+    case 3:
+        ESP_GOTO_ON_ERROR(i2c_master_write_byte(cmd_link, (lcd_cmd >> 16) & 0xFF, true), err, TAG, "write LCD cmd failed"); // fall-through
+    case 2:
+        ESP_GOTO_ON_ERROR(i2c_master_write_byte(cmd_link, (lcd_cmd >> 8) & 0xFF, true), err, TAG, "write LCD cmd failed"); // fall-through
+    case 1:
+        ESP_GOTO_ON_ERROR(i2c_master_write_byte(cmd_link, (lcd_cmd >> 0) & 0xFF, true), err, TAG, "write LCD cmd failed"); // fall-through
+    default:
+        break;
+    }
+    ESP_GOTO_ON_ERROR(i2c_master_write(cmd_link, color, color_size, true), err, TAG, "write color failed"); // LCD gram data
+    ESP_GOTO_ON_ERROR(i2c_master_stop(cmd_link), err, TAG, "issue stop failed"); // stop phase
+
+    ESP_GOTO_ON_ERROR(i2c_master_cmd_begin(i2c_panel_io->i2c_bus_id, cmd_link, portMAX_DELAY), err, TAG, "i2c transaction failed");
+    i2c_cmd_link_delete(cmd_link);
+    // trans done callback
+    if (i2c_panel_io->on_color_trans_done) {
+        i2c_panel_io->on_color_trans_done(&(i2c_panel_io->base), i2c_panel_io->user_data, NULL);
+    }
+
+    return ESP_OK;
+err:
+    if (cmd_link) {
+        i2c_cmd_link_delete(cmd_link);
+    }
+    return ret;
+}

+ 518 - 0
components/esp_lcd/src/esp_lcd_panel_io_i80.c

@@ -0,0 +1,518 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// #define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
+
+#include <stdlib.h>
+#include <string.h>
+#include <sys/cdefs.h>
+#include <sys/queue.h>
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "freertos/queue.h"
+#include "esp_attr.h"
+#include "esp_check.h"
+#include "esp_intr_alloc.h"
+#include "esp_heap_caps.h"
+#include "esp_lcd_panel_io_interface.h"
+#include "esp_lcd_panel_io.h"
+#include "esp_rom_gpio.h"
+#include "soc/soc_caps.h"
+#include "hal/dma_types.h"
+#include "hal/gpio_hal.h"
+#include "esp_private/gdma.h"
+#include "driver/gpio.h"
+#include "driver/periph_ctrl.h"
+#if SOC_LCDCAM_SUPPORTED
+#include "esp_lcd_common.h"
+#include "soc/lcd_periph.h"
+#include "hal/lcd_ll.h"
+#include "hal/lcd_hal.h"
+
+static const char *TAG = "lcd_panel.io.i80";
+
+typedef struct esp_lcd_i80_bus_t esp_lcd_i80_bus_t;
+typedef struct lcd_panel_io_i80_t lcd_panel_io_i80_t;
+typedef struct lcd_i80_trans_descriptor_t lcd_i80_trans_descriptor_t;
+
+static esp_err_t panel_io_i80_tx_param(esp_lcd_panel_io_t *io, int lcd_cmd, int lcd_cmd_bits, const void *param, size_t param_size);
+static esp_err_t panel_io_i80_tx_color(esp_lcd_panel_io_t *io, int lcd_cmd, int lcd_cmd_bits, const void *color, size_t color_size);
+static esp_err_t panel_io_i80_del(esp_lcd_panel_io_t *io);
+static esp_err_t lcd_i80_bus_create_trans_link(esp_lcd_i80_bus_handle_t bus);
+static void lcd_periph_trigger_quick_trans_done_event(esp_lcd_i80_bus_handle_t bus);
+static esp_err_t lcd_i80_bus_configure_gpio(esp_lcd_i80_bus_handle_t bus, const esp_lcd_i80_bus_config_t *bus_config);
+static void lcd_i80_switch_devices(lcd_panel_io_i80_t *cur_device, lcd_panel_io_i80_t *next_device);
+static void lcd_start_transaction(esp_lcd_i80_bus_t *bus, lcd_i80_trans_descriptor_t *trans_desc);
+static IRAM_ATTR void lcd_default_isr_handler(void *args);
+
+struct esp_lcd_i80_bus_t {
+    int bus_id;            // Bus ID, index from 0
+    portMUX_TYPE spinlock; // spinlock used to protect i80 bus members(hal, device_list, cur_trans)
+    lcd_hal_context_t hal; // Hal object
+    size_t data_width;   // Number of data lines
+    intr_handle_t intr;    // LCD peripheral interrupt handle
+    size_t num_dma_nodes;  // Number of DMA descriptors
+    size_t resolution_hz;    // LCD_CLK resolution, determined by selected clock source
+    gdma_channel_handle_t dma_chan; // DMA channel handle
+    lcd_i80_trans_descriptor_t *cur_trans; // Current transaction
+    lcd_panel_io_i80_t *cur_device; // Current working device
+    LIST_HEAD(i80_device_list, lcd_panel_io_i80_t) device_list; // Head of i80 device list
+    dma_descriptor_t dma_nodes[0]; // DMA descriptor pool, the descriptors are shared by all i80 devices
+};
+
+struct lcd_i80_trans_descriptor_t {
+    lcd_panel_io_i80_t *i80_device; // i80 device issuing this transaction
+    int cmd_value;        // Command value
+    uint32_t cmd_cycles;  // Command cycles
+    const void *data;     // Data buffer
+    uint32_t data_length; // Data buffer size
+    void *cb_user_data;   // private data used by trans_done_cb
+    bool (*trans_done_cb)(esp_lcd_panel_io_handle_t panel_io, void *user_data, void *event_data); // transaction done callback
+};
+
+struct lcd_panel_io_i80_t {
+    esp_lcd_panel_io_t base;   // Base class of generic lcd panel io
+    esp_lcd_i80_bus_t *bus;    // Which bus the device is attached to
+    int cs_gpio_num;           // GPIO used for CS line
+    unsigned int pclk_hz;      // PCLK clock frequency
+    size_t clock_prescale;     // Prescaler coefficient, determined by user's configured PCLK frequency
+    QueueHandle_t trans_queue; // Transaction queue, transactions in this queue are pending for scheduler to dispatch
+    QueueHandle_t done_queue;  // Transaction done queue, transactions in this queue are finished but not recycled by the caller
+    size_t queue_size;         // Size of transaction queue
+    size_t num_trans_working;  // Number of transactions that are undergoing (the descriptor not recycled yet)
+    void *cb_user_data;        // private data used when transfer color data
+    bool (*on_color_trans_done)(esp_lcd_panel_io_handle_t panel_io, void *user_data, void *event_data); // color data trans done callback
+    LIST_ENTRY(lcd_panel_io_i80_t) device_list_entry; // Entry of i80 device list
+    struct {
+        int dc_idle_level: 1;  // Level of DC line in IDLE phase
+        int dc_cmd_level: 1;   // Level of DC line in CMD phase
+        int dc_dummy_level: 1; // Level of DC line in DUMMY phase
+        int dc_data_level: 1;  // Level of DC line in DATA phase
+    } dc_levels;
+    struct {
+        int invert_cs: 1;          // Whether to invert the CS line
+        int reverse_color_bits: 1; // Reverse the data bits, D[N:0] -> D[0:N]
+        int swap_color_bytes: 1;   // Swap adjacent two data bytes before sending out
+        int pclk_active_neg: 1;    // The display will write data lines when there's a falling edge on WR line
+        int pclk_idle_low: 1;      // The WR line keeps at low level in IDLE phase
+    } flags;
+    lcd_i80_trans_descriptor_t trans_pool[0]; // Transaction pool
+};
+
+esp_err_t esp_lcd_new_i80_bus(const esp_lcd_i80_bus_config_t *bus_config, esp_lcd_i80_bus_handle_t *ret_bus)
+{
+    esp_err_t ret = ESP_OK;
+    esp_lcd_i80_bus_t *bus = NULL;
+    ESP_GOTO_ON_FALSE(bus_config && ret_bus, ESP_ERR_INVALID_ARG, err_arg, TAG, "invalid argument");
+    size_t num_dma_nodes = bus_config->max_transfer_bytes / DMA_DESCRIPTOR_BUFFER_MAX_SIZE + 1;
+    // DMA descriptors must be placed in internal SRAM
+    bus = heap_caps_calloc(1, sizeof(esp_lcd_i80_bus_t) + num_dma_nodes * sizeof(dma_descriptor_t), MALLOC_CAP_DMA);
+    ESP_GOTO_ON_FALSE(bus, ESP_ERR_NO_MEM, no_mem_bus, TAG, "no mem for i80 bus");
+    bus->num_dma_nodes = num_dma_nodes;
+    // register to platform
+    int bus_id = lcd_com_register_device(LCD_COM_DEVICE_TYPE_I80, bus);
+    ESP_GOTO_ON_FALSE(bus_id >= 0, ESP_ERR_NOT_FOUND, no_slot, TAG, "no free i80 bus slot");
+    bus->bus_id = bus_id;
+    // enable APB to access LCD registers
+    periph_module_enable(lcd_periph_signals.buses[bus_id].module);
+    // initialize HAL layer, so we can call LL APIs later
+    lcd_hal_init(&bus->hal, bus_id);
+    // reset peripheral and FIFO
+    lcd_ll_reset(bus->hal.dev);
+    lcd_ll_fifo_reset(bus->hal.dev);
+    lcd_ll_enable_clock(bus->hal.dev, true);
+    // install interrupt service, (LCD peripheral shares the same interrupt source with Camera peripheral with different mask)
+    // interrupt is disabled by default
+    int isr_flags = ESP_INTR_FLAG_INTRDISABLED;
+    ret = esp_intr_alloc_intrstatus(lcd_periph_signals.buses[bus_id].irq_id, isr_flags,
+                                    lcd_ll_get_interrupt_status_reg(bus->hal.dev),
+                                    LCD_LL_EVENT_TRANS_DONE, lcd_default_isr_handler, bus, &bus->intr);
+    ESP_GOTO_ON_ERROR(ret, no_int, TAG, "install interrupt failed");
+    lcd_ll_enable_interrupt(bus->hal.dev, LCD_LL_EVENT_TRANS_DONE, false); // disable all interrupts
+    lcd_ll_clear_interrupt_status(bus->hal.dev, UINT32_MAX); // clear pending interrupt
+    // install DMA service
+    ret = lcd_i80_bus_create_trans_link(bus);
+    ESP_GOTO_ON_ERROR(ret, no_dma, TAG, "install DMA failed");
+    // set peripheral clock resolution
+    bus->resolution_hz = lcd_com_select_periph_clock(&bus->hal);
+    // enable 8080 mode and set data width
+    lcd_ll_enable_rgb_mode(bus->hal.dev, false);
+    lcd_ll_set_data_width(bus->hal.dev, bus_config->data_width);
+    // number of data cycles is controlled by DMA buffer size
+    lcd_ll_enable_output_always_on(bus->hal.dev, true);
+    // enable trans done interrupt
+    lcd_ll_enable_interrupt(bus->hal.dev, LCD_LL_EVENT_TRANS_DONE, true);
+    // trigger a quick "trans done" event, and wait for the interrupt line goes active
+    // this could ensure we go into ISR handler next time we call `esp_intr_enable`
+    lcd_periph_trigger_quick_trans_done_event(bus);
+    // configure GPIO
+    ret = lcd_i80_bus_configure_gpio(bus, bus_config);
+    ESP_GOTO_ON_ERROR(ret, no_gpio, TAG, "configure GPIO failed");
+    // fill other i80 bus runtime parameters
+    LIST_INIT(&bus->device_list); // initialize device list head
+    bus->spinlock = (portMUX_TYPE)portMUX_INITIALIZER_UNLOCKED;
+    bus->data_width = lcd_ll_get_data_width(bus->hal.dev);
+    *ret_bus = bus;
+    ESP_LOGD(TAG, "new i80 bus(%d) @%p, %zu dma nodes", bus_id, bus, bus->num_dma_nodes);
+    return ESP_OK;
+
+no_gpio:
+    gdma_disconnect(bus->dma_chan);
+    gdma_del_channel(bus->dma_chan);
+no_dma:
+    esp_intr_free(bus->intr);
+no_int:
+    periph_module_disable(lcd_periph_signals.buses[bus_id].module);
+    lcd_com_remove_device(LCD_COM_DEVICE_TYPE_I80, bus->bus_id);
+no_slot:
+    free(bus);
+no_mem_bus:
+err_arg:
+    return ret;
+}
+
+esp_err_t esp_lcd_del_i80_bus(esp_lcd_i80_bus_handle_t bus)
+{
+    esp_err_t ret = ESP_OK;
+    ESP_GOTO_ON_FALSE(bus, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument");
+    ESP_GOTO_ON_FALSE(LIST_EMPTY(&bus->device_list), ESP_ERR_INVALID_STATE, err, TAG, "device list not empty");
+    int bus_id = bus->bus_id;
+    gdma_disconnect(bus->dma_chan);
+    gdma_del_channel(bus->dma_chan);
+    esp_intr_free(bus->intr);
+    periph_module_disable(lcd_periph_signals.buses[bus_id].module);
+    lcd_com_remove_device(LCD_COM_DEVICE_TYPE_I80, bus_id);
+    free(bus);
+    ESP_LOGD(TAG, "del i80 bus(%d)", bus_id);
+err:
+    return ret;
+}
+
+esp_err_t esp_lcd_new_panel_io_i80(esp_lcd_i80_bus_handle_t bus, const esp_lcd_panel_io_i80_config_t *io_config, esp_lcd_panel_io_handle_t *ret_io)
+{
+    esp_err_t ret = ESP_OK;
+    lcd_panel_io_i80_t *i80_device = NULL;
+    ESP_GOTO_ON_FALSE(bus && io_config && ret_io, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument");
+    // check if pixel clock setting is valid
+    uint32_t pclk_prescale = bus->resolution_hz / io_config->pclk_hz;
+    ESP_GOTO_ON_FALSE(pclk_prescale <= LCD_LL_CLOCK_PRESCALE_MAX, ESP_ERR_NOT_SUPPORTED, err, TAG,
+                      "prescaler can't satisfy PCLK clock %u", io_config->pclk_hz);
+    i80_device = calloc(1, sizeof(lcd_panel_io_i80_t) + io_config->trans_queue_depth * sizeof(lcd_i80_trans_descriptor_t));
+    ESP_GOTO_ON_FALSE(i80_device, ESP_ERR_NO_MEM, err, TAG, "no mem for i80 panel io");
+    // create two queues for i80 device
+    i80_device->trans_queue = xQueueCreate(io_config->trans_queue_depth, sizeof(lcd_i80_trans_descriptor_t *));
+    ESP_GOTO_ON_FALSE(i80_device->trans_queue, ESP_ERR_NO_MEM, err, TAG, "create trans queue failed");
+    i80_device->done_queue = xQueueCreate(io_config->trans_queue_depth, sizeof(lcd_i80_trans_descriptor_t *));
+    ESP_GOTO_ON_FALSE(i80_device->done_queue, ESP_ERR_NO_MEM, err, TAG, "create done queue failed");
+    // adding device to list
+    portENTER_CRITICAL(&bus->spinlock);
+    LIST_INSERT_HEAD(&bus->device_list, i80_device, device_list_entry);
+    portEXIT_CRITICAL(&bus->spinlock);
+    // we don't initialize the i80 bus at the memont, but initialize the bus when start a transaction for a new device
+    // so save these as i80 device runtime parameters
+    i80_device->bus = bus;
+    i80_device->queue_size = io_config->trans_queue_depth;
+    i80_device->clock_prescale = pclk_prescale;
+    i80_device->pclk_hz = bus->resolution_hz / pclk_prescale;
+    i80_device->dc_levels.dc_cmd_level = io_config->dc_levels.dc_cmd_level;
+    i80_device->dc_levels.dc_data_level = io_config->dc_levels.dc_data_level;
+    i80_device->dc_levels.dc_dummy_level = io_config->dc_levels.dc_dummy_level;
+    i80_device->dc_levels.dc_idle_level = io_config->dc_levels.dc_idle_level;
+    i80_device->cs_gpio_num = io_config->cs_gpio_num;
+    i80_device->flags.reverse_color_bits = io_config->flags.reverse_color_bits;
+    i80_device->flags.swap_color_bytes = io_config->flags.swap_color_bytes;
+    i80_device->flags.invert_cs = io_config->flags.invert_cs;
+    i80_device->flags.pclk_idle_low = io_config->flags.pclk_idle_low;
+    i80_device->flags.pclk_active_neg = io_config->flags.pclk_active_neg;
+    i80_device->on_color_trans_done = io_config->on_color_trans_done;
+    i80_device->cb_user_data = io_config->user_data;
+    // fill panel io function table
+    i80_device->base.del = panel_io_i80_del;
+    i80_device->base.tx_param = panel_io_i80_tx_param;
+    i80_device->base.tx_color = panel_io_i80_tx_color;
+    // we only configure the CS GPIO as output, don't connect to the peripheral signal at the moment
+    // we will connect the CS GPIO to peripheral signal when switching devices in lcd_i80_switch_devices()
+    gpio_set_level(io_config->cs_gpio_num, !io_config->flags.invert_cs);
+    gpio_set_direction(io_config->cs_gpio_num, GPIO_MODE_OUTPUT);
+    gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[io_config->cs_gpio_num], PIN_FUNC_GPIO);
+    *ret_io = &(i80_device->base);
+    ESP_LOGD(TAG, "new i80 lcd panel io @%p on bus(%d)", i80_device, bus->bus_id);
+    return ESP_OK;
+
+err:
+    if (i80_device) {
+        if (i80_device->trans_queue) {
+            vQueueDelete(i80_device->trans_queue);
+        }
+        if (i80_device->done_queue) {
+            vQueueDelete(i80_device->done_queue);
+        }
+        free(i80_device);
+    }
+    return ret;
+}
+
+static esp_err_t panel_io_i80_del(esp_lcd_panel_io_t *io)
+{
+    lcd_panel_io_i80_t *i80_device = __containerof(io, lcd_panel_io_i80_t, base);
+    esp_lcd_i80_bus_t *bus = i80_device->bus;
+    lcd_i80_trans_descriptor_t *trans_desc = NULL;
+    // wait all pending transaction to finish
+    for (size_t i = 0; i < i80_device->num_trans_working; i++) {
+        xQueueReceive(i80_device->done_queue, &trans_desc, portMAX_DELAY);
+    }
+    // remove from device list
+    portENTER_CRITICAL(&bus->spinlock);
+    LIST_REMOVE(i80_device, device_list_entry);
+    portEXIT_CRITICAL(&bus->spinlock);
+
+    ESP_LOGD(TAG, "del i80 lcd panel io @%p", i80_device);
+    vQueueDelete(i80_device->trans_queue);
+    vQueueDelete(i80_device->done_queue);
+    free(i80_device);
+    return ESP_OK;
+}
+
+static esp_err_t panel_io_i80_tx_param(esp_lcd_panel_io_t *io, int lcd_cmd, int lcd_cmd_bits, const void *param, size_t param_size)
+{
+    lcd_panel_io_i80_t *next_device = __containerof(io, lcd_panel_io_i80_t, base);
+    esp_lcd_i80_bus_t *bus = next_device->bus;
+    lcd_panel_io_i80_t *cur_device = bus->cur_device;
+    lcd_i80_trans_descriptor_t *trans_desc = NULL;
+    assert(param_size <= (bus->num_dma_nodes * DMA_DESCRIPTOR_BUFFER_MAX_SIZE) && "parameter bytes too long, enlarge max_transfer_bytes");
+    uint32_t cmd_cycles = lcd_cmd_bits / bus->data_width;
+    // in case data_width=16 and cmd_bits=8, we still need 1 cmd_cycle
+    if (cmd_cycles * bus->data_width < lcd_cmd_bits) {
+        cmd_cycles++;
+    }
+    // wait all pending transaction in the queue to finish
+    for (size_t i = 0; i < next_device->num_trans_working; i++) {
+        xQueueReceive(next_device->done_queue, &trans_desc, portMAX_DELAY);
+    }
+    next_device->num_trans_working = 0;
+
+    uint32_t intr_status = lcd_ll_get_interrupt_status(bus->hal.dev);
+    lcd_ll_clear_interrupt_status(bus->hal.dev, intr_status);
+    // switch devices if necessary
+    lcd_i80_switch_devices(cur_device, next_device);
+    // don't reverse bit/bytes for parameters
+    lcd_ll_reverse_data_bit_order(bus->hal.dev, false);
+    lcd_ll_reverse_data_byte_order(bus->hal.dev, bus->data_width, false);
+    bus->cur_trans = NULL;
+    bus->cur_device = next_device;
+    // package a transaction
+    trans_desc = &next_device->trans_pool[0];
+    trans_desc->i80_device = next_device;
+    trans_desc->cmd_cycles = cmd_cycles;
+    trans_desc->cmd_value = lcd_cmd;
+    trans_desc->data = param;
+    trans_desc->data_length = param_size;
+    trans_desc->trans_done_cb = NULL; // no callback for parameter transaction
+    // mount data to DMA links
+    lcd_com_mount_dma_data(bus->dma_nodes, trans_desc->data, trans_desc->data_length);
+    lcd_start_transaction(bus, trans_desc);
+    // polling the trans done event, but don't clear the event status
+    while (!(lcd_ll_get_interrupt_status(bus->hal.dev) & LCD_LL_EVENT_TRANS_DONE));
+    return ESP_OK;
+}
+
+static esp_err_t panel_io_i80_tx_color(esp_lcd_panel_io_t *io, int lcd_cmd, int lcd_cmd_bits, const void *color, size_t color_size)
+{
+    lcd_panel_io_i80_t *i80_device = __containerof(io, lcd_panel_io_i80_t, base);
+    esp_lcd_i80_bus_t *bus = i80_device->bus;
+    lcd_i80_trans_descriptor_t *trans_desc = NULL;
+    assert(color_size <= (bus->num_dma_nodes * DMA_DESCRIPTOR_BUFFER_MAX_SIZE) && "color bytes too long, enlarge max_transfer_bytes");
+    // in case data_width=16 and cmd_bits=8, we still need 1 cmd_cycle
+    uint32_t cmd_cycles = lcd_cmd_bits / bus->data_width;
+    if (cmd_cycles * bus->data_width < lcd_cmd_bits) {
+        cmd_cycles++;
+    }
+    if (i80_device->num_trans_working < i80_device->queue_size) {
+        trans_desc = &i80_device->trans_pool[i80_device->num_trans_working];
+    } else {
+        // transaction pool has used up, recycle one from done_queue
+        xQueueReceive(i80_device->done_queue, &trans_desc, portMAX_DELAY);
+        i80_device->num_trans_working--;
+    }
+    trans_desc->i80_device = i80_device;
+    trans_desc->cmd_cycles = cmd_cycles;
+    trans_desc->cmd_value = lcd_cmd;
+    trans_desc->data = color;
+    trans_desc->data_length = color_size;
+    trans_desc->trans_done_cb = i80_device->on_color_trans_done;
+    trans_desc->cb_user_data = i80_device->cb_user_data;
+    // send transaction to trans_queue
+    xQueueSend(i80_device->trans_queue, &trans_desc, portMAX_DELAY);
+    i80_device->num_trans_working++;
+    // enable interrupt and go into isr handler, where we fetch the transactions from trans_queue and start it
+    // we will go into `lcd_default_isr_handler` almost at once, because the "trans done" event is active at the moment
+    esp_intr_enable(bus->intr);
+    return ESP_OK;
+}
+
+static esp_err_t lcd_i80_bus_create_trans_link(esp_lcd_i80_bus_handle_t bus)
+{
+    esp_err_t ret = ESP_OK;
+    // chain DMA descriptors
+    for (int i = 0; i < bus->num_dma_nodes; i++) {
+        bus->dma_nodes[i].dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_CPU;
+        bus->dma_nodes[i].next = &bus->dma_nodes[i + 1];
+    }
+    bus->dma_nodes[bus->num_dma_nodes - 1].next = NULL; // one-off DMA chain
+    // alloc DMA channel and connect to LCD peripheral
+    gdma_channel_alloc_config_t dma_chan_config = {
+        .direction = GDMA_CHANNEL_DIRECTION_TX,
+    };
+    ret = gdma_new_channel(&dma_chan_config, &bus->dma_chan);
+    ESP_GOTO_ON_ERROR(ret, err, TAG, "alloc DMA channel failed");
+    gdma_connect(bus->dma_chan, GDMA_MAKE_TRIGGER(GDMA_TRIG_PERIPH_LCD, 0));
+    gdma_strategy_config_t strategy_config = {
+        .auto_update_desc = true,
+        .owner_check = true
+    };
+    gdma_apply_strategy(bus->dma_chan, &strategy_config);
+    return ESP_OK;
+err:
+    if (bus->dma_chan) {
+        gdma_del_channel(bus->dma_chan);
+    }
+    return ret;
+}
+
+static esp_err_t lcd_i80_bus_configure_gpio(esp_lcd_i80_bus_handle_t bus, const esp_lcd_i80_bus_config_t *bus_config)
+{
+    int bus_id = bus->bus_id;
+    // check validation of GPIO number
+    bool valid_gpio = (bus_config->wr_gpio_num >= 0) && (bus_config->dc_gpio_num >= 0);
+    for (size_t i = 0; i < bus_config->data_width; i++) {
+        valid_gpio = valid_gpio && (bus_config->data_gpio_nums[i] >= 0);
+    }
+    if (!valid_gpio) {
+        return ESP_ERR_INVALID_ARG;
+    }
+    // connect peripheral signals via GPIO matrix
+    for (size_t i = 0; i < bus_config->data_width; i++) {
+        gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[bus_config->data_gpio_nums[i]], PIN_FUNC_GPIO);
+        gpio_set_direction(bus_config->data_gpio_nums[i], GPIO_MODE_OUTPUT);
+        esp_rom_gpio_connect_out_signal(bus_config->data_gpio_nums[i], lcd_periph_signals.buses[bus_id].data_sigs[i], false, false);
+    }
+    gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[bus_config->dc_gpio_num], PIN_FUNC_GPIO);
+    gpio_set_direction(bus_config->dc_gpio_num, GPIO_MODE_OUTPUT);
+    esp_rom_gpio_connect_out_signal(bus_config->dc_gpio_num, lcd_periph_signals.buses[bus_id].dc_sig, false, false);
+    gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[bus_config->wr_gpio_num], PIN_FUNC_GPIO);
+    gpio_set_direction(bus_config->wr_gpio_num, GPIO_MODE_OUTPUT);
+    esp_rom_gpio_connect_out_signal(bus_config->wr_gpio_num, lcd_periph_signals.buses[bus_id].wr_sig, false, false);
+    return ESP_OK;
+}
+
+static void lcd_periph_trigger_quick_trans_done_event(esp_lcd_i80_bus_handle_t bus)
+{
+    // trigger a quick interrupt event by a dummy transaction, wait the LCD interrupt line goes active
+    // next time when esp_intr_enable is invoked, we can go into interrupt handler immediately
+    // where we dispatch transactions for i80 devices
+    lcd_ll_set_phase_cycles(bus->hal.dev, 0, 1, 0);
+    lcd_ll_start(bus->hal.dev);
+    while (!(lcd_ll_get_interrupt_status(bus->hal.dev) & LCD_LL_EVENT_TRANS_DONE));
+}
+
+static void lcd_start_transaction(esp_lcd_i80_bus_t *bus, lcd_i80_trans_descriptor_t *trans_desc)
+{
+    // by default, the dummy phase is disabled because it's not common for most LCDs
+    // Number of data phase cycles are controlled by DMA buffer length, we only need to enable/disable the phase here
+    lcd_ll_set_phase_cycles(bus->hal.dev, trans_desc->cmd_cycles, 0, trans_desc->data ? 1 : 0);
+    lcd_ll_set_command(bus->hal.dev, bus->data_width, trans_desc->cmd_value);
+    if (trans_desc->data) { // some specific LCD commands can have no parameters
+        gdma_start(bus->dma_chan, (intptr_t)(bus->dma_nodes));
+    }
+    lcd_ll_start(bus->hal.dev);
+}
+
+static void lcd_i80_switch_devices(lcd_panel_io_i80_t *cur_device, lcd_panel_io_i80_t *next_device)
+{
+    // we assume the next_device and cur_device are attached to the same bus
+    esp_lcd_i80_bus_t *bus = next_device->bus;
+    if (next_device != cur_device) {
+        // reconfigure PCLK for the new device
+        lcd_ll_set_pixel_clock_prescale(bus->hal.dev, next_device->clock_prescale);
+        lcd_ll_set_clock_idle_level(bus->hal.dev, !next_device->flags.pclk_idle_low);
+        lcd_ll_set_pixel_clock_edge(bus->hal.dev, next_device->flags.pclk_active_neg);
+        // configure DC line level for the new device
+        lcd_ll_set_dc_level(bus->hal.dev, next_device->dc_levels.dc_idle_level, next_device->dc_levels.dc_cmd_level,
+                            next_device->dc_levels.dc_dummy_level, next_device->dc_levels.dc_data_level);
+        if (cur_device) {
+            // disconnect current CS GPIO from peripheral signal
+            esp_rom_gpio_connect_out_signal(cur_device->cs_gpio_num, SIG_GPIO_OUT_IDX, false, false);
+        }
+        // connect CS signal to the new device
+        esp_rom_gpio_connect_out_signal(next_device->cs_gpio_num, lcd_periph_signals.buses[bus->bus_id].cs_sig,
+                                        next_device->flags.invert_cs, false);
+    }
+}
+
+IRAM_ATTR static void lcd_default_isr_handler(void *args)
+{
+    esp_lcd_i80_bus_t *bus = (esp_lcd_i80_bus_t *)args;
+    lcd_i80_trans_descriptor_t *trans_desc = NULL;
+    lcd_panel_io_i80_t *cur_device = NULL;
+    lcd_panel_io_i80_t *next_device = NULL;
+    BaseType_t high_task_woken = pdFALSE;
+    bool need_yield = false;
+    uint32_t intr_status = lcd_ll_get_interrupt_status(bus->hal.dev);
+    if (intr_status & LCD_LL_EVENT_TRANS_DONE) {
+        // disable interrupt temporarily, only re-enable when there be remained transaction in the queue
+        esp_intr_disable(bus->intr);
+        trans_desc = bus->cur_trans; // the finished transaction
+        cur_device = bus->cur_device;// the working device
+        // process finished transaction
+        if (trans_desc) {
+            assert(trans_desc->i80_device == cur_device && "transaction device mismatch");
+            // device callback
+            if (trans_desc->trans_done_cb) {
+                if (trans_desc->trans_done_cb(&cur_device->base, trans_desc->cb_user_data, NULL)) {
+                    need_yield = true;
+                }
+            }
+            // move transaction to done_queue
+            // there won't be case that will overflow the queue, so skip checking the return value
+            high_task_woken = pdFALSE;
+            xQueueSendFromISR(cur_device->done_queue, &trans_desc, &high_task_woken);
+            if (high_task_woken == pdTRUE) {
+                need_yield = true;
+            }
+            bus->cur_trans = NULL;
+        }
+        // fetch transactions from devices' trans_queue
+        // Note: the first registered device will have the highest priority to be scheduled
+        LIST_FOREACH(next_device, &bus->device_list, device_list_entry) {
+            high_task_woken = pdFALSE;
+            if (xQueueReceiveFromISR(next_device->trans_queue, &trans_desc, &high_task_woken) == pdTRUE) {
+                if (high_task_woken == pdTRUE) {
+                    need_yield = true;
+                }
+                // only clear the interrupt status when we're sure there still remains transaction to handle
+                lcd_ll_clear_interrupt_status(bus->hal.dev, intr_status);
+                // switch devices if necessary
+                lcd_i80_switch_devices(cur_device, next_device);
+                // only reverse data bit/bytes for color data
+                lcd_ll_reverse_data_bit_order(bus->hal.dev, next_device->flags.reverse_color_bits);
+                lcd_ll_reverse_data_byte_order(bus->hal.dev, bus->data_width, next_device->flags.swap_color_bytes);
+                bus->cur_trans = trans_desc;
+                bus->cur_device = next_device;
+                // mount data to DMA links
+                lcd_com_mount_dma_data(bus->dma_nodes, trans_desc->data, trans_desc->data_length);
+                // enable interrupt again, because the new transaction can trigger new trans done event
+                esp_intr_enable(bus->intr);
+                lcd_start_transaction(bus, trans_desc);
+                break; // exit for-each loop
+            }
+        }
+    }
+    if (need_yield) {
+        portYIELD_FROM_ISR();
+    }
+}
+
+#endif // SOC_LCDCAM_SUPPORTED

+ 230 - 0
components/esp_lcd/src/esp_lcd_panel_io_spi.c

@@ -0,0 +1,230 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// #define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
+
+#include <stdlib.h>
+#include <string.h>
+#include <sys/cdefs.h>
+#include "esp_lcd_panel_io_interface.h"
+#include "esp_lcd_panel_io.h"
+#include "driver/spi_master.h"
+#include "driver/gpio.h"
+#include "esp_log.h"
+#include "esp_check.h"
+
+static const char *TAG = "lcd_panel.io.spi";
+
+static esp_err_t panel_io_spi_tx_param(esp_lcd_panel_io_t *io, int lcd_cmd, int lcd_cmd_bits, const void *param, size_t param_size);
+static esp_err_t panel_io_spi_tx_color(esp_lcd_panel_io_t *io, int lcd_cmd, int lcd_cmd_bits, const void *color, size_t color_size);
+static esp_err_t panel_io_spi_del(esp_lcd_panel_io_t *io);
+static void lcd_spi_pre_trans_cb(spi_transaction_t *trans);
+static void lcd_spi_post_trans_color_cb(spi_transaction_t *trans);
+
+typedef struct {
+    spi_transaction_t base;
+    struct {
+        unsigned int dc_gpio_level: 1;
+        unsigned int trans_is_color: 1;
+    } flags;
+} lcd_spi_trans_descriptor_t;
+
+typedef struct {
+    esp_lcd_panel_io_t base;     // Base class of generic lcd panel io
+    spi_device_handle_t spi_dev; // SPI device handle
+    int dc_gpio_num;             // D/C line GPIO number
+    bool (*on_color_trans_done)(esp_lcd_panel_io_handle_t panel_io, void *user_data, void *event_data); // User register's callback, invoked when color data trans done
+    void *user_data;           // User's private data, passed directly to callback on_color_trans_done
+    size_t queue_size;         // Size of transaction queue
+    size_t num_trans_working;  // Number of transactions that are undergoing (the descriptor not recycled yet)
+    struct {
+        int dc_as_cmd_phase: 1; // D/C line value is encoded into SPI transaction command phase
+        int dc_data_level: 1;   // Indicates the level of DC line when tranfering data
+    } flags;
+    lcd_spi_trans_descriptor_t trans_pool[0]; // Transaction pool
+} esp_lcd_panel_io_spi_t;
+
+esp_err_t esp_lcd_new_panel_io_spi(esp_lcd_spi_bus_handle_t bus, const esp_lcd_panel_io_spi_config_t *io_config, esp_lcd_panel_io_handle_t *ret_io)
+{
+    esp_err_t ret = ESP_OK;
+    esp_lcd_panel_io_spi_t *spi_panel_io = NULL;
+    ESP_GOTO_ON_FALSE(bus && io_config && ret_io, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument");
+    ESP_GOTO_ON_FALSE(!(io_config->flags.dc_as_cmd_phase && io_config->dc_gpio_num >= 0),
+                      ESP_ERR_INVALID_ARG, err, TAG, "invalid DC mode");
+    spi_panel_io = calloc(1, sizeof(esp_lcd_panel_io_spi_t) + sizeof(lcd_spi_trans_descriptor_t) * io_config->trans_queue_depth);
+    ESP_GOTO_ON_FALSE(spi_panel_io, ESP_ERR_NO_MEM, err, TAG, "no mem for spi panel io");
+
+    spi_device_interface_config_t devcfg = {
+        .clock_speed_hz = io_config->pclk_hz,
+        .mode = io_config->spi_mode,
+        .spics_io_num = io_config->cs_gpio_num,
+        .queue_size = io_config->trans_queue_depth,
+        .command_bits = io_config->flags.dc_as_cmd_phase ? 1 : 0, // whether to encode DC line into command transaction
+        .pre_cb = lcd_spi_pre_trans_cb, // pre-transaction callback, mainly control DC gpio level
+        .post_cb = io_config->on_color_trans_done ? lcd_spi_post_trans_color_cb : NULL, // post-transaction, where we invoke user registered "on_color_trans_done()"
+    };
+    ret = spi_bus_add_device((spi_host_device_t)bus, &devcfg, &spi_panel_io->spi_dev);
+    ESP_GOTO_ON_ERROR(ret, err, TAG, "adding spi device to bus failed");
+
+    // if the DC line is not encoded into any spi transaction phase or it's not controlled by SPI peripheral
+    if (io_config->dc_gpio_num >= 0) {
+        gpio_config_t io_conf = {
+            .mode = GPIO_MODE_OUTPUT,
+            .pin_bit_mask = 1ULL << io_config->dc_gpio_num,
+        };
+        ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "configure GPIO for D/C line failed");
+    }
+
+    spi_panel_io->flags.dc_as_cmd_phase = io_config->flags.dc_as_cmd_phase;
+    spi_panel_io->flags.dc_data_level = !io_config->flags.dc_low_on_data;
+    spi_panel_io->on_color_trans_done = io_config->on_color_trans_done;
+    spi_panel_io->user_data = io_config->user_data;
+    spi_panel_io->dc_gpio_num = io_config->dc_gpio_num;
+    spi_panel_io->queue_size = io_config->trans_queue_depth;
+    spi_panel_io->base.tx_param = panel_io_spi_tx_param;
+    spi_panel_io->base.tx_color = panel_io_spi_tx_color;
+    spi_panel_io->base.del = panel_io_spi_del;
+    *ret_io = &(spi_panel_io->base);
+    ESP_LOGD(TAG, "new spi lcd panel io @%p", spi_panel_io);
+
+    return ESP_OK;
+
+err:
+    if (spi_panel_io) {
+        if (io_config->dc_gpio_num >= 0) {
+            gpio_reset_pin(io_config->dc_gpio_num);
+        }
+        free(spi_panel_io);
+    }
+    return ret;
+}
+
+static esp_err_t panel_io_spi_del(esp_lcd_panel_io_t *io)
+{
+    esp_err_t ret = ESP_OK;
+    spi_transaction_t *spi_trans = NULL;
+    esp_lcd_panel_io_spi_t *spi_panel_io = __containerof(io, esp_lcd_panel_io_spi_t, base);
+
+    // wait all pending transaction to finish
+    for (size_t i = 0; i < spi_panel_io->num_trans_working; i++) {
+        ret = spi_device_get_trans_result(spi_panel_io->spi_dev, &spi_trans, portMAX_DELAY);
+        ESP_GOTO_ON_ERROR(ret, err, TAG, "recycle spi transactions failed");
+    }
+    spi_bus_remove_device(spi_panel_io->spi_dev);
+    if (spi_panel_io->dc_gpio_num >= 0) {
+        gpio_reset_pin(spi_panel_io->dc_gpio_num);
+    }
+    ESP_LOGD(TAG, "del lcd panel io spi @%p", spi_panel_io);
+    free(spi_panel_io);
+
+err:
+    return ret;
+}
+
+static esp_err_t panel_io_spi_tx_param(esp_lcd_panel_io_t *io, int lcd_cmd, int lcd_cmd_bits, const void *param, size_t param_size)
+{
+    esp_err_t ret = ESP_OK;
+    spi_transaction_t *spi_trans = NULL;
+    lcd_spi_trans_descriptor_t *lcd_trans = NULL;
+    esp_lcd_panel_io_spi_t *spi_panel_io = __containerof(io, esp_lcd_panel_io_spi_t, base);
+
+    // before issue a polling transaction, need to wait queued transactions finished
+    for (size_t i = 0; i < spi_panel_io->num_trans_working; i++) {
+        ret = spi_device_get_trans_result(spi_panel_io->spi_dev, &spi_trans, portMAX_DELAY);
+        ESP_GOTO_ON_ERROR(ret, err, TAG, "recycle spi transactions failed");
+    }
+    spi_panel_io->num_trans_working = 0;
+    lcd_trans = &spi_panel_io->trans_pool[0];
+    memset(lcd_trans, 0, sizeof(lcd_spi_trans_descriptor_t));
+    lcd_trans->base.user = spi_panel_io;
+    lcd_trans->flags.dc_gpio_level = !spi_panel_io->flags.dc_data_level; // set D/C line to command mode
+    lcd_trans->base.length = lcd_cmd_bits;
+    lcd_trans->base.tx_buffer = &lcd_cmd;
+    if (spi_panel_io->flags.dc_as_cmd_phase) { // encoding DC value to SPI command phase when necessary
+        lcd_trans->base.cmd = !spi_panel_io->flags.dc_data_level;
+    }
+    // command is short, using polling mode
+    ret = spi_device_polling_transmit(spi_panel_io->spi_dev, &lcd_trans->base);
+    ESP_GOTO_ON_ERROR(ret, err, TAG, "spi transmit (polling) command failed");
+
+    if (param && param_size) {
+        lcd_trans->flags.dc_gpio_level = spi_panel_io->flags.dc_data_level; // set D/C line to data mode
+        lcd_trans->base.length = param_size * 8; // transaction length is in bits
+        lcd_trans->base.tx_buffer = param;
+        if (spi_panel_io->flags.dc_as_cmd_phase) { // encoding DC value to SPI command phase when necessary
+            lcd_trans->base.cmd = spi_panel_io->flags.dc_data_level;
+        }
+        // parameter is usually short, using polling mode
+        ret = spi_device_polling_transmit(spi_panel_io->spi_dev, &lcd_trans->base);
+        ESP_GOTO_ON_ERROR(ret, err, TAG, "spi transmit (polling) param failed");
+    }
+
+err:
+    return ret;
+}
+
+static esp_err_t panel_io_spi_tx_color(esp_lcd_panel_io_t *io, int lcd_cmd, int lcd_cmd_bits, const void *color, size_t color_size)
+{
+    esp_err_t ret = ESP_OK;
+    spi_transaction_t *spi_trans = NULL;
+    lcd_spi_trans_descriptor_t *lcd_trans = NULL;
+    esp_lcd_panel_io_spi_t *spi_panel_io = __containerof(io, esp_lcd_panel_io_spi_t, base);
+
+    // before issue a polling transaction, need to wait queued transactions finished
+    for (size_t i = 0; i < spi_panel_io->num_trans_working; i++) {
+        ret = spi_device_get_trans_result(spi_panel_io->spi_dev, &spi_trans, portMAX_DELAY);
+        ESP_GOTO_ON_ERROR(ret, err, TAG, "recycle spi transactions failed");
+    }
+    spi_panel_io->num_trans_working = 0;
+    lcd_trans = &spi_panel_io->trans_pool[0];
+    memset(lcd_trans, 0, sizeof(lcd_spi_trans_descriptor_t));
+    lcd_trans->base.user = spi_panel_io;
+    lcd_trans->flags.dc_gpio_level = !spi_panel_io->flags.dc_data_level; // set D/C line to command mode
+    lcd_trans->base.length = lcd_cmd_bits;
+    lcd_trans->base.tx_buffer = &lcd_cmd;
+    if (spi_panel_io->flags.dc_as_cmd_phase) { // encoding DC value to SPI command phase when necessary
+        lcd_trans->base.cmd = !spi_panel_io->flags.dc_data_level;
+    }
+    // command is short, using polling mode
+    ret = spi_device_polling_transmit(spi_panel_io->spi_dev, &lcd_trans->base);
+    ESP_GOTO_ON_ERROR(ret, err, TAG, "spi transmit (polling) command failed");
+
+    // sending LCD color data
+    lcd_trans->flags.trans_is_color = 1;
+    lcd_trans->flags.dc_gpio_level = spi_panel_io->flags.dc_data_level; // set D/C line to data mode
+    lcd_trans->base.length = color_size * 8; // transaction length is in bits
+    lcd_trans->base.tx_buffer = color;
+    if (spi_panel_io->flags.dc_as_cmd_phase) { // encoding DC value to SPI command phase when necessary
+        lcd_trans->base.cmd = spi_panel_io->flags.dc_data_level;
+    }
+    // color data is usually large, using queue+blocking mode
+    ret = spi_device_queue_trans(spi_panel_io->spi_dev, &lcd_trans->base, portMAX_DELAY);
+    ESP_GOTO_ON_ERROR(ret, err, TAG, "spi transmit (queue) color failed");
+    spi_panel_io->num_trans_working++;
+
+err:
+    return ret;
+}
+
+static void lcd_spi_pre_trans_cb(spi_transaction_t *trans)
+{
+    esp_lcd_panel_io_spi_t *spi_panel_io = trans->user;
+    lcd_spi_trans_descriptor_t *lcd_trans = __containerof(trans, lcd_spi_trans_descriptor_t, base);
+    if (spi_panel_io->dc_gpio_num >= 0) { // set D/C line level if necessary
+        gpio_set_level(spi_panel_io->dc_gpio_num, lcd_trans->flags.dc_gpio_level);
+    }
+}
+
+static void lcd_spi_post_trans_color_cb(spi_transaction_t *trans)
+{
+    esp_lcd_panel_io_spi_t *spi_panel_io = trans->user;
+    lcd_spi_trans_descriptor_t *lcd_trans = __containerof(trans, lcd_spi_trans_descriptor_t, base);
+    if (lcd_trans->flags.trans_is_color) {
+        if (spi_panel_io->on_color_trans_done) {
+            spi_panel_io->on_color_trans_done(&spi_panel_io->base, spi_panel_io->user_data, NULL);
+        }
+    }
+}

+ 65 - 0
components/esp_lcd/src/esp_lcd_panel_ops.c

@@ -0,0 +1,65 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include "esp_check.h"
+#include "esp_lcd_panel_ops.h"
+#include "esp_lcd_panel_interface.h"
+
+static const char *TAG = "lcd_panel";
+
+esp_err_t esp_lcd_panel_reset(esp_lcd_panel_handle_t panel)
+{
+    ESP_RETURN_ON_FALSE(panel, ESP_ERR_INVALID_ARG, TAG, "invalid panel handle");
+    return panel->reset(panel);
+}
+
+esp_err_t esp_lcd_panel_init(esp_lcd_panel_handle_t panel)
+{
+    ESP_RETURN_ON_FALSE(panel, ESP_ERR_INVALID_ARG, TAG, "invalid panel handle");
+    return panel->init(panel);
+}
+
+esp_err_t esp_lcd_panel_del(esp_lcd_panel_handle_t panel)
+{
+    ESP_RETURN_ON_FALSE(panel, ESP_ERR_INVALID_ARG, TAG, "invalid panel handle");
+    return panel->del(panel);
+}
+
+esp_err_t esp_lcd_panel_draw_bitmap(esp_lcd_panel_handle_t panel, int x_start, int y_start, int x_end, int y_end, const void *color_data)
+{
+    ESP_RETURN_ON_FALSE(panel, ESP_ERR_INVALID_ARG, TAG, "invalid panel handle");
+    return panel->draw_bitmap(panel, x_start, y_start, x_end, y_end, color_data);
+}
+
+esp_err_t esp_lcd_panel_mirror(esp_lcd_panel_handle_t panel, bool mirror_x, bool mirror_y)
+{
+    ESP_RETURN_ON_FALSE(panel, ESP_ERR_INVALID_ARG, TAG, "invalid panel handle");
+    return panel->mirror(panel, mirror_x, mirror_y);
+}
+
+esp_err_t esp_lcd_panel_swap_xy(esp_lcd_panel_handle_t panel, bool swap_axes)
+{
+    ESP_RETURN_ON_FALSE(panel, ESP_ERR_INVALID_ARG, TAG, "invalid panel handle");
+    return panel->swap_xy(panel, swap_axes);
+}
+
+esp_err_t esp_lcd_panel_set_gap(esp_lcd_panel_handle_t panel, int x_gap, int y_gap)
+{
+    ESP_RETURN_ON_FALSE(panel, ESP_ERR_INVALID_ARG, TAG, "invalid panel handle");
+    return panel->set_gap(panel, x_gap, y_gap);
+}
+
+esp_err_t esp_lcd_panel_invert_color(esp_lcd_panel_handle_t panel, bool invert_color_data)
+{
+    ESP_RETURN_ON_FALSE(panel, ESP_ERR_INVALID_ARG, TAG, "invalid panel handle");
+    return panel->invert_color(panel, invert_color_data);
+}
+
+esp_err_t esp_lcd_panel_disp_off(esp_lcd_panel_handle_t panel, bool off)
+{
+    ESP_RETURN_ON_FALSE(panel, ESP_ERR_INVALID_ARG, TAG, "invalid panel handle");
+    return panel->disp_off(panel, off);
+}

+ 235 - 0
components/esp_lcd/src/esp_lcd_panel_ssd1306.c

@@ -0,0 +1,235 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// #define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
+
+#include <stdlib.h>
+#include <sys/cdefs.h>
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "esp_lcd_panel_interface.h"
+#include "esp_lcd_panel_io.h"
+#include "esp_lcd_panel_vendor.h"
+#include "esp_lcd_panel_ops.h"
+#include "driver/gpio.h"
+#include "esp_log.h"
+#include "esp_check.h"
+
+static const char *TAG = "lcd_panel.ssd1306";
+
+// SSD1306 commands
+#define SSD1306_CMD_SET_MEMORY_ADDR_MODE  0x20
+#define SSD1306_CMD_SET_COLUMN_RANGE      0x21
+#define SSD1306_CMD_SET_PAGE_RANGE        0x22
+#define SSD1306_CMD_SET_CHARGE_PUMP       0x8D
+#define SSD1306_CMD_MIRROR_X_OFF          0xA0
+#define SSD1306_CMD_MIRROR_X_ON           0xA1
+#define SSD1306_CMD_INVERT_OFF            0xA6
+#define SSD1306_CMD_INVERT_ON             0xA7
+#define SSD1306_CMD_DISP_OFF              0xAE
+#define SSD1306_CMD_DISP_ON               0xAF
+#define SSD1306_CMD_MIRROR_Y_OFF          0xC0
+#define SSD1306_CMD_MIRROR_Y_ON           0xC8
+
+static esp_err_t panel_ssd1306_del(esp_lcd_panel_t *panel);
+static esp_err_t panel_ssd1306_reset(esp_lcd_panel_t *panel);
+static esp_err_t panel_ssd1306_init(esp_lcd_panel_t *panel);
+static esp_err_t panel_ssd1306_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data);
+static esp_err_t panel_ssd1306_invert_color(esp_lcd_panel_t *panel, bool invert_color_data);
+static esp_err_t panel_ssd1306_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y);
+static esp_err_t panel_ssd1306_swap_xy(esp_lcd_panel_t *panel, bool swap_axes);
+static esp_err_t panel_ssd1306_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap);
+static esp_err_t panel_ssd1306_disp_off(esp_lcd_panel_t *panel, bool off);
+
+typedef struct {
+    esp_lcd_panel_t base;
+    esp_lcd_panel_io_handle_t io;
+    int reset_gpio_num;
+    bool reset_level;
+    int x_gap;
+    int y_gap;
+    unsigned int bits_per_pixel;
+} ssd1306_panel_t;
+
+esp_err_t esp_lcd_new_panel_ssd1306(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel)
+{
+    esp_err_t ret = ESP_OK;
+    ssd1306_panel_t *ssd1306 = NULL;
+    ESP_GOTO_ON_FALSE(io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument");
+    ESP_GOTO_ON_FALSE(panel_dev_config->color_space == ESP_LCD_COLOR_SPACE_MONOCHROME, ESP_ERR_INVALID_ARG, err, TAG, "support monochrome only");
+    ESP_GOTO_ON_FALSE(panel_dev_config->bits_per_pixel == 1, ESP_ERR_INVALID_ARG, err, TAG, "bpp must be 1");
+    ssd1306 = calloc(1, sizeof(ssd1306_panel_t));
+    ESP_GOTO_ON_FALSE(ssd1306, ESP_ERR_NO_MEM, err, TAG, "no mem for ssd1306 panel");
+
+    if (panel_dev_config->reset_gpio_num >= 0) {
+        gpio_config_t io_conf = {
+            .mode = GPIO_MODE_OUTPUT,
+            .pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num,
+        };
+        ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "configure GPIO for RST line failed");
+    }
+
+    ssd1306->io = io;
+    ssd1306->bits_per_pixel = panel_dev_config->bits_per_pixel;
+    ssd1306->reset_gpio_num = panel_dev_config->reset_gpio_num;
+    ssd1306->reset_level = panel_dev_config->flags.reset_active_high;
+    ssd1306->base.del = panel_ssd1306_del;
+    ssd1306->base.reset = panel_ssd1306_reset;
+    ssd1306->base.init = panel_ssd1306_init;
+    ssd1306->base.draw_bitmap = panel_ssd1306_draw_bitmap;
+    ssd1306->base.invert_color = panel_ssd1306_invert_color;
+    ssd1306->base.set_gap = panel_ssd1306_set_gap;
+    ssd1306->base.mirror = panel_ssd1306_mirror;
+    ssd1306->base.swap_xy = panel_ssd1306_swap_xy;
+    ssd1306->base.disp_off = panel_ssd1306_disp_off;
+    *ret_panel = &(ssd1306->base);
+    ESP_LOGD(TAG, "new ssd1306 panel @%p", ssd1306);
+
+    return ESP_OK;
+
+err:
+    if (ssd1306) {
+        if (panel_dev_config->reset_gpio_num >= 0) {
+            gpio_reset_pin(panel_dev_config->reset_gpio_num);
+        }
+        free(ssd1306);
+    }
+    return ret;
+}
+
+static esp_err_t panel_ssd1306_del(esp_lcd_panel_t *panel)
+{
+    ssd1306_panel_t *ssd1306 = __containerof(panel, ssd1306_panel_t, base);
+    if (ssd1306->reset_gpio_num >= 0) {
+        gpio_reset_pin(ssd1306->reset_gpio_num);
+    }
+    ESP_LOGD(TAG, "del ssd1306 panel @%p", ssd1306);
+    free(ssd1306);
+    return ESP_OK;
+}
+
+static esp_err_t panel_ssd1306_reset(esp_lcd_panel_t *panel)
+{
+    ssd1306_panel_t *ssd1306 = __containerof(panel, ssd1306_panel_t, base);
+
+    // perform hardware reset
+    if (ssd1306->reset_gpio_num >= 0) {
+        gpio_set_level(ssd1306->reset_gpio_num, ssd1306->reset_level);
+        vTaskDelay(pdMS_TO_TICKS(10));
+        gpio_set_level(ssd1306->reset_gpio_num, !ssd1306->reset_level);
+        vTaskDelay(pdMS_TO_TICKS(10));
+    }
+
+    return ESP_OK;
+}
+
+static esp_err_t panel_ssd1306_init(esp_lcd_panel_t *panel)
+{
+    ssd1306_panel_t *ssd1306 = __containerof(panel, ssd1306_panel_t, base);
+    esp_lcd_panel_io_handle_t io = ssd1306->io;
+    esp_lcd_panel_io_tx_param(io, SSD1306_CMD_DISP_OFF, 8, NULL, 0);
+    esp_lcd_panel_io_tx_param(io, SSD1306_CMD_SET_MEMORY_ADDR_MODE, 8, (uint8_t[]) {
+        0x00 // horizontal addressing mode
+    }, 1);
+    esp_lcd_panel_io_tx_param(io, SSD1306_CMD_SET_CHARGE_PUMP, 8, (uint8_t[]) {
+        0x14 // enable charge pump
+    }, 1);
+    esp_lcd_panel_io_tx_param(io, SSD1306_CMD_DISP_ON, 8, NULL, 0);
+    // SEG/COM will be ON after 100ms after sending DISP_ON command
+    vTaskDelay(pdMS_TO_TICKS(100));
+    return ESP_OK;
+}
+
+static esp_err_t panel_ssd1306_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data)
+{
+    ssd1306_panel_t *ssd1306 = __containerof(panel, ssd1306_panel_t, base);
+    assert((x_start < x_end) && (y_start < y_end) && "start position must be smaller than end position");
+    esp_lcd_panel_io_handle_t io = ssd1306->io;
+    // adding extra gap
+    x_start += ssd1306->x_gap;
+    x_end += ssd1306->x_gap;
+    y_start += ssd1306->y_gap;
+    y_end += ssd1306->y_gap;
+    // one page contains 8 rows (COMs)
+    uint8_t page_start = y_start / 8;
+    uint8_t page_end = (y_end - 1) / 8;
+    // define an area of frame memory where MCU can access
+    esp_lcd_panel_io_tx_param(io, SSD1306_CMD_SET_COLUMN_RANGE, 8, (uint8_t[]) {
+        (x_start & 0x7F),
+        ((x_end - 1) & 0x7F),
+    }, 2);
+    esp_lcd_panel_io_tx_param(io, SSD1306_CMD_SET_PAGE_RANGE, 8, (uint8_t[]) {
+        (page_start & 0x07),
+        (page_end & 0x07),
+    }, 2);
+    // transfer frame buffer
+    size_t len = (y_end - y_start) * (x_end - x_start) * ssd1306->bits_per_pixel / 8;
+    esp_lcd_panel_io_tx_color(io, 0, 0, color_data, len);
+
+    return ESP_OK;
+}
+
+static esp_err_t panel_ssd1306_invert_color(esp_lcd_panel_t *panel, bool invert_color_data)
+{
+    ssd1306_panel_t *ssd1306 = __containerof(panel, ssd1306_panel_t, base);
+    esp_lcd_panel_io_handle_t io = ssd1306->io;
+    int command = 0;
+    if (invert_color_data) {
+        command = SSD1306_CMD_INVERT_ON;
+    } else {
+        command = SSD1306_CMD_INVERT_OFF;
+    }
+    esp_lcd_panel_io_tx_param(io, command, 8, NULL, 0);
+    return ESP_OK;
+}
+
+static esp_err_t panel_ssd1306_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y)
+{
+    ssd1306_panel_t *ssd1306 = __containerof(panel, ssd1306_panel_t, base);
+    esp_lcd_panel_io_handle_t io = ssd1306->io;
+
+    int command = 0;
+    if (mirror_x) {
+        command = SSD1306_CMD_MIRROR_X_ON;
+    } else {
+        command = SSD1306_CMD_MIRROR_X_OFF;
+    }
+    esp_lcd_panel_io_tx_param(io, command, 8, NULL, 0);
+    if (mirror_y) {
+        command = SSD1306_CMD_MIRROR_Y_ON;
+    } else {
+        command = SSD1306_CMD_MIRROR_X_OFF;
+    }
+    esp_lcd_panel_io_tx_param(io, command, 8, NULL, 0);
+    return ESP_OK;
+}
+
+static esp_err_t panel_ssd1306_swap_xy(esp_lcd_panel_t *panel, bool swap_axes)
+{
+    return ESP_ERR_NOT_SUPPORTED;
+}
+
+static esp_err_t panel_ssd1306_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap)
+{
+    ssd1306_panel_t *ssd1306 = __containerof(panel, ssd1306_panel_t, base);
+    ssd1306->x_gap = x_gap;
+    ssd1306->y_gap = y_gap;
+    return ESP_OK;
+}
+
+static esp_err_t panel_ssd1306_disp_off(esp_lcd_panel_t *panel, bool off)
+{
+    ssd1306_panel_t *ssd1306 = __containerof(panel, ssd1306_panel_t, base);
+    esp_lcd_panel_io_handle_t io = ssd1306->io;
+    int command = 0;
+    if (off) {
+        command = SSD1306_CMD_DISP_OFF;
+    } else {
+        command = SSD1306_CMD_DISP_ON;
+    }
+    esp_lcd_panel_io_tx_param(io, command, 8, NULL, 0);
+    return ESP_OK;
+}

+ 263 - 0
components/esp_lcd/src/esp_lcd_panel_st7789.c

@@ -0,0 +1,263 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// #define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
+
+#include <stdlib.h>
+#include <sys/cdefs.h>
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "esp_lcd_panel_interface.h"
+#include "esp_lcd_panel_io.h"
+#include "esp_lcd_panel_vendor.h"
+#include "esp_lcd_panel_ops.h"
+#include "esp_lcd_panel_commands.h"
+#include "driver/gpio.h"
+#include "esp_log.h"
+#include "esp_check.h"
+
+static const char *TAG = "lcd_panel.st7789";
+
+static esp_err_t panel_st7789_del(esp_lcd_panel_t *panel);
+static esp_err_t panel_st7789_reset(esp_lcd_panel_t *panel);
+static esp_err_t panel_st7789_init(esp_lcd_panel_t *panel);
+static esp_err_t panel_st7789_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data);
+static esp_err_t panel_st7789_invert_color(esp_lcd_panel_t *panel, bool invert_color_data);
+static esp_err_t panel_st7789_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y);
+static esp_err_t panel_st7789_swap_xy(esp_lcd_panel_t *panel, bool swap_axes);
+static esp_err_t panel_st7789_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap);
+static esp_err_t panel_st7789_disp_off(esp_lcd_panel_t *panel, bool off);
+
+typedef struct {
+    esp_lcd_panel_t base;
+    esp_lcd_panel_io_handle_t io;
+    int reset_gpio_num;
+    bool reset_level;
+    int x_gap;
+    int y_gap;
+    unsigned int bits_per_pixel;
+    uint8_t madctl_val; // save current value of LCD_CMD_MADCTL register
+    uint8_t colmod_cal; // save surrent value of LCD_CMD_COLMOD register
+} st7789_panel_t;
+
+esp_err_t esp_lcd_new_panel_st7789(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel)
+{
+    esp_err_t ret = ESP_OK;
+    st7789_panel_t *st7789 = NULL;
+    ESP_GOTO_ON_FALSE(io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument");
+    st7789 = calloc(1, sizeof(st7789_panel_t));
+    ESP_GOTO_ON_FALSE(st7789, ESP_ERR_NO_MEM, err, TAG, "no mem for st7789 panel");
+
+    if (panel_dev_config->reset_gpio_num >= 0) {
+        gpio_config_t io_conf = {
+            .mode = GPIO_MODE_OUTPUT,
+            .pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num,
+        };
+        ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "configure GPIO for RST line failed");
+    }
+
+    switch (panel_dev_config->color_space) {
+    case ESP_LCD_COLOR_SPACE_RGB:
+        st7789->madctl_val = 0;
+        break;
+    case ESP_LCD_COLOR_SPACE_BGR:
+        st7789->madctl_val |= LCD_CMD_BGR_BIT;
+        break;
+    default:
+        ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported color space");
+        break;
+    }
+
+    switch (panel_dev_config->bits_per_pixel) {
+    case 16:
+        st7789->colmod_cal = 0x55;
+        break;
+    case 18:
+        st7789->colmod_cal = 0x66;
+        break;
+    default:
+        ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported pixel width");
+        break;
+    }
+    st7789->io = io;
+    st7789->bits_per_pixel = panel_dev_config->bits_per_pixel;
+    st7789->reset_gpio_num = panel_dev_config->reset_gpio_num;
+    st7789->reset_level = panel_dev_config->flags.reset_active_high;
+    st7789->base.del = panel_st7789_del;
+    st7789->base.reset = panel_st7789_reset;
+    st7789->base.init = panel_st7789_init;
+    st7789->base.draw_bitmap = panel_st7789_draw_bitmap;
+    st7789->base.invert_color = panel_st7789_invert_color;
+    st7789->base.set_gap = panel_st7789_set_gap;
+    st7789->base.mirror = panel_st7789_mirror;
+    st7789->base.swap_xy = panel_st7789_swap_xy;
+    st7789->base.disp_off = panel_st7789_disp_off;
+    *ret_panel = &(st7789->base);
+    ESP_LOGD(TAG, "new st7789 panel @%p", st7789);
+
+    return ESP_OK;
+
+err:
+    if (st7789) {
+        if (panel_dev_config->reset_gpio_num >= 0) {
+            gpio_reset_pin(panel_dev_config->reset_gpio_num);
+        }
+        free(st7789);
+    }
+    return ret;
+}
+
+static esp_err_t panel_st7789_del(esp_lcd_panel_t *panel)
+{
+    st7789_panel_t *st7789 = __containerof(panel, st7789_panel_t, base);
+
+    if (st7789->reset_gpio_num >= 0) {
+        gpio_reset_pin(st7789->reset_gpio_num);
+    }
+    ESP_LOGD(TAG, "del st7789 panel @%p", st7789);
+    free(st7789);
+    return ESP_OK;
+}
+
+static esp_err_t panel_st7789_reset(esp_lcd_panel_t *panel)
+{
+    st7789_panel_t *st7789 = __containerof(panel, st7789_panel_t, base);
+    esp_lcd_panel_io_handle_t io = st7789->io;
+
+    // perform hardware reset
+    if (st7789->reset_gpio_num >= 0) {
+        gpio_set_level(st7789->reset_gpio_num, st7789->reset_level);
+        vTaskDelay(pdMS_TO_TICKS(10));
+        gpio_set_level(st7789->reset_gpio_num, !st7789->reset_level);
+        vTaskDelay(pdMS_TO_TICKS(10));
+    } else { // perform software reset
+        esp_lcd_panel_io_tx_param(io, LCD_CMD_SWRESET, 8, NULL, 0);
+        vTaskDelay(pdMS_TO_TICKS(10)); // spec, wait at least 5m before sending new command
+    }
+
+    return ESP_OK;
+}
+
+static esp_err_t panel_st7789_init(esp_lcd_panel_t *panel)
+{
+    st7789_panel_t *st7789 = __containerof(panel, st7789_panel_t, base);
+    esp_lcd_panel_io_handle_t io = st7789->io;
+    // LCD goes into sleep mode and display will be turned off after power on reset, exit sleep mode first
+    esp_lcd_panel_io_tx_param(io, LCD_CMD_SLPOUT, 8, NULL, 0);
+    vTaskDelay(pdMS_TO_TICKS(100));
+    esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, 8, (uint8_t[]) {
+        0
+    }, 1);
+    esp_lcd_panel_io_tx_param(io, LCD_CMD_COLMOD, 8, (uint8_t[]) {
+        st7789->colmod_cal,
+    }, 1);
+    // turn on display
+    esp_lcd_panel_io_tx_param(io, LCD_CMD_DISPON, 8, NULL, 0);
+
+    return ESP_OK;
+}
+
+static esp_err_t panel_st7789_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data)
+{
+    st7789_panel_t *st7789 = __containerof(panel, st7789_panel_t, base);
+    assert((x_start < x_end) && (y_start < y_end) && "start position must be smaller than end position");
+    esp_lcd_panel_io_handle_t io = st7789->io;
+
+    x_start += st7789->x_gap;
+    x_end += st7789->x_gap;
+    y_start += st7789->y_gap;
+    y_end += st7789->y_gap;
+
+    // define an area of frame memory where MCU can access
+    esp_lcd_panel_io_tx_param(io, LCD_CMD_CASET, 8, (uint8_t[]) {
+        (x_start >> 8) & 0xFF,
+        x_start & 0xFF,
+        ((x_end - 1) >> 8) & 0xFF,
+        (x_end - 1) & 0xFF,
+    }, 4);
+    esp_lcd_panel_io_tx_param(io, LCD_CMD_RASET, 8, (uint8_t[]) {
+        (y_start >> 8) & 0xFF,
+        y_start & 0xFF,
+        ((y_end - 1) >> 8) & 0xFF,
+        (y_end - 1) & 0xFF,
+    }, 4);
+    // transfer frame buffer
+    size_t len = (x_end - x_start) * (y_end - y_start) * st7789->bits_per_pixel / 8;
+    esp_lcd_panel_io_tx_color(io, LCD_CMD_RAMWR, 8, color_data, len);
+
+    return ESP_OK;
+}
+
+static esp_err_t panel_st7789_invert_color(esp_lcd_panel_t *panel, bool invert_color_data)
+{
+    st7789_panel_t *st7789 = __containerof(panel, st7789_panel_t, base);
+    esp_lcd_panel_io_handle_t io = st7789->io;
+    int command = 0;
+    if (invert_color_data) {
+        command = LCD_CMD_INVON;
+    } else {
+        command = LCD_CMD_INVOFF;
+    }
+    esp_lcd_panel_io_tx_param(io, command, 8, NULL, 0);
+    return ESP_OK;
+}
+
+static esp_err_t panel_st7789_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y)
+{
+    st7789_panel_t *st7789 = __containerof(panel, st7789_panel_t, base);
+    esp_lcd_panel_io_handle_t io = st7789->io;
+    if (mirror_x) {
+        st7789->madctl_val |= LCD_CMD_MX_BIT;
+    } else {
+        st7789->madctl_val &= ~LCD_CMD_MX_BIT;
+    }
+    if (mirror_y) {
+        st7789->madctl_val |= LCD_CMD_MY_BIT;
+    } else {
+        st7789->madctl_val &= ~LCD_CMD_MY_BIT;
+    }
+    esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, 8, (uint8_t[]) {
+        st7789->madctl_val
+    }, 1);
+    return ESP_OK;
+}
+
+static esp_err_t panel_st7789_swap_xy(esp_lcd_panel_t *panel, bool swap_axes)
+{
+    st7789_panel_t *st7789 = __containerof(panel, st7789_panel_t, base);
+    esp_lcd_panel_io_handle_t io = st7789->io;
+    if (swap_axes) {
+        st7789->madctl_val |= LCD_CMD_MV_BIT;
+    } else {
+        st7789->madctl_val &= ~LCD_CMD_MV_BIT;
+    }
+    esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, 8, (uint8_t[]) {
+        st7789->madctl_val
+    }, 1);
+    return ESP_OK;
+}
+
+static esp_err_t panel_st7789_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap)
+{
+    st7789_panel_t *st7789 = __containerof(panel, st7789_panel_t, base);
+    st7789->x_gap = x_gap;
+    st7789->y_gap = y_gap;
+    return ESP_OK;
+}
+
+static esp_err_t panel_st7789_disp_off(esp_lcd_panel_t *panel, bool off)
+{
+    st7789_panel_t *st7789 = __containerof(panel, st7789_panel_t, base);
+    esp_lcd_panel_io_handle_t io = st7789->io;
+    int command = 0;
+    if (off) {
+        command = LCD_CMD_DISPOFF;
+    } else {
+        command = LCD_CMD_DISPON;
+    }
+    esp_lcd_panel_io_tx_param(io, command, 8, NULL, 0);
+    return ESP_OK;
+}

+ 443 - 0
components/esp_lcd/src/esp_lcd_rgb_panel.c

@@ -0,0 +1,443 @@
+/*
+ * SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// #define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
+
+#include <stdlib.h>
+#include <sys/cdefs.h>
+#include <sys/param.h>
+#include <string.h>
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "freertos/semphr.h"
+#include "esp_attr.h"
+#include "esp_check.h"
+#include "esp_intr_alloc.h"
+#include "esp_heap_caps.h"
+#include "esp_lcd_panel_interface.h"
+#include "esp_lcd_panel_rgb.h"
+#include "esp_lcd_panel_ops.h"
+#include "esp_rom_gpio.h"
+#include "soc/soc_caps.h"
+#include "hal/dma_types.h"
+#include "hal/gpio_hal.h"
+#include "esp_private/gdma.h"
+#include "driver/gpio.h"
+#include "driver/periph_ctrl.h"
+#if SOC_LCDCAM_SUPPORTED
+#include "esp_lcd_common.h"
+#include "soc/lcd_periph.h"
+#include "hal/lcd_hal.h"
+#include "hal/lcd_ll.h"
+
+static const char *TAG = "lcd_panel.rgb";
+
+typedef struct esp_rgb_panel_t esp_rgb_panel_t;
+
+static esp_err_t rgb_panel_del(esp_lcd_panel_t *panel);
+static esp_err_t rgb_panel_reset(esp_lcd_panel_t *panel);
+static esp_err_t rgb_panel_init(esp_lcd_panel_t *panel);
+static esp_err_t rgb_panel_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data);
+static esp_err_t rgb_panel_invert_color(esp_lcd_panel_t *panel, bool invert_color_data);
+static esp_err_t rgb_panel_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y);
+static esp_err_t rgb_panel_swap_xy(esp_lcd_panel_t *panel, bool swap_axes);
+static esp_err_t rgb_panel_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap);
+static esp_err_t rgb_panel_disp_off(esp_lcd_panel_t *panel, bool off);
+static esp_err_t lcd_rgb_panel_create_trans_link(esp_rgb_panel_t *panel);
+static esp_err_t lcd_rgb_panel_configure_gpio(esp_rgb_panel_t *panel, const esp_lcd_rgb_panel_config_t *panel_config);
+static IRAM_ATTR void lcd_default_isr_handler(void *args);
+
+struct esp_rgb_panel_t {
+    esp_lcd_panel_t base;  // Base class of generic lcd panel
+    int panel_id;          // LCD panel ID
+    lcd_hal_context_t hal; // Hal layer object
+    size_t data_width;     // Number of data lines (e.g. for RGB565, the data width is 16)
+    int disp_gpio_num;     // Display control GPIO, which is used to perform action like "disp_off"
+    intr_handle_t intr;    // LCD peripheral interrupt handle
+    size_t num_dma_nodes;  // Number of DMA descriptors that used to carry the frame buffer
+    uint8_t *fb;           // Frame buffer
+    size_t fb_size;        // Size of frame buffer
+    int data_gpio_nums[SOC_LCD_RGB_DATA_WIDTH]; // GPIOs used for data lines, we keep these GPIOs for action like "invert_color"
+    size_t resolution_hz;    // Peripheral clock resolution
+    esp_lcd_rgb_timing_t timings;   // RGB timing parameters (e.g. pclk, sync pulse, porch width)
+    gdma_channel_handle_t dma_chan; // DMA channel handle
+    int new_frame_id;               // ID for new frame, we use ID to identify whether the frame content has been updated
+    int cur_frame_id;               // ID for current transferring frame
+    SemaphoreHandle_t done_sem;     // Binary semaphore, indicating if the new frame has been flushed to LCD
+    bool (*on_frame_trans_done)(esp_lcd_panel_t *panel, void *user_data); // Callback, invoked after frame trans done
+    void *user_data;                // Reserved user's data of callback functions
+    int x_gap;                      // Extra gap in x coordinate, it's used when calculate the flush window
+    int y_gap;                      // Extra gap in y coordinate, it's used when calculate the flush window
+    struct {
+        int disp_en_level: 1; // The level which can turn on the screen by `disp_gpio_num`
+        int stream_mode: 1;   // If set, the LCD transfers data continuously, otherwise, it stops refreshing the LCD when transaction done
+        int new_frame: 1;     // Whether the frame we're going to flush is a new one
+    } flags;
+    dma_descriptor_t dma_nodes[0]; // DMA descriptor pool of size `num_dma_nodes`
+};
+
+esp_err_t esp_lcd_new_rgb_panel(const esp_lcd_rgb_panel_config_t *rgb_panel_config, esp_lcd_panel_handle_t *ret_panel)
+{
+    esp_err_t ret = ESP_OK;
+    esp_rgb_panel_t *rgb_panel = NULL;
+    ESP_GOTO_ON_FALSE(rgb_panel_config && ret_panel, ESP_ERR_INVALID_ARG, err_arg, TAG, "invalid parameter");
+    ESP_GOTO_ON_FALSE(rgb_panel_config->data_width == 16, ESP_ERR_NOT_SUPPORTED, err_arg, TAG,
+                      "unsupported data width %d", rgb_panel_config->data_width);
+    // calculate the number of DMA descriptors
+    size_t fb_size = rgb_panel_config->timings.h_res * rgb_panel_config->timings.v_res * rgb_panel_config->data_width / 8;
+    size_t num_dma_nodes = fb_size / DMA_DESCRIPTOR_BUFFER_MAX_SIZE;
+    if (fb_size > num_dma_nodes * DMA_DESCRIPTOR_BUFFER_MAX_SIZE) {
+        num_dma_nodes++;
+    }
+    // DMA descriptors must be placed in internal SRAM (requested by DMA)
+    rgb_panel = heap_caps_calloc(1, sizeof(esp_rgb_panel_t) + num_dma_nodes * sizeof(dma_descriptor_t), MALLOC_CAP_DMA);
+    ESP_GOTO_ON_FALSE(rgb_panel, ESP_ERR_NO_MEM, no_mem_panel, TAG, "no mem for rgb panel");
+    rgb_panel->num_dma_nodes = num_dma_nodes;
+    // alloc frame buffer, currently we have to put the frame buffer in SRAM
+    rgb_panel->fb = heap_caps_calloc(1, fb_size, MALLOC_CAP_INTERNAL);
+    ESP_GOTO_ON_FALSE(rgb_panel->fb, ESP_ERR_NO_MEM, no_mem_fb, TAG, "no mem for frame buffer");
+    rgb_panel->fb_size = fb_size;
+    // semaphore indicates new frame trans done
+    rgb_panel->done_sem = xSemaphoreCreateBinary();
+    ESP_GOTO_ON_FALSE(rgb_panel->done_sem, ESP_ERR_NO_MEM, no_mem_sem, TAG, "create done sem failed");
+    xSemaphoreGive(rgb_panel->done_sem); // initialize the semaphore count to 1
+    // register to platform
+    int panel_id = lcd_com_register_device(LCD_COM_DEVICE_TYPE_RGB, rgb_panel);
+    ESP_GOTO_ON_FALSE(panel_id >= 0, ESP_ERR_NOT_FOUND, no_slot, TAG, "no free rgb panel slot");
+    rgb_panel->panel_id = panel_id;
+    // enable APB to access LCD registers
+    periph_module_enable(lcd_periph_signals.panels[panel_id].module);
+    // initialize HAL layer, so we can call LL APIs later
+    lcd_hal_init(&rgb_panel->hal, panel_id);
+    // install interrupt service, (LCD peripheral shares the interrupt source with Camera by different mask)
+    int isr_flags = 0;
+    ret = esp_intr_alloc_intrstatus(lcd_periph_signals.panels[panel_id].irq_id, isr_flags,
+                                    lcd_ll_get_interrupt_status_reg(rgb_panel->hal.dev),
+                                    LCD_LL_EVENT_VSYNC_END, lcd_default_isr_handler, rgb_panel, &rgb_panel->intr);
+    ESP_GOTO_ON_ERROR(ret, no_int, TAG, "install interrupt failed");
+    lcd_ll_enable_interrupt(rgb_panel->hal.dev, LCD_LL_EVENT_VSYNC_END, false); // disable all interrupts
+    lcd_ll_clear_interrupt_status(rgb_panel->hal.dev, UINT32_MAX); // clear pending interrupt
+    // install DMA service
+    rgb_panel->flags.stream_mode = !rgb_panel_config->flags.relax_on_idle;
+    ret = lcd_rgb_panel_create_trans_link(rgb_panel);
+    ESP_GOTO_ON_ERROR(ret, no_dma, TAG, "install DMA failed");
+    // configure GPIO
+    ret = lcd_rgb_panel_configure_gpio(rgb_panel, rgb_panel_config);
+    ESP_GOTO_ON_ERROR(ret, no_gpio, TAG, "configure GPIO failed");
+    // fill other rgb panel runtime parameters
+    memcpy(rgb_panel->data_gpio_nums, rgb_panel_config->data_gpio_nums, SOC_LCD_RGB_DATA_WIDTH);
+    rgb_panel->timings = rgb_panel_config->timings;
+    rgb_panel->data_width = rgb_panel_config->data_width;
+    rgb_panel->disp_gpio_num = rgb_panel_config->disp_gpio_num;
+    rgb_panel->flags.disp_en_level = !rgb_panel_config->flags.disp_active_low;
+    rgb_panel->on_frame_trans_done = rgb_panel_config->on_frame_trans_done;
+    rgb_panel->user_data = rgb_panel_config->user_data;
+    // fill function table
+    rgb_panel->base.del = rgb_panel_del;
+    rgb_panel->base.reset = rgb_panel_reset;
+    rgb_panel->base.init = rgb_panel_init;
+    rgb_panel->base.draw_bitmap = rgb_panel_draw_bitmap;
+    rgb_panel->base.disp_off = rgb_panel_disp_off;
+    rgb_panel->base.invert_color = rgb_panel_invert_color;
+    rgb_panel->base.mirror = rgb_panel_mirror;
+    rgb_panel->base.swap_xy = rgb_panel_swap_xy;
+    rgb_panel->base.set_gap = rgb_panel_set_gap;
+    // return base class
+    *ret_panel = &(rgb_panel->base);
+    ESP_LOGD(TAG, "new rgb panel(%d) @%p, fb_size=%zu", rgb_panel->panel_id, rgb_panel, rgb_panel->fb_size);
+    return ESP_OK;
+
+no_gpio:
+    gdma_disconnect(rgb_panel->dma_chan);
+    gdma_del_channel(rgb_panel->dma_chan);
+no_dma:
+    esp_intr_free(rgb_panel->intr);
+no_int:
+    periph_module_disable(lcd_periph_signals.panels[rgb_panel->panel_id].module);
+    lcd_com_remove_device(LCD_COM_DEVICE_TYPE_RGB, rgb_panel->panel_id);
+no_slot:
+    vSemaphoreDelete(rgb_panel->done_sem);
+no_mem_sem:
+    free(rgb_panel->fb);
+no_mem_fb:
+    free(rgb_panel);
+no_mem_panel:
+err_arg:
+    return ret;
+}
+
+static esp_err_t rgb_panel_del(esp_lcd_panel_t *panel)
+{
+    esp_rgb_panel_t *rgb_panel = __containerof(panel, esp_rgb_panel_t, base);
+    xSemaphoreTake(rgb_panel->done_sem, portMAX_DELAY); // wait for last flush done
+    int panel_id = rgb_panel->panel_id;
+    gdma_disconnect(rgb_panel->dma_chan);
+    gdma_del_channel(rgb_panel->dma_chan);
+    esp_intr_free(rgb_panel->intr);
+    periph_module_disable(lcd_periph_signals.panels[panel_id].module);
+    lcd_com_remove_device(LCD_COM_DEVICE_TYPE_RGB, rgb_panel->panel_id);
+    vSemaphoreDelete(rgb_panel->done_sem);
+    free(rgb_panel->fb);
+    free(rgb_panel);
+    ESP_LOGD(TAG, "del rgb panel(%d)", panel_id);
+    return ESP_OK;
+}
+
+static esp_err_t rgb_panel_reset(esp_lcd_panel_t *panel)
+{
+    esp_rgb_panel_t *rgb_panel = __containerof(panel, esp_rgb_panel_t, base);
+    lcd_ll_fifo_reset(rgb_panel->hal.dev);
+    lcd_ll_reset(rgb_panel->hal.dev);
+    return ESP_OK;
+}
+
+static esp_err_t rgb_panel_init(esp_lcd_panel_t *panel)
+{
+    esp_err_t ret = ESP_OK;
+    esp_rgb_panel_t *rgb_panel = __containerof(panel, esp_rgb_panel_t, base);
+    // configure clock
+    lcd_ll_enable_clock(rgb_panel->hal.dev, true);
+    // set peripheral clock resolution
+    rgb_panel->resolution_hz = lcd_com_select_periph_clock(&rgb_panel->hal);
+    // set PCLK frequency
+    uint32_t pclk_prescale = rgb_panel->resolution_hz / rgb_panel->timings.pclk_hz;
+    ESP_GOTO_ON_FALSE(pclk_prescale <= LCD_LL_CLOCK_PRESCALE_MAX, ESP_ERR_NOT_SUPPORTED, err, TAG,
+                      "prescaler can't satisfy PCLK clock %uHz", rgb_panel->timings.pclk_hz);
+    lcd_ll_set_pixel_clock_prescale(rgb_panel->hal.dev, pclk_prescale);
+    rgb_panel->timings.pclk_hz = rgb_panel->resolution_hz / pclk_prescale;
+    // pixel clock phase and polarity
+    lcd_ll_set_clock_idle_level(rgb_panel->hal.dev, !rgb_panel->timings.flags.pclk_idle_low);
+    lcd_ll_set_pixel_clock_edge(rgb_panel->hal.dev, rgb_panel->timings.flags.pclk_active_neg);
+    // enable RGB mode and set data width
+    lcd_ll_enable_rgb_mode(rgb_panel->hal.dev, true);
+    lcd_ll_set_data_width(rgb_panel->hal.dev, rgb_panel->data_width);
+    lcd_ll_set_phase_cycles(rgb_panel->hal.dev, 0, 0, 1); // enable data phase only
+    // number of data cycles is controlled by DMA buffer size
+    lcd_ll_enable_output_always_on(rgb_panel->hal.dev, true);
+    // configure HSYNC, VSYNC, DE signal idle state level
+    lcd_ll_set_idle_level(rgb_panel->hal.dev, !rgb_panel->timings.flags.hsync_idle_low,
+                          !rgb_panel->timings.flags.vsync_idle_low, rgb_panel->timings.flags.de_idle_high);
+    // configure blank region timing
+    lcd_ll_set_blank_cycles(rgb_panel->hal.dev, 1, 1); // RGB panel always has a front and back blank (porch region)
+    lcd_ll_set_horizontal_timing(rgb_panel->hal.dev, rgb_panel->timings.hsync_pulse_width,
+                                 rgb_panel->timings.hsync_back_porch, rgb_panel->timings.h_res,
+                                 rgb_panel->timings.hsync_front_porch);
+    lcd_ll_set_vertical_timing(rgb_panel->hal.dev, rgb_panel->timings.vsync_pulse_width,
+                               rgb_panel->timings.vsync_back_porch, rgb_panel->timings.v_res,
+                               rgb_panel->timings.vsync_front_porch);
+    // output hsync even in porch region
+    lcd_ll_enable_output_hsync_in_porch_region(rgb_panel->hal.dev, true);
+    // generate the hsync at the very begining of line
+    lcd_ll_set_hsync_position(rgb_panel->hal.dev, 0);
+    // starting sending next frame automatically
+    lcd_ll_enable_auto_next_frame(rgb_panel->hal.dev, rgb_panel->flags.stream_mode);
+    // trigger interrupt on the end of frame
+    lcd_ll_enable_interrupt(rgb_panel->hal.dev, LCD_LL_EVENT_VSYNC_END, true);
+    ESP_LOGD(TAG, "rgb panel(%d) start, pclk=%uHz", rgb_panel->panel_id, rgb_panel->timings.pclk_hz);
+err:
+    return ret;
+}
+
+static esp_err_t rgb_panel_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data)
+{
+    esp_rgb_panel_t *rgb_panel = __containerof(panel, esp_rgb_panel_t, base);
+    assert((x_start < x_end) && (y_start < y_end) && "start position must be smaller than end position");
+    // adjust the flush window by adding extra gap
+    x_start += rgb_panel->x_gap;
+    y_start += rgb_panel->y_gap;
+    x_end += rgb_panel->x_gap;
+    y_end += rgb_panel->y_gap;
+    // round the boundary
+    x_start = MIN(x_start, rgb_panel->timings.h_res);
+    x_end = MIN(x_end, rgb_panel->timings.h_res);
+    y_start = MIN(y_start, rgb_panel->timings.v_res);
+    y_end = MIN(y_end, rgb_panel->timings.v_res);
+    xSemaphoreTake(rgb_panel->done_sem, portMAX_DELAY); // wait for last transaction done
+    // convert the frame buffer to 3D array
+    int bytes_pre_pixel = rgb_panel->data_width / 8;
+    int pixels_pre_line = rgb_panel->timings.h_res;
+    const uint8_t *from = (const uint8_t *)color_data;
+    uint8_t (*to)[pixels_pre_line][bytes_pre_pixel] = (uint8_t (*)[pixels_pre_line][bytes_pre_pixel])rgb_panel->fb;
+    // manipulate the frame buffer
+    for (int j = y_start; j < y_end; j++) {
+        for (int i = x_start; i < x_end; i++) {
+            for (int k = 0; k < bytes_pre_pixel; k++) {
+                to[j][i][k] = *from++;
+            }
+        }
+    }
+    // we don't care the exact frame ID, as long as it's different from the previous one
+    rgb_panel->new_frame_id++;
+    if (!rgb_panel->flags.stream_mode) {
+        // in one-off mode, the "new frame" flag is controlled by this API
+        rgb_panel->cur_frame_id = rgb_panel->new_frame_id;
+        rgb_panel->flags.new_frame = 1;
+        // reset FIFO of DMA and LCD, incase there remains old frame data
+        gdma_reset(rgb_panel->dma_chan);
+        lcd_ll_stop(rgb_panel->hal.dev);
+        lcd_ll_fifo_reset(rgb_panel->hal.dev);
+        gdma_start(rgb_panel->dma_chan, (intptr_t)rgb_panel->dma_nodes);
+    }
+    // start LCD engine
+    lcd_ll_start(rgb_panel->hal.dev);
+    return ESP_OK;
+}
+
+static esp_err_t rgb_panel_invert_color(esp_lcd_panel_t *panel, bool invert_color_data)
+{
+    esp_rgb_panel_t *rgb_panel = __containerof(panel, esp_rgb_panel_t, base);
+    int panel_id = rgb_panel->panel_id;
+    // inverting the data line by GPIO matrix
+    for (int i = 0; i < rgb_panel->data_width; i++) {
+        esp_rom_gpio_connect_out_signal(rgb_panel->data_gpio_nums[i], lcd_periph_signals.panels[panel_id].data_sigs[i],
+                                        invert_color_data, false);
+    }
+    return ESP_OK;
+}
+
+static esp_err_t rgb_panel_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y)
+{
+    return ESP_ERR_NOT_SUPPORTED;
+}
+
+static esp_err_t rgb_panel_swap_xy(esp_lcd_panel_t *panel, bool swap_axes)
+{
+    return ESP_ERR_NOT_SUPPORTED;
+}
+
+static esp_err_t rgb_panel_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap)
+{
+    esp_rgb_panel_t *rgb_panel = __containerof(panel, esp_rgb_panel_t, base);
+    rgb_panel->x_gap = x_gap;
+    rgb_panel->x_gap = y_gap;
+    return ESP_OK;
+}
+
+static esp_err_t rgb_panel_disp_off(esp_lcd_panel_t *panel, bool off)
+{
+    esp_rgb_panel_t *rgb_panel = __containerof(panel, esp_rgb_panel_t, base);
+    if (rgb_panel->disp_gpio_num < 0) {
+        return ESP_ERR_NOT_SUPPORTED;
+    }
+    if (off) { // turn off screen
+        gpio_set_level(rgb_panel->disp_gpio_num, !rgb_panel->flags.disp_en_level);
+    } else { // turn on screen
+        gpio_set_level(rgb_panel->disp_gpio_num, rgb_panel->flags.disp_en_level);
+    }
+    return ESP_OK;
+}
+
+static esp_err_t lcd_rgb_panel_configure_gpio(esp_rgb_panel_t *panel, const esp_lcd_rgb_panel_config_t *panel_config)
+{
+    int panel_id = panel->panel_id;
+    // check validation of GPIO number
+    bool valid_gpio = (panel_config->hsync_gpio_num >= 0) && (panel_config->vsync_gpio_num >= 0) &&
+                      (panel_config->pclk_gpio_num >= 0);
+    for (size_t i = 0; i < panel_config->data_width; i++) {
+        valid_gpio = valid_gpio && (panel_config->data_gpio_nums[i] >= 0);
+    }
+    if (!valid_gpio) {
+        return ESP_ERR_INVALID_ARG;
+    }
+    // connect peripheral signals via GPIO matrix
+    for (size_t i = 0; i < panel_config->data_width; i++) {
+        gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[panel_config->data_gpio_nums[i]], PIN_FUNC_GPIO);
+        gpio_set_direction(panel_config->data_gpio_nums[i], GPIO_MODE_OUTPUT);
+        esp_rom_gpio_connect_out_signal(panel_config->data_gpio_nums[i],
+                                        lcd_periph_signals.panels[panel_id].data_sigs[i], false, false);
+    }
+    gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[panel_config->hsync_gpio_num], PIN_FUNC_GPIO);
+    gpio_set_direction(panel_config->hsync_gpio_num, GPIO_MODE_OUTPUT);
+    esp_rom_gpio_connect_out_signal(panel_config->hsync_gpio_num,
+                                    lcd_periph_signals.panels[panel_id].hsync_sig, false, false);
+    gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[panel_config->vsync_gpio_num], PIN_FUNC_GPIO);
+    gpio_set_direction(panel_config->vsync_gpio_num, GPIO_MODE_OUTPUT);
+    esp_rom_gpio_connect_out_signal(panel_config->vsync_gpio_num,
+                                    lcd_periph_signals.panels[panel_id].vsync_sig, false, false);
+    gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[panel_config->pclk_gpio_num], PIN_FUNC_GPIO);
+    gpio_set_direction(panel_config->pclk_gpio_num, GPIO_MODE_OUTPUT);
+    esp_rom_gpio_connect_out_signal(panel_config->pclk_gpio_num,
+                                    lcd_periph_signals.panels[panel_id].pclk_sig, false, false);
+    // DE signal might not be necessary for some RGB LCD
+    if (panel_config->de_gpio_num >= 0) {
+        gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[panel_config->de_gpio_num], PIN_FUNC_GPIO);
+        gpio_set_direction(panel_config->de_gpio_num, GPIO_MODE_OUTPUT);
+        esp_rom_gpio_connect_out_signal(panel_config->de_gpio_num,
+                                        lcd_periph_signals.panels[panel_id].de_sig, false, false);
+    }
+    // disp enable GPIO is optional
+    if (panel_config->disp_gpio_num >= 0) {
+        gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[panel_config->disp_gpio_num], PIN_FUNC_GPIO);
+        gpio_set_direction(panel_config->disp_gpio_num, GPIO_MODE_OUTPUT);
+        esp_rom_gpio_connect_out_signal(panel_config->disp_gpio_num, SIG_GPIO_OUT_IDX, false, false);
+    }
+    return ESP_OK;
+}
+
+static esp_err_t lcd_rgb_panel_create_trans_link(esp_rgb_panel_t *panel)
+{
+    esp_err_t ret = ESP_OK;
+    // chain DMA descriptors
+    for (int i = 0; i < panel->num_dma_nodes; i++) {
+        panel->dma_nodes[i].dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_CPU;
+        panel->dma_nodes[i].next = &panel->dma_nodes[i + 1];
+    }
+    // fix the last DMA descriptor according to whether the LCD works in stream mode
+    if (panel->flags.stream_mode) {
+        panel->dma_nodes[panel->num_dma_nodes - 1].next = &panel->dma_nodes[0]; // chain into a circle
+    } else {
+        panel->dma_nodes[panel->num_dma_nodes - 1].next = NULL; // one-off DMA chain
+    }
+    // mount the frame buffer to the DMA descriptors
+    lcd_com_mount_dma_data(panel->dma_nodes, panel->fb, panel->fb_size);
+    // alloc DMA channel and connect to LCD peripheral
+    gdma_channel_alloc_config_t dma_chan_config = {
+        .direction = GDMA_CHANNEL_DIRECTION_TX,
+    };
+    ret = gdma_new_channel(&dma_chan_config, &panel->dma_chan);
+    ESP_GOTO_ON_ERROR(ret, err, TAG, "alloc DMA channel failed");
+    gdma_connect(panel->dma_chan, GDMA_MAKE_TRIGGER(GDMA_TRIG_PERIPH_LCD, 0));
+    // the start of DMA should be prior to the start of LCD engine
+    gdma_start(panel->dma_chan, (intptr_t)panel->dma_nodes);
+
+err:
+    return ret;
+}
+
+IRAM_ATTR static void lcd_default_isr_handler(void *args)
+{
+    esp_rgb_panel_t *panel = (esp_rgb_panel_t *)args;
+    bool need_yield = false;
+    BaseType_t high_task_woken = pdFALSE;
+
+    uint32_t intr_status = lcd_ll_get_interrupt_status(panel->hal.dev);
+    lcd_ll_clear_interrupt_status(panel->hal.dev, intr_status);
+    if (intr_status & LCD_LL_EVENT_VSYNC_END) {
+        if (panel->flags.new_frame) { // the finished one is a new frame
+            if (panel->on_frame_trans_done) {
+                if (panel->on_frame_trans_done(&panel->base, panel->user_data)) {
+                    need_yield = true;
+                }
+            }
+            xSemaphoreGiveFromISR(panel->done_sem, &high_task_woken);
+            if (high_task_woken == pdTRUE) {
+                need_yield = true;
+            }
+        }
+        // in stream mode, the "new frame" flag is controlled by comparing "new frame id" and "cur frame id"
+        if (panel->flags.stream_mode) {
+            // new_frame_id is only modified in `rgb_panel_draw_bitmap()`, fetch first and use below to avoid inconsistent
+            int new_frame_id = panel->new_frame_id;
+            panel->flags.new_frame = (panel->cur_frame_id != new_frame_id);
+            panel->cur_frame_id = new_frame_id;
+        }
+    }
+    if (need_yield) {
+        portYIELD_FROM_ISR();
+    }
+}
+
+#endif // SOC_LCDCAM_SUPPORTED

+ 3 - 0
components/esp_lcd/test/CMakeLists.txt

@@ -0,0 +1,3 @@
+idf_component_register(SRC_DIRS .
+                       PRIV_INCLUDE_DIRS .
+                       PRIV_REQUIRES cmock test_utils esp_lcd)

+ 7 - 0
components/esp_lcd/test/component.mk

@@ -0,0 +1,7 @@
+#
+#Component Makefile
+#
+
+COMPONENT_SRCDIRS := .
+
+COMPONENT_ADD_LDFLAGS = -Wl,--whole-archive -l$(COMPONENT_NAME) -Wl,--no-whole-archive

+ 125 - 0
components/esp_lcd/test/test_i2c_lcd_panel.c

@@ -0,0 +1,125 @@
+#include <stdio.h>
+#include <string.h>
+#include "sdkconfig.h"
+#include "unity.h"
+#include "test_utils.h"
+#include "driver/i2c.h"
+#include "driver/gpio.h"
+#include "esp_lcd_panel_io.h"
+#include "esp_lcd_panel_vendor.h"
+#include "esp_lcd_panel_ops.h"
+#include "esp_system.h"
+
+#define TEST_LCD_H_RES          (128)
+#define TEST_LCD_V_RES          (64)
+#define TEST_I2C_SDA_GPIO       (3)
+#define TEST_I2C_SCL_GPIO       (4)
+#define TEST_I2C_HOST_ID        (0)
+#define TEST_I2C_DEV_ADDR       (0x3C)
+#define TEST_LCD_PIXEL_CLOCK_HZ (400 * 1000)
+
+TEST_CASE("lcd panel with i2c interface (ssd1306)", "[lcd]")
+{
+    const uint8_t pattern[][16] = {{
+            0x00, 0x7E, 0x42, 0x42, 0x42, 0x42, 0x7E, 0x00,
+            0x00, 0x7E, 0x42, 0x42, 0x42, 0x42, 0x7E, 0x00
+        },
+        {
+            0x81, 0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x81,
+            0x81, 0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x81
+        }
+    };
+
+    i2c_config_t conf = {
+        .mode = I2C_MODE_MASTER,
+        .sda_io_num = TEST_I2C_SDA_GPIO,
+        .scl_io_num = TEST_I2C_SCL_GPIO,
+        .sda_pullup_en = GPIO_PULLUP_ENABLE,
+        .scl_pullup_en = GPIO_PULLUP_ENABLE,
+        .master.clk_speed = TEST_LCD_PIXEL_CLOCK_HZ,
+    };
+    TEST_ESP_OK(i2c_param_config(TEST_I2C_HOST_ID, &conf));
+    TEST_ESP_OK(i2c_driver_install(TEST_I2C_HOST_ID, I2C_MODE_MASTER, 0, 0, 0));
+
+    esp_lcd_panel_io_handle_t io_handle = NULL;
+    esp_lcd_panel_io_i2c_config_t io_config = {
+        .dev_addr = TEST_I2C_DEV_ADDR,
+        .control_phase_bytes = 1, // According to SSD1306 datasheet
+        .dc_bit_offset = 6,       // According to SSD1306 datasheet
+    };
+    TEST_ESP_OK(esp_lcd_new_panel_io_i2c((esp_lcd_i2c_bus_handle_t)TEST_I2C_HOST_ID, &io_config, &io_handle));
+
+    esp_lcd_panel_handle_t panel_handle = NULL;
+    esp_lcd_panel_dev_config_t panel_config = {
+        .bits_per_pixel = 1,
+        .color_space = ESP_LCD_COLOR_SPACE_MONOCHROME,
+        .reset_gpio_num = -1,
+    };
+    TEST_ESP_OK(esp_lcd_new_panel_ssd1306(io_handle, &panel_config, &panel_handle));
+    TEST_ESP_OK(esp_lcd_panel_reset(panel_handle));
+    TEST_ESP_OK(esp_lcd_panel_init(panel_handle));
+
+    for (int i = 0; i < TEST_LCD_H_RES / 16; i++) {
+        for (int j = 0; j < TEST_LCD_V_RES / 8; j++) {
+            TEST_ESP_OK(esp_lcd_panel_draw_bitmap(panel_handle, i * 16, j * 8, i * 16 + 16, j * 8 + 8, pattern[i & 0x01]));
+        }
+    }
+
+    TEST_ESP_OK(esp_lcd_panel_del(panel_handle));
+    TEST_ESP_OK(esp_lcd_panel_io_del(io_handle));
+    TEST_ESP_OK(i2c_driver_delete(TEST_I2C_HOST_ID));
+}
+
+// The following test shows a porting example of LVGL GUI library
+// To run the LVGL tests, you need to clone the LVGL library into components directory firstly
+#if CONFIG_LV_USE_USER_DATA
+#include "test_lvgl_port.h"
+#if CONFIG_LV_COLOR_DEPTH_1
+static bool notify_lvgl_ready_to_flush(esp_lcd_panel_io_handle_t panel_io, void *user_data, void *event_data)
+{
+    lv_disp_t *disp = *(lv_disp_t **)user_data;
+    lv_disp_flush_ready(&disp->driver);
+    return false;
+}
+
+TEST_CASE("lvgl gui with i2c interface (ssd1306)", "[lcd][lvgl][ignore]")
+{
+    // initialize LVGL graphics library
+    lv_disp_t *disp = NULL;
+    lv_init();
+
+    i2c_config_t conf = {
+        .mode = I2C_MODE_MASTER,
+        .sda_io_num = TEST_I2C_SDA_GPIO,
+        .scl_io_num = TEST_I2C_SCL_GPIO,
+        .sda_pullup_en = GPIO_PULLUP_ENABLE,
+        .scl_pullup_en = GPIO_PULLUP_ENABLE,
+        .master.clk_speed = TEST_LCD_PIXEL_CLOCK_HZ,
+    };
+    TEST_ESP_OK(i2c_param_config(TEST_I2C_HOST_ID, &conf));
+    TEST_ESP_OK(i2c_driver_install(TEST_I2C_HOST_ID, I2C_MODE_MASTER, 0, 0, 0));
+
+    esp_lcd_panel_io_handle_t io_handle = NULL;
+    esp_lcd_panel_io_i2c_config_t io_config = {
+        .dev_addr = TEST_I2C_DEV_ADDR,
+        .control_phase_bytes = 1, // According to SSD1306 datasheet
+        .dc_bit_offset = 6,       // According to SSD1306 datasheet
+        .on_color_trans_done = notify_lvgl_ready_to_flush,
+        .user_data = &disp,
+    };
+    TEST_ESP_OK(esp_lcd_new_panel_io_i2c((esp_lcd_i2c_bus_handle_t)TEST_I2C_HOST_ID, &io_config, &io_handle));
+
+    esp_lcd_panel_handle_t panel_handle = NULL;
+    esp_lcd_panel_dev_config_t panel_config = {
+        .bits_per_pixel = 1,
+        .color_space = ESP_LCD_COLOR_SPACE_MONOCHROME,
+        .reset_gpio_num = -1,
+    };
+    TEST_ESP_OK(esp_lcd_new_panel_ssd1306(io_handle, &panel_config, &panel_handle));
+    TEST_ESP_OK(esp_lcd_panel_reset(panel_handle));
+    TEST_ESP_OK(esp_lcd_panel_init(panel_handle));
+
+    test_lvgl_task_loop(panel_handle, TEST_LCD_H_RES, TEST_LCD_V_RES, &disp);
+}
+#endif // CONFIG_LV_COLOR_DEPTH_1
+#endif // CONFIG_LV_USE_USER_DATA

+ 355 - 0
components/esp_lcd/test/test_i80_lcd_panel.c

@@ -0,0 +1,355 @@
+#include <stdio.h>
+#include <string.h>
+#include "unity.h"
+#include "test_utils.h"
+#include "esp_lcd_panel_io.h"
+#include "esp_lcd_panel_vendor.h"
+#include "esp_lcd_panel_ops.h"
+#include "soc/soc_caps.h"
+#include "driver/gpio.h"
+
+#define TEST_LCD_H_RES         (240)
+#define TEST_LCD_V_RES         (280)
+#define TEST_LCD_BK_LIGHT_GPIO (1)
+#define TEST_LCD_RST_GPIO      (2)
+#define TEST_LCD_CS_GPIO       (4)
+#define TEST_LCD_DC_GPIO       (5)
+#define TEST_LCD_PCLK_GPIO     (6)
+#define TEST_LCD_DATA0_GPIO    (33)
+#define TEST_LCD_DATA1_GPIO    (34)
+#define TEST_LCD_DATA2_GPIO    (35)
+#define TEST_LCD_DATA3_GPIO    (36)
+#define TEST_LCD_DATA4_GPIO    (37)
+#define TEST_LCD_DATA5_GPIO    (38)
+#define TEST_LCD_DATA6_GPIO    (39)
+#define TEST_LCD_DATA7_GPIO    (40)
+#define TEST_LCD_DATA8_GPIO    (41)
+#define TEST_LCD_DATA9_GPIO    (42)
+#define TEST_LCD_DATA10_GPIO   (15)
+#define TEST_LCD_DATA11_GPIO   (16)
+#define TEST_LCD_DATA12_GPIO   (17)
+#define TEST_LCD_DATA13_GPIO   (18)
+#define TEST_LCD_DATA14_GPIO   (19)
+#define TEST_LCD_DATA15_GPIO   (20)
+
+#if SOC_LCD_I80_SUPPORTED
+TEST_CASE("lcd i80 bus and device allocation", "[lcd]")
+{
+    esp_lcd_i80_bus_handle_t i80_buses[SOC_LCD_I80_BUSES] = {};
+    esp_lcd_i80_bus_config_t bus_config = {
+        .dc_gpio_num = TEST_LCD_DC_GPIO,
+        .wr_gpio_num = TEST_LCD_PCLK_GPIO,
+        .data_gpio_nums = {
+            TEST_LCD_DATA0_GPIO,
+            TEST_LCD_DATA1_GPIO,
+            TEST_LCD_DATA2_GPIO,
+            TEST_LCD_DATA3_GPIO,
+            TEST_LCD_DATA4_GPIO,
+            TEST_LCD_DATA5_GPIO,
+            TEST_LCD_DATA6_GPIO,
+            TEST_LCD_DATA7_GPIO,
+        },
+        .data_width = 8,
+        .max_transfer_bytes = TEST_LCD_H_RES * 40 * sizeof(uint16_t)
+    };
+    for (int i = 0; i < SOC_LCD_I80_BUSES; i++) {
+        TEST_ESP_OK(esp_lcd_new_i80_bus(&bus_config, &i80_buses[i]));
+    }
+    TEST_ASSERT_EQUAL(ESP_ERR_NOT_FOUND, esp_lcd_new_i80_bus(&bus_config, &i80_buses[0]));
+    esp_lcd_panel_io_handle_t io_handles[10] = {};
+    esp_lcd_panel_io_i80_config_t io_config = {
+        .cs_gpio_num = TEST_LCD_CS_GPIO,
+        .pclk_hz = 5000000,
+        .trans_queue_depth = 4,
+    };
+    for (int i = 0; i < 10; i++) {
+        TEST_ESP_OK(esp_lcd_new_panel_io_i80(i80_buses[0], &io_config, &io_handles[i]));
+    }
+    // can't delete bus handle before we delete all devices
+    TEST_ASSERT_EQUAL(ESP_ERR_INVALID_STATE, esp_lcd_del_i80_bus(i80_buses[0]));
+    for (int i = 0; i < 10; i++) {
+        TEST_ESP_OK(esp_lcd_panel_io_del(io_handles[i]));
+    }
+    for (int i = 0; i < SOC_LCD_I80_BUSES; i++) {
+        TEST_ESP_OK(esp_lcd_del_i80_bus(i80_buses[i]));
+    }
+}
+
+TEST_CASE("lcd i80 device swap color bytes", "[lcd]")
+{
+    esp_lcd_i80_bus_handle_t i80_bus = NULL;
+    esp_lcd_i80_bus_config_t bus_config = {
+        .dc_gpio_num = TEST_LCD_DC_GPIO,
+        .wr_gpio_num = TEST_LCD_PCLK_GPIO,
+        .data_gpio_nums = {
+            TEST_LCD_DATA0_GPIO,
+            TEST_LCD_DATA1_GPIO,
+            TEST_LCD_DATA2_GPIO,
+            TEST_LCD_DATA3_GPIO,
+            TEST_LCD_DATA4_GPIO,
+            TEST_LCD_DATA5_GPIO,
+            TEST_LCD_DATA6_GPIO,
+            TEST_LCD_DATA7_GPIO,
+        },
+        .data_width = 8,
+        .max_transfer_bytes = 20,
+    };
+    TEST_ESP_OK(esp_lcd_new_i80_bus(&bus_config, &i80_bus));
+
+    esp_lcd_panel_io_handle_t io_handles[4] = {};
+    esp_lcd_panel_io_i80_config_t io_config = {
+        .cs_gpio_num = TEST_LCD_CS_GPIO,
+        .pclk_hz = 5000000,
+        .trans_queue_depth = 10,
+        .dc_levels = {
+            .dc_idle_level = 0,
+            .dc_cmd_level = 0,
+            .dc_dummy_level = 0,
+            .dc_data_level = 1,
+        },
+    };
+
+    io_config.flags.reverse_color_bits = 0;
+    io_config.flags.swap_color_bytes = 0;
+    TEST_ESP_OK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &io_handles[0]));
+    io_config.flags.reverse_color_bits = 0;
+    io_config.flags.swap_color_bytes = 1;
+    TEST_ESP_OK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &io_handles[1]));
+    io_config.flags.reverse_color_bits = 1;
+    io_config.flags.swap_color_bytes = 0;
+    TEST_ESP_OK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &io_handles[2]));
+    io_config.flags.reverse_color_bits = 1;
+    io_config.flags.swap_color_bytes = 1;
+    TEST_ESP_OK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &io_handles[3]));
+
+    for (int i = 0; i < 4; i++) {
+        esp_lcd_panel_io_tx_param(io_handles[i], 0xA5, 8, (uint8_t[]) {
+            0x01, 0x02, 0x03, 0x04, 0x05, 0x06
+        }, 6);
+        esp_lcd_panel_io_tx_color(io_handles[i], 0x5A, 8, (uint8_t[]) {
+            0x01, 0x02, 0x03, 0x04, 0x05, 0x06
+        }, 6);
+        TEST_ESP_OK(esp_lcd_panel_io_del(io_handles[i]));
+    }
+
+    TEST_ESP_OK(esp_lcd_del_i80_bus(i80_bus));
+}
+
+TEST_CASE("lcd i80 device clock mode", "[lcd]")
+{
+    esp_lcd_i80_bus_handle_t i80_bus = NULL;
+    esp_lcd_i80_bus_config_t bus_config = {
+        .dc_gpio_num = TEST_LCD_DC_GPIO,
+        .wr_gpio_num = TEST_LCD_PCLK_GPIO,
+        .data_gpio_nums = {
+            TEST_LCD_DATA0_GPIO,
+            TEST_LCD_DATA1_GPIO,
+            TEST_LCD_DATA2_GPIO,
+            TEST_LCD_DATA3_GPIO,
+            TEST_LCD_DATA4_GPIO,
+            TEST_LCD_DATA5_GPIO,
+            TEST_LCD_DATA6_GPIO,
+            TEST_LCD_DATA7_GPIO,
+        },
+        .data_width = 8,
+        .max_transfer_bytes = 20,
+    };
+    TEST_ESP_OK(esp_lcd_new_i80_bus(&bus_config, &i80_bus));
+
+    esp_lcd_panel_io_handle_t io_handles[4] = {};
+    esp_lcd_panel_io_i80_config_t io_config = {
+        .cs_gpio_num = TEST_LCD_CS_GPIO,
+        .pclk_hz = 5000000,
+        .trans_queue_depth = 10,
+        .dc_levels = {
+            .dc_idle_level = 0,
+            .dc_cmd_level = 0,
+            .dc_dummy_level = 0,
+            .dc_data_level = 1,
+        },
+    };
+
+    io_config.flags.pclk_idle_low = 0;
+    io_config.flags.pclk_active_neg = 0;
+    TEST_ESP_OK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &io_handles[0]));
+    io_config.flags.pclk_idle_low = 0;
+    io_config.flags.pclk_active_neg = 1;
+    TEST_ESP_OK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &io_handles[1]));
+    io_config.flags.pclk_idle_low = 1;
+    io_config.flags.pclk_active_neg = 0;
+    TEST_ESP_OK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &io_handles[2]));
+    io_config.flags.pclk_idle_low = 1;
+    io_config.flags.pclk_active_neg = 1;
+    TEST_ESP_OK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &io_handles[3]));
+
+    for (int i = 0; i < 4; i++) {
+        esp_lcd_panel_io_tx_param(io_handles[i], 0xA5, 8, (uint8_t[]) {
+            0x01, 0x02, 0x03, 0x04, 0x05, 0x06
+        }, 6);
+        TEST_ESP_OK(esp_lcd_panel_io_del(io_handles[i]));
+    }
+    TEST_ESP_OK(esp_lcd_del_i80_bus(i80_bus));
+}
+
+TEST_CASE("lcd panel with i80 interface (st7789)", "[lcd]")
+{
+#define TEST_IMG_SIZE (100 * 100 * sizeof(uint16_t))
+    uint8_t *img = heap_caps_malloc(TEST_IMG_SIZE, MALLOC_CAP_DMA);
+    TEST_ASSERT_NOT_NULL(img);
+
+    gpio_config_t bk_gpio_config = {
+        .mode = GPIO_MODE_OUTPUT,
+        .pin_bit_mask = 1ULL << TEST_LCD_BK_LIGHT_GPIO
+    };
+    TEST_ESP_OK(gpio_config(&bk_gpio_config));
+
+    esp_lcd_i80_bus_handle_t i80_bus = NULL;
+    esp_lcd_i80_bus_config_t bus_config = {
+        .dc_gpio_num = TEST_LCD_DC_GPIO,
+        .wr_gpio_num = TEST_LCD_PCLK_GPIO,
+        .data_gpio_nums = {
+            TEST_LCD_DATA0_GPIO,
+            TEST_LCD_DATA1_GPIO,
+            TEST_LCD_DATA2_GPIO,
+            TEST_LCD_DATA3_GPIO,
+            TEST_LCD_DATA4_GPIO,
+            TEST_LCD_DATA5_GPIO,
+            TEST_LCD_DATA6_GPIO,
+            TEST_LCD_DATA7_GPIO,
+        },
+        .data_width = 8,
+        .max_transfer_bytes = TEST_IMG_SIZE + 10,
+    };
+    TEST_ESP_OK(esp_lcd_new_i80_bus(&bus_config, &i80_bus));
+    esp_lcd_panel_io_handle_t io_handle = NULL;
+    esp_lcd_panel_io_i80_config_t io_config = {
+        .cs_gpio_num = TEST_LCD_CS_GPIO,
+        .pclk_hz = 5000000,
+        .trans_queue_depth = 10,
+        .dc_levels = {
+            .dc_idle_level = 0,
+            .dc_cmd_level = 0,
+            .dc_dummy_level = 0,
+            .dc_data_level = 1,
+        },
+    };
+    TEST_ESP_OK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &io_handle));
+
+    esp_lcd_panel_handle_t panel_handle = NULL;
+    esp_lcd_panel_dev_config_t panel_config = {
+        .reset_gpio_num = TEST_LCD_RST_GPIO,
+        .color_space = ESP_LCD_COLOR_SPACE_RGB,
+        .bits_per_pixel = 16,
+    };
+    TEST_ESP_OK(esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle));
+
+    // turn off backlight
+    gpio_set_level(TEST_LCD_BK_LIGHT_GPIO, 0);
+    esp_lcd_panel_reset(panel_handle);
+    esp_lcd_panel_init(panel_handle);
+    esp_lcd_panel_invert_color(panel_handle, true);
+    // the gap is LCD panel specific, even panels with the same driver IC, can have different gap value
+    esp_lcd_panel_set_gap(panel_handle, 0, 20);
+    // turn on backlight
+    gpio_set_level(TEST_LCD_BK_LIGHT_GPIO, 1);
+
+    for (int i = 0; i < 200; i++) {
+        uint8_t color_byte = esp_random() & 0xFF;
+        int x_start = esp_random() % (TEST_LCD_H_RES - 100);
+        int y_start = esp_random() % (TEST_LCD_V_RES - 100);
+        memset(img, color_byte, TEST_IMG_SIZE);
+        esp_lcd_panel_draw_bitmap(panel_handle, x_start, y_start, x_start + 100, y_start + 100, img);
+    }
+    esp_lcd_panel_disp_off(panel_handle, true); // turn off screen
+
+    TEST_ESP_OK(esp_lcd_panel_del(panel_handle));
+    TEST_ESP_OK(esp_lcd_panel_io_del(io_handle));
+    TEST_ESP_OK(esp_lcd_del_i80_bus(i80_bus));
+    TEST_ESP_OK(gpio_reset_pin(TEST_LCD_BK_LIGHT_GPIO));
+    free(img);
+#undef TEST_IMG_SIZE
+}
+
+// The following test shows a porting example of LVGL GUI library
+// To run the LVGL tests, you need to clone the LVGL library into components directory firstly
+#if CONFIG_LV_USE_USER_DATA
+#include "test_lvgl_port.h"
+
+static bool notify_lvgl_ready_to_flush(esp_lcd_panel_io_handle_t panel_io, void *user_data, void *event_data)
+{
+    lv_disp_t *disp = *(lv_disp_t **)user_data;
+    lv_disp_flush_ready(&disp->driver);
+    return false;
+}
+
+TEST_CASE("lvgl gui with i80 interface (st7789)", "[lcd][lvgl][ignore]")
+{
+    // initialize LVGL graphics library
+    lv_disp_t *disp = NULL;
+    lv_init();
+
+    gpio_config_t bk_gpio_config = {
+        .mode = GPIO_MODE_OUTPUT,
+        .pin_bit_mask = 1ULL << TEST_LCD_BK_LIGHT_GPIO
+    };
+    TEST_ESP_OK(gpio_config(&bk_gpio_config));
+
+    esp_lcd_i80_bus_handle_t i80_bus = NULL;
+    esp_lcd_i80_bus_config_t bus_config = {
+        .dc_gpio_num = TEST_LCD_DC_GPIO,
+        .wr_gpio_num = TEST_LCD_PCLK_GPIO,
+        .data_gpio_nums = {
+            TEST_LCD_DATA0_GPIO,
+            TEST_LCD_DATA1_GPIO,
+            TEST_LCD_DATA2_GPIO,
+            TEST_LCD_DATA3_GPIO,
+            TEST_LCD_DATA4_GPIO,
+            TEST_LCD_DATA5_GPIO,
+            TEST_LCD_DATA6_GPIO,
+            TEST_LCD_DATA7_GPIO,
+        },
+        .data_width = 8,
+        .max_transfer_bytes = TEST_LCD_H_RES * 40 * sizeof(uint16_t)
+    };
+    TEST_ESP_OK(esp_lcd_new_i80_bus(&bus_config, &i80_bus));
+    esp_lcd_panel_io_handle_t io_handle = NULL;
+    esp_lcd_panel_io_i80_config_t io_config = {
+        .cs_gpio_num = TEST_LCD_CS_GPIO,
+        .pclk_hz = 8000000,
+        .trans_queue_depth = 10,
+        .dc_levels = {
+            .dc_idle_level = 0,
+            .dc_cmd_level = 0,
+            .dc_dummy_level = 0,
+            .dc_data_level = 1,
+        },
+        .flags = {
+            .swap_color_bytes = 1,
+        },
+        .on_color_trans_done = notify_lvgl_ready_to_flush,
+        .user_data = &disp
+    };
+    TEST_ESP_OK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &io_handle));
+
+    esp_lcd_panel_handle_t panel_handle = NULL;
+    esp_lcd_panel_dev_config_t panel_config = {
+        .reset_gpio_num = TEST_LCD_RST_GPIO,
+        .color_space = ESP_LCD_COLOR_SPACE_RGB,
+        .bits_per_pixel = 16,
+    };
+    TEST_ESP_OK(esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle));
+
+    // turn off backlight
+    gpio_set_level(TEST_LCD_BK_LIGHT_GPIO, 0);
+    esp_lcd_panel_reset(panel_handle);
+    esp_lcd_panel_init(panel_handle);
+    esp_lcd_panel_invert_color(panel_handle, true);
+    // the gap is LCD panel specific, even panels with the same driver IC, can have different gap value
+    esp_lcd_panel_set_gap(panel_handle, 0, 20);
+    // turn on backlight
+    gpio_set_level(TEST_LCD_BK_LIGHT_GPIO, 1);
+
+    test_lvgl_task_loop(panel_handle, TEST_LCD_H_RES, TEST_LCD_V_RES, &disp);
+}
+#endif // CONFIG_LV_USE_USER_DATA
+#endif // SOC_LCD_I80_SUPPORTED

+ 113 - 0
components/esp_lcd/test/test_lvgl_port.c

@@ -0,0 +1,113 @@
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "unity.h"
+#include "test_utils.h"
+#include "esp_freertos_hooks.h"
+#include "soc/soc_caps.h"
+#if CONFIG_LV_USE_USER_DATA
+#include "test_lvgl_port.h"
+
+static void my_lvgl_flush(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map)
+{
+    esp_lcd_panel_handle_t panel_handle = (esp_lcd_panel_handle_t) drv->user_data;
+
+    int offsetx1 = area->x1;
+    int offsetx2 = area->x2;
+    int offsety1 = area->y1;
+    int offsety2 = area->y2;
+
+    esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, color_map);
+}
+
+#if CONFIG_LV_COLOR_DEPTH_1
+static void my_lvgl_set_px_cb(lv_disp_drv_t *disp_drv, uint8_t *buf, lv_coord_t buf_w, lv_coord_t x, lv_coord_t y,
+                              lv_color_t color, lv_opa_t opa)
+{
+    uint16_t byte_index = x + (( y >> 3 ) * buf_w);
+    uint8_t  bit_index  = y & 0x7;
+
+    if ((color.full == 0) && (LV_OPA_TRANSP != opa)) {
+        buf[byte_index] |= (1 << bit_index);
+    } else {
+        buf[byte_index] &= ~(1 << bit_index);
+    }
+}
+
+static void my_lvgl_rounder(lv_disp_drv_t *disp_drv, lv_area_t *area)
+{
+    area->y1 = (area->y1 & (~0x7));
+    area->y2 = (area->y2 & (~0x7)) + 7;
+}
+#endif
+
+static void increase_lvgl_tick(void)
+{
+    lv_tick_inc(portTICK_PERIOD_MS);
+}
+
+static void create_demo_application(lv_disp_t *disp)
+{
+    // Get the current screen
+    lv_obj_t *scr = lv_disp_get_scr_act(disp);
+    // Create a Label on the currently active screen
+    lv_obj_t *label =  lv_label_create(scr, NULL);
+    // Modify the Label's text
+    lv_label_set_text(label, "Hello World");
+    // Align the Label to the center
+    lv_obj_align(label, NULL, LV_ALIGN_IN_TOP_MID, 0, 0);
+
+#if !CONFIG_LV_COLOR_DEPTH_1
+    // new screen_spinner
+    lv_obj_t *screen_spinner = lv_spinner_create(scr, NULL);
+    lv_obj_align(screen_spinner, label, LV_ALIGN_OUT_BOTTOM_MID, 15, 20);
+    lv_obj_set_size(screen_spinner, 100, 100);
+    lv_spinner_set_arc_length(screen_spinner, 60);
+    lv_spinner_set_spin_time(screen_spinner, 1000);
+    lv_spinner_set_type(screen_spinner, LV_SPINNER_TYPE_SPINNING_ARC);
+    lv_spinner_set_dir(screen_spinner, LV_SPINNER_DIR_FORWARD);
+
+    lv_obj_t *bar = lv_bar_create(scr, NULL);
+    lv_obj_set_size(bar, 100, 20);
+    lv_obj_align(bar, screen_spinner, LV_ALIGN_OUT_BOTTOM_MID, 0, 0);
+    lv_bar_set_anim_time(bar, 2000);
+    lv_bar_set_value(bar, 100, LV_ANIM_ON);
+#endif
+}
+
+void test_lvgl_task_loop(esp_lcd_panel_handle_t panel_handle, int h_res, int v_res, lv_disp_t **disp)
+{
+    static lv_disp_buf_t disp_buf;
+    // alloc frame buffer used by LVGL
+    lv_color_t *buf1 = heap_caps_malloc(h_res * 20 * sizeof(lv_color_t), MALLOC_CAP_DMA);
+    TEST_ASSERT_NOT_NULL(buf1);
+    lv_color_t *buf2 = heap_caps_malloc(h_res * 20 * sizeof(lv_color_t), MALLOC_CAP_DMA);
+    TEST_ASSERT_NOT_NULL(buf2);
+    lv_disp_buf_init(&disp_buf, buf1, buf2, h_res * 20);
+    // register display driver
+    lv_disp_drv_t disp_drv;
+    lv_disp_drv_init(&disp_drv);
+    disp_drv.hor_res = h_res;
+    disp_drv.ver_res = v_res;
+    disp_drv.flush_cb = my_lvgl_flush;
+#if CONFIG_LV_COLOR_DEPTH_1
+    disp_drv.rounder_cb = my_lvgl_rounder;
+    disp_drv.set_px_cb = my_lvgl_set_px_cb;
+#endif
+
+    disp_drv.buffer = &disp_buf;
+    disp_drv.user_data = panel_handle; // LV_USE_USER_DATA is disabled by default, need to enable it in menuconfig
+    *disp = lv_disp_drv_register(&disp_drv);
+
+    // Tick interface for LVGL
+    esp_register_freertos_tick_hook(increase_lvgl_tick);
+
+    // create a demo UI on that screen
+    create_demo_application(*disp);
+
+    while (1) {
+        vTaskDelay(pdMS_TO_TICKS(10));
+        lv_task_handler(); // The task running lv_task_handler should have lower priority than that running `lv_tick_inc`
+    }
+}
+
+#endif // CONFIG_LV_USE_USER_DATA

+ 4 - 0
components/esp_lcd/test/test_lvgl_port.h

@@ -0,0 +1,4 @@
+#include "esp_lcd_panel_ops.h"
+#include "lvgl.h"
+
+void test_lvgl_task_loop(esp_lcd_panel_handle_t panel_handle, int h_res, int v_res, lv_disp_t **disp);

+ 164 - 0
components/esp_lcd/test/test_rgb_panel.c

@@ -0,0 +1,164 @@
+#include <stdio.h>
+#include <string.h>
+#include "unity.h"
+#include "test_utils.h"
+#include "esp_lcd_panel_rgb.h"
+#include "esp_lcd_panel_ops.h"
+#include "soc/soc_caps.h"
+
+#define TEST_LCD_H_RES         (480)
+#define TEST_LCD_V_RES         (272)
+#define TEST_LCD_VSYNC_GPIO    (19)
+#define TEST_LCD_HSYNC_GPIO    (18)
+#define TEST_LCD_DE_GPIO       (-1)
+#define TEST_LCD_PCLK_GPIO     (17)
+#define TEST_LCD_DATA0_GPIO    (42) // B0
+#define TEST_LCD_DATA1_GPIO    (41) // B1
+#define TEST_LCD_DATA2_GPIO    (40) // B2
+#define TEST_LCD_DATA3_GPIO    (39) // B3
+#define TEST_LCD_DATA4_GPIO    (38) // B4
+#define TEST_LCD_DATA5_GPIO    (4)  // G0
+#define TEST_LCD_DATA6_GPIO    (5)  // G1
+#define TEST_LCD_DATA7_GPIO    (6)  // G2
+#define TEST_LCD_DATA8_GPIO    (7)  // G3
+#define TEST_LCD_DATA9_GPIO    (15) // G4
+#define TEST_LCD_DATA10_GPIO   (16) // G5
+#define TEST_LCD_DATA11_GPIO   (37) // R0
+#define TEST_LCD_DATA12_GPIO   (36) // R1
+#define TEST_LCD_DATA13_GPIO   (35) // R2
+#define TEST_LCD_DATA14_GPIO   (34) // R3
+#define TEST_LCD_DATA15_GPIO   (33) // R4
+#define TEST_LCD_DISP_EN_GPIO  (-1)
+
+#if SOC_LCD_RGB_SUPPORTED
+TEST_CASE("lcd rgb lcd panel", "[lcd]")
+{
+#define TEST_IMG_SIZE (100 * 100 * sizeof(uint16_t))
+    uint8_t *img = malloc(TEST_IMG_SIZE);
+    TEST_ASSERT_NOT_NULL(img);
+
+    esp_lcd_panel_handle_t panel_handle = NULL;
+    esp_lcd_rgb_panel_config_t panel_config = {
+        .data_width = 16,
+        .disp_gpio_num = TEST_LCD_DISP_EN_GPIO,
+        .pclk_gpio_num = TEST_LCD_PCLK_GPIO,
+        .vsync_gpio_num = TEST_LCD_VSYNC_GPIO,
+        .hsync_gpio_num = TEST_LCD_HSYNC_GPIO,
+        .de_gpio_num = TEST_LCD_DE_GPIO,
+        .data_gpio_nums = {
+            TEST_LCD_DATA0_GPIO,
+            TEST_LCD_DATA1_GPIO,
+            TEST_LCD_DATA2_GPIO,
+            TEST_LCD_DATA3_GPIO,
+            TEST_LCD_DATA4_GPIO,
+            TEST_LCD_DATA5_GPIO,
+            TEST_LCD_DATA6_GPIO,
+            TEST_LCD_DATA7_GPIO,
+            TEST_LCD_DATA8_GPIO,
+            TEST_LCD_DATA9_GPIO,
+            TEST_LCD_DATA10_GPIO,
+            TEST_LCD_DATA11_GPIO,
+            TEST_LCD_DATA12_GPIO,
+            TEST_LCD_DATA13_GPIO,
+            TEST_LCD_DATA14_GPIO,
+            TEST_LCD_DATA15_GPIO,
+        },
+        .timings = {
+            .pclk_hz = 20000000,
+            .h_res = TEST_LCD_H_RES,
+            .v_res = TEST_LCD_V_RES,
+            .hsync_back_porch = 43,
+            .hsync_front_porch = 2,
+            .hsync_pulse_width = 1,
+            .vsync_back_porch = 12,
+            .vsync_front_porch = 1,
+            .vsync_pulse_width = 1,
+        },
+    };
+    // Test stream mode and one-off mode
+    for (int i = 0; i < 2; i++) {
+        panel_config.flags.relax_on_idle = i;
+        TEST_ESP_OK(esp_lcd_new_rgb_panel(&panel_config, &panel_handle));
+        TEST_ESP_OK(esp_lcd_panel_reset(panel_handle));
+        TEST_ESP_OK(esp_lcd_panel_init(panel_handle));
+
+        for (int i = 0; i < 200; i++) {
+            uint8_t color_byte = esp_random() & 0xFF;
+            int x_start = esp_random() % (TEST_LCD_H_RES - 100);
+            int y_start = esp_random() % (TEST_LCD_V_RES - 100);
+            memset(img, color_byte, TEST_IMG_SIZE);
+            esp_lcd_panel_draw_bitmap(panel_handle, x_start, y_start, x_start + 100, y_start + 100, img);
+        }
+
+        TEST_ESP_OK(esp_lcd_panel_del(panel_handle));
+    }
+    free(img);
+#undef TEST_IMG_SIZE
+}
+
+// The following test shows a porting example of LVGL GUI library
+// To run the LVGL tests, you need to clone the LVGL library into components directory firstly
+#if CONFIG_LV_USE_USER_DATA
+#include "test_lvgl_port.h"
+
+static bool notify_lvgl_ready_to_flush(esp_lcd_panel_handle_t panel, void *user_data)
+{
+    lv_disp_t *disp = *(lv_disp_t **)user_data;
+    lv_disp_flush_ready(&disp->driver);
+    return false;
+}
+
+TEST_CASE("lvgl gui with rgb interface", "[lcd][lvgl][ignore]")
+{
+    // initialize LVGL graphics library
+    lv_disp_t *disp = NULL;
+    lv_init();
+
+    esp_lcd_panel_handle_t panel_handle = NULL;
+    esp_lcd_rgb_panel_config_t panel_config = {
+        .data_width = 16,
+        .disp_gpio_num = -1,
+        .pclk_gpio_num = TEST_LCD_PCLK_GPIO,
+        .vsync_gpio_num = TEST_LCD_VSYNC_GPIO,
+        .hsync_gpio_num = TEST_LCD_HSYNC_GPIO,
+        .de_gpio_num = TEST_LCD_DE_GPIO,
+        .data_gpio_nums = {
+            TEST_LCD_DATA0_GPIO,
+            TEST_LCD_DATA1_GPIO,
+            TEST_LCD_DATA2_GPIO,
+            TEST_LCD_DATA3_GPIO,
+            TEST_LCD_DATA4_GPIO,
+            TEST_LCD_DATA5_GPIO,
+            TEST_LCD_DATA6_GPIO,
+            TEST_LCD_DATA7_GPIO,
+            TEST_LCD_DATA8_GPIO,
+            TEST_LCD_DATA9_GPIO,
+            TEST_LCD_DATA10_GPIO,
+            TEST_LCD_DATA11_GPIO,
+            TEST_LCD_DATA12_GPIO,
+            TEST_LCD_DATA13_GPIO,
+            TEST_LCD_DATA14_GPIO,
+            TEST_LCD_DATA15_GPIO,
+        },
+        .timings = {
+            .pclk_hz = 20000000,
+            .h_res = TEST_LCD_H_RES,
+            .v_res = TEST_LCD_V_RES,
+            .hsync_back_porch = 43,
+            .hsync_front_porch = 2,
+            .hsync_pulse_width = 1,
+            .vsync_back_porch = 12,
+            .vsync_front_porch = 1,
+            .vsync_pulse_width = 1,
+        },
+        .on_frame_trans_done = notify_lvgl_ready_to_flush,
+        .user_data = &disp,
+    };
+    TEST_ESP_OK(esp_lcd_new_rgb_panel(&panel_config, &panel_handle));
+    TEST_ESP_OK(esp_lcd_panel_reset(panel_handle));
+    TEST_ESP_OK(esp_lcd_panel_init(panel_handle));
+
+    test_lvgl_task_loop(panel_handle, TEST_LCD_H_RES, TEST_LCD_V_RES, &disp);
+}
+#endif // CONFIG_LV_USE_USER_DATA
+#endif // SOC_LCD_RGB_SUPPORTED

+ 157 - 0
components/esp_lcd/test/test_spi_lcd_panel.c

@@ -0,0 +1,157 @@
+#include <stdio.h>
+#include <string.h>
+#include "sdkconfig.h"
+#include "unity.h"
+#include "test_utils.h"
+#include "driver/spi_master.h"
+#include "driver/gpio.h"
+#include "esp_lcd_panel_io.h"
+#include "esp_lcd_panel_vendor.h"
+#include "esp_lcd_panel_ops.h"
+#include "esp_system.h"
+
+#define TEST_LCD_H_RES          (240)
+#define TEST_LCD_V_RES          (280)
+#define TEST_SPI_CLK_GPIO       (2)
+#define TEST_SPI_MOSI_GPIO      (4)
+#define TEST_LCD_RST_GPIO       (5)
+#define TEST_LCD_DC_GPIO        (18)
+#define TEST_LCD_BK_LIGHT_GPIO  (19)
+#define TEST_SPI_CS_GPIO        (0)
+#define TEST_SPI_HOST_ID        (1)
+#define TEST_LCD_PIXEL_CLOCK_HZ (20 * 1000 * 1000)
+
+TEST_CASE("lcd panel with spi interface (st7789)", "[lcd]")
+{
+#define TEST_IMG_SIZE (100 * 100 * sizeof(uint16_t))
+    uint8_t *img = heap_caps_malloc(TEST_IMG_SIZE, MALLOC_CAP_DMA);
+    TEST_ASSERT_NOT_NULL(img);
+
+    gpio_config_t bk_gpio_config = {
+        .mode = GPIO_MODE_OUTPUT,
+        .pin_bit_mask = 1ULL << TEST_LCD_BK_LIGHT_GPIO
+    };
+    TEST_ESP_OK(gpio_config(&bk_gpio_config));
+
+    spi_bus_config_t buscfg = {
+        .miso_io_num = -1,
+        .mosi_io_num = TEST_SPI_MOSI_GPIO,
+        .sclk_io_num = TEST_SPI_CLK_GPIO,
+        .quadwp_io_num = -1,
+        .quadhd_io_num = -1,
+        .max_transfer_sz = TEST_LCD_H_RES * TEST_LCD_V_RES * sizeof(uint16_t)
+    };
+    TEST_ESP_OK(spi_bus_initialize(TEST_SPI_HOST_ID, &buscfg, SPI_DMA_CH_AUTO));
+
+    esp_lcd_panel_io_handle_t io_handle = NULL;
+    esp_lcd_panel_io_spi_config_t io_config = {
+        .dc_gpio_num = TEST_LCD_DC_GPIO,
+        .cs_gpio_num = TEST_SPI_CS_GPIO,
+        .pclk_hz = TEST_LCD_PIXEL_CLOCK_HZ,
+        .spi_mode = 0,
+        .trans_queue_depth = 10,
+    };
+    TEST_ESP_OK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)TEST_SPI_HOST_ID, &io_config, &io_handle));
+
+    esp_lcd_panel_handle_t panel_handle = NULL;
+    esp_lcd_panel_dev_config_t panel_config = {
+        .reset_gpio_num = TEST_LCD_RST_GPIO,
+        .color_space = ESP_LCD_COLOR_SPACE_RGB,
+        .bits_per_pixel = 16,
+    };
+    TEST_ESP_OK(esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle));
+
+    // turn off backlight
+    gpio_set_level(TEST_LCD_BK_LIGHT_GPIO, 0);
+    esp_lcd_panel_reset(panel_handle);
+    esp_lcd_panel_init(panel_handle);
+    esp_lcd_panel_invert_color(panel_handle, true);
+    // the gap is LCD panel specific, even panels with the same driver IC, can have different gap value
+    esp_lcd_panel_set_gap(panel_handle, 0, 20);
+    // turn on backlight
+    gpio_set_level(TEST_LCD_BK_LIGHT_GPIO, 1);
+
+    for (int i = 0; i < 200; i++) {
+        uint8_t color_byte = esp_random() & 0xFF;
+        int x_start = esp_random() % (TEST_LCD_H_RES - 100);
+        int y_start = esp_random() % (TEST_LCD_V_RES - 100);
+        memset(img, color_byte, TEST_IMG_SIZE);
+        esp_lcd_panel_draw_bitmap(panel_handle, x_start, y_start, x_start + 100, y_start + 100, img);
+    }
+    esp_lcd_panel_disp_off(panel_handle, true); // turn off screen
+    TEST_ESP_OK(esp_lcd_panel_del(panel_handle));
+    TEST_ESP_OK(esp_lcd_panel_io_del(io_handle));
+    TEST_ESP_OK(spi_bus_free(TEST_SPI_HOST_ID));
+    TEST_ESP_OK(gpio_reset_pin(TEST_LCD_BK_LIGHT_GPIO));
+    free(img);
+#undef TEST_IMG_SIZE
+}
+
+// The following test shows a porting example of LVGL GUI library
+// To run the LVGL tests, you need to clone the LVGL library into components directory firstly
+#if CONFIG_LV_USE_USER_DATA
+#include "test_lvgl_port.h"
+
+static bool notify_lvgl_ready_to_flush(esp_lcd_panel_io_handle_t panel_io, void *user_data, void *event_data)
+{
+    lv_disp_t *disp = *(lv_disp_t **)user_data;
+    lv_disp_flush_ready(&disp->driver);
+    return false;
+}
+
+TEST_CASE("lvgl gui with spi interface (st7789)", "[lcd][lvgl][ignore]")
+{
+    // initialize LVGL graphics library
+    lv_disp_t *disp = NULL;
+    lv_init();
+
+    gpio_config_t bk_gpio_config = {
+        .mode = GPIO_MODE_OUTPUT,
+        .pin_bit_mask = 1ULL << TEST_LCD_BK_LIGHT_GPIO
+    };
+    TEST_ESP_OK(gpio_config(&bk_gpio_config));
+
+    spi_bus_config_t buscfg = {
+        .miso_io_num = -1,
+        .mosi_io_num = TEST_SPI_MOSI_GPIO,
+        .sclk_io_num = TEST_SPI_CLK_GPIO,
+        .quadwp_io_num = -1,
+        .quadhd_io_num = -1,
+        .max_transfer_sz = TEST_LCD_H_RES * TEST_LCD_V_RES * 2
+    };
+    TEST_ESP_OK(spi_bus_initialize(TEST_SPI_HOST_ID, &buscfg, 1));
+
+    esp_lcd_panel_io_handle_t io_handle = NULL;
+    esp_lcd_panel_io_spi_config_t io_config = {
+        .dc_gpio_num = TEST_LCD_DC_GPIO,
+        .cs_gpio_num = TEST_SPI_CS_GPIO,
+        .pclk_hz = TEST_LCD_PIXEL_CLOCK_HZ,
+        .spi_mode = 0,
+        .trans_queue_depth = 10,
+        .on_color_trans_done = notify_lvgl_ready_to_flush,
+        .user_data = &disp // we must use "address of disp" here, since the disp object has not been allocated
+    };
+    TEST_ESP_OK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)TEST_SPI_HOST_ID, &io_config, &io_handle));
+
+    esp_lcd_panel_handle_t panel_handle = NULL;
+    esp_lcd_panel_dev_config_t panel_config = {
+        .reset_gpio_num = TEST_LCD_RST_GPIO,
+        .color_space = ESP_LCD_COLOR_SPACE_RGB,
+        .bits_per_pixel = 16,
+    };
+    TEST_ESP_OK(esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle));
+
+    // turn off backlight
+    gpio_set_level(TEST_LCD_BK_LIGHT_GPIO, 0);
+    esp_lcd_panel_reset(panel_handle);
+    esp_lcd_panel_init(panel_handle);
+    esp_lcd_panel_invert_color(panel_handle, true);
+    // the gap is LCD panel specific, even panels with the same driver IC, can have different gap value
+    esp_lcd_panel_set_gap(panel_handle, 0, 20);
+    // turn on backlight
+    gpio_set_level(TEST_LCD_BK_LIGHT_GPIO, 1);
+
+    test_lvgl_task_loop(panel_handle, TEST_LCD_H_RES, TEST_LCD_V_RES, &disp);
+}
+
+#endif // CONFIG_LV_USE_USER_DATA

+ 18 - 9
components/hal/esp32s3/include/hal/lcd_ll.h

@@ -28,7 +28,6 @@ extern "C" {
 // Interrupt event, bit mask
 #define LCD_LL_EVENT_VSYNC_END  (1 << 0)
 #define LCD_LL_EVENT_TRANS_DONE (1 << 1)
-#define LCD_LL_EVENT_MASK       (LCD_LL_EVENT_VSYNC_END | LCD_LL_EVENT_TRANS_DONE)
 
 // Clock source ID represented in register
 #define LCD_LL_CLOCK_SRC_XTAL    (1)
@@ -185,20 +184,30 @@ static inline void lcd_ll_enable_auto_next_frame(lcd_cam_dev_t *dev, bool en)
     dev->lcd_misc.lcd_next_frame_en = en;
 }
 
+static inline void lcd_ll_enable_output_hsync_in_porch_region(lcd_cam_dev_t *dev, bool en)
+{
+    dev->lcd_ctrl2.lcd_hs_blank_en = en;
+}
+
+static inline void lcd_ll_set_hsync_position(lcd_cam_dev_t *dev, uint32_t offset_in_line)
+{
+    dev->lcd_ctrl2.lcd_hsync_position = offset_in_line;
+}
+
 static inline void lcd_ll_set_horizontal_timing(lcd_cam_dev_t *dev, uint32_t hsw, uint32_t hbp, uint32_t active_width, uint32_t hfp)
 {
-    dev->lcd_ctrl2.lcd_hsync_width = hsw;
-    dev->lcd_ctrl.lcd_hb_front = hbp;
-    dev->lcd_ctrl1.lcd_ha_width = active_width;
-    dev->lcd_ctrl1.lcd_ht_width = hsw + hbp + active_width + hfp;
+    dev->lcd_ctrl2.lcd_hsync_width = hsw - 1;
+    dev->lcd_ctrl.lcd_hb_front = hbp + hsw - 1;
+    dev->lcd_ctrl1.lcd_ha_width = active_width - 1;
+    dev->lcd_ctrl1.lcd_ht_width = hsw + hbp + active_width + hfp - 1;
 }
 
 static inline void lcd_ll_set_vertical_timing(lcd_cam_dev_t *dev, uint32_t vsw, uint32_t vbp, uint32_t active_height, uint32_t vfp)
 {
-    dev->lcd_ctrl2.lcd_vsync_width = vsw;
-    dev->lcd_ctrl1.lcd_vb_front = vbp;
-    dev->lcd_ctrl.lcd_va_height = active_height;
-    dev->lcd_ctrl.lcd_vt_height = vsw + vbp + active_height + vfp;
+    dev->lcd_ctrl2.lcd_vsync_width = vsw - 1;
+    dev->lcd_ctrl1.lcd_vb_front = vbp + vsw - 1;
+    dev->lcd_ctrl.lcd_va_height = active_height - 1;
+    dev->lcd_ctrl.lcd_vt_height = vsw + vbp + active_height + vfp - 1;
 }
 
 static inline void lcd_ll_set_idle_level(lcd_cam_dev_t *dev, bool hsync_idle_level, bool vsync_idle_level, bool de_idle_level)

+ 6 - 2
components/soc/esp32s3/include/soc/soc_caps.h

@@ -9,7 +9,7 @@
 #define SOC_PCNT_SUPPORTED              1
 #define SOC_TWAI_SUPPORTED              1
 #define SOC_GDMA_SUPPORTED              1
-#define SOC_I80_LCD_SUPPORTED           1
+#define SOC_LCDCAM_SUPPORTED            1
 #define SOC_MCPWM_SUPPORTED             1
 #define SOC_DEDICATED_GPIO_SUPPORTED    1
 #define SOC_CPU_CORES_NUM               2
@@ -88,9 +88,13 @@
 
 
 /*-------------------------- LCD CAPS ----------------------------------------*/
+/* Notes: On esp32-s3, I80 bus and RGB timing generator can't work at the same time */
+#define SOC_LCD_I80_SUPPORTED           (1)  /*!< Intel 8080 LCD is supported */
+#define SOC_LCD_RGB_SUPPORTED           (1)  /*!< RGB LCD is supported */
 #define SOC_LCD_I80_BUSES               (1)  /*!< Has one LCD Intel 8080 bus */
 #define SOC_LCD_RGB_PANELS              (1)  /*!< Support one RGB LCD panel */
-#define SOC_LCD_MAX_DATA_WIDTH          (16) /*!< Maximum number of LCD data lines */
+#define SOC_LCD_I80_BUS_WIDTH           (16) /*!< Intel 8080 bus width */
+#define SOC_LCD_RGB_DATA_WIDTH          (16) /*!< Number of LCD data lines */
 
 /*-------------------------- RTCIO CAPS --------------------------------------*/
 #include "rtc_io_caps.h"

+ 2 - 2
components/soc/include/soc/lcd_periph.h

@@ -25,7 +25,7 @@ typedef struct {
     struct {
         const periph_module_t module;
         const int irq_id;
-        const int data_sigs[SOC_LCD_MAX_DATA_WIDTH];
+        const int data_sigs[SOC_LCD_I80_BUS_WIDTH];
         const int cs_sig;
         const int dc_sig;
         const int wr_sig;
@@ -33,7 +33,7 @@ typedef struct {
     struct {
         const periph_module_t module;
         const int irq_id;
-        const int data_sigs[SOC_LCD_MAX_DATA_WIDTH];
+        const int data_sigs[SOC_LCD_RGB_DATA_WIDTH];
         const int hsync_sig;
         const int vsync_sig;
         const int pclk_sig;