我把 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 秒收一个),串口打印发送 / 接收日志。

总结

  1. xQueueReceive()portMAX_DELAY是 “队列空就一直等”,这是消费者能稳定接收数据的核心;
  2. ret == pdTRUE是防御性判断,确保只有读到数据才打印;
  3. vTaskDelay(500ms)是为了演示 “消费速度更快” 的场景,即使消费任务处理完数据,也会主动让出 CPU,不会空转占用资源;
  4. 核心逻辑:消费者的 “总等待时间”(主动延时 + 队列阻塞)最终和生产者的生产速度匹配,所以能稳定每秒接收 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++),必须保证 “同一时间只有一个任务能执行这段代码”,否则会出问题。

总结:

         局部变量但是任务之间有联系,就用消息队列。全局变量,多个任务对同一个变量操作,就用互斥锁

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐