
说说STM32那些事(5)移植 TouchGFX 到某国产 F746 评估板做嵌入式这几年我一直觉得UI是产品最难啃的骨头之一。单片机跑逻辑、跑协议、跑电机控制都不是问题一到要做一个像样的界面各种方案抄过来改过去总差点意思。TouchGFX在STM32生态里算是比较省心的一个图形库渲染效果接近手机App内存优化也做得不错。这次我把TouchGFX移植到了一块国产F746评估板上过程中踩了不少坑也把整条路径摸通了。这块板子主控是STM32F746带16MB SDRAM、一块7寸RGB接口电容触摸屏硬件底子很典型如果你手头是类似配置的开发板这篇文章应该能帮你少走一两天的弯路。适合谁看刚接触TouchGFX、想让界面跑在自己板子上而不是官方开发板上的工程师以及想搞清楚LCD、SDRAM、LTDC、DMA2D这几样东西怎么配合的人。我会从环境准备、底层配置、HAL对接、触摸驱动到问题排查把整个移植链路掰开讲一遍。1. 移植前必须想明白的三件事1.1 为什么要手动移植而不是直接套官方模板很多人在拿到一块新板子时第一反应是去官方例程里找一个最接近的模板改改。这个方法不是不行但对于非官方的国产评估板来说模板匹配度通常很差。官方模板一般针对特定开发板设计引脚分布、屏幕型号、触摸接口、外部存储配置都是定死的。换一块板子光是GPIO重新分配和时钟重新配置就能折腾掉大半天。更麻烦的是官方模板里的初始化代码很多已经封装好了你只知道调用结果不知道内部逻辑。一旦出现白屏、花屏、触摸失灵这类问题排查起来完全没有头绪。我这次的F746评估板用的是一块7寸RGB屏这跟很多官方开发板用的MIPI屏或者SPI屏驱动方式完全不一样。RGB屏需要LTDC控制器持续输出像素时钟屏幕本身没有显存所以每一帧的数据都要由MCU实时刷新到屏上。这意味着LTDC、DMA2D、SDRAM这三者的配合是核心。手动移植虽然前期工作量大一点但每一步都是自己配置的出了问题至少能定位到具体模块而不是在别人封装好的代码里瞎猜。1.2 硬件资源盘点确认板子有资格跑TouchGFXTouchGFX不是随便一块开发板都能跑的它对硬件有三个比较硬性的要求。第一是内存。一个简单的界面800x480分辨率、RGB565颜色格式一帧的数据量就是800×480×2字节约768KB。如果我们想做双缓冲那至少需要1.5MB连续内存。STM32F746内部的RAM只有320KB左右远远不够所以必须依赖外部SDRAM。我手里这块板子带了16MB SDRAM完全足够这也是它能跑TouchGFX的基础。第二是屏幕接口。TouchGFX的软件渲染引擎虽然也支持SPI屏幕但性能很差只能做一些非常简单的界面。真正想要流畅的动画效果最好使用RGB接口屏幕也就是MCU通过LTDC外设直接驱动。这块板子的屏幕正好是RGB接口分辨率800x480刷新率60Hz完全符合要求。第三是硬件加速。STM32F746内置了DMA2D这是一个专门用于2D图形数据搬运和填充的外设。TouchGFX在绘制矩形、图像混合、颜色填充时大量依赖DMA2D。如果没有这个外设CPU要承担大量的像素搬运帧率会明显下降。把这些资源理清楚之后我对这次移植心里就有了底这是一块标准的F746RGB屏SDRAM组合的板子TouchGFX正是为这种硬件组合设计的。1.3 移植的整体思路四个模块搭建整条链路TouchGFX的整体架构分两层上层是图形框架也就是我们在Designer里面拖拽控件、写交互逻辑的那部分下层是硬件抽象层负责把上层发下来的绘制指令翻译成具体的LCD操作。移植工作集中在底层。具体来说需要处理四个模块LCD驱动负责把像素数据写到屏幕对应的显存地址并确保LTDC正确扫描输出触摸驱动负责读取触摸坐标并通过回调反馈给UI层SDRAM初始化保证帧缓冲所在的存储区域可以正常读写HAL对接把TouchGFX框架与STM32的外设库连接起来。这四个模块看着多其实工作量不大。LCD驱动和SDRAM初始化在STM32CubeMX里大部分都能生成触摸驱动写一个读取函数就行HAL对接则参考TouchGFX官方文档把几个关键函数补全。真正花时间的是理解每一部分为什么这样配置以及踩坑之后怎么排查。2. 工具链选择与版本匹配2.1 版本千万别乱配CubeMX、TouchGFX和固件包要一起看做STM32开发这么多年我最大的体会就是版本匹配比什么都重要。TouchGFX经历了多次大版本更新不同版本的Designer生成的工程结构差异很大而且它跟STM32CubeMX的集成方式也在变化。如果版本搭配不当可能会出现Designer生成的代码跟HAL库对不上、链接报错、某些API在固件包里找不到之类的问题。我这块板子用的是STM32CubeMX配合TouchGFX Designer。安装的时候建议把CubeMX更新到比较新的版本然后通过CubeMX的“Additional Software”功能直接安装TouchGFX组件包让CubeMX认到这个组件。这样在生成初始化代码时CubeMX会自动把TouchGFX相关文件一起生成省去很多手动拼接的麻烦。固件包方面我选的是跟HAL库大版本一致的STM32CubeF7固件包。固件包版本太旧可能缺少某些外设的新特性版本太新HAL API名字或参数又可能有变动导致TouchGFX生成的适配层代码编译不过。建议选一个经过大量用户验证的稳定版本而不是追最新。注意CubeMX和TouchGFX Designer的版本尽量都不要用太旧的版本。旧版本生成的TouchGFX适配代码在F7上可能存在Cache一致性问题新版框架对Cortex-M7的Cache处理要完整很多。2.2 编译工具链我用的是MDK-ARM编译器我最终选了Keil MDK-ARM。虽然现在STM32CubeIDE免费且功能强很多年轻工程师喜欢用它但MDK在调试界面层的效率确实更高。TouchGFX工程涉及大量的代码生成和文件关联MDK的工程管理比较成熟而且网上资料多遇到编译错误搜索解决方案也方便。个人使用的版本比较新对Cortex-M7支持得很好链接脚本和启动文件可以直接用生成好的。如果你的团队统一用的GCC工具链TouchGFX在生成工程时也可以选择对应的工具链。但我要提醒一点不同工具链下内存对齐规则和链接脚本的写法有细微差别严格按照工具链要求的目录结构来不要手动去改那些链接脚本否则可能会触发莫名其妙的对齐错误。2.3 TouchGFX Designer里能做什么不能做什么TouchGFX Designer是一个可视化的界面设计工具可以拖控件、设置样式、配置交互但它的核心输出是代码不是一个可以运行的完整固件。它生成的代码包括两部分一部分是平台相关的适配代码位于target目录里另一部分是UI层代码位于generated/gui目录。很多人第一次接触时容易混淆以为Designer生成完就能烧录跑起来实际上还需要把生成代码跟CubeMX生成的外设初始化代码整合到同一个工程里。我们在Designer里要设置好屏幕分辨率、颜色深度和缓冲策略这些参数直接决定了生成代码中像素格式和缓冲区的分配方式。如果这些参数跟底层LTDC配置不一致就会出现颜色错乱甚至是花屏。3. 用CubeMX把底层配置到位3.1 时钟树所有外设的节奏源头TouchGFX的流畅度很大程度上取决于屏幕刷新率而屏幕刷新率由LTDC的像素时钟决定。像素时钟需要从PLLSAI生成不是直接从主频分出来的。我在这块板子上的配置是这样的主频跑到216MHzPLLSAI通过分频倍频得到33.3MHz左右的像素时钟正好匹配800x48060Hz的刷新需求。算一下过程800x480分辨率60Hz刷新率行消隐和帧消隐加进去后实际像素时钟需求大概在30~34MHz之间。具体值要看屏幕数据手册给出的时序参数。CubeMX的LTDC配置界面会显示各参数对应的hback、vback等信息敲进去之后它会自动帮我们核算像素时钟是否合理。整个时钟树配置完成之后我习惯先用一个最简单的GPIO翻转程序验证时钟是否正常再进外设配置。如果时钟都不对后面所有配置都是白搭。3.2 FMC和SDRAM帧缓冲的家帧缓冲区放在SDRAM里这是TouchGFX能够跑起来的前提。SDRAM挂在FMC外设的Bank1上从地址0xC0000000开始大小16MB。CubeMX里配置SDRAM需要填一堆时序参数列如CAS、RCD、RP、RC等这些参数直接从SDRAM数据手册上抄或者查板子原理图上的型号。刚开始移植的人容易犯一个错误只配置了FMC的引脚和时序忘了在代码里做SDRAM控制器的初始化序列。SDRAM上电后需要先延时、再发送预充电命令、至少两个刷新周期、设置模式寄存器这些步骤一步都不能少。CubeMX生成的代码会把这些步骤封装在HAL_SDRAM_Init函数里但需要我们在调用LTDC之前先调用它并且确认返回状态无误。实际测试中SDRAM时序配置过紧会导致偶发数据错误表现出来就是屏幕上有零星的色点或者画面偶发撕裂。我一般会把时序参数设置得比数据手册上的最小值宽松一些保证稳定。关键经验SDRAM初始化是否成功的快速验证方法是往SDRAM的某个地址写一个固定值再读出来比较多点几个地址跑几百次确保稳定。不要直接跳到TouchGFX再调试否则一旦花屏很难判断是SDRAM问题还是LCD配置问题。3.3 LTDC屏幕的扫描引擎LTDC配置中要先配置Layer0设置显示窗口为800x480像素格式选RGB565帧缓冲地址指向SDRAM。这里有个选择RGB565还是RGB888。RGB888色彩更丰富但占用的内存是RGB565的两倍一帧是800×480×3字节约1.15MB双缓冲就超过2.3MB。TouchGFX在低内存模式下支持RGB565效果也能接受大部分UI图标的色阶变化不多RGB565的损失肉眼几乎看不出来。所以我最终选了RGB565为了节省SDRAM带宽也让双缓冲成为可能。LTDC除了Layer0还有Layer1可以叠加。画面重叠时性能会下降除非确实需要多窗口叠加效果否则只开一层就够了。我在第一次点亮屏幕时犯过一个糊涂错误没关掉Layer1结果屏幕显示一直错乱排查了好久才发现是Layer1上层占用了显存但没做颜色填充。LTDC配置完成后还要确认背光电路。RGB屏一般不通过LTDC控制背光而是单独一个GPIO控制背光的开关和亮度。F746评估板上的背光控制脚在CubeMX中需要设为GPIO输出并初始化为高电平。如果这个引脚忘了初始化屏幕就算收到数据也不会亮看起来就是白屏。3.4 DMA2D和中断优先级DMA2D的初始化很简单它不需要像普通DMA那样配置通道和请求源而是直接在代码中通过HAL_DMA2D_Start调用来搬运数据。CubeMX里不需要做任何配置只要保证它的时钟使能即可。TouchGFX的渲染引擎会频繁调用DMA2D做大量像素搬运建议把DMA2D的中断优先级设得比外设驱动的高一点避免在渲染过程中被触摸I2C或其他外设频繁打断导致性能波动。我在这块板子上把DMA2D优先级设为最高触摸读取放低优先级实测帧率稳定。4. TouchGFX工程的生成与接入4.1 Designer里的工程参数设置与缓冲策略在TouchGFX Designer里新建工程时第一项就是选择平台。这里要注意不要选择某个具体的开发板模板而是选“Empty Application”因为我们已经在CubeMX里配置好了一切底层不需要Designer再生成一遍。屏幕分辨率设为800x480颜色格式设为RGB565。接下来是缓冲区策略TouchGFX支持三种模式单缓冲、双缓冲和局部缓冲区。单缓冲占用内存最小一帧768KB但是刷新过程中如果LTDC扫描到了缓冲区正在被修改的内容画面就会出现撕裂。双缓冲需要两块缓冲区一块用于后台绘制一块用于前台扫描绘制完成后再切换彻底避免撕裂但内存占用翻倍。局部缓冲区则是在内部SRAM里只分配一小块区域只刷新变化的部分适合内存极度紧缺的场景但性能会打折。我最终选了双缓冲。一方面16MB SDRAM完全够用另一方面双缓冲带来的画面稳定性在滑动和动画场景里非常明显。虽然TouchGFX对双缓冲的支持依赖底层同步机制但只要F746的LTDC刷新和DMA2D绘制协作正常实现起来并不复杂。4.2 生成代码的结构target文件夹就是我们的主战场在Designer里完成界面配置后点击生成代码会得到一个Application模板目录。这个目录里有几个重要文件generated目录存放的是UI层生成的代码包括主循环、控件定义、图片资源等这些文件一般不需要动。target目录存放的是平台适配层代码也是移植工作的核心。里面有HAL的初始化文件、TouchGFX的HAL类实现、链接脚本和启动文件。把CubeMX生成的工程和TouchGFX生成的工程整合时要仔细核对target目录里的HAL初始化函数它们会引用CubeMX生成的HAL库函数。只要两边版本一致基本不需要改动。提示不要试图去修改TouchGFX生成的TEXTS、IMAGES等资源文件代码。图片等资源由Designer统一管理手动修改后重新生成就会被覆盖而且资源格式不匹配会导致链接失败。4.3 HAL对接把TouchGFX的请求翻译给硬件TouchGFX的HAL层由几个函数组成核心是touchgfx_init()和任务循环里的OSAL回调。touchgfx_init()里要完成显示控制器的初始化和缓冲区策略的设定。具体到我们这块板子touchgfx_init()需要调用我们生成的SDRAM初始化函数、LTDC初始化函数然后把LTDC显存地址设置为前台缓冲区地址。TouchGFX内部的RenderingBuffer会把要绘制的像素数据写入后台缓冲区当一帧绘制完成后再通过LTDC的帧完成中断触发Buffer切换。这个过程听起来简单实际对接时最怕的是CubeMX生成的外设初始化顺序不对。我习惯于在main函数里手动控制初始化顺序SystemClock_Config、FMC/SDRAM、LTDC、DMA2D最后再调用touchgfx_init()。如果SDRAM在LTDC之后初始化LTDC扫描一开始就会去读空内存出来的画面就是满屏雪花。4.4 链接脚本和内存布局TouchGFX生成的链接脚本里已经预定好了SDRAM的映射区域、帧缓冲地址和堆栈位置。对于带SDRAM的开发板链接脚本一般会把帧缓冲地址硬编码为0xC0000000偏移后的某个地址比如0xC0000000 4MB留出一部分给其他用途。如果修改了SDRAM大小或者想在SDRAM里加载一些图片资源需要同步调整链接脚本。最简单的办法是不动地址把所有资源放在内部Flash只把帧缓冲和动态内存放SDRAM。内部Flash对一块F746来说通常是够用的除非你的UI用了大量高分辨率全屏图片。5. 触摸驱动的集成5.1 先搞清触控芯片类型和接口F746评估板上配的是一块电容触摸屏触控IC走I2C接口。这类I2C触控芯片的读取流程大同小异芯片上电后通过中断引脚通知MCU有触摸事件MCU随后通过I2C读取触摸坐标寄存器。判断一块板子的触摸芯片类型很简单看原理图或者跑一个扫描脚本在I2C总线上枚举地址看看哪些地址有设备响应。一般电容触控IC的7位从机地址常见的有0x14、0x5D、0x38之类不同厂商不一样。我在这块板子上扫到的地址是0x14随后去数据手册里一查就确认了。触摸芯片在工作前需要复位一下有些还要烧写配置。我这里是标准的应用电路只要给RST引脚拉一下低电平再拉高芯片就自动进入工作模式不需要额外配置。5.2 把触摸读取封装成TouchGFX的C接口TouchGFX的输入事件系统通过继承Application类的方式工作。实际开发中我们并不需要直接去改框架而是在任务循环里周期性地读取触摸坐标然后调用TouchGFX的HAL传递输入事件。我写了一个简单的C类内部用I2C函数读取坐标寄存器。读取逻辑很简单等待中断引脚拉低然后依次读取X坐标高字节、X坐标低字节、Y坐标高字节、Y坐标低字节。有些IC在读取完状态寄存器后会自动清中断有些需要写清除指令这个要看对应的数据手册漏掉就会导致中断一直被拉低UI一直认为有触摸。封装完成后在main循环或者RTOS任务中如果检测到触摸数据更新就生成一个ClickEvent调用touchgfxHAL::getInstance()-feedEvent(event)交给上层处理。这样底层读取和UI层逻辑彻底解耦后续要加手势识别也很方便。5.3 坐标变换和屏幕方向处理移植中最容易忽略的就是坐标方向不一致。屏幕的物理方向、LTDC扫描顺序、触摸芯片的坐标原点三者之间经常出现镜像或旋转。我在初次点亮时就遇到触摸点跟显示位置偏了90度点屏幕上方却触发的是右侧按钮。解决坐标变换很简单通常只需要在按键映射时做一次坐标变换。比如如果触摸芯片的原点在右下角而显示原点在左上角就需要做x width - 1 - rawX, y height - 1 - rawY。具体变换方式可以烧一个测试程序触摸屏幕四角和中心记录原始坐标和实际位置的对应关系再确定变换公式。还有个细节TouchGFX界面设计的宽高都是横屏状态如果屏幕物理上是竖屏需要做90度旋转。旋转不仅涉及坐标映射也涉及LTDC扫描方向配置。F746评估板的屏幕是原生横屏800x480所以我没有处理旋转。如果你遇到竖屏板子建议先跳过旋转用横屏验证功能最后再处理旋转否则会把问题搞复杂。从工程效率角度说优先保证触摸坐标能正确转换成屏幕上的点再考虑手势和滑动优化。坐标转换不正确后面所有交互逻辑都是空中楼阁。6. 实际踩坑记录与排查清单6.1 白屏屏幕完全没反应白屏是最常见的现象原因通常会指向三个方面背光没亮、SDRAM没初始化、LTDC根本没开始扫描。排查顺序应该是先看背光用万用表或者示波器测背光引脚电平然后检查SDRAM初始化有没有返回成功用之前说的写读验证法最后检查LTDC寄存器确认Layer使能位和DMA请求输出位都置1。我这次遇到的白屏是因为我在CubeMX里漏配了背光控制引脚的GPIO输出模式导致背光一直在低电平。这个问题如果在开发初期就查看原理图完全可以避免。6.2 花屏或者颜色错乱像素格式不匹配花屏的表现形式很多元有满屏斜纹的有颜色偏色严重的有图像边缘有重影的。这里面最常见的原因是LTDC层面配置的像素格式跟TouchGFX缓冲区的像素格式不一致。比如TouchGFX生成了RGB565数据而LTDC配置的是RGB888屏幕就会被错误解包出来的颜色完全不对。排查方法很简单先把TouchGFX暂停在LTDC配置里写死一块纯色填充到显存地址看看屏幕能不能正确显示纯色。如果能显示纯色说明LTDC和SDRAM没问题那就是TouchGFX的像素格式跟LTDC不匹配。另外M7内核的Cache也需要注意。如果开启了Cache而DMA2D通过AXI总线访问SDRAMCPU往SDRAM里写的帧缓冲数据可能还留在Cache中DMA2D读取时就会读到旧数据表现出来就是画面偶发残留。解决方式是配置MPU把SDRAM区域设置为非缓存或者配置为write-back模式并做好Cache维护。TouchGFX新版HAL代码对Cache维护有较完整的支持我们只需要确保MPU配置跟链接脚本匹配即可。6.3 触摸完全无反应或者乱跳触摸无反应时先别急着看TouchGFX代码。用示波器看触摸IC的中断引脚有没有正常电平变化如果触摸屏幕时中断一直没变说明触摸芯片本身没工作问题在复位电路、I2C通信或者芯片配置上。确认I2C总线上能枚举出设备地址后再回头查TouchGFX侧是否正确调用了feedEvent。乱跳的情况往往跟触摸扫描频率和触摸芯片滤波有关。有些I2C触控IC的原始上报数据偶尔会冒出几个异常点需要在读取函数里做简单的滤波比如连续采样三次取中间值或者丢弃偏离上一次坐标过大的点。这个可以在底层处理不要依赖UI层去做滤波。6.4 启动即HardFault内存布局和水深问题HardFault是移植过程中最容易让人崩溃的问题。我遇到过两种情形一种是TouchGFX生成的全局对象太大内部RAM放不下链接时就报错另一种是启动了TouchGFX的帧缓冲拷贝任务但SDRAM还没初始化程序一写帧缓冲就触发总线错误。解决这两种问题都要回归内存布局。确认启动代码把MPU配置正确确认SDRAM初始化函数在main里被最早调用确认链接脚本里TouchGFX缓冲区的地址落在SDRAM映射范围内。另外断言的调试方法很管用在HardFault_Handler里查看栈回溯找到触发地址和指令就能定位到是哪个模块在乱写内存。6.5 帧率上不去或者画面撕裂帧率上不去时优先查看DMA2D是否真的参与了渲染。TouchGFX的Box渲染和ImageCopy默认会走DMA2D但如果HAL配置里没使能DMA2D框架就会退化为软件逐像素渲染性能直接掉一个数量级。画面撕裂则跟缓冲切换的时机有关。TouchGFX双缓冲模式下需要在LTDC的垂直消隐期间切换前台缓冲区否则扫描到中途切换就会出现撕裂。F746的LTDC在CubeMX中可以配置VBLANK中断TouchGFX的HAL类中通常已经实现了对VBLANK中断的等待我们要确保这个中断在NVIC中使能且回调函数被正确调用。这里我建议把DMA2D的传输完成中断优先级和LTDC的VBLANK中断优先级都配置得足够高避免被其他任务抢占导致切换时机错失。7. 性能调优思路与后续扩展经验屏幕跑通触摸能用这只能算完成了80%的工作。真正决定产品体验的是帧率和稳定度。对F746这种级别的芯片TouchGFX完全有能力跑到30帧以上甚至60帧前提是渲染策略得当。我在这块板子上做的第一个优化是图片资源压缩。TouchGFX支持L8格式纹理很多不透明的界面图片用L8索引色保存相比ARGB8888可以减少接近75%的内存占用和带宽消耗。Designer里可以配置图片的格式在内存紧张的嵌入式场景里这个选项非常值得一试。然后是减少无效重绘。TouchGFX的局部刷新机制会自动计算脏矩形但UI逻辑如果写得不好比如频繁触发全屏无效操作局部刷新也会退化为全屏刷新。我在实现动画时尽可能只用控件的移动和透明度变化不涉及大量像素级别的重绘。DMA2D的调度也很关键。当页面里有大量图片切换的时候DMA2D队列可能被长时间占满此时如果还往队列里塞新的传输请求就会阻塞UI线程。应对策略是降低图片切换的频率或者预先将常用图片加载到SDRAM中避免每次从Flash读取。关于后续扩展这块板子还可以尝试TouchGFX生成的字体管理和多语言切换配合片外Flash存储字库可以实现中文、英文等多种语言动态切换。另外把FreeRTOS真正跑起来让UI任务、触摸任务、业务逻辑任务分开整个系统的实时性会明显提升。最后说点实在的。这次移植让我对TouchGFX“半自动”的本质有了更清楚的认识它把UI层的工作做得非常傻瓜化但底层硬件适配的每一环还是得自己亲力亲为。官方文档和模板代码可以解决90%的问题剩下10%的坑都藏在外设时序、Cache一致性、中断优先级和内存布局的细节里。如果你也在做类似的移植建议每完成一个步骤就测试一个步骤不要等全部配置完再上电。一点一点点亮屏幕、点亮SDRAM、点亮DMA2D最后再把TouchGFX接进来整个过程会顺畅很多排查问题时也能快速锁定范围。