调试
0,打印状态(略)
1,断言
在编程中,断言是一种一阶逻辑,即一个结果为真或假的逻辑判断式。其目的是表示与验证软件开发者预期的结果。当程序执行到断言的位置时,对应的断言应该为真。如果断言不为真,程序会中止执行,并给出错误信息。断言在编程中是一个关键工具,用于检测程序中的各种预期条件是否满足,帮助开发者迅速定位问题。它常用于安全性检查、代码重构和性能优化等方面。
一般的C库里面,断言就是一个函数:
assert(0);
#define assert(expr) ((expr) ? (void)0 : assert_failed(__FILE__, __LINE__))#ifdef DEBUG
/* Keep the linker happy. */
void assert_failed( unsigned char* pcFile, unsigned long ulLine )
{printf("Wrong parameters value: file %s on line %d\r\n", pcFile, ulLine) ; for( ;; ){}
}
#endif
测试效果
int main( void )
{TaskHandle_t xHandleTask1;#ifdef DEBUGdebug();
#endifprvSetupHardware();printf("Hello, world!\r\n");assert(0);
}
在FreeRTOS里,使用configASSERT()
,比如:
configASSERT(0);
#define configASSERT(x) if (x==0){printf("%s %s %d\r\n", __FILE__, __FUNCTION__, __LINE__); while(1); }
测试效果
int main( void )
{TaskHandle_t xHandleTask1;#ifdef DEBUGdebug();
#endifprvSetupHardware();printf("Hello, world!\r\n");configASSERT(0);
}
2,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中被立即调用。 |
3,堆溢出钩子函数vApplicationMallocFailedHook
编程时,一般的逻辑错误都容易解决。难以处理的是内存越界、栈溢出等。
内存越界经常发生在堆的使用过程总:堆,就是使用malloc得到的内存。
并没有很好的方法检测内存越界,但是可以提供一些回调函数:
vApplicationMallocFailedHook
是 FreeRTOS 中的一个钩子函数(hook function),当内存分配失败时,FreeRTOS 会调用这个函数。这个钩子函数是可选的,用户可以根据自己的需要来实现它。它的主要目的是提供一种机制,让应用程序在内存分配失败时能够执行特定的操作,例如记录错误信息、进入安全模式或者执行其他恢复操作。
下面是一个简单的 vApplicationMallocFailedHook
函数的实现示例,它仅打印一条错误消息并进入一个无限循环:
#include "FreeRTOS.h"
#include "task.h"
#define configUSE_MALLOC_FAILED_HOOK 1/* 钩子函数,当内存分配失败时被调用 */
void vApplicationMallocFailedHook( void )
{ /* 这是一个错误情况,通常意味着系统没有足够的内存来继续运行 */ /* 打印错误消息 */ configPRINT_STRING( "Malloc Failed Hook called\n" ); /* 进入一个无限循环,因为此时系统可能处于不稳定状态 */ for( ;; ) { /* 什么都不做,或者可以尝试一些恢复操作 */ }
}
在这个例子中,configPRINT_STRING
是一个宏,用于将字符串发送到某个输出设备(例如串口)。这个宏的具体实现取决于你的应用程序配置和所使用的硬件平台。
请注意,这个函数的具体实现应该根据你的应用需求来定制。例如,在某些应用中,你可能希望尝试释放一些不再需要的资源,或者进入某种安全模式来避免系统崩溃。此外,如果系统检测到内存分配失败,这通常意味着系统已经处于资源紧张的状态,因此在这个钩子函数中执行的操作应该尽可能简单和快速。
确保你已经在你的 FreeRTOS 配置文件中启用了内存分配失败钩子(configUSE_MALLOC_FAILED_HOOK
),并将其设置为 1
,这样 vApplicationMallocFailedHook
才会在内存分配失败时被调用。
4,栈溢出钩子函数vApplicationStackOverflowHook
在切换任务(vTaskSwitchContext)时调用taskCHECK_FOR_STACK_OVERFLOW来检测栈是否溢出,如果溢出会调用:
void vApplicationStackOverflowHook( TaskHandle_t xTask, char * pcTaskName );
怎么判断栈溢出?有两种方法:
方法1:
- 当前任务被切换出去之前,它的整个运行现场都被保存在栈里,这时很可能就是它对栈的使用到达了峰值。
- 这方法很高效,但是并不精确
- 比如:任务在运行过程中调用了函数A大量地使用了栈,调用完函数A后才被调度。
#ifndef configCHECK_FOR_STACK_OVERFLOW#define configCHECK_FOR_STACK_OVERFLOW 1
#endif#if ( ( configCHECK_FOR_STACK_OVERFLOW == 1 ) && ( portSTACK_GROWTH < 0 ) )/* Only the current stack state is to be checked. */#define taskCHECK_FOR_STACK_OVERFLOW() \{ \/* Is the currently saved stack pointer within the stack limit? */ \if( pxCurrentTCB->pxTopOfStack <= pxCurrentTCB->pxStack ) \{ \vApplicationStackOverflowHook( ( TaskHandle_t ) pxCurrentTCB, pxCurrentTCB->pcTaskName ); \} \}#endif /* configCHECK_FOR_STACK_OVERFLOW == 1 */
1,定义configCHECK_FOR_STACK_OVERFLOW 为1
2,
pxTopOfStack
是pxCurrentTCB
中的一个成员,表示当前任务的栈顶指针。栈顶指针指向栈中最后一个有效位置之后的地址。当任务执行函数调用或局部变量压栈时,栈顶指针会向低地址方向移动。
pxStack
也是pxCurrentTCB
中的一个成员,表示当前任务的栈底指针。栈底指针指向栈的起始位置。
if( pxCurrentTCB->pxTopOfStack <= pxCurrentTCB->pxStack )
这个条件判断是在检查栈顶指针是否低于或等于栈底指针。如果是这样,那么意味着栈已经溢出,因为栈顶指针不应该回到或超过栈的起始位置。
在TCB结构体中,
pxStack
,表示当前任务的栈底指针,分配栈空间时就固定了。
pxTopOfStack
,表示当前任务的栈顶指针,调用函数时候,会向下压栈
如果pxTopOfStack
<=pxStack
表示栈溢出。就去调用栈溢出的钩子函数
3,上诉提到使用这种方法不够准确的原因是:
在TASKA() while 循环A()函数过程中,如果需要分配100个字节(定义的栈空间也是100),相当于栈溢出了,pxTopOfStack
并没有实时更新,而是在退出任务A保存现场的时候才更新TCB。
但如果在此处发生了切换,
又被释放掉栈空间,pxTopOfStack
又回到起始位置,并没有真实的记录原先栈溢出。
方法2:
- 创建任务时,它的栈被填入固定的值,比如:0xa5
- 检测栈里最后16字节的数据,如果不是0xa5的话表示栈即将、或者已经被用完了
- 没有方法1快速,但是也足够快
- 能捕获几乎所有的栈溢出
- 为什么是几乎所有?可能有些函数使用栈时,非常凑巧地把栈设置为0xa5:几乎不可能
#ifndef configCHECK_FOR_STACK_OVERFLOW#define configCHECK_FOR_STACK_OVERFLOW 2
#endif#if ( ( configCHECK_FOR_STACK_OVERFLOW > 1 ) && ( portSTACK_GROWTH < 0 ) )#define taskCHECK_FOR_STACK_OVERFLOW() \{ \const uint32_t * const pulStack = ( uint32_t * ) pxCurrentTCB->pxStack; \const uint32_t ulCheckValue = ( uint32_t ) 0xa5a5a5a5; \\if( ( pulStack[ 0 ] != ulCheckValue ) || \( pulStack[ 1 ] != ulCheckValue ) || \( pulStack[ 2 ] != ulCheckValue ) || \( pulStack[ 3 ] != ulCheckValue ) ) \{ \vApplicationStackOverflowHook( ( TaskHandle_t ) pxCurrentTCB, pxCurrentTCB->pcTaskName ); \} \}#endif /* #if( configCHECK_FOR_STACK_OVERFLOW > 1 ) */
/*-----------------------------------------------------------*/
1,定义个指针指向栈起始位置
2,判断最后4个连续空间的地址是否为a5
3,如果有一个不是a5,则溢出,调用栈溢出函数
问题0:在什么时候判断栈是否溢出
代码跟踪:vApplicationStackOverflowHook=》taskCHECK_FOR_STACK_OVERFLOW=》vTaskSwitchContext
即任务切换时,判断栈溢出。
所以任务1中,如果任务中间有多个函数,函数A,切换到函数B,会释放栈空间,如果溢出过,也无法发现。程序已经失控,无法切回函数B,也就无法被监控到栈溢出。
问题1:在哪里实现将栈空间都赋值成了a5
0xa5U=》tskSTACK_FILL_BYTE=》
( void ) memset( pxNewTCB->pxStack, ( int ) tskSTACK_FILL_BYTE, ( size_t ) ulStackDepth * sizeof( StackType_t ) );
=》prvInitialiseNewTask=》
xTaskCreate
问题2:vApplicationStackOverflowHook执行的具体函数是什么
vApplicationStackOverflowHook
是一个由 FreeRTOS 提供的钩子函数,它在检测到栈溢出时被调用。这个函数的默认实现通常什么都不做,或者只是简单地进入一个无限循环或执行一些基本的错误处理。然而,开发者可以提供一个自定义的 vApplicationStackOverflowHook
实现,以便在栈溢出发生时执行特定的操作。
自定义的 vApplicationStackOverflowHook
函数的具体实现取决于你的应用程序的需求和错误处理策略。以下是一个简单的示例,展示了如何在 vApplicationStackOverflowHook
中实现一些基本的错误处理:
void vApplicationStackOverflowHook( TaskHandle_t xTask, char * pcTaskName )
{ /* 停止所有任务调度,以便安全地处理错误 */ vTaskSuspendAll(); /* 记录栈溢出错误到某个存储区域或打印到控制台 */ configPRINT_STRING( "Stack overflow detected in task: " ); configPRINT_STRING( pcTaskName ); configPRINT_STRING( "\n" ); /* 你可以在这里添加其他的错误处理代码,比如重启系统、进入安全模式等 */ /* 停止系统,因为栈溢出通常是严重的错误,需要干预 */ for( ;; ) { /* 循环等待,或者执行一些简单的错误处理任务 */ }
}
在这个例子中,vApplicationStackOverflowHook
首先暂停了所有任务的调度,以确保在错误处理期间系统不会继续执行其他任务。然后,它打印出发生栈溢出的任务名称。开发者可以在这里添加更多的错误处理逻辑,比如发送错误报告到外部设备、记录详细的调试信息,或者尝试恢复系统到一个安全状态。最后,函数进入一个无限循环,因为栈溢出通常是一个严重的错误,需要人工干预。
请注意,这个函数的实现必须非常小心,因为它在栈溢出的情况下被调用。此时,系统的状态可能已经不稳定,因此应避免执行任何可能进一步破坏系统状态的操作。特别是,你应该避免在这个函数中分配内存或调用可能使用大量栈空间的函数。
如果你没有提供一个自定义的 vApplicationStackOverflowHook
实现,并且你的 FreeRTOS 配置启用了栈溢出检测,那么当栈溢出发生时,FreeRTOS 将使用默认的空实现,这通常意味着没有任何动作发生,或者只是简单地进入一个无限循环。