FreeRTOS---任务创建与删除
FreeRTOS—任务创建与删除
1 基本概念
在多任务系统里面,任务有以下三大要素:
- 做何事:函数
- 栈和TCB
- 优先级
对于每个任务我们得确定它做何事,这个可以通过函数去实现。每个任务需要设置不同的栈,同时还需要设置TCB结构体,设置TCB结构体的目的是当任务发生切换后我们还能继续找到它。最后我们还需要为任务设置优先级,这个不是必须的,但是设置后可以帮助我们处理很多场景。
对于栈和TCB结构体,我们可以动态分配,也可以静态分配,具体可以参考下面函数说明。
对于整个单片机程序,我们称之为application,应用程序。使用FreeRTOS时,我们可以在application中创建多个任务(task),有些文档把任务也称为线程(thread)。
以日常生活为例,比如这个母亲要同时做两件事:
- 喂饭:这是一个任务
- 回信息:这是另一个任务 这可以引入很多概念:
- 任务状态(State):
- 当前正在喂饭,它是running状态;另一个"回信息"的任务就是"not running"状态
- "not running"状态还可以细分:
- ready:就绪,随时可以运行
- blocked:阻塞,卡住了,母亲在等待同事回信息
- suspended:挂起,同事废话太多,不管他了
- 优先级(Priority)
- 我工作生活兼顾:喂饭、回信息优先级一样,轮流做
- 我忙里偷闲:还有空闲任务,休息一下
- 厨房着火了,什么都别说了,先灭火:优先级更高
- 栈(Stack)
- 喂小孩时,我要记得上一口喂了米饭,这口要喂青菜了
- 回信息时,我要记得刚才聊的是啥
- 做不同的任务,这些细节不一样
- 对于人来说,当然是记在脑子里
- 对于程序,是记在栈里
- 每个任务有自己的栈
- 事件驱动
- 孩子吃饭太慢:先休息一会,等他咽下去了、等他提醒我了,再喂下一口
2 任务创建与删除
2.1 什么是任务
在FreeRTOS中,任务就是一个函数,原型如下:
void ATaskFunction( void *pvParameters );
需要注意的是:
- 这个函数不能返回
- 同一个函数,可以用来创建多个任务;换句话说,多个任务可以运行同一个函数
- 函数内部,尽量使用局部变量:
- 每个任务都有自己的栈
- 每个任务运行这个函数时
- 任务A的局部变量放在任务A的栈里、任务B的局部变量放在任务B的栈里
- 不同任务的局部变量,有自己的副本
- 函数使用全局变量、静态变量的话
- 只有一个副本:多个任务使用的是同一个副本
- 需要防止冲突
示例:
void ATaskFunction( void *pvParameters )
{/* 对于不同的任务,局部变量放在任务的栈里,有各自的副本 */int32_t lVariableExample = 0;/* 任务函数通常实现为一个无限循环 */for( ;; ){/* 任务的代码 */}/* 如果程序从循环中退出,一定要使用vTaskDelete删除自己* NULL表示删除的是自己*/vTaskDelete( NULL );/* 程序不会执行到这里, 如果执行到这里就出错了 */
}
2.2 创建任务
创建任务时使用的函数如下:
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 函数指针, 任务函数const char * const pcName, // 任务的名字const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小,单位为word,10表示40字节void * const pvParameters, // 调用任务函数时传入的参数UBaseType_t uxPriority, // 优先级TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用它来操作这个任务
参数说明:
参数 | 描述 |
---|---|
pvTaskCode | 函数指针,任务对应的 C 函数。任务应该永远不退出,或者在退出时调用 “vTaskDelete(NULL)”。 |
pcName | 任务的名称,仅用于调试目的,FreeRTOS 内部不使用。pcName 的长度为 configMAX_TASK_NAME_LEN。 |
usStackDepth | 每个任务都有自己的栈,usStackDepth 指定了栈的大小,单位为 word。例如,如果传入 100,表示栈的大小为 100 word,即 400 字节。最大值为 uint16_t 的最大值。确定栈的大小并不容易,通常是根据估计来设定。精确的办法是查看反汇编代码。 |
pvParameters | 调用 pvTaskCode 函数指针时使用的参数:pvTaskCode(pvParameters)。 |
uxPriority | 任务的优先级范围为 0~(configMAX_PRIORITIES – 1)。数值越小,优先级越低。如果传入的值过大,xTaskCreate 会将其调整为 (configMAX_PRIORITIES – 1)。 |
pxCreatedTask | 用于保存 xTaskCreate 的输出结果,即任务的句柄(task handle)。如果以后需要对该任务进行操作,如修改优先级,则需要使用此句柄。如果不需要使用该句柄,可以传入 NULL。 |
返回值 | 成功时返回 pdPASS,失败时返回 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因是内存不足)。请注意,文档中提到的失败返回值是 pdFAIL 是不正确的。pdFAIL 的值为 0,而 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 的值为 -1。 |
使用静态分配内存的函数如下:
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode, // 函数指针, 任务函数const char * const pcName, // 任务的名字const uint32_t ulStackDepth, // 栈大小,单位为word,10表示40字节void * const pvParameters, // 调用任务函数时传入的参数UBaseType_t uxPriority, // 优先级StackType_t * const puxStackBuffer, // 静态分配的栈,就是一个bufferStaticTask_t * const pxTaskBuffer // 静态分配的任务结构体的指针,用它来操作这个任务
);
相比于使用动态分配内存创建任务的函数,最后2个参数不一样:
参数 | 描述 |
---|---|
pvTaskCode | 函数指针,可以简单地认为任务就是一个C函数。 它稍微特殊一点:永远不退出,或者退出时要调用"vTaskDelete(NULL)" |
pcName | 任务的名字,FreeRTOS内部不使用它,仅仅起调试作用。 长度为:configMAX_TASK_NAME_LEN |
usStackDepth | 每个任务都有自己的栈,这里指定栈大小。 单位是word,比如传入100,表示栈大小为100 word,也就是400字节。 最大值为uint16_t的最大值。 怎么确定栈的大小,并不容易,很多时候是估计。 精确的办法是看反汇编码。 |
pvParameters | 调用pvTaskCode函数指针时用到:pvTaskCode(pvParameters) |
uxPriority | 优先级范围:0~(configMAX_PRIORITIES – 1) 数值越小优先级越低, 如果传入过大的值,xTaskCreate会把它调整为(configMAX_PRIORITIES – 1) |
puxStackBuffer | 静态分配的栈内存,比如可以传入一个数组, 它的大小是usStackDepth*4。 |
pxTaskBuffer | 静态分配的StaticTask_t结构体的指针 |
返回值 | 成功:返回任务句柄; 失败:NULL |
2.3 示例1:创建任务
使用动态、静态分配内存的方式,分别创建多个任务:监测遥控器并在LCD上显示、LED闪烁、全彩LED渐变颜色、使用无源蜂鸣器播放音乐。部分代码截屏如下:
2.4 估算栈大小
对于我们上述的示例,我们使用动态分配和静态分配分别创建任务。对于这两个函数,我们都得设置栈的大小,那么我们的栈如何去选取呢?
那么栈里面保存什么内容呢,它会保存LR寄存器或者其它寄存器,还会保存局部变量,如果任务发生切换还会保存现场,这个现场就是16个寄存器,它会占据64个字节。栈中的局部变量取决于我们的代码,返回地址和其它寄存器取决于函数的调用深度,所以我们如何去评估栈的大小呢,就是得选取我们最复杂得调用关系。
对于返回地址和其它寄存器的大小,我们需要参考函数调用深度,以下面例子来讲,函数有5层调用,如果我们每层函数都足够复杂,那么每层都需要保存最大的寄存器个数即9个。那么5层调用的话最大需要保存594个字节数。
继续以我们上述示例为例,我们用音乐播放的任务来讲解。
以上根据c代码推算出栈的大小,我们分配的栈大小为128*4,足以支撑我们的任务了。
2.5 使用任务参数
多个任务可以使用同一个函数,那么怎么体现它们的差别?
- 栈不同
- 创建任务时可以传入不同的参数
我们创建3个任务,使用同一套函数,但是在LCD上打印不同的信息。
struct TaskPrintInfo{uint8_t x;uint8_t y;char name[16];
};static struct TaskPrintInfo g_Task1Info = {0,0,"Task1"};
static struct TaskPrintInfo g_Task2Info = {0,3,"Task2"};
static struct TaskPrintInfo g_Task3Info = {0,6,"Task3"};
static int g_LCDCanUse = 1;void LcdPrintTask(void *params)
{struct TaskPrintInfo *pInfo = params;uint32_t cnt = 0;int len;LCD_Init();LCD_Clear();while(1){/* 打印信息 */if(g_LCDCanUse){g_LCDCanUse = 0;len = LCD_PrintString(pInfo->x, pInfo->y, pInfo->name);len += LCD_PrintString(len, pInfo->y, ":");LCD_PrintSignedVal(len, pInfo->y, cnt++);g_LCDCanUse = 1;}mdelay(500);}
}
上述代码中的 pInfo 来自参数 params ,params 来自哪里?创建任务时传入的。
代码如下:
- 使用xTaskCreate创建任务时,第4个参数就是pvParameters
- 不同的任务,pvParameters不一样
xTaskCreate(LcdPrintTask, "task1", 128, &g_Task1Info, osPriorityAboveNormal, NULL);
xTaskCreate(LcdPrintTask, "task2", 128, &g_Task2Info, osPriorityAboveNormal, NULL);
xTaskCreate(LcdPrintTask, "task3", 128, &g_Task3Info, osPriorityAboveNormal, NULL);
那么在任务函数中为什么需要延时呢?我们使用全局变量达到了互斥的效果,如果第一个任务正在运行LCD打印,那么第二个任务判断互斥变量后无法运行,我们设置延时的目的就是希望任务调度是在延时处产生,那么互斥变量在这时就是被清除的状态。
2.6 任务的删除
删除任务时使用的函数如下:
void vTaskDelete( TaskHandle_t xTaskToDelete );
参数说明:
参数 | 描述 |
---|---|
pvTaskCode | 任务句柄,使用xTaskCreate创建任务时可以得到一个句柄。 也可传入NULL,这表示删除自己。 |
怎么删除任务?举个不好的例子:
- 自杀:vTaskDelete(NULL)
- 被杀:别的任务执行vTaskDelete(pvTaskCode),pvTaskCode是自己的句柄
- 杀人:执行vTaskDelete(pvTaskCode),pvTaskCode是别的任务的句柄
示例:
void StartDefaultTask(void *argument)
{/* USER CODE BEGIN StartDefaultTask *//* Infinite loop */uint8_t dev, data;int len;TaskHandle_t xSoundTaskHandle;BaseType_t ret;LCD_Init();LCD_Clear();IRReceiver_Init();LCD_PrintString(0,0,"waiting control");while (1){/* 读取红外遥控器 */if(0 == IRReceiver_Read(&dev,&data)){if(data == 0xa8) /* Play */{/* 创建播放音乐的任务 */extern void PlayMusic(void *params);if(xSoundTaskHandle == NULL){LCD_ClearLine(0,0);LCD_PrintString(0,0,"Create Task");ret = xTaskCreate(PlayMusic,"SoundTask",128,NULL,osPriorityNormal,&xSoundTaskHandle);}}else if(data == 0xa2) /* Power */{/* 删除播放音乐的任务 */if(xSoundTaskHandle != NULL){LCD_ClearLine(0,0);LCD_PrintString(0,0,"Delete Task");vTaskDelete(xSoundTaskHandle);PassiveBuzzer_Control(0);xSoundTaskHandle = NULL;}}}}/* USER CODE END StartDefaultTask */
}