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
变量,专门用于通知值,一个任务或者中断可以直接更新另一个任务的通知值
替代二进制信号量
- 发送:调用
xTaskNotifyGive( xReceiverTaskHandle )
- 接收:调用
ulTaskNotifyTake( pdTRUE, portMAX_DELAY )
。pdTRUE
表示接收后自动将通知值清零
替代计数信号量
- 发送:多次调用
xTaskNotifyGive()
。 - 接收:多次调用
ulTaskNotifyTake()
。通知值会自动累加计数。
替代单元素队列 (传递一个值)
- 发送方:调用
xTaskNotify( xReceiverHandle, ulValue, eSetValueWithOverwrite )
,直接将一个 32 位的值ulValue
写入目标任务的通知值。 - 接收方:用
ulTaskNotifyTake()
或xTaskNotifyWait()
来接收这个值。
替代事件组 (传递一组标志位)
- 发送方:调用
xTaskNotify( xReceiverHandle, ulBitsToSet, eSetBits )
,可以将一个或多个 bit “或”运算到目标任务的通知值上。 - 接收方:调用
xTaskNotifyWait()
来等待通知值中的特定 bit 组合。
#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;}