资讯动态

STM32H743外扩SDRAM实战:FMC配置与LVGL内存优化

发布时间:2026/10/5 6:42:54 来源:尧图企业网站定制
上周我在调一个用STM32H743跑LVGL的界面工程UI一复杂起来RGB565全屏缓冲区加JPEG解码输出片内RAM瞬间爆炸。H743虽然自带1MB RAM但一个800x480的RGB565画布就要768KB再叠加图像解码、日志缓冲、RTOS任务栈根本不够用。折腾了两天最后用CubeMXFMC外扩了一片W9825G6KH-6 SDRAM32MB内存一次到位整个工程瞬间就宽裕了。这篇文章把完整的接线、CubeMX配置、时序参数计算、代码验证和常见坑全部理一遍给同样被内存卡脖子的人做个参考。1. 为什么是FMCSDRAM方案选型的思路1.1 外扩内存的几种常见方案先盘点一下片外RAM的种类我当时也纠结过一阵并口SRAM典型如IS62WV51216。速度快、接口简单不需要刷新但是容量小、价格贵。1MB的并口SRAM价格比32MB的SDRAM还高对H743这种强调大内存的应用场景来说很不划算。PSRAM伪静态RAM。容量适中、引脚少但接口协议复杂不同主频下时序限制多调试起来并不轻松。SDRAM容量大、单位成本极低配合FMC控制器可以做到32MB甚至更大读写带宽也完全够用唯一的缺点是必须定期刷新。外扩QSPI Flash不算RAM程序放里面跑可以数据频繁读写的场景不适用。SDRAM听起来麻烦但STM32H743的FMC里内置了完整的SDRAM控制器刷新、预充电、行列地址切换这些底层操作都由硬件完成软件只需要配置寄存器、执行一次初始化命令序列后续就能像访问内部RAM一样直接读写地址空间。这也是H743对比低端MCU的一大优势等于把SDRAM的难度砍掉了大半。1.2 W9825G6KH-6的关键参数W9825G6KH-6是Winbond的256Mbit SDRAM内部结构为4M x 4banks x 16bit按字节算就是32MB容量256Mbit32MB数据位宽16bitBank数量4行数4096列数1024工作电压3.3V最高时钟166MHz-6后缀对应6ns最小CLK周期刷新要求64ms内4096行全部刷新一次选它的理由很直接市面上量大、价格便宜、丝印好买H743的FMC SDRAM模式几乎就是为这类16位SDRAM准备的。顺便提一句如果觉得32MB还不够H743的FMC支持32位总线可以用两片16位SDRAM拼成32位宽度的内存阵列容量翻倍带宽也翻倍不过这是后话了。有一点必须提醒W9825G6KH-6是3.3V器件H743的I/O可以承受3.3V但如果你把主控的电源域做成了1.8V电平匹配就要额外处理别想当然直接连。另外尽量别买拆机散新的SDRAM颗粒焊接温度过高或长期存储的芯片早期失效概率偏高调试时浪费的时间远不止省下的那几块钱。2. 硬件连接设计2.1 FMC引脚速览与接线对照H743的FMC有两组SDRAM片选信号FMC_SDNE0对应Bank1起始地址0xC0000000FMC_SDNE1对应Bank2起始地址0xD0000000。一般只用一片W9825G6KH-6那就接SDNE0地址从0xC0000000开始。我整理了一张接线对照表直接照着接就行FMC信号SDRAM引脚功能FMC_SDCLKCLK时钟输入FMC_SDCKE0CKE时钟使能FMC_SDNE0CS#片选FMC_SDNRASRAS#行选通FMC_SDCASCAS#列选通FMC_SDWEWE#写使能FMC_BA0 / FMC_BA1BA0 / BA1Bank地址FMC_A[12:0]A[12:0]行列地址复用FMC_D[15:0]DQ[15:0]16位数据总线FMC_NBL0LDQM低字节掩码FMC_NBL1UDQM高字节掩码3.3VVDD / VDDQ电源GNDVSS地这里有一个很容易踩的坑W9825G6KH-6的引脚名里没有“字节掩码”这个说法LDQM和UDQM就是对应字节读写掩码分别接FMC的NBL1和NBL0。接反了不会导致完全不能读写而是会出现“低字节正常、高字节错乱”这种半正常状态的诡异问题排查起来非常耗时。所以焊接前务必对着原理图逐个引脚确认。2.2 硬件设计注意点从实际项目经验来看SDRAM硬件设计里有几个点比想象中更重要一是电源。SDRAM的VDD和VDDQ要分别接3.3V每个供电引脚旁边放一颗100nF去耦电容电源入口再放一颗4.7uF到10uF的电容。SDRAM刷新时的电流是脉冲式的供电纹波大一点就直接表现为随机数据错误这种问题不看示波器根本查不出来。二是时钟信号。SDCLK是板上最高频的信号之一走线尽量短、尽量直不要打过孔最好有完整的参考地平面。如果是两层板尽量让SDCLK走在一整块地铜之上不要跨分割。三是数据线和地址线的等长。DQ[15:0]和A[12:0]在布线时尽量保持等长尤其是数据线组内等长。原型验证阶段不一定严格但量产板建议用T型拓扑或蛇形线补偿。四是串联电阻。数据线上可以加33欧串联电阻抑制过冲SDCLK上串联22欧到33欧也很常见。这能明显减少信号反射带来的误采样。如果你用的是现成的核心板或开发板上面已经带了SDRAM那就直接看原理图确认SDNE0、SDCKE0、NBL0/NBL1这几个关键信号有没有接到位再确定地址映射。SDRAM没有上电自检功能硬件好不好只有初始化之后才知道建议刚焊好板子先用CubeMX默认参数跑一次再用读写测试程序验证而不是急着上应用。3. CubeMX配置与关键时序3.1 CubeMX基本配置流程用CubeMX新建工程、选择STM32H743VIT6或者你自己板子对应的型号然后按这个流程走第一配置时钟树。先把HCLK拉到合理的主频H743最高480MHz常见配置是400MHz或480MHz看你的功耗和稳定性需求。这里有个关键点FMC的时钟源自HCLKSDRAM控制器的SDCLK由FMC分频得到对应CubeMX里的“SDRAM时钟分频”参数所以HCLK频率直接决定了SDCLK能跑多高。第二在Connectivity里找到FMC使能SDRAM Bank1数据宽度选16位。如果你的板子接了两片16位SDRAM拼成32位总线这里选32位但后续接线和地址计算方式会不一样本文按16位讲。第三FMC配置页里有一组SDRAM时序参数包括ClockPeriod、tRCD、tRP、tRAS、tRC、tWR、tXSR和RefreshRate。这一页是SDRAM能不能稳定运行的灵魂下一节逐个拆开讲。第四配置完生成工程。CubeMX支持生成Makefile工程方便在VSCode里用arm-none-eabi-gcc编译调试也可以生成MDK或EWARM工程逻辑完全一样。3.2 时序参数逐项解释与计算SDRAM不是上电就能直接访问的它需要满足一系列最小延迟约束。比如发出行激活命令之后必须等tRCD时间才能发列读写命令写完数据之后必须等tWR才能预充电。FMC把这些时间换算成SDCLK的周期数写入寄存器。以HCLK400MHz、FMC分频3、SDCLK133MHz时钟周期约7.5ns为例对照W9825G6KH-6手册里的tRCD18ns、tRP18ns、tRAS42ns、tRC63ns实际计算是这样的参数最小时间周期数计算保险取值LoadToActiveDelaytRCD18ns18 / 7.5 2.43RPDelaytRP18ns18 / 7.5 2.43SelfRefreshTimetRAS42ns42 / 7.5 5.66RowCycleDelaytRC63ns63 / 7.5 8.49WriteRecoveryTimetWR14ns至少2个周期2ExitSelfRefreshDelaytXSR75ns75 / 7.5 1010周期数全部向上取整再多加1个周期裕量这样初始化成功率和长期稳定性都会好很多。这里必须单独强调一下CubeMX里的“ClockPeriod for SDRAM”分频选项。如果HCLK400MHz选2分频得到SDCLK200MHz已经超出了W9825G6KH-6的166MHz上限。我一开始图省事把分频设成2读写测试居然全过但跑了半小时UI出现偶发花屏最后查出来就是SDRAM超频导致的不稳定。所以第一步就把SDCLK压到规格以内后面能省很多事情。保守做法是把分频设成4SDCLK100MHz时序余量更大。刷新率字段Refresh Rate同样关键。SDRAM要求64ms内把4096行全部刷新一遍换算下来就是每隔15.625us要发起一次刷新。RefreshRate的数值表示多少个SDCLK周期发起一次刷新。133MHz下计算如下15.625us / 7.5ns 2083个周期所以CubeMX里填2083稳妥一点填2063也可以。这个值太大会导致刷新不够频繁、数据慢慢丢失太小又会频繁打断正常读写、浪费带宽。默认情况下填2000附近基本不会出错。3.3 时钟树与引脚冲突检查在CubeMX里配置完FMC之后一定要到Pinout视图里检查一遍引脚冲突。SDNE0、SDCKE0、SDCLK、NBL0、NBL1这几个信号非常容易被其他外设占用比如SDCLK和某个串口的TX复用同一个物理引脚。遇到冲突要么调整其他外设的引脚映射要么干脆换一组SDRAM Bank用SDNE1组。还要注意GPIO输出速度。FMC相关引脚CubeMX一般会自动配置成高速模式不要手动改慢否则SDRAM时序会瞬间恶化出现各种莫名其妙的偶发错误。生成Makefile工程之后在VSCode里直接编译这里可以配合STM32CubeCLT的命令行工具链和IDE里效果一样。4. 代码验证与复用4.1 SDRAM初始化命令序列CubeMX生成的代码里HAL_SDRAM_Init函数已经把FMC的SDRAM控制器参数配置好了但SDRAM芯片本身还需要一组上电初始化命令预充电所有bank、连续两次自动刷新、加载模式寄存器。这一串命令在HAL库中通过HAL_SDRAM_SendCommand发送。不同版本的CubeMX生成的代码不完全一样有些会在初始化函数里带一部分命令序列有些不会所以这个步骤一定要确认清楚。参考代码如下FMC_SDRAM_CommandTypeDef Command; // 第一步预充电所有bank Command.CommandMode FMC_SDRAM_CMD_PRECHARGE_ALL; Command.CommandTarget FMC_SDRAM_CMD_TARGET_BANK1; Command.AutoRefreshNumber 1; Command.ModeRegisterDefinition 0; HAL_SDRAM_SendCommand(hsdram1, Command, 0x1000); // 第二步连续两次自动刷新 Command.CommandMode FMC_SDRAM_CMD_AUTOREFRESH_MODE; Command.AutoRefreshNumber 2; HAL_SDRAM_SendCommand(hsdram1, Command, 0x1000); // 第三步加载模式寄存器 Command.CommandMode FMC_SDRAM_CMD_LOAD_MODE; Command.ModeRegisterDefinition 0x23; HAL_SDRAM_SendCommand(hsdram1, Command, 0x1000);关于ModeRegisterDefinition的取值这里多解释一句。SDRAM模式寄存器里包含突发长度、突发类型、CAS延迟等信息。0x23代表CAS Latency2突发长度8顺序突发模式。如果你把CASLatency配置成3那这个值要改成0x33。这个细节直接决定SDRAM能不能正确进入预期的工作模式照抄别人的代码时一定要核对。4.2 读写测试代码初始化完成后32MB SDRAM就映射在0xC0000000地址上。验证步骤建议分成三段先做递增写读、再做地址翻转写读、最后用伪随机数据压测。参考代码如下volatile uint16_t *sdram (uint16_t *)0xC0000000; uint32_t i; uint32_t errors 0; uint32_t total 0x2000000 / 2; // 32MB / 2字节 // 测试1递增数据写读 for (i 0; i total; i) { sdram[i] (uint16_t)(i 0xFFFF); } for (i 0; i total; i) { if (sdram[i] ! (uint16_t)(i 0xFFFF)) { errors; } } // 测试2地址翻转写读防止数据线粘连 for (i 0; i total; i) { sdram[i] (uint16_t)(~(i 0xFFFF)); } for (i 0; i total; i) { if (sdram[i] ! (uint16_t)(~(i 0xFFFF))) { errors; } }如果errors为0且能稳定运行基本说明数据总线、地址总线和控制信号都是通的。这里建议先测内存后段地址比如0xC1F00000附近因为有些标称32MB的板子实际颗粒可能是16MB或引脚悬空后段地址会访问不到先测两端能更快暴露地址线问题。只测首地址附近的话容量问题不一定能暴露。4.3 对接LVGL 9.5和FreeRTOS堆SDRAM最大的价值是给GUI和RTOS堆提供空间。LVGL 9.5这块draw buffer和flush buffer都可以直接放到SDRAM。以RGB565、800x480分辨率为例一个全屏buffer就是8004802768KB放在片内1MB RAM里会直接吃掉大半个内存而放在SDRAM里就是很小一块。在LVGL初始化配置里把buffer指针指向0xC0000000段即可。JPEG硬解场景也一样。H743内置JPEG编解码器解码输出的帧缓冲动辄几百KB放片内RAM不现实扔到SDRAM刚刚好。这也是H743这类芯片的典型用法JPEG解码输出直接写到SDRAM帧缓冲再交给LTDC显示。FreeRTOS这边把configTOTAL_HEAP_SIZE调大之后堆空间需要落在SDRAM地址范围。通常有两种做法改链接脚本把堆段定位到SDRAM区域或者在启动代码里把堆指针设置到0xC0000000。这部分涉及链接脚本修改内容比较多后面可以单独写一篇。一个非常重要的提醒如果使用DMA直接把数据搬运到SDRAM比如DCMI摄像头数据、SDMMC读取、JPEG硬解输出一定要处理Cache一致性问题。否则会出现“读到的数据总是旧的”这种灵异现象。简单做法是在MPU配置里把SDRAM区域设置为non-cacheable或者在DMA传输前后调用SCB_CleanDCache和SCB_InvalidateDCache。这个坑几乎所有DMA加SDRAM的项目都会遇到。5. 踩坑实录常见问题与排查方法5.1 常见问题与原因对照表我把实际调试中遇到的高频问题整理成了一张速查表可以按图索骥现象可能原因解决方向HAL_SDRAM_Init超时或卡死时序配置过紧或SDCLK超出芯片上限调大分频放宽tRCD/tRP读回全是0x00或0xFF片选或时钟使能没接对Bank映射错误检查SDNE0/SDCKE0接线确认起始地址部分地址数据错乱地址线、数据线接错NBL反接对照接线表核查A0、NBL0/NBL1跑一段时间数据变花刷新率配置不对或SDRAM超频检查RefreshRate把SDCLK降到100MHzDMA读到的数据总是旧的Cache一致性问题MPU设为non-cacheable或手动Clean/Invalidate复位后第一次读失败初始化命令序列不完整或延时不足确保预充电、自动刷新、LoadMode都执行5.2 三个让我头疼的实际案例案例一SDClockPeriod设为2导致花屏。这个在前面提过HCLK400MHz时分频2就是200MHz超出W9825G6KH-6的166MHz上限。最坑的是读写测试能全过跑半小时才随机花屏稍微不留神就误判成别的外设问题。改成3分频后跑一整天也没问题血泪教训。案例二自绘板读回只有低字节正确。查原理图发现NBL1引脚布线画错位置导致高字节掩码一直有效。这种一半正常一半错乱的问题比完全不通难查得多因为系统能跑、能读写就是数据不对。后来我用示波器对比了LDQM和UDQM的信号才发现两个波形完全不同。案例三SDIO加DMA读写TF卡再写入SDRAM出现随机坏块。一开始怀疑是SDRAM时序不稳查了好几个钟头最后发现是DMA和Cache冲突。H743的Cache比较激进CPU读回来的是Cache里的旧数据而DMA已经写了新数据。加上SCB_InvalidateDCache之后就好了解决方案就这么简单但方向错了能折腾半天。5.3 独家的排查技巧多说一个经验。SDRAM调试时不要直接上完整的GUI应用先用最小测试程序验证再一步步往上加功能。我习惯把读写测试做成一个独立函数在任何工程里都能随时调用这样一发现问题就能快速定位是不是SDRAM本身的问题。如果怀疑时序不稳把一个循环写读压测程序跑一晚上第二天看错误计数。偶发数据错误往往在长时间运行后才会暴露短期测试通过不代表时序规格没问题。另外SDRAM焊完之后先目测引脚有没有虚焊再用放大镜检查颗粒下面有没有连锡这种低级问题用万用表量一遍也能发现。我个人实践下来的最大体会是SDRAM扩展内存确实能解决燃眉之急但一定要按数据手册来尤其是时钟和刷新率这两个参数不能凭感觉。先在CubeMX里把分频和时序算好再画板或接线比焊好以后反复试错高效得多。最后分享一个习惯每次焊完新板子先把SDRAM跑一个完整的读写测试程序确认稳定以后再上LVGL或RTOS这样能帮你把内存相关的坑隔离在应用之外。如果你在LVGL 9.5、JPEG硬解或者H743其他外设上也遇到内存相关的坑欢迎多交流。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑