本章介绍了CPU负载计算相关原理,我们就来基于RTOS进行实践。
一.前言
这一篇我们来分享CPU负载计算的原理和实现,先介绍基本原理然后基于FreeRTOS的实现进行介绍。这里推荐一本书《嵌入式软件的时间分析——汽车行业领域的嵌入式软件理论,分析及实践》[英]皮特.格利瓦(Peter Gliwa)著。第四章介绍了相关的时间参数的定义,以及CPU负载的计算原理等。
这里顺带简单提一下RTOS和前后台程序进行低功耗处理的一般方式,了解一般架构也有助于介绍CPU负载的计算原理,RTOS和前后台程序有点不一样。
对于RTOS在无任务需要处理时,会执行到最低优先级的空闲任务,在空闲任务中可以使CPU进入WFI/WFE等待事件或者等待中断的状态,此时CPU不再执行,在系统滴答定时器中断时或者其他事件中断时唤醒CPU继续运行处理,然后重复上述过程。 当然这种低功耗处理不是很低。如果要更低,可以在空闲任务时进入更低功耗的模式比如深度睡眠模式等。进入不同程度的睡眠模式则需要不同的唤醒方式,比如一般睡眠可以通过一些外设中断唤醒,深度睡眠一般可以通过外部中断或者RTC唤醒,唤醒后可以接着原来的地方运行(当然前面一般会进入对应的唤醒源的中断处理)。而更深度的掉电模式一般只能通过复位唤醒,此时就是相当于软重启了。
对于前后台程序一般的处理方式是,主循环作为类似空闲任务的角色,功能代码在中断中进行处理,这和RTOS有点不一样。 中断中功能逻辑处理完后则进入主循环进入低功耗,进入低功耗的方式和前面介绍的一样。唤醒后则进入中断处理任务,处理完又进入主循环重新进入低功耗。当然也可以实现简单的调度,在主循环中实现功能代码处理和低功耗处理,但是没有前面介绍的方式自然,因为前后台程序本来就是中断进入前台处理任务,天然的就和低功耗的需求模式对应(即平常不工作对应后台,需要工作时中断到前台执行对应前台处理任务)。
对于活动的CPU即使无事情要做,CPU也会一直执行指令,所以在RTOS中有一个空闲任务执行一个死循环不执行任何功能代码(支持低功耗处理的则在此进入低功耗模式)。而对于前后台程序也是类似,无需要处理任何事情时主循环也是一直会执行。当然上述是不考虑低功耗处理的情况,低功耗的处理模式见上一节说明。
我们简单的理解CPU负载即CPU执行有用业务代码的时间占总时间的比例,当然我们实测时这个总时间一般是抽取一定时间间隔来进行,我们假设这个间隔时间为t(0), 而这段时间内执行有效功能代码的时间是t(e),那么CPU负载U=t(e)/t(0),是一个0~1的百分比范围。
而整个时间要么执行功能代码,要么执行空闲代码,所以假设执行空闲代码是时间为t(i)则
t(e)+t(i)=t(0),
所以上述式子也可以为
U=1-t(i)/t(0).
下图比较好的说明了该公式的原理,书中以U=t(e)/t(0)来进行说明的,t(e)即各功能任务的执行时间和,t(0)-t(e)为空闲任务的执行时间。
所以对应的计算U也有两种方式,一种是通过测量t(e)来进行,一种通过测量t(i)来进行,
前者需要累加所有功能任务的执行时间,而后者只需要测量空闲任务执行时间,相对而言后者更简单。对于RTOS,t(e)即各种任务执行的时间,需要累加所有任务的执行时间,其实会麻烦点,而t(i)即空闲任务的的执行时间,空闲任务只有一个这样比较好测量,所以使用U=1-t(i)/t(0),计算更方便。一般都是使用该方式计算的。
而上述公式还有一个比较重要的参数即t(0)如何选取,选取较小则可能无法真实测量出来有效的CPU负载,因为可能这个时间内都是在执行功能代码或者都是执行空闲代码,测量出来CPU负载要不是0要不是1,选取较大则计算时间可能过长,是长时间的平均值,计时数据可能溢出,且不能实时反应当前状态。所以一般t(0)需要根据具体情况而定,比较常用的可以考虑各个周期任务的最小公倍数,有些功能任务变化很大的,比如汽车的安全相关的设备,可能在触发初始条件前都是大部分时候空闲的,而检测到触发条件比如碰撞之后,后续会高密度的执行一系列检测,判断等功能任务执行较密集,这种不同时间点变化很大。后面这部分时间就需要设置t(0)较小,才能真实反应当前状态,前期可以设置较大。
对于前后台任务,如果是前面低功耗处理模式中介绍的中断中执行业务,主循环作为空闲的的模式,则测量主循环执行时间和总执行时间占比即可,把主循环执行时间作为空闲任务的执行时间。如果不是这种方式,那就根据实际测量空闲执行的时长即可。
当然上面还有一个隐藏的参数没有介绍,就是计时的方式,一般使用硬件定时进行计时,这里也需要注意硬件定时器设置的精度高则计时时长短,反之设置计时时间长则精度差,所以也要平衡选取,考虑时间的溢出问题。 很多CPU会提供一个高位宽比如64位的计数器专门用于类似的效率统计,比如RISC-V内核就有64位的计数器作为该功能用,当然没有类似计数器的则可以使用通用硬件定时器。
FreeRTOS中没有实时计算CPU负载的接口,但是提供了每个任务的当前总运行时间的参数,提供了每个任务运行以来的平均CPU占比。
所以我们可以基于以上信息,间接的的进行计算,即按照间隔时间t0去获取这段时间内,空闲任务执行时间的变化ti,1-ti/t0即近似CPU负载。
实际以上记录的就是空闲任务的以t0间隔的实时CPU使用率,类似的其他任务也可以获取。
即我们可以获取每个任务的实时CPU使用率,然后根据空闲任务的实时CPU使用率计算整个CPU的负载。
4.1 每个任务的运行时间统计
我们先介绍下FreeRTOS每个任务的运行时间的记录原理,以及每个任务的CPU使用率的统计方法。
FreeRTOS中每个任务的TCB中都有一个参数,在任务创建时调用prvInitialiseNewTask时初始化为0,当然相应的宏configGENERATE_RUN_TIME_STATS需要使能。
#if( configGENERATE_RUN_TIME_STATS == 1 )
uint32_t ulRunTimeCounter; /*< Stores the amount of time the task has spent in the Running state. */
#endif
以下接口可以获取空闲任务的执行时间参数,我们可以利用其来计算CPU负载。
#if( ( configGENERATE_RUN_TIME_STATS == 1 ) && ( INCLUDE_xTaskGetIdleTaskHandle == 1 ) )
TickType_t xTaskGetIdleRunTimeCounter( void )
{
return xIdleTaskHandle->ulRunTimeCounter;
}
#endif
vTaskGetInfo中也可以获取指定任务的该参数,当然该函数需要使能宏configUSE_TRACE_FACILITY
#if ( configGENERATE_RUN_TIME_STATS == 1 )
{
pxTaskStatus->ulRunTimeCounter = pxTCB->ulRunTimeCounter;
}
#else
{
pxTaskStatus->ulRunTimeCounter = 0;
}
#endif
任务执行时间的测量原理如下,在上下文切换时
即函数vTaskSwitchContext中如下代码,实现ulRunTimeCounter的累加,即累加该任务的执行时间。
#if ( configGENERATE_RUN_TIME_STATS == 1 )
{
#ifdef portALT_GET_RUN_TIME_COUNTER_VALUE
portALT_GET_RUN_TIME_COUNTER_VALUE( ulTotalRunTime );
#else
ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();
#endif
/* Add the amount of time the task has been running to the
accumulated time so far. The time the task started running was
stored in ulTaskSwitchedInTime. Note that there is no overflow
protection here so count values are only valid until the timer
overflows. The guard against negative values is to protect
against suspect run time stat counter implementations - which
are provided by the application, not the kernel. */
if ( ulTotalRunTime > ulTaskSwitchedInTime ) {
pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime );
} else {
mtCOVERAGE_TEST_MARKER();
}
ulTaskSwitchedInTime = ulTotalRunTime;
}
#endif /* configGENERATE_RUN_TIME_STATS */
如下图所示,假设A切换到B执行,此时
ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();获取的是当前时刻,
ulTaskSwitchedInTim是记录的任务A开始执行时的时刻,是在上一次vTaskSwitchContext时更新的。pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime );即累加当前A任务的执行时间。
然后ulTaskSwitchedInTime = ulTotalRunTime;更新全局变量ulTaskSwitchedInTime为当前时刻,即下一个任务B开始执行的时刻。
后续B的执行时间累加也是类似。
当然获取时间戳需要实现接口
portGET_RUN_TIME_COUNTER_VALUE或portALT_GET_RUN_TIME_COUNTER_VALUE
vTaskGetRunTimeStats可以打印每个任务执行时间占用总执行时间的比例。
需要配置对应的宏。#if ( ( configGENERATE_RUN_TIME_STATS == 1 ) && ( configUSE_STATS_FORMATTING_FUNCTIONS > 0 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
这里打印的是运行以来总的比例,不是实时的占比。
注意:以上实现,没有考虑定时器的溢出问题,
即(定时器绕回时( ulTotalRunTime - ulTaskSwitchedInTime )可能小于0.
所以依赖于接口
portALT_GET_RUN_TIME_COUNTER_VALUE或portGET_RUN_TIME_COUNTER_VALUE
必须不能溢出,必须保持单调递增,所以就需要合适的选择定时器的精度以兼顾总时长和精度。对于1us精度,32位最长时间只能记录1个多小时。
4.2 实现实时的CPU负载计算
获取当前时刻和空闲任务执行时长,隔一段时间在此获取当前时刻和空闲任务执行时长。
1 - 两次空闲任务时长差/当前时刻差即CPU负载, 注意获取当前时刻和空闲任务执行时长要保证其时刻一致性,所以使用临界段保护,避免获取完一个后调度执行其他任务回来时时间已经差了很多。
进入临界段
t00 = portGET_RUN_TIME_COUNTER_VALUE();
ti0=xTaskGetIdleRunTimeCounter();
退出临界段
进入临界段
t01 = portGET_RUN_TIME_COUNTER_VALUE()
ti1=xTaskGetIdleRunTimeCounter();
退出临界段
则计算的CPU负载百分比如下
U=100 - (ti1-ti0)*100/(t01-t00)
我这里在某个任务中,每隔10ms进行一次计算
#include "FreeRTOS.h"
#include "task.h"
任务循环
uint32_t t00,t01,ti0,ti1;
while(1)
{
Alloc_Critical();
Enter_Critical();
t00 = portGET_RUN_TIME_COUNTER_VALUE();
ti0 = xTaskGetIdleRunTimeCounter();
Exit_Critical();
//间隔20ms
Enter_Critical();
t01 = portGET_RUN_TIME_COUNTER_VALUE();
ti1= xTaskGetIdleRunTimeCounter();
Exit_Critical();
printf("cpu:%d\r\n",100-(ti1-ti0)*100/(t01-t00));
}
portmacro.h中定义获取时间戳的宏
#if ( configGENERATE_RUN_TIME_STATS == 1 )
extern void vPortRunTimerInit(void);
extern uint32_t vPortGetRunTimerValue(void);
extern uint32_t timer_get_time(void);
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() vPortRunTimerInit()
#define portGET_RUN_TIME_COUNTER_VALUE() timer_get_time()
#endif
打印如下
还可以使用上位机进行可视化
五.总结
前面我们介绍了CPU负载计算的原理实现等内容。
按照我们以往的文章的思路,介绍完一个知识点后我们还需要扩展下,到目前为止我们介绍完了CPU负载相关的原理和实现。但是上述数据其实不够形象化,我们可以把上述实时的数据通过串口,或者其他方式导入到电脑进行分析,可视化处理等。
但是这样太复杂,效率不高,实时性不够,我们想能不能直接直观的看到CPU负载的实时情况呢,有! 我们之前已经分享过类似的工程实践方法了,参考文章
《https://mp.weixin.qq.com/s/C8UgVLY-011w4GWQa7N6hg超级精简系列之:高效且运行时间固定的内存池实现》即介绍了使用IO翻转的方式来实时观察执行时间。
我们也可以类似的使用IO翻转的方式来观察空闲任务的时间,即在进入空闲任务(主循环空闲周期)内翻转IO,退出时再翻转IO,这样就可以通过实时的IO状态来查看空闲的占空比,密度,波动情况等,非常直观,可以快速发现波动大,变化异常的地方。可以对关心的任务,每个任务使用一个IO,使用逻辑分析仪就可以实时观察多个任务的情况。
当然以上其实没有考虑上下文切换,中断执行等时间,按照空闲任务执行时间计算时,因为是反的,算出来的CPU负载是包括这些时间的。如果是累加各任务的执行时间来算的则没有包括这些时间,算出来负载值要偏小。使用IO翻转方式时我们可以类似的使用IO观测中断执行时间,上下文切换时间等。