From e10202a6083df27ee4bb13fe604bedc5d958fb86 Mon Sep 17 00:00:00 2001 From: morris Date: Wed, 12 May 2021 11:26:07 +0800 Subject: [PATCH] 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 --- .gitlab/CODEOWNERS | 1 + components/esp_lcd/CMakeLists.txt | 14 + components/esp_lcd/Kconfig | 20 + components/esp_lcd/component.mk | 6 + components/esp_lcd/include/esp_lcd_panel_io.h | 201 +++++++ .../esp_lcd/include/esp_lcd_panel_ops.h | 126 +++++ .../esp_lcd/include/esp_lcd_panel_rgb.h | 77 +++ .../esp_lcd/include/esp_lcd_panel_vendor.h | 57 ++ components/esp_lcd/include/esp_lcd_types.h | 26 + .../interface/esp_lcd_panel_interface.h | 126 +++++ .../interface/esp_lcd_panel_io_interface.h | 66 +++ components/esp_lcd/src/esp_lcd_common.c | 126 +++++ components/esp_lcd/src/esp_lcd_common.h | 70 +++ .../esp_lcd/src/esp_lcd_panel_commands.h | 54 ++ components/esp_lcd/src/esp_lcd_panel_io.c | 29 + components/esp_lcd/src/esp_lcd_panel_io_i2c.c | 149 +++++ components/esp_lcd/src/esp_lcd_panel_io_i80.c | 518 ++++++++++++++++++ components/esp_lcd/src/esp_lcd_panel_io_spi.c | 230 ++++++++ components/esp_lcd/src/esp_lcd_panel_ops.c | 65 +++ .../esp_lcd/src/esp_lcd_panel_ssd1306.c | 235 ++++++++ components/esp_lcd/src/esp_lcd_panel_st7789.c | 263 +++++++++ components/esp_lcd/src/esp_lcd_rgb_panel.c | 443 +++++++++++++++ components/esp_lcd/test/CMakeLists.txt | 3 + components/esp_lcd/test/component.mk | 7 + components/esp_lcd/test/test_i2c_lcd_panel.c | 125 +++++ components/esp_lcd/test/test_i80_lcd_panel.c | 355 ++++++++++++ components/esp_lcd/test/test_lvgl_port.c | 113 ++++ components/esp_lcd/test/test_lvgl_port.h | 4 + components/esp_lcd/test/test_rgb_panel.c | 164 ++++++ components/esp_lcd/test/test_spi_lcd_panel.c | 157 ++++++ components/hal/esp32s3/include/hal/lcd_ll.h | 27 +- components/soc/esp32s3/include/soc/soc_caps.h | 8 +- components/soc/include/soc/lcd_periph.h | 4 +- 33 files changed, 3856 insertions(+), 13 deletions(-) create mode 100644 components/esp_lcd/CMakeLists.txt create mode 100644 components/esp_lcd/Kconfig create mode 100644 components/esp_lcd/component.mk create mode 100644 components/esp_lcd/include/esp_lcd_panel_io.h create mode 100644 components/esp_lcd/include/esp_lcd_panel_ops.h create mode 100644 components/esp_lcd/include/esp_lcd_panel_rgb.h create mode 100644 components/esp_lcd/include/esp_lcd_panel_vendor.h create mode 100644 components/esp_lcd/include/esp_lcd_types.h create mode 100644 components/esp_lcd/interface/esp_lcd_panel_interface.h create mode 100644 components/esp_lcd/interface/esp_lcd_panel_io_interface.h create mode 100644 components/esp_lcd/src/esp_lcd_common.c create mode 100644 components/esp_lcd/src/esp_lcd_common.h create mode 100644 components/esp_lcd/src/esp_lcd_panel_commands.h create mode 100644 components/esp_lcd/src/esp_lcd_panel_io.c create mode 100644 components/esp_lcd/src/esp_lcd_panel_io_i2c.c create mode 100644 components/esp_lcd/src/esp_lcd_panel_io_i80.c create mode 100644 components/esp_lcd/src/esp_lcd_panel_io_spi.c create mode 100644 components/esp_lcd/src/esp_lcd_panel_ops.c create mode 100644 components/esp_lcd/src/esp_lcd_panel_ssd1306.c create mode 100644 components/esp_lcd/src/esp_lcd_panel_st7789.c create mode 100644 components/esp_lcd/src/esp_lcd_rgb_panel.c create mode 100644 components/esp_lcd/test/CMakeLists.txt create mode 100644 components/esp_lcd/test/component.mk create mode 100644 components/esp_lcd/test/test_i2c_lcd_panel.c create mode 100644 components/esp_lcd/test/test_i80_lcd_panel.c create mode 100644 components/esp_lcd/test/test_lvgl_port.c create mode 100644 components/esp_lcd/test/test_lvgl_port.h create mode 100644 components/esp_lcd/test/test_rgb_panel.c create mode 100644 components/esp_lcd/test/test_spi_lcd_panel.c diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index f680964b8b..3bf42b510d 100644 --- a/.gitlab/CODEOWNERS +++ b/.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 diff --git a/components/esp_lcd/CMakeLists.txt b/components/esp_lcd/CMakeLists.txt new file mode 100644 index 0000000000..31dc623873 --- /dev/null +++ b/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}) diff --git a/components/esp_lcd/Kconfig b/components/esp_lcd/Kconfig new file mode 100644 index 0000000000..d7ca5dc1c2 --- /dev/null +++ b/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 diff --git a/components/esp_lcd/component.mk b/components/esp_lcd/component.mk new file mode 100644 index 0000000000..42de4c7e4e --- /dev/null +++ b/components/esp_lcd/component.mk @@ -0,0 +1,6 @@ +# +# Component Makefile +# +COMPONENT_ADD_INCLUDEDIRS := include +COMPONENT_PRIV_INCLUDEDIRS := interface +COMPONENT_SRCDIRS := src diff --git a/components/esp_lcd/include/esp_lcd_panel_io.h b/components/esp_lcd/include/esp_lcd_panel_io.h new file mode 100644 index 0000000000..c362180f53 --- /dev/null +++ b/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 +#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 diff --git a/components/esp_lcd/include/esp_lcd_panel_ops.h b/components/esp_lcd/include/esp_lcd_panel_ops.h new file mode 100644 index 0000000000..5099233ce8 --- /dev/null +++ b/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 +#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 diff --git a/components/esp_lcd/include/esp_lcd_panel_rgb.h b/components/esp_lcd/include/esp_lcd_panel_rgb.h new file mode 100644 index 0000000000..0847e84bd2 --- /dev/null +++ b/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 +#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 diff --git a/components/esp_lcd/include/esp_lcd_panel_vendor.h b/components/esp_lcd/include/esp_lcd_panel_vendor.h new file mode 100644 index 0000000000..ac98cc3913 --- /dev/null +++ b/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 +#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 diff --git a/components/esp_lcd/include/esp_lcd_types.h b/components/esp_lcd/include/esp_lcd_types.h new file mode 100644 index 0000000000..7d4953247f --- /dev/null +++ b/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 diff --git a/components/esp_lcd/interface/esp_lcd_panel_interface.h b/components/esp_lcd/interface/esp_lcd_panel_interface.h new file mode 100644 index 0000000000..0705b145a2 --- /dev/null +++ b/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 +#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 diff --git a/components/esp_lcd/interface/esp_lcd_panel_io_interface.h b/components/esp_lcd/interface/esp_lcd_panel_io_interface.h new file mode 100644 index 0000000000..b6b85564cd --- /dev/null +++ b/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 +#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 diff --git a/components/esp_lcd/src/esp_lcd_common.c b/components/esp_lcd/src/esp_lcd_common.c new file mode 100644 index 0000000000..5d2d59e460 --- /dev/null +++ b/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 diff --git a/components/esp_lcd/src/esp_lcd_common.h b/components/esp_lcd/src/esp_lcd_common.h new file mode 100644 index 0000000000..08cac613bd --- /dev/null +++ b/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 diff --git a/components/esp_lcd/src/esp_lcd_panel_commands.h b/components/esp_lcd/src/esp_lcd_panel_commands.h new file mode 100644 index 0000000000..c2ae3933b3 --- /dev/null +++ b/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 diff --git a/components/esp_lcd/src/esp_lcd_panel_io.c b/components/esp_lcd/src/esp_lcd_panel_io.c new file mode 100644 index 0000000000..4c80eaf8da --- /dev/null +++ b/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); +} diff --git a/components/esp_lcd/src/esp_lcd_panel_io_i2c.c b/components/esp_lcd/src/esp_lcd_panel_io_i2c.c new file mode 100644 index 0000000000..cdf7722491 --- /dev/null +++ b/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 +#include +#include +#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; +} diff --git a/components/esp_lcd/src/esp_lcd_panel_io_i80.c b/components/esp_lcd/src/esp_lcd_panel_io_i80.c new file mode 100644 index 0000000000..6154b7462e --- /dev/null +++ b/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 +#include +#include +#include +#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 diff --git a/components/esp_lcd/src/esp_lcd_panel_io_spi.c b/components/esp_lcd/src/esp_lcd_panel_io_spi.c new file mode 100644 index 0000000000..f8c2d2baf2 --- /dev/null +++ b/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 +#include +#include +#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); + } + } +} diff --git a/components/esp_lcd/src/esp_lcd_panel_ops.c b/components/esp_lcd/src/esp_lcd_panel_ops.c new file mode 100644 index 0000000000..aae00a82b3 --- /dev/null +++ b/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); +} diff --git a/components/esp_lcd/src/esp_lcd_panel_ssd1306.c b/components/esp_lcd/src/esp_lcd_panel_ssd1306.c new file mode 100644 index 0000000000..9481df82dd --- /dev/null +++ b/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 +#include +#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; +} diff --git a/components/esp_lcd/src/esp_lcd_panel_st7789.c b/components/esp_lcd/src/esp_lcd_panel_st7789.c new file mode 100644 index 0000000000..1bddafebfd --- /dev/null +++ b/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 +#include +#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; +} diff --git a/components/esp_lcd/src/esp_lcd_rgb_panel.c b/components/esp_lcd/src/esp_lcd_rgb_panel.c new file mode 100644 index 0000000000..5de3e92a2a --- /dev/null +++ b/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 +#include +#include +#include +#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 diff --git a/components/esp_lcd/test/CMakeLists.txt b/components/esp_lcd/test/CMakeLists.txt new file mode 100644 index 0000000000..f845f124fd --- /dev/null +++ b/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) diff --git a/components/esp_lcd/test/component.mk b/components/esp_lcd/test/component.mk new file mode 100644 index 0000000000..8b9586c0b3 --- /dev/null +++ b/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 diff --git a/components/esp_lcd/test/test_i2c_lcd_panel.c b/components/esp_lcd/test/test_i2c_lcd_panel.c new file mode 100644 index 0000000000..188d6797ac --- /dev/null +++ b/components/esp_lcd/test/test_i2c_lcd_panel.c @@ -0,0 +1,125 @@ +#include +#include +#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 diff --git a/components/esp_lcd/test/test_i80_lcd_panel.c b/components/esp_lcd/test/test_i80_lcd_panel.c new file mode 100644 index 0000000000..ea7d8d531f --- /dev/null +++ b/components/esp_lcd/test/test_i80_lcd_panel.c @@ -0,0 +1,355 @@ +#include +#include +#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 diff --git a/components/esp_lcd/test/test_lvgl_port.c b/components/esp_lcd/test/test_lvgl_port.c new file mode 100644 index 0000000000..a81f118c01 --- /dev/null +++ b/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 diff --git a/components/esp_lcd/test/test_lvgl_port.h b/components/esp_lcd/test/test_lvgl_port.h new file mode 100644 index 0000000000..872644b7e8 --- /dev/null +++ b/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); diff --git a/components/esp_lcd/test/test_rgb_panel.c b/components/esp_lcd/test/test_rgb_panel.c new file mode 100644 index 0000000000..f2f228cd88 --- /dev/null +++ b/components/esp_lcd/test/test_rgb_panel.c @@ -0,0 +1,164 @@ +#include +#include +#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 diff --git a/components/esp_lcd/test/test_spi_lcd_panel.c b/components/esp_lcd/test/test_spi_lcd_panel.c new file mode 100644 index 0000000000..1eaa67a63f --- /dev/null +++ b/components/esp_lcd/test/test_spi_lcd_panel.c @@ -0,0 +1,157 @@ +#include +#include +#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 diff --git a/components/hal/esp32s3/include/hal/lcd_ll.h b/components/hal/esp32s3/include/hal/lcd_ll.h index 6c6c3ab46a..3813875344 100644 --- a/components/hal/esp32s3/include/hal/lcd_ll.h +++ b/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) diff --git a/components/soc/esp32s3/include/soc/soc_caps.h b/components/soc/esp32s3/include/soc/soc_caps.h index 231057f879..52250af3c1 100644 --- a/components/soc/esp32s3/include/soc/soc_caps.h +++ b/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" diff --git a/components/soc/include/soc/lcd_periph.h b/components/soc/include/soc/lcd_periph.h index fd56a06f27..6b1071b316 100644 --- a/components/soc/include/soc/lcd_periph.h +++ b/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;