一、创建任务
创建任务函数
//动态分配内存
BaseType_t xTaskCreate(
TaskFunction_t pxTaskCode, // 函数指针, 任务函数
const char * const pcName, // 任务的名字
const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小,单位为word,10表示40字节
void * const pvParameters, // 调用任务函数时传入的参数
UBaseType_t uxPriority, // 优先级
TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用它来操作这个任务
|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|
| 参数 | 描述 |
| pvTaskCode | 函数指针, 可以简单地认为任务就是一个 C 函数。它稍微特殊一点: 永远不退出, 或者退出时要调用"vTaskDelete(NULL)" |
| pcName | 任务的名字, FreeRTOS 内部不使用它, 仅仅起调试作用。长度为: configMAX_TASK_NAME_LEN |
| usStackDepth | 每个任务都有自己的栈, 这里指定栈大小。单位是 word, 比如传入 100, 表示栈大小为 100 word, 也就是 400 字节。最大值为 uint16_t 的最大值。怎么确定栈的大小, 并不容易, 很多时候是估计。精确的办法是看反汇编码。 |
| pvParameters | 调用 pvTaskCode 函数指针时用到: pvTaskCode(pvParameters) |
| uxPriority | 优先级范围: 0~(configMAX_PRIORITIES -- 1)数值越小优先级越低,如果传入过大的值, xTaskCreate 会把它调整为(configMAX_PRIORITIES -- 1) |
| pxCreatedTask | 用来保存 xTaskCreate 的输出结果: task handle。以后如果想操作这个任务, 比如修改它的优先级, 就需要这个 handle。如果不想使用该 handle, 可以传入 NULL。 |
| 返回值 | 成功: pdPASS;失败: errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY(失败原因只有内存不足)注意: 文档里都说失败时返回值是 pdFAIL, 这不对。pdFAIL 是 0, errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 是-1。 |
//静态内存分配
TaskHandle_t xTaskCreateStatic (
TaskFunction_t pxTaskCode, // 函数指针, 任务函数
const char * const pcName, // 任务的名字
const uint32_t ulStackDepth, // 栈大小,单位为word,10表示40字节
void * const pvParameters, // 调用任务函数时传入的参数
UBaseType_t uxPriority, // 优先级
StackType_t * const puxStackBuffer, // 静态分配的栈, 就是一个buffer
StaticTask_t * const pxTaskBuffer // 静态分配的任务结构体的指针, 用它来操作这个任务
);
|----------------|-----------------------------------------------------------------------------------------------------------------------------|
| 参数 | 描述 |
| pvTaskCode | 函数指针, 可以简单地认为任务就是一个 C 函数。它稍微特殊一点: 永远不退出, 或者退出时要调用"vTaskDelete(NULL)" |
| pcName | 任务的名字, FreeRTOS 内部不使用它, 仅仅起调试作用。长度为: configMAX_TASK_NAME_LEN |
| usStackDepth | 每个任务都有自己的栈, 这里指定栈大小。单位是 word, 比如传入 100, 表示栈大小为 100 word, 也就是 400 字节。最大值为 uint16_t 的最大值。怎么确定栈的大小, 并不容易, 很多时候是估计。精确的办法是看反汇编码。 |
| pvParameters | 调用 pvTaskCode 函数指针时用到: pvTaskCode(pvParameters) |
| uxPriority | 优先级范围: 0~(configMAX_PRIORITIES -- 1)数值越小优先级越低,如果传入过大的值, xTaskCreate 会把它调整为(configMAX_PRIORITIES -- 1) |
| puxStackBuffer | 静态分配的栈内存, 比如可以传入一个数组,它的大小是 usStackDepth*4。 |
| pxTaskBuffer | 静态分配的 StaticTask_t 结构体的指针 |
| 返回值 | 成功: 返回任务句柄;失败: NULL |
估算栈的大小
调用函数(数量*36)+局部变量
二、任务函数
// 多个任务共用的函数:实现计数功能
void count_task(void *pvParameters) {
// 局部变量:每个任务的栈里会独立存储一份
int count = 0;
// 从参数获取任务编号(区分任务1和任务2)
int task_num = (int)pvParameters;
while(1) {
count++; // 每个任务的count独立增长
printf("任务%d:count = %d\n", task_num, count);
vTaskDelay(pdMS_TO_TICKS(1000)); // 延时1秒
}
}
int main(void) {
// 创建任务1:调用count_task,传递参数1
xTaskCreate(count_task, "Task1", 1024, (void*)1, 1, NULL);
// 创建任务2:调用count_task,传递参数2
xTaskCreate(count_task, "Task2", 1024, (void*)2, 1, NULL);
vTaskStartScheduler(); // 启动RTOS调度器
while(1); // 调度器启动失败会到这里
}
两个任务的count各自独立增长,不会互相覆盖
多个任务使用同一个函数的核心逻辑:函数是共享的代码模板,执行时的局部变量、栈帧存在任务独立栈中,因此每个任务的执行状态互不干扰。这既节省了代码空间(不用为每个任务写重复函数),又保证了任务独立性 ------RTOS 的设计精髓之一就是 "共享代码、隔离数据"。
三、任务退出
在 FreeRTOS 中,删除任务的函数为vTaskDelete(),作用是将指定任务从系统中移除,被删除的任务不再参与调度
vTaskDelete(NULL)为删除自己
执行 vTaskDelete(pvTaskCode)为删除pvTaskCode
任务退出的本质是 "让任务从系统中消失,不再参与调度",在 FreeRTOS 中唯一的方法是调用vTaskDelete()
但是任务被删除后它占用的资源(任务栈、任务控制块 TCB)不会立刻消失,这些资源会由空闲任务(IDLE Task) 负责回收
空闲任务(IDLE Task)
空闲任务(IDLE Task)是系统启动调度器时自动创建 的一个特殊任务,优先级通常为最低(FreeRTOS 中默认是 0),是系统不可或缺的 "后台管家"。
当系统中没有更高优先级的就绪态任务时,调度器会选中空闲任务执行 ------ 它本质是一个无限循环的空函数(或极简逻辑),用来 "消耗" CPU 的空闲时间,防止 CPU 处于无任务可执行的 "空转" 状态(空转会导致功耗浪费或硬件异常)
空闲任务的简化逻辑
void vApplicationIdleHook(void); // 空闲钩子函数(用户可自定义)
void prvIdleTask(void *pvParameters) {
for(;;) { // 无限循环
// 1. 回收被删除任务的动态资源
prvCheckTasksWaitingTermination();
// 2. 执行用户自定义的空闲钩子函数(可选)
vApplicationIdleHook();
// 3. 若支持低功耗,可在此处让CPU进入休眠
}
}
使用钩子函数的注意事项
- 轻量化:钩子函数执行时机敏感(比如任务切换瞬间),不能做耗时操作(如复杂计算、串口打印大量数据),否则会影响系统实时性;
- 避免阻塞 :不要在钩子函数中调用**
vTaskDelay()、xSemaphoreTake()**等阻塞 API,否则会打乱 RTOS 的核心流程; - 按需开启 :部分钩子函数需要开启 RTOS 配置(如栈溢出钩子需设置**
configCHECK_FOR_STACK_OVERFLOW**),未开启时定义了也不会执行。
四、任务状态
任务状态可以分为四种:运行状态(Runing)、阻塞状态(Blocked)、挂起状态(Suspended)、就绪状态(Ready)

就绪状态(ready)
任务已经准备好执行,具备运行条件但暂未获得 CPU 使用权,排队等待调度器分配 CPU。
进入就绪态的典型场景:
1.任务刚被创建
2.任务从阻塞态恢复
3.挂起任务被唤醒后
4.低优先级任务被高优先级任务抢占后,高优先级任务执行完毕,低优先级任务回到就绪态。
阻塞状态(Blocked)
任务暂时 "休眠",等待某个特定事件或时间发生,此时无法被调度器选中(即使 CPU 空闲也不会执行)。
进入阻塞态的典型场景:
1.调用延时函数,等待指定时间
2.等待同步/任务对象
3.等待硬件事件,如等待串口接收完成、定时器超时。
挂起状态(Suspended)
任务被 "强制暂停",不会参与任何调度,是比阻塞态更 "被动" 的状态,只能通过手动 API 唤醒。
调用挂起函数:vTaskSuspend(TaskHandle_t xTaskToSuspend) (传任务句柄挂起指定任务,传**NULL**挂起自己)
五、Delay函数
vTaskDelay()函数
从 "调用函数的时刻" 开始,延时指定的 Tick 数
void example_task(void *pvParameters) {
while(1) {
printf("任务执行:当前时间=%lu\n", xTaskGetTickCount());
// 延时1000毫秒(相对当前调用时刻)
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
vTaskDelayUntil()函数
从 "上次任务执行的时刻" 开始,延时到指定的周期点
void periodic_task(void *pvParameters) {
TickType_t last_wake_time; // 保存上次唤醒时间
// 初始化:获取当前系统时间作为首次唤醒时间
last_wake_time = xTaskGetTickCount();
while(1) {
printf("周期任务执行:当前时间=%lu\n", xTaskGetTickCount());
// 模拟任务处理耗时(比如50ms)
vTaskDelay(pdMS_TO_TICKS(50));
// 保证每隔1000ms(1秒)执行一次(不管任务处理用了多久)
vTaskDelayUntil(&last_wake_time, pdMS_TO_TICKS(1000));
}
}