简介基于正点原子F407开发板的FSMC接口LCD屏幕驱动与TouchGFX图形库移植完整Keil工程是一份面向嵌入式开发者的实战项目。工程以STM32F407 Cortex-M4芯片为核心通过FSMC外设连接液晶屏并完成TouchGFX交互图形库的移植解决了在资源受限硬件上实现现代高交互GUI的典型难点同时支持多点触控可显著提升用户体验适用于工业人机界面、智能家电、消费电子等需要自定义UI的场景。资源包共1592个文件压缩后约77.58MB源码以C/C为主包含606个.c、378个.h、210个.hpp和113个.cpp等源文件另有78个.s启动文件、.icf链接脚本、.uvprojx工程配置以及.a/.lib预编译库文件完整覆盖从外设初始化、库编译链接到下载调试的整个开发流程。目前已有402人学习浏览适合具备一定STM32基础、希望快速上手TouchGFX的开发者。通过研读该工程可深入理解FSMC时序配置、GPIO复用、TouchGFX事件框架和图形渲染优化技巧并掌握LCD驱动适配、内存裁剪与Keil工程管理方法为后续独立开发嵌入式GUI奠定扎实基础是一份含金量高的学习参考。1. 把 TouchGFX 搬上正点原子 F407 的 FSMC 屏这条路为什么值得走很多人第一反应是 TouchGFX 只能配带 LTDC 控制器的 RGB 屏正点原子 F407 开发板上的 4.3 寸/2.8 寸 FSMC 并口屏只能老老实实跑裸机。实际上TouchGFX 的渲染结果只是一块内存里的帧缓冲只要有一组足够快的“搬运工”把它送到 LCD 控制器这件事就成立。FSMC 接口恰恰是 F407 上最高效的搬运通道。这篇笔记就是为了解决“基于正点原子 F407 开发板 FSMC 接口屏幕移植 TouchGFX 完整 Keil 工程”这件事屏幕底层怎么配、TouchGFX 的 HAL 层怎么接、帧缓冲放哪、Keil 工程怎么组织、翻车点在哪。适合手里正好有这块板子、想用 TouchGFX 做类手机界面又不想换屏幕的人。2. FSMC 接口屏幕的底层时序为什么 TouchGFX 能在 8080 并口上跑起来2.1 FSMC 在 F407 里的角色不是“快一点的 IO 模拟”而是把 LCD 变成内存FSMC 全称 Flexible Static Memory ControllerF407 用它来扩展 NOR Flash、PSRAM、SRAM 这类并行存储器。8080 接口屏幕的时序本质上和 SRAM 读写非常接近都有片选、读使能、写使能、数据总线只是多了一条 RS命令/数据选择线。正点原子 F407 开发板上的 TFTLCD 接口就是把 LCD 当作一块挂在 FSMC Bank1 上的外部 SRAM 来连的。FSMC Bank1 的起始地址是 0x60000000片选信号 NE1 对应 Bank1 的第 1 个区地址范围 0x60000000 到 0x63FFFFFF。NE1 接到 LCD 的 CSNWE 接 WRNOE 接 RD地址线 FSMC_A6 接 RS数据线 D[15:0] 接屏的 16 位数据口。CPU 向这个地址范围写数据时FSMC 硬件会自动拉出完整的读写时序不需要软件再用 GPIO 去模拟 WR 和 RD。这就是 TouchGFX 能跑的基础渲染是纯内存操作搬运靠 FSMC两者都不依赖具体屏幕型号。2.2 命令/数据地址偏移正点原子板级接线的 A6 偏移不能猜既然 RS 接在地址线 A6 上那么向地址 0x60000000 写数据时 A60LCD 把这笔数据当成命令向 0x60000040 写数据时 A61LCD 把它当成数据。这个 0x40 就是 1 左移 6 位也就是 1 6。如果你改板子把 RS 接到了 A16偏移就要换成 0x10000不是所有开发板都用 A6。/* lcd_fsmc.h */ #define LCD_BASE_ADDR (0x60000000u) #define LCD_CMD_ADDR (LCD_BASE_ADDR) /* RS 0写命令 */ #define LCD_DATA_ADDR (LCD_BASE_ADDR (1u 6)) /* RS 1写数据 */ #define LCD_WR_REG(reg) (*(volatile uint16_t *)(LCD_CMD_ADDR)) (uint16_t)(reg) #define LCD_WR_DATA(data) (*(volatile uint16_t *)(LCD_DATA_ADDR)) (uint16_t)(data)这里volatile不能省。如果不加编译器在连续多次写同一个地址时可能把写操作合并屏幕上就会出现“该更新却没更新”的诡异现象。正点原子例程里习惯用LCD_BASE 0x60000000 | 0x0000007E这种写法其中同样包含了 A6 偏移和部分总线控制位实质和我上面的宏是一回事只是更贴近寄存器层面的拼法。新手直接用LCD_CMD_ADDR / LCD_DATA_ADDR两个宏去改逻辑更直白也更容易对照原理图检查。2.3 时序参数不玄学AddressSetupTime 和 DataSetupTime 决定能不能点亮FSMC 的时序配置在 CubeMX 里是图形化的但很多人直接把生成的初始化代码丢进工程就以为万事大吉。以 HAL 库为例实际上它初始化的是 NOR/SRAM 控制器关键在下面这几项/* MX_FSMC_Init 核心片段 */ FSMC_NORSRAM_TimingTypeDef timing {0}; timing.FSMC_AddressSetupTime 1; /* 地址建立时间单位 HCLK约 5.95ns 168MHz */ timing.FSMC_AddressHoldTime 0; /* 地址保持时间 */ timing.FSMC_DataSetupTime 3; /* 数据建立时间写周期随此值加长 */ timing.FSMC_BusTurnAroundDuration 0; /* 总线转向周期连续读写时才有意义 */ timing.FSMC_CLKDivision 0; timing.FSMC_DataLatency 0; timing.FSMC_AccessMode FSMC_ACCESS_MODE_A; FSMC_NORSRAM_InitTypeDef init {0}; init.FSMC_AddressSetupTime timing.FSMC_AddressSetupTime; /* 其余 init 字段可参考 CubeMX 生成的 MX_FSMC_Init */ HAL_SRAM_Init(hsram, init, timing);这套参数的含义用大白话说就是FSMC 给出地址后等多久再拉写信号写信号拉起来后数据要保持多久才允许撤销。LCD 控制器的数据手册里通常写着“写周期最小 50ns 或 80ns”如果你的 DataSetupTime 太小屏幕会白屏或者显示花。正点原子 F407 的例程里常用AddressSetupTime1、DataSetupTime3对应写周期大约 24ns 左右对多数 4.3 寸屏幕够用。如果点亮后发现满屏雪花、颜色错乱先把DataSetupTime从 3 加到 6 再试这是最便宜的排查手段。提示FSMC 的时钟来自 AHBF407 跑 168MHz 时一个 HCLK 约 5.95ns。改时序本质是给 LCD 控制器留出足够的写恢复时间不要盲目追求“越快越好”。3. TouchGFX 与 Keil 工程缝合从 CubeMX 生成到 MDK 编译通过3.1 CubeMX 先搭硬件骨架FSMC、CRC、调试口和一个可选 RTOSTouchGFX 不是简单的源码拷贝它要求底层工程具备几样东西FSMC 初始化、CRC 外设、能跑中断的时基以及推荐一个 RTOS。CRC 这个很多人不理解TouchGFX 内部对字体、图片缓存做校验时会调用硬件 CRC 模块不开会在运行期报错。F407 有硬件 CRCCubeMX 里直接勾上就行。我的习惯是先建一个不做 UI 的最小 MDK 工程确认 FSMC 屏幕能亮再谈 TouchGFX。具体在 CubeMX 里做这几步打开 RCC选择 HSE 外部晶振时钟树配到 168MHz。SYS 里 Debug 选 Serial WireTimebase 保持 SysTick。勾选 CRC 和 FSMCFSMC 按上一章的时序参数设置。如果要跑 FreeRTOS在 Middleware 里使能建议用 CMSIS_V1因为 TouchGFX 的 target 代码默认兼容这个接口。Project Manager 里 Toolchain 选 MDK-ARM生成工程。这里有个容易翻车的地方很多人直接把 TouchGFX Designer 生成的代码塞进一个老的裸机工程结果 FSMC 初始化和 LCD 初始化互相打架。先确认 CubeMX 生成的 FSMC 代码能单独点亮屏幕再做 TouchGFX排查问题会轻松一半。3.2 TouchGFX 工程生成的目录套路和 Keil 里的文件分组TouchGFX Designer 生成工程时会生成一个包含generated、gui、target、TouchGFX等目录的项目结构。你需要把它和 CubeMX 工程合并到同一个根目录下然后在 Keil 里手动添加文件组。这不是随便 Add 一下就行分组路径错了编译报错会让你怀疑人生。我最终在 Keil 工程里保留的分组如下Keil 分组主要文件说明Application/Usermain.c、stm32f4xx_it.cCubeMX 生成的用户代码TouchGFX/GeneratedCodeGeneratedCode.c、TouchGFXConfiguration.cpp自动生成不要改TouchGFX/guigui 目录下的 .cpp/.hpp你的界面代码TouchGFX/targetTouchGFXHAL.cpp、TouchGFXHAL.hpp、OSAL.cpp、BoardConfiguration.cpp移植时主要改这里TouchGFX/liblibtouchgfx.aTouchGFX 预编译核心库MiddlewaresFreeRTOS 相关如果启用了 RTOSTouchGFX 核心库是预编译的路径通常在 CubeMX 生成的Middlewares/ST/TouchGFX/lib/core/cortex_m4hf下对应带 FPU 的 Cortex-M4。如果你的工程没加这个库编译会报一堆undefined symbol后面避坑章会细说。3.3 把 target 层的三个接口接到 FSMC写命令、写数据、初始化TouchGFX 的 target 文件是 C 写的而正点原子的 LCD 驱动是 C 写的。在 TouchGFXHAL.cpp 或者 BoardConfiguration.cpp 里用extern C包住原子驱动头文件然后把 TouchGFX 需要的接口函数指向 FSMC 操作/* TouchGFXHAL.cpp 或 BoardConfiguration.cpp 片段 */ extern C { #include lcd.h /* 正点原子 lcd.c 的头文件 */ #include lcd_fsmc.h /* 上面自定义的 FSMC 宏 */ } extern C void lcd_init(void) { LCD_Init(); /* 原子的 8080 初始化包含屏幕复位和 GRAM 配置 */ } extern C uint16_t lcd_writeCommand(uint16_t cmd) { LCD_WR_REG(cmd); /* RS0 地址写命令 */ return 0; } extern C uint16_t lcd_writeData(uint16_t data) { LCD_WR_DATA(data); /* RS1 地址写数据 */ return 0; }这段代码的逻辑很简单TouchGFX 不管底层是 FSMC 还是 SPI它只调lcd_init初始化屏幕调lcd_writeCommand发寄存器命令调lcd_writeData写显存数据。正点原子的LCD_Init已经完成了屏幕控制器初始化你不需要再改它的寄存器序列。注意这里函数名要和 target 模板里预声明的名字一致不同 TouchGFX 版本可能叫lcd_write_cmd或LCD_IO_WriteCommand以你生成的实际模板为准。3.4 帧缓冲落地修改 .sct 分散加载文件把 framebuffer 放进外部 SRAMF407 内部 SRAM 一共 192KB而一块 480x272 RGB565 的帧缓冲就要 261KB放不下。正点原子 F407 探索者板载了一颗 1MB SRAM挂在 FSMC 上地址从 0x60000000 开始正好用来放 TouchGFX 的帧缓冲。在 MDK 里这需要修改分散加载文件.sct。先在 C/C 源文件里显式定义一个段/* framebuffer.c */ __attribute__((section(.fb), aligned(32))) uint16_t touchgfx_framebuffer[480 * 272]; /* 480x272 RGB565 */然后在 Keil 的 scatter 文件里增加一个外部 RAM region; MDK scatter file 增加内容 RW_ERAM1 0x60000000 UNINIT 0x00080000 { *(.fb) }UNINIT是必须的。它告诉链接器这块区域上电后不需要拷贝初始化数据否则 MDK 的启动代码会在 main 之前访问外部 SRAM而此时 FSMC 可能还没初始化直接 HardFault。0x00080000是 512KB给单帧缓冲留了 261KB还剩一半给贴图或者后续双缓冲用。提示如果你的 TouchGFX target 里有getFrameBufferStartAddress()这个函数直接返回(uint16_t *)0x60000000也行。用分散加载段的好处是地址由链接器分配出问题时可读性更好。4. 帧率上不去的三个限制因素FSMC 刷新方式、颜色深度和内存带宽4.1 FSMC 屏没有“自动刷新”这个认知决定性能优化方向带 LTDC 的 RGB 屏幕有自己的显存LTDC 会持续把显存内容输出到屏幕CPU 只需要改显存里某一小块屏幕对应区域立刻变化。FSMC 并口屏不是这样LCD 控制器内部有 GRAM但 CPU 必须主动把每一帧的数据都写到 GRAM 里屏幕才会更新。换句话说LTDC 是“边角料自动搬运”FSMC 是“每次都要人工搬砖”。这个差别直接决定了 TouchGFX 在两种屏幕上的性能表现。你在 F407 上跑 TouchGFX如果 FSMC 时序已经调到最优那么整个渲染流程的瓶颈一定是“把帧缓冲搬运到 LCD 控制器”这一步而不是 TouchGFX 的软件渲染。明白这一点后优化方向就清楚了不是去调 TouchGFX 的渲染优先级而是尽量少搬数据。4.2 flushFrameBuffer 用矩形窗口实现脏区域刷新别每次刷整屏TouchGFX 渲染完一帧后会调用 HAL 层flushFrameBuffer(const Rect rect)这个 rect 就是本帧实际变化过的“脏矩形”。默认实现如果是整屏刷性能会很差。正确做法是把这个 rect 映射到 LCD 控制器的窗口地址只把变化区域写入 GRAM/* TouchGFXHAL.cpp 中对 flushFrameBuffer 的重写 */ void TouchGFXHAL::flushFrameBuffer(const touchgfx::Rect rect) { if (rect.isEmpty()) { return; } const uint16_t* fb (const uint16_t*)getFrameBufferStartAddress(); uint32_t pitchInWords getFrameBufferPitch() / 2; /* pitch 单位是字节转成 uint16_t 数 */ /* 1. 把 LCD 控制器的写窗口设置成脏矩形 */ LCD_SetWindow(rect.x, rect.y, rect.x rect.width() - 1, rect.y rect.height() - 1); /* 2. 逐行搬运避免一次性跨行寻址 */ for (uint16_t row 0; row rect.height(); row) { const uint16_t* src fb (rect.y row) * pitchInWords rect.x; for (uint16_t col 0; col rect.width(); col) { LCD_WR_DATA(src[col]); } } /* 3. 恢复全屏窗口否则下一次 MCU 画点、画线时 GRAM 的写地址是乱的 */ LCD_SetWindow(0, 0, LCD_WIDTH - 1, LCD_HEIGHT - 1); }这段代码的逻辑看起来不复杂但有两个细节值得注意一是getFrameBufferPitch返回的是字节数而 framebuffer 是按uint16_t寻址的所以必须除以 2二是如果漏掉最后一步恢复全屏窗口屏幕上会出现“只有一块区域能更新其他地方全部静止”的怪现象。另外LCD_SetWindow这个函数在正点原子例程里叫LCD_Set_Window或者直接由LCD_Init配置没有的话就自己实现先写命令设置列地址再写命令设置行地址。局部刷新对 TouchGFX 的实际提升非常明显一个按钮按下的波纹动画可能只有几十个像素的区域在变脏矩形刷法能让帧率从 15 帧跳到 30 帧以上代价是你必须信任 TouchGFX 的 rect 上报是准的。4.3 颜色深度和内存带宽RGB565 是 F407 FSMC 的唯一安全选择TouchGFX 工程建立时可以选择颜色深度常见的是 RGB565 和 ARGB8888。对 F407 这颗芯片来说RGB565 不是“可选优化”而是“活下来的前提”颜色深度480x272 一帧大小1MB 外部 SRAM 占用FSMC 搬运耗时RGB565261KB约 26%约 8-12msARGB8888522KB超过 50%约 16-24msARGB8888 一帧就要 522KB即使外部 SRAM 能放下FSMC 搬运时间也翻倍帧率直接跌到 10 帧附近。再加上 TouchGFX 的字体缓存、图片解码缓冲还要占空间ARGB8888 在 F407 FSMC 场景下基本没有实用价值。如果你的原型必须展示全彩效果建议直接换 LTDC 屏或者上带显存的串口屏。5. 移植避坑记录花屏、编译溢出、触摸出界与 Keil 工程迁移的五条实战经验5.1 现象屏幕能亮但滚动时花屏、横条纹原因通常是两个一是 FSMC 写时序太快LCD 控制器 GRAM 写入周期没满足二是flushFrameBuffer里窗口设置和逐行搬运顺序不一致导致写地址跳变。解决方法是先把DataSetupTime从 3 加到 6 或 8确认时序稳定后再逐步调小。如果花屏只在 TouchGFX 界面滚动时出现而裸机跑正点原子例程正常重点检查第二点最常犯的错是只设置了窗口起点没有设置窗口终点或者搬完数据没有恢复全窗口。5.2 现象Keil 编译报一堆 undefined symbol比如 lcd_xxx原因不是代码语法问题而是 TouchGFX 的 target 是 C 编译的它调用的 LCD 接口函数名和你正点原子驱动里的函数名对不上。正点原子例程叫LCD_WR_REG、LCD_WR_DATA而 TouchGFX 模板期望的是lcd_writeCommand、lcd_writeData这样的命名。解决方法是按 3.3 节写一层 C 接口映射不要直接改原子的驱动否则以后升级屏幕驱动回来还要改一遍。编译时报libtouchgfx.a找不到同理先检查Middlewares/ST/TouchGFX/lib/core/cortex_m4hf这个库路径有没有加进 Keil 的 Linker 配置。5.3 现象点 Debug 进仿真就 HardFault甚至复位后白屏原因大概率是分散加载文件没写UNINIT。当你把帧缓冲段指向外部 SRAM而 scatter 文件里没有UNINIT标记时MDK 的启动代码会在 main 之前把该区域清零此时 FSMC 还没初始化外部 SRAM 访问直接 bus fault。解决方法是给外部 SRAM region 明确加UNINIT并且只把 framebuffer 段放进去不要偷懒写.ANY (ZI)否则整个工程的 ZI 数据都可能被丢进外部 SRAM性能反而更差。顺带一提F407 的 64KB CCM RAM 不能给 DMA 用而 FSMC 搬运本质是 CPU 操作所以帧缓冲放 CCM 是可行的但贴图资源如果要用 DMA 读取就不能放 CCM。5.4 现象触摸能响应但点按钮总错位或者左右/上下反向原因很直接正点原子电阻屏驱动里 XPT2046 读出的原始 ADC 值经过了一次坐标换算这个换算可能是按屏幕方向写死的。TouchGFX 采样触摸时不再经过“屏幕方向校准”这一层你需要在TouchController::sampleTouch里做最后一道修正/* TouchController.cpp */ bool TouchController::sampleTouch(uint32_t x, uint32_t y) { if (!XPT2046_Press()) { return false; } XPT2046_GetXY(x, y); /* 正点原子 4.3 寸屏在 TouchGFX 的横屏坐标系下通常需要交换和反向 */ uint32_t tmp x; x y; y LCD_HEIGHT - 1 - tmp; if (x LCD_WIDTH) x LCD_WIDTH - 1; if (y LCD_HEIGHT) y LCD_HEIGHT - 1; return true; }这里的具体换算公式和你屏幕安装方向、TouchGFX 工程设置的横竖屏有关没有一律的答案。调试方法是在sampleTouch里临时把 x、y 用串口打印出来点屏幕四个角记录读数再反推换算公式。别想在 GUI 层做旋转那个代价比在触摸层修正大得多。5.5 现象换一台电脑或升级 Keil 后打开工程提示 pack 显示 offline器件型号找不到原因是 Keil 的 DFPDevice Family Pack没有正确安装或者工程是用旧版 MDK 创建、新版 MDK 打开后 pack 版本不匹配。解决方法是打开 Pack Installer找到 STMicroelectronics F4 系列 DFP 安装然后确认工程选项里的 Device 是 STM32F407ZGTx 而不是一片空白。另一个常见坑是 CubeMX 生成的工程用了某种特定 MDK 版本而本地 Keil MDK 5.36 或更高版本打开后编译器版本升级导致 C 语法告警变多。TouchGFX target 代码对__attribute__和模板的支持在不同编译器版本下表现不同遇到诡异编译错先检查 AC6 的 C 标准设置不要一开始就怀疑 TouchGFX 库有问题。6. 进阶用 GPIO 翻转法实测刷新率再决定要不要开双缓冲6.1 用 GPIO 翻转法看帧率别靠“眼睛觉得卡”在flushFrameBuffer函数的入口和出口各翻转一次 GPIO用逻辑分析仪或者示波器量一下高电平持续时间再数一下上升沿之间的间隔就能知道实际帧刷新周期。TouchGFX 还提供了内部的帧率统计接口但对 FSMC 屏来说GPIO 法最直接不受调试器干扰/* 在 TouchGFXHAL.cpp 中添加 */ extern C void DEBUG_GPIO_Toggle(void) { LL_GPIO_TogglePin(GPIOB, LL_GPIO_PIN_0); } void TouchGFXHAL::flushFrameBuffer(const touchgfx::Rect rect) { DEBUG_GPIO_Toggle(); /* 开始搬运 */ /* ... 原有的 flush 实现 ... */ DEBUG_GPIO_Toggle(); /* 搬运结束 */ }测出来的两个上升沿之间的时间就是一帧 CPU 从渲染到搬运完成的周期。比如一个动画每 33ms 出现一次翻转说明帧率在 30fps 左右如果这个数字在 40ms 以上先不要加特效回去看 FSMC 时序和寄存器初始化通常能挤出一倍余量。6.2 双缓冲值不值得开算清楚瓶颈再决定TouchGFX 的双缓冲是为了让“GPU/CPU 渲染下一帧”和“把上一帧送到屏幕”并行。在 F407 上渲染本身就是 CPU 干的FSMC 搬运也是 CPU 干的两者并行效果有限。更现实的问题是双缓冲需要两块帧缓冲480x272 RGB565 一共约 522KB外部 SRAM 1MB 确实放得下但留给图片资源、字体缓存的空间就被压缩了。我的建议是先用单缓冲跑起来用 GPIO 测帧率。如果帧率低是发生在“复杂界面首次加载”而不是“动画持续过程”瓶颈大概率在渲染而不是搬运开双缓冲帮助不大如果帧率低在持续滚动而且 FSMC 搬一帧耗时超过渲染耗时双缓冲才值得尝试并且最好把图片资源转成 TouchGFX 支持的压缩格式腾出 SRAM 空间。6.3 上了 800x480 屏幕之后的三个边界条件如果你计划把同样的方案搬到 7 寸 800x480 屏有几个硬边界先想清楚第一单帧缓冲 768KB RGB565已经接近板载 1MB SRAM 的上限双缓冲基本不可能第二FSMC 搬完整帧的时间会拉长到 20ms 以上触控跟手度明显下降必须依赖脏矩形局部刷新第三800x480 的 FSMC 屏幕控制器和 4.3 寸不是同一个LB 初始化序列、窗口设置命令、GRAM 写时序都不一样不能拿 4.3 寸的驱动直接套。我自己在这条路上吃过一次亏一开始迷信双缓冲把 1MB 外部 SRAM 塞得满满当当结果界面切换掉帧、加载卡顿后来把注意力放到局部刷新和图片压缩上同样的硬件反而跑出了更顺滑的切换效果。如果你也是从正点原子 F407 起步我的建议是先单缓冲跑通、测出真实帧率、再做细节优化不要上来就把资源耗在看起来高级的机制上。希望帮到你。本文还有配套的精品资源点击获取