跳到主要内容

第18章 调试与优化

18.1 调试

FreeRTOS提供了很多调试手段:

  • 打印
  • 断言:configASSERT
  • Trace
  • Hook函数(回调函数)

18.1.1 打印

printf:FreeRTOS工程里使用了microlib,里面实现了printf函数。

我们只需实现一下函数即可使用printf:

int fputc( int ch, FILE *f );

18.1.2 断言

一般的C库里面,断言就是一个函数:

void assert(scalar expression);

它的作用是:确认expression必须为真,如果expression为假的话就中止程序。

在FreeRTOS里,使用configASSERT(),比如:

##define configASSERT(x) if (!x) while(1);

我们可以让它提供更多信息,比如:

##define configASSERT(x) \
if (!x) \
{
printf("%s %s %d\r\n", __FILE__, __FUNCTION__, __LINE__); \
while(1); \
}

configASSERT(x)中,如果x为假,表示发生了很严重的错误,必须停止系统的运行。

它用在很多场合,比如:

  • 队列操作
BaseType_t xQueueGenericSend( QueueHandle_t xQueue,
const void * const pvItemToQueue,
TickType_t xTicksToWait,
const BaseType_t xCopyPosition )
{
BaseType_t xEntryTimeSet = pdFALSE, xYieldRequired;
TimeOut_t xTimeOut;
Queue_t * const pxQueue = xQueue;

configASSERT( pxQueue );
configASSERT(!((pvItemToQueue == NULL) && (pxQueue->uxItemSize != (UBaseType_t)0U)));
configASSERT( !((xCopyPosition == queueOVERWRITE) && (pxQueue->uxLength != 1 )));
  • 中断级别的判断
void vPortValidateInterruptPriority( void )
{
uint32_t ulCurrentInterrupt;
uint8_t ucCurrentPriority;

/* Obtain the number of the currently executing interrupt. */
ulCurrentInterrupt = vPortGetIPSR();

/* Is the interrupt number a user defined interrupt? */
if( ulCurrentInterrupt >= portFIRST_USER_INTERRUPT_NUMBER )
{
/* Look up the interrupt's priority. */
ucCurrentPriority = pcInterruptPriorityRegisters[ ulCurrentInterrupt ];

configASSERT( ucCurrentPriority >= ucMaxSysCallPriority );

}

18.1.3 Trace

FreeRTOS中定义了很多trace开头的宏,这些宏被放在系统个关键位置。

它们一般都是空的宏,这不会影响代码:不影响编程处理的程序大小、不影响运行时间。

我们要调试某些功能时,可以修改宏:修改某些标记变量、打印信息等待。

trace宏描述
traceTASK_INCREMENT_TICK(xTickCount)当tick计数自增之前此宏函数被调用。参数xTickCount当前的Tick值,它还没有增加。
traceTASK_SWITCHED_OUT()vTaskSwitchContext中,把当前任务切换出去之前调用此宏函数。
traceTASK_SWITCHED_IN()vTaskSwitchContext中,新的任务已经被切换进来了,就调用此函数。
traceBLOCKING_ON_QUEUE_RECEIVE(pxQueue)当正在执行的当前任务因为试图去读取一个空的队列、信号或者互斥量而进入阻塞状态时,此函数会被立即调用。参数pxQueue保存的是试图读取的目标队列、信号或者互斥量的句柄,传递给此宏函数。
traceBLOCKING_ON_QUEUE_SEND(pxQueue)当正在执行的当前任务因为试图往一个已经写满的队列或者信号或者互斥量而进入了阻塞状态时,此函数会被立即调用。参数pxQueue保存的是试图写入的目标队列、信号或者互斥量的句柄,传递给此宏函数。
traceQUEUE_SEND(pxQueue)当一个队列或者信号发送成功时,此宏函数会在内核函数xQueueSend(),xQueueSendToFront(),xQueueSendToBack(),以及所有的信号give函数中被调用,参数pxQueue是要发送的目标队列或信号的句柄,传递给此宏函数。
traceQUEUE_SEND_FAILED(pxQueue)当一个队列或者信号发送失败时,此宏函数会在内核函数xQueueSend(),xQueueSendToFront(),xQueueSendToBack(),以及所有的信号give函数中被调用,参数pxQueue是要发送的目标队列或信号的句柄,传递给此宏函数。
traceQUEUE_RECEIVE(pxQueue)当读取一个队列或者接收信号成功时,此宏函数会在内核函数xQueueReceive()以及所有的信号take函数中被调用,参数pxQueue是要接收的目标队列或信号的句柄,传递给此宏函数。
traceQUEUE_RECEIVE_FAILED(pxQueue)当读取一个队列或者接收信号失败时,此宏函数会在内核函数xQueueReceive()以及所有的信号take函数中被调用,参数pxQueue是要接收的目标队列或信号的句柄,传递给此宏函数。
traceQUEUE_SEND_FROM_ISR(pxQueue)当在中断中发送一个队列成功时,此函数会在xQueueSendFromISR()中被调用。参数pxQueue是要发送的目标队列的句柄。
traceQUEUE_SEND_FROM_ISR_FAILED(pxQueue)当在中断中发送一个队列失败时,此函数会在xQueueSendFromISR()中被调用。参数pxQueue是要发送的目标队列的句柄。
traceQUEUE_RECEIVE_FROM_ISR(pxQueue)当在中断中读取一个队列成功时,此函数会在xQueueReceiveFromISR()中被调用。参数pxQueue是要发送的目标队列的句柄。
traceQUEUE_RECEIVE_FROM_ISR_FAILED(pxQueue)当在中断中读取一个队列失败时,此函数会在xQueueReceiveFromISR()中被调用。参数pxQueue是要发送的目标队列的句柄。
traceTASK_DELAY_UNTIL()当一个任务因为调用了vTaskDelayUntil()进入了阻塞状态的前一刻此宏函数会在vTaskDelayUntil()中被立即调用。
traceTASK_DELAY()当一个任务因为调用了vTaskDelay()进入了阻塞状态的前一刻此宏函数会在vTaskDelay中被立即调用。

18.1.4 Malloc Hook函数

编程时,一般的逻辑错误都容易解决。难以处理的是内存越界、栈溢出等。

内存越界经常发生在堆的使用过程总:堆,就是使用malloc得到的内存。

并没有很好的方法检测内存越界,但是可以提供一些回调函数:

  • 使用pvPortMalloc失败时,如果在FreeRTOSConfig.h里配置configUSE_MALLOC_FAILED_HOOK为1,会调用:
void vApplicationMallocFailedHook( void );

18.1.5 栈溢出Hook函数

在切换任务(vTaskSwitchContext)时调用taskCHECK_FOR_STACK_OVERFLOW来检测栈是否溢出,如果溢出会调用:

void vApplicationStackOverflowHook( TaskHandle_t xTask, char * pcTaskName );

怎么判断栈溢出?有两种方法:

  • 方法1:
    • 当前任务被切换出去之前,它的整个运行现场都被保存在栈里,这时很可能就是它对栈的使用到达了峰值。
    • 这方法很高效,但是并不精确
    • 比如:任务在运行过程中调用了函数A大量地使用了栈,调用完函数A后才被调度。

  • 方法2:
    • 创建任务时,它的栈被填入固定的值,比如:0xa5
    • 检测栈里最后16字节的数据,如果不是0xa5的话表示栈即将、或者已经被用完了
    • 没有方法1快速,但是也足够快
    • 能捕获几乎所有的栈溢出
    • 为什么是几乎所有?可能有些函数使用栈时,非常凑巧地把栈设置为0xa5:几乎不可能

18.2 优化

在Windows中,当系统卡顿时我们可以查看任务管理器找到最消耗CPU资源的程序。

在FreeRTOS中,我们也可以查看任务使用CPU的情况、使用栈的情况,然后针对性地进行优化。

这就是查看"任务的统计"信息。

18.2.1 栈使用情况

在创建任务时分配了栈,可以填入固定的数值比如0xa5,以后可以使用以下函数查看"栈的高水位",也就是还有多少空余的栈空间:

UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask );

原理是:从栈底往栈顶逐个字节地判断,它们的值持续是0xa5就表示它是空闲的。 函数说明:

参数/返回值说明
xTask哪个任务
返回值任务运行时、任务被切换时,都会用到栈。栈里原来值(0xa5)就会被覆盖。 逐个函数从栈的尾部判断栈的值连续为0xa5的个数, 它就是任务运行过程中空闲内存容量的最小值。 注意:假设从栈尾开始连续为0xa5的栈空间是N字节,返回值是N/4。

18.2.2 任务运行时间统计

对于同优先级的任务,它们按照时间片轮流运行:你执行一个Tick,我执行一个Tick。

是否可以在Tick中断函数中,统计当前任务的累计运行时间?

不行!很不精确,因为有更高优先级的任务就绪时,当前任务还没运行一个完整的Tick就被抢占了。

我们需要比Tick更快的时钟,比如Tick周期时1ms,我们可以使用另一个定时器,让它发生中断的周期时0.1ms甚至更短。

使用这个定时器来衡量一个任务的运行时间,原理如下图所示:

  • 切换到Task1时,使用更快的定时器记录当前时间T1
  • Task1被切换出去时,使用更快的定时器记录当前时间T4
  • (T4-T1)就是它运行的时间,累加起来
  • 关键点:在vTaskSwitchContext函数中,使用更快的定时器统计运行时间

18.2.3 涉及的代码

  • 配置
#define configGENERATE_RUN_TIME_STATS 1
#define configUSE_TRACE_FACILITY 1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
  • 实现宏portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(),它用来初始化更快的定时器
  • 实现这两个宏之一,它们用来返回当前时钟值(更快的定时器)
    • portGET_RUN_TIME_COUNTER_VALUE():直接返回时钟值
    • portALT_GET_RUN_TIME_COUNTER_VALUE(Time):设置Time变量等于时钟值

代码执行流程:

  • 初始化更快的定时器:启动调度器时

在任务切换时统计运行时间

  • 获得统计信息,可以使用下列函数
    • uxTaskGetSystemState:对于每个任务它的统计信息都放在一个TaskStatus_t结构体里
    • vTaskList:得到的信息是可读的字符串,比如
    • vTaskGetRunTimeStats: 得到的信息是可读的字符串

18.2.4 函数说明

  • uxTaskGetSystemState:获得任务的统计信息
UBaseType_t uxTaskGetSystemState( TaskStatus_t * const pxTaskStatusArray,
const UBaseType_t uxArraySize,
uint32_t * const pulTotalRunTime );
参数描述
pxTaskStatusArray指向一个TaskStatus_t结构体数组,用来保存任务的统计信息。 有多少个任务?可以用*uxTaskGetNumberOfTasks()*来获得。
uxArraySize数组大小、数组项个数,必须大于或等于uxTaskGetNumberOfTasks()
pulTotalRunTime用来保存当前总的运行时间(更快的定时器),可以传入NULL
返回值传入的pxTaskStatusArray数组,被设置了几个数组项。 注意:如果传入的uxArraySize小于uxTaskGetNumberOfTasks(),返回值就是0
  • vTaskList :获得任务的统计信息,形式为可读的字符串。注意,pcWriteBuffer必须足够大。 void vTaskList( signed char *pcWriteBuffer ); 可读信息格式如下:

  • vTaskGetRunTimeStats:获得任务的运行信息,形式为可读的字符串。注意,pcWriteBuffer必须足够大。
void vTaskGetRunTimeStats( signed char *pcWriteBuffer );

可读信息格式如下:

image7

18.3 示例25: 统计单个任务的栈信息

本节代码为:1801_stack_state,主要看applications/led_app.c。

在src/user_tasks_entry.c中在创建LED任务时,设置的栈大小为128*4,代码如下:

31 xTaskCreate(led_thread_entry, "led_task", 128, NULL, 1, NULL);

这个栈是否太大了?可以在LED任务中打印栈信息,代码如下:

09 void led_thread_entry(void * pvParameters)
10 {
11 /* TODO: add your own code here */
12 UBaseType_t freeNum;
13 TaskHandle_t xTaskHandle;
/* 省略 */
20 while(1)
21 {
22 xTaskHandle = xTaskGetCurrentTaskHandle();
23 freeNum = uxTaskGetStackHighWaterMark(xTaskHandle);
24 printf("FreeStack of Task %s : %d\n\r", pcTaskGetName(xTaskHandle), freeNum);
25
26 pLedDev->Write(pLedDev, state);
27 state = !state;
28 R_BSP_SoftwareDelay(500, BSP_DELAY_UNITS_MILLISECONDS);
29 }
30 }

第22行:获得当前任务的句柄。 第23行:获得当前任务的“栈高水位”,即:freeNum为空闲栈大小(单位为4字节)。 第24行:打印。

需要使用RASC进行配置,uxTaskGetStackHighWaterMark才会被使能,如下操作:

image8

实验现象:在串口打印如下栈信息。如下:

FreeStack of Task led_task : 70
FreeStack of Task led_task : 70
FreeStack of Task led_task : 68
FreeStack of Task led_task : 68
FreeStack of Task led_task : 68
FreeStack of Task led_task : 68
FreeStack of Task led_task : 68
FreeStack of Task led_task : 68
FreeStack of Task led_task : 68
FreeStack of Task led_task : 68
FreeStack of Task led_task : 68
FreeStack of Task led_task : 68
FreeStack of Task led_task : 66
FreeStack of Task led_task : 66
FreeStack of Task led_task : 66

当程序运行足够长时间后,栈信息就稳定了。根据上述信息,在创建LED任务时可以把栈设置得小一点。原来的栈大小为128,剩余66,那么128-66= 62就够用了。当然不要那么严格,否则在LED任务里稍微增加一个局部变量、甚至稍微调整编译优化等级,都可能导致栈不够用。建议栈大小为实际使用量的1.5倍、2倍。

18.4 示例26: 统计所有任务的栈信息

本节代码为:1802_all_stack_state,主要看src/user_tasks_entry.c。

要打印所有任务的栈信息,在空闲任务里进行最合适。需要实现空闲任务的钩子函数vApplicationIdleHook。

先在RASC里使能这个钩子函数和其他统计函数,如下操作:

image9

然后在src/user_tasks_entry.c中实现空闲任务钩子函数,代码如下:

40 void vApplicationIdleHook( void )
41 {
42 static signed char pcWriteBuffer[200];
43 int i;
44 vTaskList(pcWriteBuffer);
45 for (i = 0; i < 16; i++)
46 printf("-");
47 printf("\n\r");
48 printf("%s\n\r", pcWriteBuffer);
49 &#125;

当运行这个程序时,发现串口上并没有打印信息,这表示空闲任务一直无法运行。原因是LED任务的延时函数是“死循环等待”,导致LED任务不阻塞,而LED任务的优先级比空闲任务高,所以空闲任务无法运行。如需修改applications/led_app.c:

20 while(1)
21 &#123;
22 //xTaskHandle = xTaskGetCurrentTaskHandle();
23 //freeNum = uxTaskGetStackHighWaterMark(xTaskHandle);
24 //printf("FreeStack of Task %s : %d\n\r", pcTaskGetName(xTaskHandle), freeNum);
25
26 pLedDev->Write(pLedDev, state);
27 state = !state;
28 //R_BSP_SoftwareDelay(500, BSP_DELAY_UNITS_MILLISECONDS);
29 vTaskDelay(500);
30 &#125;
31 &#125;

把第28行注释掉,改为第29行。

实验现象:在串口上打印出任务信息。如下:

----------------
IDLE X 0 52 2
game1_task B 1 94 6
led_task B 1 106 5
Tmr Svc B 1 28 3
touch_task B 1 54 4
platform_task B 1 206 9
InputTask B 1 86 8

第1列是任务名,第2列是任务状态,第3列是任务优先级,第4列是空闲栈大小,第5列是任务编号。 以led_task为例,它的空闲栈是106,跟1801_stack_state相比,本工程里led_task没有使用printf,所以栈的消耗更小。

18.5 示例27: 统计任务CPU占用率

本节代码为:1803_cpu_usage,主要看src/user_tasks_entry.c。

要带你所有任务的栈信息,在空闲任务里进行最合适。需要实现空闲任务的钩子函数vApplicationIdleHook。

先在RASC里使能这个钩子函数和其他统计函数,如下操作:

image10

然后在src/user_tasks_entry.c中实现空闲任务钩子函数,代码如下:

41 &#123;
42 static signed char pcWriteBuffer[200];
43 int i;
44 //vTaskList(pcWriteBuffer);
45 vTaskGetRunTimeStats(pcWriteBuffer);
46 for (i = 0; i < 16; i++)
47 printf("-");
48 printf("\n\r");
49 printf("%s\n\r", pcWriteBuffer);
50 &#125;

第45行:获得各个任务的CPU占用率。

要想使用vTaskGetRunTimeStats,需要提供更精确的读取时间函数,我们已经有了“system_get_us”函数,只需要修

改“ra/aws/FreeRTOS/FreeRTOS/Source/include/FreeRTOS.h”,代码如下:

759 #if ( configGENERATE_RUN_TIME_STATS == 1 )
760
761 #define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()
762 #define portGET_RUN_TIME_COUNTER_VALUE() system_get_us()

实验现象:在串口上打印出任务信息。如下:

----------------
IDLE 263399270 68%
game1_task 20115760 5%
led_task 10 <1%
Tmr Svc 5030370 1%
touch_task 94024730 24%
platform_task 150 <1%
InputTask 10 <1%

可以看到touch_ask的CPU占用率太高了。它使用查询的方式不断读取触摸屏数据,平时不操作触摸屏时也会读取。可以改进这任务,让它平时阻塞,当点击触摸屏产生中断时,在中断里把任务唤醒。