1. 开篇:为什么选择STM32H743与TouchGFX?
如果你正在玩正点原子的阿波罗开发板,手里那块STM32H743II核心板性能猛如虎,却还在用简单的LCD显示几个字符和图片,那可真是有点“大材小用”了。现在用户对嵌入式设备的界面要求越来越高,一个流畅、美观、能触摸的GUI(图形用户界面)几乎成了标配。这时候,TouchGFX就该登场了。
TouchGFX是ST官方力推的免费图形框架,专门为STM32这类资源有限的MCU优化过。它最大的好处是“所见即所得”——你用电脑上的Designer工具拖拖拽拽设计好界面,生成代码,烧录进去,效果几乎一模一样。这对于从零开始手写界面逻辑、处理触摸事件的“硬核”开发方式来说,简直是降维打击。我当初第一次用TouchGFX在H743上跑起来一个带动画的界面时,感觉就像给单片机装上了“灵魂”。
那么,把TouchGFX跑在正点原子阿波罗开发板(STM32H743II)上,再配上那块4.3寸的NT35510屏幕和ATK-MD0430触摸屏,整个过程顺不顺利?实话实说,有坑,但踩平了之后就是一条康庄大道。这篇文章,我就把自己从屏幕驱动移植、TouchGFX集成,到最终实现触摸交互的完整实战过程,掰开揉碎了讲给你听。无论你是刚接触TouchGFX的新手,还是正在为驱动适配头疼的老鸟,相信都能找到你需要的那把钥匙。
2. 工程创建与基础配置:为TouchGFX铺好路
万事开头难,但用STM32CubeIDE开头,其实挺简单。我们的目标是创建一个干净、配置正确的HAL库工程,作为TouchGFX生长的土壤。
2.1 新建工程与核心外设配置
首先,打开STM32CubeIDE,选择基于STM32H743II芯片创建新工程。芯片选型时,注意正点原子阿波罗板子用的具体型号。工程建好后,别急着生成代码,先把几个关键的外设和时钟配好。
时钟树配置是性能基石。H743最高主频可达480MHz,但我们通常先跑在400MHz。在Clock Configuration标签页,选择HSE(外部高速时钟)为晶振源(正点原子板子通常是25MHz),然后一步步配置PLL,最终让HCLK达到400MHz。这一步配置错了,后面屏幕刷新、触摸采样都可能出各种奇怪问题。配置完后,记得在Project Manager里把Toolchain/IDE选为STM32CubeIDE,并勾选Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral,这样每个外设的代码都会独立成对的文件,结构清晰,后期修改方便。
调试接口不能忘。在System Core->SYS里,把Debug选为Serial Wire。这样你的ST-Link才能正常连接和调试。
2.2 FSMC配置:连接屏幕的“高速公路”
我们的4.3寸屏通过FSMC(灵活的静态存储器控制器)与MCU通信。你可以把FSMC理解成一条专门连接外部存储设备(比如我们的屏幕,它被映射成一段内存地址)的高速公路。
在Connectivity中找到FSMC,并启用。正点原子的屏幕通常接在Bank 1的NOR/PSRAM 1上。关键参数配置如下表,这些时序参数是根据NT35510芯片手册和STM32H743的时钟计算出来的,直接关系到读写屏幕的稳定性:
| 参数项 | 配置值 | 说明 |
|---|---|---|
| Memory type | LCD Interface | 告诉FSMC我们接的是LCD。 |
| Data | 16 Bits | 我们的屏幕是16位并口。 |
| Address setup time | 3 | 对应FSMC的ADDSET,地址建立时间。 |
| Data setup time | 4 | 对应FSMC的DATAST,数据建立时间。 |
| Access mode | Mode A | 最常用的NOR Flash访问模式。 |
这里有个小细节:Address setup time和Data setup time的单位是HCLK周期。我们的HCLK是400MHz,一个周期就是2.5纳秒。根据NT35510的时序要求,我们需要计算并填入合适的周期数。比如,如果芯片要求地址建立时间大于10ns,那么ADDSET至少需要设置为4(4 * 2.5ns = 10ns)。我实测下来,按照正点原子例程里常用的3和4来配置,在400MHz下非常稳定。
配置完FSMC后,还要在Pinout & Configuration视图里,检查一下FSMC相关的引脚(如FSMC_NE1,FSMC_NWE,FSMC_NOE,FSMC_Ax,FSMC_Dx)是否已经自动分配好。通常CubeMX会自动分配,但检查一下总没错。
2.3 MPU配置:让CPU和DMA高效访问屏幕内存
STM32H743有强大的Cortex-M7内核和DMA,但它们要高效、安全地访问通过FSMC映射的那段屏幕“内存”地址,需要MPU(内存保护单元)来定义这段区域的访问属性。不配MPU,轻则性能低下,重则出现玄学般的显示错误。
在System Core->MPU中,我们需要新增一个区域(Region):
- Base Address:
0x60000000。这就是我们FSMC Bank1的起始地址,屏幕的命令和数据寄存器就映射在这里。 - Size: 通常设置为
64MB,覆盖整个Bank1区域。 - Type:
Normal。 - Cacheability:
Write Through:对于显存这类需要被DMA(TouchGFX的图形加速会用到)和CPU共同访问的区域,Write Through模式能保证数据一致性,避免缓存带来的显示错误。这是非常关键的一步,很多显示花屏、残影问题都源于此。Shareable: 通常选Not Shareable。
- Access Permissions:
Full access。
配置好MPU,CPU和DMA在读写屏幕地址时就知道该怎么做缓存策略了,这是图形流畅显示的基础保障之一。
最后,别忘了配置屏幕的背光控制引脚(比如一个普通的GPIO输出引脚)和片选引脚(如果硬件有独立片选的话)。一切就绪,点击GENERATE CODE生成工程代码。至此,我们为TouchGFX搭建的硬件抽象层(HAL)舞台就准备好了。
3. 底层LCD驱动(NT35510)移植与对接
TouchGFX本身不直接驱动屏幕,它需要一个“搬运工”来把画好的图像数据送到屏幕的显存里。这个搬运工就是我们自己实现的底层驱动。对于NT35510这款屏幕驱动IC,我们需要自己动手。
3.1 编写NT35510基础驱动文件
在工程目录下,我习惯新建一个BSP(板级支持包)文件夹,里面再放一个NT35510文件夹,专门存放屏幕驱动代码。这里需要创建两个核心文件:NT35510.h和NT35510.c。
NT35510.h里主要定义屏幕的寄存器地址、基础操作函数原型。最关键的是两个宏定义:
#define NT35510_COMM_ADDRESS (0X60000000) // 命令寄存器地址 #define NT35510_DATA_ADDRESS (0X60080000) // 数据寄存器地址为什么是0x60080000?这是由FSMC的地址线连接决定的。在正点原子的硬件连接中,通常用一根地址线(比如A18)来区分命令和数据。当A18为0时,访问命令寄存器;为1时,访问数据寄存器。0x60000000是基址,0x80000就是1 << 18(如果A18是第18位地址线)。这个地址一定要和硬件原理图、以及你在CubeMX中FSMC的地址映射对上。
NT35510.c则是具体的实现。核心函数就几个:
nt35510_write_reg/nt35510_read_reg: 向命令/数据地址写入或读取数据,就是往那两个宏定义的地址进行指针操作。nt35510_init: 初始化序列,发送一系列命令让屏幕进入工作状态,比如设置颜色格式(RGB565)、打开显示等。nt35510_caset/nt35510_raset: 设置列(X轴)和行(Y轴)的起始、结束地址。相当于告诉屏幕:“接下来我要往这块矩形区域写数据了”。nt35510_ramwr: 这是最核心的函数,向屏幕的GRAM(显存)写入一块连续的像素数据。
写驱动时,我强烈建议你先抛开TouchGFX,单独测试这个驱动是否能点亮屏幕、画点、画线。你可以写一个简单的测试函数,用nt35510_ramwr把整个屏幕刷成红色或蓝色。这一步通了,后面就成功了一大半。
3.2 实现TouchGFX的显示驱动接口
TouchGFX框架规定了一个固定的“交货”接口。它画好一帧中的某一块区域(比如一个按钮)后,会调用一个函数,把这块区域的像素数据“递”出来,我们的驱动需要接住并送到屏幕上去。
这个接口函数就是touchgfxDisplayDriverTransmitBlock。我们需要在NT35510.c中实现它。它的原型大致如下:
extern "C" void touchgfxDisplayDriverTransmitBlock(const uint8_t* pixels, uint16_t x, uint16_t y, uint16_t w, uint16_t h);参数很好理解:pixels是指向像素数据数组的指针,x, y是这块矩形区域的左上角坐标,w, h是宽和高。
在这个函数内部,我们需要做三件事:
- 调用
nt35510_caset和nt35510_raset,设置屏幕的写入区域为(x, y)到(x+w-1, y+h-1)。 - 调用
nt35510_ramwr,将pixels指向的数据全部写入屏幕。 - 调用一个回调函数
DisplayDriver_TransferCompleteCallback(),通知TouchGFX:“数据我送完了,你可以准备下一块了”。这个函数TouchGFX会自动提供。
这里有个大坑需要注意:pixels指针指向的数据格式。TouchGFX默认使用RGB888格式(24位色),但我们的NT35510屏幕通常是RGB565格式(16位色)。直接写入会导致颜色严重错误。因此,我们必须在写入前进行颜色格式转换,或者更简单的方法,在CubeMX初始化TouchGFX时,就告诉它我们使用RGB565格式。我推荐后者,因为转换会消耗CPU时间。在CubeMX的Software Packs->TouchGFX配置里,找到Display设置,将Color format选为RGB565,这样TouchGFX引擎内部就会生成16位的像素数据,我们的驱动就可以直接memcpy了(注意类型转换)。
另外两个接口函数touchgfxDisplayDriverTransmitActive(通常返回0,表示传输非活跃)和touchgfxSignalVSync(用于同步垂直刷新,我们可以在定时器中断里调用它)也需要根据实际情况实现或放置。
把这些接口函数都实现并放在NT35510.c里,然后在main.c的/* USER CODE BEGIN 2 */区域调用nt35510_init()初始化屏幕,在while(1)循环里调用touchgfxSignalVSync()(如果用到的话)。编译下载,如果配置正确,你应该能看到TouchGFX默认的灰色启动界面了!这是激动人心的第一步。
4. 触摸驱动(ATK-MD0430)集成与适配
光有显示还不够,我们还得让屏幕能“摸”。正点原子阿波罗板子通常配套ATK-MD0430电阻触摸屏,其触摸芯片通过I2C通信。我们需要将正点原子提供的触摸驱动库集成进来,并接入TouchGFX的触摸控制系统。
4.1 移植触摸驱动文件
正点原子通常会提供atk_md0430_touch.c/.h和atk_md0430_touch_iic.c/.h这四个文件。我们把它们拷贝到我们的BSP或NT35510文件夹下。这些文件实现了基于GPIO模拟I2C的通信、触摸芯片的初始化和坐标读取功能。
集成时要注意几点:
- 引脚重映射:驱动文件里用宏定义死了I2C的SCL、SDA以及触摸中断引脚(PEN)。你需要根据你的板子原理图,检查这些引脚(
PH6,PI3,PH7,PI8)是否与你的实际连接一致。如果不一致,必须在atk_md0430_touch_iic.h和atk_md0430_touch.h中修改对应的GPIO和Pin定义。 - 时钟使能:驱动里用了
__HAL_RCC_GPIOH_CLK_ENABLE()这样的宏来开启GPIO时钟。在CubeMX生成的工程里,这些时钟通常在main.c的SystemClock_Config函数里已经统一开启了。如果你发现触摸没反应,可以检查一下这部分,或者简单地在驱动初始化函数里保留这些时钟使能语句,双重保险。 - 延时函数:驱动里可能使用了
delay_ms函数。你需要将其替换为HAL库的HAL_Delay,或者自己实现一个基于HAL_GetTick()的毫秒延时。
确保这些底层驱动能独立工作。你可以写个测试程序,在main循环里不断调用atk_md0430_touch_scan,并把读取到的坐标通过串口打印出来。用手点击屏幕,看看坐标值是否随手指移动而变化。这一步调试通了,触摸硬件就没问题了。
4.2 对接TouchGFX触摸控制器
TouchGFX有一个专门的触摸控制器接口类STM32TouchController。我们需要修改位于TouchGFX/target目录下的STM32TouchController.cpp文件。
这个类有两个关键虚函数需要实现:
void init(): 触摸控制器初始化。我们就在这里调用atk_md0430_touch_init()。bool sampleTouch(int32_t& x, int32_t& y): 采样触摸点。这是框架周期性调用的函数,我们需要在这里读取触摸坐标,并赋值给x和y,同时返回true表示有触摸,false表示无触摸。
实现sampleTouch时有几个性能与稳定性要点:
- 防抖与采样率:不要在每个Tick(通常很快)里都去执行完整的I2C读取。可以设置一个软件定时,比如每30-50ms读取一次。这能有效降低CPU占用并防止误触发。我常用
HAL_GetTick()来计算时间间隔。 - 坐标处理:
atk_md0430_touch_scan读出来的是触摸芯片的原始坐标。这个坐标系的X、Y范围以及方向,可能与你的屏幕物理坐标系(以及TouchGFX的UI坐标系)不一致!你可能需要进行缩放、偏移甚至X/Y轴交换。这部分校准逻辑最好放在sampleTouch函数里,或者封装在触摸驱动层。一个简单的线性映射公式是:GUI_X = (raw_x - x_min) * screen_width / (x_max - x_min)。 - 多点触摸:ATK-MD0430支持多点,但TouchGFX标准接口只上报一个点。如果你需要多点,需要更复杂的处理。通常单点足以应对大部分交互。
一个经过优化和校准的sampleTouch函数示例片段如下:
bool STM32TouchController::sampleTouch(int32_t& x, int32_t& y) { static uint32_t lastSampleTime = 0; const uint32_t sampleInterval = 30; // 30ms采样一次 atk_md0430_touch_point_t point; if (HAL_GetTick() - lastSampleTime < sampleInterval) { return false; // 未到采样时间,直接返回无触摸 } lastSampleTime = HAL_GetTick(); if (atk_md0430_touch_scan(&point, 1) > 0) { // 坐标校准:将原始坐标映射到屏幕分辨率(480x272) // 这里的校准值需要根据你的屏幕实际测量得到 x = (point.x - 200) * 480 / (3800 - 200); y = (point.y - 200) * 272 / (3800 - 200); // 确保坐标在屏幕范围内 if (x < 0) x = 0; if (x >= 480) x = 479; if (y < 0) y = 0; if (y >= 272) y = 271; return true; } return false; }将驱动文件加入编译,并正确实现STM32TouchController后,TouchGFX框架就能接收到触摸事件了。
5. 在TouchGFX Designer中设计交互界面
底层打通后,上层就是愉快的“画画”时间了。我们打开STM32CubeIDE,在项目资源管理器里找到.touchgfx文件,双击它就会启动TouchGFX Designer。
5.1 创建第一个视图与基础控件
Designer启动后,你会看到一个画布。首先从右边的Widgets面板拖一个Box(容器)到画布上,设置它的大小和屏幕一样(比如480x272),并给它一个背景色。这就是你的界面底色。
然后,我们可以添加交互控件。比如:
- 按钮:从
Widgets里拖一个Button进来。你可以在Properties面板里设置它的位置、大小、标签文字、按下和释放状态的颜色图片。 - 滑块:拖一个
Slider进来。滑块控件稍微复杂点,它由背景、指示条和可拖动的“手柄”组成。你需要分别设置这些元素的样式。
Designer的强大之处在于,你给控件起一个唯一的Name(比如myButton),然后切换到Interactions视图,就可以为这个控件定义交互行为。例如,可以为myButton的Click事件添加一个动作:“改变另一个Box的颜色”或者“跳转到另一个屏幕”。
5.2 为控件添加实际逻辑
在Designer里定义的是界面和交互的“关系”,真正的逻辑代码(比如按下按钮后让一个LED亮灭)需要在代码里实现。
- 生成代码:在Designer中完成设计后,点击顶部的
Generate Code按钮。这会将你的界面布局转换成C++代码,并集成到你的STM32CubeIDE工程中。 - 找到视图类:回到STM32CubeIDE,刷新项目。你会在
TouchGFX/generated/gui_generated/src和TouchGFX/gui/src下找到对应的屏幕视图类文件,例如Screen1View.hpp和Screen1View.cpp。 - 绑定回调函数:在视图类的头文件里,你会看到类似
touchgfx::Button myButton;的声明。我们需要在视图类的构造函数(Screen1View::Screen1View())中,为这个按钮设置一个“动作触发器”(Action)。更常用的方法是使用Presenter-Model模式,但简单起见,可以直接在视图类里为按钮绑定一个回调函数:// 在Screen1View.hpp中声明回调函数 void buttonClickedHandler(const touchgfx::AbstractButton& src);// 在Screen1View.cpp的构造函数中绑定 Screen1View::Screen1View() { // ... 其他初始化 myButton.setAction(buttonClickedHandler); } void Screen1View::buttonClickedHandler(const touchgfx::AbstractButton& src) { if (&src == &myButton) { // 在这里写你的处理逻辑,比如切换一个标志位,或者调用HAL库函数控制GPIO HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } } - 处理滑块值:对于滑块,你通常需要读取它的当前值。可以在滑块的回调函数中,通过
sliderName.getValue()来获取一个0到100之间的整数,然后根据这个值去控制PWM输出(调整背光亮度)或者设置其他参数。
通过这种“Designer设计界面 + 代码编写逻辑”的方式,你可以快速构建出非常复杂的GUI应用。Designer负责渲染和用户输入分发,你只需要关心每个控件被操作后要做什么。
6. 编译、调试与常见问题排查
所有代码就绪,点击STM32CubeIDE的编译按钮。这个过程可能会比较长,因为TouchGFX框架本身代码量不小。编译成功后,连接开发板,下载程序。
6.1 上电测试与现象观察
正常情况下,你应该能看到你设计的界面显示在屏幕上,并且触摸按钮、滑动滑块都有反应。如果出现问题,别慌,按以下步骤排查:
白屏或花屏:
- 首先检查MPU配置:这是我遇到最多的原因。确保FSMC区域的缓存策略是
Write-Through或Non-Cacheable。 - 检查FSMC时序:如果MPU没问题,回退检查FSMC的地址/数据建立时间是否足够。可以尝试适当增加
ADDSET和DATAST的值。 - 检查屏幕初始化序列:确认
nt35510_init函数被正确调用,且其中的延时HAL_Delay是否足够。有些屏幕需要较长的复位等待时间。 - 检查颜色格式:确认TouchGFX配置的
Color format(RGB565)与nt35510_ramwr函数写入的数据格式匹配。
- 首先检查MPU配置:这是我遇到最多的原因。确保FSMC区域的缓存策略是
触摸完全无反应:
- 检查接线:确认触摸屏排线插紧了。
- 检查I2C引脚和初始化:用逻辑分析仪或示波器抓一下SCL/SDA波形,看是否有起始信号和数据传输。没有的话,检查GPIO初始化代码,确认模拟I2C的引脚模式(开漏输出、上拉)设置正确。
- 检查采样函数是否被调用:在
sampleTouch函数里加一个翻转GPIO的语句,下载运行后用示波器看这个引脚是否有脉冲,来判断函数是否被周期性执行。 - 检查坐标校准:如果
sampleTouch被调用了但UI没反应,很可能是坐标没校准。在sampleTouch里将读到的原始坐标通过串口打印出来,看看你点击屏幕时数值是否变化,以及变化范围是否合理。然后根据这个范围进行线性映射。
触摸漂移或不准:
- 这几乎肯定是坐标校准问题。你需要精确测量触摸屏四个角或中心点的原始坐标值,然后计算映射公式。可以写一个简单的校准程序,在屏幕上显示几个点,点击后记录原始坐标,然后计算校准参数。
界面卡顿或刷新慢:
- 检查帧率:在
main.c的while(1)循环里,确保touchgfxSignalVSync()的调用频率是稳定的(比如每16ms一次,对应60Hz)。可以用一个定时器来精确触发。 - 优化
TransmitBlock:确保这个函数里没有耗时的操作,比如动态内存分配或复杂的计算。它应该只做数据搬运。 - 使用DMA:对于高性能应用,可以考虑用DMA来搬运像素数据到FSMC,解放CPU。但这需要更复杂的驱动设计。
- 检查帧率:在
6.2 利用调试工具
STM32CubeIDE的调试功能非常强大。你可以:
- 在
touchgfxDisplayDriverTransmitBlock函数里设置断点,观察像素数据是否正确。 - 在
sampleTouch函数里设置断点,观察触摸坐标是否被正确采样和转换。 - 使用
Live Watch功能,实时监控触摸坐标变量或界面状态变量的值。
调试GUI项目,耐心和系统性的排查思路是关键。从电源、时钟、引脚配置,到驱动初始化、数据流,再到框架接口,一层层确认。每解决一个问题,你对整个系统的理解就会加深一层。
当屏幕亮起,界面流畅滑动,触摸精准响应时,那种成就感是无与伦比的。从底层寄存器操作到上层图形界面,你完成了一个完整的嵌入式GUI系统搭建。这套基于正点原子阿波罗STM32H743、HAL库和TouchGFX的方案,具有很强的可扩展性,你可以在此基础上添加更复杂的动画、图表、多语言支持,打造出真正产品级的交互界面。