Chapter 06

FreeRTOS 多任务

ESP-IDF 内置的实时操作系统,让 ESP32 同时处理传感器读取、网络通信与 UI 刷新

FreeRTOS 在 ESP32 中的地位

FreeRTOS 是全球使用最广泛的嵌入式实时操作系统(RTOS),由 Richard Barry 创建,现由 Amazon 维护(AWS FreeRTOS)。ESP-IDF 将其深度集成:你编写的 app_main() 函数就是 FreeRTOS 的一个任务;Wi-Fi 协议栈、蓝牙驱动都运行在各自的 FreeRTOS 任务中。

ESP-IDF 对 FreeRTOS 的修改

ESP-IDF 使用修改版 FreeRTOS(称为 ESP-FreeRTOS)支持双核对称多处理(SMP):可以将任务绑定到 Core 0 或 Core 1,或允许调度器自动分配。标准 FreeRTOS 的所有 API 都可用,但调度器在两个核上分别运行,需要注意跨核同步。

任务(Task)基础

关键概念

任务(Task)
类似线程的独立执行单元,有自己的栈空间、优先级和状态。每个任务必须是无限循环,或在结束时调用 vTaskDelete(NULL) 自删除,不能直接 return。
优先级
0 最低,configMAX_PRIORITIES-1(ESP-IDF 中为 24)最高。相同优先级的任务时间片轮转。ESP-IDF 协议栈使用较高优先级(Wi-Fi 约 23),用户任务通常 5~15。
核心亲和性
xTaskCreatePinnedToCore() 允许将任务固定在 Core 0 或 Core 1 上运行。PRO_CPU(0) 通常给 Wi-Fi/BT,APP_CPU(1) 给用户应用,可避免协议栈抖动影响实时任务。
栈大小
创建任务时必须指定栈大小(字节),建议至少 2048 字节。如果使用 printf/ESP_LOG/JSON 解析等,需要 4096~8192 字节。栈溢出是 ESP32 崩溃的常见原因。
任务状态
Running(正在执行)、Ready(等待调度)、Blocked(等待事件/延时)、Suspended(挂起)、Deleted(已删除)。调度器只运行优先级最高的 Ready 任务。

任务创建示例

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"

static const char *TAG = "TASK";

void sensor_task(void *arg)
{
    ESP_LOGI(TAG, "传感器任务运行在 Core %d", xPortGetCoreID());
    while (1) {
        /* 读取传感器 */
        ESP_LOGI(TAG, "读取传感器...");
        vTaskDelay(pdMS_TO_TICKS(2000));   // 2秒延迟(任务进入 Blocked 状态)
    }
}

void network_task(void *arg)
{
    ESP_LOGI(TAG, "网络任务运行在 Core %d", xPortGetCoreID());
    while (1) {
        /* 发送 MQTT 数据 */
        vTaskDelay(pdMS_TO_TICKS(5000));
    }
}

void app_main(void)
{
    /* 普通创建(调度器自动分配核心)*/
    xTaskCreate(
        sensor_task,    // 任务函数
        "sensor",       // 任务名(调试用)
        4096,           // 栈大小(字节)
        NULL,           // 传递给任务的参数
        5,              // 优先级
        NULL            // 任务句柄(可选)
    );

    /* 固定到 Core 1(APP_CPU),避免被 Wi-Fi 打断 */
    xTaskCreatePinnedToCore(
        network_task,
        "network",
        8192,
        NULL,
        8,
        NULL,
        1               // Core 1 = APP_CPU
    );

    /* 查看任务状态(调试)*/
    char buf[512];
    vTaskList(buf);    // 需在 menuconfig 中启用 configUSE_TRACE_FACILITY
    ESP_LOGI(TAG, "任务列表:\n%s", buf);
}

同步原语

同步原语选择指南: 需要任务间传递数据? └─ YES → Queue(队列) 需要计数资源(如缓冲池空位)? └─ YES → Counting Semaphore(计数信号量) 需要保护共享资源(防止并发访问)? └─ YES → Mutex(互斥锁) └─ 注意:Mutex 不能在 ISR 中使用! ISR → Task 通知用 Binary Semaphore 需要等待多个条件同时/任意满足? └─ YES → Event Group(事件组) 需要从 ISR 通知特定任务? └─ YES → Task Notification(任务通知,最轻量)

队列(Queue)

#include "freertos/queue.h"

typedef struct {
    float temperature;
    float humidity;
} sensor_data_t;

static QueueHandle_t sensor_queue;

void sensor_task(void *arg)
{
    sensor_data_t data;
    while (1) {
        data.temperature = 25.0f + (esp_random() % 100) / 10.0f;
        data.humidity    = 60.0f + (esp_random() % 20);
        /* 发送到队列,等待最多 100ms */
        xQueueSend(sensor_queue, &data, pdMS_TO_TICKS(100));
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void upload_task(void *arg)
{
    sensor_data_t data;
    while (1) {
        /* 从队列接收,永久等待 */
        if (xQueueReceive(sensor_queue, &data, portMAX_DELAY)) {
            ESP_LOGI("UPLOAD", "温度:%.1f 湿度:%.1f",
                     data.temperature, data.humidity);
            /* 发布到 MQTT... */
        }
    }
}

void app_main(void)
{
    sensor_queue = xQueueCreate(10, sizeof(sensor_data_t));
    xTaskCreate(sensor_task, "sensor", 4096, NULL, 5, NULL);
    xTaskCreate(upload_task, "upload", 8192, NULL, 6, NULL);
}

互斥锁(Mutex)

#include "freertos/semphr.h"

static SemaphoreHandle_t display_mutex;
static char shared_buffer[128];   // 多任务共享的显示缓冲区

static void task_a(void *arg)
{
    while (1) {
        if (xSemaphoreTake(display_mutex, pdMS_TO_TICKS(200)) == pdTRUE) {
            snprintf(shared_buffer, sizeof(shared_buffer), "Task A: %lu", xTaskGetTickCount());
            /* 更新显示... */
            xSemaphoreGive(display_mutex);
        }
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

void app_main(void)
{
    display_mutex = xSemaphoreCreateMutex();
    xTaskCreate(task_a, "task_a", 2048, NULL, 5, NULL);
}

事件组(Event Group)

#include "freertos/event_groups.h"

#define EVT_WIFI_READY   BIT0
#define EVT_SENSOR_READY BIT1
#define EVT_ALL_READY    (EVT_WIFI_READY | EVT_SENSOR_READY)

static EventGroupHandle_t system_events;

void wifi_init_task(void *arg) {
    /* ... Wi-Fi 初始化 ... */
    xEventGroupSetBits(system_events, EVT_WIFI_READY);
    vTaskDelete(NULL);
}

void sensor_init_task(void *arg) {
    /* ... 传感器初始化 ... */
    xEventGroupSetBits(system_events, EVT_SENSOR_READY);
    vTaskDelete(NULL);
}

void main_task(void *arg) {
    /* 等待 Wi-Fi 和传感器都就绪(AND 等待) */
    xEventGroupWaitBits(system_events, EVT_ALL_READY,
                         pdTRUE, pdTRUE, portMAX_DELAY);
    ESP_LOGI("MAIN", "系统就绪,开始上报数据");
    while (1) { /* ... */ }
}

看门狗(Watchdog)

ESP32 实现了两级看门狗机制,防止程序卡死:

Task WDT(任务看门狗)
监控 IDLE 任务是否定期运行(默认超时 5 秒)。若任务长时间占用 CPU 不释放(死循环不调用 vTaskDelay),IDLE 任务无法运行,触发 panic 重启。
Int WDT(中断看门狗)
监控中断服务例程是否过长(默认超时 300ms)。ISR 中长时间阻塞会触发此看门狗,原因通常是 ISR 中调用了不该调用的阻塞函数。
喂狗操作
长时间占用 CPU 的任务(如固件解压、大数据处理)可以手动调用 esp_task_wdt_reset() 重置计时,或在关键路径中插入 vTaskDelay(1) 让出 CPU。

内存诊断

#include "esp_heap_caps.h"
#include "freertos/FreeRTOS.h"

void print_memory_info(void)
{
    ESP_LOGI("MEM", "内部 DRAM 堆:");
    ESP_LOGI("MEM", "  总计: %d 字节",
             heap_caps_get_total_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT));
    ESP_LOGI("MEM", "  空闲: %d 字节",
             heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT));
    ESP_LOGI("MEM", "  最小曾经空闲: %d 字节",
             heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL));

    /* 当前任务剩余栈空间 */
    ESP_LOGI("MEM", "当前任务栈剩余: %d 字节",
             (int)uxTaskGetStackHighWaterMark(NULL) * sizeof(StackType_t));
}
栈溢出检测

在 menuconfig 中启用 FreeRTOS → Check for stack overflow(选 Method 2),当任务栈溢出时会调用 vApplicationStackOverflowHook() 而不是直接崩溃,便于调试。开发阶段建议开启,发布时可关闭节省性能。

ESP32 双核任务绑定

ESP32 有两个 Xtensa LX6 内核(Core 0 = PRO_CPU,Core 1 = APP_CPU)。Wi-Fi/BT 协议栈默认运行在 Core 0,用户任务默认由两个核心负载均衡调度。可以手动将任务绑定到特定核心,避免与协议栈争用 Core 0:

/* xTaskCreatePinnedToCore:绑定任务到指定核心 */
/* 参数:函数, 名称, 栈大小, 参数, 优先级, 句柄, 核心ID */

/* 数据上报任务绑定到 Core 0(和 Wi-Fi 同核,网络操作不用跨核切换)*/
xTaskCreatePinnedToCore(
    mqtt_report_task,     /* 任务函数 */
    "mqtt_report",        /* 任务名称,用于调试 */
    4096,                 /* 栈大小(字节),Wi-Fi 操作需要更大的栈 */
    NULL,                 /* 参数 */
    5,                    /* 优先级(0-24,数字越大越高,configMAX_PRIORITIES-1 为最高)*/
    NULL,                 /* 任务句柄(不需要则 NULL)*/
    0                     /* 核心 ID:0=PRO_CPU, 1=APP_CPU */
);

/* 传感器读取任务绑定到 Core 1(独占核心,实时性更好)*/
xTaskCreatePinnedToCore(sensor_read_task, "sensor", 2048, NULL, 10, NULL, 1);

/* 不绑定核心(tskNO_AFFINITY):任何空闲核心都可以运行 */
xTaskCreate(background_task, "bg", 2048, NULL, 1, NULL);
/* 等价于 xTaskCreatePinnedToCore(..., tskNO_AFFINITY) */
ESP32 优先级与 FreeRTOS 标准的差异

标准 FreeRTOS 优先级 0 最低,数字越大越高。ESP32 的 FreeRTOS 端口使用相同规则,最高优先级由 configMAX_PRIORITIES 决定(默认 25,即 0-24)。注意:ESP-IDF 内置任务(Wi-Fi、BT、idle)使用了多个优先级层级,用户任务通常在 1-19 之间选择。不要使用优先级 24(esp_timer 任务)和 23(Wi-Fi 任务),否则会干扰系统功能。

EventGroup 事件组:多条件同步

EventGroup 适合"等待多个事件同时发生"的场景,如等待 Wi-Fi 连接完成 + MQTT 连接完成后才开始上报数据:

#include "freertos/event_groups.h"

/* 定义事件位(每个位代表一个事件,最多 24 位)*/
#define EVT_WIFI_CONNECTED   (1 << 0)   /* bit 0:Wi-Fi 已连接 */
#define EVT_MQTT_CONNECTED   (1 << 1)   /* bit 1:MQTT 已连接 */
#define EVT_SENSOR_READY     (1 << 2)   /* bit 2:传感器初始化完成 */
#define EVT_ALL_READY        (EVT_WIFI_CONNECTED | EVT_MQTT_CONNECTED | EVT_SENSOR_READY)

EventGroupHandle_t app_events;

void init_task(void) {
    app_events = xEventGroupCreate();
}

/* Wi-Fi 连接回调中设置事件位 */
void on_wifi_connected(void) {
    xEventGroupSetBits(app_events, EVT_WIFI_CONNECTED);
    ESP_LOGI("EVT", "Wi-Fi 连接事件已设置");
}

/* 传感器初始化完成时设置 */
void on_sensor_ready(void) {
    xEventGroupSetBits(app_events, EVT_SENSOR_READY);
}

/* 上报任务:等待所有事件位同时置 1 才开始工作 */
void report_task(void *pvParam) {
    ESP_LOGI("REPORT", "等待系统就绪...");

    /* xEventGroupWaitBits 参数:
     * 事件组句柄
     * 等待的位集合(AND:全部满足;OR:任一满足)
     * pdTRUE:等到后自动清除等待的位
     * pdTRUE:AND 模式(所有指定位都要为 1)
     * pdFALSE:OR 模式(任一位为 1 即可)
     * 超时(portMAX_DELAY = 永久等待)              */
    EventBits_t bits = xEventGroupWaitBits(
        app_events,
        EVT_ALL_READY,   /* 等待全部三个位 */
        pdFALSE,         /* 不自动清除 */
        pdTRUE,          /* AND 模式:全部满足 */
        pdMS_TO_TICKS(30000)   /* 30秒超时 */
    );

    if ((bits & EVT_ALL_READY) == EVT_ALL_READY) {
        ESP_LOGI("REPORT", "系统就绪!开始上报");
        while (1) {
            /* 正常上报逻辑 */
            vTaskDelay(pdMS_TO_TICKS(10000));
        }
    } else {
        ESP_LOGE("REPORT", "等待超时!检查网络和传感器");
    }
    vTaskDelete(NULL);
}
本章小结

ESP32 上的 FreeRTOS 是嵌入式实时操作系统,提供抢占式多任务调度。核心同步原语:Queue(任务间数据传递)、Semaphore(计数/信号)、Mutex(互斥保护共享资源)、EventGroup(多事件同步)。ESP32 双核特性可通过 xTaskCreatePinnedToCore 利用,将 Wi-Fi/MQTT 任务绑定 Core 0,时序敏感任务绑定 Core 1。ISR 中必须使用 FromISR 版本 API(xQueueSendFromISR、xSemaphoreGiveFromISR),并在退出时检查 xHigherPriorityTaskWoken 以决定是否触发任务切换。看门狗(TWDT)防止任务死循环,开发阶段建议开启栈溢出检测。