了解 ESP32 FreeRTOS:初学者指南

发布时间 2023-11-26 09:10:28作者: MaxBruce

 

ESP32 FreeRTOS是什么?

ESP32 FreeRTOS是针对ESP32微控制器的一个实时操作系统(RTOS),它采用了FreeRTOS内核,可以帮助开发人员在ESP32芯片上进行多任务处理。简单来说,FreeRTOS提供了一种方式来管理软件任务并协调它们的执行。

ESP32是一个功能强大的嵌入式系统,可以用于构建各种物联网应用程序。其中,FreeRTOS是一个广泛使用的实时操作系统,它针对嵌入式系统的需求进行了优化。本文将介绍ESP32 FreeRTOS的基础知识,包括如何配置FreeRTOS内核、如何创建任务和使用消息队列进行任务通信。

对于学习单片机的人,ESP32 FreeRTOS可以让他们更轻松地编写和维护复杂的程序,而无需手动跟踪和调度任务。此外,它还可以帮助学习者更好地理解实时系统设计和多任务处理概念,并使他们能够更好地应对未来的嵌入式系统开发挑战。

在这里插入图片描述

如何使用FreeRTOS?

  1. 导入正确的FreeRTOS库

  2. 配置FreeRTOS内核,在编译器根据程序的内存分配堆栈大小、选择调度算法、启动任务通知等。

  3. 创建好任务后编写任务代码,可以使用FreeRTOS提供的API来管理任务和同步。

  4. 编译和调试
    在这里插入图片描述

哪些常用的函数?

以下是对于ESP32使用FreeRTOS常用函数的详细介绍:

xTaskCreate()

函数原型:TaskHandle_t xTaskCreate(TaskFunction_t pvTaskCode, const char *const pcName, uint32_t usStackDepth, void *pvParameters, UBaseType_t uxPriority, TaskHandle_t *const pxCreatedTask)

功能:创建一个新的任务,并将其加入到FreeRTOS任务调度器中。

参数:

  • pvTaskCode:指向任务函数的指针。
  • pcName:任务名称。可选参数,可以为NULL。
  • usStackDepth:任务的堆栈大小。单位为字节。
  • pvParameters:传递给任务函数的参数。可选参数,可以为NULL。
  • uxPriority:任务的优先级。
  • pxCreatedTask:指向一个TaskHandle_t变量的指针。可选参数,如果不需要返回任务句柄,则可以为NULL。

返回值:如果成功创建了任务,则返回任务句柄;否则返回NULL。

vTaskDelete()

函数原型:void vTaskDelete(TaskHandle_t xTaskToDelete)

功能:删除指定的任务。

参数:

  • xTaskToDelete:要删除的任务的句柄。

返回值:无。

vTaskDelay()

函数原型:void vTaskDelay(const TickType_t xTicksToDelay)

功能:延时指定的时间。

参数:

xTicksToDelay()

延时的时间,以FreeRTOS系统滴答计数器的节拍数为单位。

返回值:无。

xSemaphoreCreateBinary()

函数原型:SemaphoreHandle_t xSemaphoreCreateBinary(void)

功能:创建一个二进制信号量。

参数:无。

返回值:如果成功创建了信号量,则返回信号量句柄;否则返回NULL。

xSemaphoreGive()

函数原型:BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore)

功能:释放一个二进制信号量或者计数信号量的资源。

参数:

xSemaphore:要释放的信号量的句柄。

返回值:如果释放成功,则返回pdTRUE;否则返回pdFALSE。

  1. xSemaphoreTake()

函数原型:BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait)

功能:获取一个二进制信号量或者计数信号量的资源。

参数:

xSemaphore:要获取的信号量的句柄。

  • xTicksToWait:等待获取信号量的超时时间,以FreeRTOS系统滴答计数器的节拍数为单位。如果该参数设置为0,则非阻塞地尝试获取信号量。

返回值:如果成功获取了信号量,则返回pdTRUE;如果等待超时,则返回pdFALSE。

xQueueCreate()

函数原型:QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength, UBaseType_t uxItemSize)

功能:创建一个队列。

参数:

  • uxQueueLength:队列的可容纳元素个数。
  • uxItemSize:队列中一个元素的大小(以字节为单位)。

返回值:如果成功创建了队列,则返回队列的句柄;否则返回NULL。

xQueueSend()

函数原型:BaseType_t xQueueSend(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait)

功能:将消息发送到队列中。

参数:

  • xQueue:要发送消息的队列的句柄。
  • pvItemToQueue:指向要发送的数据的指针。
  • xTicksToWait:等待发送消息的超时时间,以FreeRTOS系统滴答计数器的节拍数为单位。如果该参数设置为0,则非阻塞地尝试发送消息。

返回值:如果成功发送了消息,则返回pdPASS;否则返回errQUEUE_FULL或者errQUEUE_BLOCKED(如果xTicksToWait大于0)。

xQueueReceive()

函数原型:BaseType_t xQueueReceive(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait)

功能:从队列中接收消息。

参数:

  • xQueue:要接收消息的队列的句柄。
  • pvBuffer:

简单示例:创建两个任务并打印任务名称

/*
如何创建freertos任务
如何分配内存、任务优先级
创建任务后loop循环还能不能使用
*/
#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>


#if CONFIG_FREERTOS_UNICORE
#define ARDUINO_RUNNING_CORE 0
#else
#define ARDUINO_RUNNING_CORE 1
#endif

//创建任务函数
void Task1(void *pvParameters);
void Task2(void *pvParameters);


void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  xTaskCreatePinnedToCore(
    Task1, "Task1"  // 任务名称
    ,
    1024  // 任务栈大小
    ,
    NULL  // 任务参数指针
    ,
    2  // 任务优先级大小 -- 值越大优先级越大
    ,
    NULL  // 任务句柄指针
    ,
    ARDUINO_RUNNING_CORE);  // 处理器核心编号


  xTaskCreatePinnedToCore(
    Task2, "Task2"  // 任务名称
    ,
    1024  // 任务栈大小
    ,
    NULL  // 任务参数指针
    ,
    1  // 任务优先级大小 -- 值越大优先级越大
    ,
    NULL  // 任务句柄指针
    ,
    ARDUINO_RUNNING_CORE);  // 处理器核心编号
}


void loop() {
  // 空闲?
  Serial.println("loop");
  delay(1000);
}


void Task1(void *pvParameters) {  // 任务1
  for (;;) {
    //
    Serial.println("task1");
    vTaskDelay(pdMS_TO_TICKS(1000));
    if (digitalRead(12) == HIGH) {
      Serial.println("GPIO12 is LOW. Deleting task...");
      vTaskDelete(NULL); // 删除当前任务
    }
  }
}


void Task2(void *pvParameters) {  // 任务2
  for (;;) {
    Serial.println("task2");
    vTaskDelay(pdMS_TO_TICKS(1000));
  }
}

部分代码理解:

#if CONFIG_FREERTOS_UNICORE 
#define ARDUINO_RUNNING_CORE 0 
#else #define ARDUINO_RUNNING_CORE 1 
#endif

当FreeRTOS配置为单核模式时,ARDUINO_RUNNING_CORE宏被定义为0,表示应用程序在主核心上运行。而当FreeRTOS配置为双核模式时,ARDUINO_RUNNING_CORE宏被定义为1,表示应用程序在第二个核心上运行。

在ESP32上,可以使用两个独立的处理器核心来运行应用程序和操作系统。在双核模式下,一个核心运行FreeRTOS调度程序,另一个核心则可用于运行用户应用程序。这种方式可以提高系统性能和响应速度。

使用队列示例

#include <Arduino.h>
#include "freertos/queue.h"

// 定义结构体类型
typedef struct {
  int id;
  char name[20];
} data_t;


// 定义队列句柄和队列长度
QueueHandle_t queue;
const int queueLen = 10;


void task1(void *pvParameters) {
  // 定义结构体变量并初始化
  data_t data = {1, "John"};

  while (1) {
    // 将结构体数据发送到队列中
    data.id++;
    xQueueSend(queue, &data, portMAX_DELAY);
    vTaskDelay(1000 / portTICK_PERIOD_MS);
  }
}

void task2(void *pvParameters) {
  while (1) {
    // 从队列中接收数据
    data_t data;
    xQueueReceive(queue, &data, portMAX_DELAY);

    // 打印接收到的数据
    Serial.printf("ID: %d, Name: %s\n", data.id, data.name);
  }
}

void setup() {
  Serial.begin(115200);
  // 创建队列
  queue = xQueueCreate(queueLen, sizeof(data_t));
  // 创建两个任务
  xTaskCreate(task1, "Task 1", 10000, NULL, 1, NULL);
  xTaskCreate(task2, "Task 2", 10000, NULL, 2, NULL);
}

void loop() {
}

在这里插入图片描述

使用队列的步骤如下:

  1. 使用xQueueCreate()函数创建一个队列,并指定队列大小和元素大小。
  2. 在生产者任务中,使用xQueueSend()函数将数据加入到队列中。
  3. 在消费者任务中,使用xQueueReceive()函数从队列中获取数据。
  4. 可以使用uxQueueMessagesWaiting()函数查看队列中当前的消息数目,以及使用uxQueueSpacesAvailable()函数查看队列中剩余空间的数量。

xQueueSend()函数用于将数据加入到队列中,其参数包括队列句柄、要发送的数据指针和等待时间。下面是这个函数的详细说明:

  • myQueue:队列句柄,由xQueueCreate()函数返回。

  • &data:要发送的数据的指针。此处使用&符号获取数据变量的地址,并将其传递给xQueueSend()函数。

  • portMAX_DELAY:等待时间,表示如果队列已满,则一直阻塞任务,直到队列有空间可用。也可以指定一个数值,表示最多等待多少个系统时钟节拍,例如500表示最多等待500个时钟节拍后退出等待。

需要注意的是,在向队列中添加数据时,通常需要检查xQueueSend()函数返回的值,以确定数据是否成功加入到队列中。如果队列已满,则xQueueSend()函数将返回errQUEUE_FULL错误代码。如果队列操作成功,则返回pdPASS代码。可以根据返回值采取相应的处理措施,以确保程序的正确性。