【ESP32 C++教程】Unit6-3 FreeRTOS任务间通信

本节主要讲解FreeRTOS任务间如何使用消息队列、事件组和二进制信号量进行通信。

消息队列

在实际的应用中,常常会遇到一个任务或者中断服务需要和另外一个任务进行通信, 这个通信的过程其实就是消息传递的过程。在没有操作系统的时候两个应用程序进行消息传递一般使用全局变量的方式,FreeRTOS 对此提供了一个叫做“队列”的机制来完成任务与任务、任务与中断之间的消息传递,由于队列用来传递消息的,所以也称为消息队列。

【ESP32 C++教程】Unit6-3 FreeRTOS任务间通信

创建队列

创建消息队列时FreeRTOS会先给消息队列分配一块内存空间,这块内存的大小等于消息队列控制块大小加上(单个消息空间大小与消息队列长度的乘积),接着再初始化消息队列,此时消息队列为空。

函数原型

QueueHandle_t xQueueCreate(
    UBaseType_t uxQueueLength, //队列长度 
    UBaseType_t uxItemSize //队列项目大小
);

消息发送

在发送消息操作的时候,为了保护数据,当且仅当队列允许入队的时候,发送者才能成功发送消息

函数原型

BaseType_t xQueueSend(
    QueueHandle_t xQueue, //队列句柄
    const void * pvItemToQueue, //队列项目指针
    TickType_t xTicksToWait //阻塞等待时间 
);
  • 在消息队列未满时,消息将放入队列尾,使用xQueueSendToFront函数可将消息放入队列首;
  • 当消息队列已满时,系统会根据用户指定的阻塞超时时间xTicksToWait将任务阻塞,在超时时间内如果还不能完成入队操作,发送消息的任务或者中断服务程序会收到一个错误码errQUEUE_FULL,然后解除阻塞状态;
  • 只有在任务中发送消息才允许进行阻塞状态,而在中断中发送消息不允许带有阻塞机制的,需要调用在中断中发送消息的API函数接口,因为发送消息的上下文环境是在中断中,不允许有阻塞的情况。

接收消息

在接收消息操作的时候,若队列中有消息,则立即读取消息,并将消息从队列中移除,若无消息,则根据给定的等待时间决定是结束读取消息还是阻塞等待

函数原型

BaseType_t xQueueReceive(
    QueueHandle_t xQueue, //队列句柄
    void *pvBuffer, //队列项目指针
    TickType_t xTicksToWait //阻塞等待时间 
);
  • xTicksToWait为0时,结束读取,不进入阻塞态,继续后面的操作;
  • xTicksToWait为n时,进入阻塞态,在阻塞期间(n个tick),若队列有消息,则进入就绪态,读取消息,继续后面的操作;若超过n个tick还无消息,从阻塞状中唤醒,返回一个超时的错误码;
  • xTicksToWait为portMAX_DELAY时,会一直阻塞到队列有消息;

示例:消息队列应用

本示例通过向消息队列发生0或1来控制LED的关闭或打开

从 https://gitee.com/billyzh/esp32-cpp-lesson 下载本教程的源码到本地硬盘文件夹,如d:\esp32-cpp-lesson
在VSCode中,选择【文件】->【打开文件夹...】选择上一步保存的文件夹打开

打开项目后,选择config.h文件,修改第10行为
#define APP_LESSON63_A 1

打开unit6-lesson63a/board_config.h文件,设置LED使用的引脚,
#define BUILTIN_LED_PIN GPIO_NUM_4
配置启用GpioLed
#define CONFIG_USE_LED_GPIO 1

创建LED实例,代码如下(unit6-lesson63a/my_board.cpp):

MyBoard::MyBoard() : Board() {
    Log::Info(TAG, "===== Create Board ...... =====");

    Log::Info(TAG, "initial led.");
    led_ = new GpioLed(BUILTIN_LED_PIN, false);

    Log::Info( TAG, "===== Board config completed. =====");
}

程序很简单,就是创建一个GpioLed实例。

消息队列应用

代码如下(unit6-lesson63a/my_application.cpp):

void MyApplication::OnInit() {
    queue_ = xQueueCreate(10, sizeof(int));

    task1_ = new Task("Task1");
    task1_->OnLoop([this](){
        Task1Loop();
    });
    task1_->Start( 4096, tskIDLE_PRIORITY+1);
    
    task2_ = new Task("Task2");
    task2_->OnLoop([this](){
        Task2Loop();
    });
    task2_->Start( 4096, tskIDLE_PRIORITY+1);
}

void MyApplication::Task1Loop() {
    state_ = (state_==0 ? 1 : 0);

    if (xQueueSend(queue_, &state_, 0) != pdPASS) {
        Log::Warn(TAG, "发送数据到队列失败。");
    }

    delay(500);
}

void MyApplication::Task2Loop() {
    int receive = 0;
    if (xQueueReceive(queue_, &receive, portMAX_DELAY) != pdPASS) {
        Log::Warn(TAG, "从队列接收数据失败。");
        return;
    }

    Led *led = Board::GetInstance().GetLed();
    if (receive==1) 
    {
        led->TurnOn();
    }
    else 
    {
        led->TurnOff();
    }
}

程序解读
1. 在OnInit方法内,创建一个消息队列和两个Task实例,然后启动任务;
2. Task1Loop方法是任务1的循环体方法,具体为向队列发生数据,每500ms发生一次数据0或1;
3. Task2Loop方法是任务2的循环体方法,具体为从队列阻塞式接收数据,当有数据时,为1点亮LED,为0熄灭LED;

编译项目并上传开发板检验

事件组

事件组(Event Group)是FreeRTOS中用于任务同步的一种机制。它允许任务等待多个事件的发生,事件的含义完全由开发者定义。事件组中的事件使用位(bit)来表示,每个位可以表示一个事件。事件组主要用于任务之间的协作,可以等待多个事件中的一个或多个事件的发生。

事件只与任务相关联,事件相互独立,一个 32 位的事件集合(高8位保留,实际可有 24位),用于标识该任务发生的事件类型,其中每一位表示一种事件类型(0 表示该事件类型未发生、1 表示该事件类型已经发生)。

【ESP32 C++教程】Unit6-3 FreeRTOS任务间通信

任务之间可以设置、清除、等待这些位,来完成类似“谁完成了什么事”、“等谁先完成”这种协调。

设置事件组某位

将事件组中你指定的某几位设置为 1,如果有任务正在等待这些位,就会立刻唤醒它们。
多次向任务设置同一事件(如果任务还未来得及读走),等效于只设置一次。

函数原型

EventBits_t xEventGroupSetBits(
	EventGroupHandle_t xEventGroup,  //事件组句柄
	const EventBits_t uxBitsToSet          //要设置的事件bit位
);

等待事件组某位(或全部)

用于等待事件组中某些事件位被设置,支持事件等待超时机制。例如,一个任务需要等待多个传感器的信号,只要其中一个传感器触发,任务就可以继续执行。

函数原型

EventBits_t xEventGroupWaitBits(
	const EventGroupHandle_t xEventGroup, //事件组句柄
	const EventBits_t uxBitsToWaitFor,          //等待的事件bit位
	const BaseType_t xClearOnExit,               //是否清除事件
	const BaseType_t xWaitForAllBits,
	TickType_t xTicksToWait 
);

清除指事件位

用于清除事件组中的某些位(即将它们置为0)。它和 xEventGroupSetBits() 相反,它不会触发阻塞在事件位上的任务,只是静默清除某些位。

函数原型

EventBits_t xEventGroupClearBits(
	EventGroupHandle_t xEventGroup,   //事件组句柄
	EventBits_t uxBitsToClear             //要设置的事件bit位
);

示例:事件组应用

本示例通过事件组的事件来触发LED的关闭或打开

从 https://gitee.com/billyzh/esp32-cpp-lesson 下载本教程的源码到本地硬盘文件夹,如d:\esp32-cpp-lesson
在VSCode中,选择【文件】->【打开文件夹...】选择上一步保存的文件夹打开

打开项目后,选择config.h文件,修改第10行为
#define APP_LESSON63_B 1

打开unit6-lesson63b/board_config.h文件,设置LED使用的引脚,
#define BUILTIN_LED_PIN GPIO_NUM_4
配置启用GpioLed
#define CONFIG_USE_LED_GPIO 1

创建LED实例,代码如下(unit6-lesson63b/my_board.cpp):

MyBoard::MyBoard() : Board() {
    Log::Info(TAG, "===== Create Board ...... =====");

    Log::Info(TAG, "initial led.");
    led_ = new GpioLed(BUILTIN_LED_PIN, false);

    Log::Info( TAG, "===== Board config completed. =====");
}

程序很简单,就是创建一个GpioLed实例。

事件组应用

代码如下(unit6-lesson63b/my_application.cpp):

void MyApplication::OnInit() {
    event_group_ = xEventGroupCreate();

    // 任务一
    task1_ = new Task("Task1");
    task1_->OnLoop([this](){
        // 一些处理
        delay(500);
        xEventGroupSetBits(event_group_, 0b00000001);
    });
    task1_->Start(4096, tskIDLE_PRIORITY+1);

    // 任务二
    task2_ = new Task("Task2");
    task2_->OnLoop([this](){
        // 一些处理
        delay(1000);
        xEventGroupSetBits(event_group_, 0b00000010);
    });
    task2_->Start(4096, tskIDLE_PRIORITY+1);
}

void MyApplication::OnLoop() {
    // 等待事件位被设置
    auto bits = xEventGroupWaitBits(event_group_, 
        0b00000011,
        pdTRUE, /* 自动清除,避免重复响应 */
        pdTRUE, /* 所有事件位被设置就返回 */
        portMAX_DELAY /* 无限期等待,也可使用pdMS_TO_TICKS指定等待时长 */
    );

    Led *led = Board::GetInstance().GetLed();
    led->TurnOn();
    
    // 有事件设置就触发(事件位OR)
    /*
    auto bits = xEventGroupWaitBits(event_group_, 
        0b00000011,
        pdTRUE, // 自动清除,避免重复响应 
        pdFALSE, // 所有事件位被设置就返回 
        portMAX_DELAY // 无限期等待,也可使用pdMS_TO_TICKS指定等待时长 
    );

    if (bits & 0b01 = 0b01) {
        Led *led = Board::GetInstance().GetLed();
        led->TurnOn();
    } else (bits & 0b10 = 0b10) {
        
    }
    */

    delay(1);
}

程序解读
1. 在OnInit方法内,创建一个事件组;创建任务一每500ms设置一次bit位0为1,创建任务二每1000ms设置一次bit位1为1;
2. 在OnLoop方法内,无限期等待事件组被设置,若bit位0和1均被设置,则点亮Led;

编译项目并上传开发板检验

二进制信号量

二值信号量(Binary Semaphore)是一种非常常用的同步机制,主要用于任务之间的同步或者任务与中断之间的同步,并且在某些场景下可以作为一个简单的资源保护机制。它只有两种状态:0和1,因此被称为“二值信号量”

信号量这个名字很恰当:
信号:起通知作用
量:还可以用来表示资源的数量

  • 当"量"只有0、1两个取值时,它就是"二进制信号量"(Binary Semaphores)
  • 当"量"没有限制时,它就是"计数型信号量"(Counting Semaphores)
支持的动作:"give"给出资源,计数值加1;"take"获得资源,计数值减1
二进制信号量跟计数型的唯一差别,就是计数值的最大值被限定为1。


创建信号量

SemaphoreHandle_t xSemaphoreCreateBinary();

获取信号量

BaseType_t xSemaphoreTake(
    SemaphoreHandle_t xSemaphore,  /* 信号量句柄 */
    TickType_t xTicksToWait        /* 超时时间 */
);

赋与信号量

BaseType_t xSemaphoreGive(
    SemaphoreHandle_t xSemaphore,  /* 信号量句柄 */
);

本示例通过二进制信号量来触发LED的打开

从 https://gitee.com/billyzh/esp32-cpp-lesson 下载本教程的源码到本地硬盘文件夹,如d:\esp32-cpp-lesson
在VSCode中,选择【文件】->【打开文件夹...】选择上一步保存的文件夹打开

打开项目后,选择config.h文件,修改第10行为
#define APP_LESSON63_C 1

打开unit6-lesson63c/board_config.h文件,设置LED使用的引脚,
#define BUILTIN_LED_PIN GPIO_NUM_4
配置启用GpioLed
#define CONFIG_USE_LED_GPIO 1

创建LED实例,代码如下(unit6-lesson63c/my_board.cpp):

MyBoard::MyBoard() : Board() {
    Log::Info(TAG, "===== Create Board ...... =====");

    Log::Info(TAG, "initial led.");
    led_ = new GpioLed(BUILTIN_LED_PIN, false);

    Log::Info( TAG, "===== Board config completed. =====");
}

程序很简单,就是创建一个GpioLed实例。

二进制信号量应用

代码如下(unit6-lesson63c/my_application.cpp):

void MyApplication::OnInit() {
    binary_semaphore_ = xSemaphoreCreateBinary();

    // 任务一
    task1_ = new Task("Task1");
    task1_->OnLoop([this](){
         /* 赋与信号量 */
        xSemaphoreGive(binary_semaphore_);
        delay(2000);
    });
    task1_->Start(4096, tskIDLE_PRIORITY+1);
}

void MyApplication::OnLoop() {
    /* 等待信号量 */
    if(xSemaphoreTake(binary_semaphore_, portMAX_DELAY) == pdTRUE)
    {
        Led *led = Board::GetInstance().GetLed();
        led->TurnOn();
        delay(1000);
        led->TurnOff();
    }
    
    delay(1);
}

程序解读
1. 在OnInit方法内,创建一个二进制信号量;创建任务一每2秒释放一次信号;
2. 在OnLoop方法内,阻塞式获取信号量,若有信号量,则点亮Led 1秒,然后熄灭;

编译项目并上传开发板检验


作业
分别用消息队列、事件组和信号量实现一个三按键抢答器

- 本文由用户 老张 发布,文中观点仅代表作者本人,不代表本站立场。
- 如需转载,请联系作者;如有侵权,请联系本站处理。

02-22   阅读(27)   评论(0)
 标签: 创客电子 ESP32 FreeRTOS ESP32-ArduinoFx

涨知识
万向节

万向节即万向接头,英文名称universal joint,是实现变角度动力传递的机件,用于需要改变传动轴线方向的位置

评论:
相关文章
MimiClaw 配置飞书机器人和添加硬件控制技能

本文本介绍配置飞书机器人为MimiClaw的一个输入/输出端,和添加一个控制WS2812与LED的控制技能。


ESP32-S3 部署 MimicLaw 完整教程:从零到成功调用 DeepSeek

一块 30 块钱的开发板 + 一个大模型 API,就能做出可以听懂人话的智能硬件。 本文记录完整安装过程和踩坑经验,确保你跟着做就能跑通。


MimiClaw 架构全解析,把 “智能龙虾” 跑在 ESP32 上

本文将从手绘架构图入手,逐层拆解 MimiClaw 的分层设计、核心模块、数据流转与底层实现,带你解剖这只“智能虾”的技术骨架,看懂在 C 语言加持下,AI 智能体如何以可穿戴设备的形态,在你身边稳稳运行、离线服务、主动响应。


如何用 platform.local.txt 深度定制 ESP32 编译流程?

本文介绍如何在不脱离 ArduinoIDE 可视化开发的前提下,通过一个名为 platform.local.txt 的小文件,实现对 ESP32 编译流程的精准控制。


优化Arduino-ESP32程序体积

本文将系统分析程序体积增长的五大根源,并提供经过验证的优化方案,帮助减小固件大小。


开发ESP32大模型AI语音助手-从软件到硬件

本文所DIY的语音助手设备端使用的是MicroPython、服务端是Python,对于很多开发者来说MicroPython入门没难度。


【ESP32 C++教程】Unit10-2:音频录制

本小节使用音频开发框架实现一个音频录制到文件的示例。


ESP32 I2S 接口深度解析:从时序、格式到 ESP-IDF 驱动实战

I2S协议通过BCLK、LRCLK和DATA三线精准传输音频数据,但时序边沿、帧格式、时钟源等细节常引发噪声或断连。本文详解ESP32的I2S实现,从协议原理到ESP-IDF v5.x代码配置,助你避开常见陷阱,确保音频稳定传输。


【ESP32 C++教程】Unit10-1:音频播放

本小节介绍音频的基础知识、音频开发框架和AudioCodec的简介,用一个音频播放示例来说明音频管道的使用。


MimiClaw – 开源超轻量级AI助手,无需高级运行环境

MimiClaw是基于ESP32-S3芯片的超轻量级AI助手,通过Telegram或WebSocket提供Claude/GPT智能服务。