From 3e5aaa26d282d7ba973cc5cba906d8728d42f78c Mon Sep 17 00:00:00 2001 From: Tom Zhang Date: Fri, 27 Feb 2026 18:09:20 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A9=B1=E5=8A=A8lcd?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 2 +- components/lcd/CMakeLists.txt | 18 + components/lcd/assets/lcd_demo.jpg | Bin 0 -> 43700 bytes components/lcd/include/lcd.h | 15 + components/lcd/internal/lcd_decode_image.h | 10 + components/lcd/internal/lcd_pretty_effect.h | 8 + components/lcd/internal/st7789p3.h | 89 ++++ components/lcd/src/lcd_decode_image.c | 47 ++ components/lcd/src/lcd_demo.c | 200 +++++++++ components/lcd/src/lcd_pretty_effect.c | 71 +++ components/lcd/src/st7789p3.c | 460 ++++++++++++++++++++ components/platform/CMakeLists.txt | 2 +- components/platform/src/display_st7789.c | 319 +------------- dependencies.lock | 15 +- dependency_whitelist.md | 2 +- main/idf_component.yml | 1 + 16 files changed, 945 insertions(+), 314 deletions(-) create mode 100644 components/lcd/CMakeLists.txt create mode 100644 components/lcd/assets/lcd_demo.jpg create mode 100644 components/lcd/include/lcd.h create mode 100644 components/lcd/internal/lcd_decode_image.h create mode 100644 components/lcd/internal/lcd_pretty_effect.h create mode 100644 components/lcd/internal/st7789p3.h create mode 100644 components/lcd/src/lcd_decode_image.c create mode 100644 components/lcd/src/lcd_demo.c create mode 100644 components/lcd/src/lcd_pretty_effect.c create mode 100644 components/lcd/src/st7789p3.c diff --git a/.vscode/settings.json b/.vscode/settings.json index 191e7bc..9406151 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,7 @@ { "idf.currentSetup": "/Users/moyyang/esp/v5.5.2/esp-idf", "idf.flashType": "UART", - "idf.port": "/dev/tty.wchusbserial5B0B0251771", + "idf.port": "/dev/tty.usbserial-140", "idf.openOcdConfigs": [ "board/esp32s3-builtin.cfg" ], diff --git a/components/lcd/CMakeLists.txt b/components/lcd/CMakeLists.txt new file mode 100644 index 0000000..952d0ed --- /dev/null +++ b/components/lcd/CMakeLists.txt @@ -0,0 +1,18 @@ +idf_component_register( + SRCS + "src/lcd_demo.c" + "src/st7789p3.c" + "src/lcd_decode_image.c" + "src/lcd_pretty_effect.c" + INCLUDE_DIRS + "include" + PRIV_INCLUDE_DIRS + "internal" + REQUIRES + driver + esp_driver_gpio + esp_driver_spi + espressif__esp_jpeg + EMBED_FILES + "assets/lcd_demo.jpg" +) diff --git a/components/lcd/assets/lcd_demo.jpg b/components/lcd/assets/lcd_demo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cacad0905071aeb6d88710ea0478e122988f060c GIT binary patch literal 43700 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 + +#include "esp_err.h" + +#define PLATFORM_LCD_DEMO_IMAGE_W 320 +#define PLATFORM_LCD_DEMO_IMAGE_H 240 + +esp_err_t platform_lcd_decode_image(uint16_t **pixels); diff --git a/components/lcd/internal/lcd_pretty_effect.h b/components/lcd/internal/lcd_pretty_effect.h new file mode 100644 index 0000000..0422b80 --- /dev/null +++ b/components/lcd/internal/lcd_pretty_effect.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +#include "esp_err.h" + +void platform_lcd_pretty_effect_calc_lines(uint16_t *dest, int line, int frame, int linect); +esp_err_t platform_lcd_pretty_effect_init(void); diff --git a/components/lcd/internal/st7789p3.h b/components/lcd/internal/st7789p3.h new file mode 100644 index 0000000..a9ce39e --- /dev/null +++ b/components/lcd/internal/st7789p3.h @@ -0,0 +1,89 @@ +#pragma once + +#include +#include +#include + +#include "driver/spi_master.h" +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct st7789p3_panel_t *st7789p3_handle_t; + +#define ST7789P3_MADCTL_MY 0x80 +#define ST7789P3_MADCTL_MX 0x40 +#define ST7789P3_MADCTL_MV 0x20 +#define ST7789P3_MADCTL_ML 0x10 +#define ST7789P3_MADCTL_BGR 0x08 +#define ST7789P3_MADCTL_MH 0x04 + +typedef struct { + spi_host_device_t spi_host; + int pin_mosi; + int pin_miso; + int pin_sclk; + int pin_cs; + int pin_dc; + int pin_rst; + int pin_bckl; + uint32_t pclk_hz; + uint8_t spi_mode; + int queue_size; + size_t max_transfer_sz; + uint16_t h_res; + uint16_t v_res; + uint8_t madctl_val; + bool invert_color; + int backlight_on_level; + bool init_spi_bus; +} st7789p3_config_t; + +#define ST7789P3_CONFIG_DEFAULT() \ + { \ + .spi_host = SPI2_HOST, \ + .pin_mosi = -1, \ + .pin_miso = -1, \ + .pin_sclk = -1, \ + .pin_cs = -1, \ + .pin_dc = -1, \ + .pin_rst = -1, \ + .pin_bckl = -1, \ + .pclk_hz = 10 * 1000 * 1000, \ + .spi_mode = 0, \ + .queue_size = 7, \ + .max_transfer_sz = 0, \ + .h_res = 320, \ + .v_res = 240, \ + .madctl_val = ST7789P3_MADCTL_MX | ST7789P3_MADCTL_MV, \ + .invert_color = false, \ + .backlight_on_level = 1, \ + .init_spi_bus = true, \ + } + +esp_err_t st7789p3_new_panel(const st7789p3_config_t *config, st7789p3_handle_t *ret_panel); +esp_err_t st7789p3_del(st7789p3_handle_t panel); + +esp_err_t st7789p3_reset(st7789p3_handle_t panel); +esp_err_t st7789p3_init(st7789p3_handle_t panel); + +esp_err_t st7789p3_set_backlight(st7789p3_handle_t panel, bool on); +esp_err_t st7789p3_set_madctl(st7789p3_handle_t panel, uint8_t madctl_val); +esp_err_t st7789p3_set_invert(st7789p3_handle_t panel, bool invert_color); + +esp_err_t st7789p3_set_window( + st7789p3_handle_t panel, uint16_t x_start, uint16_t y_start, uint16_t x_end, uint16_t y_end); +esp_err_t st7789p3_draw_bitmap(st7789p3_handle_t panel, + uint16_t x_start, + uint16_t y_start, + uint16_t x_end, + uint16_t y_end, + const uint16_t *color_data); + +esp_err_t st7789p3_fill_color(st7789p3_handle_t panel, uint16_t color); + +#ifdef __cplusplus +} +#endif diff --git a/components/lcd/src/lcd_decode_image.c b/components/lcd/src/lcd_decode_image.c new file mode 100644 index 0000000..c9cf23e --- /dev/null +++ b/components/lcd/src/lcd_decode_image.c @@ -0,0 +1,47 @@ +#include "lcd_decode_image.h" + +#include + +#include "esp_check.h" +#include "esp_log.h" +#include "jpeg_decoder.h" + +static const char *TAG = "lcd_image_dec"; + +extern const uint8_t lcd_demo_jpg_start[] asm("_binary_lcd_demo_jpg_start"); +extern const uint8_t lcd_demo_jpg_end[] asm("_binary_lcd_demo_jpg_end"); + +esp_err_t platform_lcd_decode_image(uint16_t **pixels) +{ + ESP_RETURN_ON_FALSE(pixels != NULL, ESP_ERR_INVALID_ARG, TAG, "pixels pointer is null"); + *pixels = NULL; + + esp_err_t ret = ESP_OK; + + *pixels = calloc(PLATFORM_LCD_DEMO_IMAGE_W * PLATFORM_LCD_DEMO_IMAGE_H, sizeof(uint16_t)); + ESP_GOTO_ON_FALSE(*pixels != NULL, ESP_ERR_NO_MEM, err, TAG, "alloc image buffer failed"); + + esp_jpeg_image_cfg_t jpeg_cfg = { + .indata = (uint8_t *)lcd_demo_jpg_start, + .indata_size = lcd_demo_jpg_end - lcd_demo_jpg_start, + .outbuf = (uint8_t *)(*pixels), + .outbuf_size = PLATFORM_LCD_DEMO_IMAGE_W * PLATFORM_LCD_DEMO_IMAGE_H * sizeof(uint16_t), + .out_format = JPEG_IMAGE_FORMAT_RGB565, + .out_scale = JPEG_IMAGE_SCALE_0, + .flags = { + .swap_color_bytes = 1, + }, + }; + + esp_jpeg_image_output_t out_img = {0}; + ret = esp_jpeg_decode(&jpeg_cfg, &out_img); + ESP_GOTO_ON_ERROR(ret, err, TAG, "jpeg decode failed"); + + ESP_LOGI(TAG, "lcd demo image decoded: %d x %d", out_img.width, out_img.height); + return ESP_OK; + +err: + free(*pixels); + *pixels = NULL; + return ret; +} diff --git a/components/lcd/src/lcd_demo.c b/components/lcd/src/lcd_demo.c new file mode 100644 index 0000000..b27565e --- /dev/null +++ b/components/lcd/src/lcd_demo.c @@ -0,0 +1,200 @@ +#include "lcd.h" + +#include + +#include "esp_check.h" +#include "esp_heap_caps.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "lcd_pretty_effect.h" +#include "sdkconfig.h" +#include "st7789p3.h" + +static const char *TAG = "lcd_demo"; + +#define ST7789_SPI_HOST SPI2_HOST +#define LCD_DEMO_H_RES 320 +#define LCD_DEMO_V_RES 240 +#define LCD_DEMO_PARALLEL_LINES 16 +#define LCD_DEMO_TASK_STACK_SIZE (6 * 1024) +#define LCD_DEMO_TASK_PRIORITY (tskIDLE_PRIORITY + 1) +#define LCD_DEMO_TASK_CORE_ID 1 + +#ifndef CONFIG_TQ_SCREEN_SPI_MODE +#define CONFIG_TQ_SCREEN_SPI_MODE 0 +#endif + +#ifndef CONFIG_TQ_SCREEN_BACKLIGHT_ACTIVE_HIGH +#define CONFIG_TQ_SCREEN_BACKLIGHT_ACTIVE_HIGH 1 +#endif + +#if CONFIG_TQ_SCREEN_BACKLIGHT_ACTIVE_HIGH +#define ST7789_BACKLIGHT_ON_LEVEL 1 +#else +#define ST7789_BACKLIGHT_ON_LEVEL 0 +#endif + +typedef struct { + st7789p3_handle_t panel; + TaskHandle_t demo_task; + bool initialized; +} lcd_state_t; + +static lcd_state_t s_lcd; + +static int resolve_reset_pin(void) +{ + if (CONFIG_TQ_SCREEN_RESET_PIN >= 0) { + return CONFIG_TQ_SCREEN_RESET_PIN; + } + return CONFIG_TQ_SCREEN_BACKLIGHT_PIN; +} + +static void lcd_demo_task(void *arg) +{ + st7789p3_handle_t panel = (st7789p3_handle_t)arg; + uint16_t *lines[2] = {0}; + const size_t line_buffer_bytes = LCD_DEMO_H_RES * LCD_DEMO_PARALLEL_LINES * sizeof(uint16_t); + int frame = 0; + int draw_buffer_index = 0; + + esp_err_t err = platform_lcd_pretty_effect_init(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "pretty effect init failed: %s", esp_err_to_name(err)); + goto exit; + } + + for (int i = 0; i < 2; i++) { + lines[i] = heap_caps_malloc(line_buffer_bytes, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); + if (lines[i] == NULL) { + lines[i] = heap_caps_malloc(line_buffer_bytes, MALLOC_CAP_8BIT); + } + if (lines[i] == NULL) { + ESP_LOGE(TAG, "lcd demo line buffer alloc failed"); + goto exit; + } + } + + ESP_LOGI(TAG, "ST7789P3 pretty effect demo started"); + while (true) { + frame++; + for (int y = 0; y < LCD_DEMO_V_RES; y += LCD_DEMO_PARALLEL_LINES) { + int lines_to_draw = LCD_DEMO_V_RES - y; + if (lines_to_draw > LCD_DEMO_PARALLEL_LINES) { + lines_to_draw = LCD_DEMO_PARALLEL_LINES; + } + + platform_lcd_pretty_effect_calc_lines(lines[draw_buffer_index], y, frame, lines_to_draw); + err = st7789p3_draw_bitmap(panel, 0, y, LCD_DEMO_H_RES, y + lines_to_draw, lines[draw_buffer_index]); + if (err != ESP_OK) { + ESP_LOGE(TAG, "lcd demo draw failed: %s", esp_err_to_name(err)); + goto exit; + } + + draw_buffer_index ^= 1; + vTaskDelay(pdMS_TO_TICKS(1)); + } + } + +exit: + if (lines[0] != NULL) { + heap_caps_free(lines[0]); + } + if (lines[1] != NULL) { + heap_caps_free(lines[1]); + } + s_lcd.demo_task = NULL; + vTaskDelete(NULL); +} + +void lcd_module_deinit(void) +{ +#if CONFIG_TQ_SCREEN_ENABLE + if (s_lcd.demo_task != NULL) { + vTaskDelete(s_lcd.demo_task); + s_lcd.demo_task = NULL; + } + if (s_lcd.panel != NULL) { + (void)st7789p3_del(s_lcd.panel); + s_lcd.panel = NULL; + } + s_lcd.initialized = false; +#endif +} + +esp_err_t lcd_module_init(void) +{ +#if !CONFIG_TQ_SCREEN_ENABLE + return ESP_ERR_NOT_SUPPORTED; +#else + if (s_lcd.initialized) { + return ESP_OK; + } + + st7789p3_config_t lcd_cfg = ST7789P3_CONFIG_DEFAULT(); + lcd_cfg.spi_host = ST7789_SPI_HOST; + lcd_cfg.pin_miso = CONFIG_TQ_PRINTER_MISO_PIN; + lcd_cfg.pin_mosi = CONFIG_TQ_SPI_SHARED_MOSI_PIN; + lcd_cfg.pin_sclk = CONFIG_TQ_SPI_SHARED_CLK_PIN; + lcd_cfg.pin_cs = CONFIG_TQ_SCREEN_CS_PIN; + lcd_cfg.pin_dc = CONFIG_TQ_SCREEN_DC_PIN; + lcd_cfg.pin_rst = resolve_reset_pin(); + lcd_cfg.pin_bckl = CONFIG_TQ_SCREEN_BACKLIGHT_PIN; + lcd_cfg.backlight_on_level = ST7789_BACKLIGHT_ON_LEVEL; + lcd_cfg.pclk_hz = CONFIG_TQ_SCREEN_PIXEL_CLOCK_HZ; + lcd_cfg.spi_mode = CONFIG_TQ_SCREEN_SPI_MODE; + lcd_cfg.h_res = LCD_DEMO_H_RES; + lcd_cfg.v_res = LCD_DEMO_V_RES; + lcd_cfg.madctl_val = ST7789P3_MADCTL_MX | ST7789P3_MADCTL_MV; + lcd_cfg.invert_color = true; + lcd_cfg.max_transfer_sz = LCD_DEMO_PARALLEL_LINES * LCD_DEMO_H_RES * sizeof(uint16_t) + 8; + + esp_err_t err = st7789p3_new_panel(&lcd_cfg, &s_lcd.panel); + if (err != ESP_OK) { + return err; + } + + err = st7789p3_init(s_lcd.panel); + if (err != ESP_OK) { + lcd_module_deinit(); + return err; + } + + s_lcd.initialized = true; + ESP_LOGI(TAG, + "st7789p3 ready: spi_host=%d mosi=%d clk=%d miso=%d dc=%d cs=%d rst=%d bk=%d pclk=%u", + ST7789_SPI_HOST, + CONFIG_TQ_SPI_SHARED_MOSI_PIN, + CONFIG_TQ_SPI_SHARED_CLK_PIN, + CONFIG_TQ_PRINTER_MISO_PIN, + CONFIG_TQ_SCREEN_DC_PIN, + CONFIG_TQ_SCREEN_CS_PIN, + lcd_cfg.pin_rst, + CONFIG_TQ_SCREEN_BACKLIGHT_PIN, + (unsigned)lcd_cfg.pclk_hz); + return ESP_OK; +#endif +} + +esp_err_t lcd_module_start_demo(void) +{ +#if !CONFIG_TQ_SCREEN_ENABLE + return ESP_ERR_NOT_SUPPORTED; +#else + ESP_RETURN_ON_ERROR(lcd_module_init(), TAG, "display init failed"); + if (s_lcd.demo_task != NULL) { + return ESP_OK; + } + + BaseType_t rc = xTaskCreatePinnedToCore(lcd_demo_task, + "lcd_demo", + LCD_DEMO_TASK_STACK_SIZE, + (void *)s_lcd.panel, + LCD_DEMO_TASK_PRIORITY, + &s_lcd.demo_task, + LCD_DEMO_TASK_CORE_ID); + ESP_RETURN_ON_FALSE(rc == pdPASS, ESP_ERR_NO_MEM, TAG, "lcd demo task create failed"); + return ESP_OK; +#endif +} diff --git a/components/lcd/src/lcd_pretty_effect.c b/components/lcd/src/lcd_pretty_effect.c new file mode 100644 index 0000000..76ee54c --- /dev/null +++ b/components/lcd/src/lcd_pretty_effect.c @@ -0,0 +1,71 @@ +#include "lcd_pretty_effect.h" + +#include +#include + +#include "lcd_decode_image.h" + +static uint16_t *s_pixels; +static int s_prev_frame = -1; +static int8_t s_xofs[PLATFORM_LCD_DEMO_IMAGE_W]; +static int8_t s_yofs[PLATFORM_LCD_DEMO_IMAGE_H]; +static int8_t s_xcomp[PLATFORM_LCD_DEMO_IMAGE_W]; +static int8_t s_ycomp[PLATFORM_LCD_DEMO_IMAGE_H]; + +static inline uint16_t get_bgnd_pixel(int x, int y) +{ + return (uint16_t)*(s_pixels + (y * PLATFORM_LCD_DEMO_IMAGE_W) + x); +} + +esp_err_t platform_lcd_pretty_effect_init(void) +{ + if (s_pixels != NULL) { + return ESP_OK; + } + return platform_lcd_decode_image(&s_pixels); +} + +void platform_lcd_pretty_effect_calc_lines(uint16_t *dest, int line, int frame, int linect) +{ + if (dest == NULL || linect <= 0) { + return; + } + + if (s_pixels == NULL) { + memset(dest, 0, (size_t)PLATFORM_LCD_DEMO_IMAGE_W * (size_t)linect * sizeof(uint16_t)); + return; + } + + if (frame != s_prev_frame) { + for (int x = 0; x < PLATFORM_LCD_DEMO_IMAGE_W; x++) { + s_xofs[x] = sin(frame * 0.15 + x * 0.06) * 4; + s_xcomp[x] = sin(frame * 0.11 + x * 0.12) * 4; + } + for (int y = 0; y < PLATFORM_LCD_DEMO_IMAGE_H; y++) { + s_yofs[y] = sin(frame * 0.1 + y * 0.05) * 4; + s_ycomp[y] = sin(frame * 0.07 + y * 0.15) * 4; + } + s_prev_frame = frame; + } + + for (int y = line; y < line + linect; y++) { + for (int x = 0; x < PLATFORM_LCD_DEMO_IMAGE_W; x++) { + int sample_x = x + s_yofs[y] + s_xcomp[x]; + int sample_y = y + s_xofs[x] + s_ycomp[y]; + + if (sample_x < 0) { + sample_x = 0; + } else if (sample_x >= PLATFORM_LCD_DEMO_IMAGE_W) { + sample_x = PLATFORM_LCD_DEMO_IMAGE_W - 1; + } + + if (sample_y < 0) { + sample_y = 0; + } else if (sample_y >= PLATFORM_LCD_DEMO_IMAGE_H) { + sample_y = PLATFORM_LCD_DEMO_IMAGE_H - 1; + } + + *dest++ = get_bgnd_pixel(sample_x, sample_y); + } + } +} diff --git a/components/lcd/src/st7789p3.c b/components/lcd/src/st7789p3.c new file mode 100644 index 0000000..582ed21 --- /dev/null +++ b/components/lcd/src/st7789p3.c @@ -0,0 +1,460 @@ +#include "st7789p3.h" + +#include +#include + +#include "driver/gpio.h" +#include "esp_attr.h" +#include "esp_heap_caps.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#define ST7789P3_CMD_DELAY_FLAG 0x80 +#define ST7789P3_CMD_DATA_LEN_MASK 0x1F +#define ST7789P3_TX_MAX_CHUNK_BYTES 1024 + +typedef struct { + uint8_t cmd; + uint8_t data[16]; + uint8_t data_bytes; +} st7789p3_lcd_init_cmd_t; + +struct st7789p3_panel_t { + st7789p3_config_t cfg; + spi_device_handle_t spi; + bool bus_owned; +}; + +static const char *TAG = "st7789p3"; + +// Sequence verified on the LCD used by docs/lcd_verification. +DRAM_ATTR static const st7789p3_lcd_init_cmd_t s_st7789p3_init_cmds[] = { + {0x36, {0x00}, 1}, + {0x3A, {0x55}, 1}, + {0xB2, {0x0C, 0x0C, 0x00, 0x33, 0x33}, 5}, + {0xB7, {0x45}, 1}, + {0xBB, {0x2B}, 1}, + {0xC0, {0x2C}, 1}, + {0xC2, {0x01, 0xFF}, 2}, + {0xC3, {0x11}, 1}, + {0xC4, {0x20}, 1}, + {0xC6, {0x0F}, 1}, + {0xD0, {0xA4, 0xA1}, 2}, + {0xE0, {0xD0, 0x00, 0x05, 0x0E, 0x15, 0x0D, 0x37, 0x43, 0x47, 0x09, 0x15, 0x12, 0x16, 0x19}, 14}, + {0xE1, {0xD0, 0x00, 0x05, 0x0D, 0x0C, 0x06, 0x2D, 0x44, 0x40, 0x0E, 0x1C, 0x18, 0x16, 0x19}, 14}, + {0x11, {0}, ST7789P3_CMD_DELAY_FLAG}, + {0, {0}, 0xFF}, +}; + +static esp_err_t st7789p3_validate_config(const st7789p3_config_t *config) +{ + if (config == NULL) { + return ESP_ERR_INVALID_ARG; + } + if (config->pin_mosi < 0 || config->pin_sclk < 0 || config->pin_cs < 0 || config->pin_dc < 0) { + return ESP_ERR_INVALID_ARG; + } + if (config->h_res == 0 || config->v_res == 0) { + return ESP_ERR_INVALID_ARG; + } + if (config->spi_mode > 3) { + return ESP_ERR_INVALID_ARG; + } + return ESP_OK; +} + +static esp_err_t st7789p3_configure_gpio(st7789p3_handle_t panel) +{ + uint64_t pin_mask = 0; + + pin_mask |= (1ULL << panel->cfg.pin_dc); + if (panel->cfg.pin_rst >= 0) { + pin_mask |= (1ULL << panel->cfg.pin_rst); + } + if (panel->cfg.pin_bckl >= 0) { + pin_mask |= (1ULL << panel->cfg.pin_bckl); + } + + gpio_config_t io_conf = { + .pin_bit_mask = pin_mask, + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + + return gpio_config(&io_conf); +} + +static esp_err_t st7789p3_spi_write(st7789p3_handle_t panel, int dc_level, const void *data, size_t data_size) +{ + if (data_size == 0) { + return ESP_OK; + } + + esp_err_t ret = gpio_set_level(panel->cfg.pin_dc, dc_level); + if (ret != ESP_OK) { + return ret; + } + + spi_transaction_t t = {0}; + t.length = data_size * 8; + t.tx_buffer = data; + + return spi_device_transmit(panel->spi, &t); +} + +static esp_err_t st7789p3_send_cmd(st7789p3_handle_t panel, uint8_t cmd) +{ + return st7789p3_spi_write(panel, 0, &cmd, 1); +} + +static esp_err_t st7789p3_send_data(st7789p3_handle_t panel, const void *data, size_t data_size) +{ + const uint8_t *data_ptr = (const uint8_t *)data; + size_t remaining = data_size; + size_t tx_chunk_size = ST7789P3_TX_MAX_CHUNK_BYTES; + + if (panel->cfg.max_transfer_sz > 0 && panel->cfg.max_transfer_sz < tx_chunk_size) { + tx_chunk_size = panel->cfg.max_transfer_sz; + } + + while (remaining > 0) { + size_t chunk = (remaining > tx_chunk_size) ? tx_chunk_size : remaining; + esp_err_t ret = st7789p3_spi_write(panel, 1, data_ptr, chunk); + if (ret != ESP_OK) { + return ret; + } + data_ptr += chunk; + remaining -= chunk; + } + return ESP_OK; +} + +static esp_err_t st7789p3_run_init_cmds(st7789p3_handle_t panel) +{ + for (size_t i = 0; s_st7789p3_init_cmds[i].data_bytes != 0xFF; i++) { + const st7789p3_lcd_init_cmd_t *init_cmd = &s_st7789p3_init_cmds[i]; + uint8_t data_bytes = init_cmd->data_bytes & ST7789P3_CMD_DATA_LEN_MASK; + uint8_t data[16] = {0}; + + if (data_bytes > 0) { + memcpy(data, init_cmd->data, data_bytes); + } + + if (init_cmd->cmd == 0x36 && data_bytes == 1) { + data[0] = panel->cfg.madctl_val; + } + + esp_err_t ret = st7789p3_send_cmd(panel, init_cmd->cmd); + if (ret != ESP_OK) { + return ret; + } + + ret = st7789p3_send_data(panel, data, data_bytes); + if (ret != ESP_OK) { + return ret; + } + + if (init_cmd->data_bytes & ST7789P3_CMD_DELAY_FLAG) { + vTaskDelay(pdMS_TO_TICKS(100)); + } + } + return ESP_OK; +} + +esp_err_t st7789p3_new_panel(const st7789p3_config_t *config, st7789p3_handle_t *ret_panel) +{ + if (ret_panel == NULL) { + return ESP_ERR_INVALID_ARG; + } + *ret_panel = NULL; + + esp_err_t ret = st7789p3_validate_config(config); + if (ret != ESP_OK) { + return ret; + } + + st7789p3_handle_t panel = calloc(1, sizeof(*panel)); + if (panel == NULL) { + return ESP_ERR_NO_MEM; + } + + panel->cfg = *config; + if (panel->cfg.pclk_hz == 0) { + panel->cfg.pclk_hz = 10 * 1000 * 1000; + } + if (panel->cfg.queue_size <= 0) { + panel->cfg.queue_size = 7; + } + if (panel->cfg.max_transfer_sz == 0) { + panel->cfg.max_transfer_sz = panel->cfg.h_res * 20 * sizeof(uint16_t) + 8; + } + + ret = st7789p3_configure_gpio(panel); + if (ret != ESP_OK) { + free(panel); + return ret; + } + + if (panel->cfg.init_spi_bus) { + spi_bus_config_t buscfg = { + .mosi_io_num = panel->cfg.pin_mosi, + .miso_io_num = panel->cfg.pin_miso, + .sclk_io_num = panel->cfg.pin_sclk, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + .max_transfer_sz = panel->cfg.max_transfer_sz, + }; + + ret = spi_bus_initialize(panel->cfg.spi_host, &buscfg, SPI_DMA_CH_AUTO); + if (ret == ESP_OK) { + panel->bus_owned = true; + } else if (ret == ESP_ERR_INVALID_STATE) { + ESP_LOGW(TAG, "SPI host already initialized, skip bus init"); + } else { + free(panel); + return ret; + } + } + + spi_device_interface_config_t devcfg = { + .clock_speed_hz = panel->cfg.pclk_hz, + .mode = panel->cfg.spi_mode, + .spics_io_num = panel->cfg.pin_cs, + .queue_size = panel->cfg.queue_size, + }; + + ret = spi_bus_add_device(panel->cfg.spi_host, &devcfg, &panel->spi); + if (ret != ESP_OK) { + if (panel->bus_owned) { + spi_bus_free(panel->cfg.spi_host); + } + free(panel); + return ret; + } + + *ret_panel = panel; + return ESP_OK; +} + +esp_err_t st7789p3_del(st7789p3_handle_t panel) +{ + if (panel == NULL) { + return ESP_ERR_INVALID_ARG; + } + + esp_err_t ret = ESP_OK; + esp_err_t local_ret = spi_bus_remove_device(panel->spi); + if (local_ret != ESP_OK) { + ret = local_ret; + } + + if (panel->bus_owned) { + local_ret = spi_bus_free(panel->cfg.spi_host); + if (ret == ESP_OK && local_ret != ESP_OK) { + ret = local_ret; + } + } + + free(panel); + return ret; +} + +esp_err_t st7789p3_set_backlight(st7789p3_handle_t panel, bool on) +{ + if (panel == NULL) { + return ESP_ERR_INVALID_ARG; + } + if (panel->cfg.pin_bckl < 0) { + return ESP_OK; + } + + int on_level = panel->cfg.backlight_on_level ? 1 : 0; + int off_level = on_level ? 0 : 1; + return gpio_set_level(panel->cfg.pin_bckl, on ? on_level : off_level); +} + +esp_err_t st7789p3_set_madctl(st7789p3_handle_t panel, uint8_t madctl_val) +{ + if (panel == NULL) { + return ESP_ERR_INVALID_ARG; + } + + panel->cfg.madctl_val = madctl_val; + esp_err_t ret = st7789p3_send_cmd(panel, 0x36); + if (ret != ESP_OK) { + return ret; + } + return st7789p3_send_data(panel, &madctl_val, 1); +} + +esp_err_t st7789p3_set_invert(st7789p3_handle_t panel, bool invert_color) +{ + if (panel == NULL) { + return ESP_ERR_INVALID_ARG; + } + + panel->cfg.invert_color = invert_color; + return st7789p3_send_cmd(panel, invert_color ? 0x21 : 0x20); +} + +esp_err_t st7789p3_reset(st7789p3_handle_t panel) +{ + if (panel == NULL) { + return ESP_ERR_INVALID_ARG; + } + if (panel->cfg.pin_rst < 0) { + return ESP_OK; + } + + esp_err_t ret = gpio_set_level(panel->cfg.pin_rst, 0); + if (ret != ESP_OK) { + return ret; + } + vTaskDelay(pdMS_TO_TICKS(100)); + + ret = gpio_set_level(panel->cfg.pin_rst, 1); + if (ret != ESP_OK) { + return ret; + } + vTaskDelay(pdMS_TO_TICKS(100)); + return ESP_OK; +} + +esp_err_t st7789p3_init(st7789p3_handle_t panel) +{ + if (panel == NULL) { + return ESP_ERR_INVALID_ARG; + } + + esp_err_t ret = st7789p3_set_backlight(panel, false); + if (ret != ESP_OK) { + return ret; + } + + ret = st7789p3_reset(panel); + if (ret != ESP_OK) { + return ret; + } + + ret = st7789p3_run_init_cmds(panel); + if (ret != ESP_OK) { + return ret; + } + + ret = st7789p3_set_invert(panel, panel->cfg.invert_color); + if (ret != ESP_OK) { + return ret; + } + + ret = st7789p3_send_cmd(panel, 0x29); + if (ret != ESP_OK) { + return ret; + } + vTaskDelay(pdMS_TO_TICKS(100)); + + return st7789p3_set_backlight(panel, true); +} + +esp_err_t st7789p3_set_window( + st7789p3_handle_t panel, uint16_t x_start, uint16_t y_start, uint16_t x_end, uint16_t y_end) +{ + if (panel == NULL || x_start >= x_end || y_start >= y_end) { + return ESP_ERR_INVALID_ARG; + } + if (x_end > panel->cfg.h_res || y_end > panel->cfg.v_res) { + return ESP_ERR_INVALID_ARG; + } + + uint8_t x_data[4] = { + (uint8_t)(x_start >> 8), + (uint8_t)(x_start & 0xFF), + (uint8_t)((x_end - 1) >> 8), + (uint8_t)((x_end - 1) & 0xFF), + }; + uint8_t y_data[4] = { + (uint8_t)(y_start >> 8), + (uint8_t)(y_start & 0xFF), + (uint8_t)((y_end - 1) >> 8), + (uint8_t)((y_end - 1) & 0xFF), + }; + + esp_err_t ret = st7789p3_send_cmd(panel, 0x2A); + if (ret != ESP_OK) { + return ret; + } + ret = st7789p3_send_data(panel, x_data, sizeof(x_data)); + if (ret != ESP_OK) { + return ret; + } + + ret = st7789p3_send_cmd(panel, 0x2B); + if (ret != ESP_OK) { + return ret; + } + ret = st7789p3_send_data(panel, y_data, sizeof(y_data)); + if (ret != ESP_OK) { + return ret; + } + + return st7789p3_send_cmd(panel, 0x2C); +} + +esp_err_t st7789p3_draw_bitmap(st7789p3_handle_t panel, + uint16_t x_start, + uint16_t y_start, + uint16_t x_end, + uint16_t y_end, + const uint16_t *color_data) +{ + if (panel == NULL || color_data == NULL) { + return ESP_ERR_INVALID_ARG; + } + + esp_err_t ret = st7789p3_set_window(panel, x_start, y_start, x_end, y_end); + if (ret != ESP_OK) { + return ret; + } + + size_t pixel_count = (size_t)(x_end - x_start) * (size_t)(y_end - y_start); + return st7789p3_send_data(panel, color_data, pixel_count * sizeof(uint16_t)); +} + +esp_err_t st7789p3_fill_color(st7789p3_handle_t panel, uint16_t color) +{ + if (panel == NULL) { + return ESP_ERR_INVALID_ARG; + } + + static const size_t fill_pixels = 256; + uint16_t *fill_buffer = heap_caps_malloc(fill_pixels * sizeof(uint16_t), MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); + if (fill_buffer == NULL) { + return ESP_ERR_NO_MEM; + } + + for (size_t i = 0; i < fill_pixels; i++) { + fill_buffer[i] = color; + } + + esp_err_t ret = st7789p3_set_window(panel, 0, 0, panel->cfg.h_res, panel->cfg.v_res); + if (ret != ESP_OK) { + free(fill_buffer); + return ret; + } + + size_t total_pixels = (size_t)panel->cfg.h_res * (size_t)panel->cfg.v_res; + while (total_pixels > 0) { + size_t pixels_to_send = (total_pixels > fill_pixels) ? fill_pixels : total_pixels; + ret = st7789p3_send_data(panel, fill_buffer, pixels_to_send * sizeof(uint16_t)); + if (ret != ESP_OK) { + free(fill_buffer); + return ret; + } + total_pixels -= pixels_to_send; + } + + free(fill_buffer); + return ESP_OK; +} diff --git a/components/platform/CMakeLists.txt b/components/platform/CMakeLists.txt index 1efdb67..0d35768 100644 --- a/components/platform/CMakeLists.txt +++ b/components/platform/CMakeLists.txt @@ -13,6 +13,7 @@ idf_component_register( PRIV_INCLUDE_DIRS "internal" REQUIRES + lcd bt esp_coex esp_wifi @@ -21,7 +22,6 @@ idf_component_register( nvs_flash mbedtls esp_adc - esp_lcd esp_codec_dev esp_audio_codec driver diff --git a/components/platform/src/display_st7789.c b/components/platform/src/display_st7789.c index 6bf2f46..2b5ddea 100644 --- a/components/platform/src/display_st7789.c +++ b/components/platform/src/display_st7789.c @@ -1,317 +1,18 @@ #include "platform.h" -#include -#include -#include +#include "lcd.h" -#include "driver/gpio.h" -#include "driver/spi_master.h" -#include "esp_check.h" -#include "esp_heap_caps.h" -#include "esp_lcd_panel_io.h" -#include "esp_lcd_panel_ops.h" -#include "esp_lcd_panel_vendor.h" -#include "esp_log.h" -#include "sdkconfig.h" - -static const char *TAG = "display_st7789"; - -#define ST7789_SPI_HOST SPI2_HOST -#define ST7789_COLOR_BYTES_PER_PIXEL 2 - -#ifndef CONFIG_TQ_SCREEN_SPI_MODE -#define CONFIG_TQ_SCREEN_SPI_MODE 0 -#endif - -#ifndef CONFIG_TQ_SCREEN_BACKLIGHT_ACTIVE_HIGH -#define CONFIG_TQ_SCREEN_BACKLIGHT_ACTIVE_HIGH 1 -#endif - -#if CONFIG_TQ_SCREEN_BACKLIGHT_ACTIVE_HIGH -#define ST7789_BACKLIGHT_ON_LEVEL 1 -#define ST7789_BACKLIGHT_OFF_LEVEL 0 -#else -#define ST7789_BACKLIGHT_ON_LEVEL 0 -#define ST7789_BACKLIGHT_OFF_LEVEL 1 -#endif - -typedef struct { - esp_lcd_panel_io_handle_t io_handle; - esp_lcd_panel_handle_t panel_handle; - bool bus_owned; - bool initialized; -} st7789_display_state_t; - -static st7789_display_state_t s_display; - -#if CONFIG_TQ_SCREEN_COLOR_ORDER_BGR -#define ST7789_COLOR_ORDER LCD_RGB_ELEMENT_ORDER_BGR -#else -#define ST7789_COLOR_ORDER LCD_RGB_ELEMENT_ORDER_RGB -#endif - -#if CONFIG_TQ_SCREEN_SWAP_XY -#define ST7789_SWAP_XY true -#else -#define ST7789_SWAP_XY false -#endif - -#if CONFIG_TQ_SCREEN_MIRROR_X -#define ST7789_MIRROR_X true -#else -#define ST7789_MIRROR_X false -#endif - -#if CONFIG_TQ_SCREEN_MIRROR_Y -#define ST7789_MIRROR_Y true -#else -#define ST7789_MIRROR_Y false -#endif - -static uint16_t rgb565(uint8_t r, uint8_t g, uint8_t b) { - return (uint16_t)(((uint16_t)(r & 0xF8) << 8) | - ((uint16_t)(g & 0xFC) << 3) | - ((uint16_t)(b & 0xF8) >> 3)); +void platform_display_deinit(void) +{ + lcd_module_deinit(); } -static uint16_t test_pattern_color(int x, int y, int h_res, int v_res) { - int top = v_res / 3; - int middle = (v_res * 2) / 3; - - if (y < top) { - uint8_t r = (uint8_t)((x * 255) / (h_res - 1)); - uint8_t g = (uint8_t)(255 - r); - return rgb565(r, g, 48); - } - - if (y < middle) { - uint8_t gray = (uint8_t)(((y - top) * 255) / ((middle - top) > 1 ? (middle - top - 1) : 1)); - return rgb565(gray, gray, gray); - } - - bool checker = (((x / 16) + (y / 16)) & 1) != 0; - return checker ? rgb565(250, 250, 250) : rgb565(8, 8, 8); +esp_err_t platform_display_init(void) +{ + return lcd_module_init(); } -static void fill_test_pattern_chunk(uint16_t *buf, - int y_start, - int lines, - int h_res, - int v_res) { - for (int row = 0; row < lines; ++row) { - int y = y_start + row; - for (int x = 0; x < h_res; ++x) { - uint16_t color = test_pattern_color(x, y, h_res, v_res); - - if (x == 0 || y == 0 || x == (h_res - 1) || y == (v_res - 1)) { - color = rgb565(255, 0, 0); - } else if (x == (h_res / 2) || y == (v_res / 2)) { - color = rgb565(255, 255, 255); - } - - buf[row * h_res + x] = color; - } - } -} - -void platform_display_deinit(void) { -#if CONFIG_TQ_SCREEN_ENABLE - if (CONFIG_TQ_SCREEN_BACKLIGHT_PIN >= 0) { - (void)gpio_set_level(CONFIG_TQ_SCREEN_BACKLIGHT_PIN, ST7789_BACKLIGHT_OFF_LEVEL); - } - - if (s_display.panel_handle != NULL) { - (void)esp_lcd_panel_del(s_display.panel_handle); - s_display.panel_handle = NULL; - } - if (s_display.io_handle != NULL) { - (void)esp_lcd_panel_io_del(s_display.io_handle); - s_display.io_handle = NULL; - } - if (s_display.bus_owned) { - (void)spi_bus_free(ST7789_SPI_HOST); - s_display.bus_owned = false; - } - s_display.initialized = false; -#endif -} - -esp_err_t platform_display_init(void) { -#if !CONFIG_TQ_SCREEN_ENABLE - return ESP_ERR_NOT_SUPPORTED; -#else - if (s_display.initialized) { - return ESP_OK; - } - - if (CONFIG_TQ_SCREEN_BACKLIGHT_PIN >= 0) { - ESP_RETURN_ON_ERROR(gpio_set_level(CONFIG_TQ_SCREEN_BACKLIGHT_PIN, ST7789_BACKLIGHT_OFF_LEVEL), - TAG, - "set backlight off failed"); - } - - const size_t max_transfer_size = (size_t)CONFIG_TQ_SCREEN_H_RES * - (size_t)CONFIG_TQ_SCREEN_DRAW_LINES * - ST7789_COLOR_BYTES_PER_PIXEL + 8; - - spi_bus_config_t bus_cfg = { - .sclk_io_num = CONFIG_TQ_SPI_SHARED_CLK_PIN, - .mosi_io_num = CONFIG_TQ_SPI_SHARED_MOSI_PIN, - .miso_io_num = -1, - .quadwp_io_num = -1, - .quadhd_io_num = -1, - .max_transfer_sz = (int)max_transfer_size, - }; - - esp_err_t err = spi_bus_initialize(ST7789_SPI_HOST, &bus_cfg, SPI_DMA_CH_AUTO); - if (err == ESP_OK) { - s_display.bus_owned = true; - } else if (err == ESP_ERR_INVALID_STATE) { - s_display.bus_owned = false; - ESP_LOGW(TAG, "SPI host already initialized, reuse host=%d", ST7789_SPI_HOST); - } else { - return err; - } - - esp_lcd_panel_io_spi_config_t io_cfg = { - .cs_gpio_num = CONFIG_TQ_SCREEN_CS_PIN, - .dc_gpio_num = CONFIG_TQ_SCREEN_DC_PIN, - .spi_mode = CONFIG_TQ_SCREEN_SPI_MODE, - .pclk_hz = CONFIG_TQ_SCREEN_PIXEL_CLOCK_HZ, - .trans_queue_depth = 1, - .lcd_cmd_bits = 8, - .lcd_param_bits = 8, - }; - err = esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)ST7789_SPI_HOST, &io_cfg, &s_display.io_handle); - if (err != ESP_OK) { - platform_display_deinit(); - return err; - } - - esp_lcd_panel_dev_config_t panel_cfg = { - .reset_gpio_num = CONFIG_TQ_SCREEN_RESET_PIN, - .rgb_ele_order = ST7789_COLOR_ORDER, - .bits_per_pixel = 16, - }; - err = esp_lcd_new_panel_st7789(s_display.io_handle, &panel_cfg, &s_display.panel_handle); - if (err != ESP_OK) { - platform_display_deinit(); - return err; - } - - err = esp_lcd_panel_reset(s_display.panel_handle); - if (err != ESP_OK) { - ESP_LOGE(TAG, "panel reset failed: %s", esp_err_to_name(err)); - platform_display_deinit(); - return err; - } - err = esp_lcd_panel_init(s_display.panel_handle); - if (err != ESP_OK) { - ESP_LOGE(TAG, "panel init failed: %s", esp_err_to_name(err)); - platform_display_deinit(); - return err; - } - err = esp_lcd_panel_swap_xy(s_display.panel_handle, ST7789_SWAP_XY); - if (err != ESP_OK) { - ESP_LOGE(TAG, "swap xy failed: %s", esp_err_to_name(err)); - platform_display_deinit(); - return err; - } - err = esp_lcd_panel_mirror(s_display.panel_handle, ST7789_MIRROR_X, ST7789_MIRROR_Y); - if (err != ESP_OK) { - ESP_LOGE(TAG, "mirror failed: %s", esp_err_to_name(err)); - platform_display_deinit(); - return err; - } - err = esp_lcd_panel_set_gap(s_display.panel_handle, CONFIG_TQ_SCREEN_X_GAP, CONFIG_TQ_SCREEN_Y_GAP); - if (err != ESP_OK) { - ESP_LOGE(TAG, "set gap failed: %s", esp_err_to_name(err)); - platform_display_deinit(); - return err; - } - err = esp_lcd_panel_invert_color(s_display.panel_handle, false); - if (err != ESP_OK) { - ESP_LOGE(TAG, "invert control failed: %s", esp_err_to_name(err)); - platform_display_deinit(); - return err; - } - err = esp_lcd_panel_disp_on_off(s_display.panel_handle, true); - if (err != ESP_OK) { - ESP_LOGE(TAG, "display on failed: %s", esp_err_to_name(err)); - platform_display_deinit(); - return err; - } - - if (CONFIG_TQ_SCREEN_BACKLIGHT_PIN >= 0) { - err = gpio_set_level(CONFIG_TQ_SCREEN_BACKLIGHT_PIN, ST7789_BACKLIGHT_ON_LEVEL); - if (err != ESP_OK) { - ESP_LOGE(TAG, "set backlight on failed: %s", esp_err_to_name(err)); - platform_display_deinit(); - return err; - } - } - - s_display.initialized = true; - ESP_LOGI(TAG, - "st7789 ready: spi_host=%d sclk=%d mosi=%d dc=%d cs=%d rst=%d bk=%d bk_on=%d spi_mode=%d size=%dx%d pclk=%u", - ST7789_SPI_HOST, - CONFIG_TQ_SPI_SHARED_CLK_PIN, - CONFIG_TQ_SPI_SHARED_MOSI_PIN, - CONFIG_TQ_SCREEN_DC_PIN, - CONFIG_TQ_SCREEN_CS_PIN, - CONFIG_TQ_SCREEN_RESET_PIN, - CONFIG_TQ_SCREEN_BACKLIGHT_PIN, - ST7789_BACKLIGHT_ON_LEVEL, - CONFIG_TQ_SCREEN_SPI_MODE, - CONFIG_TQ_SCREEN_H_RES, - CONFIG_TQ_SCREEN_V_RES, - (unsigned)CONFIG_TQ_SCREEN_PIXEL_CLOCK_HZ); - return ESP_OK; -#endif -} - -esp_err_t platform_display_show_test_pattern(void) { -#if !CONFIG_TQ_SCREEN_ENABLE - return ESP_ERR_NOT_SUPPORTED; -#else - ESP_RETURN_ON_ERROR(platform_display_init(), TAG, "display init failed"); - - const int h_res = CONFIG_TQ_SCREEN_H_RES; - const int v_res = CONFIG_TQ_SCREEN_V_RES; - const int draw_lines = CONFIG_TQ_SCREEN_DRAW_LINES > 0 ? CONFIG_TQ_SCREEN_DRAW_LINES : 1; - const size_t buf_pixels = (size_t)h_res * (size_t)draw_lines; - const size_t buf_size = buf_pixels * ST7789_COLOR_BYTES_PER_PIXEL; - - uint16_t *draw_buf = (uint16_t *)heap_caps_malloc(buf_size, MALLOC_CAP_DMA | MALLOC_CAP_8BIT); - if (draw_buf == NULL) { - draw_buf = (uint16_t *)heap_caps_malloc(buf_size, MALLOC_CAP_8BIT); - } - ESP_RETURN_ON_FALSE(draw_buf != NULL, ESP_ERR_NO_MEM, TAG, "alloc draw buffer failed"); - - ESP_LOGI(TAG, "draw test pattern start"); - - esp_err_t err = ESP_OK; - for (int y = 0; y < v_res && err == ESP_OK; y += draw_lines) { - int chunk_lines = draw_lines; - if (y + chunk_lines > v_res) { - chunk_lines = v_res - y; - } - - fill_test_pattern_chunk(draw_buf, y, chunk_lines, h_res, v_res); - err = esp_lcd_panel_draw_bitmap(s_display.panel_handle, 0, y, h_res, y + chunk_lines, draw_buf); - if (err != ESP_OK) { - break; - } - - // Wait for queued color transfer to finish before reusing draw_buf. - err = esp_lcd_panel_io_tx_param(s_display.io_handle, -1, NULL, 0); - } - - heap_caps_free(draw_buf); - - if (err == ESP_OK) { - ESP_LOGI(TAG, "draw test pattern done"); - } - return err; -#endif +esp_err_t platform_display_show_test_pattern(void) +{ + return lcd_module_start_demo(); } diff --git a/dependencies.lock b/dependencies.lock index 6037eb8..95fa7a2 100644 --- a/dependencies.lock +++ b/dependencies.lock @@ -19,6 +19,16 @@ dependencies: registry_url: https://components.espressif.com/ type: service version: 1.5.4 + espressif/esp_jpeg: + component_hash: defb83669293cbf86d0fa86b475ba5517aceed04ed70db435388c151ab37b5d7 + dependencies: + - name: idf + require: private + version: '>=5.0' + source: + registry_url: https://components.espressif.com/ + type: service + version: 1.3.1 espressif/esp_websocket_client: component_hash: c5a067a9fddea370c478017e66fac302f4b79c3d4027e9bdd42a019786cceb92 dependencies: @@ -56,13 +66,14 @@ dependencies: idf: source: type: idf - version: 5.5.2 + version: 5.5.1 direct_dependencies: - espressif/esp_audio_codec - espressif/esp_codec_dev +- espressif/esp_jpeg - espressif/esp_websocket_client - espressif/libpng - idf -manifest_hash: 3274b53ce89a58abbad0a03dfaff3410e35deb81305a21289606c4d2177a111b +manifest_hash: 5900d46fda5cdfbcdd8ddd277f3cf83d1ca7f79f4a98eb428297e57174c5181c target: esp32s3 version: 2.0.0 diff --git a/dependency_whitelist.md b/dependency_whitelist.md index a77af50..727d338 100644 --- a/dependency_whitelist.md +++ b/dependency_whitelist.md @@ -10,4 +10,4 @@ Entry format: - expiry: YYYY-MM-DD (must be reviewed before expiry) Current entries: -- none +- requester: `platform/platform`; target: `platform/lcd`; reason: split LCD driver/demo into dedicated component under `components/lcd`; owner: ai-agent; expiry: 2026-12-31 diff --git a/main/idf_component.yml b/main/idf_component.yml index eb66955..663bfd6 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -17,4 +17,5 @@ dependencies: espressif/esp_websocket_client: ^1.6.1 espressif/esp_codec_dev: ^1.5.4 espressif/esp_audio_codec: ^2.4.1 + espressif/esp_jpeg: ">=1.0.2" espressif/libpng: ^1.6.55