前言
和大家一样,我之前也没有接触过FSMC,只是在使用LCD屏幕的时候,LCD驱动里会有FSMC的相关配置。
是什么驱使我使用标准库来配置FSMC来解决LCD呢?那肯定是因为厂家给的驱动配置不好用呗,给了寄存器和HAL的配置驱动,唯独没有给标准库的驱动,而且寄存器的LCD驱动给的屏幕延时太高,并且可读性很差,所以我就默默的开始用HAL来移植标准库了……
正篇
FSMC简介
FSMC(Flexible Static Memory Controller,可变静态存储器控制器) 是 STM32F4 系列微控制器中的一个特殊外设,它用于连接和控制外部存储器和外设,如 SRAM、NOR Flash、NAND Flash、PC 卡、CF 卡,以及使用并行接口的 LCD 屏幕等。FSMC 可以让 MCU 通过并行接口与外部设备高速通信,就像访问内部存储器一样。
FSMC为 MCU 和外部并行设备之间提供高速、灵活的接口。简化了与外部设备的通信,无需手动控制引脚时序,FSMC 自动处理读写控制信号和时序。支持配置不同的数据总线宽度(8 位、16 位、32 位),以及可编程的时序参数,适应不同的外设需求。
- 高速通信:FSMC 支持高达数十 Mbps 的数据传输速率,适合需要高速刷新和大量数据传输的 LCD 屏幕。
- 简化编程:FSMC 自动管理了与外部设备通信所需的地址、数据和控制信号,程序员无需手动操作 GPIO 引脚。
- 并行数据传输:相比于 SPI、I2C 等串行接口,并行接口的数据吞吐量更高,适合高分辨率、大屏幕的 LCD。
- 地址映射:通过 FSMC,将外部 LCD 映射到 MCU 的内存地址空间,程序可以像访问内存一样操作 LCD。
- 读写控制:FSMC 自动生成 LCD 所需的读、写、片选、地址信号,简化了与 LCD 的通信流程。
- 时序控制:FSMC 允许配置不同的时序参数,满足不同 LCD 控制器对时序的要求。
- 数据总线:将 MCU 的 FSMC 数据线(D0~D15)连接到 LCD 的数据引脚。
- 地址总线:使用 FSMC 的地址线(A0~A25)来选择 LCD 的寄存器或 GRAM 访问。
- 控制信号:
- FSMC_NOE(输出使能,读信号,RD):通常连接到 LCD 的 RD 引脚。
- FSMC_NWE(写使能,WR):通常连接到 LCD 的 WR 引脚。
- FSMC_NE1~NE4(片选,CS):选择使用哪个片选信号来激活 FSMC 的存储区域,通常连接到 LCD 的 CS 引脚。
- FSMC_Ax(地址线,RS 或 DC):用于区分命令和数据,连接到 LCD 的 RS(Register Select)或 DC(Data/Command)引脚。
- 使能 FSMC 时钟:
RCC_AHB3PeriphClockCmd(RCC_AHB3Periph_FSMC, ENABLE); - GPIO 配置:
- 将用于 FSMC 的 GPIO 引脚设置为复用功能(Alternate Function),并配置为高速、推挽输出、无上下拉或上拉。
- 复用映射:使用
GPIO_PinAFConfig函数,将引脚的复用功能设置为 FSMC。GPIO_PinAFConfig(GPIOD, GPIO_PinSource14, GPIO_AF_FSMC); // PD14 -> FSMC_D0
- FSMC 控制器配置:
- 定义时序结构体:分别定义读取和写入时序的参数,这些参数根据 LCD 控制器的规格书进行设置。
FSMC_NORSRAMTimingInitTypeDef ReadTiming; FSMC_NORSRAMTimingInitTypeDef WriteTiming; - 初始化 FSMC_NORSRAMInitTypeDef 结构体:配置 FSMC 的操作模式、数据宽度、存储器类型等参数。
FSMC_NORSRAMInitTypeDef FSMC_NORSRAMInitStructure; FSMC_NORSRAMInitStructure.FSMC_Bank = FSMC_Bank1_NORSRAM4; // ...(其他配置) FSMC_NORSRAMInitStructure.FSMC_ReadWriteTimingStruct = &ReadTiming; FSMC_NORSRAMInitStructure.FSMC_WriteTimingStruct = &WriteTiming; - 初始化 FSMC:
FSMC_NORSRAMInit(&FSMC_NORSRAMInitStructure); - 使能 FSMC Bank:
FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM4, ENABLE);
- 定义时序结构体:分别定义读取和写入时序的参数,这些参数根据 LCD 控制器的规格书进行设置。
- 地址映射:
- 计算 LCD 控制器的基地址,通过 FSMC 的片选信号和地址线,将 LCD 映射到特定的内存地址空间。
#define LCD_BASE ((uint32_t)(0x60000000 | (FSMC_Bank1_NORSRAM4 - 1) * 0x04000000 | (1 << LCD_FSMC_AX))) #define LCD ((LCD_TypeDef *)LCD_BASE) - 通过结构体访问 LCD 寄存器和数据寄存器:
typedef struct { volatile uint16_t LCD_REG; volatile uint16_t LCD_RAM; } LCD_TypeDef;
- 计算 LCD 控制器的基地址,通过 FSMC 的片选信号和地址线,将 LCD 映射到特定的内存地址空间。
问题与解决
初步理解了什么是FSMC,就可以正常的进行问题解决了,其中比较重要的一点是,用调试的思想,查看到底是哪一步代码无法使用,下面是我遇到的问题,
void lcd_write_reg(uint16_t regno, uint16_t data)
{
printf("regno=%d,data=%d\r\n");
LCD->LCD_REG = regno; /* 写入要写的寄存器序号 */
LCD->LCD_RAM = data; /* 写入数据 */
}
就是这个代码,前面半段可以用,后半段反而不行了,找问题找了老半天也没找到,但其实如果我利用了调试的思想,一眼就能锁定BUG的位置,并解决,改代码不怕你代码跑不动,就怕你找不到位置!
我在更改FSMC配置时,并没有利用printf(“更改配置\r\n”),显示我在这个地方更改FSMC配置了,导致了无法锁定BUG位置,花费了大量的时间。下面的代码是修复过的,可以直接使用。
printf("修改FSMC的值\r\n");
if (lcddev.id == 0x5510)
{
// 直接修改 BWTR 寄存器中的写时序参数
uint32_t bank = FSMC_Bank1_NORSRAM4; // 使用 NE4,Bank4
uint32_t bwtrIndex = bank - FSMC_Bank1_NORSRAM1;
// 配置新的写时序参数
uint32_t newBwtr = 0;
newBwtr |= (2 << 0); // FSMC_BWTR_ADDSET,地址建立时间为 2 个 HCLK 周期
newBwtr |= (0 << 4); // FSMC_BWTR_ADDHLD,地址保持时间为 0
newBwtr |= (2 << 8); // FSMC_BWTR_DATAST,数据保持时间为 2 个 HCLK 周期
newBwtr |= FSMC_AccessMode_A; // 访问模式 A
// 更新 BWTR 寄存器
FSMC_Bank1E->BWTR[bwtrIndex] = newBwtr;
}
其实也不难理解,一个操作寄存器的函数,不可能无缘无故的失效,几乎可以肯定是更改了相关的配置而造成的,有了个这个思路,再配合串口,能直接发现问题所在,下面是配合串口来实现的BUG锁定:

找到了BUG的位置,就修改BUG就行了,有人会说,为什么你要中途更改FSMC的配置呢?这不是闲着蛋疼吗?
那是因为在初始化阶段,LCD 控制器可能还未完全准备好,使用较大的时序参数(较长的延迟)有助于确保初始化过程的稳定性。例如,FSMC_AddressSetupTime 和 FSMC_DataSetupTime 设置为较大的值,如 15 和 60,可以确保在初始化过程中,LCD 有足够的时间响应。
在确认 LCD 正常工作后,可以根据实际需要,调整时序参数以提高通信速度。将 FSMC_AddressSetupTime 和 FSMC_DataSetupTime 调小(如设置为 2),可以加快数据传输速度,提高屏幕刷新率。