FreeRTOS 手动移植教程(二):任务管理——多任务创建、优先级抢占与删除

上一篇我们成功搭建了标准库与 FreeRTOS 的工程,并运行了一个 LED 闪烁任务。本篇文章将在此基础上创建多个任务,直观感受 优先级抢占时间片轮转,并掌握任务删除及参数传递的方法。所有代码均基于上一篇文章的工程,可直接添加运行。


一、实验目标与硬件准备

1.1 实验现象

  • 创建三个不同优先级的任务,控制三个 LED 以不同频率闪烁;
  • 高优先级任务就绪时可立即抢占低优先级任务;
  • 同优先级任务之间自动轮流执行;
  • 某个任务运行一定次数后自行删除。

1.2 硬件连接

本文需要三个 LED,如仅有一块最小系统板,可按以下方式外接:

  • LED1:PA0 ------ 串联 220Ω 限流电阻,低电平点亮
  • LED2:PA1 ------ 串联 220Ω 限流电阻,低电平点亮
  • LED3:PC13 ------ 使用板载 LED,同样为低电平点亮

若你的 LED 是高电平点亮,只需在初始化时反向设置电平极性即可。


二、扩展板级驱动:支持多 LED

在上一篇 bsp_led.c 的基础上扩展,提供多个 LED 的初始化与翻转函数。

bsp_led.h

c 复制代码
#ifndef BSP_LED_H
#define BSP_LED_H

#include "stm32f10x.h"

void LED_InitAll(void);
void LED1_Toggle(void);
void LED2_Toggle(void);
void LED3_Toggle(void);

#endif

bsp_led.c

c 复制代码
#include "bsp_led.h"

void LED_InitAll(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    /* 使能 GPIOA 和 GPIOC 时钟 */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC, ENABLE);

    /* PA0 - LED1 */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    GPIO_SetBits(GPIOA, GPIO_Pin_0);   // 输出高电平,LED 熄灭

    /* PA1 - LED2 */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    GPIO_SetBits(GPIOA, GPIO_Pin_1);

    /* PC13 - LED3 */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
    GPIO_Init(GPIOC, &GPIO_InitStructure);
    GPIO_SetBits(GPIOC, GPIO_Pin_13);
}

void LED1_Toggle(void)
{
    GPIOA->ODR ^= GPIO_Pin_0;
}

void LED2_Toggle(void)
{
    GPIOA->ODR ^= GPIO_Pin_1;
}

void LED3_Toggle(void)
{
    GPIOC->ODR ^= GPIO_Pin_13;
}

三、创建多任务,观察优先级抢占

3.1 任务代码

main.c 中创建三个任务,优先级分别设为 1、2、3(数字越大优先级越高),每个任务控制一个 LED。

c 复制代码
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "bsp_led.h"

TaskHandle_t Task1_Handle = NULL;
TaskHandle_t Task2_Handle = NULL;
TaskHandle_t Task3_Handle = NULL;

/* LED1 闪烁任务 ------ 优先级 1(最低) */
void vTask1(void *pvParameters)
{
    while (1)
    {
        LED1_Toggle();
        vTaskDelay(pdMS_TO_TICKS(200));   // 周期约 400ms
    }
}

/* LED2 闪烁任务 ------ 优先级 2 */
void vTask2(void *pvParameters)
{
    while (1)
    {
        LED2_Toggle();
        vTaskDelay(pdMS_TO_TICKS(500));   // 周期约 1s
    }
}

/* LED3 闪烁任务 ------ 优先级 3(最高) */
void vTask3(void *pvParameters)
{
    while (1)
    {
        LED3_Toggle();
        vTaskDelay(pdMS_TO_TICKS(1000));  // 周期约 2s
    }
}

int main(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 实验现象与原理

下载运行后,观察三个 LED:

  • LED3(最高优先级):稳定以 2s 周期闪烁,几乎不受干扰;
  • LED2(中等优先级):以 1s 周期闪烁,但偶尔会有微小的停顿(被高优先级任务抢占);
  • LED1(最低优先级):闪烁频率明显低于预期(可能会很慢),因为 CPU 大部分时间被高优先级任务占用,它只能在所有高优先级任务都阻塞时才获得执行权。

原理分析:

configUSE_PREEMPTION = 1 时,内核会在每个系统节拍中断中检查是否有更高优先级任务就绪。一旦高优先级任务延时结束,立即剥夺当前任务的 CPU,通过 PendSV 切换到高优先级任务。这就解释了为何低优先级任务的实际执行频率会变慢。

注意事项:

如果高优先级任务一直在死循环且从不阻塞(例如没有 vTaskDelay),那么低优先级任务将永远得不到执行(任务饥饿)。因此在实时系统中,高优先级任务必须适时"让出" CPU。


四、同优先级任务与时间片轮转

4.1 创建两个同优先级任务

将任务 1 和任务 2 都改为优先级 1,并删除任务 3。

c 复制代码
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。

但如果将两个任务的延时都去掉,改成纯死循环:

c 复制代码
void vTask1(void *pvParameters) { while (1) LED1_Toggle(); }
void vTask2(void *pvParameters) { while (1) LED2_Toggle(); }

此时两个任务永远不阻塞,且优先级相同。在系统节拍中断中,内核会轮流切换它们,两个 LED 将以大约 1ms 的间隔交替翻转,用示波器可以观察到非常规律的方波。

时间片轮转仅在 configUSE_PREEMPTION = 1configUSE_TIME_SLICING = 1(默认开启)时,对同优先级且都就绪的任务生效。


五、任务的删除

5.1 删除自身

调用 vTaskDelete(NULL); 可以删除当前运行的任务,其占用的堆栈和 TCB 资源会被自动回收。

示例:任务运行 5 次后自我删除。

c 复制代码
void vTask_SelfDelete(void *pvParameters)
{
    int count = 0;
    while (1)
    {
        LED1_Toggle();
        vTaskDelay(pdMS_TO_TICKS(200));
        count++;
        if (count >= 5)
        {
            vTaskDelete(NULL);   // 删除自己,任务在此处结束
        }
    }
}

5.2 删除其他任务

通过任务句柄,一个任务可以删除另一个任务。例如在任务 A 中删除任务 B:

c 复制代码
TaskHandle_t TaskB_Handle = NULL;

void vTaskA(void *pvParameters)
{
    vTaskDelay(pdMS_TO_TICKS(1000));
    if (TaskB_Handle != NULL)
    {
        vTaskDelete(TaskB_Handle);   // 删除任务 B
        TaskB_Handle = NULL;
    }
    // ...
}

被删除任务的堆栈和 TCB 内存会立即释放(若使用 heap_4.cheap_2.c 等支持释放的策略),句柄也应设置为 NULL 以防止悬空指针。


六、任务参数传递

xTaskCreatepvParameters 参数可以传递任意类型指针,使同一个任务函数处理不同的硬件或数据。

6.1 传递整数 ID

直接使用整数 ID 来区分不同的 LED(将 int 强制转换为 void* 传递):

c 复制代码
void vLED_Task(void *pvParameters)
{
    int led_id = (int)pvParameters;   // 取出 ID
    while (1)
    {
        switch (led_id)
        {
            case 0: LED1_Toggle(); break;
            case 1: LED2_Toggle(); break;
            case 2: LED3_Toggle(); break;
        }
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

int main(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 传递字符串

c 复制代码
void vPrintTask(void *pvParameters)
{
    char *msg = (char *)pvParameters;
    while (1)
    {
        // 例如通过串口打印 msg
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}
// 创建时:xTaskCreate(vPrintTask, "Print", 256, "Hello RTOS", 1, NULL);

七、总结

本篇通过动手实验,展示了 FreeRTOS 最核心的任务管理机制:

  • 优先级抢占:高优先级就绪立即剥夺 CPU;
  • 时间片轮转:同优先级任务公平共享 CPU;
  • 任务删除:动态回收任务资源;
  • 参数传递:实现通用的任务处理逻辑。

这些是日常项目中使用频率最高的操作。下一篇文章将深入系统节拍与延时函数,分析 vTaskDelay 的实现原理,并介绍精度更高的 vTaskDelayUntil 用法。


下一篇:FreeRTOS 任务延时与时间管理 ------ 从裸机 delay 到 vTaskDelayUntil。

相关推荐
提伯斯6461 小时前
Jetson_Pixhawk局域网UDP连接QGC
linux·网络·嵌入式硬件·网络协议·udp·jetson
是温不嗜温2 小时前
65W QR 反激主控选型 checklist:LP8841IIC 对比 MPS/昂宝/通嘉 十项参数逐一过
嵌入式硬件·电源管理·电源芯片·ac-dc
都在酒里2 小时前
FreeRTOS 手动移植教程(五):信号量 —— 任务同步与中断通知的优雅解决方案
stm32·单片机·rtos·嵌入式软件
紫阡星影2 小时前
【STM32CubeMX项目】智能家居门禁系统
c语言·单片机·嵌入式硬件
Jumbuck_103 小时前
从零实现《三角洲行动》手游自动跑刀脚本:ADB 直控 + OpenCV 视觉识别 + 固定点位搜刮)三角洲自动跑刀教程
嵌入式硬件·yolo·目标检测·自动化·自动驾驶·三角洲·自动跑刀
txh05074 小时前
从零开始学习FOC
单片机·嵌入式硬件·学习
2601_961194024 小时前
考研政治历年真题及解析pdf
stm32·单片机·嵌入式硬件·物联网·考研·pdf
今日待办4 小时前
STM32H747I-DISCO 开发指南【数字麦克风使用】
stm32·单片机·嵌入式硬件
世微 如初4 小时前
【方案】AP5127摩托车灯驱动设计:12-100V输入,2.5A恒流
单片机·嵌入式硬件