【ESP32笔记】freeRTOS使用教学

本节目标:理解任务 / 优先级 / 阻塞延时,会用队列、信号量(互斥量)、事件组、直达任务通知这五件最常用的 FreeRTOS 工具

一、本节要解决的问题

ESP-IDF 本身就是基于 FreeRTOS 的框架:app_main() 其实就是系统创建的一个任务,Wi-Fi、日志、LVGL 等组件内部也都在跑任务。不掌握 FreeRTOS,后面的例程会看不懂。

裸机程序把所有功能塞进一个大循环:某个模块一旦需要延时等待,整个循环都被拖住。用状态机可以缓解,但代码复杂、可读性差。RTOS 的思路是把每个功能模块写成独立任务:需要等待时调用 vTaskDelay() 主动让出 CPU,调度器立即切去运行别的任务,整体程序还是"并发"的。

一句话:裸机是"排队死等",RTOS 是"该等等、该跑跑"。

二、相关知识:任务、状态与优先级

2.1 任务的四个状态

状态 含义
运行(Running) 正在占用 CPU;单核上同一时刻只有一个任务处于运行态
就绪(Ready) 条件已满足,但 CPU 正被同级或更高优先级任务占用,排队等待
阻塞(Blocked) 正在等待时间或事件(队列、信号量、事件组、通知);不消耗 CPU,通常带超时
挂起(Suspended) 被 vTaskSuspend() 显式挂起,没有超时,只能由 xTaskResume() 恢复

vTaskDelay() 是"等时间",xQueueReceive() 拿不到数据、xSemaphoreTake() 拿不到信号量是"等事件"------它们都让任务进入阻塞态,把 CPU 让给别人。这正是 RTOS 与 while(!flag) 死循环轮询最本质的区别。

2.2 优先级、Tick 与双核

  • 优先级数字越大越优先,范围是 0 到 configMAX_PRIORITIES - 1,低优先级数字表示低优先级任务,空闲任务优先级固定为 0;具体上限由 ESP-IDF menuconfig 中 FreeRTOS 的配置决定(生成到 build 目录的 FreeRTOSConfig.h)。
  • 本工程 CONFIG_FREERTOS_HZ=100,即 1 个 tick = 10ms;毫秒换算成 tick 用 pdMS_TO_TICKS(),注意换算会向下取整成整数个 tick。
  • ESP32 是双核,示例统一用 xTaskCreatePinnedToCore() 把任务固定到 CORE1 上运行,避免任务在核间迁移。

2.3 任务创建

c 复制代码
//以下创建两个任务,任务栈大小为2048字节,优先级为3,并设定运行在CORE1上
xTaskCreatePinnedToCore(taskA,"taskA",2048,NULL,3,NULL,1);
xTaskCreatePinnedToCore(taskB,"taskB",2048,NULL,3,NULL,1);

参数依次是:任务函数(原型 void fun(void *param))、任务名(调试打印用)、栈大小、任务参数、优先级、返回的任务句柄(不关心可传 NULL)、绑定到哪个核心。

提示:教程原型注释里写"栈空间大小单位是字节",这对 ESP-IDF 来说是对的;但很多其他 FreeRTOS 资料里的单位是"字(word)",照搬别家例子时别搞混。

三、知识点

3.1 队列(Queue)

队列是任务间通信的主要形式,可作为线程安全的 FIFO 使用,也能在中断和任务之间传消息。数据按值拷贝 入队:发送时会把 uxItemSize 字节的内容整体复制进队列自己的存储区,发送方的局部变量随后销毁也不影响接收。

与教程不一致 :教程把 xQueueSend 注释为"向队列头部发送一个消息",实际 FreeRTOS 中 xQueueSend 与 xQueueSendToBack 完全等价,都是发到队尾 ;要发到队头得用 xQueueSendToFront。以官方文档和头文件为准。

3.2 信号量(Semaphore)

信号量用来保护共享资源、或在任务/中断之间同步。教程的类比:雨伞池------池子里一共 6 把伞(信号量总数),已经在用伞的人(持有信号量的任务)达到 6 个以后,后来者只能等;有人还伞(Give)时,等待者之一才能拿到伞(Take)继续执行。用完必须释放信号量。

  • 二值信号量:总数只有 1,相当于"池子里只有一把伞";
  • 计数信号量:总数可自定义,适合管理多个同类资源或对事件计数。

FreeRTOS 中信号量本质上就是队列,只是"只关心数量、不关心内容"。

3.3 互斥量、临界资源与优先级继承

临界资源 :多任务系统中同一时刻只能被一个任务使用的资源,必须互斥访问。临界区:代码中访问临界资源的那一段。readme 里的类比很形象:临界资源 = 公共卫生间(所有人都能用,但一次只能进一个人),临界区 = "进门 → 锁门 → 使用 → 开门 → 出门"这一整套不能被打断的操作。

为什么不保护就会出错?经典例子:两个任务都对全局变量 count = 0 执行 count++。任务 A 读到 0、还没写回就被切走;任务 B 也读到 0,加 1 写回 1;A 恢复后也写回 1。两次自增结果却是 1------这就是竞态条件,这类 bug 复现随机、排查难度极高。

FreeRTOS 里三种保护方式:

方式 特点 适用场合
taskENTER_CRITICAL() / taskEXIT_CRITICAL() 关任务调度(双核还会获取自旋锁),期间别的任务完全得不到 CPU 只改一个变量/标志位,快进快出
互斥量 xSemaphoreCreateMutex() 拿不到锁的任务进入阻塞、让出 CPU,锁释放后被唤醒 操作外设、读写大缓存等耗时较长的临界区
关中断 最极端,影响所有中断响应 只在中断服务程序与任务共享资源时使用,普通代码不推荐

提示 :ESP-IDF 是双核环境,官方更推荐用显式自旋锁 portENTER_CRITICAL(&spinlock) / portEXIT_CRITICAL(&spinlock) 保护临界区;教程里的 taskENTER_CRITICAL() 是 FreeRTOS 通用形式,具体以所用 IDF 版本的头文件和文档为准。

互斥量(Mutex)与二值信号量的区别 :互斥量有"归属"------谁 Take 谁负责 Give,只能由持有者释放,并且支持优先级继承 。优先级继承解决的是优先级反转:高优先级任务 H 在等一把被低优先级任务 L 持有的锁,而中优先级任务 M 不停抢占 CPU,导致 L 迟迟释放不了锁、H 被无限期推迟。开启优先级继承后,H 一开始等待,L 就被临时提升到 H 的优先级运行,M 抢不过它;L 尽快释放锁,其优先级随即复原。

与教程不一致 :教程正文写"互斥锁:与二进制信号量类似,但会发生优先级翻转",这个表述有误------互斥锁带优先级继承,正是用来避免/缓解优先级反转的;普通二值信号量才没有这个能力。保护共享资源应优先用互斥量。

readme 口诀:改变量、标状态 → 用临界区,快进快出;操作外设、跑复杂逻辑 → 用互斥量,等待让权。

3.4 事件组(Event Group)

事件组是一组事件位(事件标志),按位编号引用,FreeRTOS 可用 24 位(教程图里画的 24 位就是这个)。它适合"等某个/某几个事件组合"的场合:xEventGroupWaitBits() 可设置"任意一位置位就返回"(xWaitForAllBits = pdFALSE)或"所有等待位都置位才返回"(pdTRUE),还能通过 xClearOnExit 参数在返回时自动清除。

3.5 直达任务通知(Task Notification)

每个任务自带一组通知状态:一个"挂起/非挂起"标志和一个 32 位通知值,不需要额外创建任何对象,是 FreeRTOS 里最轻量的任务间通信方式。发送用 xTaskNotify(),接收用 xTaskNotifyWait();eAction 决定怎么处理通知值:

eAction 执行的操作
eNoAction 只唤醒目标任务,通知值不更新,此时不使用 ulValue
eSetBits 目标任务的通知值按位或上 ulValue
eIncrement 目标任务的通知值自增 1(类似信号量的 Give 操作)
eSetValueWithOverwrite 无条件把目标任务的通知值设置为 ulValue
eSetValueWithoutOverwrite 仅当目标任务没有挂起通知时才写入 ulValue

四、代码分析

4.1 工程结构

复制代码
rtos/
├── CMakeLists.txt
├── main/
│   ├── CMakeLists.txt      # idf_component_register(SRCS "main.c" INCLUDE_DIRS ".")
│   └── main.c              # 五种 freeRTOS 特性的例程都在这一个文件里
└── sdkconfig

5 个例程写在同一个 main.c 中,app_main() 里注释掉其余的、只留一个:

c 复制代码
//入口函数
void app_main(void)
{
    /*
    以下是每种freeRTOS特性的测试例程的初始化函数,建议每次只开一个,否则有太多打印影响体验
    */
    //rtos_task_sample();
    //rtos_queue_sample();
    rtos_sem_sample();
    //rtos_event_sample();
    //rtos_notify_sample();
}

与教程不一致:教程 PDF 只抽讲了少量 API,给人的印象是"零散示例";仓库里其实完整实现了任务、队列、信号量、事件组、任务通知 5 个例程,注释也很全,建议对照源码逐段阅读。另外教程没提醒"5 个例程要互斥使用"------仓库代码里当前默认打开的是信号量例程。

4.2 任务例程

taskA 每 500ms 打印一次 this is taskA,taskB 每 700ms 打印一次;两者同优先级、同核心,靠 vTaskDelay() 放弃 CPU,串口上两条日志按各自的节奏交替出现。

4.3 队列例程

生产者 queue_taskA 每秒组装一个 queue_packet(只含一个 int 成员),发送后打印再延时:

c 复制代码
queue_packet packet;
packet.num = test_cnt++;
//发送queue_packet数据
xQueueSend(s_testQueue,&packet,pdMS_TO_TICKS(200));

消费者 queue_taskB 自己不做延时,阻塞在接收上,数据一到立刻被唤醒:

c 复制代码
BaseType_t ret = xQueueReceive(s_testQueue,&packet,pdMS_TO_TICKS(200));
if(ret == pdTRUE)
{
    //如果收到数据就打印出来
    ESP_LOGI(TAG,"taskB receive packet,num:%d",packet.num);
}

队列深度 5、每项大小 sizeof(queue_packet),一行创建:

c 复制代码
//初始化一个队列,队列单元内容是queue_packet结构体,最大长度是5
s_testQueue = xQueueCreate(5,sizeof(queue_packet));

4.4 信号量例程

c 复制代码
s_testBinSem = xSemaphoreCreateBinary();
s_testCountSem = xSemaphoreCreateCounting(5,0);
s_testMuxSem = xSemaphoreCreateMutex();

sem_taskA 每 2 秒 Give 三种信号量:二值信号量 1 次;计数信号量 5 次(相当于把 5 把伞都还回池子);互斥量 1 次。sem_taskB 依次:无限等待二值信号量 → 用 do-while 把计数信号量一次拿到拿空(每次最多等 200ms,拿不到就退出循环)→ 无限等待互斥量。

与教程不一致(代码写法隐患) :本工程互斥量用 xSemaphoreCreateMutex() 创建,却使用了递归 API xSemaphoreTakeRecursive() / xSemaphoreGiveRecursive()。FreeRTOS 要求递归 API 只能配合 xSemaphoreCreateRecursiveMutex() 创建的递归互斥量使用,这里属于不规范用法;自己写代码时创建函数和 Take/Give 函数一定要配对。

另外注意这个示例的语义:B 任务取走的信号量不会归还(没有对应的 Give),所以它打印完 5 个计数信号量后就退出等待;下一轮要等 A 再次 Give。真实工程里"借伞一定要还伞"。

4.5 事件组例程

event_taskA 先置 BIT0,等 1 秒再置 BIT1,再等 1.5 秒进入下一轮;event_taskB 用一次 xEventGroupWaitBits() 同时等两位:

c 复制代码
ev = xEventGroupWaitBits(s_testEvent,BIT0|BIT1,pdTRUE,pdFALSE,portMAX_DELAY);
if(ev & BIT0)
{
    ESP_LOGI(TAG,"Event BIT0 set");
}
if(ev& BIT1)
{
    ESP_LOGI(TAG,"Event BIT1 set");
}

pdTRUE 表示返回时自动清除等待位,pdFALSE 表示任意一位置位即可返回。任务侧用两个独立的 if 而不是 if/else,因为某一刻可能多位同时被置上。

4.6 直达任务通知例程

要通知某个任务,必须先拿到它的句柄,所以创建任务时传入了 &s_notifyTaskAHandle:

c 复制代码
xTaskCreatePinnedToCore(notify_taskA,"notify_taskA",2048,NULL,3,&s_notifyTaskAHandle,1);

notify_taskB 每秒把自增值直接"投递"给任务 A:

c 复制代码
xTaskNotify(s_notifyTaskAHandle, notify_val, eSetValueWithOverwrite);
notify_val++;
vTaskDelay(pdMS_TO_TICKS(1000));

任务 A 阻塞等待并读出通知值:

c 复制代码
if (xTaskNotifyWait(0x00, ULONG_MAX, &rec_val, pdMS_TO_TICKS(1000)) == pdTRUE)
{
    ESP_LOGI(TAG,"receive notify value:%lu",rec_val);
}

与教程不一致(注释与代码相反) :源码里 notify_taskA 的注释写的是"用于定时向任务通知B直接传输数据",但实际 A 是接收方、B 才是发送方,注释恰好写反;阅读时以代码为准。

五、关键 API 速查

API 作用 所在头文件
xTaskCreatePinnedToCore() 创建任务并绑定运行核心 freertos/task.h
vTaskDelay() / pdMS_TO_TICKS() 阻塞延时(毫秒换算成 tick) freertos/task.h
xQueueCreate() 创建队列(队列长度 × 单项字节数) freertos/queue.h
xQueueSend() 向队尾发送数据(等价于 xQueueSendToBack) freertos/queue.h
xQueueReceive() 从队头接收数据,可阻塞等待 freertos/queue.h
xQueueSendFromISR() 中断服务程序中的发送版本 freertos/queue.h
xSemaphoreCreateBinary() 创建二值信号量(数量最大为 1) freertos/semphr.h
xSemaphoreCreateCounting() 创建计数信号量(最大数、初始数) freertos/semphr.h
xSemaphoreCreateMutex() 创建互斥量(支持优先级继承) freertos/semphr.h
xSemaphoreTake() / xSemaphoreGive() 获取 / 释放信号量 freertos/semphr.h
xSemaphoreTakeRecursive() / xSemaphoreGiveRecursive() 递归互斥量的获取 / 释放 freertos/semphr.h
xEventGroupCreate() 创建事件组 freertos/event_groups.h
xEventGroupWaitBits() 等待一个或多个事件位 freertos/event_groups.h
xEventGroupSetBits() / xEventGroupClearBits() 置位 / 清除事件位 freertos/event_groups.h
xTaskNotify() 直达任务通知:发送 freertos/task.h
xTaskNotifyWait() 直达任务通知:等待并读取通知值 freertos/task.h

六、编译与现象验证

bash 复制代码
cd esp32-board/rtos
idf.py build
idf.py -p COM<端口> flash monitor

该工程 sdkconfig 当前目标为 esp32(经典款) 。若你的板子芯片型号不同,先执行 idf.py set-target <型号> 再编译。

依次打开 5 个例程(每次只留一个)应该看到:

  • rtos_task_sample():this is taskA 每 500ms 一条、this is taskB 每 700ms 一条,交替打印;
  • rtos_queue_sample():每秒一行 taskA send packet,num:x 和一行 taskB receive packet,num:x,两行序号一致;
  • rtos_sem_sample():每 2 秒看到 take binary semaphore、5 行 take count semaphore,count:1~5、take Mutex semaphore;
  • rtos_event_sample():先打印 Event BIT0 set,约 1 秒后打印 Event BIT1 set,然后循环;
  • rtos_notify_sample():每秒一行 receive notify value:N,N 递增。
相关推荐
wuyk5551 小时前
28 搜索引擎索引更新:从 LSM Tree 到布隆过滤器的高效实现
stm32·单片机·嵌入式硬件·算法
博纬智能RFID1 小时前
园区多个关键卡口:如何用RFID门禁做好人员进出管控?
人工智能·物联网
Xudde.1 小时前
Quaoar 靶机渗透测试(Vulnhub Writeup)
笔记·学习·安全·web安全
ss2731 小时前
PG实战笔记 | 06 PostgreSQL基础安装部署:Linux/Windows/macOS全攻略
linux·笔记·postgresql
权球物联分享物联网连接服务1 小时前
AUV潜航器数据频繁掉线卡顿?水面通信舱物联网卡选型攻略
物联网
会议咨询2 小时前
2026年人机交互、机械工程与物联网国际会议(HMIT 2026)
物联网·人机交互·机械工程
万年咸鱼2 小时前
iOS 27 适配笔记
笔记·ios·cocoa
MuMuMu12232 小时前
越华环保集团碳惠小屋:物联网 + 碳普惠,构建线下低碳行为数字化采集方案
物联网
Swilderrr2 小时前
论文精读 | Fast Approximate Similarity Join in Vector Databases(SimJoin)
笔记·学习