FreeRTOS 核心模块初步学习总结与优化
前言
本文档是本人在参考 B站 UP 主 Michael_ee 的视频教程进行学习时所做的学习笔记,在学习过程中,可能存在疏漏或错误之处,若有发现,敬请各位读者指正
这篇博客对之前发布的 FreeRTOS 部分内容进行了总结和修正,涵盖了 Task、Queue、Software Timer、Semaphore、Event、Notification、Stream Buffer 和 Message Buffer 等重要模块,在总结过程中修正了自己作为初学者时曾犯的错误,并对文档进行了格式整理,删去了不必要的内容,力求让内容更加简洁、易懂,便于参考
文章目录
- Introduction
- Why use FreeRTOS?
- The key features of FreeRTOS
- Time Slice
- VScode Development Environment Configuration
- The Meaning of FreeRTOS Function Name Prefixes
- FreeRTOS Startup Process
- Task
- Task Creation and Deletion
- xTaskCreate()
- vTaskDelete()
- Example Code:Task Creation and Deletion
- xTaskCreatePinnedToCore()
- Example Code:Creating Tasks Bound to Specific Cores
- Task Parameters
- Integer Type
- Array
- Struct
- String
- Task Priority
- Priority define
- uxTaskPriorityGet()
- vTaskPrioritySet()
- Example Code:Priority Query and Modify
- Same Priority
- Example Code:Same Priority Tasks
- Different Priority
- Example Code:Different Priority Tasks
- Modify Priority
- Task Suspend and Resume
- Task States
- vTaskSuspend() and vTaskResume()
- Example Code: Suspending and Resuming Tasks
- vTaskSuspendAll() and xTaskResumeAll()
- Example Code: Suspending and Resuming the Scheduler
- Task List
- The Role of the Task List
- vTaskList()
- Example Code: Displaying Task States
- Task Stack Setting
- usStackDepth
- uxTaskGetStackHighWaterMark()
- Example Code: Monitoring Stack Usage
- Task Watch Dog
- Interrupt Watchdog Timer (IWDT)
- Task Watchdog Timer (TWDT)
- Core functions
- Usage
- Configuration
- Example Code: Using TWDT to Monitor Task Execution
- esp\_task\_wdt\_add() and esp\_task\_wdt\_reset()
- Example Code: Adding and Resetting Tasks in TWDT
- Queue
- Queue Delivery Data
- Creating a Queue
- xQueueCreate()
- Sending Data to a Queue
- xQueueSend(), xQueueSendToFront(),xQueueSendToBack()
- Receiving Data from a Queue
- xQueueReceive()
- Checking Queue Parameters
- uxQueueMessagesWaiting()
- Example Code: Working with FreeRTOS Queues
- Example Code:Queue Multiple In Single Out
- Queue Set
- xQueueCreateSet()
- xQueueAddToSet()
- xQueueSelectFromSet()
- Example Code:Queue Sets in FreeRTOS
- Queue Mailbox
- xQueueOverwrite()
- xQueuePeek()
- Example Code:Implementing a Shared Mailbox with Multiple Readers and One Writer
- Software Timer
- Creating a Software Timer
- xTimerCreate()
- Managing Software Timers
- xTimerStart()
- xTimerStop()
- pcTimerGetName()
- pvTimerGetTimerID()
- xTimerReset()
- xTimerChangePeriod()
- Example Code:Using Software Timers
- Semaphore
- Binary Semaphore
- xSemaphoreCreateBinary()
- xSemaphoreGive()
- xSemaphoreTake()
- Example Code: Binary Semaphore for Task Synchronization
- Count Semaphore
- xSemaphoreCreateCounting()
- uxSemaphoreGetCount()
- Example Code: Counting Semaphore for Parking Lot Management
- Mutex
- xSemaphoreCreateMutex()
- Example Code:Mutex Synchronization with Task Priorities in FreeRTOS
- Recursive Mutex
- xSemaphoreCreateRecursiveMutex()
- xSemaphoreTakeRecursive()
- xSemaphoreGiveRecursive()
- Example Code:Recursive Mutex Synchronization Between Tasks in FreeRTOS
- Event Group
- Event Group Wait
- xEventGroupCreate()
- xEventGroupSetBits()
- xEventGroupWaitBits()
- Example Code:Event Group Synchronization with Multiple Tasks
- Event Group Sync
- xEventGroupSync()
- Example Code:Event Group Synchronization
- Notification
- Notification Sync
- xTaskNotifyGive()
- ulTaskNotifyTake()
- Example Code:Simple Task Notification in FreeRTOS
- Notification Value
- xTaskNotify()
- xTaskNotifyWait()
- Example Code:Task Notification with Conditional Actions Based on Values
- Stream Buffer
- xStreamBufferCreate()
- xStreamBufferSend()
- xStreamBufferReceive()
- xStreamBufferReset()
- vStreamBufferDelete()
- Example Code:Stream Buffer with Task Communication in FreeRTOS
- xStreamBufferSpacesAvailable()
- Example Code:Buffer Space Monitoring with Stream Buffer in FreeRTOS
- Message Buffer
- xMessageBufferCreate()
- xMessageBufferSend()
- xMessageBufferReceive()
- xMessageBufferReset()
- vMessageBufferDelete()
- Example Code:Message Buffer Communication with Multiple Tasks
参考资料
Michael_ee 视频教程
freeRTOS 官网
espressif 在线文档
Introduction
Why use FreeRTOS?
- 免费、开源项目
- 是一种操作系统,便于分工、测试、复用代码
- 可以更具任务要求分配优先级
- 系统占用小且简单,仅需
task.c queue.c list.c
- 已移植到多平台
The key features of FreeRTOS
- 系统具有可确定性,即系统能对最好和最坏等情况做出精准的预测
- 由于性能限制,FreeRTOS 只提供核心的实时调度功能、任务间通信、定时和同步功能
Time Slice
时间片(Time slice 或 Time quantum)是抢占式多任务操作系统中的一个概念,指的是操作系统分配给每个进程或任务的固定执行时间
每个任务在执行时间达到时间片的长度后,操作系统会进行任务切换,将CPU的控制权交给下一个任务
VScode Development Environment Configuration
基于 VScode 中的 ESP-IDF 插件进行开发 安装插件->下载 ESP-IDF->下载工具包->通过 Hello_World 例程测试环境 在 ESP-IDF 中查看示代码
>ESP-IDF:Shwo Example....
The Meaning of FreeRTOS Function Name Prefixes
函数名前缀的缩写通常用来表示函数的返回类型或功能类别,以下为具体意义
-
v
(void)- 示例:
vTaskDelete(TaskHandle_t xTask);
- 解释:删除指定的任务,且无返回值
- 示例:
-
x
(BaseType_t)- 示例:
xTaskCreate(TaskFunction_t pvTaskCode, const char * const pcName, configSTACK_DEPTH_TYPE usStackDepth, void *pvParameters, UBaseType_t uxPriority, TaskHandle_t *pxCreatedTask);
- 解释:创建任务,返回
pdPASS
(成功)或errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY
(失败)
- 示例:
-
ul
(unsigned long)- 示例:
ulTaskNotifyTake(BaseType_t xClearCountOnExit, TickType_t xTicksToWait);
- 解释:等待任务通知,返回一个
unsigned long
类型的通知计数
- 示例:
-
ux
(UBaseType_t)- 示例:
uxTaskPriorityGet(TaskHandle_t xTask);
- 解释:获取指定任务的优先级,返回类型为无符号整数
UBaseType_t
- 示例:
-
pc
(pointer to char)- 示例:
pcTaskGetName(TaskHandle_t xTask);
- 解释:返回任务的名称字符串,返回类型为
char*
- 示例:
-
e
(enum)- 示例:
eTaskGetState(TaskHandle_t xTask);
- 解释:返回任务的状态(例如
eRunning
、eReady
等),类型为枚举eTaskState
- 示例:
FreeRTOS Startup Process
- 一级 (ROM) 引导加载程序被固化在了 ESP32 内部的 ROM 中(只读) ,它会从 flash 的 0x1000 偏移地址处加载二级引导加载程序至 RAM (IRAM & DRAM) 中
- 二级引导加载程序从 flash 中加载分区表和主程序镜像至内存中(可更改),主程序中包含了 RAM 段和通过 flash 高速缓存映射的只读段
- 应用程序启动阶段运行,这时第二个 CPU 和 RTOS 调度器启动,接着运行
main_task
,从而执行app_main
(应用程序启动包含了从应用程序开始执行到app_main
函数在主任务内部运行前的所有过程)
Task
在 FreeRTOS 中,Task(任务)是操作系统中最基本的执行单元
每个任务代表一个独立的执行线程,具有自己的堆栈和上下文,任务可以并发执行,系统通过调度器管理任务的执行顺序;每个任务都有一个优先级,任务优先级高的会优先获得 CPU 时间片;任务可以是阻塞的、就绪的或挂起的,取决于它们的状态;任务间通过 队列、信号量、消息缓冲区 等机制进行通信和同步
任务创建时由 xTaskCreate() 函数分配资源,并由调度器在系统运行时调度执行
Task Creation and Deletion
xTaskCreate()
创建一个任务
- 每个任务都需要一定的 RAM 来保存任务状态(即任务控制块,TCB)和任务堆栈
- 如果使用
xTaskCreate()
函数创建任务,那么所需的 RAM 将自动从 FreeRTOS 的堆中分配 - 新创建的任务初始状态为“就绪(Ready)”状态,如果当前没有更高优先级的任务可以运行,那么这个新任务将立即进入“运行(Running)”状态
- 任务可以在调度器(调度器是一个核心组件,负责管理和控制多个任务的执行顺序,以确保系统的实时性和响应能力,它依据任务的优先级和状态(就绪、运行、阻塞等),动态地分配 CPU 给不同的任务,以实现多任务并行执行的效果)启动之前或之后创建
#include "FreeRTOS.h"
#include "task.h"
//创建任务的新实例
BaseType_t xTaskCreate(TaskFunction_t pvTaskCode,
const char* const pcName,
unsigned short usStackDepth,
void* pvParameters,
UBaseType_t usPriority,
TaskHandle_t* pxCreatedTask);
参数
pvTaskCode
任务(Tasks),本质上就是不会退出的 C 函数,通常实现为一个无限循环,pvTaskCode 参数是指向实现该任务的函数(函数名)的指针
pcName
任务名称,每个任务都可以指定一个描述性名称,主要用于调试
usStackDepth
指定堆栈深度,即堆栈可以容纳的字数(words)而不是字节数(bytes) (不同平台的字长(word size)不同,这意味着最终分配的堆栈大小会因架构而异,例如,在堆栈宽度为4字节的架构上,如果 usStackDepth 作为 100 传入,则将分配 400 字节的堆栈空间(100 * 4字节))
pvParameters
传入任务的参数,此参数的类型为“指向 void 的指针”,以允许任务参数有效地、并通过强制转换间接地接收任何类型的参数
uxPriority
任务优先级,优先级可以从最低优先级 0 分配到最高优先级(configMAX_PRIORITIES-1),configMAX_PRIORITIES 是一个用户定义的常量
高优先级任务会优先占用 CPU 资源,而低优先级任务可能因“抢占式调度”机制被暂停执行,以便高优先级任务运行;如果有多个相同优先级的任务在就绪状态,调度器会轮流执行它们(时间片轮转) ,确保公平
pxCreatedTask
任务句柄,将句柄传递给正在创建的任务的句柄,这个句柄可以用于引用 API 调用中的任务
在编程中,句柄(Handle)是一种抽象的引用,用于标识和访问操作系统或库中的资源或对象,而不直接暴露该资源的内部细节, 它通常是一个指针或整数,通过该引用可以对资源进行操作
在 FreeRTOS 中,句柄广泛用于引用各种内核对象,例如任务、队列、信号量、互斥量等;通过句柄,应用程序可以执行相关操作(如暂停、删除任务,或向队列发送消息),而无需了解对象的内部实现
如: TaskHandle_t:用于任务的句柄 QueueHandle_t:用于队列的句柄 SemaphoreHandle_t:用于信号量的句柄 TimerHandle_t:用于定时器的句柄
vTaskDelete()
用于删除创建的任务
-
被删除的任务不再存在,因此无法进入“运行”状态,也不能再通过任务句柄引用该任务
-
当一个任务被删除后,系统会通过空闲任务来释放与该任务相关的内存(包括堆栈和任务控制块)因此,如果系统使用了
vTaskDelete()
,就必须确保空闲任务有足够的处理时间去完成这个清理过程- 如果空闲任务被“饿死”(即没有获得运行时间),那么内存将无法被释放,从而可能导致内存泄漏
在 FreeRTOS 中,空闲任务是一个低优先级的后台任务,其职责之一就是回收被删除任务的内存
因此,如果有频繁创建和删除任务的需求,确保空闲任务有足够的 CPU 时间至关重要
#include "FreeRTOS.h"
#include "task.h"
void vTaskDelete(TaskHandle_t pxTask);
参数
pxTask
被删除任务的句柄,任务可以通过传递 NULL 来代替有效的任务句柄来删除自身(即在 Task 中使用 vTaskDelete(NULL);
)
Example Code:Task Creation and Deletion
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
void myTask(void *pvParam)
{
while (true)
{
printf("Hello world!\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);//任务延时 1 秒
}
}
void app_main(void)
{
TaskHandle_t myHandle = NULL;//创建句柄,用于引用新任务
//创建任务,将任务句柄传入 myHandle 中
xTaskCreate(myTask, "myTask1", 1024, NULL, 1, &myHandle);//正确传入 &myHandle,xTaskCreate函数会给myHandle赋值
vTaskDelay(2000 / portTICK_PERIOD_MS);//主任务延时 2 秒
if (myHandle != NULL)
vTaskDelete(myHandle); // 通过句柄删除 myTask
}
xTaskCreatePinnedToCore()
用于在多核环境(如 ESP32)中创建任务,并将任务绑定到指定的核心上运行
BaseType_t xTaskCreatePinnedToCore(
TaskFunction_t pvTaskCode,
const char *const pcName,
const uint32_t usStackDepth,
void *const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *const pxCreatedTask,
const BaseType_t xCoreID
);
参数
xCoreID
指定任务绑定的核心
-
可选值
-
0
:绑定到核心0(PRO_CPU) -
1
:绑定到核心1(APP_CPU) -
tskNO_AFFINITY
:不绑定特定核心,任务可以在任何核心上运行 - 超过
portNUM_PROCESSORS - 1
(如ESP32
是1
)会导致任务创建失败
-
Example Code:Creating Tasks Bound to Specific Cores
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
// 核心0上的任务
void task_core0(void *pvParameters)
{
while (1)
{
printf("Task running on Core 0\n");
vTaskDelay(1000 / portTICK_PERIOD_MS); // 延时1秒
}
}
// 核心1上的任务
void task_core1(void *pvParameters)
{
while (1)
{
printf("Task running on Core 1\n");
vTaskDelay(1000 / portTICK_PERIOD_MS); // 延时1秒
}
}
void app_main()
{
// 创建任务绑定到核心0
xTaskCreatePinnedToCore(
task_core0, // 任务函数
"TaskCore0", // 任务名称
2048, // 栈大小(以字节为单位)
NULL, // 任务参数
1, // 任务优先级
NULL, // 任务句柄
0 // 核心ID(0)
);
// 创建任务绑定到核心1
xTaskCreatePinnedToCore(
task_core1, // 任务函数
"TaskCore1", // 任务名称
2048, // 栈大小(以字节为单位)
NULL, // 任务参数
1, // 任务优先级
NULL, // 任务句柄
1 // 核心ID(1)
);
}
Task Parameters
任务函数接受一个类型为“指向无类型的指针” (void*
)的参数
赋给 pvParameters
的值将是传递给任务的值,这个参数的类型为“指向无类型的指针”,旨在允许任务参数有效地接收任何类型的参数,且可以通过类型转换间接地实现(例如,可以通过在创建任务时将整数类型转换为无类型指针,然后在任务函数定义中将无类型指针参数转换回整数类型,从而将整数类型传递给任务函数),这里传参的本质都是传递地址
Integer Type
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
void myTask(void *pvPamra)
{
int *pInt = (int*)pvPamra;
printf("I got a test number %d.\n", *pInt);
vTaskDelay(1000 / portTICK_PERIOD_MS);
vTaskDelete(NULL);
}
int testNumber = 5;
void app_main(void)
{
xTaskCreate(myTask, "myTask", 2048, (void*)&testNumber, 1, NULL);//此处需要比较大的堆栈,否则会触发看门狗
Array
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
void myTask(void *pvPamra)
{
int *pArr = (int*)pvPamra;
for(int i = 0; i < 3; i++)
{
printf("I got a test number %d.\n", pArr[i]);
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
vTaskDelete(NULL);
}
int testArray[] = {6, 7, 8};
void app_main(void)
{
xTaskCreate(myTask, "myTask", 2048, (void*)testArray, 1, NULL);//此处需要比较大的堆栈,否则会触发看门狗
}
Struct
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
typedef struct a
{
int m_num1;
int m_num2;
} xStruct;
xStruct xStrTest = {6, 9};
void myTask(void *pvPamra)
{
xStruct* pStr = (xStruct*)pvPamra;
printf("I got a %d.\n", pStr->m_num1);
printf("I got a %d.\n", pStr->m_num2);
vTaskDelay(1000 / portTICK_PERIOD_MS);
vTaskDelete(NULL);
}
void app_main(void)
{
xTaskCreate(myTask, "myTask", 2048, (void*)&xStrTest, 1, NULL);//此处需要比较大的堆栈,否则会触发看门狗
}
String
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
static const char *pStr = "Hello World!";
void myTask(void *pvPamra)
{
char* pStr = (char*)pvPamra;
printf("I got a string:%s.\n", pStr);
vTaskDelay(1000 / portTICK_PERIOD_MS);
vTaskDelete(NULL);
}
void app_main(void)
{
xTaskCreate(myTask, "myTask", 2048, (void*)pStr, 1, NULL);//此处需要比较大的堆栈,否则会触发看门狗
}
Task Priority
Priority define
每个任务的优先级从0到 (configMAX_PRIORITIES - 1)
,其中 configMAX_PRIORITIES
是在 FreeRTOSConfig.h
中定义的最大优先级数,低优先级数字表示低优先级任务
- 空闲任务的优先级为零(tskIDLE_PRIORITY)
文件路径
"idf 安装路径"\idf\v5.3.1\esp-idf\components\freertos\config\include\freertos
总共 25 个优先级别(可被修改)(ESP32_WROON 在这里设置过高的优先级(25 及以上)会触发看门狗,err:assert failed: prvInitialiseNewTask tasks.c:1088 (uxPriority < ( 25 )),assert
断言失败了,原因是提供给 prvInitialiseNewTask
函数的 uxPriority
参数超出了允许的范围)
如果 configUSE_PORT_OPTIMISED_TASK_SELECTION 设置为0,则可用的优先级数量没有上限(除了所使用的数据类型和微控制器中可用的RAM的限制),但建议使用所需的最低优先级数量,以避免
浪费RAM
uxTaskPriorityGet()
查询分配给任务的优先级
#include "FreeRTOS.h"
#include "task.h"
UBaseType_t uxTaskPriorityGet( TaskHandle_t pxTask );
参数
pxTask
任务的句柄
vTaskPrioritySet()
修改目标任务的优先级
#include "FreeRTOS.h"
#include "task.h"
void vTaskPrioritySet( TaskHandle_t pxTask, UBaseType_t uxNewPriority );
参数
pxTask
任务的句柄
uxNewPriority
修改的目标优先级
Example Code:Priority Query and Modify
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
static const char *pStr = "Hello World!";
void myTask(void *pvPamra)
{
char *pStr = (char *)pvPamra;
printf("I got a string:%s.\n", pStr);
vTaskDelay(1000 / portTICK_PERIOD_MS);
vTaskDelete(NULL);
}
void app_main(void)
{
UBaseType_t iPriority = 0;
TaskHandle_t myHandle = NULL;
xTaskCreate(myTask, "myTask", 4096, (void *)pStr, 24, &myHandle);
iPriority = uxTaskPriorityGet(myHandle);
printf("iPriority = %d\n", iPriority); // 输出24
vTaskPrioritySet(myHandle, 2);
iPriority = uxTaskPriorityGet(myHandle);
printf("iPriority = %d\n", iPriority); // 输出2
}
Same Priority
任何数量的任务可以共享同一优先级,谁先创建谁先执行,如果没有定义 configUSE_TIME_SLICING
,或将其设置为 1
,那么处于“就绪”状态且优先级相同的任务会通过时间片轮转的方式共享 CPU 时间
当 configUSE_TIME_SLICING
被设置为 0
时,同一优先级的多个任务不会自动共享 CPU 时间,表现为:
- 一旦某个同优先级任务开始运行,除非该任务主动让出 CPU(例如通过调用
taskYIELD()
或等待事件、延时等),否则它将一直运行 - 这种模式下,同优先级任务需要手动控制让出 CPU,适合特定应用场景(例如实时控制系统),以保证任务间的精确调度和更高的确定性
Example Code:Same Priority Tasks
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
void myTask(void *pvPamra)
{
while(true)
{
printf("Task1 is running.\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void myTask2(void *pvParam)
{
while(true)
{
printf("Task2 is running.\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void app_main(void)
{
TaskHandle_t myHandle = NULL;
TaskHandle_t myHandle2 = NULL;
xTaskCreate(myTask, "myTask", 4096, NULL, 1, &myHandle);
xTaskCreate(myTask2, "myTask2", 4096, NULL, 1, &myHandle2);
}
// 轮流打印"Task1/2 is running."
Different Priority
当多个任务处于“就绪”状态时,优先级最高的任务会优先进入“运行”状态,调度器会始终将优先级最高且能够运行的任务置于运行状态
Example Code:Different Priority Tasks
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
void myTask(void *pvPamra)
{
while(true)
{
printf("Task1 is running.\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void myTask2(void *pvParam)
{
while(true)
{
printf("Task2 is running.\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void app_main(void)
{
TaskHandle_t myHandle = NULL;
TaskHandle_t myHandle2 = NULL;
xTaskCreate(myTask, "myTask", 4096, NULL, 1, &myHandle);
xTaskCreate(myTask2, "myTask2", 4096, NULL, 2, &myHandle2);
}
// 这里先输出 Task1 is running.
// 随后输出 Task2 is running.
// 随后输出 Task2 is running.
// 随后输出 Task1 is running.
// 随后交替输出 Task2/1 is running.
可能的原因:
- 任务的创建顺序:
app_main
中先创建了Task1
,再创建了Task2
;虽然Task2
优先级较高,但由于任务刚创建时还未正式进入调度器运行状态,因此Task1
在此时可能会先开始运行 - 调度延迟:创建任务后,任务的调度需要一些时间才能生效,
Task1
可能在Task2
被完全启动前获得了 CPU 时间
Modify Priority
使用vTaskPrioritySet()
修改优先级,用于升级或降级
Task Suspend and Resume
Task States
Running
当任务正在执行时,称其处于“运行中”状态,此时任务正在使用处理器资源,若系统只有单核处理器,则在任何时间点只能有一个任务处于“运行中”状态
Ready
处于“就绪”状态的任务可以被执行,但因另一个优先级更高或相等的任务处于运行中状态,暂时未被执行
Blocked
当任务在等待某个时间或外部事件时,它会进入“阻塞”状态,例如,调用 vTaskDelay()
时,任务会被阻塞直到延迟时间到达(时间事件);任务也可以因等待队列、信号量、事件组或通知而被阻塞,通常具有一个“超时”期限;阻塞状态的任务不会消耗处理器资源,且不能被选为运行任务
Suspended
与阻塞状态类似,挂起状态的任务也不会被选为运行任务,挂起任务没有超时,只能通过 vTaskSuspend()
和 xTaskResume()
API调用显式地进入或退出挂起状态
vTaskSuspend() and vTaskResume()
挂起/恢复指定 Task
#include "FreeRTOS.h"
#include "task.h"
void vTaskSuspend( TaskHandle_t pxTaskToSuspend );// 注意这里是直接传Handle,不是传指针
// ---------------------------------------
#include "FreeRTOS.h"
#include "task.h"
void vTaskResume( TaskHandle_t pxTaskToResume );
参数
pxTaskToSuspend
要挂起的任务的句柄
- 使用任务的名称调用
xTaskGetHandle()
来获取句柄 - 任务可以通过将
NULL
传入而不是有效任务句柄的方式挂起自己
Example Code: Suspending and Resuming Tasks
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
void myTask(void *pvPamra)
{
while(true)
{
printf("Task1 is running.\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
//vTaskSuspend(NULL);// Task内部挂起自己
}
}
void myTask2(void *pvParam)
{
while(true)
{
printf("Task2 is running.\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void app_main(void)
{
TaskHandle_t myHandle = NULL;
TaskHandle_t myHandle2 = NULL;
xTaskCreate(myTask, "myTask", 4096, NULL, 1, &myHandle);
xTaskCreate(myTask2, "myTask2", 4096, NULL, 2, &myHandle2);
vTaskDelay(3000 / portTICK_PERIOD_MS);
vTaskSuspend(myHandle);// 主程序挂起Task1,此时只剩任务2在打印
vTaskDelay(3000 / portTICK_PERIOD_MS);
vTaskResume(myHandle);// 主程序恢复Task1
}
vTaskSuspendAll() and xTaskResumeAll()
挂起/恢复调度器,可用于运行不希望被别的任务打扰的任务时
#include "FreeRTOS.h"
#include "task.h"
void vTaskSuspendAll( void );
// ---------------------------------------
#include "FreeRTOS.h"
#include "task.h"
BaseType_t xTaskResumeAll( void );
vTaskSuspendAll()
用于挂起 FreeRTOS 调度器(scheduler),挂起调度器会阻止任务切换的发生,但不会禁用中断
- 如果在挂起调度器期间中断请求进行任务切换,该请求会被暂时搁置,直到调度器恢复后才会执行
- 在调度器挂起期间,不能调用其他 FreeRTOS API 函数,但是可以调用自己编写的函数,要注意看门狗(记得“喂狗”)
xTaskResumeAll()
只能从执行中的任务中调用,它不能在调度器启动之前调用
Example Code: Suspending and Resuming the Scheduler
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
void myTask(void *pvPamra)
{
printf("Test begin.\n");
vTaskSuspendAll();// 挂起调度器,此时Task2停止打印
for(int i = 0; i < 9999; i++)// 数值不能过大,否则有可能触发看门狗【E (5313) task_wdt: Task watchdog got triggered. The following tasks/users did not reset the watchdog in time】
{
for(int j = 0; j < 999; j++)
{
;// 空循环,占用一些CPU时间,模拟计算
}
}
xTaskResumeAll();// 恢复调度器,Task2恢复打印
vTaskDelay(1000 / portTICK_PERIOD_MS);
printf("Test end.\n");
vTaskDelete(NULL);
}
void myTask2(void *pvParam)
{
while(true)
{
printf("Task2 is running.\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void app_main(void)
{
TaskHandle_t myHandle = NULL;
TaskHandle_t myHandle2 = NULL;
xTaskCreate(myTask, "myTask", 4096, NULL, 1, &myHandle);// 两个Task设置为相同优先级
xTaskCreate(myTask2, "myTask2", 4096, NULL, 1, &myHandle2); // 在Task1模拟计算的过程中,在不挂起调度器时,由于调度器的调度,Task2会不断进行打印
}
Task List
Task List 是一个保存所有任务信息的数据结构,用于管理和跟踪系统中所有任务的状态,任务列表分为多个队列,每个队列存储不同状态的任务,比如“就绪”、“阻塞”、“挂起”等,FreeRTOS 通过这些列表实现任务调度和状态管理
The Role of the Task List
-
任务调度
- 调度器根据任务列表中的信息决定哪个任务应被调度执行,任务状态改变时,调度器会更新任务列表
-
任务状态跟踪
- 系统通过任务列表了解每个任务的状态(例如运行、就绪、阻塞或挂起),从而确保任务在合适的状态下被调度
-
任务优先级管理
- 任务列表按优先级存储任务,FreeRTOS 会优先调度高优先级任务,因此高优先级任务通常排在任务列表的前面
-
资源管理
- 当任务等待某些资源(例如队列、信号量或事件)时,任务会进入“阻塞”状态并被放入相应的阻塞列表中,等待条件满足后再回到就绪列表
常见的任务列表类型
- 就绪任务列表:存储所有准备运行的任务
- 延时任务列表:存储被延迟的任务(例如调用
vTaskDelay()
的任务) - 阻塞任务列表:存储等待事件(如队列、信号量等)的任务
vTaskList()
用于生成一个包含当前系统中所有任务状态的表格,并将表格数据写入指定的字符缓冲区,显示了每个任务的状态、优先级、堆栈使用情况等信息
#include "FreeRTOS.h"
#include "task.h"
void vTaskList( char *pcWriteBuffer );
参数
pcWriteBuffer
存储表格文本的缓冲区,该缓冲区必须足够大,否则可能导致溢出
注意
- 使用前需要在
FreeRTOSConfig.h
中将configUSE_TRACE_FACILITY
和configUSE_STATS_FORMATTING_FUNCTIONS
设置为 1 vTaskList()
是一个工具函数,不是内核的一部分-
vTaskList()
会在执行期间禁用中断,可能不适合硬实时应用
在实际使用中,可以通过 VsCode 中的插件提供的图形化界面进行配置,点击 VSCode 左下角的 menuconfig(小齿轮) → Partition Table → FAT Filesystem support → Kernel 中勾选 configUSE_TRACE_FACILITY 和 configUSE_STATS_FORMATTING_FUNCTIONS(勾选第一个以后会出现第二个)
Example Code: Displaying Task States
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
void myTask(void *pvPamra)
{
printf("Test begin.\n");
vTaskSuspendAll();
for (int i = 0; i < 9999; i++)
{
for (int j = 0; j < 999; j++)
{
;
}
}
xTaskResumeAll();
vTaskDelay(1000 / portTICK_PERIOD_MS);
printf("Test end.\n");
vTaskDelete(NULL);
}
void myTask2(void *pvParam)
{
while (true)
{
printf("Task2 is running.\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void app_main(void)
{
TaskHandle_t myHandle = NULL;
TaskHandle_t myHandle2 = NULL;
static char pcWriteBuffer[512] = {0}; // 创建缓冲区
xTaskCreate(myTask, "myTask", 4096, NULL, 1, &myHandle);
xTaskCreate(myTask2, "myTask2", 4096, NULL, 1, &myHandle2);
while (true)
{
vTaskList(pcWriteBuffer); // 传入缓冲区
printf("-------------------------------------------\n");
printf("Name State Priority Stack Num\n"); // 打印Title
printf("%s\n", pcWriteBuffer);
vTaskDelay(3000 / portTICK_PERIOD_MS);
}
}
输出结果:
表格内容
-
Name
任务创建时指定的名称 -
State
任务的当前状态-
X
任务正在执行(即调用vTaskList()
的任务) -
B
任务处于阻塞状态 -
R
任务处于就绪状态 -
S
任务处于挂起状态,或处于无超时的阻塞状态 -
D
任务已被删除,但其内存尚未被空闲任务释放
-
-
Priority
任务的优先级 -
Stack
任务堆栈的“高水位标记”,显示任务生命周期中堆栈的最小可用量,值越接近零,任务越接近堆栈溢出,这个参数用于检测堆栈是否够用 -
Num
任务的唯一编号,用于区分名称相同的任务
Task Stack Setting
在创建任务时,需要设置堆栈大小,过小的堆栈大小会导致任务无法运行,过大则会造成资源浪费
usStackDepth
参数描述
freeRTOS 中的 usStackDepth
参数用于指定任务栈的大小,每个任务在创建时由内核分配一个独立的栈,usStackDepth
表示栈可以容纳的字数(而非字节数) (例如,在一个栈宽为 4 字节的架构中,如果 usStackDepth
设为 100,那么将分配 400 字节的栈空间(100 * 4 字节)),栈深度与栈宽的乘积不能超过 size_t
类型变量能表示的最大值
空闲任务使用的栈大小由应用程序定义的常量 configMINIMAL_STACK_SIZE
来设定,这个常量在特定微控制器架构的示例程序中设为推荐的最小值;若任务需要更多的栈空间,则需要为 usStackDepth
指定更大的值
uxTaskGetStackHighWaterMark()
在FreeRTOS中,每个任务维护自己的栈空间,栈的总大小在任务创建时指定,uxTaskGetStackHighWaterMark()
函数用于查询任务的栈空间使用情况,返回一个称为“高水位标记”的值,用于判断任务在执行过程中栈空间使用是否接近溢出
通过这个函数估计程序栈的大小,相较于 TaskList() 占用资源少
#include "FreeRTOS.h"
#include "task.h"
UBaseType_t uxTaskGetStackHighWaterMark( TaskHandle_t xTask );
参数
xTask
需要查询栈高水位标记的任务句柄
- 若任务查询自身栈高水位标记,可以传递
NULL
作为句柄
返回值
返回任务开始执行以来的最小剩余栈空间量,即栈使用最深时的未使用栈空间,高水位标记越接近零,任务的栈空间越接近溢出
注意
uxTaskGetStackHighWaterMark()
执行时间较长,因此建议仅在测试和调试版本中使用,需要在 FreeRTOSConfig.h
中将 INCLUDE_uxTaskGetStackHighWaterMark
设置为1,才能使用该函数(这个貌似不用设置,但是手册中有写)
Example Code: Monitoring Stack Usage
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
void myTask(void *pvPamra)
{
while (true)
{
printf("Task1 is running.\n");// 这行起不起作用,对iStack影响较大
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void app_main(void)
{
TaskHandle_t myHandle = NULL;
xTaskCreate(myTask, "myTask", 1024, NULL, 1, &myHandle);
UBaseType_t iStack = 0;
while (true)
{
iStack = uxTaskGetStackHighWaterMark(myHandle);
printf("task1 iStack = %d\n", iStack);
vTaskDelay(3000 / portTICK_PERIOD_MS);
}
}
如果堆栈设置太小:ERROR: A stack overflow in task myTask has been detected.
Task Watch Dog
ESP-IDF 支持以下类型的看门狗定时器:
- 中断看门狗定时器 (IWDT)
- 任务看门狗定时器 (TWDT)
IWDT 看门狗负责确保 ISR(中断服务程序)不被长时间阻塞,TWDT 负责检测任务长时间运行而不让步的情况
Interrupt Watchdog Timer (IWDT)
中断看门狗定时器 (IWDT) 的目的是确保中断服务程序 (ISR) 不会长时间被阻止执行,从而避免中断延迟,并保证任务切换正常进行(因为任务切换是通过 ISR 实现的),IWDT 会监控 FreeRTOS 的时钟中断,确保在设定的超时时间内每个 CPU 都能运行时钟中断,如果没有运行,说明有某些情况正在阻止 ISR 的执行,可能导致这种阻塞的原因包括:
- 禁用中断
- 进入关键区段(也会禁用中断)
- 其他相同或更高优先级的 ISR 阻止当前 ISR 的执行
IWDT 的实现是通过定时器组 1 的看门狗定时器,结合 FreeRTOS 在每个 CPU 上的时钟中断来喂狗
如果某个 CPU 的时钟中断未能在 IWDT 超时时间内执行,则看门狗会触发超时;此时,默认行为是调用 panic 处理程序
,并显示超时原因,如 Interrupt wdt timeout on CPU0
或 Interrupt wdt timeout on CPU1
;用户可以通过调试工具(如回溯、OpenOCD、gdbstub)定位超时的原因,或在生产环境中选择直接重启芯片, 如果 panic 处理程序在 IWDT 超时后也未能执行,IWDT 会触发第二阶段超时,导致系统硬重启
Panic 处理程序(Panic Handler)是 ESP-IDF 中的一个机制,用于在系统检测到严重错误(如看门狗超时、非法内存访问等)时触发紧急处理,它的主要作用是收集错误信息,便于调试,并根据配置执行特定的恢复操作,例如重启系统
系统硬重启(Hard Reset 或 Hardware Reset)是指通过硬件的方式将系统直接重启,而不经过正常的关机或软件重启过程;硬重启会立即停止当前所有操作,清除 RAM 中的数据,并重新启动系统; 在嵌入式设备(如 ESP32)中,硬重启通常是为了在系统出现严重错误、无法继续执行时快速恢复系统的正常运行
配置
- 启用 IWDT:IWDT 默认通过
CONFIG_ESP_INT_WDT
选项启用 - 设置超时时间:通过
CONFIG_ESP_INT_WDT_TIMEOUT_MS
配置超时时间,如果启用了 PSRAM 支持,默认超时时间会更高,因为访问大量 PSRAM 的中断或关键区段执行时间较长,建议超时时间至少为 FreeRTOS 时钟周期的两倍
优化建议
如果 IWDT 超时是由于中断或关键区段执行时间过长导致,可以考虑以下优化:
- 缩短关键区段:将非关键的代码和计算放在关键区段外执行,以减少在关键区段内的处理时间
- 简化 ISR:中断处理程序应尽可能少地进行计算,最好将计算任务转移给任务来执行(例如使用队列将数据传递给任务)
- 避免阻塞:关键区段和 ISR 都不应阻塞等待事件发生,如果无法缩短执行时间,可以通过增加
CONFIG_ESP_INT_WDT_TIMEOUT_MS
的值来延长超时时间
Task Watchdog Timer (TWDT)
任务看门狗定时器(TWDT)用于监控特定任务,确保它们能够在设定的超时时间内执行
它主要监控每个 CPU 的空闲任务(Idle Task,缺省状态下监控) ,但也支持其他任务或用户自定义代码订阅;通过监控,TWDT 能够检测任务长时间运行未让出 CPU 的情况,例如死循环或不正确的代码逻辑
Core functions
- 目标: 检测任务未及时让出 CPU 的问题,这可能导致系统卡死
- 机制: 基于硬件看门狗定时器(Hardware Watchdog Timer),当发生超时时触发中断
- 自定义: 用户可定义
esp_task_wdt_isr_user_handler
来处理超时事件 - TWDT 有助于检测并处理无响应或卡死任务,提高系统稳定性
Usage
需要包含
components/esp_system/include/esp_task_wdt.h
#include "esp_task_wdt.h"
-
初始化与监控:
-
esp_task_wdt_init()
初始化 TWDT 并监控空闲任务 -
esp_task_wdt_add()
订阅特定任务以接受 TWDT 监控 -
esp_task_wdt_reset()
在任务中调用以重置 TWDT,防止触发超时
-
-
订阅管理:
-
esp_task_wdt_delete()
取消对某任务的监控 -
esp_task_wdt_deinit()
取消对空闲任务的监控并注销 TWDT
-
Configuration
- 默认超时时间: 通过
CONFIG_ESP_TASK_WDT_TIMEOUT_S
配置,需设置为任务所需最长运行时间 - 动态调整: 可使用
esp_task_wdt_init()
在运行时修改超时时间
Example Code: Using TWDT to Monitor Task Execution
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
void myTask(void *pvPamra)
{
while (true)
{
; // 不断占用
}
}
void app_main(void)
{
TaskHandle_t myHandle = NULL;
xTaskCreate(myTask, "myTask", 1024, NULL, 1, &myHandle);
}
这个程序会导致 TWDT 触发error
分析(来自 GPT-4o):
E (15315) task_wdt: Task watchdog got triggered.
// 错误时间戳为 15315 ms,任务看门狗(TWDT)触发超时,说明至少一个被监控的任务未在指定时间内重置看门狗
E (15315) task_wdt: The following tasks/users did not reset the watchdog in time:
// 以下任务或用户未按时重置看门狗
E (15315) task_wdt: - IDLE0 (CPU 0)
// CPU 0 的空闲任务(IDLE0)未及时运行,从而导致看门狗超时
// 这通常是由于高优先级任务长期占用 CPU,阻塞了空闲任务的运行
E (15315) task_wdt: Tasks currently running:
// 当前正在运行的任务状态
E (15315) task_wdt: CPU 0: myTask
// CPU 0 正在运行用户定义的任务 `myTask`,说明此任务可能长期占用了 CPU,没有及时让出给空闲任务
E (15315) task_wdt: CPU 1: IDLE1
// CPU 1 正常运行其空闲任务(IDLE1),表明问题仅发生在 CPU 0 上
E (15315) task_wdt: Print CPU 0 (current core) backtrace
// 打印 CPU 0 上的回溯信息,以帮助进一步定位问题的代码路径
Backtrace: 0x400D6D6B:0x3FFB0F80 0x400D7130:0x3FFB0FA0 0x40082DD1:0x3FFB0FD0 0x400E207C:0x3FFB5960 0x40085F89:0x3FFB5980
// 回溯路径,显示出错时的函数调用栈
//
// 1. 0x400D6D6B: task_wdt_timeout_handling at task_wdt.c:434
// - `task_wdt_timeout_handling` 是任务看门狗的超时处理函数
// 它被调用以记录超时信息并触发回溯打印
//
// 2. 0x400D7130: task_wdt_isr at task_wdt.c:507
// - `task_wdt_isr` 是任务看门狗中断服务程序,负责在超时时触发中断并调用相应的超时处理函数
//
// 3. 0x40082DD1: _xt_lowint1 at xtensa_vectors.S:1240
// - 这是 FreeRTOS 的低级中断处理程序,处理任务切换和中断管理
//
// 4. 0x400E207C: myTask at hello_world_main.c:17
// - 用户定义的任务 `myTask` 在运行时阻塞了 CPU 0 的空闲任务,导致看门狗触发
// 这里指出 `myTask` 的实现可能存在问题,需进一步检查代码逻辑
//
// 5. 0x40085F89: vPortTaskWrapper at port.c:134
// - FreeRTOS 的任务封装函数,负责调用用户任务并处理任务调度
原因:myTask 优先级是 1,比 Idle Task(0)高,且其中没有阻塞函数,因此不断在运行,而 Idle Task 得不到运行
改进:
- 在 Task 中调用阻塞函数(如 delay),此时 scheduler 可以让低优先级别的 Task 得以运行,此时低优先级 Task 可以“喂狗”
- 改变当前任务的优先级别,此时 scheduler 会自动分配给两个任务相同的时间片
esp_task_wdt_add() and esp_task_wdt_reset()
将一个任务订阅到TWDT,订阅后,该任务需要定期调用 esp_task_wdt_reset()
来防止 TWDT 超时,如果未及时调用,将触发 TWDT 超时
esp_err_t esp_task_wdt_add(TaskHandle_t)
参数
task_handle
: 任务的句柄
- 传入
NULL
时,表示将当前运行的任务订阅到 TWDT
返回值
ESP_OK
成功订阅任务到 TWDT
Others
订阅失败
Example Code: Adding and Resetting Tasks in TWDT
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "esp_task_wdt.h"
void myTask(void *pvPamra)
{
while (true)
{
printf("Task1 is running.\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void myTask2(void *pvPamra)
{
esp_task_wdt_add(NULL);
while (true)
{
printf("Task2 is running.\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
//esp_task_wdt_reset();// 注释掉时,即使优先级相同,依然会触发watch dog
}
}
void app_main(void)
{
TaskHandle_t myHandle = NULL;
xTaskCreate(myTask, "myTask", 1024, NULL, 0, &myHandle);
xTaskCreate(myTask2, "myTask2", 1024, NULL, 0, NULL);
}
error:
E (5315) task_wdt: Task watchdog got triggered.
// 表示任务看门狗定时器(TWDT)触发了超时,因为某些任务没有按时调用 `esp_task_wdt_reset()`
E (5315) task_wdt: The following tasks/users did not reset the watchdog in time:
// 以下是未按时重置看门狗的任务或用户
E (5315) task_wdt: - myTask2 (CPU 0/1)
// 任务 `myTask2` 是导致 TWDT 超时的任务,该任务运行在 CPU 0 或 CPU 1 上,没有按时调用 `esp_task_wdt_reset()`
E (5315) task_wdt: Tasks currently running:
// 列出了当前运行在每个核心上的任务
E (5315) task_wdt: CPU 0: IDLE0
// CPU 0 当前运行的是 IDLE 任务(空闲任务)
E (5315) task_wdt: CPU 1: IDLE1
// CPU 1 当前运行的是 IDLE 任务(空闲任务)
E (5315) task_wdt: Print CPU 0 (current core) backtrace
// 打印 CPU 0 上当前任务的回溯信息,以便调试和定位问题
Queue
FreeRTOS 中的队列 (Queue) 是一种重要的任务间通信机制,用于在任务与任务或任务与中断之间传递数据
它通过先进先出 (FIFO) 的方式存储和传递信息,并提供线程安全的访问保障,避免了多任务同时操作共享资源时的竞争问题; 队列支持阻塞操作,任务可以等待数据或空位出现,从而简化了任务的同步与协作
队列在嵌入式系统中有广泛的应用,例如传感器数据采集、事件通知、日志记录等,通过队列,生产者任务可以将数据发送到队列中,消费者任务从中读取并处理数据;这种机制既提高了系统的可靠性,又降低了任务间通信的复杂度
Queue Delivery Data
队列本质上就类似于一个 Buffer
Creating a Queue
xQueueCreate()
用于创建队列的函数
该函数会自动从 FreeRTOS 的堆中分配所需的 RAM,用于存储队列的状态和数据项
#include "FreeRTOS.h"
#include "queue.h"
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
参数
uxQueueLength
队列可以容纳的最大数据项数
uxItemSize
队列中每个数据项的大小(以字节为单位)
返回值
QueueHandle_t
队列创建成功,返回的值是用于引用队列的句柄
NULL
队列创建失败
Sending Data to a Queue
xQueueSend(), xQueueSendToFront(),xQueueSendToBack()
这三个函数用于将数据项发送(写入)到 FreeRTOS 队列中
-
xQueueSend()
和xQueueSendToBack()
- 功能相同,均用于将数据发送到队列的末尾(队尾)
-
xQueueSend()
是较早的版本,推荐使用 xQueueSendToBack()
替代
-
xQueueSendToFront()
- 用于将数据发送到队列的前端(队首)
#include "FreeRTOS.h"
#include "queue.h"
BaseType_t xQueueSend( QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait );
BaseType_t xQueueSendToFront( QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait );
BaseType_t xQueueSendToBack( QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait );
参数
xQueue
队列的句柄
pvItemToQueue
指向要写入队列的数据的指针
队列创建时已经指定了每个数据项的大小,因此将从 pvItemToQueue
中复制相应字节数到队列存储区
xTicksToWait
如果队列已满,任务进入 Blocked 状态等待空间可用时的最大等待时间
-
以“系统时钟节拍(ticks)”为单位
- 使用宏
pdMS_TO_TICKS()
将毫秒时间转换为时钟节拍时间 - 设置为
portMAX_DELAY
时,任务将无限期等待,前提是INCLUDE_vTaskSuspend
在FreeRTOSConfig.h
中设置为 1
- 使用宏
-
若
xTicksToWait
为 0,函数会立即返回,而不等待
返回值
pdPASS
数据成功写入队列
errQUEUE_FULL
队列已满,无法写入数据;如果指定了等待时间,但在等待期间队列仍未腾出空间,函数返回此值
Receiving Data from a Queue
xQueueReceive()
用于从 FreeRTOS 队列中接收(读取)数据项
#include "FreeRTOS.h"
#include "queue.h"
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void *pvBuffer,
TickType_t xTicksToWait );
参数
xQueue
要从中读取数据的队列的句柄
pvBuffer
指向一个缓冲区的指针,用于存储从队列中读取的数据,缓冲区的大小必须至少等于队列中每个数据项的大小
xTicksToWait
如果队列为空,任务进入 Blocked 状态等待数据可用的最大等待时间
返回值
pdPASS
成功从队列中读取到数据
errQUEUE_EMPTY
队列为空,未能读取到数据
Checking Queue Parameters
uxQueueMessagesWaiting()
用于查询当前队列中存储的项数量
#include "FreeRTOS.h"
#include "queue.h"
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );
参数
xQueue
队列的句柄
返回值
返回队列中当前存储的项数量(即队列中未被读取的消息数)
Example Code: Working with FreeRTOS Queues
// 传递数据为整形
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "freertos/queue.h" // 包括队列头文件
void sendTask(void *pvPamra)
{
QueueHandle_t myQueueHandle = (QueueHandle_t)pvPamra; // 从pvPamra中取出队列句柄
BaseType_t xStatus;
int i = 0;
while (true)
{
xStatus = xQueueSend(myQueueHandle, &i, 0); // 发送数据到Queue,等待时间为0
if (xStatus == pdPASS)
printf("Send %d to queue.\n", i);
else
printf("Send %d to queue failed.\n", i);
i++;
if (i == 8)
i = 0;
printf("sendTask is running.\n");
vTaskDelay(3000 / portTICK_PERIOD_MS);
}
}
void receiveTask(void *pvPamra)
{
QueueHandle_t myQueueHandle = (QueueHandle_t)pvPamra; // 从pvPamra中取出队列句柄
BaseType_t xStatus;
int i = 0; // 因为Queue中的数据类型是int,所以要用int类型接收
while (true)
{
if (uxQueueMessagesWaiting(myQueueHandle) != 0)
{
xStatus = xQueueReceive(myQueueHandle, &i, 0); // 从Queue中接收数据
if (xStatus == pdPASS)
printf("Receive %d from queue.\n", i);
else
printf("Receive data from queue failed.\n");
}
else
printf("Queue is empty.\n"); // 如果Queue为空,则打印Queue为空
printf("receiveTask is running.\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void app_main(void)
{
TaskHandle_t myHandle = NULL;
TaskHandle_t myHandle2 = NULL;
QueueHandle_t myQueueHandle = xQueueCreate(5, sizeof(int)); // 创建一个队列,队列长度为5,每个元素大小为int
if (myQueueHandle != NULL)
{
printf("Queue created successfully.\n"); // 当Queue创建成功后,创建两个任务,一个发送任务,一个接收任务
xTaskCreate(sendTask, "sendTask", 1024 * 5, (void *)myQueueHandle, 1, &myHandle);
xTaskCreate(receiveTask, "receiveTask", 1024 * 5, (void *)myQueueHandle, 1, &myHandle2);
}
else
{
printf("Queue creation failed.\n");
}
}
/* --------------------------------------- */
// 传递数据为结构体
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "freertos/queue.h" // 包括队列头文件
typedef struct AData
{
int m_id;
int m_data;
} xStruct;
void sendTask(void *pvPamra)
{
QueueHandle_t myQueueHandle = (QueueHandle_t)pvPamra; // 从pvPamra中取出队列句柄
BaseType_t xStatus;
xStruct xUSB = {1, 2};
while (true)
{
xStatus = xQueueSend(myQueueHandle, &xUSB, 0); // 发送数据到Queue,等待时间为0
if (xStatus == pdPASS)
printf("Send id %d to queue.\n", xUSB.m_id);
else
printf("Send id %d to queue failed.\n", xUSB.m_id);
xUSB.m_id++;
printf("sendTask is running.\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void receiveTask(void *pvPamra)
{
QueueHandle_t myQueueHandle = (QueueHandle_t)pvPamra; // 从pvPamra中取出队列句柄
BaseType_t xStatus;
xStruct xUSB = {0, 0}; // 因为Queue中的数据类型是int,所以要用int类型接收
while (true)
{
if (uxQueueMessagesWaiting(myQueueHandle) != 0)
{
xStatus = xQueueReceive(myQueueHandle, &xUSB, 0); // 从Queue中接收数据
if (xStatus == pdPASS)
printf("Receive id %d from queue.Data is %d\n", xUSB.m_id, xUSB.m_data);
else
printf("Receive data from queue failed.\n");
}
else
printf("Queue is empty.\n"); // 如果Queue为空,则打印Queue为空
printf("receiveTask is running.\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void app_main(void)
{
TaskHandle_t myHandle = NULL;
TaskHandle_t myHandle2 = NULL;
QueueHandle_t myQueueHandle = xQueueCreate(5, sizeof(xStruct)); // 创建一个队列,队列长度为5,每个元素大小为xStruct的大小
if (myQueueHandle != NULL)
{
printf("Queue created successfully.\n"); // 当Queue创建成功后,创建两个任务,一个发送任务,一个接收任务
xTaskCreate(sendTask, "sendTask", 1024 * 5, (void *)myQueueHandle, 1, &myHandle);
xTaskCreate(receiveTask, "receiveTask", 1024 * 5, (void *)myQueueHandle, 1, &myHandle2);
}
else
{
printf("Queue creation failed.\n");
}
}
/* --------------------------------------- */
// 传递数据为指针
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "freertos/queue.h" // 包括队列头文件
void sendTask(void *pvPamra)
{
QueueHandle_t myQueueHandle = (QueueHandle_t)pvPamra; // 从pvPamra中取出队列句柄
char* pStrToSend = NULL;
int id = 0;
BaseType_t xStatus;
while (true)
{
pStrToSend = (char*)malloc(50);
snprintf(pStrToSend, 50, "Hello World! id:%d\n", id);
xStatus = xQueueSend(myQueueHandle, &pStrToSend, 0);
if (xStatus == pdPASS)
printf("Send id %d to queue.\n", id);
else
printf("Send id %d to queue failed.\n", id);
id++;
printf("sendTask is running.\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void receiveTask(void *pvPamra)
{
QueueHandle_t myQueueHandle = (QueueHandle_t)pvPamra; // 从pvPamra中取出队列句柄
BaseType_t xStatus;
char* pStrToReceive = NULL;
while (true)
{
if (uxQueueMessagesWaiting(myQueueHandle) != 0)
{
xStatus = xQueueReceive(myQueueHandle, &pStrToReceive, 0); // 从Queue中接收数据
if (xStatus == pdPASS)
{
printf("Receive data: %s\n", pStrToReceive);
free(pStrToReceive);// 释放内存,防止内存泄漏
// 注意要在接收数据后再释放内存,否则会收不到数据
}
else
printf("Receive data from queue failed.\n");
}
else
printf("Queue is empty.\n"); // 如果Queue为空,则打印Queue为空
printf("receiveTask is running.\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void app_main(void)
{
TaskHandle_t myHandle = NULL;
TaskHandle_t myHandle2 = NULL;
QueueHandle_t myQueueHandle = xQueueCreate(5, sizeof(char*)); // 创建一个队列,队列长度为5,每个元素大小为char*的大小
if (myQueueHandle != NULL)
{
printf("Queue created successfully.\n"); // 当Queue创建成功后,创建两个任务,一个发送任务,一个接收任务
xTaskCreate(sendTask, "sendTask", 1024 * 5, (void *)myQueueHandle, 1, &myHandle);
xTaskCreate(receiveTask, "receiveTask", 1024 * 5, (void *)myQueueHandle, 1, &myHandle2);
}
else
{
printf("Queue creation failed.\n");
}
}
Example Code:Queue Multiple In Single Out
Queue 可以同时有多个输入,一个输出,可以根据实际需求进行调整
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "freertos/queue.h" // 包括队列头文件
void sendTask1(void *pvPamra)
{
QueueHandle_t myQueueHandle = (QueueHandle_t)pvPamra; // 从pvPamra中取出队列句柄
int id = 111;
BaseType_t xStatus;
while (true)
{
xStatus = xQueueSend(myQueueHandle, &id, 0);
if (xStatus == pdPASS)
printf("Send id %d to queue.\n", id);
else
printf("Send id %d to queue failed.\n", id);
printf("sendTask is running.\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void sendTask2(void *pvPamra)
{
QueueHandle_t myQueueHandle = (QueueHandle_t)pvPamra; // 从pvPamra中取出队列句柄
int id = 222;
BaseType_t xStatus;
while (true)
{
xStatus = xQueueSend(myQueueHandle, &id, 0);
if (xStatus == pdPASS)
printf("Send id %d to queue.\n", id);
else
printf("Send id %d to queue failed.\n", id);
printf("sendTask2 is running.\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void receiveTask(void *pvPamra)
{
QueueHandle_t myQueueHandle = (QueueHandle_t)pvPamra; // 从pvPamra中取出队列句柄
BaseType_t xStatus;
int id = 0;
while (true)
{
xStatus = xQueueReceive(myQueueHandle, &id, portMAX_DELAY); // 从Queue中接收数据,等待时间无限长,直至收到数据
// 同时由于接收优先级高于发送,所以当Queue不为空时,必定能收到数据
// 优先级高的任务会先执行,之后再执行优先级低的任务
// 所以此时只有在收到数据后,才能执行下一次发送
// 实际使用时,应根据实际需求,设置合理的优先级
if (xStatus == pdPASS)
{
printf("Receive data: %d\n", id);
}
else
printf("Receive data from queue failed.\n");
printf("receiveTask is running.\n");
}
}
void app_main(void)
{
TaskHandle_t myHandle = NULL;
TaskHandle_t myHandle2 = NULL;
TaskHandle_t myHandle3 = NULL;
QueueHandle_t myQueueHandle = xQueueCreate(5, sizeof(int)); // 创建一个队列,队列长度为5,每个元素大小为int的大小
if (myQueueHandle != NULL)
{
printf("Queue created successfully.\n"); // 当Queue创建成功后,创建两个任务,一个发送任务,一个接收任务
xTaskCreate(sendTask1, "sendTask1", 1024 * 5, (void *)myQueueHandle, 1, &myHandle);
xTaskCreate(sendTask2, "sendTask2", 1024 * 5, (void *)myQueueHandle, 1, &myHandle2);
xTaskCreate(receiveTask, "receiveTask", 1024 * 5, (void *)myQueueHandle, 2, &myHandle3); // 接收优先级高于发送
}
else
{
printf("Queue creation failed.\n");
}
}
Queue Set
队列集合(Queue Set)适用于当有多个 Task 同时向各自的 Queue 中写入数据,但都需要被一个 Task 读取时的情况,在需要单任务从多个源(队列或信号量)中高效获取数据时,Queue Set 比传统方法更优
xQueueCreateSet()
队列集合(Queue Sets)是一种机制,允许实时操作系统(RTOS)的任务在多个队列或信号量的读取操作上同时阻塞(挂起)
#include "FreeRTOS.h"
#include "queue.h"
QueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength );
参数
uxEventQueueLength
指定事件队列的最大长度, 为防止事件丢失,需根据以下规则设置:队列长度之和 + 二元信号量或互斥量的长度(1) + 计数信号量的最大计数值
返回值
NULL
创建失败
Others
创建成功,返回队列集合的句柄
使用说明
- 创建队列集合队列集合需通过调用
xQueueCreateSet()
显式创建后才能使用 - 添加队列和信号量使用
xQueueAddToSet()
将标准 FreeRTOS 队列和信号量加入集合 - 选择队列或信号量调用
xQueueSelectFromSet()
判断集合中是否有队列或信号量可被成功读取或获取
xQueueAddToSet()
将队列或信号量添加到先前由 xQueueCreateSet()
创建的队列集合中
#include "FreeRTOS.h"
#include "queue.h"
BaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore,
QueueSetHandle_t xQueueSet );
参数
xQueueOrSemaphore
要添加到队列集合中的队列或信号量的句柄,需强制转换为 QueueSetMemberHandle_t 类型
xQueueSet
目标队列集合的句柄
返回值
pdPASS
成功将队列或信号量添加到队列集合中
pdFAIL
添加失败,因为该队列或信号量已属于另一个队列集合
注意事项
- 在调用
xQueueSelectFromSet()
并获取到集合成员的句柄之前,不可对队列集合中的队列执行接收操作或对信号量执行获取操作 - 在
FreeRTOSConfig.h
文件中,必须将configUSE_QUEUE_SETS
设置为 1,否则无法使用xQueueAddToSet()
xQueueSelectFromSet()
从队列集合中选择一个队列或信号量
#include "FreeRTOS.h"
#include "queue.h"
QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet,
const TickType_t xTicksToWait );
参数
xQueueSet
队列集合的句柄,任务将可能在此集合上阻塞
xTicksToWait
任务等待的最大时间(以系统时钟节拍为单位),在此期间任务将处于阻塞状态
返回值
NULL
在 xTicksToWait 指定的时间内,集合中的队列或信号量未变为可用
Others
集合中可用的队列(或信号量)的句柄,强制转换为 QueueSetMemberHandle_t 类型
Example Code:Queue Sets in FreeRTOS
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "freertos/queue.h" // 包括队列头文件
void sendTask1(void *pvPamra)
{
QueueHandle_t myQueueHandle = (QueueHandle_t)pvPamra; // 从pvPamra中取出队列句柄
int id = 111;
BaseType_t xStatus;
while (true)
{
xStatus = xQueueSend(myQueueHandle, &id, 0);
if (xStatus == pdPASS)
printf("Send id %d to queue.\n", id);
else
printf("Send id %d to queue failed.\n", id);
printf("sendTask is running.\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void sendTask2(void *pvPamra)
{
QueueHandle_t myQueueHandle = (QueueHandle_t)pvPamra; // 从pvPamra中取出队列句柄
int id = 222;
BaseType_t xStatus;
while (true)
{
xStatus = xQueueSend(myQueueHandle, &id, 0);
if (xStatus == pdPASS)
printf("Send id %d to queue.\n", id);
else
printf("Send id %d to queue failed.\n", id);
printf("sendTask2 is running.\n");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void receiveTask(void *pvPamra)
{
QueueSetHandle_t myQueueSetHandle = (QueueSetHandle_t)pvPamra; // 从pvPamra中取出队列集句柄
BaseType_t xStatus;
int id = 0;
QueueSetMemberHandle_t myQueueMemberHandle; // 用于接收数据
while (true)
{
myQueueMemberHandle = xQueueSelectFromSet(myQueueSetHandle, portMAX_DELAY); // 从队列集中选择一个队列
xStatus = xQueueReceive(myQueueMemberHandle, &id, portMAX_DELAY); // 从队列中接收数据
if (xStatus == pdPASS)
{
printf("Receive data: %d\n", id);
}
else
printf("Receive data from queue failed.\n");
printf("receiveTask is running.\n");
}
}
void app_main(void)
{
TaskHandle_t myHandle = NULL;
TaskHandle_t myHandle2 = NULL;
TaskHandle_t myHandle3 = NULL;
QueueHandle_t myQueueHandle1 = xQueueCreate(5, sizeof(int)); // 创建一个队列,队列长度为5,每个元素大小为int的大小
QueueHandle_t myQueueHandle2 = xQueueCreate(5, sizeof(int));
QueueSetHandle_t myQueueSetHandle = xQueueCreateSet(10); // 创建一个队列集,队列集大小为队列长度之和
xQueueAddToSet(myQueueHandle1, myQueueSetHandle);
xQueueAddToSet(myQueueHandle2, myQueueSetHandle);
if (myQueueHandle1 != NULL && myQueueHandle2 != NULL && myQueueSetHandle != NULL)
{
printf("Queue created successfully.\n"); // 当Queue创建成功后,创建两个任务,一个发送任务,一个接收任务
xTaskCreate(sendTask1, "sendTask1", 1024 * 5, (void *)myQueueHandle1, 1, &myHandle);
xTaskCreate(sendTask2, "sendTask2", 1024 * 5, (void *)myQueueHandle2, 1, &myHandle2);
xTaskCreate(receiveTask, "receiveTask", 1024 * 5, (void *)myQueueSetHandle, 2, &myHandle3); // 接收优先级高于发送
}
else
{
printf("Queue creation failed.\n");
}
}
Queue Mailbox
Mailbox 不会传递数据,而是一直保留这个数据,这个数据可以被任何 Task 和中断服务读取,并根据 Task 决定程序运行情况发送方可以 overwrite Mailbox 里的值,而接收方只可以读取,而不能移除这个值
xQueueOverwrite()
xQueueOverwrite() 是 xQueueSendToBack()
的变体,即使队列已满也会向队列写入数据,并覆盖队列中已有的数据
此函数主要适用于长度为 1 的队列(即队列只有“空”或“满”两种状态,比如 Mailbox)
#include "FreeRTOS.h"
#include "queue.h"
BaseType_t xQueueOverwrite( QueueHandle_t xQueue,
const void *pvItemToQueue );
参数
xQueue
目标队列的句柄
pvItemToQueue
指向要写入队列项的指针,队列在创建时定义了每个项的大小,此函数会将该大小的数据从 pvItemToQueue 拷贝到队列存储区域
返回值
xQueueOverwrite() 是对 xQueueGenericSend() 的宏封装,返回值与 xQueueSendToFront() 相同,但由于 xQueueOverwrite() 在队列已满时也能写入,pdPASS
是唯一可能的返回值
xQueuePeek()
从队列中读取一项数据,但不会将该项从队列中移除
#include "FreeRTOS.h"
#include "queue.h"
BaseType_t xQueuePeek( QueueHandle_t xQueue,
void *pvBuffer,
TickType_t xTicksToWait );
参数
xQueue
队列的句柄,用于指定从哪个队列中读取数据
pvBuffer
指向用于接收队列数据的内存指针,内存大小必须至少等于队列项的大小
xTicksToWait
任务等待数据变为可用的最大时间
返回值
pdPASS
数据成功从队列中读取
errQUEUE_EMPTY
数据为空
注意事项
- 使用此函数不会移除队列中的数据,因此适用于需要查看但不消费队列数据的场景
- 缓存大小必须匹配队列项的大小,否则可能导致数据损坏或访问越界
Example Code:Implementing a Shared Mailbox with Multiple Readers and One Writer
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "freertos/queue.h" // 包括队列头文件
void writeTask(void *pvPamra)
{
QueueHandle_t Mailbox = (QueueHandle_t)pvPamra; // 从pvPamra中取出队列句柄
int id = 0;
BaseType_t xStatus;
while (true)
{
xStatus = xQueueOverwrite(Mailbox, &id); // 覆盖写入
if (xStatus == pdPASS)
printf("Send id %d to queue.\n", id);
else
printf("Send id %d to queue failed.\n", id);
id++;
vTaskDelay(6000 / portTICK_PERIOD_MS);
}
}
void readTask(void *pvPamra)
{
QueueHandle_t Mailbox = (QueueHandle_t)pvPamra; // 从pvPamra中取出队列句柄
int id = 0;
BaseType_t xStatus;
while (true)
{
xStatus = xQueuePeek(Mailbox, &id, 0); // 读取队列中的数据
if (xStatus == pdPASS)
printf("Read id %d from queue.\n", id);
else
printf("Read id %d from queue failed.\n", id);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void app_main(void)
{
QueueHandle_t MailboxHandle = NULL;
TaskHandle_t writeTaskHandle = NULL;
TaskHandle_t readTaskHandle = NULL;
TaskHandle_t readTaskHandle2 = NULL;
TaskHandle_t readTaskHandle3 = NULL;
MailboxHandle = xQueueCreate(1, sizeof(int)); // 创建一个邮箱,邮箱大小为1,每个元素大小为int的大小
if (MailboxHandle != NULL)
{
printf("Queue created successfully.\n"); // 当Queue创建成功后,创建两个任务,一个发送任务,一个接收任务
xTaskCreate(writeTask, "writeTask", 1024 * 5, (void *)MailboxHandle, 1, &writeTaskHandle);
xTaskCreate(readTask, "readTask1", 1024 * 5, (void *)MailboxHandle, 2, &readTaskHandle); // 在实现的功能相同时,多个Task可以共用一个函数体
xTaskCreate(readTask, "readTask2", 1024 * 5, (void *)MailboxHandle, 2, &readTaskHandle2);
xTaskCreate(readTask, "readTask3", 1024 * 5, (void *)MailboxHandle, 2, &readTaskHandle3);
}
else
{
printf("Queue creation failed.\n");
}
}
Software Timer
硬件定时器会有数量等方面的限制,使用较不灵活,而软件定时器使用更为灵活,其与硬件、平台无关,在不同的 MCU 都可以使用 FreeRTOS API 进行调用
特性 | 硬件定时器 | 软件定时器 |
---|---|---|
数量 | 固定,受 MCU 硬件资源限制(通常只有几个) | 灵活,可以根据需要动态创建(受内存和任务管理能力限制) |
依赖性 | 依赖具体硬件平台,配置方式和功能因芯片而异 | 与硬件平台无关,可通过 FreeRTOS API 在不同 MCU 上使用 |
精度 | 高精度,直接由硬件计时,通常用于实时性要求高的场景 | 精度依赖于 RTOS 的调度周期(tick 周期),不适合极高实时性场景 |
性能 | 高性能,独立运行,不占用 CPU 资源 | 运行在 RTOS 守护任务上下文中,占用 CPU 资源 |
适用场景 | 适合时间敏感的应用,如 PWM 信号生成、脉冲捕获、输入输出事件计时等 | 适合通用定时功能,如定时任务执行、软件超时处理等 |
灵活性 | 配置固定,功能和用途受限 | 灵活性高,可动态调整超时时间、回调函数等 |
使用复杂度 | 配置复杂,需根据芯片手册手动设置寄存器 | 使用方便,通过 FreeRTOS API 调用 |
移植性 | 差,代码与硬件平台强耦合 | 好,代码与硬件无关,便于跨平台移植 |
所有软件定时器的回调函数都在同一个RTOS守护任务(也称为“定时器服务任务”)的上下文中执行(该任务最初被称为“定时器服务任务”,因为最初它只用于执行软件定时器回调函数;现在同一任务也用于其他用途,因此被改名为更通用的“RTOS 守护任务”)守护任务是一个标准的FreeRTOS任务,会在调度器启动时自动创建,其优先级和堆栈大小由编译时配置常量configTIMER_TASK_PRIORITY
和configTIMER_TASK_STACK_DEPTH
分别设置,这两个常量在FreeRTOSConfig.h
中定义
需要注意,软件定时器的回调函数是在 RTOS 守护任务(Timer Service Task)的上下文中执行的,而不是在独立的任务中运行;因此,回调函数中不能调用可能使任务进入阻塞状态的 FreeRTOS API 函数,因为这会阻塞整个守护任务,导致系统运行异常
Creating a Software Timer
xTimerCreate()
创建一个新的软件定时器,并返回一个句柄以引用创建的定时器
#include "FreeRTOS.h"
#include "timers.h"
TimerHandle_t xTimerCreate( const char *pcTimerName,
const TickType_t xTimerPeriod,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction );
参数
pcTimerName
定时器的名称,仅用于调试
xTimerPeriod
,定时器周期,单位为系统 tick,不能为 0,可以使用 pdMS_TO_TICKS() 宏将毫秒转换为 tick,例如:
- 100 tick直接设置为 100
- 500ms可使用
pdMS_TO_TICKS(500)
,前提是configTICK_RATE_HZ <= 1000
uxAutoReload
设置定时器类型
-
pdTRUE
自动重载定时器(周期性触发) -
pdFALSE
一次性定时器(仅触发一次,可手动重新启动)
pvTimerID
定时器标识符,用于在回调函数中区分不同的定时器,或在回调调用之间存储值
pxCallbackFunction
定时器到期时执行的回调函数,需符合 TimerCallbackFunction_t 原型:
void vCallbackFunctionExample(TimerHandle_t xTimer);
configTICK_RATE_HZ 是 FreeRTOS 配置文件 FreeRTOSConfig.h 中定义的一个宏,它表示 每秒系统 Tick 的次数,即 FreeRTOS 的调度器每秒中断的频率(单位为 Hz)
例如:如果 configTICK_RATE_HZ = 1000,表示系统每 1 毫秒触发一次 Tick 中断,如果 configTICK_RATE_HZ = 100,表示系统每 10 毫秒触发一次 Tick 中断
返回值
NULL
定时器创建失败,原因可能是 FreeRTOS 堆内存不足
Others
定时器创建成功,返回的句柄可用于引用该定时器
配置要求(一般不用动)
- 在
FreeRTOSConfig.h
文件中,configUSE_TIMERS
和configSUPPORT_DYNAMIC_ALLOCATION
必须都设置为1
- 如果
configSUPPORT_DYNAMIC_ALLOCATION
未定义,其默认值为1
创建定时器并不会立即启动,可以使用以下函数来启动或管理定时器
// 启动定时器,如果定时器已经在运行,则从当前时间重新开始
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );
// 重置(重新启动)定时器,确保定时器启动或重新计算到期时间
BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait );
// 从中断上下文启动定时器,等效于 xTimerStart(),用于中断中调用
BaseType_t xTimerStartFromISR( TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken );
// 从中断上下文重置(重新启动)定时器,等效于 xTimerReset(),用于中断中调用
BaseType_t xTimerResetFromISR( TimerHandle_t xTimer,
BaseType_t *pxHigherPriorityTaskWoken );
// 更改定时器的周期,如果定时器未运行,则会启动定时器
BaseType_t xTimerChangePeriod( TimerHandle_t xTimer,
TickType_t xNewPeriod,
TickType_t xTicksToWait );
// 从中断上下文更改定时器的周期,等效于 xTimerChangePeriod(),用于中断中调用
BaseType_t xTimerChangePeriodFromISR( TimerHandle_t xTimer,
TickType_t xNewPeriod,
BaseType_t *pxHigherPriorityTaskWoken );
Managing Software Timers
xTimerStart()
xTimerStart() 用于启动一个软件定时器的运行
- 如果定时器尚未运行,
xTimerStart()
会计算一个到期时间,该时间相对于调用 xTimerStart()
的时刻 - 如果定时器已经在运行,则
xTimerStart()
相当于调用了xTimerReset()
,即重置定时器 - 定时器会在定义的周期后(即
xTimerStart()
调用后n
个 tick)触发回调函数,除非定时器在此期间被停止、删除或重置
#include "FreeRTOS.h"
#include "timers.h"
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );
参数
xTimer
要启动、重置或重新启动的定时器句柄
xTicksToWait
指定调用任务在 timer command queue 队列已满的情况下,等待空间可用的最大时间(单位为 tick),这是任务在进入 Blocked 状态时的阻塞时间;如果队列已满,任务会被阻塞,直到有足够的空间来发送命令
- 设置
xTicksToWait
为portMAX_DELAY
将导致任务无限期等待,直到队列中有空间 - 如果在调度器启动之前调用
xTimerStart()
,则xTicksToWait
会被忽略
当任务调用 xTimerStart() 或其他定时器相关 API 时,这些命令并不会立即由任务执行,而是通过一个队列传递给定时器服务任务,如果队列已满,新的命令会被阻塞,直到队列有空间可用
这时,调用 xTimerStart() 等 API 的任务会根据指定的阻塞时间(xTicksToWait)进入阻塞状态,等待队列空间变得可用,timer command queue 的大小由 FreeRTOS 的配置项决定;队列的大小设置影响系统可以同时处理多少个定时器命令;如果队列大小太小,可能会导致命令丢失或任务阻塞
configTIMER_QUEUE_LENGTH
定义了 timer command queue 队列的最大长度(即可以存放多少个定时器命令)
返回值
pdPASS
启动命令成功发送到定时器命令队列,如果指定了阻塞时间(即 xTicksToWait 不为零),则可能会因为队列已满,任务进入阻塞状态等待空间释放,直到数据成功写入队列
- 定时器命令的处理时间会根据定时器服务任务的优先级而有所不同,但定时器的到期时间是相对于实际调用
xTimerStart()
时刻(从队列中取出命令并实际启动定时器)的 - 定时器命令的处理时间受定时器服务任务优先级的影响,定时器服务任务的优先级由
configTIMER_TASK_PRIORITY
配置常量设置
pdFAIL
启动命令未成功发送到定时器命令队列,原因是队列已满
注意事项(一般不用动)
在 FreeRTOSConfig.h
中,configUSE_TIMERS
必须设置为 1,才能使用 xTimerStart()
函数
xTimerStop()
xTimerStop()
用于停止一个运行中的软件定时器
- 调用
xTimerStop()
可以停止一个正在运行的定时器,如果定时器已经停止或已过期,则调用 xTimerStop()
不会产生影响 -
xTimerStop()
向定时器命令队列发送停止命令,定时器服务任务会在稍后处理该命令
#include "FreeRTOS.h"
#include "timers.h"
BaseType_t xTimerStop( TimerHandle_t xTimer, TickType_t xTicksToWait );
参数
xTimer
要停止的定时器句柄
xTicksToWait
指定任务在定时器命令队列已满的情况下,最大等待时间(单位为 ticks)
返回值
pdPASS
命令成功发送到定时器命令队列
pdFAIL
命令发送到定时器命令队列失败
pcTimerGetName()
返回在创建定时器时分配的可读文本名称
#include "FreeRTOS.h"
#include "timers.h"
const char * pcTimerGetName( TimerHandle_t xTimer );
返回值
一个指向定时器名称的指针
pvTimerGetTimerID()
返回与定时器关联的标识符(ID)
- 返回在创建定时器时分配的标识符,该标识符可以通过
vTimerSetTimerID()
API 更新 - 在回调函数中可以使用该标识符区分哪个定时器到期,特别是在多个定时器共享相同的回调函数时
#include "FreeRTOS.h"
#include "timers.h"
void *pvTimerGetTimerID( TimerHandle_t xTimer );
返回值
返回与指定定时器关联的标识符(指针类型)
xTimerReset()
重置、启动或重新启动一个软件定时器,能够起到 Watch Dog 的作用
- 如果定时器正在运行,
xTimerReset()
会将定时器的到期时间重新计算为相对于调用时间的周期 - 如果定时器未运行,
xTimerReset()
会启动定时器,并将到期时间计算为相对于调用时间的周期,此时等效于xTimerStart()
- 无论定时器当前是否运行,调用
xTimerReset()
后,定时器都将开始运行
#include "FreeRTOS.h"
#include "timers.h"
BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait );
返回值
pdPASS
命令成功发送到定时器命令队列
pdFAIL
命令发送到定时器命令队列失败
xTimerChangePeriod()
xTimerChangePeriod()
用于更改软件定时器的周期
-
更改运行中定时器的周期
- 如果定时器正在运行,则新周期将用于重新计算到期时间
- 新的到期时间相对于调用
xTimerChangePeriod()
的时刻,而不是定时器最初启动的时刻
-
启动未运行的定时器
- 如果定时器未运行,则定时器将使用新的周期值计算到期时间,并开始运行
#include "FreeRTOS.h"
#include "timers.h"
BaseType_t xTimerChangePeriod( TimerHandle_t xTimer,
TickType_t xNewPeriod,
TickType_t xTicksToWait );
参数
xTimer
需要更改周期的定时器句柄
xNewPeriod
定时器的新周期(单位为 tick),可使用 pdMS_TO_TICKS() 将毫秒转换为 tick
xTicksToWait
阻塞任务的最大时间(单位为 tick),如果定时器命令队列已满,则等待空间可用
返回值
pdPASS
命令成功发送到定时器命令队列
pdFAIL
命令发送到定时器命令队列失败
Example Code:Using Software Timers
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "freertos/timers.h" // 定时器头文件
void TimerCallback(TimerHandle_t xTimer)
{
const char *pcTimerName = pcTimerGetName(xTimer);// 获取定时器名称
uint32_t *uiTimerID = (uint32_t *)pvTimerGetTimerID(xTimer);// 获取定时器ID
printf("%s expired, ID: %lu.\n", pcTimerName, *uiTimerID);// 打印定时器名称和ID
}
int id1 = 0;
int id2 = 1;
void app_main(void)
{
TimerHandle_t TimerHandle1 = NULL;
TimerHandle1 = xTimerCreate("Timer1", pdMS_TO_TICKS(1000), pdTRUE, (void *)&id1, TimerCallback);// 创建一个周期为1000ms的定时器
xTimerStart(TimerHandle1, 0);// 启动定时器
TimerHandle_t TimerHandle2 = NULL;
TimerHandle2 = xTimerCreate("Timer2", pdMS_TO_TICKS(2000), pdTRUE, (void *)&id2, TimerCallback);// 与Timer1公用同一个回调函数,观察pcTimerGetName的输出
xTimerStart(TimerHandle2, 0);// 启动定时器
// for(int i = 0; i < 10; i++)
// { // vTaskDelay(pdMS_TO_TICKS(1000)); // xTimerReset(TimerHandle2, 0);// 重置定时器,观察pcTimerGetName的输出,此时Timer2不会被打印
// }
vTaskDelay(pdMS_TO_TICKS(5000));
xTimerChangePeriod(TimerHandle2, pdMS_TO_TICKS(1000), 0);// 修改Timer2的周期为1000ms
vTaskDelay(pdMS_TO_TICKS(5000));
xTimerStop(TimerHandle1, 0);// 停止定时器
xTimerStop(TimerHandle2, 0);// 停止定时器
}
Semaphore
Binary Semaphore
信号量(Semaphore),用于控制 Task 进行同步,Binary 表示这个信号量只有两个值 0/1
xSemaphoreCreateBinary()
创建一个二值信号量,并返回一个句柄供引用
#include "FreeRTOS.h"
#include "semphr.h"
SemaphoreHandle_t xSemaphoreCreateBinary( void );
返回值
-
NULL
信号量创建失败(堆内存不足) -
Others
信号量创建成功,返回的句柄可用于引用该信号量
注意事项
- 确保在
FreeRTOSConfig.h
中启用configSUPPORT_DYNAMIC_ALLOCATION
xSemaphoreGive()
释放(或“给予”)一个先前创建并已成功“获取”的信号量
必须先通过 xSemaphoreGive()
给予信号量,才能通过 xSemaphoreTake()
获取
#include "FreeRTOS.h"
#include "semphr.h"
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
参数
xSemaphore
要释放的信号量,类型为 SemaphoreHandle_t
,必须在使用前显式创建
返回值
-
pdPASS
操作成功,信号量被成功释放 -
pdFAIL
操作失败,因为调用任务不是信号量的持有者,任务必须先成功“获取”信号量后才能释放
xSemaphoreTake()
获取(或“占用”)一个先前创建的信号量
#include "FreeRTOS.h"
#include "semphr.h"
BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait );
参数
xSemaphore
要获取的信号量,类型为 SemaphoreHandle_t,必须在使用前显式创建
xTicksToWait
任务等待信号量可用的最大时间(以 FreeRTOS 系统时钟节拍为单位)
返回值
pdPASS
信号量获取成功
pdFAIL
信号量获取失败
注意事项:
- 必须从运行中的任务中调用,不能在调度器启动前或初始化阶段调用
- 不应在临界区或调度器暂停时调用
Example Code: Binary Semaphore for Task Synchronization
- 由于任务的优先级相同,FreeRTOS 调度器会以时间片轮转方式调度两个任务信号量保证了两个任务不会同时操作共享变量
iCount
,从而避免数据竞争
Task1 和 Task2 都会试图对共享变量 iCount
进行递增操作:
- 每次操作前,任务会调用
xSemaphoreTake()
获取信号量,确保独占访问 - 操作完成后,调用
xSemaphoreGive()
释放信号量,允许其他任务访问
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "esp_log.h"
#include "freertos/semphr.h" // 信号量头文件
int iCount = 0;
SemaphoreHandle_t semaphoreHandle = NULL;
void myTask1(void *pvParameters)
{
while(1)
{
xSemaphoreTake(semaphoreHandle, portMAX_DELAY);// 锁住信号量
for(int i = 0; i < 10; i++)
{
iCount++;
ESP_LOGI("Task1", "iCount = %d\n", iCount);
vTaskDelay(pdMS_TO_TICKS(1000));
}
xSemaphoreGive(semaphoreHandle);// 释放信号量
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
void myTask2(void *pvParameters)
{
while(1)
{
xSemaphoreTake(semaphoreHandle, portMAX_DELAY);// 锁住信号量
for(int i = 0; i < 10; i++)
{
iCount++;
ESP_LOGI("Task2", "iCount = %d\n", iCount);
vTaskDelay(pdMS_TO_TICKS(1000));
}
xSemaphoreGive(semaphoreHandle);// 释放信号量
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
void app_main(void)
{
semaphoreHandle = xSemaphoreCreateBinary();
xSemaphoreGive(semaphoreHandle);
xTaskCreate(myTask1, "Task1", 2048, NULL, 5, NULL);
xTaskCreate(myTask2, "Task2", 2048, NULL, 5, NULL);
}
Count Semaphore
计数型信号量(Counting Semaphore) 用于在任务或中断之间实现资源的共享与同步,它与二值信号量的主要区别在于其内部维护了一个计数器,允许多个任务或中断“获取”信号量,而无需严格的互斥限制
xSemaphoreCreateCounting()
创建一个计数型信号量,返回一个句柄用于引用该信号量,信号量可用于事件计数或资源管理
用途
-
事件计数
- 用于记录事件发生的次数
- 每次事件发生时调用
xSemaphoreGive()
增加计数,任务通过xSemaphoreTake()
处理事件并减少计数 - 初始计数值应为 0
-
资源管理
- 用于管理一定数量的共享资源
- 每次任务获取资源时调用
xSemaphoreTake()
,计数值减少;释放资源时调用xSemaphoreGive()
,计数值增加 - 初始计数值应为可用资源的数
#include "FreeRTOS.h"
#include "semphr.h"
SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount );
参数
uxMaxCount
信号量的最大计数值,当信号量达到此值时,不能再“给予”信号量
uxInitialCount
信号量的初始计数值
返回值
NULL
创建失败,通常由于堆内存不足
Others
信号量创建成功,可通过返回的句柄引用信号量
注意事项
- 必须在
FreeRTOSConfig.h
中启用configSUPPORT_DYNAMIC_ALLOCATION
uxSemaphoreGetCount()
返回信号量的当前计数值
#include "FreeRTOS.h"
#include "semphr.h"
UBaseType_t uxSemaphoreGetCount( SemaphoreHandle_t xSemaphore );
参数
xSemaphore
信号量的句柄,用于指定需要查询的信号量
返回值
返回指定信号量的当前计数值,用于实时监控信号量的状态或判断可用资源的数量
Example Code: Counting Semaphore for Parking Lot Management
资源管理
信号量的计数值表示资源的当前可用数量,通过动态增减信号量,实现对有限资源(如停车位)的精确管理
任务协调
通过信号量,确保多个任务对共享资源的访问井然有序,避免资源竞争和冲突
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "esp_log.h"
#include "freertos/semphr.h" // 信号量头文件
int iCount = 0;
SemaphoreHandle_t semaphoreHandle = NULL;
void carInTask(void *pvParameters)
{
int empty = 0;
while (1)
{
empty = uxSemaphoreGetCount(semaphoreHandle); // 获取信号量剩余数量
ESP_LOGI("carInTask", "empty = %d\n", empty);
vTaskDelay(pdMS_TO_TICKS(1000));
if (xSemaphoreTake(semaphoreHandle, 0) == pdPASS)
{
ESP_LOGI("carInTask", "carIn\n");
}
else
{
ESP_LOGI("carInTask", "carIn failed\n");
}
}
}
void carOutTask(void *pvParameters)
{
while (1)
{
vTaskDelay(pdMS_TO_TICKS(6000));
xSemaphoreGive(semaphoreHandle);
ESP_LOGI("carOutTask", "carOut\n");
}
}
void app_main(void)
{
semaphoreHandle = xSemaphoreCreateCounting(5, 5);
// xSemaphoreGive(semaphoreHandle);// 在使用计数信号量时,无需手动释放信号量
xTaskCreate(carInTask, "carInTask", 2048, NULL, 5, NULL);
xTaskCreate(carOutTask, "carOutTask", 2048, NULL, 5, NULL);
}
Mutex
当一个任务持有互斥量时,如果另一个更高优先级的任务尝试获取同一个互斥量,持有该互斥量的任务的优先级会被提升到另一个试图获得当前互斥量的任务的优先级,以便使高优先级任务能够获取该互斥量;这种优先级提升被称为“优先级继承”,当互斥量被释放时,任务会恢复到原来的优先级
获取互斥量的任务必须始终归还互斥量,否则其他任务将无法获取该互斥量
和二进制变量的主要区别:
二进制信号量用于同步时,获取信号量(“take”)后,不需要再“归还”它,任务同步的实现是通过一个任务或中断“给予”信号量,另一个任务“获取”信号量
如果一个低优先级任务获取了二进制信号量,那么高优先级任务只能等待
互斥量则会使用优先级继承来解决这种情况,确保低优先级任务能够尽快释放互斥量(让当前任务尽快完成)
xSemaphoreCreateMutex()
创建互斥锁类型信号量,并返回可引用互斥锁的句柄
#include "FreeRTOS.h"
#include "semphr.h"
SemaphoreHandle_t xSemaphoreCreateMutex( void );
返回值
SemaphoreHandle_t
成功创建信号量,返回值是一个句柄,通过它可以引用创建的信号量
Others
如果由于没有足够的堆内存供FreeRTOS分配,信号量数据结构而无法创建信号量
Example Code:Mutex Synchronization with Task Priorities in FreeRTOS
Task1 获取到互斥量后,Task2 进入死循环,由于 Task2 优先级比 Task1 高,此时 Task1 无法运行;一段时间后 Task3 尝试获取互斥量,但此时互斥量还在 Task1,因此 Task1 的优先级被调整至和 Task3 同优先级,比 Task2 高,可以继续运行;当 Task1 运行完毕释放互斥量时,Task3 获取互斥量,此时 Task1 被 Task2 卡住,无法再运行
由于在ESP32、ESP32-S3 等双核 MCU 上,FreeRTOS对任务进行双核调度,此时若 Task1 和 Task2 分别处于不同的核心,Task2 无法卡住 Task1 ,需要将所有任务创建在同一个核心上才能实现目标现象
虽然 Task2 会因为触发看门狗被重启,但是不影响本示例代码进行的实验
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "freeRTOS/semphr.h"
SemaphoreHandle_t mutexHandle; // 创建一个互斥信号量句柄
void Task1(void *pvParam)
{
BaseType_t Status;
while (true)
{
printf("Task1 is running\n");
Status = xSemaphoreTake(mutexHandle, 1000);
if (Status == pdPASS)
{
printf("Task1 get the mutex\n");
for (int i = 0; i < 50; i++)
{
printf("i in task1: %d\n", i);
vTaskDelay(pdMS_TO_TICKS(1000));
}
xSemaphoreGive(mutexHandle);
vTaskDelay(pdMS_TO_TICKS(5000));
}
else
{
printf("Task1 failed to get the mutex\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
}
void Task2(void *pvParam)
{
printf("Task2 is running\n");
vTaskDelay(pdMS_TO_TICKS(1000)); // 给 Task1 一些时间来获取互斥信号量
while (true)
{
; // 任务 2 会直接把整个程序卡住,只有比它优先级高的任务才能执行
// vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void Task3(void *pvParam)
{
BaseType_t Status;
printf("Task3 is running\n");
vTaskDelay(pdMS_TO_TICKS(1000));
while (true)
{
Status = xSemaphoreTake(mutexHandle, 1000); // Task3 尝试获取互斥信号量
// 将会失败,因为 Task1 已经获取了互斥信号量
// 将 Task1 的优先级升高至 Task3 的优先级
// 此时 Task1 继续运行
if (Status == pdPASS)
{
printf("Task3 get the mutex\n");
for (int i = 0; i < 10; i++)
{
printf("i in task3: %d\n", i);
vTaskDelay(pdMS_TO_TICKS(1000));
}
xSemaphoreGive(mutexHandle);
vTaskDelay(pdMS_TO_TICKS(5000));
}
else
{
printf("Task3 failed to get the mutex\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
}
void app_main(void)
{
mutexHandle = xSemaphoreCreateMutex(); // 创建一个互斥信号量
vTaskSuspendAll(); // 挂起任务调度
xTaskCreatePinnedToCore(Task1, "Task1", 1024 * 5, NULL, 1, NULL, 0); // 由于 ESP32-S3 的双核调度,需要将所有任务创建在同一个核心上才能实现目标现象
xTaskCreatePinnedToCore(Task2, "Task2", 1024 * 5, NULL, 2, NULL, 0);
xTaskCreatePinnedToCore(Task3, "Task3", 1024 * 5, NULL, 3, NULL, 0);
xTaskResumeAll(); // 恢复任务调度
}
Recursive Mutex
递归互斥锁是指在调用时,已经获取了当前互斥锁的任务可以继续多次获取互斥锁用于处理不同数据(如占用了一个资源后接着占用下一个资源,使用普通的二进制变量或互斥锁需要通过创建多个变量来实现,而使用递归互斥锁则只需再获取一次即可,释放时也只需释放相同的次数)
xSemaphoreCreateRecursiveMutex()
创建递归互斥锁类型的信号量,并返回可引用递归互斥锁的句柄
#include "FreeRTOS.h"
#include "semphr.h"
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex( void );
返回值
SemaphoreHandle_t
创建互斥锁成功,返回值是一个句柄,通过它可以引用创建的互斥锁
Others
如果由于没有足够的堆内存供FreeRTOS分配,信号量数据结构而无法创建信号量
xSemaphoreTakeRecursive()
获取一个递归互斥锁类型的信号量
#include "FreeRTOS.h"
#include "semphr.h"
BaseType_t xSemaphoreTakeRecursive( SemaphoreHandle_t xMutex, TickType_t xTicksToWait );
参数
xMutex
要获取的信号量
xTicksToWait
任务等待信号量可用的最大时间(以 FreeRTOS 系统时钟节拍为单位)
返回值
pdPASS
信号量获取成功
pdFAIL
信号量获取失败
xSemaphoreGiveRecursive()
释放一个递归互斥锁类型的信号量
#include "FreeRTOS.h"
#include "semphr.h"
BaseType_t xSemaphoreGiveRecursive( SemaphoreHandle_t xMutex );
参数
xMutex
要释放的信号量
xTicksToWait
任务等待信号量可用的最大时间(以 FreeRTOS 系统时钟节拍为单位)
返回值
pdPASS
信号量释放成功
pdFAIL
信号量释放失败
Example Code:Recursive Mutex Synchronization Between Tasks in FreeRTOS
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "freeRTOS/semphr.h"
SemaphoreHandle_t mutexHandle; // 创建一个互斥信号量句柄
void Task1(void *pvParam)
{
printf("Task1 is running\n");
vTaskDelay(pdMS_TO_TICKS(1000));
while (true)
{
printf("A new loop for task1\n");
printf("Task1 is running\n");
xSemaphoreTakeRecursive(mutexHandle, portMAX_DELAY);
printf("Task1 get A\n");
for (int i = 0; i < 10; i++)
{
printf("i for A in task1: %d\n", i);
vTaskDelay(pdMS_TO_TICKS(1000));
}
xSemaphoreTakeRecursive(mutexHandle, portMAX_DELAY);
printf("Task1 get B\n");
for (int i = 0; i < 10; i++)
{
printf("i for B in task1: %d\n", i);
vTaskDelay(pdMS_TO_TICKS(1000));
}
printf("Task1 release B\n");
xSemaphoreGiveRecursive(mutexHandle);
vTaskDelay(pdMS_TO_TICKS(3000));
printf("Task1 release A\n");
xSemaphoreGiveRecursive(mutexHandle);
vTaskDelay(pdMS_TO_TICKS(3000));
// 只有当 Task1 连续释放两次信号量的时候,Task2 才能获取到信号量
}
}
void Task2(void *pvParam)
{
printf("Task2 is running\n");
vTaskDelay(pdMS_TO_TICKS(1000));
while (true)
{
printf("A new loop for task2\n");
xSemaphoreTakeRecursive(mutexHandle, portMAX_DELAY);
printf("Task2 get A\n");
for (int i = 0; i < 10; i++)
{
printf("i for A in task2: %d\n", i);
vTaskDelay(pdMS_TO_TICKS(1000));
}
printf("Task2 release A\n");
xSemaphoreGiveRecursive(mutexHandle);
vTaskDelay(pdMS_TO_TICKS(3000));
}
}
void app_main(void)
{
mutexHandle = xSemaphoreCreateRecursiveMutex(); // 创建一个递归互斥信号量
vTaskSuspendAll();
xTaskCreatePinnedToCore(Task1, "Task1", 1024 * 5, NULL, 1, NULL, 0);
xTaskCreatePinnedToCore(Task2, "Task2", 1024 * 5, NULL, 2, NULL, 0);
xTaskResumeAll();
}
Event Group
事件组是一种同步机制,用于任务之间的通信,它们允许任务设置、清除和等待多个事件的组合
每个事件组有多个位,任务可以操作这些位来表示不同的状态或事件
关键功能
-
位操作
事件组可以被看作是一个二进制位的集合,任务可以对这些位进行设置、清除和等待 -
同步机制
任务可以等待事件组中的某些位变为设定状态(例如,位为1),这样可以使任务在等待某些事件发生时暂停执行,直到事件发生 -
多任务通信
事件组可以在多个任务之间传递信息
使用场景
- 在多个任务之间传递控制信号或数据标志
- 实现任务之间的依赖关系,如任务A完成某项工作后,任务B才可以执行
Event Group Wait
xEventGroupCreate()
创建一个新的事件组,并返回可以引用创建的事件组的句柄
事件组包含的标志位(或位)的数量依赖于 configUSE_16_BIT_TICKS
配置项
-
如果
configUSE_16_BIT_TICKS = 1
,则事件组有 8 位(标志位) -
如果
configUSE_16_BIT_TICKS = 0
,则事件组有 24 位(标志位) -
配置文件路径(v5.3.1)
idf\v5.3.1\esp-idf\components\freertos\config\include\freertos
#include "FreeRTOS.h"
#include "event_groups.h"
EventGroupHandle_t xEventGroupCreate( void );
返回值
EventGroupHandle_t
创建了事件组,返回的值是创建的事件组的句柄
NULL
无法创建事件组,因为可用的 FreeRTOS 堆内存不足
xEventGroupSetBits()
在RTOS事件组中设置位
- 这个函数不能从中断中调用
#include "FreeRTOS.h"
#include "event_groups.h"
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet );
参数
xEventGroup
需要设置 bit 的事件组
uxBitsToSet
一个按位的值,表示要在事件组中设置的一个或多个位
-
通过设置不同的二进制值来指定要等待的位
- 如果想等待 bit 0 和 bit 2 设置,
uxBitsToWaitFor
应该是0x05
(即00000101
) - 如果想等待 bit 0、bit 1 和 bit 2 设置,
uxBitsToWaitFor
应该是0x07
(即00000111
)
- 如果想等待 bit 0 和 bit 2 设置,
-
可以根据需求组合多个位来构造不同的掩码值
返回值
EventBits_t
事件组中各位(bits)在调用 xEventGroupSetBits()
函数返回时的状态
可能被改变状态的情况
-
自动清除(
xClearBitsOnExit
参数- 当调用
xEventGroupSetBits()
设置位后,可能有任务正在等待这些位(通过xEventGroupWaitBits()
),如果等待任务设置了xClearBitsOnExit
参数为pdTRUE
,则这些位在任务被唤醒时会自动被清除,在xEventGroupSetBits()
返回时,返回值中的位可能已经被清除
- 当调用
-
高优先级 Task 清除位
- 如果设置事件位后,有更高优先级的任务因这些位的设置从阻塞状态切换为就绪状态(Ready),并立即执行,它可能会修改事件组的值
在 xEventGroupSetBits()
返回时,返回值可能反映的是任务执行后事件组的状态,而不是立即设置位后的状态
xEventGroupWaitBits()
读取RTOS事件组中的位,可选择进入阻塞状态(带超时)以等待一个位或一组位被设置
- 这个函数不能从中断中调用
#include "FreeRTOS.h"
#include "event_groups.h"
EventBits_t xEventGroupWaitBits( const EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait );
参数
xEventGroup
需要测试(查看)bit 的事件组
uxBitsToWaitFor
一个位运算值,用于指定在事件组中要等待的位
- 不能被设置为
0
xClearOnExit
设置是否清除事件
-
pdTRUE
- 如果设置为
pdTRUE
,那么在函数返回时,事件组中由 uxBitsToWaitFor
指定的那些位会被清除(即设置为 0),前提是函数返回的原因不是超时 - 这通常用于在检测到某些事件发生后,自动清除事件状态,避免其他任务误判这些事件仍然有效
- 如果设置为
-
pdFALSE
- 如果设置为
pdFALSE
,事件组中的位不会被清除,即便函数成功返回,这种方式适用于需要让其他任务也能检测到这些位的场景
- 如果设置为
xWaitForAllBits
决定任务等待位时的逻辑条件是 逻辑 AND(等待所有指定的位都被设置)还是 逻辑 OR(只需等待任意一个指定的位被设置)
-
pdTRUE
(逻辑 AND)- 函数会等待事件组中的所有指定位都被设置为 1
- 如果所有位在等待时间内都被设置,函数返回
- 如果等待时间到期(
xTicksToWait
超时),函数返回超时结果
-
pdFALSE
(逻辑 OR)- 函数会等待事件组中的任意一个指定位被设置为 1
- 如果任意一位在等待时间内被设置,函数立即返回
- 如果等待时间到期且没有任何位被设置,函数返回超时结果
xTicksToWait
等待一个/全部 bit 的最大时间
返回值
EventBits_t
事件组的当前值,这个值表示函数返回时,事件组中哪些位(bits)是被设置(1)的
Example Code:Event Group Synchronization with Multiple Tasks
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "freeRTOS/event_groups.h"
EventGroupHandle_t eventGroup;
#define BIT_0 (1 << 0)
#define BIT_4 (1 << 4)
void Task1(void *pvParam)
{
printf("Task1 is running\n");
while (true)
{
printf("Task1 is begin to wait\n");
// xEventGroupWaitBits(eventGroup, BIT_0 | BIT_4, pdTRUE, pdFALSE, portMAX_DELAY);
// // 检测第一位和第四位是否被设置,如果设置则唤醒Task1
// // 检测完成后,第一位和第四位将被清除
// printf("BIT_0 or BIT_4 is set, Task1 is woken up\n");
xEventGroupWaitBits(eventGroup, BIT_0 | BIT_4, pdTRUE, pdTRUE, portMAX_DELAY);
printf("BIT_0 or BIT_4 is set, Task1 is woken up\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void Task2(void *pvParam)
{
printf("Task2 is running\n");
vTaskDelay(pdMS_TO_TICKS(1000));
while (true)
{
printf("Task2 is begin to set bit0\n");
xEventGroupSetBits(eventGroup, BIT_0);
vTaskDelay(pdMS_TO_TICKS(5000));
printf("Task2 is begin to set bit4\n");
xEventGroupSetBits(eventGroup, BIT_4);
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
void app_main(void)
{
eventGroup = xEventGroupCreate(); // 创建事件组
if (eventGroup == NULL)
{
printf("Event group creation failed\n");
}
else
{
vTaskSuspendAll();
xTaskCreatePinnedToCore(Task1, "Task1", 2048, NULL, 1, NULL, 0);
xTaskCreatePinnedToCore(Task2, "Task2", 2048, NULL, 1, NULL, 0);
xTaskResumeAll();
}
}
Event Group Sync
wait
和sync
的不同:
wait
等待事件组的 Task(设为 waitTask) 在进入 wait 状态后,等待设置事件组的 Task(设为 setTask)对事件组进行设置,waitTask 在检测到事件组满足要求后继续运行,setTask 在调用xEventGroupSetBits()
后不阻塞,继续运行
sync
setTask 在设置事件组的目标位后进入阻塞状态,等待其它 setTask 对事件组进行设置,当满足各 setTask 对事件组的要求后,所有进入阻塞状态的 setTask 同时进入运行状态
即 setTask 在设置事件组之后也在 wait 事件组
xEventGroupSync()
在事件组中设置位,然后等待在同一事件组中设置位的组合
此功能通常用于同步多个任务(通常称为任务集合),其中每个任务在继续之前必须等待其他任务到达同步点
如果uxBitsToWaitFor
参数指定的位被设置或在该时间内被设置,则该函数将在其时间到期之前返回,这种情况下,由uxBitsToWaitFor
指定的所有位将在函数返回之前自动清除
这个函数不能从中断中调用
#include "FreeRTOS.h"
#include "event_groups.h"
EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
const EventBits_t uxBitsToWaitFor,
TickType_t xTicksToWait );
参数
xEventGroup
需要设置 bit 的时间组
uxBitsToSet
一个位运算值,用于指定在事件组中要设置的位
uxBitsToWaitFor
一个位运算值,用于指定在事件组中要等待的位
xTicksToWait
等待 bits 的最大时间
返回值
EventBits_t
表示事件组的状态,具体包括以下两种情况:
-
等待的位被设置
- 如果
xEventGroupSync()
返回是因为所有等待的位被设置,则返回值是事件组中这些位在被清除前的状态
- 如果
-
超时到期
- 如果
xEventGroupSync()
返回是因为超时时间到期,则可能并非所有等待的位都被设置,返回值表示超时时事件组的状态
- 如果
Example Code:Event Group Synchronization
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "freeRTOS/event_groups.h"
EventGroupHandle_t eventGroup;
#define BIT_0 (1 << 0)
#define BIT_1 (1 << 1)
#define BIT_2 (1 << 2)
#define ALL_SYNC_BITS (BIT_0 | BIT_1 | BIT_2)
void Task0(void *pvParam)
{
printf("Task0 is running\n");
while (true)
{
vTaskDelay(pdMS_TO_TICKS(1000));
printf("Task0 set BIT_0\n");
xEventGroupSync(eventGroup, BIT_0, ALL_SYNC_BITS, portMAX_DELAY); // 设置 BIT_0,进入同步等待
printf("Task0 sync\n");
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
void Task1(void *pvParam)
{
printf("Task1 is running\n");
while (true)
{
vTaskDelay(pdMS_TO_TICKS(3000));
printf("Task1 set BIT_1\n");
xEventGroupSync(eventGroup, BIT_1, ALL_SYNC_BITS, portMAX_DELAY);
printf("Task1 sync\n");
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
void Task2(void *pvParam)
{
printf("Task2 is running\n");
while (true)
{
vTaskDelay(pdMS_TO_TICKS(6000));
printf("Task2 set BIT_2\n");
xEventGroupSync(eventGroup, BIT_2, ALL_SYNC_BITS, portMAX_DELAY);
printf("Task2 sync\n");
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
void app_main(void)
{
eventGroup = xEventGroupCreate(); // 创建事件组
if (eventGroup == NULL)
{
printf("Event group creation failed\n");
}
else
{
vTaskSuspendAll();
xTaskCreatePinnedToCore(Task0, "Task0", 2048, NULL, 1, NULL, 0);
xTaskCreatePinnedToCore(Task1, "Task1", 2048, NULL, 1, NULL, 0);
xTaskCreatePinnedToCore(Task2, "Task2", 2048, NULL, 1, NULL, 0);
xTaskResumeAll();
}
}
Notification
每个任务都有一个 32 位的通知值,该值在任务创建时初始
任务通知是直接发送给任务的事件,它可以解除接收任务的阻塞,并可选择更新接收任务的通知值
通知值有两种用法:按位、增量
Notification Sync
xTaskNotifyGive()
使目标任务的通知值递增
- RTOS 任务通知功能在默认情况下是启用的,并且可以从构建中排除(每个任务节省8字节)通过在
FreeRTOSConfig.h
设置configUSE_TASK_NOTIFICATIONS
为0
#include "FreeRTOS.h"
#include "task.h"
BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );
参数
xTaskToNotify
被通知的任务的句柄,其通知值递增(增量用法)
返回值
总是返回pdPASS
ulTaskNotifyTake()
任务可以使用 ulTaskNotifyTake()
来选择性地阻塞,等待通知值变为非零,在任务的通知值不为零时返回
在退出时可以选择将通知值清零(此时通知值类似于二值信号量)或将通知值递减(此时通知值更像计数信号量)
#include "FreeRTOS.h"
#include "task.h"
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );
参数
xClearCountOnExit
-
pdFALSE
- 每次成功调用后通知值减 1,类似计数信号量的效果
-
pdTRUE
- 每次成功调用后通知值重置为 0,类似二值信号量的效果
xTicksToWait
等待通知的最大时间
返回值
任务的通知值在被递减或清除之前的值
Example Code:Simple Task Notification in FreeRTOS
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "freeRTOS/event_groups.h"
TaskHandle_t task0Handle = NULL;
TaskHandle_t task1Handle = NULL;
void Task0(void *pvParam)
{
printf("Task0 is running\n");
while (true)
{
printf("Task0 is waitting for notification\n");
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
printf("Task0 got notification\n"); // Task0 等待 Task1 的通知
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void Task1(void *pvParam)
{
printf("Task1 is running\n");
vTaskDelay(pdMS_TO_TICKS(5000));
while (true)
{
printf("Task1 is sending notification\n");
xTaskNotifyGive(task0Handle); // Task1 通知 Task0
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
void app_main(void)
{
vTaskSuspendAll();
xTaskCreatePinnedToCore(Task0, "Task0", 2048, NULL, 1, &task0Handle, 0);
xTaskCreatePinnedToCore(Task1, "Task1", 2048, NULL, 1, &task1Handle, 0);
xTaskResumeAll();
}
Notification Value
xTaskNotify()
用于直接向任务发送事件并解除阻塞,并可选地以以下方式之一更新接收任务的通知值
- 将一个 32 位的数字写入通知值
- 增加一个(增量)通知值
- 设置一个或多个通知值
- 保持通知值不变
#include "FreeRTOS.h"
#include "task.h"
BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction );
参数
xTaskToNotify
被通知的 Task 句柄
ulValue
用于更新被通知任务的通知值,如何解释 ulValue 取决于 eAction 参数的值
eAction
-
eNoAction
任务被通知,但通知值不变 -
eSetBits
任务的通知值与 ulValue 进行按位或(or) 操作 -
eIncrement
任务的通知值加 1 -
eSetValueWithOverwrite
任务的通知值被无条件设置为 ulValue,即使之前已经有通知 -
eSetValueWithoutOverwrite
如果任务已经有通知待处理,则通知值不会被改变,xTaskNotify()
将返回pdFAIL
;如果任务没有待处理的通知,则其通知值会被设置为ulValue
返回值
在除eSetValueWithoutOverwrite
所有其他情况下,返回 pdPASS
xTaskNotifyWait()
如果接收任务已经被阻塞并等待通知,当一个通知到达时,接收任务将从阻塞状态移除并清除通知
#include "FreeRTOS.h"
#include "task.h"
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t *pulNotificationValue,
TickType_t xTicksToWait );
参数
ulBitsToClearOnEntry
- 在调用
xTaskNotifyWait()
时,通知值中的某些位会在函数进入时被清除 - 如果
ulBitsToClearOnEntry
设置为 0x01,则任务通知值中的第 0 位会在函数进入时被清除 - 如果设置为
0xffffffff
(ULONG_MAX
),则通知值的所有位都会被清除,相当于将通知值重置为 0 - 注意:仅当调用时没有挂起的通知时,清除操作才会执行
ulBitsToClearOnExit
- 在接收到通知后,在函数退出前通知值中的某些位会被清除
- 如果设置为
0xffffffff
(ULONG_MAX
),则通知值的所有位都会被清除
pulNotificationValue
- 用于将任务的通知值传递给调用者
- 保存的是在清除
ulBitsToClearOnExit
的位之前的通知值 - 如果不需要获取通知值,可以将其设置为
NULL
xTicksToWait
最大等待时间
返回值
pdTRUE
接收到了通知,或在调用时通知已挂起
pdFALSE
在等待超时时间内没有接收到通知
Example Code:Task Notification with Conditional Actions Based on Values
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "freeRTOS/event_groups.h"
TaskHandle_t task0Handle = NULL;
TaskHandle_t task1Handle = NULL;
void Task0(void *pvParam)
{
printf("Task0 is running\n");
uint32_t notifiedValue = 0;
while (true)
{
xTaskNotifyWait(0x00, 0xffffffff, ¬ifiedValue, portMAX_DELAY);
if (notifiedValue == 0x00000001) // 当接收到的通知值为 0x01 时,执行相应操作
{
printf("Task0 get notification: bit_0\n");
}
else if (notifiedValue == 0x00000002)
{
printf("Task0 get notification: bit_1\n");
}
else if (notifiedValue == 0x00000004)
{
printf("Task0 get notification: bit_2\n");
}
else
{
printf("Task0 get notification: unknown\n");
}
vTaskDelay(pdMS_TO_TICKS(3000));
}
}
void Task1(void *pvParam)
{
printf("Task1 is running\n");
vTaskDelay(5000 / portTICK_PERIOD_MS);
while (true)
{
printf("Task1 is sending notification\n");
xTaskNotify(task0Handle, 0x01, eSetValueWithOverwrite); // 发送 bit_0,覆盖之前的值
vTaskDelay(5000 / portTICK_PERIOD_MS);
xTaskNotify(task0Handle, 0x02, eSetValueWithOverwrite);
vTaskDelay(5000 / portTICK_PERIOD_MS);
xTaskNotify(task0Handle, 0x03, eSetValueWithOverwrite);
vTaskDelay(5000 / portTICK_PERIOD_MS);
xTaskNotify(task0Handle, 0x04, eSetValueWithOverwrite);
vTaskDelay(5000 / portTICK_PERIOD_MS);
}
}
void app_main(void)
{
vTaskSuspendAll();
xTaskCreatePinnedToCore(Task0, "Task0", 2048, NULL, 1, &task0Handle, 0);
xTaskCreatePinnedToCore(Task1, "Task1", 2048, NULL, 1, &task1Handle, 0);
xTaskResumeAll();
}
Stream Buffer
流数据(Stream Data)是指持续不断产生和传输的数据
这些数据通常是按时间顺序排列,并实时传输和处理, 与传统的批量数据处理不同,流数据需要在数据生成的同时进行即时处理,如实时传感器数据、社交媒体的消息流、金融交易数据、网络流量等
FreeRTOS 的流缓冲区和消息缓冲区设计假设只有一个写入者和一个读取者,写入者和读取者可以是不同的任务或中断,但不支持多个写入者或多个读取者
如果有多个写入者,必须将写入操作(如
xStreamBufferSend()
)放入临界区,并使用 0 的发送阻塞时间如果有多个读取者,必须将读取操作放入临界区,并使用 0 的接收阻塞时间
临界区(Critical Section)是指在多任务或多线程环境中,由于共享资源的访问需要互斥,所以必须确保在某段代码执行期间,不会被其他任务或中断打断的代码区域
xStreamBufferCreate()
使用动态分配的内存创建新的流缓冲区
要使用 xStreamBufferCreate()
函数并启用流缓冲区功能,需要确保:
- 在
FreeRTOSConfig.h
配置文件中将configSUPPORT_DYNAMIC_ALLOCATION
设置为 1
或未定义 - 将
FreeRTOS/source/stream_buffer.c
源文件包含在构建中
#include "FreeRTOS.h"
#include "stream_buffer.h"
StreamBufferHandle_t xStreamBufferCreate( size_t xBufferSizeBytes,
size_t xTriggerLevelBytes );
参数
xBufferSizeBytes
流缓冲区在任何时候能够容纳的总字节数
xTriggerLevelBytes
触发级别,决定了流缓冲区中必须有多少字节数据,才能使被阻塞的任务继续执行
- 如果触发级别设置为 1,任务会在缓冲区有 1 个字节时解除阻塞
- 如果触发级别设置较高(例如 10),任务会等待直到缓冲区中有足够的数据,或者直到阻塞时间到期
- 设置触发级别为 0 等同于设置为 1
- 触发级别不能大于缓冲区的总大小
- xStreamBufferCreate() 设置的触发级别只对进入阻塞状态的任务有效
返回值
StreamBufferHandle_t
已创建的流缓冲区的句柄
NULL
不能创建流缓冲区,因为 FreeRTOS 没有足够的堆内存来分配流缓冲区的数据结构和存储区域
xStreamBufferSend()
将字节发送到流缓冲区,字节被复制到流缓冲区中
#include "FreeRTOS.h"
#include "stream_buffer.h"
size_t xStreamBufferSend( StreamBufferHandle_t xStreamBuffer,
const void *pvTxData,
size_t xDataLengthBytes,
TickType_t xTicksToWait );
参数
xStreamBuffer
需要从写入字节的流缓冲区的句柄
pvTxData
指向缓冲区的指针,该缓冲区保存要复制到流缓冲区的字节,需要转换为 void*
xDataLengthBytes
从 pvTxData 复制到流缓冲区的最大字节数
xTicksToWait
如果流缓冲区包含的空间太少,无法容纳另一个 xDataLengthBytes 字节,则任务应保持在挂起状态以等待流缓冲区中有足够的可用空间的最大时间
返回值
写入流缓冲区的字节数
如果一个任务在将所有 xDataLengthBytes 写入缓冲区之前超时,它仍然会写入尽可能多的字节
xStreamBufferReceive()
从流缓冲区接收字节
#include "FreeRTOS.h"
#include "stream_buffer.h"
size_t xStreamBufferReceive( StreamBufferHandle_t xStreamBuffer,
void *pvRxData,
size_t xBufferLengthBytes,
TickType_t xTicksToWait );
参数
xStreamBuffer
需要从中接收字节的流缓冲区的句柄
pvRxData
指向接收到的字节要被复制到的数组的指针
xBufferLengthBytes
pvRxData 参数所指向的缓冲区的长度即在一次呼叫中接收的最大字节数
xStreamBufferReceive 将返回尽可能多的字节,直到 xBufferLengthBytes 设置的最大值
xTicksToWait
任务应保持在阻塞状态以等待数据可用的最大时间
返回值
实际从流缓冲区读取的字节数
如果对xStreamBufferReceive()的调用在 xBufferLengthBytes 可用之前超时,则将小于 xBufferLengthBytes
xStreamBufferReset()
将流缓冲区重置为其初始空状态,流缓冲区中的任何数据都将被丢弃
只有当没有任务阻塞等待发送到流缓冲区或从流缓冲区接收时,流缓冲区才能被重置
#include "FreeRTOS.h"
#include "stream_buffer.h"
BaseType_t xStreamBufferReset( StreamBufferHandle_t xStreamBuffer );
参数
xStreamBuffer
需要重置的流缓冲区的句柄
返回值
pdPASS
流缓冲区被重置
pdFAIL
有一个任务阻塞等待发送到流缓冲区或从流缓冲区读取,流缓冲区不会被重置
vStreamBufferDelete()
删除先前通过调用xStreamBufferCreate()或xStreamBufferCreateStatic()创建的流缓冲区
#include "FreeRTOS.h"
#include "stream_buffer.h"
void vStreamBufferDelete( StreamBufferHandle_t xStreamBuffer );
参数
xStreamBuffer
需要删除的流缓冲区的句柄
Example Code:Stream Buffer with Task Communication in FreeRTOS
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "freeRTOS/stream_buffer.h"
#include "esp_log.h"
#include <string.h>
StreamBufferHandle_t StreamBufferHandle = NULL;
TaskHandle_t task0Handle = NULL;
TaskHandle_t task1Handle = NULL;
void Task0(void *pvParam)
{
printf("Task0 is running\n");
char txBuffer[50] = {0};
int bufferSize = 0;
int sendBytes = 0;
int i = 0;
vTaskDelay(pdMS_TO_TICKS(8000));
while (true)
{
i++;
bufferSize = sprintf(txBuffer, "Task0 send i:%d to stream buffer\n", i) * sizeof(char); // 将数据写入 txBuffer
sendBytes = xStreamBufferSend(StreamBufferHandle, (void *)txBuffer, bufferSize, portMAX_DELAY); // 发送数据到 Stream Buffer
printf("Task0 send string:%s\n", txBuffer);
printf("sendBytes:%d, bufferSize:%d\n", sendBytes, bufferSize);
vTaskDelay(pdMS_TO_TICKS(3000));
}
}
void Task1(void *pvParam)
{
printf("Task1 is running\n");
vTaskDelay(pdMS_TO_TICKS(5000));
int receiveBytes = 0;
char rxBuffer[50] = {0};
while (true)
{
memset(rxBuffer, 0, sizeof(rxBuffer));
receiveBytes = xStreamBufferReceive(StreamBufferHandle, (void *)rxBuffer, sizeof(rxBuffer), portMAX_DELAY); // 从 Stream Buffer 接收数据
// 由于需要接收的数据大于 rxBuffer 大小,会分成多次接收
ESP_LOGI("Task1", "receiveBytes:%d, rxBuffer:%s", receiveBytes, rxBuffer);
// 由输出可知:
// 对于前几次发送的数据,即使没有超过设定的触发级别,Task1 也能接收到数据
// 这是因为此时 Task1 还没有进入阻塞状态,会直接读取 Stream Buffer 中的数据
// 在此之后,由于 Stream Buffer 中的数据量小于触发级别,Task1 进入阻塞状态
// 此后发送的数据只有在超过触发级别时,Task1 才能接收到数据
// 如果在 Task0 发送数据之前先阻塞比 Task1 执行接收数据操作前的时间长的时间
// 即在 Task0 发送数据前,Task1 已经进入阻塞状态,则所有数据都要超过触发级别才能被接收
// 即:xStreamBufferCreate() 设置的触发级别只对进入阻塞状态的任务有效
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void app_main(void)
{
StreamBufferHandle = xStreamBufferCreate(1000, 100); // 创建 Stream Buffer,设置大小为 1000 字节,触发级别为 100 字节
if (StreamBufferHandle != NULL)
{
ESP_LOGI("app_main", "StreamBuffer create success");
vTaskSuspendAll();
xTaskCreatePinnedToCore(Task0, "Task0", 1024 * 5, NULL, 1, &task0Handle, 0);
xTaskCreatePinnedToCore(Task1, "Task1", 1024 * 5, NULL, 1, &task1Handle, 0);
xTaskResumeAll();
}
else
{
ESP_LOGE("app_main", "StreamBuffer create failed");
}
}
xStreamBufferSpacesAvailable()
查询流缓冲区以查看它包含多少空闲空间,这等于在流缓冲区满之前可以发送到流缓冲区的数据量
#include "FreeRTOS.h"
#include "stream_buffer.h"
size_t xStreamBufferSpacesAvailable( StreamBufferHandle_t xStreamBuffer );
参数
xStreamBuffer
需要拆线呢的流缓冲区句柄
返回值
在流缓冲区满之前可以写入流缓冲区的字节数
Example Code:Buffer Space Monitoring with Stream Buffer in FreeRTOS
过大的 Stream Buffer Size 会浪费本就不多的单片机资源,通过除sendTask
和receiveTask
以外的第三个 Task 监视 Buffer 中的剩余可用空间,可以帮我们将 Buffer Size 调整为合适的值
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "freeRTOS/stream_buffer.h"
#include "esp_log.h"
#include <string.h>
StreamBufferHandle_t StreamBufferHandle = NULL;
TaskHandle_t task0Handle = NULL;
TaskHandle_t task1Handle = NULL;
void Task0(void *pvParam)
{
printf("Task0 is running\n");
char txBuffer[50] = {0};
int bufferSize = 0;
int sendBytes = 0;
int i = 0;
vTaskDelay(pdMS_TO_TICKS(8000));
while (true)
{
i++;
bufferSize = sprintf(txBuffer, "Task0 send i:%d to stream buffer\n", i) * sizeof(char); // 将数据写入 txBuffer
sendBytes = xStreamBufferSend(StreamBufferHandle, (void *)txBuffer, bufferSize, portMAX_DELAY); // 发送数据到 Stream Buffer
printf("Task0 send string:%s\n", txBuffer);
printf("sendBytes:%d, bufferSize:%d\n", sendBytes, bufferSize);
vTaskDelay(pdMS_TO_TICKS(3000));
}
}
void Task1(void *pvParam)
{
printf("Task1 is running\n");
vTaskDelay(pdMS_TO_TICKS(5000));
int receiveBytes = 0;
char rxBuffer[50] = {0};
while (true)
{
memset(rxBuffer, 0, sizeof(rxBuffer));
receiveBytes = xStreamBufferReceive(StreamBufferHandle, (void *)rxBuffer, sizeof(rxBuffer), portMAX_DELAY); // 从 Stream Buffer 接收数据
ESP_LOGI("Task1", "receiveBytes:%d, rxBuffer:%s", receiveBytes, rxBuffer);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void Task2(void *pvParam)
{
printf("Task2 is running\n");
vTaskDelay(pdMS_TO_TICKS(5000));
int bufferSpace = 0;
int minBufferSpace = 1000;
while (true)
{
bufferSpace = xStreamBufferSpacesAvailable(StreamBufferHandle); // 获取 Stream Buffer 中剩余空间
if (bufferSpace < minBufferSpace)
{
minBufferSpace = bufferSpace; // 记录 Stream Buffer 中最小的剩余空间,用于后续分析
}
ESP_LOGI("Task2", "bufferSpace:%d, minBufferSpace:%d", bufferSpace, minBufferSpace);
vTaskDelay(pdMS_TO_TICKS(3000));
}
}
void app_main(void)
{
StreamBufferHandle = xStreamBufferCreate(1000, 100); // 创建 Stream Buffer,设置大小为 1000 字节,触发级别为 100 字节
if (StreamBufferHandle != NULL)
{
ESP_LOGI("app_main", "StreamBuffer create success");
vTaskSuspendAll();
xTaskCreatePinnedToCore(Task0, "Task0", 1024 * 5, NULL, 1, &task0Handle, 0);
xTaskCreatePinnedToCore(Task1, "Task1", 1024 * 5, NULL, 1, &task1Handle, 0);
xTaskCreatePinnedToCore(Task2, "Task2", 1024 * 5, NULL, 1, NULL, 0);
xTaskResumeAll();
}
else
{
ESP_LOGE("app_main", "StreamBuffer create failed");
}
}
Message Buffer
Message Buffer 是 FreeRTOS 提供的一种任务间通信机制,用于传递定长消息数据
与流缓冲区不同,消息缓冲区处理的是具有明确边界的消息(一次一条数据),通常用于需要精确数据传输的场景
它支持阻塞和非阻塞的操作,适用于实时系统中对消息传递有严格要求的应用,如传感器数据、命令传递等
Message Buffer 是专门设计用来传递定长消息的缓冲区,适用于任务间的消息通信,它关注完整的数据块传递
Stream Buffer 是用于字节流传输的缓冲区,适用于需要连续传输数据的场景,允许更灵活的字节流操作
xMessageBufferCreate()
使用动态分配的内存创建一个新的消息缓冲区
为了使 xMessageBufferCreate() 可用,必须在 FreeRTOSConfig.h
中将 configSUPPORT_DYNAMIC_ALLOCATION
设置为 1 或留空未定义
启用消息缓冲区功能需要在构建中包含 FreeRTOS/source/stream_buffer.c
源文件,因为消息缓冲区是基于流缓冲区实现的
#include "FreeRTOS.h"
#include "message_buffer.h"
MessageBufferHandle_t xMessageBufferCreate( size_t xBufferSizeBytes );
参数
xBufferSizeBytes
返回值
MessageBufferHandle_t
消息缓冲区已成功创建,返回值应存储为已创建消息缓冲区的句柄
NULL
不能创建消息缓冲区,因为FreeRTOS没有足够的堆内存来分配消息缓冲区的数据结构和存储区域
xMessageBufferSend()
将离散消息发送到消息缓冲区,消息可以是任何长度,只要符合缓冲区的可用空间
#include "FreeRTOS.h"
#include "message_buffer.h"
size_t xMessageBufferSend( MessageBufferHandle_t xMessageBuffer,
const void *pvTxData,
size_t xDataLengthBytes,
TickType_t xTicksToWait );
参数
xMessageBuffer
发送消息的消息缓冲区的句柄
pvTxData
指向要复制到消息缓冲区中的消息的指针
xDataLengthBytes
从pvTxData复制到消息缓冲区的字节数
-
当将一条消息写入消息缓冲区时,除了写入实际的消息数据外,还会写入一个额外的
sizeof(size_t)
字节来存储消息的长度-
sizeof(size_t)
通常是 4 字节(在 32 位架构上),用于存储消息的长度信息 - 如果 xDataLengthBytes 设置为 20,意味着实际的消息数据长度为 20 字节,然而,由于消息长度本身也需要存储在缓冲区中,消息缓冲区的实际内存使用将增加 24 字节:20 字节用于存储消息数据,另外 4 字节用于存储消息长度信息
-
xTicksToWait
如果消息缓冲区没有足够的空间,则调用任务应保持在阻塞状态以等待消息缓冲区中有足够的空间可用的最大时间
返回值
写入消息缓冲区的字节数,如果在有足够的空间将消息写入消息缓冲区之前,对xMessageBufferSend()
的调用超时,则返回0
,此时不会写入任何数据
如果调用没有超时,则返回xDataLengthBytes
xMessageBufferReceive()
从RTOS消息缓冲区接收离散消息,消息可以是可变长度的,并且可以从缓冲区中复制出来
#include "FreeRTOS.h"
#include "message_buffer.h"
size_t xMessageBufferReceive( MessageBufferHandle_t xMessageBuffer,
void *pvRxData,
size_t xBufferLengthBytes,
TickType_t xTicksToWait );
参数
xMessageBuffer
从中接收消息的消息缓冲区的句柄
pvRxData
指向缓冲区的指针,接收到的消息将被复制到其中
xBufferLengthBytes
pvRxData参数所指向的缓冲区的长度,设置了可以接收的消息的最大长度
如果 xBufferLengthBytes 太小而不能容纳下一条消息,则消息将留在消息缓冲区中并返回0
xTicksToWait
任务应保留的最大时间,如果消息缓冲区为空,则阻塞状态等待消息
返回值
从消息缓冲区中读取的消息的长度(以字节为单位)
如果 xMessageBufferReceive()在消息可用之前超时,则返回0
如果消息的长度大于 xBufferLengthBytes,则消息将留在消息缓冲区中,并返回0
xMessageBufferReset()
将消息缓冲区重置为其初始空状态,消息缓冲区中的任何数据都将被丢弃
只有当没有任务阻塞等待发送到消息缓冲区或从消息缓冲区接收消息缓冲区时,才能重置消息缓冲区
#include "FreeRTOS.h"
#include "message_buffer.h"
BaseType_t xMessageBufferReset( MessageBufferHandle_t xMessageBuffer );
参数
xMessageBuffer
需要重置的消息缓冲区的句柄
返回值
pdPASS
消息缓冲区被重置
pdFAIL
有一个任务阻塞等待发送到消息缓冲区或从消息缓冲区读取,消息缓冲区不会被重置
vMessageBufferDelete()
删除消息缓冲区
#include "FreeRTOS.h"
#include "message_buffer.h"
void vMessageBufferDelete( MessageBufferHandle_t xMessageBuffer );
参数
xMessageBuffer
需要删除的消息缓冲区的句柄
Example Code:Message Buffer Communication with Multiple Tasks
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "freeRTOS/message_buffer.h"
#include "esp_log.h"
#include <string.h>
MessageBufferHandle_t MessageBufferHandle = NULL;
TaskHandle_t task0Handle = NULL;
TaskHandle_t task1Handle = NULL;
void Task0(void *pvParam)
{
printf("Task0 is running\n");
char txBuffer[50] = {0};
int bufferSize = 0;
int sendBytes = 0;
for (int i = 0; i < 3; i++)
{
bufferSize = sprintf(txBuffer, "Task0 send i:%d to message buffer\n", i) * sizeof(char);
sendBytes = xMessageBufferSend(MessageBufferHandle, (void *)txBuffer, bufferSize, portMAX_DELAY);
printf("Task0 send string:%s\n", txBuffer);
printf("sendBytes:%d, bufferSize:%d\n", sendBytes, bufferSize);
vTaskDelay(pdMS_TO_TICKS(200));
}
vTaskDelete(NULL);
}
void Task1(void *pvParam)
{
printf("Task1 is running\n");
vTaskDelay(pdMS_TO_TICKS(5000));
int receiveBytes = 0;
char rxBuffer[200] = {0}; // 通过修改这个buffer的大小,可以看到需要接收的数据大小大于 buffer 的大小时,会返回0,且不接收数据
// 如果是 Stream Buffer,则可以分成多次接收
while (true)
{
memset(rxBuffer, 0, sizeof(rxBuffer));
receiveBytes = xMessageBufferReceive(MessageBufferHandle, (void *)rxBuffer, sizeof(rxBuffer), portMAX_DELAY);
// 即使接收的buffer大小足够大,每次接收的数据也只有一条完整消息
// 如果是 StreamBuffer,则可以接收多条数据
ESP_LOGI("Task1", "receiveBytes:%d, rxBuffer:%s", receiveBytes, rxBuffer);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void app_main(void)
{
MessageBufferHandle = xMessageBufferCreate(200); // 通过修改这个buffer的大小,可以看到发送的数据大小大于 buffer 的大小时,会返回0,且不发送数据
// 不能过小,否则会触发 assert 断言,导致程序重启
if (MessageBufferHandle != NULL)
{
ESP_LOGI("app_main", "MessageBuffer create success");
vTaskSuspendAll();
xTaskCreatePinnedToCore(Task0, "Task0", 1024 * 5, NULL, 1, &task0Handle, 0);
xTaskCreatePinnedToCore(Task1, "Task1", 1024 * 5, NULL, 1, &task1Handle, 0);
xTaskResumeAll();
}
else
{
ESP_LOGE("app_main", "MessageBuffer create failed");
}
}