公司动态
STM32F4 FSMC驱动LCD屏与外部SRAM:时序配置与地址映射实战
简介面向STM32F4系列嵌入式开发者的FSMC应用示例代码重点演示利用FSMC接口驱动LCD屏的完整流程适合需要学习外部存储器扩展、图形界面显示或进行底层驱动开发的初中级工程师。资源共664个文件压缩包约3.86MB以C源码和头文件为核心辅以分散加载文件sct/ld/icf用于配置内存映射以及.s汇编启动文件、.hex/.axf固件输出等覆盖了从工程构建到固件生成的各个阶段。已有63人学习下载。代码内容包含FSMC寄存器时序配置、LCD硬件连接说明、驱动初始化、基本图形与文字显示以及性能优化思路能够帮助开发者避开常见硬件时序陷阱快速将FSMC功能落地到实际产品中。通过研读示例可系统掌握STM32F4外设接口的配置方法并在此基础上扩展出更复杂的显示应用。 搞嵌入式的人只要碰过带外部总线的ARM芯片迟早会和FSMC打交道。STM32F4系列的FSMCFlexible Static Memory Controller灵活静态存储控制器是一个相当经典的外设它能让你像操作内部寄存器一样去访问外部存储器或外设速度极快时序可配。很多项目里要用TFT-LCD屏幕、外部SRAM、NOR Flash或者想直接挂一块并口设备FSMC基本是绕不开的方案。这篇文章我用一个实际可跑的STM32F4 FSMC应用示例来拆解整条链路从硬件设计、引脚分配、CubeMX配置、时序参数计算到具体读写代码和验证方法全部记录下来。同时会穿插一个我踩过的坑用FSMC驱动液晶屏时刷新花屏排查半天最后发现是时序参数里的Data Setup Time设得太短。适合刚接触FSMC的新手也想给已经会跑库函数、但没深究过底层时序的老手提个醒。1. 内容整体设计与思路拆解1.1 FSMC到底是什么为什么需要专门搞一个控制器先别急着写代码理解FSMC的工作方式比代码本身更重要。STM32F4内部集成了FSMC外设它本质上是一个“协议转换器”CPU发起一次读或写访问FSMC根据你配置的时序自动把地址、数据、读使能、写使能、片选等信号按存储器接口的规范拉高拉低。整个过程不需要CPU干预所以FSMC读写的效率非常高一次访问基本上就是几个HCLK周期。举个例子你在代码里写一句*((volatile uint16_t *)0x60000000) 0x1234;如果0x60000000对应的是Bank1的片选NE1那么FSMC会自动完成一整轮写操作拉低片选放上地址拉低写信号把0x1234放到数据线上然后等一会儿释放信号。CPU不会卡在这里干等除非你开启了等待周期。这个“自动完成”是FSMC最大的价值省掉了用GPIO模拟并口时序的时间损耗也让代码简洁到极致。FSMC支持的内存类型包括SRAM、NOR Flash、PSRAM以及带复用地址总线的NOR闪存。液晶屏这类设备虽然时序类似SRAM但它不是真正的存储器而是映射到地址空间的一个“外设寄存器”。你想往屏幕写命令或数据只需要往不同的地址偏移量写值即可FSMC会帮你把数据送到数据线上屏幕控制器比如ILI9341、ST7789自己会锁存。1.2 为什么用FSMC而不是GPIO模拟或SPI屏很多新手一开始喜欢用SPI接口的屏幕因为占用引脚少接线简单。但SPI屏的刷新率瓶颈很明显假设SPI时钟25MHz8位模式一秒最多传25M字节加上命令、延时、像素格式转换实际刷新一张240x320的RGB565图像大约需要300ms左右动起来就卡。如果换成FSMC驱动并口LCD假设HCLK是168MHzFSMC总线时钟也是168MHz通过合理配置可以达到几十兆字节每秒的写吞吐量。哪怕时序放宽一点刷新一帧也就几毫秒到十几毫秒这对显示动态波形、菜单动画、视频播放都是质的区别。再看另一个场景你需要在系统中保存大量日志数据或者跑一个复杂的算法需要大数组缓冲区。STM32F4内部SRAM通常只有128KB到192KB一旦数据量上来就捉襟见肘。挂一片外部SRAM或NOR Flash通过FSMC扩展容量可以从1MB到16MB甚至更大访问速度相比SPI Flash通常几MB/s要快一个数量级。当然这里说的是NOR Flash/SRAM不是EMMC。EMMC是MMC接口和FSMC不是一回事后面我会单独讲。1.3 示例项目的整体架构这个示例项目我会用STM32F407VET6开发板外接一块基于ILI9341的16位并口TFT屏幕同时挂一片1MB的外部SRAMIS62WV51216。整体架构很简单将LCD的DB0-DB15接到FSMC的D0-D15LCD的控制线RD、WR、RS、CS分别接FSMC的NOE、NWE、A6、NE1使用Bank1的NE1所以LCD映射地址从0x60000000开始RS接A6意味着在地址总线中RS位对应bit6所以命令地址是0x60000000数据地址是0x60000040或反过来这个设计里FSMC把LCD当作一个“SRAM设备”通过改变地址的第6位来区分命令和数据。很多并口LCD控制器都是这种思路需要特别注意地址位和命令/数据线的对应关系如果接的地址线不同偏移地址也要相应调整。我把代码分成几个模块FSMC初始化、LCD底层驱动仅与FSMC相关的部分不涉及画图库、外部SRAM的读写测试、以及一个简单的性能测量函数用来打印FSMC实际读写速度。这样既能验证功能也能直观感受一下FSMC的效率。2. 核心细节解析与实操要点2.1 引脚分配与硬件连接一个脚都不能错FSMC虽然功能强大但引脚是固定的不能像GPIO那样随意映射。使用HAL库时CubeMX会自动根据外设初始化复用功能但硬件连接必须严格对应到数据手册上的引脚定义。下面是STM32F407VET6上常用的FSMC引脚对照表FSMC信号STM32F407引脚说明NE1PD7片选信号对应Bank1区NOEPD4读使能接LCD的RDNWEPD5写使能接LCD的WRA6PF0地址线位6用于RS切换D0-D3PD14-PD15, PD0-PD1数据线低位D4-D7PE7-PE10数据线D8-D15PE11-PE15, PD8-PD9数据线高位NBL0/NBL1PE0/PE1字节掩码通常接LCD的WR_RD或直接悬空这只是VET6的引脚如果你用的是F429、F412等型号引脚可能略有不同必须先查数据手册。CubeMX会在图形界面里帮你校验但有些引脚的复用功能有冲突比如PB7、PD0都要留意外设冲突。另外电源地线要单独走FSMC这类高速并行总线对信号质量有一定要求如果飞线过长时序容易出现问题。实际连接LCD时因为ILI9341的读、写信号分开所以NOE接RDNWE接WRA6接RSD/CXCS接NE1RESET可以接到任意GPIO也可以接在FSMC某个地址线上不过最简单的还是用普通GPIO控制复位。2.2 CubeMX中的FSMC配置与关键参数用CubeMX配置FSMC其实很简单但里面的参数含义必须弄清楚。在“Connectivity”里选择FSMC然后勾选Bank1的NOR/SRAM模式。以我的项目为例参数配置如下Memory type: SRAMData bus width: 16 bitWrite operation: EnabledExtended mode: DisabledASynchronous wait: Disabled时序参数Timing里我常用的是参数名称我配置的值说明Address setup time地址建立时间2个HCLK从地址有效到片选有效的时间Data setup time数据建立时间4个HCLK写信号有效到数据有效的时间Address hold time地址保持时间2个HCLK地址在访问周期结束后保持的时间Data hold time数据保持时间2个HCLK写信号释放后数据保持的时间HCLK168MHz时一个HCLK周期约为5.95ns。我用的LCD模块时序要求是写周期最小100ns地址建立时间最小5ns数据建立时间最小10ns。换算成HCLK采用上述参数后实际访问周期 2428个HCLK约47.6ns加上数据保持时间2个HCLK总周期约59.5ns能满足约16MHz的刷新率。这里我要专门提一个容易翻车的坑Data setup time如果设置成1看起来更快但很多LCD控制器在高负载下会随机花屏或写错地址。原因在于LCD控制器内部的锁存时序需要数据保持稳定一段时间太短的话锁存失败。同样对于外部SRAM如果时序参数太激进偶尔会出现数据读回来是错的。所以配置时尽量参考存储器数据手册的“最小时间”在此基础上加一些余量而不是直接从网上抄别人的参数。2.3 地址映射的巧妙设计A6就是命令切分线FSMC把外部存储器映射到4个BankBank1又分为4个子区由NE1~NE4分别片选。每个子区的地址范围是64MB映射地址如下片选起始地址终止地址NE10x600000000x63FFFFFFNE20x640000000x67FFFFFFNE30x680000000x6BFFFFFFNE40x6C0000000x6FFFFFFF在这个地址空间里地址线的每一位都对应到外部引脚A0~A25。例如A6对应地址的bit6。所以当地址为0x60000000时A6为0当地址为0x60000040时A6为1因为0x40的bit6是1。这就实现了用地址来翻转RS引脚。我习惯用宏定义来区分命令和数据#define LCD_CMD_ADDR 0x60000000 #define LCD_DATA_ADDR 0x60000040然后用指针直接写void LCD_WriteReg(uint16_t reg, uint16_t data) { *((volatile uint16_t *)LCD_CMD_ADDR) reg; *((volatile uint16_t *)LCD_DATA_ADDR) data; }注意这里访问宽度是16位因为LCD的数据线是16根。如果你用的是8位并口屏可以把总线宽度设为8位地址偏移就不一样了需要对应地线接入A0。总之一定要根据自己实际接的地址线来算偏移。3. 实操过程与核心环节实现3.1 Keil5工程搭建与初始化代码我习惯使用STM32CubeMX先生成HAL库工程再在Keil5里编译烧录因为HAL库的FSMC初始化函数封装得清晰容易调参。生成工程时时钟配置确保HCLK168MHz。具体步骤在CubeMX里使能FSMC按上面配置。使能所需的GPIO引脚CubeMX会自动配置复用功能。生成工程后在main函数里调用MX_FSMC_Init()。编写底层的LCD读写函数。FSMC初始化代码生成后核心函数像这样static void MX_FSMC_Init(void) { FSMC_NORSRAM_TimingTypeDef Timing {0}; FSMC_NORSRAM_InitTypeDef Init {0}; __HAL_RCC_FSMC_CLK_ENABLE(); Timing.AddressSetupTime 2; Timing.AddressHoldTime 2; Timing.DataSetupTime 4; Timing.BusTurnAroundDuration 0; Timing.CLKDivision 16; Timing.DataLatency 17; Timing.AccessMode FSMC_ACCESS_MODE_A; Init.NSBank FSMC_NORSRAM_BANK1; Init.DataAddressMux FSMC_DATA_ADDRESS_MUX_DISABLE; Init.MemoryType FSMC_MEMORY_TYPE_SRAM; Init.MemoryDataWidth FSMC_NORSRAM_MEM_BUS_WIDTH_16; Init.BurstAccessMode FSMC_BURST_ACCESS_MODE_DISABLE; Init.WaitSignalPolarity FSMC_WAIT_SIGNAL_POLARITY_LOW; Init.WrapMode FSMC_WRAP_MODE_DISABLE; Init.WaitSignalActive FSMC_WAIT_TIMING_BEFORE_NE; Init.WriteOperation FSMC_WRITE_OPERATION_ENABLE; Init.WaitSignal FSMC_WAIT_SIGNAL_DISABLE; Init.ExtendedMode FSMC_EXTENDED_MODE_DISABLE; Init.WriteBurst FSMC_WRITE_BURST_DISABLE; HAL_SRAM_Init(hsram1, Init, Timing); }这段代码看起来参数多其实大多数保持默认即可真正需要按芯片调整的只有AddressSetupTime、AddressHoldTime、DataSetupTime。如果你的外部设备很慢可以适度加大这三个值。我这里用的值已经经过了实际验证稳定工作。3.2 读写LCD的完整示例从初始化到点亮屏幕以ILI9341为例初始化过程需要发送一系列命令来设置像素格式、显存窗口等。用FSMC写命令和数据非常直接static void LCD_WriteCmd(uint16_t cmd) { *((volatile uint16_t *)LCD_CMD_ADDR) cmd; } static void LCD_WriteData(uint16_t data) { *((volatile uint16_t *)LCD_DATA_ADDR) data; } void LCD_Init(void) { // 复位 LCD_RESET_CLR(); HAL_Delay(20); LCD_RESET_SET(); LCD_WriteCmd(0x01); // Soft Reset HAL_Delay(120); LCD_WriteCmd(0x3A); // Pixel Format Set LCD_WriteData(0x55); // 16 bits per pixel LCD_WriteCmd(0x28); // Display OFF LCD_WriteCmd(0x20); // Enter Normal Mode LCD_WriteCmd(0x36); // Memory Access Control LCD_WriteData(0x08 | 0x40); // BGR order, Row/Col exchange LCD_WriteCmd(0x11); // Exit Sleep Mode HAL_Delay(150); LCD_WriteCmd(0x29); // Display ON }之后填充一个指定的窗口区域void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { LCD_WriteCmd(0x2A); // Column Address Set LCD_WriteData(x0 8); LCD_WriteData(x0 0xFF); LCD_WriteData(x1 8); LCD_WriteData(x1 0xFF); LCD_WriteCmd(0x2B); // Page Address Set LCD_WriteData(y0 8); LCD_WriteData(y0 0xFF); LCD_WriteData(y1 8); LCD_WriteData(y1 0xFF); LCD_WriteCmd(0x2C); // Memory Write }然后连续写像素数据。这里有个性能优化点不要每写一个像素都重新设置命令和数据地址而是设置一次窗口后连续向数据地址写入所有像素。例如填充一块区域void LCD_FillRect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { uint32_t count (x1 - x0 1) * (y1 - y0 1); LCD_SetWindow(x0, y0, x1, y1); while (count--) { *((volatile uint16_t *)LCD_DATA_ADDR) color; } }这个while循环就是纯粹的FSMC写操作没有额外GPIO翻转所以填充速度非常快。实测填充320x240全屏用while循环4毫秒左右如果加上窗口计算和函数开销也就10毫秒以内。相比SPI屏动辄上百毫秒完全是另一个体验。3.3 外部SRAM的读写测试验证FSMC配置通用性既然FSMC配置好了顺便也能驱动SRAM。我在例程里加了外扩SRAM的快速读写测试用来验证时序是否兼容#define SRAM_BASE 0x60000000 // 与LCD同一片选时需要分时访问最好用不同片选实际项目中LCD和SRAM最好不要用同一个片选否则它们的地址会冲突。我测试时用了NE1接LCDNE2接SRAM这样SRAM基址就是0x64000000。FSMC初始化时需要对两个Bank分别做HAL_SRAM_Init时序参数可以分别调整。SRAM的速度通常比LCD快因此DataSetupTime可以更小比如设成2。这时访问周期大约35ns能满足1MB SRAM的快速读写。SRAM测试代码很简单void SRAM_Test(void) { volatile uint16_t *pSram (volatile uint16_t *)0x64000000; uint32_t i; uint16_t err 0; for (i 0; i 0x4000; i) // 写64K字节 { pSram[i] (uint16_t)(i 0xFFFF); } for (i 0; i 0x4000; i) { if (pSram[i] ! (uint16_t)(i 0xFFFF)) { err; } } if (err 0) { printf(SRAM test passed\n); } else { printf(SRAM test failed: %d errors\n, err); } }如果出现错误大概率是FSMC时序配置太激进或者硬件布线问题。这个测试既验证了FSMC对外部SRAM的读写能力也验证了LCD时序的基础一致性。3.4 性能实测FSMC比你想的快多少我简单做了一次性能测试用GPIO翻转计数的方式来估算调用LCD_FillRect填充全屏320x240用DWT计数器计时。DWT是在ARM Cortex-M4内核中非常实用的一个周期计数器比HAL_GetTick()精确得多。uint32_t tick_start, tick_end, cycles; CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; tick_start DWT-CYCCNT; LCD_FillRect(0, 0, 319, 239, 0xF800); tick_end DWT-CYCCNT; cycles tick_end - tick_start; printf(FillScreen cycles: %ld, time: %.2f ms\n, cycles, cycles / 168.0f / 1000.0f);实测全屏填充约76800个像素每个像素16位在168MHz主频下用时大约2.1ms折合每秒约366帧。当然实际内容显示不会这么极限但FSMC为高速刷新提供了充足的余量。4. 常见问题与排查技巧实录4.1 花屏或白屏先怀疑时序再怀疑接线使用FSMC驱动LCD最常见的问题是屏幕花屏、颜色错乱或者干脆白屏。很多人的第一反应是代码问题但绝大多数情况下是硬件接线或时序配置问题。我遇到过一个典型现象刚上电时屏幕显示正常运行几分钟后开始出现随机色块。用示波器抓NWE和NE1的波形发现写信号宽度偶尔会不稳定而配置的DataSetupTime只有1个HCLK。将DataSetupTime调整为3或4后问题消失。原因是环境温度升高后LCD控制器内部时序略有变化原本临界的数据建立时间不再满足要求。排查顺序我建议这样先检查RST与电源时序然后确认所有数据线、控制线是否接触良好、有没有短路再用示波器观察NOE/NWE波形是否正常。如果波形正常最后调整FSMC时序参数里DataSetupTime和AddressHoldTime。不建议直接在网上找一个配置抄不同LCD模组对时序的要求差异不小。4.2 写地址偏移错误导致命令和数据错乱另一个很常见的坑是地址偏移算错。比如你想用A0来区分命令和数据结果接到了A6上却依然用0x60000000和0x60000001来区分那数据就全乱了。最好在代码里通过宏定义将地址明确标出来并且用实际测量的方法验证编写一个测试函数向命令地址和数据地址各写一个特征值用示波器观察A6引脚的波形确认确实是0和1的差别。如果你的屏幕支持读操作很多LCD控制器也支持读还可以尝试读ID若读回的数据与预期一致说明地址映射和时序都没问题。4.3 与EMMC、SDIO和SPI外设的选型对比很多人在做存储扩展时容易把FSMC和EMMC混淆。EMMC是内嵌存储芯片使用MMC协议通常走SDIO接口或专用MMC控制器接口是串行的虽然容量大、速度快但STM32F4内部没有专用EMMC控制器只能通过SDIO接口模拟MMC协议或使用SPI模式读写时需要软件处理协议CPU开销不小。FSMC则适合并行SRAM、NOR Flash、PSRAM这类器件接口是并行的适合需要多次随机读写的场景比如显示帧缓冲、算法堆栈。如果你只是存大容量的日志或图片文件EMMC或SPI NOR Flash更适合因为容量大且占用引脚少。选型时看三个维度容量需求、实时性需求、引脚预算。顺带一提热词里提到的SPI1驱动MT6701这是一个磁编码器芯片通过SPI接口读取角度数据。虽然它与FSMC无关但可以说明一个问题在嵌入式系统里不同类型的外设接口服务于不同类型的设备FSMC面对的是并行高速内存类设备SPI面对的是低速高性价比传感器。同一个STM32F4可以同时支持FSMC和SPI不需要用FSMC去模拟SPI反之亦然。合理搭配接口才能发挥芯片的全部能力。4.4 Keil5烧录与调试FSMC程序时的注意事项Keil5下调试FSMC程序有一个需要注意的地方如果开启了Data Setup Time较小的配置并使用了--loop优化的编译器有些调试器在单步执行时可能看到外部存储器内容“不更新”这其实是缓存或编译优化导致的现象。建议在调试时打开View - Watch窗口直接查看映射地址的内容而不是依赖某个变量的值。烧录程序时如果使用ST-Link/J-Link确保下载器供电稳定。FSMC总线在初始化时会突然对外部存储器产生大量访问如果电源纹波超标可能导致外部芯片复位。我在一个项目中就遇到过用ST-Link供电调试一切正常但用外部电源供电时屏幕一直闪后来发现是NE1的驱动能力和电源去耦电容没配合好导致片上电瞬间出现电压跌落。加一个100uF电解电容后故障消失。5. 个人经验补充FSMC未来的扩展和使用建议5.1 用DMA配合FSMC实现更高效率的刷新虽然FSMC已经很快但如果要刷大量图像数据CPU持续写总归会占用内核时间。一种更高效的方案是让DMA搬运数据到FSMC的映射地址。因为FSMC映射地址对于DMA来说就是普通存储地址所以可以配置一个Memory-to-Memory DMA把图像缓冲区的内容自动搬运到LCD数据地址。以帧缓冲为例在SRAM中开辟一个与屏幕大小相同的RGB565缓冲区渲染时只操作缓冲区需要刷新屏幕时启动一次DMA传输CPU就可以去处理别的事情。DMA传输完成后触发中断再准备下一帧。这种方式在GUI应用中非常常见配合FSMC可以达到很高的帧率而且CPU占用极低。5.2 不同系列STM32的FSMC差异STM32F4系列的FSMC只能访问NOR/SRAM/PSRAM不支持SDRAM。如果你需要大容量内存可以选用带FMC接口的STM32F429或H7系列它们的FMC增加了SDRAM支持。从F4迁移到F429时FSMC代码大部分可以直接复用只需要修改初始化的存储设备类型和引脚配置。这一点在做产品选型时很重要如果预计后续要扩展SDRAM直接选F429以上的型号更省事。5.3 硬件布局的一些细节最后补充一个硬件设计上的提醒FSMC总线频率不低数据线16根再加上地址线、控制线如果全部飞线可能到20~30根这种情况下信号完整性问题就会显现。建议PCB设计时尽量缩短总线长度保持阻抗连续必要时在关键信号加串联电阻22~33欧姆。如果是开发板测试杜邦线尽量短不要用那种20cm的长线。我早期调试时用长杜邦线接LCD死活过不了80MHz的写入速度换成短排线后问题立解。这个项目做完之后我已经把我的FSMC初始化代码和LCD驱动固定成模板后续产品只要换一下芯片型号和引脚映射就能直接使用。FSMC这个外设虽然配置看起来繁琐但只要理解了地址映射和时序参数两个核心点它就变成了一个非常可靠的“高速并口工具”值得花时间吃透。本文还有配套的精品资源点击获取