ESP32 FreeRTOS学习总结

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) 删除当前任务
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
  • 接收参数:把传递过来的空指针进行强制类型转换,转换为对应传输的类型指针
传递整数
#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,但是不建议,级别越高,越占内存)。

  • 同优先级的任务:FreeRTOS将采用循环调度算法来运行他们,也就是交替执行同优先级的任务。每个任务执行一个时间片,然后将CPU时间片分配给另一个任务。
  • 优先级别高的任务先被创建和运行。
    任务的调度:
  • 在FreeRTOS中,vTaskDelay()vTaskDelayUntil()函数可以暂停当前任务的执行,等待一段时间后再继续执行。(让其他任务有机会执行)
  • taskYIELD()函数:立即将CPU时间片退让给同等级或更高优先级的任务,如果没有其他任务等待执行,则当前任务会立即继续执行。(简单的说,就是让其他任务执行)
任务的挂起和恢复

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PQOP5J54-1684032842836)(images/1.png)]
任务的状态:running、ready、blocked、suspended(挂起,暂停)

  • running:运行状态,如果MCU只有一个内核,那么在任何给定时间内只能有一个任务处于运行状态。
  • ready:准备状态(任务刚被创建时,准备执行),不处于堵塞或挂起状态(没有获得CPU执行权限,等待执行状态),因为同等级或更高优先级的任务正在执行
  • blocked:使用了vTaskDelay()或delay()函数
  • suspended:挂起状态,挂起之后,任务被恢复才能继续执行
// 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);
}
vTaskDelay()和delay()

一个tick的时间是由FreeRTOS的时钟节拍周期和时钟频率决定的,可以通过配置文件进行设置。默认情况下1 tick = 1ms

  • vTaskDelay()函数:以系统时钟节拍(tick)为单位进行延时,例如vTaskDelay(100)表示让任务暂停100个系统时钟节拍的时间。
  • delay()函数:是一个简单的延时函数,它通常在不需要多任务处理和系统保护的应用中使用。使用后会后边的程序都会被延迟执行。
vTaskDelayUntil()

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() {}

2.Queue

2023.5.12

队列:先入先出(FIFO,first in first out)
使用方法:

  1. 创建队列:长度,尺寸(每个内存空间存储的数据大小)
  2. 发送数据到队列中
  3. 从队列中取数据
// 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
队列存储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
队列的多进单出:多个任务写,一个任务读

ESP32 FreeRTOS学习总结_第1张图片
多个任务把数据写入一个队列,一个任务进行读。设置写入的任务级别为同级别,读任务的优先级别要比写任务高一级别。

  • 不推荐这种方式:容易造成系统工作混乱。最好的工作方式是一个队列只有一个写操作,可以有多个读操作,但是写操作只能有一个
队列集合(常用):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qgLZATfT-1684032842837)(images/2.png)]
多个队列,但是每个队列只有一个写操作,一个读操作(读取所有队列)
实现步骤:

  1. 创建队列集合的句柄:同时指定队列集合的总长度
  2. 将已创建的队列添加到集合中
  3. 创建一个句柄:从队列集合中获取有数据的队列
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()
{
}

3.信号量

信号量分类:二进制信号量、计数信号量、互斥信号量。
信号量就像红绿灯一样,控制车辆的通行。
信号量常用于控制对共享资源的访问和任务同步。信号量对于控制共享资源访问的场景相当于一个上锁机制,代码只有获得这个锁的钥匙才能执行。

  • 使用队列、信号量,都可以实现互斥访问
二进制信号量(常用)

二值信号量常用于互斥访问或同步,二值信号量和互斥信号量非常类似,但是互斥信号量拥有优先级继承机制,二值信号量没有优先级继承。

  • 二进制信号量可以用于一个任务控制另一个任务的运行与堵塞。
  • 二进制信号量表示只有两个值:0和1
  • 二值信号量相当于长度为1的队列
  • 二进制信号量只有两种状态:已触发和未触发,类似于一个开关。当一个任务等待一个已经触发的二进制信号量是,它会立即获得信号量,如果信号量未被触发,任务将被堵塞直到信号量被触发。
  • 可以避免资源冲突和死锁问题,提高系统的可靠性和效率
// 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
计数信号量

与二进制不同的是,计数信号量可以有更多的状态。

  • 计数信号量相当于长度大于1的队列,同二值信号量一样,不需要关系队列中存储了什么数据,只需要关心队列是否为空即可。
  • 例如在一个停车场中有10个车位,车辆进入时,车位被占用(计数器减1),车开出去后(计数器加1),为0时表示没有可用的车位。
// API
uxSemaphoreGetCount( semphrHandle); // 获得计数型信号量的值
SemaphoreHandle_t semphrHandle = xSemaphoreCreateCounting(10,0);// 创建计数型信号量,参数1:最大值,参数2:初始值
xSemaphoreGive(semphrHandle); // 释放信号量
xSemaphoreTake(semphrHandle); // 获取信号量

使用场合:事件计数、资源管理

  1. 共享资源的访问控制:当多个任务需要共享同一个资源时,可以使用计数信号量来控制资源的访问。每个任务需要访问资源时,都需要获取一个计数信号量,如果计数信号量的值为0,则任务会被堵塞,直到其他任务释放资源并增加计数信号量的值。这种方式可以避免资源冲突和死锁等问题。
  2. 控制任务的执行顺序:有些情况下,需要控制任务的执行顺序,例如任务A必须在任务B执行完成之后才能执行。可以使用计数信号量来实现这种控制。任务B执行完成后,可以增加计数信号量的值,任务A等待计数信号量的值为1时,可以获取信号量并开始执行。
    示例1:模拟停车场的停车位
#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对共享资源进行保护,如果任务均为同优先级别,可以采用二进制信号量进行共享资源保护
  • 可以理解为互斥量二进制信号量的升级版
  • 注意:使用完立即释放钥匙
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

4.任务通知(重要)

从FreeRTOS V8.2.0版本,新增了任务通知(task notify)这个功能,可用使用任务通知来代替信号量、消息队列、事件标志组这些东西。使用任务通知可用提高系统的工作效率。

FreeRTOS的每个任务都有一个32位的通知值,任务控制块中的成员变量ulNotifiedValue就是这个通知值。

  • 使用任务通知,可以控制任务的流向,执行顺序。

任务通知虽然可用提高速度,并且减少RAM的使用,但是任务通知也是有使用限制的:

  1. FreeRTOS的任务通知只能有一个接收任务,大多数的应用都是这种情况
  2. 接收任务可以因为接收任务通知而进入阻塞态,但是发送任务不会因为任务通知发送失败而阻塞。
通知同步

示例程序:任务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%。

  • 注意:设置任务的阻塞和通知顺序要注意先后顺序。
    直接任务通知不能取代二进制信号量的场景:
  1. 直接任务通知相当于严格指定任务的执行顺序,而采用二进制信号量可以做到在等待事件内各任务随机抢占CPU执行权
  2. 因此,当有2个及以上需要接收信号量时,最好采用二进制信号量,而不是直接任务通知
#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读写的大小没有限制,而队列是预先设置好的固定值
#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的空间大小
// 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

6.消息缓存

消息缓存与流媒体缓存的区别:

  1. 消息缓存一次只能接收一条完整的消息
  2. 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

你可能感兴趣的:(ESP32,FreeRTOS,c++,ESP32,FreeRTOS)