简介:《LVGL开发指南(中文V1.3)》是一份面向嵌入式GUI开发初学者与STM32工程师的实践型PDF教程,重点解决在资源受限微控制器上完成LVGL移植、界面搭建与交互设计的问题。压缩包共1个文件,为PDF文档,大小约9.54MB,便于在电脑或平板上按章节查阅。内容围绕正点原子STM32开发板展开,覆盖初识LVGL、源码获取、无操作系统移植、FreeRTOS环境移植、PC模拟器使用、初始化流程以及按钮/标签/进度条等UI组件开发,并延伸到动画交互与性能优化,读者可据此对照硬件完成显示驱动适配、例程验证和界面调试。目前已有766人学习,适合作为嵌入式图形界面入门与项目移植的参考手册。
1. 先搞懂一件事:LVGL 到底是个什么东西
很多人第一次翻开一份 LVGL 开发指南,看到 lv_obj、lv_conf.h、移植、绘制缓冲区这些词,第一反应是把文件关掉——感觉像是写给老手的,跟自己没关系。把它翻译成人话其实只有一句:LVGL 是一个跑在单片机上的图形界面库,专门帮你在小小的 LCD 屏上画出按钮、进度条、滑块、图表这些东西。
你可以把它想成嵌入式世界里的装修队。屏幕和触摸屏是毛坯房,LVGL 是那套现成的家具和施工图纸,你只要说“这儿放个按钮、点一下变个颜色”,至于像素怎么摆、颜色怎么刷、哪块区域该重画,它替你干。适合谁?适合已经能让板子跑起来、想让设备有个像样界面的人;也适合连板子都没摸过、但想先看看界面开发是怎么回事的人,因为它在电脑上就能跑起来,不用等硬件到货。
2. 为什么值得学:嵌入式做界面,为什么绕不开它
先说结论:不是 LVGL 有多神,是“自己从零写界面”这件事太贵了。零基础的人最容易犯的错,就是一上来就去啃移植那一章,啃到第三天放弃——因为你还没见过它跑起来是什么样,全是在跟看不见的东西较劲。这一章先把“为什么要学”讲透,再告诉你第一步该往哪迈。
2.1 从“点灯”到“点屏幕”:需求是怎么一步步变高的
早些年做嵌入式,人机交互基本就是几个按键加一排数码管,代码里写死“按下 K1,数字加一”,逻辑清楚,几十行搞定。现在不一样了:小家电要彩屏,充电桩要触摸面板,工业设备要实时曲线,智能家居中控要能滑动切页。用户拿手机的标准来看你的设备,界面上只要有一个地方卡顿或者错位,立刻就觉得“这东西不靠谱”。
于是问题来了。一块 320×240 的彩屏,你要在上面画一个圆角矩形按钮,还要有按下变色的效果,纯手写是什么工作量?你得自己算每个像素的坐标、自己实现圆角算法、自己做一块内存缓冲区、自己判断哪块区域脏了需要重绘、自己处理触摸坐标和控件区域的碰撞检测。一个按钮写两天,一个滑动列表写一周,写完还没法复用——换个分辨率全部重来。
这就是 LVGL 这类图形库存在的理由。它把“画”这件事抽象成了“对象”:按钮是一个对象,标签是一个对象,进度条是一个对象,你只管创建、摆位置、设样式,绘制和刷新它包了。对新手来说,最直接的好处是你不需要懂任何图形学,也能让屏幕上有东西动起来。
2.2 手写刷屏和用现成库,差在哪
我把两者的差别列成一张表,你一眼就能看出该选哪条路:
| 对比项 | 自己写刷屏代码 | 用 LVGL |
|---|---|---|
| 一个圆角按钮 | 自己算坐标和像素,一两百行 | 两行创建,一行设大小 |
| 按下变色 | 自己写状态判断和重绘逻辑 | 加一条样式,指定 PRESSED 状态 |
| 滑动列表 | 自己做惯性、边界回弹 | 容器自带滚动属性 |
| 动画 | 自己写插值和时间轴 | 配置起始值、结束值、时长 |
| 换分辨率 | 大量坐标要重算 | 改一个宏,或者用布局自适应 |
| 中文显示 | 自己取模、自己做字库 | 挂了字库就能显示 |
除了这些“省事”,LVGL 还有几个对嵌入式特别友好的特点。它是纯 C 写的,不依赖操作系统,裸机可以跑,挂了 FreeRTOS 也能跑;它对内存很抠,你可以只编译用到的控件,把 Flash 和 RAM 占用压到很小;它自带一套 PC 模拟器工程,也就是说界面代码可以先在电脑上调试,调好了再搬到板子上,这一点对新手极其重要——后面会反复提到。
顺带说一句,网络热词里“嵌入式”这三个字最近出现频率很高,很多转行的人都在问“嵌入式要不要学界面”。我的看法是:会点灯的人一抓一大把,能让设备有个像样界面的人明显少,这就是差异化的那部分。
2.3 零基础的正确起步顺序:先软后硬,先跑后懂
这是我最想对新手说的一段。血泪经验:不要按文档目录顺序学,要按“能最快看到东西”的顺序学。
一份完整的中文开发指南通常分两大块:前半部分讲移植和底层接口,后半部分一个个讲控件怎么用。新手如果从移植开始,会遇到一堆跟 LVGL 本身无关的问题——LCD 驱动没调通、引脚接错、时序不对、内存不够,任何一个环节出问题,你都不知道是 LVGL 的问题还是板子的问题,等于在解一个黑匣子。
所以我建议的顺序是这样:
第一步,在电脑上把模拟器工程跑起来,让屏幕上出现一个按钮。这一步没有任何硬件风险,出问题一定是配置问题,好排查。
第二步,拿现成的例程,改几个数字,看看按钮变大变小、颜色变深变浅。这一步建立“我改代码,屏幕就变”的直觉。
第三步,把控件挨个摸一遍:标签、按钮、进度条、滑块、开关、图表。不用记 API,知道“有这么个东西,大概叫什么名字”就够了,用的时候再查。
第四步,等你能在电脑上拼出一个登录页、一个仪表盘,再回头去做硬件移植。这时候你心里有底,遇到屏幕不亮也敢判断是显示驱动的事,不是自己代码写错了。
第五步,接真实硬件,把模拟器上的界面原样搬过去。
按这个顺序走,通常一周之内就能有能看的东西;按文档顺序硬啃,不少人卡在移植那章就没了下文。
3. 第一个例子跑起来:从空工程到屏幕上出现一个按钮
这一章我们真的动手。目标很朴素:屏幕上出现一个居中的按钮,上面写着“点我”。这个例子一旦跑通,后面所有控件都是同一套套路,你只是换函数名。
3.1 LVGL 不碰硬件:它只找你要三样东西
理解这一点,移植这件事就不吓人了。LVGL 本身完全不知道你用的是哪块屏、什么驱动 IC、触摸是电容还是电阻,它只对外要三样东西:
第一样是“画布”。它需要一块内存当缓冲区,自己在这块内存里把画面画好;画好之后通过一个回调函数告诉你“这块区域我画完了,你去刷到屏幕上”。这个回调就是刷屏函数。
第二样是“输入”。它需要一个函数,能返回“现在有没有人碰屏幕,碰在哪个坐标”。你给它这个函数,它自己去做“这一下点在哪个按钮上”的判断。
第三样是“时间”。动画、长按判断、滚动惯性都要时间基准,所以它需要一个毫秒级的计时,通常是一个每毫秒加一的计数器。
把这三样给了它,它就能跑。剩下所有工作都在软件层,跟硬件无关。反过来说,如果你的界面不动、不刷新、点了没反应,排查方向也就这三个:刷屏回调没写对、输入没注册、时间没走。
3.2 第一步:在电脑上把画面跑起来
先别碰板子。一份成熟的中文指南里通常会带一个 PC 模拟器工程,这是新手最好的起点。步骤大致是这样:
- 找到指南配套的模拟器工程目录(一般在资料包的“模拟器”或“PC 仿真”文件夹里)。
- 用工程指定的 IDE 打开项目文件——常见的是 Visual Studio 或者 CodeBlocks 这类,具体看指南里写的是哪个。
- 直接点编译运行,不要改任何代码。
- 如果弹出一个窗口,里面有个界面,说明环境是通的;如果报错,先看是不是缺了某个运行库或者工具集版本不对,这类报错在搜索引擎上很好查到。
- 找到工程里的主文件,通常叫 main.c,里面会有一个创建界面的函数,从那里开始改。
这一步的意义在于:你手上有了一个“改代码立刻能看到结果”的环境。新手最需要的就是这个反馈回路,没有它,学界面开发跟背单词没区别。
提示:模拟器窗口的分辨率一般是可以改的,先按默认的来,等你熟悉了再改成你板子上的实际分辨率,比如 320×240 或者 480×272,这样电脑上看到的效果和板子上基本一致。
3.3 第二步:十行代码,屏幕上出现一个按钮
在模拟器工程里找到创建界面的地方,写下面这段。这是 LVGL 里最典型的一段代码,所有控件都是这个结构:
/* 创建第一个界面:屏幕中央放一个按钮,按钮上写“点我” */ void my_first_ui(void) { /* 1. 创建按钮。参数是“父对象”,lv_scr_act() 表示当前的活动屏幕 */ lv_obj_t *btn = lv_btn_create(lv_scr_act()); /* 2. 设置按钮大小:宽 120 像素,高 50 像素 */ lv_obj_set_size(btn, 120, 50); /* 3. 把按钮对齐到父对象(屏幕)的中央,后两个参数是 x、y 方向的偏移 */ lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0); /* 4. 创建标签,父对象是按钮,所以标签会跟着按钮一起动 */ lv_obj_t *label = lv_label_create(btn); /* 5. 设置标签显示的文字 */ lv_label_set_text(label, "点我"); /* 6. 标签在自己父对象(按钮)内部居中 */ lv_obj_center(label); }这段代码看着简单,但每一行的含义都值得说清楚,因为后面所有控件都是这套逻辑的复制。
lv_btn_create(lv_scr_act())里的参数是父对象。屏幕是所有对象的根,你把按钮挂到屏幕上,它就显示在屏幕上;如果你把按钮挂到另一个容器上,它就跟着容器走。这个“父子关系”是 LVGL 最核心的机制,第 4 章专门讲。
lv_obj_set_size传的是宽和高,单位是像素。这里写死 120×50 只是为了好理解,实际项目里更推荐用布局让控件自己算大小。
lv_obj_align的第二个参数是参考点,LV_ALIGN_CENTER表示以父对象中心为基准,后两个数字是相对这个基准的偏移量,写 0、0 就是正中间。你想往上挪 20 像素,就写 0、-20。这套对齐方式在 LVGL 里到处都在用,记住一个就够。
lv_label_create(btn)这里把按钮当父对象,是最常见的一个小技巧:标签成了按钮的子对象,按钮移动、隐藏、删除,标签都跟着走,不用你手动同步。
lv_obj_center(label)是lv_obj_align的一个简化版,等价于“以父对象中心对齐,偏移 0、0”。
写完编译运行,窗口里应该出现一个蓝底白字的圆角按钮。如果没有,先检查这段函数有没有被调用——这是新手最常见的疏漏,函数写好了但没人调,屏幕当然一片空白。
3.4 第三步:搬到板子上,移植其实就四件事
现在把同样的界面搬到真实硬件上。别被“移植”这两个字吓到,拆开看就四件事。
第一件,准备一个已经能正常点亮屏幕的工程。这一步必须先用不涉及 LVGL 的代码验证:屏幕能刷纯色、能显示图片。如果这一步没过,先解决它,别往下走。
第二件,把 LVGL 的源码文件加进工程。一份完整指南的移植章节会详细列出来要加哪些目录、哪些文件不要加,照着做就行。核心是源码目录加上一个配置文件,配置文件决定了功能裁剪、内存大小、颜色深度。
第三件,注册显示驱动。这是移植里最关键的一段,代码长这样:
/* 显示缓冲区:宽度按屏幕宽度,高度只取 10 行,省内存 */ static lv_disp_draw_buf_t draw_buf; static lv_color_t buf[320 * 10]; /* 刷屏回调:LVGL 画完一块区域后,通过这个函数通知你去刷屏 */ static void my_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { /* 调用你屏幕上已有的区域填充函数,把这块矩形刷上去 */ lcd_fill_rect(area->x1, area->y1, area->x2, area->y2, (uint16_t *)color_p); /* 必须调用,告诉 LVGL 这块刷完了,否则界面会卡住不动 */ lv_disp_flush_ready(drv); } /* 注册显示设备 */ static void my_disp_init(void) { lv_disp_draw_buf_init(&draw_buf, buf, NULL, 320 * 10); static lv_disp_drv_t disp_drv; lv_disp_drv_init(&disp_drv); disp_drv.hor_res = 320; /* 横向分辨率 */ disp_drv.ver_res = 240; /* 纵向分辨率 */ disp_drv.flush_cb = my_flush_cb; /* 刷屏回调 */ disp_drv.draw_buf = &draw_buf; /* 绑上缓冲区 */ lv_disp_drv_register(&disp_drv); }这里的重点是my_flush_cb。LVGL 不会一次画整屏,它只会告诉你“这块区域画好了”,你把这块区域刷到屏上就行。缓冲区高度取 10 行是常见做法,整屏缓冲太占内存,10 行缓冲在大多数场景下够用而且省。lv_disp_flush_ready这行千万不能漏,漏了界面就定格在第一帧,很多人卡在这一步以为是自己屏幕坏了。
第四件,给时间、跑主循环。时间基准一般放在 1 毫秒的中断或者定时器里,主循环里定期让 LVGL 干活:
/* 每 1 毫秒调一次,给 LVGL 提供时间基准 */ void SysTick_Handler(void) { lv_tick_inc(1); } /* 主循环:让 LVGL 处理动画、事件和重绘 */ while (1) { lv_timer_handler(); delay_ms(5); }lv_timer_handler是 LVGL 的“心跳”,动画推进、事件分发、区域重绘全靠它。它需要被周期性地调用,间隔一般 5 毫秒左右。调得太慢界面会卡,调得太快浪费 CPU。
3.5 lv_conf.h 里改三个数就够入门用了
配置文件是新手最容易翻车的地方,因为它选项特别多。入门阶段你只需要盯住三个:
| 配置项 | 作用 | 新手怎么设 |
|---|---|---|
| LV_COLOR_DEPTH | 颜色深度,决定一个像素占几位 | 屏是 RGB565 就填 16,别乱填 |
| LV_MEM_SIZE | LVGL 自己的内存池大小 | 从 32KB 起,不够再往上加 |
| LV_USE_LOG | 日志开关 | 调试时打开,能省很多猜的时间 |
LV_COLOR_DEPTH填错了,典型现象是颜色发紫或者发绿,因为字节序对不上。LV_MEM_SIZE给小了,现象是创建到第几个控件之后突然不显示了,或者程序跑飞。这两个坑我在第 5 章会展开讲怎么排查。
4. 上手四个核心概念:对象、父子、样式、事件
按钮跑起来之后,很多人会陷入“只会复制粘贴改文字”的状态,稍微复杂一点的需求就卡住。原因是没有建立对象模型。这一章四个概念,理解了,LVGL 就没多少新东西了。
4.1 一切都是对象:屏幕就是最大的那个父对象
在 LVGL 里,按钮、标签、滑块、图片、甚至一个空白的容器,都是lv_obj_t *,也就是对象。它们共享同一套基础能力:都能设大小、设位置、设样式、加事件、显示或隐藏、删除。你学会了操作一个对象,就等于学会操作全部。
对象之间有父子关系。屏幕是最顶层的父对象,你创建的东西默认挂在屏幕上;你也可以把 A 挂到 B 下面,那 A 就是 B 的子对象。父子关系带来三个直接后果,全是新手必须记住的:
第一,位置是相对的。子对象的坐标是相对父对象左上角算的,父对象一动,子对象整体跟着动。你要是按屏幕坐标去算一个容器里的按钮位置,很容易偏。
第二,删除是连坐的。删掉父对象,它下面所有子对象自动一起删,不用你一个个清理。这既是方便也是坑——你删了一个容器,回头再去操作里面的标签,那就是访问已经释放的内存,程序跑飞。
第三,显示是继承的。父对象隐藏了,子对象再设成显示也看不见。
实际写界面时,这个机制非常有用。比如一个卡片,你用容器当父对象,里面放图标、标题、数值三个标签,整个卡片移动、缩放、隐藏都只需要操作容器一个对象。
4.2 样式与状态:按钮按下去没反应,八成是这两件事
LVGL 的外观靠“样式”控制。样式是一个独立的对象,里面存着背景色、边框、圆角、文字颜色、内边距这些属性。你把它“挂”到控件上,控件就长成那个样子。这样做的目的是复用——一套样式挂到十个按钮上,改一处全变。
样式挂载的时候要指定两个东西:部件部分和状态。
部件部分(part)指的是控件内部的不同零件。比如一个滑块,有主体、有指示条、有旋钮,是三块不同的东西,可以分别设样式。绝大多数入门场景用LV_PART_MAIN就够了。
状态(state)指的是控件当前处于什么情况:默认、按下、选中、禁用、聚焦。你可以只让样式在“按下”时生效,实现点击反馈。代码长这样:
/* 定义一个样式对象,放在函数外,保证生命周期够长 */ static lv_style_t style_pressed; void init_style(void) { /* 初始化样式,然后设属性:按下时背景变成深蓝色 */ lv_style_init(&style_pressed); lv_style_set_bg_color(&style_pressed, lv_color_hex(0x1B4F9C)); lv_style_set_radius(&style_pressed, 8); /* 圆角 8 像素 */ } /* 把样式挂到按钮上,只在“主体 + 按下”这个组合下生效 */ void apply_style(lv_obj_t *btn) { lv_obj_add_style(btn, &style_pressed, LV_PART_MAIN | LV_STATE_PRESSED); }这段代码里最值得注意的是样式对象要放在函数外,或者用static。如果你在函数里定义一个局部样式变量,函数一返回它就没了,控件再去引用就是野指针——现象是界面偶尔花屏或者莫名重启,特别玄学,很难查。这个坑我踩过不止一次。
LV_PART_MAIN | LV_STATE_PRESSED这个写法是把两个枚举按位或起来,意思是“这个样式只作用于主体的按下状态”。你如果只写LV_PART_MAIN,那就是所有状态下都生效,按下就没变化了——这就是“按钮按下去没反应”最常见的原因。
4.3 事件回调:让界面活过来的那根线
到这一步界面还是死的,能看不能点。让它动起来靠事件。LVGL 的事件机制很简单:你告诉某个控件“发生某件事的时候,调用这个函数”,然后事件发生时就有人来喊你。
先看代码:
/* 事件回调:所有事件都进这里,靠参数区分是哪个对象、哪种事件 */ 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, "已点击"); } void bind_event(lv_obj_t *btn) { /* 把回调挂上去,只监听“点击”这一种事件,最后一个参数是用户数据,不需要就传 NULL */ lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, NULL); }lv_event_get_target拿的是“事件发生在谁身上”,这是回调里最常用的一个取值。因为一个回调函数可以挂到多个控件上,靠它来区分。lv_event_get_child和lv_event_get_user_data也常见,前者拿子对象,后者拿你注册时传进去的自定义数据。
事件类型有几十种,入门只需要先记四个:LV_EVENT_CLICKED点击、LV_EVENT_VALUE_CHANGED值变了(滑块、开关用)、LV_EVENT_PRESSED按下、LV_EVENT_RELEASED松开。其余用到再查。
4.4 完整小例子:点一下按钮,文字跟着变
把前面的东西拼起来,就是一个能跑能点的完整例子。这段代码可以直接替换掉模拟器工程里的界面函数:
/* 按钮点击后的处理:把标签文字改掉,并让按钮变个颜色 */ static void on_click(lv_event_t *e) { lv_obj_t *btn = lv_event_get_target(e); lv_obj_t *label = lv_obj_get_child(btn, 0); static int count = 0; /* 记录点了多少次 */ count++; lv_label_set_text_fmt(label, "点了 %d 次", count); /* 带格式化的设文字 */ } void demo_ui(void) { lv_obj_t *btn = lv_btn_create(lv_scr_act()); lv_obj_set_size(btn, 140, 56); lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0); lv_obj_add_event_cb(btn, on_click, LV_EVENT_CLICKED, NULL); lv_obj_t *label = lv_label_create(btn); lv_label_set_text(label, "点我试试"); lv_obj_center(label); }lv_label_set_text_fmt是带格式化输出的版本,用法跟 C 语言的 printf 一致,做数值显示非常方便,做仪表的实时数值就靠它。注意它内部有缓冲区长度限制,字符串太长会被截断,显示长文本用普通的lv_label_set_text更稳妥。
这个例子跑通之后,你已经掌握了 LVGL 开发里 80% 的日常操作:创建对象、设大小、摆位置、设样式、加事件、改内容。剩下的控件都是在这个骨架上换皮。
4.5 再学三个就够做出像样界面:布局、动画、定时器
这三个不是必须,但学了之后界面的观感会明显不一样。
布局解决的是“我不想一个个算坐标”。给父容器设成 flex 布局,子对象会自动排成一行或者一列,还能设间距和对齐:
/* 让屏幕使用 flex 布局,子对象横向排列,居中对齐 */ lv_obj_set_layout(lv_scr_act(), LV_LAYOUT_FLEX); lv_obj_set_flex_flow(lv_scr_act(), LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(lv_scr_act(), LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);用了布局之后,增删控件不用改任何坐标,这在做列表和工具栏时省非常多事。
动画让过渡自然。比如让一个对象从左边滑到右边:
/* 创建一个动画:让 label 的 x 坐标在 500 毫秒内从 0 变到 100 */ lv_anim_t a; lv_anim_init(&a); lv_anim_set_var(&a, label); /* 动画作用的对象 */ lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_obj_set_x); /* 每帧改什么属性 */ lv_anim_set_values(&a, 0, 100); /* 起止值 */ lv_anim_set_time(&a, 500); /* 时长,单位毫秒 */ lv_anim_start(&a);定时器解决“每隔一段时间干一件事”,做时钟、做数据刷新、做轮播图都要用:
/* 每 1000 毫秒执行一次,用来刷新一个进度条的值 */ static void refresh_cb(lv_timer_t *t) { static int32_t v = 0; v = (v + 5) % 101; lv_bar_set_value(ui_bar, v, LV_ANIM_ON); /* 第三个参数表示带动画 */ } lv_timer_create(refresh_cb, 1000, NULL);这三个东西组合起来,做一个仪表盘、一个数据监控页已经完全够用了。
5. 新手避坑:这 5 个地方我第一次全踩了
这一章讲的全是失败现场。每一条我都按“现象、原因、解决”写,你遇到的时候照着对号入座就行。
5.1 坑一:屏幕全白或者花屏,什么都看不到
现象:程序下载进去,屏幕要么纯白,要么一堆彩色噪点,偶尔闪一下。
原因排第一的永远是刷屏回调。要么lv_disp_flush_ready忘了调,LVGL 以为你还在刷,就一直等,界面定格;要么坐标算错,area->x2和area->y2是闭区间,填充函数如果按“长度”来理解就会少一行一列,边缘出现错位;要么颜色格式对不上,你按 16 位 RGB565 配的,实际驱动按 24 位在处理,颜色就全乱了。
解决:先把 LVGL 放一边,写一段最简单的代码,直接往屏幕上刷三种纯色,能正常显示再回来。然后打开日志开关,看 LVGL 有没有报“flush ready 未调用”这类信息。再核对LV_COLOR_DEPTH和你的屏驱动里写的像素格式是否一致,这个必须一模一样。
5.2 坑二:界面卡成 PPT,一滑动就掉帧
现象:静态界面正常,一有动画或者一滑动,明显一卡一卡的,触摸反应也慢半拍。
原因通常是三件事叠加:缓冲区太小、刷新太频繁、主循环里干了重活。缓冲区只给了一两行,LVGL 要刷很多次才能画完一屏;lv_timer_handler调用间隔太长,动画跳帧;或者你在事件回调里做了耗时操作,比如读传感器、算浮点、写 Flash,把整个界面线程堵住了。
解决:缓冲区从 10 行起步,内存够就加到屏幕的十分之一;主循环里lv_timer_handler保持 5 毫秒左右调一次;事件回调里只做“记录状态”,真正的重活放到主循环里根据标志位去做。这条经验值一条后悔药:凡是耗时超过几毫秒的操作,都不要写在回调里。
5.3 坑三:中文显示成方块或者问号
现象:英文数字正常,中文全是方块,或者干脆是问号、乱码。
原因有两个层面。一是字库,LVGL 内置的字体只包含 ASCII 的一部分,中文字符根本不在里面,你必须自己生成一个包含所需汉字的字库,或者用文件系统从外部加载。二是编码,源码文件如果是 GBK 编码,而 LVGL 按 UTF-8 解析,那字节就对不上,显示出来一定是乱码。
解决:先把源文件统一存成 UTF-8,这一步在编辑器里就能改。然后用字体转换工具,把你界面上实际用到的汉字挑出来生成字库,只挑用到的——中文字库很占空间,几千个字随随便便几百 KB,板子 Flash 小的话根本放不下。显示效果不满意再考虑抗锯齿和字号。
5.4 坑四:改了配置文件,编译完一点变化都没有
现象:明明改了lv_conf.h里的内存大小或者功能开关,重新编译下载,行为跟没改一样。
原因基本只有一个:lv_conf.h没被真正包含进去。LVGL 的源码里是通过一个宏来决定去哪找配置文件的,这个宏一般在编译选项里定义,或者在某个总头文件里#define。如果你的工程里同时存在两份配置文件,编译器很可能用了另一份。另一种情况是这个宏压根没定义,LVGL 就退回用默认配置,你的修改全部无效。
解决:在工程里全局搜一下这个宏的名字,看它指向哪个路径,确认那个路径下就是你在改的文件。改完之后做一次全量重新编译,不要只编译改动的文件,有些 IDE 的增量编译对头文件依赖处理得不好。
5.5 坑五:程序莫名卡死,重启才能恢复
现象:界面跑着跑着突然不动了,有时候直接死机重启,复现还不太稳定,特别玄学。
原因最常见的是在中断里调用了 LVGL 的函数。LVGL 不是线程安全的,lv_timer_handler正在改内部数据结构的时候,中断里如果去创建对象或者设文字,数据结构就被改坏了,轻则花屏,重则死机。另一种是野指针:删了某个对象之后,代码里还留着一个指向它的指针继续用。
解决:中断里只做两件事——调lv_tick_inc给时间基准,或者置一个标志位。所有涉及界面的操作,一律放在主循环里做。如果是挂了操作系统的工程,那就更要注意:所有 LVGL 调用集中在同一个任务里,其他任务要改界面,通过消息队列把请求发过来,别直接跨任务调。删对象之后,把对应的指针置成 NULL,用之前先判断。
6. 下一步学什么:一条从“能点亮”到“能做出东西”的路线
把按钮点通之后,很多人会有一段迷茫期:控件太多了,不知道从哪学起。我的建议是按“做一个小项目”倒推着学,不要按 API 列表顺推。
第一个阶段,把常用控件过一遍。标签、按钮、进度条、滑块、开关、下拉框、图表,这七八个覆盖了绝大多数界面。每个控件不要背 API,只要知道它能干什么、大概叫什么名字,用的时候翻指南对应章节。一份好的中文指南会把每个控件单独列一章,从组成讲到 API 再讲到实验,按需查就行。
第二个阶段,做三个小练习。第一个是带按键的计数器,练事件和文字更新;第二个是进度条加定时器,练定时刷新;第三个是两页界面的切换,练容器和对象管理。第三个练习特别关键,因为页面切换会涉及“删掉旧页面、建新页面”,在这个过程中你会真正理解父子关系和对象销毁,这是新手和老手的分水岭。
第三个阶段,接真实硬件。到这一步再去啃移植章节,你会发现轻松很多,因为你知道自己要的是什么。移植的时候按前面说的顺序来:屏能刷纯色、加源码、注册显示、给时间、跑主循环,一步一验证。
第四个阶段,学文件系统和字库。界面上的图片和中文都要靠它们,这也是让界面“看起来像产品”的关键一步。
下面这张表可以当成你的路线图:
| 阶段 | 目标 | 大致投入 |
|---|---|---|
| 一 | 模拟器跑通,改数字能看到变化 | 一到两天 |
| 二 | 常用控件过一遍,能拼出静态页面 | 三到五天 |
| 三 | 三个小练习,事件、定时器、页面切换 | 一周左右 |
| 四 | 硬件移植,界面原样搬到板子上 | 三到五天 |
| 五 | 字库、图片、文件系统,做出完整界面 | 两周以上 |
最后说一个我自己的习惯。刚学那会儿,我总想一次写对,结果一个界面调一整天。后来我改成:每加一个控件,先编译运行看一次效果,确认了再加下一个。从那以后我每次写界面都强制走一遍这个“小步验证”,宁可多按几次编译,也不让问题堆在一起。这个东西没有捷径,但有节奏。
这份 LVGL 开发指南(中文 V1.3)把模拟器、移植、控件三块都铺好了,你不需要再另外找资料拼凑,拿到手直接从第 3 章的模拟器那节开始跑就行。希望帮到你。
本文还有配套的精品资源,点击获取