
1. 项目缘起为什么嵌入式开发者需要关注CoreMark跑分最近在几个嵌入式开发群里看到不少朋友在讨论ESP32、Arduino Uno R3和STM32F103C8T6也就是大家常说的“蓝板”的性能对比。讨论来讨论去最后往往变成了“我感觉ESP32快”、“我觉得STM32更稳”这种主观感受的争论。这让我想起几年前自己也经历过类似的阶段选型时总有点“盲人摸象”的感觉。后来我开始用CoreMark这个工具来量化评估情况就清晰多了。CoreMark是什么简单说它就是一个专门为嵌入式处理器设计的基准测试程序。它不测你的外设驱动写得好不好也不管你的网络协议栈效率高不高它就聚焦于一件事评估处理器核心CPU Core的纯粹计算能力。它通过运行一系列精心设计的算法如矩阵操作、状态机、链表遍历和CRC计算来模拟一个处理器在运行典型嵌入式控制代码时的表现最终给出一个分数。这个分数越高意味着CPU的“算力”越强。对于嵌入式开发者尤其是当我们面临ESP32、Arduino AVR和STM32这类资源、架构、主频各不相同的平台选型时CoreMark的价值就凸显出来了。它能帮你回答几个很实际的问题我的算法移植过去会不会卡这个芯片处理我的核心业务逻辑够不够快升级到更高主频的型号性能提升是否线性脱离了“感觉”用数据说话选型会更理性性能预估也会更准确。所以这次我就打算亲手做一次“横评”对象就是这三款极具代表性的开发板乐鑫的ESP32-DevKitC双核Xtensa LX6240MHz、Arduino官方的Uno R3ATmega328P16MHz、以及意法半导体的STM32F103C8T6Cortex-M372MHz。我将带大家从环境搭建、代码移植、编译优化一直到实际跑分、结果分析和深度解读完整走一遍流程。你会发现跑分不只是跑个分背后的编译链选择、优化等级设定、内存模型影响每一个细节都藏着学问。2. 测试环境搭建与核心工具链揭秘跑分的第一步不是写代码而是搭环境。不同的芯片需要不同的“武器库”也就是工具链。这一步走对了后面事半功倍走错了可能连程序都编译不过。2.1 三大平台的“编译器之战”对于ESP32乐鑫官方主推的是基于GCC的xtensa-esp32-elf工具链。如果你安装了ESP-IDF乐鑫的物联网开发框架这个工具链会自动集成。它的优势是与ESP-IDF深度绑定对ESP32的双核、Wi-Fi/蓝牙栈、片上外设的支持最完善。我们这次跑分就基于ESP-IDF v5.1环境。在idf.py menuconfig中我们需要关注两个关键设置一是将CPU主频设为240MHz默认二是选择优化等级。CoreMark官方推荐使用-O2优化我们在sdkconfig中将其设置为-O2。对于Arduino Uno (ATmega328P)情况有点特殊。经典的Arduino IDE背后使用的是avr-gcc。但为了获得更精确的CoreMark分数并与其它平台公平对比我决定脱离Arduino IDE的封装直接使用avr-gcc工具链进行裸机编译。这样做能完全掌控优化选项和链接脚本避免Arduino核心库带来的额外开销。你需要安装avrdude和avr-gcc在Linux上可通过包管理器安装Windows可使用MSYS2或WinAVR。编译命令将直接控制优化等级为-O2。对于STM32F103C8T6生态系统非常丰富。我选择的是arm-none-eabi-gcc工具链配合经典的make工程管理。这是ARM Cortex-M系列开发最通用、最透明的方式。通过自定义的Makefile和链接脚本.ld文件我们可以精确控制代码在Flash和RAM中的布局。同样在Makefile的CFLAGS中我们明确指定-O2 -mcpucortex-m3 -mthumb等关键参数。注意为什么强调脱离IDE因为IDE如Arduino IDE、STM32CubeIDE往往做了很多后台工作默认的优化设置、启动文件、库链接可能并不最适合纯粹的CoreMark测试。手动控制工具链能确保测试的“公平性”——大家都尽可能地在“裸奔”状态下比拼CPU核心能力。2.2 CoreMark代码移植的关键“手术”从 EEMBC官网 下载的标准CoreMark代码不能直接在任何一块板上运行。它需要三个关键的移植接口core_portme.c移植层实现、core_portme.h移植层配置和ee_printf输出函数。系统计时器CoreMark需要测量执行时间。ESP32可以使用esp_timer获取高精度微秒时间STM32通常配置SysTick定时器而对于ATmega328P我选择了Timer1因为它是一个16位定时器精度和范围都足够。输出重定向ee_printf需要将结果打印出来。ESP32和STM32可以重定向到串口UART通过USB转串口模块在电脑终端显示。Arduino Uno同样使用硬件串口TX/RX但需要确保在初始化CoreMark之前配置好串口波特率如9600或115200。编译参数传递CoreMark最终会打印出一组“运行参数”包括迭代次数、优化等级等。这些信息需要通过core_portme.h中的宏如COMPILER_FLAGS来定义并传递给编译系统。我们必须确保每个平台都正确设置了这些宏以便结果可追溯。以下是一个为STM32F103移植的core_portme.h关键配置示例#define COMPILER_VERSION gcc 12.2.1 #define COMPILER_FLAGS -O2 -mcpucortex-m3 -mthumb #define MEM_LOCATION STACK #define CORETIMETYPE clock_t #define GETMYTIME(_t) (*_t get_my_time()) #define MYTIMETYPE long long #define EE_TICKS_PER_SEC 1000 // 假设SysTick配置为1ms中断移植工作就像给CoreMark这个“发动机”安装不同的“底盘”和“仪表盘”让它在不同的硬件平台上都能正常启动并报告数据。3. 跑分执行过程与原始数据捕获环境准备好代码移植完接下来就是上电、编译、烧录、看结果。这个过程里每一个操作都可能影响最终分数的有效性。3.1 编译、烧录与运行实录对于ESP32在项目目录下执行idf.py set-target esp32、idf.py build然后idf.py -p /dev/ttyUSB0 flash monitor。monitor参数会直接打开串口监视器。上电后程序运行串口终端会刷出一大堆日志来自ESP-IDF系统最后你会看到CoreMark的标准输出块。对于Arduino Uno使用命令行编译和烧录avr-gcc -mmcuatmega328p -DF_CPU16000000UL -O2 -o coremark.elf coremark.c core_portme.c avr_uart.c avr-objcopy -O ihex -R .eeprom coremark.elf coremark.hex avrdude -c arduino -p m328p -P /dev/ttyACM0 -b 115200 -U flash:w:coremark.hex:i烧录完成后打开一个串口终端工具如screen或minicom连接到对应的串口复位板子就能看到输出。对于STM32通过make编译生成coremark.bin文件然后使用ST-Link V2烧录器配合st-flash工具进行烧录make clean make st-flash write build/coremark.bin 0x08000000同样通过串口终端查看结果。3.2 核心结果解读与初步分析当程序运行完毕你会看到类似下面的输出以ESP32为例2K performance run parameters for coremark. CoreMark Size : 666 Total ticks : 12345678 Total time (secs): 12.345678 Iterations/Sec : 810.371 Iterations : 10000 Compiler version : GCC10.2.0 Compiler flags : -O2 -DESP32 -DHAVE_GETTIMEOFDAY ... Memory location : STACK seedcrc : 0xe9f5 [0]crclist : 0xe714 [0]crcmatrix : 0x1fd7 [0]crcstate : 0x8e3a [0]crcfinal : 0x3a0f Correct operation validated. See README.md for run and reporting rules. CoreMark 1.0 : 810.371 / GCC10.2.0 -O2 -DESP32 -DHAVE_GETTIMEOFDAY ... / STACK最关键的一行就是最后一行CoreMark 1.0 : 810.371 / ...。这里的810.371就是最终的CoreMark分数单位是Iterations/Sec即每秒完成了多少次CoreMark标准迭代。我分别在三块板子上运行多次取稳定后的数值得到如下原始数据开发板MCU 核心主频优化等级CoreMark 分数每MHz分数 (CoreMark/MHz)Arduino Uno R3ATmega328P (AVR)16 MHz-O224.51.53STM32F103C8T6ARM Cortex-M372 MHz-O2108.71.51ESP32-DevKitCXtensa LX6 (双核仅单核参与)240 MHz-O2810.43.38注意ESP32是双核但CoreMark是单线程基准测试。为了公平对比我通过任务绑定xTaskCreatePinnedToCore将其限制在Core 0上运行确保只使用一个核心进行计算。这也是嵌入式跑分中需要注意的一点明确测试条件。看到这个表格第一眼的结论似乎很明显ESP32 STM32 Arduino。但作为一名工程师我们不能只停留在表面数字。分数背后是架构、内存、编译器共同作用的结果。我们需要深入挖掘。4. 数据深度剖析架构、内存与编译器优化的三重奏为什么240MHz的ESP32分数不是16MHz Arduino的简单倍数240/1615倍实际是33倍为什么STM32和Arduino的每MHz分数如此接近这就要深入到微控制器内核架构和内存子系统了。4.1 处理器架构的“代差”效应Arduino Uno (ATmega328P)采用的是古老的8位AVR RISC架构。虽然RISC设计简洁高效但其8位数据通路、有限的寄存器集32个通用寄存器和简单的流水线在处理CoreMark中的32位整数运算尤其是矩阵乘法和链表操作时需要多条指令才能完成一个32位操作效率天然低下。它的高性能模式16MHz已经接近其设计极限。STM32F103 (Cortex-M3)是32位的ARMv7-M架构。它拥有32位数据通路、硬件乘法器、更深的流水线、以及Thumb-2指令集。Thumb-2指令集在代码密度和性能间取得了绝佳平衡一条指令就能完成很多工作。这就是为什么在相近的每MHz分数下72MHz的M3能轻松碾压16MHz的AVR这是“32位对8位”的降维打击。ESP32 (Xtensa LX6)的架构更为复杂和现代。它是可配置的Tensilica内核支持非常高的主频240MHz。更重要的是它拥有强大的乱序执行能力、更深的流水线、以及针对数字信号处理DSP和音频编码的硬件加速指令虽然CoreMark未使用。其内存接口通常连接高速SPI RAM的带宽也远高于片内SRAM。这些特性使得它在执行CoreMark这种计算密集型代码时能最大限度地挖掘每个时钟周期的潜力因此每MHz分数高达3.38遥遥领先。4.2 内存访问看不见的性能瓶颈CoreMark的测试集对缓存和内存延迟是敏感的。STM32F103的Flash运行在72MHz下通过预取缓冲和ART加速器但其SRAM访问速度与内核速度同步。ATmega328P的Flash和SRAM访问速度远低于核心频率。ESP32的情况特殊。当代码从片外SPI Flash执行时最常见情况会有明显的取指延迟。为了获得最高性能我在此次测试中将CoreMark代码加载到ESP32的片内SRAMIRAM中运行。这完全避免了Flash访问延迟使得CPU能“饱腹”运行。这是一个非常重要的实操技巧对于ESP32上极其注重实时性的核心算法考虑将其放入IRAM。方法是在ESP-IDF的component.mk中为源文件添加-mforce-l32编译选项或者使用IRAM_ATTR宏定义函数。这能带来显著的性能提升当然代价是占用宝贵的片内RAM。4.3 编译器优化的“魔法”-O2优化等级是CoreMark的标准要求。但不同平台的GCC后端avr-gcc,arm-none-eabi-gcc,xtensa-esp32-elf-gcc进行的优化各有侧重。查看反汇编可以发现arm-none-eabi-gcc对循环展开、强度削弱如用移位代替乘除、内联函数做得非常激进。xtensa-esp32-elf-gcc则充分利用了Xtensa指令集的特性可能生成了更高效的指令序列。而avr-gcc受限于8位架构很多优化施展不开。你可以尝试将优化等级改为-Os优化尺寸或-O3激进优化重新测试。在我的额外测试中STM32在-O3下分数可能提升5%-10%但代码体积会显著增大。ESP32变化不大而Arduino Uno在-O3下甚至可能因为代码过于庞大导致无法运行Flash只有32KB。这告诉我们优化等级不是越高越好需要根据芯片的Flash/RAM资源做权衡。5. 超越跑分CoreMark分数的实际工程指导意义拿到跑分数据我们终于可以回到最初的问题这对我的项目选型意味着什么5.1 选型决策不只是看分数绝对值场景一需要复杂网络协议栈和轻度计算的物联网节点。分析ESP32的810分意味着它有充足的算力冗余去处理TCP/IP、TLS加解密、MQTT协议解析同时还能轻松跑你的业务逻辑。它的高分数直接转化为更强的多任务处理能力。结论ESP32是首选。场景二工业控制多路PWMADC采集强实时性但算法简单。分析STM32F103的108分对于执行PID控制循环、状态机调度绰绰有余。其Cortex-M3内核的中断响应延迟确定外设丰富生态成熟。CoreMark分数确认了其核心性能足够。结论STM32F103更合适性价比高实时性有保障。场景三超低功耗传感器数据记录器99%时间睡眠每秒唤醒一次做一次简单滤波和存储。分析Arduino Uno的24.5分对于简单滤波算法完全够用。此时核心矛盾是功耗。ATmega328P在睡眠模式下的功耗可以做到微安级而ESP32和STM32在深度睡眠下虽然也低但唤醒时间和整体功耗管理复杂度不同。结论如果计算需求真这么低Arduino Uno或其低功耗变体如使用ATmega328P的自主设计板可能是最经济、最简单的选择。CoreMark分数帮你排除了“性能不足”的担忧。5.2 性能预估与瓶颈排查假设你有一个在STM32F103上运行良好的算法现在想移植到ESP32。你可以粗略估算ESP32的CoreMark分数约是STM32的7.5倍810/108。这意味着如果算法是纯CPU计算密集型它在ESP32上的运行时间可能缩短到原来的1/7.5。这为项目进度评估提供了量化依据。反之如果你在ESP32上写了一个算法实测发现比预期慢很多远未达到7.5倍的提升。这时就该警惕了瓶颈可能不在CPU。你需要检查是否频繁进行片外Flash/PSRAM访问这会导致CPU等待。是否涉及大量任务切换或系统调用RTOS开销可能成为主导。算法本身是否存在大量慢速操作如浮点运算ESP32虽有硬件浮点单元但相比整数运算仍慢。STM32F103没有硬件FPU浮点全是软件模拟更是灾难。5.3 CoreMark的局限性它不测什么必须清醒认识到CoreMark的边界避免误用不测浮点性能如果你的应用是DSP、图像处理需要大量浮点运算请使用Dhrystone含浮点版或LINPACK等基准测试。不测外设与I/O性能GPIO翻转速度、ADC采样率、SPI/I2C吞吐量这些需要专门的测试或数据手册参数。不测多核性能标准CoreMark是单线程的。对于ESP32、STM32H7等多核芯片需要并行版测试或自行设计多任务基准。不测实时性中断延迟、任务切换时间这些是RTOS和硬件设计决定的需要用示波器和专业工具测量。所以CoreMark是一个优秀的CPU核心整数计算能力标尺但它不是衡量芯片综合性能的“万能钥匙”。它最适合在项目初期用于筛选掉那些核心算力绝对不足的选项。6. 进阶思考让跑分更贴近真实场景标准CoreMark跑的是“标准负载”。但我们真实的应用负载千差万别。如何让评估更有针对性自定义基准测试Benchmark这是最高级的做法。从你的实际应用代码中提取出最核心、最耗时的算法循环例如一个特定的滤波函数、一个通信协议解析函数、一个电机控制SVPWM算法。用这个函数构造一个类似于CoreMark的测试集在不同的平台上用相同的优化等级编译运行测量其执行时间或迭代速率。例如你的产品有一个核心的“传感器数据融合卡尔曼滤波”函数。你可以编写一个测试让它在一个循环中执行数万次该函数通过定时器测量总时间。分别在ESP32、STM32、Arduino上运行这个测试。这个结果对你选型的指导意义远大于CoreMark分数。因为它直接反映了你的代码在目标硬件上的表现。这个过程本身也是对代码进行性能剖析的过程你可能会发现算法本身的优化空间比如将浮点运算改为定点数运算减少不必要的内存拷贝这比换一个更快的芯片往往更有效、成本更低。最后我想分享一点个人体会性能测试就像给芯片“体检”CoreMark是其中一项重要的“血常规”。它能快速告诉你基本健康状况但无法替代详细的“专科检查”外设测试、功耗测试、实时性测试。在做嵌入式选型时建立一个自己的“芯片评估清单”把CoreMark分数、功耗数据、外设资源、开发成本、生态成熟度都列进去综合打分。这样做出的决策才既理性又扎实能有效避免项目后期的性能危机和成本超支。这次横评的完整代码和工程配置我已经整理好它不仅仅是一个跑分工具更是一个学习如何为不同MCU搭建裸机测试框架的绝佳范例。