大家好,我是张巧龙,提起MCU性能测试,最著名的就是CoreMark和Dhrystone。" k, F+ @9 D) e0 ^- \7 H8 x5 B
6 F" Q2 e: d9 @# [/ {; ?CoreMark以每秒迭代次数作为性能评价,而Dhrystone的DMIPS与Dhrystone标准相关。( X- t; U }" W6 p7 k+ ?3 v( t) q
: M/ `2 ?/ p" g j
9 F: E4 u' A2 N8 v8 K
本文讨论CoreMark,先来看看MCU厂家如何宣传自家产品性能。7 j$ n+ D0 ~1 ^4 O* t+ X
; K) b; r2 g4 R1 J* i* e2 t
▍揭开神秘面纱:CoreMark是什么?为什么它可以作为MCU的性能指标?这个数是怎么计算出来的?
/ y+ v a- ^ l9 n( s+ [
8 D4 M. g5 q z) f) p1 f T1 Y
9 C: D* ~8 S% I7 T3 P! HCoreMark是衡量嵌入式系统中微控制器性能的基准。通过包含列表处理(查找和排序)、矩阵处理(常见的矩阵操作)、状态机(确定输入流是否包含有效数字)和CRC(循环冗余校验)等算法的测试给出性能评价。
; n# n: y# B1 _0 Y* R5 z5 g: g# v- f9 I8 o& Q6 [- K7 R4 [
. u! C7 V) f; K) f需要下载的软件包是免费开源的, 可以在官方网站找到:EEMBC’s CoreMark®,网页截图如下。
' F1 f$ P: R% {- V1 l" `2 h▍作业不难:怎么将CoreMark移植到自己的MCU上?运行CoreMark需要哪些外设支持?" ^' Y$ [! H# A, ~* j Z1 Z# }
) A& d! S( `; _% L8 ~ D7 F0 C* n
- ?7 U! `+ N: U! i8 ?( ]6 [7 H& \) T3 v
51单片机也可以跑CoreMark的,所以硬件上不用担心。运行CoreMark,需要定时器提供计时功能,还需要一种向外部打印消息的手段。6 N; O# j; ~5 K3 o1 o; ^
, N( J0 Y" \) j4 g
& J5 y! F, i' S8 k1 A一个比较简单的方法是,调用stdio.h中定义的printf()函数,将其重定向到串口。现以使用STM32CubeIDE将CoreMark移植到STM32G071为例进行说明。. n$ Y+ e# E2 I
) y$ `! n# I& r. s: `; }: z! W* B$ i4 m' M
● Step1:复制必要文件到目标工程。现已准备好目标MCU的软件工程,其文件结构如下图右侧所示。从CoreMark官网获得软件包,软件包解压后文件结构如下图左侧所示。
( h% L: B4 |7 f
- q* Y- d7 s% v5 k* g b, z: ^5 x) ]0 _1 x7 V; g8 n, b. k
在右侧软件工程中新建文件夹CoreMark,将左侧绿色框中,core_list_join.c、core_main.c、core_matrix.c、core_state.c、core_util.c、coremark.h等文件放到CoreMark文件夹里。
: F6 g- Z, E/ d4 T6 s
% ?, T' y p- D* C' S3 [ m+ s6 y0 V6 v, _- ^
打开左侧simple文件夹,将core_portme.c复制到Src文件夹中,将core_portme.h复制到Inc文件夹中。" P2 K9 K+ s+ @; F8 S. j
(彩色高亮标记与图中颜色标记对应,共5个步骤) ' o1 Y9 N' N2 U- d
6 A" {5 R+ w% z! v
● Step2:由于core_main.c文件已定义了main()函数,该main()函数执行时调用core_portme.c中的portable_init()函数作为MCU初始化接口,因此需要将MCU工程中原main.c文件MCU初始化代码移动到到portable_init()函数里并删除原有main.c文件。
3 C' t- u; \$ }
. F3 P; z1 F9 r: @
/ F1 o& a8 h( b9 v' p1 B4 s3 V以STM32CubeIDE工程为例,需要移动SystemClock_Config和一堆MX_开头的初始化函数(注意相关结构体、函数原型与函数实现要一起移动)。
8 K E; X8 o ~1 d0 T0 q' v) v$ |* l0 g7 S! t
9 k% R! G" n: f' x. {1 z7 K
● Step3:CoreMark的分数最终表示为Iterations/Sec,也就是每秒迭代数,而Sec和系统Ticks相关。用过RTOS的朋友应该对这个概念很熟悉,考虑文字不太好解释,直接看core_portme.c里这个宏定义。
n1 L: t7 m+ o( A- X- y- #define EE_TICKS_PER_SEC 1000
复制代码 ● 这里我定义每秒1000个Tick,每个Tick时长1ms,对应定时器每1ms触发一次中断。使用一个计数变量,定时器进入中断一次,该变量值+1;对该变量值/1000即可求得定时器运行时长,也就是上文的Sec。5 U* a( \; T) a* _" r7 w& r0 @! E- w0 e
' C5 ` _- N) f; t
5 y% J0 G; ~: `: N# e* N! G所以EE_TICKS_PER_SEC并非一定要设置为1000,和定时器中断频率对应即可。与定时相关的函数有以下三个。0 x5 q& h+ ]( s! A
- void start_time(void);//启动计时器;
( [+ P- `& j. J L& ] - void stop_time(void);//停止计时器;
9 J3 j6 I( e0 r. `9 \% i - CORE_TICKS get_time(void);//获取计时器的计数值。
复制代码 ● 在start_time()里实现定时器启动功能,在stop_time()里实现定时器停止功能,在get_time()中获取中断计数值。数据类型“CORE_TICKS ”实际上就是“unsigned long”。
- x ?# ^! u* b1 q, r: e5 }$ j2 T" A
8 p5 a0 ~4 S# x8 o) B
} O5 ]% @4 M. b9 h为方便操作,推荐将计数变量设置为全局变量,这样可以通过extern关键字直接访问。此处以TIM1为例,代码如下:
: H: ?# V9 R" M$ I& f- 4 v$ G( U$ J0 ^5 b$ S. x
- //计数变量
& x8 S8 S1 {& [2 W8 \ - unsigned long time_ms_ticks=0;! U" T+ _; C' L
& X$ j: S$ g( z: g- //定时器启动
& E9 r3 {$ F! e - void start_time(void)
# {9 O: E9 y8 Q( c( w - {
( h2 F8 n5 C { - (&htim1)->State = HAL_TIM_STATE_BUSY;7 v5 P, ^2 G7 Q! I* n/ ~
- __HAL_TIM_ENABLE(&htim1);
( g! I" Q# Y* J- g# ? - __HAL_TIM_ENABLE_IT(&htim1, TIM_IT_UPDATE);
" f- d# K/ Y9 K- {0 J% g& W - }0 G: Z) f1 @9 M" I/ }6 N
1 c2 d/ z5 d$ R9 w0 Y8 j- //定时器停止; D/ ]" |4 K; i
- void stop_time(void)! j4 A0 s+ R! F1 j; G1 e0 n2 ?8 L
- {
/ D/ x! J i; x7 q6 V, ?, | - __HAL_TIM_DISABLE_IT(&htim1, TIM_IT_UPDATE);
- v* |" y& t9 c9 Q - __HAL_TIM_DISABLE(&htim1);
+ P1 {0 q) t6 e- W" ^1 @ - (&htim1)->State = HAL_TIM_STATE_READY;" L( K6 m& A& J' N4 Y' \8 H
- }
/ ?7 C$ u5 ?' q; ?% u& }& Y
4 `4 R" u5 G5 R- //获取中断计数值. Q# K7 |+ p' X) l, E2 Z7 h$ g4 z
- CORE_TICKS get_time(void)
/ ^+ U' v4 A6 ?( k1 f - {$ O/ k! u, J* n: W5 ^
- return time_ms_ticks;% [6 H s7 e9 ~
- }; I3 ]( V2 y; j# l$ A& M' Q9 g
$ D M, |/ i3 p" ^& d4 J7 Z; `- //中断处理函数. [' e# _- x; b) ~! c/ a# l- J
- extern unsigned long time_ms_ticks;6 e. d; F" g6 ~) O- i `- Z5 L
- void TIM1_BRK_UP_TRG_COM_IRQHandler(void)/ K" V" T; f, O
- {
2 l+ z x$ z0 p% ~' _! ? - HAL_TIM_IRQHandler(&htim1);! o0 U- Y, U# r3 ^$ O) ]
- time_ms_ticks=time_ms_ticks+1;
2 O4 T2 [7 d4 `/ } - }
复制代码 ● 注释部分不使用的代码。以下代码位于core_portme.c中,需要注释掉:) n; l# r9 Z# @7 j( M
- //#define NSECS_PER_SEC CLOCKS_PER_SEC
Z- q1 G0 W: B. u - //#define CORETIMETYPE clock_t
5 |3 v% |' ^5 Z5 G% A - //#define GETMYTIME(_t) (*_t = clock())7 B2 F2 @: r+ P, S1 V) @
- //#define MYTIMEDIFF(fin, ini) ((fin) - (ini))8 {+ c3 r! l6 `9 v% \& O; v, ~$ w$ K
- //#define TIMER_RES_DIVIDER 1
6 `/ ?: I! v3 ?: l* O: `$ V& A - //#define SAMPLE_TIME_IMPLEMENTATION 1
9 r: J1 a9 i: r1 }, Z8 r. d# w; ]9 b
: B e9 {/ q0 y8 H. K- //static CORETIMETYPE start_time_val, stop_time_val;
复制代码 ● CoreMark要求的最短测试时间为10s,若测试时间低于10s则会报错,见下图:% f& w0 R" Y* a2 S1 T6 x+ o
● 为获得有效的测试结果,需修改core_portme.c中关于ITERATIONS的设置,官方代码中ITERATIONS没有定义:0 q' {9 @: t$ n
- volatile ee_s32 seed4_volatile = ITERATIONS;
复制代码 此处使用STM32G071,主频64MHz。经测试ITERATIONS修改为1200左右即可。
$ R7 c! w- x- {1 C' \7 A- volatile ee_s32 seed4_volatile = 1200;
复制代码 ● Step4:打印测试结果时,编译器优化等级和调试等级也可以打印出来。这类信息可在core_portme.h中通过宏COMPILER_FLAGS修改。这里我使用-Ofast优化,调试等级-g3,修改如下:, f; H" k# c' n7 N: w$ w
- #ifndef COMPILER_FLAGS: u) o& X3 F+ |/ b; Z
- #define COMPILER_FLAGS "-Ofast -g3"
: _' s3 g6 C: P - #endif0 n# e# R/ G8 U
复制代码 ● 重定向printf到串口,可参考以下代码(以STM32 LPUART1为例),根据IDE不同可能需要添加float类型支持:
: i: }0 v+ z& d- x- __io_putchar (int ch)
# Y1 b! w' `, I9 U! k - {
* O& Y7 O" a0 R5 o" M& ` - HAL_UART_Transmit (&hlpuart1, (uint8_t*) &ch, 1, 0x0F);
3 r* |- [! ]. Z4 j - return ch;
0 s- B$ b. y1 x# Q% k* v" i# d! s) S - }
复制代码 ● Step5:移植完成!编译程序下载运行,得到跑分结果:
7 [1 E( `3 {. u' K1 E4 V/ _6 ^. J# e3 y1 H$ {
STM32G071RB,64MHz,108.9分!
+ Q, B# _$ X0 b$ c
8 c( s8 Z& M, F( P: G# q& K1 {5 a9 x2 a5 ^
通常使用最高主频和-Ofast优化可以获取最大分数,欢迎大家把自己的跑分放到评论区
; Y, x. |2 L- S, s! e6 D; ]& W
/ w1 K. c) p- |7 v4 c( s& m9 H' K) r+ E3 p* Y5 R6 z1 x" S
; O7 K3 h5 o# |% r& c- A0 H4 D) N9 _+ v' ~/ Q5 W
|