任务管理¶
任务(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 中创建任务¶
图形化配置¶
- 打开 Middleware → FREERTOS → Tasks and Queues
- 点击 Add 添加新任务
- 配置参数:
| 参数 | 说明 | 建议值 |
|---|---|---|
| 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);
}
}
删除任务¶
删除任务的注意事项
- 删除任务会释放任务的 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 上执行的任务。同一时刻只有一个任务处于运行态。
任务已经准备好运行,但 CPU 正在执行更高优先级的任务。一旦高优先级任务让出 CPU,这个任务就会被调度。
任务正在等待某个事件,不参与调度:
osDelay()/osDelayUntil():等待延时到期osMessageQueueGet():等待队列数据osSemaphoreAcquire():等待信号量osMutexAcquire():等待互斥量
阻塞态的任务不消耗 CPU。
任务调度策略¶
抢占式调度(默认)¶
高优先级任务一旦就绪,立即抢占正在运行的低优先级任务:
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(相对延时)¶
存在的问题:如果任务本身的执行时间不固定,两次操作之间的总间隔就不固定。
|<--执行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,说明栈分配过大,可以缩小节省内存
栈大小调优流程
- 初始给大栈(如 512 Words)
- 让系统全负载运行一段时间
- 检查
uxTaskGetStackHighWaterMark()返回值 - 确保剩余量不小于 20~30 Words 的安全余量
- 据此调整栈大小
任务运行时统计¶
启用 configGENERATE_RUN_TIME_STATS 和 configUSE_TRACE_FACILITY 后:
char stats_buf[512];
vTaskGetRunTimeStats(stats_buf);
// 通过串口打印各任务的 CPU 占用率
HAL_UART_Transmit(&huart1, (uint8_t*)stats_buf, strlen(stats_buf), 1000);
输出示例:
栈溢出检测¶
在 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 堆内存不足。解决方法:
- 增大
TOTAL_HEAP_SIZE(CubeMX → Config parameters) - 减小其他任务的栈大小
- 减少同时运行的任务数量
任务不执行 / 被'饿死'?
检查是否有更高优先级的任务一直在运行(没有 osDelay 或其他阻塞操作)。高优先级任务如果不让出 CPU,低优先级任务永远得不到执行。