4、移植到STM32:裸机移植步骤详解

好,咱们进入正题。

LVGL 移植到 STM32 裸机,说白了就是三件事:告诉 LVGL 时间怎么走、屏幕怎么刷、触摸怎么读。听起来简单,但我在项目里见过太多人卡在某个细节上,一卡就是两三天。今天我把每一步拆开揉碎了讲,你跟着走一遍,基本不会出问题。

核心思路:LVGL 本身不依赖操作系统,裸机移植只需要实现三个接口——显示驱动、输入设备驱动、系统时钟。其他都是配置工作。

4.1 移植前的准备工作

动手之前,先把家底清点清楚。我习惯列个清单,免得做到一半发现缺东西。

项目 要求 备注
MCU STM32F4/F7/H7 系列 F1 系列也能跑,但帧率会低
LCD 驱动 ILI9341/ST7789 等 需要底层写点/画矩形函数
触摸芯片 FT6206/GT911 等 I2C 接口居多
IDE Keil / IAR / STM32CubeIDE 我推荐 CubeIDE,免费且好用
LVGL 源码 v8.3.x 或 v9.x 建议用最新稳定版

嗯,这里要注意:LVGL 的源码不要直接全部扔进工程。我见过有人把整个 lvgl 文件夹拖进去,编译报错两百条。正确做法是只保留 src 目录和 lv_conf.h,其他 demo 和文档一概不要。

4.2 配置 lv_conf.h:核心参数调优

lv_conf.h 是 LVGL 的命门。你想想看,一个嵌入式 GUI 库,内存、颜色、字体全在这里定死。配错了,要么跑不起来,要么跑起来卡成幻灯片。

我一般从官方模板复制一份,然后逐项改。下面这几个参数是必调的:

/* 1. 颜色深度 —— 跟屏幕硬件匹配 */
#define LV_COLOR_DEPTH     16

/* 2. 内存大小 —— 给 LVGL 分配多少 RAM */
#define LV_MEM_SIZE        (32U * 1024U)   /* 32KB,看芯片 RAM 大小调整 */

/* 3. 显示缓冲区 —— 至少 1/10 屏幕大小 */
#define LV_DISP_DEF_REFR_PERIOD  30   /* 刷新率,单位 ms */

/* 4. 开启对触摸输入的支持 */
#define LV_INDEV_DEF_READ_PERIOD 20   /* 输入设备轮询间隔 */

/* 5. 字体 —— 只开需要的,省 ROM */
#define LV_FONT_MONTSERRAT_12    1
#define LV_FONT_MONTSERRAT_16    1
/* 其他字体全关掉 */

我的经验:LV_MEM_SIZE 不要贪大。给多了,其他任务没内存用;给少了,LVGL 频繁申请释放导致碎片。我一般先给 32KB,跑起来后用 lv_mem_monitor() 看实际用量,再微调。

还有一个坑:LV_COLOR_DEPTH 如果设成 32,但你的屏幕是 16 位色,LVGL 内部会做颜色转换,白白浪费 CPU。说白了,硬件是什么颜色格式,这里就设什么。

4.3 实现 lv_port_disp:显示驱动适配

显示驱动是移植里最核心的部分。LVGL 需要知道两件事:怎么把像素数据刷到屏幕上,以及刷屏的缓冲区怎么管理。

打开 lv_port_disp.c,找到 disp_init() 函数。这里我一般直接调用 LCD 初始化函数:

static void disp_init(void)
{
    /* 你的 LCD 初始化代码 */
    LCD_Init();
    LCD_Clear(0x0000);
}

然后是最关键的 disp_flush() 回调。LVGL 每次刷新屏幕,都会调用这个函数,把一块矩形区域的像素数据传过来:

static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
    /* 拿到区域坐标 */
    int32_t x1 = area->x1;
    int32_t y1 = area->y1;
    int32_t x2 = area->x2;
    int32_t y2 = area->y2;

    /* 设置 LCD 窗口 */
    LCD_SetWindow(x1, y1, x2, y2);

    /* 发送像素数据 —— 这里用 DMA 会快很多 */
    LCD_SendPixels((uint16_t *)color_p, (x2 - x1 + 1) * (y2 - y1 + 1));

    /* 通知 LVGL 刷新完成 */
    lv_disp_flush_ready(disp_drv);
}

我曾经踩过的坑:忘记调用 lv_disp_flush_ready()。LVGL 会一直等这个信号,你不调用,屏幕就卡死在那。排查了整整一个下午才发现。

缓冲区配置也有讲究。LVGL 支持三种模式:

模式 缓冲区大小 特点 适用场景
单缓冲 1 个屏幕缓冲区 简单,但刷新慢 RAM 极小的芯片
双缓冲 2 个屏幕缓冲区 可避免撕裂,但 RAM 翻倍 追求画面流畅
部分缓冲 1/10 屏幕大小 折中方案,最常用 大多数项目

我个人习惯用部分缓冲 + DMA 传输。缓冲区大小设为屏幕高度的 1/10,配合 DMA 后台刷屏,CPU 几乎零负担。

4.4 实现 lv_port_indev:触摸输入适配

触摸驱动比显示简单得多。LVGL 只关心一件事:当前有没有触摸点,坐标是多少。

打开 lv_port_indev.c,实现 touchpad_read() 回调:

static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
    static lv_coord_t last_x = 0;
    static lv_coord_t last_y = 0;

    /* 读取触摸芯片 */
    uint8_t touch_count = TP_Read(&last_x, &last_y);

    if (touch_count > 0)
    {
        data->point.x = last_x;
        data->point.y = last_y;
        data->state = LV_INDEV_STATE_PR;
    }
    else
    {
        data->state = LV_INDEV_STATE_REL;
    }
}

这里有个细节:坐标可能需要做映射。你的触摸芯片分辨率可能是 320x240,但 LVGL 的逻辑分辨率是 480x320,那就得做缩放。我一般这样处理:

data->point.x = (last_x * LV_HOR_RES) / TP_MAX_X;
data->point.y = (last_y * LV_VER_RES) / TP_MAX_Y;

避坑指南:触摸芯片的 X/Y 轴可能跟屏幕显示方向不一致。我遇到过 GT911 的 X 轴是反的,最后在映射时加了个 LV_HOR_RES - 1 - x 才搞定。调试时先画个十字线,点一下看看坐标对不对。

4.5 实现 lv_port_tick:系统时钟喂给 LVGL

LVGL 需要知道时间流逝,才能做动画、定时刷新。裸机下没有操作系统时钟,你得自己提供一个毫秒级的心跳。

最简单的方法:用 STM32 的 SysTick 定时器。它默认每 1ms 中断一次,我们在中断里调用 lv_tick_inc(1):

void SysTick_Handler(void)
{
    lv_tick_inc(1);
}

如果你用了 FreeRTOS,那就用 vTaskDelay 配合 lv_tick_inc。但裸机下,SysTick 是最直接的选择。

注意:不要在中断里做太多事。lv_tick_inc 本身很轻量,但如果你在中断里调用 LVGL 的其他 API,可能会出问题。我习惯只在这里喂 tick,其他操作放到主循环。

4.6 主循环:把一切串起来

所有接口都配好了,最后在主循环里调用 LVGL 的定时处理函数:

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    LCD_Init();
    TP_Init();

    lv_init();
    lv_port_disp_init();
    lv_port_indev_init();

    /* 创建你的 UI */
    lv_obj_t * btn = lv_btn_create(lv_scr_act());
    lv_obj_t * label = lv_label_create(btn);
    lv_label_set_text(label, "Hello LVGL");

    while (1)
    {
        lv_timer_handler();  /* 处理 LVGL 任务 */
        HAL_Delay(5);        /* 适当延时,别让 CPU 空转 */
    }
}

嗯,这里有个关键点:lv_timer_handler() 的调用频率。我一般每 5ms 调用一次,太快了 CPU 负载高,太慢了界面响应迟钝。你可以用示波器量一下,保证调用间隔在 5-10ms 之间就行。

4.7 整体移植流程一览

说了这么多,我画张图帮你理清思路。整个移植过程就像搭积木,每一块都扣在一起:

LVGL 裸机移植流程 1. 准备工程 2. 配置 lv_conf.h 3. 实现三个接口 lv_port_disp lv_port_tick lv_port_indev 4. 主循环调用

你看,整个流程其实就四步。但每一步里都有细节,尤其是 lv_conf.h 的配置,我建议你对照着芯片手册来调,别凭感觉写。

4.8 验证移植是否成功

移植完了,怎么知道对不对?我一般做三个测试:

  1. 跑官方 demo:LVGL 自带的 lv_demo_widgets() 能覆盖大部分 UI 元素,跑一遍就知道显示和触摸正不正常。
  2. 测帧率:在 lv_conf.h 里开启 LV_USE_PERF_MONITOR,屏幕左上角会显示实时帧率。裸机下 30fps 算及格,60fps 算优秀。
  3. 测内存:调用 lv_mem_monitor() 看内存使用情况。如果碎片率超过 30%,说明 LV_MEM_SIZE 太小或者分配策略有问题。

一个小技巧:如果屏幕不亮,先别怀疑 LVGL。直接用 LCD 驱动刷个纯色,确认硬件没问题。我见过太多人花两天调 LVGL,最后发现是屏幕排线松了。

好了,移植这一步走到这里,你的 STM32 已经能跑 LVGL 了。接下来就是 UI 设计的事了——不过那是后面章节的内容。先把今天讲的这几个接口调通,你就算正式入门了。