/* * LVGL bring-up for ESP32-2432S028R ("Cheap Yellow Display") * ESP-IDF 5.2+ / LVGL 9 / esp_lvgl_port 2.x * * main/idf_component.yml: * dependencies: * idf: ">=5.2" * espressif/esp_lvgl_port: "^2" * atanisoft/esp_lcd_touch_xpt2046: "^1" * * main/CMakeLists.txt: * idf_component_register(SRCS "testing.c" * PRIV_REQUIRES esp_driver_gpio esp_driver_spi esp_lcd * INCLUDE_DIRS ".") */ #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/gpio.h" #include "driver/spi_master.h" #include "esp_lcd_panel_io.h" #include "esp_lcd_panel_ops.h" #include "esp_lcd_panel_vendor.h" #include "esp_lcd_touch_xpt2046.h" #include "esp_lvgl_port.h" #include "esp_err.h" #include "esp_log.h" static const char *TAG = "cyd"; /* ---- Display: ST7789 on SPI2 (HSPI) ---- */ #define LCD_HOST SPI2_HOST #define PIN_LCD_SCLK 14 #define PIN_LCD_MOSI 13 #define PIN_LCD_MISO 12 #define PIN_LCD_CS 15 #define PIN_LCD_DC 2 #define PIN_LCD_RST -1 /* tied high on this board */ #define PIN_LCD_BL 21 /* active high */ /* ---- Touch: XPT2046 on SPI3 (VSPI) ---- */ #define TOUCH_HOST SPI3_HOST #define PIN_TP_SCLK 25 #define PIN_TP_MOSI 32 #define PIN_TP_MISO 39 /* input-only pin, fine for MISO */ #define PIN_TP_CS 33 #define PIN_TP_IRQ 36 /* input-only pin, unused: we poll */ /* Native panel geometry. The ST7789 GRAM is always 240x320; landscape comes * from MADCTL, not from changing these. */ #define PANEL_H_RES 240 #define PANEL_V_RES 320 /* 1 = landscape (320x240), 0 = portrait (240x320). This switch has to drive * the LVGL canvas size, the panel MADCTL and the touch axis mapping together, * or touch coordinates land in a different space than the pixels. */ #define LCD_LANDSCAPE 1 #if LCD_LANDSCAPE #define LCD_H_RES PANEL_V_RES /* 320 */ #define LCD_V_RES PANEL_H_RES /* 240 */ #else #define LCD_H_RES PANEL_H_RES /* 240 */ #define LCD_V_RES PANEL_V_RES /* 320 */ #endif /* Panel MADCTL transform. */ #define DISP_SWAP_XY (LCD_LANDSCAPE ? true : false) #define DISP_MIRROR_X (LCD_LANDSCAPE ? true : false) #define DISP_MIRROR_Y false /* Touch transform, tuned by eye -- the resistive film is glued on independently * of the controller's MADCTL. Mind the order: esp_lcd_touch mirrors in * panel-native space and swaps last, so with TP_SWAP_XY set the flags come out * crossed: TP_MIRROR_Y flips the reported x, TP_MIRROR_X flips reported y. * * Verified on this board: corner taps TL/TR/BR/BL read (28,25) (285,22) * (284,213) (22,210). The film stops responding ~25 px in from every edge, so * the extreme corners are unreachable -- avoid edge-hugging widgets. */ #define TP_SWAP_XY 1 #define TP_MIRROR_X 0 #define TP_MIRROR_Y 0 #define LCD_CMD_BITS 8 #define LCD_PARAM_BITS 8 #define LCD_PIXEL_CLOCK_HZ (40 * 1000 * 1000) /* XPT2046 is slow -- datasheet max is ~2.5 MHz. Going faster gives * garbage coordinates that look like random taps. */ #define TOUCH_CLOCK_HZ (2 * 1000 * 1000) /* Partial draw buffer. No PSRAM on this board, so keep it modest: * 320 * 40 * 2 bytes = 25.6 KB per buffer, doubled = 51.2 KB. */ #define LVGL_BUF_LINES 40 static esp_lcd_panel_handle_t s_panel = NULL; static esp_lcd_panel_io_handle_t s_panel_io = NULL; static esp_lcd_touch_handle_t s_touch = NULL; static lv_display_t *s_disp = NULL; static lv_indev_t *s_indev = NULL; static lv_obj_t *s_temp_label = NULL; static void backlight_init(void) { const gpio_config_t cfg = { .mode = GPIO_MODE_OUTPUT, .pin_bit_mask = 1ULL << PIN_LCD_BL, }; ESP_ERROR_CHECK(gpio_config(&cfg)); gpio_set_level(PIN_LCD_BL, 0); /* off until the panel is ready */ } static void display_init(void) { ESP_LOGI(TAG, "init LCD SPI bus"); const spi_bus_config_t bus = { .sclk_io_num = PIN_LCD_SCLK, .mosi_io_num = PIN_LCD_MOSI, .miso_io_num = PIN_LCD_MISO, .quadwp_io_num = -1, .quadhd_io_num = -1, .max_transfer_sz = LCD_H_RES * LVGL_BUF_LINES * sizeof(uint16_t), }; ESP_ERROR_CHECK(spi_bus_initialize(LCD_HOST, &bus, SPI_DMA_CH_AUTO)); const esp_lcd_panel_io_spi_config_t io_cfg = { .cs_gpio_num = PIN_LCD_CS, .dc_gpio_num = PIN_LCD_DC, .spi_mode = 0, .pclk_hz = LCD_PIXEL_CLOCK_HZ, .trans_queue_depth = 10, .lcd_cmd_bits = LCD_CMD_BITS, .lcd_param_bits = LCD_PARAM_BITS, }; ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi( (esp_lcd_spi_bus_handle_t)LCD_HOST, &io_cfg, &s_panel_io)); const esp_lcd_panel_dev_config_t panel_cfg = { .reset_gpio_num = PIN_LCD_RST, .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR, /* CYD is BGR */ .bits_per_pixel = 16, }; ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(s_panel_io, &panel_cfg, &s_panel)); ESP_ERROR_CHECK(esp_lcd_panel_reset(s_panel)); ESP_ERROR_CHECK(esp_lcd_panel_init(s_panel)); ESP_ERROR_CHECK(esp_lcd_panel_invert_color(s_panel, false)); ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(s_panel, true)); } static void touch_init(void) { ESP_LOGI(TAG, "init touch SPI bus"); const spi_bus_config_t bus = { .sclk_io_num = PIN_TP_SCLK, .mosi_io_num = PIN_TP_MOSI, .miso_io_num = PIN_TP_MISO, .quadwp_io_num = -1, .quadhd_io_num = -1, .max_transfer_sz = 0, }; ESP_ERROR_CHECK(spi_bus_initialize(TOUCH_HOST, &bus, SPI_DMA_DISABLED)); esp_lcd_panel_io_handle_t tp_io = NULL; esp_lcd_panel_io_spi_config_t tp_io_cfg = ESP_LCD_TOUCH_IO_SPI_XPT2046_CONFIG(PIN_TP_CS); tp_io_cfg.pclk_hz = TOUCH_CLOCK_HZ; ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi( (esp_lcd_spi_bus_handle_t)TOUCH_HOST, &tp_io_cfg, &tp_io)); /* x_max/y_max are the *pre-swap* extents: the driver scales the raw ADC * against them and mirrors against them, and only then does esp_lcd_touch * swap the axes. So they stay panel-native regardless of LCD_LANDSCAPE. */ const esp_lcd_touch_config_t tp_cfg = { .x_max = PANEL_H_RES, .y_max = PANEL_V_RES, .rst_gpio_num = GPIO_NUM_NC, .int_gpio_num = GPIO_NUM_NC, .flags = { .swap_xy = TP_SWAP_XY, .mirror_x = TP_MIRROR_X, .mirror_y = TP_MIRROR_Y, }, }; ESP_ERROR_CHECK(esp_lcd_touch_new_spi_xpt2046(tp_io, &tp_cfg, &s_touch)); } static void lvgl_init(void) { const lvgl_port_cfg_t port_cfg = ESP_LVGL_PORT_INIT_CONFIG(); ESP_ERROR_CHECK(lvgl_port_init(&port_cfg)); const lvgl_port_display_cfg_t disp_cfg = { .io_handle = s_panel_io, .panel_handle = s_panel, .buffer_size = LCD_H_RES * LVGL_BUF_LINES, .double_buffer = true, .hres = LCD_H_RES, .vres = LCD_V_RES, .monochrome = false, .rotation = { .swap_xy = DISP_SWAP_XY, .mirror_x = DISP_MIRROR_X, .mirror_y = DISP_MIRROR_Y, }, .flags = { .buff_dma = true, }, }; s_disp = lvgl_port_add_disp(&disp_cfg); const lvgl_port_touch_cfg_t touch_cfg = { .disp = s_disp, .handle = s_touch, }; s_indev = lvgl_port_add_touch(&touch_cfg); } static void arc_changed_cb(lv_event_t *e) { lv_obj_t *arc = lv_event_get_target(e); lv_label_set_text_fmt(s_temp_label, "%d \xC2\xB0""C", (int)lv_arc_get_value(arc)); } static void build_ui(void) { /* Any LVGL call from outside the port task must be locked. */ if (!lvgl_port_lock(0)) { ESP_LOGE(TAG, "could not take LVGL lock"); return; } lv_obj_t *scr = lv_screen_active(); lv_obj_t *label = lv_label_create(scr); lv_label_set_text(label, "Reflow Plate V0.1"); lv_obj_align(label, LV_ALIGN_TOP_MID, 0, 8); lv_obj_t *arc = lv_arc_create(scr); lv_obj_set_size(arc, 160, 160); lv_arc_set_range(arc, 0, 260); lv_arc_set_value(arc, 25); lv_obj_center(arc); lv_obj_add_event_cb(arc, arc_changed_cb, LV_EVENT_VALUE_CHANGED, NULL); s_temp_label = lv_label_create(scr); lv_label_set_text(s_temp_label, "25 \xC2\xB0""C"); lv_obj_center(s_temp_label); lvgl_port_unlock(); } void app_main(void) { backlight_init(); display_init(); touch_init(); lvgl_init(); build_ui(); gpio_set_level(PIN_LCD_BL, 1); /* backlight on, no white flash */ /* The LVGL port runs its own task and handles lv_timer_handler(). * Your control loop lives here, or better, in its own task. */ while (1) { vTaskDelay(pdMS_TO_TICKS(1000)); } }