FreeRTOS系列第16篇---可视化追踪调试

时间:2022-11-02 20:44:11

      使用RTOS编程,为每个任务分配多大的堆栈空间就成了一项技术活:分配多了浪费系统资源,分配少了又恐怕会发生堆栈溢出。由于中断和抢占式调度器的存在,我们要估算出一个任务需要多少堆栈是非常困难的,今天我们就介绍一种方法,来获取每个任务的剩余堆栈空间。本文以NXP LPC177x_8x系列微控制器为例。

      我们将这个功能做成一个命令,添加到《FreeRTOS系列第15篇---使用任务通知实现命令行解释器》一文介绍的命令解释列表中。当程序运行一段时间后,我们在SecureCRT软件中输入命令“task”后回车,能看到如图1-1所示的任务信息。这里只有两个任务,其中堆栈一列中的数字,代表对应任务剩余的堆栈空间,单位是StackType_t类型,这个类型在移植层定义,一般定义为4字节。

FreeRTOS系列第16篇---可视化追踪调试

图1-1:任务信息

1.使能可视化追踪和运行时间统计功能     

      如图1-1所示,要实现堆栈使用量信息以及CPU使用率信息,必须将FreeRTOSConfig.h文件中的两个宏设置为1:

         #define configUSE_TRACE_FACILITY          1               #define configGENERATE_RUN_TIME_STATS 1

      第一个宏用来使能可视化追踪功能,第二个宏用来使能运行时间统计功能。如果第二个宏设置为1,则下面两个宏必须被定义:

  1. portCONFIGURE_TIMER_FOR_RUN_TIME_STATS():用户程序需要提供一个基准时钟函数,函数完成初始化基准时钟功能,这个函数要被define到宏portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()上。这是因为运行时间统计需要一个比系统节拍中断频率还要高分辨率的基准定时器,否则,统计可能不精确。基准定时器中断频率要比统节拍中断快10~100倍。基准定时器中断频率越快,统计越精准,但能统计的运行时间也越短(比如,基准定时器10ms中断一次,8位无符号整形变量可以计到2.55秒,但如果是1秒中断一次,8位无符号整形变量可以统计到255秒)。
  2. portGET_RUN_TIME_COUNTER_VALUE():用户程序需要提供一个返回基准时钟当前“时间”的函数,这个函数要被define到宏portGET_RUN_TIME_COUNTER_VALUE()上。

         我们使用定时器1来产生基准时钟,定时器1初始化函数为:

/*** 初始化计时定时器1,用于OS任务运行时间统计*/void init_timer1_for_runtime_state(void){    TIM_TIMERCFG_Type Timer0CfgType;                    Timer0CfgType.PrescaleOption=TIM_PRESCALE_USVAL;        //预分频的单位是微秒    Timer0CfgType.PrescaleValue=500;                        //预分频后为500微秒,    TIM_Init(LPC_TIM1,TIM_TIMER_MODE,&Timer0CfgType);       LPC_TIM1->TCR=0x01;}

      定时器1被配置成每隔500微秒,TC寄存器值增一。我们将定时器1的 TC寄存器值作为基准时钟当前时间。当TC寄存器值溢出时,大概要经过24.8天,这对于我们这个应用是足够的。

      在FreeRTOSConfig.h中,定义初始化基准定时器宏和获取当前时间宏:

extern void init_timer1_for_runtime_state(void);#define TIMER1_TC         ( * ( ( volatile uint32_t * )0x40008008 ) )#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() init_timer1_for_runtime_state()#define portGET_RUN_TIME_COUNTER_VALUE() TIMER1_TC

2.获取任务信息并格式化

      获取每个任务的状态信息使用的是API函数uxTaskGetSystemState(),该函数定义为:

UBaseType_tuxTaskGetSystemState(                       TaskStatus_t * constpxTaskStatusArray,                       const UBaseType_tuxArraySize,                       unsigned long * constpulTotalRunTime );

      函数uxTaskGetSystemState()向TaskStatus_t结构体填充相关信息,系统中每一个任务的信息都可以填充到TaskStatus_t结构体数组中,数组大小由uxArraySize指定。结构体TaskStatus_t定义如下:

typedef struct xTASK_STATUS{   /* 任务句柄*/   TaskHandle_t xHandle;    /* 指针,指向任务名*/   const signed char *pcTaskName;    /*任务ID,是一个独一无二的数字*/   UBaseType_t xTaskNumber;    /*填充结构体时,任务当前的状态(运行、就绪、挂起等等)*/   eTaskState eCurrentState;    /*填充结构体时,任务运行(或继承)的优先级。*/   UBaseType_t uxCurrentPriority;    /* 当任务因继承而改变优先级时,该变量保存任务最初的优先级。仅当configUSE_MUTEXES定义为1有效。*/   UBaseType_t uxBasePriority;    /* 分配给任务的总运行时间。仅当宏configGENERATE_RUN_TIME_STATS为1时有效。*/   unsigned long ulRunTimeCounter;    /* 从任务创建起,堆栈剩余的最小数量,这个值越接近0,堆栈溢出的可能越大。 */   unsigned short usStackHighWaterMark;}TaskStatus_t;

      注意,这个函数仅用来调试用,调用此函数会挂起所有任务,直到函数结束后才恢复挂起的任务,因此任务可能被挂起很长时间。在文件FreeRTOSConfig.h中,宏configUSE_TRACE_FACILITY必须设置为1,此函数才有效。

      由于我们不使用动态内存分配策略,所以实现定义了最大任务个数并预先分配好了存储任务状态信息的数组:

#defineMAX_TASK_NUM        5TaskStatus_tpxTaskStatusArray[MAX_TASK_NUM];

      正确调用函数uxTaskGetSystemState()后,任务的信息会被放在TaskStatus_t结构体中,我们需要将这些信息格式化为容易阅读的形式,并共通过串口打印到屏幕。完成这些功能的函数叫做get_task_state(),代码如下所示:

/*获取OS任务信息*/voidget_task_state(int32_t argc,void *cmd_arg){    const chartask_state[]={'r','R','B','S','D'};    volatile UBaseType_t uxArraySize, x;    uint32_t ulTotalRunTime,ulStatsAsPercentage;     /* 获取任务总数目 */    uxArraySize = uxTaskGetNumberOfTasks();   if(uxArraySize>MAX_TASK_NUM)    {        MY_DEBUGF(CMD_LINE_DEBUG,("当前任务数量过多!\n"));    }     /*获取每个任务的状态信息 */    uxArraySize = uxTaskGetSystemState(pxTaskStatusArray, uxArraySize, &ulTotalRunTime );     #if (configGENERATE_RUN_TIME_STATS==1)       MY_DEBUGF(CMD_LINE_DEBUG,("任务名      状态  ID    优先级  堆栈    CPU使用率\n"));     /* 避免除零错误 */    if( ulTotalRunTime > 0 )    {        /* 将获得的每一个任务状态信息部分的转化为程序员容易识别的字符串格式 */        for( x = 0; x < uxArraySize; x++ )        {            char tmp[128];                       /* 计算任务运行时间与总运行时间的百分比。*/            ulStatsAsPercentage =(uint64_t)(pxTaskStatusArray[ x ].ulRunTimeCounter)*100 / ulTotalRunTime;             if( ulStatsAsPercentage > 0UL )            {                sprintf(tmp,"%-12s%-6c%-6d%-8d%-8d%d%%",pxTaskStatusArray[ x].pcTaskName,task_state[pxTaskStatusArray[ x ].eCurrentState],                                                                       pxTaskStatusArray[ x ].xTaskNumber,pxTaskStatusArray[ x].uxCurrentPriority,                                                                       pxTaskStatusArray[ x ].usStackHighWaterMark,ulStatsAsPercentage);            }            else            {                /* 任务运行时间不足总运行时间的1%*/                sprintf(tmp,"%-12s%-6c%-6d%-8d%-8dt<1%%",pxTaskStatusArray[x ].pcTaskName,task_state[pxTaskStatusArray[ x ].eCurrentState],                                                                       pxTaskStatusArray[ x ].xTaskNumber,pxTaskStatusArray[ x].uxCurrentPriority,                                                                       pxTaskStatusArray[ x ].usStackHighWaterMark);                           }           MY_DEBUGF(CMD_LINE_DEBUG,("%s\n",tmp));        }    }    MY_DEBUGF(CMD_LINE_DEBUG,("任务状态:   r-运行  R-就绪  B-阻塞  S-挂起  D-删除\n"));    #endif //#if (configGENERATE_RUN_TIME_STATS==1)}

3.添加到命令解释列表

      在《FreeRTOS系列第15篇---使用任务通知实现命令行解释器》一文我们讲过了命令表,这里只需要将get_task_state()函数添加到命令列表中,命令设置为”task”,代码如下所示:

/*命令表*/const cmd_list_structcmd_list[]={/*   命令    参数数目    处理函数        帮助信息                                  */       {"?",       0,     handle_help,     "?                                  -打印帮助信息"},                     {"reset",   0,     handle_reset,    "reset                              -重启控制器"},    {"arg",     8,     handle_arg,      "arg<arg1> <arg2> ...               -测试用,打印输入的参数"},    {"hello",   0,     printf_hello,    "hello                              -打印HelloWorld!"},    {"task",    0,     get_task_state,  "task                               -获取任务信息"},};