3. 嵌入式系统与显示基础

好,咱们正式开始接触LVGL。说实话,我第一次看到这个库的时候,心里想的是:「又一个GUI框架,能有多特别?」结果用下来,它确实有点东西。这一章,我带你把LVGL的底裤扒开看看——它到底是什么、怎么拿过来用、怎么让它跑起来。

3.1 LVGL是什么?为什么选它?

LVGL,全称Light and Versatile Graphics Library,翻译过来就是「轻量且通用的图形库」。它专门为嵌入式系统设计,目标是在资源受限的MCU上跑出像样的图形界面。

我最早接触LVGL是在一个温控器项目上。当时MCU只有256KB Flash、64KB RAM,还要显示曲线、按钮、滑动条。用emWin要授权费,用TouchGFX又跑不动。最后选了LVGL,嗯,刚刚好。

它的核心特点,我列个表给你看:

特性 说明 我的评价
开源免费 MIT协议,商用无限制 老板最爱,没有之一
资源占用低 最小配置:Flash 64KB、RAM 8KB STM32F103都能跑
控件丰富 按钮、滑块、图表、键盘、动画…30+种 够用,不够还能自己画
跨平台 支持裸机、FreeRTOS、Linux等 一套代码,到处编译
硬件加速 支持STM32 DMA2D、NXP PXP等 开了加速,帧率翻倍

一句话总结:LVGL就是嵌入式界的「Qt」。你想想看,在资源那么紧的情况下,还能做出手机App级别的界面,是不是有点意思?

3.2 LVGL的软件架构

搞懂架构,你才知道怎么用它。LVGL的分层设计很清晰,我画了张图,你一看就明白:

LVGL 软件架构分层图 应用层(Application) 你的业务逻辑:按钮点击、数据刷新、页面切换 控件层(Widgets) lv_btn / lv_label / lv_slider / lv_chart … 核心层(Core) 对象管理 / 事件分发 / 样式系统 / 动画引擎 / 内存管理 硬件抽象层(HAL / Porting) 显示驱动 / 触摸驱动 / 时钟节拍 / 文件系统接口 硬件层(MCU / LCD / Touch) 你只需要 改这一层

你看,从上到下分五层。你写应用代码,调用控件API,核心层帮你处理渲染和事件,硬件抽象层把显示和触摸接到底层MCU上。

这里有个关键点:你只需要改最下面那层——硬件抽象层。其他层,LVGL已经写好了,你直接调用就行。

我的习惯:拿到一个新板子,先花半天把HAL层调通。显示能刷出颜色、触摸能读到坐标,后面的事就顺了。千万别一上来就写界面,底没打好,上面全是坑。

3.3 下载LVGL库

下载LVGL,我推荐两种方式:

  1. 从GitHub直接下载ZIP — 适合新手,一次搞定
  2. 用Git Clone — 适合后续更新,我习惯用这个

LVGL的GitHub地址是:https://github.com/lvgl/lvgl

我个人习惯用Git,命令很简单:

git clone https://github.com/lvgl/lvgl.git
cd lvgl
git checkout v8.3.0    # 切到稳定版本

下载完后,目录结构长这样:

lvgl/
├── src/          # 核心源码
│   ├── core/     # 核心对象、事件、样式
│   ├── draw/     # 渲染引擎
│   ├── extra/    # 扩展控件
│   ├── hal/      # 硬件抽象层接口
│   └── misc/     # 工具函数
├── examples/     # 示例代码
├── demos/        # 演示工程
├── lv_conf_template.h  # 配置文件模板
└── lvgl.h        # 主头文件

注意:千万不要直接把整个lvgl文件夹扔进你的工程。你只需要src目录和lv_conf.h配置文件。我曾经见过有人把examples和demos也编译进去,结果Flash直接爆了。

3.4 移植LVGL到你的工程

移植LVGL,说白了就是让LVGL知道「怎么往屏幕上画点」和「怎么知道时间过去了多久」。我把它拆成5步:

步骤 做什么 关键函数/文件
1. 复制源码 把lvgl/src复制到工程 —
2. 配置lv_conf.h 打开宏开关,设置颜色深度、内存大小 LV_COLOR_DEPTH、LV_MEM_SIZE
3. 实现显示驱动 告诉LVGL怎么刷屏 disp_flush()
4. 实现触摸驱动 告诉LVGL触摸坐标 touchpad_read()
5. 提供时钟节拍 每1ms调用一次lv_tick_inc() lv_tick_inc(1)

咱们重点看显示驱动怎么写。这是最核心的部分:

/* 显示刷新回调函数 */
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
    int32_t x, y;
    for(y = area->y1; y <= area->y2; y++) {
        for(x = area->x1; x <= area->x2; x++) {
            /* 把color_p指向的颜色写到屏幕(x,y)位置 */
            set_pixel(x, y, color_p->full);
            color_p++;
        }
    }
    /* 通知LVGL刷新完成 */
    lv_disp_flush_ready(disp_drv);
}

/* 初始化显示驱动 */
void lv_port_disp_init(void)
{
    lv_disp_drv_t disp_drv;
    lv_disp_drv_init(&disp_drv);
    disp_drv.flush_cb = disp_flush;
    disp_drv.hor_res = 320;   /* 屏幕宽度 */
    disp_drv.ver_res = 240;   /* 屏幕高度 */
    lv_disp_drv_register(&disp_drv);
}

避坑指南:我曾经在移植时忘了调用lv_disp_flush_ready(),结果屏幕死活不刷新。查了半天,原来是LVGL在等这个信号才继续送下一帧数据。记住,刷完屏一定要喊一声「我好了」。

触摸驱动也类似,核心就是告诉LVGL「手指按在哪」:

static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
    int16_t x, y;
    uint8_t pressed;

    /* 从硬件读取触摸状态 */
    read_touch(&pressed, &x, &y);

    if(pressed) {
        data->state = LV_INDEV_STATE_PR;
        data->point.x = x;
        data->point.y = y;
    } else {
        data->state = LV_INDEV_STATE_REL;
    }
}

3.5 创建第一个LVGL工程:Hello World

好,移植完了,咱们来写第一个界面。目标很简单:屏幕上显示一行「Hello, LVGL!」文字。

先看完整代码:

#include "lvgl.h"
#include "lv_port_disp.h"
#include "lv_port_indev.h"

int main(void)
{
    /* 1. 初始化硬件 */
    hardware_init();

    /* 2. 初始化LVGL */
    lv_init();

    /* 3. 初始化显示和触摸驱动 */
    lv_port_disp_init();
    lv_port_indev_init();

    /* 4. 创建Hello World界面 */
    lv_obj_t * label = lv_label_create(lv_scr_act());
    lv_label_set_text(label, "Hello, LVGL!");
    lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);

    /* 5. 主循环 */
    while(1) {
        lv_timer_handler();  /* 处理LVGL任务 */
        delay_ms(5);         /* 适当延时 */
    }
}

这段代码,我一行行给你拆开讲:

  • lv_init() — 初始化LVGL内部模块,必须第一个调用
  • lv_port_disp_init() — 注册显示驱动,告诉LVGL怎么画屏
  • lv_port_indev_init() — 注册输入设备驱动,告诉LVGL触摸信息
  • lv_label_create(lv_scr_act()) — 在当前活动屏幕上创建一个标签控件
  • lv_label_set_text() — 设置标签显示的文本
  • lv_obj_align() — 把标签居中放置
  • lv_timer_handler() — LVGL的心跳函数,处理动画、刷新、事件

核心要点:lv_timer_handler()必须周期性调用,否则LVGL不会做任何事。我见过有人把它放在中断里,结果系统卡死。正确的做法是放在主循环或RTOS的低优先级任务里,每5~10ms调用一次。

3.6 编译与运行

编译前,检查一下你的lv_conf.h配置:

/* 关键配置项 */
#define LV_COLOR_DEPTH     16    /* 16位色,RGB565 */
#define LV_MEM_SIZE        (32U * 1024U)  /* 32KB内存池 */
#define LV_TICK_CUSTOM     1     /* 使用自定义时钟节拍 */
#define LV_USE_DEMO_RENDER 0     /* 关闭演示,节省空间 */

编译通过后,下载到板子。如果一切正常,屏幕中央会显示:

Hello, LVGL!

如果屏幕没显示,别慌。先检查这几样:

  • 显示驱动里的set_pixel()函数是否真的能点亮像素?单独写个测试函数验证一下。
  • lv_tick_inc()是否在定时器中断里被调用?频率对不对?
  • lv_conf.h里的LV_COLOR_DEPTH是否和屏幕硬件匹配?

我曾经在16位色屏上配了32位色,结果颜色全乱套。调了一下午才发现是这里的问题。

3.7 本章小结

这一章,咱们干了三件事:

  • 搞懂了LVGL是什么、为什么选它
  • 理清了LVGL的五层架构,知道改哪层、不动哪层
  • 亲手移植了LVGL,并跑通了第一个Hello World界面

说实话,第一次看到「Hello, LVGL!」出现在自己板子上的时候,还是有点小激动的。你想想看,从裸机画点,到能显示文字,这一步跨出去,后面就是一片新天地。

嗯,基础打好了,后面咱们就可以玩点真的了。