一、任务创建
创建任务可以分为动态创建、静态创建。核心区别就在于任务控制块(TCB)和堆栈的内存由谁分配。
- 动态创建
xTaskCreate()内存由 FreeRTOS 在运行时自动从堆(heap)里分配。 - 静态创建
xTaskCreateStatic()内存由你自己预先定义好数组,FreeRTOS 直接用,不碰堆。
1、任务栈
每个任务独立拥有的一块内存(RAM),专门用来保存该任务运行时的"现场"和临时数据。
1、任务栈里有什么:
- 上下文(Context):发生任务切换时,CPU 的寄存器(R0–R12、LR、PC、xPSR 等)会被自动压入该任务的栈——这就是"现场保护"。
- 局部变量:函数里
int a;这种自动变量存在栈上。 - 函数调用链:调用函数时的返回地址、传参。
- 中断嵌套时的现场(若在该任务运行时进中断)。
2、任务栈大小的创建
动态创建
// 第3个参数 128 = 栈深度(字数),M3上 = 128 * 4 = 512 字节 xTaskCreate(vTask, "Task1", 128, NULL, 1, NULL); vTaskStartScheduler();静态创建
#define TASK1_STACK_SIZE 128 //定义栈的大小 static StackType_t Task1Stack[TASK1_STACK_SIZE]; // static StaticTask_t Task1TCB;2任务函数
void vMyTask(void *pvParameters) { // 可选:任务启动前的初始化 for (;;) { // 任务要循环做的事 vTaskDelay(pdMS_TO_TICKS(100)); // 延时100ms,让出CPU } // 注意:for(;;) 永不返回,任务函数不能执行到这里 }1、TCB
Task Control Block(任务控制块),是 FreeRTOS 用来管理每个任务的核心数据结构——相当于任务的"身份证 + 档案袋"。
TCB 里主要记了什么(FreeRTOS 源码tskTaskControlBlock结构体)
typedef struct tskTaskControlBlock { volatile StackType_t *pxTopOfStack; /* 栈顶 */ ListItem_t xStateListItem; /* 任务节点 */ StackType_t *pxStack; /* 任务栈起始地址 */ char pcTaskName[ configMAX_TASK_NAME_LEN ]; /* 任务名称,字符串形式 */ TickType_t xTicksToDelay; /* 用于延时 */ UBaseType_t uxPriority; }tskTCB; typedef tskTCB TCB_t;2、创建任务函数
1、动态函数
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // ① 任务函数名(入口地址) const char *pcName, // ② 任务名(调试用字符串) uint16_t usStackDepth, // ③ 栈深度(单位:字, 128=512字节@M3) void *pvParameters, // ④ 传给任务的参数(不用就NULL) UBaseType_t uxPriority, // ⑤ 优先级(数值越大越高) TaskHandle_t *pxCreatedTask); // ⑥ 句柄输出(可NULL,要删任务时必用) xTaskCreate(led_task, "led_task", 128, NULL, 1, &led_task_handle); // ①函数 ②名字 ③栈 ④参数 ⑤优先级 ⑥句柄地址2、静态函数
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode, // ① 任务函数名 const char *pcName, // ② 任务名 uint32_t ulStackDepth, // ③ 栈深度(字) void *pvParameters, // ④ 参数 UBaseType_t uxPriority, // ⑤ 优先级 StackType_t *puxStackBuffer, // ⑥ 你提供的栈数组 TCB_t *pxTaskBuffer); // ⑦ 你提供的TCB xTaskCreateStatic(vMyTask, "T1", 128, NULL, 1, xStack, &xTCB); // ①函数 ②名 ③栈 ④参数 ⑤优先级 ⑥栈数组 ⑦TCB地址二、任务调度
FreeRTOS 决定"这一刻该让哪个任务占用 CPU"的机制。它是 RTOS 的"大脑"。
1. 调度核心原则:优先级抢占
- 每个任务有优先级。
- 高优先级任务一旦就绪,立即抢走 CPU(抢占),低优先级任务马上暂停。
- 同优先级之间用时间片轮转(默认每个 tick 切换一次,让大家轮流跑)。
2. 任务的 4 种状态
- 就绪:能跑,等 CPU。
- 运行:正在跑。
- 阻塞:在等延时/事件(
vTaskDelay、ulTaskNotifyTake),主动让出 CPU→ 别的任务趁机跑。 - 挂起:
vTaskSuspend()强制停,需显式恢复。
任务调度 = 按"优先级抢占 + 同优先级时间片"规则,在 SysTick/PendSV 驱动下,不断保存旧任务现场、恢复新任务现场,让多个任务"看起来同时跑"。
三、任务切换
把 CPU 从「任务 A」交给「任务 B」时,保存 A 的现场、恢复 B 的现场的过程。调度决定"切谁",切换负责"怎么切"。
1. 切换 = 换一套寄存器现场CPU 只有一套寄存器(R0~R15、xPSR)。任务 A 跑的时候这些寄存器里是 A 的数据。要让 B 跑,必须:
- 保存:把 A 的寄存器值存进 A 的任务栈(之前
pxPortInitialiseStack摆好的那片)。 - 恢复:从 B 的任务栈把 B 的寄存器值弹回 CPU。
- 切换瞬间完成,B 就像"从来没被打断过"一样继续跑
2、两个栈指针:MSP vs PSP
- MSP(主栈):中断/启动代码用。
- PSP(进程栈):任务用。切换的本质就是把 PSP 指向新任务的栈顶(TCB 里的
pxTopOfStack),CPU 之后就从新栈取数据。
3、
切换触发 + 执行位置(关键)切换不在 SysTick 里直接做,而在PendSV 中断里做:
SysTick中断 → 判断需要切换? → 挂起PendSV(请求切换) ↓ (SysTick返回) PendSV中断 → 真正做上下文切换(保存A/恢复B) ← 切换在这里发生 ↓ 返回线程模式, 运行任务B为什么绕一层 PendSV?因为 SysTick 可能被更高优先级中断打断,把真正的切换放到最低优先级的 PendSV,能保证切换完整、不被撕裂。
任务切换 = 在 PendSV 里把旧任务寄存器现场压进旧栈、从新栈恢复新任务现场、并把 PSP 指向新栈,从而无缝交接 CPU
四、任务间通信
FreeRTOS 任务之间传数据/通信,主要有6 种机制
① 全局变量(最朴素,但需注意)
int g_shared = 0; // 任务A写、任务B读- 简单,但不是线程安全的:多任务同时读写会竞争。
- 适合:只读标志位、或配合临界区/关中断保护。
② 队列 Queue(最常用,正经传数据)
QueueHandle_t xQ = xQueueCreate(10, sizeof(int)); // 长度10,每项int // 任务A发 int val = 123; xQueueSend(xQ, &val, portMAX_DELAY); // 任务B收(阻塞等) int got; xQueueReceive(xQ, &got, portMAX_DELAY);- 线程安全,内部已加保护;可传任意结构体。
- 适合:传传感器数据、命令包等。
任务通知 Task Notification(最轻量)
// 发送方(com_task) xTaskNotifyGive(power_task_handle); // 发"通知" // 接收方(power_task) uint32_t res = ulTaskNotifyTake(pdTRUE, 10000); // 阻塞等,超时10s if(res != 0) { /* 收到通知,执行关机 */ }- 每个任务自带一个 32 位通知值,比队列快、省内存。
- 缺点:只能一对一(一个通知只能指定一个目标任务)。
④ 信号量 Semaphore(用于"同步/资源计数")
SemaphoreHandle_t xSem = xSemaphoreCreateBinary(); xSemaphoreGive(xSem); // 释放 xSemaphoreTake(xSem, portMAX_DELAY); // 获取(阻塞)- 二值信号量:事件通知(类似单次通知)。
- 计数信号量:资源池(如缓冲区空位数量)。
- 只传"有/无"状态,不传具体数据。
⑤ 互斥量 Mutex(保护共享资源)
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex(); xSemaphoreTake(xMutex, portMAX_DELAY); // 访问共享资源(如 LCD、全局结构体) xSemaphoreGive(xMutex);- 解决"全局变量竞争"问题,带优先级继承(防优先级反转)。
- 和信号量区别:互斥量必须"谁拿谁放",用于保护资源而非传信号。
⑥ 事件组 Event Group(一对多/多事件组合)
EventGroupHandle_t xEv = xEventGroupCreate(); xEventGroupSetBits(xEv, BIT_0); // 置位 xEventGroupWaitBits(xEv, BIT_0|BIT_1, pdTRUE, pdTRUE, ...); // 等多个事件- 一个变量里多个位,可等"任意一个/全部"事件发生。
- 适合:多事件组合触发。
五、任务同步
1、为什么要同步
多任务"并行"运行时,任务间需要协调:等某个事件发生、共享资源不被同时改写、多个条件都满足再执行。同步机制就是解决"何时能继续"和"数据不被破坏"的问题。
2、主要同步机制
信号量(Semaphore)
- 二值信号量:只有 0/1 两种状态,常用于任务与中断/任务间的事件通知(同步,不保护资源)。
SemaphoreHandle_t xSem; // 任务A:等事件 xSemaphoreTake(xSem, portMAX_DELAY); // 阻塞等待 // 事件到了,往下执行 // 中断/任务B:触发事件 xSemaphoreGiveFromISR(xSem, &pxHigher); // 中断里用 FromISR 版本- 计数信号量:计数值 >0 表示还有资源/事件,适合"资源池"(如缓冲区数量)
互斥量(Mutex)
专门用来保护共享资源(临界区),防止多个任务同时访问。
SemaphoreHandle_t xMutex; xSemaphoreTake(xMutex, portMAX_DELAY); // 进入临界区 // 操作共享变量 xSemaphoreGive(xMutex); // 离开临界区关键点:互斥量有优先级继承机制,能缓解优先级反转问题。不能在中断里用互斥量(中断不能阻塞)。
递归互斥量(Recursive Mutex)
同一个任务可以多次获取,必须释放相同次数,适合函数递归调用共享资源。
事件标志组(Event Group)
一个变量里多个 bit,支持"等任意一个"或"等全部",适合多事件组合触发。
// 等 bit0 和 bit1 都置位才继续 xEventGroupWaitBits(eg, BIT0|BIT1, pdTRUE, pdTRUE, portMAX_DELAY);任务通知(Task Notification)
FreeRTOS 的轻量级机制,取代信号量做同步,更快、更省内存(每个任务自带一个通知值)。
xTaskNotifyGive(taskHandle); // 发送通知 ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // 等待通知