2023.5.11
FreeRTOS中文数据手册:https://www.freertos.org/zh-cn-cmn-s/RTOS.html
感谢以下两位B站UP主的教程:孤独的二进制、Michael_ee
创建任务常用API:
任务函数 | 描述 |
---|---|
xTaskCreate() | 使用动态的方法创建一个任务 |
xTaskCreatePinnedToCore | 指定任务的运行核心(最后一个参数) |
vTaskDelete(NULL) | 删除当前任务 |
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); // 任务句柄
任务间传参可以使用多种方式,常见的为:
队列的读写效率相比全局变量慢一些
使用全局变量进行传参时:
(void *)pt
#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
#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() {}
#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() {}
#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,但是不建议,级别越高,越占内存)。
vTaskDelay()
和vTaskDelayUntil()
函数可以暂停当前任务的执行,等待一段时间后再继续执行。(让其他任务有机会执行)taskYIELD()
函数:立即将CPU时间片退让给同等级或更高优先级的任务,如果没有其他任务等待执行,则当前任务会立即继续执行。(简单的说,就是让其他任务执行)[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PQOP5J54-1684032842836)(images/1.png)]
任务的状态:running、ready、blocked、suspended(挂起,暂停)
vTaskDelay()或delay()
函数// API:
TaskHandle_t pxtask = NULL; // 创建任务的句柄
xTaskCreatePinnedToCore(task1, "", 1024 * 2, NULL, 1, &pxtask, 1);
vTaskSuspend(pxtask); // 挂起任务,任务不再执行
vTaskResume(pxtask); // 恢复被挂起的任务,继续执行
vTaskSuspendAll(); // 挂起所有函数,挂起后不可以执行
vTaskResumeAll(); // 恢复所有挂起函数
创建任务时,如果给任务分配的内存空间过小,会导致程序不断重启。如果分配的内存空间过多,会造成资源浪费。
// API:
ESP.getHeapSize() // 本程序Heap最大尺寸(空间总大小)
ESP.getFreeHeap() // 当前Free Heap最大尺寸(当前可用剩余空间大小)
uxTaskGetStackHighWaterMark(taskHandle) // 计算当前任务剩余多少内存
示例程序:
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);
}
一个tick的时间是由FreeRTOS的时钟节拍周期和时钟频率决定的,可以通过配置文件进行设置。默认情况下1 tick = 1ms
vTaskDelay()
函数:以系统时钟节拍(tick)为单位进行延时,例如vTaskDelay(100)表示让任务暂停100个系统时钟节拍的时间。delay()
函数:是一个简单的延时函数,它通常在不需要多任务处理和系统保护的应用中使用。使用后会后边的程序都会被延迟执行。vTaskDelayUntil
函数比vTaskDelay
函数定时精准。
// API
TickType_t xLastWakeTime = xTaskGetTickCount(); // 获取当前时间
const TickType_t xFrequency = 3000; // 需要的时间间隔
vTaskDelayUntil(&xLastWakeTime, xFrequency);
while(1){
vTaskDelayUntil(&xLastWakeTime, xFrequency);
// 下边为需要运行的函数
}
//
示例程序:
#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() {}
2023.5.12
队列:先入先出(FIFO,first in first out)
使用方法:
// portMAX_DELAY - 无限Block
// TickType_t timeOut = portMAX_DELAY; // 无限等待,直到队列中有数据,或者等待数据有空位置可以存储新数据
TickType_t timeOut = 10;
xStatus = xQueueSend(Qhandle, &i, timeOut); // 往队列里发送数据,如果队列里内容是满的就等待10ms再次尝试发送
API | 描述 |
---|---|
xQueueCreate() | 创建一个队列 |
xQueueSend() | 往队列里写数据 |
xQueueReceive | 从队列里读数据 |
uxQueueMessagesWaiting(队列句柄) | 返回值为队列中参数的个数,可用于接收数据时,先判断一下队列里是否有数据 |
// 创建一个队列
QueueHandle_t Qhandle = xQueueCreate(5, sizeof(int)); // 创建一个队列,长度为5,每个空间的大小为int
#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() {}
运行结果:
发送成功:0
接收成功:0
发送成功:1
接收成功:1
发送成功:2
接收成功:2
发送成功:3
接收成功:3
跟上面的案例类似,只是队列中每个元素类型为struct
,并且发送和接收的数据存储也要设置为struct
类型
#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的规则进行数据的发送和接收
发送成功:2
接收成功:1
发送成功:3
接收成功:2
发送成功:4
接收成功:3
例如传递字符串。传递大型数据时,把指针对应的数据进行传递。
malloc()
函数:在使用malloc开辟空间时,使用完一定要释放空间,如果不释放会造成内存泄漏。malloc()函数返回的实际是一个无类型指针,必须在其前面加上指针类型强制转换才可以使用。指针自身 = (指针类型*)malloc(sizeof(指针类型)*数据数量)int *p = NULL;
p = (int *)malloc(sizeof(int)*10);
// 使用完之后采用free()进行释放
free(p);
p = NULL; // 让其重新指向NULL
多个任务把数据写入一个队列,一个任务进行读。设置写入的任务级别为同级别,读任务的优先级别要比写任务高一级别。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qgLZATfT-1684032842837)(images/2.png)]
多个队列,但是每个队列只有一个写操作,一个读操作(读取所有队列)
实现步骤:
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为句柄
示例程序:这个程序编译不成功,还没有解决
#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()
{
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CUCtItFd-1684032842837)(images/3.png)]
只有一个队列,一个任务写,多个任务读
// API
QueueHandle_t Mailbox = xQueueCreate(5, sizeof(int)); // 创建一个队列邮箱
xQueueOverwrite(); // 往队列中写数据
xQueuePeek(); // 从队列中读数据
示例程序:运行不成功
#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()
{
}
信号量分类:二进制信号量、计数信号量、互斥信号量。
信号量就像红绿灯一样,控制车辆的通行。
信号量常用于控制对共享资源的访问和任务同步。信号量对于控制共享资源访问的场景相当于一个上锁机制,代码只有获得这个锁的钥匙才能执行。
二值信号量常用于互斥访问或同步,二值信号量和互斥信号量非常类似,但是互斥信号量拥有优先级继承机制,二值信号量没有优先级继承。
// API
SemaphoreHandle_t xHandler = xSemaphoreCreateBinary(); // 创建二进制信号量
xSemaphoreGive(xHandler); // 释放信号量
xSemaphoreTake(xHanlder, timeout); // 在指定时间内获取信号量,返回值为pdPASS, 或者pdFAIL
示例程序1:按键控制LED的亮灭(已验证)
#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:采用二进制信号量对任务进行管理,对全局变量进行读写
#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()
{
}
运行结果:
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
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
与二进制不同的是,计数信号量可以有更多的状态。
// API
uxSemaphoreGetCount( semphrHandle); // 获得计数型信号量的值
SemaphoreHandle_t semphrHandle = xSemaphoreCreateCounting(10,0);// 创建计数型信号量,参数1:最大值,参数2:初始值
xSemaphoreGive(semphrHandle); // 释放信号量
xSemaphoreTake(semphrHandle); // 获取信号量
使用场合:事件计数、资源管理
#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的工作原理可以想象成共享的资源被锁在一个箱子里,只有一把钥匙,有钥匙的任务才能对共享资源进行访问。
mutex
对共享资源进行保护,如果任务均为同优先级别,可以采用二进制信号量
进行共享资源保护互斥量
是二进制信号量
的升级版SemaphoreHandle_t mutexHandler = xSemaphoreCreateMutex(); // 创建一个Mutex互斥量
xSemaphoreGive(mutexHandler); // 释放信号量
xSemaphoreTake(mutexHandler, timeout); // 在指定时间内获取信号量,返回值为pdPASS, 或者pdFAIL
示例程序:
// 对于互斥量通常创建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()
{
}
允许同一任务在持有互斥量的情况下再次获取该互斥量,而不会导致死锁。
递归互斥量可以用于需要对同一资源进行多层保护的情况,例如嵌套调用的函数。
SemaphoreHandle_t mutexHandler = xSemaphoreCreateRecursiveMutex(); // 创建递归互斥量
xSemaphoreTakeRecursive(mutexHandler); // 获取信号量
xSemaphoreGiveRecursive(mutexHandler); // 释放信号量
#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()
{
}
运行结果:
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
从FreeRTOS V8.2.0版本,新增了任务通知(task notify)这个功能,可用使用任务通知来代替信号量、消息队列、事件标志组这些东西。使用任务通知可用提高系统的工作效率。
FreeRTOS的每个任务都有一个32位的通知值,任务控制块中的成员变量ulNotifiedValue就是这个通知值。
任务通知虽然可用提高速度,并且减少RAM的使用,但是任务通知也是有使用限制的:
示例程序:任务2通知任务1执行,如果任务1没有接收到任务通知,就一直处于阻塞状态。更详细的内容,参考FreRTOS中文数据手册:2.18 xTaskNotifyGive()
#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() {}
运行结果:
task1 wait notification!
task2 notify task1!
task1 got notification
task1 wait notification!
通过通知不同的值,可以控制任务进入不同的处理流程。
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()
#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() {}
运行结果:
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%。
#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() {}
可以通过设置任务通知值的方式达到想要的效果。
流媒体:音频、视频
#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);
xTriggerLevelBytes
的设置非常重要。在接收函数中,如果buffer中有数据,首先会将能够接收的数据接收下来,然后堵塞当前的任务,直到buffer中的数据大于xTriggerLevelBytes
在创建stream buffer时,如果创建的buffer太大,会造成资源浪费,太小系统工作会非常不稳定。
// API
size_t xStreamBufferBytesAvailable( StreamBufferHandle_t xStreamBuffer ); // stream buffer已使用字节
size_t xStreamBufferSpacesAvailable( StreamBufferHandle_t xStreamBuffer ); // sream buffer可用空间字节
示例:
#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()
{
}
运行结果:
--------------
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
消息缓存与流媒体缓存的区别:
message buffer
在接收buffer信息时,如果定义的buffer空间大小,小于一条消息的长度,则无法正常接收一条完整的消息,返回值为0。而对于stream buffer
,只要buffer中有数据,就可以获取对应长度的数据。// 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);
示例程序:发送和接收三条消息
#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() {}
运行结果:
--------------
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