超级嵌入式系统“性能/时间”工具箱
时间:2021-11-09 13:59:21
手机看文章
扫描二维码
随时随地手机看文章
[导读]没有y【开篇就白给?】对大家熟悉的Cortex-M处理起来说,无论是强调极致资源和低功耗的Cortex-M0、还是频率达到上GHz且能与某些应用处理器掰一掰手腕的Cortex-M7,都不会缺席了SysTick的身影。正因为SysTick是官方钦定的“不可或缺”的“基础设施”,无论...
没有y
【开篇就白给?】对大家熟悉的Cortex-M处理起来说,无论是强调极致资源和低功耗的Cortex-M0、还是频率达到上GHz且能与某些应用处理器掰一掰手腕的Cortex-M7,都不会缺席了SysTick的身影。
正因为SysTick是官方钦定的“不可或缺”的“基础设施”,无论是RTOS系统还是裸机应用,几乎所有的嵌入式固件都会用到它。在这一背景下,如果我告诉你,有一个基于C语言的模块,提供以下功能:
使用了SysTick却不会占用SysTick;
或者说提供以上功能的同时,用户的原有的SysTick应用(比如RTOS调度器或是普通的应用延时)丝毫不会受到影响;
再直白点说:以上功能都是白送的,每个Cortex-M处理器都能立即享有,且不受到芯片型号的影响,
你是不是要直呼:“真就白给?”
是的!作为一个在Github上开源的C语言模块,它真就白给!
【请张嘴……啊~】perf_counter版本一路进化,从加入对GCC、IAR的支持、通过Library简化用户部署以来,从版本1.6.1开始更是把模块的部署做到了极致的简化:
这次,你只要从下面的链接“一次性”的下载 CMSIS-Pack,就可以在MDK环境中实现傻瓜式的部署:
https://raw.githubusercontent.com/GorgonMeducer/perf_counter/CMSIS-Pack/cmsis-pack/GorgonMeducer.perf_counter.1.6.3.pack
也可以单击文章末尾的“阅读原文”进行下载。
下载后,双击pack文件进行无脑安装:
在确认了开原许可Apache 2.0的条款后,一路Next,直到单击Finish,完成整个安装过程:
一般来说,部署会非常顺利,但如果出现了安装错误,比如下面这种:
则很可能是您所使用的MDK版本太低导致的——是时候更新下MDK啦。关注【裸机思维】公众号后发送关键字"MDK",即可获得最新的MDK网盘链接。
当我们想在任何已有工程中部署perf_counter时,只需要单击MDK工具栏上如下图所示的图标:
打开RTE配置窗口:
我们会注意到,在列表的末端出现了一个Utilities条目,依次展开后勾选Performance下的perf_counter——默认情况下系统会自动选择以库的形式来实现模块的部署——这也是我吐血推荐的方式,因为它省去了不必要的编译麻烦。
单击OK按钮后,我们会发现Utilities已经被加入到了工程管理器中,而perf_counter.lib也已经成功的部署到了目标工程中:
看过我往期文章《【教程】如何用GCC“零汇编”白嫖MDK》的小伙伴一定知道,MDK也可以使用GCC作为编译器。perf_counter也将这种情况考虑在内——当用户实际使用的是GCC时,对应的 libperf_counter_gcc.a(而不是arm compiler 5或arm compielr 6下的perf_counter.lib)会被加入到工程中。
GCC环境下使用perf_counter略微有一些注意事项,由于在文章《【教程】如何用GCC“零汇编”白嫖MDK》的末尾已经有过非常详尽的介绍,这里就不再赘述了。
当我们在MDK环境下使用Arm Compiler 6作为编译器时,需要打开对GNU扩展和C99(极其以上)语言标准的支持,具体方法如下图所示:在Language C标准下拉列表中选择带有gnu前缀的选项——如果没有什么特别的顾虑,推荐直接拉满——使用gnu11即可。
如果你使用的是较老的Arm Compiler 5,则应该同时勾选C99 Mode和GNU extensions两个选项。找不到上述两个选项的小伙伴,应该认真考虑升级你们的MDK版本了。
【一键终结甜咸之争?】虽然在RTE中,perf_counter推荐并默认使用library的方式来部署,
但考虑到总有小伙伴对黑盒子有莫名的恐惧:
(图片来自网络,侵删)
因此,perf_counter贴心的提供了以Source源代码来进行部署的方式:
此时,相关的perf_counter.c和systick_wrapper_ual.s会取代原本的库文件加入到编译中:
当然,使用Source方式来编译也是有代价的,即perf_counter的源代码对CMSIS有依赖——当你的工程中并未在RTE配置界面中勾选CMSIS-CORE,就会出现类似下图所示的黄色警告信息:“Additional software components required”。
此时,你可以简单的单击Resolve按钮来解决问题——你会发现,所谓的解决方案就是RTE自动把Source模式依赖的CMSIS-CORE帮你勾选上了而已:
如果你对此并无异议,则问题圆满解决;如果你的系统中存在别的版本的CMSIS(比如很多正点原子、野火、以及CubeMX生成的工程都很可能会携带不经由RTE配置界面来管理的CMSIS),则很有可能出现CMSIS版本冲突——而关于冲突的解决方案,则稍微复杂一些——你可以参考我的文章《CMSIS玩家的“阴间成就”指南》来实现某种取舍……又或者……
还是推荐你继续使用Library模式吧,毕竟它对CMSIS没有任何依赖。
【一键更新的……嵌入式软件模块?】一旦你安装了perf_counter任何一个版本的pack,都会在MDK的pack-installer中留下痕迹:
此时,只要通过菜单 Pcak->Check For Update,我们就能实时的查询 perf_counter是否存在最新版本:
如果Pack-Installer真的从github上发现了更新,就会以黄色Update的图标来告知我们:
此时,单击Update按钮,即可安装最新版本:
那么,如何才能鼓励博主多多更新、加入更多更好的功能呢?
当然还是要靠有能力科学上网的小伙伴多多Star呀!
https://github.com/GorgonMeducer/perf_counter.git
【库的初始化和注意事项】
关于头文件在任何需要使用perf_counter的C语言源文件中,我们需要首先加入对头文件的引用:
关于库的初始化一般来说,用户会在某一个地方,比如 main() 函数内完成对CPU工作频率的配置,我们应该在完成这一工作之后确保全局变量 SystemCoreClock 被正确的更新——保存当前CPU的工作频率,比如:
1、用户自己的应用里完全没有使用SysTick。此时,在编译时,我们多半会看到类似如下的错误提示:
对于这种情况,我们需要在任意的C文件中添加一个SysTick中断处理程序:
2、用户自己的应用里使用了SysTick,拥有自己的初始化过程。对于这种情况,我们需要确保一件事情:即,SysTick的CTRL寄存器的 BIT2(SysTick_CTRL_CLKSOURCE_Msk)是否被置位了——如果其值是1,说明SysTick使用了跟CPU一样的工作频率,那么SysTick的测量结果就是CPU的周期数;如果其值是0,说明SysTick使用了来自于别处的时钟源,这个时钟源具体频率是多少就只能看芯片手册了(比如STM32就喜欢将系统频率做 1/8 分频后提供给SysTick作为时钟源),此时SysTick测量出来的结果就不是CPU的周期数。
在确保了 CTRL 寄存器的 BIT2 被正确置位,并且SysTick中断被使能(置位 BIT1,SysTick_CTRL_TICKINT_Msk )后,我们可以简单的通过 init_cycle_counter() 函数告诉perf_counter模块:SysTick 被用户占用了——这里传递 true 就实现这一功能。
关于Library的匹配问题perf_counter.lib 库在编译的时候,开启了 Short enums/wchar(分别对应命令行的 -fshort-enums -fshort-wchar)。这么做其实没什么特别的原因,但如果你的工程使用了不同的配置,例如:
下图的工程配置中,没有勾选 "Short enums/wchar"
你一定会看到这样的编译错误:
既然知道了原因,解决方法就很简单,要么在工程配置中勾选上这一选项;要么使用源代码编译的模式。
【时间类服务】
微秒级阻塞延时perf_counter提供了一个us级阻塞延时函数 delay_us(),它的函数原型如下:
具体评估方法,请参考我往期的文章《【实时性迷思】CPU究竟跑的有多快?》,这里就不做赘述。
全局系统时间perf_counter提供了API函数get_system_ticks(),用于方便用户获取自 SysTick启动以来系统已经经历过的总周期数,其函数原型如下:
考虑到 SystemCoreClock 记录了当前的系统频率,我们可以借助它将由 get_system_ticks() 所获得的周期数转化为物理时间,比如,我们可以自己定义如下的宏:
非阻塞式多重延时在状态机中,非阻塞式的延时往往是必不可少的功能,包括但不限于:
下图就是一个支持多实例的非阻塞延时的状态机,即便你没有看过我的状态机系列文章,对应的逻辑应该也算是浅显易懂。
这里的核心思想是:
下面的状态图展示了如何在执行某些动作(或者子状态机)的同时,进行超时判断:
这里值得注意的细节是:
具体状态图的解读和翻译方式,还不熟悉的小伙伴可以单击这里来阅读状态机系列文章,这里就不再赘述。
随机数发生几乎所有的C语言教程都在介绍过随机数的发生,比如:
【开篇就白给?】对大家熟悉的Cortex-M处理起来说,无论是强调极致资源和低功耗的Cortex-M0、还是频率达到上GHz且能与某些应用处理器掰一掰手腕的Cortex-M7,都不会缺席了SysTick的身影。
正因为SysTick是官方钦定的“不可或缺”的“基础设施”,无论是RTOS系统还是裸机应用,几乎所有的嵌入式固件都会用到它。在这一背景下,如果我告诉你,有一个基于C语言的模块,提供以下功能:
- 精确测量系统性能
- 精确测量函数执行时间
- 精确测量中断响应延迟
- 提供精确到us级的阻塞或非阻塞的延时服务
- 改善伪随机数的随机数特性
- 提供系统时间戳
- ……
使用了SysTick却不会占用SysTick;
或者说提供以上功能的同时,用户的原有的SysTick应用(比如RTOS调度器或是普通的应用延时)丝毫不会受到影响;
再直白点说:以上功能都是白送的,每个Cortex-M处理器都能立即享有,且不受到芯片型号的影响,
你是不是要直呼:“真就白给?”
是的!作为一个在Github上开源的C语言模块,它真就白给!
【请张嘴……啊~】perf_counter版本一路进化,从加入对GCC、IAR的支持、通过Library简化用户部署以来,从版本1.6.1开始更是把模块的部署做到了极致的简化:
这次,你只要从下面的链接“一次性”的下载 CMSIS-Pack,就可以在MDK环境中实现傻瓜式的部署:
https://raw.githubusercontent.com/GorgonMeducer/perf_counter/CMSIS-Pack/cmsis-pack/GorgonMeducer.perf_counter.1.6.3.pack
也可以单击文章末尾的“阅读原文”进行下载。
下载后,双击pack文件进行无脑安装:
在确认了开原许可Apache 2.0的条款后,一路Next,直到单击Finish,完成整个安装过程:
一般来说,部署会非常顺利,但如果出现了安装错误,比如下面这种:
则很可能是您所使用的MDK版本太低导致的——是时候更新下MDK啦。关注【裸机思维】公众号后发送关键字"MDK",即可获得最新的MDK网盘链接。
当我们想在任何已有工程中部署perf_counter时,只需要单击MDK工具栏上如下图所示的图标:
打开RTE配置窗口:
我们会注意到,在列表的末端出现了一个Utilities条目,依次展开后勾选Performance下的perf_counter——默认情况下系统会自动选择以库的形式来实现模块的部署——这也是我吐血推荐的方式,因为它省去了不必要的编译麻烦。
单击OK按钮后,我们会发现Utilities已经被加入到了工程管理器中,而perf_counter.lib也已经成功的部署到了目标工程中:
看过我往期文章《【教程】如何用GCC“零汇编”白嫖MDK》的小伙伴一定知道,MDK也可以使用GCC作为编译器。perf_counter也将这种情况考虑在内——当用户实际使用的是GCC时,对应的 libperf_counter_gcc.a(而不是arm compiler 5或arm compielr 6下的perf_counter.lib)会被加入到工程中。
GCC环境下使用perf_counter略微有一些注意事项,由于在文章《【教程】如何用GCC“零汇编”白嫖MDK》的末尾已经有过非常详尽的介绍,这里就不再赘述了。
当我们在MDK环境下使用Arm Compiler 6作为编译器时,需要打开对GNU扩展和C99(极其以上)语言标准的支持,具体方法如下图所示:在Language C标准下拉列表中选择带有gnu前缀的选项——如果没有什么特别的顾虑,推荐直接拉满——使用gnu11即可。
如果你使用的是较老的Arm Compiler 5,则应该同时勾选C99 Mode和GNU extensions两个选项。找不到上述两个选项的小伙伴,应该认真考虑升级你们的MDK版本了。
【一键终结甜咸之争?】虽然在RTE中,perf_counter推荐并默认使用library的方式来部署,
但考虑到总有小伙伴对黑盒子有莫名的恐惧:
“……往往要親眼看着黃酒從罎子裏舀出,看過壺子底裏有水沒有,又親看將壺子放在熱水裏,然後放心……”https://zh.m.wikisource.org/zh/孔乙己
(图片来自网络,侵删)
因此,perf_counter贴心的提供了以Source源代码来进行部署的方式:
此时,相关的perf_counter.c和systick_wrapper_ual.s会取代原本的库文件加入到编译中:
当然,使用Source方式来编译也是有代价的,即perf_counter的源代码对CMSIS有依赖——当你的工程中并未在RTE配置界面中勾选CMSIS-CORE,就会出现类似下图所示的黄色警告信息:“Additional software components required”。
此时,你可以简单的单击Resolve按钮来解决问题——你会发现,所谓的解决方案就是RTE自动把Source模式依赖的CMSIS-CORE帮你勾选上了而已:
如果你对此并无异议,则问题圆满解决;如果你的系统中存在别的版本的CMSIS(比如很多正点原子、野火、以及CubeMX生成的工程都很可能会携带不经由RTE配置界面来管理的CMSIS),则很有可能出现CMSIS版本冲突——而关于冲突的解决方案,则稍微复杂一些——你可以参考我的文章《CMSIS玩家的“阴间成就”指南》来实现某种取舍……又或者……
还是推荐你继续使用Library模式吧,毕竟它对CMSIS没有任何依赖。
【一键更新的……嵌入式软件模块?】一旦你安装了perf_counter任何一个版本的pack,都会在MDK的pack-installer中留下痕迹:
此时,只要通过菜单 Pcak->Check For Update,我们就能实时的查询 perf_counter是否存在最新版本:
如果Pack-Installer真的从github上发现了更新,就会以黄色Update的图标来告知我们:
此时,单击Update按钮,即可安装最新版本:
那么,如何才能鼓励博主多多更新、加入更多更好的功能呢?
当然还是要靠有能力科学上网的小伙伴多多Star呀!
https://github.com/GorgonMeducer/perf_counter.git
【库的初始化和注意事项】
关于头文件在任何需要使用perf_counter的C语言源文件中,我们需要首先加入对头文件的引用:
#include "perf_counter.h"
需要注意的是,通过RTE方式部署的perf_counter并不会在工程管理器中引入 perf_counter.h 以供用户查看。想要查看perf_counter.h查找可用API的小伙伴,可以“简单的”在上述代码行上单击右键,从弹出菜单中选择“Open document 'perf_counter'” 来实现对头文件的访问。关于库的初始化一般来说,用户会在某一个地方,比如 main() 函数内完成对CPU工作频率的配置,我们应该在完成这一工作之后确保全局变量 SystemCoreClock 被正确的更新——保存当前CPU的工作频率,比如:
extern uint32_t SystemCoreClock;
void main(void)
{
system_clock_update(); //! 更新CPU工作频率
SystemCoreClock = 72000000ul //! 假设更新后的系统频率是 72MHz
...
}
一般来说,你的芯片工程如果本身都是基于较新的CMSIS框架而创建的,你的启动文件中已经为你定义好了全局变量 SystemCoreClock——当然,凡事都有例外,如果你在编译的时候报告找不到变量 SystemCoreClock 或者说“Undefined symbol __SystemCoreClock” 之类的,你自己定义一下就好了,比如:uint32_t SystemCoreClock;
void main(void)
{
system_clock_update(); //! 更新CPU工作频率
SystemCoreClock = 72000000ul //! 假设更新后的系统频率是 72MHz
...
}
在这以后,我们需要对 perf_counter 库进行初始化。这里分两种情况:1、用户自己的应用里完全没有使用SysTick。此时,在编译时,我们多半会看到类似如下的错误提示:
Error: L6218E: Undefined symbol $Super$$SysTick_Handler (referred from systick_wrapper_ual.o).
对于这种情况,我们需要在任意的C文件中添加一个SysTick中断处理程序:
#include "perf_counter.h"
...
__attribute__((used)) //!< 避免下面的处理程序被编译器优化掉
void SysTick_Handler(void)
{
}
然后我们在 main() 函数里初始化 perf_counter 服务:#include
...
void main(void)
{
system_clock_update(); //! 更新CPU工作频率
SystemCoreClock = 72000000ul //! 假设更新后的系统频率是 72MHz
init_cycle_counter(false);
...
}
需要特别注意的是:由于用户并没有自己初始化 SysTick,因此我们需要将这一情况告知 perf_counter 库——由它来完成对 SysTick 的初始化——这里传递 false 给函数 init_cycle_counter() 就是这个功能。如果由perf_counter 库自己来初始化SysTick,它会为了自己功能更可靠将 SysTick的溢出值(LOAD寄存器)设置为最大值(0x00FFFFFF)。2、用户自己的应用里使用了SysTick,拥有自己的初始化过程。对于这种情况,我们需要确保一件事情:即,SysTick的CTRL寄存器的 BIT2(SysTick_CTRL_CLKSOURCE_Msk)是否被置位了——如果其值是1,说明SysTick使用了跟CPU一样的工作频率,那么SysTick的测量结果就是CPU的周期数;如果其值是0,说明SysTick使用了来自于别处的时钟源,这个时钟源具体频率是多少就只能看芯片手册了(比如STM32就喜欢将系统频率做 1/8 分频后提供给SysTick作为时钟源),此时SysTick测量出来的结果就不是CPU的周期数。
在确保了 CTRL 寄存器的 BIT2 被正确置位,并且SysTick中断被使能(置位 BIT1,SysTick_CTRL_TICKINT_Msk )后,我们可以简单的通过 init_cycle_counter() 函数告诉perf_counter模块:SysTick 被用户占用了——这里传递 true 就实现这一功能。
#include
...
void main(void)
{
system_clock_update(); //! 更新CPU工作频率
SystemCoreClock = 72000000ul //! 假设更新后的系统频率是 72MHz
init_cycle_counter(true);
...
}
关于Library的匹配问题perf_counter.lib 库在编译的时候,开启了 Short enums/wchar(分别对应命令行的 -fshort-enums -fshort-wchar)。这么做其实没什么特别的原因,但如果你的工程使用了不同的配置,例如:
下图的工程配置中,没有勾选 "Short enums/wchar"
你一定会看到这样的编译错误:
.\Out\example.axf: Error: L6242E: Cannot link object perf_counter.o as its attributes are incompatible with the image attributes.
... wchart-16 clashes with wchart-32.
... packed-enum clashes with enum_is_int.
既然知道了原因,解决方法就很简单,要么在工程配置中勾选上这一选项;要么使用源代码编译的模式。
【时间类服务】
微秒级阻塞延时perf_counter提供了一个us级阻塞延时函数 delay_us(),它的函数原型如下:
extern void delay_us(int32_t iUs);
实际上,由于函数调用的开销,delay_us在时间判断上会存在一个“不积累”的误差——根据优化等级的不同,其具体CPU周期数存在差异,如果我们以Library方式进行部署时,这一误差大约在 /-25个CPU周期左右——这一信息实际上告诉我们:- 在使用Library的情况下,当你的CPU频率超过50MHz时,delay_us() 可以提供最小<1us的延时误差;
- 当你的系统频率不满足上述条件时,以系统频率 12MHz为参考,则可以认为delay_us误差为不积累的 /- 2us。
具体评估方法,请参考我往期的文章《【实时性迷思】CPU究竟跑的有多快?》,这里就不做赘述。
全局系统时间perf_counter提供了API函数get_system_ticks(),用于方便用户获取自 SysTick启动以来系统已经经历过的总周期数,其函数原型如下:
__attribute__((nothrow))
extern int64_t get_system_ticks(void);
可以看到,其返回值是一个 64位的有符号整数,即便抛开符号位,也基本可以确信:无论芯片频率如何,在人类灭绝之前,不会发生溢出问题。考虑到 SystemCoreClock 记录了当前的系统频率,我们可以借助它将由 get_system_ticks() 所获得的周期数转化为物理时间,比如,我们可以自己定义如下的宏:
#define get_system_ms() \
(get_system_ticks() / (SystemCoreClock / 1000ul))
#define get_system_us() \
(get_system_ticks() / (SystemCoreClock / 1000000ul))
比如,这里的 get_system_ms() 可以告诉我们从SysTick启动以来(一般大约可以等效为从系统复位开始)已经过去了多少毫秒。是不是特别方便?非阻塞式多重延时在状态机中,非阻塞式的延时往往是必不可少的功能,包括但不限于:
- 机构控制的延时;
- 电路的时序控制;
- 通信协议的超时处理;
- ……
下图就是一个支持多实例的非阻塞延时的状态机,即便你没有看过我的状态机系列文章,对应的逻辑应该也算是浅显易懂。
这里的核心思想是:
- 在延时的开始时刻,通过 get_system_ticks() 的衍生方法 get_system_ms() 来获取当前的系统时间戳;
- 计算目标时刻的系统时间戳并保存在状态机类中(保存在 iTargetTime里);
- 在随后的状态中以非阻塞的方式轮询 get_system_ms() 以检查约定的时间是否已经到来。
下面的状态图展示了如何在执行某些动作(或者子状态机)的同时,进行超时判断:
这里值得注意的细节是:
- 在延时的开始时刻,通过 get_system_ticks() 的衍生方法 get_system_ms() 来获取当前的系统时间戳;
- 计算目标时刻的系统时间戳并保存在状态机类中(保存在 iTargetTime里);
- 在读取字符失败时,通过对比当前的系统时间戳来判断是否超时。
具体状态图的解读和翻译方式,还不熟悉的小伙伴可以单击这里来阅读状态机系列文章,这里就不再赘述。
随机数发生几乎所有的C语言教程都在介绍过随机数的发生,比如:
#include
#include
int main (void)
{
int i, n;
time_t t;
n = 5;
/* Intializes random number generator */
srand((unsigned) time(