FreeRTOS 手动移植教程(二):任务管理——多任务创建、优先级抢占与删除
上一篇我们成功搭建了标准库与 FreeRTOS 的工程并运行了一个 LED 闪烁任务。本篇文章将在此基础上创建多个任务直观感受优先级抢占与时间片轮转并掌握任务删除及参数传递的方法。所有代码均基于上一篇文章的工程可直接添加运行。一、实验目标与硬件准备1.1 实验现象创建三个不同优先级的任务控制三个 LED 以不同频率闪烁高优先级任务就绪时可立即抢占低优先级任务同优先级任务之间自动轮流执行某个任务运行一定次数后自行删除。1.2 硬件连接本文需要三个 LED如仅有一块最小系统板可按以下方式外接LED1PA0 —— 串联 220Ω 限流电阻低电平点亮LED2PA1 —— 串联 220Ω 限流电阻低电平点亮LED3PC13 —— 使用板载 LED同样为低电平点亮若你的 LED 是高电平点亮只需在初始化时反向设置电平极性即可。二、扩展板级驱动支持多 LED在上一篇bsp_led.c的基础上扩展提供多个 LED 的初始化与翻转函数。bsp_led.h#ifndefBSP_LED_H#defineBSP_LED_H#includestm32f10x.hvoidLED_InitAll(void);voidLED1_Toggle(void);voidLED2_Toggle(void);voidLED3_Toggle(void);#endifbsp_led.c#includebsp_led.hvoidLED_InitAll(void){GPIO_InitTypeDef GPIO_InitStructure;/* 使能 GPIOA 和 GPIOC 时钟 */RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOC,ENABLE);/* PA0 - LED1 */GPIO_InitStructure.GPIO_PinGPIO_Pin_0;GPIO_InitStructure.GPIO_ModeGPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_SpeedGPIO_Speed_50MHz;GPIO_Init(GPIOA,GPIO_InitStructure);GPIO_SetBits(GPIOA,GPIO_Pin_0);// 输出高电平LED 熄灭/* PA1 - LED2 */GPIO_InitStructure.GPIO_PinGPIO_Pin_1;GPIO_Init(GPIOA,GPIO_InitStructure);GPIO_SetBits(GPIOA,GPIO_Pin_1);/* PC13 - LED3 */GPIO_InitStructure.GPIO_PinGPIO_Pin_13;GPIO_Init(GPIOC,GPIO_InitStructure);GPIO_SetBits(GPIOC,GPIO_Pin_13);}voidLED1_Toggle(void){GPIOA-ODR^GPIO_Pin_0;}voidLED2_Toggle(void){GPIOA-ODR^GPIO_Pin_1;}voidLED3_Toggle(void){GPIOC-ODR^GPIO_Pin_13;}三、创建多任务观察优先级抢占3.1 任务代码在main.c中创建三个任务优先级分别设为 1、2、3数字越大优先级越高每个任务控制一个 LED。#includestm32f10x.h#includeFreeRTOS.h#includetask.h#includebsp_led.hTaskHandle_t Task1_HandleNULL;TaskHandle_t Task2_HandleNULL;TaskHandle_t Task3_HandleNULL;/* LED1 闪烁任务 —— 优先级 1最低 */voidvTask1(void*pvParameters){while(1){LED1_Toggle();vTaskDelay(pdMS_TO_TICKS(200));// 周期约 400ms}}/* LED2 闪烁任务 —— 优先级 2 */voidvTask2(void*pvParameters){while(1){LED2_Toggle();vTaskDelay(pdMS_TO_TICKS(500));// 周期约 1s}}/* LED3 闪烁任务 —— 优先级 3最高 */voidvTask3(void*pvParameters){while(1){LED3_Toggle();vTaskDelay(pdMS_TO_TICKS(1000));// 周期约 2s}}intmain(void){LED_InitAll();xTaskCreate(vTask1,Task1,128,NULL,1,Task1_Handle);xTaskCreate(vTask2,Task2,128,NULL,2,Task2_Handle);xTaskCreate(vTask3,Task3,128,NULL,3,Task3_Handle);vTaskStartScheduler();while(1);}3.2 实验现象与原理下载运行后观察三个 LEDLED3最高优先级稳定以 2s 周期闪烁几乎不受干扰LED2中等优先级以 1s 周期闪烁但偶尔会有微小的停顿被高优先级任务抢占LED1最低优先级闪烁频率明显低于预期可能会很慢因为 CPU 大部分时间被高优先级任务占用它只能在所有高优先级任务都阻塞时才获得执行权。原理分析当configUSE_PREEMPTION 1时内核会在每个系统节拍中断中检查是否有更高优先级任务就绪。一旦高优先级任务延时结束立即剥夺当前任务的 CPU通过 PendSV 切换到高优先级任务。这就解释了为何低优先级任务的实际执行频率会变慢。注意事项如果高优先级任务一直在死循环且从不阻塞例如没有vTaskDelay那么低优先级任务将永远得不到执行任务饥饿。因此在实时系统中高优先级任务必须适时“让出” CPU。四、同优先级任务与时间片轮转4.1 创建两个同优先级任务将任务 1 和任务 2 都改为优先级 1并删除任务 3。xTaskCreate(vTask1,Task1,128,NULL,1,Task1_Handle);xTaskCreate(vTask2,Task2,128,NULL,1,Task2_Handle);两个任务优先级相同当它们同时就绪时FreeRTOS 默认会采用时间片轮转Time Slicing每个任务轮流运行一个系统节拍默认为 1ms。4.2 实验验证两个 LED 仍会按照各自vTaskDelay设置的频率闪烁互不影响。这是因为它们在各自延时期间都处于阻塞态不会浪费 CPU。但如果将两个任务的延时都去掉改成纯死循环voidvTask1(void*pvParameters){while(1)LED1_Toggle();}voidvTask2(void*pvParameters){while(1)LED2_Toggle();}此时两个任务永远不阻塞且优先级相同。在系统节拍中断中内核会轮流切换它们两个 LED 将以大约 1ms 的间隔交替翻转用示波器可以观察到非常规律的方波。时间片轮转仅在configUSE_PREEMPTION 1且configUSE_TIME_SLICING 1默认开启时对同优先级且都就绪的任务生效。五、任务的删除5.1 删除自身调用vTaskDelete(NULL);可以删除当前运行的任务其占用的堆栈和 TCB 资源会被自动回收。示例任务运行 5 次后自我删除。voidvTask_SelfDelete(void*pvParameters){intcount0;while(1){LED1_Toggle();vTaskDelay(pdMS_TO_TICKS(200));count;if(count5){vTaskDelete(NULL);// 删除自己任务在此处结束}}}5.2 删除其他任务通过任务句柄一个任务可以删除另一个任务。例如在任务 A 中删除任务 BTaskHandle_t TaskB_HandleNULL;voidvTaskA(void*pvParameters){vTaskDelay(pdMS_TO_TICKS(1000));if(TaskB_Handle!NULL){vTaskDelete(TaskB_Handle);// 删除任务 BTaskB_HandleNULL;}// ...}被删除任务的堆栈和 TCB 内存会立即释放若使用heap_4.c或heap_2.c等支持释放的策略句柄也应设置为 NULL 以防止悬空指针。六、任务参数传递xTaskCreate的pvParameters参数可以传递任意类型指针使同一个任务函数处理不同的硬件或数据。6.1 传递整数 ID直接使用整数 ID 来区分不同的 LED将 int 强制转换为 void* 传递voidvLED_Task(void*pvParameters){intled_id(int)pvParameters;// 取出 IDwhile(1){switch(led_id){case0:LED1_Toggle();break;case1:LED2_Toggle();break;case2:LED3_Toggle();break;}vTaskDelay(pdMS_TO_TICKS(500));}}intmain(void){LED_InitAll();xTaskCreate(vLED_Task,LED0,128,(void*)0,1,NULL);xTaskCreate(vLED_Task,LED1,128,(void*)1,2,NULL);xTaskCreate(vLED_Task,LED2,128,(void*)2,3,NULL);vTaskStartScheduler();while(1);}虽然直接将整数强制转换为指针是一种常见技巧但请注意它依赖于 CPU 架构。在 32 位 Cortex-M 上指针与 int 宽度相同此用法安全有效。6.2 传递字符串voidvPrintTask(void*pvParameters){char*msg(char*)pvParameters;while(1){// 例如通过串口打印 msgvTaskDelay(pdMS_TO_TICKS(1000));}}// 创建时xTaskCreate(vPrintTask, Print, 256, Hello RTOS, 1, NULL);七、总结本篇通过动手实验展示了 FreeRTOS 最核心的任务管理机制优先级抢占高优先级就绪立即剥夺 CPU时间片轮转同优先级任务公平共享 CPU任务删除动态回收任务资源参数传递实现通用的任务处理逻辑。这些是日常项目中使用频率最高的操作。下一篇文章将深入系统节拍与延时函数分析vTaskDelay的实现原理并介绍精度更高的vTaskDelayUntil用法。下一篇FreeRTOS 任务延时与时间管理 —— 从裸机 delay 到 vTaskDelayUntil。