STM32外扩SDRAM:FMC时序与刷新机制详解
简介基于STM32单片机的SDRAM驱动程序软件源码面向嵌入式开发工程师与STM32进阶学习者解决外部SDRAM初始化配置与数据读写访问难题。源码提供bsp_InitExtSDRAM()初始化函数完成FMC控制器时序与刷新配置适配MT48LC4M32B2TG芯片外部SDRAM物理地址固定映射于0xC0000000支持指针方式直接读写DemoExtSDRAM例程演示串口命令交互、读写测试与错误检测流程便于理解Bank、行列地址与突发传输机制。压缩包共683个文件以440个C源码、164个H头文件及48个汇编文件为核心另附hex固件、pdf说明、工程配置与烧录脚本整体4.85MB目录组织清晰适合直接移植或二次开发。已有182人学习下载适合需要快速掌握STM32FMC驱动SDRAM的开发者参考。1. 在STM32上驱动MT48LC4M32B2TG核心不是“读写指令”而是“时序模型”很多人做图像缓冲或者音频采样时发现STM32内部RAM不够第一反应是换大容量型号其实外挂一颗MT48LC4M32B2TG就能把可用内存提到16MB这个量级。这颗芯片是Micron的老款SDRAM32位数据总线常见于各种开发板和核心板上但驱动它和操作GPIO完全是两回事上电后要按顺序做预充电、自刷新、模式寄存器加载之后还要每15.625us刷新一次否则数据就悄悄丢掉。这篇内容就是讲清楚怎么在带FMC控制器的STM32上把MT48LC4M32B2TG初始化、读写、刷新都做对适合已经能用CubeMX生成代码但看不懂时序参数含义或者运行一段时间后随机出现数据错乱的人。2. MT48LC4M32B2TG引脚与STM32 FMC/FSMC接口的选型逻辑2.1 先把SDRAM的复用地址线框出来MT48LC4M32B2TG的地址线是行地址和列地址复用的同一个引脚在不同命令阶段代表不同位。A0到A12用于行地址列地址只用到其中一部分BA0和BA1用于选择内部bank。这个结构决定了读和写都必须先发ACTIVE命令把bank和行地址锁存进去再发读或写命令带上列地址。拿到源码包或者自己写驱动时第一步不是打开C文件而是找数据手册里的“Address Configuration”表格确认Row地址位数和Column地址位数。因为同一个封装可能有不同配置有的是4096行×1024列有的是2048行×1024列配置错了FMC控制器也检查不出来只会表现为读写数据乱掉。我一般会用CubeMX里的RowBits和ColumnBits下拉框逐项试然后配合后面的回读测试确认。2.2 FMC还是FSMC先检查你的STM32系列FSMC是STM32F1的静态存储控制器不支持SDRAM。F1系列要驱动MT48LC4M32B2TG只能靠GPIO模拟时序刷新中断一来还得暂停当前任务效率很低而且时序很难稳定。带FMC控制器的型号主要从F4开始F405/407/427/429以及F7、H7这些都有SDRAM bank可以直接把时序交给硬件。FMC的SDRAM控制器提供了两个bankBank1地址范围0xC0000000Bank2地址范围0xD0000000。接一片MT48LC4M32B2TG用哪个都行只要硬件上把SDNE引脚接到对应的片选上。很多开发板默认接SDNE0对应Bank1所以代码里要检查清楚不能想当然。2.3 引脚分配表以STM32F407接一颗32位SDRAM为例下面的映射表是典型的FMC连接方式不同开发板可能把数据线高16位和低16位互换但FMC内部有字节交换机制只要引脚编号没错就行。SDRAM功能FMC引脚说明地址线A0-A12FMC_A0-FMC_A12行/列地址复用Bank选择BA0/BA1FMC_BA0/FMC_BA14个内部bank数据线D0-D31FMC_D0-FMC_D3132位总线时钟FMC_SDCLKSDRAM时钟时钟使能FMC_SDCKE0接CKE片选FMC_SDNE0对应Bank1行选通FMC_NRAS锁存行地址列选通FMC_NCAS锁存列地址字节掩码FMC_NBL0-NBL3对应D0-D7, D8-D15等PCB布局上FMC数据线尽量等长地址线可以稍微放松一点但SDCLK和SDCKE要走短走直不要让SDRAM时钟绕一圈再接芯片。实际经验是时钟路线上串联一个22Ω电阻能明显减少过冲特别是STM32主频跑到168MHz时。2.4 源码包里驱动文件该怎么拆常见的源码组织方式是一个sdram.c、一个sdram.h底层用HAL库的HAL_SDRAM_*函数封装。驱动文件里通常包含四部分GPIO和FMC硬件初始化、SDRAM时序参数配置、上电初始化命令序列、读写测试函数。不要把所有初始化塞进main.cSDRAM属于外设应该自己成模块。3. STM32CubeMX初始化SDRAM时序参数的计算和落盘3.1 先确认SDCLK频率再谈参数STM32的FMC SDRAM控制器没有独立PLLSDCLK来自HCLK分频。CubeMX里SDRAM时钟周期可选HCLK、HCLK/2、HCLK/3等。假设HCLK是168MHz选HCLK/2就是84MHz一个周期约11.9ns。MT48LC4M32B2TG老颗粒最快支持到125MHz或更高84MHz在安全范围内。后续所有时序参数都是以SDCLK周期数为单位配置的所以一旦改动了HCLK或者分频系数下面那组数字全部要重新算。最容易踩的坑是用HAL库默认值能初始化成功但换了工作主频后忘了同步修改。3.2 HAL库SDRAM时序结构体的7个参数HAL库用FMC_SDRAM_TimingTypeDef表示时序每个成员对应SDRAM手册里一个时间参数。以常见配置为例FMC_SDRAM_InitTypeDef sdram_init {0}; sdram_init.SDBank FMC_SDRAM_BANK1; sdram_init.ColumnBitsNumber FMC_SDRAM_COLUMN_BITS_NUM_10; sdram_init.RowBitsNumber FMC_SDRAM_ROW_BITS_NUM_12; sdram_init.MemoryDataWidth FMC_SDRAM_MEM_BUS_WIDTH_32; sdram_init.InternalBankNumber FMC_SDRAM_INTERN_BANK_NUM_4; sdram_init.CASLatency FMC_SDRAM_CAS_LATENCY_3; sdram_init.WriteProtection FMC_SDRAM_WRITE_PROTECTION_DISABLE; sdram_init.SDClockPeriod FMC_SDRAM_CLOCK_PERIOD_2; sdram_init.ReadBurst FMC_SDRAM_RBURST_ENABLE; sdram_init.ReadPipeDelay FMC_SDRAM_RPIPE_DELAY_2; FMC_SDRAM_TimingTypeDef sdram_timing {0}; sdram_timing.LoadToActiveDelay 2; /* tMRD模式寄存器读取到ACTIVE */ sdram_timing.ExitSelfRefreshDelay 7; /* tXSR退出自刷新后等待时间 */ sdram_timing.SelfRefreshTime 4; /* tRASACTIVE到PRECHARGE */ sdram_timing.RowCycleDelay 7; /* tRC行周期时间 */ sdram_timing.WriteRecoveryTime 2; /* tWR写恢复到PRECHARGE */ sdram_timing.RPDelay 2; /* tRPPRECHARGE周期 */ sdram_timing.RCDDelay 2; /* tRCD行地址到列地址 */结构体里SelfRefreshTime容易看名字误解成自刷新持续时间实际上对应tRAS。如果SDCLK是84MHz一个周期约11.9nsSelfRefreshTime 4表示约47.6ns满足tRAS最小42ns要求留了一点余量。WriteRecoveryTime 2约23.8ns也是按手册最小值取整后加一得到的。3.3 上电初始化命令顺序不能打乱SDRAM上电后要依次执行空操作、预充电所有bank、至少两次自动刷新、加载模式寄存器。FMC控制器本身不会自动执行这段序列必须用HAL_SDRAM_SendCommand手动发。常见顺序如下FMC_SDRAM_CommandTypeDef cmd; memset(cmd, 0, sizeof(cmd)); /* 1. 使能时钟 */ cmd.CommandMode FMC_SDRAM_CMD_CLK_ENABLE; cmd.CommandTarget FMC_SDRAM_CMD_TARGET_BANK1; HAL_SDRAM_SendCommand(hsdram1, cmd, 1); HAL_Delay(1); /* 2. 预充电所有bank */ cmd.CommandMode FMC_SDRAM_CMD_PRECHARGE_ALL; HAL_SDRAM_SendCommand(hsdram1, cmd, 1); /* 3. 8次自动刷新 */ cmd.CommandMode FMC_SDRAM_CMD_AUTOREFRESH_MODE; cmd.AutoRefreshNumber 8; HAL_SDRAM_SendCommand(hsdram1, cmd, 1); /* 4. 加载模式寄存器CAS3突发长度1 */ cmd.CommandMode FMC_SDRAM_CMD_LOAD_MODE; cmd.ModeRegisterDefinition 0x23; HAL_SDRAM_SendCommand(hsdram1, cmd, 1);ModeRegisterDefinition 0x23的含义是低3位设置为突发长度1第3位设置为突发类型顺序第4到第6位设置CAS为3。这段代码里HAL_Delay(1)是为了保证时钟稳定不能省。3.4 Keil5打开工程后的两个常见问题用CubeMX生成MDK工程后如果用Keil5打开发现找不到芯片先检查是否安装了对应的STM32芯片包而不是先怀疑源码。另一个问题是编译器报FMC_SDRAM_CMD_AUTOREFRESH_MODE未定义说明Keil的Device Pack版本和HAL库版本不匹配直接升级到CubeMX配套的Pack版本就行。4. 驱动源码中的读写时序从预充电到列选通4.1 读写状态机里到底发生了什么一次普通读操作分三步先发ACTIVE命令带上bank地址和行地址等tRCD时间后发读命令带上列地址和Burst长度再等CAS延迟后数据才出现在总线上。写操作类似但要注意必须满足tWR才能在写后立刻发预充电命令。MT48LC4M32B2TG的32位数据宽度意味着一条读命令会读出4个字节如果突发长度设为8则会连续读出8个32位字。不少驱动源码为了简化会把突发长度设为1把FMC的ReadBurst关掉这样每个地址只对应一次数据传送调试时更容易定位问题。4.2 裸机读写代码直接用指针映射FMC初始化完成后SDRAM就像一块普通内存直接通过地址指针访问。下面的代码展示了最基本写入和读回#define SDRAM_BASE0 ((uint32_t *)0xC0000000) uint32_t sdram_data_buf[32]; void sdram_rw_test_simple(void) { volatile uint32_t *sdram SDRAM_BASE0; uint32_t i; for (i 0; i 32; i) { sdram[i] 0xDEAD0000 i; /* 写入一个带序号的特征值 */ } for (i 0; i 32; i) { sdram_data_buf[i] sdram[i]; /* 读回 */ if (sdram_data_buf[i] ! (0xDEAD0000 i)) { error_handler(); /* 进入错误处理 */ } } }这个测试能验证数据线有没有焊短路以及最基础的读写路径是否通。如果读回的值是0xDEAD0000和0x00000001交替出现先怀疑数据线的高16位和低16位之间有错位。4.3 突发长度设为1为什么更安全把模式寄存器的突发长度设为1每次读写只有一个数据周期不需要考虑后续数据的地址回绕特别是在屏幕刷新这种连续突发读场景里可以省去设计FIFO的麻烦。代价是总线效率降低每笔访问都要重新发列地址带宽大概只有理论值的六成左右。对于MT48LC4M32B2TG这种老芯片我倾向于把突发长度保留为1优先保证稳定性。只有在明确测出带宽不够、画面刷新跟不上时才去改成8突发并且需要同时把ReadBurst使能否则FMC会当突发命令没有预料到后续数据。4.4 读写时序出错时的现象时序参数偏紧时很少在初始化阶段报错而是在高低温或者高主频下随机出现。常见现象是屏幕上半部分正常、下半部分花屏或者同一个地址读两次值不一样。这时候不要先怀疑SDRAM芯片坏优先把RCDDelay和CASLatency各加1再试。驱动源码里预留一组宽松参数和一组紧凑参数会方便很多。5. 刷新周期计算STM32定时器与FMC自动刷新5.1 为什么是64ms/4096行MT48LC4M32B2TG内部电容存储数据需要周期性刷新。典型规格是每行刷新间隔不超过64ms整个芯片有约4096行所以平均每行刷新间隔是15.625us。如果超过这个时间某一行没刷新那一行数据就可能丢失而且不会立刻被发现往往在几十毫秒后才在画面上体现为一条亮线或一个坏像素。5.2 用FMC硬件自动刷新刷新率怎么配置FMC SDRAM控制器内置一个刷新定时器配置好刷新周期后完全自动运行不需要占用CPU。刷新周期数的计算公式是uint32_t clock_freq_hz 84000000; /* SDCLK为84MHz时 */ uint32_t refresh_count; /* 期望刷新间隔15.625us换算成SDCLK周期数 */ refresh_count (uint32_t)((refresh_interval_sec * clock_freq_hz) / 4096); /* 实际HAL理解这里是两次刷新命令之间的SDCLK周期数需要再减20 */ refresh_count refresh_count - 20; HAL_SDRAM_ProgramRefreshRate(hsdram1, refresh_count);代码里减20是一个经验值因为FMC内部从计数到真正发出刷新命令还有延迟。这个值太小会频繁刷新占用总线带宽太大则会引发数据丢失。调试时可以用一个计数器统计刷新的次数确认实际间隔接近15.625us而不是只看寄存器里的数字。注意刷新计数最终写入的是寄存器后加1个单位具体映射要看参考手册的REFCOUNT位段。5.3 没有FMC时用STM32定时器模拟刷新如果你的板子用的是STM32F1这类没有FMC的型号就只能把GPIO模拟模式下的SDRAM驱动配合STM32定时器使用。常见做法是开一个基础定时器中断周期设为15.625us在中断服务函数里发送一次自动刷新命令void TIM2_IRQHandler(void) { if (LL_TIM_IsActiveFlag_UPDATE(TIM2)) { LL_TIM_ClearFlag_UPDATE(TIM2); sdram_refresh_manual(); /* 拉低RAS、CAS执行自刷新 */ } }这种模拟方式要求中断响应足够快如果系统里有其他关闭全局中断的长临界区刷新就会丢。实际项目中我见过连续运行几个小时才出错的情况排查起来很费劲所以只要芯片型号允许尽量用硬件FMC刷新。5.4 刷新参数和定时器中断的优先级协同使用STM32CubeMX自动生成代码时HAL_SDRAM_ProgramRefreshRate一般放在MX_FMC_Init2之后执行。如果还手动启用了定时器模拟刷新两者不能同时开否则FMC刷新和软件刷新冲突数据反而错得更快。简单起见有FMC的型号只用硬件刷新没有FMC的型号才用定时器。6. 验证驱动是否稳定地址线回读与电源噪声排查6.1 快速自检伪随机数写读回固定花样只能发现数据线问题地址线错位往往在连续地址写入时才会暴露。用伪随机数做回读测试比固定数据更可靠srand(0x12345678); for (uint32_t i 0; i 65536; i) { uint32_t val rand(); /* 伪随机序列 */ sdram[i] val; } srand(0x12345678); for (uint32_t i 0; i 65536; i) { if (sdram[i] ! rand()) { printf(addr mismatch: 0x%08lX\n, 0xC0000000 i * 4); break; } }回读失败时打印的地址有实际价值如果0xC0000008和0xC0000004的内容对调说明FMC_A2和FMC_A2之外某个地址线交叉如果所有错位集中在高16位数据检查D16-D23与D24-D31的排线顺序。6.2 地址线测试不能只写顺序数只用sdram[i] i做测试地址线A2和A3接反是测不出来的因为写入和读回走的是同一套错误映射。要准备几组互补地址的专门测试比如对同一地址先写0x0000FFFF再对相邻地址写0xFFFF0000回读确认互不串扰。这种测试跑10万次没有错误驱动才算真正稳定。6.3 用ST-LINK Utility的Memory窗口直接看数据环境不方便接串口的时候直接用ST-LINK Utility或STM32CubeProgrammer打开内存窗口在地址栏输入0xC0000000就能看到SDRAM里的原始内容。先让固件写一段特征值再用工具查看如果工具读到的值和代码写入的不同基本可以确定是驱动时序或硬件布线问题而不是程序逻辑问题。另一个容易被忽视的坑是SDRAM电源引脚旁的电容数量和容值不能省。MT48LC4M32B2TG在切换行和列时会产生几十纳秒级的电流尖峰如果VDD和VDDQ的滤波电容离芯片太远刷新命令偶尔会失败。遇到随机性数据翻转先用示波器看SDCLK上升沿有没有过冲再决定改驱动参数还是改布局。本文还有配套的精品资源点击获取