FreeRTOS 实时操作系统详解

2025.7.20

创建任务

// 这是一个任务函数,它会不停地打印 "你好"
void vHelloTask(void *pvParameters) {
while(1) {
printf("你好,我是一个任务!\n");
vTaskDelay(1000); // 延迟1秒
}
}
// 在你的 main 函数里,你会这样做来启动这个任务
int main(void) {
// 创建任务
xTaskCreate(
vHelloTask, // 任务函数
"Hello Task", // 任务名字 (用于调试)
1000, // 任务堆栈大小 (给任务分配的内存)
NULL, // 传递给任务的参数 (这里不用)
1, // 任务优先级 (数字越大,优先级越高)
NULL // 任务句柄 (这里不用)
);
// 启动调度器,让 "店长" 开始工作
vTaskStartScheduler();
// 程序永远不会运行到这里
while(1);
}

任务间的传递

使用队列,先进先出,要是满了来新的,新的会等待旧的空出新位置才进去

// 在 main 函数中,需要先创建好 "传送带"
QueueHandle_t xOrderQueue;
xOrderQueue = xQueueCreate(5, sizeof(int)); // 创建一个能放5个整数订单的队列
// “点单员” 任务
void vOrderTakerTask(void *pvParameters) {
int orderID = 1; // 假设 1 代表珍珠奶茶
while(1) {
// ... 接到新订单 ...
printf("点单员:收到新订单!\n");
// 把订单号 1 发送到传送带上
xQueueSend(xOrderQueue, &orderID, portMAX_DELAY);
}
}
// “奶茶师” 任务
void vBaristaTask(void *pvParameters) {
int receivedOrder;
while(1) {
// 等待从传送带上接收订单
if (xQueueReceive(xOrderQueue, &receivedOrder, portMAX_DELAY)) {
printf("奶茶师:收到订单 %d,开始制作!\n", receivedOrder);
// ... 开始制作奶茶 ...
}
}
}

共享资源

当你要保护一个被多个任务访问的变量或外设时,用互斥锁

当一个任务需要等待另一个任务或中断完成某件事时,用二进制信号量

信号量

有资源访问门禁卡的盒子本身就是信号量,想要访问资源,就要使用xSemaphoreTake()拿一张门禁卡,没卡了后来的人就要等待别人出来(阻塞态),使用xSemaphoreGive()归还门禁卡,体现了信号量的核心作用:控制对一组有限资源的访问

计数信号量 (Counting Semaphore)

用于管理多个同类资源的池,比如,系统里有 3 个可用的内存缓冲区,或者一个网页服务器能同时处理 10 个网络连接,计数值可以是从 0 到你设定的任意最大值(比如 5)

使用xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ); 创建, 你需要指定最大计数值和初始计数值

二进制信号量 (Binary Semaphore)

计数值只能是 0 或 1,更多用于任务同步,提供数据准备好的标志位

使用xSemaphoreCreateBinary();创建 ,创建出来时,它默认是“空”的(没有卡),你必须先 Give 一次,才能被 Take

**典型场景:**一个任务(生产者)在准备数据,另一个任务(消费者)需要处理这些数据,消费者任务一开始就尝试 xSemaphoreTake(),但因为信号量是空的,它会立刻被阻塞,当生产者任务把数据准备好之后,它就执行一次 xSemaphoreGive(),这就像是举起一个信号旗,消费者任务因为等到了这个信号,马上被唤醒(拿到了那张唯一的卡),然后开始处理数据

互斥锁 (Mutexes)

避免同时读写同一个数据造成数据错误

任务A要访问一个资源,必须先使用xSemaphoreTake(mutex_handle, ...)得到访问的钥匙(唯一的),如果有别的任务在用,就阻塞态一下,用完之后使用xSemaphoreGive(mutex_handle)还钥匙

特性:优先级继承(Priority Inheritance)

**问题情境:**任务A(低优先级)得到了钥匙准备访问资源,任务B(高优先级)有个紧急任务也要访问资源,CPU先抢占A,发现钥匙在A,CPU回到A,所以B只能等待A完成返回钥匙,但是此时有个C(中优先级)进行一个不紧不慢的任务,C优先级比A高,抢占了A,导致了高优先级的B在等一个中优先级的C

互斥锁解决方案:调度器会临时把低优先级的任务A提升到和紧急的任务B一样的优先级,保证先归还钥匙,钥匙归还后A的优先级会恢复正常

高级同步与控制

事件组(event groups)

// 首先,为我们的事件定义“灯泡”的编号
#define WIFI_CONNECTED_BIT (1 << 0) // 第 0 个灯泡代表 WiFi 连接事件
#define SENSOR_READY_BIT (1 << 1) // 第 1 个灯泡代表传感器就绪事件
// 全局的事件组句柄
EventGroupHandle_t xSystemEventGroup;
// 上传数据的任务
void vUploaderTask(void *pvParameters) {
// 在 main 函数中,我们已经通过 xEventGroupCreate() 创建了 xSystemEventGroup
while(1) {
// 等待 WiFi 和传感器两个事件都发生
// 这个函数会阻塞任务,直到条件满足
xEventGroupWaitBits(
xSystemEventGroup, // 指定要等待的事件组
WIFI_CONNECTED_BIT | SENSOR_READY_BIT, // 我们感兴趣的“灯泡”组合
pdTRUE, // 关键:等待成功后,自动熄灭这两个灯泡,以便下次继续等待
pdTRUE, // 关键:等待这两个灯泡 ALL (全部) 都亮起
portMAX_DELAY // 无限期等待
);
// 代码能执行到这里,说明两个条件都已满足
printf("条件达成,开始上传数据...\n");
// ... 执行上传操作 ...
}
}
// 在 WiFi 管理任务中...
void vWifiManagerTask(void *pvParameters){
// ... 一系列操作后,WiFi 连接成功 ...
printf("WiFi 已连接!\n");
xEventGroupSetBits(xSystemEventGroup, WIFI_CONNECTED_BIT); // 点亮 WiFi 的灯泡
}
// 在传感器管理任务中...
void vSensorManagerTask(void *pvParameters){
// ... 采集到新数据 ...
printf("传感器数据已就绪!\n");
xEventGroupSetBits(xSystemEventGroup, SENSOR_READY_BIT); // 点亮传感器的灯泡
}

xEventGroupWaitBits中当我们使用 | (按位或) 时,我们是在合并这些目标:

0000 0010 (天气安全)
| 0000 0100 (水位充足)
-----------
= 0000 0110 (我们的“目标清单”)

我们传给 xEventGroupWaitBits 的是 0000 0110 这个整数。这个数字告诉函数:“我感兴趣的事件是第 1 位和第 2 位,请帮我留意。”,轮到 xEventGroupWaitBits 函数工作了。它拿到了我们的“目标清单” 0000 0110,并且我们告诉了它需要等待所有位 (pdTRUE)。

假设当前事件组的实际状态是 0000 0010 (只有天气安全,水位还不充足)。

函数内部的检查逻辑,就非常像你刚才想的 & 操作了。它会这样做: if ( (当前状态 & 目标清单) == 目标清单 )

定时器

2秒打印一次消息的定时器

// 这是定时器响起时要执行的回调函数
// 注意:它的参数是固定的 TimerHandle_t 类型
void vMyTimerCallback(TimerHandle_t xTimer) {
printf("闹钟时间到!这是一个由软件定时器触发的消息。\n");
}
void main_app() {
// ... 其他初始化代码 ...
printf("正在创建一个 2 秒钟的自动重载定时器...\n");
TimerHandle_t xMyTimer = xTimerCreate(
"MyTimer", // 定时器的名字,仅用于调试
pdMS_TO_TICKS(2000), // 定时周期:2000毫秒
pdTRUE, // pdTRUE 表示这是个自动重载定时器
(void *)0, // 一个可选的ID,这里我们不用
vMyTimerCallback // 指定回调函数
);
// 检查定时器是否创建成功,然后启动它
if (xMyTimer != NULL) {
xTimerStart(xMyTimer, 0); // 0 表示立即启动,不等任何时间
}
// 启动调度器,之后定时器服务任务就会开始工作
vTaskStartScheduler();
}

高级任务交互与资源管理

任务通知 (Task Notifications)

任务间通信的直线电话,速度快

每个任务在创建时,内部的任务控制块TCB中已经内嵌了一个uint32_t变量,专门用于通知值,一个任务或者中断可以直接更新另一个任务的通知值

替代二进制信号量

替代计数信号量

替代单元素队列 (传递一个值)

替代事件组 (传递一组标志位)

#include "FreeRTOS.h"
#include "task.h"
#include <stdio.h>
// 全局变量,用于保存需要被通知的任务的句柄。
// 中断服务程序(ISR)需要通过这个句柄来知道该通知哪个任务。
static TaskHandle_t xButtonHandlerTaskHandle = NULL;
/**
* @brief 处理按钮事件的任务
*/
void vButtonHandlerTask(void *pvParameters)
{
printf("任务:vButtonHandlerTask 已启动,正在等待按钮中断...\n");
while (1)
{
// ulTaskNotifyTake() 是一个专门设计的接收通知的API。
// 第一个参数 pdTRUE: 表示它的行为像一个二进制信号量。
// 接收到通知后,通知值会被清零。
// 第二个参数 portMAX_DELAY: 如果没有通知,任务将永远阻塞在这里,不消耗CPU。
uint32_t ulNotificationValue = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
if (ulNotificationValue > 0)
{
// 一旦代码执行到这里,就意味着任务收到了来自中断的通知。
printf("任务:收到按钮按下通知!正在处理事件...\n");
// 在这里可以执行实际的按钮处理逻辑,比如开关LED、发送消息等。
}
}
}
/**
* @brief 假设这是按钮的外部中断服务程序 (ISR)
* 具体名称取决于你的微控制器型号,例如 EXTI0_IRQHandler
*/
void EXTI_Button_IRQHandler(void)
{
// 这个变量是ISR中调用FreeRTOS API的标准模式
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 清除中断标志位 (这是硬件相关的操作)
// Clear_Interrupt_Flag();
// 关键步骤:从ISR中给指定的任务发送一个通知。
// 这个函数非常轻量级,适合在中断中使用。
// 它会递增目标任务的通知值。
if (xButtonHandlerTaskHandle != NULL)
{
vTaskNotifyGiveFromISR(xButtonHandlerTaskHandle, &xHigherPriorityTaskWoken);
}
// 如果 xHigherPriorityTaskWoken 的值变为 pdTRUE,
// 说明 vButtonHandlerTask 的优先级高于当前被中断的任务,
// 那么在退出中断后应立即进行一次上下文切换。
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
/**
* @brief 主函数/程序入口
*/
int main(void)
{
// ...硬件初始化,比如时钟、串口、中断控制器等...
printf("系统启动,正在创建任务...\n");
// 创建按钮处理任务
// 关键:最后一个参数 &xButtonHandlerTaskHandle 用来传出被创建任务的句柄。
xTaskCreate(vButtonHandlerTask, // 任务函数
"ButtonHandler", // 任务名
1024, // 堆栈大小
NULL, // 传递给任务的参数
2, // 任务优先级
&xButtonHandlerTaskHandle // 传出任务句柄
);
// 启动 FreeRTOS 调度器
vTaskStartScheduler();
// 程序不会执行到这里
while (1);
return 0;
}

内存管理