/* * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: CC0-1.0 */ #include "esp_timer.h" #undef ESP_UTILS_LOG_TAG #define ESP_UTILS_LOG_TAG "LvPort" #include "esp_lib_utils.h" #include "lvgl_v8_port.h" using namespace esp_panel::drivers; #define LVGL_PORT_ENABLE_ROTATION_OPTIMIZED (1) #define LVGL_PORT_BUFFER_NUM_MAX (2) static SemaphoreHandle_t lvgl_mux = nullptr; // LVGL mutex static TaskHandle_t lvgl_task_handle = nullptr; static esp_timer_handle_t lvgl_tick_timer = NULL; static void *lvgl_buf[LVGL_PORT_BUFFER_NUM_MAX] = {}; #if LVGL_PORT_ROTATION_DEGREE != 0 static void *get_next_frame_buffer(LCD *lcd) { static void *next_fb = NULL; static void *fbs[2] = { NULL }; if (next_fb == NULL) { fbs[0] = lcd->getFrameBufferByIndex(0); fbs[1] = lcd->getFrameBufferByIndex(1); next_fb = fbs[1]; } else { next_fb = (next_fb == fbs[0]) ? fbs[1] : fbs[0]; } return next_fb; } __attribute__((always_inline)) static inline void copy_pixel_8bpp(uint8_t *to, const uint8_t *from) { *to++ = *from++; } __attribute__((always_inline)) static inline void copy_pixel_16bpp(uint8_t *to, const uint8_t *from) { *(uint16_t *)to++ = *(const uint16_t *)from++; } __attribute__((always_inline)) static inline void copy_pixel_24bpp(uint8_t *to, const uint8_t *from) { *to++ = *from++; *to++ = *from++; *to++ = *from++; } #define _COPY_PIXEL(_bpp, to, from) copy_pixel_##_bpp##bpp(to, from) #define COPY_PIXEL(_bpp, to, from) _COPY_PIXEL(_bpp, to, from) #define ROTATE_90_ALL_BPP() \ { \ to_bytes_per_line = h * to_bytes_per_piexl; \ to_index_const = (w - x_start - 1) * to_bytes_per_line; \ for (int from_y = y_start; from_y < y_end + 1; from_y++) { \ from_index = from_y * from_bytes_per_line + x_start * from_bytes_per_piexl; \ to_index = to_index_const + from_y * to_bytes_per_piexl; \ for (int from_x = x_start; from_x < x_end + 1; from_x++) { \ COPY_PIXEL(LV_COLOR_DEPTH, to + to_index, from + from_index); \ from_index += from_bytes_per_piexl; \ to_index -= to_bytes_per_line; \ } \ } \ } /** * @brief Optimized transpose function for RGB565 format. * * @note ESP32-P4 1024x600 full-screen: 738ms -> 34ms * @note ESP32-S3 480x480 full-screen: 380ms -> 37ms */ #define ROTATE_90_OPTIMIZED_16BPP(block_w, block_h) \ { \ for (int i = 0; i < h; i += block_h) { \ max_height = (i + block_h > h) ? h : (i + block_h); \ for (int j = 0; j < w; j += block_w) { \ max_width = (j + block_w > w) ? w : (j + block_w); \ start_y = w - 1 - j; \ for (int x = i; x < max_height; x++) { \ from_next = (uint16_t *)from + x * w; \ for (int y = j, mirrored_y = start_y; y < max_width; y += 4, mirrored_y -= 4) { \ ((uint16_t *)to)[(mirrored_y) * h + x] = *((uint32_t *)(from_next + y)) & 0xFFFF; \ ((uint16_t *)to)[(mirrored_y - 1) * h + x] = (*((uint32_t *)(from_next + y)) >> 16) & 0xFFFF; \ ((uint16_t *)to)[(mirrored_y - 2) * h + x] = *((uint32_t *)(from_next + y + 2)) & 0xFFFF; \ ((uint16_t *)to)[(mirrored_y - 3) * h + x] = (*((uint32_t *)(from_next + y + 2)) >> 16) & 0xFFFF; \ } \ } \ } \ } \ } #define ROTATE_180_ALL_BPP() \ { \ to_bytes_per_line = w * to_bytes_per_piexl; \ to_index_const = (h - 1) * to_bytes_per_line + (w - x_start - 1) * to_bytes_per_piexl; \ for (int from_y = y_start; from_y < y_end + 1; from_y++) { \ from_index = from_y * from_bytes_per_line + x_start * from_bytes_per_piexl; \ to_index = to_index_const - from_y * to_bytes_per_line; \ for (int from_x = x_start; from_x < x_end + 1; from_x++) { \ COPY_PIXEL(LV_COLOR_DEPTH, to + to_index, from + from_index); \ from_index += from_bytes_per_piexl; \ to_index -= to_bytes_per_piexl; \ } \ } \ } #define ROTATE_270_OPTIMIZED_16BPP(block_w, block_h) \ { \ for (int i = 0; i < h; i += block_h) { \ max_height = i + block_h > h ? h : i + block_h; \ for (int j = 0; j < w; j += block_w) { \ max_width = j + block_w > w ? w : j + block_w; \ for (int x = i; x < max_height; x++) { \ from_next = (uint16_t *)from + x * w; \ for (int y = j; y < max_width; y += 4) { \ ((uint16_t *)to)[y * h + (h - 1 - x)] = *((uint32_t *)(from_next + y)) & 0xFFFF; \ ((uint16_t *)to)[(y + 1) * h + (h - 1 - x)] = (*((uint32_t *)(from_next + y)) >> 16) & 0xFFFF; \ ((uint16_t *)to)[(y + 2) * h + (h - 1 - x)] = *((uint32_t *)(from_next + y + 2)) & 0xFFFF; \ ((uint16_t *)to)[(y + 3) * h + (h - 1 - x)] = (*((uint32_t *)(from_next + y + 2)) >> 16) & 0xFFFF; \ } \ } \ } \ } \ } #define ROTATE_270_ALL_BPP() \ { \ to_bytes_per_line = h * to_bytes_per_piexl; \ from_index_const = x_start * from_bytes_per_piexl; \ to_index_const = x_start * to_bytes_per_line + (h - 1) * to_bytes_per_piexl; \ for (int from_y = y_start; from_y < y_end + 1; from_y++) { \ from_index = from_y * from_bytes_per_line + from_index_const; \ to_index = to_index_const - from_y * to_bytes_per_piexl; \ for (int from_x = x_start; from_x < x_end + 1; from_x++) { \ COPY_PIXEL(LV_COLOR_DEPTH, to + to_index, from + from_index); \ from_index += from_bytes_per_piexl; \ to_index += to_bytes_per_line; \ } \ } \ } __attribute__((always_inline)) IRAM_ATTR static inline void rotate_copy_pixel( const uint8_t *from, uint8_t *to, uint16_t x_start, uint16_t y_start, uint16_t x_end, uint16_t y_end, uint16_t w, uint16_t h, uint16_t rotate ) { int from_bytes_per_piexl = sizeof(lv_color_t); int from_bytes_per_line = w * from_bytes_per_piexl; int from_index = 0; int to_bytes_per_piexl = LV_COLOR_DEPTH >> 3; int to_bytes_per_line; int to_index = 0; int to_index_const = 0; #if (LV_COLOR_DEPTH == 16) && LVGL_PORT_ENABLE_ROTATION_OPTIMIZED int max_height = 0; int max_width = 0; int start_y = 0; uint16_t *from_next = NULL; #endif // uint32_t time = esp_log_timestamp(); switch (rotate) { case 90: #if (LV_COLOR_DEPTH == 16) && LVGL_PORT_ENABLE_ROTATION_OPTIMIZED ROTATE_90_OPTIMIZED_16BPP(32, 256); #else ROTATE_90_ALL_BPP(); #endif break; case 180: ROTATE_180_ALL_BPP(); break; case 270: #if (LV_COLOR_DEPTH == 16) && LVGL_PORT_ENABLE_ROTATION_OPTIMIZED ROTATE_270_OPTIMIZED_16BPP(32, 256); #else int from_index_const = 0; ROTATE_270_ALL_BPP(); #endif break; default: break; } // ESP_LOGI(TAG, "rotate: end, time used:%d", (int)(esp_log_timestamp() - time)); } #endif /* LVGL_PORT_ROTATION_DEGREE */ #if LVGL_PORT_AVOID_TEAR #if LVGL_PORT_DIRECT_MODE #if LVGL_PORT_ROTATION_DEGREE != 0 typedef struct { uint16_t inv_p; uint8_t inv_area_joined[LV_INV_BUF_SIZE]; lv_area_t inv_areas[LV_INV_BUF_SIZE]; } lv_port_dirty_area_t; static lv_port_dirty_area_t dirty_area; static void flush_dirty_save(lv_port_dirty_area_t *dirty_area) { lv_disp_t *disp = _lv_refr_get_disp_refreshing(); dirty_area->inv_p = disp->inv_p; for (int i = 0; i < disp->inv_p; i++) { dirty_area->inv_area_joined[i] = disp->inv_area_joined[i]; dirty_area->inv_areas[i] = disp->inv_areas[i]; } } typedef enum { FLUSH_STATUS_PART, FLUSH_STATUS_FULL } lv_port_flush_status_t; typedef enum { FLUSH_PROBE_PART_COPY, FLUSH_PROBE_SKIP_COPY, FLUSH_PROBE_FULL_COPY, } lv_port_flush_probe_t; /** * @brief Probe dirty area to copy * * @note This function is used to avoid tearing effect, and only work with LVGL direct-mode. */ static lv_port_flush_probe_t flush_copy_probe(lv_disp_drv_t *drv) { static lv_port_flush_status_t prev_status = FLUSH_STATUS_PART; lv_port_flush_status_t cur_status; lv_port_flush_probe_t probe_result; lv_disp_t *disp_refr = _lv_refr_get_disp_refreshing(); uint32_t flush_ver = 0; uint32_t flush_hor = 0; for (int i = 0; i < disp_refr->inv_p; i++) { if (disp_refr->inv_area_joined[i] == 0) { flush_ver = (disp_refr->inv_areas[i].y2 + 1 - disp_refr->inv_areas[i].y1); flush_hor = (disp_refr->inv_areas[i].x2 + 1 - disp_refr->inv_areas[i].x1); break; } } /* Check if the current full screen refreshes */ cur_status = ((flush_ver == drv->ver_res) && (flush_hor == drv->hor_res)) ? (FLUSH_STATUS_FULL) : (FLUSH_STATUS_PART); if (prev_status == FLUSH_STATUS_FULL) { if ((cur_status == FLUSH_STATUS_PART)) { probe_result = FLUSH_PROBE_FULL_COPY; } else { probe_result = FLUSH_PROBE_SKIP_COPY; } } else { probe_result = FLUSH_PROBE_PART_COPY; } prev_status = cur_status; return probe_result; } static inline void *flush_get_next_buf(LCD *lcd) { return get_next_frame_buffer(lcd); } /** * @brief Copy dirty area * * @note This function is used to avoid tearing effect, and only work with LVGL direct-mode. */ static void flush_dirty_copy(void *dst, void *src, lv_port_dirty_area_t *dirty_area) { lv_coord_t x_start, x_end, y_start, y_end; for (int i = 0; i < dirty_area->inv_p; i++) { /* Refresh the unjoined areas*/ if (dirty_area->inv_area_joined[i] == 0) { x_start = dirty_area->inv_areas[i].x1; x_end = dirty_area->inv_areas[i].x2; y_start = dirty_area->inv_areas[i].y1; y_end = dirty_area->inv_areas[i].y2; rotate_copy_pixel( (uint8_t *)src, (uint8_t *)dst, x_start, y_start, x_end, y_end, LV_HOR_RES, LV_VER_RES, LVGL_PORT_ROTATION_DEGREE ); } } } static void flush_callback(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map) { LCD *lcd = (LCD *)drv->user_data; const int offsetx1 = area->x1; const int offsetx2 = area->x2; const int offsety1 = area->y1; const int offsety2 = area->y2; void *next_fb = NULL; lv_port_flush_probe_t probe_result = FLUSH_PROBE_PART_COPY; lv_disp_t *disp = lv_disp_get_default(); /* Action after last area refresh */ if (lv_disp_flush_is_last(drv)) { /* Check if the `full_refresh` flag has been triggered */ if (drv->full_refresh) { /* Reset flag */ drv->full_refresh = 0; // Rotate and copy data from the whole screen LVGL's buffer to the next frame buffer next_fb = flush_get_next_buf(lcd); rotate_copy_pixel( (uint8_t *)color_map, (uint8_t *)next_fb, offsetx1, offsety1, offsetx2, offsety2, LV_HOR_RES, LV_VER_RES, LVGL_PORT_ROTATION_DEGREE ); /* Switch the current LCD frame buffer to `next_fb` */ lcd->switchFrameBufferTo(next_fb); /* Waiting for the current frame buffer to complete transmission */ ulTaskNotifyValueClear(NULL, ULONG_MAX); ulTaskNotifyTake(pdTRUE, portMAX_DELAY); /* Synchronously update the dirty area for another frame buffer */ flush_dirty_copy(flush_get_next_buf(lcd), color_map, &dirty_area); flush_get_next_buf(lcd); } else { /* Probe the copy method for the current dirty area */ probe_result = flush_copy_probe(drv); if (probe_result == FLUSH_PROBE_FULL_COPY) { /* Save current dirty area for next frame buffer */ flush_dirty_save(&dirty_area); /* Set LVGL full-refresh flag and set flush ready in advance */ drv->full_refresh = 1; disp->rendering_in_progress = false; lv_disp_flush_ready(drv); /* Force to refresh whole screen, and will invoke `flush_callback` recursively */ lv_refr_now(_lv_refr_get_disp_refreshing()); } else { /* Update current dirty area for next frame buffer */ next_fb = flush_get_next_buf(lcd); flush_dirty_save(&dirty_area); flush_dirty_copy(next_fb, color_map, &dirty_area); /* Switch the current LCD frame buffer to `next_fb` */ lcd->switchFrameBufferTo(next_fb); /* Waiting for the current frame buffer to complete transmission */ ulTaskNotifyValueClear(NULL, ULONG_MAX); ulTaskNotifyTake(pdTRUE, portMAX_DELAY); if (probe_result == FLUSH_PROBE_PART_COPY) { /* Synchronously update the dirty area for another frame buffer */ flush_dirty_save(&dirty_area); flush_dirty_copy(flush_get_next_buf(lcd), color_map, &dirty_area); flush_get_next_buf(lcd); } } } } lv_disp_flush_ready(drv); } #else static void flush_callback(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map) { LCD *lcd = (LCD *)drv->user_data; /* Action after last area refresh */ if (lv_disp_flush_is_last(drv)) { /* Switch the current LCD frame buffer to `color_map` */ lcd->switchFrameBufferTo(color_map); /* Waiting for the last frame buffer to complete transmission */ ulTaskNotifyValueClear(NULL, ULONG_MAX); ulTaskNotifyTake(pdTRUE, portMAX_DELAY); } lv_disp_flush_ready(drv); } #endif /* LVGL_PORT_ROTATION_DEGREE */ #elif LVGL_PORT_FULL_REFRESH && LVGL_PORT_DISP_BUFFER_NUM == 2 static void flush_callback(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map) { LCD *lcd = (LCD *)drv->user_data; /* Switch the current LCD frame buffer to `color_map` */ lcd->switchFrameBufferTo(color_map); /* Waiting for the last frame buffer to complete transmission */ ulTaskNotifyValueClear(NULL, ULONG_MAX); ulTaskNotifyTake(pdTRUE, portMAX_DELAY); lv_disp_flush_ready(drv); } #elif LVGL_PORT_FULL_REFRESH && LVGL_PORT_DISP_BUFFER_NUM == 3 #if LVGL_PORT_ROTATION_DEGREE == 0 static void *lvgl_port_lcd_last_buf = NULL; static void *lvgl_port_lcd_next_buf = NULL; static void *lvgl_port_flush_next_buf = NULL; #endif void flush_callback(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map) { LCD *lcd = (LCD *)drv->user_data; #if LVGL_PORT_ROTATION_DEGREE != 0 const int offsetx1 = area->x1; const int offsetx2 = area->x2; const int offsety1 = area->y1; const int offsety2 = area->y2; void *next_fb = get_next_frame_buffer(lcd); /* Rotate and copy dirty area from the current LVGL's buffer to the next LCD frame buffer */ rotate_copy_pixel( (uint8_t *)color_map, (uint8_t *)next_fb, offsetx1, offsety1, offsetx2, offsety2, LV_HOR_RES, LV_VER_RES, LVGL_PORT_ROTATION_DEGREE ); /* Switch the current LCD frame buffer to `next_fb` */ lcd->switchFrameBufferTo(next_fb); #else drv->draw_buf->buf1 = color_map; drv->draw_buf->buf2 = lvgl_port_flush_next_buf; lvgl_port_flush_next_buf = color_map; /* Switch the current LCD frame buffer to `color_map` */ lcd->switchFrameBufferTo(color_map); lvgl_port_lcd_next_buf = color_map; #endif lv_disp_flush_ready(drv); } #endif IRAM_ATTR bool onLcdVsyncCallback(void *user_data) { BaseType_t need_yield = pdFALSE; #if LVGL_PORT_FULL_REFRESH && (LVGL_PORT_DISP_BUFFER_NUM == 3) && (LVGL_PORT_ROTATION_DEGREE == 0) if (lvgl_port_lcd_next_buf != lvgl_port_lcd_last_buf) { lvgl_port_flush_next_buf = lvgl_port_lcd_last_buf; lvgl_port_lcd_last_buf = lvgl_port_lcd_next_buf; } #else TaskHandle_t task_handle = (TaskHandle_t)user_data; // Notify that the current LCD frame buffer has been transmitted xTaskNotifyFromISR(task_handle, ULONG_MAX, eNoAction, &need_yield); #endif return (need_yield == pdTRUE); } #else void flush_callback(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map) { LCD *lcd = (LCD *)drv->user_data; const int offsetx1 = area->x1; const int offsetx2 = area->x2; const int offsety1 = area->y1; const int offsety2 = area->y2; lcd->drawBitmap(offsetx1, offsety1, offsetx2 - offsetx1 + 1, offsety2 - offsety1 + 1, (const uint8_t *)color_map); // For RGB LCD, directly notify LVGL that the buffer is ready if (lcd->getBus()->getBasicAttributes().type == ESP_PANEL_BUS_TYPE_RGB) { lv_disp_flush_ready(drv); } } static void update_callback(lv_disp_drv_t *drv) { LCD *lcd = (LCD *)drv->user_data; auto transformation = lcd->getTransformation(); static bool disp_init_mirror_x = transformation.mirror_x; static bool disp_init_mirror_y = transformation.mirror_y; static bool disp_init_swap_xy = transformation.swap_xy; switch (drv->rotated) { case LV_DISP_ROT_NONE: lcd->swapXY(disp_init_swap_xy); lcd->mirrorX(disp_init_mirror_x); lcd->mirrorY(disp_init_mirror_y); break; case LV_DISP_ROT_90: lcd->swapXY(!disp_init_swap_xy); lcd->mirrorX(disp_init_mirror_x); lcd->mirrorY(!disp_init_mirror_y); break; case LV_DISP_ROT_180: lcd->swapXY(disp_init_swap_xy); lcd->mirrorX(!disp_init_mirror_x); lcd->mirrorY(!disp_init_mirror_y); break; case LV_DISP_ROT_270: lcd->swapXY(!disp_init_swap_xy); lcd->mirrorX(!disp_init_mirror_x); lcd->mirrorY(disp_init_mirror_y); break; } ESP_UTILS_LOGD("Update display rotation to %d", drv->rotated); #if ESP_UTILS_CONF_LOG_LEVEL == ESP_UTILS_LOG_LEVEL_DEBUG transformation = lcd->getTransformation(); disp_init_mirror_x = transformation.mirror_x; disp_init_mirror_y = transformation.mirror_y; disp_init_swap_xy = transformation.swap_xy; ESP_UTILS_LOGD("Current mirror x: %d, mirror y: %d, swap xy: %d", disp_init_mirror_x, disp_init_mirror_y, disp_init_swap_xy); #endif } #endif /* LVGL_PORT_AVOID_TEAR */ void rounder_callback(lv_disp_drv_t *drv, lv_area_t *area) { LCD *lcd = (LCD *)drv->user_data; uint8_t x_align = lcd->getBasicAttributes().basic_bus_spec.x_coord_align; uint8_t y_align = lcd->getBasicAttributes().basic_bus_spec.y_coord_align; if (x_align > 1) { // round the start of coordinate down to the nearest aligned value area->x1 &= ~(x_align - 1); // round the end of coordinate up to the nearest aligned value area->x2 = (area->x2 & ~(x_align - 1)) + x_align - 1; } if (y_align > 1) { // round the start of coordinate down to the nearest aligned value area->y1 &= ~(y_align - 1); // round the end of coordinate up to the nearest aligned value area->y2 = (area->y2 & ~(y_align - 1)) + y_align - 1; } } static lv_disp_t *display_init(LCD *lcd) { ESP_UTILS_CHECK_FALSE_RETURN(lcd != nullptr, nullptr, "Invalid LCD device"); ESP_UTILS_CHECK_FALSE_RETURN(lcd->getRefreshPanelHandle() != nullptr, nullptr, "LCD device is not initialized"); static lv_disp_draw_buf_t disp_buf; static lv_disp_drv_t disp_drv; // Alloc draw buffers used by LVGL auto lcd_width = lcd->getFrameWidth(); auto lcd_height = lcd->getFrameHeight(); int buffer_size = 0; ESP_UTILS_LOGD("Malloc memory for LVGL buffer"); #if !LVGL_PORT_AVOID_TEAR // Avoid tearing function is disabled buffer_size = lcd_width * LVGL_PORT_BUFFER_SIZE_HEIGHT; for (int i = 0; (i < LVGL_PORT_BUFFER_NUM) && (i < LVGL_PORT_BUFFER_NUM_MAX); i++) { lvgl_buf[i] = heap_caps_malloc(buffer_size * sizeof(lv_color_t), LVGL_PORT_BUFFER_MALLOC_CAPS); assert(lvgl_buf[i]); ESP_UTILS_LOGD("Buffer[%d] address: %p, size: %d", i, lvgl_buf[i], buffer_size * sizeof(lv_color_t)); } #else // To avoid the tearing effect, we should use at least two frame buffers: one for LVGL rendering and another for LCD refresh buffer_size = lcd_width * lcd_height; #if (LVGL_PORT_DISP_BUFFER_NUM >= 3) && (LVGL_PORT_ROTATION_DEGREE == 0) && LVGL_PORT_FULL_REFRESH // With the usage of three buffers and full-refresh, we always have one buffer available for rendering, // eliminating the need to wait for the LCD's sync signal lvgl_port_lcd_last_buf = lcd->getFrameBufferByIndex(0); lvgl_buf[0] = lcd->getFrameBufferByIndex(1); lvgl_buf[1] = lcd->getFrameBufferByIndex(2); lvgl_port_lcd_next_buf = lvgl_port_lcd_last_buf; lvgl_port_flush_next_buf = lvgl_buf[1]; #elif (LVGL_PORT_DISP_BUFFER_NUM >= 3) && (LVGL_PORT_ROTATION_DEGREE != 0) lvgl_buf[0] = lcd->getFrameBufferByIndex(2); #elif LVGL_PORT_DISP_BUFFER_NUM >= 2 for (int i = 0; (i < LVGL_PORT_DISP_BUFFER_NUM) && (i < LVGL_PORT_BUFFER_NUM_MAX); i++) { lvgl_buf[i] = lcd->getFrameBufferByIndex(i); } #endif #endif /* LVGL_PORT_AVOID_TEAR */ // initialize LVGL draw buffers lv_disp_draw_buf_init(&disp_buf, lvgl_buf[0], lvgl_buf[1], buffer_size); ESP_UTILS_LOGD("Register display driver to LVGL"); lv_disp_drv_init(&disp_drv); disp_drv.flush_cb = flush_callback; #if (LVGL_PORT_ROTATION_DEGREE == 90) || (LVGL_PORT_ROTATION_DEGREE == 270) disp_drv.hor_res = lcd_height; disp_drv.ver_res = lcd_width; #else disp_drv.hor_res = lcd_width; disp_drv.ver_res = lcd_height; #endif #if LVGL_PORT_AVOID_TEAR // Only available when the tearing effect is enabled #if LVGL_PORT_FULL_REFRESH disp_drv.full_refresh = 1; #elif LVGL_PORT_DIRECT_MODE disp_drv.direct_mode = 1; #endif #else // Only available when the tearing effect is disabled if (lcd->getBasicAttributes().basic_bus_spec.isFunctionValid(LCD::BasicBusSpecification::FUNC_SWAP_XY) && lcd->getBasicAttributes().basic_bus_spec.isFunctionValid(LCD::BasicBusSpecification::FUNC_MIRROR_X) && lcd->getBasicAttributes().basic_bus_spec.isFunctionValid(LCD::BasicBusSpecification::FUNC_MIRROR_Y)) { disp_drv.drv_update_cb = update_callback; } else { disp_drv.sw_rotate = 1; } #endif /* LVGL_PORT_AVOID_TEAR */ disp_drv.draw_buf = &disp_buf; disp_drv.user_data = (void *)lcd; // Only available when the coordinate alignment is enabled if ((lcd->getBasicAttributes().basic_bus_spec.x_coord_align > 1) || (lcd->getBasicAttributes().basic_bus_spec.y_coord_align > 1)) { disp_drv.rounder_cb = rounder_callback; } return lv_disp_drv_register(&disp_drv); } static void touchpad_read(lv_indev_drv_t *indev_drv, lv_indev_data_t *data) { Touch *tp = (Touch *)indev_drv->user_data; TouchPoint point; /* Read data from touch controller */ int read_touch_result = tp->readPoints(&point, 1, 0); if (read_touch_result > 0) { data->point.x = point.x; data->point.y = point.y; data->state = LV_INDEV_STATE_PRESSED; } else { data->state = LV_INDEV_STATE_RELEASED; } } static lv_indev_t *indev_init(Touch *tp) { ESP_UTILS_CHECK_FALSE_RETURN(tp != nullptr, nullptr, "Invalid touch device"); ESP_UTILS_CHECK_FALSE_RETURN(tp->getPanelHandle() != nullptr, nullptr, "Touch device is not initialized"); static lv_indev_drv_t indev_drv_tp; ESP_UTILS_LOGD("Register input driver to LVGL"); lv_indev_drv_init(&indev_drv_tp); indev_drv_tp.type = LV_INDEV_TYPE_POINTER; indev_drv_tp.read_cb = touchpad_read; indev_drv_tp.user_data = (void *)tp; return lv_indev_drv_register(&indev_drv_tp); } #if !LV_TICK_CUSTOM static void tick_increment(void *arg) { /* Tell LVGL how many milliseconds have elapsed */ lv_tick_inc(LVGL_PORT_TICK_PERIOD_MS); } static bool tick_init(void) { // Tick interface for LVGL (using esp_timer to generate 2ms periodic event) const esp_timer_create_args_t lvgl_tick_timer_args = { .callback = &tick_increment, .name = "LVGL tick" }; ESP_UTILS_CHECK_ERROR_RETURN( esp_timer_create(&lvgl_tick_timer_args, &lvgl_tick_timer), false, "Create LVGL tick timer failed" ); ESP_UTILS_CHECK_ERROR_RETURN( esp_timer_start_periodic(lvgl_tick_timer, LVGL_PORT_TICK_PERIOD_MS * 1000), false, "Start LVGL tick timer failed" ); return true; } static bool tick_deinit(void) { ESP_UTILS_CHECK_ERROR_RETURN( esp_timer_stop(lvgl_tick_timer), false, "Stop LVGL tick timer failed" ); ESP_UTILS_CHECK_ERROR_RETURN( esp_timer_delete(lvgl_tick_timer), false, "Delete LVGL tick timer failed" ); return true; } #endif static void lvgl_port_task(void *arg) { ESP_UTILS_LOGD("Starting LVGL task"); uint32_t task_delay_ms = LVGL_PORT_TASK_MAX_DELAY_MS; while (1) { if (lvgl_port_lock(-1)) { task_delay_ms = lv_timer_handler(); lvgl_port_unlock(); } if (task_delay_ms > LVGL_PORT_TASK_MAX_DELAY_MS) { task_delay_ms = LVGL_PORT_TASK_MAX_DELAY_MS; } else if (task_delay_ms < LVGL_PORT_TASK_MIN_DELAY_MS) { task_delay_ms = LVGL_PORT_TASK_MIN_DELAY_MS; } vTaskDelay(pdMS_TO_TICKS(task_delay_ms)); } } IRAM_ATTR bool onDrawBitmapFinishCallback(void *user_data) { lv_disp_drv_t *drv = (lv_disp_drv_t *)user_data; lv_disp_flush_ready(drv); return false; } bool lvgl_port_init(LCD *lcd, Touch *tp) { ESP_UTILS_CHECK_FALSE_RETURN(lcd != nullptr, false, "Invalid LCD device"); auto bus_type = lcd->getBus()->getBasicAttributes().type; #if LVGL_PORT_AVOID_TEAR ESP_UTILS_CHECK_FALSE_RETURN( (bus_type == ESP_PANEL_BUS_TYPE_RGB) || (bus_type == ESP_PANEL_BUS_TYPE_MIPI_DSI), false, "Avoid tearing function only works with RGB/MIPI-DSI LCD now" ); ESP_UTILS_LOGI( "Avoid tearing is enabled, mode: %d, rotation: %d", LVGL_PORT_AVOID_TEARING_MODE, LVGL_PORT_ROTATION_DEGREE ); #endif lv_disp_t *disp = nullptr; lv_indev_t *indev = nullptr; lv_init(); #if !LV_TICK_CUSTOM ESP_UTILS_CHECK_FALSE_RETURN(tick_init(), false, "Initialize LVGL tick failed"); #endif ESP_UTILS_LOGI("Initializing LVGL display driver"); disp = display_init(lcd); ESP_UTILS_CHECK_NULL_RETURN(disp, false, "Initialize LVGL display driver failed"); // Record the initial rotation of the display lv_disp_set_rotation(disp, LV_DISP_ROT_NONE); // For non-RGB LCD, need to notify LVGL that the buffer is ready when the refresh is finished if (bus_type != ESP_PANEL_BUS_TYPE_RGB) { ESP_UTILS_LOGD("Attach refresh finish callback to LCD"); lcd->attachDrawBitmapFinishCallback(onDrawBitmapFinishCallback, (void *)disp->driver); } if (tp != nullptr) { ESP_UTILS_LOGD("Initialize LVGL input driver"); indev = indev_init(tp); ESP_UTILS_CHECK_NULL_RETURN(indev, false, "Initialize LVGL input driver failed"); #if LVGL_PORT_ROTATION_DEGREE != 0 auto &transformation = tp->getTransformation(); #if LVGL_PORT_ROTATION_DEGREE == 90 tp->swapXY(!transformation.swap_xy); tp->mirrorY(!transformation.mirror_y); #elif LVGL_PORT_ROTATION_DEGREE == 180 tp->mirrorX(!transformation.mirror_x); tp->mirrorY(!transformation.mirror_y); #elif LVGL_PORT_ROTATION_DEGREE == 270 tp->swapXY(!transformation.swap_xy); tp->mirrorX(!transformation.mirror_x); #endif #endif } ESP_UTILS_LOGD("Create mutex for LVGL"); lvgl_mux = xSemaphoreCreateRecursiveMutex(); ESP_UTILS_CHECK_NULL_RETURN(lvgl_mux, false, "Create LVGL mutex failed"); ESP_UTILS_LOGD("Create LVGL task"); BaseType_t core_id = (LVGL_PORT_TASK_CORE < 0) ? tskNO_AFFINITY : LVGL_PORT_TASK_CORE; BaseType_t ret = xTaskCreatePinnedToCore(lvgl_port_task, "lvgl", LVGL_PORT_TASK_STACK_SIZE, NULL, LVGL_PORT_TASK_PRIORITY, &lvgl_task_handle, core_id); ESP_UTILS_CHECK_FALSE_RETURN(ret == pdPASS, false, "Create LVGL task failed"); #if LVGL_PORT_AVOID_TEAR lcd->attachRefreshFinishCallback(onLcdVsyncCallback, (void *)lvgl_task_handle); #endif return true; } bool lvgl_port_lock(int timeout_ms) { ESP_UTILS_CHECK_NULL_RETURN(lvgl_mux, false, "LVGL mutex is not initialized"); const TickType_t timeout_ticks = (timeout_ms < 0) ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms); return (xSemaphoreTakeRecursive(lvgl_mux, timeout_ticks) == pdTRUE); } bool lvgl_port_unlock(void) { ESP_UTILS_CHECK_NULL_RETURN(lvgl_mux, false, "LVGL mutex is not initialized"); xSemaphoreGiveRecursive(lvgl_mux); return true; } bool lvgl_port_deinit(void) { #if !LV_TICK_CUSTOM ESP_UTILS_CHECK_FALSE_RETURN(tick_deinit(), false, "Deinitialize LVGL tick failed"); #endif ESP_UTILS_CHECK_FALSE_RETURN(lvgl_port_lock(-1), false, "Lock LVGL failed"); if (lvgl_task_handle != nullptr) { vTaskDelete(lvgl_task_handle); lvgl_task_handle = nullptr; } ESP_UTILS_CHECK_FALSE_RETURN(lvgl_port_unlock(), false, "Unlock LVGL failed"); #if LV_ENABLE_GC || !LV_MEM_CUSTOM lv_deinit(); #else ESP_UTILS_LOGW("LVGL memory is custom, `lv_deinit()` will not work"); #endif #if !LVGL_PORT_AVOID_TEAR for (int i = 0; i < LVGL_PORT_BUFFER_NUM; i++) { if (lvgl_buf[i] != nullptr) { free(lvgl_buf[i]); lvgl_buf[i] = nullptr; } } #endif if (lvgl_mux != nullptr) { vSemaphoreDelete(lvgl_mux); lvgl_mux = nullptr; } return true; }