本文将演示CoreMark移植到GD32/STM32上进行性能测评。文章末尾给出了GD32F130C8T6、GD32F303CCT6、STM32F103ZET6、Air32F103CBT6这几款单片机的测试对比结果。
访问CoreMark的Github主页,下载如下图所示的源文件。在工程中新建一个子目录CoreMark,并放入其中。在Keil开发环境中,需要对CoreMark源文件(.c)加入到分组中,并指定CoreMark的头文件的包含路径,这些步骤不再赘述。
main函数
CoreMark代码框架定义了main函数,因此我们要屏蔽掉自己写的main函数。在这个mian函数中,首先调用了portable_init()函数,这个函数就是我们移植的时候,实现系统时钟以及串口初始化的地方。
int main(int argc, char *argv[])
{
//省略
/* first call any initializations needed */
portable_init(&(results[0].port), &argc, argv);
//省略
}
栈深度的调整
CoreMark默认使用栈内存实现数据的存储和计算,它会在main函数中定义一个2KB大小的数组。而在单片机的xxx_startup.s汇编启动文件中,对栈的容量限制为1KB,因此必须扩大栈容量,否则会进入HardFault。笔者因为这个问题调试了很久。建议定义为4KB,如下所示。
; Stack Configuration
; Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
;
;Stack_Size EQU 0x00000400
Stack_Size EQU 0x00001000
对串口的适配
CoreMark使用串口将测试信息输出,这样我们就可以在电脑的串口调试助手上看到评分。具体来说,CoreMark使用ee_printf()函数来输出数据。那么我们就要配置串口,并实现ee_printf()函数。
在portable_init()函数中我们实现对串口的配置,并实现ee_printf()函数,如下:
//#include "xxxxx" //MCU指定的头文件
#include
#include
#include
//USART0配置函数
void USART0_config(void)
{
rcu_periph_clock_enable(RCU_GPIOA);
rcu_periph_clock_enable(RCU_USART0);
//PA9 USART0_Tx
gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9);
//PA10 USART0_Rx
gpio_init(GPIOA, GPIO_MODE_IPU, GPIO_OSPEED_50MHZ, GPIO_PIN_10);
usart_deinit(USART0);
usart_baudrate_set(USART0, 115200);
usart_receive_config(USART0, USART_RECEIVE_ENABLE);
usart_transmit_config(USART0, USART_TRANSMIT_ENABLE);
usart_enable(USART0);
}
//实现ee_printf
int ee_printf(const char *fmt, ...)
{
char buf[256];
va_list args;
va_start(args, fmt);
int len = vsnprintf((char*)buf,256,fmt,args); //需要C99支持
va_end(args);
if(len > 0)
{
for(int i=0;iportable_id = 1;
}
由于我们直接实现了ee_printf()函数而不是依赖C标准库中的printf,因此需要在core_portme.h中对将HAS_PRINTF 宏修改为0,并同时声明ee_printf()函数。
#ifndef HAS_PRINTF
#define HAS_PRINTF 0
int ee_printf(const char *fmt, ...) ;
#endif
对SysTick的适配
CoreMark需要使用一个定时器进行计时,它的主要操作流程为:开启定时器,运行性能测试代码片段,停止定时器,计算时间差值,用这个差值来衡量CPU的性能,差值越小说明CPU的计算速度越快。由于SysTick是对于CM3/CM4内核的单片机来说是标配,且配置过程简单,所以本文使用SysTick作为CoreMark的时基。
对SysTick的配置在core_portme.c文件中进行。在这个文件中,CoreMark定义了2个全局变量start_time_val和stop_time_val。每次迭代开始的时候,CoreMark使用start_time()函数来初始化并启动SysTick,并使用start_time_val保存启动SysTick时的初始tick数,由于tick初始值为0,所以start_time_val初始化为0即可。
我们需要自己在这个文件中定义一个全局变量Tick,来记录从定时器启动以来经过的tick数。在本文中,笔者在start_time()函数使用CMSIS定义的SysTick_Config()函数来配置SysTick每1ms中断一次,并在中断函数中递增全局变量Tick。
每次迭代完成后,CoreMark使用stop_time()函数来停止SysTick,并将Tick数存储在全局变量start_time_val中。然后,CoreMark使用get_time()函数来计算得到从定时器启动到结束经过的tick数,然后通过time_in_secs()函数将tick数转换为秒数。为了能让CoreMark正确计算出秒数,我们必须根据自己的定时器配置正确定义EE_TICKS_PER_SEC 的值,这个宏代表了安装我们当前的定时器配置,1秒会产生多少个tick。本文SysTick配置为1ms中断一次,因此定义为1000。CoreMark预定义的get_time()函数和time_in_secs()函数可以不用修改。
//注释原有的EE_TICKS_PER_SEC 的定义,重新定义为1000,也就是每秒钟SysTick中断1000次,产生1000个tick计数
//#define EE_TICKS_PER_SEC (NSECS_PER_SEC / TIMER_RES_DIVIDER)
#define EE_TICKS_PER_SEC 1000 //每隔1ms中断一次,也就是1秒有1000个tick
volatile uint32_t Tick ; //定义SysTick的中断次数计数器,每中断一次,就在中断函数中加1
//SysTick的中断函数
void SysTick_Handler(void)
{
Tick++;
}
//启动定时器并将start_time_val设置为当前的定时器Tick数
void start_time(void)
{
//GETMYTIME(&start_time_val);
Tick = 0; //每次初始化时让tick数从0开始
start_time_val = Tick; //当前tick数保存到全局变量start_time_val
SysTick_Config(SystemCoreClock/1000); //配置并启动SysTick,这里配置Systick每隔1ms中断一次
}
//停止定时器并将stop_time_val设置为当前的定时器Tick数
void stop_time(void)
{
//GETMYTIME(&stop_time_val);
SysTick->CTRL = 0; //停止SysTick
stop_time_val = Tick; //当前tick数保存到全局变量stop_time_val
}
//==========================【以下代码不用修改】==============================
//通过stop_time_val和start_time_val的差值得到CPU跑评测代码消耗的时间,单位是tick数。
CORE_TICKS get_time(void)
{
CORE_TICKS elapsed =
(CORE_TICKS) (MYTIMEDIFF(stop_time_val, start_time_val));
return elapsed;
}
//将get_time()函数返回的tick数转换为秒单位时间。
//需要根据定时器的tick周期正确定义EE_TICKS_PER_SEC的值。
secs_ret time_in_secs(CORE_TICKS ticks)
{
secs_ret retval = ((secs_ret)ticks) / (secs_ret)EE_TICKS_PER_SEC;
return retval;
}
如果你想超频
像STM32,GD32这种都是在Reset_Handler函数中调用SystemInit() 函数 执行的系统时钟初始化,也就是在复位完成后系统时钟就配置好了。如果你想超频,可以在portable_init()函数中对系统时钟进行二次配置,实现超频。
在core_portme.h中对ITERATIONS进行定义,它定义了测试代码迭代运行的次数,这个值的大小不会影响评分结果,但是必须要让单片机运行至少10秒,值太大会让评测时间变长。这里给个参考值,一般72MHz的主频可以使用2000,120MHz可以使用3500。
//#######################################
#define ITERATIONS 3500 /*定义迭代次数*/
//#######################################
在core_portme.h中对MAIN_HAS_NOARGC进行定义,定义为1时,coremark使用的main函数的参数列表为空;定义为0时coremark使用的main函数的参数列表为main(int argc, char *argv[])。由于单片机的main函数一般都是不带参数的,因此这里修改为1。
#ifndef MAIN_HAS_NOARGC
#define MAIN_HAS_NOARGC 1
#endif
在core_portme.h中对编译器版本和编译器参数进行定义。这2个宏不会影响跑分结果,只是coremark在打印评测结果的时候会输出定义的值。如果你只想看单片机性能,这2个宏可以不用在意。
#define COMPILER_VERSION "Armcc v5.06"
#define COMPILER_FLAGS \
"-O3 -Otime"
输出样例
实验环境
MCU | 内核 | CPU主频 | 编译器和编译参数 | 每秒迭代次数* | 功耗 |
GD32F303CCT6 | M4 | 120M | Armcc v5.06 -O3 -Otime | 279.307318 | 3.3V@17mA |
GD32F303CCT6 | M4 | 72M | Armcc v5.06 -O3 -Otime | 167.560322 | 3.3V@11mA |
GD32F130C8T6 | M3 | 72M | Armcc v5.06 -O3 -Otime | 162.737713 | 3.3V@23mA |
GD32F130C8T6 | M3 | 48M | Armcc v5.06 -O3 -Otime | 108.469954 | 3.3V@17mA |
STM32F103ZET6 | M3 | 72M | Armcc v5.06 -O3 -Otime | 134.188162 | |
Air32F103CBT6 | M3 | 72M | Armcc v5.06 -O3 -Otime | 184.162063 | 3.3V@18mA |
Air32F103CBT6 | M3 | 120M | Armcc v5.06 -O3 -Otime | 306.983883 | 3.3V@23mA |
注*:数值越大代表CPU运算能力越强