
简介面向STM32嵌入式开发者的TFTLCD触摸屏显示工程源码包完整实现LCD显示驱动与触摸交互逻辑适用于工业控制、消费电子的图形界面开发。压缩包共84个文件含40个h头文件与38个c源文件另有汇编启动文件、keilkilll.bat清理脚本、Hex固件和Keil工程文件整体仅323KB按HARDWARE、SYSTEM、CORE、USER模块划分。目前已有312人浏览学习适合正在学习STM32图形显示或准备移植触摸屏驱动的开发者。内容覆盖SPI/GPIO接口配置、LCD初始化、文本与位图显示、触摸坐标读取与中断响应并附带KEY、IIC、24CXX外设驱动代码以及触摸控制器适配和显示缓冲更新思路可直接用于项目。附带Hex文件可立即烧录验证对理解STM32驱动TFTLCD完整流程具很高实践价值。1. 拿到 STM32-TFTLCD 工程先别急着烧录网上下载的STM32-TFTLCD.rar这类工程名几乎已经说明了它包含的硬件组合STM32 主控、TFT 彩色液晶屏、电阻式触摸屏以及一份作者整理好的 Keil 工程。很多人在第一步就踩坑——解压后直接编译烧录结果白屏、花屏、触摸完全没反应。原因通常不在代码逻辑而是开发板的 Flash 型号、LCD 控制器的 ID、FSMC 引脚映射和触摸屏的 SPI 接口对不上。这篇文章要把这条链路拆开讲清楚从 LCD 控制器时序、FSMC 并口驱动到 XPT2046 触摸采样和校准最后落到中文显示、滤波这些实操技巧。适合正在做 STM32 毕设、入门嵌入式图形界面或者从裸机点灯转向带屏开发的人。对已经能跑通显示但触摸不准的读者校准和滤波部分也有查漏补缺的价值。2. TFTLCD 驱动里的时序与控制器 ID先把 ILI9341 和 ST7789 分清楚STM32-TFTLCD工程里LCD 驱动代码的核心不是画点函数而是针对具体控制器的初始化序列。TFT 屏常见的控制器有 ILI9341、ST7789、ILI9488它们虽然都接受 8080 并口或 SPI 命令但初始化寄存器的值并不通用。直接套用一个工程里的LCD_Init()如果控制器 ID 对不上画面就会异常。2.1 先读 LCD 控制器 ID再决定要不要改初始化序列大多数 LCD 控制器支持读取 ID 寄存器。ILI9341 的读 ID 命令是0xD3返回 3 个字节ST7789 的读 ID 命令是0x04返回 1 个字节。在LCD_Init()最前面加一段读 ID 的调试代码能避免用错初始化序列uint16_t LCD_ReadID(void) { uint16_t id 0; LCD_CS_CLR; // 拉低片选 LCD_RS_CLR; // 命令模式 LCD_WR_CLR; LCD_WR_SET; LCD_RS_SET; // 数据模式 // 发送命令 0xD3 (ILI9341) LCD_CS_SET; /* 实际工程中需要按 8080 时序发送命令和读取数据 */ return id; }上面只是示意关键逻辑是通过 FSMC 的地址线复用把命令和数据映射到不同的内存地址然后读取返回字节。常见做法是在工程里维护一张表列出已知控制器的 ID 和对应的初始化数组例如0x9341对应 ILI93410x7789对应 ST7789。参数说明LCD_CS_CLR、LCD_RS_CLR这些宏定义在正点原子或野火的例程里通常被重定向为对 FSMC 地址的操作比如(*((volatile uint16_t *)0x6C000000)) cmd。命令地址和数据地址相差0x00020000正好对应RS引脚的一根地址线。调试时用串口把读到的 ID 打出来再去对照初始化序列比盲改寄存器高效得多。2.2 FSMC 并口模式的配置参数时序从哪里抄STM32F103 这种不带 LTDC 的芯片驱动 TFTLCD 常走 FSMC 的 NOR/SRAM 模式把 LCD 当作外部 SRAM 来写。FSMC 的时序参数直接影响刷新率和稳定性。以下是一段基于标准外设库的 FSMC 配置核心代码适用于 16 位数据线、8080 接口的 LCDvoid FSMC_LCD_Init(void) { FSMC_NORSRAMInitTypeDef FSMC_NORSRAMInitStructure; FSMC_NORSRAMTimingInitTypeDef readWriteTiming; // 读时序 readWriteTiming.FSMC_AddressSetupTime 0x01; readWriteTiming.FSMC_AddressHoldTime 0x00; readWriteTiming.FSMC_DataSetupTime 0x03; readWriteTiming.FSMC_BusTurnAroundDuration 0x00; readWriteTiming.FSMC_CLKDivision 0x00; readWriteTiming.FSMC_DataLatency 0x00; readWriteTiming.FSMC_AccessMode FSMC_AccessMode_A; FSMC_NORSRAMInitStructure.FSMC_Bank FSMC_Bank1_NORSRAM4; FSMC_NORSRAMInitStructure.FSMC_DataAddressMux FSMC_DataAddressMux_Disable; FSMC_NORSRAMInitStructure.FSMC_MemoryType FSMC_MemoryType_SRAM; FSMC_NORSRAMInitStructure.FSMC_MemoryDataWidth FSMC_MemoryDataWidth_16b; FSMC_NORSRAMInitStructure.FSMC_BurstAccessMode FSMC_BurstAccessMode_Disable; FSMC_NORSRAMInitStructure.FSMC_WaitSignalPolarity FSMC_WaitSignalPolarity_Low; FSMC_NORSRAMInitStructure.FSMC_WrapMode FSMC_WrapMode_Disable; FSMC_NORSRAMInitStructure.FSMC_WaitSignalActive FSMC_WaitSignalActive_BeforeWaitState; FSMC_NORSRAMInitStructure.FSMC_WriteOperation FSMC_WriteOperation_Enable; FSMC_NORSRAMInitStructure.FSMC_MemoryDataWidth FSMC_MemoryDataWidth_16b; FSMC_NORSRAMInitStructure.FSMC_WriteBurst FSMC_WriteBurst_Disable; FSMC_NORSRAMInitStructure.FSMC_ReadWriteTimingStruct readWriteTiming; FSMC_NORSRAMInitStructure.FSMC_WriteTimingStruct readWriteTiming; FSMC_NORSRAMInit(FSMC_NORSRAMInitStructure); FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM4, ENABLE); }这段配置选用了 Bank1 的第 4 区对应的片选是FSMC_NE4起始地址0x6C000000。FSMC_AddressSetupTime 0x01表示地址建立时间为 1 个 HCLK 周期FSMC_DataSetupTime 0x03表示数据建立时间为 3 个 HCLK。如果屏幕刷新有条纹或者偶发花屏优先加大FSMC_DataSetupTime而不是改 GPIO 速度。参数对照关系见下表FSMC 时序参数典型值影响AddressSetupTime1地址建立时间过短会导致 LCD 采样到错误的寄存器地址DataSetupTime3-5数据建立时间不足时画面会有随机噪点BusTurnAroundDuration0读切换方向时的总线空闲周期AccessModeA大多数 LCD 控制器推荐 Mode A读写时序对称2.3 像素时钟、扫描方向和坐标原点TFTLCD 控制器内部有一个像素时钟决定一帧画面的刷新速度。FSMC 写一个像素需要多个 HCLK 周期理论上帧率上限 HCLK / (每像素周期数 × 总像素数)。以 320×240 的 ILI9341 为例每像素大约需要 3 个 HCLK 周期HCLK72MHz 时帧率约 72M / (3 × 76800) ≈ 312 帧但这只是写显存的理论值实际还要算上命令开销。扫描方向由控制器的Memory Access Control寄存器0x36控制。工程里常见的LCD_Display_Dir(uint8_t dir)函数实际上就是在修改这个寄存器的MY、MX、MV位。横屏和竖屏的坐标原点不同触摸校准参数也因此要跟着变。如果你把屏幕旋转 90 度却没有同步修改触摸屏的坐标换算点按位置就会整体偏移。3. STM32 触摸屏工程的最小可用配置SPI 引脚、XPT2046 采样和校准STM32-TFTLCD里的触摸屏部分绝大多数是电阻式四线触摸屏控制器为 XPT2046 或 ADS7843。触摸IC通过 SPI 与 STM32 通信采样得到 X/Y 两个轴的电压值然后通过校准公式换算成 LCD 像素坐标。3.1 XPT2046 的 SPI 通信时序与读取代码XPT2046 是 12 位逐次逼近型 ADC控制字为 8 位一次转换返回 12 位数据。下面是基于 STM32 SPI1 的读取函数uint16_t TP_Read_ADC(uint8_t cmd) { uint16_t value 0; SPI_Touch-CR1 | SPI_CR1_SPE; // 使能 SPI while ((SPI_Touch-SR SPI_SR_TXE) 0); // 等待发送寄存器空 SPI_Touch-DR cmd; // 发送控制字 while ((SPI_Touch-SR SPI_SR_RXNE) 0); // 等待接收 SPI_Touch-DR; // 读走第一个无效数据 while ((SPI_Touch-SR SPI_SR_RXNE) 0); value SPI_Touch-DR; // 高 8 位 return value; }控制字cmd的 bit7 是起始位bit6-4 选择通道bit3 是 12 位/8 位模式选择bit2 是参考电压选择。读取 X 坐标时cmd 0xD0读取 Y 坐标时cmd 0x90这两个值在工程里通常已经定义好。注意 XPT2046 的 SPI 模式是模式 0CPOL0CPHA1有的库函数会配置成模式 0 导致读到的值全是 0。检查 SPI 初始化里的SPI_CPHA_High设置这是新手最容易忽略的点。3.2 触摸坐标到屏幕坐标的校准公式拿到 ADC 原始值后需要做线性校准。常见的两点校准原理是屏幕坐标(x, y)与触摸 ADC 值(tx, ty)存在线性关系x (tx - tx_min) * (LCD_WIDTH - 0) / (tx_max - tx_min) y (ty - ty_min) * (LCD_HEIGHT - 0) / (ty_max - ty_min)工程里往往提供一个TP_Adjust函数在屏幕四个角落显示校准点让用户点击后记录tx_min/tx_max/ty_min/ty_max。这些值需要保存在非易失存储中否则每次重启都要重新校准。下面是校准参数的读取和套用typedef struct { int16_t x_min; int16_t x_max; int16_t y_min; int16_t y_max; } TP_CALIBRATION; void TP_Get_Coordinate(uint16_t *x, uint16_t *y) { uint16_t tx, ty; TP_Read_XY(tx, ty); // 多次采样取均值 // 线性映射 *x (tx - calib.x_min) * (LCD_WIDTH - 1) / (calib.x_max - calib.x_min); *y (ty - calib.y_min) * (LCD_HEIGHT - 1) / (calib.y_max - calib.y_min); // 越界处理 if (*x LCD_WIDTH) *x LCD_WIDTH - 1; if (*y LCD_HEIGHT) *y LCD_HEIGHT - 1; }参数说明calib结构体在工程启动时从 SPI Flash 或 EEPROM 读取。如果校准参数异常比如x_max x_min要做分母判零处理否则会出现硬件错误。工程里的TP_Read_XY通常会连续采样 5 次去掉最大值和最小值后取平均这一步不要省略。3.3 触摸屏的电源管理和检测脚XPT2046 的PENIRQ引脚通常连接到 STM32 的 EXTI 中断用来检测触摸按下事件。在工程里这个引脚被配置为下降沿触发的外部中断。读取触摸前需要短暂延时等待 ADC 稳定否则第一次读到的电压值会偏大。如果屏没触摸但PENIRQ一直为低除了硬件虚焊还有一个常见原因XPT2046 的 VCC 电源纹波过大导致内部比较器误触发。排查方法是用示波器看触摸屏排线接口的 3.3V 电源如果纹波超过 50mV就在供电端并联一个 10µF 电容和 0.1µF 电容。4. 从编译烧录到画面显示STM32 TFTLCD 工程最常见的 3 个故障与排错路径这一步遇到的白屏、花屏、触摸漂移九成不是代码逻辑问题而是环境配置和硬件连接问题。你需要一套可复现的排查流程而不是一遍遍改初始化函数。4.1 编译通过但下载后屏不亮先查 FSMC 地址线接线屏幕不亮但代码能跑首先用调试器在LCD_Init()里打断点单步执行到初始化完成。如果执行后屏幕依然全白检查 FSMC 的数据线D0-D15和地址线A6用于 RS 控制是否与 LCD 模块的实际引脚一一对应。常见接法是信号STM32 FSMC 引脚CSFSMC_NE4 (PD7)RSFSMC_A6 (PF0)WRFSMC_NWE (PD5)RDFSMC_NOE (PD4)D0-D15PD14-PD15, PE7-PE15 等如果你的板子把 RS 接到了FSMC_A0那么读写命令的地址要改为0x60000000Bank1 NE1。这时直接抄网上的工程就会出错因为别人用的是 NE4A6。验证地址线是否正确的方法在调试器 Watch 窗口写入*(volatile uint16_t *)0x6C000000 0x1234;用示波器或者逻辑分析仪看数据线上是否出现对应的电平组合。4.2 花屏、上下颠倒、颜色错乱初始化序列和屏参不匹配花屏有三种表现画面有规律的重影FSMC_DataSetupTime太小增大到 5 再试。颜色完全错乱比如红色显示成蓝色初始化序列里设置了错误的 RGB 顺序。ILI9341 的0x3A寄存器用于设置像素格式0x36寄存器的BGR位会交换红蓝通道。多数模块默认是 RGB 顺序但有些厂商的屏是 BGR。在LCD_Init()里单独写一个寄存器测试函数依次把扫描方向和 BGR 位翻转很快能定位。上下颠倒或左右镜像0x36寄存器的MY/MX/MV位状态没匹配屏幕安装方向。这个不是故障是屏幕物理方向与驱动设置不一致。把你需要的横屏方向参数整理成一组宏例如#define LANDSCAPE_REVERSED 0xA0 #define PORTRAIT 0x48 #define PORTRAIT_REVERSED 0x88替换LCD_WriteReg(0x36, value)即可。4.3 触摸不准、越点越偏校准参数丢失和数据滤波不足触摸不准的排查顺序是先确认触摸原始 ADC 值是否随按压位置线性变化。在调试器里观察TP_Read_ADC(0xD0)的返回值手指从左上角滑到右下角值应该从一个值平移到另一个值。如果跳变剧烈先加滤波#define FILTER_N 8 uint16_t TP_Read_Filter(uint8_t cmd) { uint32_t sum 0; uint16_t min_val 0xFFFF, max_val 0; for (uint8_t i 0; i FILTER_N; i) { uint16_t val TP_Read_ADC(cmd); sum val; if (val min_val) min_val val; if (val max_val) max_val val; } sum - min_val max_val; // 去掉最大和最小值 return sum / (FILTER_N - 2); // 取平均值 }这个函数把 8 次采样中的最大值和最小值剔除剩下的 6 次取平均。FILTER_N越大抗干扰越强但每次触摸的响应时间会变长。屏幕刷新率 60Hz 以下时FILTER_N 8是平衡点。如果原始值稳定但坐标还是偏就是校准参数失效。TP_Adjust后把校准值打印出来写入一个备份参数表防止 Flash 擦写失败导致乱值。5. 把中文显示和抗干扰采样一起加进你的 STM32 TFTLCD 工程工程能显示英文和数字后下一步往往是在屏幕上显示中文。TFTLCD 不像点阵 LCD 内置字库所有字符都需要外部字库或者取模数组。中文显示的关键是把 GBK 编码转换成字模地址并按照屏幕的像素格式绘制出来。5.1 使用外部 SPI Flash 字库显示中文的代码结构常见做法是把 16x16 或 24x24 的 GBK 字库文件烧录到 W25Q64 这类 SPI Flash 中然后通过FTLCDC驱动读出字模。但这需要 Flash 文件系统镜像工程体积会变大。对STM32-TFTLCD这种一个 .rar 里的裸机工程更轻量的方案是只生成你需要的字模数组把常用汉字放进一个 C 文件。// 字模数组格式每个汉字 16x16 点阵每个点一个 bit共 32 字节 const unsigned char chinese_font[][32] { {0x00,0x00,0x7F,0xFC,...}, // 温 {0x00,0x00,0x03,0xF8,...}, // 度 }; void LCD_ShowChinese(uint16_t x, uint16_t y, uint8_t *str, uint16_t color, uint16_t bgcolor) { while (*str) { if (*str 0x81 *(str1) 0x40) { // 判断是否中文 GBK 双字节 // 根据 str 的 GBK 编码查表获得字模索引 uint16_t index (*str - 0x81) * 190 (*(str1) - 0x40); LCD_ShowChar_16x16(x, y, chinese_font[index], color, bgcolor); x 16; str 2; } else { // 处理英文 LCD_ShowChar(x, y, *str, color, bgcolor); x 8; str; } } }注意这里查找索引用的是简化 GB2312 公式编码范围仅在 0xA1A1 到 0xF7FE 之间时有效。如果你的字符串里包含特殊符号最好先把字符串统一转成 GBK 码。另一种做法是把字模直接转换成 C 数组用 Makefile 脚本自动生成但在这类教学工程里手工管理数组更直观。5.2 用排错状态机验证显示和触摸是否互相阻塞显示和触摸共用一个 STM32 时很容易出现触摸响应卡顿。原因是显示全屏时占用了大量 CPU 时间触摸扫描周期被拉长。我在工程里常用一个简易状态机来分时调度typedef enum { TASK_LCD 0, TASK_TOUCH, TASK_UPDATE, } TASK_STATE; volatile TASK_STATE state TASK_LCD; void Main_Loop(void) { switch (state) { case TASK_LCD: LCD_Refresh(); // 刷显示区域 state TASK_TOUCH; break; case TASK_TOUCH: if (TP_Read_XY(x, y)) { // 处理触摸事件 } state TASK_UPDATE; break; case TASK_UPDATE: // 更新需要显示的数值 state TASK_LCD; break; default: state TASK_LCD; } }每次循环只执行一个任务触摸扫描不会因为一次全屏刷新而丢数据。如果你用 RTOS也可以用信号量来同步但裸机状态机的开销更小更容易在下载来的工程上直接改。5.3 验证最终效果的两个简单方法第一用回读测试验证触摸坐标转换在屏幕上画一个固定点程序读回触摸坐标并与该点比较。连续点击 20 次计算偏差均值。如果偏差超过 3 个像素检查校准参数或增加采样次数。第二查看 FPS 实测值在LCD_Refresh()前后翻转一个 GPIO用逻辑分析仪测量刷新一帧的时间。320×240 的 16 位色全屏刷新在 72MHz 主频下如果超过 40ms就需要检查 FSMC 时序是否偏保守或者考虑改用 DMA 传输FSMC_Data数据。把FSMC_DataSetupTime从 3 降到 2再把刷新循环里的delay删掉通常能把帧时间压低三分之一。本文还有配套的精品资源点击获取