From a0c00b161dbdf47a007cf5709f05a74fa239e2f3 Mon Sep 17 00:00:00 2001 From: nick Date: Tue, 9 Jun 2026 20:57:32 -0600 Subject: [PATCH] dashboard: add waveshare lvgl baseline --- .../esp32-s3-dashboard/esp32-s3-dashboard.ino | 133 ++- .../esp_panel_board_custom_conf.h | 431 +++++++++ firmware/esp32-s3-dashboard/lvgl_v8_port.cpp | 859 ++++++++++++++++++ firmware/esp32-s3-dashboard/lvgl_v8_port.h | 174 ++++ 4 files changed, 1549 insertions(+), 48 deletions(-) create mode 100644 firmware/esp32-s3-dashboard/esp_panel_board_custom_conf.h create mode 100644 firmware/esp32-s3-dashboard/lvgl_v8_port.cpp create mode 100644 firmware/esp32-s3-dashboard/lvgl_v8_port.h diff --git a/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino b/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino index 848fe69..0efab77 100644 --- a/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino +++ b/firmware/esp32-s3-dashboard/esp32-s3-dashboard.ino @@ -1,58 +1,95 @@ -/* - Overland Controller - ESP32-S3 Dashboard Bring-Up +#include +#include - Target: - Waveshare ESP32-S3 Touch LCD 5B - 1024x600 +#include +#include "lvgl_v8_port.h" +#include - Purpose: - Verify build environment and establish - the first dashboard firmware entry point. +using namespace esp_panel::drivers; +using namespace esp_panel::board; - Future: - LVGL - Cargo ESP API integration - CAN - OBD-II -*/ +/** + * To use the built-in examples and demos of LVGL uncomment the includes below respectively. + */ + // #include + // #include -#include +void setup() +{ + String title = "LVGL porting example"; -const char* DASHBOARD_VERSION = "0.0.1-bringup"; - -unsigned long lastPrint = 0; - -void setup() { Serial.begin(115200); - delay(1000); - Serial.println(); - Serial.println("================================"); - Serial.println("Overland Controller Dashboard"); - Serial.println("================================"); - Serial.print("Version: "); - Serial.println(DASHBOARD_VERSION); - Serial.println("Target: Waveshare ESP32-S3 Touch LCD 5B"); - Serial.println("Display: 1024x600"); - Serial.println("API Base: /api/v1"); - Serial.println(); -} + Serial.println("Initializing board"); + Board *board = new Board(); + board->init(); -void loop() { - if (millis() - lastPrint > 5000) { - lastPrint = millis(); - - Serial.println("--- Dashboard Heartbeat ---"); - - if (WiFi.status() == WL_CONNECTED) { - Serial.print("WiFi Connected: "); - Serial.println(WiFi.localIP()); - } else { - Serial.println("WiFi Not Connected"); - } - - Serial.println("Cargo API: /api/v1"); - Serial.println(); + #if LVGL_PORT_AVOID_TEARING_MODE + auto lcd = board->getLCD(); + // When avoid tearing function is enabled, the frame buffer number should be set in the board driver + lcd->configFrameBufferNumber(LVGL_PORT_DISP_BUFFER_NUM); +#if ESP_PANEL_DRIVERS_BUS_ENABLE_RGB && CONFIG_IDF_TARGET_ESP32S3 + auto lcd_bus = lcd->getBus(); + /** + * As the anti-tearing feature typically consumes more PSRAM bandwidth, for the ESP32-S3, we need to utilize the + * "bounce buffer" functionality to enhance the RGB data bandwidth. + * This feature will consume `bounce_buffer_size * bytes_per_pixel * 2` of SRAM memory. + */ + if (lcd_bus->getBasicAttributes().type == ESP_PANEL_BUS_TYPE_RGB) { + static_cast(lcd_bus)->configRGB_BounceBufferSize(lcd->getFrameWidth() * 10); } +#endif +#endif + assert(board->begin()); + + Serial.println("Initializing LVGL"); + lvgl_port_init(board->getLCD(), board->getTouch()); + + Serial.println("Creating UI"); + /* Lock the mutex due to the LVGL APIs are not thread-safe */ + lvgl_port_lock(-1); + + /** + * Create the simple labels + */ + lv_obj_t *label_1 = lv_label_create(lv_scr_act()); + lv_label_set_text(label_1, "Hello World!"); + lv_obj_set_style_text_font(label_1, &lv_font_montserrat_30, 0); + lv_obj_align(label_1, LV_ALIGN_CENTER, 0, -20); + lv_obj_t *label_2 = lv_label_create(lv_scr_act()); + lv_label_set_text_fmt( + label_2, "ESP32_Display_Panel (%d.%d.%d)", + ESP_PANEL_VERSION_MAJOR, ESP_PANEL_VERSION_MINOR, ESP_PANEL_VERSION_PATCH + ); + lv_obj_set_style_text_font(label_2, &lv_font_montserrat_16, 0); + lv_obj_align_to(label_2, label_1, LV_ALIGN_OUT_BOTTOM_MID, 0, 10); + lv_obj_t *label_3 = lv_label_create(lv_scr_act()); + lv_label_set_text_fmt(label_3, "LVGL (%d.%d.%d)", LVGL_VERSION_MAJOR, LVGL_VERSION_MINOR, LVGL_VERSION_PATCH); + lv_obj_set_style_text_font(label_3, &lv_font_montserrat_16, 0); + lv_obj_align_to(label_3, label_2, LV_ALIGN_OUT_BOTTOM_MID, 0, 10); + + /** + * Try an example. Don't forget to uncomment header. + * See all the examples online: https://docs.lvgl.io/master/examples.html + * source codes: https://github.com/lvgl/lvgl/tree/e7f88efa5853128bf871dde335c0ca8da9eb7731/examples + */ + // lv_example_btn_1(); + + /** + * Or try out a demo. + * Don't forget to uncomment header and enable the demos in `lv_conf.h`. E.g. `LV_USE_DEMO_WIDGETS` + */ + lv_demo_widgets(); + // lv_demo_benchmark(); + // lv_demo_music(); + // lv_demo_stress(); + + /* Release the mutex */ + lvgl_port_unlock(); +} + +void loop() +{ + Serial.println("IDLE loop"); + delay(1000); } diff --git a/firmware/esp32-s3-dashboard/esp_panel_board_custom_conf.h b/firmware/esp32-s3-dashboard/esp_panel_board_custom_conf.h new file mode 100644 index 0000000..d4d2a87 --- /dev/null +++ b/firmware/esp32-s3-dashboard/esp_panel_board_custom_conf.h @@ -0,0 +1,431 @@ +/* + * SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * @file esp_panel_board_custom_conf.h + * @brief Configuration file for custom ESP development boards + * @author + * @link + * + * This file contains all the configurations needed for a custom board using ESP Panel. + * Users can modify these configurations according to their hardware design. + */ + +#pragma once + +#define ESP_PANEL_USE_1024_600_LCD (1) // 0: 800x480, 1: 1024x600 +#define ESP_OPEN_TOUCH 1 // 1 initiates the touch, 0 closes the touch. + +/** + * @brief Flag to enable custom board configuration (0/1) + * + * Set to `1` to enable custom board configuration, `0` to disable + */ +#define ESP_PANEL_BOARD_DEFAULT_USE_CUSTOM (1) + +#if ESP_PANEL_BOARD_DEFAULT_USE_CUSTOM +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////// Please update the following macros to configure general parameters /////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * @brief Board name (format: "Manufacturer:Model") + */ +#if ESP_PANEL_USE_1024_600_LCD + #define ESP_PANEL_BOARD_NAME "Waveshare:ESP32-S3-Touch-LCD-5B" +#else + #define ESP_PANEL_BOARD_NAME "Waveshare:ESP32-S3-Touch-LCD-5" +#endif + +/** + * @brief Panel resolution configuration in pixels + */ +#if ESP_PANEL_USE_1024_600_LCD + /* LCD resolution in pixels */ + #define ESP_PANEL_BOARD_WIDTH (1024) + #define ESP_PANEL_BOARD_HEIGHT (600) +#else + /* LCD resolution in pixels */ + #define ESP_PANEL_BOARD_WIDTH (800) + #define ESP_PANEL_BOARD_HEIGHT (480) +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////// Please update the following macros to configure the LCD panel ///////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * @brief LCD panel configuration flag (0/1) + * + * Set to `1` to enable LCD panel support, `0` to disable + */ +#define ESP_PANEL_BOARD_USE_LCD (1) + +#if ESP_PANEL_BOARD_USE_LCD +/** + * @brief LCD controller selection + */ +#define ESP_PANEL_BOARD_LCD_CONTROLLER ST7262 + +/** + * @brief LCD bus type selection + */ +#define ESP_PANEL_BOARD_LCD_BUS_TYPE (ESP_PANEL_BUS_TYPE_RGB) + + +/** + * @brief LCD bus parameters configuration + * + * Configure parameters based on the selected bus type. Parameters for other bus types will be ignored. + * For detailed parameter explanations, see: + * https://docs.espressif.com/projects/esp-idf/en/v5.3.1/esp32s3/api-reference/peripherals/lcd/index.html + * https://docs.espressif.com/projects/esp-iot-solution/en/latest/display/lcd/index.html + */ +#if ESP_PANEL_BOARD_LCD_BUS_TYPE == ESP_PANEL_BUS_TYPE_RGB + + /** + * @brief RGB bus + */ + /** + * Set to 0 if using simple "RGB" interface which does not contain "3-wire SPI" interface. + */ + #define ESP_PANEL_BOARD_LCD_RGB_USE_CONTROL_PANEL (0) // 0/1. Typically set to 1 + + /* For refresh panel (RGB) */ +#if ESP_PANEL_USE_1024_600_LCD + #define ESP_PANEL_BOARD_LCD_RGB_CLK_HZ (21 * 1000 * 1000) + #define ESP_PANEL_BOARD_LCD_RGB_HPW (30) + #define ESP_PANEL_BOARD_LCD_RGB_HBP (145) + #define ESP_PANEL_BOARD_LCD_RGB_HFP (170) + #define ESP_PANEL_BOARD_LCD_RGB_VPW (2) + #define ESP_PANEL_BOARD_LCD_RGB_VBP (23) + #define ESP_PANEL_BOARD_LCD_RGB_VFP (12) +#else + #define ESP_PANEL_BOARD_LCD_RGB_CLK_HZ (16 * 1000 * 1000) + #define ESP_PANEL_BOARD_LCD_RGB_HPW (4) + #define ESP_PANEL_BOARD_LCD_RGB_HBP (8) + #define ESP_PANEL_BOARD_LCD_RGB_HFP (8) + #define ESP_PANEL_BOARD_LCD_RGB_VPW (4) + #define ESP_PANEL_BOARD_LCD_RGB_VBP (16) + #define ESP_PANEL_BOARD_LCD_RGB_VFP (16) +#endif + + #define ESP_PANEL_BOARD_LCD_RGB_PCLK_ACTIVE_NEG (1) // 0: rising edge, 1: falling edge. Typically set to 0 + // The following sheet shows the valid combinations of + // data width and pixel bits: + // ┏---------------------------------┳- -------------------------------┓ + #define ESP_PANEL_BOARD_LCD_RGB_DATA_WIDTH (16) // | 16 | 8 | + #define ESP_PANEL_BOARD_LCD_RGB_PIXEL_BITS (ESP_PANEL_LCD_COLOR_BITS_RGB565) // | ESP_PANEL_LCD_COLOR_BITS_RGB565 | ESP_PANEL_LCD_COLOR_BITS_RGB888 | + // ┗---------------------------------┻---------------------------------┛ + // To understand color format of RGB LCD, see: https://docs.espressif.com/projects/esp-iot-solution/en/latest/display/lcd/rgb_lcd.html#color-formats + #define ESP_PANEL_BOARD_LCD_RGB_BOUNCE_BUF_SIZE (ESP_PANEL_BOARD_WIDTH * 10) + // Bounce buffer size in bytes. It is used to avoid screen drift + // for ESP32-S3. Typically set to `ESP_PANEL_BOARD_WIDTH * 10` + // The size should satisfy `size * N = LCD_width * LCD_height`, + // where N is an even number. + // For more details, see: https://github.com/esp-arduino-libs/ESP32_Display_Panel/blob/master/docs/FAQ.md#how-to-fix-screen-drift-issue-when-driving-rgb-lcd-with-esp32-s3 + #define ESP_PANEL_BOARD_LCD_RGB_IO_HSYNC (46) + #define ESP_PANEL_BOARD_LCD_RGB_IO_VSYNC (3) + #define ESP_PANEL_BOARD_LCD_RGB_IO_DE (5) // -1 if not used + #define ESP_PANEL_BOARD_LCD_RGB_IO_PCLK (7) + #define ESP_PANEL_BOARD_LCD_RGB_IO_DISP (-1) // -1 if not used. Typically set to -1 + + // The following sheet shows the mapping of ESP GPIOs to + // LCD data pins with different data width and color format: + // ┏------┳- ------------┳--------------------------┓ + // | ESP: | 8-bit RGB888 | 16-bit RGB565 | + // |------|--------------|--------------------------| + // | LCD: | RGB888 | RGB565 | RGB666 | RGB888 | + // ┗------|--------------|--------|--------|--------| + #define ESP_PANEL_BOARD_LCD_RGB_IO_DATA0 (14) // | D0 | B0 | B0-1 | B0-3 | + #define ESP_PANEL_BOARD_LCD_RGB_IO_DATA1 (38) // | D1 | B1 | B2 | B4 | + #define ESP_PANEL_BOARD_LCD_RGB_IO_DATA2 (18) // | D2 | B2 | B3 | B5 | + #define ESP_PANEL_BOARD_LCD_RGB_IO_DATA3 (17) // | D3 | B3 | B4 | B6 | + #define ESP_PANEL_BOARD_LCD_RGB_IO_DATA4 (10) // | D4 | B4 | B5 | B7 | + #define ESP_PANEL_BOARD_LCD_RGB_IO_DATA5 (39) // | D5 | G0 | G0 | G0-2 | + #define ESP_PANEL_BOARD_LCD_RGB_IO_DATA6 (0) // | D6 | G1 | G1 | G3 | + #define ESP_PANEL_BOARD_LCD_RGB_IO_DATA7 (45) // | D7 | G2 | G2 | G4 | +#if ESP_PANEL_BOARD_LCD_RGB_DATA_WIDTH > 8 // ┗--------------┫--------|--------|--------| + #define ESP_PANEL_BOARD_LCD_RGB_IO_DATA8 (48) // | G3 | G3 | G5 | + #define ESP_PANEL_BOARD_LCD_RGB_IO_DATA9 (47) // | G4 | G4 | G6 | + #define ESP_PANEL_BOARD_LCD_RGB_IO_DATA10 (21) // | G5 | G5 | G7 | + #define ESP_PANEL_BOARD_LCD_RGB_IO_DATA11 (1) // | R0 | R0-1 | R0-3 | + #define ESP_PANEL_BOARD_LCD_RGB_IO_DATA12 (2) // | R1 | R2 | R4 | + #define ESP_PANEL_BOARD_LCD_RGB_IO_DATA13 (42) // | R2 | R3 | R5 | + #define ESP_PANEL_BOARD_LCD_RGB_IO_DATA14 (41) // | R3 | R4 | R6 | + #define ESP_PANEL_BOARD_LCD_RGB_IO_DATA15 (40) // | R4 | R5 | R7 | + // ┗--------┻--------┻--------┛ +#endif // ESP_PANEL_BOARD_LCD_RGB_DATA_WIDTH + +#endif // ESP_PANEL_BOARD_LCD_BUS_TYPE + +/** + * @brief LCD color configuration + */ +#define ESP_PANEL_BOARD_LCD_COLOR_BITS (ESP_PANEL_LCD_COLOR_BITS_RGB565) + // ESP_PANEL_LCD_COLOR_BITS_RGB565/RGB666/RGB888 +#define ESP_PANEL_BOARD_LCD_COLOR_BGR_ORDER (0) // 0: RGB, 1: BGR +#define ESP_PANEL_BOARD_LCD_COLOR_INEVRT_BIT (0) // 0/1 + +/** + * @brief LCD transformation configuration + */ +#define ESP_PANEL_BOARD_LCD_SWAP_XY (0) // 0/1 +#define ESP_PANEL_BOARD_LCD_MIRROR_X (0) // 0/1 +#define ESP_PANEL_BOARD_LCD_MIRROR_Y (0) // 0/1 +#define ESP_PANEL_BOARD_LCD_GAP_X (0) // [0, ESP_PANEL_BOARD_WIDTH] +#define ESP_PANEL_BOARD_LCD_GAP_Y (0) // [0, ESP_PANEL_BOARD_HEIGHT] + +/** + * @brief LCD reset pin configuration + */ +#define ESP_PANEL_BOARD_LCD_RST_IO (-1) // Reset pin, -1 if not used +#define ESP_PANEL_BOARD_LCD_RST_LEVEL (0) // Reset active level, 0: low, 1: high + +#endif // ESP_PANEL_BOARD_USE_LCD + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////// Please update the following macros to configure the touch panel /////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * @brief Touch panel configuration flag (0/1) + * + * Set to `1` to enable touch panel support, `0` to disable + */ +#define ESP_PANEL_BOARD_USE_TOUCH (ESP_OPEN_TOUCH) + +#if ESP_PANEL_BOARD_USE_TOUCH +/** + * @brief Touch controller selection + */ +#define ESP_PANEL_BOARD_TOUCH_CONTROLLER GT911 + +/** + * @brief Touch bus type selection + * + * Supported types: + * - `ESP_PANEL_BUS_TYPE_I2C` + * - `ESP_PANEL_BUS_TYPE_SPI` + */ +#define ESP_PANEL_BOARD_TOUCH_BUS_TYPE (ESP_PANEL_BUS_TYPE_I2C) + +#if (ESP_PANEL_BOARD_TOUCH_BUS_TYPE == ESP_PANEL_BUS_TYPE_I2C) || \ + (ESP_PANEL_BOARD_TOUCH_BUS_TYPE == ESP_PANEL_BUS_TYPE_SPI) +/** + * If set to 1, the bus will skip to initialize the corresponding host. Users need to initialize the host in advance. + * + * For drivers which created by this library, even if they use the same host, the host will be initialized only once. + * So it is not necessary to set the macro to `1`. For other drivers (like `Wire`), please set the macro to `1` + * ensure that the host is initialized only once. + */ +#define ESP_PANEL_BOARD_TOUCH_BUS_SKIP_INIT_HOST (0) // 0/1. Typically set to 0 +#endif + +/** + * @brief Touch bus parameters configuration + */ +#if ESP_PANEL_BOARD_TOUCH_BUS_TYPE == ESP_PANEL_BUS_TYPE_I2C + + /** + * @brief I2C bus + */ + /* For general */ + #define ESP_PANEL_BOARD_TOUCH_I2C_HOST_ID (0) // Typically set to 0 +#if !ESP_PANEL_BOARD_TOUCH_BUS_SKIP_INIT_HOST + /* For host */ + #define ESP_PANEL_BOARD_TOUCH_I2C_CLK_HZ (400 * 1000) + // Typically set to 400K + #define ESP_PANEL_BOARD_TOUCH_I2C_SCL_PULLUP (1) // 0/1. Typically set to 1 + #define ESP_PANEL_BOARD_TOUCH_I2C_SDA_PULLUP (1) // 0/1. Typically set to 1 + #define ESP_PANEL_BOARD_TOUCH_I2C_IO_SCL (9) + #define ESP_PANEL_BOARD_TOUCH_I2C_IO_SDA (8) +#endif + /* For panel */ + #define ESP_PANEL_BOARD_TOUCH_I2C_ADDRESS (0) // Typically set to 0 to use the default address. + // - For touchs with only one address, set to 0 + // - For touchs with multiple addresses, set to 0 or + // the address. Like GT911, there are two addresses: + // 0x5D(default) and 0x14 + +#endif // ESP_PANEL_BOARD_TOUCH_BUS_TYPE + +/** + * @brief Touch panel transformation flags + */ +#define ESP_PANEL_BOARD_TOUCH_SWAP_XY (0) // 0/1 +#define ESP_PANEL_BOARD_TOUCH_MIRROR_X (0) // 0/1 +#define ESP_PANEL_BOARD_TOUCH_MIRROR_Y (0) // 0/1 + +/** + * @brief Touch panel control pins + */ +#define ESP_PANEL_BOARD_TOUCH_RST_IO (-1) // Reset pin, -1 if not used +#define ESP_PANEL_BOARD_TOUCH_RST_LEVEL (0) // Reset active level, 0: low, 1: high +#define ESP_PANEL_BOARD_TOUCH_INT_IO (4) // Interrupt pin, -1 if not used +#define ESP_PANEL_BOARD_TOUCH_INT_LEVEL (0) // Interrupt active level, 0: low, 1: high + +#endif // ESP_PANEL_BOARD_USE_TOUCH + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////// Please update the following macros to configure the backlight //////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * @brief Backlight configuration flag (0/1) + * + * Set to `1` to enable backlight support, `0` to disable + */ +#define ESP_PANEL_BOARD_USE_BACKLIGHT (1) + +#if ESP_PANEL_BOARD_USE_BACKLIGHT +/** + * @brief Backlight control type selection + * + * Supported types: + * - `ESP_PANEL_BACKLIGHT_TYPE_SWITCH_GPIO`: Use GPIO switch to control the backlight, only support on/off + * - `ESP_PANEL_BACKLIGHT_TYPE_SWITCH_EXPANDER`: Use IO expander to control the backlight, only support on/off + * - `ESP_PANEL_BACKLIGHT_TYPE_PWM_LEDC`: Use LEDC PWM to control the backlight, support brightness adjustment + * - `ESP_PANEL_BACKLIGHT_TYPE_CUSTOM`: Use custom function to control the backlight + */ +#define ESP_PANEL_BOARD_BACKLIGHT_TYPE (ESP_PANEL_BACKLIGHT_TYPE_SWITCH_EXPANDER) + +#if (ESP_PANEL_BOARD_BACKLIGHT_TYPE == ESP_PANEL_BACKLIGHT_TYPE_SWITCH_GPIO) || \ + (ESP_PANEL_BOARD_BACKLIGHT_TYPE == ESP_PANEL_BACKLIGHT_TYPE_SWITCH_EXPANDER) || \ + (ESP_PANEL_BOARD_BACKLIGHT_TYPE == ESP_PANEL_BACKLIGHT_TYPE_PWM_LEDC) + + /** + * @brief Backlight control pin configuration + */ + #define ESP_PANEL_BOARD_BACKLIGHT_IO (2) // Output GPIO pin number + #define ESP_PANEL_BOARD_BACKLIGHT_ON_LEVEL (1) // Active level, 0: low, 1: high + +#endif // ESP_PANEL_BOARD_BACKLIGHT_TYPE + +/** + * @brief Backlight idle state configuration (0/1) + * + * Set to 1 if want to turn off the backlight after initializing. Otherwise, the backlight will be on. + */ +#define ESP_PANEL_BOARD_BACKLIGHT_IDLE_OFF (0) + +#endif // ESP_PANEL_BOARD_USE_BACKLIGHT + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////// Please update the following macros to configure the IO expander ////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * @brief IO expander configuration flag (0/1) + * + * Set to `1` to enable IO expander support, `0` to disable + */ +#define ESP_PANEL_BOARD_USE_EXPANDER (1) + +#if ESP_PANEL_BOARD_USE_EXPANDER +/** + * @brief IO expander chip selection + */ +#define ESP_PANEL_BOARD_EXPANDER_CHIP CH422G + +/** + * @brief IO expander I2C bus parameters configuration + */ +/** + * If set to 1, the bus will skip to initialize the corresponding host. Users need to initialize the host in advance. + * + * For drivers which created by this library, even if they use the same host, the host will be initialized only once. + * So it is not necessary to set the macro to `1`. For other devices, please set the macro to `1` ensure that the + * host is initialized only once. + */ +#define ESP_PANEL_BOARD_EXPANDER_SKIP_INIT_HOST (0) // 0/1 +/* For general */ +#define ESP_PANEL_BOARD_EXPANDER_I2C_HOST_ID (0) // Typically set to 0 +/* For host */ +#if !ESP_PANEL_BOARD_EXPANDER_SKIP_INIT_HOST +#define ESP_PANEL_BOARD_EXPANDER_I2C_CLK_HZ (400 * 1000) + // Typically set to 400K +#define ESP_PANEL_BOARD_EXPANDER_I2C_SCL_PULLUP (1) // 0/1. Typically set to 1 +#define ESP_PANEL_BOARD_EXPANDER_I2C_SDA_PULLUP (1) // 0/1. Typically set to 1 +#define ESP_PANEL_BOARD_EXPANDER_I2C_IO_SCL (9) +#define ESP_PANEL_BOARD_EXPANDER_I2C_IO_SDA (8) +#endif // ESP_PANEL_BOARD_EXPANDER_SKIP_INIT_HOST +/* For device */ +#define ESP_PANEL_BOARD_EXPANDER_I2C_ADDRESS (0x20) // The actual I2C address. Even for the same model of IC, + // the I2C address may be different, and confirmation based on + // the actual hardware connection is required +#endif // ESP_PANEL_BOARD_USE_EXPANDER + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////// Please utilize the following macros to execute any additional code if required ///////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * @brief Post-begin function for IO expander initialization + * + * @param[in] p Pointer to the board object + * @return true on success, false on failure + */ +#define ESP_PANEL_BOARD_EXPANDER_POST_BEGIN_FUNCTION(p) \ + { \ + auto board = static_cast(p); \ + auto expander = static_cast(board->getIO_Expander()->getBase()); \ + expander->enableAllIO_Output(); \ + return true; \ + } + +/** + * @brief Pre-begin function for LCD initialization + * + * @param[in] p Pointer to the board object + * @return true on success, false on failure + */ +#define ESP_PANEL_BOARD_LCD_PRE_BEGIN_FUNCTION(p) \ + { \ + constexpr int LCD_RST = 3; \ + auto board = static_cast(p); \ + auto expander = board->getIO_Expander()->getBase(); \ + expander->digitalWrite(LCD_RST, 0); \ + vTaskDelay(pdMS_TO_TICKS(10)); \ + expander->digitalWrite(LCD_RST, 1); \ + vTaskDelay(pdMS_TO_TICKS(100)); \ + return true; \ + } + +#if ESP_PANEL_BOARD_USE_TOUCH +/** + * @brief Pre-begin function for touch panel initialization + * + * @param[in] p Pointer to the board object + * @return true on success, false on failure + */ +#define ESP_PANEL_BOARD_TOUCH_PRE_BEGIN_FUNCTION(p) \ + { \ + constexpr gpio_num_t TP_INT = static_cast(ESP_PANEL_BOARD_TOUCH_INT_IO); \ + constexpr int TP_RST = 1; \ + auto board = static_cast(p); \ + auto expander = board->getIO_Expander()->getBase(); \ + gpio_set_direction(TP_INT, GPIO_MODE_OUTPUT); \ + gpio_set_level(TP_INT, 0); \ + vTaskDelay(pdMS_TO_TICKS(10)); \ + expander->digitalWrite(TP_RST, 0); \ + vTaskDelay(pdMS_TO_TICKS(100)); \ + expander->digitalWrite(TP_RST, 1); \ + vTaskDelay(pdMS_TO_TICKS(200)); \ + gpio_reset_pin(TP_INT); \ + return true; \ + } +#endif +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////// File Version /////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * Do not change the following versions. These version numbers are used to check compatibility between this + * configuration file and the library. Rules for version numbers: + * 1. Major version mismatch: Configurations are incompatible, must use library version + * 2. Minor version mismatch: May be missing new configurations, recommended to update + * 3. Patch version mismatch: No impact on functionality + */ +#define ESP_PANEL_BOARD_CUSTOM_FILE_VERSION_MAJOR 1 +#define ESP_PANEL_BOARD_CUSTOM_FILE_VERSION_MINOR 0 +#define ESP_PANEL_BOARD_CUSTOM_FILE_VERSION_PATCH 0 + +#endif // ESP_PANEL_BOARD_USE_CUSTOM + +// *INDENT-ON* diff --git a/firmware/esp32-s3-dashboard/lvgl_v8_port.cpp b/firmware/esp32-s3-dashboard/lvgl_v8_port.cpp new file mode 100644 index 0000000..8c6042f --- /dev/null +++ b/firmware/esp32-s3-dashboard/lvgl_v8_port.cpp @@ -0,0 +1,859 @@ +/* + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#include "esp_timer.h" +#undef ESP_UTILS_LOG_TAG +#define ESP_UTILS_LOG_TAG "LvPort" +#include "esp_lib_utils.h" +#include "lvgl_v8_port.h" + +using namespace esp_panel::drivers; + +#define LVGL_PORT_ENABLE_ROTATION_OPTIMIZED (1) +#define LVGL_PORT_BUFFER_NUM_MAX (2) + +static SemaphoreHandle_t lvgl_mux = nullptr; // LVGL mutex +static TaskHandle_t lvgl_task_handle = nullptr; +static esp_timer_handle_t lvgl_tick_timer = NULL; +static void *lvgl_buf[LVGL_PORT_BUFFER_NUM_MAX] = {}; + +#if LVGL_PORT_ROTATION_DEGREE != 0 +static void *get_next_frame_buffer(LCD *lcd) +{ + static void *next_fb = NULL; + static void *fbs[2] = { NULL }; + + if (next_fb == NULL) { + fbs[0] = lcd->getFrameBufferByIndex(0); + fbs[1] = lcd->getFrameBufferByIndex(1); + next_fb = fbs[1]; + } else { + next_fb = (next_fb == fbs[0]) ? fbs[1] : fbs[0]; + } + + return next_fb; +} + +__attribute__((always_inline)) +static inline void copy_pixel_8bpp(uint8_t *to, const uint8_t *from) +{ + *to++ = *from++; +} + +__attribute__((always_inline)) +static inline void copy_pixel_16bpp(uint8_t *to, const uint8_t *from) +{ + *(uint16_t *)to++ = *(const uint16_t *)from++; +} + +__attribute__((always_inline)) +static inline void copy_pixel_24bpp(uint8_t *to, const uint8_t *from) +{ + *to++ = *from++; + *to++ = *from++; + *to++ = *from++; +} + +#define _COPY_PIXEL(_bpp, to, from) copy_pixel_##_bpp##bpp(to, from) +#define COPY_PIXEL(_bpp, to, from) _COPY_PIXEL(_bpp, to, from) + +#define ROTATE_90_ALL_BPP() \ + { \ + to_bytes_per_line = h * to_bytes_per_piexl; \ + to_index_const = (w - x_start - 1) * to_bytes_per_line; \ + for (int from_y = y_start; from_y < y_end + 1; from_y++) { \ + from_index = from_y * from_bytes_per_line + x_start * from_bytes_per_piexl; \ + to_index = to_index_const + from_y * to_bytes_per_piexl; \ + for (int from_x = x_start; from_x < x_end + 1; from_x++) { \ + COPY_PIXEL(LV_COLOR_DEPTH, to + to_index, from + from_index); \ + from_index += from_bytes_per_piexl; \ + to_index -= to_bytes_per_line; \ + } \ + } \ + } + +/** + * @brief Optimized transpose function for RGB565 format. + * + * @note ESP32-P4 1024x600 full-screen: 738ms -> 34ms + * @note ESP32-S3 480x480 full-screen: 380ms -> 37ms + */ +#define ROTATE_90_OPTIMIZED_16BPP(block_w, block_h) \ + { \ + for (int i = 0; i < h; i += block_h) { \ + max_height = (i + block_h > h) ? h : (i + block_h); \ + for (int j = 0; j < w; j += block_w) { \ + max_width = (j + block_w > w) ? w : (j + block_w); \ + start_y = w - 1 - j; \ + for (int x = i; x < max_height; x++) { \ + from_next = (uint16_t *)from + x * w; \ + for (int y = j, mirrored_y = start_y; y < max_width; y += 4, mirrored_y -= 4) { \ + ((uint16_t *)to)[(mirrored_y) * h + x] = *((uint32_t *)(from_next + y)) & 0xFFFF; \ + ((uint16_t *)to)[(mirrored_y - 1) * h + x] = (*((uint32_t *)(from_next + y)) >> 16) & 0xFFFF; \ + ((uint16_t *)to)[(mirrored_y - 2) * h + x] = *((uint32_t *)(from_next + y + 2)) & 0xFFFF; \ + ((uint16_t *)to)[(mirrored_y - 3) * h + x] = (*((uint32_t *)(from_next + y + 2)) >> 16) & 0xFFFF; \ + } \ + } \ + } \ + } \ + } + +#define ROTATE_180_ALL_BPP() \ + { \ + to_bytes_per_line = w * to_bytes_per_piexl; \ + to_index_const = (h - 1) * to_bytes_per_line + (w - x_start - 1) * to_bytes_per_piexl; \ + for (int from_y = y_start; from_y < y_end + 1; from_y++) { \ + from_index = from_y * from_bytes_per_line + x_start * from_bytes_per_piexl; \ + to_index = to_index_const - from_y * to_bytes_per_line; \ + for (int from_x = x_start; from_x < x_end + 1; from_x++) { \ + COPY_PIXEL(LV_COLOR_DEPTH, to + to_index, from + from_index); \ + from_index += from_bytes_per_piexl; \ + to_index -= to_bytes_per_piexl; \ + } \ + } \ + } + +#define ROTATE_270_OPTIMIZED_16BPP(block_w, block_h) \ + { \ + for (int i = 0; i < h; i += block_h) { \ + max_height = i + block_h > h ? h : i + block_h; \ + for (int j = 0; j < w; j += block_w) { \ + max_width = j + block_w > w ? w : j + block_w; \ + for (int x = i; x < max_height; x++) { \ + from_next = (uint16_t *)from + x * w; \ + for (int y = j; y < max_width; y += 4) { \ + ((uint16_t *)to)[y * h + (h - 1 - x)] = *((uint32_t *)(from_next + y)) & 0xFFFF; \ + ((uint16_t *)to)[(y + 1) * h + (h - 1 - x)] = (*((uint32_t *)(from_next + y)) >> 16) & 0xFFFF; \ + ((uint16_t *)to)[(y + 2) * h + (h - 1 - x)] = *((uint32_t *)(from_next + y + 2)) & 0xFFFF; \ + ((uint16_t *)to)[(y + 3) * h + (h - 1 - x)] = (*((uint32_t *)(from_next + y + 2)) >> 16) & 0xFFFF; \ + } \ + } \ + } \ + } \ + } + +#define ROTATE_270_ALL_BPP() \ + { \ + to_bytes_per_line = h * to_bytes_per_piexl; \ + from_index_const = x_start * from_bytes_per_piexl; \ + to_index_const = x_start * to_bytes_per_line + (h - 1) * to_bytes_per_piexl; \ + for (int from_y = y_start; from_y < y_end + 1; from_y++) { \ + from_index = from_y * from_bytes_per_line + from_index_const; \ + to_index = to_index_const - from_y * to_bytes_per_piexl; \ + for (int from_x = x_start; from_x < x_end + 1; from_x++) { \ + COPY_PIXEL(LV_COLOR_DEPTH, to + to_index, from + from_index); \ + from_index += from_bytes_per_piexl; \ + to_index += to_bytes_per_line; \ + } \ + } \ + } + +__attribute__((always_inline)) +IRAM_ATTR static inline void rotate_copy_pixel( + const uint8_t *from, uint8_t *to, uint16_t x_start, uint16_t y_start, uint16_t x_end, uint16_t y_end, uint16_t w, + uint16_t h, uint16_t rotate +) +{ + int from_bytes_per_piexl = sizeof(lv_color_t); + int from_bytes_per_line = w * from_bytes_per_piexl; + int from_index = 0; + + int to_bytes_per_piexl = LV_COLOR_DEPTH >> 3; + int to_bytes_per_line; + int to_index = 0; + int to_index_const = 0; + +#if (LV_COLOR_DEPTH == 16) && LVGL_PORT_ENABLE_ROTATION_OPTIMIZED + int max_height = 0; + int max_width = 0; + int start_y = 0; + uint16_t *from_next = NULL; +#endif + + // uint32_t time = esp_log_timestamp(); + switch (rotate) { + case 90: +#if (LV_COLOR_DEPTH == 16) && LVGL_PORT_ENABLE_ROTATION_OPTIMIZED + ROTATE_90_OPTIMIZED_16BPP(32, 256); +#else + ROTATE_90_ALL_BPP(); +#endif + break; + case 180: + ROTATE_180_ALL_BPP(); + break; + case 270: +#if (LV_COLOR_DEPTH == 16) && LVGL_PORT_ENABLE_ROTATION_OPTIMIZED + ROTATE_270_OPTIMIZED_16BPP(32, 256); +#else + int from_index_const = 0; + ROTATE_270_ALL_BPP(); +#endif + break; + default: + break; + } + // ESP_LOGI(TAG, "rotate: end, time used:%d", (int)(esp_log_timestamp() - time)); +} +#endif /* LVGL_PORT_ROTATION_DEGREE */ + +#if LVGL_PORT_AVOID_TEAR +#if LVGL_PORT_DIRECT_MODE +#if LVGL_PORT_ROTATION_DEGREE != 0 +typedef struct { + uint16_t inv_p; + uint8_t inv_area_joined[LV_INV_BUF_SIZE]; + lv_area_t inv_areas[LV_INV_BUF_SIZE]; +} lv_port_dirty_area_t; + +static lv_port_dirty_area_t dirty_area; + +static void flush_dirty_save(lv_port_dirty_area_t *dirty_area) +{ + lv_disp_t *disp = _lv_refr_get_disp_refreshing(); + dirty_area->inv_p = disp->inv_p; + for (int i = 0; i < disp->inv_p; i++) { + dirty_area->inv_area_joined[i] = disp->inv_area_joined[i]; + dirty_area->inv_areas[i] = disp->inv_areas[i]; + } +} + +typedef enum { + FLUSH_STATUS_PART, + FLUSH_STATUS_FULL +} lv_port_flush_status_t; + +typedef enum { + FLUSH_PROBE_PART_COPY, + FLUSH_PROBE_SKIP_COPY, + FLUSH_PROBE_FULL_COPY, +} lv_port_flush_probe_t; + +/** + * @brief Probe dirty area to copy + * + * @note This function is used to avoid tearing effect, and only work with LVGL direct-mode. + */ +static lv_port_flush_probe_t flush_copy_probe(lv_disp_drv_t *drv) +{ + static lv_port_flush_status_t prev_status = FLUSH_STATUS_PART; + lv_port_flush_status_t cur_status; + lv_port_flush_probe_t probe_result; + lv_disp_t *disp_refr = _lv_refr_get_disp_refreshing(); + + uint32_t flush_ver = 0; + uint32_t flush_hor = 0; + for (int i = 0; i < disp_refr->inv_p; i++) { + if (disp_refr->inv_area_joined[i] == 0) { + flush_ver = (disp_refr->inv_areas[i].y2 + 1 - disp_refr->inv_areas[i].y1); + flush_hor = (disp_refr->inv_areas[i].x2 + 1 - disp_refr->inv_areas[i].x1); + break; + } + } + /* Check if the current full screen refreshes */ + cur_status = ((flush_ver == drv->ver_res) && (flush_hor == drv->hor_res)) ? (FLUSH_STATUS_FULL) : (FLUSH_STATUS_PART); + + if (prev_status == FLUSH_STATUS_FULL) { + if ((cur_status == FLUSH_STATUS_PART)) { + probe_result = FLUSH_PROBE_FULL_COPY; + } else { + probe_result = FLUSH_PROBE_SKIP_COPY; + } + } else { + probe_result = FLUSH_PROBE_PART_COPY; + } + prev_status = cur_status; + + return probe_result; +} + +static inline void *flush_get_next_buf(LCD *lcd) +{ + return get_next_frame_buffer(lcd); +} + +/** + * @brief Copy dirty area + * + * @note This function is used to avoid tearing effect, and only work with LVGL direct-mode. + */ +static void flush_dirty_copy(void *dst, void *src, lv_port_dirty_area_t *dirty_area) +{ + lv_coord_t x_start, x_end, y_start, y_end; + for (int i = 0; i < dirty_area->inv_p; i++) { + /* Refresh the unjoined areas*/ + if (dirty_area->inv_area_joined[i] == 0) { + x_start = dirty_area->inv_areas[i].x1; + x_end = dirty_area->inv_areas[i].x2; + y_start = dirty_area->inv_areas[i].y1; + y_end = dirty_area->inv_areas[i].y2; + + rotate_copy_pixel( + (uint8_t *)src, (uint8_t *)dst, x_start, y_start, x_end, y_end, LV_HOR_RES, LV_VER_RES, + LVGL_PORT_ROTATION_DEGREE + ); + } + } +} + +static void flush_callback(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map) +{ + LCD *lcd = (LCD *)drv->user_data; + const int offsetx1 = area->x1; + const int offsetx2 = area->x2; + const int offsety1 = area->y1; + const int offsety2 = area->y2; + void *next_fb = NULL; + lv_port_flush_probe_t probe_result = FLUSH_PROBE_PART_COPY; + lv_disp_t *disp = lv_disp_get_default(); + + /* Action after last area refresh */ + if (lv_disp_flush_is_last(drv)) { + /* Check if the `full_refresh` flag has been triggered */ + if (drv->full_refresh) { + /* Reset flag */ + drv->full_refresh = 0; + + // Rotate and copy data from the whole screen LVGL's buffer to the next frame buffer + next_fb = flush_get_next_buf(lcd); + rotate_copy_pixel( + (uint8_t *)color_map, (uint8_t *)next_fb, offsetx1, offsety1, offsetx2, offsety2, + LV_HOR_RES, LV_VER_RES, LVGL_PORT_ROTATION_DEGREE + ); + + /* Switch the current LCD frame buffer to `next_fb` */ + lcd->switchFrameBufferTo(next_fb); + + /* Waiting for the current frame buffer to complete transmission */ + ulTaskNotifyValueClear(NULL, ULONG_MAX); + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + + /* Synchronously update the dirty area for another frame buffer */ + flush_dirty_copy(flush_get_next_buf(lcd), color_map, &dirty_area); + flush_get_next_buf(lcd); + } else { + /* Probe the copy method for the current dirty area */ + probe_result = flush_copy_probe(drv); + + if (probe_result == FLUSH_PROBE_FULL_COPY) { + /* Save current dirty area for next frame buffer */ + flush_dirty_save(&dirty_area); + + /* Set LVGL full-refresh flag and set flush ready in advance */ + drv->full_refresh = 1; + disp->rendering_in_progress = false; + lv_disp_flush_ready(drv); + + /* Force to refresh whole screen, and will invoke `flush_callback` recursively */ + lv_refr_now(_lv_refr_get_disp_refreshing()); + } else { + /* Update current dirty area for next frame buffer */ + next_fb = flush_get_next_buf(lcd); + flush_dirty_save(&dirty_area); + flush_dirty_copy(next_fb, color_map, &dirty_area); + + /* Switch the current LCD frame buffer to `next_fb` */ + lcd->switchFrameBufferTo(next_fb); + + /* Waiting for the current frame buffer to complete transmission */ + ulTaskNotifyValueClear(NULL, ULONG_MAX); + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + + if (probe_result == FLUSH_PROBE_PART_COPY) { + /* Synchronously update the dirty area for another frame buffer */ + flush_dirty_save(&dirty_area); + flush_dirty_copy(flush_get_next_buf(lcd), color_map, &dirty_area); + flush_get_next_buf(lcd); + } + } + } + } + + lv_disp_flush_ready(drv); +} + +#else + +static void flush_callback(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map) +{ + LCD *lcd = (LCD *)drv->user_data; + + /* Action after last area refresh */ + if (lv_disp_flush_is_last(drv)) { + /* Switch the current LCD frame buffer to `color_map` */ + lcd->switchFrameBufferTo(color_map); + + /* Waiting for the last frame buffer to complete transmission */ + ulTaskNotifyValueClear(NULL, ULONG_MAX); + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + } + + lv_disp_flush_ready(drv); +} +#endif /* LVGL_PORT_ROTATION_DEGREE */ + +#elif LVGL_PORT_FULL_REFRESH && LVGL_PORT_DISP_BUFFER_NUM == 2 + +static void flush_callback(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map) +{ + LCD *lcd = (LCD *)drv->user_data; + + /* Switch the current LCD frame buffer to `color_map` */ + lcd->switchFrameBufferTo(color_map); + + /* Waiting for the last frame buffer to complete transmission */ + ulTaskNotifyValueClear(NULL, ULONG_MAX); + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + + lv_disp_flush_ready(drv); +} + +#elif LVGL_PORT_FULL_REFRESH && LVGL_PORT_DISP_BUFFER_NUM == 3 + +#if LVGL_PORT_ROTATION_DEGREE == 0 +static void *lvgl_port_lcd_last_buf = NULL; +static void *lvgl_port_lcd_next_buf = NULL; +static void *lvgl_port_flush_next_buf = NULL; +#endif + +void flush_callback(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map) +{ + LCD *lcd = (LCD *)drv->user_data; + +#if LVGL_PORT_ROTATION_DEGREE != 0 + const int offsetx1 = area->x1; + const int offsetx2 = area->x2; + const int offsety1 = area->y1; + const int offsety2 = area->y2; + void *next_fb = get_next_frame_buffer(lcd); + + /* Rotate and copy dirty area from the current LVGL's buffer to the next LCD frame buffer */ + rotate_copy_pixel( + (uint8_t *)color_map, (uint8_t *)next_fb, offsetx1, offsety1, offsetx2, offsety2, LV_HOR_RES, + LV_VER_RES, LVGL_PORT_ROTATION_DEGREE + ); + + /* Switch the current LCD frame buffer to `next_fb` */ + lcd->switchFrameBufferTo(next_fb); +#else + drv->draw_buf->buf1 = color_map; + drv->draw_buf->buf2 = lvgl_port_flush_next_buf; + lvgl_port_flush_next_buf = color_map; + + /* Switch the current LCD frame buffer to `color_map` */ + lcd->switchFrameBufferTo(color_map); + + lvgl_port_lcd_next_buf = color_map; +#endif + + lv_disp_flush_ready(drv); +} +#endif + +IRAM_ATTR bool onLcdVsyncCallback(void *user_data) +{ + BaseType_t need_yield = pdFALSE; +#if LVGL_PORT_FULL_REFRESH && (LVGL_PORT_DISP_BUFFER_NUM == 3) && (LVGL_PORT_ROTATION_DEGREE == 0) + if (lvgl_port_lcd_next_buf != lvgl_port_lcd_last_buf) { + lvgl_port_flush_next_buf = lvgl_port_lcd_last_buf; + lvgl_port_lcd_last_buf = lvgl_port_lcd_next_buf; + } +#else + TaskHandle_t task_handle = (TaskHandle_t)user_data; + // Notify that the current LCD frame buffer has been transmitted + xTaskNotifyFromISR(task_handle, ULONG_MAX, eNoAction, &need_yield); +#endif + return (need_yield == pdTRUE); +} + +#else + +void flush_callback(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map) +{ + LCD *lcd = (LCD *)drv->user_data; + const int offsetx1 = area->x1; + const int offsetx2 = area->x2; + const int offsety1 = area->y1; + const int offsety2 = area->y2; + + lcd->drawBitmap(offsetx1, offsety1, offsetx2 - offsetx1 + 1, offsety2 - offsety1 + 1, (const uint8_t *)color_map); + // For RGB LCD, directly notify LVGL that the buffer is ready + if (lcd->getBus()->getBasicAttributes().type == ESP_PANEL_BUS_TYPE_RGB) { + lv_disp_flush_ready(drv); + } +} + +static void update_callback(lv_disp_drv_t *drv) +{ + LCD *lcd = (LCD *)drv->user_data; + auto transformation = lcd->getTransformation(); + static bool disp_init_mirror_x = transformation.mirror_x; + static bool disp_init_mirror_y = transformation.mirror_y; + static bool disp_init_swap_xy = transformation.swap_xy; + + switch (drv->rotated) { + case LV_DISP_ROT_NONE: + lcd->swapXY(disp_init_swap_xy); + lcd->mirrorX(disp_init_mirror_x); + lcd->mirrorY(disp_init_mirror_y); + break; + case LV_DISP_ROT_90: + lcd->swapXY(!disp_init_swap_xy); + lcd->mirrorX(disp_init_mirror_x); + lcd->mirrorY(!disp_init_mirror_y); + break; + case LV_DISP_ROT_180: + lcd->swapXY(disp_init_swap_xy); + lcd->mirrorX(!disp_init_mirror_x); + lcd->mirrorY(!disp_init_mirror_y); + break; + case LV_DISP_ROT_270: + lcd->swapXY(!disp_init_swap_xy); + lcd->mirrorX(!disp_init_mirror_x); + lcd->mirrorY(disp_init_mirror_y); + break; + } + + ESP_UTILS_LOGD("Update display rotation to %d", drv->rotated); + +#if ESP_UTILS_CONF_LOG_LEVEL == ESP_UTILS_LOG_LEVEL_DEBUG + transformation = lcd->getTransformation(); + disp_init_mirror_x = transformation.mirror_x; + disp_init_mirror_y = transformation.mirror_y; + disp_init_swap_xy = transformation.swap_xy; + ESP_UTILS_LOGD("Current mirror x: %d, mirror y: %d, swap xy: %d", disp_init_mirror_x, disp_init_mirror_y, disp_init_swap_xy); +#endif +} + +#endif /* LVGL_PORT_AVOID_TEAR */ + +void rounder_callback(lv_disp_drv_t *drv, lv_area_t *area) +{ + LCD *lcd = (LCD *)drv->user_data; + uint8_t x_align = lcd->getBasicAttributes().basic_bus_spec.x_coord_align; + uint8_t y_align = lcd->getBasicAttributes().basic_bus_spec.y_coord_align; + + if (x_align > 1) { + // round the start of coordinate down to the nearest aligned value + area->x1 &= ~(x_align - 1); + // round the end of coordinate up to the nearest aligned value + area->x2 = (area->x2 & ~(x_align - 1)) + x_align - 1; + } + + if (y_align > 1) { + // round the start of coordinate down to the nearest aligned value + area->y1 &= ~(y_align - 1); + // round the end of coordinate up to the nearest aligned value + area->y2 = (area->y2 & ~(y_align - 1)) + y_align - 1; + } +} + +static lv_disp_t *display_init(LCD *lcd) +{ + ESP_UTILS_CHECK_FALSE_RETURN(lcd != nullptr, nullptr, "Invalid LCD device"); + ESP_UTILS_CHECK_FALSE_RETURN(lcd->getRefreshPanelHandle() != nullptr, nullptr, "LCD device is not initialized"); + + static lv_disp_draw_buf_t disp_buf; + static lv_disp_drv_t disp_drv; + + // Alloc draw buffers used by LVGL + auto lcd_width = lcd->getFrameWidth(); + auto lcd_height = lcd->getFrameHeight(); + int buffer_size = 0; + + ESP_UTILS_LOGD("Malloc memory for LVGL buffer"); +#if !LVGL_PORT_AVOID_TEAR + // Avoid tearing function is disabled + buffer_size = lcd_width * LVGL_PORT_BUFFER_SIZE_HEIGHT; + for (int i = 0; (i < LVGL_PORT_BUFFER_NUM) && (i < LVGL_PORT_BUFFER_NUM_MAX); i++) { + lvgl_buf[i] = heap_caps_malloc(buffer_size * sizeof(lv_color_t), LVGL_PORT_BUFFER_MALLOC_CAPS); + assert(lvgl_buf[i]); + ESP_UTILS_LOGD("Buffer[%d] address: %p, size: %d", i, lvgl_buf[i], buffer_size * sizeof(lv_color_t)); + } +#else + // To avoid the tearing effect, we should use at least two frame buffers: one for LVGL rendering and another for LCD refresh + buffer_size = lcd_width * lcd_height; +#if (LVGL_PORT_DISP_BUFFER_NUM >= 3) && (LVGL_PORT_ROTATION_DEGREE == 0) && LVGL_PORT_FULL_REFRESH + + // With the usage of three buffers and full-refresh, we always have one buffer available for rendering, + // eliminating the need to wait for the LCD's sync signal + lvgl_port_lcd_last_buf = lcd->getFrameBufferByIndex(0); + lvgl_buf[0] = lcd->getFrameBufferByIndex(1); + lvgl_buf[1] = lcd->getFrameBufferByIndex(2); + lvgl_port_lcd_next_buf = lvgl_port_lcd_last_buf; + lvgl_port_flush_next_buf = lvgl_buf[1]; + +#elif (LVGL_PORT_DISP_BUFFER_NUM >= 3) && (LVGL_PORT_ROTATION_DEGREE != 0) + + lvgl_buf[0] = lcd->getFrameBufferByIndex(2); + +#elif LVGL_PORT_DISP_BUFFER_NUM >= 2 + + for (int i = 0; (i < LVGL_PORT_DISP_BUFFER_NUM) && (i < LVGL_PORT_BUFFER_NUM_MAX); i++) { + lvgl_buf[i] = lcd->getFrameBufferByIndex(i); + } + +#endif +#endif /* LVGL_PORT_AVOID_TEAR */ + + // initialize LVGL draw buffers + lv_disp_draw_buf_init(&disp_buf, lvgl_buf[0], lvgl_buf[1], buffer_size); + + ESP_UTILS_LOGD("Register display driver to LVGL"); + lv_disp_drv_init(&disp_drv); + disp_drv.flush_cb = flush_callback; +#if (LVGL_PORT_ROTATION_DEGREE == 90) || (LVGL_PORT_ROTATION_DEGREE == 270) + disp_drv.hor_res = lcd_height; + disp_drv.ver_res = lcd_width; +#else + disp_drv.hor_res = lcd_width; + disp_drv.ver_res = lcd_height; +#endif +#if LVGL_PORT_AVOID_TEAR // Only available when the tearing effect is enabled +#if LVGL_PORT_FULL_REFRESH + disp_drv.full_refresh = 1; +#elif LVGL_PORT_DIRECT_MODE + disp_drv.direct_mode = 1; +#endif +#else // Only available when the tearing effect is disabled + if (lcd->getBasicAttributes().basic_bus_spec.isFunctionValid(LCD::BasicBusSpecification::FUNC_SWAP_XY) && + lcd->getBasicAttributes().basic_bus_spec.isFunctionValid(LCD::BasicBusSpecification::FUNC_MIRROR_X) && + lcd->getBasicAttributes().basic_bus_spec.isFunctionValid(LCD::BasicBusSpecification::FUNC_MIRROR_Y)) { + disp_drv.drv_update_cb = update_callback; + } else { + disp_drv.sw_rotate = 1; + } +#endif /* LVGL_PORT_AVOID_TEAR */ + disp_drv.draw_buf = &disp_buf; + disp_drv.user_data = (void *)lcd; + // Only available when the coordinate alignment is enabled + if ((lcd->getBasicAttributes().basic_bus_spec.x_coord_align > 1) || + (lcd->getBasicAttributes().basic_bus_spec.y_coord_align > 1)) { + disp_drv.rounder_cb = rounder_callback; + } + + return lv_disp_drv_register(&disp_drv); +} + +static void touchpad_read(lv_indev_drv_t *indev_drv, lv_indev_data_t *data) +{ + Touch *tp = (Touch *)indev_drv->user_data; + TouchPoint point; + + /* Read data from touch controller */ + int read_touch_result = tp->readPoints(&point, 1, 0); + if (read_touch_result > 0) { + data->point.x = point.x; + data->point.y = point.y; + data->state = LV_INDEV_STATE_PRESSED; + } else { + data->state = LV_INDEV_STATE_RELEASED; + } +} + +static lv_indev_t *indev_init(Touch *tp) +{ + ESP_UTILS_CHECK_FALSE_RETURN(tp != nullptr, nullptr, "Invalid touch device"); + ESP_UTILS_CHECK_FALSE_RETURN(tp->getPanelHandle() != nullptr, nullptr, "Touch device is not initialized"); + + static lv_indev_drv_t indev_drv_tp; + + ESP_UTILS_LOGD("Register input driver to LVGL"); + lv_indev_drv_init(&indev_drv_tp); + indev_drv_tp.type = LV_INDEV_TYPE_POINTER; + indev_drv_tp.read_cb = touchpad_read; + indev_drv_tp.user_data = (void *)tp; + + return lv_indev_drv_register(&indev_drv_tp); +} + +#if !LV_TICK_CUSTOM +static void tick_increment(void *arg) +{ + /* Tell LVGL how many milliseconds have elapsed */ + lv_tick_inc(LVGL_PORT_TICK_PERIOD_MS); +} + +static bool tick_init(void) +{ + // Tick interface for LVGL (using esp_timer to generate 2ms periodic event) + const esp_timer_create_args_t lvgl_tick_timer_args = { + .callback = &tick_increment, + .name = "LVGL tick" + }; + ESP_UTILS_CHECK_ERROR_RETURN( + esp_timer_create(&lvgl_tick_timer_args, &lvgl_tick_timer), false, "Create LVGL tick timer failed" + ); + ESP_UTILS_CHECK_ERROR_RETURN( + esp_timer_start_periodic(lvgl_tick_timer, LVGL_PORT_TICK_PERIOD_MS * 1000), false, + "Start LVGL tick timer failed" + ); + + return true; +} + +static bool tick_deinit(void) +{ + ESP_UTILS_CHECK_ERROR_RETURN( + esp_timer_stop(lvgl_tick_timer), false, "Stop LVGL tick timer failed" + ); + ESP_UTILS_CHECK_ERROR_RETURN( + esp_timer_delete(lvgl_tick_timer), false, "Delete LVGL tick timer failed" + ); + return true; +} +#endif + +static void lvgl_port_task(void *arg) +{ + ESP_UTILS_LOGD("Starting LVGL task"); + + uint32_t task_delay_ms = LVGL_PORT_TASK_MAX_DELAY_MS; + while (1) { + if (lvgl_port_lock(-1)) { + task_delay_ms = lv_timer_handler(); + lvgl_port_unlock(); + } + if (task_delay_ms > LVGL_PORT_TASK_MAX_DELAY_MS) { + task_delay_ms = LVGL_PORT_TASK_MAX_DELAY_MS; + } else if (task_delay_ms < LVGL_PORT_TASK_MIN_DELAY_MS) { + task_delay_ms = LVGL_PORT_TASK_MIN_DELAY_MS; + } + vTaskDelay(pdMS_TO_TICKS(task_delay_ms)); + } +} + +IRAM_ATTR bool onDrawBitmapFinishCallback(void *user_data) +{ + lv_disp_drv_t *drv = (lv_disp_drv_t *)user_data; + + lv_disp_flush_ready(drv); + + return false; +} + +bool lvgl_port_init(LCD *lcd, Touch *tp) +{ + ESP_UTILS_CHECK_FALSE_RETURN(lcd != nullptr, false, "Invalid LCD device"); + + auto bus_type = lcd->getBus()->getBasicAttributes().type; +#if LVGL_PORT_AVOID_TEAR + ESP_UTILS_CHECK_FALSE_RETURN( + (bus_type == ESP_PANEL_BUS_TYPE_RGB) || (bus_type == ESP_PANEL_BUS_TYPE_MIPI_DSI), false, + "Avoid tearing function only works with RGB/MIPI-DSI LCD now" + ); + ESP_UTILS_LOGI( + "Avoid tearing is enabled, mode: %d, rotation: %d", LVGL_PORT_AVOID_TEARING_MODE, LVGL_PORT_ROTATION_DEGREE + ); +#endif + + lv_disp_t *disp = nullptr; + lv_indev_t *indev = nullptr; + + lv_init(); +#if !LV_TICK_CUSTOM + ESP_UTILS_CHECK_FALSE_RETURN(tick_init(), false, "Initialize LVGL tick failed"); +#endif + + ESP_UTILS_LOGI("Initializing LVGL display driver"); + disp = display_init(lcd); + ESP_UTILS_CHECK_NULL_RETURN(disp, false, "Initialize LVGL display driver failed"); + // Record the initial rotation of the display + lv_disp_set_rotation(disp, LV_DISP_ROT_NONE); + + // For non-RGB LCD, need to notify LVGL that the buffer is ready when the refresh is finished + if (bus_type != ESP_PANEL_BUS_TYPE_RGB) { + ESP_UTILS_LOGD("Attach refresh finish callback to LCD"); + lcd->attachDrawBitmapFinishCallback(onDrawBitmapFinishCallback, (void *)disp->driver); + } + + if (tp != nullptr) { + ESP_UTILS_LOGD("Initialize LVGL input driver"); + indev = indev_init(tp); + ESP_UTILS_CHECK_NULL_RETURN(indev, false, "Initialize LVGL input driver failed"); + +#if LVGL_PORT_ROTATION_DEGREE != 0 + auto &transformation = tp->getTransformation(); +#if LVGL_PORT_ROTATION_DEGREE == 90 + tp->swapXY(!transformation.swap_xy); + tp->mirrorY(!transformation.mirror_y); +#elif LVGL_PORT_ROTATION_DEGREE == 180 + tp->mirrorX(!transformation.mirror_x); + tp->mirrorY(!transformation.mirror_y); +#elif LVGL_PORT_ROTATION_DEGREE == 270 + tp->swapXY(!transformation.swap_xy); + tp->mirrorX(!transformation.mirror_x); +#endif +#endif + } + + ESP_UTILS_LOGD("Create mutex for LVGL"); + lvgl_mux = xSemaphoreCreateRecursiveMutex(); + ESP_UTILS_CHECK_NULL_RETURN(lvgl_mux, false, "Create LVGL mutex failed"); + + ESP_UTILS_LOGD("Create LVGL task"); + BaseType_t core_id = (LVGL_PORT_TASK_CORE < 0) ? tskNO_AFFINITY : LVGL_PORT_TASK_CORE; + BaseType_t ret = xTaskCreatePinnedToCore(lvgl_port_task, "lvgl", LVGL_PORT_TASK_STACK_SIZE, NULL, + LVGL_PORT_TASK_PRIORITY, &lvgl_task_handle, core_id); + ESP_UTILS_CHECK_FALSE_RETURN(ret == pdPASS, false, "Create LVGL task failed"); + +#if LVGL_PORT_AVOID_TEAR + lcd->attachRefreshFinishCallback(onLcdVsyncCallback, (void *)lvgl_task_handle); +#endif + + return true; +} + +bool lvgl_port_lock(int timeout_ms) +{ + ESP_UTILS_CHECK_NULL_RETURN(lvgl_mux, false, "LVGL mutex is not initialized"); + + const TickType_t timeout_ticks = (timeout_ms < 0) ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms); + return (xSemaphoreTakeRecursive(lvgl_mux, timeout_ticks) == pdTRUE); +} + +bool lvgl_port_unlock(void) +{ + ESP_UTILS_CHECK_NULL_RETURN(lvgl_mux, false, "LVGL mutex is not initialized"); + + xSemaphoreGiveRecursive(lvgl_mux); + + return true; +} + +bool lvgl_port_deinit(void) +{ +#if !LV_TICK_CUSTOM + ESP_UTILS_CHECK_FALSE_RETURN(tick_deinit(), false, "Deinitialize LVGL tick failed"); +#endif + + ESP_UTILS_CHECK_FALSE_RETURN(lvgl_port_lock(-1), false, "Lock LVGL failed"); + if (lvgl_task_handle != nullptr) { + vTaskDelete(lvgl_task_handle); + lvgl_task_handle = nullptr; + } + ESP_UTILS_CHECK_FALSE_RETURN(lvgl_port_unlock(), false, "Unlock LVGL failed"); + +#if LV_ENABLE_GC || !LV_MEM_CUSTOM + lv_deinit(); +#else + ESP_UTILS_LOGW("LVGL memory is custom, `lv_deinit()` will not work"); +#endif +#if !LVGL_PORT_AVOID_TEAR + for (int i = 0; i < LVGL_PORT_BUFFER_NUM; i++) { + if (lvgl_buf[i] != nullptr) { + free(lvgl_buf[i]); + lvgl_buf[i] = nullptr; + } + } +#endif + if (lvgl_mux != nullptr) { + vSemaphoreDelete(lvgl_mux); + lvgl_mux = nullptr; + } + + return true; +} diff --git a/firmware/esp32-s3-dashboard/lvgl_v8_port.h b/firmware/esp32-s3-dashboard/lvgl_v8_port.h new file mode 100644 index 0000000..ce8ed05 --- /dev/null +++ b/firmware/esp32-s3-dashboard/lvgl_v8_port.h @@ -0,0 +1,174 @@ +/* + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ +#pragma once + +#include "sdkconfig.h" +#ifdef CONFIG_ARDUINO_RUNNING_CORE +#include +#endif +#include "esp_display_panel.hpp" +#include "lvgl.h" + +// *INDENT-OFF* + +/** + * LVGL related parameters, can be adjusted by users + */ +#define LVGL_PORT_TICK_PERIOD_MS (2) // The period of the LVGL tick task, in milliseconds + +/** + * + * LVGL buffer related parameters, can be adjusted by users: + * + * (These parameters will be useless if the avoid tearing function is enabled) + * + * - Memory type for buffer allocation: + * - MALLOC_CAP_SPIRAM: Allocate LVGL buffer in PSRAM + * - MALLOC_CAP_INTERNAL: Allocate LVGL buffer in SRAM + * + * (The SRAM is faster than PSRAM, but the PSRAM has a larger capacity) + * (For SPI/QSPI LCD, it is recommended to allocate the buffer in SRAM, because the SPI DMA does not directly support PSRAM now) + * + * - The size (in bytes) and number of buffers: + * - Lager buffer size can improve FPS, but it will occupy more memory. Maximum buffer size is `width * height`. + * - The number of buffers should be 1 or 2. + */ +#define LVGL_PORT_BUFFER_MALLOC_CAPS (MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT) // Allocate LVGL buffer in SRAM +// #define LVGL_PORT_BUFFER_MALLOC_CAPS (MALLOC_CAP_SPIRAM) // Allocate LVGL buffer in PSRAM +#define LVGL_PORT_BUFFER_SIZE_HEIGHT (20) +#define LVGL_PORT_BUFFER_NUM (2) + +/** + * LVGL timer handle task related parameters, can be adjusted by users + */ +#define LVGL_PORT_TASK_MAX_DELAY_MS (500) // The maximum delay of the LVGL timer task, in milliseconds +#define LVGL_PORT_TASK_MIN_DELAY_MS (2) // The minimum delay of the LVGL timer task, in milliseconds +#define LVGL_PORT_TASK_STACK_SIZE (6 * 1024) // The stack size of the LVGL timer task, in bytes +#define LVGL_PORT_TASK_PRIORITY (2) // The priority of the LVGL timer task +#ifdef ARDUINO_RUNNING_CORE +#define LVGL_PORT_TASK_CORE (ARDUINO_RUNNING_CORE) // Valid if using Arduino +#else +#define LVGL_PORT_TASK_CORE (1) // Valid if using ESP-IDF +#endif + // The core of the LVGL timer task, `-1` means the don't specify the core + // Default is the same as the main core + // This can be set to `1` only if the SoCs support dual-core, + // otherwise it should be set to `-1` or `0` + +/** + * Avoid tering related configurations, can be adjusted by users. + * + * (Currently, This function only supports RGB LCD and the version of LVGL must be >= 8.3.9) + */ +/** + * Set the avoid tearing mode: + * - 0: Disable avoid tearing function + * - 1: LCD double-buffer & LVGL full-refresh + * - 2: LCD triple-buffer & LVGL full-refresh + * - 3: LCD double-buffer & LVGL direct-mode (recommended) + */ +#ifdef CONFIG_LVGL_PORT_AVOID_TEARING_MODE +#define LVGL_PORT_AVOID_TEARING_MODE (CONFIG_LVGL_PORT_AVOID_TEARING_MODE) + // Valid if using ESP-IDF +#else +#define LVGL_PORT_AVOID_TEARING_MODE (3) // Valid if using Arduino +#endif + +#if LVGL_PORT_AVOID_TEARING_MODE != 0 +/** + * When avoid tearing is enabled, the LVGL software rotation `lv_disp_set_rotation()` is not supported. + * But users can set the rotation degree(0/90/180/270) here, but this function will reduce FPS. + * + * Set the rotation degree: + * - 0: 0 degree + * - 90: 90 degree + * - 180: 180 degree + * - 270: 270 degree + */ +#ifdef CONFIG_LVGL_PORT_ROTATION_DEGREE +#define LVGL_PORT_ROTATION_DEGREE (CONFIG_LVGL_PORT_ROTATION_DEGREE) + // Valid if using ESP-IDF +#else +#define LVGL_PORT_ROTATION_DEGREE (0) // Valid if using Arduino +#endif + +/** + * Here, some important configurations will be set based on different anti-tearing modes and rotation angles. + * No modification is required here. + * + * Users should use `lcd_bus->configRgbFrameBufferNumber(LVGL_PORT_DISP_BUFFER_NUM);` to set the buffer number before. If screen drifting occurs, please refer to the Troubleshooting section in the README. + * initializing the LCD bus + */ +#define LVGL_PORT_AVOID_TEAR (1) +// Set the buffer number and refresh mode according to the different modes +#if LVGL_PORT_AVOID_TEARING_MODE == 1 + #define LVGL_PORT_DISP_BUFFER_NUM (2) + #define LVGL_PORT_FULL_REFRESH (1) +#elif LVGL_PORT_AVOID_TEARING_MODE == 2 + #define LVGL_PORT_DISP_BUFFER_NUM (3) + #define LVGL_PORT_FULL_REFRESH (1) +#elif LVGL_PORT_AVOID_TEARING_MODE == 3 + #define LVGL_PORT_DISP_BUFFER_NUM (2) + #define LVGL_PORT_DIRECT_MODE (1) +#else + #error "Invalid avoid tearing mode, please set macro `LVGL_PORT_AVOID_TEARING_MODE` to one of `LVGL_PORT_AVOID_TEARING_MODE_*`" +#endif +// Check rotation +#if (LVGL_PORT_ROTATION_DEGREE != 0) && (LVGL_PORT_ROTATION_DEGREE != 90) && (LVGL_PORT_ROTATION_DEGREE != 180) && \ + (LVGL_PORT_ROTATION_DEGREE != 270) + #error "Invalid rotation degree, please set to 0, 90, 180 or 270" +#elif LVGL_PORT_ROTATION_DEGREE != 0 + #ifdef LVGL_PORT_DISP_BUFFER_NUM + #undef LVGL_PORT_DISP_BUFFER_NUM + #define LVGL_PORT_DISP_BUFFER_NUM (3) + #endif +#endif +#endif /* LVGL_PORT_AVOID_TEARING_MODE */ + +// *INDENT-ON* + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Porting LVGL with LCD and touch panel. This function should be called after the initialization of the LCD and touch panel. + * + * @param lcd The pointer to the LCD panel device, mustn't be nullptr + * @param tp The pointer to the touch panel device, set to nullptr if is not used + * + * @return true if success, otherwise false + */ +bool lvgl_port_init(esp_panel::drivers::LCD *lcd, esp_panel::drivers::Touch *tp); + +/** + * @brief Deinitialize the LVGL porting. + * + * @return true if success, otherwise false + */ +bool lvgl_port_deinit(void); + +/** + * @brief Lock the LVGL mutex. This function should be called before calling any LVGL APIs when not in LVGL task, + * and the `lvgl_port_unlock()` function should be called later. + * + * @param timeout_ms The timeout of the mutex lock, in milliseconds. If the timeout is set to `-1`, it will wait indefinitely. + * + * @return true if success, otherwise false + */ +bool lvgl_port_lock(int timeout_ms); + +/** + * @brief Unlock the LVGL mutex. This function should be called after using LVGL APIs when not in LVGL task, and the + * `lvgl_port_lock()` function should be called before. + * + * @return true if success, otherwise false + */ +bool lvgl_port_unlock(void); + +#ifdef __cplusplus +} +#endif