427 lines
16 KiB
C
427 lines
16 KiB
C
/*
|
||
* 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);
|
||
}
|