From 38d56d5a98e378352fa8218f9a7c9af69529fd39 Mon Sep 17 00:00:00 2001 From: Liu Zhongwei Date: Wed, 8 May 2024 20:03:27 +0800 Subject: [PATCH] feat(display): restore st77903 qspi lcd driver & example --- .github/workflows/upload_component.yml | 1 + .gitlab/ci/build.yml | 10 + .gitlab/ci/rules.yml | 16 + .gitlab/ci/target_test.yml | 20 + README.md | 1 + README_CN.md | 1 + components/.build-rules.yml | 4 + .../display/lcd/esp_lcd_st77903/README.md | 11 - .../lcd/esp_lcd_st77903_qspi/CHANGELOG.md | 33 + .../lcd/esp_lcd_st77903_qspi/CMakeLists.txt | 9 + .../display/lcd/esp_lcd_st77903_qspi/Kconfig | 45 + .../lcd/esp_lcd_st77903_qspi/README.md | 55 + .../esp_lcd_st77903_qspi.c | 1307 +++++++++++++++++ .../esp_lcd_st77903_qspi/idf_component.yml | 10 + .../include/esp_lcd_st77903_qspi.h | 267 ++++ .../lcd/esp_lcd_st77903_qspi/license.txt | 202 +++ .../test_apps/CMakeLists.txt | 6 + .../test_apps/main/CMakeLists.txt | 1 + .../test_apps/main/idf_component.yml | 7 + .../main/test_esp_lcd_st77903_qspi.c | 241 +++ .../test_apps/pytest_esp_lcd_st77903_qspi.py | 9 + .../test_apps/sdkconfig.defaults | 4 + .../test_apps/sdkconfig.defaults.esp32s3 | 7 + .../lcd/esp_lcd_st77903_rgb/CHANGELOG.md | 7 + .../display/lcd/esp_lcd_st77903_rgb/README.md | 34 +- .../esp_lcd_st77903_rgb/esp_lcd_st77903_rgb.c | 3 + .../lcd/esp_lcd_st77903_rgb/idf_component.yml | 2 +- .../include/esp_lcd_st77903_rgb.h | 13 - .../test_apps/main/test_esp_lcd_st77903_rgb.c | 176 ++- .../lcd/qspi_without_ram/CMakeLists.txt | 7 + .../display/lcd/qspi_without_ram/README.md | 138 +- .../lcd/qspi_without_ram/main/CMakeLists.txt | 12 + .../qspi_without_ram/main/Kconfig.projbuild | 118 ++ .../main/example_qspi_without_ram.c | 165 +++ .../qspi_without_ram/main/idf_component.yml | 10 + .../lcd/qspi_without_ram/main/lvgl_port.c | 586 ++++++++ .../lcd/qspi_without_ram/main/lvgl_port.h | 159 ++ .../lcd/qspi_without_ram/partitions.csv | 5 + .../sdkconfig.ci.avoid_tear_mode1 | 3 + .../sdkconfig.ci.avoid_tear_mode2 | 3 + .../sdkconfig.ci.avoid_tear_mode3 | 3 + .../qspi_without_ram/sdkconfig.ci.defaults | 0 .../lcd/qspi_without_ram/sdkconfig.defaults | 32 + 43 files changed, 3654 insertions(+), 89 deletions(-) delete mode 100644 components/display/lcd/esp_lcd_st77903/README.md create mode 100644 components/display/lcd/esp_lcd_st77903_qspi/CHANGELOG.md create mode 100644 components/display/lcd/esp_lcd_st77903_qspi/CMakeLists.txt create mode 100644 components/display/lcd/esp_lcd_st77903_qspi/Kconfig create mode 100644 components/display/lcd/esp_lcd_st77903_qspi/README.md create mode 100644 components/display/lcd/esp_lcd_st77903_qspi/esp_lcd_st77903_qspi.c create mode 100644 components/display/lcd/esp_lcd_st77903_qspi/idf_component.yml create mode 100644 components/display/lcd/esp_lcd_st77903_qspi/include/esp_lcd_st77903_qspi.h create mode 100644 components/display/lcd/esp_lcd_st77903_qspi/license.txt create mode 100644 components/display/lcd/esp_lcd_st77903_qspi/test_apps/CMakeLists.txt create mode 100644 components/display/lcd/esp_lcd_st77903_qspi/test_apps/main/CMakeLists.txt create mode 100644 components/display/lcd/esp_lcd_st77903_qspi/test_apps/main/idf_component.yml create mode 100644 components/display/lcd/esp_lcd_st77903_qspi/test_apps/main/test_esp_lcd_st77903_qspi.c create mode 100644 components/display/lcd/esp_lcd_st77903_qspi/test_apps/pytest_esp_lcd_st77903_qspi.py create mode 100644 components/display/lcd/esp_lcd_st77903_qspi/test_apps/sdkconfig.defaults create mode 100644 components/display/lcd/esp_lcd_st77903_qspi/test_apps/sdkconfig.defaults.esp32s3 create mode 100644 examples/display/lcd/qspi_without_ram/CMakeLists.txt create mode 100644 examples/display/lcd/qspi_without_ram/main/CMakeLists.txt create mode 100644 examples/display/lcd/qspi_without_ram/main/Kconfig.projbuild create mode 100644 examples/display/lcd/qspi_without_ram/main/example_qspi_without_ram.c create mode 100644 examples/display/lcd/qspi_without_ram/main/idf_component.yml create mode 100644 examples/display/lcd/qspi_without_ram/main/lvgl_port.c create mode 100644 examples/display/lcd/qspi_without_ram/main/lvgl_port.h create mode 100644 examples/display/lcd/qspi_without_ram/partitions.csv create mode 100644 examples/display/lcd/qspi_without_ram/sdkconfig.ci.avoid_tear_mode1 create mode 100644 examples/display/lcd/qspi_without_ram/sdkconfig.ci.avoid_tear_mode2 create mode 100644 examples/display/lcd/qspi_without_ram/sdkconfig.ci.avoid_tear_mode3 create mode 100644 examples/display/lcd/qspi_without_ram/sdkconfig.ci.defaults create mode 100644 examples/display/lcd/qspi_without_ram/sdkconfig.defaults diff --git a/.github/workflows/upload_component.yml b/.github/workflows/upload_component.yml index 5877859e3..e68a825d8 100644 --- a/.github/workflows/upload_component.yml +++ b/.github/workflows/upload_component.yml @@ -33,6 +33,7 @@ jobs: components/display/lcd/esp_lcd_sh8601; components/display/lcd/esp_lcd_spd2010; components/display/lcd/esp_lcd_st7701; + components/display/lcd/esp_lcd_st77903_qspi; components/display/lcd/esp_lcd_st77903_rgb; components/display/lcd/esp_lcd_st77916; components/display/lcd/esp_lcd_st77922; diff --git a/.gitlab/ci/build.yml b/.gitlab/ci/build.yml index 51a17dac8..4601f848c 100644 --- a/.gitlab/ci/build.yml +++ b/.gitlab/ci/build.yml @@ -812,6 +812,16 @@ build_components_display_lcd_esp_lcd_st7701_test_apps: variables: EXAMPLE_DIR: components/display/lcd/esp_lcd_st7701/test_apps +build_components_display_lcd_esp_lcd_st77903_qspi_test_apps: + extends: + - .build_examples_template + - .rules:build:components_display_lcd_esp_lcd_st77903_qspi_test_apps + parallel: + matrix: + - IMAGE: espressif/idf:release-v5.3 + variables: + EXAMPLE_DIR: components/display/lcd/esp_lcd_st77903_qspi/test_apps + build_components_display_lcd_esp_lcd_st77903_rgb_test_apps: extends: - .build_examples_template diff --git a/.gitlab/ci/rules.yml b/.gitlab/ci/rules.yml index ee97bc3c6..bb96397a5 100644 --- a/.gitlab/ci/rules.yml +++ b/.gitlab/ci/rules.yml @@ -86,6 +86,9 @@ .patterns-components_display_lcd_esp_lcd_st7701: &patterns-components_display_lcd_esp_lcd_st7701 - "components/display/lcd/esp_lcd_st7701/**/*" +.patterns-components_display_lcd_esp_lcd_st77903_qspi: &patterns-components_display_lcd_esp_lcd_st77903_qspi + - "components/display/lcd/esp_lcd_st77903_qspi/**/*" + .patterns-components_display_lcd_esp_lcd_st77903_rgb: &patterns-components_display_lcd_esp_lcd_st77903_rgb - "components/display/lcd/esp_lcd_st77903_rgb/**/*" @@ -705,6 +708,8 @@ - <<: *if-trigger-job - <<: *if-dev-push changes: *patterns-build_system + - <<: *if-dev-push + changes: *patterns-components_display_lcd_esp_lcd_st77903_qspi - <<: *if-dev-push changes: *patterns-example_display_lcd_qspi_without_ram @@ -1386,6 +1391,17 @@ - <<: *if-dev-push changes: *patterns-components_display_lcd_esp_lcd_st7701 +.rules:build:components_display_lcd_esp_lcd_st77903_qspi_test_apps: + rules: + - <<: *if-protected + - <<: *if-label-build + - <<: *if-label-target_test + - <<: *if-trigger-job + - <<: *if-dev-push + changes: *patterns-build_system + - <<: *if-dev-push + changes: *patterns-components_display_lcd_esp_lcd_st77903_qspi + .rules:build:components_display_lcd_esp_lcd_st77903_rgb_test_apps: rules: - <<: *if-protected diff --git a/.gitlab/ci/target_test.yml b/.gitlab/ci/target_test.yml index 44e5382d3..710dcbd7d 100644 --- a/.gitlab/ci/target_test.yml +++ b/.gitlab/ci/target_test.yml @@ -392,6 +392,26 @@ components_test_esp_lcd_st7701: TEST_FOLDER: components/display/lcd/esp_lcd_st7701 TEST_ENV: esp32_s3_lcd_ev_board +components_test_esp_lcd_st77903_qspi: + extends: + - .pytest_template + - .rules:build:components_display_lcd_esp_lcd_st77903_qspi_test_apps + needs: + - job: "build_components_display_lcd_esp_lcd_st77903_qspi_test_apps" + artifacts: true + optional: false + parallel: + matrix: + - IDF_VERSION: "5.3" + tags: + - esp32s3 + - esp32s3_lcd_ev + image: $DOCKER_TARGET_TEST_v5_1_ENV_IMAGE + variables: + TEST_TARGET: esp32s3 + TEST_FOLDER: components/display/lcd/esp_lcd_st77903_qspi + TEST_ENV: esp32_s3_lcd_ev_board + components_test_esp_lcd_st77903_rgb: extends: - .pytest_template diff --git a/README.md b/README.md index 88e31d487..03549da45 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ The registered components in ESP-IoT-Solution are listed below: | [esp_lcd_sh8601](https://components.espressif.com/components/espressif/esp_lcd_sh8601) | [![Component Registry](https://components.espressif.com/components/espressif/esp_lcd_sh8601/badge.svg)](https://components.espressif.com/components/espressif/esp_lcd_sh8601) | | [esp_lcd_spd2010](https://components.espressif.com/components/espressif/esp_lcd_spd2010) | [![Component Registry](https://components.espressif.com/components/espressif/esp_lcd_spd2010/badge.svg)](https://components.espressif.com/components/espressif/esp_lcd_spd2010) | | [esp_lcd_st7701](https://components.espressif.com/components/espressif/esp_lcd_st7701) | [![Component Registry](https://components.espressif.com/components/espressif/esp_lcd_st7701/badge.svg)](https://components.espressif.com/components/espressif/esp_lcd_st7701) | +| [esp_lcd_st77903_qspi](https://components.espressif.com/components/espressif/esp_lcd_st77903_qspi) | [![Component Registry](https://components.espressif.com/components/espressif/esp_lcd_st77903_qspi/badge.svg)](https://components.espressif.com/components/espressif/esp_lcd_st77903_qspi) | [esp_lcd_st77903_rgb](https://components.espressif.com/components/espressif/esp_lcd_st77903_rgb) | [![Component Registry](https://components.espressif.com/components/espressif/esp_lcd_st77903_rgb/badge.svg)](https://components.espressif.com/components/espressif/esp_lcd_st77903_rgb) | [esp_lcd_st77916](https://components.espressif.com/components/espressif/esp_lcd_st77916) | [![Component Registry](https://components.espressif.com/components/espressif/esp_lcd_st77916/badge.svg)](https://components.espressif.com/components/espressif/esp_lcd_st77916) | [esp_lcd_st77922](https://components.espressif.com/components/espressif/esp_lcd_st77922) | [![Component Registry](https://components.espressif.com/components/espressif/esp_lcd_st77922/badge.svg)](https://components.espressif.com/components/espressif/esp_lcd_st77922) diff --git a/README_CN.md b/README_CN.md index db41fd4f8..6fe3fe7b1 100644 --- a/README_CN.md +++ b/README_CN.md @@ -66,6 +66,7 @@ ESP-IoT-Solution 中注册的组件如下: | [esp_lcd_sh8601](https://components.espressif.com/components/espressif/esp_lcd_sh8601) | [![Component Registry](https://components.espressif.com/components/espressif/esp_lcd_sh8601/badge.svg)](https://components.espressif.com/components/espressif/esp_lcd_sh8601) | | [esp_lcd_spd2010](https://components.espressif.com/components/espressif/esp_lcd_spd2010) | [![Component Registry](https://components.espressif.com/components/espressif/esp_lcd_spd2010/badge.svg)](https://components.espressif.com/components/espressif/esp_lcd_spd2010) | | [esp_lcd_st7701](https://components.espressif.com/components/espressif/esp_lcd_st7701) | [![Component Registry](https://components.espressif.com/components/espressif/esp_lcd_st7701/badge.svg)](https://components.espressif.com/components/espressif/esp_lcd_st7701) | +| [esp_lcd_st77903_qspi](https://components.espressif.com/components/espressif/esp_lcd_st77903_qspi) | [![Component Registry](https://components.espressif.com/components/espressif/esp_lcd_st77903_qspi/badge.svg)](https://components.espressif.com/components/espressif/esp_lcd_st77903_qspi) | [esp_lcd_st77903_rgb](https://components.espressif.com/components/espressif/esp_lcd_st77903_rgb) | [![Component Registry](https://components.espressif.com/components/espressif/esp_lcd_st77903_rgb/badge.svg)](https://components.espressif.com/components/espressif/esp_lcd_st77903_rgb) | [esp_lcd_st77916](https://components.espressif.com/components/espressif/esp_lcd_st77916) | [![Component Registry](https://components.espressif.com/components/espressif/esp_lcd_st77916/badge.svg)](https://components.espressif.com/components/espressif/esp_lcd_st77916) | [esp_lcd_st77922](https://components.espressif.com/components/espressif/esp_lcd_st77922) | [![Component Registry](https://components.espressif.com/components/espressif/esp_lcd_st77922/badge.svg)](https://components.espressif.com/components/espressif/esp_lcd_st77922) diff --git a/components/.build-rules.yml b/components/.build-rules.yml index f9c26fd50..b7acfef8f 100644 --- a/components/.build-rules.yml +++ b/components/.build-rules.yml @@ -82,6 +82,10 @@ components/display/lcd/esp_lcd_st7701/test_apps: enable: - if: IDF_TARGET in ["esp32s3"] +components/display/lcd/esp_lcd_st77903_qspi: + enable: + - if: IDF_TARGET in ["esp32s3"] + components/display/lcd/esp_lcd_st77903_rgb: enable: - if: IDF_TARGET in ["esp32s3"] diff --git a/components/display/lcd/esp_lcd_st77903/README.md b/components/display/lcd/esp_lcd_st77903/README.md deleted file mode 100644 index 32be6137b..000000000 --- a/components/display/lcd/esp_lcd_st77903/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# ESP LCD ST77903 QSPI - -Implementation of the ST77903 LCD controller with [esp_lcd](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/lcd.html) component. - -| LCD controller | Communication interface | Component name | Link to datasheet | -| :------------: | :---------------------: | :------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -| ST77903 | QSPI/RGB | esp_lcd_ST77903 | [PDF1](https://dl.espressif.com/AE/esp-iot-solution/ST77903_SPEC_P0.5.pdf), [PDF2](https://dl.espressif.com/AE/esp-iot-solution/ST77903_Customer_Application_Notes.pdf) | - -## How to get the component - -The QSPI driver for ST77903 relies on certain new features that have not been merged into the `ESP-IDF` on Github. Currently, this driver is available on the internal Glab repository. If you require access, please reach out to our business team or contact us at `sales@espressif.com` to obtain Glab permissions. diff --git a/components/display/lcd/esp_lcd_st77903_qspi/CHANGELOG.md b/components/display/lcd/esp_lcd_st77903_qspi/CHANGELOG.md new file mode 100644 index 000000000..43ac028c2 --- /dev/null +++ b/components/display/lcd/esp_lcd_st77903_qspi/CHANGELOG.md @@ -0,0 +1,33 @@ +# ChangeLog + +## v0.4.1- 2024-05-09 + +### Enhancements: + +* Upload the driver to the espressif repository +* Update test_apps +* use `xPortInIsrContext()` to check whether the function is called in the ISR + +### bugfix + +* Fix the non-reentrant of the `lcd_write_cmd` function + +## v0.4.0- 2024-01-30 + +### Enhancements: + +* Retain the RGB interface driver in the esp-iot-solution repository, and keep only the QSPI interface driver in this repository + +## v0.3.1 - 2023-11-03 + +### bugfix + +* Fix the missing check of flags `auto_del_panel_io` and `mirror_by_cmd` for RGB interface +* Remove LCD command `29h` from the initialization sequence for QSPI interface + +## v0.3.0 - 2023-09-08 + +### Enhancements: + +* Implement the driver for the ST77903 LCD controller +* Support QSPI and RGB interface diff --git a/components/display/lcd/esp_lcd_st77903_qspi/CMakeLists.txt b/components/display/lcd/esp_lcd_st77903_qspi/CMakeLists.txt new file mode 100644 index 000000000..d6e1115e4 --- /dev/null +++ b/components/display/lcd/esp_lcd_st77903_qspi/CMakeLists.txt @@ -0,0 +1,9 @@ +idf_component_register( + SRCS "esp_lcd_st77903_qspi.c" + INCLUDE_DIRS "include" + PRIV_REQUIRES esp_timer esp_psram + REQUIRES driver esp_lcd +) + +include(package_manager) +cu_pkg_define_version(${CMAKE_CURRENT_LIST_DIR}) diff --git a/components/display/lcd/esp_lcd_st77903_qspi/Kconfig b/components/display/lcd/esp_lcd_st77903_qspi/Kconfig new file mode 100644 index 000000000..a87e12c29 --- /dev/null +++ b/components/display/lcd/esp_lcd_st77903_qspi/Kconfig @@ -0,0 +1,45 @@ +menu "LCD ST77903 QSPI Configuration" + config LCD_ST77903_ISR_IRAM_SAFE + bool "LCD ISR IRAM-Safe" + default n + help + Ensure the LCD interrupt is IRAM-Safe by allowing the interrupt handler to be + executable when the cache is disabled (e.g. SPI Flash write). + If you want the LCD driver to keep flushing the screen even when cache ops disabled, + you can enable this option. Note, this will also increase the IRAM usage. + + config LCD_VSYNC_FRONT_NUM + int "LCD VSYNC Front Porch" + default 8 + range 0 65535 + + config LCD_VSYNC_BACK_NUM + int "LCD VSYNC Back Porch" + default 8 + range 0 65535 + + config LCD_LINE_INTERVAL_MIN_US + int "Minimum LCD Line Interval (us)" + default 42 + range 40 100 + + config LCD_TASK_CHECK_TIME_MS + int "LCD Task Check Time (ms)" + default 10 + range 1 100 + + config LCD_TASK_STOP_WAIT_TIME_MS + int "LCD Task Stop Wait Time (ms)" + default 200 + range 1 1000 + + config LCD_TASK_STOP_TIME_MAX_MS + int "Maximum LCD Task Stop Time (ms)" + default 1000 + range 1 10000 + + config LCD_READ_WAIT_TIME_MAX_MS + int "LCD Read Wait Time (ms)" + default 10 + range 1 100 +endmenu diff --git a/components/display/lcd/esp_lcd_st77903_qspi/README.md b/components/display/lcd/esp_lcd_st77903_qspi/README.md new file mode 100644 index 000000000..5898a4c02 --- /dev/null +++ b/components/display/lcd/esp_lcd_st77903_qspi/README.md @@ -0,0 +1,55 @@ +# ESP LCD ST77903 (QSPI) + +Implementation of the ST77903 QSPI LCD controller with [esp_lcd](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/lcd.html) component. + +| LCD controller | Communication interface | Component name | Link to datasheet | +| :------------: | :---------------------: | :------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| ST77903 | QSPI | esp_lcd_ST77903_qspi | [PDF1](https://dl.espressif.com/AE/esp-iot-solution/ST77903_SPEC_P0.5.pdf), [PDF2](https://dl.espressif.com/AE/esp-iot-solution/ST77903_Customer_Application_Notes.pdf) | + +## Initialization Code + +```c +/** + * Uncomment these line if use custom initialization commands. + * The array should be declared as static const and positioned outside the function. + */ +// static const st77903_lcd_init_cmd_t lcd_init_cmds[] = { +// // {cmd, { data }, data_size, delay_ms} +// {0xf0, (uint8_t []){0xc3}, 1, 0}, +// {0xf0, (uint8_t []){0x96}, 1, 0}, +// {0xf0, (uint8_t []){0xa5}, 1, 0}, +// ... +// }; + + ESP_LOGI(TAG, "Install st77903 panel driver"); + esp_lcd_panel_handle_t panel_handle = NULL; + st77903_qspi_config_t qspi_config = ST77903_QSPI_CONFIG_DEFAULT(EXAMPLE_LCD_HOST, + EXAMPLE_PIN_NUM_LCD_QSPI_CS, + EXAMPLE_PIN_NUM_LCD_QSPI_PCLK, + EXAMPLE_PIN_NUM_LCD_QSPI_DATA0, + EXAMPLE_PIN_NUM_LCD_QSPI_DATA1, + EXAMPLE_PIN_NUM_LCD_QSPI_DATA2, + EXAMPLE_PIN_NUM_LCD_QSPI_DATA3, + 1, + EXAMPLE_LCD_QSPI_H_RES, + EXAMPLE_LCD_QSPI_V_RES); + st77903_vendor_config_t vendor_config = { + .qspi_config = &qspi_config, + // .init_cmds = lcd_init_cmds, // Uncomment these line if use custom initialization commands + // .init_cmds_size = sizeof(lcd_init_cmds) / sizeof(st77903_lcd_init_cmd_t), + .flags = { + .mirror_by_cmd = 1, // Implemented by LCD command `36h` + }, + }; + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = EXAMPLE_PIN_NUM_LCD_RST, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, // Implemented by LCD command `36h` + .bits_per_pixel = EXAMPLE_LCD_BIT_PER_PIXEL, // Implemented by LCD command `3Ah` (16/18/24) + .vendor_config = &vendor_config, + }; + ESP_ERROR_CHECK(esp_lcd_new_panel_st77903(&panel_config, &panel_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel_handle, true, false)); // This function can only be called when the refresh task is not running + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true)); // This function can control the display on/off and the refresh task run/stop + ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle)); // Start the refresh task +``` diff --git a/components/display/lcd/esp_lcd_st77903_qspi/esp_lcd_st77903_qspi.c b/components/display/lcd/esp_lcd_st77903_qspi/esp_lcd_st77903_qspi.c new file mode 100644 index 000000000..4a9700dda --- /dev/null +++ b/components/display/lcd/esp_lcd_st77903_qspi/esp_lcd_st77903_qspi.c @@ -0,0 +1,1307 @@ +/* + * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "freertos/queue.h" +#include "hal/dma_types.h" +#include "hal/spi_ll.h" +#include "rom/cache.h" +#include "soc/soc.h" +#include "driver/gpio.h" +#include "driver/spi_master.h" +#include "esp_check.h" +#include "esp_heap_caps.h" +#include "esp_lcd_panel_commands.h" +#include "esp_lcd_panel_interface.h" +#include "esp_lcd_panel_ops.h" +#include "esp_private/spi_common_internal.h" +#include "esp_private/spi_master_internal.h" +#include "esp_psram.h" +#include "esp_timer.h" +#include "sdkconfig.h" + +#include "esp_lcd_st77903_qspi.h" + +#define LCD_VSYNC_FRONT_NUM (CONFIG_LCD_VSYNC_FRONT_NUM) +#define LCD_VSYNC_BACK_NUM (CONFIG_LCD_VSYNC_BACK_NUM) + +#define SPI_CMD_BITS (8) +#define SPI_PARAM_BITS (24) +#define SPI_MODE (0) + +#define SPI_SEG_GAP_BASIC_US (2) +#define SPI_SEG_GAP_GET_US(hor_res, bytes) MAX(0, (int)(MAX(LCD_LINE_INTERVAL_MIN_US, (hor_res + 9) / 10) - \ + bytes / 20 - SPI_SEG_GAP_BASIC_US)) +#define SPI_SEG_GAP_GET_CLK_LEN(time_us) (time_us * (APB_CLK_FREQ / 1000000)) + +#define LCD_LINE_INTERVAL_MIN_US (CONFIG_LCD_LINE_INTERVAL_MIN_US) + +#define TASK_CHECK_TIME_MS (CONFIG_LCD_TASK_CHECK_TIME_MS) +#define TASK_STOP_WAIT_TIME_MS (CONFIG_LCD_TASK_STOP_WAIT_TIME_MS) +#define TASK_STOP_TIME_MAX_MS (CONFIG_LCD_TASK_STOP_TIME_MAX_MS) +#define READ_WAIT_TIME_MAX_MS (CONFIG_LCD_READ_WAIT_TIME_MAX_MS) + +#define ST77903_INS_DATA (0xDE) +#define ST77903_INS_READ (0xDD) +#define ST77903_INS_CMD (0xD8) +#define ST77903_CMD_HSYNC (0x60) +#define ST77903_CMD_VSYNC (0x61) +#define ST77903_CMD_BPC (0xB5) +#define ST77903_CMD_DISCN (0xB6) + +typedef struct { + int id; +} custom_spi_device_t ; + +typedef struct { + void *panel; + bool is_color; + bool is_vfp; +} custom_user_data_t; + +typedef struct { + esp_lcd_panel_t base; + // SPI host + spi_host_device_t spi_host_id; + spi_device_handle_t spi_write_dev; + spi_device_handle_t spi_read_dev; + spi_multi_transaction_t **trans_pool; + spi_multi_transaction_t vsync_front_pool[LCD_VSYNC_FRONT_NUM]; + spi_multi_transaction_t vsync_back_pool[LCD_VSYNC_BACK_NUM + 1]; + spi_multi_transaction_t write_cmd_seg; + size_t trans_pool_size; + size_t trans_pool_num; + // Frame buffer + void **fbs; + size_t fb_num; + size_t bits_per_pixel; + size_t bytes_per_pixel; + size_t bytes_per_line; + size_t bytes_per_fb; + uint16_t cur_fb_index; + uint16_t next_fb_index; + uint16_t load_line_cnt; + uint16_t write_pool_index; + SemaphoreHandle_t sem_count_free_trans; + custom_user_data_t trans_user; + custom_user_data_t vfp_user; + custom_user_data_t vbp_user; + void *user_ctx; // Reserved user's data of callback functions + st77903_qspi_vsync_cb_t on_vsync; + // Boucne buffer + uint16_t load_bb_index; + uint16_t cur_bb_fb_index; + void **bbs; + size_t bb_size; + QueueHandle_t queue_load_mem_info; + st77903_qspi_bounce_buf_finish_cb_t on_bounce_frame_finish; + // Read register + uint8_t read_reg; + uint8_t *read_data; + uint8_t read_data_size; + SemaphoreHandle_t sem_read_ready; + SemaphoreHandle_t sem_read_done; + // Others + int cs_io_num; + int reset_gpio_num; + struct { + unsigned int enable_cal_fps : 1; + unsigned int fb_in_psram : 1; + unsigned int reset_level: 1; + unsigned int skip_init_host: 1; + unsigned int stop_refresh_task: 1; + unsigned int stop_load_task: 1; + unsigned int panel_need_reconfig: 1; + unsigned int enable_read_reg: 1; + unsigned int wait_frame_end: 1; + unsigned int mirror_by_cmd: 1; + } flags; + struct { + uint8_t refresh_priority; + uint16_t refresh_size; + int refresh_core; + TaskHandle_t refresh_task_handle; + uint8_t load_priority; + uint16_t load_size; + int load_core; + TaskHandle_t load_task_handle; + } task; + uint8_t fps; + uint16_t hor_res; + uint16_t ver_res; + const st77903_lcd_init_cmd_t *init_cmds; + uint16_t init_cmds_size; + int rotate_mask; // Panel rotate_mask mask, Or'ed of `panel_rotate_mask_t` + 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 + uint8_t madctl_val; // Save current value of LCD_CMD_MADCTL register + uint8_t colmod_val; // Save current value of LCD_CMD_COLMOD register +} st77903_qspi_panel_t; + +typedef struct { + void *from; + void *to; + void *next_buf; + uint32_t bytes; + uint32_t next_buf_bytes; +} queue_load_mem_info_t; + +#define PANEL_SWAP_XY 0 +#define PANEL_MIRROR_Y 1 +#define PANEL_MIRROR_X 2 + +typedef enum { + ROTATE_MASK_SWAP_XY = BIT(PANEL_SWAP_XY), + ROTATE_MASK_MIRROR_Y = BIT(PANEL_MIRROR_Y), + ROTATE_MASK_MIRROR_X = BIT(PANEL_MIRROR_X), +} panel_rotate_mask_t; + +static const char *TAG = "st77903.qspi"; + +static bool load_trans_pool(st77903_qspi_panel_t *panel, spi_multi_transaction_t *seg_trans); +static void post_trans_color_cb(spi_transaction_t *trans); +static esp_err_t lcd_cmd_config(st77903_qspi_panel_t *panel); +static esp_err_t lcd_write_color(st77903_qspi_panel_t *panel); +static esp_err_t lcd_write_cmd(st77903_qspi_panel_t *panel, uint8_t cmd, const void *param, size_t param_size); +static void load_memory_task(void *arg); +static void refresh_task(void *arg); +static esp_err_t stop_refresh(st77903_qspi_panel_t *panel); + +static esp_err_t panel_st77903_qspi_init(esp_lcd_panel_t *handle); +static esp_err_t panel_st77903_qspi_reset(esp_lcd_panel_t *handle); +static esp_err_t panel_st77903_qspi_del(esp_lcd_panel_t *handle); +static esp_err_t panel_st77903_qspi_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_st77903_qspi_mirror(esp_lcd_panel_t *handle, bool mirror_x, bool mirror_y); +static esp_err_t panel_st77903_qspi_swap_xy(esp_lcd_panel_t *handle, bool swap_axes); +static esp_err_t panel_st77903_qspi_set_gap(esp_lcd_panel_t *handle, int x_gap, int y_gap); +static esp_err_t panel_st77903_qspi_disp_on_off(esp_lcd_panel_t *handle, bool on_off); + +static esp_err_t st77903_qspi_alloc_buffers(st77903_qspi_panel_t *panel) +{ + // fb_in_psram is only an option, if there's no PSRAM on board, we fallback to alloc from SRAM + bool fb_in_psram = false; + if (panel->flags.fb_in_psram) { +#if CONFIG_SPIRAM_USE_MALLOC || SPIRAM_USE_MALLOC + if (esp_psram_is_initialized()) { + fb_in_psram = true; + } +#endif + } + + // Alloc frame buffer + panel->fbs = (void **)heap_caps_malloc(panel->fb_num * sizeof(void *), MALLOC_CAP_INTERNAL); + for (int i = 0; i < panel->fb_num; i++) { + if (fb_in_psram) { + panel->fbs[i] = heap_caps_calloc(1, panel->bytes_per_fb, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + } else { + panel->fbs[i] = heap_caps_calloc(1, panel->bytes_per_fb, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA); + } + ESP_RETURN_ON_FALSE(panel->fbs[i], ESP_ERR_NO_MEM, TAG, "No mem for frame buffer(%dKB)", panel->bytes_per_fb / 1024); + } + panel->cur_fb_index = 0; + panel->next_fb_index = 0; + panel->flags.fb_in_psram = fb_in_psram; + ESP_LOGD(TAG, "Frame buffer size: %zu, total: %zuKB", panel->bytes_per_fb, panel->fb_num * panel->bytes_per_fb / 1024); + + // Alloc bounce buffer + if (panel->bb_size) { + panel->bbs = (void **)heap_caps_malloc(panel->trans_pool_num * sizeof(void *), MALLOC_CAP_INTERNAL); + for (int i = 0; i < panel->trans_pool_num; i++) { + panel->bbs[i] = heap_caps_aligned_calloc(32, 1, panel->bb_size, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA); + ESP_RETURN_ON_FALSE(panel->bbs[i], ESP_ERR_NO_MEM, TAG, "No mem for bounce buffer(%d, need %dKB)", + panel->trans_pool_num, panel->trans_pool_num * panel->bb_size / 1024); + } + ESP_LOGD(TAG, "Bounce buffer size: %zu, total: %zuKB", panel->bb_size, panel->trans_pool_num * panel->bb_size / 1024); + panel->cur_bb_fb_index = 0; + } + + // Alloc trans pool + panel->trans_pool = (spi_multi_transaction_t **)heap_caps_malloc(panel->trans_pool_num * sizeof(spi_multi_transaction_t *), MALLOC_CAP_INTERNAL); + uint16_t trans_pool_bytes = panel->trans_pool_size * sizeof(spi_transaction_t); + for (int i = 0; i < panel->trans_pool_num; i++) { + panel->trans_pool[i] = (spi_multi_transaction_t *)heap_caps_calloc(panel->trans_pool_size, sizeof(spi_multi_transaction_t), MALLOC_CAP_DMA); + ESP_RETURN_ON_FALSE(panel->trans_pool[i], ESP_ERR_NO_MEM, TAG, "No mem for trans pool(%d, need %dKB)", + panel->trans_pool_size, trans_pool_bytes / 1024); + } + ESP_LOGD(TAG, "Trans pool size: %zu, total: %zuKB", panel->trans_pool_size, panel->trans_pool_num * trans_pool_bytes / 1024); + + // Init the constant data for trans pool + panel->trans_user.panel = (void *)panel; + panel->trans_user.is_color = true; + spi_multi_transaction_t trans_pool_temp = { + .base = { + .cmd = ST77903_INS_DATA, + .length = panel->bytes_per_line * 8, + .addr = (((uint32_t)ST77903_CMD_HSYNC) << 8), + .flags = SPI_TRANS_MODE_QIO, + .user = (void *) &panel->trans_user, + }, + .sct_gap_len = SPI_SEG_GAP_GET_CLK_LEN(SPI_SEG_GAP_GET_US(panel->hor_res, panel->bytes_per_line)), + }; + ESP_LOGD(TAG, "segment_gap_clock_len: %ld", trans_pool_temp.sct_gap_len); + + for (int i = 0; i < panel->trans_pool_num; i++) { + for (int j = 0; j < panel->trans_pool_size; j++) { + memcpy(&panel->trans_pool[i][j], &trans_pool_temp, sizeof(spi_multi_transaction_t)); + } + } + + // Init the constant data for vsync pool + panel->vbp_user.panel = (void *)panel; + panel->vbp_user.is_color = false; + panel->vbp_user.is_vfp = false; + spi_multi_transaction_t vsync_pool_temp = { + .base = { + .cmd = ST77903_INS_CMD, + .length = 0, + .addr = ((uint32_t)ST77903_CMD_HSYNC) << 8, + .user = (void *) &panel->vbp_user, + }, + .sct_gap_len = SPI_SEG_GAP_GET_CLK_LEN(LCD_LINE_INTERVAL_MIN_US), + }; + for (int i = 0; i < LCD_VSYNC_BACK_NUM + 1; i++) { + memcpy(&panel->vsync_back_pool[i], &vsync_pool_temp, sizeof(spi_multi_transaction_t)); + } + panel->vsync_back_pool[0].base.addr = ((uint32_t)ST77903_CMD_VSYNC) << 8; + + panel->vfp_user.panel = (void *)panel; + panel->vfp_user.is_color = false; + panel->vfp_user.is_vfp = true; + vsync_pool_temp.base.user = (void *)&panel->vfp_user; + for (int i = 0; i < LCD_VSYNC_FRONT_NUM; i++) { + memcpy(&panel->vsync_front_pool[i], &vsync_pool_temp, sizeof(spi_multi_transaction_t)); + } + + return ESP_OK; +} + +static int get_spi_device_id(spi_device_handle_t dev) +{ + return ((custom_spi_device_t *)dev)->id; +} + +esp_err_t esp_lcd_new_panel_st77903_qspi(const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel) +{ + ESP_RETURN_ON_FALSE(panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, TAG, "Invalid arguements"); + st77903_vendor_config_t *vendor_config = (st77903_vendor_config_t *)panel_dev_config->vendor_config; + + ESP_RETURN_ON_FALSE(vendor_config, ESP_ERR_INVALID_ARG, TAG, "`vendor_config` is necessary"); + const st77903_qspi_config_t *config = vendor_config->qspi_config; + ESP_RETURN_ON_FALSE(config->trans_pool_size && config->trans_pool_num, ESP_ERR_INVALID_ARG, TAG, "Trans pool size and num must > 0"); + ESP_RETURN_ON_FALSE(config->hor_res && config->ver_res, ESP_ERR_INVALID_ARG, TAG, "Screen resolution must > 0"); + + esp_err_t ret = ESP_OK; + st77903_qspi_panel_t *panel = NULL; + size_t bytes_per_pixel = ((panel_dev_config->bits_per_pixel > 16) ? 24 : 16) / 8; + size_t dma_nodes_per_spi_trans = 1 + lldesc_get_required_num(bytes_per_pixel * config->hor_res); + + // Init SPI bus if necessary + if (!config->flags.skip_init_host) { + spi_bus_config_t spi_config = { + .sclk_io_num = config->qspi.sclk_io_num, + .data0_io_num = config->qspi.data0_io_num, + .data1_io_num = config->qspi.data1_io_num, + .data2_io_num = config->qspi.data2_io_num, + .data3_io_num = config->qspi.data3_io_num, + .flags = SPICOMMON_BUSFLAG_QUAD, + .max_transfer_sz = config->trans_pool_size * config->trans_pool_num * dma_nodes_per_spi_trans * + DMA_DESCRIPTOR_BUFFER_MAX_SIZE * 2, + }; + ESP_RETURN_ON_ERROR(spi_bus_initialize(config->qspi.host_id, &spi_config, SPI_DMA_CH_AUTO), TAG, "SPI bus init failed"); + ESP_LOGD(TAG, "SPI max transfer size: %dKB", spi_config.max_transfer_sz / 1024); + ESP_LOGD(TAG, "Init SPI bus[%d]", config->qspi.host_id); + } + + spi_device_handle_t spi_read_device = NULL; + if (config->flags.enable_read_reg) { + spi_device_interface_config_t dev_config = { + .command_bits = SPI_CMD_BITS, + .address_bits = SPI_PARAM_BITS, + .mode = SPI_MODE, + .clock_speed_hz = config->qspi.read_pclk_hz, + .spics_io_num = config->qspi.cs_io_num, + .queue_size = 2, + .flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_3WIRE, + }; + ESP_GOTO_ON_ERROR(spi_bus_add_device((spi_host_device_t)config->qspi.host_id, &dev_config, &spi_read_device), err, + TAG, "Create spi read device failed"); + ESP_LOGD(TAG, "Create SPI read device[%d]", get_spi_device_id(spi_read_device)); + } + + // Create SPI device which is used to write data + spi_device_handle_t spi_write_device = NULL; + spi_device_interface_config_t dev_config = { + .command_bits = SPI_CMD_BITS, + .address_bits = SPI_PARAM_BITS, + .mode = SPI_MODE, + .clock_speed_hz = config->qspi.write_pclk_hz, + .spics_io_num = config->qspi.cs_io_num, + .queue_size = config->trans_pool_num, + .flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_NO_RETURN_RESULT, + .post_cb = post_trans_color_cb, + }; + ESP_GOTO_ON_ERROR(spi_bus_add_device((spi_host_device_t)config->qspi.host_id, &dev_config, &spi_write_device), err, + TAG, "Create spi write device failed"); + ESP_GOTO_ON_ERROR(spi_bus_multi_trans_mode_enable(spi_write_device, true), err, TAG, "Segment mode enable failed"); + ESP_LOGD(TAG, "Create SPI write device[%d]", get_spi_device_id(spi_write_device)); + + 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"); + } + + panel = (st77903_qspi_panel_t *)heap_caps_calloc(1, sizeof(st77903_qspi_panel_t), MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); + ESP_GOTO_ON_FALSE(panel, ESP_ERR_NO_MEM, err, TAG, "Malloc failed"); + + switch (panel_dev_config->rgb_ele_order) { + case LCD_RGB_ELEMENT_ORDER_RGB: + panel->madctl_val = 0; + break; + case LCD_RGB_ELEMENT_ORDER_BGR: + panel->madctl_val |= LCD_CMD_BGR_BIT; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported color element order"); + break; + } + + switch (panel_dev_config->bits_per_pixel) { + case 16: // RGB565 + panel->colmod_val = 0x05; + break; + case 18: // RGB666 + panel->colmod_val = 0x06; + break; + case 24: // RGB888 + panel->colmod_val = 0x07; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "Unsupported pixel width"); + break; + } + + // SPI host + panel->spi_host_id = config->qspi.host_id; + panel->spi_write_dev = spi_write_device; + panel->spi_read_dev = spi_read_device; + panel->trans_pool_size = config->trans_pool_size; + panel->trans_pool_num = config->trans_pool_num; + // Frame buffer + panel->fb_num = (config->fb_num == 0) ? 1 : config->fb_num; + panel->bits_per_pixel = panel_dev_config->bits_per_pixel; + panel->bytes_per_pixel = bytes_per_pixel; + panel->bytes_per_line = config->hor_res * bytes_per_pixel; + panel->bytes_per_fb = panel->bytes_per_line * config->ver_res; + panel->load_line_cnt = 0; + panel->write_pool_index = 0; + // Bounce buffer + panel->load_bb_index = 0; + if (config->flags.fb_in_psram) { + panel->bb_size = panel->trans_pool_size * panel->bytes_per_line; + } + // Others + panel->cs_io_num = config->qspi.cs_io_num; + panel->reset_gpio_num = panel_dev_config->reset_gpio_num; + panel->flags.enable_cal_fps = config->flags.enable_cal_fps; + panel->flags.fb_in_psram = config->flags.fb_in_psram; + panel->flags.reset_level = panel_dev_config->flags.reset_active_high; + panel->flags.skip_init_host = config->flags.skip_init_host; + panel->flags.stop_refresh_task = 1; + panel->flags.stop_load_task = 1; + panel->flags.panel_need_reconfig = 1; + panel->flags.enable_read_reg = config->flags.enable_read_reg; + panel->flags.wait_frame_end = 0; + panel->flags.mirror_by_cmd = vendor_config->flags.mirror_by_cmd; + panel->task.refresh_priority = config->task.refresh_priority; + panel->task.refresh_core = config->task.refresh_core; + panel->task.refresh_size = config->task.refresh_size; + panel->task.load_priority = config->task.load_priority; + panel->task.load_core = config->task.load_core; + panel->task.load_size = config->task.load_size; + panel->hor_res = config->hor_res; + panel->ver_res = config->ver_res; + panel->init_cmds = vendor_config->init_cmds; + panel->init_cmds_size = vendor_config->init_cmds_size; + panel->base.init = panel_st77903_qspi_init; + panel->base.draw_bitmap = panel_st77903_qspi_draw_bitmap; + panel->base.reset = panel_st77903_qspi_reset; + panel->base.del = panel_st77903_qspi_del; + panel->base.mirror = panel_st77903_qspi_mirror; + panel->base.swap_xy = panel_st77903_qspi_swap_xy; + panel->base.set_gap = panel_st77903_qspi_set_gap; + panel->base.disp_on_off = panel_st77903_qspi_disp_on_off; + + ESP_GOTO_ON_ERROR(st77903_qspi_alloc_buffers(panel), err, TAG, "Alloc buffers failed"); + + // Create semaphore for refresh task + panel->sem_count_free_trans = xSemaphoreCreateCounting(panel->trans_pool_num, 0); + ESP_RETURN_ON_FALSE(panel->sem_count_free_trans, ESP_ERR_NO_MEM, TAG, "No mem for refresh semaphore"); + // Create queue for load task + if (panel->flags.fb_in_psram) { + panel->queue_load_mem_info = xQueueCreate(panel->trans_pool_num, sizeof(queue_load_mem_info_t)); + ESP_RETURN_ON_FALSE(panel->queue_load_mem_info, ESP_ERR_NO_MEM, TAG, "No mem for queue load mem info"); + } + + if (panel->flags.enable_read_reg) { + // Create semaphore for reading register + panel->sem_read_ready = xSemaphoreCreateBinary(); + panel->sem_read_done = xSemaphoreCreateBinary(); + ESP_RETURN_ON_FALSE(panel->sem_read_ready && panel->sem_read_done, ESP_ERR_NO_MEM, TAG, "No mem for read semaphore"); + } + + *ret_panel = &panel->base; + ESP_LOGD(TAG, "Create panel @%p", panel); + + return ESP_OK; + +err: + if (spi_write_device) { + spi_bus_remove_device(spi_write_device); + } + if (spi_read_device) { + spi_bus_remove_device(spi_read_device); + } + if (!config->flags.skip_init_host) { + spi_bus_free(config->qspi.host_id); + } + free(panel); + + return ret; +} + +esp_err_t esp_lcd_st77903_qspi_register_event_callbacks(esp_lcd_panel_t *handle, const st77903_qspi_event_callbacks_t *callbacks, void *user_ctx) +{ + ESP_RETURN_ON_FALSE(handle && callbacks, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + st77903_qspi_panel_t *panel = __containerof(handle, st77903_qspi_panel_t, base); +#if CONFIG_LCD_ST77903_ISR_IRAM_SAFE + if (callbacks->on_vsync) { + ESP_RETURN_ON_FALSE(esp_ptr_in_iram(callbacks->on_vsync), ESP_ERR_INVALID_ARG, TAG, "on_vsync callback not in IRAM"); + } + if (callbacks->on_bounce_frame_finish) { + ESP_RETURN_ON_FALSE(esp_ptr_in_iram(callbacks->on_bounce_frame_finish), ESP_ERR_INVALID_ARG, TAG, "on_bounce_frame_finish callback not in IRAM"); + } + if (user_ctx) { + ESP_RETURN_ON_FALSE(esp_ptr_in_iram(user_ctx), ESP_ERR_INVALID_ARG, TAG, "user_ctx not in IRAM"); + } +#endif + panel->on_vsync = callbacks->on_vsync; + panel->on_bounce_frame_finish = callbacks->on_bounce_frame_finish; + panel->user_ctx = user_ctx; + return ESP_OK; +} + +esp_err_t esp_lcd_st77903_qspi_get_fps(esp_lcd_panel_handle_t handle, uint8_t *fps) +{ + st77903_qspi_panel_t *panel = __containerof(handle, st77903_qspi_panel_t, base); + ESP_RETURN_ON_FALSE(panel && fps, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + ESP_RETURN_ON_FALSE(panel->flags.enable_cal_fps, ESP_ERR_INVALID_STATE, TAG, "FPS calculation is disabled, please enable it first"); + + *fps = panel->fps; + return ESP_OK; +} + +static esp_err_t panel_st77903_qspi_init(esp_lcd_panel_t *handle) +{ + st77903_qspi_panel_t *panel = __containerof(handle, st77903_qspi_panel_t, base); + if (panel->task.refresh_task_handle != NULL) { + return ESP_OK; + } + + if (panel->flags.fb_in_psram) { + // Because data in PSRAM can't be transmitted by SPI DMA currently, + // so we have to copy them from frame buffer (PSRAM) to bounce buffer (SRAM) first. + // Load memory task is used to copy data from frame buffer to bounce buffer + panel->flags.stop_load_task = 0; + xTaskCreatePinnedToCore( + load_memory_task, "lcd_load_mem", panel->task.load_size, panel, panel->task.load_priority, + &panel->task.load_task_handle, panel->task.load_core + ); + } + + // Configure the LCD using initial commands + if (panel->flags.panel_need_reconfig) { + panel->flags.panel_need_reconfig = 0; + ESP_RETURN_ON_ERROR(lcd_cmd_config(panel), TAG, "LCD cmd config failed"); + ESP_LOGD(TAG, "LCD cmd config done"); + } + + // Here, we should load transaction pool in advance to meet fast SPI timing + // Then the rest loading operations will be finished by `post_trans_color_cb()` automatically + panel->load_bb_index = 0; + panel->load_line_cnt = 0; + for (int i = 0; i < panel->trans_pool_num; i++) { + load_trans_pool(panel, panel->trans_pool[i]); + } + // Refresh task is used to transmit the segments through SPI + panel->write_pool_index = 0; + panel->flags.stop_refresh_task = 0; + xTaskCreatePinnedToCore( + refresh_task, "lcd_refresh", panel->task.refresh_size, panel, panel->task.refresh_priority, + &panel->task.refresh_task_handle, panel->task.refresh_core + ); + + return ESP_OK; +} + +static esp_err_t panel_st77903_qspi_reset(esp_lcd_panel_t *handle) +{ + st77903_qspi_panel_t *panel = __containerof(handle, st77903_qspi_panel_t, base); + + // Stop LCD refreh related tasks + ESP_RETURN_ON_ERROR(stop_refresh(panel), TAG, "Stop refresh failed"); + + // Perform hardware reset + if (panel->reset_gpio_num >= 0) { + gpio_set_level(panel->reset_gpio_num, !panel->flags.reset_level); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(panel->reset_gpio_num, panel->flags.reset_level); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(panel->reset_gpio_num, !panel->flags.reset_level); + vTaskDelay(pdMS_TO_TICKS(120)); + } else { + // Perform software reset + ESP_RETURN_ON_ERROR(lcd_write_cmd(panel, LCD_CMD_SWRESET, NULL, 0), TAG, "LCD write cmd failed"); + vTaskDelay(pdMS_TO_TICKS(120)); + } + panel->flags.panel_need_reconfig = 1; + + return ESP_OK; +} + +static esp_err_t panel_st77903_qspi_del(esp_lcd_panel_t *handle) +{ + st77903_qspi_panel_t *panel = __containerof(handle, st77903_qspi_panel_t, base); + + // Stop LCD refreh related tasks + ESP_RETURN_ON_ERROR(stop_refresh(panel), TAG, "Stop refresh failed"); + + // Reset gpio + if (panel->reset_gpio_num >= 0) { + gpio_reset_pin(panel->reset_gpio_num); + } + + // Reset SPI + ESP_RETURN_ON_ERROR(spi_bus_remove_device(panel->spi_write_dev), TAG, "SPI write device remove failed"); + if (panel->flags.enable_read_reg) { + ESP_RETURN_ON_ERROR(spi_bus_remove_device(panel->spi_read_dev), TAG, "SPI read device remove failed"); + } + if (!panel->flags.skip_init_host) { + ESP_RETURN_ON_ERROR(spi_bus_free(panel->spi_host_id), TAG, "SPI bus free failed"); + } + + // Free others + if (panel->flags.enable_read_reg) { + vSemaphoreDelete(panel->sem_read_ready); + vSemaphoreDelete(panel->sem_read_done); + } + + // Free buffers' memory + for (int i = 0; i < panel->trans_pool_num; i++) { + free(panel->trans_pool[i]); + } + for (int i = 0; i < panel->fb_num; i++) { + free(panel->fbs[i]); + } + free(panel->fbs); + vSemaphoreDelete(panel->sem_count_free_trans); + if (panel->flags.fb_in_psram) { + vQueueDelete(panel->queue_load_mem_info); + for (int i = 0; i < panel->trans_pool_num; i++) { + free(panel->bbs[i]); + } + free(panel->bbs); + } + + ESP_LOGD(TAG, "Del panel @%p", panel); + free(panel); + + return ESP_OK; +} + +__attribute__((always_inline)) +static inline void copy_pixel_16bpp(uint8_t *to, const uint8_t *from) +{ + *to++ = *from++; + *to++ = *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_CODE_BLOCK(_bpp) \ + switch (panel->rotate_mask) \ + { \ + case 0: \ + { \ + uint8_t *to = fb + (y_start * h_res + x_start) * bytes_per_pixel; \ + for (int y = y_start; y < y_end; y++) \ + { \ + memcpy(to, from, copy_bytes_per_line); \ + to += bytes_per_line; \ + from += copy_bytes_per_line; \ + } \ + } \ + break; \ + case ROTATE_MASK_MIRROR_X: \ + for (int y = y_start; y < y_end; y++) \ + { \ + uint32_t index = (y * h_res + (h_res - 1 - x_start)) * bytes_per_pixel; \ + for (size_t x = x_start; x < x_end; x++) \ + { \ + copy_pixel_##_bpp##bpp(to + index, from); \ + index -= bytes_per_pixel; \ + from += bytes_per_pixel; \ + } \ + } \ + break; \ + case ROTATE_MASK_MIRROR_Y: \ + { \ + uint8_t *to = fb + ((v_res - 1 - y_start) * h_res + x_start) * bytes_per_pixel; \ + for (int y = y_start; y < y_end; y++) \ + { \ + memcpy(to, from, copy_bytes_per_line); \ + to -= bytes_per_line; \ + from += copy_bytes_per_line; \ + } \ + } \ + break; \ + case ROTATE_MASK_MIRROR_X | ROTATE_MASK_MIRROR_Y: \ + for (int y = y_start; y < y_end; y++) \ + { \ + uint32_t index = ((v_res - 1 - y) * h_res + (h_res - 1 - x_start)) * bytes_per_pixel; \ + for (size_t x = x_start; x < x_end; x++) \ + { \ + copy_pixel_##_bpp##bpp(to + index, from); \ + index -= bytes_per_pixel; \ + from += bytes_per_pixel; \ + } \ + } \ + break; \ + case ROTATE_MASK_SWAP_XY: \ + for (int y = y_start; y < y_end; y++) \ + { \ + for (int x = x_start; x < x_end; x++) \ + { \ + uint32_t j = y * copy_bytes_per_line + x * bytes_per_pixel - offset; \ + uint32_t i = (x * h_res + y) * bytes_per_pixel; \ + copy_pixel_##_bpp##bpp(to + i, from + j); \ + } \ + } \ + break; \ + case ROTATE_MASK_SWAP_XY | ROTATE_MASK_MIRROR_X: \ + for (int y = y_start; y < y_end; y++) \ + { \ + for (int x = x_start; x < x_end; x++) \ + { \ + uint32_t j = y * copy_bytes_per_line + x * bytes_per_pixel - offset; \ + uint32_t i = (x * h_res + h_res - 1 - y) * bytes_per_pixel; \ + copy_pixel_##_bpp##bpp(to + i, from + j); \ + } \ + } \ + break; \ + case ROTATE_MASK_SWAP_XY | ROTATE_MASK_MIRROR_Y: \ + for (int y = y_start; y < y_end; y++) \ + { \ + for (int x = x_start; x < x_end; x++) \ + { \ + uint32_t j = y * copy_bytes_per_line + x * bytes_per_pixel - offset; \ + uint32_t i = ((v_res - 1 - x) * h_res + y) * bytes_per_pixel; \ + copy_pixel_##_bpp##bpp(to + i, from + j); \ + } \ + } \ + break; \ + case ROTATE_MASK_SWAP_XY | ROTATE_MASK_MIRROR_X | ROTATE_MASK_MIRROR_Y: \ + for (int y = y_start; y < y_end; y++) \ + { \ + for (int x = x_start; x < x_end; x++) \ + { \ + uint32_t j = y * copy_bytes_per_line + x * bytes_per_pixel - offset; \ + uint32_t i = ((v_res - 1 - x) * h_res + h_res - 1 - y) * bytes_per_pixel; \ + copy_pixel_##_bpp##bpp(to + i, from + j); \ + } \ + } \ + break; \ + default: \ + break; \ + } + +static esp_err_t panel_st77903_qspi_draw_bitmap(esp_lcd_panel_t *handle, int x_start, int y_start, int x_end, int y_end, const void *color_data) +{ + st77903_qspi_panel_t *panel = __containerof(handle, st77903_qspi_panel_t, base); + + ESP_RETURN_ON_FALSE((x_start >= 0) && (y_start >= 0), ESP_ERR_INVALID_ARG, TAG, "start position must be bigger than zero point"); + ESP_RETURN_ON_FALSE((x_start < x_end) && (y_start < y_end), ESP_ERR_INVALID_ARG, TAG, "start position must be smaller than end position"); + ESP_RETURN_ON_FALSE(color_data, ESP_ERR_INVALID_ARG, TAG, "Invalid color_data"); + + void **frame_buf = panel->fbs; + bool do_copy = true; + for (int i = 0; i < panel->fb_num; i++) { + if (frame_buf[i] == color_data) { + panel->next_fb_index = i; + do_copy = false; + break; + } + } + + // adjust the flush window by adding extra gap + x_start += panel->x_gap; + y_start += panel->y_gap; + x_end += panel->x_gap; + y_end += panel->y_gap; + // round the boundary + int h_res = panel->hor_res; + int v_res = panel->ver_res; + if (panel->rotate_mask & ROTATE_MASK_SWAP_XY) { + x_start = MIN(x_start, v_res); + x_end = MIN(x_end, v_res); + y_start = MIN(y_start, h_res); + y_end = MIN(y_end, h_res); + } else { + x_start = MIN(x_start, h_res); + x_end = MIN(x_end, h_res); + y_start = MIN(y_start, v_res); + y_end = MIN(y_end, v_res); + } + + int bytes_per_pixel = panel->bytes_per_pixel; + int pixels_per_line = panel->hor_res; + uint32_t bytes_per_line = bytes_per_pixel * pixels_per_line; + uint8_t *fb = frame_buf[panel->cur_fb_index]; + + if (do_copy) { + // copy the UI draw buffer into internal frame buffer + const uint8_t *from = (const uint8_t *)color_data; + uint32_t copy_bytes_per_line = (x_end - x_start) * bytes_per_pixel; + size_t offset = y_start * copy_bytes_per_line + x_start * bytes_per_pixel; + uint8_t *to = fb; + if (2 == bytes_per_pixel) { + COPY_PIXEL_CODE_BLOCK(16) + } else if (3 == bytes_per_pixel) { + COPY_PIXEL_CODE_BLOCK(24) + } + } + + return ESP_OK; +} + +static esp_err_t panel_st77903_qspi_mirror(esp_lcd_panel_t *handle, bool mirror_x, bool mirror_y) +{ + st77903_qspi_panel_t *panel = __containerof(handle, st77903_qspi_panel_t, base); + + if (panel->flags.mirror_by_cmd) { + ESP_RETURN_ON_FALSE(panel->task.refresh_task_handle == NULL, ESP_ERR_INVALID_STATE, TAG, + "Mirror by command is not supported when refresh task is running, " + "please call `esp_lcd_panel_disp_on_off()` to stop refresh task first"); + // Control mirror through LCD command + if (mirror_x) { + panel->madctl_val |= LCD_CMD_MH_BIT; + } else { + panel->madctl_val &= ~LCD_CMD_MH_BIT; + } + if (mirror_y) { + panel->madctl_val |= LCD_CMD_ML_BIT; + } else { + panel->madctl_val &= ~LCD_CMD_ML_BIT; + } + ESP_RETURN_ON_ERROR(lcd_write_cmd(panel, LCD_CMD_MADCTL, &panel->madctl_val, 1), TAG, "LCD write cmd failed"); + } else { + // Control mirror through software + panel->rotate_mask &= ~(ROTATE_MASK_MIRROR_X | ROTATE_MASK_MIRROR_Y); + panel->rotate_mask |= (mirror_x << PANEL_MIRROR_X | mirror_y << PANEL_MIRROR_Y); + } + return ESP_OK; +} + +static esp_err_t panel_st77903_qspi_swap_xy(esp_lcd_panel_t *handle, bool swap_axes) +{ + st77903_qspi_panel_t *panel = __containerof(handle, st77903_qspi_panel_t, base); + panel->rotate_mask &= ~(ROTATE_MASK_SWAP_XY); + panel->rotate_mask |= swap_axes << PANEL_SWAP_XY; + return ESP_OK; +} + +static esp_err_t panel_st77903_qspi_set_gap(esp_lcd_panel_t *handle, int x_gap, int y_gap) +{ + ESP_RETURN_ON_FALSE(x_gap >= 0 && y_gap >= 0, ESP_ERR_INVALID_ARG, TAG, "x_gap and y_gap should not be less than 0"); + st77903_qspi_panel_t *panel = __containerof(handle, st77903_qspi_panel_t, base); + panel->x_gap = x_gap; + panel->y_gap = y_gap; + + return ESP_OK; +} + +static esp_err_t stop_refresh(st77903_qspi_panel_t *panel) +{ + if (panel->task.refresh_task_handle == NULL) { + return ESP_OK; + } + + int64_t start_time = esp_timer_get_time(); + int64_t end_time = TASK_STOP_TIME_MAX_MS * 1000 + start_time; + + // Stop refrehs & load mem task + panel->flags.stop_refresh_task = 1; + while ((panel->task.refresh_task_handle != NULL) && (esp_timer_get_time() < end_time)) { + vTaskDelay(pdTICKS_TO_MS(TASK_CHECK_TIME_MS)); + } + while ((panel->task.load_task_handle != NULL) && (esp_timer_get_time() < end_time)) { + vTaskDelay(pdTICKS_TO_MS(TASK_CHECK_TIME_MS)); + } + ESP_RETURN_ON_FALSE(esp_timer_get_time() < end_time, ESP_ERR_TIMEOUT, TAG, "Stop refresh task timeout, " + "please increase task priority or `TASK_STOP_TIME_MAX_MS`"); + + vTaskDelay(pdTICKS_TO_MS(TASK_STOP_WAIT_TIME_MS)); + if (panel->queue_load_mem_info) { + xQueueReset(panel->queue_load_mem_info); + } + for (int i = 0; i < panel->trans_pool_size; i++) { + xSemaphoreTake(panel->sem_count_free_trans, 0); + } + + return ESP_OK; +} + +static esp_err_t panel_st77903_qspi_disp_on_off(esp_lcd_panel_t *handle, bool on_off) +{ + st77903_qspi_panel_t *panel = __containerof(handle, st77903_qspi_panel_t, base); + + if (!on_off) { + // Stop refresh task before sending the command + ESP_RETURN_ON_ERROR(stop_refresh(panel), TAG, "Stop refresh failed"); + // Send command to turn off display + ESP_RETURN_ON_ERROR(lcd_write_cmd(panel, LCD_CMD_DISPOFF, NULL, 0), TAG, "send command failed"); + } else { + ESP_RETURN_ON_FALSE(panel->task.refresh_task_handle == NULL, ESP_ERR_INVALID_STATE, TAG, + "Cannot set display on when refresh task is running"); + // Send command to turn on display + ESP_RETURN_ON_ERROR(lcd_write_cmd(panel, LCD_CMD_DISPON, NULL, 0), TAG, "send command failed"); + // Start refresh task if it is not running + ESP_RETURN_ON_ERROR(panel_st77903_qspi_init(handle), TAG, "Start refresh failed"); + } + + return ESP_OK; +} + +esp_err_t esp_lcd_st77903_qspi_get_frame_buffer(esp_lcd_panel_handle_t handle, uint32_t fb_num, void **fb0, ...) +{ + ESP_RETURN_ON_FALSE(handle, ESP_ERR_INVALID_ARG, TAG, "Invalid handle"); + + st77903_qspi_panel_t *panel = __containerof(handle, st77903_qspi_panel_t, base); + ESP_RETURN_ON_FALSE(fb_num && fb_num <= panel->fb_num, ESP_ERR_INVALID_ARG, TAG, "Frame buffer num out of range(< %d)", panel->fb_num); + + void **fb_itor = fb0; + va_list args; + va_start(args, fb0); + for (int i = 0; i < fb_num; i++) { + if (fb_itor) { + *fb_itor = panel->fbs[i]; + fb_itor = va_arg(args, void **); + } + } + va_end(args); + return ESP_OK; +} + +esp_err_t esp_lcd_st77903_qspi_read_reg(esp_lcd_panel_handle_t handle, uint8_t reg, uint8_t *data, size_t data_size, TickType_t timeout) +{ + ESP_RETURN_ON_FALSE(handle, ESP_ERR_INVALID_ARG, TAG, "Invalid handle"); + ESP_RETURN_ON_FALSE(data && data_size, ESP_ERR_INVALID_ARG, TAG, "Invalid data"); + st77903_qspi_panel_t *panel = __containerof(handle, st77903_qspi_panel_t, base); + ESP_RETURN_ON_FALSE(panel->flags.enable_read_reg, ESP_ERR_NOT_SUPPORTED, TAG, "Read reg is not enabled"); + + panel->read_reg = reg; + panel->read_data = data; + panel->read_data_size = data_size; + panel->flags.wait_frame_end = true; + if (xSemaphoreTake(panel->sem_read_done, timeout) != pdTRUE) { + return ESP_ERR_TIMEOUT; + } + + return ESP_OK; +} + +IRAM_ATTR static bool load_trans_pool(st77903_qspi_panel_t *panel, spi_multi_transaction_t *seg_trans) +{ + uint32_t cur_bb_fb_index = panel->cur_bb_fb_index; + uint32_t cur_fb_index = panel->cur_fb_index; + uint32_t load_line_cnt = panel->load_line_cnt; + uint16_t trans_num = MIN(panel->trans_pool_size, panel->ver_res - load_line_cnt); + uint8_t *load_buf; + uint8_t *next_load_buf; + uint16_t next_trans_num; + void **fbs = panel->fbs; + void **bbs = panel->bbs; + BaseType_t need_yield = pdFALSE; + bool in_isr = (xPortInIsrContext() == pdTRUE); + + panel->load_line_cnt += trans_num; + // We should switch to next frame buffer after the last line of the frame is loaded + if (panel->load_line_cnt >= panel->ver_res) { + panel->load_line_cnt = 0; + if (panel->flags.fb_in_psram) { + panel->cur_bb_fb_index = panel->next_fb_index; + } + panel->cur_fb_index = panel->next_fb_index; + if (panel->on_bounce_frame_finish) { + if (panel->on_bounce_frame_finish(&panel->base, NULL, panel->user_ctx)) { + need_yield = pdTRUE; + } + } + } + + if (panel->flags.fb_in_psram) { + // If frame buffers are in PSRAM, they can't be transmitted by SPI DMA currently, so we have to use bounce buffers to transmit them + // Here, we assemble the addresses to queue and use load memory task to copy data from frame buffer to bounce buffer + load_buf = (uint8_t *)fbs[cur_bb_fb_index] + load_line_cnt * panel->bytes_per_line; + next_load_buf = (uint8_t *)fbs[panel->cur_bb_fb_index] + panel->load_line_cnt * panel->bytes_per_line; + next_trans_num = MIN(panel->trans_pool_size, panel->ver_res - panel->load_line_cnt); + queue_load_mem_info_t load_info = { + .from = (void *)load_buf, + .to = (void *)bbs[panel->load_bb_index], + .bytes = trans_num * panel->bytes_per_line, + .next_buf = (void *)next_load_buf, + .next_buf_bytes = next_trans_num * panel->bytes_per_line, + }; + if (in_isr) { + xQueueSendFromISR(panel->queue_load_mem_info, &load_info, &need_yield); + } else { + xQueueSend(panel->queue_load_mem_info, &load_info, portMAX_DELAY); + } + load_buf = (uint8_t *)(bbs[panel->load_bb_index]); + panel->load_bb_index++; + if (panel->load_bb_index >= panel->trans_pool_num) { + panel->load_bb_index = 0; + } + } else { + // If frame buffers are in SRAM, they can be transmitted by SPI DMA directly + // So, we should release semaphore to allow refresh task continue + load_buf = (uint8_t *)fbs[cur_fb_index] + load_line_cnt * panel->bytes_per_line; + if (in_isr) { + xSemaphoreGiveFromISR(panel->sem_count_free_trans, &need_yield); + } else { + xSemaphoreGive(panel->sem_count_free_trans); + } + } + + for (int i = 0; i < trans_num; i++) { + seg_trans[i].base.tx_buffer = (void *)load_buf; + load_buf += panel->bytes_per_line; + } + + return (need_yield == pdTRUE); +} + +IRAM_ATTR static void post_trans_color_cb(spi_transaction_t *trans) +{ + BaseType_t need_yield = pdFALSE; + custom_user_data_t *user = (custom_user_data_t *)trans->user; + + if (user != NULL) { + st77903_qspi_panel_t *panel = user->panel; + if (user->is_color) { // Trans color end + if (load_trans_pool(panel, (spi_multi_transaction_t *)trans)) { + need_yield = pdTRUE; + } + } else if (panel->flags.enable_read_reg && user->is_vfp && panel->flags.wait_frame_end) { // Trans VFP end + xSemaphoreGiveFromISR(panel->sem_read_ready, &need_yield); + } else if (!user->is_vfp) { // Trans VBP end + esp_rom_delay_us(LCD_LINE_INTERVAL_MIN_US); + if (panel->on_vsync) { + if (panel->on_vsync(&panel->base, NULL, panel->user_ctx)) { + need_yield = pdTRUE; + } + } + } + } + + if (need_yield == pdTRUE) { + portYIELD_FROM_ISR(); + } +} + +static esp_err_t lcd_write_cmd(st77903_qspi_panel_t *panel, uint8_t cmd, const void *param, size_t param_size) +{ + spi_multi_transaction_t *send_seg = &panel->write_cmd_seg; + memset(send_seg, 0, sizeof(spi_multi_transaction_t)); + + send_seg->base.cmd = ST77903_INS_CMD; + send_seg->base.user = NULL; + send_seg->base.addr = ((uint32_t)cmd) << 8; + send_seg->base.length = param_size * 8; + send_seg->base.tx_buffer = (param_size == 0) ? NULL : (void *)param; + ESP_RETURN_ON_ERROR(spi_device_queue_multi_trans(panel->spi_write_dev, send_seg, 1, portMAX_DELAY), TAG, "SPI segment trans failed"); + esp_rom_delay_us(LCD_LINE_INTERVAL_MIN_US); + + return ESP_OK; +} + +static esp_err_t lcd_write_color(st77903_qspi_panel_t *panel) +{ + int row_count = panel->ver_res; + int pool_size = 0; + + do { + pool_size = MIN(row_count, panel->trans_pool_size); + xSemaphoreTake(panel->sem_count_free_trans, portMAX_DELAY); + ESP_RETURN_ON_ERROR(spi_device_queue_multi_trans(panel->spi_write_dev, panel->trans_pool[panel->write_pool_index], pool_size, + portMAX_DELAY), TAG, "SPI segment trans failed"); + panel->write_pool_index++; + if (panel->write_pool_index >= panel->trans_pool_num) { + panel->write_pool_index = 0; + } + row_count -= pool_size; + } while (row_count > 0); + + return ESP_OK; +} + +static esp_err_t lcd_read_reg(st77903_qspi_panel_t *panel) +{ + ESP_RETURN_ON_FALSE(panel->flags.enable_read_reg, ESP_ERR_INVALID_STATE, TAG, "Read reg not enabled"); + + spicommon_cs_initialize(panel->spi_host_id, panel->cs_io_num, get_spi_device_id(panel->spi_read_dev), 1); + ESP_RETURN_ON_ERROR(spi_bus_multi_trans_mode_enable(panel->spi_write_dev, false), TAG, "Segment mode disenable failed"); + + spi_transaction_t send_seg = { + .cmd = ST77903_INS_READ, + .addr = ((uint32_t)panel->read_reg) << 8, + .rxlength = 8 * panel->read_data_size, + .tx_buffer = NULL, + .rx_buffer = panel->read_data, + }; + ESP_RETURN_ON_ERROR(spi_device_polling_transmit(panel->spi_read_dev, &send_seg), TAG, "SPI polling trans failed"); + ESP_RETURN_ON_ERROR(spi_bus_multi_trans_mode_enable(panel->spi_write_dev, true), TAG, "Segment mode enable failed"); + spicommon_cs_initialize(panel->spi_host_id, panel->cs_io_num, get_spi_device_id(panel->spi_write_dev), 1); + + return ESP_OK; +} + +static esp_err_t lcd_write_vsync(st77903_qspi_panel_t *panel, bool is_front) +{ + if (is_front) { + ESP_RETURN_ON_ERROR(spi_device_queue_multi_trans(panel->spi_write_dev, panel->vsync_front_pool, LCD_VSYNC_FRONT_NUM, portMAX_DELAY), + TAG, "spi seg trans failed"); + } else { + ESP_RETURN_ON_ERROR(spi_device_queue_multi_trans(panel->spi_write_dev, panel->vsync_back_pool, LCD_VSYNC_BACK_NUM + 1, portMAX_DELAY), + TAG, "spi seg trans failed"); + } + + return ESP_OK; +} + +static void load_memory_task(void *arg) +{ + st77903_qspi_panel_t *panel = (st77903_qspi_panel_t *)arg; + BaseType_t result = pdFALSE; + queue_load_mem_info_t load_info; + + ESP_LOGD(TAG, "Load memory task start"); + while (1) { + result = pdFALSE; + while (result == pdFALSE) { + result = xQueueReceive(panel->queue_load_mem_info, &load_info, pdMS_TO_TICKS(TASK_CHECK_TIME_MS)); + if (panel->flags.stop_load_task) { + panel->flags.stop_load_task = false; + goto end; + } + } + memcpy(load_info.to, load_info.from, load_info.bytes); +#if CONFIG_IDF_TARGET_ESP32S3 + // Preload the next bounce buffer from psram + Cache_Start_DCache_Preload((uint32_t)load_info.next_buf, load_info.next_buf_bytes, 0); +#endif + xSemaphoreGive(panel->sem_count_free_trans); + } + +end: + panel->task.load_task_handle = NULL; + ESP_LOGD(TAG, "Load memory task stop"); + vTaskDelete(NULL); +} + +static void refresh_task(void *arg) +{ + st77903_qspi_panel_t *panel = (st77903_qspi_panel_t *)arg; + uint16_t frame_cnt = 0; + int64_t start_time = 0; + esp_err_t ret = ESP_OK; + + ESP_LOGD(TAG, "Refresh task start"); + if (panel->flags.enable_cal_fps) { + start_time = esp_timer_get_time(); + } + + while (!panel->flags.stop_refresh_task) { + // Send vsync front command + ESP_GOTO_ON_ERROR(lcd_write_vsync(panel, 0), end, TAG, "LCD write vsync front cmd failed"); + // Send the whole frame data + ESP_GOTO_ON_ERROR(lcd_write_color(panel), end, TAG, "LCD write color failed"); + // Send vsync back commands + ESP_GOTO_ON_ERROR(lcd_write_vsync(panel, 1), end, TAG, "LCD write vsync back cmd failed"); + + // Read LCD register + if (panel->flags.enable_read_reg && panel->flags.wait_frame_end) { + xSemaphoreTake(panel->sem_read_ready, 0); + // Wait for the frame end + if (xSemaphoreTake(panel->sem_read_ready, pdMS_TO_TICKS(READ_WAIT_TIME_MAX_MS)) == pdTRUE) { + panel->flags.wait_frame_end = false; + ESP_GOTO_ON_ERROR(lcd_read_reg(panel), end, TAG, "LCD read reg failed"); + xSemaphoreGive(panel->sem_read_done); + } + } + + // Calculate FPS + if (panel->flags.enable_cal_fps) { + if (++frame_cnt == 100) { + frame_cnt = 0; + panel->fps = (int)(100000000ULL / (esp_timer_get_time() - start_time)); + start_time = esp_timer_get_time(); + } + } + } + +end: + panel->flags.stop_load_task = true; + panel->task.refresh_task_handle = NULL; + ESP_LOGD(TAG, "Refresh task stop (%s)", esp_err_to_name(ret)); + vTaskDelete(NULL); +} + +static const st77903_lcd_init_cmd_t vendor_specific_init_default[] = { +// {cmd, { data }, data_size, delay_ms} + {0xf0, (uint8_t []){0xc3}, 1, 0}, + {0xf0, (uint8_t []){0x96}, 1, 0}, + {0xf0, (uint8_t []){0xa5}, 1, 0}, + {0xe9, (uint8_t []){0x20}, 1, 0}, + {0xe7, (uint8_t []){0x80, 0x77, 0x1f, 0xcc}, 4, 0}, + {0xc1, (uint8_t []){0x77, 0x07, 0xcf, 0x16}, 4, 0}, + {0xc2, (uint8_t []){0x77, 0x07, 0xcf, 0x16}, 4, 0}, + {0xc3, (uint8_t []){0x22, 0x02, 0x22, 0x04}, 4, 0}, + {0xc4, (uint8_t []){0x22, 0x02, 0x22, 0x04}, 4, 0}, + {0xc5, (uint8_t []){0xed}, 1, 0}, + {0xe0, (uint8_t []){0x87, 0x09, 0x0c, 0x06, 0x05, 0x03, 0x29, 0x32, 0x49, 0x0f, 0x1b, 0x17, 0x2a, 0x2f}, 14, 0}, + {0xe1, (uint8_t []){0x87, 0x09, 0x0c, 0x06, 0x05, 0x03, 0x29, 0x32, 0x49, 0x0f, 0x1b, 0x17, 0x2a, 0x2f}, 14, 0}, + {0xe5, (uint8_t []){0xbe, 0xf5, 0xb1, 0x22, 0x22, 0x25, 0x10, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22}, 14, 0}, + {0xe6, (uint8_t []){0xbe, 0xf5, 0xb1, 0x22, 0x22, 0x25, 0x10, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22}, 14, 0}, + {0xec, (uint8_t []){0x40, 0x03}, 2, 0}, + {0xb2, (uint8_t []){0x00}, 1, 0}, + {0xb3, (uint8_t []){0x01}, 1, 0}, + {0xb4, (uint8_t []){0x00}, 1, 0}, + {0xa5, (uint8_t []){0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x2a, 0x8a, 0x02}, 9, 0}, + {0xa6, (uint8_t []){0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x2a, 0x8a, 0x02}, 9, 0}, + {0xba, (uint8_t []){0x0a, 0x5a, 0x23, 0x10, 0x25, 0x02, 0x00}, 7, 0}, + {0xbb, (uint8_t []){0x00, 0x30, 0x00, 0x2c, 0x82, 0x87, 0x18, 0x00}, 8, 0}, + {0xbc, (uint8_t []){0x00, 0x30, 0x00, 0x2c, 0x82, 0x87, 0x18, 0x00}, 8, 0}, + {0xbd, (uint8_t []){0xa1, 0xb2, 0x2b, 0x1a, 0x56, 0x43, 0x34, 0x65, 0xff, 0xff, 0x0f}, 11, 0}, + {0x35, (uint8_t []){0x00}, 1, 0}, + {0x21, (uint8_t []){0x00}, 0, 0}, + {0x11, (uint8_t []){0x00}, 0, 120}, +}; + +static esp_err_t lcd_cmd_config(st77903_qspi_panel_t *panel) +{ + ESP_RETURN_ON_ERROR(lcd_write_cmd(panel, 0xf0, (uint8_t []) { + 0xc3 + }, 1), TAG, "Write cmd failed"); + ESP_RETURN_ON_ERROR(lcd_write_cmd(panel, 0xf0, (uint8_t []) { + 0x96 + }, 1), TAG, "Write cmd failed"); + ESP_RETURN_ON_ERROR(lcd_write_cmd(panel, 0xf0, (uint8_t []) { + 0xa5 + }, 1), TAG, "Write cmd failed"); + uint8_t NL = (panel->ver_res >> 1) - 1; + uint8_t NC = (panel->hor_res >> 3) - 1; + // Set Resolution + ESP_RETURN_ON_ERROR(lcd_write_cmd(panel, ST77903_CMD_DISCN, (uint8_t []) { + NL, NC + }, 2), TAG, "Write cmd failed"); + // Set VFP & VBP + ESP_RETURN_ON_ERROR(lcd_write_cmd(panel, ST77903_CMD_BPC, (uint8_t []) { + 0x00, LCD_VSYNC_FRONT_NUM, 0x00, LCD_VSYNC_BACK_NUM + }, 4), TAG, "Write cmd failed"); + // Set color format + ESP_RETURN_ON_ERROR(lcd_write_cmd(panel, LCD_CMD_MADCTL, (uint8_t []) { + panel->madctl_val + }, 1), TAG, "Write cmd failed"); + ESP_RETURN_ON_ERROR(lcd_write_cmd(panel, LCD_CMD_COLMOD, (uint8_t []) { + panel->colmod_val + }, 1), TAG, "Write cmd failed"); + + // vendor specific initialization, it can be different between manufacturers + // should consult the LCD supplier for initialization sequence code + const st77903_lcd_init_cmd_t *init_cmds = NULL; + uint16_t init_cmds_size = 0; + if (panel->init_cmds) { + init_cmds = panel->init_cmds; + init_cmds_size = panel->init_cmds_size; + } else { + init_cmds = vendor_specific_init_default; + init_cmds_size = sizeof(vendor_specific_init_default) / sizeof(st77903_lcd_init_cmd_t); + } + + bool is_cmd_overwritten = false; + bool is_cmd_conflicting = false; + for (int i = 0; i < init_cmds_size; i++) { + // Check if the command has been used or conflicts with the internal + switch (init_cmds[i].cmd) { + case LCD_CMD_MADCTL: + is_cmd_overwritten = true; + panel->madctl_val = ((uint8_t *)init_cmds[i].data)[0]; + break; + case LCD_CMD_COLMOD: + is_cmd_overwritten = true; + panel->colmod_val = ((uint8_t *)init_cmds[i].data)[0]; + break; + case ST77903_CMD_DISCN: + if ((init_cmds[i].data_bytes >= 2) && ((((uint8_t *)init_cmds[i].data)[0] != NL) || + (((uint8_t *)init_cmds[i].data)[1] != NC))) { + is_cmd_conflicting = true; + } + break; + case ST77903_CMD_BPC: + if ((init_cmds[i].data_bytes >= 4) && ((((uint8_t *)init_cmds[i].data)[1] != LCD_VSYNC_FRONT_NUM) || + (((uint8_t *)init_cmds[i].data)[3] != LCD_VSYNC_BACK_NUM))) { + is_cmd_conflicting = true; + } + break; + default: + is_cmd_overwritten = false; + is_cmd_conflicting = false; + break; + } + + if (is_cmd_overwritten) { + ESP_LOGW(TAG, "The %02Xh command has been used and will be overwritten by external initialization sequence", + init_cmds[i].cmd); + } else if (is_cmd_conflicting) { + ESP_LOGE(TAG, "The %02Xh command conflicts with the internal, please remove it from external initialization sequence", + init_cmds[i].cmd); + } + + // Only send the command if it is not conflicted + if (!is_cmd_conflicting) { + ESP_RETURN_ON_ERROR(lcd_write_cmd(panel, init_cmds[i].cmd, init_cmds[i].data, init_cmds[i].data_bytes), TAG, + "Write cmd failed"); + vTaskDelay(pdMS_TO_TICKS(init_cmds[i].delay_ms)); + } + } + ESP_LOGD(TAG, "send init commands success"); + + return ESP_OK; +} diff --git a/components/display/lcd/esp_lcd_st77903_qspi/idf_component.yml b/components/display/lcd/esp_lcd_st77903_qspi/idf_component.yml new file mode 100644 index 000000000..bdcf4e373 --- /dev/null +++ b/components/display/lcd/esp_lcd_st77903_qspi/idf_component.yml @@ -0,0 +1,10 @@ +version: "0.4.1" +description: ESP LCD ST77903 QSPI +url: https://github.com/espressif/esp-iot-solution/tree/master/components/display/lcd/esp_lcd_st77903_qspi +repository: https://github.com/espressif/esp-iot-solution.git +issues: https://github.com/espressif/esp-iot-solution/issues +dependencies: + idf: ">=5.3" + cmake_utilities: "0.*" +examples: + - path: ../../../../examples/display/lcd/qspi_without_ram diff --git a/components/display/lcd/esp_lcd_st77903_qspi/include/esp_lcd_st77903_qspi.h b/components/display/lcd/esp_lcd_st77903_qspi/include/esp_lcd_st77903_qspi.h new file mode 100644 index 000000000..0e545d28f --- /dev/null +++ b/components/display/lcd/esp_lcd_st77903_qspi/include/esp_lcd_st77903_qspi.h @@ -0,0 +1,267 @@ +/* + * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "driver/spi_common.h" +#include "esp_err.h" +#include "esp_lcd_panel_vendor.h" +#include "esp_lcd_types.h" +#include "freertos/FreeRTOS.h" +#include "soc/soc_caps.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * This structure is the configuration for ST77903 using QSPI interface. + */ +typedef struct { + struct { + spi_host_device_t host_id; /*!< SPI host ID */ + int write_pclk_hz; /*!< SPI clock frequency in Hz (normally use 40000000) */ + int read_pclk_hz; /*!< SPI clock frequency in Hz (normally use 1000000) */ + int cs_io_num; /*!< GPIO pin for LCD CS signal, set to -1 if not used */ + int sclk_io_num; /*!< GPIO pin for LCD SCK(SCL) signal */ + int data0_io_num; /*!< GPIO pin for LCD D0(SDA) signal */ + int data1_io_num; /*!< GPIO pin for LCD D1 signal */ + int data2_io_num; /*!< GPIO pin for LCD D2 signal */ + int data3_io_num; /*!< GPIO pin for LCD D3 signal */ + } qspi; + struct { + uint8_t refresh_priority; /*!< Priority of refresh task */ + uint32_t refresh_size; /*!< Stack size of refresh task */ + int refresh_core; /*!< Pined core of refresh task, set to `tskNO_AFFINITY` if not pin to any core */ + uint8_t load_priority; /*!< Priority of load memory task */ + uint32_t load_size; /*!< Stack size of load memory task */ + int load_core; /*!< Pined core of load memory task, set to `tskNO_AFFINITY` if not pin to any core */ + } task; + size_t fb_num; /*!< Number of screen-sized frame buffers that allocated by the driver. + * By default (set to either 0 or 1) only one frame buffer will be used. + */ + size_t trans_pool_size; /*!< Size of one transaction pool. Each pool contains mutiple transactions which used to transfer color data. + * Each transaction contains one line of LCD frame buffer. + * It also decides the size of bounce buffer if `flags.fb_in_psram` is set to 1. + */ + size_t trans_pool_num; /*!< Number of transaction pool. Generally, it's recommended to set it to at least 2, and preferably to 3 */ + uint16_t hor_res; /*!< Horizontal resolution of LCD */ + uint16_t ver_res; /*!< Vertical resolution of LCD */ + struct { + unsigned int fb_in_psram : 1; /*=5.1.0" + esp_lcd_panel_io_additions: "^1" + esp_lcd_st77903_qspi: + version: "*" + override_path: "../../../esp_lcd_st77903_qspi" diff --git a/components/display/lcd/esp_lcd_st77903_qspi/test_apps/main/test_esp_lcd_st77903_qspi.c b/components/display/lcd/esp_lcd_st77903_qspi/test_apps/main/test_esp_lcd_st77903_qspi.c new file mode 100644 index 000000000..907979b60 --- /dev/null +++ b/components/display/lcd/esp_lcd_st77903_qspi/test_apps/main/test_esp_lcd_st77903_qspi.c @@ -0,0 +1,241 @@ +/* + * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "driver/spi_master.h" +#include "driver/gpio.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_heap_caps.h" +#include "esp_log.h" +#include "esp_timer.h" +#include "esp_lcd_panel_io_interface.h" +#include "esp_lcd_panel_ops.h" +#include "unity.h" +#include "unity_test_runner.h" +#include "unity_test_utils_memory.h" + +#include "esp_lcd_st77903_qspi.h" + +#define TEST_LCD_HOST SPI2_HOST +#define TEST_LCD_H_RES (400) +#define TEST_LCD_V_RES (400) +#define TEST_LCD_BIT_PER_PIXEL (16) +#define TEST_LCD_READ_ENABLE (0) + +#define TEST_PIN_NUM_LCD_QSPI_CS (GPIO_NUM_12) +#define TEST_PIN_NUM_LCD_QSPI_PCLK (GPIO_NUM_10) +#define TEST_PIN_NUM_LCD_QSPI_DATA0 (GPIO_NUM_13) +#define TEST_PIN_NUM_LCD_QSPI_DATA1 (GPIO_NUM_11) +#define TEST_PIN_NUM_LCD_QSPI_DATA2 (GPIO_NUM_14) +#define TEST_PIN_NUM_LCD_QSPI_DATA3 (GPIO_NUM_9) +#define TEST_PIN_NUM_LCD_RST (GPIO_NUM_47) +#define TEST_PIN_NUM_BK_LIGHT (-1) // set to -1 if not used +#define TEST_LCD_BK_LIGHT_ON_LEVEL (1) +#define TEST_LCD_BK_LIGHT_OFF_LEVEL !TEST_LCD_BK_LIGHT_ON_LEVEL + +#define TEST_LCD_REG_RDDST (0x09) +#define TEST_LCD_REG_VALUE (0x00265284ULL) +#define TEST_DELAY_TIME_MS (3000) + +static char *TAG = "st77903_qspi_test"; + +static esp_lcd_panel_handle_t test_init_lcd(bool enable_read) +{ +#if TEST_PIN_NUM_BK_LIGHT >= 0 + ESP_LOGI(TAG, "Turn on LCD backlight"); + gpio_config_t bk_gpio_config = { + .mode = GPIO_MODE_OUTPUT, + .pin_bit_mask = 1ULL << TEST_PIN_NUM_BK_LIGHT + }; + TEST_ESP_OK(gpio_config(&bk_gpio_config)); + TEST_ESP_OK(gpio_set_level(TEST_PIN_NUM_BK_LIGHT, TEST_LCD_BK_LIGHT_ON_LEVEL)); +#endif + + ESP_LOGI(TAG, "Install st77903 panel driver"); + esp_lcd_panel_handle_t panel_handle = NULL; + st77903_qspi_config_t qspi_config = ST77903_QSPI_CONFIG_DEFAULT(TEST_LCD_HOST, + TEST_PIN_NUM_LCD_QSPI_CS, + TEST_PIN_NUM_LCD_QSPI_PCLK, + TEST_PIN_NUM_LCD_QSPI_DATA0, + TEST_PIN_NUM_LCD_QSPI_DATA1, + TEST_PIN_NUM_LCD_QSPI_DATA2, + TEST_PIN_NUM_LCD_QSPI_DATA3, + 1, + TEST_LCD_H_RES, + TEST_LCD_V_RES); + if (enable_read) { + qspi_config.flags.enable_read_reg = 1; + } +#if CONFIG_IDF_TARGET_ESP32C6 + qspi_config.flags.fb_in_psram = 0; + qspi_config.trans_pool_num = 2; +#endif + st77903_vendor_config_t vendor_config = { + .qspi_config = &qspi_config, + .flags = { + .mirror_by_cmd = 1, + }, + }; + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = TEST_PIN_NUM_LCD_RST, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR, + .bits_per_pixel = TEST_LCD_BIT_PER_PIXEL, + .vendor_config = &vendor_config, + }; + TEST_ESP_OK(esp_lcd_new_panel_st77903_qspi(&panel_config, &panel_handle)); + TEST_ESP_OK(esp_lcd_panel_reset(panel_handle)); + TEST_ESP_OK(esp_lcd_panel_disp_on_off(panel_handle, true)); + TEST_ESP_OK(esp_lcd_panel_init(panel_handle)); + + return panel_handle; +} + +static void test_deinit_lcd(esp_lcd_panel_handle_t panel_handle) +{ + TEST_ESP_OK(esp_lcd_panel_del(panel_handle)); +#if TEST_PIN_NUM_BK_LIGHT >= 0 + TEST_ESP_OK(gpio_reset_pin(TEST_PIN_NUM_BK_LIGHT)); +#endif +} + +static void test_draw_color_bar(esp_lcd_panel_handle_t panel_handle, uint16_t h_res, uint16_t v_res) +{ + uint8_t byte_per_pixel = (TEST_LCD_BIT_PER_PIXEL + 7) / 8; + uint16_t row_line = v_res / byte_per_pixel / 8; + uint8_t *color = (uint8_t *)heap_caps_calloc(1, row_line * h_res * byte_per_pixel, MALLOC_CAP_DMA); + + for (int j = 0; j < byte_per_pixel * 8; j++) { + for (int i = 0; i < row_line * h_res; i++) { + for (int k = 0; k < byte_per_pixel; k++) { + color[i * byte_per_pixel + k] = (SPI_SWAP_DATA_TX(BIT(j), TEST_LCD_BIT_PER_PIXEL) >> (k * 8)) & 0xff; + } + } + TEST_ESP_OK(esp_lcd_panel_draw_bitmap(panel_handle, 0, j * row_line, h_res, (j + 1) * row_line, color)); + } + + uint16_t color_line = row_line * byte_per_pixel * 8; + uint16_t res_line = v_res - color_line; + if (res_line) { + for (int i = 0; i < res_line * h_res; i++) { + for (int k = 0; k < byte_per_pixel; k++) { + color[i * byte_per_pixel + k] = 0xff; + } + } + TEST_ESP_OK(esp_lcd_panel_draw_bitmap(panel_handle, 0, color_line, h_res, v_res, color)); + } + + free(color); +} + +TEST_CASE("test st77903_qspi to draw color bar", "[st77903_qspi][draw_color_bar]") +{ + ESP_LOGI(TAG, "Initialize LCD device"); + esp_lcd_panel_handle_t panel_handle = test_init_lcd(false); + + ESP_LOGI(TAG, "Show color bar drawn by software"); + test_draw_color_bar(panel_handle, TEST_LCD_H_RES, TEST_LCD_V_RES); + vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_TIME_MS)); + + ESP_LOGI(TAG, "Deinitialize LCD device"); + test_deinit_lcd(panel_handle); +} + +#if TEST_LCD_READ_ENABLE +TEST_CASE("test st77903_qspi to read register", "[st77903_qspi][read_reg]") +{ + ESP_LOGI(TAG, "Initialize LCD device"); + esp_lcd_panel_handle_t panel_handle = test_init_lcd(true); + + ESP_LOGI(TAG, "Show color bar drawn by software"); + test_draw_color_bar(panel_handle, TEST_LCD_H_RES, TEST_LCD_V_RES); + vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_TIME_MS)); + + ESP_LOGI(TAG, "Read register 0x%02x from LCD device", TEST_LCD_REG_RDDST); + int reg_value = 0; + TEST_ESP_OK(esp_lcd_st77903_qspi_read_reg(panel_handle, TEST_LCD_REG_RDDST, (uint8_t *)®_value, sizeof(reg_value), portMAX_DELAY)); + ESP_LOGI(TAG, "[0x%02x]: 0x%08x", TEST_LCD_REG_RDDST, reg_value); + TEST_ASSERT_TRUE(reg_value == TEST_LCD_REG_VALUE); + + ESP_LOGI(TAG, "Deinitialize LCD device"); + test_deinit_lcd(panel_handle); +} +#endif + +TEST_CASE("test st77903_qspi to rotate", "[st77903_qspi][rotate]") +{ + uint16_t w = 0; + uint16_t h = 0; + int64_t t = 0; + + ESP_LOGI(TAG, "Initialize LCD device"); + esp_lcd_panel_handle_t panel_handle = test_init_lcd(false); + + ESP_LOGI(TAG, "Rotate the screen"); + for (size_t i = 0; i < 8; i++) { + if (i & 4) { + w = TEST_LCD_V_RES; + h = TEST_LCD_H_RES; + } else { + w = TEST_LCD_H_RES; + h = TEST_LCD_V_RES; + } + + TEST_ESP_OK(esp_lcd_panel_disp_on_off(panel_handle, false)); + TEST_ESP_OK(esp_lcd_panel_mirror(panel_handle, i & 2, i & 1)); + TEST_ESP_OK(esp_lcd_panel_disp_on_off(panel_handle, true)); + + TEST_ESP_OK(esp_lcd_panel_swap_xy(panel_handle, i & 4)); + + ESP_LOGI(TAG, "Rotation: %d", i); + t = esp_timer_get_time(); + test_draw_color_bar(panel_handle, w, h); + t = esp_timer_get_time() - t; + ESP_LOGI(TAG, "@resolution %dx%d time per frame=%.2fMS\r\n", w, h, (float)t / 1000.0f); + vTaskDelay(pdMS_TO_TICKS(1000)); + } + + ESP_LOGI(TAG, "Deinitialize LCD device"); + test_deinit_lcd(panel_handle); +} + +// Some resources are lazy allocated in the LCD driver, the threadhold is left for that case +#define TEST_MEMORY_LEAK_THRESHOLD (300) + +static size_t before_free_8bit; +static size_t before_free_32bit; + +void setUp(void) +{ + before_free_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT); + before_free_32bit = heap_caps_get_free_size(MALLOC_CAP_32BIT); +} + +void tearDown(void) +{ + size_t after_free_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT); + size_t after_free_32bit = heap_caps_get_free_size(MALLOC_CAP_32BIT); + unity_utils_check_leak(before_free_8bit, after_free_8bit, "8BIT", TEST_MEMORY_LEAK_THRESHOLD); + unity_utils_check_leak(before_free_32bit, after_free_32bit, "32BIT", TEST_MEMORY_LEAK_THRESHOLD); +} + +void app_main(void) +{ + /** + * __ _____ _____ _____ ___ ___ _____ ____ __ ___ _____ + * / _\/__ \___ |___ / _ \ / _ \___ / /___ \/ _\ / _ \\_ \ + * \ \ / /\/ / / / / (_) | | | ||_ \ // / /\ \ / /_)/ / /\/ + * _\ \ / / / / / / \__, | |_| |__) | / \_/ / _\ \/ ___/\/ /_ + * \__/ \/ /_/ /_/ /_/ \___/____/ \___,_\ \__/\/ \____/ + */ + printf(" __ _____ _____ _____ ___ ___ _____ ____ __ ___ _____\r\n"); + printf("/ _\\/__ \\___ |___ / _ \\ / _ \\___ / /___ \\/ _\\ / _ \\\\_ \\\r\n"); + printf("\\ \\ / /\\/ / / / / (_) | | | ||_ \\ // / /\\ \\ / /_)/ / /\\/\r\n"); + printf("_\\ \\ / / / / / / \\__, | |_| |__) | / \\_/ / _\\ \\/ ___/\\/ /_\r\n"); + printf("\\__/ \\/ /_/ /_/ /_/ \\___/____/ \\___,_\\ \\__/\\/ \\____/\r\n"); + unity_run_menu(); +} diff --git a/components/display/lcd/esp_lcd_st77903_qspi/test_apps/pytest_esp_lcd_st77903_qspi.py b/components/display/lcd/esp_lcd_st77903_qspi/test_apps/pytest_esp_lcd_st77903_qspi.py new file mode 100644 index 000000000..a4db85684 --- /dev/null +++ b/components/display/lcd/esp_lcd_st77903_qspi/test_apps/pytest_esp_lcd_st77903_qspi.py @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Apache-2.0 +import pytest +from pytest_embedded import Dut + +@pytest.mark.target('esp32s3') +@pytest.mark.env('esp32_s3_lcd_ev_board') +def test_usb_stream(dut: Dut)-> None: + dut.run_all_single_board_cases() diff --git a/components/display/lcd/esp_lcd_st77903_qspi/test_apps/sdkconfig.defaults b/components/display/lcd/esp_lcd_st77903_qspi/test_apps/sdkconfig.defaults new file mode 100644 index 000000000..2fcf8f5ce --- /dev/null +++ b/components/display/lcd/esp_lcd_st77903_qspi/test_apps/sdkconfig.defaults @@ -0,0 +1,4 @@ +CONFIG_FREERTOS_HZ=1000 +CONFIG_ESP_TASK_WDT_EN=n +CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=4096 +CONFIG_COMPILER_OPTIMIZATION_PERF=y diff --git a/components/display/lcd/esp_lcd_st77903_qspi/test_apps/sdkconfig.defaults.esp32s3 b/components/display/lcd/esp_lcd_st77903_qspi/test_apps/sdkconfig.defaults.esp32s3 new file mode 100644 index 000000000..88ddc8c5c --- /dev/null +++ b/components/display/lcd/esp_lcd_st77903_qspi/test_apps/sdkconfig.defaults.esp32s3 @@ -0,0 +1,7 @@ +CONFIG_SPIRAM=y +CONFIG_SPIRAM_MODE_OCT=y +CONFIG_SPIRAM_FETCH_INSTRUCTIONS=y +CONFIG_SPIRAM_RODATA=y +CONFIG_SPIRAM_SPEED_80M=y +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y +CONFIG_ESP32S3_DATA_CACHE_LINE_64B=y diff --git a/components/display/lcd/esp_lcd_st77903_rgb/CHANGELOG.md b/components/display/lcd/esp_lcd_st77903_rgb/CHANGELOG.md index 5fca3ff08..77eb601c7 100644 --- a/components/display/lcd/esp_lcd_st77903_rgb/CHANGELOG.md +++ b/components/display/lcd/esp_lcd_st77903_rgb/CHANGELOG.md @@ -1,5 +1,12 @@ # ChangeLog +## v0.1.0 - 2024-5-10 + +### bugfix: + +* Fix some errors in README.md +* Update test_apps + ## v0.0.1 - 2023-1-30 ### Enhancements: diff --git a/components/display/lcd/esp_lcd_st77903_rgb/README.md b/components/display/lcd/esp_lcd_st77903_rgb/README.md index d28c868c9..2adb761cf 100644 --- a/components/display/lcd/esp_lcd_st77903_rgb/README.md +++ b/components/display/lcd/esp_lcd_st77903_rgb/README.md @@ -1,33 +1,18 @@ # ESP LCD ST77903 RGB -Implementation of the ST77903 LCD controller with [esp_lcd](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/lcd.html) component. +Implementation of the ST77903 RGB LCD controller with [esp_lcd](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/lcd.html) component. | LCD controller | Communication interface | Component name | Link to datasheet | | :------------: | :---------------------: | :------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -| ST77903 | QSPI/RGB | esp_lcd_ST77903 | [PDF1](https://dl.espressif.com/AE/esp-iot-solution/ST77903_SPEC_P0.5.pdf)| +| ST77903 | RGB | esp_lcd_ST77903_rgb | [PDF1](https://dl.espressif.com/AE/esp-iot-solution/ST77903_SPEC_P0.5.pdf)| ## Initialization Code -### RGB Interface For most RGB LCDs, they typically use a "3-Wire SPI + Parallel RGB" interface. The "3-Wire SPI" interface is used for transmitting command data and the "Parallel RGB" interface is used for sending pixel data. It's recommended to use the [esp_lcd_panel_io_additions](https://components.espressif.com/components/espressif/esp_lcd_panel_io_additions) component to bit-bang the "3-Wire SPI" interface through **GPIO** or an **IO expander** (like [TCA9554](https://components.espressif.com/components/espressif/esp_io_expander_tca9554)). To do this, please first add this component to your project manually. Then, refer to the following code to initialize the ST77903 controller. ```c - ESP_LOGI(TAG, "Install 3-wire SPI panel IO"); - spi_line_config_t line_config = { - .cs_io_type = IO_TYPE_EXPANDER, // Set to `IO_TYPE_GPIO` if using GPIO, same to below - .cs_expander_pin = EXAMPLE_LCD_IO_SPI_CS, - .scl_io_type = IO_TYPE_GPIO, - .scl_gpio_num = EXAMPLE_LCD_IO_SPI_SCK, - .sda_io_type = IO_TYPE_GPIO, - .sda_gpio_num = EXAMPLE_LCD_IO_SPI_SDO, - .io_expander = expander_handle, // Set to NULL if not using IO expander - }; - esp_lcd_panel_io_3wire_spi_config_t io_config = ST77903_RGB_PANEL_IO_3WIRE_SPI_CONFIG(line_config, 0); - esp_lcd_panel_io_handle_t io_handle = NULL; - ESP_ERROR_CHECK(esp_lcd_new_panel_io_3wire_spi(&io_config, &io_handle)); - /** * Uncomment these line if use custom initialization commands. * The array should be declared as static const and positioned outside the function. @@ -40,6 +25,20 @@ It's recommended to use the [esp_lcd_panel_io_additions](https://components.espr // ... // }; + ESP_LOGI(TAG, "Install 3-wire SPI panel IO"); + spi_line_config_t line_config = { + .cs_io_type = IO_TYPE_GPIO, // Set to `IO_TYPE_EXPANDER` if using GPIO, same to below + .cs_expander_pin = EXAMPLE_LCD_IO_SPI_CS, + .scl_io_type = IO_TYPE_GPIO, + .scl_gpio_num = EXAMPLE_LCD_IO_SPI_SCK, + .sda_io_type = IO_TYPE_GPIO, + .sda_gpio_num = EXAMPLE_LCD_IO_SPI_SDO, + .io_expander = NULL, // Set handle if using IO expander + }; + esp_lcd_panel_io_3wire_spi_config_t io_config = ST77903_RGB_PANEL_IO_3WIRE_SPI_CONFIG(line_config, 0); + esp_lcd_panel_io_handle_t io_handle = NULL; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_3wire_spi(&io_config, &io_handle)); + ESP_LOGI(TAG, "Install st77903 panel driver"); esp_lcd_panel_handle_t panel_handle = NULL; const esp_lcd_rgb_panel_config_t rgb_config = { @@ -70,7 +69,6 @@ It's recommended to use the [esp_lcd_panel_io_additions](https://components.espr // .init_cmds = lcd_init_cmds, // Uncomment these line if use custom initialization commands // .init_cmds_size = sizeof(lcd_init_cmds) / sizeof(st77903_lcd_init_cmd_t), .flags = { - .use_rgb_interface = 1, .mirror_by_cmd = 0, // Only work when `auto_del_panel_io` is set to 0 .auto_del_panel_io = 1, /** * Send initialization commands and delete the panel IO instance during creation if set to 1. diff --git a/components/display/lcd/esp_lcd_st77903_rgb/esp_lcd_st77903_rgb.c b/components/display/lcd/esp_lcd_st77903_rgb/esp_lcd_st77903_rgb.c index 54ab05756..107c53bd3 100644 --- a/components/display/lcd/esp_lcd_st77903_rgb/esp_lcd_st77903_rgb.c +++ b/components/display/lcd/esp_lcd_st77903_rgb/esp_lcd_st77903_rgb.c @@ -23,6 +23,9 @@ #include "esp_lcd_st77903_rgb.h" +#define ST77903_CMD_BPC (0xB5) +#define ST77903_CMD_DISCN (0xB6) + typedef struct { esp_lcd_panel_io_handle_t io; int reset_gpio_num; diff --git a/components/display/lcd/esp_lcd_st77903_rgb/idf_component.yml b/components/display/lcd/esp_lcd_st77903_rgb/idf_component.yml index 814b2666c..cc9b73d8c 100644 --- a/components/display/lcd/esp_lcd_st77903_rgb/idf_component.yml +++ b/components/display/lcd/esp_lcd_st77903_rgb/idf_component.yml @@ -1,4 +1,4 @@ -version: "0.0.1" +version: "0.1.0" targets: - esp32s3 description: ESP LCD ST77903 RGB diff --git a/components/display/lcd/esp_lcd_st77903_rgb/include/esp_lcd_st77903_rgb.h b/components/display/lcd/esp_lcd_st77903_rgb/include/esp_lcd_st77903_rgb.h index ec8667768..8cd9683ed 100644 --- a/components/display/lcd/esp_lcd_st77903_rgb/include/esp_lcd_st77903_rgb.h +++ b/components/display/lcd/esp_lcd_st77903_rgb/include/esp_lcd_st77903_rgb.h @@ -7,22 +7,9 @@ #pragma once #include - -#include "driver/spi_common.h" -#include "esp_err.h" #include "esp_lcd_panel_rgb.h" #include "esp_lcd_panel_vendor.h" #include "esp_lcd_types.h" -#include "freertos/FreeRTOS.h" -#include "soc/soc_caps.h" - -#define ST77903_INS_DATA (0xDE) -#define ST77903_INS_READ (0xDD) -#define ST77903_INS_CMD (0xD8) -#define ST77903_CMD_HSYNC (0x60) -#define ST77903_CMD_VSYNC (0x61) -#define ST77903_CMD_BPC (0xB5) -#define ST77903_CMD_DISCN (0xB6) #ifdef __cplusplus extern "C" { diff --git a/components/display/lcd/esp_lcd_st77903_rgb/test_apps/main/test_esp_lcd_st77903_rgb.c b/components/display/lcd/esp_lcd_st77903_rgb/test_apps/main/test_esp_lcd_st77903_rgb.c index f0f047148..b87e5b6cc 100644 --- a/components/display/lcd/esp_lcd_st77903_rgb/test_apps/main/test_esp_lcd_st77903_rgb.c +++ b/components/display/lcd/esp_lcd_st77903_rgb/test_apps/main/test_esp_lcd_st77903_rgb.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -13,6 +13,7 @@ #include "freertos/semphr.h" #include "esp_heap_caps.h" #include "esp_log.h" +#include "esp_timer.h" #include "esp_lcd_panel_io_additions.h" #include "esp_lcd_panel_io_interface.h" #include "esp_lcd_panel_ops.h" @@ -22,8 +23,8 @@ #include "esp_lcd_st77903_rgb.h" -#define TEST_LCD_RGB_H_RES (320) -#define TEST_LCD_RGB_V_RES (480) +#define TEST_LCD_H_RES (320) +#define TEST_LCD_V_RES (480) #define TEST_LCD_RGB_DATA_WIDTH (8) #define TEST_LCD_BIT_PER_PIXEL (24) #define TEST_RGB_BIT_PER_PIXEL (24) @@ -39,44 +40,34 @@ #define TEST_PIN_NUM_LCD_RGB_DATA3 (GPIO_NUM_13) #define TEST_PIN_NUM_LCD_RGB_DATA4 (GPIO_NUM_14) #define TEST_PIN_NUM_LCD_RGB_DATA5 (GPIO_NUM_21) -#define TEST_PIN_NUM_LCD_RGB_DATA6 (GPIO_NUM_47) -#define TEST_PIN_NUM_LCD_RGB_DATA7 (GPIO_NUM_48) +#define TEST_PIN_NUM_LCD_RGB_DATA6 (GPIO_NUM_8) +#define TEST_PIN_NUM_LCD_RGB_DATA7 (GPIO_NUM_18) #define TEST_PIN_NUM_LCD_SPI_CS (GPIO_NUM_45) #define TEST_PIN_NUM_LCD_SPI_SCK (GPIO_NUM_40) #define TEST_PIN_NUM_LCD_SPI_SDO (GPIO_NUM_42) -#define TEST_PIN_NUM_LCD_RST (GPIO_NUM_2) +#define TEST_PIN_NUM_LCD_RST (GPIO_NUM_2) +#define TEST_PIN_NUM_BK_LIGHT (GPIO_NUM_0) // set to -1 if not used +#define TEST_LCD_BK_LIGHT_ON_LEVEL (1) +#define TEST_LCD_BK_LIGHT_OFF_LEVEL !TEST_LCD_BK_LIGHT_ON_LEVEL #define TEST_DELAY_TIME_MS (3000) -static char *TAG = "st77903_test"; +static char *TAG = "st77903_rgb_test"; -static void test_draw_bitmap(esp_lcd_panel_handle_t panel_handle, uint16_t h_res, uint16_t v_res, - uint8_t bits_per_pixel, bool swap_byte) +static void test_init_lcd(esp_lcd_panel_handle_t *lcd, esp_lcd_panel_io_handle_t *io) { - uint16_t row_line = v_res / bits_per_pixel; - uint8_t byte_per_pixel = bits_per_pixel / 8; - uint8_t *color = (uint8_t *)heap_caps_calloc(1, row_line * h_res * byte_per_pixel, MALLOC_CAP_DMA); - TEST_ASSERT_NOT_NULL(color); - for (int j = 0; j < bits_per_pixel; j++) { - for (int i = 0; i < row_line * h_res; i++) { - for (int k = 0; k < byte_per_pixel; k++) { - if (swap_byte) { - color[i * byte_per_pixel + k] = (SPI_SWAP_DATA_TX(BIT(j), bits_per_pixel) >> (k * 8)) & 0xff; - } else { - color[i * byte_per_pixel + k] = (BIT(j) >> (k * 8)) & 0xff; - } - } - } - TEST_ESP_OK(esp_lcd_panel_draw_bitmap(panel_handle, 0, j * row_line, h_res, (j + 1) * row_line, color)); - } - free(color); - vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_TIME_MS)); -} +#if TEST_PIN_NUM_BK_LIGHT >= 0 + ESP_LOGI(TAG, "Turn on LCD backlight"); + gpio_config_t bk_gpio_config = { + .mode = GPIO_MODE_OUTPUT, + .pin_bit_mask = 1ULL << TEST_PIN_NUM_BK_LIGHT + }; + TEST_ESP_OK(gpio_config(&bk_gpio_config)); + TEST_ESP_OK(gpio_set_level(TEST_PIN_NUM_BK_LIGHT, TEST_LCD_BK_LIGHT_ON_LEVEL)); +#endif -TEST_CASE("test st77903 to draw color bar with RGB interface", "[st77903][rgb]") -{ ESP_LOGI(TAG, "Install 3-wire SPI panel IO"); spi_line_config_t line_config = { .cs_io_type = IO_TYPE_GPIO, @@ -91,9 +82,9 @@ TEST_CASE("test st77903 to draw color bar with RGB interface", "[st77903][rgb]") esp_lcd_panel_io_handle_t io_handle = NULL; TEST_ESP_OK(esp_lcd_new_panel_io_3wire_spi(&io_config, &io_handle)); - ESP_LOGI(TAG, "Install st77903 panel driver"); + ESP_LOGI(TAG, "Install st77903_rgb panel driver"); esp_lcd_panel_handle_t panel_handle = NULL; - const esp_lcd_rgb_panel_config_t rgb_config = { + esp_lcd_rgb_panel_config_t rgb_config = { .clk_src = LCD_CLK_SRC_DEFAULT, .psram_trans_align = 64, .data_width = TEST_LCD_RGB_DATA_WIDTH, @@ -115,11 +106,14 @@ TEST_CASE("test st77903 to draw color bar with RGB interface", "[st77903][rgb]") }, .timings = ST77903_RGB_320_480_PANEL_48HZ_RGB_TIMING(), .flags.fb_in_psram = 1, + .bounce_buffer_size_px = TEST_LCD_H_RES * 10, }; + rgb_config.timings.h_res = TEST_LCD_H_RES; + rgb_config.timings.v_res = TEST_LCD_V_RES; st77903_vendor_config_t vendor_config = { .rgb_config = &rgb_config, .flags = { - .auto_del_panel_io = 1, + .mirror_by_cmd = 1, }, }; const esp_lcd_panel_dev_config_t panel_config = { @@ -132,9 +126,101 @@ TEST_CASE("test st77903 to draw color bar with RGB interface", "[st77903][rgb]") TEST_ESP_OK(esp_lcd_panel_reset(panel_handle)); TEST_ESP_OK(esp_lcd_panel_init(panel_handle)); - test_draw_bitmap(panel_handle, TEST_LCD_RGB_H_RES, TEST_LCD_RGB_V_RES, TEST_LCD_BIT_PER_PIXEL, false); + if (lcd) { + *lcd = panel_handle; + } + if (io) { + *io = io_handle; + } +} +static void test_deinit_lcd(esp_lcd_panel_handle_t panel_handle, esp_lcd_panel_io_handle_t io_handle) +{ TEST_ESP_OK(esp_lcd_panel_del(panel_handle)); + TEST_ESP_OK(esp_lcd_panel_io_del(io_handle)); +#if TEST_PIN_NUM_BK_LIGHT >= 0 + TEST_ESP_OK(gpio_reset_pin(TEST_PIN_NUM_BK_LIGHT)); +#endif +} + +static void test_draw_color_bar(esp_lcd_panel_handle_t panel_handle, uint16_t h_res, uint16_t v_res) +{ + uint8_t byte_per_pixel = (TEST_LCD_BIT_PER_PIXEL + 7) / 8; + uint16_t row_line = v_res / byte_per_pixel / 8; + uint8_t *color = (uint8_t *)heap_caps_calloc(1, row_line * h_res * byte_per_pixel, MALLOC_CAP_DMA); + + for (int j = 0; j < byte_per_pixel * 8; j++) { + for (int i = 0; i < row_line * h_res; i++) { + for (int k = 0; k < byte_per_pixel; k++) { + color[i * byte_per_pixel + k] = (BIT(j) >> (k * 8)) & 0xff; + } + } + TEST_ESP_OK(esp_lcd_panel_draw_bitmap(panel_handle, 0, j * row_line, h_res, (j + 1) * row_line, color)); + } + + uint16_t color_line = row_line * byte_per_pixel * 8; + uint16_t res_line = v_res - color_line; + if (res_line) { + for (int i = 0; i < res_line * h_res; i++) { + for (int k = 0; k < byte_per_pixel; k++) { + color[i * byte_per_pixel + k] = 0xff; + } + } + TEST_ESP_OK(esp_lcd_panel_draw_bitmap(panel_handle, 0, color_line, h_res, v_res, color)); + } + + free(color); +} + +TEST_CASE("test st77903_rgb to draw color bar", "[st77903_rgb][draw_color_bar]") +{ + ESP_LOGI(TAG, "Initialize LCD device"); + esp_lcd_panel_handle_t panel_handle = NULL; + esp_lcd_panel_io_handle_t io_handle = NULL; + test_init_lcd(&panel_handle, &io_handle); + + ESP_LOGI(TAG, "Show color bar drawn by software"); + test_draw_color_bar(panel_handle, TEST_LCD_H_RES, TEST_LCD_V_RES); + vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_TIME_MS)); + + ESP_LOGI(TAG, "Deinitialize LCD device"); + test_deinit_lcd(panel_handle, io_handle); +} + +TEST_CASE("test st77903_rgb to rotate", "[st77903_rgb][rotate]") +{ + esp_lcd_panel_handle_t panel_handle = NULL; + esp_lcd_panel_io_handle_t io_handle = NULL; + uint16_t w = 0; + uint16_t h = 0; + int64_t t = 0; + + ESP_LOGI(TAG, "Initialize LCD device"); + test_init_lcd(&panel_handle, &io_handle); + + ESP_LOGI(TAG, "Rotate the screen"); + for (size_t i = 0; i < 8; i++) { + if (i & 4) { + w = TEST_LCD_V_RES; + h = TEST_LCD_H_RES; + } else { + w = TEST_LCD_H_RES; + h = TEST_LCD_V_RES; + } + + TEST_ESP_OK(esp_lcd_panel_mirror(panel_handle, i & 2, i & 1)); + TEST_ESP_OK(esp_lcd_panel_swap_xy(panel_handle, i & 4)); + + ESP_LOGI(TAG, "Rotation: %d", i); + t = esp_timer_get_time(); + test_draw_color_bar(panel_handle, w, h); + t = esp_timer_get_time() - t; + ESP_LOGI(TAG, "@resolution %dx%d time per frame=%.2fMS\r\n", w, h, (float)t / 1000.0f); + vTaskDelay(pdMS_TO_TICKS(1000)); + } + + ESP_LOGI(TAG, "Deinitialize LCD device"); + test_deinit_lcd(panel_handle, io_handle); } // Some resources are lazy allocated in the LCD driver, the threadhold is left for that case @@ -160,16 +246,16 @@ void tearDown(void) void app_main(void) { /** - * __ _____ _____ _____ ___ ___ _____ - * / _\/__ \___ |___ / _ \ / _ \___ / - * \ \ / /\/ / / / / (_) | | | ||_ \ - * _\ \ / / / / / / \__, | |_| |__) | - * \__/ \/ /_/ /_/ /_/ \___/____/ - */ - printf(" __ _____ _____ _____ ___ ___ _____\r\n"); - printf("/ _\\/__ \\___ |___ / _ \\ / _ \\___ /\r\n"); - printf("\\ \\ / /\\/ / / / / (_) | | | ||_ \\\r\n"); - printf("_\\ \\ / / / / / / \\__, | |_| |__) |\r\n"); - printf("\\__/ \\/ /_/ /_/ /_/ \\___/____/\r\n"); + * __ _____ _____ _____ ___ ___ _____ __ ___ ___ + * / _\/__ \___ |___ / _ \ / _ \___ / /__\ / _ \ / __\ + * \ \ / /\/ / / / / (_) | | | ||_ \ / \// / /_\//__\// + * _\ \ / / / / / / \__, | |_| |__) | / _ \/ /_\\/ \/ \ + * \__/ \/ /_/ /_/ /_/ \___/____/ \/ \_/\____/\_____/ + */ + printf(" __ _____ _____ _____ ___ ___ _____ __ ___ ___\r\n"); + printf("/ _\\/__ \\___ |___ / _ \\ / _ \\___ / /__\\ / _ \\ / __\\\r\n"); + printf("\\ \\ / /\\/ / / / / (_) | | | ||_ \\ / \\// / /_\\//__\\//\r\n"); + printf("_\\ \\ / / / / / / \\__, | |_| |__) | / _ \\/ /_\\\\/ \\/ \\\r\n"); + printf("\\__/ \\/ /_/ /_/ /_/ \\___/____/ \\/ \\_/\\____/\\_____/\r\n"); unity_run_menu(); } diff --git a/examples/display/lcd/qspi_without_ram/CMakeLists.txt b/examples/display/lcd/qspi_without_ram/CMakeLists.txt new file mode 100644 index 000000000..87e15d60a --- /dev/null +++ b/examples/display/lcd/qspi_without_ram/CMakeLists.txt @@ -0,0 +1,7 @@ +# For more information about build system see +# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html +# The following five lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(qspi_without_ram) diff --git a/examples/display/lcd/qspi_without_ram/README.md b/examples/display/lcd/qspi_without_ram/README.md index 2f5be3c0b..eb7ce68d7 100644 --- a/examples/display/lcd/qspi_without_ram/README.md +++ b/examples/display/lcd/qspi_without_ram/README.md @@ -6,6 +6,142 @@ # QSPI LCD (without RAM) Example +[esp_lcd](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/lcd.html) provides several panel drivers out-of box, e.g. ST7789, SSD1306, NT35510. However, there're a lot of other panels on the market, it's beyond `esp_lcd` component's responsibility to include them all. + +`esp_lcd` allows user to add their own panel drivers in the project scope (i.e. panel driver can live outside of esp-idf), so that the upper layer code like LVGL porting code can be reused without any modifications, as long as user-implemented panel driver follows the interface defined in the `esp_lcd` component. + +This example shows how to use ST77903 display driver from Component manager in esp-idf project. These components are using API provided by `esp_lcd` component. This example will draw a fancy dash board with the LVGL library. + +This example uses the [esp_timer](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/esp_timer.html) to generate the ticks needed by LVGL and uses a dedicated task to run the `lv_timer_handler()`. Since the LVGL APIs are not thread-safe, this example uses a mutex which be invoked before the call of `lv_timer_handler()` and released after it. The same mutex needs to be used in other tasks and threads around every LVGL (lv_...) related function call and code. For more porting guides, please refer to [LVGL porting doc](https://docs.lvgl.io/master/porting/index.html). + ## How to use the example -The QSPI driver for ST77903 relies on certain new features that have not been merged into the `ESP-IDF` on Github. Currently, this driver is available on the internal Glab repository. If you require access, please reach out to our business team or contact us at `sales@espressif.com` to obtain Glab permissions. +### Hardware Required + +* An ESP32-S3R2 or ESP32-S3R8 development board +* A ST77903 LCD panel, with QSPI interface +* An USB cable for power supply and programming + +### Hardware Connection + +The connection between ESP Board and the LCD is as follows: + +``` + ESP Board ST77903 Panel (QSPI) +┌──────────────────────┐ ┌────────────────────┐ +│ GND ├─────────────►│ GND │ +│ │ │ │ +│ 3V3 ├─────────────►│ VCC │ +│ │ │ │ +│ CS ├─────────────►│ CS │ +│ │ │ │ +│ SCK ├─────────────►│ CLK │ +│ │ │ │ +│ D3 ├─────────────►│ IO3 │ +│ │ │ │ +│ D2 ├─────────────►│ IO2 │ +│ │ │ │ +│ D1 ├─────────────►│ IO1 │ +│ │ │ │ +│ D0 ├─────────────►│ IO0 │ +│ │ │ │ +│ RST ├─────────────►│ RSTN │ +└──────────────────────┘ └────────────────────┘ +``` + +The LCD parameters and GPIO number used by this example can be changed in [lv_port.h](main/lv_port.h). +Especially, please pay attention to the **vendor specific initialization**, it can be different between manufacturers and should consult the LCD supplier for initialization sequence code. + +### Build and Flash + +Run `idf.py -p PORT build flash monitor` to build, flash and monitor the project. A fancy animation will show up on the LCD as expected. + +The first time you run `idf.py` for the example will cost extra time as the build system needs to address the component dependencies and downloads the missing components from registry into `managed_components` folder. + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html) for full steps to configure and use ESP-IDF to build projects. + +### Example Output + +```bash +... +I (679) example: Turn off LCD backlight +I (684) gpio: GPIO[8]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 +I (693) lvgl_port: Install ST77903 panel driver +I (699) gpio: GPIO[15]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 +I (717) st77903: LCD panel create success, version: 0.3.0 +I (1099) lvgl_port: Initialize LVGL library +I (1101) lvgl_port: Install LVGL tick timer +I (1102) lvgl_port: Starting LVGL task +I (1109) example: Turn on LCD backlight +I (1109) example: Display LVGL demos +I (1125) main_task: Returned from app_main() +... +``` + +## Performance Test + +### Test Environment + +#### For ESP32-S3R2 + +| Item | Value | +| :--------------------: | :----------------: | +| Resolution of LCD | 400 * 400 | +| Configuration of PSRAM | Quad, 120M | +| Configuration of Flash | QIO, 120M | +| Version of LVGL | v8.3.9 | +| Test Demo of LVGL | Music player | +| Basic Configurations | sdkconfig.defaults | + +#### For ESP32-S3R8 + +| Item | Value | +| :--------------------: | :--------------------------------: | +| Resolution of LCD | 400 * 400 | +| Configuration of PSRAM | Octal, 120M | +| Configuration of Flash | QIO, 120M | +| Version of LVGL | v8.3.9 | +| Test Demo of LVGL | Music player | +| Basic Configurations | sdkconfig.defaults.psram_octal_ddr | + +### Description of Buffering Mode + +| Buffering Mode | Description | Special Configurations | +| :------------: | :-----------------------------------------------: | :---------------------------: | +| Mode1 | One buffer with 100-line heights in internal SRAM | * | +| Mode2 | One buffer with frame-size in internal PSRAM | sdkconfig.test.psram_buffer | +| Mode3 | Full-refresh with two frame-size PSRAM buffers | sdkconfig.test.full_refresh_1 | +| Mode4 | Direct-mode with two frame-size PSRAM buffers | sdkconfig.test.direct_mode | +| Mode5 | Full-refresh with three frame-size PSRAM buffers | sdkconfig.test.full_refresh_2 | + +**Note:** To test the above modes, run the following commands to configure project (take ESP32-S3R8 and `Mode4` as an example): +``` +rm -rf build sdkconfig sdkconfig.old +idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults.psram_octal_ddr;sdkconfig.test.direct_mode" reconfigure +``` + +### Average FPS using ESP32-S3R2 + +| Buffering Mode | Average FPS | +| :------------: | :---------: | +| Mode1 | 22 | +| Mode2 | 16 | +| Mode3 | 14 | +| Mode4 | 18 | +| Mode5 | 16 | + +### Average FPS using ESP32-S3R8 + +| Buffering Mode | Average FPS | +| :------------: | :---------: | +| Mode1 | 30 | +| Mode2 | 25 | +| Mode3 | 25 | +| Mode4 | 26 | +| Mode5 | 29 | + +## Troubleshooting + +For any technical queries, please open an [issue] (https://github.com/espressif/esp-iot-solution/issues) on GitHub. We will get back to you soon. diff --git a/examples/display/lcd/qspi_without_ram/main/CMakeLists.txt b/examples/display/lcd/qspi_without_ram/main/CMakeLists.txt new file mode 100644 index 000000000..14a21a854 --- /dev/null +++ b/examples/display/lcd/qspi_without_ram/main/CMakeLists.txt @@ -0,0 +1,12 @@ +set(LV_DEMO_DIR ../managed_components/lvgl__lvgl/demos) +file(GLOB_RECURSE LV_DEMOS_SOURCES ${LV_DEMO_DIR}/*.c) + +idf_component_register( + SRCS "example_qspi_without_ram.c" "lvgl_port.c" ${LV_DEMOS_SOURCES} + INCLUDE_DIRS "." ${LV_DEMO_DIR}) + +set_source_files_properties( + ${LV_DEMOS_SOURCES} + PROPERTIES + COMPILE_OPTIONS -DLV_LVGL_H_INCLUDE_SIMPLE + COMPILE_OPTIONS -Wno-format) diff --git a/examples/display/lcd/qspi_without_ram/main/Kconfig.projbuild b/examples/display/lcd/qspi_without_ram/main/Kconfig.projbuild new file mode 100644 index 000000000..fcae750ca --- /dev/null +++ b/examples/display/lcd/qspi_without_ram/main/Kconfig.projbuild @@ -0,0 +1,118 @@ +menu "Example Configuration" + menu "Touch Controller" + config EXAMPLE_LCD_TOUCH_CONTROLLER_CST816S + bool "Enable LCD CST816S Touch" + default n + help + Enable this option if you wish to use display touch. + endmenu + + menu "Display" + config EXAMPLE_LVGL_PORT_TASK_MAX_DELAY_MS + int "LVGL timer task maximum delay (ms)" + default 500 + range 2 2000 # Example range, adjust as needed + help + The maximum delay of the LVGL timer task, in milliseconds. + + config EXAMPLE_LVGL_PORT_TASK_MIN_DELAY_MS + int "LVGL timer task minimum delay (ms)" + default 10 + range 1 100 # Example range, adjust as needed + help + The minimum delay of the LVGL timer task, in milliseconds. + + config EXAMPLE_LVGL_PORT_TASK_PRIORITY + int "LVGL task priority" + default 2 + help + The Board Support Package will create a task that will periodically handle LVGL operation in lv_timer_handler(). + + config EXAMPLE_LVGL_PORT_TASK_STACK_SIZE_KB + int "LVGL task stack size (KB)" + default 6 + help + Size(KB) of LVGL task stack. + + config EXAMPLE_LVGL_PORT_TASK_CORE + int "LVGL timer task core" + default -1 + range -1 1 + help + The core of the LVGL timer task. + Set to -1 to not specify the core. + Set to 1 only if the SoCs support dual-core, otherwise set to -1 or 0. + + config EXAMPLE_LVGL_PORT_TICK + int "LVGL tick period" + default 2 + range 1 100 + help + Period of LVGL tick timer. + + config EXAMPLE_LVGL_PORT_AVOID_TEAR_ENABLE + bool "Avoid tearing effect" + default "n" + help + Avoid tearing effect through LVGL buffer mode and double frame buffers of QSPI LCD. This feature is only available for QSPI LCD without GRAM. + + choice + depends on EXAMPLE_LVGL_PORT_AVOID_TEAR_ENABLE + prompt "Select Avoid Tearing Mode" + default EXAMPLE_LVGL_PORT_AVOID_TEAR_MODE_3 + config EXAMPLE_LVGL_PORT_AVOID_TEAR_MODE_1 + bool "Mode1: LCD double-buffer & LVGL full-refresh" + config EXAMPLE_LVGL_PORT_AVOID_TEAR_MODE_2 + bool "Mode2: LCD triple-buffer & LVGL full-refresh" + config EXAMPLE_LVGL_PORT_AVOID_TEAR_MODE_3 + bool "Mode3: LCD double-buffer & LVGL direct-mode" + help + The current tearing prevention mode supports both full refresh mode and direct mode. Tearing prevention mode may consume more PSRAM space + endchoice + + config EXAMPLE_LVGL_PORT_AVOID_TEAR_MODE + depends on EXAMPLE_LVGL_PORT_AVOID_TEAR_ENABLE + int + default 0 if EXAMPLE_LVGL_PORT_AVOID_TEAR_MODE_1 + default 1 if EXAMPLE_LVGL_PORT_AVOID_TEAR_MODE_2 + default 2 if EXAMPLE_LVGL_PORT_AVOID_TEAR_MODE_3 + + choice + depends on EXAMPLE_LVGL_PORT_AVOID_TEAR_ENABLE + prompt "Select rotation" + default EXAMPLE_LVGL_PORT_ROTATION_0 + config EXAMPLE_LVGL_PORT_ROTATION_0 + bool "Rotation 0" + config EXAMPLE_LVGL_PORT_ROTATION_90 + bool "Rotation 90" + config EXAMPLE_LVGL_PORT_ROTATION_180 + bool "Rotation 180" + config EXAMPLE_LVGL_PORT_ROTATION_270 + bool "Rotation 270" + endchoice + + config EXAMPLE_LVGL_PORT_ROTATION_DEGREE + int + default 0 if EXAMPLE_LVGL_PORT_ROTATION_0 + default 90 if EXAMPLE_LVGL_PORT_ROTATION_90 + default 180 if EXAMPLE_LVGL_PORT_ROTATION_180 + default 270 if EXAMPLE_LVGL_PORT_ROTATION_270 + + choice + depends on !EXAMPLE_LVGL_PORT_AVOID_TEAR_ENABLE + prompt "Select LVGL buffer memory capability" + default EXAMPLE_LVGL_PORT_BUF_INTERNAL + config EXAMPLE_LVGL_PORT_BUF_PSRAM + bool "PSRAM memory" + config EXAMPLE_LVGL_PORT_BUF_INTERNAL + bool "Internal memory" + endchoice + + config EXAMPLE_LVGL_PORT_BUF_HEIGHT + depends on !EXAMPLE_LVGL_PORT_AVOID_TEAR_ENABLE + int "LVGL buffer height" + default 100 + help + Height of LVGL buffer. The width of the buffer is the same as that of the LCD. + endmenu +endmenu diff --git a/examples/display/lcd/qspi_without_ram/main/example_qspi_without_ram.c b/examples/display/lcd/qspi_without_ram/main/example_qspi_without_ram.c new file mode 100644 index 000000000..fe2834234 --- /dev/null +++ b/examples/display/lcd/qspi_without_ram/main/example_qspi_without_ram.c @@ -0,0 +1,165 @@ +/* + * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "driver/gpio.h" +#include "driver/i2c.h" +#include "esp_heap_caps.h" +#include "esp_log.h" +#include "esp_lcd_st77903_qspi.h" +#include "esp_lcd_panel_ops.h" +#include "esp_lcd_touch.h" +#include "esp_lcd_touch_cst816s.h" +#include "lvgl_port.h" +#include "lv_demos.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////// Please update the following configuration according to your LCD spec ////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#define EXAMPLE_LCD_H_RES (400) +#define EXAMPLE_LCD_V_RES (400) +#define EXAMPLE_LCD_BIT_PER_PIXEL (16) +#define EXAMPLE_LCD_HOST (SPI2_HOST) +#define EXAMPLE_LCD_FB_BUF_NUM (LVGL_PORT_LCD_QSPI_BUFFER_NUMS) +#define EXAMPLE_PIN_NUM_LCD_CS (GPIO_NUM_12) +#define EXAMPLE_PIN_NUM_LCD_SCK (GPIO_NUM_10) +#define EXAMPLE_PIN_NUM_LCD_D0 (GPIO_NUM_13) +#define EXAMPLE_PIN_NUM_LCD_D1 (GPIO_NUM_11) +#define EXAMPLE_PIN_NUM_LCD_D2 (GPIO_NUM_14) +#define EXAMPLE_PIN_NUM_LCD_D3 (GPIO_NUM_9) +#define EXAMPLE_PIN_NUM_BK_LIGHT (-1) +#define EXAMPLE_PIN_NUM_LCD_RST (GPIO_NUM_47) +#define EXAMPLE_LCD_BK_LIGHT_ON_LEVEL (1) +#define EXAMPLE_LCD_BK_LIGHT_OFF_LEVEL (!EXAMPLE_LCD_BK_LIGHT_ON_LEVEL) + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////// Please update the following configuration according to your touch spec //////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#if CONFIG_EXAMPLE_LCD_TOUCH_CONTROLLER_CST816S +#define TOUCH_HOST (I2C_NUM_0) +#define EXAMPLE_PIN_NUM_TOUCH_SCL (GPIO_NUM_45) +#define EXAMPLE_PIN_NUM_TOUCH_SDA (GPIO_NUM_48) +#define EXAMPLE_PIN_NUM_TOUCH_RST (-1) // -1 if not used +#define EXAMPLE_PIN_NUM_TOUCH_INT (GPIO_NUM_21) // -1 if not used +#endif + +static const char *TAG = "example"; + +// static const st77903_lcd_init_cmd_t lcd_init_cmds[] = { +// // {cmd, { data }, data_size, delay_ms} +// {0xFF, (uint8_t []){0x77, 0x01, 0x00, 0x00, 0x10}, 5, 0}, +// {0xC0, (uint8_t []){0x3B, 0x00}, 2, 0}, +// {0xC1, (uint8_t []){0x0D, 0x02}, 2, 0}, +// {0xC2, (uint8_t []){0x31, 0x05}, 2, 0}, +// ... +// }; + +void app_main() +{ +#if EXAMPLE_PIN_NUM_BK_LIGHT >= 0 + ESP_LOGI(TAG, "Turn off LCD backlight"); + gpio_config_t bk_gpio_config = { + .mode = GPIO_MODE_OUTPUT, + .pin_bit_mask = 1ULL << EXAMPLE_PIN_NUM_BK_LIGHT + }; + ESP_ERROR_CHECK(gpio_config(&bk_gpio_config)); +#endif + + esp_lcd_panel_handle_t lcd_handle = NULL; + ESP_LOGI(TAG, "Install ST77903 panel driver"); + st77903_qspi_config_t qspi_config = ST77903_QSPI_CONFIG_DEFAULT(EXAMPLE_LCD_HOST, + EXAMPLE_PIN_NUM_LCD_CS, + EXAMPLE_PIN_NUM_LCD_SCK, + EXAMPLE_PIN_NUM_LCD_D0, + EXAMPLE_PIN_NUM_LCD_D1, + EXAMPLE_PIN_NUM_LCD_D2, + EXAMPLE_PIN_NUM_LCD_D3, + EXAMPLE_LCD_FB_BUF_NUM, + EXAMPLE_LCD_H_RES, + EXAMPLE_LCD_V_RES); + st77903_vendor_config_t vendor_config = { + .qspi_config = &qspi_config, + // .init_cmds = lcd_init_cmds, // Uncomment these line if use custom initialization commands + // .init_cmds_size = sizeof(lcd_init_cmds) / sizeof(lcd_init_cmds[0]), + .flags = { + .mirror_by_cmd = 1, + }, + }; + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = EXAMPLE_PIN_NUM_LCD_RST, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, + .bits_per_pixel = EXAMPLE_LCD_BIT_PER_PIXEL, + .vendor_config = &vendor_config, + }; + ESP_ERROR_CHECK(esp_lcd_new_panel_st77903_qspi(&panel_config, &lcd_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_reset(lcd_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_mirror(lcd_handle, true, false)); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(lcd_handle, true)); + ESP_ERROR_CHECK(esp_lcd_panel_init(lcd_handle)); + + esp_lcd_touch_handle_t tp_handle = NULL; +#if CONFIG_EXAMPLE_LCD_TOUCH_CONTROLLER_CST816S + ESP_LOGI(TAG, "Initialize I2C bus"); + const i2c_config_t i2c_conf = { + .mode = I2C_MODE_MASTER, + .sda_io_num = EXAMPLE_PIN_NUM_TOUCH_SDA, + .sda_pullup_en = GPIO_PULLUP_ENABLE, + .scl_io_num = EXAMPLE_PIN_NUM_TOUCH_SCL, + .scl_pullup_en = GPIO_PULLUP_ENABLE, + .master.clk_speed = 400 * 1000, + }; + ESP_ERROR_CHECK(i2c_param_config(TOUCH_HOST, &i2c_conf)); + ESP_ERROR_CHECK(i2c_driver_install(TOUCH_HOST, i2c_conf.mode, 0, 0, 0)); + + esp_lcd_panel_io_handle_t tp_io_handle = NULL; + const esp_lcd_panel_io_i2c_config_t tp_io_config = ESP_LCD_TOUCH_IO_I2C_CST816S_CONFIG(); + + // Attach the TOUCH to the I2C bus + ESP_LOGI(TAG, "Initialize I2C panel IO"); + ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c((esp_lcd_i2c_bus_handle_t)TOUCH_HOST, &tp_io_config, &tp_io_handle)); + + ESP_LOGI(TAG, "Initialize touch controller CST816S"); + esp_lcd_touch_config_t tp_cfg = { + .x_max = EXAMPLE_LCD_H_RES, + .y_max = EXAMPLE_LCD_V_RES, + .rst_gpio_num = EXAMPLE_PIN_NUM_TOUCH_RST, + .int_gpio_num = EXAMPLE_PIN_NUM_TOUCH_INT, + .levels = { + .reset = 0, + .interrupt = 0, + }, + .flags = { + .swap_xy = 0, + .mirror_x = 0, + .mirror_y = 0, + }, + }; + if (tp_cfg.int_gpio_num != GPIO_NUM_NC) { + init_touch_isr_mux(); + tp_cfg.interrupt_callback = lvgl_port_touch_isr_cb; + } + ESP_ERROR_CHECK(esp_lcd_touch_new_i2c_cst816s(tp_io_handle, &tp_cfg, &tp_handle)); +#endif // CONFIG_EXAMPLE_LCD_TOUCH_CONTROLLER_CST816S + + ESP_ERROR_CHECK(lvgl_port_init(lcd_handle, tp_handle)); + +#if EXAMPLE_PIN_NUM_BK_LIGHT >= 0 + ESP_LOGI(TAG, "Turn on LCD backlight"); + gpio_set_level(EXAMPLE_PIN_NUM_BK_LIGHT, EXAMPLE_LCD_BK_LIGHT_ON_LEVEL); +#endif + + ESP_LOGI(TAG, "Display LVGL demos"); + // Lock the mutex due to the LVGL APIs are not thread-safe + if (lvgl_port_lock(-1)) { + //lv_demo_stress(); + // lv_demo_benchmark(); + lv_demo_music(); + + // Release the mutex + lvgl_port_unlock(); + } +} diff --git a/examples/display/lcd/qspi_without_ram/main/idf_component.yml b/examples/display/lcd/qspi_without_ram/main/idf_component.yml new file mode 100644 index 000000000..856d737f1 --- /dev/null +++ b/examples/display/lcd/qspi_without_ram/main/idf_component.yml @@ -0,0 +1,10 @@ +dependencies: + lvgl/lvgl: + version: ">8.3.9,<9" + public: true + esp_lcd_st77903_qspi: + version: "*" + override_path: "../../../../../components/display/lcd/esp_lcd_st77903_qspi" + esp_lcd_touch_cst816s: + version: "^1" + public: true diff --git a/examples/display/lcd/qspi_without_ram/main/lvgl_port.c b/examples/display/lcd/qspi_without_ram/main/lvgl_port.c new file mode 100644 index 000000000..724895ac5 --- /dev/null +++ b/examples/display/lcd/qspi_without_ram/main/lvgl_port.c @@ -0,0 +1,586 @@ +/* + * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/task.h" +#include "esp_timer.h" +#include "esp_log.h" +#include "esp_lcd_panel_ops.h" +#include "lvgl.h" +#include "lvgl_port.h" +#include "esp_lcd_st77903_qspi.h" + +static const char *TAG = "lv_port"; +static SemaphoreHandle_t lvgl_mux; // LVGL mutex +static TaskHandle_t lvgl_task_handle = NULL; +static SemaphoreHandle_t touch_isr_mux = NULL; + +#if EXAMPLE_LVGL_PORT_ROTATION_DEGREE != 0 +static void *get_next_frame_buffer(esp_lcd_panel_handle_t panel_handle) +{ + static void *next_fb = NULL; + static void *fb[2] = { NULL }; + if (next_fb == NULL) { + ESP_ERROR_CHECK(esp_lcd_st77903_qspi_get_frame_buffer(panel_handle, 2, &fb[0], &fb[1])); + next_fb = fb[1]; + } else { + next_fb = (next_fb == fb[0]) ? fb[1] : fb[0]; + } + return next_fb; +} + +IRAM_ATTR static void rotate_copy_pixel(const uint16_t *from, uint16_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 rotation) +{ + int from_index = 0; + + int to_index = 0; + int to_index_const = 0; + + switch (rotation) { + case 90: + to_index_const = (w - x_start - 1) * h; + for (int from_y = y_start; from_y < y_end + 1; from_y++) { + from_index = from_y * w + x_start; + to_index = to_index_const + from_y; + for (int from_x = x_start; from_x < x_end + 1; from_x++) { + *(to + to_index) = *(from + from_index); + from_index += 1; + to_index -= h; + } + } + break; + case 180: + to_index_const = h * w - x_start - 1; + for (int from_y = y_start; from_y < y_end + 1; from_y++) { + from_index = from_y * w + x_start; + to_index = to_index_const - from_y * w; + for (int from_x = x_start; from_x < x_end + 1; from_x++) { + *(to + to_index) = *(from + from_index); + from_index += 1; + to_index -= 1; + } + } + break; + case 270: + to_index_const = (x_start + 1) * h - 1; + for (int from_y = y_start; from_y < y_end + 1; from_y++) { + from_index = from_y * w + x_start; + to_index = to_index_const - from_y; + for (int from_x = x_start; from_x < x_end + 1; from_x++) { + *(to + to_index) = *(from + from_index); + from_index += 1; + to_index += h; + } + } + break; + default: + break; + } +} +#endif /* EXAMPLE_LVGL_PORT_ROTATION_DEGREE */ + +#if LVGL_PORT_AVOID_TEAR_ENABLE +#if LVGL_PORT_DIRECT_MODE +#if EXAMPLE_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; + +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; + +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]; + } +} + +/** + * @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(void *panel_handle) +{ + return get_next_frame_buffer(panel_handle); +} + +/** + * @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(src, dst, x_start, y_start, x_end, y_end, LV_HOR_RES, LV_VER_RES, EXAMPLE_LVGL_PORT_ROTATION_DEGREE); + } + } +} + +static void flush_callback(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; + 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; + + // Roate and copy data from the whole screen LVGL's buffer to the next frame buffer + next_fb = flush_get_next_buf(panel_handle); + rotate_copy_pixel((uint16_t *)color_map, next_fb, offsetx1, offsety1, offsetx2, offsety2, LV_HOR_RES, LV_VER_RES, EXAMPLE_LVGL_PORT_ROTATION_DEGREE); + + /* Switch the current QSPI frame buffer to `next_fb` */ + esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, 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(panel_handle), color_map, &dirty_area); + flush_get_next_buf(panel_handle); + } 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(panel_handle); + flush_dirty_save(&dirty_area); + flush_dirty_copy(next_fb, color_map, &dirty_area); + + /* Switch the current QSPI frame buffer to `next_fb` */ + esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, 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(panel_handle), color_map, &dirty_area); + flush_get_next_buf(panel_handle); + } + } + } + } + + 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) +{ + esp_lcd_panel_handle_t panel_handle = (esp_lcd_panel_handle_t) drv->user_data; + const int offsetx1 = area->x1; + const int offsetx2 = area->x2; + const int offsety1 = area->y1; + const int offsety2 = area->y2; + + /* Action after last area refresh */ + if (lv_disp_flush_is_last(drv)) { + /* Switch the current QSPI frame buffer to `color_map` */ + esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, 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 /* EXAMPLE_LVGL_PORT_ROTATION_DEGREE */ + +#elif LVGL_PORT_FULL_REFRESH && LVGL_PORT_LCD_QSPI_BUFFER_NUMS == 2 + +static void flush_callback(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; + const int offsetx1 = area->x1; + const int offsetx2 = area->x2; + const int offsety1 = area->y1; + const int offsety2 = area->y2; + + /* Switch the current QSPI frame buffer to `color_map` */ + esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, 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_LCD_QSPI_BUFFER_NUMS == 3 + +#if EXAMPLE_LVGL_PORT_ROTATION_DEGREE == 0 +static void *lvgl_port_qspi_last_buf = NULL; +static void *lvgl_port_qspi_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) +{ + esp_lcd_panel_handle_t panel_handle = (esp_lcd_panel_handle_t) drv->user_data; + const int offsetx1 = area->x1; + const int offsetx2 = area->x2; + const int offsety1 = area->y1; + const int offsety2 = area->y2; + +#if EXAMPLE_LVGL_PORT_ROTATION_DEGREE != 0 + void *next_fb = get_next_frame_buffer(panel_handle); + + /* Rotate and copy dirty area from the current LVGL's buffer to the next QSPI frame buffer */ + rotate_copy_pixel((uint16_t *)color_map, next_fb, offsetx1, offsety1, offsetx2, offsety2, LV_HOR_RES, LV_VER_RES, EXAMPLE_LVGL_PORT_ROTATION_DEGREE); + + /* Switch the current QSPI frame buffer to `next_fb` */ + esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, 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 QSPI frame buffer to `color_map` */ + esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, color_map); + + lvgl_port_qspi_next_buf = color_map; +#endif + + lv_disp_flush_ready(drv); +} +#endif + +IRAM_ATTR static bool qspi_lcd_on_trans_event(esp_lcd_panel_handle_t panel, const st77903_qspi_event_data_t *edata, void *user_ctx) +{ + BaseType_t need_yield = pdFALSE; +#if LVGL_PORT_FULL_REFRESH && (LVGL_PORT_LCD_QSPI_BUFFER_NUMS == 3) && (EXAMPLE_LVGL_PORT_ROTATION_DEGREE == 0) + if (lvgl_port_qspi_next_buf != lvgl_port_qspi_last_buf) { + lvgl_port_flush_next_buf = lvgl_port_qspi_last_buf; + lvgl_port_qspi_last_buf = lvgl_port_qspi_next_buf; + } +#elif LVGL_PORT_AVOID_TEAR_ENABLE + // Notify that the current QSPI frame buffer has been transmitted + xTaskNotifyFromISR(lvgl_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) +{ + esp_lcd_panel_handle_t panel_handle = (esp_lcd_panel_handle_t) drv->user_data; + const int offsetx1 = area->x1; + const int offsetx2 = area->x2; + const int offsety1 = area->y1; + const int offsety2 = area->y2; + + /* Just copy data from the color map to the QSPI frame buffer */ + esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, color_map); + + lv_disp_flush_ready(drv); +} + +#endif /* LVGL_PORT_AVOID_TEAR_ENABLE */ + +static lv_disp_t *display_init(esp_lcd_panel_handle_t panel_handle) +{ + assert(panel_handle); + + static lv_disp_draw_buf_t disp_buf = { 0 }; // Contains internal graphic buffer(s) called draw buffer(s) + static lv_disp_drv_t disp_drv = { 0 }; // Contains LCD panel handle and callback functions + + // alloc draw buffers used by LVGL + void *buf1 = NULL; + void *buf2 = NULL; + int buffer_size = 0; + + ESP_LOGD(TAG, "Malloc memory for LVGL buffer"); +#if LVGL_PORT_AVOID_TEAR_ENABLE + // To avoid the tearing effect, we should use at least two frame buffers: one for LVGL rendering and another for QSPI output + buffer_size = LVGL_PORT_H_RES * LVGL_PORT_V_RES; +#if (LVGL_PORT_LCD_QSPI_BUFFER_NUMS == 3) && (EXAMPLE_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 QSPI's sync signal + ESP_ERROR_CHECK(esp_lcd_st77903_qspi_get_frame_buffer(panel_handle, 3, &lvgl_port_qspi_last_buf, &buf1, &buf2)); + lvgl_port_qspi_next_buf = lvgl_port_qspi_last_buf; + lvgl_port_flush_next_buf = buf2; +#elif (LVGL_PORT_LCD_QSPI_BUFFER_NUMS == 3) && (EXAMPLE_LVGL_PORT_ROTATION_DEGREE != 0) + // Here we are using three frame buffers, one for LVGL rendering, and the other two for QSPI driver (one of them is used for rotation) + void *fbs[3]; + ESP_ERROR_CHECK(esp_lcd_st77903_qspi_get_frame_buffer(panel_handle, 3, &fbs[0], &fbs[1], &fbs[2])); + buf1 = fbs[2]; +#else + ESP_ERROR_CHECK(esp_lcd_st77903_qspi_get_frame_buffer(panel_handle, 2, &buf1, &buf2)); +#endif + + // Register the QSPI event callback to notify LVGL that the current frame buffer has been transmitted + st77903_qspi_event_callbacks_t cbs = { + .on_vsync = qspi_lcd_on_trans_event, + }; + esp_lcd_st77903_qspi_register_event_callbacks(lcd_handle, &cbs, NULL); +#else + // Normmaly, for QSPI LCD, we just use one buffer for LVGL rendering + buffer_size = LVGL_PORT_H_RES * LVGL_PORT_BUFFER_HEIGHT; + buf1 = heap_caps_malloc(buffer_size * sizeof(lv_color_t), LVGL_PORT_BUFFER_MALLOC_CAPS); + assert(buf1); + ESP_LOGI(TAG, "LVGL buffer size: %dKB", buffer_size * sizeof(lv_color_t) / 1024); +#endif /* LVGL_PORT_AVOID_TEAR_ENABLE */ + + // initialize LVGL draw buffers + lv_disp_draw_buf_init(&disp_buf, buf1, buf2, buffer_size); + + ESP_LOGD(TAG, "Register display driver to LVGL"); + lv_disp_drv_init(&disp_drv); +#if EXAMPLE_LVGL_PORT_ROTATION_90 || EXAMPLE_LVGL_PORT_ROTATION_270 + disp_drv.hor_res = LVGL_PORT_V_RES; + disp_drv.ver_res = LVGL_PORT_H_RES; +#else + disp_drv.hor_res = LVGL_PORT_H_RES; + disp_drv.ver_res = LVGL_PORT_V_RES; +#endif + disp_drv.flush_cb = flush_callback; + disp_drv.draw_buf = &disp_buf; + disp_drv.user_data = panel_handle; +#if LVGL_PORT_FULL_REFRESH + disp_drv.full_refresh = 1; +#elif LVGL_PORT_DIRECT_MODE + disp_drv.direct_mode = 1; +#endif + return lv_disp_drv_register(&disp_drv); +} + +static void touchpad_read(lv_indev_drv_t *indev_drv, lv_indev_data_t *data) +{ + esp_lcd_touch_handle_t tp = (esp_lcd_touch_handle_t)indev_drv->user_data; + assert(tp); + + uint16_t touchpad_x; + uint16_t touchpad_y; + uint8_t touchpad_cnt = 0; + + /* Read data from touch controller into memory */ + if (touch_isr_mux == NULL || xSemaphoreTake(touch_isr_mux, 0) == pdTRUE) { + esp_lcd_touch_read_data(tp); + } + + /* Read data from touch controller */ + bool touchpad_pressed = esp_lcd_touch_get_coordinates(tp, &touchpad_x, &touchpad_y, NULL, &touchpad_cnt, 1); + if (touchpad_pressed && touchpad_cnt > 0) { + data->point.x = touchpad_x; + data->point.y = touchpad_y; + data->state = LV_INDEV_STATE_PRESSED; + ESP_LOGD(TAG, "Touch position: %d,%d", touchpad_x, touchpad_y); + } else { + data->state = LV_INDEV_STATE_RELEASED; + } +} + +static lv_indev_t *indev_init(esp_lcd_touch_handle_t tp) +{ + assert(tp); + + static lv_indev_drv_t indev_drv_tp; + + /* Register a touchpad input device */ + 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 = tp; + + return lv_indev_drv_register(&indev_drv_tp); +} + +void lvgl_port_touch_isr_cb(esp_lcd_touch_handle_t tp) +{ + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + xSemaphoreGiveFromISR(touch_isr_mux, &xHigherPriorityTaskWoken); + + if (xHigherPriorityTaskWoken) { + portYIELD_FROM_ISR(); + } +} + +SemaphoreHandle_t init_touch_isr_mux(void) +{ + touch_isr_mux = xSemaphoreCreateBinary(); + assert(touch_isr_mux); + + return touch_isr_mux; +} + +static void tick_increment(void *arg) +{ + /* Tell LVGL how many milliseconds have elapsed */ + lv_tick_inc(LVGL_PORT_TICK_PERIOD_MS); +} + +static esp_err_t 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_timer_handle_t lvgl_tick_timer = NULL; + ESP_ERROR_CHECK(esp_timer_create(&lvgl_tick_timer_args, &lvgl_tick_timer)); + return esp_timer_start_periodic(lvgl_tick_timer, LVGL_PORT_TICK_PERIOD_MS * 1000); +} + +static void lvgl_port_task(void *arg) +{ + ESP_LOGD(TAG, "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)); + } +} + +esp_err_t lvgl_port_init(esp_lcd_panel_handle_t lcd_handle, esp_lcd_touch_handle_t tp_handle) +{ + lv_init(); + ESP_ERROR_CHECK(tick_init()); + + lv_disp_t *disp = display_init(lcd_handle); + assert(disp); + + if (tp_handle) { + lv_indev_t *indev = indev_init(tp_handle); + assert(indev); +#if EXAMPLE_LVGL_PORT_ROTATION_90 + esp_lcd_touch_set_swap_xy(tp_handle, true); + esp_lcd_touch_set_mirror_y(tp_handle, true); +#elif EXAMPLE_LVGL_PORT_ROTATION_180 + esp_lcd_touch_set_mirror_x(tp_handle, true); + esp_lcd_touch_set_mirror_y(tp_handle, true); +#elif EXAMPLE_LVGL_PORT_ROTATION_270 + esp_lcd_touch_set_swap_xy(tp_handle, true); + esp_lcd_touch_set_mirror_x(tp_handle, true); +#endif + } + + lvgl_mux = xSemaphoreCreateRecursiveMutex(); + assert(lvgl_mux); + + ESP_LOGI(TAG, "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); + if (ret != pdPASS) { + ESP_LOGE(TAG, "Failed to create LVGL task"); + return ESP_FAIL; + } + + return ESP_OK; +} + +bool lvgl_port_lock(int timeout_ms) +{ + assert(lvgl_mux && "lvgl_port_init must be called first"); + + const TickType_t timeout_ticks = (timeout_ms < 0) ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms); + return xSemaphoreTakeRecursive(lvgl_mux, timeout_ticks) == pdTRUE; +} + +void lvgl_port_unlock(void) +{ + assert(lvgl_mux && "lvgl_port_init must be called first"); + xSemaphoreGiveRecursive(lvgl_mux); +} diff --git a/examples/display/lcd/qspi_without_ram/main/lvgl_port.h b/examples/display/lcd/qspi_without_ram/main/lvgl_port.h new file mode 100644 index 000000000..e76225921 --- /dev/null +++ b/examples/display/lcd/qspi_without_ram/main/lvgl_port.h @@ -0,0 +1,159 @@ +/* + * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "esp_err.h" +#include "esp_lcd_types.h" +#include "esp_lcd_touch.h" +#include "lvgl.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * LVGL related parameters, can be adjusted by users + * + */ +#define LVGL_PORT_H_RES (400) +#define LVGL_PORT_V_RES (400) +#define LVGL_PORT_TICK_PERIOD_MS (CONFIG_EXAMPLE_LVGL_PORT_TICK) + +/** + * LVGL timer handle task related parameters, can be adjusted by users + * + */ +#define LVGL_PORT_TASK_MAX_DELAY_MS (CONFIG_EXAMPLE_LVGL_PORT_TASK_MAX_DELAY_MS) // The maximum delay of the LVGL timer task, in milliseconds +#define LVGL_PORT_TASK_MIN_DELAY_MS (CONFIG_EXAMPLE_LVGL_PORT_TASK_MIN_DELAY_MS) // The minimum delay of the LVGL timer task, in milliseconds +#define LVGL_PORT_TASK_STACK_SIZE (CONFIG_EXAMPLE_LVGL_PORT_TASK_STACK_SIZE_KB * 1024) // The stack size of the LVGL timer task, in bytes +#define LVGL_PORT_TASK_PRIORITY (CONFIG_EXAMPLE_LVGL_PORT_TASK_PRIORITY) // The priority of the LVGL timer task +#define LVGL_PORT_TASK_CORE (CONFIG_EXAMPLE_LVGL_PORT_TASK_CORE) // The core of the LVGL timer task, +// `-1` means the don't specify the core +/** + * + * 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) + * + */ +#if CONFIG_EXAMPLE_LVGL_PORT_BUF_PSRAM +#define LVGL_PORT_BUFFER_MALLOC_CAPS (MALLOC_CAP_SPIRAM) +#elif CONFIG_EXAMPLE_LVGL_PORT_BUF_INTERNAL +#define LVGL_PORT_BUFFER_MALLOC_CAPS (MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT) +#endif +#define LVGL_PORT_BUFFER_HEIGHT (CONFIG_EXAMPLE_LVGL_PORT_BUF_HEIGHT) + +/** + * Avoid tering related configurations, can be adjusted by users. + * + */ +#define LVGL_PORT_AVOID_TEAR_ENABLE (CONFIG_EXAMPLE_LVGL_PORT_AVOID_TEAR_ENABLE) // Set to 1 to enable +#if LVGL_PORT_AVOID_TEAR_ENABLE +/** + * Set the avoid tearing mode: + * - 0: LCD double-buffer & LVGL full-refresh + * - 1: LCD triple-buffer & LVGL full-refresh + * - 2: LCD double-buffer & LVGL direct-mode (recommended) + * + */ +#define LVGL_PORT_AVOID_TEAR_MODE (CONFIG_EXAMPLE_LVGL_PORT_AVOID_TEAR_MODE) + +/** + * Set the rotation degree of the LCD panel when the avoid tearing function is enabled: + * - 0: 0 degree + * - 90: 90 degree + * - 180: 180 degree + * - 270: 270 degree + * + */ +#define EXAMPLE_LVGL_PORT_ROTATION_DEGREE (CONFIG_EXAMPLE_LVGL_PORT_ROTATION_DEGREE) + +/** + * Below configurations are automatically set according to the above configurations, users do not need to modify them. + * + */ +#if LVGL_PORT_AVOID_TEAR_MODE == 0 +#define LVGL_PORT_LCD_QSPI_BUFFER_NUMS (2) +#define LVGL_PORT_FULL_REFRESH (1) +#elif LVGL_PORT_AVOID_TEAR_MODE == 1 +#define LVGL_PORT_LCD_QSPI_BUFFER_NUMS (3) +#define LVGL_PORT_FULL_REFRESH (1) +#elif LVGL_PORT_AVOID_TEAR_MODE == 2 +#define LVGL_PORT_LCD_QSPI_BUFFER_NUMS (2) +#define LVGL_PORT_DIRECT_MODE (1) +#else +#error "Invalid avoid tearing mode" +#endif /* LVGL_PORT_AVOID_TEAR_MODE */ + +#if EXAMPLE_LVGL_PORT_ROTATION_DEGREE == 0 +#define EXAMPLE_LVGL_PORT_ROTATION_0 (1) +#else +#if EXAMPLE_LVGL_PORT_ROTATION_DEGREE == 90 +#define EXAMPLE_LVGL_PORT_ROTATION_90 (1) +#elif EXAMPLE_LVGL_PORT_ROTATION_DEGREE == 180 +#define EXAMPLE_LVGL_PORT_ROTATION_180 (1) +#elif EXAMPLE_LVGL_PORT_ROTATION_DEGREE == 270 +#define EXAMPLE_LVGL_PORT_ROTATION_270 (1) +#endif +#ifdef LVGL_PORT_LCD_QSPI_BUFFER_NUMS +#undef LVGL_PORT_LCD_QSPI_BUFFER_NUMS +#define LVGL_PORT_LCD_QSPI_BUFFER_NUMS (3) +#endif +#endif /* EXAMPLE_LVGL_PORT_ROTATION_DEGREE */ +#else +#define LVGL_PORT_LCD_QSPI_BUFFER_NUMS (1) +#define LVGL_PORT_FULL_REFRESH (0) +#define LVGL_PORT_DIRECT_MODE (0) +#endif /* LVGL_PORT_AVOID_TEAR_ENABLE */ + +typedef struct { + esp_lcd_panel_handle_t lcd_handle; + esp_lcd_touch_handle_t tp_handle; + struct { + bool tp_int_en; + } flags; +} lvgl_port_config_t; + +/** + * @brief Initialize LVGL port + * + * @param[in] lcd_handle: LCD panel handle + * @param[in] tp_handle: Touch panel handle + * + * @return + * - ESP_OK: Success + * - ESP_ERR_INVALID_ARG: Invalid argument + * - Others: Fail + */ +esp_err_t lvgl_port_init(); + +/** + * @brief Take LVGL mutex + * + * @param[in] timeout_ms: Timeout in [ms]. 0 will block indefinitely. + * + * @return + * - true: Mutex was taken + * - false: Mutex was NOT taken + */ +bool lvgl_port_lock(int timeout_ms); + +/** + * @brief Give LVGL mutex + * + */ +void lvgl_port_unlock(void); + +#ifdef __cplusplus +} +#endif diff --git a/examples/display/lcd/qspi_without_ram/partitions.csv b/examples/display/lcd/qspi_without_ram/partitions.csv new file mode 100644 index 000000000..417a304c4 --- /dev/null +++ b/examples/display/lcd/qspi_without_ram/partitions.csv @@ -0,0 +1,5 @@ +# Name, Type, SubType, Offset, Size, Flags +# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap +nvs, data, nvs, , 0x6000, +phy_init, data, phy, , 0x1000, +factory, app, factory, , 3M, diff --git a/examples/display/lcd/qspi_without_ram/sdkconfig.ci.avoid_tear_mode1 b/examples/display/lcd/qspi_without_ram/sdkconfig.ci.avoid_tear_mode1 new file mode 100644 index 000000000..cd41684e5 --- /dev/null +++ b/examples/display/lcd/qspi_without_ram/sdkconfig.ci.avoid_tear_mode1 @@ -0,0 +1,3 @@ +CONFIG_EXAMPLE_LVGL_PORT_AVOID_TEAR_ENABLE=y +CONFIG_EXAMPLE_LVGL_PORT_AVOID_TEAR_MODE_1=y +CONFIG_EXAMPLE_LVGL_PORT_AVOID_TEAR_MODE=1 diff --git a/examples/display/lcd/qspi_without_ram/sdkconfig.ci.avoid_tear_mode2 b/examples/display/lcd/qspi_without_ram/sdkconfig.ci.avoid_tear_mode2 new file mode 100644 index 000000000..75c8be190 --- /dev/null +++ b/examples/display/lcd/qspi_without_ram/sdkconfig.ci.avoid_tear_mode2 @@ -0,0 +1,3 @@ +CONFIG_EXAMPLE_LVGL_PORT_AVOID_TEAR_ENABLE=y +CONFIG_EXAMPLE_LVGL_PORT_AVOID_TEAR_MODE_2=y +CONFIG_EXAMPLE_LVGL_PORT_AVOID_TEAR_MODE=2 diff --git a/examples/display/lcd/qspi_without_ram/sdkconfig.ci.avoid_tear_mode3 b/examples/display/lcd/qspi_without_ram/sdkconfig.ci.avoid_tear_mode3 new file mode 100644 index 000000000..450a8ed12 --- /dev/null +++ b/examples/display/lcd/qspi_without_ram/sdkconfig.ci.avoid_tear_mode3 @@ -0,0 +1,3 @@ +CONFIG_EXAMPLE_LVGL_PORT_AVOID_TEAR_ENABLE=y +CONFIG_EXAMPLE_LVGL_PORT_AVOID_TEAR_MODE_3=y +CONFIG_EXAMPLE_LVGL_PORT_AVOID_TEAR_MODE=3 diff --git a/examples/display/lcd/qspi_without_ram/sdkconfig.ci.defaults b/examples/display/lcd/qspi_without_ram/sdkconfig.ci.defaults new file mode 100644 index 000000000..e69de29bb diff --git a/examples/display/lcd/qspi_without_ram/sdkconfig.defaults b/examples/display/lcd/qspi_without_ram/sdkconfig.defaults new file mode 100644 index 000000000..e5577f618 --- /dev/null +++ b/examples/display/lcd/qspi_without_ram/sdkconfig.defaults @@ -0,0 +1,32 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) 5.2.0 Project Minimal Configuration +# +CONFIG_IDF_TARGET="esp32s3" +CONFIG_ESPTOOLPY_FLASHMODE_QIO=y +CONFIG_ESPTOOLPY_FLASHFREQ_120M=y +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_COMPILER_OPTIMIZATION_PERF=y +CONFIG_SPIRAM=y +CONFIG_SPIRAM_FETCH_INSTRUCTIONS=y +CONFIG_SPIRAM_RODATA=y +CONFIG_SPIRAM_SPEED_120M=y +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y +CONFIG_ESP32S3_DATA_CACHE_LINE_64B=y +CONFIG_ESP_SYSTEM_PANIC_PRINT_HALT=y +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 +CONFIG_FREERTOS_HZ=1000 +CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID=y +CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y +CONFIG_LV_COLOR_16_SWAP=y +CONFIG_LV_MEM_CUSTOM=y +CONFIG_LV_MEMCPY_MEMSET_STD=y +CONFIG_LV_USE_PERF_MONITOR=y +CONFIG_LV_PERF_MONITOR_ALIGN_CENTER=y +CONFIG_LV_ATTRIBUTE_FAST_MEM_USE_IRAM=y +CONFIG_LV_FONT_MONTSERRAT_12=y +CONFIG_LV_FONT_MONTSERRAT_16=y +CONFIG_LV_USE_DEMO_BENCHMARK=y +CONFIG_LV_USE_DEMO_STRESS=y +CONFIG_LV_USE_DEMO_MUSIC=y +CONFIG_LV_DEMO_MUSIC_AUTO_PLAY=y