目录
前面我们已经梳理了 FreeRTOS 中链表的相关内容,从本篇开始,我们将学习什么是任务,以及动态和静态创建任务的方法。
我们在前面提到过,任务就是一个个不断执行功能的死循环函数。在裸机系统中,单片机实现的所有功能都放在while循环下。在 FreeRTOS 中也是一样,只不过将不同的功能放在不同的死循环函数中,由调度器来管理这些任务的运行。
在程序运行时,全局变量与局部变量的定义,中断发生时当前状态的存储,函数返回地址等都需要内存来存放。这些内容都存放在栈里面。在裸机系统中,上述所有的东西都存在同一段栈空间内。
对于操作系统来讲,栈的分配与释放均由其自动实现。如果把裸机系统的循环看作一个任务的话,由于操作系统存在多个任务且任务间相互独立,所以每个任务都有一段独立的栈空间。
任务控制块
在 task.c 中,有一个任务控制块(TCB)的声明。TCB就相当于任务的身份证,包括任务的所有信息,比如任务的栈指针, 任务名等。通过任务控制块,操作系统可以通过任务控制块来实现对任务的全部操作。
下面为TCB结构体声明。
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack;
#if ( portUSING_MPU_WRAPPERS == 1 )
xMPU_SETTINGS xMPUSettings;
#endif
ListItem_t xStateListItem;
ListItem_t xEventListItem;
UBaseType_t uxPriority;
StackType_t *pxStack;
char pcTaskName[ configMAX_TASK_NAME_LEN ];
#if ( ( portSTACK_GROWTH > 0 ) || ( configRECORD_STACK_HIGH_ADDRESS == 1 ) )
StackType_t *pxEndOfStack;
#endif
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
UBaseType_t uxCriticalNesting;
#endif
#if ( configUSE_TRACE_FACILITY == 1 )
UBaseType_t uxTCBNumber;
UBaseType_t uxTaskNumber;
#endif
#if ( configUSE_MUTEXES == 1 )
UBaseType_t uxBasePriority;
UBaseType_t uxMutexesHeld;
#endif
#if ( configUSE_APPLICATION_TASK_TAG == 1 )
TaskHookFunction_t pxTaskTag;
#endif
#if( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 )
void *pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
#endif
#if( configGENERATE_RUN_TIME_STATS == 1 )
uint32_t ulRunTimeCounter;
#endif
#if ( configUSE_NEWLIB_REENTRANT == 1 )
struct _reent xNewLib_reent;
#endif
#if( configUSE_TASK_NOTIFICATIONS == 1 )
volatile uint32_t ulNotifiedValue;
volatile uint8_t ucNotifyState;
#endif
#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
uint8_t ucStaticallyAllocated;
#endif
#if( INCLUDE_xTaskAbortDelay == 1 )
uint8_t ucDelayAborted;
#endif
} tskTCB;
以下对一些重要内容进行解释。
pxTopOfStack:栈顶指针
xStateListItem:链表节点。在链表部分,节点内有个TCB指针指向该节点;在TCB中,就是通过这个指针来指向这个节点。
xEventListItem:这个是指向事件链表的指针,功能和上个一样。
uxPriority:优先级。
pxStack:任务栈起始地址。
pcTaskName:任务名称,内容格式为字符串,长度由宏 configMAX_TASK_NAME_LEN 来控制,这个宏在 FreeRTOSConfig.h 中定义,默认为 16。
接下来,我们就开始进行简单实战来验证。
环境准备
无论是创建动态还是静态任务,我们都需要做一些提前准备,比如配置创建任务方式的宏,还有硬件初始化等。
任务创建宏
首先,我们需要在 FreeRTOSConfig.h 中定义支持动态和静态声明的宏。这两个宏在 FreeRTOS.h 中有相关定义,在其中如果我们没有定义下面这两个宏,系统则会默认定义他们。
#define configSUPPORT_STATIC_ALLOCATION 0
#define configSUPPORT_DYNAMIC_ALLOCATION 1
在上述两行代码中,我们开启动态创建而关闭了静态创建。需要注意的是,我们不能同时开启动态和静态创建,如果同时开启的话则会在编译的时候爆出问题。
硬件初始化
正如同我们在裸机系统中先进行初始化一样,在 FreeRTOS 中我们也需要先进行硬件初始化。这种操作在整个系统的声明生命周期中只需要运行一次,没必要放到任务中去。
void Hardware_Config(void);
void Hardware_Config(void)
{
USART_Config();
printf("Init_Task is ok \r\n");
}
在上述初始化函数中,我对 串口 进行了初始化,在main函数中这个函数应该放在创建任务之前。
添加外设文件
这里我们需要根据我们的需求和要初始化的外设来添加我们需要的外设文件。
接下来,我们正式开始创建任务。
动态创建任务
相比于静态创建,动态创建关于内存的部分有操作系统替我们处理,相对来说比较简单。
定义动态任务函数
首先,我们需要定义一个任务函数。
static void UART_Task(void* param)
{
while(1)
{
printf("UART_Task is ok \r\n");
vTaskDelay(500);
}
}
上述代码中,我定义了一个每500ms打印一次的任务。
定义任务句柄
任务控制块指针可以方便之后对任务的操作。而任务句柄则是指向任务控制块的指针。所以操作任务都需要经过任务句柄。
static TaskHandle_t AppTaskCreate_Handle = NULL;
static TaskHandle_t dynamic_Task_Handle = NULL;
在动态创建任务时,任务的内存因为是由系统分配而非提前预设好,而且任务句柄在动态创建任务的函数中作为参数,也就是这个句柄对任务本身进行操作,所以为空即可。
需要注意的是,每个任务都要有自己的任务句柄, 不可复用。
任务创建函数
我们创建一个任务创建函数用于创建所有任务。这样,我们在main函数中只需要调用这一个函数就能完成所有任务的创建。
void AppTaskCreate(void)
{
xTaskCreate((TaskFunction_t )UART_Task, /* 任务入口函数 */
(const char* )"UART_Task",/* 任务名字 */
(uint16_t )256, /* 任务栈大小 */
(void* )NULL, /* 任务入口函数参数 */
(UBaseType_t )2, /* 任务的优先级 */
(TaskHandle_t* )&UART_Task_Handle);/* 任务控制块指针 */
printf("UART_Task successful!\r\n");
vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务
}
在任务创建完成后,将任务句柄删除。因为 AppTaskCreate_Handle 在main函数中作为 AppTaskCreate() 的任务句柄,所以删除 AppTaskCreate_Handle 就相当于删除任务。
main函数实现
接下来,就是在 main 函数中实现。先进行初始化,然后执行任务创建函数并启动调度器。
关于调度器的内容我们会在之后进行学习,这里先知道他的作用是管理任务执行的就好。毕竟单调的理论会磨灭学习的热情。
/* 开发板硬件初始化 */
Hardware_Config();
printf("Task Creating\r\n");
/* 创建AppTaskCreate任务 */
xTaskCreate((TaskFunction_t )AppTaskCreate, /* 任务入口函数 */
(const char* )"AppTaskCreate",/* 任务名字 */
(uint16_t )256, /* 任务栈大小 */
(void* )NULL,/* 任务入口函数参数 */
(UBaseType_t )1, /* 任务的优先级 */
(TaskHandle_t* )&AppTaskCreate_Handle);/* 任务控制块指针 */
vTaskStartScheduler(); /* 启动任务,开启调度 */
while(1); /* 正常不会执行到这里 */
效果展示
通过这段log我们可以清晰的看出系统的执行过程。执行完初始化之后,进入了任务创建函数。因为创建了任务,所以会先执行一遍创建的任务。完成任务创建后,打印了successful。之后,便每500ms执行一遍任务。
静态创建任务
修改宏
首先,开启静态创建。
#define configSUPPORT_STATIC_ALLOCATION 1
#define configSUPPORT_DYNAMIC_ALLOCATION 1
定义静态任务函数
这个任务的功能依然是每500ms打印一次l任务执行成功的log。
static void static_Task(void* param)
{
while(1)
{
printf("static_Task is ok \r\n");
vTaskDelay(500);
}
}
空闲任务与定时器任务堆栈函数和任务控制块指针
在操作系统没有任务执行时,CPU会进入空闲任务,如果没有其他任务要zhix
在静态创建任务时,我们需要实现 vApplicationGetIdleTaskMemory() 和 vApplicationGetTimerTaskMemory()。这两个函数是用户设定的空闲(Idle)任务与定时器(Timer)任务的堆栈大小。因为静态创建任务需要我们自己分配内存,这两个函数就是实现这个功能的。
static StackType_t Idle_Task_Stack[configMINIMAL_STACK_SIZE];
static StackType_t Timer_Task_Stack[configMINIMAL_STACK_SIZE*2];
static StaticTask_t Idle_Task_TCB;
static StaticTask_t Timer_Task_TCB;
void vApplicationGetTimerTaskMemory(
StaticTask_t **ppxTimerTaskTCBBuffer, //任务控制块内存
StackType_t **ppxTimerTaskStackBuffer, //任务堆栈内存
uint32_t *pulTimerTaskStackSize);//任务堆栈大小
void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer,
StackType_t **ppxTimerTaskStackBuffer,
uint32_t *pulTimerTaskStackSize)
{
*ppxTimerTaskTCBBuffer=&Timer_Task_TCB;
*ppxTimerTaskStackBuffer=Timer_Task_Stack;
*pulTimerTaskStackSize=configMINIMAL_STACK_SIZE*2;
}
void vApplicationGetIdleTaskMemory(
StaticTask_t **ppxIdleTaskTCBBuffer, //任务控制块内存
StackType_t **ppxIdleTaskStackBuffer, //任务堆栈内存
uint32_t *pulIdleTaskStackSize);//任务堆栈大小
void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize)
{
*ppxIdleTaskTCBBuffer=&Idle_Task_TCB;
*ppxIdleTaskStackBuffer=Idle_Task_Stack;
*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;
}
定义任务控制块指针和栈以及任务句柄
静态创建任务需要使用我们自己预先划分内存。所以我们需要将任务控制块、任务句柄以及栈空间都进行定义。
static StackType_t AppTaskCreate_Stack[128];
static StackType_t static_Task_Stack[128];
static StaticTask_t AppTaskCreate_TCB;
static StaticTask_t LED_Task_TCB;
static TaskHandle_t AppTaskCreate_Handle;
static TaskHandle_t static_Task_Handle;
任务创建函数
任务句柄指向我们创建的任务控制块。
static void AppTaskCreate(void)
{
static_Task_Handle = xTaskCreateStatic((TaskFunction_t )static_Task,
(const char* )"static_Task",
(uint32_t )128,
(void* )NULL,
(UBaseType_t )4,
(StackType_t* )static_Task_Stack,
(StaticTask_t* )&static_Task_TCB);
vTaskDelete(AppTaskCreate_Handle);
}
main函数实现
最后,就是在 main 函数中实现。
/* 开发板硬件初始化 */
Hardware_Config();
printf("Task Creating\r\n");
/* 创建AppTaskCreate任务 */
AppTaskCreate_Handle = xTaskCreateStatic((TaskFunction_t )AppTaskCreate,
(const char* )"AppTaskCreate",
(uint32_t )128,
(void* )NULL,
(UBaseType_t )3,
(StackType_t* )AppTaskCreate_Stack,
(StaticTask_t* )&AppTaskCreate_TCB);
vTaskStartScheduler(); /* 启动任务,开启调度 */
while(1); /* 正常不会执行到这里 */
效果展示
多任务创建
如果我们要创建多个任务,无论是动态还是静态,首先要定义好句柄;其次静态创建的话还需要划分好栈空间;最后在 AppTaskCreate 一次执行任务创建函数,并划分好优先级就完成了。