简介:面向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对应 ILI9341,0x7789对应 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 接口的 LCD:
void 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 时序参数 | 典型值 | 影响 |
|---|---|---|
| AddressSetupTime | 1 | 地址建立时间过短会导致 LCD 采样到错误的寄存器地址 |
| DataSetupTime | 3-5 | 数据建立时间不足时,画面会有随机噪点 |
| BusTurnAroundDuration | 0 | 读切换方向时的总线空闲周期 |
| AccessMode | A | 大多数 LCD 控制器推荐 Mode A,读写时序对称 |
2.3 像素时钟、扫描方向和坐标原点
TFTLCD 控制器内部有一个像素时钟,决定一帧画面的刷新速度。FSMC 写一个像素需要多个 HCLK 周期,理论上帧率上限 = HCLK / (每像素周期数 × 总像素数)。以 320×240 的 ILI9341 为例,每像素大约需要 3 个 HCLK 周期,HCLK=72MHz 时帧率约 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 模式是模式 0(CPOL=0,CPHA=1),有的库函数会配置成模式 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 引脚 |
|---|---|
| CS | FSMC_NE4 (PD7) |
| RS | FSMC_A6 (PF0) |
| WR | FSMC_NWE (PD5) |
| RD | FSMC_NOE (PD4) |
| D0-D15 | PD14-PD15, PE7-PE15 等 |
如果你的板子把 RS 接到了FSMC_A0,那么读写命令的地址要改为0x60000000(Bank1 NE1)。这时直接抄网上的工程就会出错,因为别人用的是 NE4+A6。
验证地址线是否正确的方法:在调试器 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 && *(str+1) >= 0x40) { // 判断是否中文 GBK 双字节 // 根据 str 的 GBK 编码查表获得字模索引 uint16_t index = (*str - 0x81) * 190 + (*(str+1) - 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删掉,通常能把帧时间压低三分之一。
本文还有配套的精品资源,点击获取