1. 调试
FreeRTOS提供了很多调试手段:
- 打印
- 断言:configASSERT
- Trace
- Hook函数(回调函数)
1.1 打印
printf:FreeRTOS工程里使用了microlib,里面实现了printf函数。
我们只需实现一下函数即可使用printf:
int fputc( int ch, FILE *f );
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 );
}
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中被立即调用。 |
1.4 Malloc Hook函数
编程时,一般的逻辑错误都容易解决。难以处理的是内存越界、栈溢出等。
内存越界经常发生在堆的使用过程总:堆,就是使用malloc得到的内存。
并没有很好的方法检测内存越界,但是可以提供一些回调函数:
- 使用pvPortMalloc失败时,如果在FreeRTOSConfig.h里配置 configUSE_MALLOC_FAILED_HOOK 为1,会调用:
void vApplicationMallocFailedHook( void );
1.5 栈溢出Hook函数
在切换任务(vTaskSwitchContext)时调用taskCHECK_FOR_STACK_OVERFLOW来检测栈是否溢出,如果溢出会调用:
void vApplicationStackOverflowHook( TaskHandle_t xTask, char * pcTaskName );
怎么判断栈溢出?有两种方法:
- 方法1:
- 当前任务被切换出去之前,它的整个运行现场都被保存在栈里,这时 很可能 就是它对栈的使用到达了峰值。
- 这方法很高效,但是并不精确
- 比如:任务在运行过程中调用了函数A大量地使用了栈,调用完函数A后才被调度。
-
方法2:
- 创建任务时,它的栈被填入固定的值,比如:0xa5
- 检测栈里最后16字节的数据,如果不是0xa5的话表示栈即将、或者已经被用完了
- 没有方法1快速,但是也足够快
- 能捕获 几乎所有 的栈溢出
- 为什么是几乎所有?可能有些函数使用栈时,非常凑巧地把栈设置为0xa5:几乎不可能
2.精算任务的栈大小
我们要使用到串口工具,来打印任务栈的信息
2.1只打印一个任务剩余的栈大小
在FreeRTOS中在给任务分配栈后,栈里面的内容全部会初始化为0xA5,我们会去调用一个函数,这个函数会从栈底向栈顶遍历直到这个存储空间的值不为0xA5,我们就将这个地址减去栈底地址,就得到了这个任务剩余栈空间大小。
UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask );
函数说明:
参数/返回值 | 说明 |
---|---|
xTask | 哪个任务,输入任务句柄 |
返回值 | 任务运行时、任务被切换时,都会用到栈。栈里原来值(0xa5)就会被覆盖。 逐个函数从栈的尾部判断栈的值连续为0xa5的个数, 它就是任务运行过程中空闲内存容量的最小值。 注意:假设从栈尾开始连续为0xa5的栈空间是N字节,返回值是N/4。 |
xTaskHandle = xTaskGetCurrentTaskHandle();//获取当前任务句柄
freeNum = uxTaskGetStackHighWaterMark(xTaskHandle);//获取剩余栈的大小
printf("FreeStack of Task %s : %d\n\r", pcTaskGetName(xTaskHandle), freeNum);//串口打印
2.2打印所有任务的栈信息
- vTaskList :获得任务的统计信息,形式为可读的字符串。注意,pcWriteBuffer必须足够大。
void vTaskList( signed char *pcWriteBuffer );
static signed char pcWriteBuffer[200];//全局的
//在空闲任务钩子函数中打印
vTaskList( pcWriteBuffer );
printf("%s\n\r",pcWriteBuffer );
可读信息格式如下:
注:每个任务的空闲栈最好留4*20个字节,防止临时添加变量栈溢出。
3.打印每个任务占用CPU率,并优化任务
3.1如何计算一个任务CPU的占比?
原理:计算一个周期内任务运行占用的时间比,我们Tick中断的计数值Count,但是对于CPU来讲这1ms太慢了,并且Tick中断作为计算任务运行时间太不精准了。有可能在一个任务执行时写了队列唤醒了更高优先级的任务切换了。我们得用别的方式来计算。
我们使用一个硬件定时器来作为时钟基准来计算。
在定时器中有两个寄存器Reload和Counter,定时器每发生一次中断,Counter会从0开始累加,累加到与Reload的值相同就产生一次中断。绿色时间怎么算呢?Counter/Reload*1000000(单位ns)。
我们想使用系统从开始到现在的运行时间函数需要对FreeRTOS进行相关配置。
3.2相关配置
- 配置
#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: 得到的信息是可读的字符串
3.3函数说明
- 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 );
可读信息格式如下:
3.3.1vTaskGetRunTimeStats()函数如何获取到底层时间的?
4.如何优化
剩余栈过大我们就缩小栈。
打印占用cpu率,根据任务占用CPU率是否正常来判断。比如:
任务没有触发执行但是占用CPU过大,比如在使用mpu6050时就算没有摇晃它它也会一直产生中断,这就需要我们去配置mpu6050相关寄存器,在有倾斜加速度时进入中断,这样就减少了mpu6050中断执行,避免了在中断里写队列去去唤醒任务,这样就减少了mpu6050任务占用CPU。