尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

STM32H743外扩SRAM配置指南:FMC时序与内存管理实战

STM32H743外扩SRAM配置指南:FMC时序与内存管理实战 1. 项目缘起为什么H743还需要外扩SRAM最近在做一个基于STM32H743的图像处理项目算法跑起来后内部SRAM1MB很快就捉襟见肘了。虽然H743内部有1MB的SRAM但其中一部分被系统、堆栈、各种外设缓冲区占用后留给应用层动态内存的空间其实没想象中那么大。特别是当算法需要开辟几个大尺寸的缓冲区比如640x480的RGB图像一张就要近1MB时内存不足的问题就暴露无遗。这时候外扩SRAM就成了一个非常直接有效的解决方案。我选用了IS61WV204816这颗芯片这是一颗16位数据宽度、4Mbit512K x 16bit即1MB容量的异步SRAM。选择它的原因很简单容量适中刚好能弥补内部RAM的不足16位宽与STM32H743的FMCFlexible Memory Controller灵活存储控制器接口匹配度高能获得不错的访问速度而且异步SRAM的时序相对简单配置起来比SDRAM要容易一些。这个项目的核心就是通过STM32CubeMX这个强大的图形化配置工具将这颗外部的IS61WV204816 SRAM正确地挂载到STM32H743的FMC总线上并最终在Keil MDK或IAR这样的IDE中能够像使用内部RAM一样方便地使用这片外部内存。整个过程涉及硬件连接、CubeMX的FMC参数配置、软件层面的内存管理分散加载文件或malloc重定向等多个环节任何一个环节出错都可能导致访问失败。下面我就把整个配置流程和踩过的坑详细地梳理一遍。2. 硬件连接与FMC引脚分配在打开CubeMX之前硬件连接是基础。IS61WV204816是标准的异步SRAM接口包括地址线A0-A18、数据线I/O0-I/O15、控制线/CE, /OE, /WE, /LB, /UB。STM32H743的FMC灵活存储控制器支持多种存储器类型其中就包括我们需要的NOR/PSRAM异步SRAM属于此类。2.1 关键引脚连接表首先你需要根据芯片手册和STM32H743的数据手册将两者的引脚正确连接。下面是一个典型的连接对应表假设使用FMC的Bank 1即NOR/PSRAM 1IS61WV204816 引脚STM32H743 FMC 引脚 (Bank1)功能说明A0-A18FMC_A0-FMC_A18地址总线。注意H743的FMC_A[25:0]可用我们只需要A0-A18因为512K地址需要19根线2^19 524288。I/O0-I/O15FMC_D0-FMC_D1516位数据总线。/CE (Chip Enable)FMC_NE1片选信号。FMC有NE1~NE4对应不同存储区域Bank我们通常用NE1。/OE (Output Enable)FMC_NOE输出使能读使能。/WE (Write Enable)FMC_NWE写使能。/LB (Lower Byte)FMC_NBL0低字节使能对应数据线D0-D7。/UB (Upper Byte)FMC_NBL1高字节使能对应数据线D8-D15。注意/LB和/UB是字节使能信号对于16位操作通常两者同时有效拉低。FMC的NBL0和NBL1正是用来控制高低字节的完美匹配。2.2 硬件设计注意事项上拉电阻SRAM的数据线I/O是双向的理论上可以加上拉电阻如10K以确保空闲时为高电平增强稳定性尤其在布线较长或干扰较大的环境中。但在我的实际项目中由于PCB空间紧凑且走线较短省去了这些上拉工作也正常。电源与去耦确保IS61WV204816的VCC3.3V和STM32H743的VDD电压一致并为SRAM芯片的电源引脚就近放置一个0.1uF的陶瓷去耦电容这对高速信号完整性至关重要。地址线对齐STM32H743是32位CPU但它通过FMC访问16位宽设备时会自动处理地址对齐。这里有一个关键点在软件中我们访问外部SRAM的地址是基于0x60000000Bank1的起始地址的。当我们用指针访问时H743的FMC硬件会自动将CPU发出的地址右移一位因为16位设备按字寻址然后输出到地址线A[18:0]上。这意味着你软件里地址0x60000000对应SRAM的A00地址0x60000002对应SRAM的A01因为地址2是一个16位字的偏移。这一点在理解后续的分散加载文件配置时很重要。硬件连接确认无误后就可以打开STM32CubeMX进行软件配置了。3. CubeMX中FMC的详细配置步骤启动CubeMX选择你的STM32H743型号。首先完成基本的时钟树配置比如主频设置为400MHz然后我们重点看FMC。3.1 激活并配置FMC外设在左侧“Pinout Configuration”标签页的“Connectivity”分类下找到“FMC”。点击它在右侧模式选择中勾选“FMC”。此时CubeMX会自动为你分配FMC所需的大量引脚地址线、数据线、控制线。你应该能在芯片图形界面上看到这些引脚变成绿色。如果某些引脚因为与其他功能冲突而无法分配比如某些引脚默认是JTAG你可能需要在“Pinout”视图手动解决冲突或者更换FMC的Bank例如使用NE2对应的引脚组。接下来在FMC的配置界面我们需要添加一个存储器设备。点击“Add”按钮或“NOR/PSRAM/SRAM/ROM...”旁边的加号。选择存储器类型在“Memory type”下拉框中选择“SRAM”。选择数据宽度在“Data width”中选择“16 bits”。这与IS61WV204816匹配。选择Bank在“Bank”中根据你硬件使用的片选NE信号选择。如果你连接的是FMC_NE1就选“Bank 1 - NOR/PSRAM 1”。这个选择决定了后续访问的基地址。3.2 关键时序参数配置重中之重这是整个配置的核心也是最容易出错的地方。时序参数必须同时满足STM32H743 FMC控制器和IS61WV204816 SRAM芯片两者的要求。我们需要查阅两份数据手册STM32H743参考手册查找FMC章节关于NOR/PSRAM的时序寄存器描述如FMC_BTRx, FMC_BWTRx。IS61WV204816数据手册查找读周期和写周期的时序参数表如tRC,tAA,tOE,tWE,tPWE等。CubeMX的配置界面将这些参数转化为了更直观的“时间”设置单位是纳秒ns。我们需要根据两个芯片中要求更严格即时间更长的那个来设置。假设我的H743运行在200MHzHCLK一个FMC时钟周期就是5ns。IS61WV204816在3.3V电压下典型的读访问时间tAA最大为10ns。下面是一个相对保守且通用的配置示例你可以在理解后根据你的主频和SRAM型号进行微调Common Parameters (通用参数):Memory type: SRAMData width: 16 bitsBurst access mode: Disabled (异步SRAM不支持突发模式)Signal Parameters (信号参数):Address setup time: 这里对应FMC的ADDSET。它表示地址建立到读/写命令有效的时间。对于SRAM这个时间可以很短。我通常设置为1个HCLK周期5ns。在CubeMX里它会根据你输入的ns值自动计算所需的周期数。Address hold time: 对应ADDHLD。地址保持时间。同样可以设短比如0ns0个周期。Data setup time: 对应DATAST。这是最重要的参数之一。对于读操作它表示读使能(NOE)有效后数据在多长时间内必须稳定在数据总线上。这个值必须大于等于SRAM的tOE输出使能有效到数据有效时间。IS61WV204816的tOE典型值也很短。但为了稳定我会设置得宽裕一些比如4个HCLK周期20ns。在CubeMX中你直接输入20ns。Timing Parameters - Read (读时序):Bus turnaround time: 对应BUSTURN。总线转换时间用于防止读写操作间冲突。可以先设为0。CLK division ratio: Disabled (异步模式不用时钟分频)。Data latency: Disabled (异步模式无数据延迟)。Access mode: Mode A (这是NOR/PSRAM的标准访问模式)。Timing Parameters - Write (写时序):Address setup time: 同读时序可以保持一致。Data setup time: 对应写操作的DATAST。表示写使能(NWE)有效后数据需要保持稳定的时间。必须大于SRAM的tPWE写脉冲宽度和tSD数据建立到写结束时间。同样设置一个保守值如4个周期20ns。我的经验是在项目初期为了确保通信成功可以将Data setup time读和写设置得大一些比如30-40ns。等系统稳定后再根据SRAM芯片手册的最小要求值和系统性能需求逐步减小这个值以优化速度。一个配置过快的时序会导致随机读写错误数据校验失败而配置稍慢一些只会影响最高访问带宽但功能是正常的。配置完成后点击“Generate Code”CubeMX会为你生成FMC的初始化代码fmc.c和fmc.h。初始化函数MX_FMC_Init()会在main.c中自动调用它完成了FMC控制器的所有寄存器配置。4. 软件层面如何访问外部SRAM硬件和底层驱动配置好了接下来就是在应用程序中如何使用这片“额外”的1MB内存。主要有两种主流方法一是修改分散加载文件将堆或特定变量直接分配到外部SRAM二是重写_sbrk或malloc相关的函数将动态内存分配导向外部SRAM。4.1 方法一修改分散加载文件Scatter File这是我最推荐的方法因为它将内存布局的控制权完全交给了开发者清晰且高效。以Keil MDK-ARM为例。确定外部SRAM的地址范围我们使用的是FMC Bank1 (NE1)其起始地址是0x6000 0000。IS61WV204816容量是1MB所以地址范围是0x6000 0000到0x600F FFFF1MB 0x100000字节。创建或修改分散加载文件在项目文件夹下创建一个.sct文件例如STM32H743_EXTSRAM.sct并在Keil的“Options for Target” - “Linker”选项卡中取消勾选“Use Memory Layout from Target Dialog”然后指定这个文件。编写分散加载描述文件内容如下LR_IROM1 0x08000000 0x00200000 { ; 加载区域Flash ER_IROM1 0x08000000 0x00200000 { ; 执行区域Flash *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_IRAM1 0x20000000 0x00020000 { ; 内部DTCM RAM (128KB) .ANY (RW ZI) } RW_IRAM2 0x24000000 0x00080000 { ; 内部AXI SRAM (512KB) .ANY (RW ZI) } RW_EXTSRAM 0x60000000 0x00100000 { ; 外部SRAM (1MB) *(.extsram) .ANY (RW ZI) } }关键解释RW_EXTSRAM定义了一个新的执行区域地址从0x60000000开始大小为0x001000001MB。*(.extsram)这一行意味着所有在代码中通过__attribute__((section(.extsram)))修饰的全局变量或数组都会被链接器放到这个区域。.ANY (RW ZI)是一个“兜底”规则。链接器会先将有特定section指定的变量如.extsram分配好然后将剩余的RW读写和ZI零初始化数据分配到.ANY匹配的区域。注意链接器按顺序分配。所以如果把RW_EXTSRAM放在后面并且内部RAM区域RW_IRAM1/2的.ANY已经消耗了所有匹配项那么外部SRAM的.ANY可能就匹配不到东西了。因此一种更可控的做法是不在内部RAM的规则里使用.ANY或者调整顺序确保你想放到外部RAM的数据能被正确匹配。在代码中使用外部SRAM指定变量到外部SRAM// 定义一个大的数组并指定其存放在 .extsram 段 uint8_t large_buffer[1024*1024] __attribute__((section(.extsram))); // 1MB数组这样large_buffer就会被链接器放置到0x60000000开始的外部SRAM中。将堆Heap分配到外部SRAM如果你想让malloc动态分配的内存也来自外部SRAM可以在分散加载文件中专门为堆划分一个区域。RW_EXTSRAM 0x60000000 0x00100000 { .ANY (RW ZI) *(.heap) ; 假设你修改了启动文件将堆段命名为 .heap }但这需要你修改启动文件startup_stm32h743xx.s中的堆栈定义将Heap_Size对应的段名改为.heap并确保链接器能找到。这种方法更底层操作需谨慎。4.2 方法二重写_sbrk函数这是更动态的一种方式不修改链接脚本而是接管C库的内存分配请求。malloc家族函数最终会调用_sbrk来请求扩大堆空间。我们可以重写这个函数让它从一个位于外部SRAM的静态数组中分配内存。在外部SRAM中定义堆池// 在外部SRAM中定义一个大的数组作为堆池 __attribute__((section(.extsram))) static uint8_t ext_heap_pool[1024*768]; // 768KB的堆池 static uint8_t *current_heap_end ext_heap_pool;重写_sbrk函数通常可以在syscalls.c文件中找到这个函数或者自己实现一个。#include errno.h #include stdint.h extern uint8_t _end; // 由链接器提供内部RAM中已初始化数据结束的位置 extern uint8_t _estack; // 栈顶 extern uint8_t ext_heap_pool[]; // 外部堆池起始 extern uint8_t *current_heap_end; void *_sbrk(intptr_t incr) { uint8_t *prev_heap_end; const uint8_t *heap_limit (uint8_t*)_estack; // 内部堆的理论上限通常到栈底 prev_heap_end current_heap_end; // 如果当前堆尾还在内部RAM区域_end附近 if (current_heap_end (uint8_t*)_end) { // 第一次调用_sbrk或者内部堆还没用完这里我们直接切换到外部堆池 // 更完善的实现可以优先用内部堆用完再切外部。这里简单起见直接全用外部。 current_heap_end ext_heap_pool; prev_heap_end current_heap_end; } // 检查分配是否超出外部堆池范围 if (current_heap_end incr ext_heap_pool sizeof(ext_heap_pool)) { errno ENOMEM; return (void*)-1; } current_heap_end incr; return (void*)prev_heap_end; }注意这是一个简化示例。一个健壮的实现需要更精细地管理内部堆和外部堆池可能涉及多个堆池和分配策略。这种方法的好处是所有通过malloc的分配都会自动使用外部SRAM无需修改每个变量属性。缺点是_sbrk的实现与平台和编译器紧密相关需要小心处理。5. 验证与调试如何确认SRAM工作正常配置完成后最激动人心也最紧张的一步就是验证。直接读写可能因为时序不对而得到随机值甚至导致硬件错误HardFault。我建议按以下步骤由简入繁地进行验证。5.1 基础读写测试编写一个简单的测试函数对外部SRAM的固定地址进行连续的写、读、比较操作。#include stdint.h #include string.h #define EXT_SRAM_BASE ((volatile uint16_t*)0x60000000) // 以16位为单位访问 #define EXT_SRAM_SIZE (1024*1024) // 1MB int test_external_sram(void) { uint32_t test_offset 0x1000; // 测试一个偏移地址避免地址0可能存在的特殊问题 uint16_t write_pattern 0xA55A; // 一个易辨认的测试模式 uint16_t read_back; // 单点测试 EXT_SRAM_BASE[test_offset] write_pattern; read_back EXT_SRAM_BASE[test_offset]; if (read_back ! write_pattern) { printf(单点测试失败写入0x%04X 读取0x%04X\r\n, write_pattern, read_back); return -1; } printf(单点测试通过。\r\n); // 块测试测试一小块区域如4KB uint32_t block_size 4096; uint16_t *ptr (uint16_t*)EXT_SRAM_BASE[0]; for (uint32_t i 0; i block_size/2; i) { // 因为uint16_t所以循环次数是字节数的一半 ptr[i] (uint16_t)(i 0xFFFF); // 写入递增模式 } for (uint32_t i 0; i block_size/2; i) { if (ptr[i] ! (uint16_t)(i 0xFFFF)) { printf(块测试失败于地址 0x%08lX。期望0x%04X 实际0x%04X\r\n, (uint32_t)ptr[i], (uint16_t)(i 0xFFFF), ptr[i]); return -2; } } printf(块测试4KB通过。\r\n); return 0; }将这段代码放入main函数初始化FMC后调用。如果单点测试都失败那基本是硬件连接、电源或FMC时序配置有重大问题。5.2 使用内存测试算法March C对于更严格的测试可以使用标准的SRAM测试算法如March C。它能检测地址线粘连、数据线短路、存储单元故障等多种问题。网上可以找到开源实现。如果March C测试通过那么这片SRAM的可靠性就非常高。5.3 在调试器中查看内存这是最直观的方法。在Keil或IAR的调试模式下让程序运行到初始化FMC之后暂停。然后打开“Memory”窗口输入外部SRAM的地址例如0x60000000。手动修改这个地址的值比如写入0x1234然后立刻读取看看是否一致。你还可以在测试函数中设置断点观察变量值。如果读取值全是0xFF或0x00或者写入后立刻读回不一致检查硬件确认电源、地、所有连线无误。用万用表或示波器检查片选NE1、读使能NOE、写使能NWE在访问期间是否有正确的电平跳变。检查时序回到CubeMX大幅增加Data setup time读和写比如增加到100ns甚至200ns。如果此时测试通过说明之前时序太紧。然后逐步减小这个值找到稳定工作的最小边界。检查地址/数据线编写一个“走马灯”测试程序依次让每根地址线或数据线输出高电平用示波器测量对应引脚看是否有信号。这可以排查虚焊或短路。5.4 性能粗略评估测试通过后可以简单评估一下访问速度。在循环中连续读写一大块外部SRAM用定时器测量耗时与操作内部RAM的时间对比。由于FMC是挂在AXI总线上的并且有访问延迟外部异步SRAM的速度通常几十MHz的有效带宽会远低于内部几百MHz的TCM或AXI SRAM。但对于缓存图像、音频帧这类大数据块它仍然绰绰有余。6. 实战中的坑与经验总结最后分享几个我在项目实战中踩过的坑和总结的经验这些在官方文档里不一定找得到。坑一时序配置的“单位”混淆CubeMX的时序参数输入框单位是ns但底层寄存器配置的是HCLK周期数。CubeMX会根据你输入的ns和当前HCLK频率自动换算。关键点在于这个换算不是四舍五入而是ceil向上取整。例如HCLK200MHz周期5ns你输入7ns的Data setup timeCubeMX会计算7ns / 5ns 1.4个周期它会配置为2个周期即10ns。所以你以为配了7ns实际生效是10ns。理解这一点对精确调优时序很重要。坑二分散加载文件中的.ANY陷阱如前所述.ANY指令会匹配所有未被前面更具体规则分配的数据。如果你在内部RAM区域和外部RAM区域都使用了.ANY (RW ZI)链接器会按顺序分配可能导致你本想放到外部RAM的变量被前面的内部RAM区域“截胡”。最佳实践对于明确要放在外部SRAM的大数组或数据结构使用__attribute__((section(自定义段名)))明确指定并在分散加载文件中为该段名创建专属的执行区域。对于堆使用重写_sbrk的方法可能更清晰。坑三未初始化的变量ZI段零初始化ZI变量在启动时会被系统自动清零。如果你将.bss段ZI数据的一部分分配到了外部SRAM你必须确保在main函数执行之前启动代码已经完成了对外部SRAM对应区域的清零操作。标准的启动文件如startup_stm32h743xx.s中的Reset_Handler只会处理链接脚本中定义的内部RAM的ZI段清零。如果你用分散加载文件将ZI数据放到了外部SRAM你需要在初始化FMCMX_FMC_Init()之后手动清零外部SRAM中对应的ZI区域。或者更优雅的方式是修改启动文件在SystemInit之后、main之前调用一个你自己的ExtSram_Init()函数来执行清零。但这涉及修改汇编启动文件有一定难度。坑四缓存Cache一致性问题STM32H743有指令缓存I-Cache和数据缓存D-Cache。FMC访问的外部存储器区域默认是不启用缓存的。但如果你为了提升性能通过MPU内存保护单元将外部SRAM区域配置为可缓存Cacheable那么就必须小心处理缓存一致性问题。例如DMA外设如摄像头接口、SDIO直接读写外部SRAM时CPU缓存里的数据可能就是旧的脏数据需要手动或配置DMA进行缓存维护操作Clean, Invalidate。在项目初期建议保持外部SRAM为非缓存MPU_ACCESS_NOT_CACHEABLE等所有功能稳定后再考虑启用缓存和解决一致性问题。经验将配置参数化我将FMC的时序参数如地址建立时间、数据建立时间定义为宏放在fmc.h头文件中。这样当更换不同速度等级的SRAM芯片或者调整HCLK频率时我只需要修改这几个宏并重新生成代码非常方便。经验预留测试引脚在硬件设计时我会将SRAM的关键控制线如NE1,NOE,NWE通过零欧姆电阻引出到测试点。当调试不通时可以断开电阻用逻辑分析仪或示波器直接测量STM32发出的时序波形与SRAM数据手册的要求对比这是定位硬件时序问题最直接的方法。通过以上步骤你应该能够成功地将IS61WV204816这颗外部SRAM集成到你的STM32H743项目中并稳定地使用这宝贵的1MB额外内存空间。整个过程从硬件连接到软件配置再到验证调试每一步都需要耐心和细致。一旦调通它将成为你应对复杂项目、处理大容量数据的得力助手。
返回列表