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() 而不是直接崩溃,便于调试。开发阶段建议开启,发布时可关闭节省性能。