引言

在 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 为例)

// 假设 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 并执行读写测试,若失败则调整参数。

#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 稳定性测试,避免后期集成时出现随机故障。