ESP32下FreeRTOS实时操作系统使用
文章目录
- ESP32下FreeRTOS实时操作系统使用
-
- 一、概述
- 二、为什么要使用实时操作系统RTOS?
- 三、FreeRTOS任务
-
- 3.1 什么是 FreeRTOS 任务?
- 3.2 FreeRTOS 任务的特点
- 3.3 FreeRTOS 任务的生命周期
- 3.4 FreeRTOS 任务的状态
- 3.5 FreeRTOS 任务与多线程的区别
- 3.6 常用 FreeRTOS 任务函数
-
- 3.6.1 任务创建函数`xTaskCreate`()原型和头文件:
- 3.6.2 任务创建函数`xTaskCreatePinnedToCore`()原型和头文件:(仅在 ESP-IDF 中可用)
- 3.6.3 任务延时函数`vTaskDelay`()原型和头文件:
- 3.6.4 任务删除函数`vTaskDelete`()原型和头文件:
- 3.6.5 暂停任务函数`vTaskSuspend`()原型和头文件:
- 3.6.6 恢复挂起任务函数`vTaskResume`()原型和头文件:
- 3.6.7 返回指定任务优先级函数`uxTaskPriorityGet`()原型和头文件:
- 3.6.8 设置任务新优先级函数`vTaskPrioritySet`()原型和头文件:
- 3.6.9 获取当前正在运行任务的句柄函数`xTaskGetCurrentTaskHandle`()原型和头文件:
- 3.7 任务创建实例
- 四、FreeRTOS队列
-
- 4.1 FreeRTOS 队列的特点
- 4.2 常用 FreeRTOS 队列函数
-
- 4.2.1 创建队列函数`xQueueCreate`()原型和头文件:
- 4.2.2 发送数据到队列函数`xQueueSend`()原型和头文件:
- 4.2.3 发送数据到队列(中断安全版本)函数`xQueueSendFromISR`()原型和头文件:
- 4.2.4 从队列接收数据函数`xQueueReceive`()原型和头文件:
- 4.2.5 从队列接收数据(中断安全版本)函数`xQueueReceiveFromISR`()原型和头文件:
- 4.2.6 获取队列中元素个数函数`uxQueueMessagesWaiting`()原型和头文件:
- 4.2.7 获取队列中剩余空间函数`uxQueueSpacesAvailable`()原型和头文件:
- 4.3 FreeRTOS队列示例
- 五、FreeRTOS信号量(Semaphore)
- 六、FreeRTOS互斥锁
- 七、FreeRTOS事件组
-
- 7.1 事件组的特性
- 7.2 FreeRTOS事件组常用函数
-
- 7.2.1 创建事件组函数`xEventGroupCreate`()原型和头文件:
- 7.2.2 等待事件组的标志位函数`xEventGroupWaitBits`()原型和头文件:
- 7.2.3 设置事件组中的标志位函数`xEventGroupSetBits`()原型和头文件:
- 7.2.4 清除事件组中的标志位函数**`xEventGroupClearBits`**()原型和头文件:
- 7.2.5 在中断服务例程中清除事件组中的标志位函数`xEventGroupClearBitsFromISR`()原型和头文件:
- 7.2.6 获取当前事件组中的标志位函数`xEventGroupGetBits`()原型和头文件:
- 7.3 事件组使用示例
- 八、FreeRTOS直达任务通知
一、概述
我们的ESP32是基于ESP-IDF来开发的,而ESP-IDF是基于FreeRTOS的框架,里面用到的组件,包括应用程序都是基于FreeRTOS来开发的,因此我们必须掌握FreeRTOS的使用,这里我们我深究FreeRTOS的原理,只关注FreeRTOS API接口的使用,在前面我们学习了Linux系统高级编程,如:文件、进程、进程间通信、线程、多线程、网络等,我们学习过更高级的Linux操作系统,那么使用这个FreeRTOS实时操作系统应该会很简单。
二、为什么要使用实时操作系统RTOS?
在低端设备中程序基本可以分为:裸机和RTOS,针对简单的程序,我们使用裸机程序完全可以满足,一旦功能复杂,程序模块众多的时候,裸机程序很难满足我们的需求,因此我们需要使用操作系统。下面我们举个例子:
-
裸机程序:
while(1){ func1(); func2(); func3(); }
-
RTOS程序:
while(1){ func1(); } while(1){ func2(); } while(1){ func3(); }
对于裸机程序我们需要把所有的功能模块写在一个while
大循环里,可以想象其中任何一个功能模块发生阻塞或者延时等待时,其他的功能模块都要等待。而对于RTOS程序可以创建多个任务处理不同的功能模块,当模块A需要阻塞时,它可以放弃自己的执行时间片,把时间片让给其他任务,这样整个程序可以高效的运行。
三、FreeRTOS任务
3.1 什么是 FreeRTOS 任务?
在 FreeRTOS 中,任务(Task)是一个独立运行的程序代码块,它是实时操作系统的基本执行单元。FreeRTOS 的多任务机制允许多个任务“并行运行”(实际上是通过快速切换任务实现的,称为任务调度)。每个任务都有自己的运行逻辑和执行周期,FreeRTOS 会在后台管理这些任务的执行。
3.2 FreeRTOS 任务的特点
- 并发性:
- FreeRTOS 会在多个任务之间切换,使得它们看起来像是同时运行(多任务)。但在单核 MCU 上,任务的切换是由 FreeRTOS 调度器(Scheduler)以时间片轮转或优先级方式实现的。
- 独立性:
- 每个任务都是独立的,具有自己的上下文(寄存器值、堆栈等)。
- 优先级:
- 每个任务可以设置一个优先级(通常用整数表示),优先级越高,任务获得 CPU 时间的可能性越大。
- 状态管理:
- 任务可以处于不同的状态,如 运行(Running)、就绪(Ready)、阻塞(Blocked) 和 挂起(Suspended)。
- 堆栈:
- 每个任务都需要单独的堆栈来存储其运行时的上下文信息。
3.3 FreeRTOS 任务的生命周期
- 创建任务:
- 使用
xTaskCreate
或xTaskCreatePinnedToCore
创建一个任务,并指定任务函数、名称、堆栈大小、优先级等。
- 使用
- 任务调度:
- FreeRTOS 调度器启动后,任务会根据优先级和状态被安排执行。
- 任务状态切换:
- FreeRTOS 会根据任务的运行情况、事件触发、延时(
vTaskDelay
)等条件切换任务的状态。
- FreeRTOS 会根据任务的运行情况、事件触发、延时(
- 删除任务:
- 使用
vTaskDelete
删除不再需要的任务,并释放其分配的资源。
- 使用
3.4 FreeRTOS 任务的状态
一个 FreeRTOS 任务可以处于以下几种状态:
- Running(运行中):
- 当前任务正在 CPU 上执行。
- Ready(就绪):
- 任务准备好运行,等待 CPU 分配时间片。
- Blocked(阻塞):
- 任务正在等待某个事件(如延时、信号量、队列消息等)发生。
- Suspended(挂起):
- 任务被暂停,直到另一个任务显式唤醒它。
3.5 FreeRTOS 任务与多线程的区别
FreeRTOS 的任务与传统的操作系统中的线程有一些相似之处,但也有以下区别:
- FreeRTOS 更轻量,适用于资源受限的嵌入式系统。
- 任务的调度是实时性的,优先保证高优先级任务的运行。
- 没有标准的进程/线程隔离机制,任务之间共享整个内存空间。
3.6 常用 FreeRTOS 任务函数
3.6.1 任务创建函数xTaskCreate
()原型和头文件:
原型:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名称
configSTACK_DEPTH_TYPE usStackDepth, // 堆栈大小
void *pvParameters, // 任务参数
UBaseType_t uxPriority, // 任务优先级
TaskHandle_t *pxCreatedTask // 任务句柄
);
参数:
pvTaskCode
:指向任务函数的指针,该函数的原型为void TaskFunction(void *pvParameters)
。pcName
:任务的名称(字符串),主要用于调试,可设置为 NULL。usStackDepth
:任务堆栈的大小(以字为单位,通常是 4 字节一个字)。pvParameters
:传递给任务的参数,可为 NULL。uxPriority
:任务的优先级,数值越大优先级越高。pxCreatedTask
:返回任务的句柄,可用于后续操作(如删除任务),可设置为 NULL。
返回值:
- 如果创建成功,返回
pdPASS
;否则返回errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY
。
头文件:
#include "freertos/FreeRTOS.h" // 包含FreeRTOS头文件
#include "freertos/task.h" // 包含任务管理头文件
3.6.2 任务创建函数xTaskCreatePinnedToCore
()原型和头文件:(仅在 ESP-IDF 中可用)
原型:
BaseType_t xTaskCreatePinnedToCore(
TaskFunction_t pvTaskCode,
const char * const pcName,
const uint32_t usStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask,
const BaseType_t xCoreID
);
参数:
-
pvTaskCode
:指向任务函数的指针,该函数的原型为void TaskFunction(void *pvParameters)
。 -
pcName
:任务的名称(字符串),主要用于调试,可设置为 NULL。 -
usStackDepth
:任务堆栈的大小(以字为单位,通常是 4 字节一个字)。 -
pvParameters
:传递给任务的参数,可为 NULL。 -
uxPriority
:任务的优先级,数值越大优先级越高。 -
pxCreatedTask
:返回任务的句柄,可用于后续操作(如删除任务),可设置为 NULL。 -
xCoreID
:指定运行任务的 CPU 核心,0
或1
(在双核芯片上),可以用tskNO_AFFINITY
表示任何核心。
头文件:
#include "freertos/FreeRTOS.h" // 包含FreeRTOS头文件
#include "freertos/task.h" // 包含任务管理头文件
3.6.3 任务延时函数vTaskDelay
()原型和头文件:
原型:
void vTaskDelay(const TickType_t xTicksToDelay);
参数:
xTicksToDelay
:延时的时钟节拍数,使用宏pdMS_TO_TICKS(ms)
将毫秒转为节拍数。
功能:
- 将当前任务置为阻塞状态,指定时间到达后进入就绪状态。
头文件:
#include "freertos/FreeRTOS.h" // 包含FreeRTOS头文件
#include "freertos/task.h" // 包含任务管理头文件
3.6.4 任务删除函数vTaskDelete
()原型和头文件:
原型:
void vTaskDelete(TaskHandle_t xTaskToDelete);
参数:
xTaskToDelete
:需要删除的任务句柄,传递NULL
表示删除调用该函数的任务本身。
功能:
- 删除指定任务并释放其资源。
头文件:
#include "freertos/FreeRTOS.h" // 包含FreeRTOS头文件
#include "freertos/task.h" // 包含任务管理头文件
3.6.5 暂停任务函数vTaskSuspend
()原型和头文件:
原型:
void vTaskSuspend(TaskHandle_t xTaskToSuspend);
参数:
xTaskToSuspend
:需要挂起的任务句柄,传递NULL
表示挂起调用此函数的任务。
功能:
- 暂停任务的执行,任务进入挂起状态。
头文件:
#include "freertos/FreeRTOS.h" // 包含FreeRTOS头文件
#include "freertos/task.h" // 包含任务管理头文件
3.6.6 恢复挂起任务函数vTaskResume
()原型和头文件:
原型:
void vTaskResume(TaskHandle_t xTaskToResume);
参数:
xTaskToResume
:需要恢复的任务句柄。
功能:
- 将挂起的任务恢复到就绪状态。
头文件:
#include "freertos/FreeRTOS.h" // 包含FreeRTOS头文件
#include "freertos/task.h" // 包含任务管理头文件
3.6.7 返回指定任务优先级函数uxTaskPriorityGet
()原型和头文件:
原型:
UBaseType_t uxTaskPriorityGet(const TaskHandle_t xTask);
参数:
xTask
:需要获取优先级的任务句柄,传递NULL
表示获取调用该函数的任务的优先级。
返回值:
- 返回指定任务的优先级。
头文件:
#include "freertos/FreeRTOS.h" // 包含FreeRTOS头文件
#include "freertos/task.h" // 包含任务管理头文件
3.6.8 设置任务新优先级函数vTaskPrioritySet
()原型和头文件:
原型:
void vTaskPrioritySet(TaskHandle_t xTask, UBaseType_t uxNewPriority);
参数:
xTask
:需要更改优先级的任务句柄,传递NULL
表示更改调用该函数的任务的优先级。uxNewPriority
:新优先级。
功能:
- 设置任务的新优先级。
头文件:
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
3.6.9 获取当前正在运行任务的句柄函数xTaskGetCurrentTaskHandle
()原型和头文件:
原型:
TaskHandle_t xTaskGetCurrentTaskHandle(void);
功能:
- 获取当前正在运行任务的句柄。
头文件:
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
3.7 任务创建实例
以下是一个使用 FreeRTOS 创建任务的示例程序,该程序适用于 ESP32(使用 ESP-IDF 开发框架)。
3.7.1 示例说明:
- 创建两个任务:
- Task1:每秒打印 “Task 1 Running”
- Task2:每两秒打印 “Task 2 Running”
- 使用
vTaskDelay()
进行任务间的延时 - 在
app_main()
中创建任务
3.7.2 完整代码:
#include <stdio.h> // 包含标准输入输出头文件
#include "freertos/FreeRTOS.h" // 包含FreeRTOS头文件
#include "freertos/task.h" // 包含任务管理头文件
#include "esp_log.h" // 包含日志头文件
// 定义任务句柄(可选,用于后续控制任务)
TaskHandle_t Task1Handle = NULL;
TaskHandle_t Task2Handle = NULL;
// 任务1:每秒打印一次
void Task1(void *pvParameters) {
while (1) {
ESP_LOGI("Task1", "Task 1 Running");
vTaskDelay(pdMS_TO_TICKS(1000)); // 1000ms (1秒)
}
}
// 任务2:每两秒打印一次
void Task2(void *pvParameters) {
while (1) {
ESP_LOGI("Task2", "Task 2 Running");
vTaskDelay(pdMS_TO_TICKS(2000)); // 2000ms (2秒)
}
}
// FreeRTOS 主程序
void app_main(void) {
ESP_LOGI("Main", "Starting FreeRTOS Tasks");
// 创建任务1
xTaskCreate(
Task1, // 任务函数
"Task1", // 任务名称
2048, // 任务堆栈大小(单位:字)
NULL, // 任务参数
1, // 任务优先级
&Task1Handle // 任务句柄
);
// 创建任务2
xTaskCreate(
Task2, // 任务函数
"Task2", // 任务名称
2048, // 任务堆栈大小
NULL, // 任务参数
2, // 任务优先级(比Task1高)
&Task2Handle // 任务句柄
);
}
3.7.3 代码解析:
- 头文件:
#include "freertos/FreeRTOS.h"
→ FreeRTOS 核心库#include "freertos/task.h"
→ 任务管理库#include "esp_log.h"
→ ESP-IDF 日志库(仅适用于 ESP32)#include <stdio.h>
→ 标准输入输出库(通用)
- 任务
Task1
和Task2
:ESP_LOGI("Task1", "Task 1 Running");
→ 通过ESP_LOGI
记录日志vTaskDelay(pdMS_TO_TICKS(1000));
→ 任务休眠 1 秒
- 创建任务
xTaskCreate()
:- 参数:
- 任务函数(
Task1
或Task2
) - 任务名称(字符串)
- 堆栈大小(
2048
,通常足够) - 任务参数(这里传
NULL
) - 优先级(
1
和2
,优先级2
比1
高) - 任务句柄(可用于管理任务)
- 任务函数(
- 参数:
- ESP-IDF 任务调度:
app_main()
是 ESP32 的入口函数,相当于main()
。xTaskCreate()
创建任务后,FreeRTOS 任务调度开始运行。
3.7.4 运行效果:
当程序运行时,串口日志输出如下:
I (1000) Task1: Task 1 Running
I (2000) Task2: Task 2 Running
I (2000) Task1: Task 1 Running
I (4000) Task2: Task 2 Running
I (4000) Task1: Task 1 Running
...
说明:
Task1
每 1 秒打印一次Task2
每 2 秒打印一次
通过任务的使用,FreeRTOS 可以在单片机中实现并发处理和实时响应。它非常适合需要同时管理多个任务(如传感器读取、通信协议处理、UI 更新等)的嵌入式项目。
四、FreeRTOS队列
FreeRTOS 队列(Queue) 是一种 任务间通信机制,允许任务之间或者任务与中断服务例程(ISR)之间安全地交换数据。队列可以存储不同数据类型,并支持先进先出(FIFO)的存取方式。
4.1 FreeRTOS 队列的特点
✅ 任务间通信:用于不同任务之间传递数据。
✅ 中断安全:可以从中断服务例程(ISR)中发送或接收数据(需使用特定 API)。
✅ 同步机制:队列可以用作同步工具,任务可以阻塞等待数据。
✅ 数据缓存:允许多个数据项排队等待接收。
4.2 常用 FreeRTOS 队列函数
4.2.1 创建队列函数xQueueCreate
()原型和头文件:
原型:
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize);
✅ 功能:创建一个新的队列,并返回该队列的句柄。
🔹 参数说明:
uxQueueLength
:队列最多可容纳的元素个数。uxItemSize
:队列中每个元素的大小(字节)。
🔹 返回值:
- 成功:返回队列句柄(
QueueHandle_t
)。 - 失败:返回
NULL
(可能是内存不足)。
🔹 头文件:
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
🔹 示例:
QueueHandle_t myQueue = xQueueCreate(5, sizeof(int)); // 创建存储 5 个整数的队列
4.2.2 发送数据到队列函数xQueueSend
()原型和头文件:
原型:
BaseType_t xQueueSend(QueueHandle_t xQueue, const void * pvItemToQueue, TickType_t xTicksToWait);
✅ 功能:将数据放入队列(如果队列已满,可选择阻塞等待)。
🔹 参数说明:
xQueue
:目标队列的句柄。pvItemToQueue
:指向要发送数据的指针。xTicksToWait
:如果队列已满,阻塞的时间(单位:tick,portMAX_DELAY
表示无限等待)。
🔹 返回值:
pdPASS
(成功)。errQUEUE_FULL
(队列已满,超时未能放入数据)。
🔹 头文件:
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
🔹 示例:
int value = 100;
xQueueSend(myQueue, &value, portMAX_DELAY);
4.2.3 发送数据到队列(中断安全版本)函数xQueueSendFromISR
()原型和头文件:
在 FreeRTOS 中,普通的 xQueueSend()
不能在中断服务程序(ISR) 中使用。要在 ISR(中断服务程序)中安全地向队列发送数据,需要使用中断安全版本 API。
原型:
BaseType_t xQueueSendFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
✅ 功能:
🔹 在 ISR(中断服务程序)中安全地将数据发送到队列。
🔹 如果队列已满,数据不会被添加,并立即返回。
🔹 参数说明:
xQueue
:目标队列的句柄(必须已创建)。pvItemToQueue
:指向要发送数据的指针(数据的大小由xQueueCreate()
设定)。pxHigherPriorityTaskWoken
:指向一个变量,如果任务切换应该发生,该变量会被设置为pdTRUE
。
🔹 返回值:
pdPASS
数据成功放入队列。errQUEUE_FULL
(队列已满,超时未能放入数据)。
🔹 头文件:
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
4.2.4 从队列接收数据函数xQueueReceive
()原型和头文件:
原型:
BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait);
✅ 功能:从队列接收数据(如果队列为空,可选择阻塞等待)。
🔹 参数说明:
xQueue
:目标队列的句柄。pvBuffer
:用于存储接收数据的指针。xTicksToWait
:如果队列为空,阻塞的时间(单位:tick,portMAX_DELAY
表示无限等待)。
🔹 返回值: