STM32单片机uCOS-Ⅲ系统12 CPU利用率与堆栈检测
目录
一、CPU 利用率的基本概念及作用
二、CPU 利用率统计初始化
三、堆栈溢出检测概念及作用
四、堆栈溢出检测过程
五、统计任务 OS_StatTask()
六、堆栈检测 OSTaskStkChk()
七、任务堆栈大小的确定
八、实现
一、CPU 利用率的基本概念及作用
CPU 利用率其实就是系统运行的程序占用的 CPU 资源,表示机器在某段时间程序运行的情况,如果这段时间中,程序一直在占用 CPU 的使用权,那么可以认为 CPU 的利用率是 100%。 CPU 的利用率越高,说明机器在这个时间上运行了很多程序,反之较少。利用率的高低与 CPU 性能强弱有直接关系,就像一段一模一样的程序,如果使用运算速度很慢的 CPU,它可能要运行 1000ms,而使用很运算速度很快的 CPU 可能只需要 10ms,那么在1000ms 这段时间中,前者的 CPU 利用率就是 100%,而后者的 CPU 利用率只有 1%,因为1000ms 内前者都在使用 CPU 做运算,而后者只使用 10ms 的时间做运算,剩下的时间CPU 可以做其他事情。
uCOS 是多任务操作系统,对 CPU 都是分时使用的:比如 A 任务占用 10ms,然后 B任务占用 30ms,然后空闲 60ms,再又是 A 任务占 10ms, B 任务占 30ms,空闲 60ms;如果在一段时间内都是如此,那么这段时间内的利用率为 40%,因为整个系统中只有 40%的时间是 CPU 处理数据的时间。
一个系统设计的好坏,可以使用 CPU 利用率来衡量,一个好的系统必然是能完美响应急需的处理,并且系统的资源不会过于浪费(性价比高)。举个例子,假设一个系统的CPU 利用率经常在 90%~100%徘徊,那么系统就很少有空闲的时候,这时候突然有一些事情急需 CPU 的处理,但是此时 CPU 都很可能被其他任务在占用了,那么这个紧急事件就有可能无法被相应,即使能被相应,那么占用 CPU 的任务又处于等待状态,这种系统就是不够完美的,因为资源处理得太过于紧迫;反过来,假如 CPU 的利用率在 1%以下,那么我们就可以认为这种产品的资源过于浪费,搞一个那么好的 CPU 去干着没啥意义的活(大部分时间处于空闲状态),作为产品的设计,既不能让资源过于浪费,也不能让资源过于紧迫,这种设计才是完美的,在需要的时候能及时处理完突发事件,而且资源也不会过剩,性价比更高。
uCOS 提供的 CPU 利用率统计是一个可选功能,只有将 OS_CFG_STAT_TASK_EN 宏定义使能后用户才能使用 CPU 利用率统计相关函数, 该宏定义位于 os_cfg.h 文件中。
二、CPU 利用率统计初始化
uCOS 对 CPU 利用率进行统计是怎么实现的呢? 简单来说, CPU 利用率统计的原理很简单,我们知道,系统中必须存在空闲任务, 当且仅当 CPU 空闲的时候才会去执行空闲任务,那么我们就可以让 CPU 在空闲任务中一直做加法运算,假设某段时间 T 中 CPU 一直都在空闲任务中做加法运算(变量自加),那么这段时间算出来的值就是 CPU 空闲时候的最大值,我们假设为 100,那么当系统中有其他任务的时候, CPU 就不可能一直处于空闲任务做运算了,那么同样的一段时间 T 里,空闲任务算出来的值变成了 80,那么是不是可以说明空闲任务只占用了系统的 80%的资源,剩下的 20%被其他任务占用了,这是显而易见的,同样的,利用这个原理,我们就能知道 CPU 的利用率大约是多少了(这种计算不会很精确)。
假设 CPU 在 T 时间内空闲任务中运算的最大值为 OSStatTaskCtrMax(100),而有其他任务参与时候 T 时间内空闲任务运算的值为 80(OSStatTaskCtr),那么 CPU 的利 用 率 CPUUsage 的 公 式 应 该 为 : CPUUsage ( % ) = 100 *( 1- OSStatTaskCtr / OSStatTaskCtrMax) ,假设有一次空闲任务运算的值为 100( OSStatTaskCtr),说明没有其他任务参与,那么 CPU 的利用率就是 0%,如果 OSStatTaskCtr 的值为 0,那么表示这段时间里 CPU 都没在空闲任务中运算,那么 CPU 的利用率自然就是 100%。
三、堆栈溢出检测概念及作用
如果处理器有 MMU 或者 MPU,检测堆栈是否溢出是非常简单的, MMU 和 MPU 是处理器上特殊的硬件设施,可以检测非法访问,如果任务企图访问未被允许的内存空间的话, 就会产生警告, 但是我们使用的 STM32 是没有 MMU 和 MPU 的,但是可以使用软件模拟堆栈检测,但是软件的模拟比较难以实现, 但是 uCOS 为我们提供了堆栈使用情况统计的功能,直接使用即可,如果需要使用堆栈溢出检测的功能,就需要用户自己在App_OS_TaskSwHook()钩子函数中自定义实现(我们不实现该功能), 需要使用 uCOS 为我 们 提 供 的 堆 栈 检 测 功 能 , 想 要 使 用 该 功 能 就 需 要 在 os_cfg_app.h 文 件 中 将OS_CFG_STAT_TASK_STK_CHK_EN 宏定义配置为 1。
某些处理器中有一些堆栈溢出检测相关的寄存器, 当 CPU 的堆栈指针小于(或大于,取决于堆栈的生长方向)设置于这个寄存器的值时, 就会产生一个异常(中断), 异常处理程序就需要确保未允许访问空间代码的安全 (可能会发送警告给用户, 或者其他处理)。
任务控制块中的成员变量 StkLimitPtr 就是为这种目的而设置的,如图所示。每个任务的堆栈必须分配足够大的内存空间供任务使用, 在大多数情况下, StkLimitPtr 指针的值可以设置接近于栈顶( &TaskStk[0], 假定堆栈是从高地址往低地址生长的, 事实上 STM32的栈生长方向就是向下生长,从高地址向低地址生长), StkLimitPtr 的值在创建任务的时候由用户指定。
那么 uCOS 中对于没有 MPU 的处理器是怎么做到堆栈检测的呢?
当 uCOS 从一个任务切换到另一个任务的时候,它会调用一个钩子函数OSTaskSwHook(),它允许用户扩展上下文切换时的功能。所以,如果处理器没有硬件支持溢出检测功能,就可以在该钩子函数中添加代码软件模拟该功能。在切换到任务 B 前, 我们需要检测将要被载入 CPU 堆栈指针的值是否超出该任务 B 的任务控制块中 StkLimitPtr的限制。因为软件不能在溢出时就迅速地做出反应,所以应该设置 StkLimitPtr 的值尽可能远离栈顶,保证有足够的溢出缓冲, 具体见。 软件检测不会像硬件检测那样有效,但也可以有效防止堆栈溢出。
四、堆栈溢出检测过程
在前面的章节中我们已经详细讲解了堆栈相关的知识, 每个任务独立的堆栈空间对任务来说是至关重要的, 堆栈空间中保存了任务运行过程中需要保存局部变量、寄存器等重要的信息,如果设置的堆栈太小,任务无法正常运行,可能还会出现各种奇怪的错误,如果发现我们的程序出现奇怪的错误,一定要检查堆栈空间,包括 MSP 的堆栈,系统任务的堆栈,用户任务的堆栈。
μCOS 是怎么检测任务使用了多少堆栈的呢? 以 STM32 的堆栈生长方向为例子(高地址向低地址生长), 在任务初始化的时候先将任务所有的堆栈都置 0,使用后的堆栈不为 0,在检测的时候只需从堆栈的低地址开始对为 0 的堆栈空间进行计数统计, 然后通过计算就可以得出任务的堆栈使用了多少, 这样子用户就可以根据实际情况进行调整任务堆栈的大小, 这些信息同样也会在统计任务每隔 1/OSCfg_StatTaskRate_Hz 秒就进行更新。
五、统计任务 OS_StatTask()
uCOS 提供了统计任务的函数, 该函数为系统内部函数(任务),在使能宏定义OS_CFG_STAT_TASK_EN 后, 系统会自动创建一个统计任务——OS_StatTask(), 它会在任务中计算整个系统的 CPU 利用率,各个任务的 CPU 利用率和各个任务的堆栈使用信息。
六、堆栈检测 OSTaskStkChk()
uCOS 提供 OSTaskStkChk() 函数用来进行堆栈检测, 在使用之前必须将宏定义
OS_CFG_STAT_TASK_STK_CHK_EN 配置为 1,对于需要进行任务堆栈检测的任务,在
其被 OSTaskCreate()函数创建时,选项参数 opt 还需包含 OS_OPT_TASK_STK_CHK。统
计任务会以我们设定的运行频率不断更新堆栈使用的情况并且保存到任务控制块的 StkFree
和 StkUsed 成员变量中,这两个变量分别表示任务堆栈的剩余空间与已使用空间大小,单
位为任务堆栈大小的单位(在 STM32 中采用 4 字节)
七、任务堆栈大小的确定
任务堆栈的大小取决于该任务的需求, 设定堆栈大小时, 我们就需要考虑:所有可能被堆栈调用的函数及其函数的嵌套层数,相关局部变量的大小, 中断服务程序所需要的空间, 另外,堆栈还需存入 CPU 寄存器,如果处理器有浮点数单元 FPU 寄存器的话还需存入 FPU 寄存器。
嵌入式系统的潜规则,避免写递归函数,这样子可以人为计算出一个任务需要的堆栈空间大小,逐级嵌套所有可能被调用的函数, 计数被调用函数中所有的参数, 计算上下文切换时的 CPU 寄存器空间, 计算切换到中断时所需的 CPU 寄存器空间( 假如 CPU 没有独立的堆栈用于处理中断), 计算处理中断服务函数( ISR) 所需的堆栈空间, 将这些值相加即可得到任务最小的需求空间,但是我们不可能计算出精确的堆栈空间,我们通常会将这个值再乘以 1.5 到 2.0 以确保任务的安全运行。这个计算的值是假定在任务所有的执行路线都是已知的情况下的, 但这在真正的应用中并不太可能。
比如说,如果调用 printf()函数或者其它的函数, 这些函数所需要的空间是很难测得或者说就是不可能知道的, 在这种情况下, 我们这种人为计算任务堆栈大小的方法就变得不太可能了,那么我们可以在刚开始创建任务的时候给任务设置一个较大的堆栈空间, 并监测该任务运行时堆栈空间的实际使用量,运行一段时间后得到任务的最大堆栈使用情况(或者叫任务堆栈最坏结果),然后用该值乘 1.5 到 2.0 作为堆栈空间大小就差不多可以作为任务堆栈的空间大小,这样子得到的值就会比较精确一点,在调试阶段可以这样子进行测试, 发现崩溃就增大任务的堆栈空间,直到任务能正常稳定运行为止。
八、实现
#include <includes.h>
OS_SEM SemOfKey; //标志KEY1是否被单击的多值信号量
static OS_TCB AppTaskStartTCB;
static OS_TCB AppTaskLed1TCB;
static OS_TCB AppTaskLed2TCB;
static OS_TCB AppTaskUsartTCB;
static OS_TCB AppTaskStatusTCB;
static CPU_STK AppTaskStartStk[APP_TASK_START_STK_SIZE];
static CPU_STK AppTaskLed1Stk [ APP_TASK_LED1_STK_SIZE ];
static CPU_STK AppTaskLed2Stk [ APP_TASK_LED2_STK_SIZE ];
static CPU_STK AppTaskUsartStk [ APP_TASK_USART_STK_SIZE ];
static CPU_STK AppTaskStatusStk [ APP_TASK_STATUS_STK_SIZE ];
static void AppTaskStart (void *p_arg);
static void AppTaskLed1 ( void * p_arg );
static void AppTaskLed2 ( void * p_arg );
static void AppTaskUsart ( void * p_arg );
static void AppTaskStatus ( void * p_arg );
int main (void)
{
OS_ERR err;
OSInit(&err); /* Init uC/OS-III. */
OSTaskCreate((OS_TCB *)&AppTaskStartTCB, /* Create the start task */
(CPU_CHAR *)"App Task Start",
(OS_TASK_PTR ) AppTaskStart,
(void *) 0,
(OS_PRIO ) APP_TASK_START_PRIO,
(CPU_STK *)&AppTaskStartStk[0],
(CPU_STK_SIZE) APP_TASK_START_STK_SIZE / 10,
(CPU_STK_SIZE) APP_TASK_START_STK_SIZE,
(OS_MSG_QTY ) 5u,
(OS_TICK ) 0u,
(void *) 0,
(OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
(OS_ERR *)&err);
OSStart(&err); /* Start multitasking (i.e. give control to uC/OS-III). */
}
static void AppTaskStart (void *p_arg)
{
CPU_INT32U cpu_clk_freq;
CPU_INT32U cnts;
OS_ERR err;
(void)p_arg;
BSP_Init(); /* Initialize BSP functions */
CPU_Init();
cpu_clk_freq = BSP_CPU_ClkFreq(); /* Determine SysTick reference freq. */
cnts = cpu_clk_freq / (CPU_INT32U)OSCfg_TickRate_Hz; /* Determine nbr SysTick increments */
OS_CPU_SysTickInit(cnts); /* Init uC/OS periodic time src (SysTick). */
Mem_Init(); /* Initialize Memory Management Module */
#if OS_CFG_STAT_TASK_EN > 0u
OSStatTaskCPUUsageInit(&err); /* Compute CPU capacity with no task running */
#endif
CPU_IntDisMeasMaxCurReset();
/* 创建多值信号量 SemOfKey */
OSSemCreate((OS_SEM *)&SemOfKey, //指向信号量变量的指针
(CPU_CHAR *)"SemOfKey", //信号量的名字
(OS_SEM_CTR )1, //信号量这里是指示事件发生,所以赋值为0,表示事件还没有发生
(OS_ERR *)&err); //错误类型
/* Create the Led1 task */
OSTaskCreate((OS_TCB *)&AppTaskLed1TCB,
(CPU_CHAR *)"App Task Led1",
(OS_TASK_PTR ) AppTaskLed1,
(void *) 0,
(OS_PRIO ) APP_TASK_LED1_PRIO,
(CPU_STK *)&AppTaskLed1Stk[0],
(CPU_STK_SIZE) APP_TASK_LED1_STK_SIZE / 10,
(CPU_STK_SIZE) APP_TASK_LED1_STK_SIZE,
(OS_MSG_QTY ) 5u,
(OS_TICK ) 0u,
(void *) 0,
(OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
(OS_ERR *)&err);
/* Create the Led2 task */
OSTaskCreate((OS_TCB *)&AppTaskLed2TCB,
(CPU_CHAR *)"App Task Led2",
(OS_TASK_PTR ) AppTaskLed2,
(void *) 0,
(OS_PRIO ) APP_TASK_LED2_PRIO,
(CPU_STK *)&AppTaskLed2Stk[0],
(CPU_STK_SIZE) APP_TASK_LED2_STK_SIZE / 10,
(CPU_STK_SIZE) APP_TASK_LED2_STK_SIZE,
(OS_MSG_QTY ) 5u,
(OS_TICK ) 0u,
(void *) 0,
(OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
(OS_ERR *)&err);
/* Create the Usart task */
OSTaskCreate((OS_TCB *)&AppTaskUsartTCB,
(CPU_CHAR *)"App Task Usart",
(OS_TASK_PTR ) AppTaskUsart,
(void *) 0,
(OS_PRIO ) APP_TASK_USART_PRIO,
(CPU_STK *)&AppTaskUsartStk[0],
(CPU_STK_SIZE) APP_TASK_USART_STK_SIZE / 10,
(CPU_STK_SIZE) APP_TASK_USART_STK_SIZE,
(OS_MSG_QTY ) 5u,
(OS_TICK ) 0u,
(void *) 0,
(OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
(OS_ERR *)&err);
/* Create the status task */
OSTaskCreate((OS_TCB *)&AppTaskStatusTCB,
(CPU_CHAR *)"App Task Status",
(OS_TASK_PTR ) AppTaskStatus,
(void *) 0,
(OS_PRIO ) APP_TASK_STATUS_PRIO,
(CPU_STK *)&AppTaskStatusStk[0],
(CPU_STK_SIZE) APP_TASK_STATUS_STK_SIZE / 10,
(CPU_STK_SIZE) APP_TASK_STATUS_STK_SIZE,
(OS_MSG_QTY ) 5u,
(OS_TICK ) 0u,
(void *) 0,
(OS_OPT )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
(OS_ERR *)&err);
OSTaskDel ( & AppTaskStartTCB, & err );
}
static void AppTaskLed1 ( void * p_arg )
{
OS_ERR err;
uint32_t i;
(void)p_arg;
while (DEF_TRUE) { /* Task body, always written as an infinite loop. */
printf("AppTaskLed1 Running\n");
for(i=0;i<10000;i++) //模拟任务占用cpu
{
;
}
LED1_TOGGLE;
OSTimeDlyHMSM (0,0,0,500,OS_OPT_TIME_PERIODIC,&err);
}
}
static void AppTaskLed2 ( void * p_arg )
{
OS_ERR err;
uint32_t i;
(void)p_arg;
while (DEF_TRUE) { /* Task body, always written as an infinite loop. */
printf("AppTaskLed2 Running\n");
for(i=0;i<100000;i++) //模拟任务占用cpu
{
;
}
LED2_TOGGLE;
OSTimeDlyHMSM (0,0,0,500,OS_OPT_TIME_PERIODIC,&err);
}
}
static void AppTaskUsart ( void * p_arg )
{
OS_ERR err;
uint32_t i;
(void)p_arg;
while (DEF_TRUE) {
for(i=0;i<500000;i++) //模拟任务占用cpu
{
;
}
printf("AppTaskUsart Running\n");
OSTimeDlyHMSM (0,0,0,500,OS_OPT_TIME_PERIODIC,&err);
}
}
static void AppTaskStatus ( void * p_arg )
{
OS_ERR err;
CPU_SR_ALLOC();
(void)p_arg;
while (DEF_TRUE) {
OS_CRITICAL_ENTER(); //进入临界段,避免串口打印被打断
printf("------------------------------------------------------------\n");
printf ( "CPU利用率:%d.%d%%\r\n",
OSStatTaskCPUUsage / 100, OSStatTaskCPUUsage % 100 );
printf ( "CPU最大利用率:%d.%d%%\r\n",
OSStatTaskCPUUsageMax / 100, OSStatTaskCPUUsageMax % 100 );
printf ( "LED1任务的CPU利用率:%d.%d%%\r\n",
AppTaskLed1TCB.CPUUsageMax / 100, AppTaskLed1TCB.CPUUsageMax % 100 );
printf ( "LED1任务的CPU利用率:%d.%d%%\r\n",
AppTaskLed2TCB.CPUUsageMax / 100, AppTaskLed2TCB.CPUUsageMax % 100 );
printf ( "LED2任务的CPU利用率:%d.%d%%\r\n",
AppTaskUsartTCB.CPUUsageMax / 100, AppTaskUsartTCB.CPUUsageMax % 100 );
printf ( "统计任务的CPU利用率:%d.%d%%\r\n",
AppTaskStatusTCB.CPUUsageMax / 100, AppTaskStatusTCB.CPUUsageMax % 100 );
printf ( "LED1任务的已用和空闲堆栈大小分别为:%d,%d\r\n",
AppTaskLed1TCB.StkUsed, AppTaskLed1TCB.StkFree );
printf ( "LED2任务的已用和空闲堆栈大小分别为:%d,%d\r\n",
AppTaskLed2TCB.StkUsed, AppTaskLed2TCB.StkFree );
printf ( "Usart任务的已用和空闲堆栈大小分别为:%d,%d\r\n",
AppTaskUsartTCB.StkUsed, AppTaskUsartTCB.StkFree );
printf ( "统计任务的已用和空闲堆栈大小分别为:%d,%d\r\n",
AppTaskStatusTCB.StkUsed, AppTaskStatusTCB.StkFree );
printf("------------------------------------------------------------\n");
OS_CRITICAL_EXIT(); //退出临界段
OSTimeDlyHMSM (0,0,0,500,OS_OPT_TIME_PERIODIC,&err);
}
}