3、FSMC接口驱动LCD:STM32 FSMC外设原理、FSMC与8080接口的映射关系、HAL库FSMC配置步骤、地址映射与数据读写、实战:用FSMC驱动3.5寸LCD

各位同学,今天我们来啃一块硬骨头——FSMC驱动LCD。

说实话,很多初学者一听到FSMC就头大,觉得这东西太底层、太复杂。但我告诉你,一旦你搞懂了FSMC和8080接口的映射关系,你会发现这玩意儿其实挺巧妙的。我当年第一次用FSMC驱动3.5寸屏的时候,也是折腾了好几天,最后点亮屏幕那一刻,那种成就感,嗯,至今难忘。

3.1 FSMC外设原理:它到底在干什么?

FSMC,全称是Flexible Static Memory Controller,灵活静态存储控制器。说白了,它就是STM32内部专门用来跟外部存储器打交道的一个硬件模块。

你想想看,如果不用FSMC,你要驱动一个LCD,得用GPIO模拟时序,一根一根线去拉电平,效率低不说,还占CPU。FSMC的作用就是把这些繁琐的时序操作交给硬件去完成,你只需要往某个地址写数据,FSMC自动帮你把时序搞定。

我个人习惯把FSMC理解成一个「地址翻译官」。你给它一个地址,它就知道你要访问哪个外设,然后自动产生对应的片选信号、读写信号、数据信号。是不是有点像单片机内部的地址译码器?没错,原理上是一样的。

核心要点:FSMC本质上是一个并行总线控制器,它把STM32的内部AHB总线协议转换成外部存储器的访问协议。对于LCD来说,我们用的是它的NOR/PSRAM模式。

3.2 FSMC与8080接口的映射关系

8080接口是什么?它是Intel公司早年提出的一种并行总线标准,广泛应用于LCD、SRAM等外设。它包含8位或16位数据线、读写控制线、片选线、以及最重要的——命令/数据选择线(RS)。

这里有个关键点:FSMC的地址线A[0](或A[16]以上)可以映射到8080接口的RS引脚。怎么理解?

我举个例子。假设你把LCD的RS引脚接到FSMC的A0上。那么当你访问地址0x60000000时,A0=0,对应RS=0,表示传输的是命令。当你访问地址0x60000001时,A0=1,对应RS=1,表示传输的是数据。

你看,就这么简单。一个地址的差异,就区分了命令和数据。这就是FSMC和8080接口的映射关系——用地址线来模拟命令/数据选择。

我的经验:实际项目中,我习惯把RS接到A16或A18上,而不是A0。为什么?因为A0变化太频繁,容易跟其他地址访问冲突。用高位地址线,映射关系更清晰,调试起来也方便。

下面这张图展示了FSMC与8080接口的典型连接方式:

FSMC与8080接口连接示意图 STM32 FSMC外设 AHB总线接口 NOR/PSRAM控制器 地址/数据复用 3.5寸LCD ILI9488/ILI9341 DB0-DB15 (数据线) RD (读使能) WR (写使能) RS (命令/数据选择) FSMC_D[0:15] FSMC_NOE (读使能) FSMC_NWE (写使能) FSMC_A[16] → RS FSMC_NEx (片选使能) 地址映射关系 命令地址: 0x60000000 | 数据地址: 0x60020000

3.3 HAL库FSMC配置步骤

配置FSMC,说白了就是告诉STM32:你要用哪些引脚、时序参数是多少、数据宽度是多少。HAL库把这部分封装成了几个结构体,用起来还算顺手。

配置步骤大致如下:

  1. 使能FSMC时钟:__HAL_RCC_FSMC_CLK_ENABLE()
  2. 配置GPIO引脚:把对应的FSMC引脚设置为复用功能
  3. 初始化FSMC外设:调用HAL_SRAM_Init(),传入时序参数
  4. 配置NOR/PSRAM时序:设置地址建立时间、数据保持时间等

这里我贴一段我常用的初始化代码,你可以直接拿去用:

// FSMC 初始化函数
void MX_FSMC_Init(void)
{
    FSMC_NORSRAM_TimingTypeDef Timing = {0};
    SRAM_HandleTypeDef hsram;
    
    // 1. 使能FSMC时钟
    __HAL_RCC_FSMC_CLK_ENABLE();
    
    // 2. 配置GPIO(省略具体引脚配置,根据你的板子来)
    // ... GPIO初始化代码 ...
    
    // 3. 配置FSMC外设
    hsram.Instance = FSMC_NORSRAM_DEVICE;
    hsram.Extended = FSMC_NORSRAM_EXTENDED_DEVICE;
    
    // 4. 配置NOR/PSRAM时序参数
    Timing.AddressSetupTime = 3;       // 地址建立时间
    Timing.AddressHoldTime = 1;        // 地址保持时间
    Timing.DataSetupTime = 5;          // 数据建立时间
    Timing.BusTurnAroundDuration = 1;  // 总线周转时间
    Timing.CLKDivision = 0;            // 时钟分频
    Timing.DataLatency = 0;            // 数据延迟
    Timing.AccessMode = FSMC_ACCESS_MODE_A;  // 访问模式A
    
    // 5. 初始化
    HAL_SRAM_Init(&hsram, &Timing, NULL);
}

注意:时序参数不是随便填的。我曾经在一个项目里,因为DataSetupTime设得太小,导致LCD显示花屏。后来用示波器一量,发现数据还没稳定就被读走了。建议你根据LCD数据手册的时序图来设置,或者像我一样,先设大一点,再慢慢调小。

3.4 地址映射与数据读写

配置好FSMC之后,怎么读写LCD呢?其实就两步:

  • 写命令:往命令地址写一个16位数据
  • 写数据:往数据地址写一个16位数据

假设我们把LCD的RS接到了FSMC的A16上,那么:

  • 命令地址 = 基地址 + (0 << 16) = 0x60000000
  • 数据地址 = 基地址 + (1 << 16) = 0x60020000

代码实现起来很简单:

// 定义命令和数据地址
#define LCD_CMD_ADDR   ((uint16_t *)0x60000000)
#define LCD_DATA_ADDR  ((uint16_t *)0x60020000)

// 写命令
void LCD_WriteCmd(uint16_t cmd)
{
    *LCD_CMD_ADDR = cmd;
}

// 写数据
void LCD_WriteData(uint16_t data)
{
    *LCD_DATA_ADDR = data;
}

// 读数据(有些LCD支持读操作)
uint16_t LCD_ReadData(void)
{
    return *LCD_DATA_ADDR;
}

你看,就这么几行代码。FSMC帮你搞定了所有时序细节,你只需要关心地址和数据。

我的习惯:我一般会把命令和数据地址定义成宏,然后在读写函数里直接用。这样代码看起来干净,而且不容易出错。另外,记得用volatile关键字,防止编译器优化掉你的内存访问。

3.5 实战:用FSMC驱动3.5寸LCD

好了,理论说完了,我们来点实际的。下面是一个完整的驱动3.5寸LCD的示例,芯片用的是ILI9488,分辨率480x320。

首先,初始化LCD:

// LCD初始化
void LCD_Init(void)
{
    // 软复位
    LCD_WriteCmd(0x01);
    HAL_Delay(120);
    
    // 进入睡眠模式
    LCD_WriteCmd(0x11);
    HAL_Delay(120);
    
    // 设置像素格式为16位
    LCD_WriteCmd(0x3A);
    LCD_WriteData(0x55);  // 16位色
    
    // 设置显示方向
    LCD_WriteCmd(0x36);
    LCD_WriteData(0x48);  // 竖屏
    
    // 开启显示
    LCD_WriteCmd(0x29);
    
    // 清屏为黑色
    LCD_Clear(0x0000);
}

然后,画一个点:

// 画点函数
void LCD_DrawPixel(uint16_t x, uint16_t y, uint16_t color)
{
    // 设置列地址
    LCD_WriteCmd(0x2A);
    LCD_WriteData(x >> 8);
    LCD_WriteData(x & 0xFF);
    
    // 设置行地址
    LCD_WriteCmd(0x2B);
    LCD_WriteData(y >> 8);
    LCD_WriteData(y & 0xFF);
    
    // 写像素数据
    LCD_WriteCmd(0x2C);
    LCD_WriteData(color);
}

最后,填充一个矩形区域(这是提升性能的关键):

// 填充矩形区域(批量写,速度更快)
void LCD_FillRect(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color)
{
    uint32_t i, total;
    
    // 设置列地址范围
    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);
    total = (x2 - x1 + 1) * (y2 - y1 + 1);
    for(i = 0; i < total; i++)
    {
        *LCD_DATA_ADDR = color;  // 直接写数据地址,效率最高
    }
}

性能对比:用FSMC驱动LCD,刷一帧480x320的图片,大约只需要15ms。如果用GPIO模拟,同样的操作可能要200ms以上。这就是硬件加速的魅力。

嗯,到这里,FSMC驱动LCD的核心内容就讲完了。你可能会问:为什么我点亮了屏幕,但显示不对?大概率是时序参数没调好,或者地址映射搞错了。我建议你用逻辑分析仪抓一下波形,看看FSMC输出的时序跟LCD数据手册要求的是否一致。

我曾经在一个项目里,因为FSMC的地址线接错了,导致命令和数据完全反了,折腾了两天才发现。所以,硬件连接一定要仔细核对原理图。

好了,这一章的内容就到这里。记住,FSMC不是洪水猛兽,它只是一个帮你干苦力活的硬件模块。你给它地址,它帮你干活,就这么简单。


公众号:蓝海资料掘金营,微信deep3321