从本篇开始,将不再太过于关心 FreeRTOS 的内核细节,把重心转移到对 FreeRTOS 的应用上来。
本篇代码大部分参考野火的 FreeRTOS 教程。
在 FreeRTOS 中,我们可以选择两个不同的函数进行任务的创建:
静态和动态的区别在于:
静态创建任务:
动态创建任务:
也就是说,我们可以在一个任务运行的时候创建一个新任务!如下:
要在一个任务运行的时候创建新任务,可以在任务函数中调用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会被添加到任务列表中,并在调度器启动后开始执行。
看到这里,可能你有个疑惑:即使可以这么做,我们也要先写好任务2的上下文,那么还不如在编程时直接创建两个任务而不是等到运行的时候再创建任务2,在运行时创建任务2有什么意义吗?
实际上,对于初学者来说,我们的程序不够复杂,可能很难体会到动态创建任务的好处。在复杂的系统中,运行时动态创建任务的一个主要优点是可以动态地根据系统的需求来创建任务。这样可以在运行时根据实际情况来决定是否创建任务,以及创建多少个任务。这种灵活性可以提高系统的可扩展性和适应性。
考虑以下这些具体的场景,动态创建任务可能优于静态创建任务:
/**********************************************************************
* @ 函数名 : 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、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 ) );
在野火的教程书中,代码上任务栈大小是 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 以不同频率闪烁为例:
我们在 AppTaskCreate 任务中创建其他的任务:
/**************************** 任务句柄 ********************************/
/*
* 任务句柄是一个指针,用于指向一个任务,当任务创建好之后,它就具有了一个任务句柄
* 以后我们要想操作这个任务都需要通过这个任务句柄,如果是自身的任务操作自己,那么
* 这个句柄可以为NULL。
*/
/* 创建任务句柄 */
static TaskHandle_t AppTaskCreate_Handle = NULL;
/* LED1任务句柄 */
static TaskHandle_t LED1_Task_Handle = NULL;
/* LED2任务句柄 */
static TaskHandle_t LED2_Task_Handle = NULL;
/***********************************************************************
* @ 函数名 : 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");
}
}
/***********************************************************************
* @ 函数名 : BSP_Init
* @ 功能说明: 板级外设初始化,所有板子上的初始化均可放在这个函数里面
* @ 参数 :
* @ 返回值 : 无
*********************************************************************/
static void BSP_Init(void)
{
/*
* STM32中断优先级分组为4,即4bit都用来表示抢占优先级,范围为:0~15
* 优先级分组只需要分组一次即可,以后如果有其他的任务需要用到中断,
* 都统一用这个优先级分组,千万不要再分组,切忌。
*/
NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 );
/* LED 初始化 */
LED_GPIO_Config();
/* 串口初始化 */
USART_Config();
}
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); /* 正常不会执行到这里 */
}