ESP32 Arduino教程:使用FreeRTOS队列进行任务间通信

本 esp32 Arduino 教程的目的是解释如何使用 freertos 队列在两个不同的任务之间进行通信。测试是使用集成在esp32 开发板中的dfrobot 的 esp32 模块设备进行的。

引言

本文主要解释如何基于内核使用FreeRTOS队列实现任务之间的通信。除了任务间的通信之外,队列还可以在任务与中断服务程序之间进行通信[1](本篇教程暂不展开介绍)。
如需有关FreeRTOS队列的入门介绍,请参见上一篇文章:ESP32 Arduino教程:FreeRTOS队列。如果您需要了解如何在Arduino环境下处理FreeRTOS任务,请点击这:ESP32 Arduino:创建FreeRTOS任务。
简单来说,队列的作用,就是由某个任务用来生成一些内容供其他任务使用,遵循FIFO(先进先出)原则[1]。本例中,我们将让一个任务在队列中放入一些整数,以供另一个任务使用。
尽管本文是以整数为例进行说明,但是FreeRTOS队列实际上允许使用更复杂的数据结构(比如结构体等)。
特别重要的一点是,队列可以安全地用于任务间通信[1],这就意味着我们不需要使用其他同步方法。
另一个需要注意的重要操作是执行阻止API调用。写入满队列或读取空队列都会导致任务被阻止,直到预定时间之后才能继续运行[1]。如果一个以上的任务都被队列阻止,那么最高优先级的任务将会在操作可以完成时最先解除阻止。

设置

鉴于我们的任务需要对队列进行访问,因此我们先要声明一个QueueHandle_t类型的全局变量。该变量将始终指向我们所创建的队列。插入和消耗数据的任务也都要用到这个变量。
我们还将声明一个全局变量,用于保存队列最大长度,这样便于代码重用和修改。我们将初始化值设为10,但其实您可以使用任何数值。

QueueHandle_t queue;
 
int queueSize = 10;

在我们的设置函数中,我们将首先打开一个串行连接,然后将我们的数据发送给Arduino IDE Serial Monitor,并对结果进行分析。
接下来,我们将通过调用xQueueCreate函数创建队列。该函数的第一个参数是在一定时间内队列所能保存的最大数据项个数,第二个参数是每个数据项的大小(以字节表示)[2]。此处需要注意两点,一是队列中的每个数据项必须具有相同的大小,二是需要指定数据项的实际大小,而不是指向它的指针大小(因为数据项是复制而不是引用的)[2]。
这并不意味着我们不能使用指针来传递对消息的引用,而只是说明我们放到队列中的变量是直接复制的。我们当然可以在队列中放入指向更长消息的指针,而不是消息本身。尽管如此,本例仅为入门级示例,为简单起见,我们将只在队列中放入实际的变量(一些整数值)。
因此,对于第一个参数而言,我们将使用先前声明的全局变量,并使用size of函数来定义其数值(因为每个数据项都是一个整数)。
该函数若执行成功,将会返回一个队列句柄,这个句柄将被保存到我们的全局变量中。如果队列未能成功创建,则函数将会返回NULL,我们需要检查问题出在了哪里并对代码进行改进。

Serial.begin(112500);
  
queue = xQueueCreate( queueSize, sizeof( int ) );
  
if(queue == NULL){
  Serial.println("Error creating the queue");
}

有了队列之后,我们再来创建任务。其中一个任务叫做producer,它负责向队列中添加数据,而另一个叫做consumer的任务则会从队列中消耗数据。
创建任务需要使用xTaskCreate函数,该函数有大量参数需要指定,此处不再对其进行详细解释。有关如何在Arduino内核上运行FreeRTOS任务的详细说明,请参见这篇先前的教程。
对于每一个创建的任务来说,我们都需要指定其任务函数(包括实际的任务代码)。producer的任务函数叫做producerTask,而consumer的任务函数则是consumerTask。稍后我们会对它们的代码进行分析。
整个设置函数的完整代码(包括任务创建代码)如下所示。为简单起见,在队列未成功创建时,我们只是将错误消息打印到控制台上,并没有对错误进行进一步的处理,原因在于对于这种简单的测试来说,本来就不太可能真正出现错误。当然,在实际的应用中,我们应该通过错误处理程序对异常情况进行处理。

void setup() {
  
  Serial.begin(112500);
  
  queue = xQueueCreate( queueSize, sizeof( int ) );
  
  if(queue == NULL){
    Serial.println("Error creating the queue");
  }
  
  xTaskCreate(
                    producerTask,     /* Task function. */
                    "Producer",       /* String with name of task. */
                    10000,            /* Stack size in words. */
                    NULL,             /* Parameter passed as input of the task */
                    1,                /* Priority of the task. */
                    NULL);            /* Task handle. */
  
  xTaskCreate(
                    consumerTask,     /* Task function. */
                    "Consumer",       /* String with name of task. */
                    10000,            /* Stack size in words. */
                    NULL,             /* Parameter passed as input of the task */
                    1,                /* Priority of the task. */
                    NULL);            /* Task handle. */
  
}

producer任务

该任务只是将一些数据项放到队列中。我们将使用一个从0到队列大小减1之间的循环来实现这个任务。目标就是向队列中插入尽可能多的数据项,然后结束任务。每个数据项对应于当前的迭代数值。
实际的数据插入通过调用xQueueSend函数实现。它的第一个参数是对队列的引用(之前已保存到一个全局变量中),第二个参数是一个指针,指向将要插入队列的数据项,最后一个参数是当队列已满时任务将要等待的最长时间(时钟计数值,以tick表示)。
本例并不会分配超过队列大小的数据项,所以上述等待时间参数就无关紧要。尽管如此,我们将使用portMAX_DELAY值,表示任务将会一直等待,直到队列中有空间可以插入数据项为止。
程序最后调用vTaskDelete函数(http://www.freertos.org/a00126.html ),输入参数为NULL,其功能是删除任务。这个简单函数的完整源代码如下所示。

void producerTask( void * parameter )
{
  
    for( int i = 0; i

consumer任务

comsumer任务在一个循环内执行,它会消耗之前插入的数据项。我们先得定义一个缓冲区,用于复制队列中的数据项。
数据项的大小应该和队列创建时所定义的一致。对于本例而言,数据项只是一个整数。
要从队列中获取实际的数据项,需要调用xQueueReceive函数。它的第一个参数是队列句柄,第二个参数是一个指针,指向要复制数据项的缓冲区,最后一个参数是当队列为空时任务将要等待的时间长度(时钟计数值,以tick表示)。
同样地,我们将使用portMAX_DELAY作为该函数的最后一个参数。如果没有数据项可用,那么任务将会一直等待。
在接收到数据项之后,我们将简单地采用相同的循环迭代将其打印到串行端口,这样我们就能腾出缓冲区以接收下一个数据项。
与上一个任务函数一样,在循环执行结束后,我们会调用vTaskDelete函数将任务删除。该函数代码如下所示。

void consumerTask( void * parameter)[/align]{
    int element;
  
    for( int i = 0; i< queueSize; i++ ){
  
        xQueueReceive(queue, &element, portMAX_DELAY);
        Serial.print(element);
        Serial.print("|");
    }
  
    vTaskDelete( NULL );
  
}

最终代码

最终的完整源代码如下所示。您可以将其复制粘贴到您的Arduino IDE中进行测试,queueSize数值可以任意修改。由于代码都在相应的任务中执行,所以主函数实际上不执行任何功能。

QueueHandle_t queue;[/align]int queueSize = 10;
  
void setup() {
  
  Serial.begin(112500);
  
  queue = xQueueCreate( queueSize, sizeof( int ) );
  
  if(queue == NULL){
    Serial.println("Error creating the queue");
  }
  
  xTaskCreate(
                    producerTask,     /* Task function. */
                    "Producer",       /* String with name of task. */
                    10000,            /* Stack size in words. */
                    NULL,             /* Parameter passed as input of the task */
                    1,                /* Priority of the task. */
                    NULL);            /* Task handle. */
  
  xTaskCreate(
                    consumerTask,     /* Task function. */
                    "Consumer",       /* String with name of task. */
                    10000,            /* Stack size in words. */
                    NULL,             /* Parameter passed as input of the task */
                    1,                /* Priority of the task. */
                    NULL);            /* Task handle. */
  
}
  
void loop() {
  delay(100000);
}
  
void producerTask( void * parameter )
{
  
    for( int i = 0;i

测试代码

要对代码进行测试,只需使用Arduino IDE对其进行编译并上传到您的ESP32开发板即可。然后,打开IDE Serial Monitor观察运行结果。您应该看到如图1所示的输出,producer任务插入到队列中的数值会被consumer任务以相同的顺序打印出来。

ESP32 Arduino教程:使用FreeRTOS队列进行任务间通信_第1张图片
图1 - 任务间通信程序的输出结果。

注:本文作者是Nuno Santos,他是一位和蔼可亲的电子和计算机工程师,住在葡萄牙里斯本 (Lisbon)。
他写了200多篇有关ESP32、ESP8266的有用的教程和项目。涉及arduino、micropython、 Picoweb、Espruino、Bluetooth、RFID、IDF……等等非常广泛,说是最全的完全不为过。

精华教程:

ESP32 MicroPython教程:uPyCraft IDE入门
ESP32 MicroPython教程:解析JSON
ESP32 MicroPython教程:MicroPython支持
ESP32 MicroPython教程:连接Wi-Fi网络
ESP32 / ESP8266 MicroPython教程:自动连接WiFi
ESP32 / ESP8266 MicroPython教程:从文件系统运行脚本
ESP32 / ESP8266 MicroPython教程:HTTP GET请求
ESP32 Arduino教程:用于构建ESP32编译环境的Arduino IDE软件
ESP32 Arduino教程:FreeRTOS队列性能测试
ESP32 RFID教程:打印MFRC522固件版本
ESP32 Picoweb教程:获取请求的HTTP方法
……

还有更多教程: ESP32教程 合集

英文版 :ESP32 tutorial合集

你可能感兴趣的:(ESP32,arduino)