STM32F746上基于CubeMX和TouchGFX的GUI移植实战指南
我先把Final坑踩完再放出正文。直接给结果。
1. 为什么用CubeMX搭TouchGFX:这套组合解决什么问题
STM32F746Discovery这块板子,很多人买回来第一件事就是点个灯、跑个RTOS,然后就吃灰了。其实这块板子最值钱的地方在于板上集成了4.3寸480x272的RGB屏、SDRAM、QSPI Flash、音频Codec,天生就是做图形界面(GUI)的料。但真要把TouchGFX跑起来,很多人在第一步就卡住了:TouchGFX工程到底怎么和HAL库关联?时钟和显存配置怎么弄?LTDC和DMA2D又是什么关系?网上资料大多是零散的,要么只讲CubeMX配置,要么只讲TouchGFX Designer画界面,很少有人把整条链路串起来讲清楚。
这篇文章就是我实际在F746Discovery上从零到一完成TouchGFX移植的全过程记录,**基于STM32CubeMX 6.x + TouchGFX Designer 4.2x + MDK-ARM(或STM32CubeIDE)**的成熟组合,一步步带你把工程跑起来。无论是刚接触TouchGFX的新手,还是已经能跑Demo但想搞懂原理的人,都可以参考这套流程。
先说结论:用STM32CubeMX做底层初始化,用TouchGFX Designer做UI设计,两个工具通过.ioc文件互联,是目前官方推荐也是坑最少的一条路。你不需要手写LTDC初始化,不需要手动配FMC访问SDRAM,这些CubeMX都能生成,你只需要明白每个配置选项背后的意义,出问题时才知道去哪里排查。
环境版本上我强烈建议使用最新稳定版。TouchGFX从4.10开始版权免费开放,4.18之后基本不需要再关心License文件的问题;STM32CubeMX 6.10以上自带TouchGFX Generator插件,不需要额外装别的乱七八糟的东西。MDK和CubeIDE二选一即可,我这次用MDK演示,CubeIDE流程几乎一样。
2. STM32CubeMX工程配置:把底层基础打牢
2.1 新建工程与芯片选型
打开STM32CubeMX,点击“New Project”,在Board Selector选项卡里输入STM32F746G-DISCOVERY,直接选中这块板卡。为什么要用Board Selector而不是MCU Selector?因为官方板定义文件里已经帮你预设了板上外设的默认引脚(比如LCD的LTDC引脚、SDRAM的FMC引脚、触摸的I2C引脚),不用自己对着原理图一个个找。
选中之后CubeMX会自动加载板卡的默认配置,左侧Pinout & Configuration区域你会看到很多外设已经被勾选。此时先不要急着改,先把工程名和存放路径设置好。Project Name建议用小写字母和下划线,比如touchgfx_f746_demo,路径不要带空格和中文,否则后面MDK编译会出一些莫名其妙的问题。
点击右上角GENERATE CODE前,建议先做三件事:设置好工具链(Toolchain/IDE选择MDK-ARM V5.32或STM32CubeIDE)、确认固件包版本已安装、确认TouchGFX Generator组件可用。如果左侧列表中看不到TouchGFX Generator,说明CubeMX版本太老或者安装时没有勾选TouchGFX组件,这个插件是CubeMX内置的,不需要单独下载,但部分精简安装包会把它裁掉,建议直接到ST官网下载最新版CubeMX重装。
2.2 时钟树配置:80MHz还是216MHz,别拍脑袋定
STM32F746最高主频216MHz,但TouchGFX的官方Demo普遍跑在200MHz左右,不是216MHz,为什么?因为TouchGFX示例工程为了兼顾低功耗和稳定性做了折中。不过我们自己移植,完全可以用216MHz,只要电源配置正确(VOS scale 1、Overdrive使能),稳定性没问题。我实测216MHz下跑复杂动画没有异常。
重点在于配置方法:在Clock Configuration页面,输入HSE为25MHz(Discovery板载晶振就是25MHz),然后配置PLL。关键路径是PLLCLK -> SYSCLK,而LTDC的像素时钟(LCD-TFT)也是从PLL的另一个输出分频得到的,所以PLL配置直接决定屏幕刷新率。
我的实际配置如下:
- HSE:25MHz
- 主PLL:PLLM = 25,PLLN = 432,PLLP = 2,PLLQ = 9,PLLR = 7
- SYSCLK = 25 / 25 * 432 / 2 = 216MHz
- 总线分频:AHB = /1,APB1 = /4,APB2 = /2
- LTDC像素时钟 = 25 / 25 * 432 / 9 = 48MHz(这里用到的是PLLQ,不是PLLP)
有人会问PLLQ为什么是9?因为480x272的RGB屏,行场消隐加一起大约需要16.7MHz到20MHz的像素时钟,但TouchGFX内部渲染可能还需要一些余量。48MHz不是必须的,如果你用的是其他分辨率屏幕,需要根据屏的Datasheet算像素时钟。我试过24MHz也能出画面,但刷新率会明显低,触摸跟手性变差,所以还是按官方推荐配置来。
注意:LTDC像素时钟不是越高越好。RGB接口的屏幕对时钟上限有要求,超过了会花屏或黑屏。F746Discovery原装屏RK043FN48H的典型像素时钟是9.6MHz,但TouchGFX示例工程用到了更高频率也没问题,因为内部有FIFO缓冲。
2.3 外设初始化:LTDC、FMC-SDRAM、I2C触摸、DMA2D
在Pinout页面,按以下清单核对并启用外设:
LTDC(LCD-TFT):这是显示的核心外设。配置里需要设置:
- 水平同步宽度(HSYNC):40像素
- 垂直同步宽度(VSYNC):9行
- 水平前沿(HBP):30像素
- 垂直前沿(VBP):15像素
- 水平后沿(HFP):30像素
- 垂直后沿(VFP):15像素
这些参数是屏厂规格书里给的,不能随便改,否则画面会偏移或者闪屏。颜色格式选RGB565还是RGB888?TouchGFX支持RGB565和ARGB8888两种主流格式。RGB565显存占用减半,但色彩有损失;ARGB8888色彩好,但显存翻倍。F746Discovery带有8MB SDRAM,跑ARGB8888毫无压力,我建议直接选ARGB8888。
FMC(外部存储控制器):SDRAM配置为16位数据宽度、13位行地址、11位列地址、4个Bank。时序参数(TRCD、TRP、TRC等)需要参考SDRAM芯片手册(IS42S16400J),但TouchGFX生成的代码里其实会自动带一份合适的配置,前提是你在CubeMX里把SDRAM的型号参数选对。这里我要强调一个容易被忽略的点:SDRAM的“Bank数”必须选4,如果选2,虽然也能初始化,但内存访问可能出现随机崩溃。
I2C触摸:Discovery板上的触摸芯片FT5336挂在I2C1上,地址0x38。CubeMX会自动识别板载I2C配置,你只需要确认中断引脚(Touch INT)已启用外部中断。这个中断很重要,TouchGFX依靠它来判断触摸事件,如果不配中断,触摸会变得迟钝甚至完全失灵。
DMA2D(图形加速器):CubeMX不需要单独配置DMA2D,它只是一个挂在AHB总线上的外设,TouchGFX的驱动会直接操作它的寄存器。但是必须确保DMA2D的时钟(AHB1)已经使能,这在CubeMX里默认就是使能的,但如果你手动精简过时钟树,容易把它关掉,那TouchGFX的拷贝和混合操作就会全部退化为CPU搬运,性能直接掉一半以上。
2.4 TouchGFX Generator配置三要素
很多教程到这里就结束了,但移植TouchGFX的关键其实在这一节。在CubeMX左侧列表里找到Middleware and Software Packs -> TouchGFX Generator,配置以下三项:
Application Monitor:建议选择Console,方便通过串口或者IDE的调试控制台查看TouchGFX运行日志。如果选No,出问题时排查成本会很高。不要选GUI Builder的Monitor,那是给模拟器用的。
TouchGFX Generator的归属:建议单独生成到Middlewares目录,并且在生成时勾选“Generate TouchGFX configuration”,这样它会自动生成TouchGFX文件夹,里面包含target和generated两个子目录。target里是硬件抽象层驱动(LCD、触摸、DMA2D的适配代码),generated里是UI框架代码。
Framebuffer策略:这里选择Double buffer(双缓冲)。双缓冲意味着需要两块帧缓冲区的显存,ARGB8888格式下,480x272分辨率一块缓冲区需要480 * 272 * 4 = 522,240字节,约510KB,两块就是约1MB。SDRAM完全放得下。双缓冲的好处是TouchGFX在绘制下一帧时可以同时让LTDC读取上一帧,画面不会撕裂,这是GUI最基本的体验保障。
2.5 生成工程:顺序决定成败
生成顺序有个讲究:先单独生成一次CubeMX工程,确认编译通过,然后再用CubeMX联调TouchGFX。如果你第一次生成就把TouchGFX Generator打开,CubeMX会尝试当场调用TouchGFX Designer,如果此时TouchGFX Designer没安装或者版本不匹配,生成会报错。所以新手建议分两步:
第一步:TouchGFX Generator选择No,先生成普通工程,用MDK打开编译,确保HAL层没问题。
第二步:回到CubeMX,把TouchGFX Generator打开,配置好之后再次生成。此时CubeMX会在工程里插入TouchGFX的相关代码,并自动调用TouchGFX Designer生成一个默认的UI工程。
这里有个非常坑的细节:如果CubeMX生成时报“TouchGFXEngine.dll not found”之类的错误,大概率是TouchGFX Designer没有安装成功或者环境变量没配好。重装TouchGFX Designer到默认路径(C:\ST\TouchGFX\4.21.x)能解决99%的问题。别问我怎么知道的。
3. TouchGFX Designer设计UI并导出工程
3.1 认识TouchGFX Designer:它不是画图工具,是代码生成器
很多人第一次打开TouchGFX Designer,会以为这是一个类似Photoshop的界面设计工具,其实不对。它的核心工作是生成C++代码,界面上的拖拽只是让你以可视化的方式组合控件,最终生成的是一套完整的UI应用代码。
对于F746Discovery,你在Creator阶段只需要关心两块:
- Screens(屏幕):一个应用至少有一个Screen
- Canvas(画布):每个Screen上有哪些控件
我建议第一个实验不要追求花哨,就做一个最经典的“模拟仪表盘”或“按钮 + 文本 + 波形图”组合。原因是控件越简单,出问题的环节越少,先跑通整条链路,再逐步加复杂度。
3.2 创建新工程:目标平台选对没
在TouchGFX Designer中新建工程,选择“STM32F746G-DISCOVERY”模板,此时它会自动加载该板卡的LCD分辨率(480x272)和色彩格式(ARGB8888)。你不要手动修改这些参数,因为CubeMX生成的硬件抽象层是基于这些参数编译的,改了会导致画面偏移。
创建完成后,左侧是控件库,中间是画布,右侧是属性面板。从控件库里拖一个Button和TextArea到画布上。双击文本,可以修改默认文本内容,也可以在Texts选项卡里新增一个Text,关联字体。
字体这里有个大坑:TouchGFX默认不嵌入中文字体,如果你的界面要显示中文,需要在Texts -> Typographies里添加中文字体文件(比如微软雅黑或思源黑体),并设置Unicode范围。加载一个完整的中文字体文件,Flash占用会飙升到几MB,F746的QSPI Flash能装下,但要注意不是所有位置都能存。我建议做实验阶段先用英文界面,把流程跑通后再加中文,别一开始就给自己挖坑。
3.3 添加交互:按钮按下改变文本内容
在TouchGFX Designer里,交互(Interaction)是通过“添加规则”的方式实现的。选中Button控件,在右侧Interactions选项卡里点击“+”,添加一个Click事件,动作选择Set Text,目标选择之前拖进去的TextArea,文本内容选择另一条文本。这样运行时按下按钮,文本就会变化。
这个交互生成的代码在generated/gui_generated/src/screen1_screen/Screen1ViewBase.cpp里,你不需要手动修改Base类文件,TouchGFX在每次重新生成UI时会覆盖generated目录下的所有文件,所以如果你想自定义逻辑,应该改Screen1View.cpp和Screen1Presenter.cpp,而不是Base文件。
3.4 导出工程:让UI代码和HAL代码见面
在TouchGFX Designer里点击右上角的“Generate Code”按钮,它会将生成的UI代码输出到CubeMX工程目录下的TouchGFX/generated和TouchGFX/gui目录。然后在CubeMX里再次点击生成,CubeMX会调用TouchGFX Generator,把UI代码与HAL层代码整合到一起。
如果不是用CubeMX直接生成,而是手动把TouchGFX工程和HAL工程拼在一起,会非常痛苦,涉及到编译宏、链接脚本、启动文件等多个地方的修改。所以这也是为什么我前面强调用CubeMX + TouchGFX Designer的组合,两个工具之间天然兼容,省去大量手工集成。
快速判断UI是否生成成功:打开工程目录下的
TouchGFX/generated/gui_generated文件夹,如果能看到按Screen名称命名的子文件夹,以及generated目录下有fonts、images、texts等子目录,说明TouchGFX Designer已经成功生成了UI资源代码。
4. 代码集成、编译与烧录实测
4.1 工程目录结构解读:搞清楚每一块是干什么的
打开MDK工程后,你会看到左侧文件树里多出了几个分组:
Application/User/Core:CubeMX生成的main.c、stm32f7xx_it.c等Application/User/TouchGFX/target:硬件抽象层,包括TouchGFXHAL.cpp、STM32F7DMA.cpp、LCD相关驱动Application/User/TouchGFX/app_touchgfx.c:TouchGFX的入口初始化函数TouchGFX/generated:UI框架生成的代码,包括文本、图片、字体、交互逻辑TouchGFX/gui:UI业务逻辑,自己改代码的地方
核心入口在main函数里。在main()中,CubeMX会生成一个MX_TouchGFX_Init()调用,以及一个MX_TouchGFX_Process()循环调用。前者负责初始化TouchGFX框架,后者必须放在主循环的while(1)里,不断驱动UI刷新。
/* main.c 中的关键代码片段 */ int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_LTDC_Init(); MX_FMC_Init(); MX_I2C1_Init(); /* 其他外设初始化... */ MX_TouchGFX_Init(); while (1) { MX_TouchGFX_Process(); } }4.2 帧缓冲与内存分配检查
编译前,有一个非常关键的检查点:确认链接脚本中SDRAM的内存区间是否被正确纳入可执行区域。TouchGFX的帧缓冲默认分配在SDRAM中,但SDRAM的地址范围必须在链接脚本中声明为可用内存,否则链接时会出现“Region overflow”或者“Undefined symbol”之类的错误。
F746的FMC-SDRAM映射地址是0xC0000000开始,最大8MB(0xC0000000 - 0xC07FFFFF)。打开MDK工程的.sct文件(分散加载文件),检查是否包含SDRAM区域:
LR_IROM1 0x08000000 0x00200000 { ER_IROM1 0x08000000 0x00200000 { *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) .ANY (+XO) } RW_IRAM1 0x20000000 0x00050000 { .ANY (+RW +ZI) } RW_SDRAM 0xC0000000 0x00800000 { *.o (TouchGFX_Framebuffer) .ANY (+RW +ZI) } }如果你的.sct文件里没有SDRAM区域,需要手动添加。CubeMX自动生成的工程默认不会自动把SDRAM加入MDK的链接脚本,这是一个非常隐蔽的坑。我的做法是:把TouchGFX帧缓冲相关的模块强制放到SDRAM区域,其余变量放在内部SRAM,这样既能充分利用SDRAM大容量,又不会因为SDRAM访问速度慢(大约15ns访问周期)拖累实时性高的代码。
4.3 MDK编译配置:优化等级和C++标准不能乱选
MDK编译TouchGFX工程时,有几个编译选项会直接影响能不能编过、以及跑起来性能如何:
- C++标准:必须支持C++11及以上。TouchGFX 4.x的代码大量使用了
auto、nullptr、右值引用等C++11特性,MDK老版本默认C++98是编不过的。MDK 5.37之后默认就是C++11,老版本请在Options for Target -> C/C++ -> C++ Language里手动选择。 - 优化等级:调试阶段建议选
-O0或者-O1,跑正式性能测试再开-O2或-O3。我遇到过一个现象:-O3下TouchGFX的某个成员函数因为严格别名优化被改写了行为,导致界面卡死,后来改成-O2就好了。所以不是所有代码都适合开最高优化。 - 微库(MicroLIB):不要勾选MicroLIB。TouchGFX的C++运行时需要完整的标准库支持,MicroLIB阉割了部分C++特性,连接时会报大量“undefined reference”。
- One ELF Section per Function:建议勾选。这样链接器可以去掉没有被引用的函数,减小固件体积,同时避免某些永远不会被调到的代码报错。
4.4 烧录与首次运行:看见画面之前还有三道关
编译通过后,用ST-Link(Discovery板载)连接电脑,MDK里选择下载算法为STM32F7xxx Flash,点击Download。如果一切顺利,你会看到以下现象:
第一关:白屏或黑屏。这说明LTDC已经初始化但帧缓冲地址不对。检查TouchGFXHAL.cpp里setFrameBufferStartAddress的地址是否指向SDRAM。TouchGFX默认会计算一个地址,一般是从0xC0000000开始偏移若干字节,如果你在CubeMX里配置了其他用途的SDRAM分区,可能冲突。
第二关:画面有内容但不刷新。这通常意味着TouchGFX的主循环没有跑起来,或者定时器中断配置有问题。F746的TouchGFX依赖一个TIM来产生帧同步和动画时钟,在CubeMX里默认配置了TIM7作为基础定时器,如果你在优化时把这个定时器关掉了,UI会变成“静态图”。
第三关:触摸无响应。先看中断配置。FT5336的触摸中断引脚在CubeMX里默认配置为EXTI13(因为PA13被复用了?不对,这个板子的触摸中断在PI7)。打开stm32f7xx_it.c,确认EXTI15_10_IRQHandler中是否调用了HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_7)。TouchGFX的触摸驱动通过轮询I2C获取触摸点,但需要中断唤醒。
5. 常见问题速查与高性价比调优清单
5.1 编译报错与链接问题速查表
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
No space in execution regions with .ANY selector | 内部SRAM已满,变量没有分配到SDRAM | 在.sct中增加SDRAM区域,将大数组分配到SDRAM |
Undefined symbol TouchGFXGeneratedHAL::getInstance() | TouchGFX Generator未生成HAL代码 | 回到CubeMX确认TouchGFX Generator打开并重新生成 |
fatal error: touchgfx/hal/HAL.hpp: No such file or directory | 头文件路径缺失 | Options for Target -> C/C++ -> Include Paths中添加TouchGFX/target和TouchGFX/generated/include |
Error: L6218E: Undefined symbol __aeabi_assert | 勾选了MicroLIB | 取消MicroLIB选项 |
Error: L6406E: No space in execution regions | 链接脚本Flash区域太小 | 检查芯片型号是否选对,F746是1MB Flash,不要误选512KB型号 |
| 编译通过但烧录后立即HardFault | SDRAM时序配置不合理 | 检查FMC-SDRAM初始化时序,TRCD/TRP/TRC是否过小,适当放宽 |
5.2 界面性能不达标的三个排查方向
如果界面跑起来了,但切换动画卡顿或者触摸延迟明显,优先从这三个方向排查:
第一个方向:DMA2D是否真正参与渲染。打开调试器,在TouchGFXHAL.cpp的blitCopy函数里设置断点,如果断点从未命中,说明TouchGFX没有启用DMA2D加速。检查TouchGFXConfiguration里是否设置了framebuffer策略为双缓冲,并且在HAL初始化时是否调用了setFrameBufferStrategy。
第二个方向:SDRAM访问仲裁。FMC总线上同时挂着LTDC和DMA2D,两者都要频繁访问SDRAM,如果总线优先级配置不当,DMA2D的带宽会被LTDC抢占。在CubeMX的FMC配置里,有一个Memory Bus Priority设置,建议将DMA2D的优先级设为High,LTDC设为Medium。这个细节官方文档里提到过,但很多人没注意到。
第三个方向:CPU主频和Flash等待周期。F7系列在216MHz下,Flash的等待周期需要配置为7个周期,如果等待周期不够,CPU取指会变慢,整个UI线程的渲染效率会下降。打开system_stm32f7xx.c,确认FLASH_ACR寄存器的LATENCY位是否设置正确。
5.3 双缓冲和局部更新的底层原理
TouchGFX性能好的核心在于三个技术:局部更新(Partial Rendering)、脏矩形(Dirty Rectangles)和DMA2D硬件加速。它并不会每帧都重绘整个屏幕,而是只重绘发生变化的区域。
比如你只是把按钮从“按下”状态切回“释放”状态,TouchGFX会计算出按钮所在的最小矩形区域,只刷新这块区域。F746的LTDC支持设置显示窗口的位置和大小,TouchGFX利用这个特性只把新数据写入帧缓冲区中对应的区域。
明白这个原理后,你就能理解为什么画面中的静态部分越多,整体帧率越高。如果你的界面元素大量使用图片而不是实时绘制的向量图形,TouchGFX可以直接搬运像素而不需要CPU逐像素计算,DMA2D在这种场景下能发挥最大优势。
实测经验:一个包含背景图、3个按钮、2个文本和1个波形图的界面,在F746@216MHz + ARGB8888 + 双缓冲下,帧率稳定在55~60fps,CPU占用大约40%。如果不启用DMA2D,帧率会掉到30fps左右。所以DMA2D一定要确认在正常工作。
6. 从能跑到好用:几个值得投入的小优化
当你已经把Demo跑起来,下一步建议做这三件事,投入产出比最高:
第一件事:把“单缓冲”升级为“双缓冲+VSYNC同步”。很多新手跑起来后发现画面有撕裂感(tearing),这是因为LTDC读取帧缓冲和TouchGFX写入帧缓冲没有同步。在TouchGFX Designer的工程设置里,确认Frame buffer策略是Double buffer,然后在main.c里开启LTDC的VSYNC中断,TouchGFX会利用这个中断来同步翻页。配置好后,画面会非常干净。
第二件事:用图片压缩减小Flash占用。TouchGFX支持L8格式的图片,这是一种索引色格式,可以将图片压缩到原来RGB888的1/3大小。如果你的界面里有大面积背景图,用L8格式可以显著减小固件体积,同时性能几乎不受影响。TouchGFX Designer里选中图片资源,在属性面板里把Image Format改成L8,然后重新生成代码即可。
第三件事:开启功耗优化。在不需要刷新画面的时候,可以让TouchGFX进入Block状态,此时可以进入低功耗模式。TouchGFX提供了HAL::setFrameRateCompensation和OSWrappers::waitForVSync等接口,配合STM32的WFI指令,在电池供电的设备上可以节省大量功耗。F746Discovery本身是开发板,功耗优化意义不大,但这个思路在后续做产品时会很有用。
7. 最后再分享一个调式技巧:串口日志的妙用
整个移植过程中,我最推荐大家从一开始就开启TouchGFX的Application Monitor的Console输出。在main.c中添加一个简单的串口重映射(比如用printf重定向到USART),然后TouchGFX内部的所有Assert、错误信息、内存分配失败日志都会通过串口工具输出到电脑上。
很多图形界面上的疑难杂症,比如“画面闪烁”“触摸有时灵有时不灵”“动画突然停顿”,在串口日志里往往能直接看到对应的错误信息。比如有一次我遇到动画卡顿,日志里不停刷[ERROR] Frame buffer underflow,一查是SDRAM带宽不足,增加DMA2D优先级后问题立刻消失。没有串口日志的话,这种问题排查起来真的像大海捞针。
在我做的这些实验里,踩得最惨的坑就是生成顺序和.sct文件的SDRAM分区,这两个问题耗费的时间比写代码还多。所以这篇详细版把这个过程尽量写透,希望能帮你少走这些弯路。如果你在移植过程中遇到了其他奇怪的问题,可以先按这篇文章的步骤重新走一遍,大部分问题都出在配置遗漏或者版本不匹配上。
