Skip to content

第七章:FreeRTOS 多任务编程

7.1 为什么需要 RTOS?

单任务(超级循环)的问题:
  while(1) {
      read_sensor();    // 需要 10ms
      update_display(); // 需要 50ms
      handle_uart();    // 需要 5ms
      check_button();   // 需要 1ms
  }
  → 总循环 66ms,按键响应最慢延迟 66ms
  → 某个任务卡住,全部卡住

FreeRTOS 多任务:
  任务A(传感器,优先级2):每 100ms 运行一次
  任务B(显示,优先级1):每 50ms 运行一次
  任务C(UART,优先级3):有数据时立即运行
  → 各任务独立,互不干扰,响应及时

7.2 任务(Task)

创建和删除任务

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

static const char *TAG = "RTOS";

// 任务函数:必须是无限循环,或调用 vTaskDelete(NULL) 结束
void task_a(void *pvParameters)
{
    int count = 0;
    while (1) {
        ESP_LOGI(TAG, "[任务A] 计数: %d", count++);
        vTaskDelay(pdMS_TO_TICKS(1000));  // 让出 CPU 1 秒
    }
    // 如果任务需要退出:
    // vTaskDelete(NULL);  // NULL = 删除自己
}

void task_b(void *pvParameters)
{
    // 接收参数
    int param = *(int *)pvParameters;
    ESP_LOGI(TAG, "[任务B] 参数: %d", param);

    for (int i = 0; i < 5; i++) {
        ESP_LOGI(TAG, "[任务B] 第 %d 次", i);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
    ESP_LOGI(TAG, "[任务B] 完成,自我删除");
    vTaskDelete(NULL);  // 任务完成后必须删除自己!
}

void app_main(void)
{
    // xTaskCreate 参数:
    //   函数指针, 任务名, 栈大小(字节), 参数, 优先级, 句柄
    xTaskCreate(task_a, "task_a", 2048, NULL, 5, NULL);

    static int param = 42;
    TaskHandle_t task_b_handle;
    xTaskCreate(task_b, "task_b", 2048, &param, 3, &task_b_handle);

    // 在特定核心上运行(ESP32-S3 双核)
    // xTaskCreatePinnedToCore(task_a, "task_a", 2048, NULL, 5, NULL, 0);
    // 核心0 = PRO_CPU,核心1 = APP_CPU
}

任务优先级

ESP-IDF 优先级范围:0(最低)~ configMAX_PRIORITIES-1(最高)
通常 configMAX_PRIORITIES = 25

建议分配:
  0     → 空闲任务(系统保留)
  1     → 低优先级后台任务(日志、统计)
  5     → 普通应用任务
  10    → 实时性要求较高(传感器采集)
  15    → 高实时性(电机控制)
  20+   → 系统级任务(Wi-Fi、BT 协议栈)

原则:优先级越高,越优先执行;相同优先级,时间片轮转

任务状态

Running  → 正在执行(单核同时只有一个)
Ready    → 就绪,等待 CPU
Blocked  → 阻塞(等待延时、信号量、队列等)
Suspended → 挂起(被 vTaskSuspend 暂停)
Deleted  → 已删除

状态转换:
  创建 → Ready
  调度 → Running
  vTaskDelay → Blocked → (时间到) → Ready
  vTaskSuspend → Suspended → (vTaskResume) → Ready
  vTaskDelete → Deleted

7.3 队列(Queue)

队列是任务间通信的主要方式

c
#include "freertos/queue.h"

// 定义消息结构
typedef struct {
    uint8_t  type;      // 消息类型
    uint32_t value;     // 数据
    char     text[32];  // 文本
} msg_t;

static QueueHandle_t msg_queue;

// 生产者任务
void producer_task(void *arg)
{
    msg_t msg;
    int i = 0;
    while (1) {
        msg.type  = 0x01;
        msg.value = i++;
        snprintf(msg.text, sizeof(msg.text), "消息 #%d", i);

        // 发送到队列(等待最多 100ms)
        if (xQueueSend(msg_queue, &msg, pdMS_TO_TICKS(100)) != pdTRUE) {
            ESP_LOGW("PROD", "队列满,消息丢弃");
        }
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

// 消费者任务
void consumer_task(void *arg)
{
    msg_t msg;
    while (1) {
        // 阻塞等待消息(永久等待)
        if (xQueueReceive(msg_queue, &msg, portMAX_DELAY) == pdTRUE) {
            ESP_LOGI("CONS", "收到消息: type=%d value=%"PRIu32" text=%s",
                     msg.type, msg.value, msg.text);
        }
    }
}

void app_main(void)
{
    // 创建队列:深度 10,每个元素 sizeof(msg_t)
    msg_queue = xQueueCreate(10, sizeof(msg_t));
    configASSERT(msg_queue != NULL);

    xTaskCreate(producer_task, "producer", 2048, NULL, 5, NULL);
    xTaskCreate(consumer_task, "consumer", 2048, NULL, 5, NULL);
}

队列常用操作

c
// 发送(到队尾)
xQueueSend(queue, &item, timeout);

// 发送(到队头,高优先级消息)
xQueueSendToFront(queue, &item, timeout);

// 接收(并从队列移除)
xQueueReceive(queue, &item, timeout);

// 查看(不移除)
xQueuePeek(queue, &item, timeout);

// 查询队列中的消息数量
uxQueueMessagesWaiting(queue);

// 查询队列剩余空间
uxQueueSpacesAvailable(queue);

// 清空队列
xQueueReset(queue);

// ISR 中使用(带 FromISR 后缀)
xQueueSendFromISR(queue, &item, &xHigherPriorityTaskWoken);
xQueueReceiveFromISR(queue, &item, &xHigherPriorityTaskWoken);

7.4 信号量(Semaphore)

二值信号量(互斥/同步)

c
#include "freertos/semphr.h"

static SemaphoreHandle_t sem;

// 任务A:等待信号
void task_wait(void *arg)
{
    while (1) {
        // 等待信号量(阻塞)
        if (xSemaphoreTake(sem, portMAX_DELAY) == pdTRUE) {
            ESP_LOGI("SEM", "收到信号,开始处理");
            // 处理工作...
        }
    }
}

// 任务B(或 ISR):发出信号
void task_signal(void *arg)
{
    while (1) {
        vTaskDelay(pdMS_TO_TICKS(2000));
        xSemaphoreGive(sem);  // 发出信号
        ESP_LOGI("SEM", "已发出信号");
    }
}

void app_main(void)
{
    sem = xSemaphoreCreateBinary();
    xTaskCreate(task_wait,   "wait",   2048, NULL, 5, NULL);
    xTaskCreate(task_signal, "signal", 2048, NULL, 5, NULL);
}

互斥量(Mutex)—— 保护共享资源

c
static SemaphoreHandle_t mutex;
static int shared_counter = 0;

// 安全地修改共享变量
void safe_increment(void)
{
    xSemaphoreTake(mutex, portMAX_DELAY);  // 加锁
    shared_counter++;                       // 临界区
    xSemaphoreGive(mutex);                 // 解锁
}

// 错误示例(不加锁的竞态条件):
// 任务A读取 counter=5
// 任务B读取 counter=5(切换发生在这里)
// 任务A写入 counter=6
// 任务B写入 counter=6(应该是 7!)

void app_main(void)
{
    mutex = xSemaphoreCreateMutex();
    // 两个任务同时调用 safe_increment,结果正确
}

计数信号量

c
// 计数信号量:控制资源池(如连接池、缓冲区池)
SemaphoreHandle_t count_sem = xSemaphoreCreateCounting(
    5,  // 最大计数值(资源总数)
    5   // 初始计数值
);

// 获取资源
xSemaphoreTake(count_sem, portMAX_DELAY);
// 使用资源...
// 释放资源
xSemaphoreGive(count_sem);

7.5 事件组(Event Group)

c
#include "freertos/event_groups.h"

// 定义事件位
#define EVT_WIFI_CONNECTED  BIT0
#define EVT_SENSOR_READY    BIT1
#define EVT_DATA_RECEIVED   BIT2

static EventGroupHandle_t evt_group;

// 等待多个事件同时发生
void task_main(void *arg)
{
    // 等待 Wi-Fi 连接 AND 传感器就绪(两个都要)
    EventBits_t bits = xEventGroupWaitBits(
        evt_group,
        EVT_WIFI_CONNECTED | EVT_SENSOR_READY,  // 等待的位
        pdTRUE,   // 等到后清除这些位
        pdTRUE,   // pdTRUE=AND(全部),pdFALSE=OR(任一)
        portMAX_DELAY
    );

    if (bits & EVT_WIFI_CONNECTED) ESP_LOGI("EVT", "Wi-Fi 已连接");
    if (bits & EVT_SENSOR_READY)   ESP_LOGI("EVT", "传感器就绪");
    ESP_LOGI("EVT", "所有条件满足,开始工作");
}

// 设置事件位
void wifi_connected_cb(void)
{
    xEventGroupSetBits(evt_group, EVT_WIFI_CONNECTED);
}

7.6 任务通知(Task Notification)

c
// 比信号量更轻量,每个任务内置一个通知值

TaskHandle_t target_task;

// 发送通知(类似信号量 Give)
xTaskNotifyGive(target_task);

// 等待通知(类似信号量 Take)
uint32_t count = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

// 发送带值的通知
xTaskNotify(target_task, 0x1234, eSetValueWithOverwrite);

// 等待带值的通知
uint32_t value;
xTaskNotifyWait(0, ULONG_MAX, &value, portMAX_DELAY);

7.7 内存与栈管理

c
// 查看任务栈使用情况(调试用)
UBaseType_t stack_left = uxTaskGetStackHighWaterMark(NULL);
ESP_LOGI(TAG, "当前任务剩余栈: %u words", stack_left);

// 查看堆内存
ESP_LOGI(TAG, "空闲堆: %lu bytes", esp_get_free_heap_size());
ESP_LOGI(TAG, "最小空闲堆: %lu bytes", esp_get_minimum_free_heap_size());

// 在 PSRAM 中分配大内存(ESP32-S3 N16R8 有 8MB PSRAM)
void *big_buf = heap_caps_malloc(1024 * 1024, MALLOC_CAP_SPIRAM);
if (big_buf == NULL) {
    ESP_LOGE(TAG, "PSRAM 分配失败");
}
heap_caps_free(big_buf);

// 在内部 SRAM 中分配(速度快)
void *fast_buf = heap_caps_malloc(1024, MALLOC_CAP_INTERNAL);

📝 第七章练习题

练习 7-1:多任务基础(基础)

目标:掌握任务创建和调度

任务:创建三个任务:

  • 任务A(优先级5):每 1 秒打印 "任务A运行"
  • 任务B(优先级3):每 500ms 打印 "任务B运行"
  • 任务C(优先级1):每 200ms 打印 "任务C运行"

观察:三个任务是否都在运行?优先级如何影响执行顺序?


练习 7-2:生产者-消费者(基础)

目标:掌握队列通信

任务

  • 生产者任务:每 200ms 生成一个随机数(0~99),放入队列
  • 消费者任务:从队列取出数据,计算并打印:当前值、历史最大值、历史最小值、平均值
  • 队列深度:5
  • 当队列满时,生产者打印警告并丢弃数据

练习 7-3:互斥量保护(进阶)

目标:理解竞态条件和互斥量

任务

  • 创建一个全局计数器
  • 两个任务同时对计数器执行 10000 次 +1 操作
  • 版本1:不加互斥量,观察最终结果(应该 < 20000)
  • 版本2:加互斥量,验证结果正好是 20000
  • 打印两个版本的结果和耗时

练习 7-4:事件驱动系统(进阶)

目标:事件组 + 多任务协作

任务:模拟一个简单的系统启动流程:

事件位定义:
  BIT0 = 硬件初始化完成
  BIT1 = 配置加载完成
  BIT2 = 网络连接完成

启动任务:等待 BIT0 AND BIT1 AND BIT2 全部置位后,打印 "系统启动完成"

硬件初始化任务:延时 1s 后置位 BIT0
配置加载任务:延时 2s 后置位 BIT1
网络连接任务:延时 3s 后置位 BIT2

要求:打印每个事件发生的时间戳

练习 7-5:任务看门狗(挑战)

目标:系统健壮性设计

任务:实现一个软件看门狗系统:

  • 监控任务:每 5 秒检查所有被监控任务是否"喂狗"
  • 被监控任务A:正常运行,每 2 秒喂狗一次
  • 被监控任务B:模拟卡死(运行 10 秒后停止喂狗)
  • 当某任务超时未喂狗,打印警告并重启该任务

数据结构

c
typedef struct {
    TaskHandle_t handle;
    const char  *name;
    uint32_t     last_feed_ms;   // 上次喂狗时间
    uint32_t     timeout_ms;     // 超时时间
    bool         enabled;
} watchdog_entry_t;

练习 7-6:思考题

  1. vTaskDelay(pdMS_TO_TICKS(1000)) 期间,任务处于什么状态?CPU 在做什么?

  2. 互斥量(Mutex)和二值信号量(Binary Semaphore)都能实现互斥,有什么区别?什么是优先级反转?互斥量如何解决它?

  3. 队列深度设为多少合适?太小和太大各有什么问题?

  4. ESP32-S3 是双核的,xTaskCreatexTaskCreatePinnedToCore 有什么区别?什么任务适合固定在某个核心上?

  5. 任务栈大小设为 2048 字节够用吗?如何判断栈是否溢出?


本章小结

知识点掌握程度自评
任务创建与优先级⬜⬜⬜⬜⬜
队列通信⬜⬜⬜⬜⬜
二值信号量⬜⬜⬜⬜⬜
互斥量⬜⬜⬜⬜⬜
事件组⬜⬜⬜⬜⬜
内存管理⬜⬜⬜⬜⬜

上一章← ADC 与 DAC下一章存储与 NVS →

个人知识库