Appearance
第七章: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, ¶m, 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 → Deleted7.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:思考题
vTaskDelay(pdMS_TO_TICKS(1000))期间,任务处于什么状态?CPU 在做什么?互斥量(Mutex)和二值信号量(Binary Semaphore)都能实现互斥,有什么区别?什么是优先级反转?互斥量如何解决它?
队列深度设为多少合适?太小和太大各有什么问题?
ESP32-S3 是双核的,
xTaskCreate和xTaskCreatePinnedToCore有什么区别?什么任务适合固定在某个核心上?任务栈大小设为 2048 字节够用吗?如何判断栈是否溢出?
本章小结
| 知识点 | 掌握程度自评 |
|---|---|
| 任务创建与优先级 | ⬜⬜⬜⬜⬜ |
| 队列通信 | ⬜⬜⬜⬜⬜ |
| 二值信号量 | ⬜⬜⬜⬜⬜ |
| 互斥量 | ⬜⬜⬜⬜⬜ |
| 事件组 | ⬜⬜⬜⬜⬜ |
| 内存管理 | ⬜⬜⬜⬜⬜ |
上一章:← ADC 与 DAC下一章:存储与 NVS →