ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32N657X0H3外部存储集成:QSPI Flash与SDRAM方案详解

STM32N657X0H3外部存储集成:QSPI Flash与SDRAM方案详解 STM32N657X0H3 这颗芯片拿到手后第一件事不是写 GPIO 翻转而是先把外部存储规划清楚。它的内部 SRAM 虽然不小但一旦开始跑神经网络推理、图像数据搬运、或者多路音频缓冲内存就会立刻变成瓶颈。我这次项目里把 QSPI Flash 和 SDRAM 同时接了上去一个负责存放代码和只读资源一个负责堆运行时的数据池。折腾了两周踩了不少坑也把从 CubeMX 配置到启动代码执行的整条链路摸清楚了。这篇文章就把这套集成方案从硬件到代码完整拆一遍适合正在画板或者已经拿到样片准备调底层驱动的工程师参考。1. 为什么 STM32N657X0H3 必须认真规划外部存储1.1 这颗芯片的内部存储压力来自哪里STM32N657X0H3 属于 STM32N6 系列内核是 Cortex-M55还带了一个专用于神经网络加速的 Neural-ART 单元。这颗 NPU 跑模型的时候输入特征图、中间层数据、输出结果都要在内存里来回倒。虽然芯片内部有 MB 级别的 SRAM但一个稍大的图像分类模型加上多路摄像头输入内部 SRAM 往往只能勉强装下中间结果一旦涉及大尺寸 YUV 图像、拼接后的全景图或者离线渲染缓冲就会直接爆掉。我最初也想过只用内部 SRAM 凑合实际测下来只要 NPU 开启并且持续处理 720p 视频流内存占用轻松超过内部可用 SRAM 的一半。这时候系统一旦再跑个网络协议栈内存申请就会触发堆溢出。所以从设计第一天我就决定必须外置存储而且是两类一起做。QSPI Flash 解决“代码放哪、只读数据放哪”SDRAM 解决“动态数据往哪放、帧缓冲往哪放”。1.2 整体集成方案选型QSPI SDRAM 的组合逻辑外部存储的组合方式其实有几种可选方案比如用 SDIO 接 SD 卡、用 FMC 接并行 NOR Flash、或者上高性能的 HyperRAM。但放在 N657X0H3 这个场景下QSPI Flash 和 SDRAM 的组合是最均衡的。QSPI Flash 的优势在于引脚少、接口速度快支持 Memory Mapped 模式。这意味着芯片可以直接把外部 Flash 映射到内部地址空间直接从 Flash 里取指执行。对于存放启动代码和只读数据来说非常合适比从 SD 卡启动简单得多。SDRAM 则是最成熟、单位容量成本最低的动态内存方案一颗 16 位宽、32MB 的 SDRAM 就能满足大部分图像缓冲需求而且 STM32N6 系列自带的 FMC 控制器原生支持 SDRAM硬件上不需要额外的逻辑转换。我在画板时同时保留了 QSPI 和 SDRAM 的完整接口两个一起用不是为了炫技而是为了让系统具备真正的产品化能力代码可以整个放进 QSPI Flash 实现开机秒启运行时的堆、栈、帧缓冲全部放到 SDRAM内部 SRAM 留给中断处理、DMA 描述符和关键实时数据。选型逻辑清晰之后就要开始处理每个接口的细节。QSPI 看着简单实际上坑不少先拆 QSPI。2. QSPI 接口集成从硬件引脚到代码初始化2.1 引脚分配与硬件布线要点N657X0H3 的 QSPI 接口在 STM32CubeMX 里对应的是 OCTOSPI 外设支持 1.8V 和 3.3V 两种电压域。默认情况下可以配置为标准 SPI 模式也可以配置为 Dual、Quad 甚至 Octal 模式。项目里用的是“QSPI”这个称呼本质上就是 Quad SPI四线数据。布线时四根数据线要尽量等长时钟线到 Flash 的路径要短尤其要注意串阻的匹配。我实际画板时在 CLK 线上串联了一个 22Ω 电阻数据线上各串了一个 33Ω 电阻。这个做法不是为了滤波而是为了抑制信号反射。Flash 芯片离 MCU 很近时可能看不出来问题但一旦板子布局紧张走线绕了弯没有串阻会出现偶发读取出错而且这种错误用示波器很难抓到因为它跟温度、电压都相关。电源滤波同样关键。QSPI Flash 的电源引脚旁边必须放置 0.1μF 和 4.7μF 的电容从 MCU 的 VDD 域单独拉一条线过去不要跟 SDRAM 的电源共用一条粗走线否则 Flash 写入接近完成时的电流尖峰会干扰 SDRAM 的时序。2.2 CubeMX 中的 QSPI 配置步骤在 STM32CubeMX 里配置 QSPI 并不复杂但有几个关键参数必须对准。我以常用的 W25Q256JV 这颗 Flash 为例说说我的配置思路。打开芯片型号后在左侧外设列表里找到 OCTOSPI启用它。时钟源选择外部时钟分频系数根据系统主频计算。我这边主频跑在 600MHzOCTOSPI 时钟源经过分频后把 Flash 的时钟频率控制在 100MHz 左右。W25Q256JV 支持最高 133MHz 的快读但为了稳定性我留了 25% 的余量选择 100MHz 作为实际运行频率量产后还能应对不同批次 Flash 的差异。接口配置方面连接模式选择 Single Data Rate采样模式选择同步读。指令宽度、地址宽度、数据宽度都按 Quad 模式设置。需要特别注意的是刚开始配置时不要一上来就开启 Memory Mapped。先在 Indirect 模式下把擦除、写入、读取都调通再接 Memory Mapped这样排查问题会快很多。配置完成后生成的初始化代码里会看到 HAL_QSPI_Init 和 HAL_QSPI_MemoryMapped 两个函数的调用前者负责基本参数后者负责映射模式配置。如果从没在 CubeMX 里开过 OCTOSPI建议先生成代码跑一个最简单的 Read ID 命令确认硬件链路是通的。2.3 QSPI 驱动初始化与读写验证代码代码层面的第一步是从 Flash 里读回 JEDEC ID。这句代码能验证 SPI 时序、引脚连接、供电是否正常。我通常会用 HAL 库里的几个基础函数来测试但更习惯自己封装一层调用避免后面迁移 Flash 型号时被 HAL 库的参数绑死。/* 头文件里定义 */ #define QSPI_FLASH_ID_EXPECTED 0xEF4019 /* 读 ID 命令 */ uint8_t cmd[4] {0x9F, 0x00, 0x00, 0x00}; uint8_t rxData[4] {0}; HAL_QSPI_CommandTypeDef cmdCfg; cmdCfg.Instruction 0x9F; cmdCfg.InstructionMode QSPI_INSTRUCTION_1_LINE; cmdCfg.AddressSize QSPI_ADDRESS_24_BITS; cmdCfg.AddressMode QSPI_ADDRESS_NONE; cmdCfg.DataMode QSPI_DATA_4_LINES; cmdCfg.DataSize 24; /* 读回 3 字节 ID这里留 4 字节做缓冲 */ cmdCfg.DummyCycles 0; cmdCfg.DummyCycleMode QSPI_DUMMY_CYCLE_NONE; HAL_QSPI_Command(hqspi, cmdCfg, HAL_QPSI_TIMEOUT_DEFAULT_VALUE); HAL_QSPI_Receive(hqspi, rxData, HAL_QPSI_TIMEOUT_DEFAULT_VALUE);读回的三字节分别是 Manufacturer ID、Memory Type 和 Capacity。W25Q256JV 返回的第一字节是 0xEF对应 Winbond。如果读到 0xFF 或者全 0很可能引脚接线有问题或者 Caps 电容没焊好。接下来做整块擦除这个操作时间比较长对容量大的 Flash 可能会超过 30 秒需要注意 HAL 库的超时参数设置。擦除完成后随便写一段数据再读出来比对。写数据前要发写使能命令这个很容易忽略。HAL 库实际上已经把写使能封装在 HAL_QSPI_Command 里了但如果你自己拼命令就必须记得 0x06 命令。/* 写使能 */ uint8_t writeEnableCmd 0x06; HAL_QSPI_Command(hqspi, (HAL_QSPI_CommandTypeDef){ .Instruction writeEnableCmd, .InstructionMode QSPI_INSTRUCTION_1_LINE, .AddressMode QSPI_ADDRESS_NONE, .DataMode QSPI_DATA_NONE, .DummyCycles 0 }, HAL_QPI_TIMEOUT_DEFAULT_VALUE);写使能之后再发送 Page Program 命令一次写入 256 字节。页写操作要求地址偏移不能跨页如果写入数据横跨两页边界就必须拆成两条命令。这个问题在实际项目中经常出现尤其当你直接写一个文件系统镜像进 Flash 时很容易因为地址越界产生静默写失败。我的处理方式是写一个“跨页拆分”的封装函数所有读写都走它避免在应用逻辑里反复处理边界情况。2.4 用 QSPI 加载代码谈谈 Memory Mapped 模式QSPI 真正的价值体现在 Memory Mapped 模式。这个模式下MCU 把外部 Flash 当作内部地址空间的一部分可以直接通过指针读取。对于 STM32N657X0H3QSPI 映射的地址区域通常从 0x70000000 开始具体地址要在参考手册里确认。我这次没有让代码直接整个放到 QSPI 里执行而是把代码放在 Flash 中系统上电后先由内部 BootROM 加载一部分到内部 SRAM 运行然后初始化 QSPI 和 SDRAM再把 app 从 QSPI 拷贝到 SDRAM 运行。为什么不直接 XIPexecute in place因为同步模式下代码逐条从 QSPI 取指执行延迟比 SDRAM 高而且对缓存命中率要求很高一旦出现分支跳转密集的代码性能损失明显。SDRAM 的随机访问延迟低得多把代码丢到 SDRAM 里跑实际体验会更顺。如果只是存放资源文件、模型参数、字体库这类只读数据XIP 完全够用。模型参数本来就是要连续读取的QSPI 映射访问和普通内存访问在逻辑上可以无缝切换。我在加载 NPU 模型时直接让模型数据指向 QSPI 映射地址省掉了一次从 Flash 到 RAM 的整体拷贝。这个做法不但省内存还让启动时间缩短了大半。从应用视角来看QSPI 集成的最终结果就是系统启动后既有一个容量可观的只读存储区域又有一个可以动态读写的代码/数据区。但这只是第一步SDRAM 的集成才是整个系统能不能跑起来的核心。3. SDRAM 集成布线、时序与初始化3.1 FMC SDRAM 控制器基本概念STM32N657X0H3 的 FMC 控制器提供了 SDRAM 接口它把 SDRAM 的控制逻辑全部封装在硬件里。你不需要手动产生行激活、列寻址、预充电等命令只需要在初始化时配置好时序参数然后在内存映射区域里正常读写即可。对于嵌入式开发者来说这跟用内部 SRAM 差不多但前提是底层配置正确。SDRAM 控制器的核心配置项包括列地址位数、行地址位数、bank 数量、CAS 延迟、写恢复时间、行预充电时间、行激活到读/写的时间等。这些参数并不能随意填而是要根据 SDRAM 芯片手册里的 AC 特性表做换算。换算不正确SDRAM 初始化时也许能通过但运行一段时间之后就会出现随机数据错误这是整个系统里最难排查的问题之一。3.2 硬件连接与信号完整性问题SDRAM 的硬件连接比 QSPI 复杂得多。数据线至少 16 根还有地址线、时钟线、控制线。我第一次画两层板想省事结果 SDRAM 跑不到额定频率后来换成四层板才稳定。硬件布局上SDRAM 需要尽量靠近 MCU所有信号线等长控制误差控制在 50mil 以内。数据线组 DQ0-15 和 DQS 要一起走线地址线组和 bank 地址线一起走线尽量避免跨分割区域。SDRAM 的时钟引脚要单独处理我建议直接用 MCU 的 FMC_CLK 引脚而不是自己从普通 GPIO 分频输出因为 FMC_CLK 有专用的延迟补偿电路时序更准。电源方面SDRAM 的 VDD 和 VDDQ 引脚各放一个 100nF 电容容量大一点也更稳妥芯片下方最好能有完整的 GND 平面这样回流路径短信号质量会好很多。如果 SDRAM 旁边还有电机驱动、继电器这类大电流干扰源建议用屏蔽罩或者把 SDRAM 布线区域独立出来。3.3 SDRAM 初始化序列与时序参数计算SDRAM 初始化有一个严格的上电顺序先提供稳定电源等待 100μs 以上然后发送 NOP 命令再发预充电命令连续发两个自动刷新命令设置模式寄存器最后再执行若干次自动刷新。这个流程在 FMC 硬件里已经被支持你在 CubeMX 里填入参数后硬件会自动按顺序执行。但时序参数必须自己算。以常见的 W9825G6KH 为例它的 CAS 延迟有 2 和 3 可选行预充电时间tRP是 20ns行激活时间tRCD也是 20ns。如果你的 FMC 时钟是 100MHz那么每个时钟周期是 10ns。tRP 20ns 就需要 2 个时钟周期。换算成 FMC 配置的数值要写成 2 而不是 20因为控制器内部是以时钟周期为单位计数的。我在 CubeMX 里通常先看 SDRAM 数据手册里的 AC 特性表整理成一个表然后逐个填入参数SDRAM 手册要求FMC 时钟 100MHz 时计算值tRP预充电时间20ns2tRCD激活到读写20ns2CAS Latency33Burst Length1 或 21Write Recovery Time2 或 32这里重点提醒Burst Length 不要用满SDRAM 支持 1/2/4/8 长度但实际使用中设置成 1 或 2 就够了。过长的 Burst 会占用总线导致 DMA 和其他并发访问的总线延迟变大。我这边最终选 1配合固定长寻址性能最稳定。初始化完成后最重要的就是做一次全地址读写测试。不能只读几个字节要跑一个随机地址和全地址的 0xAA、0x55、0x00、0xFF 数据校验最好用 DMA 大量写入再读回比对。这一步能筛掉大部分布线、供电、时序问题。3.4 用代码验证 SDRAM 读写稳定性SDRAM 初始化代码由 CubeMX 生成一般会调用 HAL_SDRAM_Init。但配置完成后我还会写一个自检函数用多种数据模式反复读改写定位是否出现 1-bit 翻转、固定地址位错误、整块刷新失败等问题。uint32_t testPattern[] { 0x00000000, 0xFFFFFFFF, 0x55555555, 0xAAAAAAAA, 0x12345678, 0x87654321, 0xDEADBEEF, 0x0F0F0F0F }; uint32_t errors 0; uint32_t startAddr 0xD0000000; /* 根据 FMC 映射区域调整 */ uint32_t testLen 32 * 1024 * 1024; for (int i 0; i sizeof(testPattern) / sizeof(uint32_t); i) { for (uint32_t offset 0; offset testLen; offset 4) { *((volatile uint32_t *)(startAddr offset)) testPattern[i]; } for (uint32_t offset 0; offset testLen; offset 4) { if (*((volatile uint32_t *)(startAddr offset)) ! testPattern[i]) { errors; } } }这个测试跑完没有错误才说明 SDRAM 基础链路是通的。如果出现固定地址位错误先检查数据线和地址线的映射关系比如 DQ0 是否恰好接到了 SDRAM 的 DQ0而不是接到了 DQ1。这种问题在手工焊接样板时经常出现而且原理图里可能看不出来必须用万用表逐脚核对。如果出现偶发错误基本都是时序参数的问题建议把 CAS Latency 调大一档或者把刷新周期调短一点牺牲性能换稳定。从实际经验来看SDRAM 初始化成功后系统里最大的风险不是“初始化失败”而是“初始化成功了但跑一段时间后死机”。这类问题大多和刷新周期有关。SDRAM 需要定期刷新FMC 控制器里的刷新定时器要配置成 Refresh Rate 64ms / 4096 行 ≈ 15.625μs。如果你用的 SDRAM 是 8192 行这个值就要减半。配置错误导致刷新频率太低芯片的电容电荷会逐渐流失数据就悄悄损坏了。4. 系统级集成调试遇到的坑和排查思路4.1 启动阶段如何把代码跑起来外设都初始化好只是开始真正麻烦的是启动流程。STM32N657X0H3 的启动模式有很多种内部 BootROM 会根据 BOOT 引脚配置决定从哪加载程序。我这次为了方便量产把最终应用代码放在 QSPI Flash启动顺序是这样的芯片上电BootROM 执行配置时钟和基本外设。BootROM 读取 QSPI Flash 的前几个字节判断有没有有效的启动头。将启动头指向的固件镜像拷贝到 SDRAM 指定地址。跳到 SDRAM 中执行main。这个流程的关键在于QSPI 的初始化代码必须在跳转之前执行否则 BootROM 根本不知道从哪里读镜像。ST 提供了配套的 boot loader 示例但直接使用前一定要确认和自己的 Flash 型号匹配。不同 Flash 的读命令、状态寄存器、时序都不同直接套用很容易卡死在“读取启动头”这一步。如果不想依赖 BootROM 的复杂流程也可以让内部 SRAM 中的一段小程序先跑起来再由这段小程序初始化 QSPI 和 SDRAM最后跳转到 SDRAM。这种方式可定制性更强但需要自己写链接脚本把启动代码段放到 SRAM 里。我在链接脚本里的做法是把 startup 代码和SystemInit放在 SRAM 固定地址把主应用链接到 SDRAM 区域。这样上电后 BootROM 直接跳到 SRAM 中的启动代码启动代码把 SDRAM 初始化好然后把主应用从 QSPI 拷贝过来。流程图并不复杂但细节非常多。尤其要留意 SDRAM 地址映射和链接脚本偏移必须完全一致否则跳转后直接 HardFault。4.2 缓存一致性QSPI 和 SDRAM 共享数据时的关键点Cortex-M55 核内置了 ICache 和 DCache这对性能是好事但对外设共享数据来说是个大坑。QSPI 映射区域的数据如果被 DCache 缓存了CPU 读到的可能是旧数据SDRAM 区域被 DMA 写入了新数据但 CPU 的 DCache 里还留着旧缓存这时读出来就是错的。我遇到的一个典型场景是网络摄像头 DMA 直接把一帧图像写入 SDRAMCPU 从 SDRAM 读取做图像处理。第一次运行图像全是花的。排查半天发现是 DCache 未清理CPU 一直在读旧的 SDRAM 数据。解决办法是在 DMA 写入完成后先调用SCB_CleanDCache_by_Addr或SCB_InvalidateDCache_by_Addr把对应地址范围的缓存失效。/* 确定要失效的地址范围 */ uint32_t bufferStart frameBufferAddr; uint32_t bufferSize frameWidth * frameHeight * 2; /* 例如 RGB565 */ SCB_InvalidateDCache_by_Addr((uint32_t *)bufferStart, (int32_t)bufferSize);对于 QSPI 映射区域如果只读不改可以把该区域标注为 cacheable这样读模型参数时性能很不错。但如果 QSPI Flash 会被运行时擦写比如 OTA 升级那在擦写操作之前和之后都要做缓存维护否则可能出现指令缓存继续执行旧代码的极端情况。我在最后一次 OTA 调测时就是因为没有清 ICache升级完成跳转后不停跑旧固件。后来在跳转前加了一个完整 Cache 清理操作问题才消失。缓存策略的正确做法是SDRAM 作为普通内存完全打开 CacheDMA 与 CPU 共享的缓冲区要么配置为 non-cacheable要么在使用前后各做一次 clean/invalidateQSPI 映射区域用于只读数据时使用 cacheable 但标记为 write-through避免修改不可见。4.3 常见问题速查表整合这次调试过程中遇到的问题整理成一张速查表方便后来者直接对照排查。现象可能原因排查与解决办法QSPI 读不到 IDFlash 电源没上、时钟极性/相位错、数据线接反测量电源检查 GPIO 复用功能逐脚核对接线QSPI 写入成功但读回全 0写使能命令缺失、跨页写确认每次写前发写使能处理好页边界QSPI Memory Mapped 访问 HardFault映射未配置、Cache 未使能重映射检查 CubeMX 的 Memory Mapped 参数查看总线矩阵SDRAM 初始化后读写偶发错误刷新率错误、时序参数偏紧计算刷新定时器调大 CAS 延迟SDRAM 数据固定位翻转数据线接错、焊接短路用万用表逐脚核查重新焊接DMA 与 SDRAM 数据不一致DCache 缓存一致性问题清理缓存或配置 non-cacheable 区域BootROM 无法从 QSPI 启动启动头格式错误、Flash 时序不匹配对照 ST 官方 boot loader 源码核查跳转到 SDRAM 后 HardFault链接脚本地址与 FMC 映射不一致核对链接脚本中的 RAM 起点和长度这些坑单独看都不复杂但组合在一起就非常折磨人。比如说SDRAM 时序问题导致的偶发错误很难通过单元测试发现必须靠长时间压力测试。我建议上板之后先跑一个 8 小时以上的循环读写测试再接入实际应用。这个测试能筛掉大部分内存类问题避免后面莫名其妙地死机。4.4 一个容易被忽略的细节启动阶段时钟配置的先后顺序关于 QSPI 和 SDRAM 集成还有一个隐藏的坑值得单独提时钟初始化顺序。N657X0H3 内部 PLL 启动需要时间如果 QSPI 和 SDRAM 的时钟源在 PLL 锁定之前就被配置成高速模式硬件就会一直等待 Lock 标志导致初始化卡死。我遇到的情况是把SystemClock_Config放在 QSPI 初始化之前执行结果系统上电后直接死在 QSPI 的HAL_QSPI_Init里。查了几小时才发现是因为 QSPI 时钟源选择了 PLL而 PLL 还没稳定。解决办法是先配置 PLL等 Lock 到位再初始化 QSPI。其实 CubeMX 默认生成的代码顺序是对的但如果你像我一样喜欢手动裁剪启动代码就很容易踩到这个。还有一个和时钟相关的细节SDRAM 的刷新请求也依赖系统时钟如果系统进入低功耗模式FMC 的刷新时钟有可能被关闭。如果你要在低功耗模式下让 SDRAM 数据不丢失就必须在进入低功耗前把 SDRAM 里的关键数据搬到内部 SRAM或者使用带自刷新功能的 SDRAM。W9825G6KH 是支持自刷新的但需要通过标准命令进入不是说关掉时钟它就自己进了。我在早期原型机里踩过这个坑系统休眠后醒来SDRAM 数据全乱了。这类体验只有在真正跑系统的时候才会意识到。调试这类问题的通用套路是先隔离问题再找根因。不要一上来就怀疑控制器有问题先从供电、时钟、引脚这些最基础的东西查起。调试代码时把初始化函数里每一步的返回值都打印出来实时看卡在哪一步比盲猜高效得多。对我来说STM32N657X0H3 的外设集成并不是最难的部分最难的是把 QSPI、SDRAM、D-Cache、DMA、启动流程这些模块结合起来之后整个系统的行为会变得非常复杂。但一旦你把每一条链路都理清楚用各种测试把基础打牢后面的应用层开发就会非常顺畅。这套存储架构一旦稳定跑起来容量和性能上的优势就能给上层应用提供很大的发挥空间。
返回列表