FreeRTOS多任务管理
文章目录1、任务1.1 任务简介1.2 任务调度1.3 任务的状态 (就绪态 / 运行态 / 阻塞态 / 挂起态)1.4 空闲任务2、动态创建两个任务2.1 定义动态内存空间的堆(heap)2.2 定义任务函数2.3 定义 任务控制块 指针2.4 动态创建任务 xTaskCreate()2.5 启动任务 vTaskStartScheduler()3、常用的任务函数3.1 任务挂起 vTaskSuspend()3.2 任务恢复 vTaskResume()3.3 任务删除 vTaskDelete()3.4 阻塞延时函数3.4.1 相对延时函数 vTaskDelay()3.4.2 绝对延时函数 vTaskDelayUntil()1、任务1.1 任务简介(1) 在裸机系统中系统的主体就是 main 函数里面顺序执行的无限循环这个无限循环里面 CPU 按照顺序完成各种事情。(2) 在FreeRTOS中根据功能的不同把整个系统分割成一个个独立且无限循环、无法返回的函数这个函数就称为任务。1.2 任务调度任何时刻只有一个任务得到运行FreeRTOS调度器 决定运行哪个任务。调度器会不断的启动、停止每一个任务宏观看上去所有的任务都在同时在执行。FreeRTOS 中的任务是抢占式调度机制高优先级的任务可打断低优先级任务低优先级任务必须在高优先级任务阻塞或结束后 才能得到调度。1.3 任务的状态 (就绪态 / 运行态 / 阻塞态 / 挂起态)任务状态通常分为以下四种1、就绪态(ready)该任务在就绪列表中就绪的任务已经具备执行的能力只等待调度器进行调度新创建的任务会初始化为就绪态。2、运行态(running)该状态表明任务正在执行此时它占用处理器FreeRTOS调度器选择运行的永远是处于最高优先级的 就绪态任务当任务被运行的一刻它的任务状态就变成了运行态。3、阻塞态(blocked)正在运行的任务发生阻塞(延时、读信号量等待、读写队列或者等待读写事件) 时该任务会从就绪列表中删除任务状态由运行态变成阻塞态。4、挂起态(suspended)处于挂起态的任务 对调度器而言是不可见的让一个任务进入挂起状态的唯一办法就是调用 vTaskSuspend() 函数而把一个挂起状态的任务恢复的唯一途径 就是调用 vTaskResume() 或 vTaskResumeFromISR() 函数。挂起态与阻塞态的区别当任务有较长的时间不允许运行的时候我们可以挂起任务这样 调度器就不会管这个任务的任何信息直到我们调用恢复任务的 API 函数而任务处于阻塞态的时候系统还需要判断 阻塞态的任务是否超时是否可以解除阻塞。他们之间的转换关系如下图(1)创建任务→就绪态 (ready)任务创建完成后进入就绪态表明任务已准备就绪随时可以运行只等待调度器进行调度。(2)就绪态→运行态 (running)发生任务切换时就绪列表中最高优先级的任务被执行从而进入运行态。(3)运行态→就绪态有更高优先级任务创建或者恢复后会发生任务调度此刻就绪列表中最高优先级任务变为运行态那么原先运行的任务由运行态变为就绪态依然在就绪列表中等待最高优先级的任务运行完毕继续运行原来的任务 (此处可以看做是 CPU使用权被更高优先级的任务抢占了)。(4)运行态→阻塞态 (blocked)正在运行的任务发生阻塞 (延时、读信号量等待) 时该任务会从就绪列表中删除任务状态由运行态变成阻塞态然后发生任务切换运行就绪列表中当前最高优先级任务。(5)阻塞态→就绪态阻塞的任务被恢复后 (任务恢复、延时时间超时、读信号量超时 或读到信号量等)此时被恢复的任务会被加入就绪列表从而由阻塞态变成就绪态如果此时被恢复任务的优先级高于正在运行任务的优先级则会发生任务切换将该任务将再次转换任务状态由就绪态变成运行态。(6) (7) (8)就绪态、阻塞态、运行态→挂起态 (suspended)任务可以通过调用 vTaskSuspend() API 函数都可以将处于任何状态的任务挂起被挂起的任务得不到CPU的使用权也不会参与调度除非它从挂起态中解除。(9)挂起态→就绪态把一个挂起状态的任务 恢复的唯一途径就是调用 vTaskResume() 或 vTaskResumeFromISR() API 函数如果此时被恢复任务的优先级高于正在运行任务的优先级则会发生任务切换将该任务将再次转换任务状态由就绪态 变成 运行态。1.4 空闲任务空闲任务是系统在启动调度器的时候创建的优先级最低的任务空闲任务主体主要是做一些系统内存的清理工作。如果没有其它任务可以运行这个时候CPU就运行空闲任务。2、动态创建两个任务2.1 定义动态内存空间的堆(heap)使用动态内存即 堆(heap)也属于 SRAM。FreeRTOS 做法是在SRAM里面定义一个大数组也就是堆内存供 FreeRTOS的动态内存分配函数使用在第一次使用的时候系统会将定义的堆内存进行初始化。//系统所有总的堆大小#defineconfigTOTAL_HEAP_SIZE((size_t)(36*1024))(1)staticuint8_tucHeap[configTOTAL_HEAP_SIZE];(2)(1) 堆内存的大小为configTOTAL_HEAP_SIZE在 FreeRTOSConfig.h 中由用户自己定义configSUPPORT_DYNAMIC_ALLOCATION这个宏定义在使用FreeRTOS操作系统的时候必须开启。(2) 从内部SRAM 里面定义一个静态数组 ucHeap大小由configTOTAL_HEAP_SIZE这个宏决定目前定义为 36KB。2.2 定义任务函数创建两个任务分别 让 LED 灯以 500ms / 1000ms的频率闪烁具体实现见代码清单staticvoidLED1_Task(void*parameter){while(1){LED1_ON;vTaskDelay(500);/* 延时500个tick */LED1_OFF;vTaskDelay(500);/* 延时500个tick */}}staticvoidLED2_Task(void*parameter){while(1){(1)LED2_ON;vTaskDelay(1000);/* 延时1000个tick */(2)LED2_OFF;vTaskDelay(1000);/* 延时1000个tick */}}(1) 任务必须是一个死循环。(2) 任务里面的延时函数必须使用 FreeRTOS 里面提供的延时函数vTaskDelay()。调用 vTaskDelay() 函数的时候当前任务进入阻塞状态调度器会切换到其它就绪的任务从而实现多任务。2.3 定义 任务控制块 指针任务控制块(TCB) 是在任务创建的时候分配内存空间创建任务创建函数会返回一个指针用于指向任务控制块所以要预先为任务栈定义一个任务控制块指针也就是任务句柄。任务句柄的数据类型为 TaskHandle_t在 task.h 中定义实际上就是一个空指针具体实现见代码清单/* 任务句柄 */typedefvoid*TaskHandle_t;定义任务句柄/**************************** 任务句柄 ********************************//* * 任务句柄是一个指针用于指向一个任务当任务创建好之后它就具有了一个任务句柄 * 以后我们要想操作这个任务都需要通过这个任务句柄如果是自身的任务操作自己那么 * 这个句柄可以为 NULL。 */staticTaskHandle_t AppTaskCreate_HandleNULL;/* 创建任务句柄 */staticTaskHandle_t LED1_Task_HandleNULL;/* LED1 任务句柄 */staticTaskHandle_t LED2_Task_HandleNULL;/* LED2 任务句柄 */2.4 动态创建任务 xTaskCreate()使用动态内存的时候使用 xTaskCreate() 函数来创建一个任务。/* 创建 AppTaskCreate 任务 */xReturnxTaskCreate((TaskFunction_t)AppTaskCreate,/* 任务入口函数 */(1)(constchar*)AppTaskCreate,/* 任务名字 */(2)(uint16_t)512,/* 任务栈大小 */(3)(void*)NULL,/* 任务入口函数参数 */(4)(UBaseType_t)1,/* 任务的优先级 */(5)(TaskHandle_t*)AppTaskCreate_Handle);/* 任务控制块 指针 */(6)/* 启动任务调度 */if(pdPASSxReturn)vTaskStartScheduler();/* 启动任务开启调度 */(1) 任务入口函数即任务函数的名称。(用户自定义用于创建任务)(2) 任务名字字符串形式 最大长度由 FreeRTOSConfig .h 中定义的configMAX_TASK_NAME_LEN宏指定多余部分会被自动截掉这里任务名字最好要与任务函数入口名字一致方便进行调试。(3) 任务堆栈大小单位为字在32位的处理器下一个字等于4个字节那么任务大小就为 512*4 2048 字节。(4) 任务入口函数形参不用的时候配置为 0 或者 NULL即可。(5) 任务的优先级。 优先级范围根据 FreeRTOSConfig .h 中的宏configMAX_PRIORITIES决定如果使能configUSE_PORT_OPTIMISED_TASK_SELECTION这个宏定义则最多支持 32 个优先级如果不用特殊方法查找下一个运行的任务那么则不强制要求限制最大可用优先级数目。在 FreeRTOS 中数值越大优先级越高0代表最低优先级。(6) 任务控制块指针。 在使用动态内存的时候任务创建函数 xTaskCreate() 会返回一个指针 指向任务控制块该任务控制块是 xTaskCreate() 函数里面动态分配的一块内存。任务创建好后是处于任务就绪(ready)在就绪态的任务可以参与操作系统的调度。说明为了方便管理任务的创建都通过 AppTaskCreate() 任务创建函数来执行staticvoidAppTaskCreate(void){BaseType_t xReturnpdPASS;/* 定义一个创建信息返回值默认为 pdPASS */taskENTER_CRITICAL();//进入临界区/* 创建 LED1_Task 任务 */xReturnxTaskCreate((TaskFunction_t)LED1_Task,/* 任务入口函数 */(constchar*)LED1_Task,/* 任务名字 */(uint16_t)512,/* 任务栈大小 */(void*)NULL,/* 任务入口函数参数 */(UBaseType_t)2,/* 任务的优先级 */(TaskHandle_t*)LED1_Task_Handle);/* 任务控制块指针 */if(pdPASSxReturn)printf(创建 LED1_Task 任务成功!\r\n);/* 创建 LED2_Task 任务 */xReturnxTaskCreate((TaskFunction_t)LED2_Task,/* 任务入口函数 */(constchar*)LED2_Task,/* 任务名字 */(uint16_t)512,/* 任务栈大小 */(void*)NULL,/* 任务入口函数参数 */(UBaseType_t)3,/* 任务的优先级 */(TaskHandle_t*)LED2_Task_Handle);/* 任务控制块指针 */if(pdPASSxReturn)printf(创建 LED2_Task 任务成功!\r\n);vTaskDelete(AppTaskCreate_Handle);//删除 AppTaskCreate 任务taskEXIT_CRITICAL();//退出临界区}2.5 启动任务 vTaskStartScheduler()在创建完任务的时候我们需要开启调度器因为创建仅仅是把任务添加到系统中还没真正调度并且空闲任务也没实现定时器任务也没实现这些都是在开启调度函数vTaskStartScheduler() 中实现的。任务调度器只启动一次之后就不会再次执行了。从此任务管理都由 FreeRTOS 管理此时是真正进入实时操作系统。/***************************************************************** * brief 主函数 * param 无 * retval 无 * note 第一步开发板硬件初始化 第二步创建 APP 应用任务 第三步启动 FreeRTOS开始多任务调度 ****************************************************************/intmain(void){BaseType_t xReturnpdPASS;/* 定义一个创建信息返回值默认为 pdPASS *//* 第1步 开发板硬件初始化 */BSP_Init();/* 第2步 创建 AppTaskCreate 任务 */xReturnxTaskCreate((TaskFunction_t)AppTaskCreate,/* 任务入口函数 */(1)(constchar*)AppTaskCreate,/* 任务名字 */(2)(uint16_t)512,/* 任务栈大小 */(3)(void*)NULL,/* 任务入口函数参数 */(4)(UBaseType_t)1,/* 任务的优先级 */(5)(TaskHandle_t*)AppTaskCreate_Handle);/* 任务控制块指针 */(6)/* 第3步 启动任务调度 */if(pdPASSxReturn)vTaskStartScheduler();/* 启动任务开启调度 */elsereturn-1;while(1);/* 正常不会执行到这里 */}3、常用的任务函数3.1 任务挂起 vTaskSuspend()任务可以通过调用 vTaskSuspend() 函数都可以将处于任何状态的任务挂起被挂起的任务得不到 CPU 的使用权也不会参与调度它相对于调度器而言是不可见的除非它从挂起态中解除。注任务可以调用 vTaskSuspend() 这个函数来挂起任务自身但是在挂起自身的时候会进行一次任务上下文切换需要挂起自身就将 xTaskToSuspend 设置为NULL传递进来即可。无论任务是什么状态都可以被挂起只要调用了 vTaskSuspend() 这个函数就会挂起成功不论是挂起其他任务还是挂起任务自身。代码示例staticTaskHandle_t LED_Task_HandleNULL;/*LED任务句柄*/staticvoidKEY_Task(void*parameter){while(1){if(Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN)KEY_ON){/*K1被按下*/printf(挂起 LED 任务\n);vTaskSuspend(LED_Task_Handle);/*挂起LED任务*/}vTaskDelay(20);/*延时20个tick*/}}3.2 任务恢复 vTaskResume()任务恢复 就是让挂起的任务 重新进入就绪状态恢复的任务会保留挂起前 的状态信息在恢复的时候 根据挂起时的状态继续运行。如果被恢复任务在所有就绪态任务中处于最高优先级列表的第一位那么系统将进行任务上下文的切换。代码示例staticTaskHandle_t LED_Task_HandleNULL;/* LED 任务句柄 */staticvoidKEY_Task(void*parameter){while(1){if(Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN)KEY_ON){/* K2 被按下 */printf(恢复 LED 任务\n);vTaskResume(LED_Task_Handle);/* 恢复LED任务*/}vTaskDelay(20);/* 延时20个tick */}}3.3 任务删除 vTaskDelete()vTaskDelete() 用于删除一个任务。(1) 当一个任务删除另外一个任务时形参为任务句柄(2) 如果是删除自身 则形参为 NULL。要想使用该函数必须在 FreeRTOSConfig.h 中把INCLUDE_vTaskDelete定义为 1删除的任务将从所有就绪阻塞挂起和事件列表中删除。代码示例/* 删除任务本身 */vTaskDelete(NULL);/* 在其他任务删除 DeleteTask 任务 */vTaskDelete(DeleteHandle);3.4 阻塞延时函数3.4.1 相对延时函数 vTaskDelay()阻塞延时的阻塞,是指任务调用该延时函数后任务会被剥离 CPU 使用权然后进入阻塞状态直到延时结束任务重新获取 CPU 使用权才可以继续运行。在任务阻塞的这段时间CPU可以去执行其它的任务如果其它的任务也在延时状态那么 CPU就将运行空闲任务。延时的时长由形参xTicksToDelay决定单位为系统节拍周期 比如系统的时钟节拍周期为 1ms那么调用 vTaskDelay(1) 的延时时间则为 1ms。vTaskDelay() 延时是相对性的延时它指定的延时时间是从调用 vTaskDelay() 结束后开始计算的经过指定的时间后延时结束。比如 vTaskDelay(100) 从调用 vTaskDelay() 结束后任务进入阻塞状态经过 100 个系统时钟节拍周期后任务解除阻塞。因此vTaskDelay() 并不适用与周期性 执行任务的场合。此外其它任务和中断活动也会影响到 vTaskDelay() 的调用比如调用前高优先级任务抢占了当前任务进而影响到任务的下一次执行的时间。任务的延时在实际中运用特别多因为需要暂停一个任务让任务放弃 CPU延时结束后再继续运行该任务如果任务中没有阻塞的话比该任务优先级低的任务 则无法得到CPU的使用权就无法运行。代码示例voidvTaskA(void*pvParameters){while(1){// ...// 这里为任务主体代码// .../* 调用相对延时函数, 阻塞 1000 个 个 tick */vTaskDelay(1000);}}3.4.2 绝对延时函数 vTaskDelayUntil()在 FreeRTOS 中除了相对延时函数还有绝对延时函数 vTaskDelayUntil()这个绝对延时常用于较精确的周期运行任务比如我有一个任务希望它以固定频率定期执行而不受外部的影响任务从上一次运行开始到下一次运行开始的时间间隔是绝对的而不是相对的。函数原型voidvTaskDelayUntil(TickType_t*constpxPreviousWakeTime,constTickType_t xTimeIncrement);代码实例voidvTaskA(void*pvParameters){/* 用于保存上次时间。调用后系统自动更新 */staticportTickType PreviousWakeTime;/* 设置延时时间将时间转为节拍数 */constportTickType TimeIncrementpdMS_TO_TICKS(1000);/* 获取当前系统时间 */PreviousWakeTimexTaskGetTickCount();while(1){/* 调用绝对延时函数, 任务时间间隔为 1000 个 个 tick */vTaskDelayUntil(PreviousWakeTime TimeIncrement);// 这里为任务主体代码}}注意在使用的时候要将延时时间转化为系统节拍在任务主体之前要调用延时函数。任务会先调用 vTaskDelayUntil() 使任务进入阻塞态等到时间到了就从阻塞中解除然后执行主体代码任务主体代码执行完毕。会继续调用 vTaskDelayUntil() 使任务进入阻塞态然后就是循环这样子执行。即使任务在执行过程中发生中断那么也不会影响这个任务的运行周期仅仅是缩短了阻塞的时间而已到了要唤醒的时间依旧会将任务唤醒。