简介这份 lvgl-8.3.10 源码包面向嵌入式 GUI 开发者、STM32 单片机学习者以及需要为资源受限设备搭建图形界面的工程师提供从基础组件到高级视觉效果的一站式实现参考。压缩包共 1183 个文件、约 24.51MB以 455 个 C 源文件和 213 个头文件为核心辅以 150 个 Python 脚本、134 个 Markdown 文档及 RST 说明并包含示例图片、字体与演示素材目录层次较清晰便于按模块查阅、移植和裁剪。已有 1824 人学习下载适合作为 LVGL 入门、项目选型和界面开发的实践资料。包内覆盖按钮、图表、列表、滑动条、图片等模块化组件以及动画、抗锯齿、透明度、平滑滚动和图层混合等图形引擎能力并支持触摸屏、键盘、编码器、按键等输入方式借助完整源码与示例资源可较快完成界面搭建、性能验证和跨硬件平台适配也能为组件定制和资源优化提供可复用的代码基础。1. 三个按钮卡半秒为什么在单片机上啃 LVGL 8.3.10 源码包拿到 lvgl-8.3.10 源码包那天我先没看界面代码而是让一块 STM32F4 板子跑空屏。320×240 的 SPI 屏三个按钮点一下要等半秒才变色他怀疑屏幕时序改了三天分频问题还在。真正原因很反直觉GUI 任务被主循环里的延时拖住LVGL 的刷新周期根本没跑起来。嵌入式设计里这类现象太常见界面 GUI 设计不是把控件摆上去就完事底层心跳、刷新回调、输入读取任何一环掉链子看起来都像屏的问题。这套源码适合手里有 STM32 或其他单片机、要用 C 写界面、愿意自己掌控移植层的人。它给的是 LVGL 8.3.10 核心源码、配置模板和显示/输入移植模板不是现成工程。下面按结构、移植、搭建、排查、验证五步走。2. 拆开 lvgl-8.3.10目录、配置与移植入口源码包到手最怕的是把整个目录拖进 Keil 然后点编译。LVGL 的目录里既有核心库也有示例、演示和移植模板全塞进去只会让编译时间变长、错误变多。先弄清楚哪些文件该进工程再谈配置和心跳。2.1 目录里哪几个文件夹决定成败解压后先看根目录。src是核心源码必须全部加入工程examples是控件示例按需取demos是演示程序占用 Flash 和 RAM 都大前期不建议编进去lv_conf_template.h是配置模板要复制成lv_conf.hlvgl.h是总入口lv_port_disp_template.c/h和lv_port_indev_template.c/h是显示、输入移植模板不是拿来做编译单元的而是拷内容到自己的lv_port_disp.c、lv_port_indev.c。路径作用处理建议src/LVGL 核心控件、绘制、字体、驱动框架全部加入工程examples/各控件用法示例只挑需要的参考不整包编译demos/综合演示界面前期关闭验证性能时再开lv_conf_template.h配置模板复制为lv_conf.h改#if 1lvgl.h对外总头文件工程包含路径指向lvgl根目录lv_port_disp_template.c/h显示移植模板拷到自己工程后重命名避免和模板重名lv_port_indev_template.c/h输入移植模板同上触摸、按键都走这个入口包含路径一般只加lvgl根目录让lvgl.h能按相对路径找到src/...。lv_conf.h放在工程包含路径能搜到的地方常见做法是放在lvgl同级目录并在编译器里定义LV_CONF_INCLUDE_SIMPLE让 LVGL 直接#include lv_conf.h。如果这个宏没定义内部会按相对路径找配置目录一挪就报找不到。注意lv_port_disp_template.c和lv_port_indev_template.c不要原样加入编译。它们只是模板原样编译容易和自己的lv_port_disp_init、lv_port_indev_init重名。2.2 先把 lv_conf.h 改成能编译的最小配置配置项很多第一轮不要全改。先把颜色深度、内存池、日志和演示开关定下来能点亮屏再逐项加。lv_conf_template.h顶部通常有#if 0不改的话整个配置被跳过编译会走默认值现象就是“我明明改了配置却不生效”。/* lv_conf.h第一轮只动这几个其他先保持模板默认 */ #if 1 /* 模板里是 #if 0必须改成 1否则配置整体失效 */ #define LV_COLOR_DEPTH 16 /* 大多数 SPI 屏是 RGB565 */ #define LV_COLOR_16_SWAP 0 /* SPI 字节序不对时再改 1 */ #define LV_MEM_CUSTOM 0 /* 用 LVGL 内部内存池 */ #define LV_MEM_SIZE (48U * 1024U) /* 控件和对象的内存池RAM 紧可先降到 24KB */ #define LV_USE_LOG 0 /* 调试期可开 1发布前关掉 */ #define LV_USE_DEMO_WIDGETS 0 /* 不编译综合演示 */ #define LV_USE_DEMO_BENCHMARK 0 #endifLV_COLOR_DEPTH必须和屏幕实际颜色格式一致16 位色下lv_color_t占 2 字节后面算绘图缓冲区大小都按这个来。LV_COLOR_16_SWAP只负责 16 位像素的高低位交换不是颜色顺序校正如果红蓝反了先查屏幕初始化序列和 RGB/BGR 配置不要指望这个宏解决所有颜色问题。LV_MEM_SIZE是 LVGL 内部对象池按钮、标签、样式、动画这些对象从这里分配。320×240 的界面48KB 比较稳如果单片机 RAM 只有 64KB先降到 24KB再通过减少页面对象、及时删除不用的页面来控制。绘图缓冲区是另外定义的静态数组不走这个内存池算 RAM 时别漏掉。LV_USE_LOG调试期可以开但要接一个lv_log_register_print_cb输出到串口否则开了也看不到。发布前关掉能省不少代码空间。LV_USE_DEMO_*前期一律关等显示和输入都通了再单独开一个 demo 做性能参考。2.3 把源码包塞进 Keil/IAR/Makefile 的三种姿势Keil 和 IAR 的做法类似新建一个lvgl分组把lvgl/src下所有.c加进去包含路径加lvgl根目录和lv_conf.h所在目录预定义宏加LV_CONF_INCLUDE_SIMPLE。不要只加lvgl/src/coreLVGL 8.3 的绘制、字体、控件、驱动框架分散在多个子目录漏一个就在链接时报未定义。Makefile 或 CMake 工程可以用通配收集省得手工维护文件列表。下面这段 CMake 把lvgl/src下所有 C 文件编成静态库并把包含路径和配置宏带出去set(LVGL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/lvgl) file(GLOB_RECURSE LVGL_SRC ${LVGL_DIR}/src/*.c) add_library(lvgl STATIC ${LVGL_SRC}) target_include_directories(lvgl PUBLIC ${LVGL_DIR}) target_compile_definitions(lvgl PUBLIC LV_CONF_INCLUDE_SIMPLE)file(GLOB_RECURSE ...)的优点是新增源文件不用改脚本缺点是新增文件后 CMake 不一定自动重跑改完目录最好手动重新生成一次。target_include_directories只暴露lvgl根目录不要把lvgl/src下每个子目录都加进去头文件包含关系按 LVGL 自己的相对路径走路径加多了反而容易和别的库撞名。如果工程里已经有一份lv_conf.h要确认编译器实际搜到的是哪一份。常见翻车是工程目录一份、lvgl目录一份改了其中一份编译用的是另一份。排查时在lv_conf.h里加一行#error this lv_conf编译一次就能看到实际路径确认后再删掉。2.4 让心跳先跑起来SysTick 与 lv_task_handlerLVGL 需要两个时间源一个是 1ms 心跳用来跑动画、定时器和输入消抖一个是主循环里的lv_task_handler用来处理刷新、事件和定时任务。心跳不进lv_task_handler界面就不会动lv_task_handler调得太慢按钮响应就像卡住。#include lvgl.h void SysTick_Handler(void) { lv_tick_inc(1); /* SysTick 配成 1ms这里就传 1配成 2ms 就传 2 */ } int main(void) { board_init(); lv_init(); /* 先初始化 LVGL 内核 */ lv_port_disp_init(); /* 再注册显示驱动 */ lv_port_indev_init(); /* 再注册输入驱动 */ while (1) { lv_task_handler(); /* GUI 主循环建议 5ms 左右调用一次 */ delay_ms(5); } }lv_tick_inc(1)放在 SysTick 中断里参数是“这次经过了多少毫秒”必须和 SysTick 周期一致。有人把 SysTick 配成 1ms却在中断里写lv_tick_inc(10)动画会快十倍触摸消抖也会乱。lv_task_handler不要放中断里它内部会刷屏、跑事件回调执行时间不固定。如果跑 RTOS常见做法是单独建一个 GUI 任务任务里循环调用lv_task_handler()外面加互斥锁。其他任务要改界面时不要直接调lv_obj_set_text而是发消息给 GUI 任务或者拿锁再改。LVGL 8.3 默认不是线程安全的两个任务同时碰同一个对象轻则花屏重则 HardFault。注意lv_init()必须在lv_port_disp_init()之前。显示驱动注册时会用到内核里的显示链表顺序反了可能注册不上。3. 显示与输入移植把 flush_cb 和 read_cb 写成可复现的驱动移植层只做两件事把 LVGL 画好的像素送到屏把触摸状态送回去。听起来简单但flush_cb里少调一次lv_disp_flush_ready界面就只刷第一帧触摸坐标没映射按钮就永远点不中。下面按显示、输入、最小验证三步走。3.1 显示驱动单缓冲、双缓冲和 DMA 的取舍先求通再求快。第一版用单缓冲缓冲区开 40 行320×240 的屏大约 25KB能明显降低撕裂又不会把 RAM 吃光。lv_disp_draw_buf_init的第三个参数是缓冲区像素数第四个参数给第二个缓冲区单缓冲传NULL双缓冲传第二块地址。#define LCD_HOR_RES 320 #define LCD_VER_RES 240 static lv_disp_draw_buf_t draw_buf; static lv_color_t buf_1[LCD_HOR_RES * 40]; /* 40 行16 位色下约 25KB */ static void disp_flush(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { uint16_t w area-x2 - area-x1 1; uint16_t h area-y2 - area-y1 1; LCD_SetWindow(area-x1, area-y1, area-x2, area-y2); LCD_WritePixels((uint16_t *)color_p, w * h); lv_disp_flush_ready(drv); /* 必须调用且一次刷新只调一次 */ } void lv_port_disp_init(void) { lv_disp_draw_buf_init(draw_buf, buf_1, NULL, LCD_HOR_RES * 40); static lv_disp_drv_t disp_drv; lv_disp_drv_init(disp_drv); disp_drv.hor_res LCD_HOR_RES; disp_drv.ver_res LCD_VER_RES; disp_drv.flush_cb disp_flush; disp_drv.draw_buf draw_buf; lv_disp_drv_register(disp_drv); }area的坐标是闭区间算宽高要x2 - x1 1少加一就会每行少一个像素界面右边出现一条残影。color_p指向本次要刷的像素起始地址LCD_WritePixels按uint16_t写是因为 16 位色下每个像素两个字节如果你的屏驱动接口是 8 位字节流就按w * h * 2字节写别把像素数当字节数。用 DMA 时disp_flush里只启动传输不能在启动后立刻调lv_disp_flush_ready。正确做法是把disp_drv改成全局或文件级静态在 DMA 完成中断里调static lv_disp_drv_t disp_drv; static void lcd_dma_done_cb(void) { lv_disp_flush_ready(disp_drv); }DMA 完成回调里只调一次传输启动失败时也要补调否则 LVGL 会一直等这一帧后面所有刷新都停住。双缓冲适合 DMA一块缓冲在刷屏另一块给 LVGL 画下一帧但 RAM 要翻倍。320×240 全屏双缓冲是 320×240×2×2约 300KB多数单片机扛不住所以更常见的是两块 40 行的小缓冲。3.2 输入驱动触摸坐标校准与按下释放状态触摸驱动最容易犯的错是把原始 ADC 值直接塞给 LVGL。原始值范围可能是 0~4095屏幕坐标是 0~319不映射当然点不中。先读原始坐标再做线性映射必要时交换 X/Y 或镜像。static void touch_read(lv_indev_drv_t *drv, lv_indev_data_t *data) { uint16_t x, y; bool pressed touch_read_xy(x, y); if (pressed) { /* TOUCH_X_MAX / TOUCH_Y_MAX 用实测原始最大值下面只是示意 */ int32_t sx (int32_t)x * LCD_HOR_RES / TOUCH_X_MAX; int32_t sy (int32_t)y * LCD_VER_RES / TOUCH_Y_MAX; if (sx 0) sx 0; if (sx LCD_HOR_RES) sx LCD_HOR_RES - 1; if (sy 0) sy 0; if (sy LCD_VER_RES) sy LCD_VER_RES - 1; >static void btn_event_cb(lv_event_t *e) { lv_obj_t *btn lv_event_get_target(e); lv_obj_t *label lv_obj_get_child(btn, 0); lv_label_set_text(label, clicked); } void ui_test_create(void) { lv_obj_t *scr lv_scr_act(); lv_obj_set_style_bg_color(scr, lv_color_hex(0x101418), 0); lv_obj_t *btn lv_btn_create(scr); lv_obj_set_size(btn, 120, 48); lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0); lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL); lv_obj_t *label lv_label_create(btn); lv_label_set_text(label, test); lv_obj_center(label); lv_obj_t *bar lv_bar_create(scr); lv_obj_set_size(bar, 160, 12); lv_obj_align(bar, LV_ALIGN_CENTER, 0, 60); lv_bar_set_value(bar, 60, LV_ANIM_OFF); }按钮文字变了但进度条不显示先查flush_cb的区域计算进度条显示但按钮点不动查触摸坐标和LV_INDEV_STATE_REL两者都正常但背景有花点查 SPI 字节序和LV_COLOR_16_SWAP。这三步能把问题锁在显示、输入、配置三个格子里比一上来调整个界面快得多。4. 界面 GUI 设计落地从控件树到事件回调的工程化写法能跑 demo 不等于能交付界面。demo 里控件少、逻辑简单真正做产品时页面一多、回调一乱代码会比驱动还难维护。我的习惯是先把控件树画在纸上再写创建函数样式统一抽出来事件回调只做“发命令”不在里面跑业务。4.1 先画控件树再写代码屏幕、容器、控件的层级LVGL 的对象是树形结构父对象删除时子对象一起删。页面切换不要用lv_obj_del(lv_scr_act())把屏幕删了常见做法是在当前屏幕上放一个页面容器切换时lv_obj_clean(page)清掉子控件再建新页面。容器用lv_obj_create创建大小用LV_PCT(100)跟随父对象布局用 flex 排。static lv_obj_t *ui_create_page(lv_obj_t *parent) { lv_obj_t *page lv_obj_create(parent); lv_obj_set_size(page, LV_PCT(100), LV_PCT(100)); lv_obj_set_flex_flow(page, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(page, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_all(page, 8, 0); lv_obj_set_style_border_width(page, 0, 0); return page; }LV_PCT(100)是百分比尺寸父容器变宽变高时子对象跟着变比写死 320×240 更适合不同屏。lv_obj_set_flex_flow(page, LV_FLEX_FLOW_COLUMN)让子控件从上到下排lv_obj_set_flex_align三个参数分别管主轴、交叉轴、轨道对齐。lv_obj_set_style_pad_all给容器内边距避免控件贴边。控件树不要太深。每多一层容器绘制和布局就多一层开销。一个页面通常“屏幕 → 顶部栏 → 内容区 → 控件”就够了。列表项多的时候不要一次创建几百个按钮用 LVGL 的列表或表格控件或者只创建当前可见的几项滚动时复用。4.2 用样式和主题统一风格别在每个控件上重复设置同一个按钮颜色如果在每个按钮创建后都写一遍lv_obj_set_style_bg_color后面改配色要改几十处。把样式对象抽出来初始化一次创建控件时挂上去。样式对象必须是静态或全局生命周期不能定义在函数里返回函数栈一退样式内存就没了。static lv_style_t style_btn_primary; static lv_style_t style_btn_pressed; static void ui_style_init(void) { lv_style_init(style_btn_primary); lv_style_set_bg_color(style_btn_primary, lv_color_hex(0x2F6FED)); lv_style_set_bg_opa(style_btn_primary, LV_OPA_COVER); lv_style_set_radius(style_btn_primary, 8); lv_style_set_text_color(style_btn_primary, lv_color_white()); lv_style_init(style_btn_pressed); lv_style_set_bg_color(style_btn_pressed, lv_color_hex(0x1B4FBF)); } static void ui_apply_primary(lv_obj_t *btn) { lv_obj_add_style(btn, style_btn_primary, LV_STATE_DEFAULT); lv_obj_add_style(btn, style_btn_pressed, LV_STATE_PRESSED); }lv_style_set_bg_opa设成LV_OPA_COVER才是实心背景默认可能是透明按钮看起来像只有文字。LV_STATE_PRESSED是按下状态松开后自动回默认状态。样式可以叠加后添加的优先级更高不要给同一个状态重复加同属性样式后期排查很痛苦。字体、圆角、边框、阴影这些也放样式里。不同页面共用一套style_btn_primary改配色只改一处。需要局部微调时再用lv_obj_set_style_*覆盖单个控件覆盖项越少越好。4.3 事件回调与页面切换把业务逻辑挡在 GUI 线程外按钮回调里最忌讳三件事长延时、阻塞式等待、直接操作硬件。回调本文还有配套的精品资源点击获取