# 引言 在 STM32F4 系列(如 STM32F429/439)中,FMC(Flexible Memory Controller)支持外部 SDRAM,为图形缓冲、大数组等提供廉价大容量存储。然而,SDRAM 需要周期性刷新以保持数据,而总线访问(读/写)与刷新操作共享同一物理通道。若配置不当,刷新请求会抢占总线,导致访问延迟超时或数据冲突,表现为系统运行不稳定、HardFault 或数据随机错误。本文聚焦于这一嵌入式开发中的经典问题,提供从原理到实践的完整排查方案。 # SDRAM 时序基础与冲突根源 SDRAM 内部以电容存储数据,必须周期性刷新(典型周期 64ms 内刷新所有行)。STM32F4 的 FMC 通过硬件自动生成刷新命令,但刷新操作会占用总线带宽。关键时序参数包括: - **tRC**(行周期时间):两次行激活之间的最小间隔。 - **tRCD**(行激活到列读写延迟):行激活命令到读写命令的间隔。 - **tRP**(预充电时间):行预充电命令到下一次行激活的间隔。 - **刷新周期**:FMC 的刷新定时器(RT)配置,决定刷新频率。 冲突根源:当 CPU 或 DMA 频繁访问 SDRAM 时,若刷新请求恰逢总线繁忙,FMC 会插入等待状态,导致访问延迟增加。若刷新间隔过短(RT 值过小),刷新请求过于频繁,总线利用率下降;若过长,则可能超过 SDRAM 的最大刷新间隔,导致数据丢失。此外,总线延迟(如读/写等待状态)与刷新周期不匹配,可能造成 FMC 状态机死锁。 # 排查步骤 ## 1. 确认硬件连接与时钟配置 首先检查 SDRAM 的时钟(FMC 时钟通常为 HCLK 分频),确保 SDRAM 时钟频率在规格范围内(如 100MHz)。使用逻辑分析仪或示波器测量 SDRAM 时钟和片选信号,排除硬件时序问题。 ## 2. 配置 FMC SDRAM 控制器 在 STM32CubeMX 中启用 FMC 并选择 SDRAM 型号,或手动配置寄存器。核心寄存器包括: - **SDCR**(SDRAM 控制寄存器):设置列地址位数、行地址位数、CAS 延迟等。 - **SDTR**(SDRAM 时序寄存器):设置 tRC、tRCD、tRP、tWR 等。 - **SDCMR**(SDRAM 命令模式寄存器):用于发送配置命令。 - **SDRTR**(SDRAM 刷新定时器寄存器):设置刷新周期。 ### 关键配置示例(以 32MB SDRAM,时钟 100MHz 为例) ```c // 假设 FMC 时钟为 100MHz,SDRAM 为 32MB,列宽 8 位,行宽 12 位 void SDRAM_Init(void) { // 1. 使能 FMC 时钟 __HAL_RCC_FMC_CLK_ENABLE(); // 2. 配置 SDRAM 控制器 FMC_SDRAM_InitTypeDef sdram_init = {0}; sdram_init.SDBank = FMC_SDRAM_BANK1; sdram_init.ColumnBitsNumber = FMC_SDRAM_COLUMN_BITS_NUM_8; sdram_init.RowBitsNumber = FMC_SDRAM_ROW_BITS_NUM_12; sdram_init.MemoryDataWidth = FMC_SDRAM_MEM_BUS_WIDTH_16; sdram_init.InternalBankNumber = FMC_SDRAM_INTERN_BANKS_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; // 100MHz -> 2 HCLK sdram_init.ReadBurst = FMC_SDRAM_RBURST_ENABLE; sdram_init.ReadPipeDelay = FMC_SDRAM_RPIPE_DELAY_1; // 3. 配置时序参数(单位:HCLK 周期) FMC_SDRAM_TimingTypeDef sdram_timing = {0}; sdram_timing.LoadToActiveDelay = 2; // tRCD sdram_timing.ExitSelfRefreshDelay = 7; // tXSR sdram_timing.SelfRefreshTime = 4; // tRAS sdram_timing.RowCycleDelay = 7; // tRC sdram_timing.WriteRecoveryTime = 2; // tWR sdram_timing.RPDelay = 2; // tRP sdram_timing.RCDDelay = 2; // tRCD // 4. 初始化 FMC SDRAM HAL_SDRAM_Init(&hsdram, &sdram_init, &sdram_timing); // 5. 配置刷新周期(关键!) // 刷新周期 = (刷新间隔 / 行数) * 时钟周期 // 例如:64ms / 4096 行 = 15.625us,在 100MHz 下为 1562 个时钟周期 // 但实际需考虑总线延迟,通常设置稍小值(如 1400) HAL_SDRAM_ProgramRefreshRate(&hsdram, 1400); } ``` ## 3. 调整刷新周期与总线延迟 若出现冲突,首先尝试调整刷新定时器(SDRTR 的 COUNT 字段)。计算公式: ``` COUNT = (刷新间隔时间 * FMC时钟频率) - 20 ``` 其中 20 为 FMC 内部固定开销。例如,64ms/4096 行 = 15.625us,在 100MHz 下为 1562 周期,减去 20 得 1542。但实际调试中,建议从 1500 开始,逐步减小(如每次减 50),观察稳定性。 同时,检查总线延迟(如读/写等待状态)。在 SDTR 中增加 tRCD 或 tRP 可缓解时序冲突,但会降低性能。平衡点在于:刷新周期不能太紧(否则频繁抢占总线),总线延迟不能太长(否则访问超时)。 ## 4. 使用调试工具验证 - **逻辑分析仪**:抓取 FMC 的 NBL、CAS、RAS 信号,观察刷新命令与读写命令的时序重叠。 - **STM32CubeMonitor**:监控 SDRAM 访问错误计数(若使用 ECC)。 - **软件测试**:编写压力测试程序,连续读写 SDRAM 并校验数据,同时触发 DMA 传输,观察是否出错。 # 完整代码示例:压力测试与冲突检测 以下代码演示如何初始化 SDRAM 并执行读写测试,若失败则调整参数。 ```c #include "stm32f4xx_hal.h" extern SDRAM_HandleTypeDef hsdram; void SDRAM_Test(void) { uint32_t *sdram_base = (uint32_t *)0xC0000000; // 假设 SDRAM 映射地址 uint32_t data[256]; uint32_t errors = 0; // 写入模式 for (int i = 0; i < 256; i++) { sdram_base[i] = 0xDEADBEEF ^ i; } // 读取并校验 for (int i = 0; i < 256; i++) { data[i] = sdram_base[i]; if (data[i] != (0xDEADBEEF ^ i)) { errors++; } } if (errors > 0) { // 错误处理:可能刷新冲突,尝试调整刷新率 HAL_SDRAM_ProgramRefreshRate(&hsdram, 1300); // 减小刷新周期 // 重新测试 } else { // 成功 } } // 在 main 中调用 int main(void) { HAL_Init(); SystemClock_Config(); SDRAM_Init(); SDRAM_Test(); while (1); } ``` # 注意事项 - **刷新周期调整需谨慎**:过小会导致刷新过于频繁,降低性能;过大则可能超过 SDRAM 最大刷新间隔(如 64ms),造成数据丢失。 - **总线延迟与刷新周期相互影响**:增加 tRCD 等延迟会延长单次访问时间,但可能减少与刷新冲突的概率;反之,缩短延迟可能加剧冲突。 - **DMA 访问**:DMA 控制器与 CPU 共享 FMC,若 DMA 频繁访问 SDRAM,需确保刷新优先级设置合理(FMC 内部固定优先级)。 - **温度影响**:SDRAM 刷新间隔受温度影响,高温下需缩短刷新周期(如 32ms),否则数据保持时间缩短。 - **使用 HAL 库时**:HAL_SDRAM_ProgramRefreshRate 的第二个参数是刷新周期(以 FMC 时钟周期计),需根据实际时钟计算。 # 总结 总线延迟与刷新周期冲突是 STM32F4 外接 SDRAM 的常见问题,根源在于刷新请求与总线访问的时序竞争。通过合理配置 FMC 时序参数和刷新定时器,并借助逻辑分析仪和压力测试,可以有效定位和解决。建议在项目初期就进行 SDRAM 稳定性测试,避免后期集成时出现随机故障。