☰
LVGL嵌入式GUI移植与界面开发实战:从白屏到可交互界面
2026/11/3 6:20:36 网站建设 项目流程

简介:《LVGL开发指南(中文V1.3)》是一份面向嵌入式图形界面开发者的PDF技术文档,主要服务使用STM32与LVGL构建触摸屏UI的初中级工程师及高校学习者。内容围绕LVGL图形库的移植与应用展开,从认识LVGL、源码获取与移植条件讲起,依次覆盖无操作系统移植、基于FreeRTOS的带系统移植、PC模拟器使用及初始化流程等关键环节,并延伸到控件创建、动画交互与性能优化等实践主题,可帮助读者建立从裸机到RTOS环境的完整移植思路,对照例程完成屏幕适配与功能验证。资源包共1个文件,为1份PDF文档,大小约9.54MB,便于在电脑或移动端随时查阅。目前已有766人学习,适合需要系统梳理LVGL移植流程、排查显示与事件处理问题、补足STM32图形界面开发经验的读者参考。

1. 从一块白屏到能点的界面:这份 LVGL 开发指南卡在哪个环节

做嵌入式 GUI 的人大多遇过这个瞬间:手上那块 STM32 开发板已经能点亮屏幕,接着想加两个按钮、一页设置菜单,于是翻开《LVGL开发指南》准备动手,先打开了 LVGL 源码——几百个源文件、上千行lv_conf.h,照着网上帖子改完编译通过,烧进去一片白。问题往往不出在代码能力上,而是缺一份把「移植」和「写界面」这两段路铺平的材料。

这份中文 V1.3 的指南干的正是这件事:前八章全部围绕“怎么让 LVGL 在自己板子上跑起来”,从裸机移植、FreeRTOS 移植、PC 模拟器三条路,讲到lv_conf.h、显示驱动、输入设备、时基、文件系统和字库;后面对接部件篇,把lv_obj、lv_arc、lv_bar、lv_btn这些控件按“组成—属性—API—实验”四段式拆开。适合两类人:刚接触 MCU 图形界面、需要照着步骤点亮第一帧的新手;以及裸机 GUI 已经能跑、准备把界面做规范的熟手。

2. 教程骨架拆解:基础篇与部件篇的分层设计

这份文档不是零散例程的堆砌,它的章节顺序本身就对应一条依赖链:没有显示驱动就没有画面,没有对象树和样式就写不出界面,没有字库和文件系统就上不了中文和图片。看懂这条链,才知道哪一章可以跳、哪一章跳了后面一定卡。

2.1 基础篇与部件篇:先让屏亮,再让屏能看

基础篇(第 1~8 章)解决的是“底层能不能动”的问题。第 1 章给移植要求和源码获取方式,属于热身;第 2、3 章是同一件事的两种形态——无操作系统移植和带操作系统移植,区别只在“谁负责周期性调用lv_timer_handler”;第 4 章把同一套代码搬到 PC 上跑模拟器,省去反复烧录的时间;第 5 章是整份文档里密度最高的一章,把初始化流程、配置文件、显示接口、输入设备、时基逐个拆开;第 6 章开始进入内核对象模型;第 7、8 章补上文件系统和字库这两块“不移植也能跑,但要上产品就必须做”的内容。

部件篇(第 9 章往后)换了节奏,每个控件都按固定四段写:部件的组成、相关知识点、API 函数表、完整实验(硬件设计—软件设计—下载验证)。这种结构看起来重复,实际是留给二次开发的模板——你新增一个自定义控件时,照这个顺序写文档、写代码,别人接手不会骂人。

从这个划分里能提炼出四层结构,后面所有代码都挂在这四层上:

层内容对应章节
硬件抽象层显示刷新、触摸读取、毫秒时基第 2、3、5 章
内核层对象树、样式、事件、定时器、动画第 6 章
资源层文件系统、字体、图片第 7、8 章
部件层与应用层lv_btn、lv_arc、lv_bar等,以及自己拼的页面第 9 章起

读懂这张表的价值在于排查顺序:屏幕不亮先去硬件抽象层找,界面逻辑不对再往内核层找,中文乱码基本跑不出资源层。很多人一上来就在业务代码里翻,翻半天发现是flush_cb少了一句lv_disp_flush_ready。

2.2 三条移植路线:裸机、RTOS、PC 模拟器各管什么

文档给了三条移植路线,新手最容易犯的错是把它们当成“三个版本选一个”,实际上是三种不同用途的工具。

无操作系统移植(第 2 章)是最小可运行集,主循环里裸调lv_timer_handler,适合验证驱动、跑通第一帧、排查硬件问题。它的好处是干扰少,屏幕不亮时不用怀疑任务调度;坏处是界面一复杂,主循环里其他业务就会把刷新周期拉长,出现明显的卡顿。

带操作系统移植(第 3 章)把 LVGL 单独放进一个 FreeRTOS 任务,配一个固定优先级和栈深,靠vTaskDelay让出 CPU。这条路的坑集中在两处:任务栈给不够会在绘图时进 HardFault;多个任务同时操作同一个控件会破坏对象树,常见做法是约定“谁创建谁修改”,或者用消息队列把 UI 操作收敛到一个任务里。

PC 模拟器(第 4 章)是最容易被低估的一条。它的真正价值不是“没板子也能学”,而是把界面开发和硬件调试解耦:布局、样式、动画这些纯软件的事在 PC 上调完,再挪到板子上,剩下的问题必然出在驱动或内存。我一般会要求团队里的界面代码先在模拟器上过一遍,模拟器里都跑不动的逻辑,上板只会更难查。

路线周期调用方式适用场景主要风险
无操作系统主循环while(1)里调用验证驱动、小界面业务阻塞导致刷新变慢
FreeRTOS独立任务 +vTaskDelay多任务产品栈溢出、跨任务改控件
PC 模拟器SDL 窗口事件循环界面设计与调样式与真实屏存在色差和性能差

2.3 章节与产出对照:把文档当成一份可执行的计划

把目录直接翻译成任务清单,比通读一遍再回头找要高效得多。下面是我整理的一份对照,左边是章节,中间是这一章做完应该拿到的东西,右边是验收方式——注意验收标准全部是“能看到什么”,不是“讀完了”。

章节应拿到的产出验收方式
第 1~2 章裸机工程能编译、能烧录屏幕出现官方 demo 的按钮
第 3 章FreeRTOS 下 LVGL 任务稳定运行连续跑 30 分钟不死机、不卡顿
第 4 章PC 模拟器工程本地窗口能拖动滑块
第 5 章自己的lv_port_disp.c/lv_port_indev.c触摸点哪打哪,无偏移
第 6 章一套样式与事件回调按钮点击有反馈、页面可切换
第 7~8 章从 SD 卡读图、显示中文中文不乱码、图片不花屏
第 9 章起按部件逐个复现实验每个控件的实验都能独立跑通

这里有个血泪经验:第 5 章不要跳。很多人第 2 章跑通 demo 就直奔部件篇,觉得配置和驱动“已经能用了”,结果后面一改分辨率、一换屏幕、一加双缓冲就全线崩,回头补第 5 章的成本比一开始看完要高得多。

3. 移植落地:从加文件到点亮第一帧

移植这件事听着玄,落到操作上就是四步:把源码放进工程、把配置改对、把显示和触摸接上、把心跳喂上。每一步都有明确的失败特征,认准特征比背步骤有用。

3.1 工程文件增删与 lv_conf.h 的关键宏

第一步是把 LVGL 源码目录整体加进工程。常见做法是保留src整个目录,把examples/porting下的模板拷出来改成自己的lv_port_disp.c、lv_port_indev.c;lv_conf.h从lv_conf_template.h复制一份改名,放在工程能搜索到的路径下,并在编译器预定义宏里加上LV_CONF_INCLUDE_SIMPLE,让lvgl.h用#include "lv_conf.h"的方式找到它,而不是去改库文件里的相对路径。

配置文件上千行,真正需要动的其实只有几个。下面这段是我一般在移植阶段先确认的部分:

/* lv_conf.h:移植阶段只需要盯住这几行 */ #define LV_COLOR_DEPTH 16 /* 与 LCD 的像素格式一致,RGB565 就是 16 */ #define LV_MEM_CUSTOM 0 /* 0 表示用 LVGL 自带的内存池,不用 malloc */ #define LV_MEM_SIZE (48U * 1024U) /* 自带内存池大小,按 RAM 余量给 */ #define LV_DISP_DEF_REFR_PERIOD 30 /* 默认刷新周期(ms),太高会卡,太低会白烧 CPU */ #define LV_INDEV_DEF_READ_PERIOD 30 /* 触摸扫描周期(ms) */ #define LV_TICK_CUSTOM 0 /* 0 表示自己调 lv_tick_inc,1 表示用系统时基 */ #define LV_USE_LOG 0 /* 调试期可以开 1,正式版关掉省空间 */ #define LV_USE_DEMO_WIDGETS 1 /* 移植验证用,验完记得关 */

LV_COLOR_DEPTH是最容易配错的一项:配成 32 而屏是 RGB565,画面颜色会整片错乱;配成 16 而屏是 RGB888,颜色会发灰。LV_MEM_SIZE给太小,创建十几个控件之后就会在lv_obj_create里返回 NULL,表现为“界面少了一块”,很难往内存上想。LV_TICK_CUSTOM配成 1 又没实现对应的时基接口,动画和定时器会全部停在原地。

提示:lv_conf.h里绝大多数宏第一次都不用碰,先把这几个改对跑通,再按需打开宏。一次性全改,出问题就找不出是哪一行引起的。

3.2 绘制缓冲区与 flush_cb:屏幕刷新的唯一出口

LVGL 不直接操作 LCD,它把要画的内容画进一块内存缓冲区,然后调你注册的回调把这块内存推给屏幕。这个回调就是flush_cb,它是整个显示链路唯一的出口,也是白屏问题的第一嫌疑人。

/* 显示刷新回调:把 LVGL 画好的脏矩形推到 LCD */ 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; /* 本次刷新的高度 */ /* 只刷脏矩形,不是整屏重画;color_p 里是 w*h 个像素 */ lcd_fill_rect(area->x1, area->y1, w, h, (uint16_t *)color_p); /* 少了这一句,LVGL 会一直等这块区域刷完,表现为白屏或局部花屏 */ lv_disp_flush_ready(drv); }

参数里area是这次要刷新的矩形区域,color_p是像素数据首地址,格式由LV_COLOR_DEPTH决定,16 位时可以直接当uint16_t *用。w和h必须用x2 - x1 + 1算,少加这个 1 会出现每行差一个像素的斜纹,属于典型的“看起来像屏坏了”的问题。

注册显示驱动时缓冲区大小要量力而行:

void lv_port_disp_init(void) { static lv_disp_draw_buf_t draw_buf; /* 10 行缓冲:320 * 10 * 2 字节 = 6.4KB,双缓冲再翻一倍 */ static lv_color_t buf_1[MY_DISP_HOR_RES * 10]; /* 第二个参数传 NULL 表示单缓冲,传 buf_2 表示双缓冲 */ lv_disp_draw_buf_init(&draw_buf, buf_1, NULL, MY_DISP_HOR_RES * 10); static lv_disp_drv_t disp_drv; lv_disp_drv_init(&disp_drv); disp_drv.hor_res = MY_DISP_HOR_RES; /* 水平分辨率,必须与屏一致 */ disp_drv.ver_res = MY_DISP_VER_RES; /* 垂直分辨率 */ disp_drv.flush_cb = disp_flush; /* 上面那个回调 */ disp_drv.draw_buf = &draw_buf; lv_disp_drv_register(&disp_drv); }

缓冲区给 10 行还是 40 行,直接影响刷新时的撕裂感和内存占用。行数少,刷新次数多但每次快,小 RAM 芯片用这种;行数多或者上双缓冲,画面更稳但 RAM 吃不消。用 DMA 推屏的话,flush_cb里启动传输后要等传输完成再调lv_disp_flush_ready,提前调用会让 LVGL 在 DMA 还没写完时改写缓冲区,出现随机横条。

3.3 时基与任务处理:主循环里的心跳

LVGL 内部所有和时间相关的东西——动画进度、定时器超时、长按判定——都依赖一个毫秒计数。这个计数要你自己喂,常见做法是在 SysTick 中断里加一句:

/* 1ms 一次的系统节拍,顺手喂给 LVGL */ void SysTick_Handler(void) { HAL_IncTick(); lv_tick_inc(1); /* 参数单位是毫秒,必须与实际中断周期一致 */ }

中断周期如果是 1ms 却写成lv_tick_inc(10),动画会快十倍;反过来写成lv_tick_inc(0),所有动画就永远停在第一帧。这个错很低级,但中断里调个函数容易被忽略,尤其是在别人搭好的工程上改。

心跳的另一半是lv_timer_handler,它负责跑定时器、推进动画、处理输入、触发重绘,必须在主循环里周期性调用:

/* 裸机主循环 */ while (1) { lv_timer_handler(); /* 返回值是建议的下次调用间隔(ms) */ HAL_Delay(5); }

返回值很多人直接丢掉,其实拿它做延时更省 CPU:界面静止时它可能返回几十毫秒,界面在跑动画时返回几毫秒。有操作系统的工程把它放进独立任务:

/* FreeRTOS 下的 LVGL 任务 */ void lvgl_task(void *arg) { lv_init(); lv_port_disp_init(); lv_port_indev_init(); for (;;) { lv_timer_handler(); vTaskDelay(pdMS_TO_TICKS(5)); /* 让出 CPU,5ms 是常用经验值 */ } }

这个任务的栈别按普通任务给。绘图过程里函数调用层次不浅,尤其开了双缓冲和抗锯齿之后,栈给 512 字常常不够。我在 Cortex-M3 上一般从 2KB 起,跑起来看uxTaskGetStackHighWaterMark的余量再收。

3.4 输入设备注册与下载验证

触摸接进来的方式和显示对称:注册一个输入设备,提供一个读取回调,LVGL 自己按LV_INDEV_DEF_READ_PERIOD的周期来问你要坐标。

/* 触摸读取回调:LVGL 周期性调用,问当前有没有按下、按在哪 */ static void touch_read(lv_indev_drv_t *drv, lv_indev_data_t *data) { if (tp_is_pressed()) /* 触摸驱动给出的按下标志 */ { >/* 在屏幕上建一个容器,容器里再放按钮 */ lv_obj_t *cont = lv_obj_create(lv_scr_act()); lv_obj_set_size(cont, 200, 120); lv_obj_align(cont, LV_ALIGN_CENTER, 0, 0); /* 相对屏幕居中 */ lv_obj_t *btn = lv_btn_create(cont); /* 父对象是 cont,不是屏幕 */ lv_obj_set_pos(btn, 10, 10); /* 相对 cont 左上角偏移 */

lv_obj_align的对齐基准是父对象,lv_obj_set_pos的坐标基准也是父对象,混用这两个函数时容易算错。删除父对象会连带删除所有子对象,所以页面切换时删掉根容器就够了,不必逐个清理;但如果删除动作发生在该对象自己的事件回调里,要用lv_obj_del_async延迟到本轮事件处理结束再删,直接删会把正在执行的回调栈打穿。

4.2 样式、部件与状态:一套样式管一类控件

LVGL 的样式分成三层:lv_style_t是样式对象,lv_obj_add_style把样式挂到某个对象的某个部件上,lv_obj_set_style_*做局部覆盖。部件(part)指的是控件的组成部分,比如滑块有主体和指示器,状态(state)指按下、选中等。

/* 定义一套按钮样式,注意必须 static 或全局,局部变量出函数就失效 */ static lv_style_t style_btn; lv_style_init(&style_btn); lv_style_set_bg_color(&style_btn, lv_color_hex(0x1E90FF)); lv_style_set_radius(&style_btn, 8); lv_style_set_text_color(&style_btn, lv_color_white()); lv_obj_t *btn = lv_btn_create(lv_scr_act()); /* 第三个参数指定部件和状态,这里作用于主体、默认态 */ lv_obj_add_style(btn, &style_btn, LV_PART_MAIN | LV_STATE_DEFAULT);

样式对象声明成局部变量是新手最常见的翻车点:函数返回后栈上那块内存被复用,界面表现成颜色随机、文字闪烁,看起来像玄学问题,其实是野指针。局部覆盖用lv_obj_set_style_bg_color(btn, color, LV_PART_MAIN),它的优先级高于add_style,适合做“这一个按钮特殊一点”的微调,不适合大批量设置——每个对象存一份属性,控件一多 RAM 就上去了。

4.3 事件回调与 user_data:一个回调管一排按钮

lv_obj_add_event_cb挂回调时能带一个void *用户数据,这是把“一排按钮共用一个处理函数”的关键。不用给每个按钮写一个回调,也就不会出现几十个几乎一样的函数。

/* 统一的按钮回调,靠 user_data 区分是哪个按钮 */ static void btn_event_cb(lv_event_t *e) { lv_event_code_t code = lv_event_get_code(e); lv_obj_t *btn = lv_event_get_target(e); /* 触发事件的对象 */ uint32_t id = (uint32_t)(uintptr_t)lv_event_get_user_data(e); if (code == LV_EVENT_CLICKED) { switch (id) { case 1: ui_switch_page(PAGE_SETTING); break; case 2: ui_switch_page(PAGE_ABOUT); break; default: break; } } } /* 挂载时把编号塞进 user_data,注意指针宽度转换 */ lv_obj_add_event_cb(btn1, btn_event_cb, LV_EVENT_CLICKED, (void *)(uintptr_t)1);

lv_event_get_target拿到的是触发者本身,lv_event_get_current_target拿到的是挂载回调的那个对象,冒泡事件里两者不同。user_data装整数时一定要经过uintptr_t中转,直接强转指针在 64 位模拟器上会告警。回调里不要做耗时操作,读文件、写 Flash 这类动作丢进队列,让主循环去处理,否则界面会明显卡住。

4.4 定时器与动画:让数值动起来

定时器和动画经常被混用。定时器是“每隔一段时间跑一次函数”,适合周期采集、状态轮询;动画是“在一段时间内把某个值从 A 平滑推到 B”,内部靠lv_timer_handler驱动。

/* 让圆弧从 0 走到 100,耗时 800ms,缓出效果 */ static void arc_value_cb(void *var, int32_t v) { lv_arc_set_value((lv_obj_t *)var, v); } lv_anim_t a; lv_anim_init(&a); lv_anim_set_var(&a, arc); /* 要动画的对象 */ lv_anim_set_exec_cb(&a, arc_value_cb); /* 每帧调一次,把当前值写进对象 */ lv_anim_set_values(&a, 0, 100); /* 起止值 */ lv_anim_set_time(&a, 800); /* 总时长(ms) */ lv_anim_set_path_cb(&a, lv_anim_path_ease_out); /* 先快后慢 */ lv_anim_start(&a);

lv_anim_set_exec_cb的签名固定是void (*)(void *var, int32_t value),写错了编译器不一定报错,表现是动画跑到一半跳变。同一对象上重复lv_anim_start会叠加多条动画,互相抢着改属性,画面会抖;重启动画前先lv_anim_del(arc, arc_value_cb)清掉旧的,这个习惯能省很多排查时间。定时器的用法类似,lv_timer_create(cb, period_ms, user_data),创建后可以用lv_timer_pause暂停而不是删了重建,避免频繁申请释放内存。

4.5 文件系统与字库:资源不占 Flash 的做法

界面里一旦有图片和中文,就要面对资源放哪的问题。全塞进内部 Flash 的 C 数组简单但占空间,做法是把图片和字体放 SD 卡或外部 Flash,通过lv_fs接口按路径读。第一步是注册一个盘符:

/* 注册 'A' 盘,回调里对接自己的 FATFS 读写函数 */ static lv_fs_drv_t fs_drv; lv_fs_drv_init(&fs_drv); fs_drv.letter = 'A'; /* 之后路径写成 "A:/img/logo.bin" */ fs_drv.open_cb = fs_open; /* 打开文件,返回自定义句柄 */ fs_drv.read_cb = fs_read; /* 读数据 */ fs_drv.seek_cb = fs_seek; /* 定位,图片解析依赖它 */ fs_drv.close_cb = fs_close; lv_fs_drv_register(&fs_drv);

四个回调里seek_cb最容易被漏,漏了之后小图能显示、大图直接花屏或者卡死,因为图片解码要来回跳读文件位置。

中文字库的思路是把需要的汉字子集转成 C 数组编进固件,或者同样放进文件系统按需加载:

/* 用转换工具生成的字体文件里已经有这个声明 */ LV_FONT_DECLARE(font_source_han_16); /* 挂到标签上,第四个参数是部件,用 LV_PART_MAIN 即可 */ lv_obj_set_style_text_font(label, &font_source_han_16, LV_PART_MAIN);

源文件必须是 UTF-8 编码,转换工具里也要把要用的汉字加进字符集。最常见的乱码原因不是字体没加载,而是编辑器把文件存成了 GBK,字符串里的字节和字库里对不上,屏幕上就是一串方框。

5. 避坑与排查:移植阶段最容易翻车的五件事

5.1 白屏、花屏、颜色错乱

现象:程序能跑、串口有打印、屏幕全白或者出一条条横向彩纹。

原因基本集中在三处:flush_cb里忘了调lv_disp_flush_ready,LVGL 一直等这块区域完成,后续不再发起刷新;LV_COLOR_DEPTH和 LCD 实际像素格式不一致,16 位配成 32 位会整片偏色;color_p强转成uint16_t *时字节序搞反,红蓝互换。

解决:先在flush_cb里打个断点,确认它被调用了、area的宽高合理;再确认lv_disp_flush_ready在每次回调末尾都执行到(DMA 场景下要在传输完成回调里执行);最后把一张纯红色图片刷到屏幕上,颜色不对就是格式或字节序的问题。

5.2 创建几个控件后返回 NULL

现象:界面刚启动时正常,切两次页面之后新页面的控件少了一半,串口没有报错。

原因:LV_MEM_SIZE给得太小,LVGL 自带内存池耗尽,lv_obj_create返回 NULL,后续设置属性的代码作用在空指针上,某些版本里直接进 HardFault。反复创建删除页面而不清理旧对象,也会加速耗尽。

解决:把LV_MEM_SIZE加大到 48KB 或 64KB 试一次,问题消失就说明是内存;同时确认页面切换时旧根容器真的被删掉了,lv_obj_del之后把保存它的指针置 NULL。调试期可以打开LV_USE_MEM_MONITOR看内存水位。

5.3 触摸点哪打哪变成点哪都偏

现象:按钮要点偏左两厘米才触发,或者横竖坐标反了。

原因:触摸驱动输出的是原始 AD 值没做校准;或者显示做了 90 度旋转,lv_disp_set_rotation生效了但触摸坐标没跟着换算;也有的是把lv_indev_data_t的 x、y 赋值顺序写反了。

解决:先在touch_read里把坐标打到串口,手指点左上角看是不是 (0,0),不是就先做四点校准;显示旋转过的,触摸坐标要么在驱动层换算,要么注册第二个lv_indev时把disp关联到旋转后的显示上,别两边各转一次。

5.4 中文显示成方框

现象:英文数字正常,中文全是一个个小方块。

原因:字库里没有这些汉字,转换工具生成时字符集没选全;或者源文件编码不是 UTF-8;或者字体挂上去了但LV_FONT_...对应的宏在lv_conf.h里没打开。

解决:确认源文件编码;确认转换工具里加入了实际用到的汉字(按项目文案生成子集,别全量导);确认lv_obj_set_style_text_font用的是LV_PART_MAIN且对象确实拿到了这个样式;最后确认编译进固件的字体数组大小和声明一致。

5.5 界面跑一会儿变卡、动画一顿一顿

现象:刚上电很流畅,操作几十秒后明显掉帧,动画像在跳帧。

原因:主循环里lv_timer_handler的调用被业务阻塞,或者HAL_Delay给得太大;每帧都在回调里创建删除对象,内存池碎片化;flush_cb里逐点写屏而不是整块填,或者开了单缓冲还全屏刷新。

解决:先量lv_timer_handler的实际调用间隔,超过 30ms 就要查主循环;回调里只改属性不做资源操作;能整块填就别逐点写,必要时上 DMA;确认缓冲区行数够,别用 1 行缓冲去跑动画。

6. 二次开发:把例程界面改成自己产品界面的三条路子

例程跑通只是起点,真正交付时要把教程里的散装控件组织成产品结构。我的做法是先定一条页面切换的骨架,再往上堆具体界面:

/* 每个页面一个结构体,创建和销毁成对出现,避免内存泄漏 */ typedef struct { const char *name; lv_obj_t *(*create)(lv_obj_t *parent); void (*destroy)(lv_obj_t *root); } ui_page_t; static lv_obj_t *setting_page_create(lv_obj_t *parent) { lv_obj_t *root = lv_obj_create(parent); lv_obj_set_size(root, LV_PCT(100), LV_PCT(100)); /* 具体控件挂在 root 下面 */ return root; } void ui_switch_page(const ui_page_t *page) { static lv_obj_t *cur_root = NULL; if (cur_root) { lv_obj_del(cur_root); /* 删根节点,子对象一起回收 */ cur_root = NULL; } cur_root = page->create(lv_scr_act()); }

改造时有三个方向值得优先做。第一是把样式集中管理,别在每个创建函数里写颜色和圆角,抽一个ui_theme.c存几张全局样式表,换皮肤时只改一处。第二是把业务数据和界面解耦,控件只负责显示,数据变了通过事件或定时器刷进去,不要让传感器采集代码直接操作lv_obj_t。第三是把资源路径统一,图片和字体全部走lv_fs,换素材不动代码。

例程里的写法产品化改造点收益
每个界面一个函数,控件直接建在屏幕上页面结构体 + 统一创建销毁切换不泄漏内存
颜色、圆角写在创建函数里抽主题样式表换皮肤只改一处
采集和显示写在一个回调里数据层与 UI 层分离界面卡顿不影响采集
图片用 C 数组改走lv_fs路径换图不用重编固件

验证方式也别只看“能跑”:切 50 次页面看内存水位是否回落,开一晚上看有没有缓慢变卡,触摸连续点 500 下看有没有漏事件。这套检查现在已经成了我交付前的固定动作。

这类问题的后悔药基本没有,只能靠先留出观察窗口。我现在拿到任何一份带移植章节的教程,都是先把flush_cb、时基、内存池三个位置各加一行日志,再开始改界面,因为界面代码写得再漂亮,底下的心跳是错的,后面查起来就是黑匣子。希望这些踩过的坑,能让你少走一段弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询