初学freertos
的是 “队列空就一直等”,这是消费者能稳定接收数据的核心;是防御性判断,确保只有读到数据才打印;是为了演示 “消费速度更快” 的场景,即使消费任务处理完数据,也会主动让出 CPU,不会空转占用资源;核心逻辑:消费者的 “总等待时间”(主动延时 + 队列阻塞)最终和生产者的生产速度匹配,所以能稳定每秒接收 1 个数据。
·
我把 FreeRTOS 的学习拆解成三步递进的 demo,先从单个任务入手,再到两个任务用消息队列通信,最后加上互斥锁保护共享资源
第一步:基础版 - 仅创建单个 FreeRTOS 任务
这是最基础的示例,核心是理解xTaskCreate()的用法,以及 FreeRTOS 任务的基本结构(无限循环 + 延时)。
1.1 完整代码
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
// 日志标签,方便调试
#define TAG "SINGLE_TASK"
/**
* @brief 单个测试任务
* @param pvParameters 任务参数(这里未使用,传NULL)
*/
void single_test_task(void *pvParameters)
{
// 任务计数变量
int task_count = 0;
// FreeRTOS任务必须是无限循环(退出会导致任务删除)
while (1)
{
task_count++;
ESP_LOGI(TAG, "单个任务运行中,计数:%d", task_count);
// 任务延时1秒(pdMS_TO_TICKS把毫秒转FreeRTOS的tick数)
// 延时期间CPU会切换到其他任务(这里只有一个任务,会进入空闲任务)
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void app_main(void)
{
ESP_LOGI(TAG, "ESP32 FreeRTOS 单个任务示例启动");
// 核心函数:创建FreeRTOS任务
BaseType_t ret = xTaskCreate(
single_test_task, // 任务函数
"test_task", // 任务名称(仅调试用)
4096, // 任务栈大小(ESP32建议至少4096字节)
NULL, // 传递给任务函数的参数
1, // 任务优先级(0最低,一般不用0,避免抢占空闲任务)
NULL // 任务句柄(不需要则传NULL)
);
// 检查任务是否创建成功
if (ret != pdPASS)
{
ESP_LOGE(TAG, "任务创建失败!");
}
// app_main本身也是FreeRTOS任务,这里可以删除自身(不影响创建的任务)
vTaskDelete(NULL);
}
1.2 核心解释
xTaskCreate():FreeRTOS 创建任务的核心函数,6 个参数必须理解:- 任务函数:必须是
void (*)(void*)类型,无限循环结构; - 栈大小:ESP32 中栈太小会导致任务崩溃,基础任务给 4096 足够;
- 优先级:数值越大优先级越高,同优先级任务会分时调度;
- 任务函数:必须是
vTaskDelay():任务主动让出 CPU,延时期间不会占用资源;- 运行效果:串口每秒打印一次计数,直到断电
第二步:进阶版 - 两个任务通过消息队列通信
这一步重点理解任务间解耦通信,消息队列是 FreeRTOS 中最常用的任务通信方式,核心是 “生产者任务发数据,消费者任务收数据”。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_log.h"
#define TAG "QUEUE_DEMO"
// 全局队列句柄(两个任务都需要访问)
QueueHandle_t data_queue;
/**
* @brief 生产者任务:生产数据并发送到消息队列
*/
void producer_task(void *pvParameters)
{
int send_data = 0;
while (1)
{
send_data++;
ESP_LOGI(TAG, "生产者任务:准备发送数据 %d", send_data);
// 核心函数:向队列发送数据
// 参数:队列句柄、数据指针、等待时间(portMAX_DELAY=队列满时一直等)
xQueueSend(data_queue, &send_data, portMAX_DELAY);
ESP_LOGI(TAG, "生产者任务:数据 %d 发送成功\n", send_data);
vTaskDelay(pdMS_TO_TICKS(1000)); // 1秒生产一次
}
}
/**
* @brief 消费者任务:从消息队列接收数据
*/
void consumer_task(void *pvParameters)
{
int recv_data = 0;
while (1)
{
// 核心函数:从队列接收数据
// 参数:队列句柄、存储数据的缓冲区、等待时间(portMAX_DELAY=队列空时一直等)
BaseType_t ret = xQueueReceive(data_queue, &recv_data, portMAX_DELAY);
if (ret == pdTRUE)
{
ESP_LOGI(TAG, "消费者任务:接收到数据 %d\n", recv_data);
}
// 消费速度比生产快,所以大部分时间在等队列数据
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void app_main(void)
{
ESP_LOGI(TAG, "ESP32 FreeRTOS 消息队列示例启动");
// 第一步:创建消息队列
// 参数:队列长度(最多存10个数据)、每个数据的字节数(int占4字节)
data_queue = xQueueCreate(10, sizeof(int));
if (data_queue == NULL)
{
ESP_LOGE(TAG, "队列创建失败!");
return;
}
// 第二步:创建生产者和消费者任务
xTaskCreate(producer_task, "producer", 4096, NULL, 1, NULL);
xTaskCreate(consumer_task, "consumer", 4096, NULL, 1, NULL);
vTaskDelete(NULL);
}
xQueueCreate():创建消息队列,必须指定 “队列长度” 和 “单个数据大小”;xQueueSend():生产者向队列写数据,队列满时会阻塞(直到有空闲位置);xQueueReceive():消费者从队列读数据,队列空时会阻塞(直到有数据);- 核心优势:两个任务完全解耦,生产者只负责发数据,消费者只负责收数据,不用关心对方状态;
- 运行效果:生产者每秒发一个数字,消费者每 0.5 秒收一次(实际 1 秒收一个),串口打印发送 / 接收日志。
总结
xQueueReceive()的portMAX_DELAY是 “队列空就一直等”,这是消费者能稳定接收数据的核心;ret == pdTRUE是防御性判断,确保只有读到数据才打印;vTaskDelay(500ms)是为了演示 “消费速度更快” 的场景,即使消费任务处理完数据,也会主动让出 CPU,不会空转占用资源;- 核心逻辑:消费者的 “总等待时间”(主动延时 + 队列阻塞)最终和生产者的生产速度匹配,所以能稳定每秒接收 1 个数据。
第三步:实战版 - 消息队列 + 互斥锁(保护共享资源)
这一步是项目中最常用的场景:两个任务不仅要通信,还要操作共享资源(比如全局变量、硬件外设),互斥锁用来防止多个任务同时操作共享资源导致数据错乱。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "esp_log.h"
#define TAG "MUTEX_DEMO"
// 全局句柄:队列(通信)+ 互斥锁(保护共享资源)
QueueHandle_t data_queue;
SemaphoreHandle_t resource_mutex;
// 共享资源(两个任务都会操作,必须加锁保护)
int shared_counter = 0;
/**
* @brief 生产者任务:发队列+修改共享资源
*/
void producer_task(void *pvParameters)
{
int send_data = 0;
while (1)
{
// 1. 发送数据到队列
send_data++;
xQueueSend(data_queue, &send_data, portMAX_DELAY);
ESP_LOGI(TAG, "生产者:发送数据 %d", send_data);
// 2. 修改共享资源(核心:先加锁,再操作)
// xSemaphoreTake:获取互斥锁,拿到锁才继续执行
if (xSemaphoreTake(resource_mutex, portMAX_DELAY) == pdTRUE)
{
shared_counter++; // 临界区:操作共享资源
ESP_LOGI(TAG, "生产者:共享计数器 = %d(已加锁)", shared_counter);
xSemaphoreGive(resource_mutex); // 释放互斥锁(必须释放,否则其他任务死等)
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
/**
* @brief 消费者任务:收队列+读取共享资源
*/
void consumer_task(void *pvParameters)
{
int recv_data = 0;
while (1)
{
// 1. 从队列接收数据
if (xQueueReceive(data_queue, &recv_data, portMAX_DELAY) == pdTRUE)
{
ESP_LOGI(TAG, "消费者:接收数据 %d", recv_data);
}
// 2. 读取共享资源(同样需要加锁)
if (xSemaphoreTake(resource_mutex, portMAX_DELAY) == pdTRUE)
{
ESP_LOGI(TAG, "消费者:共享计数器 = %d(已加锁)", shared_counter);
xSemaphoreGive(resource_mutex);
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void app_main(void)
{
ESP_LOGI(TAG, "ESP32 FreeRTOS 队列+互斥锁示例启动");
// 1. 创建队列(通信)
data_queue = xQueueCreate(10, sizeof(int));
if (data_queue == NULL) { ESP_LOGE(TAG, "队列创建失败"); return; }
// 2. 创建互斥锁(保护共享资源)
resource_mutex = xSemaphoreCreateMutex();
if (resource_mutex == NULL) { ESP_LOGE(TAG, "互斥锁创建失败"); return; }
// 3. 创建两个任务
xTaskCreate(producer_task, "producer", 4096, NULL, 1, NULL);
xTaskCreate(consumer_task, "consumer", 4096, NULL, 1, NULL);
vTaskDelete(NULL);
}
xSemaphoreCreateMutex():创建互斥锁,返回句柄(NULL 表示创建失败);xSemaphoreTake():获取锁,只有拿到锁的任务能进入 “临界区”(操作共享资源的代码);xSemaphoreGive():释放锁,必须由拿到锁的任务释放(否则会导致死锁);- 为什么需要锁?如果不加锁,可能出现 “生产者刚把 counter 加 1 一半,消费者就读取”,导致数据错误;
- 运行效果:每秒打印 “生产者发数据→修改计数器(加锁)→消费者收数据→读取计数器(加锁)”,计数器数值始终正确。
先明确 “共享资源” 和 “临界区”
- 共享资源:代码里的
shared_counter是全局变量,生产者任务和消费者任务都会操作它(生产者加 1,消费者读取),这就是 “多个任务共享的资源”; - 临界区:操作共享资源的代码(这里就是
shared_counter++),必须保证 “同一时间只有一个任务能执行这段代码”,否则会出问题。
总结:
局部变量但是任务之间有联系,就用消息队列。全局变量,多个任务对同一个变量操作,就用互斥锁
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐



所有评论(0)