跳转至

任务管理

任务(Task)是 FreeRTOS 的核心,每个任务就像一个独立运行的小程序,拥有自己的栈空间和优先级。掌握任务的创建、调度和状态管理,是使用 RTOS 的第一步。


任务的基本概念

什么是任务?

在 FreeRTOS 中,任务就是一个永远不退出的函数,它有自己的:

  • 栈空间:存储局部变量、函数调用链
  • 优先级:决定调度顺序(数字越大优先级越高)
  • 任务控制块(TCB):内核用来管理任务的数据结构
// 任务函数的标准模板
void MyTask(void *argument)
{
    // 初始化代码(只执行一次)
    uint32_t count = 0;

    for (;;)  // 无限循环,永不退出
    {
        // 任务的具体工作
        count++;
        osDelay(100);  // 延时,让出 CPU
    }

    // ⚠️ 永远不应该执行到这里
    // 如果意外退出循环,必须删除自己
    osThreadTerminate(NULL);
}

任务函数绝不能 return

如果任务函数退出了循环,必须在末尾调用 osThreadTerminate(NULL) 删除自身,否则会导致硬件错误(HardFault)

任务 vs 裸机函数

对比 裸机函数 FreeRTOS 任务
执行方式 顺序调用 并发(调度器切换)
延时方式 HAL_Delay()(死等) osDelay()(让出 CPU)
栈空间 共享主栈 每个任务独立栈
优先级 有(支持抢占)
独立性 互相影响 互相隔离

CubeMX 中创建任务

图形化配置

  1. 打开 Middleware → FREERTOS → Tasks and Queues
  2. 点击 Add 添加新任务
  3. 配置参数:
参数 说明 建议值
Task Name 任务名称 有意义的名字,如 LED_Task
Priority 优先级 osPriorityNormal(按需调整)
Stack Size (Words) 栈大小(单位:字=4字节) 128(即 512B,简单任务够用)
Entry Function 任务入口函数名 LED_Task
Code Generation Option 代码生成选项 Default(生成到 freertos.c)
Parameter 传递给任务的参数 一般填 NULL
Allocation 内存分配方式 Dynamic(动态分配)

Stack Size 怎么定?

  • 简单任务(点灯、读 GPIO):128 Words(512B)
  • 一般任务(传感器读取、串口通信):256 Words(1KB)
  • 复杂任务(使用 printf、浮点运算、大数组):512+ Words(2KB+)
  • 不确定时先给大一点,后面可以用栈水位检测来优化

生成的代码分析

CubeMX 生成后,在 freertos.c 中会自动创建任务:

/* freertos.c 中自动生成的代码 */

/* 任务属性定义(自动生成,不要修改) */
const osThreadAttr_t LED_Task_attributes = {
    .name = "LED_Task",
    .stack_size = 128 * 4,           // 128 Words = 512 Bytes
    .priority = (osPriority_t) osPriorityNormal,
};

/* 任务句柄(自动生成) */
osThreadId_t LED_TaskHandle;

/* 初始化函数中创建任务(自动生成,不要修改) */
void MX_FREERTOS_Init(void)
{
    LED_TaskHandle = osThreadNew(LED_Task, NULL, &LED_Task_attributes);
}

/* 任务函数(在 USER CODE 区域写你的代码) */
void LED_Task(void *argument)
{
    /* USER CODE BEGIN LED_Task */
    for (;;)
    {
        // 在这里写你的任务逻辑
        osDelay(1);
    }
    /* USER CODE END LED_Task */
}

手动创建和删除任务

有时需要在运行时动态创建或删除任务(不通过 CubeMX 配置),可以使用 API 手动操作。

动态创建任务

/* 在某个任务中动态创建新任务 */
osThreadId_t newTaskHandle;

const osThreadAttr_t newTask_attr = {
    .name = "DynamicTask",
    .stack_size = 256 * 4,
    .priority = (osPriority_t) osPriorityAboveNormal,
};

void SomeTask(void *argument)
{
    /* USER CODE BEGIN SomeTask */
    for (;;)
    {
        if (需要创建新任务的条件)
        {
            // 动态创建任务,传入参数 (void*)42
            newTaskHandle = osThreadNew(DynamicTask, (void*)42, &newTask_attr);

            if (newTaskHandle == NULL)
            {
                // 创建失败,通常是内存不足
                Error_Handler();
            }
        }
        osDelay(100);
    }
    /* USER CODE END SomeTask */
}

void DynamicTask(void *argument)
{
    uint32_t param = (uint32_t)argument;  // param == 42

    for (;;)
    {
        // 使用传入的参数
        osDelay(500);
    }
}

删除任务

// 删除自己
osThreadTerminate(NULL);

// 删除其他任务(通过句柄)
osThreadTerminate(newTaskHandle);

删除任务的注意事项

  • 删除任务会释放任务的 TCB 和栈内存(动态分配时)
  • 但任务中申请的其他资源(队列、信号量、动态内存)不会自动释放
  • 删除任务前,确保该任务不持有任何互斥量,否则可能导致死锁
  • 尽量让任务自己删除自己(更安全)

任务优先级

优先级规则

FreeRTOS 中数字越大,优先级越高(与 NVIC 中断优先级相反!)。

CMSIS_V2 预定义了以下优先级:

优先级常量 数值 适用场景
osPriorityIdle 1 空闲任务(系统内部使用)
osPriorityLow 8 不重要的后台任务
osPriorityBelowNormal 16 低于普通
osPriorityNormal 24 一般任务(默认)
osPriorityAboveNormal 32 高于普通
osPriorityHigh 40 重要任务
osPriorityRealtime 48 实时关键任务

慎用 osPriorityRealtime

最高优先级任务如果不主动让出 CPU(如缺少 osDelay),将永远霸占 CPU,其他所有任务都得不到执行。

动态修改优先级

// 获取当前任务优先级
osPriority_t prio = osThreadGetPriority(taskHandle);

// 修改任务优先级
osThreadSetPriority(taskHandle, osPriorityHigh);

任务状态详解

四种状态的转换

stateDiagram-v2
    [*] --> Ready : osThreadNew() 创建
    Ready --> Running : 调度器选中(最高优先级就绪任务)
    Running --> Ready : 被更高优先级任务抢占
    Running --> Blocked : 调用 osDelay / 等待队列 / 等待信号量
    Running --> Suspended : 调用 osThreadSuspend()
    Blocked --> Ready : 延时到期 / 事件到来
    Suspended --> Ready : 调用 osThreadResume()
    Running --> [*] : osThreadTerminate() 删除

各状态详细说明

当前正在 CPU 上执行的任务。同一时刻只有一个任务处于运行态。

// 这个任务正在运行态
void HighPriorityTask(void *argument)
{
    for (;;)
    {
        do_something();  // ← CPU 正在执行这里
        osDelay(10);     // ← 调用后立即切到 Blocked
    }
}

任务已经准备好运行,但 CPU 正在执行更高优先级的任务。一旦高优先级任务让出 CPU,这个任务就会被调度。

任务正在等待某个事件,不参与调度:

  • osDelay() / osDelayUntil():等待延时到期
  • osMessageQueueGet():等待队列数据
  • osSemaphoreAcquire():等待信号量
  • osMutexAcquire():等待互斥量

阻塞态的任务不消耗 CPU

被显式挂起的任务,只能通过 osThreadResume() 恢复。

// 挂起任务
osThreadSuspend(taskHandle);

// 恢复任务
osThreadResume(taskHandle);

任务调度策略

抢占式调度(默认)

高优先级任务一旦就绪,立即抢占正在运行的低优先级任务:

sequenceDiagram
    participant Low as 低优先级任务
    participant High as 高优先级任务
    participant CPU as CPU

    Low->>CPU: 正在运行
    Note over High: 高优先级任务就绪<br>(如延时结束)
    High-->>Low: 抢占!
    High->>CPU: 立即接管 CPU
    Note over High: 执行完毕,进入阻塞
    Low->>CPU: 恢复运行

时间片轮转

当多个任务优先级相同时,调度器按时间片(默认 1ms)轮流让它们运行:

// 两个同优先级任务,各运行 1ms 后切换
void TaskA(void *argument)
{
    for (;;)
    {
        // 每 1ms 被切走,然后再被调度回来
        重复执行某些操作();
    }
}

void TaskB(void *argument)
{
    for (;;)
    {
        重复执行某些操作();
    }
}

时间片大小配置

在 CubeMX 的 FreeRTOS 配置 → Config parameters → TICK_RATE_HZ 中设置,默认 1000(即 1ms 一个 tick)。


空闲任务与空闲钩子

空闲任务

FreeRTOS 自动创建一个空闲任务(Idle Task),优先级最低(0),当所有其他任务都阻塞时,空闲任务运行。

空闲任务的职责:

  • 回收被删除任务的内存
  • 执行低功耗处理
  • 执行用户注册的空闲钩子函数

空闲钩子(Hook)

在 CubeMX 中启用 USE_IDLE_HOOK 后,可以在空闲时执行自定义操作:

/* freertos.c 中 */
void vApplicationIdleHook(void)
{
    /* USER CODE BEGIN vApplicationIdleHook */
    // 在这里可以做低功耗处理
    // 注意:不能调用任何会阻塞的 API(如 osDelay)!
    __WFI();  // 等待中断,降低功耗
    /* USER CODE END vApplicationIdleHook */
}

空闲钩子注意事项

空闲钩子中绝对不能调用任何会阻塞的 API(如 osDelay()osMessageQueueGet()),否则系统将无法正常调度。


osDelay 与 osDelayUntil

osDelay(相对延时)

osDelay(100);  // 从"现在"开始延时 100ms

存在的问题:如果任务本身的执行时间不固定,两次操作之间的总间隔就不固定。

|<--执行10ms-->|<--延时100ms-->|<--执行15ms-->|<--延时100ms-->|
|<---------110ms---------->|<---------115ms---------->|
                            ↑ 间隔不固定!

osDelayUntil(绝对延时)

uint32_t tick = osKernelGetTickCount();  // 获取当前 tick

for (;;)
{
    传感器采样();  // 执行时间不固定

    tick += 100;             // 下次运行时刻 = 上次 + 100ms
    osDelayUntil(tick);      // 精确等到那个时刻
}

无论任务执行多久,两次操作之间的总间隔固定是 100ms:

|<--执行10ms-->|<--等90ms-->|<--执行15ms-->|<--等85ms-->|
|<--------100ms-------->|<--------100ms-------->|
                          ↑ 间隔固定!

何时用哪个?

  • osDelay():普通延时,对周期精度要求不高(如 LED 闪烁)
  • osDelayUntil():精确周期执行(如 PID 控制、传感器定时采样)

实战案例

案例 1:多任务 LED + 串口

在 CubeMX 中创建 3 个任务:

任务名 优先级 栈大小 功能
LED_Task Normal 128 PC13 LED 每 500ms 翻转
UART_Task AboveNormal 256 定时通过串口发送数据
ADC_Task High 256 读取 ADC 并处理
/* LED 任务 */
void LED_Task(void *argument)
{
    for (;;)
    {
        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
        osDelay(500);
    }
}

/* 串口任务 */
void UART_Task(void *argument)
{
    char msg[64];
    uint32_t count = 0;

    for (;;)
    {
        snprintf(msg, sizeof(msg), "System running: %lu s\r\n", count++);
        HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), 100);
        osDelay(1000);
    }
}

/* ADC 采集任务 */
void ADC_Task(void *argument)
{
    uint32_t tick = osKernelGetTickCount();
    uint32_t adc_value;

    for (;;)
    {
        HAL_ADC_Start(&hadc1);
        HAL_ADC_PollForConversion(&hadc1, 10);
        adc_value = HAL_ADC_GetValue(&hadc1);
        HAL_ADC_Stop(&hadc1);

        // 处理 ADC 数据...
        float voltage = adc_value * 3.3f / 4096.0f;

        tick += 50;  // 每 50ms 精确采样一次
        osDelayUntil(tick);
    }
}

案例 2:传递参数给任务

同一个函数创建多个任务,通过参数区分行为:

/* CubeMX 中创建两个任务,都使用同一个入口函数 Blink_Task */
/* 但传入不同参数 */

typedef struct {
    GPIO_TypeDef* port;
    uint16_t pin;
    uint32_t delay_ms;
} BlinkParam_t;

/* 参数定义(静态,生命周期必须长于任务) */
static BlinkParam_t led1_param = { GPIOC, GPIO_PIN_13, 500 };
static BlinkParam_t led2_param = { GPIOA, GPIO_PIN_5,  200 };

/* 在 MX_FREERTOS_Init 中手动创建 */
void MX_FREERTOS_Init(void)
{
    /* USER CODE BEGIN RTOS_THREADS */
    const osThreadAttr_t blink_attr = {
        .name = "BlinkTask",
        .stack_size = 128 * 4,
        .priority = (osPriority_t) osPriorityNormal,
    };

    osThreadNew(Blink_Task, &led1_param, &blink_attr);
    osThreadNew(Blink_Task, &led2_param, &blink_attr);
    /* USER CODE END RTOS_THREADS */
}

/* 通用闪烁任务 */
void Blink_Task(void *argument)
{
    BlinkParam_t *param = (BlinkParam_t*)argument;

    for (;;)
    {
        HAL_GPIO_TogglePin(param->port, param->pin);
        osDelay(param->delay_ms);
    }
}

参数生命周期

传递给任务的参数(指针指向的数据)必须在任务运行期间始终有效。不能传递局部变量的地址,因为函数退出后局部变量就被销毁了。使用 static 变量或全局变量。


任务调试技巧

栈水位检测

检查任务的栈使用情况,帮助优化栈大小。在 CubeMX 中启用 INCLUDE_uxTaskGetStackHighWaterMark

/* 获取任务栈的历史最小剩余量(单位:Word) */
UBaseType_t watermark = uxTaskGetStackHighWaterMark(taskHandle);

// 如果 watermark < 20,说明栈快溢出了,需要加大栈空间
// 如果 watermark > 200,说明栈分配过大,可以缩小节省内存

栈大小调优流程

  1. 初始给大栈(如 512 Words)
  2. 让系统全负载运行一段时间
  3. 检查 uxTaskGetStackHighWaterMark() 返回值
  4. 确保剩余量不小于 20~30 Words 的安全余量
  5. 据此调整栈大小

任务运行时统计

启用 configGENERATE_RUN_TIME_STATSconfigUSE_TRACE_FACILITY 后:

char stats_buf[512];
vTaskGetRunTimeStats(stats_buf);
// 通过串口打印各任务的 CPU 占用率
HAL_UART_Transmit(&huart1, (uint8_t*)stats_buf, strlen(stats_buf), 1000);

输出示例:

Task            Abs Time        % Time
LED_Task        1234            < 1%
UART_Task       5678            2%
ADC_Task        12345           5%
IDLE            234567          92%

栈溢出检测

在 CubeMX 中启用 CHECK_FOR_STACK_OVERFLOW(建议设为方法 2),然后实现钩子函数:

void vApplicationStackOverflowHook(xTaskHandle xTask, signed char *pcTaskName)
{
    /* USER CODE BEGIN vApplicationStackOverflowHook */
    // 栈溢出了!打印出问题任务的名字
    // 在实际项目中可以记录错误日志或重启
    printf("Stack Overflow in task: %s\r\n", pcTaskName);
    while (1);  // 停在这里方便调试
    /* USER CODE END vApplicationStackOverflowHook */
}

常见问题

创建任务失败(返回 NULL)怎么办?

通常是 FreeRTOS 堆内存不足。解决方法:

  1. 增大 TOTAL_HEAP_SIZE(CubeMX → Config parameters)
  2. 减小其他任务的栈大小
  3. 减少同时运行的任务数量

任务不执行 / 被'饿死'?

检查是否有更高优先级的任务一直在运行(没有 osDelay 或其他阻塞操作)。高优先级任务如果不让出 CPU,低优先级任务永远得不到执行。

多个任务操作同一个外设(如串口)会出问题吗?

会!多个任务同时调用 HAL_UART_Transmit() 会导致数据乱码。解决方案:

  • 使用互斥量(Mutex)保护共享资源(详见 信号量与互斥量
  • 或创建一个专门的串口任务,其他任务通过队列发送数据给它(详见 队列与通信