STM32H743外扩SDRAM实战:FMC接口驱动W9825G6KH-6解决内存不足

发布时间:2026/10/5 4:44:23
STM32H743外扩SDRAM实战:FMC接口驱动W9825G6KH-6解决内存不足
最近在调一块基于STM32H743的板子界面要跑LVGL还得挂摄像头做JPEG硬件编码预览。程序堆到一半HardFault开始频繁出现。查到最后问题出在内存上H743内部SRAM加起来虽然有1MB左右但LVGL缓冲区、JPEG输出缓冲、协议栈、RTOS任务栈一分剩下的空间根本不够用。于是我决定把外扩SDRAM这条路走通目标芯片是W9825G6KH-6容量32MB走FMC接口。从CubeMX配置、硬件接线到初始化代码、稳定性测试我完整走了一遍这里把过程和踩过的坑都记录下来给同样被内存折磨的人一个参考。这个方案适合正在做图形界面、图像采集、音频缓存或跑较大协议栈的朋友。如果你只是跑个简单逻辑控制内部SRAM绰绰有余没必要上SDRAM。但如果你已经开始对着H743的内存分配表发愁或者LVGL的draw buffer只能抠抠搜搜开几个KB那这篇文章应该能帮你省下不少时间。1. 先盘一盘H743的内存内部SRAM到底哪里不够用1.1 H743的SRAM组织分配H743的内存并不是一块单独的RAM而是分布在不同总线域上的多块SRAM。这一点和F4系列差异很大也是很多人刚上手时容易忽略的地方。内存区域地址范围容量所在总线域主要用途ITCM0x0000000064KBD1域代码执行紧耦合指令内存DTCM0x20000000128KBD1域关键数据、栈CPU直连访问最快AXI SRAM0x24000000512KBD1域大块数据缓冲区LCD显存、DMA常用SRAM10x30000000128KBD2域通用数据外设DMA可访问SRAM20x30020000128KBD2域通用数据SRAM30x3004000064KBD2域通用数据SRAM40x3880000064KBD3域低功耗后台数据BDMA可访问看起来容量确实不少但实际用起来很快就会感觉到吃力。LVGL做UI时draw buffer如果只有几十KB复杂一点的界面刷新率会肉眼可见地掉下去。而JPEG硬件编码输出YUV图像数据时一帧720p的YUV422数据就接近2MB内部SRAM放不下。再看TCP/IP协议栈、文件系统缓存、USB描述符缓冲区这些加起来轻轻松松吃掉几百KB。H743的1MB SRAM在这种场景下真的不够看。1.2 什么场景会上SDRAM以及为什么它是合理选择内部SRAM不够时常见的选择有并口SRAM、SPI PSRAM和SDRAM。并口SRAM速度快但容量做不大16MB容量的并口SRAM价格离谱且占引脚多。SPI PSRAM虽然接线简单但带宽受限于SPI时钟做LVGL全屏刷新或者摄像头数据缓冲时会明显卡顿。SDRAM的优势在于容量/价格比高32MB的成本远低于同容量SRAM而且H743内置FMC控制器支持SDRAM硬件刷新、命令序列都由外设完成不占CPU。W9825G6KH-6是Winbond的一颗256Mbit32MBSDRAM16位数据宽度4个Bank工作电压3.3V。它的封装是常见的54脚TSOP手工焊接难度不大非常适合做扩展内存。H743的FMC接口正好支持SDRAM设备映射地址从0xC0000000开始配置好之后直接以内存指针方式访问用起来比外部SRAM还简单。2. FMC外扩SDRAM的方案选型与硬件接线要点2.1 为什么非要用FMC而不是GPIO模拟SDRAM和SRAM最大的区别在于需要定期刷新。SDRAM内部存储单元本质上是电容电荷电荷会泄漏所以必须在64ms内对所有行完成一次刷新否则数据就丢了。如果用GPIO模拟时序你需要手动产生RAS、CAS、WE这些信号组合还得在刷新窗口内暂停其他操作去一行一行地刷新。以32MB容量的典型参数来算4096行需要在64ms内刷新完平均每行间隔15.625usCPU几乎什么事都干不了。FMC自带SDRAM控制器之后这些事全部由硬件接管。你只需要配置好时序参数之后按普通内存读写就行。刷新操作由FMC在后台自动调度CPU零参与。这也是我推荐直接用FMC而不是自己写模拟时序的根本原因。H743的FMC控制器还支持自动预充电、自动刷新、突发访问配合DMA可以达到很高的吞吐量。2.2 W9825G6KH-6这颗芯片怎么看W9825G6KH-6的命名规则W9825表示256MbitG6表示16位I/O、4Bank结构KH代表TSOP封装-6代表速度等级为6ns也就是CL3时最高可以跑到166MHz左右不同批次以数据手册为准。芯片内部结构是4个Bank每个Bank包含若干行和列行、列地址由FMC分时复用送出所以外部地址线数量不等于CPU访问容量需要的地址线数量。这颗芯片的关键时序参数在数据手册里有明确标注。初始化和稳定性调试时我们需要关注的参数包括tRCDRAS到CAS延迟、tRP预充电时间、tRC行周期时间、tRAS最小行激活时间、tWR写恢复时间、tXSR退出自刷新时间等。手册里给出的典型值一般是在100MHz或133MHz条件下的数据实际配置时需要根据H743的FMC时钟频率换算成周期数。我用的W9825G6KH-6手册上典型的参数值是tRCD20nstRP20nstRC63nstRAS45nstWR15nstXSR70ns。这些数值会作为后续CubeMX时序配置的计算依据。另外刷新周期为64ms/4096行换算成每行刷新间隔是15.625us这个数字后面算RefreshRate也会用到。2.3 硬件接线引脚规划与封装提醒硬件接线层面FMC对外提供一组标准SDRAM总线接口不需要额外逻辑芯片。FMC信号连接到W9825G6KHFMC_SDCLKCLKFMC_SDNE0CS#FMC_SDCKE0CKEFMC_SDNRASRAS#FMC_SDNCASCAS#FMC_SDNWEWE#FMC_DQM0LDQMFMC_DQM1UDQMFMC_A[12:0]A[12:0]FMC_SDBA0/SDBA1BA0/BA1FMC_D[15:0]DQ[15:0]注意FMC_SDNE0对应SDRAM Bank1地址起始为0xC0000000。如果以后扩展第二片SDRAM可以用SDNE1对应0xD0000000。不是所有H743封装都把完整FMC引脚引出来的LQFP100这类小封装根本放不下这么多地址线和数据线。我自己就吃过这个亏画PCB时没确认封装结果引脚不够用被迫换了LQFP176封装的型号。选型阶段务必带着CubeMX的引脚视图过一遍确认目标封装的FMC引脚够不够。硬件layout上有几个细节需要强调SDRAM时钟线FMC_SDCLK尽量短不要走太细的线尽量减少过孔DQ和地址线要等长至少保证组内等长偏差控制在5mm内SDRAM的VDD和VDDQ就近放10410uF电容去耦。飞线调试的时候建议直接把FMC时钟频率降一半能避开很多莫名其妙的信号完整性问题。3. CubeMX一步步配置FMC与SDRAM时序参数3.1 新建工程与时钟树先把HCLK定下来用CubeMX新建工程时芯片选STM32H743然后进入Clock Configuration界面。FMC控制器的内核时钟继承自HCLKHCLK直接决定SDRAM时序参数换算的结果所以第一步先把HCLK定下来。我这里用的外部晶振25MHz配置PLL后HCLK跑到200MHz。为什么不用240MHzHCLK越高SDRAM时序参数按周期数计算时越难满足W9825G6KH-6的最小时间要求。比如63ns的tRC在200MHz周期5ns下需要13个周期如果跑到240MHz周期4.17ns63ns需要取整到16个周期。周期数变大不是问题问题是SDCLK分频跟不上。SDCLK来源是HCLKCubeMX里可以选除以2或除以3。我倾向于HCLK200MHz、SDCLK100MHz的组合时序余量最充裕稳定性测试最容易通过。等调稳定了再考虑往上拉频率。时钟树里还需要确认FMC外设时钟已经使能。在H743的CubeMX配置里FMC挂在AHB3总线上只要外设勾选FMC时钟会自动使能。GPIO复用时钟也会在生成代码时自动处理。3.2 FMC外设界面配置列、行、Bank、CAS一个都不能错在Pinout Configuration界面左侧选择Connectivity - FMC勾选SDRAM Bank1。这时右侧会出现SDRAM配置面板需要填的参数主要有Column bits number列地址宽度。W9825G6KH-6按1024列计算选10 bits。Row bits number行地址宽度。4096行选12 bits。Internal bank number芯片内部Bank数选4 banks。Memory data width数据总线宽度选16 bits。CAS latency写模式寄存器时决定的CAS延迟选3。Write protection关闭。SDClock period这里填2代表SDCLK HCLK / 2 100MHz。Read burst建议先选Disable后面调优再开。Read pipe delay默认1即可。这几项里最容易错的是Column bits和Row bits。不同厂商、不同容量的SDRAM行列地址宽度不一样必须对着芯片手册确认。W9825G6KH-6的数据手册会明确写“4 Banks x 4096 Rows x 1024 Columns x 16 Bits”照着填就不会错。如果填错最典型的表现是寻址错乱前几KB读写正常往后越界访问或者覆盖到其他地址排查起来很费劲。配置完成后CubeMX的芯片引脚视图会把需要用到的FMC引脚自动拉出来显示为绿色。留意一下是否有引脚冲突尤其当芯片封装引脚不足时CubeMX会报错。这个时候只能换大封装而不是手动改IO因为FMC的SDRAM信号是固定复用脚不能随意重映射。3.3 时序参数表按数据手册换算成FMC时钟周期CubeMX中SDRAM的时序参数单位不是纳秒而是FMC时钟周期数。所以需要把数据手册上的时间参数除以FMC时钟周期再向上取整。我的配置是HCLK200MHzFMC时钟周期5ns。如果SDCLK选择HCLK/2那么SDRAM运行时序仍然以FMC内核时钟为基准也就是5ns的整数倍。下面这个表是我计算和填入的结果CubeMX参数对应手册时序手册典型值计算方式填入值RCDDelaytRCD20nsceil(20/5)4RPDelaytRP20nsceil(20/5)4RowsCycleDelaytRC63nsceil(63/5)13SelfRefreshTimetRAS45nsceil(45/5)9WriteRecoveryTimetWR15nsceil(15/5)3ExitSelfRefreshDelaytXSR70nsceil(70/5)14LoadToActiveDelaytMRD2个周期手册值2Refresh Rate这个参数需要单独算。SDRAM的刷新周期是64ms4096行每行刷新间隔15.625us。RefreshRate的值等于这个间隔除以FMC时钟周期也就是15.625us / 5ns 3125。在CubeMX中把Refresh Rate填成3125。如果用的是240MHz的HCLK周期约4.17nsRefreshRate就要填大约3750但我强烈建议100MHz SDCLK起步先把系统跑稳再谈性能。注意CubeMX里这些时序参数的单位是“FMC时钟周期”不是“SDRAM时钟周期”。很多人在这里会搞混。哪怕SDCLK被分频到了100MHz时序计算的基准依然是FMC内核时钟的5ns不要除以2再算一遍。3.4 MPU Region设置缓存一致性这次一次说清这是整个外扩SDRAM调试里最容易踩、也最隐蔽的坑Cortex-M7的Cache一致性。H743默认开启了D-Cache和I-Cache。CPU读SDRAM时数据会先进入Cache写数据时也可能停留在Cache里。如果这时DMA、JPEG硬件编解码器或者LCD控制器直接访问同一块SDRAM地址就会拿到过期的数据或者覆盖掉还没写回的内容。表现就是单纯CPU循环读写测试没问题但一旦摄像头DMA把图像写到SDRAMCPU再去读读到的可能是旧数据花屏、错位、死机接踵而至。解决办法就是在MPU中把FMC SDRAM所在的内存区域配置为non-cacheable。CubeMX里进入System Core - MPU使能MPU添加一个RegionBase address0xC0000000Size32MBTEX1C0B1SShareable勾选这样设置之后CPU对SDRAM的读写全部绕过Cache直接访问物理内存DMA和CPU之间不再存在一致性问题。代价是CPU访问SDRAM的速度会慢一些。但对SDRAM来说本身速度就远低于AXI SRAMcache带来的性能收益有限不如先保证数据一致性。如果你项目里对性能要求极高也可以配置为write-through并手动做cache clean但这是后话不建议新手一上来就这么干。还有一部分人会选择直接关掉D-Cache来避免问题但这样内部SRAM和Flash区域的访问性能都会受影响属于杀敌八百自损一千。用MPU单独把SDRAM区域配置成non-cacheable其他区域保持cache才是最合理的方案。3.5 生成工程后初始化代码还需要补全CubeMX生成代码后FMC的初始化部分已经包含了HAL_SDRAM_Init但这里有个非常关键的步骤CubeMX生成的代码并不会自动向SDRAM发送模式寄存器配置命令需要在用户代码里手动补一段初始化序列。我习惯在main函数里HAL_SDRAM_Init之后、正式使用SDRAM之前调用一个自己写的SDRAM_InitSequence函数void SDRAM_InitSequence(void) { FMC_SDRAM_CommandTypeDef cmd; /* 1. 开启时钟 */ cmd.CommandMode FMC_SDRAM_CMD_CLK_ENABLE; cmd.CommandTarget FMC_SDRAM_CMD_TARGET_BANK1; cmd.AutoRefreshNumber 1; cmd.ModeRegisterDefinition 0; HAL_SDRAM_SendCommand(hsdram1, cmd, 0x1000); /* 2. 预充电所有Bank */ cmd.CommandMode FMC_SDRAM_CMD_PALL; HAL_SDRAM_SendCommand(hsdram1, cmd, 0x1000); /* 3. 连续两次自动刷新 */ cmd.CommandMode FMC_SDRAM_CMD_AUTOREFRESH_MODE; cmd.AutoRefreshNumber 2; HAL_SDRAM_SendCommand(hsdram1, cmd, 0x1000); /* 4. 配置模式寄存器CL3BL1写突发模式单次 */ cmd.CommandMode FMC_SDRAM_CMD_LOAD_MODE; cmd.AutoRefreshNumber 1; cmd.ModeRegisterDefinition 0x230; HAL_SDRAM_SendCommand(hsdram1, cmd, 0x1000); /* 5. 设置刷新率计数器 */ HAL_SDRAM_ProgramRefreshRate(hsdram1, 3125); }这里有几个细节需要注意。ModeRegisterDefinition的值0x230对应的是CL3、Burst Length1、Write Burst Mode设置为Single。如果用的SDRAM芯片是其他CL档位需要按模式寄存器定义重新计算CAS Latency位在A6:A4上BL在A2:A0上。第一次调试时不要急着上Burst模式BL1最容易跑稳。另外命令发送超时时间给0x1000那个0表示永不超时建议给一个足够大的值避免初始化时断在中间。还有一点CubeMX生成的MX_FMC_Init函数里会填充时序结构体但它默认填的值不一定适配你的芯片。生成代码之后一定要回到刚才的时序参数表逐项核对生成的.Init.LoadToActiveDelay等字段是否符合预期。我在第一次调的时候就遇到过CubeMX自动填了LoadToActiveDelay1而W9825G6KH要求tMRD至少2个周期结果SDRAM初始化后状态机混乱读写全失败。4. 读写测试与LVGL实际使用4.1 先从最简单的写读测试开始SDRAM初始化完成后第一件事不是直接跑LVGL而是做一遍基础读写测试。基址选择宏定义#define SDRAM_BASE_ADDR ((uint32_t)0xC0000000)最简单的测试方式就是往整个32MB空间写一个固定值再读回来验证uint32_t status 0; uint32_t *ptr (uint32_t *)SDRAM_BASE_ADDR; uint32_t test_size 32 * 1024 * 1024 / 4; for (uint32_t i 0; i test_size; i) { ptr[i] 0xA5A5A5A5; } for (uint32_t i 0; i test_size; i) { if (ptr[i] ! 0xA5A5A5A5) { status 1; break; } }注意要把指针声明为volatile不然编译器优化后可能省略读操作导致错误测不出来。写固定值可以测出大部分数据线短路、断路问题但还不能完整暴露行、列寻址错误。所以接下来要做更细致的地址线测试。4.2 边界与随机测试地址线问题有一个经典测试方法在每个2的幂次边界写特殊值再读回来。比如在地址0x00000000、0x00001000、0x00002000、0x00004000、0x00008000……一直到0x02000000分别写入地址本身再全片遍历校验。这样如果某根地址线断开、短路或者接错会在对应的边界位置暴露出来。我还建议跑一个伪随机读写测试防止固定pattern掩盖了时序问题。用线性同余生成器产生伪随机序列写入并读回比较。代码不复杂但能覆盖更多的bank切换和行切换场景。测试时把整个32MB都跑一遍如果出现零星错误不要急按照第5节的思路排查。这里还有一个容易被忽略的问题测试代码中的数组、栈、全局变量可能放在内部SRAM这是没问题的。但如果测试代码本身被链接到SDRAM地址而SDRAM还没初始化就会直接HardFault。所以测试工程请确保代码和栈都在内部SRAM或Flash里不要在SDRAM未初始化前去访问0xC0000000区域。4.3 用LVGL9.5验证一下外扩内存基础测试通过后我把LVGL9.5的draw buffer放到了SDRAM里。之前在内部SRAM时LVGL只能开两个320x240的16位色buffer大概150KB左右复杂界面刷新还是卡。放到SDRAM之后我直接开了一个全屏大小的buffer分辨率800x48016位色深单帧缓冲大约是768KB两个buffer就是1.5MBSDRAM完全没压力。实际跑起来的效果界面刷新率比之前有明显提升原因是SDRAM带宽足够支撑全屏缓冲的反复读写。但也要说句公道话SDRAM的访问延迟比内部SRAM高如果LVGL的渲染代码对单像素操作特别密集速度还是不如SRAM。所以我的做法是LVGL的draw buffer放SDRAM但RTOS任务栈、关键状态标志、临界区数据继续放在内部SRAM。这样性能和容量的平衡最好。JPEG硬件编码那边我也把输出帧缓冲指向了SDRAM。H743的JPEG加速器支持YUV格式输出一帧大图轻松吃掉几百KB甚至1MB放SDRAM再合适不过。需要注意的是一旦JPEG或DMA和CPU同时访问SDRAMMPU设置就一定要正确否则之前提到的Cache一致性问题会在图像花屏上反复折磨你。5. 常见问题和调优经验回放5.1 初始化后读回全是0xFF或0x00这个现象基本可以断定SDRAM根本没有进入正常工作状态。优先排查以下几个点先检查硬件连接看SDNE0有没有拉低RAS、CAS、WE信号有没有接反CKE是不是一直被拉低。如果硬件确认无误再看初始化命令序列有没有完整执行尤其是Load Mode Register那一步很多工程写到这里就跳过了。还有一个高频原因模式寄存器值写错比如CL3结果写成了CL2SDRAM虽然能响应命令但后续读写全部对不上时序。如果以上都没问题就把Refresh Rate先填大一点比如填成0x0FFF排除刷新频率异常导致的初始失败。5.2 运行时数据偶尔被踩、画面花屏这种问题大多数和Cache一致性有关。如果你已经配置了MPU non-cacheable但还是花屏先检查MPU Region是否真的生效。MPU要设置PRIVILEGED_DEFAULT1或者明确使能Region不然配置了等于没配。我建议在main函数里加一句uint32_t mpu_status HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);然后通过调试器查看MPU_RASR寄存器确认Region的TEX、C、B位符合预期。排除Cache问题后再考虑电源和信号完整性。SDRAM工作频率越高对电源纹波和走线阻抗越敏感。H743核心板加杜邦线飞线接SDRAM模组的时候100MHz很容易出偶发错误降到50MHz就稳定很多。批量做板的时候尽量保证SDRAM的VDD去耦电容靠近芯片引脚CLK走线短且少打过孔。5.3 时序参数再往后还能怎么调跑稳100MHz之后如果还想追求更高性能可以尝试把HCLK降到240MHz但SDCLK分频3得到80MHz的SDRAM时钟或者保持HCLK200MHz、SDCLK100MHz然后把Burst模式打开。这里给一条调优路径先把Burst Length从1改成8同时FMC的Read Burst使能Read Pipe Delay调整为2。跑一遍4.2的测试如果稳定再尝试把CAS Latency从3降到2看数据手册是否支持同时需要同步修改模式寄存器和CubeMX里的CAS配置。每次只改一个变量跑完整片错误测试再动下一个不要同时改多个参数否则出了问题根本不知道是谁引起的。最后再分享一个小技巧W9825G6KH-6这类SDRAM调试时PCB上留一组测试点把SDCLK、SDNE0、RAS、CAS、DQ0这几个关键信号引出来。稳定性出问题时用示波器看SDCLK的过冲和边沿完整性很多偶发问题一眼就能定位。我在量过波形之后发现CLK走线过长导致振铃调整布线后一边通过。这个经验不一定所有人都会遇到但真遇到的时候能省下好几个晚上。