简介:这份资源面向嵌入式开发初学者与需要图形界面方案的工程师,提供基于STM32F103驱动4.3寸TFT LCD电容触摸屏的完整工程,屏幕分辨率800×480,并兼容STM32F1系列其他型号。压缩包共171个文件,以92个h头文件与75个c源文件为主体,另含uvprojx、uvoptx工程文件、hex烧录文件及s启动文件,整体约1.12MB,覆盖HAL库外设驱动、LCD初始化、触摸坐标解析与图形绘制等模块。已有3276人学习下载。读者可从中获取SPI/I2C接口配置、帧缓冲渲染、电容触摸事件处理及GPIO引脚映射适配等关键实现,工程可直接在Keil中编译烧录,便于快速搭建带触摸交互的显示系统,并作为STM32F1系列屏幕驱动开发的参考模板。
1. STM32F103 驱动 4.3 寸 800×480 电容触摸屏:从点亮到触控的完整路径
手里有一块 4.3 寸 800×480 的 TFT 电容触摸屏,主控是 STM32F103,怎么把它跑起来?这个问题在嵌入式圈子里出现的频率极高,因为 STM32F103 最小系统板便宜、资料多、生态成熟,而 4.3 寸 800×480 又是工业 HMI 和手持设备最常见的屏幕规格之一。两者组合在一起,就是一套典型的低成本人机交互方案。但真正动手时会发现,屏幕排线有 40pin 和 45pin 之分,驱动芯片有 NT35510、ILI9488、SSD1963 等多种型号,触摸控制器又有 FT5206、GT911、FT6236 等不同选择,稍不注意就买错屏、接错线、调不通。这篇笔记按实际调试顺序展开:先确认硬件链路和驱动选型,再打通 FSMC 刷屏,然后接入电容触摸,最后处理坐标映射和稳定性问题。适合有 STM32 基础、想快速把屏幕跑起来的工程师,也适合正在选型阶段、想评估这套方案可行性的开发者。
2. 硬件链路与驱动选型:先搞清楚你手里那块屏到底怎么接
2.1 4.3 寸 800×480 屏的两种典型驱动架构
市面上 4.3 寸 800×480 的 TFT 屏,按接口和驱动方式大致分两类。第一类是 RGB 接口屏,驱动芯片只负责时序转换,显存需要主控提供,STM32F103 没有 LTDC 控制器,直接驱动 RGB 屏非常吃力,通常需要外加 SSD1963 这类桥接芯片。第二类是 MCU 接口屏,驱动芯片自带显存,主控通过 8080 并口或 SPI 写入像素数据,STM32F103 用 FSMC 就能高效驱动。你手里那块屏如果是 40pin 排线,大概率是后者,驱动芯片常见 NT35510、ILI9488、R61509 等。
选型时最关键的判断依据是:屏幕模块是否自带驱动芯片和显存。如果排线里包含 CS、RS、WR、RD、DB0-DB15 这些信号,说明是 MCU 并口屏,可以直接用 FSMC 驱动。如果只有 R0-R7、G0-G7、B0-B7、HSYNC、VSYNC、PCLK 这些信号,那就是 RGB 屏,STM32F103 驱动起来会非常吃力,不建议新手碰。
注意:买屏时一定要向卖家确认驱动芯片型号和接口类型,不要只看尺寸和分辨率。同一尺寸同一分辨率的屏,驱动芯片可能完全不同,初始化代码也不通用。
2.2 FSMC 并口驱动的引脚分配与地址映射
STM32F103 的 FSMC 支持 NOR/PSRAM 模式,用来驱动 8080 并口屏时,通常把屏幕当作一个 16 位宽的 SRAM 来访问。关键引脚包括:FSMC_NE 作为片选,FSMC_NOE 作为读使能,FSMC_NWE 作为写使能,FSMC_A 某一位作为命令/数据切换(RS),FSMC_D0-D15 作为数据总线。
以常见的接线方式为例,假设使用 FSMC_NE1(对应基地址 0x60000000),RS 接在 FSMC_A16 上,那么命令地址就是 0x60000000,数据地址就是 0x60020000(因为 A16 对应偏移 1<<16 乘以 2 字节宽度)。这个地址计算是很多新手翻车的地方,写错一位屏幕就完全没反应。
/* 命令和数据的地址定义,根据实际接线调整 */ #define LCD_BASE ((uint32_t)0x60000000) #define LCD_CMD_ADDR (LCD_BASE) #define LCD_DATA_ADDR (LCD_BASE + (1 << 17)) /* A16 偏移,16位总线 */ /* 写命令 */ void LCD_WriteCmd(uint16_t cmd) { *(volatile uint16_t *)LCD_CMD_ADDR = cmd; } /* 写数据 */ void LCD_WriteData(uint16_t data) { *(volatile uint16_t *)LCD_DATA_ADDR = data; }上面代码里1 << 17是因为 16 位总线时地址线 A16 对应字节偏移为 2 的 16 次方乘以 2,即 1<<17。如果你把 RS 接在 A17 上,这里就要改成1 << 18。这个偏移量必须和实际接线严格对应,否则命令和数据会写反,屏幕表现为花屏或全白。
2.3 FSMC 初始化参数怎么配才不花屏
FSMC 的时序参数直接决定刷屏是否稳定。太快了屏幕来不及响应,出现花屏或噪点;太慢了刷新率上不去,动画卡顿。对于 4.3 寸 800×480 的屏,常见驱动芯片的写时序通常在 50ns 到 100ns 量级。
/* FSMC NOR/SRAM 初始化结构体 */ FSMC_NORSRAMInitTypeDef FSMC_InitStruct; FSMC_NORSRAMTimingInitTypeDef FSMC_TimingStruct; FSMC_TimingStruct.FSMC_AddressSetupTime = 1; /* 地址建立时间 */ FSMC_TimingStruct.FSMC_AddressHoldTime = 0; /* 地址保持时间 */ FSMC_TimingStruct.FSMC_DataSetupTime = 5; /* 数据建立时间,关键参数 */ FSMC_TimingStruct.FSMC_BusTurnAroundDuration = 0; FSMC_TimingStruct.FSMC_CLKDivision = 0; FSMC_TimingStruct.FSMC_DataLatency = 0; FSMC_TimingStruct.FSMC_AccessMode = FSMC_AccessMode_B; FSMC_InitStruct.FSMC_Bank = FSMC_Bank1_NORSRAM1; FSMC_InitStruct.FSMC_DataAddressMux = FSMC_DataAddressMux_Disable; FSMC_InitStruct.FSMC_MemoryType = FSMC_MemoryType_SRAM; FSMC_InitStruct.FSMC_MemoryDataWidth = FSMC_MemoryDataWidth_16b; FSMC_InitStruct.FSMC_BurstAccessMode = FSMC_BurstAccessMode_Disable; FSMC_InitStruct.FSMC_WaitSignalPolarity = FSMC_WaitSignalPolarity_Low; FSMC_InitStruct.FSMC_WrapMode = FSMC_WrapMode_Disable; FSMC_InitStruct.FSMC_WaitSignalActive = FSMC_WaitSignalActive_BeforeWaitState; FSMC_InitStruct.FSMC_WriteOperation = FSMC_WriteOperation_Enable; FSMC_InitStruct.FSMC_WaitSignal = FSMC_WaitSignal_Disable; FSMC_InitStruct.FSMC_ExtendedMode = FSMC_ExtendedMode_Disable; FSMC_InitStruct.FSMC_WriteBurst = FSMC_WriteBurst_Disable; FSMC_InitStruct.FSMC_ReadWriteTimingStruct = &FSMC_TimingStruct; FSMC_InitStruct.FSMC_WriteTimingStruct = &FSMC_TimingStruct;FSMC_DataSetupTime是最关键的参数。设太小屏幕反应不过来,设太大刷新率下降。我一般从 5 开始试,如果花屏就加到 8 或 10,如果稳定就试着降到 3 看能不能跑。另外注意,STM32F103 的 FSMC 时钟来自 HCLK,如果系统跑 72MHz,一个 HCLK 周期约 13.9ns,DataSetupTime 为 5 时数据建立时间约 69.5ns,大多数屏都能接受。
3. 从点亮到刷屏:初始化序列与像素写入的实操细节
3.1 屏幕初始化序列的移植与适配
不同驱动芯片的初始化序列差异很大,但结构类似:先软复位,然后设置电源参数、伽马校正、像素格式、行列地址范围,最后开显示。以 NT35510 为例,初始化序列通常有上百条命令,每条包含命令码和若干参数。
移植初始化序列时,最容易出错的地方是延时。有些命令之间需要毫秒级延时,比如软复位后至少等 120ms,电源设置后等 20ms。如果延时不够,屏幕可能初始化失败,表现为白屏或花屏。另一个坑是参数个数,有些命令后面跟的参数数量是固定的,少写一个参数可能导致后续命令全部错位。
/* 典型的初始化序列片段,以实际驱动芯片手册为准 */ static void LCD_InitSequence(void) { LCD_WriteCmd(0xF000); LCD_WriteData(0x55); LCD_WriteCmd(0xF001); LCD_WriteData(0xAA); LCD_WriteCmd(0xF002); LCD_WriteData(0x52); LCD_WriteCmd(0xF003); LCD_WriteData(0x08); LCD_WriteCmd(0xF004); LCD_WriteData(0x01); /* 软复位 */ LCD_WriteCmd(0x01); HAL_Delay(120); /* 电源设置 */ LCD_WriteCmd(0xB000); LCD_WriteData(0x0D); LCD_WriteCmd(0xB001); LCD_WriteData(0x0D); /* ... 省略中间大量命令 ... */ /* 设置像素格式为 16bit RGB565 */ LCD_WriteCmd(0x3A); LCD_WriteData(0x55); /* 开显示 */ LCD_WriteCmd(0x29); HAL_Delay(50); }上面代码里的0xF000系列命令是 NT35510 的制造商命令集入口,不同厂家可能不同。如果你拿到的初始化代码来自其他驱动芯片,直接替换命令码通常不行,需要对照数据手册逐条核对。我一般会先找屏幕卖家要初始化代码,然后在它的基础上改 FSMC 时序和引脚定义,这样成功率最高。
3.2 设置绘图窗口与连续写像素
刷屏效率的关键在于连续写像素。如果每写一个像素都发一次命令,速度会慢到无法接受。正确做法是先设置绘图窗口(列地址和行地址范围),然后连续写入像素数据,驱动芯片会自动递增地址。
/* 设置绘图窗口 */ void LCD_SetWindow(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { LCD_WriteCmd(0x2A); /* 列地址设置 */ LCD_WriteData(x1 >> 8); LCD_WriteData(x1 & 0xFF); LCD_WriteData(x2 >> 8); LCD_WriteData(x2 & 0xFF); LCD_WriteCmd(0x2B); /* 行地址设置 */ LCD_WriteData(y1 >> 8); LCD_WriteData(y1 & 0xFF); LCD_WriteData(y2 >> 8); LCD_WriteData(y2 & 0xFF); LCD_WriteCmd(0x2C); /* 写显存 */ } /* 填充矩形区域 */ void LCD_FillRect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) { uint32_t total = (uint32_t)w * h; LCD_SetWindow(x, y, x + w - 1, y + h - 1); while (total--) { LCD_WriteData(color); } }LCD_SetWindow里的列地址和行地址都是 16 位,高字节在前。注意 x2 和 y2 是结束坐标,不是宽度和高度,写错会导致窗口偏移。LCD_FillRect里用uint32_t计算总数是为了防止 w×h 超过 65535 时溢出,800×480 全屏填充时总数是 384000,必须用 32 位变量。
3.3 用 DMA 加速刷屏的可行性与限制
STM32F103 的 FSMC 写操作本身已经比较快,但 CPU 逐像素写入仍然占用大量时间。全屏填充 800×480 约 38 万次写操作,每次写操作包括地址计算和总线访问,实测全屏填充约 30-50ms,刷新率约 20-30fps。如果要做流畅动画,这个速度不够。
用 DMA 加速的思路是:把像素数据放在内存缓冲区,DMA 自动搬运到 FSMC 数据地址。但 STM32F103 的 DMA 不支持外设到外设的传输,只能内存到外设。所以需要先在内存里准备好一整行或一整块像素数据,然后触发 DMA 搬运到 FSMC 数据地址。
/* DMA 搬运一行像素到 LCD */ void LCD_DMA_WriteLine(uint16_t *buf, uint32_t len) { DMA_InitTypeDef DMA_InitStruct; DMA_Cmd(DMA1_Channel1, DISABLE); DMA_InitStruct.DMA_PeripheralBaseAddr = LCD_DATA_ADDR; DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)buf; DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralDST; DMA_InitStruct.DMA_BufferSize = len; DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStruct.DMA_Mode = DMA_Mode_Normal; DMA_InitStruct.DMA_Priority = DMA_Priority_High; DMA_InitStruct.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel1, &DMA_InitStruct); DMA_Cmd(DMA1_Channel1, ENABLE); while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET); DMA_ClearFlag(DMA1_FLAG_TC1); }DMA 搬运的限制在于:每次搬运前需要设置窗口,搬运完成后要等 DMA 传输完成才能进行下一次操作。如果缓冲区不够大,频繁设置窗口和等待 DMA 完成的开销会抵消部分加速效果。实际测试中,用 DMA 搬运一行 800 像素,比 CPU 逐像素写快约 3-5 倍,但整体刷新率提升有限,因为窗口设置和命令写入仍然占用时间。
4. 电容触摸接入:I2C 读取、中断处理与坐标映射
4.1 FT5206/GT911 的 I2C 通信与寄存器读取
4.3 寸电容触摸屏常用的触摸控制器是 FT5206 和 GT911,两者都是 I2C 接口,寄存器结构类似。FT5206 的 I2C 地址通常是 0x38,GT911 是 0x5D 或 0x14。读取触摸坐标的基本流程是:读取状态寄存器判断是否有触摸,然后读取坐标寄存器获取触摸点和触摸点数。
/* 读取 FT5206 触摸坐标 */ uint8_t Touch_ReadPoints(uint16_t *x, uint16_t *y) { uint8_t buf[4]; uint8_t points; /* 读取触摸点数量寄存器 */ I2C_ReadReg(FT5206_ADDR, 0x02, &points, 1); points &= 0x0F; if (points == 0) return 0; /* 读取第一个触摸点坐标 */ I2C_ReadReg(FT5206_ADDR, 0x03, buf, 4); *x = ((uint16_t)(buf[0] & 0x0F) << 8) | buf[1]; *y = ((uint16_t)(buf[2] & 0x0F) << 8) | buf[3]; return points; }坐标数据的高 4 位在第一个字节的低 4 位,低 8 位在第二个字节,拼接时要注意移位顺序。GT911 的寄存器地址和 FT5206 不同,但数据格式类似。如果你用的是 GT911,需要先确认 I2C 地址,有些模块通过 INT 引脚在上电时决定地址是 0x5D 还是 0x14。
4.2 触摸中断引脚配置与去抖处理
电容触摸控制器通常有一个 INT 引脚,当有触摸事件时拉低或拉高,通知主控读取坐标。用中断方式比轮询方式效率高,但要注意去抖。电容屏的触摸信号本身比较干净,但机械振动或电磁干扰可能导致误触发。
/* 外部中断配置,假设 INT 接在 PA0 */ void Touch_INT_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; EXTI_InitTypeDef EXTI_InitStruct; NVIC_InitTypeDef NVIC_InitStruct; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); EXTI_InitStruct.EXTI_Line = EXTI_Line0; EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling; EXTI_InitStruct.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStruct); NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0x01; NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0x01; NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStruct); } /* 中断服务函数 */ void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) != RESET) { EXTI_ClearITPendingBit(EXTI_Line0); /* 设置标志位,在主循环中读取坐标 */ touch_flag = 1; } }中断服务函数里不要直接读 I2C,因为 I2C 读取耗时较长,会阻塞其他中断。正确做法是在中断里设置标志位,主循环检测到标志位后再读取坐标。另外,FT5206 的 INT 引脚在触摸持续时可能保持低电平,需要在读取坐标后等待 INT 恢复高电平,否则会重复触发。
4.3 触摸坐标与屏幕坐标的映射校准
触摸控制器返回的坐标范围通常和屏幕分辨率不一致。比如 FT5206 返回的 X 范围可能是 0-799,Y 范围是 0-479,正好匹配 800×480。但有些模块返回的是 0-4095 的原始值,需要做线性映射。更麻烦的是,触摸屏的坐标系可能和屏幕坐标系方向不一致,需要交换 X/Y 或翻转方向。
/* 触摸坐标映射到屏幕坐标 */ void Touch_MapToScreen(uint16_t raw_x, uint16_t raw_y, uint16_t *scr_x, uint16_t *scr_y) { /* 假设原始坐标范围 0-4095,屏幕 800x480 */ /* 根据实际方向调整交换和翻转 */ *scr_x = (uint32_t)raw_x * 800 / 4095; *scr_y = (uint32_t)raw_y * 480 / 4095; /* 如果方向反了,用下面这行 */ /* *scr_x = 800 - *scr_x; */ }映射公式本身简单,但方向调整需要根据实际屏幕安装方向来定。我一般会在屏幕上画四个角点,然后依次触摸,记录原始坐标,再反推映射关系。如果 X 和 Y 反了,就交换;如果某个方向反了,就用最大值减去当前值。这个校准过程一次做好,后面就不用再动。
5. 避坑与排查:屏幕不亮、触摸漂移、刷屏撕裂的 5 个典型问题
5.1 上电后屏幕全白或全黑,没有任何显示
现象:给板子上电后,背光亮但屏幕全白,或者背光不亮屏幕全黑,初始化代码跑完也没反应。
原因:最常见的是 FSMC 地址映射错误,命令和数据地址写反了,导致初始化命令根本没写进驱动芯片。其次是 FSMC 时序太快,驱动芯片来不及响应。还有可能是背光控制引脚没拉高,或者复位引脚时序不对。
解决:先用万用表量背光引脚电压,确认背光是否开启。然后用逻辑分析仪抓 FSMC 的写信号,看命令和数据是否写到了正确的地址。如果地址不对,检查 RS 接的是哪根地址线,重新计算偏移量。如果地址对但没反应,把FSMC_DataSetupTime从 5 加到 10 甚至 15 试试。复位引脚要在初始化前拉低至少 10ms 再拉高,然后等 120ms 再发命令。
5.2 屏幕能显示但颜色不对,红色和蓝色反了
现象:刷红色显示蓝色,刷蓝色显示红色,其他颜色也偏。
原因:RGB565 格式中红色和蓝色的位顺序搞反了。有些驱动芯片默认是 BGR 顺序,有些是 RGB 顺序,初始化序列里有一个命令控制这个顺序。
解决:在初始化序列里找到像素格式设置命令(通常是 0x3A),把参数从 0x55 改成 0x65 或反之。如果改命令没用,就在写像素数据时手动交换红蓝分量:
/* 交换 RGB565 中的红蓝分量 */ uint16_t swap_rb(uint16_t color) { return ((color & 0xF800) >> 11) | (color & 0x07E0) | ((color & 0x001F) << 11); }这个函数会牺牲一些刷屏速度,但能快速验证问题。确认是颜色顺序问题后,优先改初始化命令,不要长期用软件交换。
5.3 触摸坐标跳变严重,手指没动但坐标乱飞
现象:手指按在屏幕上不动,读出来的坐标在几十个像素范围内跳变,或者偶尔跳到屏幕边缘。
原因:I2C 通信受到干扰,或者触摸控制器的供电不稳。电容触摸屏对电源纹波比较敏感,如果 3.3V 电源上有较大噪声,触摸坐标会漂移。另外,I2C 上拉电阻太大或太小也会导致通信误码。
解决:先在触摸控制器的 VDD 引脚旁边加一个 0.1uF 和 10uF 的退耦电容。然后检查 I2C 上拉电阻,通常 4.7k 到 10k 比较合适,太小会增加功耗,太大会导致上升沿变缓。如果问题依旧,在软件里加滑动平均滤波:
/* 滑动平均滤波,窗口大小 5 */ #define FILTER_SIZE 5 static uint16_t x_buf[FILTER_SIZE], y_buf[FILTER_SIZE]; static uint8_t filter_idx = 0; void Touch_Filter(uint16_t *x, uint16_t *y) { uint32_t sum_x = 0, sum_y = 0; x_buf[filter_idx] = *x; y_buf[filter_idx] = *y; filter_idx = (filter_idx + 1) % FILTER_SIZE; for (int i = 0; i < FILTER_SIZE; i++) { sum_x += x_buf[i]; sum_y += y_buf[i]; } *x = sum_x / FILTER_SIZE; *y = sum_y / FILTER_SIZE; }滤波能改善跳变,但会引入延迟。如果触摸响应要求高,窗口大小降到 3。
5.4 刷屏时出现横向撕裂或闪烁
现象:快速刷新屏幕内容时,画面出现横向撕裂,或者局部闪烁。
原因:刷屏过程中,FSMC 写入速度太快,驱动芯片的显存刷新和主控写入冲突。或者刷屏没有等待前一次 DMA 完成,导致数据覆盖。
解决:在每次设置窗口前加一个短延时,或者等待驱动芯片的 TE( tearing effect)信号。但 STM32F103 的 FSMC 没有 TE 信号输入,只能靠软件延时。我一般会在LCD_SetWindow后加 1-2 微秒的延时,给驱动芯片一点缓冲时间。另外,如果用 DMA 刷屏,必须等 DMA 传输完成标志置位后再进行下一次操作,否则数据会错乱。
5.5 触摸和刷屏同时进行时,触摸响应变慢或丢失
现象:屏幕在刷新内容时,触摸反应迟钝,甚至完全没反应。
原因:刷屏占用大量 CPU 时间和 FSMC 总线,触摸中断被延迟处理。如果刷屏是在主循环里阻塞进行的,触摸中断虽然能触发,但主循环没时间读取 I2C 坐标。
解决:把刷屏操作拆分成小块,每刷一小块就检查触摸标志位。或者用定时器中断触发刷屏,主循环专门处理触摸。更好的做法是用 DMA 刷屏,CPU 在 DMA 传输期间可以处理触摸。但要注意 DMA 传输和 I2C 读取不能同时访问 FSMC 和 I2C 总线,需要合理安排优先级。
6. 进阶技巧:用定时器中断实现稳定刷屏与触摸并行
前面提到刷屏和触摸并行处理是个难点,这里展开讲一个我实际用过的方案:用 STM32F103 的定时器 3 产生固定周期中断,在中断里刷一小块屏幕,主循环处理触摸。这样刷屏和触摸互不阻塞,实测触摸响应延迟低于 20ms,刷屏帧率稳定在 25fps 左右。
先配置定时器 3,假设系统时钟 72MHz,预分频 7199,自动重装载值 99,中断频率就是 72MHz / 7200 / 100 = 100Hz,即每 10ms 触发一次中断。
/* 定时器 3 配置,100Hz 中断 */ void TIM3_Init(void) { TIM_TimeBaseInitTypeDef TIM_InitStruct; NVIC_InitTypeDef NVIC_InitStruct; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); TIM_InitStruct.TIM_Prescaler = 7199; TIM_InitStruct.TIM_Period = 99; TIM_InitStruct.TIM_ClockDivision = TIM_CKD_DIV1; TIM_InitStruct.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_InitStruct); TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); NVIC_InitStruct.NVIC_IRQChannel = TIM3_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0x02; NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0x02; NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStruct); TIM_Cmd(TIM3, ENABLE); } /* 刷屏缓冲区,一行像素 */ static uint16_t line_buf[800]; static uint16_t current_line = 0; /* 定时器中断服务函数 */ void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); /* 刷一行像素 */ LCD_SetWindow(0, current_line, 799, current_line); for (int i = 0; i < 800; i++) { LCD_WriteData(line_buf[i]); } current_line++; if (current_line >= 480) { current_line = 0; /* 一帧刷完,可以在这里更新缓冲区内容 */ } } }这个方案的关键点:定时器中断优先级要低于触摸中断,但高于主循环。每 10ms 刷一行,480 行需要 4.8 秒刷完一帧,帧率只有 0.2fps,太慢了。所以实际使用时,每次中断刷 10 行,这样 48 次中断刷完一帧,耗时 480ms,帧率约 2fps,还是不够。
要提高到 25fps,需要每 10ms 刷 20 行,即每次中断刷 20 行,24 次中断刷完一帧,耗时 240ms,帧率约 4fps。还是不够。问题在于 CPU 逐像素写入太慢,必须用 DMA。
改进方案:在定时器中断里触发 DMA 搬运一行,DMA 传输完成后触发下一个中断。但这样中断嵌套会复杂化。更实际的做法是:用定时器中断触发 DMA 搬运一整块(比如 40 行),DMA 传输期间 CPU 处理触摸,DMA 完成后设置标志位,下一次定时器中断检查标志位再触发下一块。
/* DMA 传输完成标志 */ volatile uint8_t dma_done = 1; /* 定时器中断里触发 DMA */ void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); if (dma_done) { dma_done = 0; /* 触发 DMA 搬运 40 行,共 32000 像素 */ LCD_SetWindow(0, current_block * 40, 799, current_block * 40 + 39); LCD_DMA_WriteBlock(frame_buf + current_block * 40 * 800, 40 * 800); current_block++; if (current_block >= 12) { current_block = 0; } } } } /* DMA 传输完成中断 */ void DMA1_Channel1_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC1) != RESET) { DMA_ClearITPendingBit(DMA1_IT_TC1); dma_done = 1; } }这个方案里,DMA 搬运 32000 像素大约需要 32000 × 70ns ≈ 2.24ms,远小于 10ms 的中断周期。所以每次定时器中断都能触发一次 DMA,刷完 12 块需要 120ms,帧率约 8fps。如果要把帧率提到 25fps,需要减少每块的行数,比如每次搬 10 行,但这样 DMA 传输时间占比降低,中断开销增加。实际调试时需要在帧率和 CPU 占用之间找平衡。
我一般会先用这个框架跑通,然后根据实际需求调整块大小。如果只是显示静态界面和简单动画,8fps 够用。如果要流畅滑动列表,至少需要 20fps,那就得把 FSMC 时序再优化,或者换更高主频的 STM32F4 系列。
最后说一个血泪教训:调试屏幕时,先确保 FSMC 读写单个像素正确,再调初始化序列,最后接触摸。不要一上来就全接好然后一起调,出了问题根本不知道是哪一层。我习惯先用最简单的代码点亮屏幕,显示纯色,确认硬件链路没问题,再逐步加功能。这个顺序能省下大量排查时间。希望帮到你。
本文还有配套的精品资源,点击获取