ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32H743与TouchGFX工程全解析:底层驱动到MVP架构

STM32H743与TouchGFX工程全解析:底层驱动到MVP架构

简介:这是一套正点原子STM32H743IIT6的CubeMX与Touchgfx联合工程,面向需要快速搭建嵌入式图形界面的开发者,尤其适合熟悉STM32但初次接触Touchgfx的人群。H743基于Cortex-M7内核,性能强劲,工程选用4.3英寸800x480液晶屏,并完成触摸驱动移植,可支撑动画、菜单、图标等复杂2D交互界面。压缩包共1172个文件,约71.44MB,包含大量hpp、h、cpp源码,a、lib静态库,uvprojx、ioc、uvoptx等工程配置,以及o、crf、hex、axf等编译中间文件与最终固件,目录结构完整,便于导入CubeMX或Keil后直接查阅和继续开发。目前已有2223人学习下载,通过学习该工程,可以掌握HAL库外设配置、Touchgfx界面布局与事件处理、触摸屏驱动适配等关键环节,减少从零搭建环境的时间,适合工业控制、消费电子等领域的GUI开发参考。

1. 从一块 H743 开发板说起:TouchGFX 工程到底解决了什么问题

很多人拿到带有 4.3 寸 RGB 屏的 STM32H743 开发板,第一件事就是想跑一个 TouchGFX 的 Demo,结果卡在 CubeMX 和 TouchGFX Designer 两个工具怎么配合上:谁先生成、代码生成到哪个目录、重新生成会不会覆盖手写的逻辑,全是黑匣子。这套工程的价值就在于把 touchgfx stm32h743 的完整链路一次性打通了——屏幕显示、电容触摸、帧缓冲、FreeRTOS 调度全部接好,打开工程烧进去,默认画面就能在屏上跑起来。它适合两类人:一是想快速验证 TouchGFX 效果、不愿意从空白工程一步步配 LTDC 和触摸驱动的嵌入式 UI 新手;二是准备做产品界面,想直接拿这套骨架换成自己画面的进阶开发者。后面我会按工程结构、核心代码、编译下载、避坑和二次开发五个部分把它拆透。

2. 工程结构拆解:H743 + RGB 屏幕的 UI 工程骨架

H743IIT6 是一颗 Cortex-M7 内核、主频 480MHz 的芯片,片内 2MB Flash、1MB RAM,自带 LTDC 液晶控制器和 DMA2D 图形加速器。这意味着它不需要外接控制器芯片,RGB 接口屏幕可以直接挂在 LTDC 上,TouchGFX 的帧缓冲通过 DMA2D 搬运,CPU 占用很低。这套工程就是把上述外设全部初始化好,再叠加上 TouchGFX 的 GUI 框架,是一个典型的高端嵌入式 UI 骨架。

2.1 硬件资源分配:引脚、时钟、外设一览

先看外设分配。打开工程根目录的.ioc文件(用 CubeMX 打开),能看到以下资源被占用:

外设引脚/通道在工程中的用途
LTDC 数据线LCD_R[7:0]、LCD_G[7:0]、LCD_B[7:0]RGB888 并行数据输出到屏幕
LTDC 控制线HSYNC、VSYNC、DE、PCLK行同步、场同步、数据使能、像素时钟
触摸 I2CI2C1_SCL / I2C1_SDA读取 GT911 触摸芯片坐标
触摸中断一个 GPIO 的 EXTI 外部中断有触摸事件时通知 MCU 读取坐标
背光控制一个 TIM 通道的 PWM调节屏幕亮度
SDRAMFMC 接口,地址映射到 0xC0000000帧缓冲和 GUI 缓存的主力内存

这里最容易被忽略的是背光和触摸中断。背光如果不初始化,屏幕会亮但是暗到几乎看不见,很多人会误以为是 LTDC 时序配错了;触摸中断如果漏配,TouchGFX 里的触摸事件就只能靠轮询,画面流畅度会明显下降。

时钟配置上,LTDC 的像素时钟来源于 PLL3Q。以这块板子常见的 480x272 屏幕为例,60Hz 刷新率下像素时钟大约需要 9MHz~10MHz;如果是 800x480 的屏幕,这个值要到 33MHz 左右。CubeMX 时钟树里能看到 LTDC 的时钟频率,改屏幕分辨率时第一件事就是回来重新算这个值,否则会出现画面偏移或者完全无显示。

2.2 目录结构:TouchGFX 生成的代码与手写代码的分界

整个工程目录结构大致如下:

项目根目录 ├── Core/ │ ├── Inc/ main.h、stm32h7xx_it.h 等 │ └── Src/ main.c、stm32h7xx_it.c、stm32h7xx_hal_msp.c ├── Drivers/ │ ├── CMSIS/ │ └── STM32H7xx_HAL_Driver/ ├── TouchGFX/ │ ├── target/ HAL 层平台实现,和芯片绑定的代码 │ ├── generated/ Designer 自动生成的字体、文本、图片缓存 │ ├── gui/ Model/View/Presenter 三层的源码 │ └── app/ touchgfx_app 入口,main 函数调用它 ├── Src/ 老版本 CubeMX 的布局,新工程多在 Core 下 ├── STM32H743IITX_FLASH.ld └── 工程名.ioc

理解这个结构的关键是分清「谁管哪块」:TouchGFX/gui是业务代码,你要写的界面逻辑都在这里;TouchGFX/generated是 Designer 自动生成的,包括字体位图、文本字典、缓存,不要手改;TouchGFX/target是平台适配层,负责把 TouchGFX 的 HAL 抽象接口映射到 STM32H7 的 LTDC、DMA2D 上。很多二次开发翻车,都是因为改了 generated 里的文件,下次 Designer 一生成就被覆盖了,白改。正确做法是把自定义代码写到gui/src下自己新建的.cpp文件里,或者放在 View/Presenter 的 USER_CODE 保护区段内。

2.3 生成流程:CubeMX 与 TouchGFX Designer 的配合方式

这套工程能跑起来,依赖一个固定的生成顺序。先打开.ioc文件,在 CubeMX 里确认 LTDC、FMC SDRAM、I2C、触摸中断、背光 PWM 都已经配置完成,然后生成底层代码;接着打开 TouchGFX Designer,它会识别到 CubeMX 生成的工程信息,自动匹配屏幕分辨率和色彩深度,你在 Designer 里拖好控件、点击 Generate Code,UI 代码就生成到了TouchGFX目录下。

CubeMX 重新生成会不会影响 TouchGFX 的代码?这套机制设计得还算聪明:TouchGFX 在target目录下生成的.c/.h文件带有特殊标记,CubeMX 会把它们识别为用户代码,重新生成时不会清除。反过来,如果先在 TouchGFX Designer 里改了界面,再回 CubeMX 里重新生成底层,生成的代码会重新编译进Core/Src,但不会去动TouchGFX目录下的文件。所以实际操作中,两个工具可以来回切换,只要不手动去删 TouchGFX 目录就行。

3. 核心模块代码走读:屏幕初始化、触摸上报与 MVP 框架

工程跑起来后,从按电源键到屏幕出现画面,中间经历了三个关键环节:LTDC 初始化让屏幕亮起来、触摸驱动把手指坐标上报给 TouchGFX、MVP 三层结构把业务逻辑和界面渲染解耦。逐个拆开看。

3.1 LTDC 初始化:屏幕从黑到亮的全部配置

屏幕显示的底层是 LTDC,它负责把帧缓冲里的像素数据按设定的时序输出到屏幕。初始化代码在 CubeMX 生成的ltdc.c里,核心是配置 Layer 的参数:

/* 以 480x272 的 RGB 屏为例 */ LTDC_LayerCfgTypeDef pLayerCfg; pLayerCfg.X0 = 0; pLayerCfg.Y0 = 0; pLayerCfg.X1 = 480; pLayerCfg.Y1 = 272; pLayerCfg.PixelFormat = LTDC_PIXEL_FORMAT_RGB565; /* 每个像素 16bit,省带宽 */ pLayerCfg.FBStartAdress = (uint32_t)frameBuffer0; /* 帧缓冲地址,指向 SDRAM */ pLayerCfg.Alpha = 255; pLayerCfg.Alpha0 = 0; pLayerCfg.BlendingFactor1 = LTDC_BLENDING_FACTOR1_CA; pLayerCfg.BlendingFactor2 = LTDC_BLENDING_FACTOR2_CA; pLayerCfg.ColorFrameBuffer = 0x00000000; pLayerCfg.FixedColor = 0x0000000; HAL_LTDC_ConfigLayer(&hltdc, &pLayerCfg, 0);

X0/Y0/X1/Y1定义了显示窗口,必须和屏幕分辨率一致,如果这里写 800x480 而屏幕物理分辨率是 480x272,画面会只显示左上角一块。PixelFormat选择 RGB565 意味着每个像素占 2 字节,屏幕一帧 480x272 需要约 261KB 缓冲;如果选 ARGB8888,每像素 4 字节,带宽翻倍,但对颜色精度要求高的场合值得。FBStartAdress指向帧缓冲起始地址,这套工程里一般指向 SDRAM 的 0xC0000000 区域,因为 H743 片内 RAM 虽然不小,但双缓冲 GUI 后剩余空间就不够用了。

值得注意的一点:LTDC 的 PixelFormat 必须和 TouchGFX Designer 里设置的色彩深度匹配。如果 Designer 里选了 ARGB8888,而这里配了 RGB565,画面会出现明显的偏色或者是高频闪烁,这类问题排查起来很隐蔽。

3.2 触摸链路:GT911 从 I2C 中断到 View 回调

触摸芯片是电容屏常用的 GT911,走 I2C 接口。手指按下时,GT911 的 INT 引脚拉低,触发 GPIO 外部中断。在中断回调里读取触摸坐标,然后上报给 TouchGFX 的 HAL。核心读取逻辑如下:

/* 在 HAL_GPIO_EXTI_Callback 里,判断是触摸 INT 引脚后执行 */ uint8_t buf[8]; /* 0x814E 是 GT911 的坐标数据寄存器起始地址,16bit 地址 */ HAL_I2C_Mem_Read(&hi2c1, 0x5D << 1, 0x814E, I2C_MEMADD_SIZE_16BIT, buf, 8, 20); /* buf 格式:status(1) + 点1X_L/H(2) + 点1Y_L/H(2) + 点2X_L/H(2) + 点2Y_L/H(2) */ uint16_t x = (buf[3] << 8) | buf[2]; uint16_t y = (buf[5] << 8) | buf[4];

这里有两个容易踩的细节。一是 GT911 的 7bit 地址有 0x14 和 0x5D 两种,具体由复位时 INT 引脚的电平决定,不同批次的屏幕模组可能不一样,很多工程直接写死一个地址,换一块屏就触摸失灵了。二是0x814E这个寄存器地址是 GT911 的数据缓冲区起始位置,每次读取后芯片会清空 buffer 状态,如果读取时序太快,可能读到上一次的残留数据。这套工程里通过中断方式触发读取,配合一个轻量的去抖标志位,基本能避免这个问题。

触摸坐标读到后,通过 TouchGFX 的HAL::touchEvent接口上报,TouchGFX 内部会把屏幕物理坐标映射到逻辑坐标,然后分发到当前活动的 View。所以应用层拿到的是一个已经处理好的坐标事件,不需要关心 I2C 细节。

3.3 MVP 三层:View、Presenter、Model 如何串起界面逻辑

TouchGFX 的架构严格遵循 MVP 模式。View 负责绘制和接收用户输入,不直接操作业务数据;Presenter 是中介,监听 View 的事件并调用 Model 的接口;Model 持有业务数据,变化时通过 listener 通知 Presenter。以这套工程默认 Demo 里的一个按钮点击为例:

/* Screen1Presenter.cpp - Presenter 层 */ void Screen1Presenter::onButtonClicked() { /* View 回调进来,先更新 Model 里的业务数据 */ model->setTemperature(28.5f); /* Model 数据变化后,需要通过 View 刷新画面 */ view->showStatus("已保存"); }

这个调用的触发顺序是:用户在屏幕上点击按钮 → View 的buttonClicked信号触发 → View 调用presenter->onButtonClicked()→ Presenter 改 Model,再调用view->showStatus()更新界面。Model 和 View 之间不直接通信,都通过 Presenter 中转。

跨界面传数据是 MVP 里比较绕的地方。比如第一屏设置了一个参数,第二屏要读到这个参数。做法是在 Model 里保存状态,然后通过ModelListener广播给所有注册的 Presenter,每个 Presenter 决定自己要不要响应。这套工程里常见的模式是在Model::tick()里做轮询检测,数据变化时调listener->sampleDataChanged(),各屏的 Presenter 按照自己的逻辑处理。理解了这个数据流,后面改界面时就不会出现在 View 里直接 new 一个 Model 这种破坏架构的写法了。

4. 编译、下载与调试:让工程在你的板子上跑起来

拿到工程源码,下一步就是把它编译烧录到板子上。这套工程涉及两个代码生成工具和至少三种可用的编译器,环节里稍微有个版本错位就容易出幺蛾子。

4.1 工具链准备:CubeMX、TouchGFX Designer 与编译器版本

需要准备的工具如下:STM32CubeMX(用于打开.ioc文件、重新生成底层代码)、TouchGFX Designer(用于可视化编辑界面和生成 UI 代码)、以及三选一的编译器:STM32CubeIDE(自带 GCC 工具链)、Keil MDK、IAR EWARM。从工程源码直接编译的角度来说,STM32CubeIDE 最省事,导入工程后可以直接构建,不需要额外配置头文件路径;Keil 工程在.uvprojx里已经配好了大部分路径,但要注意安装的器件支持包版本要和固件包匹配;IAR 的.ewp工程文件同样可以直接打开,但 IAR 对中文路径的支持比较差,工程路径里尽量不要出现中文和空格。

版本匹配方面,TouchGFX Designer 生成的代码对 CubeMX 版本有一定要求。理论上 TouchGFX 4.x 生成的工程配套 CubeMX 6.x 使用,打开.ioc后 CubeMX 会根据文件里的元数据自动识别外设配置,即使版本不完全一致也能正常生成,但强烈建议保持两个工具都更新到较新版本。如果编译时遇到touchgfx头文件找不到,优先检查 TouchGFX 组件的安装路径是否被 IDE 正确识别,而不是急着去改代码。

4.2 编译参数与链接脚本:堆栈、帧缓冲位置怎么调

编译阶段最常调整的文件是链接脚本STM32H743IITX_FLASH.ld。H743 的 2MB Flash 足够放下代码和字库,但帧缓冲和 GUI 的缓存必须放到外部 SDRAM,否则即便编译通过,运行时也会因为内存不足而 hardfault。链接脚本里与 SDRAM 相关的部分长这样:

/* 在 MEMORY 区域定义外部 SDRAM,H743 的 FMC 把它映射到 0xC0000000 */ SDRAM (rwx) : ORIGIN = 0xC0000000, LENGTH = 8M /* 将帧缓冲符号固定到 SDRAM 起始地址 */ . = ALIGN(4); _fb_start = ORIGIN(SDRAM);

如果换了一块容量更大的 SDRAM 板子,要同步改LENGTH值;如果板子上根本没有外挂 SDRAM,那只能把帧缓冲放到片内 AXI SRAM(0x24000000 区域,H743 有 512KB AXI SRAM),但要精打细算——TouchGFX 双缓冲 + 图形缓存很容易超。一个常见的经验值供参考:

屏幕分辨率像素格式单帧缓冲大小双缓冲总大小
480x272RGB565261 KB522 KB
480x272ARGB8888522 KB1.04 MB
800x480RGB565768 KB1.5 MB
800x480ARGB88881.5 MB3 MB

H743 片内 1MB RAM 在 800x480 ARGB8888 双缓冲下是不太够的,所以这套工程把帧缓冲放在 SDRAM 是必然选择。改链接脚本后,记得在代码里确认frameBuffer0的地址和_fb_start一致,否则 LTDC 从错误地址取像素,表现出来的就是花屏。

4.3 下载调试:H743 内部 Flash 与外部 QSPI 的两种玩法

程序编译出来是.elf或.hex,H743 的 2MB 内部 Flash 足够放下整套 GUI 程序和资源,直接用 ST-Link 或 J-Link 烧录即可。调试时推荐开启 SWV(串口线查看器),把 TouchGFX 的日志输出重定向到printf,这样运行时可以实时看到帧率、内存占用和断言失败信息。TouchGFX 的HAL::setTaskDelay相关的调度日志也能通过这种方式打印出来,排查卡顿非常有用。

当产品图片资源多到 Flash 放不下时,就需要把资源放到外挂的 QSPI Flash 里。QSPI 在 memory-mapped 模式下映射到 0x90000000 地址,TouchGFX 的Bitmap可以直接从该地址读取图片数据而不需要拷贝到 RAM,这样 2MB 内部 Flash 只放代码,几十 MB 的图片资源放 QSPI。但这个模式下有个限制:QSPI Flash 的读取速度远低于内部 Flash,如果界面切换时大量加载大图,会出现明显的加载延迟。缓解办法是把最常用的图片资源作为内部 Flash 的一部分,不常用的放外部,TouchGFX 的资源分配表里可以逐项指定存储位置。

5. 避坑与排查:TouchGFX 工程最常见的五个翻车现场

这套工程跑通是一回事,改出自己的东西是另一回事。下面这五个问题是我在实际调试和帮别人排查时遇到频率最高的,每一个都值得记下来。

5.1 现象:上电后花屏或图像错位

烧录完程序,屏幕亮了但画面像被撕碎一样,颜色不对或者图像明显偏移。原因基本逃不出两个方向:帧缓冲地址不对,或者像素格式不匹配。帧缓冲地址不对多数发生在改过链接脚本之后,frameBuffer0指向了未初始化或者被别的变量占用的内存区域;像素格式不匹配则是 LTDC 层配置和 TouchGFX Designer 设置不一致,比如 LTDC 配了 RGB565,Designer 生成代码时选的 ARGB8888。解决方法是先在 Designer 里确认识别到的色彩深度,再打开ltdc.c核对LTDC_LayerCfgTypeDef里的PixelFormat字段,两者保持一致;同时确认帧缓冲地址 32 字节对齐,DMA2D 搬运数据时对对齐有硬性要求,不对齐会出现随机错位。

5.2 现象:GT911 触摸完全无响应

程序跑起来,屏幕显示正常,但手指点上去没任何反应。先排查 I2C 通信:用示波器或者逻辑分析仪抓 SCL/SDA,看复位后 GT911 有没有 ACK 应答。GT911 的 I2C 地址有 0x14 和 0x5D 两种,由复位时 INT 引脚的电平决定,不少屏幕模组出厂时已经用硬件电阻固定了地址,但工程代码里写死的却是另一个。解决方法是写一个扫描程序,回环遍历两个地址,看哪个地址有 ACK,然后改掉工程里的地址常量。另一个高频原因:触摸中断引脚没有配置成下降沿触发,或者中断回调函数里没有正确调用HAL_GPIO_EXTI_IRQHandler,导致坐标读取函数根本没有被执行到。

5.3 现象:链接时报告 Flash 溢出

工程编译到最后一步,提示region FLASH overflowed by ... bytes。H743 的 2MB Flash 看起来很大,但 TouchGFX 的字体、图片转成 C 数组后体积膨胀得很厉害,尤其是一套包含多字号多语言字库的界面,轻松吃掉几百 KB。解决思路有三个:在 Designer 里把不需要的字体和图片资源删掉,优先压缩图片为 L8 格式或 RGB565 减少体积;把大尺寸资源放到外部 QSPI Flash;最后才是考虑压缩字库。很多人在这一步直接加大链接脚本的 Flash 区域,这不是解决办法,因为芯片物理容量就那么大,超出的部分烧录时会被拒绝。

5.4 现象:画面撕裂,横向断层

屏幕刷新过程中出现一条横向的分界线,分界线上下画面不一致,动态切换界面时尤其明显。原因是 LTDC 正在从帧缓冲 A 读取数据输出到屏幕时,TouchGFX 已经把新的画面写入了帧缓冲 A(或者切换了显示地址),于是同一帧画面上下来自不同时间点的渲染结果。解决思路是启用 TouchGFX 的 VSync 同步机制:LTDC 的 Line Interrupt 在每次扫描到指定行时触发中断,TouchGFX 在这个中断里切换到新渲染完成的缓冲,保证屏幕扫描和缓冲切换同步。具体到代码里,需要确认HAL::getInstance()->vSync相关的回调被正确实现,并且 LTDC 的 Line Interrupt 中断优先级要足够高,否则切换指令被延迟执行,照样撕裂。这套工程里默认配置了 VSync,但如果你换过屏幕分辨率,Line Interrupt 的触发行号要跟着改。

5.5 现象:CubeMX 重新生成工程后,UI 代码神秘消失

在 CubeMX 里改了一个引脚配置,点击生成,再编译时发现 TouchGFX 里自己写的逻辑全没了,或者报一堆找不到符号的错误。原因几乎可以断定是手写代码没有放在保护区内。CubeMX 在重新生成时会保留所有位于/* USER CODE BEGIN */和/* USER CODE END */之间的代码,其他位置的代码会被视为生成产物直接覆盖。TouchGFX 的 View 和 Presenter 源码里同样有类似的保护段,写在保护段外面,重新生成时就会被冲掉。解决方法是养成习惯:所有手写逻辑都放进 USER_CODE 区间,或者像前面提到的,把自定义逻辑放到独立的.cpp文件里,只把声明留在 View/Presenter 的头文件中。从那以后我每次改完.ioc都会先确认手写代码的位置,避免一晚上的工作被一次生成清空。

6. 二次开发实战:把默认 Demo 改成你自己的产品界面

跑通默认工程只是第一步,真正的工作量在改界面。这里按从易到难的顺序讲三个改造手感和验证方法。

最直观的改动是替换启动画面和 Logo。进入 TouchGFX Designer,左侧资源列表里找到assets/images下的启动图,直接导入你自己的图片,注意保持分辨率一致或者开启缩放。生成后 Designer 会自动更新图片缓存,不需要动任何代码。这是最快的改造,适合刚接触 TouchGFX 的人建立信心。

再进一步是加自定义控件。以实时温度曲线为例:在 Designer 里新建一个 Custom Widget,生成代码后,在它的draw()方法里用Canvas或者Buffer绘制曲线。核心逻辑是在 Model 里保存最近 N 个温度采样点,每次采样后触发 View 刷新,draw()里根据采样点坐标逐点连线。这样做的好处是曲线渲染走 DMA2D 加速,CPU 占用极低;代价是自定义控件的绘制逻辑要自己维护,TouchGFX 自带的Graph控件虽然能用,但可定制性差一些。

最后是帧率与内存的平衡。默认工程在 480x272 分辨率下跑 60fps 很轻松,但如果换到 800x480 的屏幕,双缓冲 ARGB8888 就要 3MB 内存,帧率也会掉。比较务实的验证方法是:先改用 RGB565 看颜色是否满足产品需求,同时把 Designer 里不用的控件动画删掉,再用HAL::getInstance()->getFrameRate()打印实际帧率。这套工程的优势在于硬件已经按高性能方案设计好了,你只需要在画质和内存之间做取舍,而不是从零开始调底层。

这个工程拆完,给我留下最深印象的是它的模块边界清晰:CubeMX 管底层、Designer 管界面、Model/Presenter 管逻辑,各有各的地盘。从那以后我再接任何 TouchGFX 项目,都会强制先花半小时把.ioc和TouchGFX目录的边界画清楚,搞清楚哪些代码会被工具覆盖、哪些是长期保留的,然后再动手改界面。希望这个拆解能帮你在自己的板子上少走几步弯路。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进