【NUCLEO-H5E5ZJ评测】CoreMark 移植实录:从 CubeMX 到 700.89 分
[TOC]
本文记录我在 NUCLEO-H5E5ZJ 开发板上移植 CoreMark 的完整过程:从查阅数据手册(DS)和原理图确认关键参数,到创建 CubeMX 工程、命令行 CMake 构建、printf 串口重定向,再到 CoreMark 的移植与多轮优化,最终在 250MHz 主频下跑出 700.89 CoreMark(2.80 CoreMark/MHz)的成绩。
过程中踩了不少坑——包括板卡 HSE 默认频率与实际晶振不符导致首次时钟方案翻车、首次构建大面积缺文件(链接脚本 MEMORY 为空、缺 startup 文件——根因是 H5 支持包未安装,正确解法是先安装支持包、再创建项目,见 2.1)等,都一并在文中记录。
本文全程使用命令行工具链(STM32CubeCLT 1.22 + CMake + Ninja + GCC 14.3.1),配合 STM32CubeMX 6.18.1 完成配置与代码生成。完整工程代码见文末链接。
一、准备工作
1.1 CoreMark 简介
来自 CoreMark 官方页面的解释:
CoreMark is a simple, yet sophisticated benchmark that is designed specifically to test the functionality of a processor core. Running CoreMark produces a single-number score allowing users to make quick comparisons between processors.
翻译一下:
CoreMark 是一个简单而又精密的基准测试程序,专门为测试处理器核功能而设计。运行 CoreMark 会产生一个"单个数字"的分数,允许用户在不同 CPU 之间进行快速比较。
STM32H5E5ZJ 的数据手册首页就标注了官方跑分:1023 CoreMark(4.092 CoreMark/MHz)。这就是我们要挑战的目标。
1.2 主控MCU最高主频
查阅数据手册(DS):
- Arm Cortex-M33 内核,最高频率 250 MHz(VOS0 电压档,
PWR_REGULATOR_VOLTAGE_SCALE0)
- 4 MB Flash、1.5 MB RAM(SRAM1 256K + SRAM2 128K + SRAM3 384K + SRAM4 384K + SRAM5 384K)
- 官方 CoreMark 成绩:1023 分(4.092 CoreMark/MHz)
- VCO 输出范围:宽范围 128~560 MHz(来自参考手册 RM0517,配置 PLL 时会用到)
1.3 ST-LINK虚拟串口
要跑 CoreMark 并输出结果,先得解决"串口输出到 PC"的问题。NUCLEO-H5E5ZJ 板载 ST-LINK-V3EC 调试器,自带虚拟串口(VCP)。查阅原理图 MCU IOs 页:

可以确认 ST-LINK 与主控 MCU 的连接关系为:
| ST-LINK 侧信号 |
MCU 引脚 |
MCU 复用功能 |
| DBGIN.VCP_TX |
PD8 |
USART3_TX(boot 串口) |
| DBGIN.VCP_RX |
PD9 |
USART3_RX |
| DBGIN.SWDIO |
PA13 |
SWDIO |
| DBGIN.SWCLK |
PA14 |
SWCLK |
| DBGIN.SWO |
PB3 |
SWO |
| DBGIN.JTDI |
PA15 |
JTDI |
再查数据手册确认 PD8 的复用功能表,USART3_TX(boot) 明确在列——这说明 PD8/PD9 除了是 VCP 串口,还是 ST 出厂的 bootloader 串口,选它做 printf 输出最合适。
调试方面,PA13(SWDIO)/PA14(SWCLK) 是 SWD 两线调试的最小集合,CubeMX 默认 SYS 调试使能后即可用。
1.4 CubeMX 与 CubeCLT
本文用到两个 ST 官方工具,分工明确:CubeMX 负责"生成工程",CubeCLT 负责"编译烧录"。开工前先装好,后面各章直接用。
STM32CubeMX(图形化配置与代码生成器)
- 是什么 / 为什么要装:ST 官方的图形化配置工具,用于选型 MCU 或开发板、配置引脚外设与时钟树、选择中间件,最后一键生成工程骨架(初始化代码 + 构建系统)。没有它,外设初始化、时钟树计算、链接脚本、CMake 工程都得手写。
- 本文用它干什么:第二章创建 NUCLEO-H5E5ZJ 板级工程(ioc 文件)、把时钟树配到 250MHz、配置 USART3、生成 CMake 工程;后续每次调整配置(如 ICACHE 改 2-way)也都在 ioc 里改再重新生成。
- 安装提醒:从 ST 官网下载安装包(自带 Java 运行环境,无需另装 JRE),本文使用 6.18.1。装完第一件事:打开 Package Manager 安装对应 MCU 系列的固件支持包——本文是 STM32Cube FW_H5 V1.7.0。支持包是 CubeMX 生成代码的"原料库"(CMSIS 设备文件、HAL 驱动、板级 BSP 全在里面),它的完整性直接决定生成质量:不先安装就建工程,生成会缺 startup、缺 HAL 文件、链接脚本存储器映射为空——这正是本文踩过的最大的坑,详见 2.1。
STM32CubeCLT(命令行工具链全家桶)
- 是什么 / 为什么要装:ST 官方打包的命令行开发工具集(Cross-compiler Toolchain),一次安装配齐:Arm GNU Toolchain for STM32(arm-none-eabi-gcc,本文 14.3.Rel1)、CMake、Ninja、STM32CubeProgrammer 命令行版(STM32_Programmer_CLI,烧录)、ST-LINK GDB Server,外加 ST Arm Clang(st-arm-clang,LLVM 系编译器,第 5.6 节会用到)。装它就不用分别下载 GCC、CMake、Ninja、烧录器再逐个配 PATH。
- 本文用它干什么:
cmake --preset Release / cmake --build 命令行构建(2.4 节)、STM32_Programmer_CLI SWD 烧录(3.3 节)、st-arm-clang 构建(5.6 节);第二章排查时钟问题时还用它从 SWD 读 RCC 寄存器。
- 安装提醒:ST 官网下载,默认安装到
D:\ST\STM32CubeCLT_<版本>,本文 1.22。安装时建议全选组件;装完后确认各工具的 bin 目录已在 PATH(安装器可自动添加,或使用开始菜单里的 "STM32CubeCLT Environment" 命令行入口),命令行能直接跑 cmake、ninja、STM32_Programmer_CLI 即可。
二、项目骨架
这一章把工程从零建起来:CubeMX 创建板级工程(ioc 配置)→ 生成 CMake 代码 → 命令行构建通过。跑通这三步,开发环境就算验证完毕。
2.1 CubeMX配置要点
在 CubeMX 中创建 NUCLEO-H5E5ZJ 板级工程(Board Selector 搜索 H5E5ZJ),需要配置的核心内容如下:
- 调试口:SYS → Serial Wire(PA13/PA14 被占用为 SWDIO/SWCLK);
- USART3:PD8=USART3_TX、PD9=USART3_RX,异步模式,**115200-8-N-1**;
- 时钟:HSE 48MHz → PLL1(M=12, N=125, P=2)→ SYSCLK 250MHz。注意板卡 ioc 的 HSE 默认频率是 25MHz、与板上实际晶振不符,直接用会翻车——后果见下方坑点总结,配置步骤见 2.2;
- ICACHE:直接映射模式(后文优化时改为 2-way);
- Project Manager:Toolchain/IDE 选 CMake。
配置坑点总结——创建工程前先核对两项:
- 必须先安装 H5 支持包:CubeMX → Package Manager 安装 STM32Cube FW_H5 V1.7.0(若用 GitHub 克隆固件包,记得补齐全部 git 子模块)。未安装的后果:生成的工程缺 startup、缺 HAL 文件、链接脚本 MEMORY 为空,编译直接失败;
- HSE 输入频率默认 25MHz,板上实际晶振是 48MHz(X2 @ PH0/PH1):照默认值配 PLL 会实际跑在 480MHz 超频档,芯片僵死、串口无输出。按 2.2 的 48÷12×125÷2 配置即可。
2.2 时钟源配置
正确配置分两步,都在 CubeMX 里完成。
第一步:启用 HSE 晶振。 Pinout & Configuration 页左侧 System Core → RCC,把 High Speed Clock (HSE) 从 Disable 改为 Crystal/Ceramic Resonator——PH0/PH1 会自动分配为 RCC_OSC_IN/RCC_OSC_OUT:

第二步:配时钟树。 切到 Clock Configuration 页,按下面顺序设置(关键:输入频率框默认是 25,必须手动改成 48):
- HSE 输入频率框:48 MHz;
- PLL Source Mux 单选 HSE;
- PLLM = 12(48÷12 = 4MHz,满足 VCO 输入 2~16MHz 宽范围要求);
- PLLN = 125(4×125 = 500MHz,满足 VCO 输出 128~560MHz 宽范围要求);
- PLLP = 2(500÷2 = 250MHz);
- System Clock Mux 选 PLLCLK,SYSCLK 显示 250MHz 即成功。

时钟树全部变蓝(无红色冲突)后生成代码,SystemClock_Config()(Core/Src/main.c)就是我们要的样子:
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE0); /* VOS0,250MHz 的前提 */
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLL1_SOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 12;
RCC_OscInitStruct.PLL.PLLN = 125;
RCC_OscInitStruct.PLL.PLLP = 2;
...
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
...
HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5);
VOS0 电压档与 FLASH_LATENCY_5 都由 CubeMX 按 250MHz 自动生成,无需手工干预。
2.3 代码生成配置
CubeMX 的 Project Manager 中把 Toolchain/IDE 切换为 CMake 后生成。生成的工程结构:
NUCLEO-H5E5ZJ-CoreMark/
├── CMakeLists.txt # 顶层 CMake(用户可自由修改)
├── CMakePresets.json # Debug/Release 预设(Ninja 生成器)
├── STM32H5E5xx.ld # GCC 链接脚本
├── cmake/
│ ├── gcc-arm-none-eabi.cmake
│ └── stm32cubemx/CMakeLists.txt # CubeMX 生成的源文件清单
├── Core/
│ ├── Inc/ # main.h / usart.h / gpio.h / icache.h ...
│ └── Src/ # main.c / usart.c / gpio.c / icache.c / syscalls.c ...
└── Drivers/ # CMSIS + HAL(固件包链接)
注意 CMakePresets.json 已经定义好了 Debug/Release 两个预设,直接用 cmake --preset Release 配置即可。
2.4 CMake构建验证
构建命令两行(构建成功的截图,注意最后打印的内存占用):
cmake --preset Release
cmake --build build\Release

工具链信息:CMake 3.26.4 + Ninja 1.11.1 + Arm GNU Toolchain 14.3.Rel1(GCC 14.3.1)。
至此项目骨架就绪、构建链路验证完毕。
三、串口链路
这一章打通固件 printf 到 PC 的整条数据链路。数据从 MCU 固件到 pyserial 窗口,实际经过了这样一长串环节:
固件 printf()
→ C 库(newlib-nano)_write 系统调用 [MCU 内,syscalls.c 弱符号]
→ __io_putchar(逐字符) [MCU 内,用户实现,USER CODE 区]
→ HAL_UART_Transmit() 写 USART3 发送寄存器 [MCU 内,HAL]
→ PD8 引脚输出 UART 电平(115200-8N1) [PCB 走线]
→ 板载 ST-LINK-V3EC 的 VCP_RX 引脚 [板载调试器 MCU 接收]
→ ST-LINK 固件把字节打包成 USB CDC 数据 [ST-LINK MCU]
→ USB Type-C 线 [物理连接]
→ Windows 枚举出虚拟串口 "STLink Virtual COM Port"(本机 COM29)
→ pyserial 打开 COM29 读取 [PC 侧]
反方向(PC → MCU)同理:pyserial 写 COM29 → USB CDC → ST-LINK → PD9(USART3_RX)→ 固件读 USART3。注意这条链路上有两个"串口":MCU 侧是真实的 UART(波特率 115200 是它的限制),PC 侧是 USB 虚拟串口(USB 带宽远高于 115200,瓶颈永远在 UART 段)。理解了这条链路,串口没输出时就能分段定位:固件卡死?PD8 没波形?ST-LINK 固件没桥接?驱动没枚举?
3.1 printf 重定向
CubeMX 生成的 syscalls.c 已提供 _write 弱符号,只需在 Core/Src/usart.c 末尾的 USER CODE 区添加 __io_putchar:
/* USER CODE BEGIN 1 */
#ifdef __GNUC__
/* GCC 工具链 printf 重定向:syscalls.c 中弱定义的 _write 会逐字符调用 __io_putchar */
int __io_putchar(int ch)
{
if (HAL_UART_Transmit(&huart3, (uint8_t *)&ch, 1, HAL_MAX_DELAY) != HAL_OK)
{
return -1;
}
return ch;
}
#endif
/* USER CODE END 1 */
所有改动都放在 USER CODE BEGIN/END 区域内,之后无论怎么重新生成代码都不会丢失。
3.2 printf 测试
main.c 的 USER CODE 区:
/* USER CODE BEGIN 2 */
printf("\r\n[NUCLEO-H5E5ZJ] System Clock: %lu Hz\r\n", SystemCoreClock);
printf("[NUCLEO-H5E5ZJ] CoreMark build (USART3 VCP 115200-8N1)\r\n");
/* USER CODE END 2 */
/* USER CODE BEGIN WHILE */
int count = 0;
while (1)
{
printf("[NUCLEO-H5E5ZJ] hello %d\r\n", ++count);
HAL_GPIO_TogglePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin);
HAL_Delay(1000);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
主循环之前先打印当前系统时钟频率(SystemCoreClock),便于确认时钟配置真实生效。
3.3 命令行烧录
STM32_Programmer_CLI.exe -c port=SWD mode=Normal -w build\Release\NUCLEO-H5E5ZJ-CoreMark.elf -hardRst
输出确认连接正常:
ST-LINK SN : 002400333335511035383531
Board : NUCLEO-H5E5ZJ
Voltage : 3.28V
SWD freq : 8000 KHz
Device ID : 0x47A
Device name : STM32H5F4xJ/H5F5xJ/H5E4xJ/H5E5xJ
Device CPU : Cortex-M33
3.4 pyserial 验证
先 pip 安装:python -m pip install pyserial。读取脚本(完整见仓库 tools/serial_read.py):
import serial, serial.tools.list_ports, time
def find_stlink_port():
for p in serial.tools.list_ports.comports():
if p.description and "STLink" in p.description:
return p.device
with serial.Serial(port, 115200, timeout=1) as ser:
ser.reset_input_buffer()
end = time.time() + dur
while time.time() < end:
line = ser.readline().decode(errors="replace").rstrip()
if line:
print(line)
先开监听、再复位(否则会错过开机横幅),成功看到:
[NUCLEO-H5E5ZJ] System Clock: 250000000 Hz
[NUCLEO-H5E5ZJ] CoreMark build (USART3 VCP 115200-8N1)
[NUCLEO-H5E5ZJ] hello 1
[NUCLEO-H5E5ZJ] hello 2
...

串口每秒打印一行,绿灯同步闪烁——MCU 时钟、串口、GPIO 全部正常,数据链路全通。
四、CoreMark 移植
4.1 添加源码
CoreMark 代码仓:https://github.com/eembc/coremark
把核心的 7 个文件复制到 Core/Src/:
core_list_join.c core_main.c core_matrix.c core_state.c
core_util.c coremark.h core_portme.c core_portme.h
并在 CMakeLists.txt 把它们加入构建(顺带加上浮点 printf 支持):
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
# Add user sources here
Core/Src/core_list_join.c
Core/Src/core_main.c
Core/Src/core_matrix.c
Core/Src/core_state.c
Core/Src/core_util.c
Core/Src/core_portme.c
)
# 使 newlib-nano 的 printf 支持浮点数输出(CoreMark 结果为浮点数)
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE
-u _printf_float
)
4.2 修改 core_portme.c
替换计时实现为 HAL 的 Tick(1ms 分辨率):
#include "stm32h5xx_hal.h"
#define CORETIMETYPE uint32_t
#define GETMYTIME(_t) (*_t = HAL_GetTick())
#define EE_TICKS_PER_SEC (1000)
4.3 修改 core_portme.h
#define ITERATIONS 12000 /* 保证运行至少 10 秒(规则要求) */
#define FLAGS_STR "-Ofast" /* 与实际编译选项一致,输出时原样打印 */
#define MAIN_HAS_NOARGC 1
#define MAIN_HAS_NORETURN 1
#define MEM_METHOD MEM_STATIC /* 关键:默认的 MEM_STACK 会爆栈 */
这里有个隐蔽的坑:core_portme.h 默认的 MEM_METHOD 是 MEM_STACK——CoreMark 会在栈上分配数据结构。而 CubeMX 默认工程栈只有 0x400(1KB),直接爆栈 HardFault、串口静默卡死。改成 MEM_STATIC(静态数组,RAM 1.5MB 空间充裕)即可。如果坚持用 MEM_STACK,记得把 ioc 里的 StackSize 调大到 0x1000 以上。
4.4 修改 main.c
core_main.c 里自带 main(),与 CubeMX 生成的 main() 冲突,参考官方移植说明把它的 main 改名为 core_main(),然后在我们的 main 里调用:
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include "coremark.h"
/* USER CODE END Includes */
...
printf("[NUCLEO-H5E5ZJ] CoreMark build (USART3 VCP 115200-8N1)\r\n");
core_main();
五、性能优化
5.1 首跑成绩
Release 默认 -Os,首跑成绩:
CoreMark 1.0 : 537.778973 / GCC14.3.1 20250623 -Os / STACK
(输出里的 STACK 是 core_portme.h 里 MEM_LOCATION 的显示标签,改用 MEM_STATIC 后我把它同步改成了 STATIC。)
对照官方 1023 分差距明显,开始优化。
5.2 编译选项
Release 的 -Os 对性能敏感代码太保守。在 CMakeLists.txt 覆盖:
# CoreMark 跑分优化:-Ofast 覆盖 Release 默认的 -Os
target_compile_options(${CMAKE_PROJECT_NAME} PRIVATE
-Ofast
)
同时同步 FLAGS_STR "-Ofast"。成绩:700.89(+30.3%)。
5.3 cache 实验
STM32H5 的 cache 是独立的 IP(不像 M7 集成在核内):
- ICACHE(16KB 指令缓存):CubeMX 默认生成 1-way 直接映射,改为 2 路组相联(
Core/Src/icache.c 中 ICACHE_1WAY → ICACHE_2WAYS);
- DCACHE(8KB 数据缓存):CubeMX 未生成,在
main.c 的 USER CODE 区直接操作寄存器使能(注意先开外设时钟!):
/* USER CODE BEGIN 2 */
/* 启用 DCACHE(H5 系列 DCACHE 为独立 IP,CubeMX 本工程未生成其初始化,这里直接操作寄存器) */
SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_DCACHE1EN); /* 打开 DCACHE 外设时钟 */
(void)READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_DCACHE1EN); /* 时钟使能后的读回延迟 */
DCACHE1->CR |= DCACHE_CR_CACHEINV; /* 失效整个缓存 */
DCACHE1->CR |= DCACHE_CR_EN; /* 使能 DCACHE */
while (DCACHE1->SR & DCACHE_SR_BUSYF) { /* 等待失效完成 */ }
/* 开启 Flash 预取,降低 250MHz 下取指等待造成的流水停顿 */
SET_BIT(FLASH->ACR, FLASH_ACR_PRFTEN);
第一次加 DCACHE 时忘了开 RCC_AHB1ENR_DCACHE1EN 时钟,寄存器写不进去直接 HardFault,串口无输出——用 CubeProgrammer 读回寄存器才定位到。
实测结果:以上三项加起来对分数几乎没有影响(700.89 纹丝不动)。 说明对 CoreMark 这种紧凑循环,ICACHE 命中率已经很高,数据集(666 字节)也远小于 DCACHE,瓶颈不在取指/数据缓存上。
5.4 负优化实验
实验一:CoreMark 代码搬进 RAM 执行。 通过链接脚本的文件名匹配把 core_*.c 的代码段重定向到 RAM,main() 里先 memcpy 再跑分:
.ramfunc :
{
__ramfunc_start__ = .;
*core_list_join.c.obj*(.text .text*)
*core_main.c.obj*(.text .text*)
...
} > RAM AT> FLASH
__ramfunc_load__ = LOADADDR(.ramfunc);
__ramfunc_size__ = SIZEOF(.ramfunc);
结果 675.98 分,不升反降(-3.6%)。结论:这颗芯片的 Flash 读取加速器(ART)+ ICACHE 供指已经没有等待,RAM 执行省下的取指时间抵不过复制与访问路径的差异。这个结论反过来说明:在 H5E5 上不值得为跑分做"代码搬运 RAM"这类高成本改造。
实验二:LTO 链接时优化。 -flto 全量重编:698.45 分,与 700.89 在噪声范围内,无收益。
5.5 最终成绩
| 配置 |
分数 |
CoreMark/MHz |
| -Os + ICACHE 1-way |
537.78 |
2.15 |
| -Ofast + ICACHE 2-way + DCACHE + 预取(最终版) |
700.89 |
2.80 |
| 上述 + RAM 执行 |
675.98 |
2.70 |
| 上述 + LTO |
698.45 |
2.79 |
最终串口输出:

2K performance run parameters for coremark.
CoreMark Size : 666
Total ticks : 17121
Total time (secs): 17.121000
Iterations/Sec : 700.893639
Iterations : 12000
Compiler version : GCC14.3.1 20250623
Compiler flags : -Ofast
Memory location : STATIC
seedcrc : 0xe9f5
[0]crclist : 0xe714
[0]crcmatrix : 0x1fd7
[0]crcstate : 0x8e3a
[0]crcfinal : 0xd340
Correct operation validated. See README.md for run and reporting rules.
CoreMark 1.0 : 700.893639 / GCC14.3.1 20250623 -Ofast / STATIC
CRC 全部校验通过,结果有效。
5.6 换编译器
CubeMX 生成的 CMake 工程里其实自带了两套工具链文件:gcc-arm-none-eabi.cmake 和 starm-clang.cmake(ST Arm Clang,基于 LLVM)。前面所有成绩都是 GCC 的,这一节用 st-arm-clang 构建同一工程做对比。
它随 STM32CubeCLT 1.22 安装(见 1.4),版本为 ST Arm Clang 21.1.1(LLVM 21),默认搭配 picolibc C 库(工具链文件里 STARM_TOOLCHAIN_CONFIG 可选 HYBRID/NEWLIB/PICOLIBC 三种配置,默认 PICOLIBC)。CMake 预设里没有 clang 预设,手动指定工具链文件配置一个独立构建目录(不影响 GCC 的 build/Release):
cmake -S . -B build/clang -G Ninja -DCMAKE_BUILD_TYPE=Release ^
-DCMAKE_TOOLCHAIN_FILE=cmake/starm-clang.cmake
cmake --build build/clang
构建一遍通过,产物 33544 B,比 GCC 的 40984 B 小了约 18%。
代码修改(三处,均做成 clang/GCC 自适应,GCC 构建行为不变):
main.c 主循环前打印编译器名称和版本:
/* USER CODE BEGIN WHILE */
printf("\r\n[NUCLEO-H5E5ZJ] System Clock: %lu Hz\r\n", SystemCoreClock);
#if defined(__clang__)
printf("[NUCLEO-H5E5ZJ] Compiler: st-arm-clang %s\r\n", __clang_version__);
#elif defined(__GNUC__)
printf("[NUCLEO-H5E5ZJ] Compiler: arm-none-eabi-gcc %s\r\n", __VERSION__);
#endif
core_portme.h 里 CoreMark 自报的编译器版本/编译选项原来硬编码了 "GCC" 前缀(clang 也会定义 __GNUC__,会输出错误的编译器版本串),按编译器分流:
#ifndef COMPILER_VERSION
#if defined(__clang__)
#define COMPILER_VERSION "st-arm-clang "__clang_version__
#elif defined(__GNUC__)
#define COMPILER_VERSION "GCC"__VERSION__
#else
#define COMPILER_VERSION "Please put compiler version here (e.g. gcc 4.1)"
#endif
#endif
#ifndef COMPILER_FLAGS
#if defined(__clang__)
#define COMPILER_FLAGS "-O3 -ffast-math" /* st-arm-clang 21 已移除 -Ofast */
#else
#define COMPILER_FLAGS \
FLAGS_STR
#endif
#endif
CMakeLists.txt 按编译器分流浮点 printf 与优化选项:
# newlib-nano(GCC)的 printf 浮点支持需拉入 _printf_float 符号;
# st-arm-clang 默认用 picolibc,浮点 printf 内建,无该符号
if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE
-u _printf_float
)
endif()
# CoreMark 跑分优化:覆盖各工具链 Release 默认优化(GCC -Os / clang -Oz)
# clang 21 已移除 -Ofast,等价改用 -O3 -ffast-math
if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
target_compile_options(${CMAKE_PROJECT_NAME} PRIVATE
-Ofast
)
else()
target_compile_options(${CMAKE_PROJECT_NAME} PRIVATE
-O3 -ffast-math
)
endif()
三个适配点的原因:
-u _printf_float 是 newlib-nano 的浮点 printf 开关(拉入 _printf_float 符号);picolibc 的浮点 printf 是内建的,库中根本没有这个符号,不加分流直接链接报 undefined reference;
-Ofast 在 clang 21 中已被移除,改用等价的 -O3 -ffast-math;CoreMark 的结果校验是整数 CRC,浮点只是计时换算,fast-math 不影响结果有效性(本次 CRC 全部通过);
- st-arm-clang 的 Release 默认优化是
-Oz(clang 的工具链文件),上面的 -O3 -ffast-math 追加在其后覆盖之,与 GCC 侧 -Ofast 覆盖 -Os 同理。
串口输出(st-arm-clang 版):
[NUCLEO-H5E5ZJ] System Clock: 250000000 Hz
[NUCLEO-H5E5ZJ] Compiler: st-arm-clang 21.1.1
[NUCLEO-H5E5ZJ] CoreMark starting...
2K performance run parameters for coremark.
CoreMark Size : 666
Total ticks : 16959
Total time (secs): 16.959000
Iterations/Sec : 707.588891
Iterations : 12000
Compiler version : st-arm-clang 21.1.1
Compiler flags : -O3 -ffast-math
Memory location : STATIC
seedcrc : 0xe9f5
[0]crclist : 0xe714
[0]crcmatrix : 0x1fd7
[0]crcstate : 0x8e3a
[0]crcfinal : 0xd340
Correct operation validated. See README.md for run and reporting rules.
CoreMark 1.0 : 707.588891 / st-arm-clang 21.1.1 -O3 -ffast-math / STATIC
与 GCC 对比(同为 250MHz、MEM_STATIC、12000 次迭代,CRC 全部校验通过):
|
GCC 14.3.1 |
st-arm-clang 21.1.1 |
| 优化选项 |
-Ofast |
-O3 -ffast-math |
| C 库 |
newlib-nano |
picolibc |
| CoreMark |
700.89 |
707.59 |
| CoreMark/MHz |
2.80 |
2.83 |
| FLASH 占用 |
40984 B |
33544 B(-18%) |
结论:两者基本持平(+0.96%,在跑分波动范围内),但 st-arm-clang 的固件体积明显更小(一部分功劳属于 picolibc)。免费工具链里的红利就到这里了——离官方 IAR 的 1023 分仍差约 31%,这条鸿沟只能用 IAR 来填。
六、结果讨论
700.89 分(2.80 CoreMark/MHz)是什么水平?
- 对照官方数据手册标注的 1023 分(4.092/MHz):约为官方的 68.5%。需要注意官方成绩通常用 IAR 商用编译器测得,GCC 与 IAR 在 CoreMark 上普遍有 20%~40% 的差距,这一点在我上一篇 STM32H7S78-DK 的 CoreMark 测评里也一样;
- 对照 250MHz Cortex-M33 + GCC 的公开跑分:2.8~3.0 CoreMark/MHz 属于正常发挥水平;
- 优化过程中确认了一个重要事实:H5E5 的 Flash 访问加速(ART/预取)+ ICACHE 已经把取指等待压到零,否则 RAM 执行实验不可能不涨反跌。芯片的存储子系统设计是称职的。
如果追求极限分数,剩下的路只有换 IAR 了——免费的 ArmClang 这条路本文已经替你踩过:CubeMX 生成的工程里预留了 starm-clang.cmake(ST Arm Clang 工具链文件),实测 707.59 分,与 GCC 基本持平,适配过程与对比数据见 5.6。
本文完成了 NUCLEO-H5E5ZJ 的 CoreMark 从零移植到跑分优化的全过程,要点回顾:
- 资料先行:DS 确认 250MHz 上限与官方跑分基准;原理图确认 ST-LINK VCP 接的是 USART3(PD8/PD9),SWD 为 PA13/PA14;
- 板卡 HSE 默认值是陷阱:板上实际有 48MHz HSE 晶振(X2 @ PH0/PH1),但板卡 ioc 默认 HSE_VALUE=25MHz 与之不符,按默认值配 PLL 会实际跑到 480MHz 超频僵死;正确方案 HSE 48MHz → PLL(M=12,N=125,P=2)→ 250MHz(注意点见 2.1,配置见 2.2);读 RCC 寄存器排查时记得用 CubeProgrammer 的 HotPlug 模式(默认模式读到的是复位值);
- 首次构建"空 MEMORY 链接脚本 + 缺 startup"的根因是 H5 支持包不完整(固件包子模块没拉全),不是 CubeMX 的缺陷——先安装支持包、再创建项目即可一遍跑通(见 2.1);
- printf 重定向只需在 usart.c 的 USER CODE 区加
__io_putchar,浮点输出加 -u _printf_float;
- CoreMark 移植三件套:HAL_GetTick 计时、MEM_METHOD 改 STATIC(默认 STACK 会爆 1KB 栈)、main() 改名 core_main();
- 优化收益主要来自编译选项(-Os → -Ofast,+30%),cache 配置在 CoreMark 这种负载下已不是瓶颈,RAM 执行与 LTO 均无收益;换 st-arm-clang 21.1.1(LLVM + picolibc)为 707.59,与 GCC 基本持平但固件小 18%(见 5.6);
- 最终 700.89 分(2.80 CoreMark/MHz),为官方 IAR 成绩的 68.5%,符合 GCC 的正常水平。
七、参考资料
本文完整工程代码(ioc + CubeMX 生成的 CMake 工程 + CoreMark 移植):
https://gitcode.com/xusiwei1236/STM32H5E5-CoreMark
仓库以 git 管理,本文各阶段——初始工程、时钟方案修正(HSI→HSE)、CoreMark 移植、-Ofast 优化、st-arm-clang 支持——的完整变更历史都在仓库提交记录中,可逐一回溯。
参考链接:
- CoreMark 官方页面:https://www.eembc.org/coremark/
- CoreMark 源代码仓:https://github.com/eembc/coremark
- STM32CubeH5 固件包(GitHub):https://github.com/STMicroelectronics/STM32CubeH5
- STM32H5E5ZJ 数据手册(DS)
- RM0517 参考手册(PLL 约束:输入 2
16MHz,宽范围 VCO 128560MHz)
- NUCLEO-H5E5ZJ 原理图(ST-LINK VCP 与 USART3 连接关系)