资讯动态

LVGL v8到v9迁移实战:ESP32-S3显示驱动与配置系统解析

发布时间:2026/9/27 1:27:20 来源:尧图企业网站定制
LVGL v9发布之后我身边很多做ESP32项目的朋友都来问同一个问题手里的v8工程到底要不要升我的回答通常是——如果你没打算长期维护这个项目那可以停在v8v8不会因为你没升级就坏掉但如果你还要在上面持续加功能那越晚升越痛苦。v9这代版本不是普通的小版本迭代配置系统、显示驱动模型、绘制管线和API命名全都动了大手术。今天就把我在ESP32-S3上把一个实际运行的v8界面工程迁到v9的全过程拆开来讲包括迁移策略、那些编译过了但行为变了的坑以及怎么在设计上给下一次大版本升级提前铺路。这次以一块常见的240x320 ST7789屏幕、温湿度采集显示界面为例整个过程是完全可以照着复现的。1. 迁移前先摸清底牌v8与v9在ESP32上的实质性差异1.1 配置系统大重构lv_conf.h不再是唯一入口v8时代lv_conf.h是唯一的配置入口。所有宏不管三七二十一都堆在里面LV_COLOR_DEPTH、LV_MEM_SIZE、LV_USE_XXX……改一个参数就得重新编译整个库。v9把配置拆成了两部分核心的lv_conf.h仍然存在但很多与平台、驱动、第三方库相关的配置被扔进了组件级配置里。如果你用ESP-IDF的标准组件方式拉取lvgl会发现v9可以直接走menuconfig配置文件生成在build目录下的lv_conf.h里自己再放一个同名文件很容易出现两个配置谁生效的困惑。另一个明显的变动是LV_COLOR_DEPTH没了取而代之的是LV_COLOR_FORMAT。这不是简单的改名字而是从位深到格式的思维转变。v8你写LV_COLOR_DEPTH 16库内部默认它是RGB565v9里可以显式声明LV_COLOR_FORMAT_RGB565、LV_COLOR_FORMAT_ARGB8888等等意味着同一块屏可以支持更多颜色布局不需要在显示驱动里写死。对ESP32来说这个灵活性的代价是flush回调里对buffer格式的处理必须写对否则会出现颜色错乱或者刷新闪屏。1.2 显示设备与输入设备从驱动结构体到对象化创建v8里要驱动一块屏流程是初始化lv_disp_draw_buf_t、初始化lv_disp_drv_t、注册flush回调最后lv_disp_drv_register拿到lv_disp_t。这套流程本身很顺手但它的设计是一个驱动一个显示设备要支持双屏就得复制两套结构体冗余感很强。v9把显示设备直接变成了LVGL对象树里的lv_display_t创建方式类似创建一个控件lv_display_create()之后用setter函数往里面填回调、buffer和分辨率。输入设备也被同样对象化lv_indev_create() lv_indev_set_read_cb()不再是结构体里面填函数指针。这种对象化改造对ESP32的实时意义是切换屏幕、插拔显示器这种操作在代码里变成了一等公民而不是靠全局变量硬切。不过从v8迁移过来的人第一反应往往是——我在v8里写了一大坨disp_drv的初始化代码现在找不着北了。这是正常的思路要从配置一个结构体切换成创建一个对象并配置它。1.3 绘制管线与颜色格式看不见的底层变化v9的绘制管线是重写的。v8时期的draw_ctx、lv_draw_rect等接口批量退役取而代之的是display级别显式管理的draw buffer以及按需创建的lv_draw_buf_t。对大多数ESP32应用来说你不需要深入绘制管线内部但要知道三个影响第一flush回调里获取buffer的方式变了老的draw_buf-buf1这种直接访问基本失效第二缓冲区渲染模式的名字从LV_DISP_RENDER_MODE_PARTIAL等变成了LV_DISPLAY_RENDER_MODE_PARTIAL前缀从DISP变成了DISPLAY第三如果以前开了GPU加速相关的第三方渲染器迁移到v9要先全部关闭等新版适配方案稳定后再开。1.4 一个速查表高频API的v8/v9对照下面这个表是我在迁移时反复对着改的列在这里就当给同样在搬代码的同学省点时间。以我在项目里改到的高频API为标准不代表全部变化功能点v8写法v9新写法获取当前屏幕lv_scr_act()lv_screen_active()注册显示设备lv_disp_drv_registerlv_display_create设置flush回调disp_drv.flush_cb cblv_display_set_flush_cb(disp, cb)获取水平分辨率lv_disp_get_hor_reslv_display_get_horizontal_resolution切换屏幕显示lv_disp_load_scrlv_screen_load动画时长设置lv_anim_set_timelv_anim_set_duration清除对象标记lv_obj_clear_flaglv_obj_remove_flag图片控件lv_img_create / lv_img_set_srclv_image_create / lv_image_set_src刷新完成通知lv_disp_flush_readylv_display_flush_ready这张表只是开胃菜。真正的难度在于配置、驱动逻辑、以及那些名字没变但语义变了的细节后面我会一个一个讲。2. 迁移前的资源账ESP32上跑v9的内存与性能预算2.1 一张屏的帧缓冲到底吃掉多少内存先算笔账。假设常见的320x240 LCDRGB565格式一行大小是3202640字节双缓冲行缓冲只要1280字节SRAM完全无压力。但很多初学者贪心把缓冲开成全屏3202402153600字节约150KB单buffer双缓冲直接300KB。ESP32经典款SRAM总共520KB左右还要跑FreeRTOS、协议栈和业务代码全屏双缓冲基本不可能。v9如果再把颜色格式换成ARGB8888一块全屏buffer就是240320*4307200字节300KB直接没影了。所以我给迁移的第一个建议v8用什么buffer策略v9刚迁移时也先用什么策略。我自己的项目在v8时期是一行双缓冲partial模式v9延续这个策略性能数据几乎不变。别一上来就为了新版本渲染更快去扩大buffer那是第二步才做的事情。2.2 PSRAM不是万能药从DMA、带宽到稳定性如果你的板子有SPI RAM可以把draw buffer分配在PSRAM用heap_caps_malloc(buf_size, MALLOC_CAP_SPIRAM)。但注意ESP32的DMA访问PSRAM在不同IDF版本上行为有差异有些情况下SPI外设直接读PSRAM数据会掉帧或者花屏。保险做法是用SPI接口的屏幕时维护一个小型SRAM行缓冲做DMA源再由flush回调从PSRAM的完整画面区域拷贝。虽然多一次copy但稳定性好很多。这个结论同样适用于v8只是v9改颜色格式后buffer变大更容易碰到这个坑。另外一个容易忽视的点是PSRAM分配失败。v9的draw buffer分配逻辑里如果你用LV_USE_OS开启了系统内存管理buffer会走LVGL自带的内存池而不是直接走malloc。迁移时先确认你的buffer是用lv_display_set_buffers传入的还是让LVGL内部自己分配的这两种方式在PSRAM上的分配路径完全不同搞错了会反复在编译过了但刷新花屏的问题上打转。2.3 组件管理方式决定迁移成本v9对ESP-IDF有最低版本要求我建议直接用当前较新的IDF 5.x而不是停在4.x。另一个关键选择是用IDF组件管理器拉LVGL而不是把源码用git submodule塞进工程。原因很实际在idf_component.yml里声明依赖lvgl/lvgl: ^9.2升级只改一行版本号而且官方组件和ESP-IDF的编译流程耦合得比较好很多配置项可以走Kconfig。submodule方式也不是不行但每次同步上游、处理本地改动和配置合并都会多花时间。用组件管理器还有一个好处依赖关系是声明式的LVGL v9依赖的其他小库比如lv_drivers相关封装会自动解析不用你手动去GitHub上碰运气找对应版本。迁移到一半最怕的就是从某个fork拉下来的LVGL带了一堆私有改动换到官方组件后所有差异都收口到版本号的semver范围内调试时脑子不会乱。3. 手上有个v8项目我是这么一步步迁到v9的3.1 用v9模板重建lv_conf.h而不是手动改旧文件千万不要手改旧文件。把v9源码包自带的lv_conf.h模板拷出来在lvgl目录下先让它在默认配置下编译通过再逐项打开你需要的功能。这样能确保你不是在沿用旧选项列表。我对比了两个版本的配置差异列出我实际遇到的几个映射功能v8配置v9配置颜色深度/格式LV_COLOR_DEPTH 16LV_COLOR_FORMAT LV_COLOR_FORMAT_RGB565内存管理LV_MEM_SIZE LV_MEM_CUSTOM配合LV_USE_OS交给系统堆或LVGL内存池系统心跳LV_TICK_CUSTOM 1保留LV_TICK_CUSTOM第三方渲染器LV_USE_GPU_*系列v9先全关用LV_DRAW_SW_DRAW_UNITS输入设备相关LV_INDEV_READ_PERIOD等配置项重命名需逐个核对第一次用新模板编译时默认配置下什么功能都不开能编译过说明基础环境没问题然后再把label、btn、img、anim这些模块逐个打开。这个过程有点像把一栋房子拆了重建但好处是一旦一开始就编译过后面加功能时报错范围会小很多。3.2 显示驱动代码改写示例v8的显示驱动初始化长这样// v8 static lv_disp_draw_buf_t draw_buf; static lv_disp_drv_t disp_drv; static lv_color_t buf1[320 * 10]; static lv_color_t buf2[320 * 10]; lv_disp_draw_buf_init(draw_buf, buf1, buf2, 320 * 10); lv_disp_drv_init(disp_drv); disp_drv.hor_res 240; disp_drv.ver_res 320; disp_drv.flush_cb my_flush_cb; disp_drv.buffer draw_buf; lv_disp_drv_register(disp_drv);v9变成了对象创建方式// v9 static lv_display_t *disp; static lv_color_t buf1[320 * 10]; static lv_color_t buf2[320 * 10]; disp lv_display_create(240, 320); lv_display_set_flush_cb(disp, my_flush_cb); lv_display_set_buffers(disp, buf1, buf2, 320 * 10, LV_DISPLAY_RENDER_MODE_PARTIAL);flush回调本身也要改重点是最后一行// v8 void my_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { // 发送数据到屏幕 lv_disp_flush_ready(disp_drv); } // v9 void my_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *color_p) { // 发送数据到屏幕 lv_display_flush_ready(disp); }漏改flush_ready这一行的表现是第一帧出来后就不再刷新而且你不容易从报错里找到原因因为编译完全不报错。我在迁移时就在这个坑里卡了半小时最后是翻日志发现每帧都在等flush complete超时才意识到是回调通知没送出去。3.3 触摸驱动和事件注册的迁移输入设备在v8里是这样的// v8 lv_indev_drv_t indev_drv; lv_indev_drv_init(indev_drv); indev_drv.type LV_INDEV_TYPE_POINTER; indev_drv.read_cb my_touchpad_read_cb; lv_indev_drv_register(indev_drv);v9改成// v9 lv_indev_t *indev; indev lv_indev_create(); lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER); lv_indev_set_read_cb(indev, my_touchpad_read_cb);除了注册方式read_cb的签名也变了。v8的read_cb里传lv_indev_drv_t指针v9传lv_indev_t指针。如果你的触摸代码里有坐标范围转换、翻转之类的处理这部分逻辑本身不用动但函数声明务必换成新签名否则编译时会有warning运行时不一定会崩但某些IDF版本下可能直接hardfault。3.4 全局批量替换后必须手动复查的地方我可以提供一个我实际用过的批量替换脚本先处理高频API改名sed -i s/lv_scr_act()/lv_screen_active()/g; s/lv_disp_load_scr/lv_screen_load/g; s/lv_obj_clear_flag/lv_obj_remove_flag/g; s/lv_anim_set_time/lv_anim_set_duration/g src/main/*.c但脚本替换完不能直接收工有几类变化必须手工复查。首先是lv_img改成lv_image这种如果直接在全局把lv_img替换成lv_image会把lv_image_create、lv_image_set_src等函数名和lv_image_t类型一起误替换结果产生一连串编译错误。我的做法是先只替换函数调用比如lv_img_create - lv_image_create、lv_img_set_src - lv_image_set_src类型层面的lv_img_t再单独处理。其次是事件判断凡是if(evt-code LV_EVENT_CLICKED)这种要逐个确认短按、长按的语义是否还符合需求。第三是布局赋值代码例如百分比、LV_PCT、LV_SIZE_CONTENT的表达式v9对布局时机的处理更严格这些代码看起来没变但行为可能变了。4. 编译通过不等于万事大吉v9里那些不报错但行为彻底变了的地方4.1 默认视觉风格和布局行为变化v9对很多控件的默认样式做了统一调整最直观的是圆角、间距和阴影。v8里按钮可能是一个规规矩矩的直角矩形到v9默认带了一点圆角和更明显的边框。如果你的UI对像素级细节有要求迁移后会明显感觉界面变胖了。这不算bug但值得提前知道免得以为是自己配色配错了。更隐蔽的是布局行为差异。v8里通过obj_set_height(obj, LV_SIZE_CONTENT)让label自适应高度v9依然支持LV_SIZE_CONTENT但在某些flex布局组合下子控件的尺寸计算时机更严格导致界面出现第一次显示正常数据更新后位置错乱的现象。解决办法是一致使用lv_obj_update_layout()刷新并且不要在事件回调里直接去读依赖布局的size要等一个布局更新事件时机再处理。4.2 图片控件改名后资源数组也跟着变v9把lv_img整套改名为lv_image。注意这个改动非常容易踩坑lv_img_create是函数lv_img是类型。全局搜索替换时如果直接s/lv_img/lv_image/会把lv_image_create这种函数名和类型名一起替换结果造成重复定义或遗留lv_img类型。资源数组声明也有变化v8里常见的const lv_img_dsc_t改成了lv_image_dsc_t如果你在代码里直接引用这两个类型必须跟随改名。图像显示方面还有一个坑v9默认对图像解码的流程做了调整某些资源如果不重新转换可能出现图标显示不完整或者图像偏移。这不是显示驱动的问题而是图像描述符里的header格式不兼容。迁移时直接用新版转换工具把图片重新生成一遍顺手把代码里的lv_image_set_src路径核对一边能省下后面大量的排查时间。4.3 字体文件格式不兼容中文显示最容易踩雷用字体转换工具生成的字体文件v8和v9格式不兼容。在v8工程里一直正常显示的中文字库搬到v9后可能出现编译期数组尺寸告警或者直接乱码。因为v9的lv_font_t内部由get_glyph_dsc回调驱动和v8的静态字典结构完全不同。图像资源同理C数组格式也有变动。所以迁移时不要沿用旧的资源文件用新版工具重新转换一遍。这个步骤最枯燥但也是必须提前规划的一步不然界面加载完只有英文中文全变方框会打击信心。我在迁移时用的是默认字体先编译过然后才重新转换中文字体。这里给个建议中文字体文件一般都比较占空间转换时把不要用的字符集范围去掉v9对字体子集化的支持比v8更友好生成的数组更小加载也快。4.4 动画、定时器与事件语义的微妙变化v9对点击事件做了更细的拆分短按和长按的语义更强。以前代码里用LV_EVENT_CLICKED判断点击v9里这个判断仍然触发但如果你需要区分短按和长按官方推荐改用LV_EVENT_SHORT_CLICKED、LV_EVENT_LONG_PRESSED。如果你的代码原本用LV_EVENT_RELEASED做点击处理在v9里它仍然触发但配合indev滚动时行为会有些微妙差异。我迁移时遇到过列表项无法点击的怪现象排查后才发现事件码常量值变了编译不报错行为完全不同。动画方面lv_anim_set_time改名为lv_anim_set_duration只是表面的更实质的是动画回调的时间基准和v8不完全一样。v8里某些动画回调可能在动画帧之间被连续调用v9对回调的触发时机做了更严格的调度如果你的动画回调里依赖每次调用都推进一帧的逻辑就可能出现动画变快或者变慢的问题。迁移后要专门跑一遍所有带动画的界面确认速度感没有肉眼可见的变化。定时器也一样。v8和v9都要求周期调用lv_timer_handler()但v9对定时器的优先级和执行窗口更敏感。我在FreeRTOS任务里跑lv_timer_handler任务优先级给低了会出现动画掉帧给高了又会影响触摸采样。这个坑在v8时代虽然也存在但v9因为绘制管线更复杂表现更明显。调优先级时建议用触摸优先、动画其次、UI刷新再次的顺序实测下来最稳。5. 面向未来怎么把代码设计成不太怕LVGL大版本更新5.1 三层架构驱动层、桥接层、页面层我现在的项目代码分层非常明确。底层是bsp/platfrom层只负责屏幕初始化、背光控制、触摸读取完全不认识LVGL。中间是ui_bridge层封装所有业务需要调用的UI方法比如ui_show_temperature(25.6)、ui_show_alert(LEVEL_WARN)、ui_set_progress(80)。这层是唯一允许直接使用LVGL API的公共入口。最上层是page层和业务逻辑业务代码调用ui_bridge层的接口绝不直接碰lv_label_*这些函数。这套结构的好处是LVGL升级时需要改动的范围被限制在page层和ui_bridge的实现文件里业务逻辑、协议、数据采集完全不受影响。桥接层看起来多写了一点代码但它换来的是升级时不带脑子也不会改崩业务的安全感。我现在的做法是给ui_bridge层定义统一头文件接口全部以uint8_t、const char*这类基础类型作为参数这样LVGL版本变化再大接口签名也能保持稳定。5.2 用版本宏给未来留后门LVGL自带LVGL_VERSION_MAJOR宏可以在需要兼容的代码里做条件编译。如果你实在不想一次重构完可以在公共头文件里写一个兼容适配层把v8的常用API映射到v9。比如#if LVGL_VERSION_MAJOR 9 #define lv_scr_act() lv_screen_active() #define lv_disp_flush_ready(disp_drv) lv_display_flush_ready(disp_drv) #endif这种做法适合过渡期但别长期依赖因为映射只能解决名字问题解决不了逻辑差异。我建议在迁移完成后的第一个版本里把适配层代码全部删掉让业务代码直接用新API重写一遍为下一个大版本做好准备。因为适配层一旦留下来会变成技术债的逃逸通道时间越久越没人愿意真正清理。5.3 依赖锁定组件版本的管理思路用idf_component.yml声明依赖时建议用范围内的版本锁定lvgl/lvgl: 9.0,10.0。这样既不会自动跨大版本升级也能吃到9.x的小版本修复。如果上游仓库发不兼容更新CI会在编译阶段就报错你有充分时间评估。另外绝不本地修改lvgl源码。真要改要么提PR、要么用官方支持的补丁机制并留下注释否则下次升级会完全失控。我在migrate后发现团队项目里最容易出乱子的就是某人本地给LVGL打了补丁但没告诉别人。版本锁定的意义不仅是对抗上游更是让所有成员的开发环境一致。用组件管理器semver范围后任何人拉代码都能build出同一个LVGL版本排除了大量为什么你那边正常我这不正常的吵架场景。5.4 一套可复用的升级检查清单下面这个清单是我在v8到v9迁移结束后整理出来的下次再有大版本升级直接照着跑配置lv_conf.h基于新版本模板重建功能开关逐一核对不要沿用旧文件显示驱动flush回调命名、buffer所有权、渲染模式检查刷新完成通知测试输入设备indev事件清理滚动、点击、长按行为分别测试字体与图片全部用新版工具重新导出中文和小图标专门验证控件改名lv_img到lv_image工具批量替换后人工复查动画与定时器时长、回调、FreeRTOS任务优先级肉眼对比迁移前后动画内存用heap_caps_get_free_size记录迁移前后峰值重点跑数据刷新场景功耗display sleep路径回归确认v9的刷新流程不会意外唤醒屏幕这套清单不保证覆盖所有工程但能把迁移风险控制在可接受范围内。结尾这些迁移经验是我在把自用的ESP32温湿度显示界面从v8搬到v9时一点点攒出来的版本之间隔得越远手动工作量越大。写这篇文章最重要的一个建议是不要等v8项目无法维护了才动手迁移在你还熟悉这套代码的时候挑一个功能冻结的时间窗口利用上面这套流程做一次体检式迁移代价远比未来某天被上游驱动卡住时被迫升级小得多。如果你也正在做类似迁移拿这篇里的检查清单做对照一次迁移把升级能力本身也沉淀下来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑