【NUCLEO-H5E5ZJ评测】M.2存储扩展实测
系列第 2 篇。上一篇《CoreMark移植实录》在板上跑通了 CPU 性能基线,这一篇给板子"插上一块 64MB 外置闪存"——用 ST 官方的 M.2 存储套件,把 XSPI 八线总线的真实性能、稳定工作边界、甚至"从外部 NOR 执行代码"的代价,全部用数据说话。
一、准备工作
1.1 存储套件简介
本文使用的扩展硬件是 ST 的 B-M2MEM-PACK1 套件——五块 M.2 串行存储子板,覆盖不同厂商、容量、电压与总线方案:
| 板卡 |
器件 |
类型 |
容量 |
电压 |
最高时钟 |
总线 |
| MB1927-33BA |
Macronix MX25LM51245G |
Octal NOR |
512 Mbit(64 MB) |
3.3 V |
200 MHz |
1/8 线 STR+DTR |
| MB1927-18BA |
Macronix MX25UW25645G |
Octal NOR |
256 Mbit |
1.8 V |
200 MHz |
1/8 线 STR+DTR |
| MB1928-33LA |
ISSI IS25LP032D |
Quad NOR |
32 Mbit |
3.3 V |
133 MHz |
1/2/4 线 STR |
| MB1928-33LB |
Winbond W25Q16JV |
Quad NOR |
16 Mbit |
3.3 V |
133 MHz |
1/2/4 线 STR |
| MB1928-18LA |
ST M95P32 |
EEPROM |
32 Mbit |
1.8 V |
80 MHz |
1 线 STR |
每块板都带 128Kbit I2C 识别 EEPROM、一颗由主控 GPIO 控制开关的 LDO(负责存储器的上电/断电复位)、红绿双 LED 和可供自行换料的空焊盘。
本文只测 MB1927-33BA,选它的理由很直接:
- NUCLEO-H5E5ZJ 的 M.2 插槽 VDDIO 固定 3.3V,两块 18xx(1.8V)板不能直接用——模组上的 LDO 只转换存储器供电,信号线没有电平转换,详见 1.2 节的原理图查证;
- 它是套件里唯一支持**八线 + DTR(双沿采样)**的模组,是五块中理论读写性能最高的(8 位总线 @200MHz,理论带宽远超 Quad NOR 的 66 MB/s);
- 容量最大(64MB),足够玩文件系统镜像和代码外置。
一个容易被搞混的点:512 Mbit = 64 MB。存储行业标容量用 Mbit,写吞吐用 MB/s,单位差 8 倍,读数据时留意。
1.2 M.2 接口定义
NUCLEO-H5E5ZJ 板背面有一颗 M.2 Key A 连接器(CN19),专用于外扩串行存储,接到片上 XSPI1 接口。查 UM3601 的引脚表可以得到完整映射(八线一组信号全齐,含 DQS 和 NCLK):
| 功能 |
M.2 引脚 |
MCU 引脚 |
| CLK / NCLK |
41 / 43 |
PF10 / (NCLK 板上已引出,DTR 模式用 DQS 采样,例程未配置) |
| DQS |
25 |
PB2 |
| NCS |
20 |
PB10 |
| D0~D7 |
35/37/43…67 |
PD11、PD12、PF7、PD13、PE7、PD5、PD6、PC0 |
| LDO_EN |
18 |
PG5 |
| LED1/LED2 |
42 / 38 |
— |
两个必须知道的"坑":
配置坑点总结——上手前先核对两点:
- M.2 Key A 是 ST 私有引脚(规范见 TN1618)。虽然形状上和标准 M.2 设备兼容,但 UM3611 明确警告:不要把第三方 M.2 设备(WiFi 卡之类)插进这个槽,电气定义不同,可能损坏双方硬件;
- 电压必须匹配:33xx 板 3.3V、18xx 板 1.8V,主控侧 IO 电平必须与模组一致。一个常见的疑问是"18xx 板上不是有 LDO 吗?"——翻 MB1927 官方原理图可以看清楚:板载 LDO 把 M.2 送来的 3V3 转成 1.8V,只给存储器芯片供电;OCTOSPI 的 CLK/DQS/IO0-7/NCS 信号线从连接器经串联电阻直连存储器(全板唯一的电平转换器 MAX13046 只管复位和 I2C 侧信号)。于是 18xx 板要求主机 VDD 也是 1.8V:正向 3.3V 驱动超过 1.8V 器件的绝对最大额定(约 2.2V),反向 1.8V 输出又低于 3.3V IO 的 VIH(约 2.31V),双向都不成立。而 NUCLEO-H5E5ZJ 的电源树只有 3.3V 档(UM3601 无 1.8V VDD 选项),所以本板只认 33xx。
1.3 器件特性概览
MX25LM51245G 是 Macronix 的 OctalFlash,关键参数:
- 容量 64MB,页 256B,擦除粒度 4KB/32KB/64KB;
- 两种工作模式:1 线 SPI(上电默认,兼容传统命令集)和 8 线 OPI(STR 最高 133MHz / DTR 最高 200MHz);
- 模式切换靠 Configuration Register 2(CR2)里的 DOPI 位——易失,断电即回到 SPI 模式;
- OPI 读命令
0xEE11(DTR 指令对),20 个 dummy 周期,DQS 采样;
- 换算成理论带宽:8 线 DTR @133MHz ≈ 8bit × 2 沿 × 133M = 266 MB/s 的引脚级上限。
后文的实测会回答:这个 266 MB/s 离实际有多远。
二、硬件连接
2.1 模组安装
断电状态下把 MB1927-33BA 插入板背面 CN19(金手指有方向,Key A 缺口对准),拧上 M.2 固定螺丝即可。无任何跳线——33BA 与主板电压天然匹配。
2.2 XSPI 引脚分配
工程用 STM32CubeMX 配置:Pinout 里把 OCTOSPI1 设为 Octo 模式,按 1.2 节映射把 11 个信号分配到对应引脚(CubeMX 会自动给出 AF10_OCTOSPI1 复用),NCS 用 NCS1。

外设参数页(Connectivity → OCTOSPI1)的配置:

对应生成的关键初始化参数:
hxspi1.Init.MemoryType = HAL_XSPI_MEMTYPE_MACRONIX;
hxspi1.Init.MemorySize = HAL_XSPI_SIZE_512MB; /* 32 位地址模式 */
hxspi1.Init.ChipSelectHighTimeCycle = 10;
hxspi1.Init.ClockPrescaler = 0; /* 内核时钟不分频 */
hxspi1.Init.DelayBlockBypass = HAL_XSPI_DELAY_BLOCK_ON; /* 使能 DLYB 延时块 */
注意 HAL_XSPI_SIZE_512MB 的含义是"32 位寻址",不是容量——OPI 模式下地址固定 32 位,必须这么配。
2.3 时钟配置
CPU 侧沿用系列第一篇的满频配置:板载 HSE 48MHz 晶振经 PLL1(÷M12 ×N125 ÷P2,VCO 500MHz)倍频到 250MHz,内核电压档 VOS0、Flash 等待级 5,AHB 与三条 APB 总线全部同频满速——这套链路决定了 DWT 计时基准和 CPU 轮询类测试的上限。

XSPI1 的内核时钟则不挂在主 PLL 上,走独立的 PLL2:HSE 48MHz ÷ M24 × N100 ÷ R4 = 50MHz 作为初始化频率(器件默认 1 线 SPI 模式下的安全频率),OCTOSPI1 的内核时钟源在时钟树的 Kernel Clocks 里选择 PLL2R。独立时钟链的好处是变频互不牵连:调整 XSPI 频率完全不动 CPU 与总线时钟,4.1 节的频率扫描就靠它在运行中逐档重配 PLL2 并重整定 DLYB。

2.4 上电与复位控制
模组上的存储器由 LDO 供电,LDO 使能脚(PG5)接主控。上电时序有讲究:
/* MX_GPIO_Init 的 USER CODE 区(必须先于 XSPI 初始化执行) */
HAL_GPIO_WritePin(LDO_M2_Slot_GPIO_Port, LDO_M2_Slot_Pin, GPIO_PIN_RESET);
HAL_Delay(10); /* LDO 关断保持——对存储器做一次冷复位 */
HAL_GPIO_WritePin(LDO_M2_Slot_GPIO_Port, LDO_M2_Slot_Pin, GPIO_PIN_SET);
HAL_Delay(200); /* LDO 输出稳定 */
坑点实录:这套"先断电再上电"的冷复位必须放在 XSPI 初始化之前。因为 XSPI 初始化里有一步 DLYB(延时块)标定,需要存储器已经供电、DQS 链路正常。我第一次把 LDO 上电放在了 XSPI 初始化之后,结果初始化就出错。
冷复位还有一个重要用途:器件的 CR2 寄存器(八线模式配置)是易失的。任何时刻想让它回到已知的 SPI 模式,断一次电就行——后文的测试固件里 reset 命令就是这个原理。
三、驱动配置
3.1 XSPI 初始化配置
驱动直接复用 ST 固件包的 MemoryToolbox(Projects/NUCLEO-H5E5ZJ/Examples/XSPI/XSPI_MemoryToolbox_M2_MX25LM51245G),它把 MX25LM51245G 的命令层封装成了现成函数:读 JEDEC/SFDP、读写寄存器、擦除、页编程、间接读、memory-mapped 使能/退出,全部带超时和轮询。memory_toolbox.h 通过宏选择器件(M2_MX25LM51245G),换成套件里其他模组只需换宏和对应的头文件。
初始化阶段的 DLYB 标定(决定 DQS 采样相位)照抄例程:
HAL_XSPI_DLYB_CfgTypeDef cfg = {0};
HAL_XSPI_DLYB_GetClockPeriod(&hxspi1, &cfg); /* 实测输入时钟对应的延时刻度 */
cfg.PhaseSel /= 4; /* 采样点取 1/4 周期 */
HAL_XSPI_DLYB_SetConfig(&hxspi1, &cfg);
3.2 器件识别验证
上电默认是 1 线 SPI 模式,第一件事读 JEDEC ID:
[M2] 1. JEDEC ID (SPI 1-line) : PASS (C2 85 3A)
C2 85 3A 与 Macronix MX25LM51245G 数据手册一致——模组、插槽、走线、命令时序整条链路通了。
3.3 八线模式切换
从 SPI 模式切到 Octal DTR 的完整序列(全部在 1 线 SPI 模式下完成寄存器操作,最后一条命令生效即切换):
- 写 CR(0x15/0x01)把驱动强度提到最高挡(24Ω);
- 写 CR2 @0x300 更新 OPI 读的 dummy 周期;
- 把 XSPI 内核时钟提到 133MHz(PLL2:N=133、R=2),并重配 DLYB;
- 写 CR2 @0x000 置位 DOPI——从这条命令起,器件按 8 线 DTR 应答;
- 用 8 线 DTR 方式读状态寄存器回读验证。
串口输出:
[M2] 3. Octal DTR @133MHz : PASS
坑点实录:第 3 步"先提频、再重配 DLYB、最后切模式"的顺序不能乱。DLYB 的采样相位与频率强相关,例程在 133MHz 下用实测值 PhaseSel=1, Units=10。另外这一套序列在每次冷复位后都要重跑——CR2 断电即失。
3.4 读写通路验证
切到八线 DTR 后做一轮完整通路验证:擦一个 4KB 扇区 → 读回确认全 0xFF → 写一页 256B 伪随机 pattern → 读回比对 → memory-mapped 模式使能 → 用 memcpy 直读外部地址空间比对 → 退出。十步全部 PASS:
=============================================
[NUCLEO-H5E5ZJ-M2] M.2 bench (MB1927-33BA / MX25LM51245G)
MCU: STM32H5E5ZJ @ 250000000 Hz
XSPI kernel clock: 50000000 Hz
[M2] slot LDO on (PG5), memory cold-reset done
[M2] 1. JEDEC ID (SPI 1-line) : PASS (C2 85 3A)
[M2] 2. Write enable check : PASS
[M2] 3. Octal DTR @133MHz : PASS
[M2] 4. Bench scratch ready : PASS
---------------------------------------------
type 'help' for commands
>
到这里,测试台就绪了。固件实现了一个串口命令行(info / reset / dtr / freq / rd / mm / mmr / wr / erase / xip),配一个 pyserial 脚本逐条发送、等 > 提示符同步,后续所有数据都是这样"现场驱动"出来的。

四、性能实测
计时统一用 DWT 周期计数器(250MHz 主频),读测试累计 8MB、写/擦按区域实测,每个稳定档都带 pattern 回读比对。
4.1 频率稳定包络
先回答最关键的问题:这套硬件能跑多快? 用 freq 命令逐档提频(PLL2 动态分频 + DLYB 自整定 + pattern 校验),实测结果:
| XSPI 频率 |
读吞吐(memmap) |
随机页延迟 |
判定 |
| 50 MHz |
1.56 MB/s |
164 µs/页 |
稳定 |
| 66 MHz |
2.01 MB/s |
127 µs/页 |
稳定 |
| 100 MHz |
2.89 MB/s |
88 µs/页 |
稳定(推荐) |
| 133 MHz |
3.68 MB/s |
69 µs/页 |
边缘:boot 稳定性自检 FAIL→复测又 PASS |
| 166 MHz |
— |
— |
总线事务挂死 |
| 200 MHz |
— |
— |
挂死 |
133MHz 是数据手册标称值,但实测处于临界状态:同一份代码、同一频率,boot 时三次 64KB 读校验偶发失败。固件因此做了一个自适应机制——boot 自检 memmap 连续读三遍,不稳就自动降档:
[M2] 5. memmap stability @133000 kHz : FAIL
[bench] DLYB tuned: Units=10 PhaseSel=1
[bench] XSPI kernel: 100000 kHz (PLL2 N=100 R=2, VCO=200000 kHz)
[M2] 5. memmap stability @100000 kHz : PASS
结论:官方标称 133MHz,这块板上稳稳当当能跑的是 100MHz。M.2 连接器 + 板级走线引入的信号完整性代价,数据手册不会告诉你。166MHz 起连事务都完不成(HAL 等标志超时循环),200MHz 同样。
4.2 读吞吐对比
两种读路径、两种粒度,在 100MHz 稳定档下的对比:
| 读方式 |
吞吐 |
64KB 耗时 |
说明 |
| 间接轮询(HAL FIFO 轮询) |
4.30 MB/s |
19.5 ms |
与 XSPI 频率无关 |
| memory-mapped(memcpy 直读) |
2.89 MB/s |
28.9 ms |
与频率成正比 |
| memory-mapped 随机 256B 页 |
88 µs/页 |
— |
延迟视角 |
两个反直觉的现象,根因都在"谁在等谁":
- 间接轮询 4.30 MB/s 恒定不变(50MHz 与 133MHz 一模一样)——瓶颈根本不在总线,而在 CPU:HAL 逐 4 字节轮询 FIFO 标志,每 4 字节花掉约 240 个 CPU 周期。总线越快,CPU 白等越多;
- memory-mapped 吞吐严格正比于频率(1.56 → 2.01 → 2.89 → 3.68 MB/s 对应 50/66/100/133MHz)——CPU 每次从外部地址空间 load 32 位,XSPI 都要重付一整笔事务开销(指令 + 32 位地址 + 模式字节 + 20 dummy ≈ 137 个时钟),数据传输本身只占 16 个时钟。固定开销吃掉了 90% 的带宽。
理论 266 MB/s 与实测个位数 MB/s 之间的差距,就这样一层层摊开了:在 Cortex-M33 上用 CPU 逐字读外置 NOR,事务开销才是天花板。想逼近带宽上限的路径只有两条:让 CPU 一次取更多字节(突发 load/DMA),或干脆用 MDMA 搬运——这是下一篇玩 DMA 时的事了。
4.3 页编程写入
页编程(256B/页,含写使能、命令、轮询 WIP 的完整周期):
[wr] region 64 KB: erase=352 ms, program=50 ms
[wr] program throughput : 1.29 MB/s
约 190 µs/页,折合 1.3~1.4 MB/s。数据手册标称页编程时间(典型 150µs 量级)占比不小,加上每页一次写使能 + WIP 轮询的往返,这就是 NOR 顺序写的真实速率——比读慢,但符合器件特性。
4.4 擦除与摊销
三种粒度的擦除实测(每种 4~16 次取平均,擦后 0xFF 校验全过):
| 粒度 |
平均耗时 |
等效速率 |
| 4KB 扇区 |
22 ms |
183 KB/s |
| 32KB 块 |
237 ms |
137 KB/s |
| 64KB 块 |
245 ms |
266 KB/s |
注意 32KB 与 64KB 的擦除时间几乎一样——大块擦除的耗时主要由器件内部操作决定,能一次擦大块就不要拆成多个小扇区。
把擦除摊进写入,得到"有效写吞吐":写满 1MB ≈ 擦除 3.4s + 编程 0.8s ≈ 0.16 MB/s。这个数字适合拿来对比 SD 卡篇——NOR 的定位是"随机小块读写 + 掉电可靠",不是顺序大容量吞吐。
五、外置执行
5.1 XIP 执行方案
XSPI 支持 memory-mapped,意味着外部 NOR 的内容对 CPU 来说就是普通内存——代码也能放进去原地执行(XIP)。为了量化这件事,固件做了个专门实验:
- 链接脚本增加一个
.xip_code 段:VMA 在 OCTOSPI 地址空间(0x90000000),LMA 在内部 Flash——即编译时链接到外部地址、但镜像随固件存放在内部 Flash;
- 被测函数是一个自包含的 CRC32(叶函数,无外部调用),整段仅 48 字节;
xip 命令流程:擦 NOR 扇区 → 把镜像从 Flash LMA 搬进 NOR → 使能 memory-mapped → 同一段机器码分别以两个地址作为函数指针执行 64KB 数据的 CRC,各跑四轮,ICACHE 开/关各测一组。
/* 链接脚本:代码段 VMA 在外部 NOR,装载镜像存内部 Flash */
.xip_code : { _xip_vma_start = .; KEEP(*(.xip_code)) _xip_vma_end = .; } >XSPI AT>FLASH
/* 双址执行:同一段机器码,两个入口 */
fn_flash = (XipFn_t)((uint32_t)_xip_lma_start | 1U); /* 内部 Flash 副本 */
fn_xip = (XipFn_t)((uint32_t)(OCTOSPI1_BASE + nor_base) | 1U); /* 外部 NOR 原址 */
5.2 同码异址对照
实测结果(100MHz DTR):
| 执行位置 |
ICACHE 关 |
ICACHE 开 |
| 内部 Flash |
195.28 ms |
66.06 ms(3.0× 加速) |
| 外部 NOR XIP |
1226.35 ms |
1226.23 ms |
| NOR 执行代价 |
6.28× |
18.56× |
CRC 四组全等(313C74FE),外部执行的功能正确性没有问题。但性能数据把话说得很清楚:
- ICACHE 关:外部执行慢 6.28 倍——每条指令取指都是一笔 XSPI 事务(137 时钟起步),按位循环每处理 1 字节数据要取 10~20 条指令,取指开销全面碾压执行本身;
- ICACHE 开:内部 Flash 被缓存后快了 3 倍(195→66ms),但外部 NOR 的耗时纹丝不动——实测 ICACHE 不缓存外部 NOR 的取指。这算本次测评最意外的发现:文档上 ICACHE 名义上可覆盖外部存储,实测在这个配置下并没有。
结论一句话:这块外置 NOR 适合放数据、资源、文件系统镜像;XIP 放冷代码可以(省内部 Flash),放热代码等于自降 6~19 倍主频。原计划"把 CoreMark 搬进 NOR 跑分"因此取消——在量级差距面前,跑分没有信息量,函数级对照已经把结论钉死了。
六、结果讨论
6.1 实测数据汇总
MB1927-33BA(MX25LM51245G @ NUCLEO-H5E5ZJ XSPI1,Octal DTR,100MHz 稳定档):
| 项目 |
实测 |
对照 |
| 读(memory-mapped,顺序) |
2.89 MB/s |
引脚理论 200 MB/s @100MHz DTR |
| 读(间接轮询) |
4.30 MB/s |
CPU 轮询瓶颈,与频率无关 |
| 随机 256B 页读延迟 |
88 µs |
含每访问 ~137 时钟固定开销 |
| 页编程(256B) |
1.3~1.4 MB/s |
含写使能与 WIP 轮询 |
| 擦除 4KB / 32KB / 64KB |
22 / 237 / 245 ms |
大块擦除耗时近似 |
| 有效写吞吐(含擦除摊销) |
0.16 MB/s |
1MB 擦+写实测 |
| XIP 执行 |
6.28×(18.56×)慢于内部 Flash |
同码异址,ICACHE 开/关 |
| 稳定频率包络 |
≤100 MHz |
133 边缘,≥166 挂死 |
6.2 选型与使用建议
给打算在这块板上用 M.2 存储扩展的朋友几条落地建议:
- 频率设 100MHz,别贪 133——把 boot 自检(连续大块读校验)写进初始化,不稳自动降档,比 hardcode 一个频率可靠得多;
- 读数据优先 memory-mapped + 大块搬运,小粒度随机读要有延迟预算(百 µs 级);想突破单字 load 的开销天花板,用 MDMA 或让 CPU 发突发访问;
- 写全按页编程走,跨页写必须拆页;擦除按最大可用块来,32KB 和 4KB 耗时差 10 倍但吞吐反而更高;
- 代码放内部 Flash,数据放外部 NOR——除非你想体验 6 倍减速的 XIP;
- 每次冷复位(或掉电)后记住:器件回到 SPI 模式,八线切换序列要重跑。
七、参考资料
- B-M2MEM-PACK1 用户手册(UM3611):模组清单、M.2 Key A 引脚、LDO/LED/焊桥
- NUCLEO-H5E5ZJ 用户手册(UM3601):CN19 → XSPI1 引脚映射
- TN1618:ST M.2 串行存储接口规范(私有引脚警告的出处)
- Macronix MX25LM51245G 数据手册:命令集、时序、CR/CR2 寄存器
- STM32Cube_FW_H5 固件包
XSPI_MemoryToolbox_M2_MX25LM51245G 例程(驱动基座)
- 本篇工程与全部实测数据:
https://gitcode.com/xusiwei1236/STM32H5E5-M2-Code(代码,含阶段化提交记录)与 STM32H5E5-M2-Test(测试记录与串口日志)