【说在前面的话】
在本系列前面的文章《实时性迷思(2)——“时间片轮转”的沙子》中,我们详细介绍了实时性的概念、澄清了一些常见的误解并介绍了一种评估实时性任务对CPU资源占用的方法,即:
当前实时性任务所消耗的CPU资源百分比为:
这里的
就是“事件n”的CPU资源占用。
如果对细节还不清楚的小伙伴,可以单击这里。这里我们假设你已经对这个公式的基本原理了然于心。
在实践中,我们应该如何利用上述公式评估 RTOS 中一个任务的系统资源占用呢?这里我们就要借助 perf_counter 所提供的专门工具了。
熟悉我公众号文章的小伙伴都知道:perf_counter 是我维护的一个开源项目,致力于在不干扰用户原有SysTick功能的情况下,额外获得一个测量CPU时钟周期的能力,这就是大家很熟悉的运算符 __cycleof__()
/*! demo of __cycleof__() operation */
__cycleof__() {
printf("Hello world\r\n");
}
该运算符会测量花括号中代码所使用的CPU周期数。当然,如果不习惯这种奇怪的语法,也可以使用 perf_counter 所提供的简单API函数。比如,同样的功能也可以使用下面的代码来实现:
start_cycle_counter(); {
printf("Hello world\r\n");
}
int32_t cycles = stop_cycle_counter();
简单直观。
【如何测量一个任务的系统占用】
在RTOS中,假如我们有一个任务:
static void task_a (void *argument)
{
...
while (1) {
uint32_t wTick = osKernelGetTickCount();
any_workload(); // 我们用这个函数来模拟任意的功能代码
// 该任务尝试以 20 ms(50Hz) 为“稳定”间隔,周期性的执行
osDelayUntil(wTick + 20);
}
}
可以看出,该函数具有以下特点:
这样的任务在我们的日常应用中非常典型,随手动能举出一堆例子,比如:
如何才能方便的测量它的系统占用率呢?这是个好问题。
熟悉perf_counter的小伙伴也许会立即给出如下的答案:
static void task_a (void *argument)
{
...
while (1) {
int32_t cycle_used;
start_cycle_counter();
//! 以下是原来的要测量的代码,我们打包起来不动它们
{
uint32_t wTick = osKernelGetTickCount();
any_workload(); // 我们用这个函数来模拟任意的功能代码
// 该任务尝试以 20 ms(50Hz)为“稳定”间隔,周期性的执行
osDelayUntil(wTick + 20);
}
// 读取结果
cycle_used = stop_cycle_counter();
//! 把 cycle_used转化成毫秒,并保存在 cpu_usage里
float cpu_usage = (float)cycle_used / (float)(SystemCoreClock / 1000ul);
//! 计算CPU占用的百分比
cpu_usage = (cpu_usage / 20.0f) * 100.0f;
}
}
这段代码的思路总的来说是正确的:
但这里有个小瑕疵:osDelayUntil() 执行期间,当前任务其实是处于挂起状态——RTOS会进行任务调度,在该任务休眠期间执行别的任务——因此,不应该把这一期间的CPU周期数记录到最终结果里。实际上,正是因为这样的原因,上述代码的测量结果将始终是 100%——简直测了个寂寞。
既然知道问题出在哪里,就可以对上述代码进行小小的修改:
static void task_a (void *argument)
{
...
while (1) {
int32_t cycle_used;
start_cycle_counter();
//! 以下是原来的要测量的代码,我们打包起来不动它们
{
uint32_t wTick = osKernelGetTickCount();
any_workload(); // 我们用这个函数来模拟任意的功能代码
// 读取结果
cycle_used = stop_cycle_counter();
// 该任务尝试以 20 ms(50Hz)为“稳定”间隔,周期性的执行
osDelayUntil(wTick + 20);
}
//! 把 cycle_used转化成毫秒,并保存在 cpu_usage里
float cpu_usage = (float)cycle_used / (float)(SystemCoreClock / 1000ul);
//! 计算CPU占用的百分比
cpu_usage = (cpu_usage / 20.0f) * 100.0f;
}
}
这还真是头疼医头脚痛医脚啊……看似在osDelayUntil()之前读取结果就能获取 any_workload() 所使用的CPU周期数……但
你不考虑高优先级任务、中断之类在执行循环体的时候发生抢占么?抢占后的CPU周期数也要从测量结果中扣除啊!
是啊,RTOS 什么时候进行任务切换,我们也没法掌控啊。这可如何是好?
别慌,perf_counter 为此专门提供了一个RTOS特供版的API:
start_task_cycle_counter() 和 stop_task_cycle_counter() 来取代原有的版本——它们原理也不复杂,就是在RTOS的调度器加个“摄像头”,只要任务切换走了,就暂停当前任务的计数器,等到任务切换回来以后再重新打开——这就做到了只测量当前任务实际使用的尽周期数。
因此,上述代码可以简单的修改为:
static void task_a (void *argument)
{
init_task_cycle_counter();
...
while (1) {
int64_t cycle_used;
start_task_cycle_counter();
//! 以下是原来的要测量的代码,我们打包起来不动它们
{
uint32_t wTick = osKernelGetTickCount();
any_workload(); // 我们用这个函数来模拟任意的功能代码
// 该任务尝试以 20 ms(50Hz)为“稳定”间隔,周期性的执行
osDelayUntil(wTick + 20);
}
// 读取结果
cycle_used = stop_task_cycle_counter();
//! 把 cycle_used转化成毫秒,并保存在 cpu_usage里
float cpu_usage = (float)cycle_used / (float)(SystemCoreClock / 1000ul);
//! 计算CPU占用的百分比
cpu_usage = (cpu_usage / 20.0f) * 100.0f;
}
}
特别提示:别忘记每个任务一开始的地方用 init_task_cycle_counter() 初始化下当前任务的CPU计数器——为什么要这么做?因为perf_counter为每个任务都独立提供了一个默认的CPU计数器啊。
你以为我要说“完美”了么?原本我也是这么想的,感谢开源社区的甲方爸爸们,为了提了如下的需求:
这是最终完成稿:
static void task_a (void *argument)
{
init_task_cycle_counter();
...
__super_loop_monitor__(100) {
uint32_t wTick = osKernelGetTickCount();
any_workload(); // 我们用这个函数来模拟任意的功能代码
// 该任务尝试以 20 ms(50Hz) 为“稳定”间隔,周期性的执行
osDelayUntil(wTick + 20);
}
}
居然这么优雅?是的!上述代码只是:
那结果呢?结果输出在哪里?答案是:
static void task_a (void *argument)
{
init_task_cycle_counter();
int64_t cycle_used;
int64_t time_elapsed;
__super_loop_monitor__(100, {
cycle_used = __cpu_usage__.lTaskUsedCycles;
time_elapsed = __cpu_usage__.lTimeElapsed;
}) {
uint32_t wTick = osKernelGetTickCount();
any_workload(); // 我们用这个函数来模拟任意的功能代码
// 该任务尝试以 20 ms(50Hz) 为“稳定”间隔,周期性的执行
osDelayUntil(wTick + 20);
}
}
这里的语法结构是这样的:
__super_loop_monitor__(<平均多少次输出结果>,
{
//任意的代码片断,其中可以通过匿名结构体 __cpu_usage__
// 来读取所需的结果
}
) {
// 任务的超级循体
}
在前面的例子代码中,其核心是这样的:
int64_t cycle_used;
int64_t time_elapsed;
__super_loop_monitor__(100,
{
cycle_used = __cpu_usage__.lTaskUsedCycles;
time_elapsed = __cpu_usage__.lTimeElapsed;
}) {
...
}
【重要的使用注意事项】
在 Arm Compiler 5(armcc)中要选择 C99和GNU extensions选项:
如果你的MDK中找不到上述两个选项,则说明你的MDK版本太低了,推荐升级。你可以在关注公众号【裸机思维】后发送关键字"MDK"或者通过菜单获取最新的MDK网盘链接。
【perf_counter和RTOS补丁的部署】
最后,我们来说说 perf_counter 和对应 RTOS补丁的部署。
首先,perf_counter在MDK中是以 cmsis-pack的形式进行部署的,您可以在关注【裸机思维】公众号后,发送消息“perf_counter” 来获取最新的网盘链接。具体部署细节,可以参考文章《【喂到嘴边了的模块】超级嵌入式系统“性能/时间”工具箱》。
当我们成功安装对应的 cmsis-pack后,打开 RTE 窗口,可以找到到 Utilities 下的 perf_counter模块。将其展开后,勾选Core,并根据你所使用的RTOS种类选择一个对应的补丁:
可以看到, perf_counter支持了目前市面上比较主流的一些RTOS。目前,对列表中的大部分RTOS来说,勾选对应的patch就完成了对应的支持。
【ThreadX的注意事项】
你需要打开工程配置,跳转到 Asm界面,在Misc Controls中追加如下的命令行选项:
-include "Pre_Include_Global.h"
此外,目前 perf_counter的ThreadX补丁仅在 Arm Compiler 6(armclang)下有效,对于Arm Compiler 5(armcc)的用户来说,我尽力了……但很抱歉。
【RTX的注意事项】
在RTX配置中,找到 RTX_Config.h
在 Configuration Wizard界面下:
【其它注意事项】
1、需要特别说明的是:如果你使用的 RTOS 是使用 library 的形式部署的,请切换回 Source形式。
这是由于 Patch 所附着的一些 RTOS调度器函数可能会在 Library中以 inline 的形式存在——无法被我们的Patch附着——只有以Source源代码形式进行编译,Compiler才能意识到不能在这些关键的函数身上耍小聪明。
2、由于 perf_counter 是在每个任务的栈底消耗 48个字节作为CPU计数器,因此请务必关闭RTOS的栈溢出检测功能——因为这 48个字节显然破坏了水印,会导致栈溢出的误判。此外,perf_counter提供了额外的水印,如果你发现48个字节的最后4字节不是 0xDEADBEEF或者0x8492A53C,则说明发生了栈溢出。
3、由于 perf_counter 的API要访问 SysTick寄存器,因此请务必在配置RTOS的任务时,让其工作在特权模式下——如果RTOS任务工作在非特权模式下,任何针对SysTick寄存器的访问都将触发Hardfault。
【说在后面的话】
其实很多RTOS自己就提供了类似的功能,比如FreeRTOS和ThreadX就是这样,但也有一些RTOS并非如此,比如大家熟悉的 RT-Thread和RTX5。
一方面,perf_counter 为上述所有 RTOS 提供了测量功能和统一的API;另一方面,perf_counter 还对这些统计功能做了增强,比如:
perf_counter是一个寄放在Github上的开源项目,虽然小众但已经很多小伙伴的日常工作带来了便利——这让我非常自豪。
https://github.com/GorgonMeducer/perf_counter
你们的Star就是我更新的动力。谢谢