FreeRTOS 知识点
一、配置过程
二、基本知识点
2.1 抢占优先级和响应优先级
在 FreeRTOS 中,任务的调度方式主要有 抢占式(Preemptive) 和 协作式(Cooperative) 两种模式,它们的核心区别在于 任务如何释放 CPU 控制权,以及 调度器如何决定任务切换。以下是详细对比:
1.抢占式调度(Preemptive Scheduling)
高优先级任务可立即抢占低优先级任务,无需等待当前任务主动让出 CPU。
依赖配置:需在 FreeRTOSConfig.h
中启用:
#define configUSE_PREEMPTION 1 // 启用抢占式调度
#define configUSE_TIME_SLICING 1 // 可选:同优先级任务时间片轮转
工作流程
- 任务就绪:当一个高优先级任务进入就绪状态(如被创建、延迟结束、收到信号量等),调度器会立即检查是否需要切换。
- 抢占发生:如果新就绪的任务优先级高于当前任务,CPU 会立即切换到高优先级任务。
- 无需主动让步:即使当前任务未调用
taskYIELD()
或vTaskDelay()
,也会被强制打断。
优点
- 实时性强:高优先级任务能快速响应(适合中断服务、紧急事件处理)。
- 自动化调度:开发者无需手动管理任务切换。
缺点
- 资源竞争风险:需注意优先级反转(Priority Inversion)问题(可通过互斥量优先级继承解决)。
- 上下文切换开销:频繁抢占会增加 CPU 负载。
2. 协作式调度(Cooperative Scheduling)
特点
- 任务必须主动释放 CPU,否则会一直运行直到完成。
- 依赖配置:需在 FreeRTOSConfig.h中关闭抢占:
#define configUSE_PREEMPTION 0 // 关闭抢占式调度
工作流程
- 任务运行:当前任务会一直占用 CPU,直到:
- 调用 taskYIELD()主动让出 CPU。
- 调用阻塞 API(如 vTaskDelay(), xQueueReceive())。
- 调度器介入:只有当任务主动放弃 CPU 时,调度器才会选择下一个最高优先级就绪任务运行。
优点
- 确定性高:任务切换完全由代码控制,避免不可预知的抢占。
- 资源竞争少:无需频繁处理共享数据的互斥问题(适合简单系统)。
- 低开销:减少上下文切换次数。
缺点
- 实时性差:高优先级任务可能因低优先级任务不释放 CPU 而无法及时响应。
- 开发者负担:需手动插入 taskYIELD(),否则可能导致低优先级任务“饿死”。
3. 如何选择?
选抢占式:
- 需要快速响应中断或高优先级事件(如传感器数据处理)。
- 系统中有多个不同优先级的任务。
选协作式:
- 资源受限的裸机升级项目(减少调度复杂性)。
- 任务执行时间短且可预测(如串口协议解析)。
2.2 响应优先级和抢占优先级
1. 优先级的基本机制
FreeRTOS 使用 单一的优先级数值(通常为 0到 configMAX_PRIORITIES-1,默认最高优先级为 configMAX_PRIORITIES-1)来管理任务调度。
数值越大,优先级越高(例如优先级 3> 2)。
高优先级任务可抢占(Preempt)低优先级任务,无需等待当前任务主动释放 CPU。
2. 抢占优先级(Preemptive Priority)
定义:高优先级任务立即抢占低优先级任务的 CPU 使用权。
表现:
如果任务 A(优先级 3)就绪,而当前运行的是任务 B(优先级 2),FreeRTOS 会立即切换到任务 A。
这是 FreeRTOS 默认的调度行为(需配置 configUSE_PREEMPTION=1)。
关键点:
抢占是自动的,无需任务主动让步(除非使用 taskYIELD())。确保高优先级任务能实时响应。
3. 响应优先级(Response Priority)
定义:任务在就绪状态下被调度的顺序,完全由优先级决定。
表现:
当多个任务同时就绪时,调度器会选择优先级最高的任务运行(即响应最快)。
例如:任务 A(优先级 3)和任务 B(优先级 2)同时就绪,任务 A 会优先被调度。
关键点:
“响应优先级”是抢占式调度的结果,而非独立配置。
与“抢占优先级”是同一机制的两个视角:
- 抢占:强调中断当前任务的行为。
- 响应:强调任务被选中的顺序。
FreeRTOS 中 “实时性” 的体现是什么: 抢占式:高优先级任务可打断低优先级任务(只要高优先级任务就绪,立即执行),实时性强;
2.3 FreeRTOS 的任务状态有哪些?状态之间如何切换?
- 考察点:任务生命周期的理解(高频基础题)。
- 核心答点:5 种状态 ——就绪(Ready)、运行(Running)、阻塞(Blocked)、挂起(Suspended)、删除(Deleted);
- 切换逻辑:运行→就绪(被高优先级任务抢占)、运行→阻塞(调用
vTaskDelay()
/ 等待信号量)、阻塞→就绪(延时到 / 信号量触发)、就绪→运行(调度器选择最高优先级就绪任务)、任意状态→挂起(vTaskSuspend()
)、挂起→就绪(xTaskResume()
)。
2.4 FreeRTOS 的内核组成有哪些核心模块
考察点:内核架构的整体认知。
核心答点:任务管理(创建 / 删除 / 切换)、时间管理(定时器 / 延时)、同步与通信(信号量 / 队列 / 事件组 / 互斥锁)、内存管理(堆 / 栈分配)、中断管理(临界区 / 中断安全 API)。
2.5 FreeRTOS 的 “临界区” 是什么?如何保护临界区?
考察点:中断与任务的资源冲突解决逻辑。
核心答点:临界区是 “不能被中断打断的代码段”(如操作共享变量);
保护方式:
- 任务级:
taskENTER_CRITICAL()
/taskEXIT_CRITICAL()
(关闭任务调度,不关闭中断); - 中断级:
taskENTER_CRITICAL_FROM_ISR()
/taskEXIT_CRITICAL_FROM_ISR()
(关闭中断,需在中断服务函数中使用)。
2.6 详细说明 FreeRTOS 的 “临界区” 是什么?如何保护临界区。
在 FreeRTOS 中,临界区(Critical Section) 是指一段 “不允许被中断或其他任务打断” 的代码段,通常用于操作共享资源(如全局变量、硬件寄存器、外设等),以防止多任务并发或中断触发导致的数据竞争和不一致问题。
1. 任务级临界区保护(在任务中使用)
适用于任务代码中需要保护的临界区,核心是 “禁止任务调度器切换”(但不禁止中断,仅禁止因任务调度导致的打断)。
// 进入临界区:禁止任务调度
taskENTER_CRITICAL();// 临界区代码(操作共享资源)
// ...// 退出临界区:恢复任务调度
taskEXIT_CRITICAL();
工作原理:
taskENTER_CRITICAL()
会关闭 FreeRTOS 调度器(通过禁止 PendSV 中断,PendSV 是任务切换的触发源),但不影响其他硬件中断(如定时器、串口中断);- 临界区代码执行期间,高优先级任务即使就绪也无法抢占当前任务;
taskEXIT_CRITICAL()
会恢复调度器,若有高优先级任务就绪,会触发任务切换。
注意事项:
- 临界区代码要尽可能短,避免影响系统实时性(高优先级任务会被阻塞等待);
- 不可嵌套调用(多次调用
taskENTER_CRITICAL()
需对应相同次数的taskEXIT_CRITICAL()
,但不建议嵌套)。
2. 中断级临界区保护(在中断服务程序中使用)
适用于中断服务程序(ISR)中需要保护的临界区,核心是 “暂时关闭中断”(防止被更高优先级中断打断)。
// 进入临界区:保存当前中断状态并关闭中断
UBaseType_t uxSavedInterruptStatus;
uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();// 临界区代码(操作共享资源)
// ...// 退出临界区:恢复中断状态
taskEXIT_CRITICAL_FROM_ISR(uxSavedInterruptStatus);
工作原理:
taskENTER_CRITICAL_FROM_ISR()
会先保存当前的中断使能状态,再关闭全局中断(通过修改 CPU 状态寄存器,如 ARM 的 PRIMASK);- 临界区代码执行期间,所有中断(无论优先级)都被禁止,确保代码不被任何中断打断;
taskEXIT_CRITICAL_FROM_ISR()
会恢复进入临界区前的中断状态(避免误关闭其他中断)。
注意事项:
- 中断中的临界区必须极致简短(微秒级),否则会严重影响系统对外部事件的响应(如丢失中断);
- 必须使用返回值
uxSavedInterruptStatus
作为参数传递给退出函数,否则可能导致中断无法恢复。
2.7 FreeRTOS 静态创建和动态创建
在 FreeRTOS 中,任务创建主要有两种方式:静态创建和动态创建。这两种方式各有特点,适用于不同的应用场景。
考察点:任务创建的两种内存分配方式。
核心答点:两种核心创建函数;
区别:
xTaskCreate()
:动态分配任务栈和控制块(依赖 FreeRTOS 堆管理,无需用户手动分配内存);xTaskCreateStatic()
:静态分配(需用户手动指定任务栈数组、控制块变量,不依赖堆,适合内存受限场景)。
2.7 什么是 “空闲任务(Idle Task)”?它的作用是什么?
考察点:FreeRTOS 内核的基础任务。
核心答点:空闲任务是 FreeRTOS 启动调度器(vTaskStartScheduler()
)时自动创建的最低优先级(优先级 0)任务;
作用:
- 当无其他就绪任务时,CPU 执行空闲任务(避免 CPU 空转);
- 回收 “动态创建且被删除” 的任务的内存(需开启
configUSE_IDLE_HOOK
配置空闲钩子函数)。
2.8 FreeRTOS 中的信号量(Semaphore)有哪几种类型?分别用在什么场景?
特性 | 二进制信号量(Binary Semaphore) | 计数信号量(Counting Semaphore) | 互斥信号量(Mutex) |
---|---|---|---|
值范围 | 只能为 0 或 1 | 0 到最大计数(用户定义) | 只能为 0 或 1 (类似二进制) |
核心用途 | 任务间 / 中断 - 任务同步;简单互斥 | 有限资源的并发访问控制(如连接池) | 共享资源的互斥访问(解决优先级反转) |
所有权 | 无(任何任务可释放) | 无(任何任务可释放) | 有(只有持有者可释放) |
优先级继承 | 无 | 无 | 有(避免优先级反转) |
创建函数 | xSemaphoreCreateBinary() | xSemaphoreCreateCounting() | xSemaphoreCreateMutex() |
典型初始值 | 同步场景为 0 ;互斥场景为 1 | 资源初始数量(如 5 表示 5 个资源) | 1 (资源空闲) |
1. 二进制信号量(Binary Semaphore)
适用场景:任务间同步、中断与任务同步,或简单互斥(无优先级继承需求)。
示例:中断与任务同步
#include "FreeRTOS.h"
#include "semphr.h"SemaphoreHandle_t xBinarySemaphore;// 初始化:创建二进制信号量(初始值为0)
void vSetup() {xBinarySemaphore = xSemaphoreCreateBinary();if (xBinarySemaphore != NULL) {// 创建任务xTaskCreate(vTaskHandleEvent, "HandleEvent", 128, NULL, 1, NULL);vTaskStartScheduler();}
}// 中断服务程序:触发时释放信号量
void EXTI_IRQHandler() {BaseType_t xHigherPriorityTaskWoken = pdFALSE;// 清除中断标志(硬件相关)EXTI_ClearFlag();// 从ISR中释放信号量(必须用FromISR版本)xSemaphoreGiveFromISR(xBinarySemaphore, &xHigherPriorityTaskWoken);// 若需切换任务,请求调度portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}// 任务:等待中断信号并处理
void vTaskHandleEvent(void *pvParam) {for (;;) {// 等待信号量(永久阻塞,直到中断触发)if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdPASS) {printf("处理中断事件\n");}}
}
2. 计数信号量(Counting Semaphore)
适用场景:控制对有限数量资源的并发访问(如允许 5 个任务同时使用某个资源)。
示例:控制 3 个串口资源的并发访问
#include "FreeRTOS.h"
#include "semphr.h"SemaphoreHandle_t xCountingSemaphore;
#define MAX_SERIAL_PORT 3 // 最大串口数量// 初始化:创建计数信号量(初始值为3,表示3个可用资源)
void vSetup() {xCountingSemaphore = xSemaphoreCreateCounting(MAX_SERIAL_PORT, MAX_SERIAL_PORT);if (xCountingSemaphore != NULL) {// 创建5个任务竞争串口资源for (int i = 0; i < 5; i++) {xTaskCreate(vTaskUseSerial, "SerialTask", 128, (void*)i, 1, NULL);}vTaskStartScheduler();}
}// 任务:使用串口资源
void vTaskUseSerial(void *pvParam) {int taskId = (int)pvParam;for (;;) {// 尝试获取串口资源(最多等待100ms)if (xSemaphoreTake(xCountingSemaphore, pdMS_TO_TICKS(100)) == pdPASS) {printf("任务%d:获取串口成功,开始传输数据\n", taskId);vTaskDelay(pdMS_TO_TICKS(500)); // 模拟数据传输printf("任务%d:释放串口\n", taskId);xSemaphoreGive(xCountingSemaphore); // 释放资源} else {printf("任务%d:获取串口失败,重试\n", taskId);}vTaskDelay(pdMS_TO_TICKS(100));}
}
3. 互斥信号量(Mutex)
适用场景:共享资源的互斥访问,尤其适用于存在优先级差异的任务(解决优先级反转问题)。
示例:保护共享内存的访问
#include "FreeRTOS.h"
#include "semphr.h"SemaphoreHandle_t xMutex;
int g_sharedMemory = 0; // 共享资源// 初始化:创建互斥信号量(初始值为1,资源空闲)
void vSetup() {xMutex = xSemaphoreCreateMutex();if (xMutex != NULL) {// 创建3个不同优先级的任务xTaskCreate(vHighPriorityTask, "HighTask", 128, NULL, 3, NULL);xTaskCreate(vMediumPriorityTask, "MediumTask", 128, NULL, 2, NULL);xTaskCreate(vLowPriorityTask, "LowTask", 128, NULL, 1, NULL);vTaskStartScheduler();}
}// 低优先级任务:持有共享资源
void vLowPriorityTask(void *pvParam) {for (;;) {xSemaphoreTake(xMutex, portMAX_DELAY);printf("低优先级任务:持有共享资源\n");vTaskDelay(pdMS_TO_TICKS(2000)); // 长时间占用资源printf("低优先级任务:释放共享资源\n");xSemaphoreGive(xMutex);vTaskDelay(pdMS_TO_TICKS(1000));}
}// 中优先级任务:频繁运行(可能抢占低优先级任务)
void vMediumPriorityTask(void *pvParam) {for (;;) {printf("中优先级任务:运行中\n");vTaskDelay(pdMS_TO_TICKS(100));}
}// 高优先级任务:需要访问共享资源
void vHighPriorityTask(void *pvParam) {for (;;) {printf("高优先级任务:等待共享资源\n");xSemaphoreTake(xMutex, portMAX_DELAY);printf("高优先级任务:访问共享资源\n");xSemaphoreGive(xMutex);vTaskDelay(pdMS_TO_TICKS(1000));}
}
关键特性:当低优先级任务持有互斥锁时,高优先级任务等待期间会触发优先级继承(低优先级任务临时提升至与高优先级任务相同的优先级),避免中优先级任务抢占 CPU 导致的优先级反转。
2.9 队列(Queue)的作用是什么?它的核心特性有哪些?
1 队列的核心特性
- 数据传递方式:采用拷贝传递(数据被复制到队列缓冲区),而非指针传递,避免内存访问冲突。
- 先进先出(FIFO):默认按入队顺序出队,也可配置为优先级出队(高优先级数据优先)。
- 多对多通信:多个任务 / 中断可向同一队列发送数据,多个任务可从同一队列接收数据。
- 阻塞机制:发送 / 接收数据时可指定超时时间,无数据 / 空间时阻塞等待,提高 CPU 效率。
- 中断安全:提供
FromISR
系列 API,支持从中断服务程序(ISR)中操作队列。
2. 队列的关键概念
- 队列长度:可存储的最大数据项数量(创建时指定)。
- 数据项大小:每个数据项的字节数(创建时指定,所有数据项大小相同)。
- 队头 / 队尾:数据入队从队尾添加,出队从队头移除(FIFO 模式)。
- 阻塞超时:
- 发送时:队列满时,任务阻塞等待空间,超时后返回失败。
- 接收时:队列空时,任务阻塞等待数据,超时后返回失败。
3. 代码实例
任务间通过队列传递数据
场景:TaskSender
周期性产生数据并发送到队列,TaskReceiver
从队列接收并处理数据。
#include "FreeRTOS.h"
#include "queue.h"
#include <stdio.h>// 定义队列句柄
QueueHandle_t xDataQueue;// 发送任务:产生数据并发送到队列
void vTaskSender(void *pvParameters) {int32_t lDataToSend = 0;BaseType_t xStatus;for (;;) {// 发送数据到队列(队尾),超时时间0(不阻塞)xStatus = xQueueSend(xDataQueue, &lDataToSend, 0);if (xStatus != pdPASS) {printf("队列满,发送失败!\n");} else {printf("发送数据: %d\n", lDataToSend);lDataToSend++;}vTaskDelay(pdMS_TO_TICKS(500)); // 每500ms发送一次}
}// 接收任务:从队列接收数据并处理
void vTaskReceiver(void *pvParameters) {int32_t lReceivedData;BaseType_t xStatus;for (;;) {// 从队列接收数据,超时时间1000ms(等待1秒)xStatus = xQueueReceive(xDataQueue, &lReceivedData, pdMS_TO_TICKS(1000));if (xStatus == pdPASS) {printf("接收数据: %d\n", lReceivedData);} else {printf("1秒内未收到数据!\n");}}
}int main(void) {// 创建队列:长度为5,每个数据项为int32_t(4字节)xDataQueue = xQueueCreate(5, sizeof(int32_t));if (xDataQueue != NULL) {// 创建发送和接收任务xTaskCreate(vTaskSender, "Sender", 128, NULL, 1, NULL);xTaskCreate(vTaskReceiver, "Receiver", 128, NULL, 2, NULL);// 启动调度器vTaskStartScheduler();}// 若调度器启动失败,进入死循环for (;;);return 0;
}
中断与任务通过队列传递数据
场景:外部中断触发时,ISR 向队列发送事件标志,任务从队列接收并处理中断事件。
#include "FreeRTOS.h"
#include "queue.h"
#include "stm32f4xx.h" // 以STM32为例,其他平台需适配QueueHandle_t xInterruptQueue;// 初始化外部中断(硬件相关)
void vInitInterrupt() {// 配置GPIO为输入,使能外部中断(省略具体硬件配置)NVIC_EnableIRQ(EXTI0_IRQn); // 使能EXTI0中断
}// 中断服务程序:向队列发送事件
void EXTI0_IRQHandler(void) {BaseType_t xHigherPriorityTaskWoken = pdFALSE;uint32_t ulEvent = 1; // 事件标志(1表示中断触发)// 清除中断标志EXTI_ClearITPendingBit(EXTI_Line0);// 从ISR发送数据到队列(必须使用FromISR版本)xQueueSendFromISR(xInterruptQueue, &ulEvent, &xHigherPriorityTaskWoken);// 若有更高优先级任务被唤醒,请求任务切换portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}// 处理任务:接收中断事件并处理
void vTaskInterruptHandler(void *pvParameters) {uint32_t ulReceivedEvent;BaseType_t xStatus;for (;;) {// 等待中断事件(永久阻塞)xStatus = xQueueReceive(xInterruptQueue, &ulReceivedEvent, portMAX_DELAY);if (xStatus == pdPASS) {printf("收到中断事件,执行处理逻辑\n");// 此处添加中断事件的具体处理代码}}
}int main(void) {// 创建队列:长度为3,每个数据项为uint32_txInterruptQueue = xQueueCreate(3, sizeof(uint32_t));if (xInterruptQueue != NULL) {vInitInterrupt(); // 初始化中断xTaskCreate(vTaskInterruptHandler, "IntHandler", 128, NULL, 1, NULL);vTaskStartScheduler();}for (;;);return 0;
}
优先级队列(数据插队)
场景:紧急数据通过 xQueueSendToFront()
插入队头,优先被处理。
// 发送紧急数据(插入队头)
void vSendEmergencyData(int32_t lEmergencyData) {BaseType_t xStatus;// 紧急数据插入队头,确保优先处理xStatus = xQueueSendToFront(xDataQueue, &lEmergencyData, pdMS_TO_TICKS(100));if (xStatus == pdPASS) {printf("紧急数据 %d 已插入队头\n", lEmergencyData);}
}
2.10. 事件组(Event Group)的作用是什么?和信号量、队列有什么区别?
核心特性
- 事件表示:使用一个 32 位无符号整数(
EventBits_t
)存储事件,每个位代表一个独立事件(bit0~bit31)。 - 逻辑触发:任务可等待事件的 “逻辑与”(所有指定事件都发生)或 “逻辑或”(任一指定事件发生)。
- 事件持久性:事件发生后会保持置位状态,直到被显式清除(或任务读取时自动清除)。
- 多任务等待:多个任务可同时等待同一事件组的不同事件组合。
- 中断安全:支持从中断服务程序(ISR)中设置事件位。
2.11 什么是 “任务通知(Task Notification)”?它相比队列、信号量有什么优势?
考察点:FreeRTOS 高效同步机制(较新特性)。
核心答点:任务通知是 FreeRTOS v8.2.0 后新增的机制,通过 “直接向任务发送通知” 实现同步 / 通信(每个任务有一个 32 位的通知值);
优势:
- 更高效:无需创建队列 / 信号量等内核对象,直接操作任务控制块,减少内存开销和 CPU 消耗;
- 灵活:支持多种通知类型(如设置值、递增、覆盖、脉冲等),可替代二进制信号量、计数信号量、队列(单数据)等场景。
通知类型(Action)
发送通知时可指定对接收任务通知值的操作,共 8 种类型(通过 eAction
参数设置),核心类型包括:
eNoAction
:仅发送通知,不修改通知值。eSetBits
:按位或操作(类似事件组的置位)。eIncrement
:通知值递增(类似计数信号量)。eSetValueWithOverwrite
:直接覆盖通知值(类似队列发送,覆盖模式)。
功能 | 任务中使用的 API | 中断中使用的 API(ISR) |
---|---|---|
发送通知 | xTaskNotify() | xTaskNotifyFromISR() |
等待通知 | ulTaskNotifyTake() (简化版) | - |
等待通知(高级) | xTaskNotifyWait() | - |
清除通知 | 无需单独 API,xTaskNotifyWait() 可清除 | - |
2.12 FreeRTOS 有哪几种内存分配方案(堆管理方案)?分别有什么特点?
考察点:内存管理的核心方案(高频基础题)。
核心答点:5 种堆管理方案(定义在 heap_1.c
~ heap_5.c
中,用户需选择一个引入工程);
- 堆 1(heap_1):仅支持动态分配(
pvPortMalloc()
),不支持释放(vPortFree()
),适合无需删除任务 / 信号量的场景(简单、安全); - 堆 2(heap_2):支持分配和释放,采用 “最佳适配” 算法,但不合并相邻空闲块,易产生内存碎片;
- 堆 3(heap_3):封装标准 C 库的
malloc()
和free()
,依赖编译器的内存管理,可重入(适合有操作系统支持的场景); - 堆 4(heap_4):支持分配和释放,采用 “最佳适配”+“空闲块合并”,减少内存碎片,支持动态调整堆大小;
- 堆 5(heap_5):基于堆 4,支持 “非连续内存块”(如将 RAM 分为多个区域,堆 5 可管理这些分散的内存),适合内存布局复杂的场景。