#include #include "sdkconfig.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_err.h" #include "esp_log.h" #include "esp_timer.h" #include "esp_lcd_panel_ops.h" #include "esp_lcd_panel_rgb.h" #include "driver/i2c_master.h" #include "esp_lcd_touch_gt911.h" #include "lvgl.h" #include "common.h" #include "lcd.h" #define LCD_PIXEL_SIZE 2 // in ms #define LVGL_TICK_PERIOD 2 #define LVGL_TASK_MAX_DELAY 500 #define LVGL_TASK_MIN_DELAY 1 #define LVGL_TASK_PRIORITY 2 #define LVGL_TASK_STACK_SIZE (8 * 1024) static const char *TAG = "valconomy-lcd"; // LVGL library is not thread-safe, we will call LVGL APIs from different tasks, so use a mutex to protect it static SemaphoreHandle_t lvgl_mtx = NULL; static bool val_on_vsync(esp_lcd_panel_handle_t panel, const esp_lcd_rgb_panel_event_data_t *event_data, void *user_ctx) { return false; } static void val_lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) { esp_lcd_panel_handle_t panel_handle = lv_display_get_user_data(disp); // pass the draw buffer to the driver esp_lcd_panel_draw_bitmap(panel_handle, area->x1, area->y1, area->x2 + 1, area->y2 + 1, px_map); lv_disp_flush_ready(disp); } static void val_increase_lvgl_tick(void *arg) { /* Tell LVGL how many milliseconds has elapsed */ lv_tick_inc(LVGL_TICK_PERIOD); } static void val_lvgl_port_task(void *arg) { ESP_LOGI(TAG, "Starting LVGL task"); uint32_t task_delay_ms = LVGL_TASK_MAX_DELAY; while (1) { // Lock the mutex due to the LVGL APIs are not thread-safe if (val_lvgl_lock(-1)) { task_delay_ms = lv_timer_handler(); // Release the mutex val_lvgl_unlock(); } if (task_delay_ms > LVGL_TASK_MAX_DELAY) { task_delay_ms = LVGL_TASK_MAX_DELAY; } else if (task_delay_ms < LVGL_TASK_MIN_DELAY) { task_delay_ms = LVGL_TASK_MIN_DELAY; } vTaskDelay(pdMS_TO_TICKS(task_delay_ms)); } } static void val_lvgl_touch_read(lv_indev_t *indev, lv_indev_data_t *data) { uint16_t touchpad_x[1] = {0}; uint16_t touchpad_y[1] = {0}; uint8_t touchpad_cnt = 0; // Read touch controller data esp_lcd_touch_handle_t touch_panel = lv_indev_get_user_data(indev); esp_lcd_touch_read_data(touch_panel); // Get coordinates bool touchpad_pressed = esp_lcd_touch_get_coordinates(touch_panel, touchpad_x, touchpad_y, NULL, &touchpad_cnt, 1); if (touchpad_pressed && touchpad_cnt > 0) { data->point.x = touchpad_x[0]; data->point.y = touchpad_y[0]; data->state = LV_INDEV_STATE_PR; } else { data->state = LV_INDEV_STATE_REL; } } bool val_lvgl_lock(int timeout_ms) { // Convert timeout in milliseconds to FreeRTOS ticks // If `timeout_ms` is set to -1, the program will block until the condition is met const TickType_t timeout_ticks = (timeout_ms == -1) ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms); return xSemaphoreTakeRecursive(lvgl_mtx, timeout_ticks) == pdTRUE; } void val_lvgl_unlock(void) { xSemaphoreGiveRecursive(lvgl_mtx); } esp_lcd_touch_handle_t val_setup_touch(void) { ESP_LOGI(TAG, "Install touch panel driver"); esp_lcd_panel_io_i2c_config_t io_config = ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG(); io_config.scl_speed_hz = SCL_SPEED; esp_lcd_panel_io_handle_t io_handle = NULL; ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c(i2c_bus_handle, &io_config, &io_handle)); esp_lcd_touch_io_gt911_config_t gt911_config = { .dev_addr = io_config.dev_addr, }; esp_lcd_touch_config_t tp_cfg = { .x_max = LCD_HRES, .y_max = LCD_VRES, .rst_gpio_num = -1, .int_gpio_num = 4, .levels = { .reset = 0, .interrupt = 0, }, .flags = { .swap_xy = 0, .mirror_x = 0, .mirror_y = 0, }, .driver_data = >911_config, }; esp_lcd_touch_handle_t tp; ESP_ERROR_CHECK(esp_lcd_touch_new_i2c_gt911(io_handle, &tp_cfg, &tp)); return tp; } esp_lcd_panel_handle_t val_setup_lcd(void) { uint8_t i2c_b; i2c_b = 0x01; // Output enable ESP_ERROR_CHECK(i2c_master_transmit(exio_cfg_handle, &i2c_b, 1, -1)); i2c_b = 0x00; // Reset everything ESP_ERROR_CHECK(i2c_master_transmit(exio_o_handle, &i2c_b, 1, -1)); vTaskDelay(pdMS_TO_TICKS(10)); i2c_b = 0x0c; // Pull LCD + touch out of reset ESP_ERROR_CHECK(i2c_master_transmit(exio_o_handle, &i2c_b, 1, -1)); vTaskDelay(pdMS_TO_TICKS(100)); ESP_LOGI(TAG, "Install RGB LCD panel driver"); esp_lcd_panel_handle_t panel_handle = NULL; esp_lcd_rgb_panel_config_t panel_config = { .data_width = 16, // RGB565 .psram_trans_align = 64, .num_fbs = 2, .clk_src = LCD_CLK_SRC_DEFAULT, .disp_gpio_num = -1, // Only accessible via GPIO expander .pclk_gpio_num = 7, .vsync_gpio_num = 3, .hsync_gpio_num = 46, .de_gpio_num = 5, .data_gpio_nums = { 14, // B3 38, // B4 18, // B5 17, // B6 10, // B7 39, // G2 0, // G3 45, // G4 48, // G5 47, // G6 21, // G7 1, // R3 2, // R4 42, // R5 41, // R6 40, // R7 }, .timings = { .pclk_hz = 18 * 1000 * 1000, // 18000000 / (hs+hbp+hfp+hres) / (vs+vbp+hfp+vres) = ~42Hz .h_res = LCD_HRES, .v_res = LCD_VRES, .hsync_back_porch = 8, .hsync_front_porch = 8, .hsync_pulse_width = 4, .vsync_back_porch = 16, .vsync_front_porch = 16, .vsync_pulse_width = 4, .flags = { .pclk_active_neg = true, }, }, .flags.fb_in_psram = true, // allocate frame buffer in PSRAM }; ESP_ERROR_CHECK(esp_lcd_new_rgb_panel(&panel_config, &panel_handle)); ESP_LOGI(TAG, "Initialize RGB LCD panel"); ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle)); ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle)); ESP_LOGI(TAG, "Turn on LCD backlight"); i2c_b = 0x0e; // LCD + touch out of reset, backlight on ESP_ERROR_CHECK(i2c_master_transmit(exio_o_handle, &i2c_b, 1, -1)); return panel_handle; } lv_display_t *val_setup_lvgl(esp_lcd_panel_handle_t lcd_panel, esp_lcd_touch_handle_t touch_panel) { ESP_LOGI(TAG, "Initialize LVGL library"); lv_init(); // create a lvgl display lv_display_t *display = lv_display_create(LCD_HRES, LCD_VRES); // associate the rgb panel handle to the display lv_display_set_user_data(display, lcd_panel); // set color depth lv_display_set_color_format(display, LV_COLOR_FORMAT_RGB565); // create draw buffers void *buf1 = NULL; void *buf2 = NULL; ESP_LOGI(TAG, "Use frame buffers as LVGL draw buffers"); ESP_ERROR_CHECK(esp_lcd_rgb_panel_get_frame_buffer(lcd_panel, 2, &buf1, &buf2)); // set LVGL draw buffers and direct mode lv_display_set_buffers(display, buf1, buf2, LCD_HRES * LCD_VRES * LCD_PIXEL_SIZE, LV_DISPLAY_RENDER_MODE_DIRECT); // set the callback which can copy the rendered image to an area of the display lv_display_set_flush_cb(display, val_lvgl_flush_cb); ESP_LOGI(TAG, "Register event callbacks"); esp_lcd_rgb_panel_event_callbacks_t cbs = { .on_vsync = val_on_vsync, }; ESP_ERROR_CHECK(esp_lcd_rgb_panel_register_event_callbacks(lcd_panel, &cbs, display)); lv_indev_t *indev = lv_indev_create(); lv_indev_set_user_data(indev, touch_panel); lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER); lv_indev_set_read_cb(indev, val_lvgl_touch_read); ESP_LOGI(TAG, "Install LVGL tick timer"); // Tick interface for LVGL (using esp_timer to generate 2ms periodic event) const esp_timer_create_args_t lvgl_tick_timer_args = { .callback = &val_increase_lvgl_tick, .name = "lvgl_tick" }; esp_timer_handle_t lvgl_tick_timer = NULL; ESP_ERROR_CHECK(esp_timer_create(&lvgl_tick_timer_args, &lvgl_tick_timer)); ESP_ERROR_CHECK(esp_timer_start_periodic(lvgl_tick_timer, LVGL_TICK_PERIOD * 1000)); lvgl_mtx = xSemaphoreCreateRecursiveMutex(); assert(lvgl_mtx); ESP_LOGI(TAG, "Create LVGL task"); xTaskCreate(val_lvgl_port_task, "LVGL", LVGL_TASK_STACK_SIZE, NULL, LVGL_TASK_PRIORITY, NULL); return display; }