STM32 FSMC驱动ILI9341 TFT2.8彩屏时序与初始化实战
简介:面向STM32嵌入式开发者的2.8英寸TFT彩屏驱动实践包,以STM32F103VET6为主控,围绕FSMC并行接口驱动ILI9341液晶控制器展开,同时给出标准库与HAL库两套驱动写法,适合已掌握GPIO、中断基础、希望打通「微控制器—外部LCD控制器」链路的中级学习者,也适合作为课程设计与小项目的显示模块起点。压缩包共524个文件、约29.62MB,主体为135个h头文件与88个c源文件,另有调试与编译中间产物(84个d依赖、84个o目标、82个crf浏览信息)、Keil工程文件(uvprojx、uvoptx、sct分散加载、axf与hex可执行镜像),以及说明文档、网页笔记和datasheet类pdf,工程层次完整、可直接编译对照。已有1586人学习下载。资源价值在于把FSMC的Bank1时序配置、GPIO复用连线、ILI9341初始化命令序列、显示窗口与像素格式设置等关键环节落到可运行代码,读者可对照F10x手册与两份库的差异,理解寄存器直写与抽象API的取舍,并借map、lst等产物排查链接与内存占用问题。
1. tft2.8.zip 拆开看:一块 240×320 屏为什么先要盯 FSMC 时序
把 tft2.8.zip 解到工程里,第一眼看到的不该是 UI,而是 STM32F10x 手册里 FSMC 那几页时序表。这个资源包围绕 TFT2.8 彩屏和 ILI9341 驱动展开,同时给了 HAL 与标准库两套底层适配,适合手里有 240×320 并口屏、想用 FSMC 把刷屏从 GPIO 模拟里解放出来的开发者。它解决的是“屏能亮但刷得慢、换库就移植不动”的具体问题,不是通用图形库。新手能按接线和初始化序列跑通,熟手可以直接看时序参数与地址映射的边界。先读 ID 再点屏,是我踩过白屏之后留下的习惯。
2. 接线与 FSMC 映射:把 16 位并口挂到 0x6C000000 的四个关键参数
2.1 先从引脚表反推子区和地址线
ILI9341 的并口屏常见是 8080 时序,数据线 D0-D15、片选 CS、写 WR、读 RD、命令/数据选择 RS、复位 RESET、背光 BL。STM32F10x 带 FSMC 的型号会把这些信号映射到 Bank1 的 NOR/SRAM 子区,常见接法是 CS 接 NE4,RS 接 A18 或 A16,数据线接 FSMC_D0-D15。子区 4 的基址是 0x6C000000,这个基址不是随便选的,它决定了后面命令地址和数据地址的偏移。很多人把屏当 GPIO 并口点得亮,一换 FSMC 就白屏,问题往往不在初始化序列,而在 RS 地址线算错了。
16 位数据宽度下,FSMC 的 HADDR[25:1] 对应外部地址线 A[24:0],所以地址偏移不是简单的1 << 线号,而是1 << (线号 + 1)。RS 接 A18 时,数据地址偏移是 0x80000;RS 接 A16 时,偏移是 0x20000。网上不少代码写 0x6C040000,那是把 8 位模式的偏移直接搬过来了,16 位并口下会差一倍,表现就是命令和数据串位、花屏或者完全没反应。我一般会把地址宏单独放一个头文件,换板子只改一行,不把魔法数字散落在驱动里。
/* lcd_bus.h:地址宏只留一处,换 RS 接线时只改 LCD_RS_LINE */ #define LCD_FSMC_BASE 0x6C000000UL /* Bank1 NE4 基址 */ #define LCD_RS_LINE 18U /* RS 接 FSMC_A18 */ /* 16 位模式下 HADDR[25:1] 对应 A[24:0],偏移为 1<<(线号+1) */ #define LCD_DATA_OFFSET (1UL << (LCD_RS_LINE + 1U)) #define LCD_CMD_ADDR (LCD_FSMC_BASE) #define LCD_DATA_ADDR (LCD_FSMC_BASE + LCD_DATA_OFFSET) #define LCD_WR_CMD(cmd) (*(volatile uint16_t *)LCD_CMD_ADDR = (uint16_t)(cmd)) #define LCD_WR_DATA(dat) (*(volatile uint16_t *)LCD_DATA_ADDR = (uint16_t)(dat)) #define LCD_RD_DATA() (*(volatile uint16_t *)LCD_DATA_ADDR)这段宏的逻辑是把“命令”和“数据”拆成两个固定地址:访问LCD_CMD_ADDR时 RS 为低,访问LCD_DATA_ADDR时 RS 为高。参数上只需要确认三件事:CS 是否接 NE4,RS 实际接的是 A18 还是 A16,数据线是不是 16 位全接。如果 RS 接 A16,把LCD_RS_LINE改成 16,偏移会变成 0x20000;如果屏排线只接了 8 位数据,那整套地址和时序都要按 8 位模式重算,不能直接套这份 16 位配置。
2.2 GPIO 与 FSMC 初始化:标准库版本先跑通
标准库工程里,FSMC 的 GPIO 要配成复用推挽,速度先给 50MHz;复位脚和背光脚是普通推挽,不在这段复用配置里。数据线分布在多个端口,具体引脚以芯片手册的 FSMC 复用表为准,下面这段给的是常见 F103ZE 类封装下的写法,换封装要按手册改端口。FSMC 本身要开 AHB 时钟,GPIO 开 APB2 时钟,顺序不重要,但漏开 FSMC 时钟会直接卡在初始化后无响应。
/* 标准库:Bank1 NE4 + 16 位 SRAM 模式,RS 接 A18 */ static void LCD_FSMC_Init(void) { GPIO_InitTypeDef gpio; FSMC_NORSRAMInitTypeDef fsmc; FSMC_NORSRAMTimingInitTypeDef rd, wr; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_FSMC, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE | RCC_APB2Periph_GPIOG, ENABLE); /* D0-D15:按封装查表,这里只示意常见分组 */ gpio.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_14 | GPIO_Pin_15; gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOD, &gpio); gpio.GPIO_Pin = GPIO_Pin_7 | GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_11 | GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_Init(GPIOE, &gpio); /* NE4=PG12,A18=PD13,实际以板子原理图为准 */ gpio.GPIO_Pin = GPIO_Pin_12; GPIO_Init(GPIOG, &gpio); gpio.GPIO_Pin = GPIO_Pin_13; GPIO_Init(GPIOD, &gpio); /* 读时序先保守,写时序先跑起来再压 */ rd.FSMC_AddressSetupTime = 1; rd.FSMC_AddressHoldTime = 0; rd.FSMC_DataSetupTime = 15; rd.FSMC_BusTurnAroundDuration = 0; rd.FSMC_CLKDivision = 0; rd.FSMC_DataLatency = 0; rd.FSMC_AccessMode = FSMC_AccessMode_A; wr = rd; wr.FSMC_AddressSetupTime = 0; wr.FSMC_DataSetupTime = 1; fsmc.FSMC_Bank = FSMC_Bank1_NORSRAM4; fsmc.FSMC_DataAddressMux = FSMC_DataAddressMux_Disable; fsmc.FSMC_MemoryType = FSMC_MemoryType_SRAM; fsmc.FSMC_MemoryDataWidth = FSMC_MemoryDataWidth_16b; fsmc.FSMC_BurstAccessMode = FSMC_BurstAccessMode_Disable; fsmc.FSMC_AsynchronousWait = FSMC_AsynchronousWait_Disable; fsmc.FSMC_WaitSignalPolarity = FSMC_WaitSignalPolarity_Low; fsmc.FSMC_WrapMode = FSMC_WrapMode_Disable; fsmc.FSMC_WaitSignalActive = FSMC_WaitSignalActive_BeforeWaitState; fsmc.FSMC_WriteOperation = FSMC_WriteOperation_Enable; fsmc.FSMC_WaitSignal = FSMC_WaitSignal_Disable; fsmc.FSMC_ExtendedMode = FSMC_ExtendedMode_Enable; fsmc.FSMC_WriteBurst = FSMC_WriteBurst_Disable; fsmc.FSMC_ReadWriteTimingStruct = &rd; fsmc.FSMC_WriteTimingStruct = ≀ FSMC_NORSRAMInit(&fsmc); FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM4, ENABLE); }这段初始化的关键参数有四个:FSMC_Bank选 NE4,FSMC_MemoryDataWidth选 16 位,FSMC_ExtendedMode打开读写分开时序,FSMC_WriteOperation使能写。读时序的FSMC_DataSetupTime = 15是给读 ID 留的余量,写时序先给 1 个周期,屏能亮再往下压。FSMC_AccessMode_A是异步 SRAM 的常用模式,不要随手改成 B/C/D 去试,除非你清楚对应时序公式。写时序太快时,常见现象不是完全不亮,而是局部花屏、颜色噪点,或者刷到某一行突然断掉,这种比全白更难查。
2.3 时序参数先慢后快,别一上来追 72MHz
HCLK 跑 72MHz 时,一个周期约 13.9ns。ILI9341 的并口写周期有余量,但读 ID 和读 GRAM 对时序更敏感,尤其是排线长、电平转换芯片多的时候。我一般先让读时序很慢,读到正确的 0x9341,再把写时序压到 1 到 2 个数据建立周期。下面这张表是常用起点,不是唯一答案,屏排线短、供电干净时可以继续压;如果出现花屏,先把写DATAST加到 3 或 5,再回头查地址线。
| 参数 | 读时序起点 | 写时序起点 | 作用 |
|---|---|---|---|
ADDSET | 1 | 0 | 地址建立时间,RS 变化到 WR/RD 有效 |
ADDHLD | 0 | 0 | 地址保持时间,异步模式一般不用长 |
DATAST | 15 | 1 | 数据建立/保持,读 ID 要留足 |
BTR | 0 | 0 | 总线恢复,长排线可加 1 |
CLKDIV | 0 | 0 | 异步模式不用时钟分频 |
参数怎么调:读 ID 返回 0xFFFF 或 0x0000,先把读DATAST加到 20,确认 CS 和 RS 地址没错;写花屏先把写DATAST加到 3,再检查数据线有没有虚焊。读时序里ADDSET=1是给 RS 建立留一个周期,写时序ADDSET=0在短排线下通常能跑。不要一次把读写都压到最小,否则出问题时分不清是读坏还是写坏。血泪经验是:先让屏稳定显示一张纯色图,再谈帧率。
2.4 HAL 版本改哪几行
HAL 工程不需要把标准库那套FSMC_NORSRAMInit抄一遍,核心是HAL_SRAM_Init。GPIO 模式从GPIO_Mode_AF_PP换成GPIO_MODE_AF_PP,速度用GPIO_SPEED_FREQ_HIGH,时钟使能改成__HAL_RCC_FSMC_CLK_ENABLE()。HAL_SRAM_Init内部会完成 NORSRAM 初始化和 Bank 使能,传进去的FSMC_NORSRAM_TimingTypeDef和标准库字段语义基本一致,只是名字更长。
| 标准库写法 | HAL 写法 |
|---|---|
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_FSMC, ENABLE) | __HAL_RCC_FSMC_CLK_ENABLE() |
GPIO_Mode_AF_PP | GPIO_MODE_AF_PP |
GPIO_Speed_50MHz | GPIO_SPEED_FREQ_HIGH |
FSMC_NORSRAMInit(&fsmc) | HAL_SRAM_Init(&hsram, &rd, &wr) |
FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM4, ENABLE) | HAL_SRAM_Init内部使能 |
HAL 下要确认hsram.Instance指向FSMC_NORSRAM_DEVICE,扩展模式指向FSMC_NORSRAM_EXTENDED_DEVICE,并且工程里真的把stm32f1xx_hal_sram.c加进来了。只开HAL_SRAM_MODULE_ENABLED不加源文件,编译能过但链接会缺函数。换库时别把标准库的FSMC_NORSRAMCmd和 HAL 的HAL_SRAM_Init混着调,重复初始化会让 Bank 状态变得很玄学。
3. ILI9341 初始化序列:从软复位到 RGB565 扫描方向的命令级复现
3.1 读写原语:命令、数据、读回
ILI9341 的驱动层不需要一上来就写图形库,先把命令、数据、读回三个原语做稳。写命令是往命令地址写 16 位,写数据是往数据地址写 16 位,读数据是从数据地址读 16 位。读 ID 时要注意总线切换后的第一个数据经常是假值,常见做法是空读一到两次再取返回值。读时序没调好时,读回值会在 0x0000 和 0xFFFF 之间跳,这不是屏坏了,而是建立时间不够。
static inline void LCD_WriteCmd(uint16_t cmd) { LCD_WR_CMD(cmd); } static inline void LCD_WriteData(uint16_t data) { LCD_WR_DATA(data); } static uint16_t LCD_ReadData(void) { return LCD_RD_DATA(); } static uint16_t LCD_ReadID(void) { LCD_WriteCmd(0x04); /* 读 ID 命令 */ (void)LCD_ReadData(); /* 空读,丢掉总线切换假数据 */ (void)LCD_ReadData(); return LCD_ReadData(); /* 常见返回 0x9341 */ }这段代码的关键在LCD_ReadID的空读。参数上,0x04命令返回多个字节,16 位总线每次读 16 位,实际拼接要按芯片手册来,这里只取常见的低 16 位做存在性判断。如果返回值稳定是 0x9341,说明 CS、RS、读时序基本通了;如果一直是 0xFFFF,优先查读DATAST和 CS 是否真的拉低;如果一直是 0x0000,查数据线是否全被拉低或者 RS 地址是否落在命令区。
3.2 初始化表逐段拆
ILI9341 的初始化序列看起来长,其实分四段:电源和 VCOM 配置、驱动能力与帧率、像素格式和扫描方向、Gamma 与睡眠退出。下面这份是常见 240×320 屏的起点序列,不同排线的 Gamma 和 VCOM 可能有差异,但命令顺序大体一致。写序列时不要一边写一边延时,只有软复位、退出睡眠、开显示这三个点需要明显延时,其他命令连续写即可。
static void LCD_InitSequence(void) { LCD_WriteCmd(0x01); /* 软复位 */ LCD_DelayMs(5); LCD_WriteCmd(0xCF); /* 电源控制 B */ LCD_WriteData(0x00); LCD_WriteData(0x83); LCD_WriteData(0x30); LCD_WriteCmd(0xED); /* 电源控制序列 */ LCD_WriteData(0x64); LCD_WriteData(0x03); LCD_WriteData(0x12); LCD_WriteData(0x81); LCD_WriteCmd(0xE8); /* 驱动时序控制 A */ LCD_WriteData(0x85); LCD_WriteData(0x <p> <a href="https://download.csdn.net/download/sf9090/12733469" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>