1、内存分配失败回调函数
1.1、简单介绍
在移植FreeRTOS的时候在main.c文件中定义了如下的内存分配失败回调函数:
/* 内存分配失败回调函数 */
void vApplicationMallocFailedHook(void)
{
}
此函数是用户在启用内存分配失败时定义的回调函数,在系统为任务、信号量、队列、软件定时器等申请内存时,若FreeRTOS堆栈中可用的空闲内存不足,则由内存申请函数调用。
如果需要使用此功能,需将configUSE_MALLOC_FAILED_HOOK宏定义为1。该宏时控制是否启用内存分配失败回调函数。宏定义为1时启用,为0时不启用。内存分配失败回调函数常用来提示用户创建的任务、信号量、队列等需要的堆栈超过FreeRTOS堆栈中可用的空闲堆栈,导致创建失败。FreeRTOS堆栈大小在配置文件FreeRTOSConfig.h中由configTOTAL_HEAP_SIZE宏定义。
1.2、简单示例
(1)在freertos_demo.c中创建一个任务:任务vTaskLED3完成R_LED 闪烁任务。
// 任务创建函数
void Task_Start(void)
{
xTaskCreate(vTaskLED3, // 任务指针
"vTaskLED3", // 任务描述
TSD, // 堆栈深度
NULL, // 给任务传递的参数
2, // 任务优先级
&TaskLED3_Handle // 任务句柄
);
}
void vTaskLED3(void * pvParameters) // 定义LED任务
{
while (1)
{
for (uint8_t j=0; j<10; j++)
{
R_LED(ON);
vTaskDelay(300/portTICK_RATE_MS);
R_LED(OFF);
vTaskDelay(300/portTICK_RATE_MS);
}
}
}
(2)在freertos_demo.h中宏定义如下四个值:
#define Task_Consumption 104 // 任务自身消耗
#define TSW 4 // 任务堆栈宽度
#define TSD 246 // 任务堆栈深度
#define SNTS 1976 // 系统不允许分配给任务堆栈
(3)在main()函数中调用任务创建函数和启动调度器函数,以及调用xPortGetFreeHeapSize()函数获取FreeRTOS未分配的任务堆栈大小。
size_t FreeHeapSize = 0; // 定义变量来接收系统未分配的堆栈大小
int main(void)
{
/* 硬件初始化 */
Led_Init(); // Led初始化
USART1_Init(115200); // USART1初始化
FreeHeapSize = xPortGetFreeHeapSize(); // 获取FreeRTOS未分配的堆栈大小
/* FreeRTOS运行函数 */
Task_Start();
vTaskStartScheduler(); // 启用调度器函数
while(1);
}
(4)在内存分配失败回调函数中调用打印函数,输出失败原因:
/* 内存分配失败回调函数 */
void vApplicationMallocFailedHook(void)
{
printf("FreeRTOS's remaining tasks can be applied for stack size of %d bytes\n\r", FreeHeapSize-SNTS);
printf("This task requires a %d bytes stack \n\r", TSD*TSW + Task_Consumption);
printf("Indequate memory application failed!\n\r");
}
(5)在如下位置做一下更改,两次更改分别编译并下载程序到开发板,然后观察现象。
实验现象:
更改1的现象为R_LED闪烁,创建任务时申请内存成功;
更改2的现象为R_LED不闪烁,并且串口输出如下信息,代表创建任务时申请内存失败;
2、任务栈溢出回调函数
2.1、简单介绍
简单地说,栈溢出就是用户分配的栈空间不够用了。以一个简单的实力来分析栈的生长方向从高地址向低地址生长。
图中(1)的位置是RTOS的某个任务调用了函数test()前的SP栈指针位置。下面是该任务test的代码:
void test(void)
{
int i;
int array[10];
/* user code */
}
图中(2)的位置是调用函数test()所需要保存的返回地址的栈空间。这个一步不是必需的,对于Cortex-M3和Cortex-M4内核是将其保存到LR寄存器中,如果LR寄存器中保存了上一级函数的返回地址,则需要将LR寄存器中的内容先入栈。
图中(3)位置时局部变量int i和int array[10]占用的栈空间,但申请了栈空间后已经越界。这就是所谓的栈溢出。如果用户在函数test()中通过数组array修改了这部分越界区的数据,且这部分越界的栈空间暂时没有用到或者数据不是很重要,那么情况还不算严重,但是如果存储的是关键数据,则会直接导致系统崩溃。
图中(4)位置是局部变量申请了栈空间后,栈指针向下偏移(返回地址 + 变量i + 10个数组元素)X 4 = 48个字节。
图中(5)位置可能是其他任务的栈空间,也可能是全局变量或者其他用途的存储区,如果test()函数在使用中还有用到栈的地方就会从这里申请,这部分越界的空间暂时没有用到或者数据不是很重要,那么情况还不算严重,但是如果存储的是关键数据,则会直接导致系统崩溃。
在移植FreeRTOS时在main.c文件中定义的栈溢出回调函数为:
/* 堆栈溢出回调函数 */
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
{
}
在系统为任务分配内存后,任务在运行过程中,任务栈不足发生溢出情况时,系统会调用堆栈溢出回调函数。用户需要使用此功能需将configCHECK_FOR_STACK_OVERFLOW宏定义为非0。该宏用于控制是否启用堆栈溢出回调函数,宏定义为非0时启用,为0时不启用。
堆栈溢出有两种检测方式:宏定义为1时使用方法一,宏定义为2时使用方法二。堆栈溢出回调函数常用在开发或者测试阶段,开启堆栈溢出检测后会增大上下文切换的开销。注意:堆栈溢出检测是在任务切换的时候进行的。空闲任务的堆栈大小使用宏configMANIMAL_STACK_SIZE定义。
堆栈溢出检测方法一:在任务切换时检测任务栈指针是否过界,如果过界,则在任务切换的时候会触发栈溢出回调函数。这种方法存在弊端——若在任务运行过程中栈溢出,则在任务结束之前恢复溢出,这种方法检测不到。
堆栈溢出检测方法二:任务创建的时候将任务栈所有数据初始化为0xa5,所以会检测末尾的16个字节是否都是0xa5,通过这种方式来检测任务栈是否溢出。但这种检测方法的弊端是栈内数据未修改,溢出的数据修改了任务栈以外的数据而引起硬件异常的情况是检测不到的。
2.2、简单示例
(1)在freertos_demo.c中创建两个任务:任务vTaskLED执行LED闪烁,任务vTaskOD完成堆栈溢出功能。
// 任务创建函数
void Task_Start(void)
{
xTaskCreate(vTaskLED3, // 任务指针
"vTaskLED3", // 任务描述
TSD, // 堆栈深度
NULL, // 给任务传递的参数
2, // 任务优先级
&TaskLED3_Handle // 任务句柄
);
xTaskCreate(vTaskOD, // 任务指针
"vTaskOD", // 任务描述
100, // 堆栈深度
NULL, // 给任务传递的参数
2, // 任务优先级
&TaskOD_Handle // 任务句柄
);
}
void vTaskLED3(void * pvParameters) // 定义LED任务
{
while (1)
{
for (uint8_t j=0; j<10; j++)
{
R_LED(ON);
vTaskDelay(300/portTICK_RATE_MS);
R_LED(OFF);
vTaskDelay(300/portTICK_RATE_MS);
}
}
}
void vTaskOD(void * pvParameters) // 定义OD任务
{
uint32_t buffer[100];
uint32_t i = 0;
while(1)
{
for (i=100; i>0; i--) // 这里对数组逆顺序赋值。为了不引起硬件异常
{
buffer[i] = 0x55;
}
}
}
(2)在堆栈溢出回调函数中调用打印函数,打印溢出任务:
/* 堆栈溢出回调函数 */
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
{
printf("Stack overflow of task %s\n\r", pcTaskName);
}
(3)实验效果:程序运行后会打印出vTaskOD任务栈溢出情况,如下所示。
3、任务栈的大小
3.1、计算任务栈的大小
在FreeRTOS应用设计中,每个任务都需要自己的栈空间,功能不同,每个任务需要的栈大小也是不同的。将如下几个选项简单地累加可以得到一个粗略的栈大小。
(1)函数的嵌套使用
针对每一级函数用到的栈空间有如下4项:
- 函数局部变量。
- 函数形参。一般情况下函数的形参是直接使用的CPU寄存器,不需要使用栈空间,但是这个函数中如果还嵌套了一个函数,那么这个存储了函数形参的CPU寄存器内容是要入栈的。所以建议将这部分算在栈大小中。
- 函数返回地址。针对Cortex-M3和Cortex-M4内核的MCU,一般函数的返回地址是专门保存到LR(LinkRegister)寄存器中的,如果这个函数中还调用了一个函数,那么这个存储了函数返回地址的LR寄存器内容是要入栈的。所以建议将这部分也算在栈大小中。
- 函数内部的状态保存操作也需要额外的栈空间。
(2)任务切换
任务切换时所有的寄存器都需要入栈,对于带FPU浮点处理单元的Cortex-M4内核MCU来说,FPU寄存器也是需要入栈的。
(3)针对Cortex-M3和Cortex-M4内核的MCU
在任务执行过程中,如果发生中断,那么Cortex-M3内核的MCU有8个寄存器是自动入栈的,这个栈是任务栈,进入中断以后其余寄存器入栈以及发生中断嵌套用的都是系统栈。
Cortex-M4内核的MCU有8个通用寄存器和18个浮点寄存器是自动入栈的,这个栈是任务栈,进入中断以后其余通用寄存器和浮点寄存器入栈以及发生中断嵌套用的都是系统栈。
(4)进入中断以后使用的局部变量以及可能发生的中断嵌套用的都是系统栈
在实际应用中,将这些都加起来是一件非常麻烦的工作,上面这些栈空间加起来的总和只是栈的最小需求,实际分配的栈大小可以在最小栈需求的基础上乘以一个安全系数,一般取1.5~2。上面的计算是用户可以确定的栈大小,项目应用中还存在无法确定的栈大小,例如,调用printf()函数就很难确定实际的栈消耗。又例如,通过栈函数指针实现函数的间接调用,因为函数指针不是固定地指向一个函数进行调用,而是根据不同的程序设计指向不同的函数,这使得栈大小的计算变得比较麻烦。
注意,不要编写递归代码,因为不知道递归的层数,栈的大小也是不好确定的。
一般来说,用户可以事先给任务分配一个大的栈空间,然后将任务栈的使用情况打印出来,运行一段时间就会有个大概的范围。
3.2、获取任务未使用堆栈大小的函数
/**
* 函数:UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t xTask);
* 参数:xTask-任务句柄,NULL表示查看当前任务的堆栈使用情况
* 返回值:返回最小剩余堆栈空间,以字为单位
* 功能:获取任务未使用的栈大小,返回的值为字数,而不是字节数
*/
UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t xTask);
若需要使用此功能,需将include_uxTaskGetStackHighWaterMark宏定义为1。任务使用的堆栈将随着任务的执行和中断的处理而增大或缩小。uxTaskGetStackHighWaterMark()返回任务开始执行以来可用的最小剩余堆栈空间,即任务堆栈处于最大(最深)值时未使用的堆栈数量。
3.3、简单示例
(1)在freertos_demo.c中创建任务vTaskGD,实现获取任务剩余堆栈大小和打印信息功能。
// 任务创建函数
void Task_Start(void)
{
xTaskCreate(vTaskGD, // 任务指针
"vTaskGD", // 任务描述
100, // 堆栈深度
NULL, // 给任务传递的参数
2, // 任务优先级
&TaskGD_Handle // 任务句柄
);
}
void vTaskGD(void * pvParameters) // 定义GD任务
{
uint32_t stackNum = 0;
while(1)
{
stackNum = uxTaskGetStackHighWaterMark(NULL); // 获取任务剩余堆栈大小
printf("The remaining %d bytes of the current task stack\n\r", stackNum);
vTaskDelay(1000/portTICK_PERIOD_MS);
}
}
(2)在FreeRTOSConfig.h配置文件中定义开启使用获取任务未使用堆栈大小函数的宏。
(3)实验效果:程序运行后会打印任务vTaskGD堆栈剩余的大小,如下所示。