# ESP32 FreeRTOS **Repository Path**: leisure27/esp32-free-rtos ## Basic Information - **Project Name**: ESP32 FreeRTOS - **Description**: ESP32 FreeRTOS学习总结:任务管理、任务间传参、队列、信号量、事件组等待、事件组同步、任务通知、流媒体缓存、消息缓存。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 6 - **Forks**: 2 - **Created**: 2023-05-13 - **Last Updated**: 2026-01-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README 2023.5.11 FreeRTOS中文数据手册:https://www.freertos.org/zh-cn-cmn-s/RTOS.html 感谢以下两位B站UP主的教程:孤独的二进制、Michael_ee ### 1.Task 创建任务常用API: | 任务函数 | 描述 | | ----------------------- | -------------------------------- | | xTaskCreate() | 使用动态的方法创建一个任务 | | xTaskCreatePinnedToCore | 指定任务的运行核心(最后一个参数) | | vTaskDelete(NULL) | 删除当前任务 | ```c++ BaseType_t xTaskCreate(TaskFunction_t pxTaskCode, // 任务函数名 const char *const pcName, // 任务备注 const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小 void *const pvParameters, // 传入的参数 UBaseType_t uxPriority, // 任务优先级 TaskHandle_t *const pxCreatedTask); // 任务句柄 ``` #### 任务间传参 任务间传参可以使用多种方式,常见的为: 1. **使用全局变量**:需要注意并发读写的问题,**当有两个任务及以上对全局变量进行读写时,需要使用信号量或互斥量进行保护。** 2. **使用队列**:需要注意队列的大小和数据类型的一致性,**不需要使用信号量或互斥量进行保护**。`队列的读写效率相比全局变量慢一些` 使用全局变量进行传参时: * 传入参数:传递的为指针,且必须进行强制类型转换为空指针`(void *)pt` * 接收参数:把传递过来的空指针进行强制类型转换,转换为对应传输的类型指针 ##### 传递整数 ```c #include int a = 1; void mytask(void *pt) { int *b = (int *)pt; Serial.println(*b); while (1) { } } void setup() { Serial.begin(115200); xTaskCreatePinnedToCore(mytask, "", 1024 * 3, (void *)&a, 1, NULL, 1); } void loop() {} ``` 输出结果为1 ##### 传递数组 ```c #include int arr[] = {1, 2, 3}; void mytask(void *pt) { int *b = (int *)pt; int len = sizeof(arr) / sizeof(int); // 数组的长度,注意这里指针占4个字节,要用原数组名 Serial.println(len); for (int i = 0; i < len; i++) { Serial.print(*(b + i)); // 输出数组元素 Serial.print(","); } while (1) { } } void setup() { Serial.begin(115200); // 数组名代表数组元素的首地址,所以不需要& xTaskCreatePinnedToCore(mytask, "", 1024 * 3, (void *)arr, 1, NULL, 1); vTaskDelete(NULL); } void loop() {} ``` ##### 传递结构体 ```c #include typedef struct { int a; int b; } Mystruct; Mystruct test1 = {1, 2}; void mytask(void *pt) { Mystruct *test2 = (Mystruct *)pt; // 强制类型转换为结构体指针 Serial.println(test2->a); Serial.println(test2->b); while (1) { } } void setup() { Serial.begin(115200); xTaskCreatePinnedToCore(mytask, "", 1024 * 3, (void *)&test1, 1, NULL, 1); vTaskDelete(NULL); } void loop() {} ``` ##### 传递字符串 ```C++ #include const char *str = "hello,world!"; void mytask(void *pt) { char *pstr = (char *)pt; Serial.println(pstr); // 输出hello,world vTaskDelete(NULL); } void setup() { Serial.begin(115200); xTaskCreatePinnedToCore(mytask, "", 1024 * 3, (void *)str, 1, NULL, 1); vTaskDelete(NULL); } void loop() {} ``` #### 任务的优先级 注意:中断任务的优先级永远高于任何任务的优先级。 在ESP32中,默认一共有25个优先级别,最低为0,最高为24。(可修改相关的配置函数进行修改优先级的数目超过25,但是不建议,级别越高,越占内存)。 * 同优先级的任务:FreeRTOS将采用循环调度算法来运行他们,也就是交替执行同优先级的任务。每个任务执行一个时间片,然后将CPU时间片分配给另一个任务。 * 优先级别高的任务先被创建和运行。 **任务的调度:** * 在FreeRTOS中,`vTaskDelay()`和`vTaskDelayUntil()`函数**可以暂停当前任务的执行,等待一段时间后再继续执行。**(让其他任务有机会执行) * `taskYIELD()`函数:立即将CPU时间片退让给同等级或更高优先级的任务,如果没有其他任务等待执行,则当前任务会立即继续执行。(简单的说,就是让其他任务执行) #### 任务的挂起和恢复 ![](images/1.png) **任务的状态:running、ready、blocked、suspended(挂起,暂停)** * running:运行状态,如果MCU只有一个内核,那么在任何给定时间内只能有一个任务处于运行状态。 * ready:准备状态(任务刚被创建时,准备执行),不处于堵塞或挂起状态(没有获得CPU执行权限,等待执行状态),因为同等级或更高优先级的任务正在执行 * blocked:使用了`vTaskDelay()或delay()`函数 * suspended:挂起状态,挂起之后,任务被恢复才能继续执行 ```c // API: TaskHandle_t pxtask = NULL; // 创建任务的句柄 xTaskCreatePinnedToCore(task1, "", 1024 * 2, NULL, 1, &pxtask, 1); vTaskSuspend(pxtask); // 挂起任务,任务不再执行 vTaskResume(pxtask); // 恢复被挂起的任务,继续执行 vTaskSuspendAll(); // 挂起所有函数,挂起后不可以执行 vTaskResumeAll(); // 恢复所有挂起函数 ``` #### 任务的堆栈设置和调试 创建任务时,如果给任务分配的内存空间过小,会导致程序不断重启。如果分配的内存空间过多,会造成资源浪费。 ```c++ // API: ESP.getHeapSize() // 本程序Heap最大尺寸(空间总大小) ESP.getFreeHeap() // 当前Free Heap最大尺寸(当前可用剩余空间大小) uxTaskGetStackHighWaterMark(taskHandle) // 计算当前任务剩余多少内存 ``` 示例程序: ```c TaskHandle_t taskHandle; // 创建任务的句柄 void setup() { Serial.begin(115200); xTaskCreatePinnedToCore(mytask, "", 1024*3 , NULL, 1, &taskHandle, 1); int waterMark = uxTaskGetStackHighWaterMark(taskHandle); Serial.print("Task Free Memory: "); // 任务剩余空间 Serial.print(waterMark); vTaskDelete(NULL); } ``` #### vTaskDelay()和delay() 一个tick的时间是由FreeRTOS的时钟节拍周期和时钟频率决定的,可以通过配置文件进行设置。默认情况下`1 tick = 1ms` * `vTaskDelay()`函数:**以系统时钟节拍(tick)为单位进行延时**,例如vTaskDelay(100)表示让任务暂停100个系统时钟节拍的时间。 * `delay()`函数:是一个简单的延时函数,它通常在不需要多任务处理和系统保护的应用中使用。使用后会后边的程序都会被延迟执行。 #### vTaskDelayUntil() `vTaskDelayUntil`函数比`vTaskDelay`函数定时精准。 ```c // API TickType_t xLastWakeTime = xTaskGetTickCount(); // 获取当前时间 const TickType_t xFrequency = 3000; // 需要的时间间隔 vTaskDelayUntil(&xLastWakeTime, xFrequency); while(1){ vTaskDelayUntil(&xLastWakeTime, xFrequency); // 下边为需要运行的函数 } // ``` 示例程序: ```c #include void mytask(void *pt) { TickType_t xLastWakeTime = xTaskGetTickCount(); // 获取当前时间 const TickType_t xFrequency = 1000; // 需要的时间间隔 while (1) { vTaskDelayUntil(&xLastWakeTime, xFrequency); Serial.println(xTaskGetTickCount()); // 输出当前时间进行验证 } } void setup() { Serial.begin(115200); xTaskCreatePinnedToCore(mytask, "", 1024 * 3, NULL, 1, NULL, 1); vTaskDelete(NULL); } void loop() {} ``` ### 2.Queue 2023.5.12 队列:先入先出(FIFO,first in first out) 使用方法: 1. 创建队列:长度,尺寸(每个内存空间存储的数据大小) 2. 发送数据到队列中 3. 从队列中取数据 ```c++ // portMAX_DELAY - 无限Block // TickType_t timeOut = portMAX_DELAY; // 无限等待,直到队列中有数据,或者等待数据有空位置可以存储新数据 TickType_t timeOut = 10; xStatus = xQueueSend(Qhandle, &i, timeOut); // 往队列里发送数据,如果队列里内容是满的就等待10ms再次尝试发送 ``` | API | 描述 | | -------------------------------- | ---------------------------------------------------------------------- | | xQueueCreate() | 创建一个队列 | | xQueueSend() | 往队列里写数据 | | xQueueReceive | 从队列里读数据 | | uxQueueMessagesWaiting(队列句柄) | 返回值为队列中参数的个数,可用于接收数据时,先判断一下队列里是否有数据 | ```c++ // 创建一个队列 QueueHandle_t Qhandle = xQueueCreate(5, sizeof(int)); // 创建一个队列,长度为5,每个空间的大小为int ``` #### 队列存储int数据 ```c #include // 创建队列的句柄 QueueHandle_t Qhandle = xQueueCreate(5, sizeof(int)); void send(void *pt) { int i = 0; while (1) { if (xQueueSend(Qhandle, &i, portMAX_DELAY) != pdPASS) { Serial.println(F("队列数据发送失败")); } else { Serial.print(F("发送成功:")); Serial.println(i); } i++; if (i == 8) i = 0; vTaskDelay(1000); } } void receive(void *pt) { int j = 0; // 存储接收的队列数据 while (1) { if (xQueueReceive(Qhandle, &j, portMAX_DELAY) != pdPASS) { Serial.println(F("接收失败")); } else { Serial.print(F("接收成功:")); Serial.println(j); } } } void setup() { Serial.begin(115200); xTaskCreatePinnedToCore(send, "", 1024 * 5, NULL, 1, NULL, 1); // 发送数据 xTaskCreatePinnedToCore(receive, "", 1024 * 5, NULL, 1, NULL, 1); // 接收数据 vTaskDelete(NULL); } void loop() {} ``` 运行结果: ```c 发送成功:0 接收成功:0 发送成功:1 接收成功:1 发送成功:2 接收成功:2 发送成功:3 接收成功:3 ``` #### 队列传递结构体(重点) 跟上面的案例类似,只是队列中每个元素类型为`struct`,并且发送和接收的数据存储也要设置为`struct`类型 ```c #include // 创建一个结构体 typedef struct { int a; int b; } Mystruct; // 创建队列的句柄 QueueHandle_t Qhandle = xQueueCreate(5, sizeof(Mystruct)); void send(void *pt) { Mystruct struct1 = {1, 2}; while (1) { if (xQueueSend(Qhandle, &struct1, portMAX_DELAY) != pdPASS) { Serial.println(F("队列数据发送失败")); } else { Serial.print(F("发送成功:")); struct1.a++; Serial.println(struct1.a); } vTaskDelay(1000); } } void receive(void *pt) { Mystruct struct2; // 接收结构体数据 while (1) { if (xQueueReceive(Qhandle, &struct2, portMAX_DELAY) != pdPASS) { Serial.println(F("接收失败")); } else { Serial.print(F("接收成功:")); Serial.println(struct2.a); } } } void setup() { Serial.begin(115200); xTaskCreatePinnedToCore(send, "", 1024 * 5, NULL, 1, NULL, 1); // 发送数据 xTaskCreatePinnedToCore(receive, "", 1024 * 5, NULL, 1, NULL, 1); // 接收数据 vTaskDelete(NULL); } void loop() {} ``` 运行结果:按照FIFO的规则进行数据的发送和接收 ```c 发送成功:2 接收成功:1 发送成功:3 接收成功:2 发送成功:4 接收成功:3 ``` #### 队列传递大型数据时 例如传递字符串。传递大型数据时,把指针对应的数据进行传递。 * `malloc()`函数:在使用malloc开辟空间时,使用完一定要释放空间,如果不释放会造成内存泄漏。**malloc()函数返回的实际是一个无类型指针,必须在其前面加上指针类型强制转换才可以使用**。指针自身 = (指针类型*)malloc(sizeof(指针类型)*数据数量) ```c int *p = NULL; p = (int *)malloc(sizeof(int)*10); // 使用完之后采用free()进行释放 free(p); p = NULL; // 让其重新指向NULL ``` #### 队列的多进单出:多个任务写,一个任务读 ![](https://s1.ax1x.com/2023/04/07/ppT4OGF.png) 多个任务把数据写入一个队列,一个任务进行读。设置写入的任务级别为同级别,读任务的优先级别要比写任务高一级别。 * 不推荐这种方式:容易造成系统工作混乱。**最好的工作方式是一个队列只有一个写操作,可以有多个读操作,但是写操作只能有一个**。 #### 队列集合(常用): ![](images/2.png) 多个队列,但是每个队列只有一个写操作,一个读操作(读取所有队列) 实现步骤: 1. 创建队列集合的句柄:同时指定队列集合的总长度 2. 将已创建的队列添加到集合中 3. 创建一个句柄:从队列集合中获取有数据的队列 ```c++ QueueHandle_t Qhandle1 = xQueueCreate(5, sizeof(int)); // 队列1 QueueHandle_t Qhandle2 = xQueueCreate(5, sizeof(int)); // 队列2 QueueSetHandle_t QueueSet = xQueueCreateSet(10); // 队列集合句柄,10为队列的总长度 xQueueAddToSet(Qhandle1, QueueSet); // 把队列1加入到队列集合中 xQueueAddToSet(Qhandle2, QueueSet); // 把队列2加入到队列集合中 QueueSetMemberHandle_t QueueData = xQueueSelectFromSet(QueueSet, portMAX_DELAY); // 从队列集合中获取有数据的队列, QueueData为句柄 ``` 示例程序:**这个程序编译不成功,还没有解决** ```c #include QueueHandle_t Qhandle1 = xQueueCreate(5, sizeof(int)); // 队列1 QueueHandle_t Qhandle2 = xQueueCreate(5, sizeof(int)); // 队列2 QueueSetHandle_t QueueSet = xQueueCreateSet(10); // 队列集合句柄 xQueueAddToSet(Qhandle1, QueueSet); // 把队列1加入到队列集合中 xQueueAddToSet(Qhandle2, QueueSet); // 把队列2加入到队列集合中 QueueSetMemberHandle_t QueueData = xQueueSelectFromSet(QueueSet, portMAX_DELAY); // 从队列集合中获取有数据的队列 void send1(void *pt) { int i = 1; // 任务1要发送的数据 while (1) { if (xQueueSend(Qhandle1, &i, portMAX_DELAY) != pdPASS) { Serial.println("发送失败"); } else { Serial.println("发送成功"); } vTaskDelay(1000); } } void send2(void *pt) { int i = 2; // 任务2要发送的数据 while (1) { if (xQueueSend(Qhandle2, &i, portMAX_DELAY) != pdPASS) { Serial.println("发送失败"); } else { Serial.println("发送成功"); } vTaskDelay(1000); } } void receive(void *pt) { int i; // 存储接收数据 while (1) { if (xQueueReceive(QueueData, &i, portMAX_DELAY) != pdPASS) // portMAX_DELAY,一直等待,直到队列中有数据 { Serial.println("接收失败"); } else { Serial.print("接收成功:"); Serial.println(i); } // vTaskDelay(1000); // 采用了portMAX_DELAY,这里就不需要delay了 } } void setup() { Serial.begin(9600); Serial.println("队列创建成功"); xTaskCreatePinnedToCore(send1, "", 1024 * 5, NULL, 1, NULL, 1); // 两个相同的优先级别,轮流发送数据 xTaskCreatePinnedToCore(send2, "", 1024 * 5, NULL, 1, NULL, 1); xTaskCreatePinnedToCore(receive, "", 1024 * 5, NULL, 2, NULL, 1); // 优先级别2,只要队列中有数据,就读 } void loop() { } ``` #### 队列邮箱(常用): ![](images/3.png) **只有一个队列,一个任务写,多个任务读** ```c // API QueueHandle_t Mailbox = xQueueCreate(5, sizeof(int)); // 创建一个队列邮箱 xQueueOverwrite(); // 往队列中写数据 xQueuePeek(); // 从队列中读数据 ``` 示例程序:运行不成功 ```c #include QueueHandle_t Mailbox = xQueueCreate(5, sizeof(int)); void send(void *pt) { int i = 1; // 任务1要发送的数据 while (1) { if (xQueueOverwrite(Mailbox, &i) != pdPASS) { Serial.println("发送失败"); } else { Serial.println("发送成功"); i++; } vTaskDelay(1000); } } void receive1(void *pt) { int i; // 存储接收数据 while (1) { if (xQueuePeek(Mailbox, &i, 1000) != pdPASS) // portMAX_DELAY,一直等待,直到队列中有数据 { Serial.println("接收失败"); } else { Serial.print("接收成功:"); Serial.println(i); } } } void receive2(void *pt) { int i; // 存储接收数据 while (1) { if (xQueuePeek(Mailbox, &i,1000) != pdPASS) // portMAX_DELAY,一直等待,直到队列中有数据 { Serial.println("接收失败"); } else { Serial.print("接收成功:"); Serial.println(i); } } } void setup() { Serial.begin(9600); Serial.println("队列创建成功"); xTaskCreatePinnedToCore(send, "", 1024 * 5, NULL, 2, NULL, 1); xTaskCreatePinnedToCore(receive1, "", 1024 * 5, NULL, 2, NULL, 1); xTaskCreatePinnedToCore(receive2, "", 1024 * 5, NULL, 2, NULL, 1); } void loop() { } ``` ### 3.信号量 信号量分类:二进制信号量、计数信号量、互斥信号量。 信号量就像红绿灯一样,控制车辆的通行。 信号量**常用于控制对共享资源的访问和任务同步**。信号量对于控制共享资源访问的场景相当于一个**上锁机制**,代码只有获得这个锁的钥匙才能执行。 * **使用队列、信号量,都可以实现互斥访问**。 #### 二进制信号量(常用) 二值信号量常用于互斥访问或同步,二值信号量和互斥信号量非常类似,但是互斥信号量拥有优先级继承机制,二值信号量没有优先级继承。 * **二进制信号量可以用于一个任务控制另一个任务的运行与堵塞。** * 二进制信号量表示只有两个值:0和1 * 二值信号量相当于长度为1的队列 * 二进制信号量只有两种状态:已触发和未触发,类似于一个开关。当一个任务等待一个已经触发的二进制信号量是,它会立即获得信号量,如果信号量未被触发,任务将被堵塞直到信号量被触发。 * 可以避免资源冲突和死锁问题,提高系统的可靠性和效率 ```c // API SemaphoreHandle_t xHandler = xSemaphoreCreateBinary(); // 创建二进制信号量 xSemaphoreGive(xHandler); // 释放信号量 xSemaphoreTake(xHanlder, timeout); // 在指定时间内获取信号量,返回值为pdPASS, 或者pdFAIL ``` **示例程序1:按键控制LED的亮灭(已验证)** ```c #include SemaphoreHandle_t xHandler = xSemaphoreCreateBinary(); // 创建二进制信号量 TickType_t timeOut = 1000; void task1(void *pt) { pinMode(23, OUTPUT); while (1) { if (xSemaphoreTake(xHandler, timeOut) == pdTRUE) { digitalWrite(23, !digitalRead(23)); } } } void task2(void *pt) { pinMode(22, INPUT_PULLUP); while (1) { if (digitalRead(22) == LOW) { xSemaphoreGive(xHandler); vTaskDelay(120); // button debounce } } } void setup() { Serial.begin(9600); xTaskCreatePinnedToCore(task1, "", 1024 * 5, NULL, 1, NULL, 1); // 两个相同的优先级别,轮流发送数据 xTaskCreatePinnedToCore(task2, "", 1024 * 5, NULL, 1, NULL, 1); } void loop() { } ``` 示例2:**采用二进制信号量对任务进行管理,对全局变量进行读写** * **该示例验证了,使用二进制信号量可以很好的控制任务的执行顺序。** ```c #include int a = 0; SemaphoreHandle_t xHandler = xSemaphoreCreateBinary(); // 创建二进制信号量 void task1(void *pt) { while (1) { xSemaphoreTake(xHandler, portMAX_DELAY); // 无限等待,直到获取信号量 for (int i = 0; i < 10; i++) { a++; printf("mytask1 a = %d\n", a); } xSemaphoreGive(xHandler); // 执行完之后,需要再次释放信号量 vTaskDelay(1000); } } void task2(void *pt) { while (1) { xSemaphoreTake(xHandler, portMAX_DELAY); // 无限等待,直到获取信号量 for (int i = 0; i < 10; i++) { a++; printf("mytask2 a = %d\n", a); } xSemaphoreGive(xHandler); // 执行完之后,需要再次释放信号量 vTaskDelay(1000); } } void setup() { Serial.begin(115200); xSemaphoreGive(xHandler); // 首先释放一次信号量,不然运行不了 xTaskCreatePinnedToCore(task1, "", 1024 * 5, NULL, 1, NULL, 1); // task1先获取信号量,执行一次 xTaskCreatePinnedToCore(task2, "", 1024 * 5, NULL, 1, NULL, 1); // 然后task2获取信号量,执行一次,task11再执行 } void loop() { } ``` 运行结果: * 采用二进制信号量: ```c mytask1 a = 1 mytask1 a = 2 mytask1 a = 3 mytask1 a = 4 mytask1 a = 5 mytask1 a = 6 mytask1 a = 7 mytask1 a = 8 mytask1 a = 9 mytask1 a = 10 mytask2 a = 11 mytask2 a = 12 mytask2 a = 13 mytask2 a = 14 mytask2 a = 15 mytask2 a = 16 mytask2 a = 17 mytask2 a = 18 mytask2 a = 19 mytask2 a = 20 mytask1 a = 21 ``` * 不采用二进制信号量运行结果: ```c ytask1 a = 1 mytask1 a = 2 mytask1 a = 3 mytask1 a = 4 mytask1 a = 6 mytask1 a = 7 mytask1 a = 8 mytask1 a = 9 **mytask1 a = 10** **mytask1 a = 11** **mytask2 a = 5** **mytask2 a = 12** mytask2 a = 13 ``` #### 计数信号量 与二进制不同的是,计数信号量可以有更多的状态。 * 计数信号量相当于长度大于1的队列,同二值信号量一样,不需要关系队列中存储了什么数据,只需要关心队列是否为空即可。 * 例如在一个停车场中有10个车位,车辆进入时,车位被占用(计数器减1),车开出去后(计数器加1),为0时表示没有可用的车位。 ```c // API uxSemaphoreGetCount( semphrHandle); // 获得计数型信号量的值 SemaphoreHandle_t semphrHandle = xSemaphoreCreateCounting(10,0);// 创建计数型信号量,参数1:最大值,参数2:初始值 xSemaphoreGive(semphrHandle); // 释放信号量 xSemaphoreTake(semphrHandle); // 获取信号量 ``` 使用场合:事件计数、资源管理 1. 共享资源的访问控制:当多个任务需要共享同一个资源时,可以使用计数信号量来控制资源的访问。每个任务需要访问资源时,都需要获取一个计数信号量,如果计数信号量的值为0,则任务会被堵塞,直到其他任务释放资源并增加计数信号量的值。这种方式可以避免资源冲突和死锁等问题。 2. 控制任务的执行顺序:有些情况下,需要控制任务的执行顺序,例如任务A必须在任务B执行完成之后才能执行。可以使用计数信号量来实现这种控制。任务B执行完成后,可以增加计数信号量的值,任务A等待计数信号量的值为1时,可以获取信号量并开始执行。 示例1:模拟停车场的停车位 ```c #include // 创建计数型信号量,参数1:最大值,参数2:初始值 SemaphoreHandle_t semphrHandle = xSemaphoreCreateCounting(5, 5); // 初值为5,代表初始有5个空车位 void carintask(void *pt) { int emptySpace = 0; // 空的停车位 BaseType_t iResult; while (1) { emptySpace = uxSemaphoreGetCount(semphrHandle); printf("emptySpace = %d\n", emptySpace); iResult = xSemaphoreTake(semphrHandle, 0); // 获取信号量 if (iResult == pdPASS) printf("One car in\n"); else printf("No Space\n"); vTaskDelay(1000); } } void caroutTask(void *pt) { while (1) { vTaskDelay(6000); xSemaphoreGive(semphrHandle); // 释放信号量 printf("One car out\n"); } } void setup() { Serial.begin(115200); xTaskCreatePinnedToCore(carintask, "", 1024 * 5, NULL, 1, NULL, 1); xTaskCreatePinnedToCore(caroutTask, "", 1024 * 5, NULL, 1, NULL, 1); } void loop() { } ``` #### 互斥信号量(常用) **与二进制信号量十分相似**。Mutex的工作原理可以想象成共享的资源被锁在一个箱子里,只有一把钥匙,有钥匙的任务才能对共享资源进行访问。 * **互斥量与二进制信号量的区别**:优先级继承,在FreeRTOS中,当一个任务持有一个互斥量时,该任务对共享资源的访问是独占的,其他试图获取该互斥量的任务将被堵塞。**当一个优先级更高的任务试图获取已经被持有的互斥量时,FreeRTOS会自动暂时提高持有互斥量任务的优先级别,使其具有与试图获取互斥量的任务相同的优先级别**。这样可以确保高优先级别的任务在获取共享资源时能够及时执行,并避免低优先级别任务长时间持有共享资源。**当持有互斥量的任务释放互斥量时,其优先级别将恢复到原始值,而不是保持被继承的优先级**。这样可以确保任务在不需要共享资源时恢复其原始优先级,以避免低优先级任务一直持有高优先级任务的优先级,导致高优先级任务无法及时执行其他任务。 * 也就是对于不同优先级别的任务,采用`mutex`对共享资源进行保护,如果任务均为同优先级别,可以采用`二进制信号量`进行共享资源保护 * 可以理解为`互斥量`是`二进制信号量`的升级版 * 注意:使用完立即释放钥匙 ```c SemaphoreHandle_t mutexHandler = xSemaphoreCreateMutex(); // 创建一个Mutex互斥量 xSemaphoreGive(mutexHandler); // 释放信号量 xSemaphoreTake(mutexHandler, timeout); // 在指定时间内获取信号量,返回值为pdPASS, 或者pdFAIL ``` 示例程序: ```c // 对于互斥量通常创建3个任务 #include SemaphoreHandle_t mutexHandler = xSemaphoreCreateMutex(); // 创建mutex句柄 void task1(void *pt) { printf("task1 begin\n"); while (1) { xSemaphoreTake(mutexHandler, portMAX_DELAY); printf("tsak1 take\n"); for (size_t i = 0; i < 15; i++) { printf("task1 i = %d\n", i); vTaskDelay(1000); } xSemaphoreGive(mutexHandler); printf("tsak1 give\n"); } } void task2(void *pt) { printf("task2 begin\n"); vTaskDelay(1000); // 让低优先级别的任务有机会执行 while (1) { ; } } void task3(void *pt) { printf("task3 begin\n"); vTaskDelay(1000); // 让低优先级别的任务有机会执行 while (1) { xSemaphoreTake(mutexHandler, portMAX_DELAY); // 获取信号量 printf("tsak3 take\n"); for (size_t i = 0; i < 10; i++) { printf("task3 i = %d\n", i); vTaskDelay(1000); } xSemaphoreGive(mutexHandler); // 释放信号量 printf("tsak3 give\n"); } } void setup() { Serial.begin(115200); xTaskCreatePinnedToCore(task1, "", 1024 * 5, NULL, 1, NULL, 1); // task1获取到信号量时,如果task3高优先级的任务也尝试获取该信号量,会将task1的优先级暂时升级为3 xTaskCreatePinnedToCore(task2, "", 1024 * 5, NULL, 2, NULL, 1); xTaskCreatePinnedToCore(task3, "", 1024 * 5, NULL, 3, NULL, 1); // 优先级别最高,最先执行 vTaskDelete(NULL); // 删除当前任务 } void loop() { } ``` ##### 递归互斥量 允许同一任务在持有互斥量的情况下再次获取该互斥量,而不会导致死锁。 递归互斥量可以用于需要对同一资源进行多层保护的情况,例如嵌套调用的函数。 ```c SemaphoreHandle_t mutexHandler = xSemaphoreCreateRecursiveMutex(); // 创建递归互斥量 xSemaphoreTakeRecursive(mutexHandler); // 获取信号量 xSemaphoreGiveRecursive(mutexHandler); // 释放信号量 ``` * 注意:使用递归互斥量时,获取和释放的次数要相等,以避免死锁的情况。 ```c #include SemaphoreHandle_t mutexHandler = xSemaphoreCreateRecursiveMutex(); // 创建mutex句柄 void task1(void *pt) { while (1) { printf("task1 begin\n"); xSemaphoreTakeRecursive(mutexHandler, portMAX_DELAY); // 第一次取得信号量 printf("tsak1 take\n"); for (size_t i = 0; i < 5; i++) { printf("task1 i = %d for A\n", i); vTaskDelay(1000); } xSemaphoreTakeRecursive(mutexHandler, portMAX_DELAY); // 第二次取得信号量 for (size_t i = 0; i < 5; i++) { printf("task1 i = %d for B\n", i); vTaskDelay(1000); } xSemaphoreGiveRecursive(mutexHandler); xSemaphoreGiveRecursive(mutexHandler); printf("tsak1 give\n"); taskYIELD(); } } void task2(void *pt) { vTaskDelay(1000); while (1) { printf("task2 begin\n"); xSemaphoreTakeRecursive(mutexHandler, portMAX_DELAY); printf("tsak2 take\n"); xSemaphoreGiveRecursive(mutexHandler); printf("tsak2 give\n"); taskYIELD(); } } void setup() { Serial.begin(115200); xTaskCreatePinnedToCore(task1, "", 1024 * 5, NULL, 1, NULL, 1); xTaskCreatePinnedToCore(task2, "", 1024 * 5, NULL, 1, NULL, 1); vTaskDelete(NULL); // 删除当前任务 } void loop() { } ``` 运行结果: ```c task1 begin tsak1 take task1 i = 0 for A task2 begin task1 i = 1 for A task1 i = 2 for A task1 i = 3 for A task1 i = 4 for A task1 i = 0 for B task1 i = 1 for B task1 i = 2 for B task1 i = 3 for B task1 i = 4 for B tsak1 give tsak2 take tsak2 give task1 begin task2 begin tsak2 take tsak2 give tsak1 take ``` ### 4.任务通知(重要) **从FreeRTOS V8.2.0版本,新增了任务通知(task notify)这个功能,可用使用任务通知来代替信号量、消息队列、事件标志组这些东西。使用任务通知可用提高系统的工作效率。** FreeRTOS的每个任务都有一个32位的通知值,任务控制块中的成员变量ulNotifiedValue就是这个通知值。 * 使用任务通知,可以控制任务的流向,执行顺序。 任务通知虽然可用提高速度,并且减少RAM的使用,但是任务通知也是有使用限制的: 1. FreeRTOS的任务通知只能有一个接收任务,大多数的应用都是这种情况 2. 接收任务可以因为接收任务通知而进入阻塞态,但是发送任务不会因为任务通知发送失败而阻塞。 #### 通知同步 示例程序:任务2通知任务1执行,如果任务1没有接收到任务通知,就一直处于阻塞状态。更详细的内容,参考FreRTOS中文数据手册:`2.18 xTaskNotifyGive()` ```c #include static TaskHandle_t xTask1 = NULL, xTask2 = NULL; // 创建任务的句柄 void task1(void *pt) { while (1) { printf("task1 wait notification!\n"); ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // 阻塞自身,等待通知执行下面的程序 printf("task1 got notification\n"); } } void task2(void *pt) { while (1) { vTaskDelay(1000); // 1s发送1次通知 printf("task2 notify task1!\n"); xTaskNotifyGive(xTask1); // 通知任务1解锁阻塞状态 } } void setup() { Serial.begin(115200); xTaskCreatePinnedToCore(task1, "", 1024 * 5, NULL, 1, &xTask1, 1); xTaskCreatePinnedToCore(task2, "", 1024 * 5, NULL, 1, &xTask2, 1); vTaskDelete(NULL); // 删除当前任务 } void loop() {} ``` 运行结果: ```c task1 wait notification! task2 notify task1! task1 got notification task1 wait notification! ``` #### 任务通知值 通过通知不同的值,可以控制任务进入不同的处理流程。 ```c BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry, // 在进入函数时,清除所有函数的通知值 uint32_t ulBitsToClearOnExit, // 在退出的时候清楚 uint32_t *pulNotificationValue, // 取得当前任务通知的值 TickType_t xTicksToWait); // 等待时间 BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify, // 任务通知的句柄 uint32_t ulValue, // 需要发送的任务通知值 eNotifyAction eAction); // 常用eSetValueWithOverwrite ``` 示例程序:通过设置不同的通知值,执行不同的事件,参考数据手册:`2.14 xTaskNotify()` ```c #include static TaskHandle_t xTask1 = NULL, xTask2 = NULL; // 创建任务的句柄 void task1(void *pt) { uint32_t ulNotifiedValue; while (1) { printf("task1 wait notification!\n"); xTaskNotifyWait(0x00, /* Don't clear any notification bits on entry. */ ULONG_MAX, /* Reset the notification value to 0 on exit. */ &ulNotifiedValue, /* Notified value pass out in ulNotifiedValue. */ portMAX_DELAY); /* Block indefinitely. */ /* Process any events that have been latched in the notified value. */ if ((ulNotifiedValue & 0x01) != 0) { /* Bit 0 was set - process whichever event is represented by bit 0. */ printf("task1 process bit0 event!\n"); } if ((ulNotifiedValue & 0x02) != 0) { /* Bit 1 was set - process whichever event is represented by bit 1. */ printf("task1 process bit1 event!\n"); } if ((ulNotifiedValue & 0x04) != 0) { /* Bit 2 was set - process whichever event is represented by bit 2. */ printf("task1 process bit2 event!\n"); } /* Etc. */ } } void task2(void *pt) { while (1) { vTaskDelay(1000); // 1s发送1次通知 printf("task2 notify bit0!\n"); xTaskNotify(xTask1, 0x01, eSetValueWithOverwrite); vTaskDelay(1000); printf("task2 notify bit1!\n"); xTaskNotify(xTask1, 0x02, eSetValueWithOverwrite); vTaskDelay(1000); printf("task2 notify bit2!\n"); xTaskNotify(xTask1, 0x04, eSetValueWithOverwrite); } } void setup() { Serial.begin(115200); xTaskCreatePinnedToCore(task1, "", 1024 * 5, NULL, 1, &xTask1, 1); xTaskCreatePinnedToCore(task2, "", 1024 * 5, NULL, 1, &xTask2, 1); vTaskDelete(NULL); // 删除当前任务 } void loop() {} ``` 运行结果: ```c task1 wait notification! task2 notify bit0! task1 process bit0 event! task1 wait notification! task2 notify bit1! task1 process bit1 event! task1 wait notification! task2 notify bit2! task1 process bit2 event! ``` #### 任务通知取代信号量 **使用直接任务通知取代二进制信号量,由于没有了二进制信号量这个中间媒介,不仅节省了内存,而且速度也会快45%。** * 注意:设置任务的阻塞和通知顺序要注意先后顺序。 直接任务通知不能取代二进制信号量的场景: 1. **直接任务通知相当于严格指定任务的执行顺序**,而采用二进制信号量可以做到在等待事件内各任务随机抢占CPU执行权 2. 因此,当有2个及以上需要接收信号量时,最好采用二进制信号量,而不是直接任务通知 ```c #include static TaskHandle_t xTask1 = NULL, xTask2 = NULL; // 创建任务的句柄 void task1(void *pt) { while (1) { xTaskNotifyGive(xTask2); // 通知任务2执行 /* Block to wait for prvTask2() to notify this task. */ ulTaskNotifyTake(pdTRUE, portMAX_DELAY); } } void task2(void *pt) { while (1) { /* Block to wait for prvTask1() to notify this task. */ ulTaskNotifyTake(pdTRUE, portMAX_DELAY); xTaskNotifyGive(xTask1); // 通知任务1执行 } } void setup() { Serial.begin(115200); xTaskCreatePinnedToCore(task1, "", 1024 * 5, NULL, 1, &xTask1, 1); xTaskCreatePinnedToCore(task2, "", 1024 * 5, NULL, 1, &xTask2, 1); vTaskDelete(NULL); // 删除当前任务 } void loop() {} ``` #### 直接任务当作邮箱 **可以通过设置任务通知值的方式达到想要的效果。** ### 5.流媒体缓存 流媒体:音频、视频 * 适合一个任务读,一个任务写,不适合多任务读写 * 与队列的不同:**stream buffer读写的大小没有限制**,而队列是预先设置好的固定值 ```c #include // 首先添加流媒体相关的头文件 // 创建streambuffer StreamBufferHandle_t xStreamBufferCreate(size_t xBufferSizeBytes, // 参数1:buffer的大小 size_t xTriggerLevelBytes); // 参数2:最小一帧数据的大小,例如一个声音最少8个字节,则设置为8, // Stream Buffer内数据超过这个数值,才会被读取,否则一直接收进行存储,达到这个值进行一次读取 // 发送流媒体数据 size_t xStreamBufferSend(StreamBufferHandle_t xStreamBuffer, // 句柄 const void *pvTxData, // 需要发送数据的指针,需要进行强制类型转换 size_t xDataLengthBytes, // 需要发送数据的长度,可以用sizeof()计算 TickType_t xTicksToWait); // 堵塞时间 // 接收流媒体数据 size_t xStreamBufferReceive(StreamBufferHandle_t xStreamBuffer, void *pvRxData, // 接收数据,需要进行强制类型转换 size_t xBufferLengthBytes, // 存储接收数据的buffer长度 TickType_t xTicksToWait); ``` * 注意:创建stream buffer时,`xTriggerLevelBytes`的设置非常重要。**在接收函数中,如果buffer中有数据,首先会将能够接收的数据接收下来,然后堵塞当前的任务,直到buffer中的数据大于`xTriggerLevelBytes`** #### 确定stream buffer的大小 在创建stream buffer时,如果创建的buffer太大,会造成资源浪费,太小系统工作会非常不稳定。 * 使用stream buffer时,通常创建3个任务 * 任务1发送数据,任务2接收数据,任务3监控stream buffer的空间大小 ```c // API size_t xStreamBufferBytesAvailable( StreamBufferHandle_t xStreamBuffer ); // stream buffer已使用字节 size_t xStreamBufferSpacesAvailable( StreamBufferHandle_t xStreamBuffer ); // sream buffer可用空间字节 ``` 示例: ```c #include #include #include // 参数1:stream buffer的总大小,参数2:每帧数据的大小 // 参数2作用:接收数据时,如果小于这个值将处于堵塞状态,直到接收buffer里存储的字节大于这个值,才会接收一次 StreamBufferHandle_t streamHandler = xStreamBufferCreate(200, 50); void task1(void *pt) { char tx_buffer[50]; int str_len = 0; // 字符串长度 int i = 0; int send_bytes = 0; // 实际发送的数据 while (1) { i++; str_len = sprintf(tx_buffer, "hello send i= %d ", i); // 要发送的数据 send_bytes = xStreamBufferSend(streamHandler, (void *)tx_buffer, str_len, portMAX_DELAY); // 没有发送成功就一直处于堵塞状态 printf("--------------\n"); printf("Send: str_len =%d, send_bytes= %d\n", str_len, send_bytes); vTaskDelay(3000); // taskYIELD(); } } void task2(void *pt) { char rx_buffer[50]; // 存储接收的数据 int rec_bytes = 0; // 接收到多少数据 while (1) { memset(rx_buffer, 0, sizeof(rx_buffer)); // 初始化buffer为0 rec_bytes = xStreamBufferReceive(streamHandler, (void *)rx_buffer, sizeof(rx_buffer), portMAX_DELAY); printf("--------------\n"); printf("Receive: rec_bytes=%d, rec_data: %s\n", rec_bytes, rx_buffer); } } void task3(void *pt) { size_t buf_space = 0; // stream buffer可用空间 int min_space = 1000; // buffer的初始值 while (1) { buf_space = xStreamBufferSpacesAvailable(streamHandler); if (buf_space < min_space) { min_space = buf_space; } // 通过观察min_space的输出值,当接收数据可用正常接收时,min_space的值 ,采用1000-min_space得到的结果就是需要设置的buffer空间大小 printf("buf_space = %d, min_space = %d\n", buf_space, min_space); vTaskDelay(3000); } } void setup() { Serial.begin(115200); xTaskCreatePinnedToCore(task1, "发送", 1024 * 5, NULL, 1, NULL, 1); xTaskCreatePinnedToCore(task2, "接收", 1024 * 5, NULL, 1, NULL, 1); xTaskCreatePinnedToCore(task3, "监控", 1024 * 5, NULL, 1, NULL, 1); vTaskDelete(NULL); // 删除当前任务 } void loop() { } ``` 运行结果: ```c -------------- Send: str_len =16, send_bytes= 16 -------------- buf_space = 200, min_space = 200 Receive: rec_bytes=16, rec_data: hello send i= 1 -------------- Send: str_len =16, send_bytes= 16 buf_space = 184, min_space = 184 -------------- Send: str_len =16, send_bytes= 16 buf_space = 168, min_space = 168 -------------- Send: str_len =16, send_bytes= 16 buf_space = 152, min_space = 152 -------------- Send: str_len =16, send_bytes= 16 -------------- Receive: rec_bytes=50, rec_data: hello send i= 2 hello send i= 3 hello send i= 4 he6 9ô -------------- Receive: rec_bytes=14, rec_data: llo send i= 5 buf_space = 186, min_space = 152 ``` ### 6.消息缓存 消息缓存与流媒体缓存的区别: 1. **消息缓存一次只能接收一条完整的消息** 2. **`message buffer`在接收buffer信息时,如果定义的buffer空间大小,小于一条消息的长度,则无法正常接收一条完整的消息,返回值为0。而对于`stream buffer`,只要buffer中有数据,就可以获取对应长度的数据。** ```c // API #include // 添加相关库文件 MessageBufferHandle_t xMessageBufferCreate( size_t xBufferSizeBytes ); // 创建,参数:buffer的大小 // 接收信息 size_t xMessageBufferReceive(MessageBufferHandle_t xMessageBuffer, void *pvRxData, size_t xBufferLengthBytes, TickType_t xTicksToWait); // 发送信息 size_t xMessageBufferSend(MessageBufferHandle_t xMessageBuffer, const void *pvTxData, size_t xDataLengthBytes, TickType_t xTicksToWait); ``` 示例程序:发送和接收三条消息 ```C #include #include #include // 添加相关库文件 MessageBufferHandle_t messageHandler = xMessageBufferCreate(1000); // 创建消息缓存buffer void task1(void *pt) { char tx_buffer[50]; int str_len = 0; // 字符串长度 int i = 0; int send_bytes = 0; // 实际发送的数据 // 创建三条消息 for (int i = 0; i < 3; i++) { str_len = sprintf(tx_buffer, "hello, nomber %d\n", i); send_bytes = xMessageBufferSend(messageHandler, (void *)tx_buffer, str_len, portMAX_DELAY); printf("--------------\n"); printf("Send:i=%d, send_bytes = %d\n", i, send_bytes); } vTaskDelete(NULL); } void task2(void *pt) { char rx_buffer[200]; // 存储接收的数据 int rec_bytes = 0; // 接收到多少数据 vTaskDelay(3000); // 先延时3s,让消息发送到缓存区 while (1) { memset(rx_buffer, 0, sizeof(rx_buffer)); // 初始化buffer为0 rec_bytes = xMessageBufferReceive(messageHandler, (void *)rx_buffer, sizeof(rx_buffer), portMAX_DELAY); printf("--------------\n"); printf("Receive: rec_bytes=%d, rec_data: %s\n", rec_bytes, rx_buffer); } } void setup() { Serial.begin(115200); xTaskCreatePinnedToCore(task1, "发送", 1024 * 5, NULL, 1, NULL, 1); xTaskCreatePinnedToCore(task2, "接收", 1024 * 5, NULL, 1, NULL, 1); vTaskDelete(NULL); // 删除当前任务 } void loop() {} ``` 运行结果: ```c -------------- Send:i=0, send_bytes = 16 -------------- Send:i=1, send_bytes = 16 -------------- Send:i=2, send_bytes = 16 -------------- Receive: rec_bytes=16, rec_data: hello, nomber 0 -------------- Receive: rec_bytes=16, rec_data: hello, nomber 1 -------------- Receive: rec_bytes=16, rec_data: hello, nomber 2 ```