目录
一、创建函数(初步介绍)
1. 动态创建任务函数
1.1 函数说明
1.2 动态创建任务的步骤
1.3 动态创建任务函数的内部实现
2. 静态创建任务函数
2.1 函数说明
2.2 静态创建任务步骤
2.3 静态创建任务的内部实现
二、删除函数(初步介绍)
1. 函数说明
2. 删除流程
3. 内部实现过程
三、动态创建任务函数(详细)
1. 项目的背景
2. 编写代码
3. 相关补充
一、创建函数(初步介绍)
| API函数 | 描述 |
| xTaskCreate() | 动态方式创建任务 |
| xTaskCreateStatic() | 静态方式创建任务 |
| vTaskDelete() | 删除任务 |
- 动态创建任务:任务的任务控制块(TCB)和任务的栈空间所需的内存,均有FreeRTOS自己分配
- 静态创建任务:任务的任务控制块(TCB)和任务的栈空间所需的内存,需用户分配提供。
1. 动态创建任务函数
1.1 函数说明
BaseType_t xTaskCreate ( TaskFunction_t pxTaskCode, /* 指向任务函数的指针 */ const char * const pcName, /* 任务名字,最大长度configMAX_TASK_NAME_LEN */ const configSTACK_DEPTH_TYPE usStackDepth, /* 任务堆栈大小,默认单位4字节 1字=4字节*/ void * const pvParameters, /* 传递给任务函数的参数 */ UBaseType_t uxPriority, /* 任务优先级,范围:0 ~ configMAX_PRIORITIES - 1 */ TaskHandle_t * const pxCreatedTask /* 任务句柄,就是任务的任务控制块 */ )创建完成后,参数会往任务控制块(TCB)中存放,随后句柄存放此任务在TCB中的地址。
函数的返回值说明:
- pdPASS:任务创建成功。
- errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY:任务创建失败。
1.2 动态创建任务的步骤
- 将宏 configSUPPORT_DYNAMIC_ALLOCATION配置为 1
- 定义函数入口参数。
- 编写任务函数。
至于为什么要将宏 configSUPPORT_DYNAMIC_ALLOCATION配置为1呢?我们用 ctrl + F在工程里面搜索将会在 FreeRTOS.h 中得到:
因为在工程中有的宏不是说一开始就有初始值的,而是先经过一些初始化后,发现没有,自动定义赋初值的,所以说宏 configSUPPORT_DYNAMIC_ALLOCATION一开始就是为1,默认就是开启的。
人话总结:自己先定义好一个句柄类型的变量,然后对该变量的成员进行赋值,然后再将该句柄的地址交给函数,函数就会在os里面申请tcb并填上对应的结构体成员的值
注:此函数创建的任务会立刻进入就绪状态,由任务调度器调度运行。
1.3 动态创建任务函数的内部实现
- 申请堆栈内存&任务控制块内存。
- TCB结构体成员赋值。
- 添加新任务到就绪列表
第一步申请栈内存和任务控制块内存是由FreeRTOS自动帮完成的,然后接下来是第二部分重点,TCB结构体成员的赋值,其实上是我们在调用xTaskCreate()创建任务时就已经给TCB结构体成员赋值好了。
但是有一个疑惑,就是这两个结构体的成员也不一样啊,但是为什么给xTaskCreate()赋值就会给TCB结构体赋值呢?
原因就在于,xTaskCreate()层属于接口层面,面向程序人员,而TCB结构体属于实现层,他们之间是一种映射关系而非一对一关系的。xTaskCreate 函数内部并不会傻傻地列出一行 tcb->参数1 = 传入的参数1。它会创建一个中间任务来调用一个私有初始化函数,通常叫 prvInitialiseNewTask,来把接口参数“填充”或“映射”到 TCB 结构体的对应成员中。
通过xTaskCreate()创建好任务后,任务就会自动处于就绪列表。
TCB结构体成员介绍:
typedef struct tskTaskControlBlock { volatile StackType_t * pxTopOfStack; /* 任务栈栈顶,必须为TCB的第一个成员 */ ListItem_t xStateListItem; /* 任务状态列表项 */ ListItem_t xEventListItem; /* 任务事件列表项 */ UBaseType_t uxPriority; /* 任务优先级,数值越大,优先级越大 */ StackType_t * pxStack; /* 任务栈起始地址 */ char pcTaskName[ configMAX_TASK_NAME_LEN ]; /* 任务名字 */ … 省略很多条件编译的成员 } tskTCB;任务栈栈顶,在任务切换时的任务上下文保存、任务恢复息息相关。每个任务都有属于自己的任务控制块,类似身份证。
2. 静态创建任务函数
2.1 函数说明
TaskHandle_t xTaskCreateStatic ( TaskFunction_t pxTaskCode, /* 指向任务函数的指针 */ const char * const pcName, /* 任务函数名 */ const uint32_t ulStackDepth, /* 任务堆栈大小,单位是4字节 */ void * const pvParameters, /* 传递的任务函数参数 */ UBaseType_t uxPriority, /* 任务优先级 */ StackType_t * const puxStackBuffer, /* 任务堆栈,一般为数组,由用户分配 */ StaticTask_t * const pxTaskBuffer /* 任务控制块指针,由用户分配 */ )跟动态的区别就是,动态的句柄变为了两个成员变量(因为静态是用户自己设置),那么静态创建的任务句柄呢?请看返回值:
返回值如下:
- NULL:用户没有提供相应的内存,任务创建失败。
- 其他值:任务句柄,任务创建成功。
2.2 静态创建任务步骤
- 将宏configSUPPORT_STATIC_ALLOCATION 配置为 1。
- 定义空闲任务&定时器任务的任务堆栈及TCB
- 实现接口函数:
- vApplicationGetIdleTaskMemory()
- vApplicationGetTimerTaskMemory()(如果开启软件定时器)
- 定义函数入口参数。
- 编写任务函数。
我还没明白,稍等
此函数创建的任务会立刻进入就绪态,由任务调度器调度运行。
2.3 静态创建任务的内部实现
- TCB结构体成员变量赋值
- 添加新任务到就绪列表
二、删除函数(初步介绍)
1. 函数说明
/* 删除函数 * 参数为要删除任务的句柄 * TaskHandle_t:表示任务句柄类型 */ void vTaskDelete( TaskHandle_t xTaskToDelete )当输入参数为NULL时,表示删除任务自身(当前正在运行的任务)
注:参数是句柄是因为句柄是任务在TCB中的地址,TCB是实现层,删除是要删除TCB实现层。
该函数用于删除已被创建的任务,被删除的任务将从就绪态任务列表、阻塞态任务列表、挂起态任务列表和事件列表中移除。
我们的删除函数实际上是标记删除,真正删除是靠空闲任务来释放被删除任务中由系统分配的内存。需要注意的一点是:由用户在任务中申请的内存,则需要用户在任务删除前释放出来,否则会导致内存泄漏。
2. 删除流程
- 使用删除任务函数,需将宏INCLUDE_vTaskDelete 配置为 1
- 入口参数输入需要删除的任务句柄(NULL代表删除本身)
3. 内部实现过程
- 获取所要删除任务的控制块(句柄就是TCB控制块的地址):通过传入的任务句柄,判断所需要删除哪个任务,NULL代表删除自身。
- 将被删除任务,移除所在列表:将该任务在所在列表中移除,包括:就绪、阻塞、挂起、事件等列表。
- 判断所需要删除的任务:如果删除任务自身,需先添加到等待删除列表(因为删除函数实际上就是标记删除),内存释放将在空闲任务执行;如果删除其他任务,释放内存,任务数量 - 1。
- 更新下个任务的阻塞时间:更新下一个任务的阻塞超时时间,以防被删除的任务就是下一个阻塞超时的任务。
举例:假如我在一个任务的中间执行删除函数,由于删除函数是标记删除,他会把自身任务添加到等待删除列表,后面的代码继续执行。随后任务删除后,等到空闲任务执行时,会释放系统分配的内存。然后任务数量-1。
三、动态创建任务函数(详细)
1. 项目的背景
PA8~11分别为KEY1~4,PB3~5分别为LED1~3
再打开usart2:
2. 过程
首先我们需要编写FreeRTOS_Demo.h文件,还是.h文件的老套路:
#ifndef __FREERTOS_DEMO_H__ #define __FREERTOS_DEMO_H__ #endif仅此而已。
我们来看FreeRTOS_Demo.c文件,我们先导入一些RTOS必要的.h文件
/* FreeRTOS最基本的头文件,必须要有的 */ #include "FreeRTOS.h" #include "task.h"随后根据实际情况导入一些需要的头文件:
/* 需要用到的其他头文件,根据实际情况添加 */ #include "LED.h" #include "KEY.h" #include "usart.h" #include "FreeRTOS_Demo.h"首先,我们要先创建一个启动任务函数,启动任务函数中需要创建一个启动任务和开启任务调度器,我们要提前定义好启动任务需要用到的参数
/* 启动任务函数的配置 */ #define TASK_Start_STACK 128 //任务栈大小 #define TASK_Start_PRIORITY 1 //任务优先级 TaskHandle_t Task_Start_Handle; //任务句柄 void Task_Start(void *pvParameters); //启动任务 /* 启动任务函数 */ void FreeRTOS_Start(void) { /* 1.创建一个启动任务 */ xTaskCreate( (TaskFunction_t)Task_Start, //任务启动函数的地址 (char *)"Task_Start", //任务名称 (configSTACK_DEPTH_TYPE) TASK_Start_STACK, //任务栈大小,以字为单位,默认最小是128字 (void *) NULL, //任务形参,由于没有参数,就填NULL (UBaseType_t) TASK_Start_PRIORITY, //任务优先级,范围是0 ~ configMAX_PRIORITIES-1 (TaskHandle_t *) &Task_Start_Handle //任务句柄,用于储存TCB任务控制块的地址 ); /* 2.启动调度器 */ vTaskStartScheduler(); }随后就是编写启动任务,我们是通过这个启动任务来启动自己的任务的,所以我们需要先提前定义好创建自己的任务时的相关参数
/* 任务1的配置 */ #define TASK1_STACK 128 #define TASK1_PRIORITY 2 TaskHandle_t Task1_Handle; void Task1(void *pvParameters); /* 任务2的配置 */ #define TASK2_STACK 128 #define TASK2_PRIORITY 3 TaskHandle_t Task2_Handle; void Task2(void *pvParameters); /* 任务3的配置 */ #define TASK3_STACK 128 #define TASK3_PRIORITY 4 TaskHandle_t Task3_Handle; void Task3(void *pvParameters); void Task_Start(void *pvParameters) { /* 进入临界区,防止其他任务在创建任务时被调度 */ taskENTER_CRITICAL(); //临界区有进入也有退出,进入时要关闭中断,退出时要开启中断 /* 创建任务1 */ xTaskCreate( (TaskFunction_t)Task1, (char *)"Task1", (configSTACK_DEPTH_TYPE) TASK1_STACK, (void *) NULL, (UBaseType_t) TASK1_PRIORITY, (TaskHandle_t *) &Task1_Handle ); /* 创建任务2 */ xTaskCreate( (TaskFunction_t)Task2, (char *)"Task2", (configSTACK_DEPTH_TYPE) TASK2_STACK, (void *) NULL, (UBaseType_t) TASK2_PRIORITY, (TaskHandle_t *) &Task2_Handle ); /* 创建任务3 */ xTaskCreate( (TaskFunction_t)Task3, (char *)"Task3", (configSTACK_DEPTH_TYPE) TASK3_STACK, (void *) NULL, (UBaseType_t) TASK3_PRIORITY, (TaskHandle_t *) &Task3_Handle ); /* 删除启动任务自身 */ vTaskDelete(NULL); /* 退出临界区 */ taskEXIT_CRITICAL(); }随后就是编写自己的实际任务了
/* 任务1:LED1闪烁 */ void Task1(void *pvParameters) { for(;;) { printf("Task1执行...\r\n"); LED_Turn_Toggle(LED1_Pin); vTaskDelay(500); //这里的延时不能使用HAL_Delay(),因为HAL_Delay不会使任务进入阻塞态,需要用RTOS专用的延时函数 } } /* 任务2:LED2闪烁 */ void Task2(void *pvParameters) { for(;;) { printf("任务2执行...\r\n"); LED_Turn_Toggle(LED2_Pin); vTaskDelay(500); } } /* 任务3:按键1按下就删除任务1 */ void Task3(void *pvParameters) { uint8_t key = 0; while(1) { key = Key_Detect(); if(key == KEY1_PRESS) { /* 防止重复删除任务1 */ if(Task1_Handle != NULL) { printf("任务3执行...\r\n"); /* 删除任务1 */ vTaskDelete(Task1_Handle); } } vTaskDelay(500); /* 因为任务3的优先级最高,所以需要延时500ms,让他让出CPU资源给任务1和任务2 */ } }需要注意的点:
- 启动任务 void Task_Start(void *pvParameters) 的最后要删除自身 vTaskDelete(NULL);因为启动任务只能启动一次,不能重复启动
- 我们在自己的任务中使用延时函数时,不能使用HAL_Delay()因为这个延时函数不能让任务进入阻塞态,我们要使用RTOS提供的延时函数:vTaskDelay()
- 在启动任务的开始和结尾我们要添加进入临界区和退出临界区的函数 taskENTER_CRITICAL(); 和 taskEXIT_CRITICAL();这是为了防止中断或任务切换在系统核心数据结构还未完全初始化时介入,从而导致系统崩溃
随后来看一下LED和KEY的代码:
LED.c和LED.h
/*---------------------LED.h begin---------------------*/ #ifndef __LED_H__ #define __LED_H__ #include "gpio.h" #define LED uint16_t void LED_Turn_ON(LED led); void LED_Turn_OFF(LED led); void LED_Turn_Toggle(LED led); void LED_Turn_Off_All(LED led[], LED len); #endif /*---------------------LED.h end-----------------------*/ /*---------------------LED.c begin----------------------*/ #include "LED.h" void LED_Turn_ON(LED led) { HAL_GPIO_WritePin(GPIOB, led, GPIO_PIN_SET); } void LED_Turn_OFF(LED led) { HAL_GPIO_WritePin(GPIOB, led, GPIO_PIN_RESET); } void LED_Turn_Toggle(LED led) { HAL_GPIO_TogglePin(GPIOB, led); } void LED_Turn_Off_All(LED led[], LED len) { LED i; for(i = 0; i < len; i++) { LED_Turn_OFF(led[i]); } } /*---------------------LED.c end------------------------*/KEY.c和KEY.h
/*---------------------KEY.h begin----------------------*/ #ifndef __KEY_H__ #define __KEY_H__ #include "main.h" #define KEY1 HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin) #define KEY2 HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin) #define KEY3 HAL_GPIO_ReadPin(KEY3_GPIO_Port, KEY3_Pin) #define KEY4 HAL_GPIO_ReadPin(KEY4_GPIO_Port, KEY4_Pin) #define KEY1_PRESS 1 #define KEY2_PRESS 2 #define KEY3_PRESS 3 #define KEY4_PRESS 4 uint8_t Key_Detect(void); #endif /*---------------------KEY.h end------------------------*/ /*---------------------KEY.c begin----------------------*/ #include "Key.h" uint8_t Key_Detect(void) { uint8_t res = 0; if(KEY1 == GPIO_PIN_RESET || KEY2 == GPIO_PIN_RESET || KEY3 == GPIO_PIN_RESET || KEY4 == GPIO_PIN_RESET) { HAL_Delay(10); if(KEY1 == GPIO_PIN_RESET) res = KEY1_PRESS; if(KEY2 == GPIO_PIN_RESET) res = KEY2_PRESS; if(KEY3 == GPIO_PIN_RESET) res = KEY3_PRESS; if(KEY4 == GPIO_PIN_RESET) res = KEY4_PRESS; } return res; } /*---------------------KEY.c end------------------------*/3. 相关补充
我们的FreeRTOS_Start()启动任务相关代码:
void FreeRTOS_Start(void) { /* 1.创建一个启动任务 */ xTaskCreate( (TaskFunction_t)Task_Start, //任务启动函数的地址 (char *)"Task_Start", //任务名称 (configSTACK_DEPTH_TYPE) TASK_Start_STACK, //任务栈大小,以字为单位,默认最小是128字 (void *) NULL, //任务形参,由于没有参数,就填NULL (UBaseType_t) TASK_Start_PRIORITY, //任务优先级,范围是0 ~ configMAX_PRIORITIES-1 (TaskHandle_t *) &Task_Start_Handle //任务句柄,用于储存TCB任务控制块的地址 ); /* 2.启动调度器 */ vTaskStartScheduler(); } void Task_Start(void *pvParameters) { /* 进入临界区,防止其他任务在创建任务时被调度 */ taskENTER_CRITICAL(); //临界区有进入也有退出,进入时要关闭中断,退出时要开启中断 /* 创建任务1 */ xTaskCreate( (TaskFunction_t)Task1, (char *)"Task1", (configSTACK_DEPTH_TYPE) TASK1_STACK, (void *) NULL, (UBaseType_t) TASK1_PRIORITY, (TaskHandle_t *) &Task1_Handle ); /* 创建任务2 */ xTaskCreate( (TaskFunction_t)Task2, (char *)"Task2", (configSTACK_DEPTH_TYPE) TASK2_STACK, (void *) NULL, (UBaseType_t) TASK2_PRIORITY, (TaskHandle_t *) &Task2_Handle ); /* 创建任务3 */ xTaskCreate( (TaskFunction_t)Task3, (char *)"Task3", (configSTACK_DEPTH_TYPE) TASK3_STACK, (void *) NULL, (UBaseType_t) TASK3_PRIORITY, (TaskHandle_t *) &Task3_Handle ); /* 删除启动任务自身 */ vTaskDelete(NULL); /* 退出临界区 */ taskEXIT_CRITICAL(); }我们创建好启动任务后,会启动任务调度器,然后任务调度器就会开始调度任务,此时只有一个任务就是启动任务,所以他就会开始执行启动任务,然后创建三个任务。此刻我们有一个任务,就是启动任务,那么我们执行启动任务是先创建任务1,再创建任务2,最后创建任务3,如果没有临界区的话,那么就会导致刚创建好任务1时,因为启动任务和任务1的优先级不同(我们的案例的优先级排序是:启动任务 < 任务1 < 任务2 < 任务3 )。就会让CPU先执行任务1,只有当任务1含有延时时,才会让出CPU继续去执行启动任务,再创建任务2,随后还是一样的道理。有延时才会让出CPU去执行启动任务,直到启动任务执行完毕。
所以,临界区是让启动任务把所有任务创建好后才被任务调度器嗲用,而不是创建部分任务后就被任务调度器调用。
四、静态创建任务函数(详细)
1. 静态相较于动态的区别:
区别就在于这两个需要用户自己去实现,我们需要先创建好,然后把地址传进来;
2. 过程
我们在启动函数中静态创建任务时,输入xTaskCreateStatic()会发现没有代码提示,并且还进不去函数。
这是因为我们需要在 FreeRTOSConfig.h 里面定义一个宏:
#define configSUPPORT_STATIC_ALLOCATION 1这个宏的意思就是是否支持静态分配,只有我们添加了这个宏后才会进入xTaskCreateStatic()函数内部(注:动态分配默认是开启的)
静态创建的函数:
因为静态和动态创建时,前面几个参数都一样,所以可以直接复制动态的参数。最后两个参数是任务栈的地址和TCB的地址;
栈:链表或数组的实现
栈和TCB都需要我们自己来实现
随后把这两个参数赋值给静态创建的函数就好
最后一步就是:静态创建的返回值是一个句柄
最终是这样:
/* 1.创建一个启动任务 */ Task_Start_Handle = xTaskCreateStatic( (TaskFunction_t)Task_Start, //任务启动函数的地址 (char *)"Task_Start", //任务名称 (uint32_t)(configSTACK_DEPTH_TYPE) TASK_Start_STACK, //任务栈大小,以字为单位,默认最小是128字 (void *) NULL, //任务形参,由于没有参数,就填NULL (UBaseType_t) TASK_Start_PRIORITY, //任务优先级,范围是0 ~ configMAX_PRIORITIES-1 (StackType_t *)Task_Start_Stack, //任务栈的地址 (StaticTask_t *) &Task_Start_Tcb //TCB地址 );然后就是把启动任务中三个创建任务的函数按照以上步骤创建就好;
最后的最后就是我们还需要实现两个函数:
我们就是通过vApplicationGetIdleTaskMemory接口函数来处理用户自己的资源的。
而且当我们打开定时器任务时:(把宏 congigUSE_TIMERS赋值为1)时,会为我们创建一个定时器任务
话不多说,直接来实现两个接口函数:在此之前要在FreeRTOSConfig.h中先添加定时器相关的配置
/* 软件定时器的配置 */ #define configUSE_TIMERS 1 /* 1: 使能软件定时器, 默认: 0 */ #define configTIMER_TASK_PRIORITY ( configMAX_PRIORITIES - 1 ) /* 定义软件定时器任务的优先级, 无默认configUSE_TIMERS为1时需定义 */ #define configTIMER_QUEUE_LENGTH 5 /* 定义软件定时器命令队列的长度, 无默认configUSE_TIMERS为1时需定义 */ #define configTIMER_TASK_STACK_DEPTH ( configMINIMAL_STACK_SIZE * 2) /* 定义软件定时器任务的栈空间大小, 无默认configUSE_TIMERS为1时需定义 *//* ===============================静态创建方式,需要手动指定两个特殊任务的资源======================================= */ /* 空闲任务的配置 */ StackType_t Idle_Task_Stack[configMINIMAL_STACK_SIZE]; //静态任务的栈,以数组形式表示 StaticTask_t Idle_Task_Tcb; //静态任务的TCB结构体 /* 软件定时器的配置 */ StackType_t Timer_Task_Stack[configTIMER_TASK_STACK_DEPTH]; //静态任务的栈,以数组形式表示 StaticTask_t Timer_Task_Tcb; //静态任务的TCB结构体 /* 分配空闲任务的资源*/ void vApplicationGetIdleTaskMemory( StaticTask_t ** ppxIdleTaskTCBBuffer, //TCB任务控制块的地址 StackType_t ** ppxIdleTaskStackBuffer, //任务空间栈的地址 uint32_t * pulIdleTaskStackSize ) //任务栈大小,以字为单位 { *ppxIdleTaskTCBBuffer = &Idle_Task_Tcb; *ppxIdleTaskStackBuffer = Idle_Task_Stack; *pulIdleTaskStackSize = configMINIMAL_STACK_SIZE; } /* 分配软件定时器的资源 */ void vApplicationGetTimerTaskMemory( StaticTask_t ** ppxTimerTaskTCBBuffer, StackType_t ** ppxTimerTaskStackBuffer, uint32_t * pulTimerTaskStackSize ) { *ppxTimerTaskTCBBuffer = &Timer_Task_Tcb; *ppxTimerTaskStackBuffer = Timer_Task_Stack; *pulTimerTaskStackSize = configTIMER_TASK_STACK_DEPTH; }