1. 从“为什么需要FSMC/FMC”说起
如果你刚开始接触STM32,尤其是那些带屏幕、带SRAM、带NAND Flash的“大”项目,你可能会被一个叫FSMC或者FMC的外设搞得一头雾水。数据手册上那一大堆寄存器、时序图,看起来复杂得让人想放弃。别急,今天我们就来把这个东西彻底掰开揉碎了讲清楚,让你从“这是什么鬼”变成“原来这么回事”。
首先,FSMC和FMC本质上是一回事,都是STM32用来连接外部存储器的控制器。FSMC是“Flexible Static Memory Controller”的缩写,主要用在STM32F1、F4等系列上。而FMC是“Flexible Memory Controller”,可以看作是FSMC的升级版,功能更强,支持的内存类型更多,主要用在STM32F7、H7等高性能系列上。为了方便,我们下文统一用FSMC来指代这个功能模块,但在原理上是相通的。
那么,STM32内部不是有Flash和SRAM吗?为什么还要外接?原因很简单:不够用,或者不够快。比如你要驱动一个800x480的RGB屏,一帧图像的数据量就接近1.5MB,STM32内部的SRAM通常只有几十到几百KB,根本放不下。再比如,你想跑一个复杂的图形界面或者文件系统,需要大容量的存储,内部Flash也捉襟见肘。这时候,你就需要外挂一片SRAM来当“显存”或“数据池”,或者外挂一片NOR Flash、NAND Flash、SDRAM来扩展存储。
问题来了:怎么连接?最笨的办法就是用普通的GPIO口,模拟8080并行总线的时序,一个字节一个字节地去“怼”数据。我早期做项目就这么干过,代码写起来冗长,速度慢(受限于CPU操作IO的速度),而且极度占用CPU资源,屏幕刷一下,CPU啥也别干了。FSMC就是为了解决这个问题而生的。它的核心价值,就是把访问外部存储器这个“体力活”,交给一个专用的硬件控制器来完成。你只需要配置好这个控制器的参数(告诉它外接的是什么芯片、速度多快),然后就可以像访问内部内存一样,用一个指针直接读写外部芯片了。CPU发出一条读/写指令,FSMC硬件会自动产生符合该芯片时序要求的控制信号(片选、读/写、地址、数据线),整个过程无需CPU干预,效率极高。
简单来说,FSMC就是一个“协议转换器”和“信号发生器”。它把CPU发出的内部总线访问(比如通过AHB总线),转换成符合外部芯片通信协议(如8080并行接口、SRAM接口、NOR Flash接口)的波形。这就像你(CPU)想去一个说外语的国家(外部芯片),FSMC就是一个随身翻译(协议转换)兼司机(信号驱动),你只需要说出目的地,剩下的沟通和路线它全包了。
2. FSMC的“地址映射”魔法:把外部芯片变成内存的一部分
这是理解FSMC最核心、也最巧妙的一点。STM32的CPU寻址空间是4GB(32位地址线)。这片巨大的“土地”被划分成了不同的区域,用于映射不同的东西:内部Flash、内部SRAM、外设寄存器等等。FSMC控制器独占了一大块连续的地址空间,通常是0x6000 0000到0x9FFF FFFF(具体起始和大小因型号而异,需查数据手册)。
FSMC内部又把这大片地址空间,分成了好几个“Bank”(存储块),常见的有4个(Bank1-Bank4)。每个Bank对应着一组独立的外部设备,拥有独立的片选信号(NE1, NE2, NE3, NE4 或 NEx)。更重要的是,每个Bank还可以通过配置,支持不同类型的内存:比如Bank1通常用于NOR Flash/PSRAM,Bank2和Bank3可用于NAND Flash,Bank4可用于PC卡等。对于最常用的LCD屏(8080接口)和SRAM,我们通常使用支持NOR/PSRAM的Bank,比如Bank1。
那么,这个“映射”具体是怎么发生的呢?假设我们使用Bank1,并将其起始地址配置为0x6000 0000。当我们把一片16位宽度的SRAM芯片连接到FSMC的Bank1上后,神奇的事情发生了:你访问单片机内存地址0x6000 0000,实际上就是在访问这片外部SRAM芯片的第一个存储单元。
举个例子:
- 你执行一句C语言:
*(volatile uint16_t *)(0x60000000) = 0x1234; - CPU试图向地址0x6000 0000写入数据0x1234。
- 内存管理单元发现这个地址落在FSMC管理的地址范围内,于是把这个写请求转交给FSMC控制器。
- FSMC控制器一看,这个地址属于Bank1的区域,于是它:
- 拉低对应Bank1的片选信号NE1(假设我们用的是NE1),选中我们的SRAM芯片。
- 根据配置的内存宽度(16位),在地址线A[25:0](具体用了多少根取决于芯片容量)上输出地址0x0(因为0x6000 0000是Bank1的基址,相对偏移为0)。
- 拉低写使能信号NWE。
- 将数据0x1234放到16位数据线D[15:0]上。
- 等待预先配置好的“写建立时间”、“写保持时间”结束。
- 拉高NWE和NE1,完成一次写操作。
整个过程,你只用了一条指针赋值语句,FSMC硬件就自动完成了一整套复杂的并行总线时序!读操作也是同理。这样一来,对外部存储器的操作,在编程层面就简化成了对一段特定内存地址的读写,极其方便高效。驱动LCD屏时,我们只需要把LCD的数据/命令寄存器地址映射到FSMC的某个地址,然后向这个地址写数据或命令即可,LCD驱动芯片会通过8080接口识别并执行。
2.1 关键配置:存储器类型与数据宽度
在配置FSMC时,你必须明确告诉它两件事:你接的是什么类型的芯片?以及数据总线是8位还是16位?
存储器类型:主要分为SRAM/NOR、PSRAM、NOR Flash等。不同类型的芯片,其读写时序的特征不同。例如,NOR Flash通常需要较长的地址建立时间。FSMC通过“存储器类型”配置来选择内置的、最接近该类型芯片的默认时序模型,作为我们进一步调整的基础。
数据宽度:这是最容易出错的地方之一。它指的是FSMC控制器与外部芯片之间数据总线的位宽,可以是8位或16位。这个选择直接影响硬件连接和地址生成。
- 8位宽度:使用数据线D[7:0]。当你访问一个32位变量(如
uint32_t)时,FSMC会自动将其拆成4个连续的8位访问,分4次完成传输。此时,地址线A[0]通常用于区分数据/命令(在8080接口中),而A[1]对应外部存储器的地址线A0。 - 16位宽度:使用数据线D[15:0]。访问16位变量(
uint16_t)一次完成。访问8位变量(uint8_t)时,FSMC也能处理,但需要特别注意字节对齐(可能会涉及高/低字节选择信号NBL)。此时,地址线A[0]不起寻址作用(因为16位宽时,最小寻址单元是2字节),外部存储器的地址线A0应连接到FSMC的A[1]。这一点是硬件设计时的关键,接错了就无法正确寻址。
注意:很多新手在画PCB或连接液晶屏时,容易忽略数据宽度对地址线连接的影响。如果你的LCD是16位8080接口,那么LCD的地址线A0(通常标记为RS,命令/数据选择引脚)应该接到STM32的FSMC_A[1]上,而不是FSMC_A[0]。这是一个经典的“坑”。
3. FSMC时序模型深度拆解:不仅仅是几个时间参数
FSMC之所以“Flexible”(灵活),就在于它允许我们精细地控制访问外部芯片的时序,以适应不同速度、不同型号的芯片。它的时序模型可以理解为对一个读或写操作周期的“分解动作”。我们以SRAM/NOR Flash Bank的时序配置寄存器(FSMC_BTR和FSMC_BWTR,分别对应读时序和写时序)为例来讲解。
每个时序寄存器都包含以下几个关键参数(单位是HCLK时钟周期):
- ADDSET: 地址建立时间。在片选信号有效后,地址信号需要保持稳定的时间,才能发出读/写命令。对应外部芯片数据手册里的
t_SU(ADDR)。 - ADDHLD: 地址保持时间。在读写信号无效后,地址信号还需要保持稳定的时间。这个参数在某些模式下可能不需要。
- DATAST: 数据建立时间。对于读操作,是从读使能有效到FSMC采样数据线之前必须等待的时间(确保外部芯片的数据已经稳定输出)。对于写操作,是写使能有效后,数据需要保持稳定的时间。对应读周期的
t_SU(DATA)或写周期的t_WP。 - BUSTURN: 总线周转时间。主要用于在两个不同的读操作之间,或者读操作之后紧跟写操作时,插入一个空闲周期,防止总线冲突。对于连续访问同一设备的操作,通常可以设为0。
这些参数怎么来的?答案一定来自外部芯片的数据手册(Datasheet)。绝对不能拍脑袋随便填。我们以一个常见的16位SRAM芯片IS62WV51216为例,看看如何从数据手册映射到FSMC配置。
假设我们芯片的关键时序参数如下(在特定电压和温度下):
t_RC(读周期时间): 最小55nst_AA(地址访问时间): 最大55nst_WC(写周期时间): 最小55nst_PWE(写使能脉冲宽度): 最小45nst_SU(ADDR)(地址建立到写使能有效): 最小0nst_HD(ADDR)(写使能无效后地址保持): 最小10ns
我们的STM32系统时钟(HCLK)为72MHz,则一个HCLK周期约为13.89ns。
配置读时序:
- 核心是满足
t_AA。t_AA是地址有效后,数据稳定输出的最大时间。FSMC的读周期总时间 = (ADDSET + DATAST + 1) * HCLK周期。其中DATAST必须覆盖t_AA。 - 假设我们设置
ADDSET = 1(约13.89ns),那么DATAST需要满足:(1 + DATAST + 1) * 13.89ns >= 55ns。计算可得DATAST至少需要55/13.89 - 2 ≈ 1.96,取整为2。所以DATAST可以设为2或3以留有余量。 - 因此,读时序可以配置为:
ADDSET=1,DATAST=3。总读周期时间约为(1+3+1)*13.89=69.45ns,大于芯片要求的55ns,满足条件。
配置写时序:
- 核心是满足
t_PWE(写使能脉冲宽度)和t_HD(ADDR)。在FSMC的“模式A”或“模式2”写时序中,DATAST参数决定了写使能信号低电平的宽度。 - 我们需要
DATAST * HCLK周期 >= t_PWE。45ns / 13.89ns ≈ 3.24,所以DATAST至少为4。 - 同时,
ADDSET需要满足t_SU(ADDR)(本例为0,最小可为0)和t_HD(ADDR)。t_HD(ADDR)要求写使能无效后地址至少保持10ns。FSMC的时序保证在写使能无效后,地址还会保持ADDSET个周期。所以ADDSET * 13.89ns >= 10ns,ADDSET至少为1。 - 因此,写时序可以配置为:
ADDSET=1,DATAST=4。
实操心得:在实际项目中,尤其是驱动TFT液晶屏时,屏驱动IC的时序要求往往比SRAM更严格,而且对读写速度有要求(影响刷屏率)。我的经验是,先根据数据手册计算一个保守值,然后在实际硬件上测试。可以通过逐步减小
DATAST和ADDSET参数,直到屏幕显示出现雪花、错点或不稳定,然后再回调1-2个周期作为最终值。这样能在稳定性和速度之间取得最佳平衡。另外,如果屏幕初始化或读写一直失败,首先应该用逻辑分析仪或示波器抓取FSMC引脚的实际波形,对照数据手册的时序图逐一检查,这是最直接的调试手段。
4. 硬件连接实战指南与常见陷阱
理解了原理和时序,最终要落到硬件连接上。这里以STM32F407驱动一款16位8080并行接口的ILI9341 TFT屏为例,说明FSMC的典型连接方式和必须避开的“坑”。
引脚分配:
- 数据线 D[15:0]: 连接STM32的FSMC_D[15:0]到LCD的D[15:0]。务必注意,如果LCD是16位接口,必须连接全部16根,不能只接8根,否则颜色显示会错乱(高8位缺失)。
- 地址线 A[1]: 连接STM32的FSMC_A[1]到LCD的RS(寄存器选择)引脚。如前所述,16位模式下A[0]不用于寻址,LCD的A0/RS引脚接A[1]。
- 控制线:
- 片选 NE1: 连接STM32的FSMC_NE1(可能是PG9,具体查芯片数据手册)到LCD的CS#(片选)引脚。
- 写使能 NWE: 连接STM32的FSMC_NWE到LCD的WR#(写使能)引脚。
- 读使能 NOE: 连接STM32的FSMC_NOE到LCD的RD#(读使能)引脚。
- 复位和背光: 这些引脚通常用普通GPIO控制,不占用FSMC资源。
配置步骤(以HAL库为例):
开启时钟: 使能FSMC和对应GPIO端口的时钟。
配置GPIO: 将所有用到的FSMC数据线、地址线、控制线引脚模式设置为复用推挽输出,速度设置为High或Very High,以提供足够的驱动能力。
配置FSMC: 填充
SRAM_HandleTypeDef结构体。Instance: 选择FSMC Bank,例如FSMC_NORSRAM_DEVICE或FSMC_NORSRAM_EXTENDED_DEVICE。Init:MemoryType:FSMC_MEMORY_TYPE_SRAM(对于8080接口的LCD,通常配置为SRAM模式,虽然它不是SRAM,但时序类似)。DataAddressMux:FSMC_DATA_ADDRESS_MUX_DISABLE(数据和地址线不复用)。MemoryDataWidth:FSMC_NORSRAM_MEM_BUS_WIDTH_16(16位宽度)。BurstAccessMode:FSMC_BURST_ACCESS_MODE_DISABLE(8080接口一般不支持突发模式)。WaitSignalPolarity:FSMC_WAIT_SIGNAL_POLARITY_LOW。WrapMode:FSMC_WRAP_MODE_DISABLE。WaitSignalActive:FSMC_WAIT_TIMING_BEFORE_WS。WriteOperation:FSMC_WRITE_OPERATION_ENABLE(必须开启写!)。WaitSignal:FSMC_WAIT_SIGNAL_DISABLE。ExtendedMode:FSMC_EXTENDED_MODE_ENABLE(重要!启用扩展模式,才能分别配置读时序和写时序)。AsynchronousWait:FSMC_ASYNCHRONOUS_WAIT_DISABLE。WriteBurst:FSMC_WRITE_BURST_DISABLE。
Timing和ExtendedTiming: 分别填入我们前面计算好的读时序和写时序参数结构体。
初始化: 调用
HAL_SRAM_Init()函数。定义访问地址: 根据连接,Bank1的起始地址是
0x60000000,我们使用NE1,且RS接在A[1]上。那么:- LCD的命令寄存器地址:
0x60000000(A[1]=0,即RS=0)。 - LCD的数据寄存器地址:
0x60000000 + 2(A[1]=1,即RS=1。因为16位总线,地址偏移2对应A[1]的跳变)。 在代码中,我们可以这样定义:#define LCD_CMD_ADDR ((uint32_t)0x60000000)#define LCD_DATA_ADDR ((uint32_t)0x60000002)然后通过指针进行读写:*(__IO uint16_t *)LCD_CMD_ADDR = 0x2A; // 发送命令
- LCD的命令寄存器地址:
常见陷阱与排查:
- 屏幕全白或全黑,无任何显示: 首先检查背光是否开启,复位时序是否正确。然后,用逻辑分析仪检查FSMC的CS、WR、RD、A[1]、D[15:0]是否有波形。如果完全没有波形,检查FSMC时钟和GPIO配置是否正确,
WriteOperation是否使能。 - 屏幕有显示但错乱、花屏: 这是最典型的问题。首要怀疑对象是时序参数。可能是
DATAST或ADDSET设置得太小,不满足LCD驱动IC的建立保持时间。其次是数据/命令地址错误,确认LCD_CMD_ADDR和LCD_DATA_ADDR的计算是否正确,特别是A[1]的连接。最后检查数据线连接,是否虚焊、错位(D0-D15顺序)。 - 读写速度慢: 在确保稳定的前提下,可以尝试优化(减小)
DATAST和ADDSET参数。另外,检查是否开启了DMA。对于刷屏这种大数据量操作,应该使用FSMC配合DMA,将显存数据直接搬运到LCD_DATA_ADDR,可以极大解放CPU。 - 使用SDRAM时更复杂: SDRAM(如IS42S16400J)需要初始化序列(预充电、模式寄存器设置、自动刷新等),其FSMC配置(FMC_SDRAM)与SRAM不同,涉及刷新率、延迟、突发长度等更多参数。调试SDRAM时,建议先从官方例程的配置开始,并务必仔细核对原理图连接,特别是时钟、行列地址线、Bank选择线的连接。
FSMC/FMC是一个功能强大但稍显复杂的模块,初次接触会觉得寄存器繁多。但只要你理解了其“地址映射”的核心思想,掌握了时序参数的计算方法,并细心完成硬件连接,它就会成为你项目中最得力的助手,轻松驱动起各种外部存储器,为你的STM32应用打开一片新天地。在下一部分,我们将深入FSMC的扩展模式、不同存储器的配置差异,以及如何利用DMA与FSMC配合实现高效数据传输。