/* * 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_lcd_panel_ops.h" #include "esp_lcd_panel_rgb.h" #include "esp_lcd_touch.h" #include "esp_timer.h" #include "esp_log.h" #include "lvgl.h" #include "lvgl_port.h" static const char *TAG = "lv_port"; static SemaphoreHandle_t lvgl_mux; static TaskHandle_t lvgl_task_handle = NULL; /* ------------------------------------------------------------------------- * Software rotation helpers (used with AVOID_TEAR + ROTATION != 0) * ---------------------------------------------------------------------- */ #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_rgb_panel_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, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, int32_t w, int32_t h, int32_t rotation) { int from_index, to_index, to_index_const; switch (rotation) { case 90: to_index_const = (w - x_start - 1) * h; for (int from_y = y_start; from_y <= y_end; 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; from_x++) { *(to + to_index) = *(from + from_index); from_index++; to_index -= h; } } break; case 180: to_index_const = h * w - x_start - 1; for (int from_y = y_start; from_y <= y_end; 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; from_x++) { *(to + to_index) = *(from + from_index); from_index++; to_index--; } } break; case 270: to_index_const = (x_start + 1) * h - 1; for (int from_y = y_start; from_y <= y_end; 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; from_x++) { *(to + to_index) = *(from + from_index); from_index++; to_index += h; } } break; default: break; } } #endif /* EXAMPLE_LVGL_PORT_ROTATION_DEGREE */ /* ------------------------------------------------------------------------- * Flush callbacks – selected at compile time via Kconfig * ---------------------------------------------------------------------- */ #if LVGL_PORT_AVOID_TEAR_ENABLE #if LVGL_PORT_DIRECT_MODE #if EXAMPLE_LVGL_PORT_ROTATION_DEGREE != 0 /* Direct mode + rotation: on last flush rotate the full frame into the next * hardware frame buffer, then wait for VSYNC before telling LVGL we're done. */ static void flush_callback(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); if (lv_display_flush_is_last(disp)) { int32_t w = LVGL_PORT_H_RES; int32_t h = LVGL_PORT_V_RES; void *next_fb = get_next_frame_buffer(panel_handle); rotate_copy_pixel((uint16_t *)px_map, next_fb, 0, 0, w - 1, h - 1, w, h, EXAMPLE_LVGL_PORT_ROTATION_DEGREE); esp_lcd_panel_draw_bitmap(panel_handle, 0, 0, w, h, next_fb); ulTaskNotifyValueClear(NULL, ULONG_MAX); ulTaskNotifyTake(pdTRUE, portMAX_DELAY); /* Keep the second hardware frame buffer in sync */ void *next_fb2 = get_next_frame_buffer(panel_handle); rotate_copy_pixel((uint16_t *)px_map, next_fb2, 0, 0, w - 1, h - 1, w, h, EXAMPLE_LVGL_PORT_ROTATION_DEGREE); } lv_display_flush_ready(disp); } #else /* ROTATION == 0 */ /* Direct mode, no rotation: switch the hardware frame buffer on last flush */ static void flush_callback(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); const int offsetx1 = area->x1; const int offsetx2 = area->x2; const int offsety1 = area->y1; const int offsety2 = area->y2; if (lv_display_flush_is_last(disp)) { esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, px_map); ulTaskNotifyValueClear(NULL, ULONG_MAX); ulTaskNotifyTake(pdTRUE, portMAX_DELAY); } lv_display_flush_ready(disp); } #endif /* EXAMPLE_LVGL_PORT_ROTATION_DEGREE */ #elif LVGL_PORT_FULL_REFRESH && LVGL_PORT_LCD_RGB_BUFFER_NUMS == 2 /* Full refresh, double buffer: switch frame buffer and wait for VSYNC */ static void flush_callback(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); const int offsetx1 = area->x1; const int offsetx2 = area->x2; const int offsety1 = area->y1; const int offsety2 = area->y2; esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, px_map); ulTaskNotifyValueClear(NULL, ULONG_MAX); ulTaskNotifyTake(pdTRUE, portMAX_DELAY); lv_display_flush_ready(disp); } #elif LVGL_PORT_FULL_REFRESH && LVGL_PORT_LCD_RGB_BUFFER_NUMS == 3 /* Full refresh, triple buffer: zero-copy using all three hardware frame buffers */ #if EXAMPLE_LVGL_PORT_ROTATION_DEGREE == 0 static void *lvgl_port_rgb_last_buf = NULL; static void *lvgl_port_rgb_next_buf = NULL; static void *lvgl_port_flush_next_buf = NULL; #endif static void flush_callback(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); 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_copy_pixel((uint16_t *)px_map, next_fb, offsetx1, offsety1, offsetx2, offsety2, LVGL_PORT_H_RES, LVGL_PORT_V_RES, EXAMPLE_LVGL_PORT_ROTATION_DEGREE); esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, next_fb); #else /* Zero-copy triple buffer: rotate LVGL render buffers to follow the HW * frame buffers. px_map is one of the three HW frame buffers returned by * esp_lcd_rgb_panel_get_frame_buffer(), so draw_bitmap is zero-copy. */ lv_display_set_buffers(disp, px_map, lvgl_port_flush_next_buf, LVGL_PORT_H_RES * LVGL_PORT_V_RES * sizeof(lv_color_t), LV_DISPLAY_RENDER_MODE_FULL); lvgl_port_flush_next_buf = px_map; esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, px_map); lvgl_port_rgb_next_buf = px_map; #endif lv_display_flush_ready(disp); } #endif /* LVGL_PORT_DIRECT_MODE / LVGL_PORT_FULL_REFRESH */ #else /* !LVGL_PORT_AVOID_TEAR_ENABLE */ /* Simple partial-refresh flush: copy the dirty area to the frame buffer */ static void flush_callback(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); const int offsetx1 = area->x1; const int offsetx2 = area->x2; const int offsety1 = area->y1; const int offsety2 = area->y2; esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, px_map); lv_display_flush_ready(disp); } #endif /* LVGL_PORT_AVOID_TEAR_ENABLE */ /* ------------------------------------------------------------------------- * Display initialisation (LVGL 9) * ---------------------------------------------------------------------- */ static lv_display_t *display_init(esp_lcd_panel_handle_t panel_handle) { assert(panel_handle); void *buf1 = NULL; void *buf2 = NULL; int buffer_size = 0; lv_display_render_mode_t render_mode = LV_DISPLAY_RENDER_MODE_PARTIAL; #if EXAMPLE_LVGL_PORT_ROTATION_90 || EXAMPLE_LVGL_PORT_ROTATION_270 lv_display_t *disp = lv_display_create(LVGL_PORT_V_RES, LVGL_PORT_H_RES); #else lv_display_t *disp = lv_display_create(LVGL_PORT_H_RES, LVGL_PORT_V_RES); #endif assert(disp); lv_display_set_user_data(disp, panel_handle); ESP_LOGD(TAG, "Malloc memory for LVGL buffer"); #if LVGL_PORT_AVOID_TEAR_ENABLE buffer_size = LVGL_PORT_H_RES * LVGL_PORT_V_RES; #if (LVGL_PORT_LCD_RGB_BUFFER_NUMS == 3) && (EXAMPLE_LVGL_PORT_ROTATION_DEGREE == 0) && LVGL_PORT_FULL_REFRESH ESP_ERROR_CHECK(esp_lcd_rgb_panel_get_frame_buffer(panel_handle, 3, &lvgl_port_rgb_last_buf, &buf1, &buf2)); lvgl_port_rgb_next_buf = lvgl_port_rgb_last_buf; lvgl_port_flush_next_buf = buf2; render_mode = LV_DISPLAY_RENDER_MODE_FULL; #elif (LVGL_PORT_LCD_RGB_BUFFER_NUMS == 3) && (EXAMPLE_LVGL_PORT_ROTATION_DEGREE != 0) void *fbs[3]; ESP_ERROR_CHECK(esp_lcd_rgb_panel_get_frame_buffer(panel_handle, 3, &fbs[0], &fbs[1], &fbs[2])); buf1 = fbs[2]; buf2 = NULL; #if LVGL_PORT_FULL_REFRESH render_mode = LV_DISPLAY_RENDER_MODE_FULL; #else render_mode = LV_DISPLAY_RENDER_MODE_DIRECT; #endif #else ESP_ERROR_CHECK(esp_lcd_rgb_panel_get_frame_buffer(panel_handle, 2, &buf1, &buf2)); #if LVGL_PORT_FULL_REFRESH render_mode = LV_DISPLAY_RENDER_MODE_FULL; #elif LVGL_PORT_DIRECT_MODE render_mode = LV_DISPLAY_RENDER_MODE_DIRECT; #endif #endif #else /* !LVGL_PORT_AVOID_TEAR_ENABLE */ 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); render_mode = LV_DISPLAY_RENDER_MODE_PARTIAL; #endif /* LVGL_PORT_AVOID_TEAR_ENABLE */ lv_display_set_flush_cb(disp, flush_callback); lv_display_set_buffers(disp, buf1, buf2, buffer_size * sizeof(lv_color_t), render_mode); return disp; } /* ------------------------------------------------------------------------- * Touch input (LVGL 9) * ---------------------------------------------------------------------- */ static void touchpad_read(lv_indev_t *indev, lv_indev_data_t *data) { esp_lcd_touch_handle_t tp = lv_indev_get_user_data(indev); assert(tp); uint16_t touchpad_x; uint16_t touchpad_y; uint8_t touchpad_cnt = 0; esp_lcd_touch_read_data(tp); 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, lv_display_t *disp) { assert(tp); lv_indev_t *indev = lv_indev_create(); lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER); lv_indev_set_read_cb(indev, touchpad_read); lv_indev_set_user_data(indev, tp); lv_indev_set_display(indev, disp); return indev; } /* ------------------------------------------------------------------------- * LVGL tick via esp_timer * ---------------------------------------------------------------------- */ static void tick_increment(void *arg) { lv_tick_inc(LVGL_PORT_TICK_PERIOD_MS); } static esp_err_t tick_init(void) { 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); } /* ------------------------------------------------------------------------- * LVGL task * ---------------------------------------------------------------------- */ 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)); } } /* ------------------------------------------------------------------------- * Public API * ---------------------------------------------------------------------- */ 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_display_t *disp = display_init(lcd_handle); assert(disp); if (tp_handle) { lv_indev_t *indev = indev_init(tp_handle, disp); 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); } bool lvgl_port_notify_rgb_vsync(void) { BaseType_t need_yield = pdFALSE; #if LVGL_PORT_FULL_REFRESH && (LVGL_PORT_LCD_RGB_BUFFER_NUMS == 3) && (EXAMPLE_LVGL_PORT_ROTATION_DEGREE == 0) if (lvgl_port_rgb_next_buf != lvgl_port_rgb_last_buf) { lvgl_port_flush_next_buf = lvgl_port_rgb_last_buf; lvgl_port_rgb_last_buf = lvgl_port_rgb_next_buf; } #elif LVGL_PORT_AVOID_TEAR_ENABLE xTaskNotifyFromISR(lvgl_task_handle, ULONG_MAX, eNoAction, &need_yield); #endif return (need_yield == pdTRUE); }