M5Stack Tab5 (ESP32-P4 + ST7121 MIPI) LVGL8.3 完整移植记录
【前言】
昨天完成了M5Stack Tab5的LVGL移植,实步实现的lvgl的屏幕适配等特此记录如下,主要是方便以后查阅,也希望帮助到有缘人。
【概述】
适配硬件:M5Stack Tab5(ESP32-P4 RISC-V + ST7121 MIPI 竖屏 720×1280)
适配环境:ESP-IDF 5.x
LVGL 版本:8.3.11(规避 LVGL9 RISC-V 工具链编译报错)
实现功能:MIPI 全屏渲染、多点触摸交互、多任务互斥安全、无图形扭曲、文字清晰、点击计数 UI Demo
一、项目前置:添加 LVGL 组件依赖
进入项目根目录终端执行以下命令,自动写入idf_component.yml并在线拉取组件
# 锁定LVGL 8.3稳定分支,禁止自动升级V9 idf.py add-dependency "lvgl/lvgl^8.3.11" # 乐鑫官方LVGL适配工具层(内存、时基工具,与M5GFX自定义刷新不冲突) idf.py add-dependency "espressif/esp_lvgl_port"
依赖说明
lvgl/lvgl^8.3.11:LVGL 图形库本体,版本锁死避免 API 大范围变更;
espressif/esp_lvgl_port:官方配套工具,提供 PSRAM、系统 tick 封装,可选保留。
缓存清理
添加依赖后必须执行完整清理,防止旧缓存冲突
idf.py fullclean
二、lv_conf.h 配置
在esp-idf项目中在根目录的sdkconfig配置文件中进行配置添加如下配置。注意没有lv_config.h而是通过lv_conf_kconfig.h(sdkconfig 翻译层)给配置给lvgl
#define LV_MEM_SIZE (64U * 1024U) // 增大内存池适配大屏 #define LV_USE_LOG 1 #define LV_LOG_LEVEL LV_LOG_INFO #define LV_TICK_CUSTOM 0 // 手动lv_tick_inc喂时基 #define LV_FONT_MONTSERRAT_28 1 #define LV_FONT_MONTSERRAT_36 1 #define LV_USE_PERF_MONITOR 0 #define LV_USE_MEM_MONITOR 0
三、CMakeLists.txt 配置(main 目录)
idf_component_register( SRCS "hello_world_main.cpp" INCLUDE_DIRS "." PRIV_REQUIRES m5unified lvgl esp_lvgl_port spi_flash driver )
四、完整可运行源码 main/hello_world_main.cpp
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "lvgl.h"
// 固定Tab5硬件板型宏
#define M5BOARD_M5TAB5
#include "M5Unified.h"
// 全局硬件与同步信号
static M5GFX* lcd;
static SemaphoreHandle_t lvgl_sem = NULL;
// ST7121原生竖屏分辨率 宽720 高1280(严禁颠倒)
#define LCD_W 720
#define LCD_H 1280
// UI全局色彩定义
#define COL_BG lv_color_hex(0x0A0E14)
#define COL_CARD lv_color_hex(0x141C2A)
#define COL_BORDER lv_color_hex(0x2A3A4F)
#define COL_CYAN lv_color_hex(0x00D4FF)
#define COL_GREEN lv_color_hex(0x00FF88)
#define COL_AMBER lv_color_hex(0xFFB800)
#define COL_WHITE lv_color_hex(0xFFFFFF)
#define COL_GRAY lv_color_hex(0x8B9AAB)
#define COL_DIM lv_color_hex(0x4A5868)
// 业务全局变量
static uint32_t tap_count = 0;
static lv_obj_t* lbl_count;
// 更新计数文本
static void refresh_label(void)
{
lv_label_set_text_fmt(lbl_count, "%lu", (unsigned long)tap_count);
}
// LVGL屏幕刷新回调【核心底层对接M5GFX】
static void disp_flush(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p)
{
uint32_t w = area->x2 - area->x1 + 1;
uint32_t h = area->y2 - area->y1 + 1;
lcd->pushImage(area->x1, area->y1, w, h, (uint16_t*)color_p);
// 必须调用,告知LVGL缓冲区可用
lv_disp_flush_ready(disp_drv);
}
// LVGL渲染独立任务
static void lvgl_task(void* arg)
{
// ST7121官方驱动要求上电800ms初始化等待,避免乱码黑屏
vTaskDelay(pdMS_TO_TICKS(800));
lv_init();
// PSRAM分配分片绘图缓冲,禁止栈大数组
static lv_disp_draw_buf_t draw_buf;
const uint32_t buf_size = LCD_W * 180;
lv_color_t* buf = (lv_color_t*)heap_caps_malloc(buf_size * sizeof(lv_color_t), MALLOC_CAP_SPIRAM);
lv_disp_draw_buf_init(&draw_buf, buf, nullptr, buf_size);
// 注册显示驱动
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.hor_res = LCD_W;
disp_drv.ver_res = LCD_H;
disp_drv.flush_cb = disp_flush;
disp_drv.draw_buf = &draw_buf;
lv_disp_drv_register(&disp_drv);
// 注册触摸输入驱动
static lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = [](lv_indev_drv_t *drv, lv_indev_data_t *data)
{
M5.update();
uint8_t n = M5.Touch.getCount();
if (n > 0) {
const auto& tp = M5.Touch.getTouchPointRaw(0);
int x = tp.x; if (x < 0) x = 0; if (x >= LCD_W) x = LCD_W - 1;
int y = tp.y; if (y < 0) y = 0; if (y >= LCD_H) y = LCD_H - 1;
data->state = LV_INDEV_STATE_PRESSED;
data->point.x = x;
data->point.y = y;
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
};
lv_indev_drv_register(&indev_drv);
// ========= UI界面初始化 =========
lv_obj_t* screen = lv_scr_act();
lv_obj_set_style_bg_color(screen, COL_BG, LV_PART_MAIN);
// 标题
lv_obj_t* lbl_title = lv_label_create(screen);
lv_label_set_text(lbl_title, "Touch Counter");
lv_obj_set_style_text_font(lbl_title, &lv_font_montserrat_28, LV_PART_MAIN);
lv_obj_set_style_text_color(lbl_title, COL_CYAN, LV_PART_MAIN);
lv_obj_align(lbl_title, LV_ALIGN_TOP_MID, 0, 120);
// 分割线
lv_obj_t* sep = lv_obj_create(screen);
lv_obj_set_size(sep, 200, 2);
lv_obj_set_style_bg_color(sep, COL_CYAN, LV_PART_MAIN);
lv_obj_set_style_border_width(sep, 0, LV_PART_MAIN);
lv_obj_align(sep, LV_ALIGN_TOP_MID, 0, 175);
lv_obj_clear_flag(sep, LV_OBJ_FLAG_CLICKABLE);
// 计数数字
lbl_count = lv_label_create(screen);
lv_label_set_text(lbl_count, "0");
lv_obj_set_style_text_font(lbl_count, &lv_font_montserrat_36, LV_PART_MAIN);
lv_obj_set_style_text_color(lbl_count, COL_WHITE, LV_PART_MAIN);
lv_obj_align(lbl_count, LV_ALIGN_CENTER, 0, -30);
// 单位文字
lv_obj_t* lbl_unit = lv_label_create(screen);
lv_label_set_text(lbl_unit, "taps");
lv_obj_set_style_text_font(lbl_unit, &lv_font_montserrat_28, LV_PART_MAIN);
lv_obj_set_style_text_color(lbl_unit, COL_GRAY, LV_PART_MAIN);
lv_obj_align(lbl_unit, LV_ALIGN_CENTER, 0, 30);
// 底部按钮布局计算
const int btn_y = 1080;
const int btn_h = 110;
const int btn_w = 216;
const int btn_gap = 12;
const int btn_x0 = 24;
// 按钮快速创建lambda
auto make_btn = [&](int x, lv_color_t bg, const char* txt, lv_color_t txt_col, bool bordered) {
lv_obj_t* b = lv_btn_create(screen);
lv_obj_set_size(b, btn_w, btn_h);
lv_obj_set_pos(b, x, btn_y);
lv_obj_set_style_bg_color(b, bg, LV_PART_MAIN);
lv_obj_set_style_radius(b, 8, LV_PART_MAIN);
if (bordered) {
lv_obj_set_style_border_color(b, COL_BORDER, LV_PART_MAIN);
lv_obj_set_style_border_width(b, 1, LV_PART_MAIN);
} else {
lv_obj_set_style_border_width(b, 0, LV_PART_MAIN);
}
lv_obj_t* l = lv_label_create(b);
lv_label_set_text(l, txt);
lv_obj_set_style_text_font(l, &lv_font_montserrat_28, LV_PART_MAIN);
lv_obj_set_style_text_color(l, txt_col, LV_PART_MAIN);
lv_obj_center(l);
return b;
};
// 三个功能按钮
lv_obj_t* btn_inc = make_btn(btn_x0, COL_GREEN, "+1", COL_BG, false);
lv_obj_t* btn_dec = make_btn(btn_x0 + btn_w + btn_gap, COL_AMBER, "-1", COL_BG, false);
lv_obj_t* btn_rst = make_btn(btn_x0 + 2*(btn_w + btn_gap), COL_CARD, "Reset", COL_WHITE, true);
// 按钮点击事件绑定
lv_obj_add_event_cb(btn_inc, [](lv_event_t* e) {
if (lv_event_get_code(e) == LV_EVENT_CLICKED) { tap_count++; refresh_label(); }
}, LV_EVENT_CLICKED, NULL);
lv_obj_add_event_cb(btn_dec, [](lv_event_t* e) {
if (lv_event_get_code(e) == LV_EVENT_CLICKED) {
if (tap_count > 0) tap_count--;
refresh_label();
}
}, LV_EVENT_CLICKED, NULL);
lv_obj_add_event_cb(btn_rst, [](lv_event_t* e) {
if (lv_event_get_code(e) == LV_EVENT_CLICKED) { tap_count = 0; refresh_label(); }
}, LV_EVENT_CLICKED, NULL);
// 渲染主循环
while (1)
{
lv_tick_inc(10); // 手动递增LVGL时基,点击/动画生效必备
xSemaphoreTake(lvgl_sem, portMAX_DELAY);
lv_timer_handler(); // LVGL统一事件、渲染调度入口
xSemaphoreGive(lvgl_sem);
vTaskDelay(pdMS_TO_TICKS(10));
}
}
// 程序入口
extern "C" void app_main(void)
{
M5.begin();
lcd = &M5.Display;
lcd->setBrightness(255);
// 关键:仅硬件单层旋转,禁止LVGL叠加旋转,杜绝平行四边形扭曲
lcd->setRotation(0);
lcd->fillScreen(TFT_BLACK);
// 创建LVGL互斥信号量,多任务安全保护
lvgl_sem = xSemaphoreCreateMutex();
// 任务栈12288,大屏渲染栈需求高
xTaskCreate(lvgl_task, "lvgl_render", 12288, NULL, 2, NULL);
// 主线程持续刷新触摸硬件
while (1)
{
M5.update();
vTaskDelay(pdMS_TO_TICKS(10));
}
}五、移植踩坑避坑指南
虽然看起来工作量不多,但是期间的道路是非常多的,主要有以下一些点,特记录如下:
1. 图形扭曲(按钮变平行四边形)
根因:M5GFX 硬件旋转 + LVGL 软件旋转双层叠加
解决:全程lcd->setRotation(0),LVGL 层不设置任何旋转参数
2. 触摸点击无响应
循环缺少lv_tick_inc();
触摸回调未执行M5.update();
未加互斥锁,多任务抢占 LVGL 资源卡死。
3. 顶部花屏、文字模糊、撕裂
缓冲过小:建议LCD_W * 180;
使用writePixel单点输出,替换为pushImageDMA 批量传输;
分辨率写反 720/1280。
4. 编译 LVGL9 报unrecognized opcode typedef
ESP32-P4 RISC-V 工具链与 LVGL9 存在底层头文件冲突,固定使用8.3.11版本。
5. 黑屏 / 上电乱码
LVGL 任务开头必须延时pdMS_TO_TICKS(800)等待 ST7121 初始化完成。
6. 程序 HardFault 死机
绘图缓冲定义在局部栈,必须heap_caps_malloc分配 PSRAM;
LVGL 任务栈过小,最低 12288;
3 多任务同时调用 lv_xxx,无互斥信号量保护。
3.1. 图形扭曲(按钮变平行四边形)
根因:M5GFX 硬件旋转 + LVGL 软件旋转双层叠加
解决:全程lcd->setRotation(0),LVGL 层不设置任何旋转参数
3.2. 触摸点击无响应
循环缺少lv_tick_inc();
触摸回调未执行M5.update();
未加互斥锁,多任务抢占 LVGL 资源卡死。
3.3. 顶部花屏、文字模糊、撕裂
缓冲过小:建议LCD_W * 180;
使用writePixel单点输出,替换为pushImageDMA 批量传输;
分辨率写反 720/1280。
4. 编译 LVGL9 报unrecognized opcode typedef
ESP32-P4 RISC-V 工具链与 LVGL9 存在底层头文件冲突,固定使用8.3.11版本。
5. 黑屏 / 上电乱码
LVGL 任务开头必须延时pdMS_TO_TICKS(800)等待 ST7121 初始化完成。
6. 程序 HardFault 死机
6.1 绘图缓冲定义在局部栈,必须heap_caps_malloc分配 PSRAM;
6.2 LVGL 任务栈过小,最低 12288;
6.3 多任务同时调用 lv_xxx,无互斥信号量保护。
六、硬件适配补充说明
1 屏幕驱动:ST7121 MIPI-DSI,像素序 BGR,色彩颠倒无需软件循环交换,M5Unified 底层已适配;
2 分辨率:物理竖屏固定 宽720 高1280,禁止写 1280×720;
3 PSRAM:Tab5 默认开启,缓冲必须使用MALLOC_CAP_SPIRAM分配;
4 任务优先级:LVGL 渲染任务优先级建议 2 及以上,保证刷新流畅。
【效果展示】

我要赚赏金
