1. 项目缘起为什么要在XMC上移植CoreMark最近在折腾英飞凌的XMC系列MCU手头正好有个项目需要对不同型号的XMC进行性能摸底。光看数据手册上的主频和DMIPS/MHz数据总觉得有点虚毕竟实际运行效率还跟编译器优化、内存访问速度、外设配置等一大堆因素有关。这时候一个标准化的、公认的CPU性能基准测试程序就显得尤为重要了。CoreMark作为EEMBC嵌入式微处理器基准评测协会推出的一个经典、轻量级的CPU性能基准测试自然就成了我的首选。CoreMark的核心价值在于它提供了一个统一的“标尺”。它不依赖于任何特定的硬件外设如屏幕、网络只专注于测试处理器核心在常见编程模式下的性能比如列表处理、矩阵操作、状态机等。这样我就可以公平地比较不同XMC型号比如XMC1300, XMC4500之间或者同一型号在不同编译器、不同优化等级下的真实性能差异。这对于项目前期的芯片选型、代码优化方向的判断都极具参考意义。然而官方提供的CoreMark源码包只是一个“裸”的C程序它需要一个“移植层”来适配目标平台。这个移植层主要干三件事提供精确的计时器、实现一个基本的输出函数用来打印结果、以及正确配置系统的时钟和内存。对于XMC来说英飞凌主推的Dave App开发环境我用的版本是4.3.2提供了强大的代码生成和配置能力用它来搭建这个移植层既能保证底层驱动的正确性又能让整个过程变得清晰、可复现。这就是本次“XMC系列实验之十五Dave 4.3.2 移植CoreMark”的由来。我的目标不仅仅是把CoreMark跑起来更是要弄明白在Dave框架下如何干净、标准地完成这类基础算法的移植为后续更复杂的应用比如从热搜词里看到的FreeRTOS、LWIP、LVGL等打下坚实的基础。2. 环境准备与Dave工程创建工欲善其事必先利其器。在开始移植之前我们需要把开发环境和工程框架搭建好。这个过程虽然基础但很多坑往往就埋在这里。2.1 Dave IDE 4.3.2 的安装与基本认知首先确保你安装的是Dave 4.3.2。这个版本虽然不算最新但对XMC系列的支持非常成熟稳定。安装过程没什么特别的跟着向导走就行。安装完成后你需要理解Dave IDE的几个核心概念这能帮你更好地理解我们后续的操作Apps这是Dave的核心。一个“App”就是一个针对特定外设或功能如UART、时钟配置、PWM的软件模块。Dave提供了图形化配置界面你点点鼠标配置参数它就能生成高质量、经过验证的底层驱动代码。我们移植CoreMark所需的计时器和串口输出就会用到对应的App。Dave工程结构当你新建一个工程时Dave会自动生成一个结构清晰的文件夹。你会看到Libraries存放Dave内核和芯片支持包、Generated存放所有由Dave Apps生成的代码千万不要手动修改这里的文件、以及Sources存放用户自己的应用代码如main.c和我们要移植的CoreMark。我们的移植工作主要就是在Sources目录下进行。编译工具链Dave默认集成GCC for ARM工具链。对于CoreMark这种对性能极其敏感的程序编译器优化选项至关重要。我们后续会详细探讨。2.2 创建基础工程与时钟配置启动Dave选择“New Dave Project”。根据你手头的XMC开发板型号选择正确的芯片比如我使用的是XMC4500 Relax Kit就选择XMC4500-F100x1024。工程模板选择最简单的“Empty Dave CE Project”因为我们不需要任何现成的示例代码要从零开始构建。工程创建后第一件也是最重要的事就是配置系统时钟。CoreMark的得分“Iterations/Sec”直接依赖于系统主频的准确性。如果时钟配置错了跑出来的分数将毫无意义。打开“Dave Apps”视图在“System”分类下找到“CLOCK_XMC4” App对于XMC1系列可能是“CLOCK_XMC1”把它拖到你的项目配置区。双击这个App图标进行配置。这里需要根据你的硬件实际情况来设置。以XMC4500内部时钟为例fPLL: 这是系统主频的来源。通常我们会配置PLL将外部晶振如12MHz倍频到120MHzXMC4500的最高主频。确保你配置的fPLL值是正确的。MCLK (CPU Clock): 应设置为与fPLL相同的频率即120MHz。fSYSCLK: 系统时钟通常也与MCLK一致。配置完成后Dave会在Generated目录下生成CLOCK_XMC4.c/h文件并在main()函数调用SystemCoreClockUpdate()函数前自动调用CLOCK_XMC4_Init()。务必确认SystemCoreClock这个全局变量的值与你配置的MCLK频率一致。你可以在main()函数开始时打印一下这个值来验证。2.3 添加串口输出支持UART APPCoreMark需要将测试结果打印出来我们最常用的方式就是通过串口UART输出到PC端的串口助手。在Dave中添加UART支持非常方便。在“Dave Apps”的“Communication”分类下找到“UART” App拖入配置区。配置UART参数选择正确的引脚例如XMC4500 Relax Kit的调试串口是P1.5 (TX)和P1.4 (RX)波特率设置为115200数据位8停止位1无校验。这里有一个关键点CoreMark的移植层要求实现一个ee_printf函数用于格式化输出。Dave的UART App提供了阻塞发送函数UART_Transmit。我们需要在用户代码区Sources目录下自己实现一个简单的、基于UART_Transmit的ee_printf。通常我们会利用标准库的vsprintf进行格式化然后将格式化后的字符串通过UART发送出去。记得在工程设置中开启USE_FULL_ASSERT并包含stdio.h以支持vsprintf。至此一个具备正确时钟和基础调试输出的Dave工程骨架就搭建完成了。接下来我们就要请出今天的主角——CoreMark源码。3. CoreMark源码解析与文件结构梳理从EEMBC官网下载CoreMark的源码包通常是一个zip文件。解压后你会发现里面文件不少但核心的只有几个。理解每个文件的作用是成功移植的关键。3.1 核心源码文件说明core_list_join.c/core_list_join.h包含链表操作相关的测试代码。这部分主要考察指针操作和内存访问。core_matrix.c/core_matrix.h包含矩阵乘法相关的测试代码。这部分对CPU的算术逻辑单元ALU和寄存器压力较大。core_state.c/core_state.h实现了一个小的状态机用于测试分支预测和控制流性能。core_util.c/core_util.h包含一些工具函数如CRC16计算用于验证算法执行结果的正确性。core_main.c这是CoreMark的主程序。它组织了上述三个测试算法的执行流程控制迭代次数计算最终分数并调用ee_printf输出结果。这个文件我们通常不需要修改。core_portme.c/core_portme.h这就是移植层所有与平台相关的代码都集中在这里。我们需要修改的几乎全部是这两个文件。core_portme.h定义了各种宏如时钟频率、迭代次数、对齐方式等。core_portme.c则包含了几个必须实现的函数如计时器初始化、开始/结束计时、输出函数等。coremark.h总头文件包含了一些通用的类型定义和宏。3.2 移植层core_portme.h关键宏定义解析打开core_portme.h我们需要根据XMC平台修改以下关键宏COMPILER_VERSION,COMPILER_FLAGS,MEM_LOCATION这些是信息性宏用于在结果中标识你的编译环境。如实填写即可例如#define COMPILER_VERSION gcc-arm-none-eabi 10.3.1 #define COMPILER_FLAGS -O3 -mcpucortex-m4 -mthumb -mfloat-abihard -mfpufpv4-sp-d16 // 根据你的优化选项填写 #define MEM_LOCATION STACK // CoreMark的数据通常分配在栈上CORETIMETYPE定义计时器使用的数据类型。由于我们使用SysTick一个24位递减计数器作为时钟源它通常是一个32位无符号整数。定义为typedef unsigned int CORETIMETYPE;。GETMYTIME(_t)定义如何获取当前时间戳。我们将把它指向SysTick的计数器值获取函数。MYTIMEDIFF(fin, ini)定义计算两个时间戳差值的方法。对于递减计数器需要特别注意处理溢出wrap-around的情况。公式通常是(ini - fin) 0xFFFFFF针对24位计数器。EE_TICKS_PER_SEC这是最重要的宏之一它定义了你所使用的计时器每秒产生多少个“滴答”ticks。如果我们使用SysTick并且将其配置为每1ms产生一次中断即1000 Hz那么这个值就是1000。但注意CoreMark计算分数时依赖于这个值的准确性它必须和你实际的计时器频率严格对应。HAS_FLOAT,HAS_TIME_H对于裸机嵌入式系统通常没有标准的time.h库和浮点打印支持所以定义为0。MAIN_HAS_NOARGC,MAIN_HAS_NORETURN对于嵌入式裸机程序main函数通常没有参数也不返回定义为1。3.3 必须实现的移植函数core_portme.c在core_portme.c中我们需要实现以下几个函数void portable_init(core_portable *p, int *argc, char *argv[])初始化函数。在这里我们应该初始化系统时钟但Dave已经帮我们在main()之前做了、初始化SysTick定时器、初始化UART等。参数p和argv在嵌入式环境中通常不用可以忽略。void start_time(void)在CoreMark主循环开始前调用。我们需要在这里重置或记录一个开始时间戳。对于SysTick我们可以清除计数器并使其开始计数或者简单地记录当前的计数器值。void stop_time(void)在CoreMark主循环结束后调用。记录结束时间戳。CORE_TICKS get_time(void)获取当前的时间戳以EE_TICKS_PER_SEC定义的tick为单位。这个函数会被GETMYTIME宏使用。实现就是直接返回SysTick计数器的当前值。secs_ret time_in_secs(CORE_TICKS ticks)将tick数转换为秒数。公式很简单return (secs_ret)ticks / (secs_ret)EE_TICKS_PER_SEC;。ee_u32 default_num_contexts定义并发上下文数量对于单核裸机程序就是1。ee_u32 coremark_score这个变量会被core_main.c用来存储计算出的分数我们无需修改。此外我们还需要在某个地方比如在portable_init里或单独的文件实现ee_printf函数如前所述基于Dave UART App的发送函数。4. SysTick定时器精准计时的实现与陷阱CoreMark的性能分数是“每秒迭代次数”其计算完全依赖于我们提供的计时器是否精准。在Cortex-M核的MCU上SysTick定时器是首选的精准、低开销的计时源。但在Dave环境下使用它有一些细节需要特别注意。4.1 SysTick的配置与初始化SysTick是ARM Cortex-M内核的一个24位递减计数器通常连接到处理器时钟MCLK。在Dave工程中我们不通过Dave App来配置SysTick因为Dave的调度器或某些App可能会占用SysTick。为了给CoreMark一个干净的计时环境我们选择手动配置。在portable_init函数中我们可以添加如下代码进行SysTick配置#include “XMC4500.h” // 包含设备头文件其中定义了SysTick寄存器 void portable_init(core_portable *p, int *argc, char *argv[]) { (void)p; (void)argc; (void)argv; // 避免未使用参数警告 // 假设系统时钟频率 SystemCoreClock 120000000 (120MHz) // 我们希望 SysTick 每 1ms 产生一个中断即频率为 1000Hz // 重装载值 (SystemCoreClock / 1000) - 1 uint32_t reload_value (SystemCoreClock / 1000) - 1; // 检查重装载值是否超出24位范围 (0xFFFFFF) if (reload_value 0xFFFFFF) { reload_value 0xFFFFFF; // 使用最大可能值 // 此时需要重新计算实际的 EE_TICKS_PER_SEC // 实际 ticks_per_sec SystemCoreClock / (reload_value 1) // 这个值需要在 core_portme.h 中更新并重新编译 } SysTick-LOAD reload_value; // 设置重装载值 SysTick-VAL 0; // 清除当前值 SysTick-CTRL SysTick_CTRL_CLKSOURCE_Msk | // 使用处理器时钟 SysTick_CTRL_ENABLE_Msk; // 使能SysTick但不开启中断 // 注意我们没有开启 SysTick_CTRL_TICKINT_Msk因为我们不需要中断只需要计数器。 }这里的关键是不开启SysTick中断。CoreMark只需要一个自由运行的计数器来测量时间间隔中断会产生不必要的开销影响测试结果的准确性。EE_TICKS_PER_SEC宏此时应设置为1000。4.2 时间差计算的溢出处理SysTick是一个24位递减计数器减到0后会从重装载值LOAD重新开始递减。这意味着计数器值会周期性“溢出”。MYTIMEDIFF宏必须正确处理这种情况。假设我们定义GETMYTIME(_t)为*(_t) SysTick-VAL即获取当前递减计数器的值。由于是递减时间越晚这个值越小除非发生溢出。MYTIMEDIFF(fin, ini)需要计算从ini时刻到fin时刻经过的tick数。正确的计算方法是#define MYTIMEDIFF(fin, ini) ((ini) (fin)) ? ((ini) - (fin)) : ((0xFFFFFF - (fin)) (ini) 1)这个逻辑是如果起始值ini大于等于结束值fin说明没有发生溢出直接相减。如果ini小于fin说明中间发生了至少一次溢出那么差值等于从fin到0的计数加上从重装载值到ini的计数再加1因为从0到重装载值也算一个tick。4.3 一个极易忽略的坑编译器优化与VolatileSysTick-VAL是一个内存映射的寄存器。编译器在优化代码时可能会认为连续读取同一个内存地址的值是不变的从而将其缓存到寄存器中。这会导致我们通过GETMYTIME获取的时间戳不更新造成计时错误。解决方案必须确保对SysTick-VAL的访问是“易变的”volatile。在Dave生成的设备头文件如XMC4500.h中寄存器的定义通常已经包含了volatile关键字。但我们自己在core_portme.c中声明访问它的指针或宏时也要注意。最稳妥的方式是直接使用设备头文件中提供的访问方式或者将GETMYTIME的实现定义为#define GETMYTIME(_t) do { *(_t) *((volatile uint32_t *)0xE000E018); } while (0) // 0xE000E018 是 SysTick-VAL 的地址同时在get_time()函数中返回类型应为volatile CORE_TICKS或者确保函数内部读取寄存器时使用了volatile指针。5. 在Dave工程中集成与编译CoreMark现在我们已经有了修改好的移植层文件接下来就是将它们和CoreMark的核心源码一起加入到Dave工程中并进行编译。5.1 源码文件的组织与添加在你的Dave工程Sources目录下创建一个子文件夹例如coremark。将CoreMark的所有核心源码文件core_list_join.c,core_matrix.c,core_state.c,core_util.c,core_main.c复制到coremark文件夹中。将你修改后的core_portme.c和core_portme.h也放入coremark文件夹。同时将通用的coremark.h也放进去。回到Dave IDE在“Project Explorer”视图中右键点击你的工程下的“Sources”文件夹选择“Add” - “Add Files...”。导航到coremark文件夹选中所有.c和.h文件注意不要包含可能存在的README或makefile将它们添加到工程中。需要设置头文件包含路径。右键点击工程选择“Properties”。在“C/C Build” - “Settings” - “GNU ARM Cross C Compiler” - “Includes”中添加coremark文件夹的路径通常是“${ProjDirPath}/Sources/coremark”。这样编译器才能找到coremark.h等头文件。5.2 修改main.c文件我们的main.c会变得非常简洁因为CoreMark的主循环在core_main.c的main函数里。为了避免冲突我们需要将Dave工程默认的main函数改名。打开main.c将int main(void)函数改名为int coremark_main(void)。在这个函数内部我们只做最必要的硬件初始化和调用CoreMark的入口。实际上CoreMark的移植层初始化portable_init和主测试coremark_main在core_main.c中会处理大部分事情。我们的coremark_main可能看起来像这样#include “DAVE.h” // DAVE™生成的App头文件 #include “coremark.h” int coremark_main(void) { // 1. DAVE™初始化函数它会调用所有已添加App的初始化代码包括CLOCK和UART。 DAVE_Init(); // 2. 可选通过串口打印一个开始消息确认系统已启动。 // 这里可以调用你实现的 ee_printf 的简单版本或者直接使用UART_Transmit。 // 例如uart_send_string(“\r\nXMC CoreMark Test Start...\r\n”); // 3. 调用CoreMark的主函数。注意此函数在core_main.c中定义它内部会调用portable_init。 // 它需要参数但对于嵌入式系统我们传递空或默认值。 int argc 1; char *argv[1] {“coremark”}; // 模拟命令行参数 int result core_main(argc, argv); // core_main定义在core_main.c中 // 4. CoreMark运行结束可以进入休眠或闪烁LED指示完成。 while(1) { // 闪烁LED或保持空闲 } // return 0; // 对于嵌入式系统main函数通常不返回 }最后我们需要告诉链接器程序的入口点。在Dave工程属性中“C/C Build” - “Settings” - “GNU ARM Cross C Linker” - “General”确保“Entry Point”设置为Reset_Handler这是Cortex-M启动文件的默认入口。程序的执行流程是启动文件 -Reset_Handler-SystemInit时钟初始化-__main运行时库初始化-main。由于我们改名了需要修改启动文件或链接器脚本吗通常不需要。因为Dave的启动文件最后会调用main()。我们只需要确保coremark_main被正确调用。一个更简单的方法是保持Dave生成的main函数名不变但在这个main函数里直接调用coremark_main的内容或者直接包含core_main.c并调用core_main。为了避免困惑我推荐后一种方式即保留Dave的main.c框架在其main函数中直接调用CoreMark的入口。5.3 编译器优化选项的精细调整CoreMark的分数极度依赖编译器优化。在Dave IDE中优化选项在工程属性的“C/C Build” - “Settings” - “GNU ARM Cross C Compiler” - “Optimization”里设置。优化等级必须使用-O3最高级别的速度优化来获得有竞争力的分数。-O0无优化的分数会低得可怜仅用于调试。特定优化-funroll-loops循环展开对CoreMark这类计算密集型基准测试通常有正面效果。-ffast-math快速数学运算放宽浮点精度规则以提升速度。但CoreMark主要用整数运算这个影响不大。注意-Og优化调试体验不适合性能测试。架构特定选项务必与你的芯片匹配。例如XMC4500是Cortex-M4F应包含-mcpucortex-m4-mthumb-mfloat-abihard(如果使用FPU)-mfpufpv4-sp-d16链接时优化LTO可以尝试开启-flto。这会在链接阶段进行跨模块优化有时能带来额外性能提升但可能会增加编译时间。配置完成后编译工程。确保没有错误和警告一些关于未使用参数的警告可以忽略。如果遇到ee_printf中vsprintf链接错误需要在工程设置中勾选“Use newlib-nano”或类似选项以包含标准输入输出库的特定版本。6. 运行测试、结果解读与性能分析编译成功下载程序到XMC开发板。打开串口助手如Putty、Tera Term配置正确的COM口和波特率115200复位开发板。你应该能看到CoreMark的运行输出。6.1 典型输出与结果解读一个成功的CoreMark运行输出大致如下2K performance run parameters for coremark. CoreMark Size : 666 Total ticks : 30000 Total time (secs): 30.000000 Iterations/Sec : 333.333333 Iterations : 10000 Compiler version : GCC10.3.1 20210824 Compiler flags : -O3 -mcpucortex-m4 ... Memory location : STACK seedcrc : 0xe9f5 [0]crclist : 0xe714 [0]crcmatrix : 0x1fd7 [0]crcstate : 0x8e3a [0]crcfinal : 0x33ff Correct operation validated. See README.md for run and reporting rules. CoreMark 1.0 : 333.333333 / GCC10.3.1 20210824 -O3 -mcpucortex-m4 ... / STACK我们需要关注几个关键数据Iterations/Sec这是最终的CoreMark分数。本例中为333.33。它表示每秒完成了333.33次完整的CoreMark测试迭代。Total time (secs)总运行时间。CoreMark默认会运行至少10秒通过EE_TICKS_PER_SEC和迭代次数控制以确保结果稳定。30秒是常见值。Correct operation validated这是最重要的提示它后面的CRC值crcfinal必须与官方参考值seedcrc匹配或者符合EEMBC规定的CRC校验规则具体规则在CoreMark包里的README.md中。如果校验失败说明你的移植或编译过程有问题结果无效常见失败原因有内存越界、编译器优化破坏了算法逻辑、计时错误导致迭代次数计算不对等。Compiler flags这里记录了你的编译选项是结果可复现的关键。6.2 性能分析你的分数正常吗拿到分数后你可能会问这个分数对于120MHz的Cortex-M4来说正常吗我们可以做一个粗略估算。CoreMark的分数大致与CPU主频和每MHz性能CoreMark/MHz相关。Cortex-M4内核的典型CoreMark/MHz值在3.0到3.5之间具体取决于编译器、内存子系统是否零等待状态、以及是否启用FPU等。理论估算120 MHz * 3.2 CoreMark/MHz ≈ 384 CoreMark。我们的实测结果是333.33。这比理论估算略低但在合理范围内。差异可能来自内存速度如果代码或数据存放在访问速度较慢的Flash中尤其是没有缓存或预取机制会成为瓶颈。XMC4500的Flash通常需要插入等待周期。编译器差异不同的GCC版本、不同的优化选项组合会产生差异。SysTick开销虽然我们没开中断但读取SysTick计数器、计算差值等操作本身也有极小的开销。其他后台活动确保没有其他中断如看门狗、系统定时器在干扰测试。Dave默认生成的工程可能包含一些后台任务需要检查。6.3 优化尝试与对比实验移植成功并得到有效结果后你可以进行一系列对比实验这非常有价值优化等级对比分别用-O0,-O1,-O2,-O3,-Os优化尺寸编译运行记录分数。你会直观看到优化等级对性能的巨大影响。代码位置对比尝试将CoreMark代码从Flash复制到RAM中运行需要修改链接器脚本。RAM的访问速度通常比Flash快这可以帮你判断Flash访问是否是性能瓶颈。分数提升会非常明显。CPU主频对比如果你的板子支持尝试在不同的系统时钟频率下运行测试修改CLOCK_XMC4配置并更新SystemCoreClock和EE_TICKS_PER_SEC。绘制分数-频率曲线观察是否线性增长。不同芯片对比用同样的方法在XMC1300Cortex-M0或XMC4700Cortex-M4F上运行对比同主频下不同内核的性能差异。7. 从CoreMark移植到复杂组件思路延伸成功移植CoreMark绝不仅仅是为了跑个分。更重要的是你掌握了在Dave环境下为XMC芯片移植一个“裸”的、对时间敏感的标准C库或算法的基础方法论。这套方法论可以无缝应用到那些热搜词里的复杂组件上。移植FreeRTOS/ LWIP这些组件同样需要你提供“移植层”porting layer。FreeRTOS需要你实现任务切换的底层汇编、系统节拍定时器正好可以用SysTick但这次要开中断、以及堆栈初始化等。LWIP需要你实现网络驱动以太网MAC或PHY的读写、定时器以及可能的内存管理。CoreMark移植中你对系统时钟、编译环境、工程组织的熟悉是完成这些更大规模移植的基石。移植LVGLLVGL是一个图形库它的移植层需要你实现显示驱动填充帧缓冲区、输入设备驱动触摸屏、按键以及一个提供毫秒级延时的定时器。是不是感觉和CoreMark需要ee_printf和GETMYTIME很像是的模式是相通的理解库的核心需求用目标平台XMCDave Apps的能力去实现这些底层接口。移植其他算法库如AI推理模型就像热搜词里提到的“在keil里面移植ai推理模型”在Dave里也是一样。你需要关注的是模型权重和数据在内存中的布局是否需要对齐、是否使用了CMSIS-NN等针对Cortex-M优化的计算库、以及如何高效地利用可能存在的硬件加速器如XMC4000系列的DSP指令。CoreMark移植让你熟悉了如何将纯计算代码嵌入到Dave工程中并管理其内存和性能。实操心得移植任何外部代码到Dave环境最核心的思维转变是——“Dave App负责硬件用户代码负责逻辑”。首先用Dave Apps图形化配置好芯片的时钟、引脚、外设UART, SPI, I2C等。然后将外部库的“硬件依赖层”剥离出来用你已经配置好的Dave App的API去实现它。最后将库的核心逻辑代码通常是与硬件无关的加入到你的Sources目录。这样既能享受Dave配置的便捷和稳定又能灵活引入强大的第三方软件生态。这次CoreMark移植就是一次完美的练兵。