ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32上LVGL页面切换与内存优化实战:三种方案与避坑指南

STM32上LVGL页面切换与内存优化实战:三种方案与避坑指南 STM32上跑LVGL做复杂UI页面切来切去几乎是标配需求。我之前做一块基于STM32F407的仪表屏界面有主页、参数设置、曲线显示、校准页加起来七八个页面一开始图省事把每个页面都做成了容器全部挂在一个screen下面用隐藏/显示切换。页面少的时候还好等把图表、进度条、弹出键盘全堆上去RAM先扛不住了切换几次之后整个界面越点越卡最后直接HardFault。后面我把页面切换方案重新梳理了一遍同时把LVGL的内存管理彻底调了一轮才算把这块板子稳下来。这篇文章把我在这个过程中踩过的坑和最终验证过的做法整理出来核心就是三块LVGL页面切换的几种主流方案怎么选、切换过程中内存消耗在哪、以及STM32这种RAM不充裕的平台上如何做内存优化。代码基于LVGL v8/v9的APISTM32部分我会标注具体外设方便你直接移植到自己板子上。1. LVGL页面切换的三种主流方案1.1 方案一整屏加载用lv_scr_load/lv_scr_load_anim切屏第一种方案最直接把每个页面都做成一个独立的screen对象切换时调用lv_scr_load把新的screen加载为当前屏幕。LVGL的screen本来就是顶层对象硬件上整个显示区域都是它的所以这种方案从逻辑上最贴合“页面切换”这个概念。写起来大概是这样的/* 创建新屏幕 */ lv_obj_t *scr_setting lv_obj_create(NULL); lv_obj_set_style_bg_color(scr_setting, lv_color_hex(0xF5F6FA), 0); /* 往里面加控件 */ lv_obj_t *label lv_label_create(scr_setting); lv_label_set_text(label, Settings); lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); /* 切换过去无动画 */ lv_scr_load(scr_setting); /* 带滑动动画切换 */ lv_scr_load_anim(scr_setting, LV_SCR_LOAD_ANIM_MOVE_LEFT, 250, 0, false);注意lv_scr_load_anim的最后一个参数是auto_del如果传trueLVGL在动画结束后会自动把当前屏幕对象删除。我在项目里一般是传false然后在需要的时候手动管理旧screen避免在动画回调里操作对象导致悬空指针。这套方案的优点是思路简单每个页面互相独立切换时旧页面的UI状态可以随对象删除一起释放也可以留着复用。缺点也同样明显如果每次切过去都临时从零创建页面控件那用户每点一次按钮你就要重新创建几十个对象如果创建过程有延迟屏幕会先白一下再出内容观感很差。所以我在使用方案一时实际配合的是“按需创建 页面复用”策略也就是对常用页面保留screen对象不常用页面在使用完后删除。这个后面在内存优化部分详细讲。1.2 方案二容器显隐同一屏幕下切换多个页面第二种方案是很多入门教程里常见的做法创建一个主screen然后在上面放若干个全屏容器每个容器当作一个页面切换时通过LV_OBJ_FLAG_HIDDEN隐藏当前页面、显示目标页面。/* 主屏 */ lv_obj_t *main_scr lv_obj_create(NULL); /* 页面1 */ lv_obj_t *page_home lv_obj_create(main_scr); lv_obj_set_size(page_home, lv_pct(100), lv_pct(100)); lv_obj_clear_flag(page_home, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_bg_color(page_home, lv_color_hex(0xFFFFFF), 0); /* 页面2 */ lv_obj_t *page_setting lv_obj_create(main_scr); lv_obj_set_size(page_setting, lv_pct(100), lv_pct(100)); lv_obj_clear_flag(page_setting, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_bg_color(page_setting, lv_color_hex(0xEEF2F7), 0); /* 切换逻辑 */ void switch_to_page(lv_obj_t *target_page, lv_obj_t *current_page) { lv_obj_add_flag(current_page, LV_OBJ_FLAG_HIDDEN); lv_obj_clear_flag(target_page, LV_OBJ_FLAG_HIDDEN); lv_obj_move_foreground(target_page); }这套方案最大的优点是快。所有页面对象都已经创建好了切换的时候只是修改标志位不需要为每个页面重复执行创建函数代码里的逻辑也很好懂。但坑也在这里。页面常驻意味着内存常驻一个主页上放几个图表、几个带样式的容器RAM占用立刻上去。我最初的项目就是因为所有页面都常驻内存静态内存池被塞满程序跑了几分钟后开始随机崩溃。而且页面多了以后LVGL在遍历对象树的时候也要消耗更多CPU时间即使页面被隐藏控件的事件处理和样式刷新计算仍在LVGL内部逻辑里占开销。所以方案二更适合页面数量少、单个页面控件不复杂、而且页面之间需要频繁切换且需要保留状态的场景。比如一个只有两三个状态页的醒目重开关。1.3 方案三tabview和tileview用LVGL自带组件做页切换第三种是用LVGL内置的lv_tabview或lv_tileview来管理页面。这两个组件本身就是为“多页面切换”设计的。lv_tabview在顶部生成一组选项卡点击Tab切换下面的内容页lv_tileview则类似手机桌面可以左右/上下滑动切换整屏页面。/* TabView */ lv_obj_t *tabview lv_tabview_create(main_scr, LV_DIR_TOP, 48); lv_obj_t *tab_home lv_tabview_add_tab(tabview, Home); lv_obj_t *tab_setting lv_tabview_add_tab(tabview, Setting); /* 在tab_home里加内容 */ lv_obj_t *label_home lv_label_create(tab_home); lv_label_set_text(label_home, Home Page);/* TileView */ lv_obj_t *tileview lv_tileview_create(main_scr); lv_obj_t *tile1 lv_tileview_add_tile(tileview, 0, 0, LV_DIR_RIGHT); lv_obj_t *tile2 lv_tileview_add_tile(tileview, 1, 0, LV_DIR_LEFT);这个方案的优势是代码量最少用户体验也符合习惯尤其是tileview天然支持滑动手势做多页引导页、联机调试页很顺手。缺点也很明显样式受组件结构限制想做成完全自定义的过渡动画或者不规则布局会比较麻烦而且tabview的各Tab页实际上是常驻创建的跟方案二一样存在内存占用偏大的问题。从我的实际体验看lv_tileview特别适合左右滑动切换的页面比如设备向导、分步设置流程lv_tabview适合设置页这类按模块分的界面。如果你的页面切换逻辑完全由自定义按钮控制那用tabview反而别扭。1.4 三种方案怎么选我在不同项目里分别用过这几种方案总结出的选择标准其实很朴素考虑因素整屏加载容器显隐tab/tileview页面数量多适合不适合一般页面切换频率高看是否复用适合适合需要保留页面状态需手动保留天然保留天然保留自定义动画/交互灵活灵活受限RAM占用控制最可控较差较差代码量中等少最少如果页面不超过5个且内容简单选容器显隐最省事。如果页面数量多或者单页内容复杂优先考虑整屏加载配合按需创建。如果交互上能接受Tab或滑动形式直接用tileview/tabview是最稳的。2. 页面切换时内存到底花在哪了2.1 对象生命周期与内存分配很多人对LVGL内存的理解停留在“控件对象占用ram”这一层但实际页面切换时最容易被忽略的是对象的创建和销毁过程。LVGL里每个控件都是一个lv_obj_t结构体它内部包含了子对象链表、布局参数、样式列表、事件回调等这些数据全部来自LVGL内部的内存分配器。当你新建一个screen或者在一个容器里添加十几个控件LVGL会持续向内存池申请小块内存。比如一个lv_label_create表面上看只是创建一个标签但背后可能涉及显示文本、样式、位置信息等至少4到5次内存申请。页面创建花的时间很大程度就花在这堆内存分配上。当你删除一个页面时LVGL会递归释放这个对象及其所有子对象的内存。这个过程如果频繁发生内存池里就会被“切割”得七零八落产生大量碎片。在STM32的RAM里碎片化会让“剩余总内存够用但分配不到连续内存”的情况反复出现最终表现为某次创建控件时直接触发断言甚至死机。2.2 隐藏不等于免费我在用容器显隐方案时犯过一个典型错误以为页面被LV_OBJ_FLAG_HIDDEN隐藏后就不再占用资源。实际隐藏标记只是让LVGL不绘制这个对象对象本身和它子对象的RAM占用一个字节都不会少。更隐蔽的是隐藏对象样式里如果有动画、渐变、opacity这种效果LVGL在渲染时依然需要对它做遍历处理在低主频STM32上这会导致页面切换点击后有可感知的延迟。所以“隐藏页面”省的是绘制时间和切换逻辑省不了内存。如果你做的是RAM只有64KB甚至32KB的设备页面一多容器显隐方案会非常吃力。2.3 碎片化是隐藏的杀手页面切换的常见崩溃路径不是“内存不够”而是“内存碎片”。比如你连续多次进入设置页、退出设置页每次进入都重新创建一批控件退出时删除内存池内部就会出现很多小的空闲块。它们单个都不大但加起来还有几KB导致后续某个页面需要一块比较大的连续内存时分配失败。LVGL在内存分配失败时会调用LV_ASSERT_MEM常见的现象就是程序切到某个页面时卡死在断言处或者直接进HardFault。最麻烦的是这个问题不是必然出现的它和之前的操作路径强相关。你要复现就必须反复切换页面而且往往是在你测试了十几分钟后才突然出现。所以我在做页面切换时一直强调两条内存策略要么频繁创建删除但定期对页面做复用要么干脆让页面对象常驻但人为控制页面数量。这两条是一枚硬币的两面。3. STM32上的内存优化实操3.1 先看清楚LVGL的内存池配置LVGL的内存配置集中在lv_conf.h。核心是用自己的内存分配器还是C库的malloc/free这个由LV_MEM_CUSTOM控制。/* 使用LVGL内置内存分配器 */ #define LV_MEM_CUSTOM 0 /* 内置内存池大小 */ #define LV_MEM_SIZE (48U * 1024U)当LV_MEM_CUSTOM为0时LVGL会在内部维护一个大数组作为内存池所有控件对象都在这个池里分配。我建议在STM32工程里都用这种方式不要用LV_MEM_CUSTOM 1直接走后端malloc因为内置分配器可以配合lv_mem_monitor随时查看内存使用情况这对定位问题太重要了。LV_MEM_SIZE设多少没有标准答案需要结合屏幕分辨率、页面复杂度和芯片RAM决定。我常用的起点是如果你的MCU总RAM有128KB先给LVGL分64KB跑起来后用内存监控工具看实际占用再慢慢调小如果RAM小而屏幕大优先缩减显示缓冲区的尺寸而不是裁剪内存池。3.2 工具先上用lv_mem_monitor盯住内存当你怀疑页面切换导致内存泄漏或者碎片化时不要盲猜直接把LVGL的内部内存状态打印出来。在代码里周期性调用lv_mem_monitor_t mon; lv_mem_monitor(mon); LV_LOG_USER(total%d free%d big%d frag%d%%, mon.total_size, mon.free_size, mon.free_biggest_size, mon.frag_pct);我最关心三个字段free_size是剩余空闲总量free_biggest_size是最大能分配连续块frag_pct是碎片率。如果frag_pct持续上升说明你的页面切换逻辑里存在大量频繁创建删除对象的情况就需要优化了。在实际调试时我会把这个信息通过串口打印出来然后在UI上来回切换页面每次切换后打印一次。正常情况下free_size波动应该是有规律地在一两个值之间跳动如果它一路下降不回头说明有对象没有被释放是泄漏如果free_size稳定但free_biggest_size越来越小那就是碎片化严重。3.3 页面切换的两种内存策略按需创建与对象复用针对页面切换的内存优化我总结两种可以落地的模式。按需创建模式适合内容重、页面切换不频繁的场景static lv_obj_t *scr2 NULL; void open_scr2(void) { if (scr2 NULL) { scr2 lv_obj_create(NULL); lv_obj_set_style_bg_color(scr2, lv_color_hex(0xFFFFFF), 0); /* 创建该页所有控件 */ } lv_scr_load(scr2); } void close_scr2(void) { if (scr2 ! NULL) { lv_obj_del(scr2); scr2 NULL; lv_scr_load(main_scr); } }对象复用模式适合页面切换频繁、状态需要保留的场景static lv_obj_t *scr2 NULL; lvb_obj_t *get_scr2(void) { if (scr2 NULL) { scr2 lv_obj_create(NULL); /* 创建控件 */ } return scr2; }核心区别在于一个用完就删一个只建一次。前者内存峰值低但切换成本高后者切换快但长期占用内存。我在实际项目里会混合用主页这种一直需要的常驻二级设置页按需创建返回时删除。另外要强调删除对象后保存该对象的指针变量务必备份必须置为NULL。否则下次切换时会访问到已经释放的悬空指针这几乎是我遇到过最多的一类崩溃。3.4 显示缓冲区、动画与图片的进一步优化除了控件对象占用的内存LVGL另一个内存大户是显示缓冲区。在STM32上液晶屏的刷新依赖LVGL把渲染好的像素数据交给驱动接口这个过程中间的缓冲数组就是用RAM换速度。常见的配置是#define LV_HOR_RES_MAX 480 #define LV_VER_RES_MAX 272 #define LV_COLOR_DEPTH 16 static lv_disp_draw_buf_t draw_buf; static lv_color_t buf1[LV_HOR_RES_MAX * LV_VER_RES_MAX / 10]; static lv_color_t buf2[LV_HOR_RES_MAX * LV_VER_RES_MAX / 10]; lv_disp_draw_buf_init(draw_buf, buf1, buf2, LV_HOR_RES_MAX * LV_VER_RES_MAX / 10);LV_COLOR_DEPTH 16也就是RGB565时一个像素2字节。480x272分辨率的完整一屏就是480x272x2约261KB如果在单片机里直接放两个全屏缓冲区大部分STM32直接放弃。所以一般是分成若干行缓冲区比如每次只刷十几行缓冲区大小控制在一屏的1/10到1/20。我做页面切换动画的时候还有个心得lv_scr_load_anim在播放动画时需要额外的工作内存来保存中间过渡帧。如果内存吃紧建议把动画时间缩短到200ms以内或者干脆不用动画直接lv_scr_load。在资源紧张的方案里“无动画直接切换”往往是最稳的。图片资源也要注意。LVGL加载图片时如果图片是RGB565的C数组它会直接引用数组所在的内存。如果数组定义在STM32的RAM里那图片就等于白白吃掉一块RAM。正确的做法是定义成const让它待在Flash里const lv_img_dsc_t img_logo { .header.cf LV_IMG_CF_TRUE_COLOR, .header.w 80, .header.h 80, .data_size 80 * 80 * 2, .data logo_data, // 这个数组是const的 };页面切换时图片本身没有额外内存复制但如果图片数据放在RAM里每次切到图片页RAM占用就快速上涨。只要把大数组转成const内存压力会立刻小很多。3.5 和FreeRTOS配合时的注意点如果项目里同时用了FreeRTOS和LVGL我一般建议单独开一个任务跑LVGL并且把lv_timer_handler放在任务的循环里。void lvgl_task(void *arg) { while (1) { lv_timer_handler(); vTaskDelay(5); } }这个任务栈大小不能省。LVGL的控件创建函数、动画回调都跑在这个任务的栈上栈太小会直接溢出页面切换时尤为明显。我一般给它分配1024到2048字节的栈空间具体取决于页面创建时有没有局部大数组。同时FreeRTOS的heap和LVGL的LV_MEM_SIZE是两套内存。不要以为FreeRTOS堆够大LVGL就一定能用LVGL默认只从自己LV_MEM_SIZE定义的内存池里分配。如果在两个组件之间出现内存互相“借”的情况多半是你把LV_MEM_CUSTOM设成1并接上了FreeRTOS的malloc这也可以但排查问题会更绕我建议前期先用LVGL内置分配器。4. 真机调试中的典型问题和排查记录4.1 一切换就HardFault / 马上死机页面切换触发HardFault绝大部分是指针问题。常见原因无非三种访问了已删除的对象、回调里删除了当前正在处理消息的对象、以及把lv_obj_create(NULL)创建出来的screen错误挂到了别的screen下面。排查思路很土但有效先关掉动画用lv_scr_load直接切换如果不再死机问题多半出在动画回调或动画期间的对象生命周期再检查所有删对象的地方删完有没有置NULL。我习惯在每次切页函数入口和出口加上串口日志打印当前要操作的对象地址崩了之后回看日志基本能定位到是哪一个页面指针出了问题。另外LVGL里有lv_obj_del_async这个接口如果你确定某个对象需要在事件回调里删除自己或者兄弟对象直接用这个它会等到当前事件处理完再执行删除能避开大部分回调中的删除陷阱。4.2 页面切换花屏、残影花屏和残影一般不是页面切换逻辑的问题而是显示缓冲区和flush函数配合不好。最常见的原因是flush中断和LVGL刷新动作不同步在数据还没完全传完时又在同一块buffer上写入新数据导致上一帧内容被撕开。解决办法首先保证flush回调里一定要正确调用lv_disp_flush_ready(disp)否则LVGL不知道缓冲区已经空了就不会继续渲染下一块。其次确认用的是双缓冲还是单缓冲双缓冲能减少撕裂但RAM占用更高单缓冲下发生残影优先检查驱动DMA传输是否被意外打断。如果只在动画切换时有残影可以尝试把动画时间调长一点或者把显示缓冲区增大一点。动画期间LVGL需要把新旧两帧都处理进来缓冲区太小会产生绘制不完整的效果。4.3 内存碎片导致越切越卡这个现象很有迷惑性刚开始设备一切正常反复切页二十分钟后页面打开变慢甚至在某一次切换时卡住。操作流程是把lv_mem_monitor的数据通过串口输出连续切换页面二十分钟观察frag_pct。如果碎片率从个位数涨到二十甚至三十基本可以断定是频繁删建控件造成的。解决办法有两个方向。一个是把页面改成复用模式同一类页面只创建一次之后切换只做隐藏/显示另一个是如果必须每次创建那么页面里的控件数量要克制尤其不要频繁创建lv_style_t或者动态设置局部样式。我在项目里把样式全部定义为静态/全局对象之后碎片率从28%降到了个位数效果相当明显。4.4 一个比较省心的组合做了这一堆优化之后我目前最常用的组合是主页用一个常驻screen二级页面用独立screen按需创建三级弹窗全部用容器显隐来实现。主页和二级页之间用无动画的lv_scr_load切换容器的显示隐藏用于弹窗/下拉面板。这套组合在STM32F407 480x272屏幕上跑得比较稳内存池64KB显示缓冲区约25行所有大图放Flash整体RAM余量始终保持在10%以上。这个组合不是绝对的但它的思路值得参考核心页面常驻保证切换速度次要页面按需分配控制峰值内存不常用的临时模块用轻量容器管理让每种方案只处理自己最擅长的场景。我在页面切换这件事上折腾最久、也最建议你重视的一点是不要等设备跑崩了才去查内存。LVGL的页面切换问题概率性很强最有效的办法就是一开始就把内存监控打出来、把页面的生命周期设计清楚然后尽早让页面在真机上反复切换测试。你提前模拟的每一次切换都是在帮以后的自己少踩一个“切十几分钟突然死机”的坑。
返回列表