前言
从本篇开始,将不再太过于关心 FreeRTOS 的内核细节,把重心转移到对 FreeRTOS 的应用上来。
本篇代码大部分参考野火的 FreeRTOS 教程。
一、静态任务和动态任务创建的区别
1. 概念解析
在 FreeRTOS 中,我们可以选择两个不同的函数进行任务的创建:
- xTaskCreateStatic() 静态任务创建函数
- xTaskCreate() 动态任务创建函数
静态和动态的区别在于:
-
静态创建任务:
- 静态创建任务是在编译时分配任务所需的内存空间。
- 创建任务时需要定义并初始化一个StaticTask_t类型的变量,该变量用于存储任务的相关信息。
- 静态创建任务的内存空间在任务整个运行期间都被任务所占用,直到任务被删除。
- 静态创建任务通常在应用程序的启动阶段进行,任务的数量是固定的,无法在运行时动态调整。
-
动态创建任务:
- 动态创建任务是在运行时通过动态内存分配函数(例如pvPortMalloc())来分配任务所需的内存空间。
- 创建任务时,不需要定义和初始化额外的变量,任务的相关信息直接存储在动态分配的内存中。
- 动态创建任务的内存空间可以在任务完成后释放,可以在运行时动态地创建和删除任务。
- 动态创建任务通过调用函数xTaskCreate()来实现,任务的数量可以在运行时根据需求进行动态调整。
2. 动态创建任务的应用
也就是说,我们可以在一个任务运行的时候创建一个新任务!如下:
要在一个任务运行的时候创建新任务,可以在任务函数中调用xTaskCreate()函数。这样,当任务运行到这个调用语句时,会创建一个新的任务,并将其添加到任务列表中。新任务会在调度器启动后开始执行。
- 可能的代码如下:
void Task1(void *pvParameters)
{
// 任务1的代码
// 创建新任务
BaseType_t status = xTaskCreate(Task2, "Task2", configMINIMAL_STACK_SIZE, NULL, 2, NULL);
if (status != pdPASS) {
// 创建任务失败的处理
}
// 任务1的其余代码
}
void Task2(void *pvParameters)
{
// 任务2的代码
// 任务2的其余代码
}
int main(void)
{
// FreeRTOS初始化和任务创建
xTaskCreate(Task1, "Task1", configMINIMAL_STACK_SIZE, NULL, 1, NULL);
// 启动调度器
vTaskStartScheduler();
// 不应该跳转到这里
while (1) {}
return 0;
}
在上面的示例中,当任务1运行到创建新任务的代码时,会调用xTaskCreate()函数创建任务2。任务2会被添加到任务列表中,并在调度器启动后开始执行。
3. 动态创建任务的好处
看到这里,可能你有个疑惑:即使可以这么做,我们也要先写好任务2的上下文,那么还不如在编程时直接创建两个任务而不是等到运行的时候再创建任务2,在运行时创建任务2有什么意义吗?
实际上,对于初学者来说,我们的程序不够复杂,可能很难体会到动态创建任务的好处。在复杂的系统中,运行时动态创建任务的一个主要优点是可以动态地根据系统的需求来创建任务。这样可以在运行时根据实际情况来决定是否创建任务,以及创建多少个任务。这种灵活性可以提高系统的可扩展性和适应性。
① 运行时创建任务的应用场景和意义:
- 动态任务管理:在某些应用中,任务的数量和类型可能会根据用户的交互或特定条件的触发而动态改变。通过在运行时创建任务,可以根据需要来创建或删除任务,以适应不同的系统状态和用户行为。
- 资源管理和优化:有时系统中的任务可能共享某些资源,例如共享内存区域或外设。在某些情况下,只有在需要时才会分配这些资源,以避免资源浪费。通过在运行时创建任务,可以根据资源的可用性和需求来创建任务,以有效地管理系统资源和优化性能。
- 动态优先级调整:有些任务可能有不同的优先级,而优先级可能会随着系统状态的变化而改变。通过在运行时创建任务,可以根据系统的需求和优先级策略来动态地调整任务的优先级,以满足系统的实时性要求。
② 动态创建任务的实际应用
考虑以下这些具体的场景,动态创建任务可能优于静态创建任务:
- 事件驱动系统:在某些系统中,任务的创建可能是由特定的事件触发的。例如,当用户按下某个按钮或触发某个传感器时,可以根据不同的事件动态创建任务来响应用户操作或处理传感器数据。
- 多协议通信系统:在某些通信系统中,需要支持多种通信协议,例如TCP/IP、UDP等。根据网络条件和通信需求的变化,可以动态创建任务以处理不同的协议,以实现系统的灵活性和适应性。
- 并行处理系统:在需要处理大量计算密集型任务的系统中,可以根据系统资源和负载情况动态创建任务。通过根据系统负载动态调整任务数量,可以最大程度地利用多核处理器或分布式计算系统的性能。
- 系统调试和故障排查:在调试和故障排查过程中,可能需要动态添加一些特定的监控任务或记录任务,以收集系统信息并帮助分析问题。通过在运行时动态创建这些任务,可以根据需要收集所需的信息,而不需要一直运行这些任务,从而减少对系统性能的影响。
二、任务函数的结构
- 任务必须是一个无限循环的函数,一般用 while(1) 或者 for(;😉 作为循环体,在循环体里面编写任务正文。这是因为如果任务在循环之外返回(通过 LR 返回),那么当 LR 指向了非法的内存时,可能会发生 HardFault_Handler,导致系统出错。为了避免这种情况,FreeRTOS 中的任务返回值会指向一个任务退出函数 prvTaskExitError(),而该函数是一个死循环。
- 任务的延时需要使用 vTaskDelay() 这种阻塞延时,而不能使用循环的忙等延时。这是因为在阻塞延时时,任务会被挂起,从而让出 CPU 控制权,让其他任务得以执行,提高 CPU 利用率;而如果使用循环延时,对于非最高优先级的任务来说,他们将不断循环直到其执行任务的时间片结束,但是如果是最高优先级的任务使用了循环延时,就会一直循环执行最高优先级任务,其他优先级较低的任务就得不到执行,这种情况是要避免的。所以 FreeRTOS 中任务延时尽量使用阻塞延时。
以下面这个 LED任务为例:
/**********************************************************************
* @ 函数名 : LED_Task
* @ 功能说明: LED_Task任务主体
* @ 参数 :
* @ 返回值 : 无
********************************************************************/
static void LED1_Task(void* parameter)
{
while (1)
{
LED1_ON;
vTaskDelay(500); /* 延时500个tick */
printf("LED1_Task Running,LED1_ON\r\n");
LED1_OFF;
vTaskDelay(500); /* 延时500个tick */
printf("LED1_Task Running,LED1_OFF\r\n");
}
}
三、任务创建时内存分配的相关问题
1. STM32 的存储空间详解
- 因为在我们的程序中 FreeRTOS 的内存分配是建立在 STM32 上的,所以首先我们需要了解 STM32 的存储空间。
推荐一篇好文,详细地对 STM32 的存储空间作了介绍:
STM32深入系列01——内存简述(Flash和SRAM)
2. FreeRTOS 的内存管理实现
- 而在 FreeRTOS 中,提供了5种内存管理实现。分别为:heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c。后面笔者会写一篇有关 FreeRTOS 的内存管理的文章。
可以先参考这篇好文:
【freeRTOS内存管理策略详解】
3. 什么是内存对齐
-
内存对齐是指将数据存储在内存中时按照规定的边界对齐方式进行存储。对齐方式是按照特定字节的倍数对数据进行对齐,通常是按照 1、2、4、8 等字节进行对齐。对齐的基本单位是字节,也可以是其他数据类型的大小。
-
内存对齐的目的是为了优化内存访问,提高处理器的访问速度和效率。特定的硬件架构和处理器通常对对齐的内存访问更高效,而对非对齐的内存访问可能会引起额外的开销或性能损失。
-
例如,假设要存储一个 int 类型的数据(通常是 4 字节)在内存中,如果数据按照 4 字节对齐存储,即保证存储地址是 4 的倍数,那么处理器就可以以更高效的方式访问这个数据,提高读取和写入的速度。如果数据非对齐存储,处理器可能需要进行额外的内存访问操作,导致性能下降。
-
在 FreeRTOS 中,我们使用 8 字节对齐,要求数据按照8字节的倍数进行存储,这样可以保证数据存储在连续的内存块中,提高内存访问的效率。
-
额外开销:当申请13字节内存,按照最接近的8字节进行对齐。这意味着实际上会分配16字节的内存空间来满足对齐要求。
代码:
定义对齐的字节数:
#define portBYTE_ALIGNMENT 8
根据定义的字节数确定对齐使用的掩码:
这里的掩码是 0x0007,也就是二进制 0000 0000 0000 0111
#if portBYTE_ALIGNMENT == 32
#define portBYTE_ALIGNMENT_MASK ( 0x001f )
#endif
#if portBYTE_ALIGNMENT == 16
#define portBYTE_ALIGNMENT_MASK ( 0x000f )
#endif
#if portBYTE_ALIGNMENT == 8
#define portBYTE_ALIGNMENT_MASK ( 0x0007 )
#endif
#if portBYTE_ALIGNMENT == 4
#define portBYTE_ALIGNMENT_MASK ( 0x0003 )
#endif
#if portBYTE_ALIGNMENT == 2
#define portBYTE_ALIGNMENT_MASK ( 0x0001 )
#endif
#if portBYTE_ALIGNMENT == 1
#define portBYTE_ALIGNMENT_MASK ( 0x0000 )
#endif
计算栈顶指针:
(栈指针 + 栈深 - 1)
pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );
对齐栈顶指针:
这里的掩码是 0x0007,也就是二进制 0000 0000 0000 0111,取反就是低三位都为 0(1111 1111 1111 1000),再与 pxTopOfStack 与就可以将pxTopOfStack 低三位清 0,也就变成了 8 的倍数(原来的内存地址是 **** **** **** ****,现在的内存地址是 **** **** **** * ✖ 8字节),这就是内存对齐的原理。
pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
最后,检查内存对齐是否正确:
原理:掩码除了低三位都为 0,而如果栈顶指针正确对齐,其低三位都为 0,所以两者与起来应该结果为 0。
这里使用断言检测结构是否为 0。
/* Check the alignment of the calculated top of stack is correct. */
configASSERT( ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack & ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) == 0UL ) );
4. 动态创建任务时,任务栈究竟多大
- 由于静态任务创建的任务栈是我们自行分配的,很容易清楚其任务栈大小。但是对于动态创建任务,由于任务栈的空间分配是内核帮我们分配的,而且封装较深,再加上野火的教程中对任务栈大小的描述出现了错误,导致笔者花了很多时间去理解,下面进行说明!
① 野火教程中出现的错误
在野火的教程书中,代码上任务栈大小是 512(查看源码可知这个参数指的应该是字数,也就是 512字 = 512 × 4 = 2048 字节 ),下面的解释却是 128 × 4 = 512字节。前后矛盾,让人迷惑:
② 对野火教程的更正
实际上,动态创建任务这个函数的任务栈深度这个参数应该指的是字!
③ 为什么栈深度指的是字的数目?
xTaskCreate() 函数中,使用了 pvPortMalloc() 进行空间分配:
( size_t ) usStackDepth ) * sizeof( StackType_t ) 就是我们传入的栈深度 × StackType_t 的字节数,而 StackType_t 实际上就是 unsigned int,也就是 4 个字节。
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint16_t usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask ) /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
{
TCB_t *pxNewTCB;
BaseType_t xReturn;
/* If the stack grows down then allocate the stack then the TCB so the stack
does not grow into the TCB. Likewise if the stack grows up then allocate
the TCB then the stack. */
#if( portSTACK_GROWTH > 0 )
{
/* Allocate space for the TCB. Where the memory comes from depends on
the implementation of the port malloc function and whether or not static
allocation is being used. */
pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) );
if( pxNewTCB != NULL )
{
/* Allocate space for the stack used by the task being created.
The base of the stack memory stored in the TCB so the task can
be deleted later if required. */
pxNewTCB->pxStack = ( StackType_t * ) pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
if( pxNewTCB->pxStack == NULL )
{
/* Could not allocate the stack. Delete the allocated TCB. */
vPortFree( pxNewTCB );
pxNewTCB = NULL;
}
}
}
#else /* portSTACK_GROWTH */
{
StackType_t *pxStack;
/* Allocate space for the stack used by the task being created. */
pxStack = ( StackType_t * ) pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
if( pxStack != NULL )
{
/* Allocate space for the TCB. */
pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) ); /*lint !e961 MISRA exception as the casts are only redundant for some paths. */
if( pxNewTCB != NULL )
{
/* Store the stack location in the TCB. */
pxNewTCB->pxStack = pxStack;
}
else
{
/* The stack cannot be used as the TCB was not created. Free
it again. */
vPortFree( pxStack );
}
}
else
{
pxNewTCB = NULL;
}
}
#endif /* portSTACK_GROWTH */
if( pxNewTCB != NULL )
{
#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
{
/* Tasks can be created statically or dynamically, so note this
task was created dynamically in case it is later deleted. */
pxNewTCB->ucStaticallyAllocated = tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB;
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );
prvAddNewTaskToReadyList( pxNewTCB );
xReturn = pdPASS;
}
else
{
xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
}
return xReturn;
}
#endif /* configSUPPORT_DYNAMIC_ALLOCATION */
而pvPortMalloc() 函数的结构如下:
在这个函数中,真正分配内存的是 pvReturn
变量。在函数的开头,pvReturn
被初始化为 NULL
,表示当前还没有成功分配内存。
在函数执行的过程中,会经过一系列的条件判断和操作,最终可能会将 pxBlock
结构体中的内存空间分配给 pvReturn
。
-
首先,会调用
vTaskSuspendAll()
函数来暂停任务调度,以确保在进行内存分配操作时不会被其他任务中断。 -
然后,会进行一些初始化操作,检查是否需要对堆进行初始化。
-
接下来,会根据用户请求的内存大小
xWantedSize
进行一系列判断和计算。其中会判断请求的大小是否合法,是否需要对内存进行对齐操作等。 -
然后,会遍历空闲内存块链表,找到合适大小的内存块。如果找到了合适的内存块,会将其分配给
pvReturn
变量,并进行相应的调整和更新。 -
最后,会更新剩余可用内存的大小,并根据需求设定一些标记和指针,表示该内存块已被分配出去。
-
在函数结尾,会调用
traceMALLOC()
函数进行一些跟踪记录操作,并恢复任务调度,继续执行其他任务。 -
最终,函数会根据分配结果返回相应的指针给用户。
因此,这个函数中真正分配内存的是 pvReturn
变量,它会被赋值为成功分配的内存空间的指针。
这个函数使用一种被称为内存块的东西来记录空闲内存块并把空闲内存块分配给任务栈。空闲内存块的结构体如下:
/* Define the linked list structure. This is used to link free blocks in order
of their memory address. */
typedef struct A_BLOCK_LINK
{
struct A_BLOCK_LINK *pxNextFreeBlock; /*<< The next free block in the list. */
size_t xBlockSize; /*<< The size of the free block. */
} BlockLink_t;
这个结构体存储了指向下一个内存块的指针以及当前内存块的大小,而内存块大小是以字节为单位的。
值得一提的是,每一块空闲的内存中可供利用的部分是这块内存减去该空闲内存块结构体的大小。
至此,我们可以清楚看到,动态创建任务时传入的任务栈深度参数的单位是字,转换为字节时应该乘以 4。
四、任务创建、启动流程
以启动两个 LED 以不同频率闪烁为例:
1. 任务句柄创建
我们在 AppTaskCreate 任务中创建其他的任务:
/**************************** 任务句柄 ********************************/
/*
* 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
* 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
* 这个句柄可以为NULL。
*/
/* 创建任务句柄 */
static TaskHandle_t AppTaskCreate_Handle = NULL;
/* LED1任务句柄 */
static TaskHandle_t LED1_Task_Handle = NULL;
/* LED2任务句柄 */
static TaskHandle_t LED2_Task_Handle = NULL;
2. 任务主体编写
/***********************************************************************
* @ 函数名 : AppTaskCreate
* @ 功能说明: 为了方便管理,所有的任务创建函数都放在这个函数里面
* @ 参数 : 无
* @ 返回值 : 无
**********************************************************************/
static void AppTaskCreate(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
taskENTER_CRITICAL(); //进入临界区
/* 创建LED_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )LED1_Task, /* 任务入口函数 */
(const char* )"LED1_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )2, /* 任务的优先级 */
(TaskHandle_t* )&LED1_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建LED1_Task任务成功!\r\n");
/* 创建LED_Task任务 */
xReturn = xTaskCreate((TaskFunction_t )LED2_Task, /* 任务入口函数 */
(const char* )"LED2_Task",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )3, /* 任务的优先级 */
(TaskHandle_t* )&LED2_Task_Handle);/* 任务控制块指针 */
if(pdPASS == xReturn)
printf("创建LED2_Task任务成功!\r\n");
vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
taskEXIT_CRITICAL(); //退出临界区
}
/**********************************************************************
* @ 函数名 : LED_Task
* @ 功能说明: LED_Task任务主体
* @ 参数 :
* @ 返回值 : 无
********************************************************************/
static void LED1_Task(void* parameter)
{
while (1)
{
LED1_ON;
vTaskDelay(500); /* 延时500个tick */
printf("LED1_Task Running,LED1_ON\r\n");
LED1_OFF;
vTaskDelay(500); /* 延时500个tick */
printf("LED1_Task Running,LED1_OFF\r\n");
}
}
/**********************************************************************
* @ 函数名 : LED_Task
* @ 功能说明: LED_Task任务主体
* @ 参数 :
* @ 返回值 : 无
********************************************************************/
static void LED2_Task(void* parameter)
{
while (1)
{
LED2_ON;
vTaskDelay(500); /* 延时500个tick */
printf("LED2_Task Running,LED2_ON\r\n");
LED2_OFF;
vTaskDelay(500); /* 延时500个tick */
printf("LED2_Task Running,LED2_OFF\r\n");
}
}
3. 硬件初始化函数
- 中断优先级分组
- 其他外设初始化
/***********************************************************************
* @ 函数名 : BSP_Init
* @ 功能说明: 板级外设初始化,所有板子上的初始化均可放在这个函数里面
* @ 参数 :
* @ 返回值 : 无
*********************************************************************/
static void BSP_Init(void)
{
/*
* STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
* 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
* 都统一用这个优先级分组,千万不要再分组,切忌。
*/
NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
/* LED 初始化 */
LED_GPIO_Config();
/* 串口初始化 */
USART_Config();
}
4. main 函数主体
- 硬件初始化
- AppTaskCreate 任务的创建
- 任务调度的启动
int main(void)
{
BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
/* 开发板硬件初始化 */
BSP_Init();
printf("这是一个[野火]-STM32全系列开发板-FreeRTOS-动态创建多任务实验!\r\n");
/* 创建AppTaskCreate任务 */
xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate, /* 任务入口函数 */
(const char* )"AppTaskCreate",/* 任务名字 */
(uint16_t )512, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )1, /* 任务的优先级 */
(TaskHandle_t* )&AppTaskCreate_Handle);/* 任务控制块指针 */
/* 启动任务调度 */
if(pdPASS == xReturn)
vTaskStartScheduler(); /* 启动任务,开启调度 */
else
return -1;
while(1); /* 正常不会执行到这里 */
}