这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 嵌入式开发 » 国产MCU » 【ESP32P4】2、M5StackTab5LVGL8.3移植记录

共1条 1/1 1 跳转至

【ESP32P4】2、M5StackTab5LVGL8.3移植记录

高工
2026-06-21 07:51:35     打赏

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 及以上,保证刷新流畅。

【效果展示】

8432fc5c57de7c6093b2dfdc0ffbc9be.jpg





关键词: ESP32P4     M5Stack     LVGL8.3    

共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]