1. 为什么这次UI框架的升级让我这个老嵌入式开发坐不住了
做了这么多年STM32开发,说实话,以前一提到“给单片机做界面”,很多人的第一反应还是点几个LED、驱动个OLED屏显示字符,最多加个简单的菜单循环。但这两年,随着MCU主频越来越高、RAM和Flash容量越给越足,再加上屏幕分辨率从320x240一路卷到800x480甚至更高,嵌入式UI的开发方式完全变了。
我自己实际接触STM32的UI软件框架,是从一个带4.3寸屏的HMI项目开始的。当时项目要求做动态曲线、多级菜单、支持中英文切换,还要有圆角按钮和滑动动画。用老一套的“自己画点”思路,写三天三夜也写不完,而且效果惨不忍睹。后来换了成熟的UI框架,整个开发周期直接缩短到原来的三分之一。
所以说这次的“UI Software Framework for STM32 MCUs Gets Upgrade”这个事,对做嵌入式产品的人来说是个很有价值的信号。它背后意味着什么,有哪些坑要避,哪几个框架值得跟,我结合自己的实际项目经验把这事拆开聊透。
这篇文章适合刚准备给STM32项目加屏幕的人,也适合已经在用UI框架但想升级版本、优化性能的朋友。不管你用的是TouchGFX、LVGL,还是商用的emWin,核心思路都是相通的,看完你应该知道怎么选、怎么移植、怎么把性能榨出来。
2. 升级的不只是版本号,而是嵌入式UI开发的整体思路
2.1 从“画点”到“框架”:嵌入式UI开发范式的转变
先说一个很实际的问题:MCU上跑的UI框架,和PC或者手机上用的UI框架,本质区别在哪?
答案就俩字:资源。PC上写个界面,内存好几个G,CPU好几个核,随便挥霍。MCU就不行了,以常见的STM32F407来说,192KB RAM、1MB Flash,这配置已经算中端偏上。要在这种条件下跑出一个64K色、带透明效果、能流畅切换动画的界面,每一字节内存都得精打细算。
所以STM32上的UI软件框架,核心能力不在“控件多不多”或者“动画炫不炫”,而在三件事:
- 内存占用是否可控:能不能在低RAM下跑起来,是否支持动态内存和静态内存混合使用。
- 渲染效率是否够高:无论是软件渲染还是走硬件加速(比如STM32的DMA2D、LTDC、Chrom-ART),能不能把帧率顶上去。
- 开发效率是否够好:能不能可视化拖控件,能不能在PC上模拟调试,能不能和主业务逻辑解耦。
这就解释了为什么很多框架每隔一段时间就大版本更新一次。并不是闲着没事刷版本号,而是这三方面始终有优化空间。比如界面的复杂度上来了,占用的内存指数级增长,老版本的内存管理策略扛不住了;再比如屏幕分辨率提升了,老版本只支持RGB565,新屏幕要用ARGB8888,就得改渲染管线。
2.2 这次升级的几个核心方向,每个都卡在项目痛点上
结合我自己的观察,STM32生态里的UI框架升级主要围绕这几个方向:
第一个方向是降低接入门槛。以前给STM32配UI框架,尤其是TouchGFX这类,配置过程相当折腾:需要安装独立的软件、生成代码后再手动往工程里拖、屏幕驱动得自己调时序。现在的升级方向是深度集成到STM32CubeMX里,选好芯片型号、配好时钟和屏幕引脚,UI工程直接生成,省一大半时间。
第二个方向是渲染引擎的底层重构。像LVGL从8.x升级到9.x,把渲染器重写了,引入了draw unit的概念,可以同时接入软件渲染和硬件加速(比如给某些MCU接入专用的GPU驱动),对复杂图形的绘制效率提升非常明显。TouchGFX也在这两年把纹理压缩、局部刷新这些技术做得更成熟,在低端MCU上也能跑出不错的视觉效果。
第三个方向是工具链的配套升级。现在做UI,谁还纯手写代码摆控件?可视化编辑器是标配。升级后的框架基本都提供了PC端的模拟器,能在电脑上先调试UI逻辑,再烧到板子上,调试效率完全不一样。还有一些工具链开始支持从设计稿(比如Figma)直接导出UI资源,这个方向虽然还不是很成熟,但已经有框架在探索了。
这三个方向说白了,都是为了让工程师“少写代码、多出效果”。以前调一个按钮的点击区域,可能得改坐标、改图片资源,烧录几十次才能看效果。现在框架升级后,在模拟器里改一下拖一下,效果秒出,我自己的体会是光这一块就能省出两三天时间。
3. 主流UI框架选型:TouchGFX、LVGL、emWin到底怎么选
3.1 框架对比:没有绝对的最好,只有匹配你的项目
很多新手爱问“哪个UI框架最好”,这个问题其实问错了。选框架不是选最好的,是选最匹配的。我做个表,把目前STM32上最常用的三个框架放在一起对比,都是我自己实际用过的感受。
| 对比维度 | TouchGFX | LVGL | emWin/STemWin |
|---|---|---|---|
| 授权方式 | 免费(STM32芯片上) | 免费开源(MIT) | STM32上免费(需通过Cube包获取) |
| 开发工具 | TouchGFX Designer,可视化强 | 可选GUI Guider、SquareLine Studio | AppWizard(较老) |
| 内存占用 | 低,尤其是带缓存优化后 | 中低,可配置性强 | 中低 |
| 渲染性能 | 高,深度利用DMA2D/LTDC | 高(9.x版本提升明显) | 中高 |
| 学习曲线 | 中等,依赖可视化工具 | 较陡,但资料多 | 中等偏旧 |
| 适合场景 | 追求视觉效果、产品化强的项目 | 跨平台需求多、社区活跃的项目 | 老项目维护、工业控制类 |
我个人的建议是:如果你用的是STM32芯片,且想要最好的UI效果和工具链体验,优先考虑TouchGFX,尤其是ST官方芯片,它对DMA2D和LTDC的配合是原生级的。如果你的项目之后可能要换别的MCU平台,或者希望代码的开放性和社区支持更强,选LVGL。
emWin我现在的态度是能不用就不用,除非是在维护老项目。它的更新节奏明显跟不上TouchGFX和LVGL,UI效果的上限也比较受限,新项目没必要从它起步。
3.2 为什么TouchGFX在STM32生态里越用越顺手
TouchGFX从被ST收购之后,和STM32CubeMX的集成度越来越高,这是它最大的护城河。在CubeMX里勾选TouchGFX组件,配置好屏幕的LTDC或FMC接口,生成代码后,用TouchGFX Designer打开工程,拖控件、做交互、加动画,一键生成代码再回到IDE编译下载,整个流程非常顺滑。
尤其值得说的是TouchGFX的缓存机制。它支持帧缓冲的多缓冲模式和局部刷新,在STM32F429这类带LTDC的芯片上,DMA2D可以做到后台搬运像素数据,CPU只负责绘制逻辑,实际跑下来动画帧率稳定在60fps甚至更高,这对HMI产品来说是质变级别的体验。
另外一个我特别喜欢的点,是它的资源管理。图片、字体、文本全部通过工具链转换成C数组,打包进Flash,占RAM极少。做多语言界面的时候,文本一键切换,不用自己维护字符串表。这个功能在工业HMI上非常实用。
3.3 LVGL 9.x的升级到底升级了什么
再说说LVGL。LVGL 8.x到9.x的升级,看似是版本号+1,其实变化非常大。首先是渲染器架构重构,引入了多层绘制管线的概念,把图形处理分拆给不同的draw unit,这为后续接入GPU或者专用2D加速单元留好了口子。所以在9.x上跑STM32的DMA2D加速,比8.x更自然、更顺。
其次是内存管理。LVGL 9.x重构了内存分配策略,改进了碎片的处理,还引入了内存池的配置方式。对RAM小的MCU来说,这是个实际的好处。我做过一个用STM32G474的项目,RAM只有128KB,跑LVGL 9.x + 320x240的屏,UI部分控制在30KB以内,剩余空间给业务逻辑,完全够用。
再就是控件和效果上的升级。9.x新增了flex和grid布局支持,类似CSS的弹性布局,写复杂界面不用再手动算坐标。另外对圆角、阴影、渐变这些视觉效果做了渲染优化,低端MCU上也能出不错的观感。
4. 实操记录:从CubeMX到TouchGFX,完整跑通一个UI项目
4.1 准备阶段:芯片选型和工程配置的细节
这个项目我用的是一块STM32H750VBT6,外接5寸800x480的RGB屏,接口是RGB888 + 触摸板I2C。选H7的原因很简单:跑UI需要主频高,H750主频能到480MHz,加上它内置的2MB Flash(实际H750是128KB,但可以通过外部QSPI Flash跑XIP),预算可控,性能不打折。
第一步还是老规矩,在STM32CubeMX里配好基础工程。这里有几个关键点值得单独拎出来说:
- 时钟树一定要先配好:RGB屏需要像素时钟,800x480@60Hz大约需要33.3MHz的像素时钟。在H750上要保证LTDC的时钟源分配正确,否则屏直接不亮或者闪烁。我建议直接把CubeMX里的Clock Configuration页面截图留档,方便后续排查问题。
- LTDC引脚分配:RGB888一共24根数据线+同步信号,引脚复用要一个个确认。H750的引脚比较多,但也要注意别和调试口、外部Flash的引脚冲突。
- DMA2D:这个是硬件加速的关键,必须在CubeMX里使能,虽然它不需要额外配置,但寄存器时钟要开。
- 帧缓冲的放置位置:800x480,RGB888,一帧画面需要的空间是800×480×4约1.5MB。这在内部RAM完全放不下,只能放到外部SDRAM。所以外挂SDRAM是必须的,配FMC接口,地址映射到0xC0000000。
这些配置我花了大概一个晚上排查,主要是SDRAM的时序参数。SDRAM的刷新周期、CAS延迟这些参数如果不对,板子跑起来会随机花屏,而且在调试模式下可能正常、独立运行时才崩,非常隐蔽。建议先把SDRAM的读写测试写好,确保这块稳定了再往后走。
4.2 TouchGFX Designer里搭建界面和生成代码
CubeMX配置完成后,在Project Manager里勾选“Generate Code”时会同步启动TouchGFX Designer,或者也可以先从CubeMX激活TouchGFX组件,再在正式生成代码前打开Designer做界面。
我这次项目的界面很简单,就三个页面:主页(显示温湿度曲线)、设置页(参数调整)、关于页。在TouchGFX Designer里操作如下:
- 新建一个Screen,命名MainScreen。
- 从Widget库里拖一个Box进来,作为背景;再拖一个TextArea显示标题。
- 添加一个Graph控件,绑定到一个数据源,用于动态曲线显示。
- 添加两个Button,分别绑定Screen Transition跳转到设置页和关于页。
- 在Properties面板里设置字体,中文字体需要额外导入,我用了一个开源的中文字体文件,转换后生成字库。
Designer最大的好处是实时预览,所有控件的位置、大小、颜色都可以直接拖拽调整,不用反复改代码烧录。我把界面搭好大概花了一小时,框架性工作基本就结束了,剩下的就是业务逻辑。
4.3 在C++工程里写业务逻辑
生成代码后,整个工程是C++的(TouchGFX的核心代码是C++写的),主循环里会调用tick()函数处理UI刷新和事件分发。业务数据的注入一般有两种方式:
第一种是在Model类里写方法。TouchGFX的MVP架构里,Model负责和底层数据交互,View和Presenter负责界面显示和用户输入。我就在Model里加了一个setTemperature(float value)的方法,把采集到的温湿度值推送进UI。
第二种方式是用自定义控件,把ADC采样、传感器读取这些逻辑封装成一个类,通过定时器在后台线程更新,然后直接调用UI的接口刷新Graph控件的数据点。
这两种方式我目前混合使用。业务数据更新频率高、数据量大的,放在后台单独处理,只把最终结果传给UI;低频交互类的,就直接在Model里处理,逻辑简洁。
这里有一个经验之谈:千万不要在UI线程里做耗时操作,比如阻塞式读取I2C传感器、Flash写操作。UI线程一旦卡住,触摸响应、动画全部掉帧,用户体验立刻变差。我的做法是把所有数据采集放到一个RTOS任务里,通过消息队列和UI线程通信。
4.4 编译、下载和调试的注意事项
H750的内部Flash只有128KB,而一个带中文字库的TouchGFX工程,固件随便就超过这个大小。所以我用的是外部QSPI Flash,通过Board Loader的方式加载。在CubeMX里配置QSPI接口,然后调整链接脚本把一部分只读数据放到外部Flash地址。TouchGFX生成的资源(图片、字体)默认是放在内部的,大了就会链接失败,所以我手动把资源段重定向到外部QSPI Flash。
这一步是最容易出现链接错误的,如果出现region FLASH overflowed之类的报错,多半就是资源段太大了。解决方法是把touchgfx相关section的地址改成QSPI Flash的地址空间。我踩了一次坑,花了半天时间各种改链接脚本,最后发现还要在启动代码里初始化QSPI控制器,否则上电后从外部Flash读数据全读成0xFF,界面显示全是乱码。
下载调试我用的是STM32CubeProgrammer,配合ST-Link,烧录速度还不错。
5. 性能优化实战:让UI真正做到丝滑流畅
5.1 帧缓冲策略:单缓冲、双缓冲还是局部刷新?
UI框架跑得顺不顺,和帧缓冲策略关系非常大。这个技术指标直接决定动画的流畅度和内存占用,值得展开讲。
单缓冲是最简单的模式:MCU往一个帧缓冲里画图,画完后通过LTDC扫描显示到屏幕上。中间如果有动画刷新,会出现明显的撕裂现象——屏幕上半部分是新画面,下半部分还是旧的。入门级项目可以凑合用,但对视觉要求稍高的产品就不行了。
双缓冲是在RAM里准备两个帧缓冲,一个用于后台绘制,另一个用于LTDC扫描显示。绘制完成后切换两者,避免撕裂。代价是内存占用翻倍——800x480 RGB888的双缓冲需要3MB,SDRAM是必须的。
TouchGFX还提供了一种更聪明的方案:局部缓冲。小尺寸屏幕(比如320x240)可以只准备几个小块缓冲,每次只刷新变化区域,内存占用大幅降低。这个模式特别适合RAM紧张的MCU,但代价是复杂动画效果会打折,适合那种界面以静态显示和简单切换为主的产品。
我实际项目的做法是:默认双缓冲,保证动画效果最好。如果产品后期需要降成本换小RAM芯片,再切换成局部刷新方案。这种“先保证效果、后做减法”的思路,更适合产品化开发。
5.2 把DMA2D和LTDC的作用发挥到极致
STM32系列里,和UI渲染最相关的两个外设是LTDC和DMA2D。
LTDC是LCD控制器,负责从内存里读取像素数据、按时序输出到屏幕。它支持多图层混合,比如一个图层放背景图,另一个图层放前景控件,LTDC硬件自动完成alpha混合。这个能力如果用好了,可以省掉软件层面的大量混合计算。
DMA2D是2D图形加速引擎,支持像素复制、填充、混合、格式转换等操作。它的典型应用场景包括:
- 图片搬运:把一张图片从Flash搬到帧缓冲,纯DMA操作,CPU零负担。
- 颜色格式转换:比如把RGB565的图片转成RGB888,DMA2D硬件直接完成,比软件快几个数量级。
- 批量填充:比如清屏操作,用DMA2D填充纯色,一条指令搞定。
- Alpha混合:把带透明度通道的前景图直接混合到背景上。
TouchGFX在底层已经把这些封装好了,如果你的工程里有自定义控件,强烈建议直接调用HAL::DMA2D相关的API,别用软件循环画图,性能差距是数量级的。
5.3 图片和字体的优化:Flash空间省下来的都是钱
嵌入式UI里,Flash空间往往比RAM更紧张。一张800x480的RGB888背景图,直接存bmp要1.5MB,这还没算其他资源。所以在UI资源这块,我总结了几条实用经验:
- 图片统一压缩:TouchGFX支持PNG、JPG等格式的图片导入,内部会自动编码成可硬件解码的格式。对于大面积纯色或渐变背景,用简单的色块绘制代替图片,Flash占用几乎可以忽略。
- 字库按需裁剪:中文字体动辄几万字符,全部包含会撑爆Flash。用字体工具只保留用到的几百个字(这叫字模裁剪),体积能缩到几KB。TouchGFX Designer里可以直接导入TTF字体文件,并设置字符范围,我通常只保留项目用到的那部分字符。
- 纹理格式选择:TouchGFX支持A4(4bit透明度)、L8(8bit灰度)等多种纹理格式。如果图片不需要全彩,选低色深格式能大幅压缩体积,视觉效果几乎不变。
6. 我做UI开发时踩过的坑和排查记录
6.1 STM32延时函数卡死,UI还怎么跑?——浅谈中断优先级的隐患
有一次我做一个带LVGL的项目,在调试串口PID输出的时候,发现只要UI绘制动画,同时串口在收发数据,系统就卡死。起初怀疑是LVGL的bug,排查了半天,最后定位到罪魁祸首:延时函数(HAL_Delay)卡死在中断里。
原因很简单:HAL_Delay依赖SysTick中断,如果我在一个中断服务函数里调用了它,而这个中断的优先级比SysTick更高或者同级,SysTick中断就无法触发,延时函数就永远等不到时间溢出,系统就死锁了。
这个问题的通用解法是:中断服务函数里永远不要调用HAL_Delay,也不要做复杂UI操作。把这些事情丢给RTOS的任务去做。如果项目没有RTOS,那就用一个全局标志位,在中断里置位,在主循环里响应。
6.2 花屏和闪屏的真凶:SDRAM时序和图层配置
花屏问题我至少遇到过三种情况:
第一种,上电花屏。原因大多是SDRAM初始化代码在LTDC开启之后才执行,或者SDRAM参数配置不对。解决方法是:在main函数的最早期初始化SDRAM,先做读写测试,再启动LTDC。
第二种,随机区域花屏,时好时坏。这种通常是SDRAM时序接近临界值,温度和电压稍微波动就会出错。解决方法是把SDRAM的刷新率调高一点、时序余量调大,同时检查PCB布线——SDRAM对走线长度和阻抗匹配比较敏感。
第三种,切换到某个页面后大面积花屏。怀疑是DMA2D访问了非法的内存地址,比如越界写到了SDRAM之外,破坏了其他数据。老老实实检查数组索引和缓冲区的分配,一般能找到原因。
6.3 触摸漂移和响应不灵敏的问题
我项目里用的是电容触摸屏I2C接口,偶尔出现触摸漂移和响应迟钝。排查思路记录如下:
- 首先确认I2C通信是否正常,用逻辑分析仪抓I2C波形,看触摸控制器是否返回正确坐标。
- 其次看初始化时序,电容触摸控制器需要上电后稍等几毫秒才能通过I2C访问,我代码里加了一个10ms延时解决了大部分问题。
- 触摸准确度的校准参数也要检查。TouchGFX里有触摸校准接口,如果屏幕坐标映射不对,会出现点击A按钮触发B按钮的错位现象。
7. 借助这次升级,怎么规划你自己的UI项目
7.1 从零开始?先做最小验证再铺开
很多朋友拿到开发板,第一反应就是“我要做个很酷的界面”。但我的建议是:先做一个最小验证,跑通链路,再加功能。
最小验证包含哪些?一个能亮起来的屏幕、一个能点的触摸、一个能跳转的页面、一个能刷新的动画。这个链路跑通,说明整个技术栈是可用的,再往上面堆功能就不会有大方向问题。
我习惯把这个最小验证控制在3天以内。第一天配置CubeMX、点亮屏;第二天接入UI框架、跑通界面跳转;第三天写一个小动画或者动态曲线,验证性能和交互。第4天开始才做正式的功能开发。
7.2 团队协作时,UI和业务逻辑如何分工
如果项目是两三个人一起做,UI和业务逻辑一定要解耦。我的做法是:UI工程师只负责Designer里的界面设计和交互逻辑,业务工程师在Model类里填数据。两边通过约定好的Model接口对接,互不干扰。
在代码层面,我要求所有和硬件相关的代码(传感器、通信、控制)都封装成独立模块,不直接在View或者Presenter里调用。这样UI和业务可以并行开发,联调时也容易定位问题。
7.3 版本升级和兼容性的问题
你如果是老项目想升级UI框架的版本,别急着直接替换库文件。LVGL从8到9的API变化不小,很多控件属性名、函数名都改了,直接升级会编译出一堆error。我做升级时的方法是:
- 先把老版本的代码用git打好tag。
- 新建分支,替换新版本库文件。
- 逐个解决编译错误,优先处理底层接口(比如触摸驱动、显示驱动层的API)。
- 启动后跑一遍所有页面,对比效果。
- 回归测试所有业务交互。
整个过程大概需要2到3天,具体时长取决于项目的界面复杂度和对老API的依赖程度。如果项目界面特别复杂,我的建议是别过度追求新版本,除非你有明确的性能优化需求或者新功能必用不可。
8. 最后分享一点我自己做嵌入式UI的体会
做STM32的UI开发这几年,我最大的感受是:框架和工具越来越成熟,但工程师的“内功”还是不能丢。UI框架解决的是“怎么画”的问题,而“画什么”“为什么这么画”以及“性能瓶颈在哪里”,还是得靠对MCU底层原理的深入理解。
比如你在用TouchGFX,就算它帮你把DMA2D封装好了,如果你不清楚DMA2D的带宽占用会影响CPU访存性能,你仍然可能遇到莫名的卡顿。再比如你在用LVGL,就算它帮你把内存管理做得很智能,如果你对MCU的RAM布局没有概念,依然会在大量界面创建销毁时碰到碎片问题。
所以我建议各位,不管用哪个框架,都花点时间把MCU的这几块底层吃透:时钟树、RAM/Flash地址映射、DMA传输原理、中断优先级机制。这些知识永远不会过时,也是你在遇到UI疑难杂症时,能够快速定位问题的底气所在。
最后再分享一个小技巧:如果你在HMI项目复杂到一定程度,需要考虑把UI任务放在RTOS的一个高优先级任务里,并给UI任务设置足够的栈空间。我踩过一次因为这个栈溢出导致页面随机重启的坑,排查了很久才发现是任务栈不够。给UI任务留的栈,宁可多给,不要省,这个建议值不少时间。