当前位置:首页 > 公众号精选 > 技术让梦想更伟大
[导读]没有y【开篇就白给?】对大家熟悉的Cortex-M处理起来说,无论是强调极致资源和低功耗的Cortex-M0、还是频率达到上GHz且能与某些应用处理器掰一掰手腕的Cortex-M7,都不会缺席了SysTick的身影。正因为SysTick是官方钦定的“不可或缺”的“基础设施”,无论...

没有y


【开篇就白给?】

对大家熟悉的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 5arm compielr 6下的perf_counter.lib)会被加入到工程中。



GCC环境下使用perf_counter略微有一些注意事项,由于在文章《【教程】如何用GCC“零汇编”白嫖MDK》的末尾已经有过非常详尽的介绍,这里就不再赘述了。


当我们在MDK环境下使用Arm Compiler 6作为编译器时,需要打开对GNU扩展和C99(极其以上)语言标准的支持,具体方法如下图所示:在Language C标准下拉列表中选择带有gnu前缀的选项——如果没有什么特别的顾虑,推荐直接拉满——使用gnu11即可。


如果你使用的是较老的Arm Compiler 5,则应该同时勾选C99 ModeGNU extensions两个选项。找不到上述两个选项的小伙伴,应该认真考虑升级你们的MDK版本了。



【一键终结甜咸之争?】

虽然在RTE中,perf_counter推荐并默认使用library的方式来部署,

但考虑到总有小伙伴对黑盒子有莫名的恐惧:
“……往往要親眼看着黃酒從罎子裏舀出,看過壺子底裏有水沒有,又親看將壺子放在熱水裏,然後放心……https://zh.m.wikisource.org/zh/孔乙己

(图片来自网络,侵删)

因此,perf_counter贴心的提供了以Source源代码来进行部署的方式:


此时,相关的perf_counter.csystick_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,都会在MDKpack-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(
本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
关闭
关闭