LVGL v8.3.11 移植说明
源码:
main/APP/lvgl_demo.c/lvgl_demo.h
1. 配置总览
| 项目 | 值 |
|---|---|
| LVGL 版本 | v8.3.11 |
| 颜色格式 | RGB565 (16-bit) |
| 缓冲策略 | 全屏双缓冲 (full_refresh=1) |
| 缓冲区位置 | PSRAM (MALLOC_CAP_SPIRAM) |
| 单缓冲大小 | 800×480×2 = 768,000 字节 |
| 双缓冲合计 | ~1.46 MB |
| Tick 源 | esp_timer 硬件定时器 (1ms) |
2. 数据流
两块缓冲区 (buf1、buf2) 给 LVGL 和 DMA 一个buf用于LVGL画, 另一个buf用于DMA同步送屏幕。
2.1 双缓冲工作原理
持续循环 ──────────────────────────────────────────────→
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ buf1 ← LVGL 画 │ │ buf1 → DMA 送 │ │ buf1 ← LVGL 画 │
│ buf2 → DMA 送 │ │ buf2 ← LVGL 画 │ │ buf2 → DMA 送 │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ 画完 → 交换 │ 画完 → 交换 │ 画完 → 交换
▼ ▼ ▼
flush_cb(): "LVGL,这块buf画好了。拿去另一个buf继续画。" ← 只做这一件事为什么需要双缓冲:
单缓冲: ████ 画 ████ 等DMA送 ████ 画 ████ 等DMA送 ████ ...(串行,画完得等)
双缓冲: ████ 画 ████ 画 ████ 画 ████ 画 ████ 画 ████ ...(并行,不等)
████ 送 ████ 送 ████ 送 ████ 送 ████ 送 ████2.2 帧生命周期
┌─────────────── 帧生命周期 (一次性, ~30 ms/frame) ───────────────────┐
│ │
│ lv_demo_task (Core 1, 优先级 1) │
│ │ │
│ ├─① lv_timer_handler() ← 每 10ms 调用 │
│ │ │ UI 逻辑 (事件/动画/样式) │
│ │ │ 渲染 → 写 buf1 或 buf2 (LVGL 内部分配) │
│ │ └─② flush_cb(drv, area, color_map) │
│ │ │ 接收 LVGL 完成的帧数据指针 │
│ │ ├─ lock xMutex → 保存 g_area / g_color │
│ │ ├─ lv_disp_flush_ready() ★ 告知 LVGL: 缓冲区已释放 │
│ │ ├─ unlock xMutex │
│ │ └─③ xSemaphoreGive() ─────────────────────────────┐ │
│ │ │ │
│ │ ④ (释放后 LVGL 可立即渲染下一帧到另一缓冲区) │ │
│ │ │ │
│ lv_flush_task (Core 1, 优先级 2) │ │
│ │ │ │
│ │ ← 阻塞在 xSemaphoreTake() ←─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │
│ ├─⑤ lock xMutex → 读 g_area / g_color │
│ ├─⑥ lcd_lv_cb(x1,y1,x2,y2, color_map, sizeof(lv_color_t)) │
│ │ │ 设窗口 + QSPI DMA 传输 → LCD GRAM │
│ │ │ 时间 = old_time → current_time (fps 统计用) │
│ │ └─ 必要时 vTaskDelay((MIN_PERIOD - dt)/1000) 速率节流 │
│ ├─⑦ unlock xMutex │
│ └─⑧ 循环回 ⑤ (等待下一帧信号量) │
│ │
└─────────────────────────────────────────────────────────────────────┘3. 线程与同步配置
c
/* ── 线程配置 ───────────────────────────────────── */
#define LV_DEMO_TASK_PRIO 1
#define LV_DEMO_STK_SIZE (5 * 1024)
#define LV_FLUSH_TASK_PRIO 2
#define LV_FLUSH_STK_SIZE (5 * 1024)
/* ── 线程句柄 ───────────────────────────────────── */
TaskHandle_t LV_DEMOTask_Handler;
TaskHandle_t LV_FLUSHTask_Handler;
/* ── 同步与共享数据 ─────────────────────────────── */
SemaphoreHandle_t xSemaphore = NULL; // flush_cb → 刷新线程 通知
SemaphoreHandle_t xMutex = NULL; // 保护 g_area / g_color
lv_area_t g_area; // 当前刷新区域
lv_color_t *g_color = NULL; // 当前像素数据指针4. 入口 lvgl_demo() — 六步初始化
c
void lvgl_demo(void)
{
lv_init(); // ① LVGL 内核
lv_port_disp_init(); // ② 显示设备 (LCD + 双缓冲)
lv_port_indev_init(); // ③ 触摸输入
if (xSemaphore == NULL) xSemaphore = xSemaphoreCreateBinary();
if (xMutex == NULL) xMutex = xSemaphoreCreateMutex();
const esp_timer_create_args_t tick_args = { // ④ 1ms Tick
.callback = &increase_lvgl_tick,
.name = "lvgl_tick"
};
esp_timer_handle_t tick_timer = NULL;
ESP_ERROR_CHECK(esp_timer_create(&tick_args, &tick_timer));
ESP_ERROR_CHECK(esp_timer_start_periodic(tick_timer, 1000));
xTaskCreatePinnedToCore(lv_demo_task, // ⑤ UI 线程
"lv_demo_task", LV_DEMO_STK_SIZE, NULL,
LV_DEMO_TASK_PRIO, &LV_DEMOTask_Handler, 1);
xTaskCreatePinnedToCore(lv_flush_task, // ⑥ 刷新线程
"lv_flush_task", LV_FLUSH_STK_SIZE, NULL,
LV_FLUSH_TASK_PRIO, &LV_FLUSHTask_Handler, 1);
}5. 显示端口 lv_port_disp_init()
c
void lv_port_disp_init(void)
{
lcd_init(); // 硬件初始化 (详见 lcd.md)
void *buf1 = heap_caps_malloc(
qspilcd_dev.width * qspilcd_dev.height * sizeof(lv_color_t),
MALLOC_CAP_SPIRAM);
if (buf1 == NULL) printf("lvgl req buffer fail\n");
void *buf2 = heap_caps_malloc(
qspilcd_dev.width * qspilcd_dev.height * sizeof(lv_color_t),
MALLOC_CAP_SPIRAM);
if (buf2 == NULL) printf("lvgl req buffer fail\n");
static lv_disp_draw_buf_t disp_buf;
lv_disp_draw_buf_init(&disp_buf, buf1, buf2,
qspilcd_dev.width * qspilcd_dev.height);
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.hor_res = qspilcd_dev.width;
disp_drv.ver_res = qspilcd_dev.height;
disp_drv.flush_cb = lvgl_disp_flush_cb;
disp_drv.draw_buf = &disp_buf;
disp_drv.user_data = qspilcd_handle;
disp_drv.full_refresh = 1; // ★ 全屏刷新
lv_disp_drv_register(&disp_drv);
}6. 刷新回调 flush_cb
c
static void lvgl_disp_flush_cb(lv_disp_drv_t *drv,
const lv_area_t *area,
lv_color_t *color_map)
{
static int64_t last_time = 0; // 帧间隔统计
int64_t current_time = esp_timer_get_time();
if (xMutex) xSemaphoreTake(xMutex, portMAX_DELAY);
g_area = *area;
g_color = color_map;
lv_disp_flush_ready(drv); // ★ 立即告知 LVGL: 缓冲区已释放
if (xMutex) xSemaphoreGive(xMutex);
if (xSemaphore) xSemaphoreGive(xSemaphore); // ★ 唤醒刷新线程
last_time = esp_timer_get_time();
}核心策略: 不等 DMA 完成就立即调用
lv_disp_flush_ready(),让 LVGL 可马上渲染下一帧到另一缓冲区,刷新线程在后台异步完成 DMA 传输。
7. 刷新线程
c
#define MIN_PERIOD 0 // 最小帧间隔(μs), 0=不限速
void lv_flush_task(void *pvParameters)
{
static uint32_t fps_cnt = 0;
int64_t current_time, old_time;
while (1) {
if (xSemaphore) xSemaphoreTake(xSemaphore, portMAX_DELAY); // 阻塞等新帧
if (xMutex) xSemaphoreTake(xMutex, portMAX_DELAY);
old_time = esp_timer_get_time();
lcd_lv_cb(g_area.x1, g_area.y1,
g_area.x2, g_area.y2,
(void*)g_color, sizeof(lv_color_t)); // ★ DMA 传输
fps_cnt++;
current_time = esp_timer_get_time();
if ((current_time - old_time) < MIN_PERIOD)
vTaskDelay((MIN_PERIOD - (current_time - old_time)) / 1000);
if (xMutex) xSemaphoreGive(xMutex);
}
}8. LVGL 主线程
c
void lv_demo_task(void *pvParameters)
{
lv_demo_widgets(); // ★ 当前 Demo, 可切换
while (1) {
lv_timer_handler(); // UI 逻辑 + 渲染
vTaskDelay(pdMS_TO_TICKS(10)); // ~100Hz
}
}可选 Demo
| 函数 | 说明 |
|---|---|
lv_demo_widgets() | 控件演示 (默认) |
lv_demo_music() | 音乐播放器界面 |
lv_demo_benchmark() | 性能跑分 |
lv_demo_stress() | 压力测试 |
lv_demo_keypad_encoder() | 编码器输入测试 |
lv_example_win_1() | Window 控件示例 |
demo1_init() | NXP GUI Guider 仪表盘 |
lv_mainstart() | SD 卡文件浏览器 |
create_buttons_demo() | 按钮演示 |
tdo_lv_display_init() | TDO 自定义界面 |
9. Tick 时基
c
static void increase_lvgl_tick(void *arg)
{
lv_tick_inc(1); // 每 1ms +1, 驱动 lv_timer
}10. 触摸输入注册
c
void lv_port_indev_init(void)
{
esp32_ctp_init(); // 初始化触摸硬件 (详见 ctp.md)
static lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = touchpad_read;
lv_indev_drv_register(&indev_drv);
}
void touchpad_read(lv_indev_drv_t *drv, lv_indev_data_t *data)
{
static int64_t last_x = 0, last_y = 0;
static int64_t last_time = 0; // FPS 统计基准时间
static uint32_t frame_count = 0;
int64_t current_time = esp_timer_get_time();
if ((current_time - last_time) >= 1000000) {
last_time = current_time; // 每秒重置
frame_count = 0;
}
if (touchpad_is_pressed()) {
touchpad_get_xy(&last_x, &last_y);
data->state = LV_INDEV_STATE_PR;
frame_count++;
} else {
data->state = LV_INDEV_STATE_REL;
}
data->point.x = (lv_coord_t)last_x;
data->point.y = (lv_coord_t)last_y;
}11. 线程配置
| 线程 | 优先级 | 堆栈 | 核心 | 触发方式 |
|---|---|---|---|---|
lv_demo_task | 1 (低) | 5 KB | Core 1 | 10ms 周期轮询 |
lv_flush_task | 2 (高) | 5 KB | Core 1 | 信号量事件驱动 |
刷新线程优先级更高,确保障 xSemaphoreGive 后立即抢占 UI 线程开始 DMA 传输。均绑 Core 1,Core 0 留给 Wi-Fi/BLE。
12. 移植检查清单
- [ ] 实现
lv_port_disp_init()→ 注册flush_cb+ 双缓冲 +full_refresh=1 - [ ] 实现
lv_port_indev_init()→ 注册read_cb - [ ] 实现
flush_cb→ 复制坐标后立即调lv_disp_flush_ready()(不等 DMA) - [ ] 实现
lv_flush_task→ 阻塞等信号量 →lcd_lv_cb()→ 释放互斥锁 → 循环 - [ ] 实现
lv_demo_task→lv_demo_xxx()+while(1) { lv_timer_handler(); vTaskDelay(10); } - [ ] 创建 1ms Tick 定时器 → 回调中调用
lv_tick_inc(1) - [ ] 初始化信号量/互斥锁 →
xSemaphoreCreateBinary/Mutex()
13. 常见问题
| 现象 | 检查项 |
|---|---|
| 屏幕无显示 | PSRAM 已启用? RST 时序? SPI 波形? 缓冲区 malloc 是否成功? |
| 花屏 | 颜色格式 RGB565? 窗口坐标范围? |
| 卡死 | 栈水位 uxTaskGetStackHighWaterMark()? PSRAM 剩余? |
| 帧率低 | MIN_PERIOD 是否限制? vTaskDelay 节流是否正确? |
| 触摸无响应 | I2C 地址? 上拉电阻? TDO_CTP_ONCE 匹配? esp32_ctp_init() 已调用? |