STM32F4硬核实战:FSMC驱动TFTLCD从原理到代码全解析,附避坑指南+调试干货
序言
在嵌入式开发的世界里,STM32F4系列MCU因其强大的性能和灵活性,成为了无数工程师的首选。而当我们需要为项目添加一个直观的人机交互界面时,用FSMC(灵活静态存储器控制器)驱动TFTLCD几乎是绕不开的核心技能——这不仅是硬件接口的挑战,更是时序逻辑与代码实现的深度博弈。
你是否曾在配置FSMC时序时因参数匹配问题抓耳挠腮?是否在驱动LCD时被地址映射、数据宽度等细节搞得晕头转向?这篇文章将从底层原理出发,带你打通“FSMC驱动TFTLCD”的任督二脉:从FSMC的存储区块划分、信号引脚功能,到TFTLCD的接口类型、驱动芯片指令集,再到硬件电路与代码的深度耦合实现,每一个环节都配有原理图解析、时序图说明和调试案例。
特别值得一提的是,文中详细记录了“读取芯片ID时因指针类型错误导致调试失败”的真实踩坑经历——这些在官方手册里不会写明的细节,恰恰是开发中最容易卡壳的地方。无论是想系统学习STM32外设开发的新手,还是需要优化项目显示模块的资深工程师,都能从原理剖析、代码集成到实战调试中,找到提升开发效率的关键钥匙。
文末彩蛋:完整的代码片段、ILI9341手册关键时序图已整理完毕,关注作者即可获取更多嵌入式开发干货。如果你也曾在FSMC驱动LCD的路上踩过坑,或是有更高效的实现方案,欢迎在评论区交流——让我们一起把嵌入式开发的细节抠到极致!
📌 互动引导:
- 觉得内容实用?点击❤️点赞支持,让更多开发者看到!
- 收藏本文,随时查阅FSMC与TFTLCD开发的核心要点。
- 转发给团队伙伴,共同攻克嵌入式显示开发难题~
- 关注作者,后续持续更新STM32外设实战、RTOS优化等硬核内容!
一、STM32F4的FSMC原理详解
1. FSMC概述
FSMC(Flexible Static Memory Controller,灵活静态存储器控制器)是STM32F4系列微控制器中的一个重要外设,主要用于连接外部存储器设备。它提供了与多种类型存储器的接口能力,包括:
- NOR Flash/PSRAM存储器
- NAND Flash/PC卡存储器
- SRAM存储器
- 16位或8位数据宽度的LCD接口(通过模拟8080时序)
2. FSMC主要特性
- 多种存储器支持:支持SRAM、ROM、NOR Flash、NAND Flash、PC卡等
- 大地址空间:最多可支持1GB的寻址空间(分为4个256MB的存储块)
- 可配置时序:可为不同速度的存储器配置不同的访问时序
- 数据宽度灵活:支持8位、16位数据宽度
- 写使能和字节选择:支持字节/半字/字访问
- 扩展模式:支持同步传输和扩展模式
3. STM32F4 FSMC结构图解析
图中展示了STM32F4系列微控制器中FSMC(灵活静态存储器控制器)的内部结构和相关信号引脚。下面我将详细解析图中的各个部分和引脚含义。
1. FSMC整体结构
从图中可以看出FSMC主要由以下几个部分组成:
-
与内核的连接:
- 通过AHB总线与内核通信
- 中断信号连接到NVIC(嵌套向量中断控制器)
- 时钟来自HCLK(高速时钟)
-
主要功能模块:
- 配置寄存器组
- NOR/PSRAM存储器控制器
- NAND/PC卡存储器控制器
-
外部信号引脚:
- NOR/PSRAM相关信号
- NAND相关信号
- PC卡相关信号
- 共享信号
2. 引脚功能详解
2.1 NOR/PSRAM存储器相关信号
-
FSMC_NE[4:1]:
- NOR Flash/PSRAM片选信号
- 对应Bank1的4个子存储块(Bank1-1到Bank1-4)
- 低电平有效
-
FSMC_NL (或NADV):
- 地址有效信号(Address Valid)
- 用于锁存地址信号
-
FSMC_NL[1:0]:
- 字节锁存信号(Byte Latch)
- 用于16位模式下的字节选择
-
FSMC_CLK:
- 时钟输出信号
- 用于同步模式下的时钟提供
-
FSMC_A[25:0]:
- 26位地址总线
- 可寻址最大64MB空间(2^26)
-
FSMC_D[15:0]:
- 16位数据总线
- 支持8位或16位数据传输
-
FSMC_NWE:
- 写使能信号(Write Enable)
- 低电平有效
-
FSMC_NOE:
- 输出使能信号(Output Enable)
- 低电平有效
-
FSMC_NWAIT:
- 等待信号
- 用于插入等待状态,延长访问周期
2.2 NAND Flash相关信号
-
FSMC_NCE[3:2]:
- NAND Flash片选信号
- 对应Bank2和Bank3
-
FSMC_INT[3:2]:
- 中断信号
- 用于NAND Flash的中断通知
-
FSMC_INTR:
- 中断请求信号
- 连接到NVIC
2.3 PC卡相关信号
-
FSMC_NCE[4:1]:
- PC卡片选信号
- 对应Bank4
-
FSMC_NIORD:
- I/O读信号(I/O Read)
- 低电平有效
-
FSMC_NIOWR:
- I/O写信号(I/O Write)
- 低电平有效
-
FSMC_NREG:
- 寄存器选择信号(Register Select)
- 用于选择PC卡中的寄存器
-
FSMC_CD:
- 卡检测信号(Card Detect)
- 用于检测PC卡是否插入
2.4 共享信号
这些信号在不同模式下有不同的功能:
-
FSMC_NBL[1:0]:
- 字节锁存信号(Byte Lane)
- 在16位模式下用于选择高/低字节
- FSMC_NBL0对应数据低字节(D[7:0])
- FSMC_NBL1对应数据高字节(D[15:8])
-
FSMC_NWAT:
- 等待信号(Wait)
- 用于扩展访问周期
3. 功能模块解析
-
NOR/PSRAM存储器控制器:
- 管理NOR Flash和PSRAM的访问
- 支持异步和同步访问模式
- 可配置不同的时序参数
-
NAND/PC卡存储器控制器:
- 管理NAND Flash和PC卡的访问
- 支持ECC(错误校验与纠正)功能
- 提供特定的控制信号
-
配置寄存器:
- 存储FSMC的工作模式配置
- 包括时序参数、数据宽度、存储器类型等设置
4. FSMC在整个MCU的位置
5. FSMC使用注意事项
-
地址对齐:
- 16位模式下,HADDR地址需要右移1位对应到FSMC_A[24:0]
- 8位模式下,HADDR地址直接对应FSMC_A[25:0]
-
信号复用:
- 部分信号在不同模式下功能不同
- 需要根据实际使用的存储器类型正确配置
-
时序配置:
- 必须根据外部存储器的时序要求
- 设置正确的建立时间、保持时间等参数
4. FSMC存储区块划分
FSMC将1GB的地址空间划分为4个存储块(Bank):
-
Bank1:用于NOR Flash/PSRAM/SRAM,分为4个子块(每个64MB)
- Bank1-1: 0x6000 0000 - 0x63FF FFFF
- Bank1-2: 0x6400 0000 - 0x67FF FFFF
- Bank1-3: 0x6800 0000 - 0x6BFF FFFF
- Bank1-4: 0x6C00 0000 - 0x6FFF FFFF
-
Bank2/3:用于NAND Flash(各256MB)
- Bank2: 0x7000 0000 - 0x7FFF FFFF
- Bank3: 0x8000 0000 - 0x8FFF FFFF
-
Bank4:用于PC卡(256MB)
- Bank4: 0x9000 0000 - 0x9FFF FFFF
5. FSMC主要信号引脚
FSMC使用以下主要信号线与外部存储器通信:
- 地址总线:FSMC_A[25:0] (最多26位地址线)
- 数据总线:FSMC_D[15:0] (16位数据线)
- 控制信号:
- FSMC_NE[3:0]:片选信号(对应Bank1的4个子块)
- FSMC_NWE:写使能
- FSMC_NOE:读使能
- FSMC_NBL[1:0]:字节选择(用于16位模式下的字节选择)
- FSMC_NWAIT:等待信号(用于插入等待状态)
对于不同的存储器类型,还会使用其他特定信号线。
6. FSMC时序配置
FSMC的时序参数可通过寄存器配置,主要包括:
- 地址建立时间(ADDSET):地址建立到读/写信号有效的时间
- 地址保持时间(ADDHLD):仅用于同步突发访问模式
- 数据建立时间(DATAST):读/写信号有效到数据稳定的时间
- 总线恢复时间(BUSTURN):两次访问之间的最小间隔时间
这些参数需要根据外部存储器的时序要求进行配置。
7. FSMC配置步骤
配置FSMC通常包括以下步骤:
- 使能FSMC时钟:通过RCC_AHB3ENR寄存器
- 配置GPIO:将相关引脚配置为FSMC复用功能
- 配置FSMC寄存器:
- 选择存储器类型(SRAM、NOR、NAND等)
- 设置数据宽度(8位或16位)
- 配置时序参数
- 使能相应的存储块
- 访问外部存储器:通过指针访问映射的地址空间
8. 注意事项
- 地址映射:FSMC使用固定的地址映射,访问外部存储器就像访问内部存储器一样。
- 时序匹配:必须根据外部存储器手册配置正确的时序参数
- 数据宽度:16位模式下地址需要左移1位(地址线A0连接存储器的A1)
- 功耗考虑:不使用时可以关闭FSMC时钟以节省功耗
- 干扰问题:高速操作时需要注意信号完整性和电磁兼容性问题
通过合理配置FSMC,STM32F4可以高效地与各种外部存储器或LCD模块通信,大大扩展了系统的存储和显示能力。
二、TFTLCD简介
1. 什么是 TFT LCD?
TFT LCD(Thin-Film Transistor Liquid Crystal Display,薄膜晶体管液晶显示器) 是一种 主动矩阵式液晶显示器,广泛应用于智能手机、平板电脑、电脑显示器、工业控制屏等领域。
- TFT(薄膜晶体管):每个像素点由独立的晶体管控制,可实现 高刷新率、高对比度、快速响应。
- LCD(液晶显示器):利用液晶分子的光学特性显示图像,需要 背光源(LED 或 CCFL)。
2. TFT LCD 的基本结构
TFT LCD 主要由以下部分组成:
- 液晶层(Liquid Crystal):控制光线通过与否。
- TFT 阵列(像素驱动):每个像素点对应一个 TFT 开关。
- 彩色滤光片(Color Filter):生成 RGB 三原色,实现彩色显示。
- 背光模块(Backlight):提供光源(通常为 LED)。
- 偏光片(Polarizer):控制光线偏振方向。
3. TFT LCD 的工作原理
- 背光发射白光 → 穿过 偏光片(变成偏振光)。
- TFT 控制液晶分子旋转,改变光的偏振方向。
- 光线通过 彩色滤光片,形成 RGB 子像素。
- 最终光线通过 第二层偏光片,显示不同颜色。
✅ 优点:
- 高分辨率、色彩鲜艳。
- 响应速度较快(适合视频播放)。
- 可视角度较大(IPS 屏可达 178°)。
❌ 缺点:
- 需要背光,功耗较高(OLED 更省电)。
- 黑色显示不如 OLED 纯粹(因为背光无法完全关闭)。
4. TFT LCD 的接口类型
TFT LCD 通常通过以下接口与控制器(如 STM32、ESP32)通信:
-
SPI(串行接口)
- 优点:引脚少(仅需 4-6 根线),适合低速显示。
- 缺点:刷新率低,适合小屏(如 1.8 寸)。
-
8080 并行接口(MCU 模式)
- 8/16 位数据总线 + 控制信号(WR, RD, CS, RS)。
- 优点:速度快,适合 2.4~3.5 寸屏。
-
RGB 接口
- 直接传输 RGB 数据 + HSYNC/VSYNC 同步信号。
- 优点:超高刷新率,适合大屏(如 4.3 寸以上)。
-
MIPI DSI(移动设备专用)
- 高速串行接口,用于手机、平板。
5. TFT LCD 的驱动芯片
常见的 TFT LCD 驱动 IC 包括:
- ILI9341(240×320,SPI/8080,经典款)
- ST7789(240×240/320,SPI,圆形屏常用)
- ILI9488(320×480,8080,3.5 寸屏)
- SSD1963(480×272,RGB,大屏驱动)
6. ILI9341常用命令
由于ILI9341
驱动芯片很常用,而我的开发板配套的TFTLCD驱动芯片就是它,所以下面介绍一下ILI9341
芯片的常用命令。比如读取芯片ID、控制显示模式、设置地址等。具体命令如下:
-
读取芯片ID指令:读ID指令(0XD3)用于读取LCD控制器的ID。该指令后面跟4个参数,最后2个参数读出来是0X93和0X41,恰好是控制器ILI9341的数字部分,借此可判别所用的LCD驱动器型号,以便根据型号执行对应驱动IC的初始化代码,实现一个代码兼容多款LCD的功能。
-
存储访问控制指令:存储访问控制指令(0X36),可以控制ILI9341存储器的读写方向,即控制连续写GRAM时GRAM指针的增长方向,进而控制显示方式(读GRAM同理) 。该指令紧跟一个参数,主要关注MY、MX、MV这三个位,通过对这三个位的设置能控制整个ILI9341的全部扫描方向。
-
列地址设置指令:列地址设置指令(0X2A),在默认的从左到右、从上到下扫描方式下,用于设置横坐标(x坐标) 。它带有4个参数,实际是2个坐标值:SC和EC,即列地址的起始值和结束值,其中SC必须小于等于EC,且0≤SC/EC≤239。通常设置x坐标时,设置SC即可,若EC无变化,在初始化ILI9341时设置一次便可提高速度。
-
页地址设置指令:页地址设置指令(0X2B),在默认扫描方式下,用于设置纵坐标(y坐标)。它带有4个参数,实际为2个坐标值:SP和EP,即页地址的起始值和结束值,SP必须小于等于EP,且0≤SP/EP≤319 。一般设置y坐标时,设置SP即可,若EP无变化,在初始化时设置一次即可。
-
写GRAM指令:写GRAM指令(0X2C),发送该指令后,便可以向LCD的GRAM中写入颜色数据。该指令支持连续写,收到指令后,数据有效位宽变为16位,GRAM的地址会根据MY/MX/MV设置的扫描方向自增。例如在从左到右、从上到下的扫描方式下,设置好起始坐标(通过SC,SP设置)后,每写入一个颜色值,GRAM地址自动自增1(SC++),碰到EC时回到SC,同时SP++,直至坐标(EC,EP)结束,期间无需再次设置坐标,可提高写入速度。
-
读GRAM指令:读GRAM指令(0X2E)用于读取ILI9341的显存(GRAM) 。ILI9341收到该指令后,第一次输出的是无效的dummy数据,第二次开始输出的才是有效的GRAM数据(从坐标:SC,SP开始)。输出规律为:每个颜色分量占8个位,一次输出2个颜色分量。若只读取一个点的颜色值,接收参数3即可;若要连续读取(利用GRAM地址自增),则按相应规律接收颜色数据。
三、使用FSMC驱动TFTLCD
1. 电路原理图
下面是我的开发板上LCD的接口图,明显采用8080 并行接口
。下面着重关注一下RS
引脚连接的地址线,LCD_CS
引脚连接的片选信号线,BL
引脚连接的MCU引脚。
先看LCD_CS
引脚连接的片选信号线是PG12,复用为FSMC_NE4
。
RS
引脚连接的地址线是FSMC_A6
。
FSMC_NOE
和FSMC_NWE
连接到了STM32的PD4
和PD5
,都是固定引脚,不用管他。
BL
引脚连接的MCU引脚是PB15
。
2. 配置FSMC
根据以上的硬件连接图,我们可以配置FSMC。
1. 模式配置
由于前面说了,我们使用的是NE4,所以就选择NE4;内存类型直接选择LCD接口;RS引脚连接的是A6,所以选择A6;数据位是16位的。
2. 参数配置
选择LCD接口,使能写操作,对于时序配置,参考LCD的驱动芯片ILI9341手册来决定。
3. ILI9341手册中8080并行接口时序图
4. 时序配置
由于读时序,数据建立时间最长是450ns;写时序,数据建立时间最长是66ns,写数据的话,数据建立时间大于66纳秒就可以。而时钟频率是168MHz,所以每个时钟周期是1/168us,约等于6ns。所以配置地址建立时间数值=1,也就是6纳秒;数据建立时间数值=80,也就是480纳秒;总线读写操作切换间隔=1,也就是6ns。这样的设置足够满足要求。
3. 代码集成
初始化函数
STM32CubeIDE生成的代码中,会有初始化FSMC的函数,并且在初始化阶段自动调用,我们不用操心。
MX_FSMC_Init();
读取驱动芯片ID
在初始化之后,我们可以读取一下芯片ID,如果读取到了。如果正确读取到了,那就说明我们的驱动已经正常工作了。
1. 直接操作地址的方式实现
#define TFTLCD_BASE 0x6C000000
#define TFTLCD_COMMAND (TFTLCD_BASE & ~(1 << (6 + 1)))
#define TFTLCD_COMMAND_OP ((uint16_t *)TFTLCD_COMMAND)
#define TFTLCD_DATA (TFTLCD_BASE | (1 << (6 + 1)))/*0000 1000 0000*/
#define TFTLCD_DATA_OP ((uint16_t *)TFTLCD_DATA)/*0000 1000 0000*/
/*初始化TFTLCD*/
uint16_t * pcmd = TFTLCD_COMMAND_OP;
uint16_t * pdata = TFTLCD_DATA_OP;
uint32_t id = 0;
*pcmd = 0xD3;
id = *pdata;/*读取的东西撇弃掉*/
id = *pdata;/*00*/
id = *pdata;/*93*/
id <<= 8;
id |= *pdata;/*41*/
上面的(1 << (6 + 1)
是因为PA6
连接到RS
引脚的缘故。而+1
的原因是HADDR的右移一位造成的。只要控制PA6对应的地址线高低电平,就可以得到发送数据和命令的效果。
2. 调试验证
3. 定义结构体的方式实现(原子哥的实现方式)
//LCD地址结构体
typedef struct
{
__IO uint16_t LCD_REG;
__IO uint16_t LCD_RAM;
} LCD_TypeDef;
#define LCD_BASE ((uint32_t)(0x6C000000 | 0x0000007E))
#define LCD ((LCD_TypeDef *) LCD_BASE)
回顾上面第一章 4. FSMC存储区块划分
,就可以理解上面的地址定义。
在MCU内部的地址是由HADDR决定的,它与FSMC有一一对应的关系,但根据FSMC使用的是8位宽,还是16位宽,HADDR决定是否右移一位与FSMC对应。
首先,0x6C000000
的地址取决于下图,我们使用的是第4区,所以HADDR[27:26]=11,地址范围是0x6C000000
起始的。
那后面的0x0000007E
换算成二进制,最后面是0111 1110
。而针对16bit位宽,HADDR[0]没有用到,所以HADDR[25:1]右移一位,对应到了芯片引脚的FSMC_A[24:0]。打比方,0000 0010
这里只有第1位等于1,但对应到FSMC,就是0000 0001
,也就是只有A[0] = 1。
于是,0111 1110
右移一位,得到0011 1111
,于是此时的PA6,也就是地址的Bit6 = 0。注意,这个时候,地址增加1,对应到实际的地址操作是增加了2个字节,因为是16位寻址。跨度是2个字节。这个时候,回头看下结构体LCD_TypeDef
的定义,里面定义了2个uint16_t的整型变量。LCD_REG所在的地址是LCD_BASE,也就是PA6=0;而LCD_RAM对应的地址是LCD_BASE+2,对应的PA6=1。如此就区别了数据和命令的发送/接收。
在ILI9341芯片中,与数据/命令选择相关的信号是 D/CX(原理图显示为RS),其电平状态决定了传输内容的类型:
- 当D/CX为高电平时:传输的内容为 数据(如显示数据、命令参数)。
- 当D/CX为低电平时:传输的内容为 命令。
对于RS
引脚,高电平
代表数据
,低电平
代表命令
。所以对LCD_REG
写操作就是写命令;对LCD_RAM
的读写操作就是读写数据。
下面调试一下,看看地址:
下面是具体的代码,对于TFTLCD读写操作:
/*写命令*/
void LCD_WR_REG(volatile uint16_t regval)
{
LCD->LCD_REG=regval;
}
/*读数据*/
uint16_t LCD_RD_DATA(void)
{
volatile uint16_t ram;
ram = LCD->LCD_RAM;
return ram;
}
void LCD_Init(void)
{
uint32_t id = 0;
LCD_WR_REG(0XD3);
id = LCD_RD_DATA(); //dummy read
id = LCD_RD_DATA(); //读到0X00
id = LCD_RD_DATA(); //读取93
id = id << 8;
id |= LCD_RD_DATA(); //读取41
}
4. 调试验证
5. 记录踩的一个坑
在上面 1. 直接操作地址的方式实现
章节,我踩了一个坑,导致读取显示屏驱动芯片的ID错误,百思不得其解……让我无心午休,一直检查到现在(15:03),终于找到了问题的根源,并解决了。
- 错误点
将地址强转指针的时候,使用了uint32_t
。我当时想,STM32不是32位的MCU吗?转换成的地址就应该是uint32_t
。
#define TFTLCD_BASE 0x6C000000
#define TFTLCD_COMMAND (TFTLCD_BASE & ~(1 << (6 + 1)))
#define TFTLCD_COMMAND_OP ((uint32_t *)TFTLCD_COMMAND)
#define TFTLCD_DATA (TFTLCD_BASE | (1 << (6 + 1)))/*0000 1000 0000*/
#define TFTLCD_DATA_OP ((uint32_t *)TFTLCD_DATA)/*0000 1000 0000*/
- 解决方法
在LCD的访问上,LCD与FSMC是16
位宽的地址线,所以只能使用uint16_t
。
#define TFTLCD_BASE 0x6C000000
#define TFTLCD_COMMAND (TFTLCD_BASE & ~(1 << (6 + 1)))
#define TFTLCD_COMMAND_OP ((uint16_t *)TFTLCD_COMMAND)
#define TFTLCD_DATA (TFTLCD_BASE | (1 << (6 + 1)))/*0000 1000 0000*/
#define TFTLCD_DATA_OP ((uint16_t *)TFTLCD_DATA)/*0000 1000 0000*/
绘制一个矩形
涉及的命令有0x2A、0x2B、0x2C,具体的使用方法请参考前面第二章:6. ILI9341常用命令
章节内容。
void LCD_Draw_Rectangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color)
{
LCD_WR_REG(0x2A);
LCD_WR_DATA(x1 >> 8);
LCD_WR_DATA(x1 & 0xff);
LCD_WR_DATA(x2 >> 8);
LCD_WR_DATA(x2 & 0xff);
LCD_WR_REG(0x2B);
LCD_WR_DATA(y1 >> 8);
LCD_WR_DATA(y1 & 0xff);
LCD_WR_DATA(y2 >> 8);
LCD_WR_DATA(y2 & 0xff);
LCD_WR_REG(0x2C);
for (uint8_t i = 0; i < (x2 - x1 + 1) * (y2 - y1 + 1); i++)
{
LCD_WR_DATA(color); /*只写进去一个点*/
}
}
在初始化之后调用此函数,可以在LCD上绘制实心矩形。分别给定两个坐标点(x1,y1)和(x2,y2),就确定了矩形的位置,之后给定颜色,就可以绘制出矩形。当然坐标点要在显示屏的范围内。
如果已经正确读取了芯片ID,也正确绘制了矩形,那么就已经实现了FSMC驱动LCD的功能。其余的操作就是移植开发板的驱动代码了。这里就不赘述了。
如果移植上遇到困难,欢迎留言。
四、参考文献
《ILI9341参考手册》
《STM32F4中文参考手册》
《正点原子 STM32F4开发手册》
全文完,如果对你有帮助,欢迎点赞、转发、收藏!