
本节目标理解任务 / 优先级 / 阻塞延时会用队列、信号量互斥量、事件组、直达任务通知这五件最常用的 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_HZ100即 1 个 tick 10ms毫秒换算成 tick 用pdMS_TO_TICKS()注意换算会向下取整成整数个 tick。ESP32 是双核示例统一用xTaskCreatePinnedToCore()把任务固定到 CORE1 上运行避免任务在核间迁移。2.3 任务创建//以下创建两个任务任务栈大小为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 写回 1A 恢复后也写回 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只唤醒目标任务通知值不更新此时不使用 ulValueeSetBits目标任务的通知值按位或上 ulValueeIncrement目标任务的通知值自增 1类似信号量的 Give 操作eSetValueWithOverwrite无条件把目标任务的通知值设置为 ulValueeSetValueWithoutOverwrite仅当目标任务没有挂起通知时才写入 ulValue四、代码分析4.1 工程结构rtos/ ├── CMakeLists.txt ├── main/ │ ├── CMakeLists.txt # idf_component_register(SRCS main.c INCLUDE_DIRS .) │ └── main.c # 五种 freeRTOS 特性的例程都在这一个文件里 └── sdkconfig5 个例程写在同一个 main.c 中app_main()里注释掉其余的、只留一个//入口函数voidapp_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 taskAtaskB 每 700ms 打印一次两者同优先级、同核心靠vTaskDelay()放弃 CPU串口上两条日志按各自的节奏交替出现。4.3 队列例程生产者 queue_taskA 每秒组装一个queue_packet只含一个 int 成员发送后打印再延时queue_packet packet;packet.numtest_cnt;//发送queue_packet数据xQueueSend(s_testQueue,packet,pdMS_TO_TICKS(200));消费者 queue_taskB 自己不做延时阻塞在接收上数据一到立刻被唤醒BaseType_t retxQueueReceive(s_testQueue,packet,pdMS_TO_TICKS(200));if(retpdTRUE){//如果收到数据就打印出来ESP_LOGI(TAG,taskB receive packet,num:%d,packet.num);}队列深度 5、每项大小sizeof(queue_packet)一行创建//初始化一个队列队列单元内容是queue_packet结构体最大长度是5s_testQueuexQueueCreate(5,sizeof(queue_packet));4.4 信号量例程s_testBinSemxSemaphoreCreateBinary();s_testCountSemxSemaphoreCreateCounting(5,0);s_testMuxSemxSemaphoreCreateMutex();sem_taskA 每 2 秒 Give 三种信号量二值信号量 1 次计数信号量 5 次相当于把 5 把伞都还回池子互斥量 1 次。sem_taskB 依次无限等待二值信号量 → 用 do-while 把计数信号量一次拿到拿空每次最多等 200ms拿不到就退出循环→ 无限等待互斥量。与教程不一致代码写法隐患本工程互斥量用xSemaphoreCreateMutex()创建却使用了递归 APIxSemaphoreTakeRecursive()/xSemaphoreGiveRecursive()。FreeRTOS 要求递归 API 只能配合xSemaphoreCreateRecursiveMutex()创建的递归互斥量使用这里属于不规范用法自己写代码时创建函数和 Take/Give 函数一定要配对。另外注意这个示例的语义B 任务取走的信号量不会归还没有对应的 Give所以它打印完 5 个计数信号量后就退出等待下一轮要等 A 再次 Give。真实工程里借伞一定要还伞。4.5 事件组例程event_taskA 先置 BIT0等 1 秒再置 BIT1再等 1.5 秒进入下一轮event_taskB 用一次xEventGroupWaitBits()同时等两位evxEventGroupWaitBits(s_testEvent,BIT0|BIT1,pdTRUE,pdFALSE,portMAX_DELAY);if(evBIT0){ESP_LOGI(TAG,Event BIT0 set);}if(evBIT1){ESP_LOGI(TAG,Event BIT1 set);}pdTRUE表示返回时自动清除等待位pdFALSE表示任意一位置位即可返回。任务侧用两个独立的if而不是if/else因为某一刻可能多位同时被置上。4.6 直达任务通知例程要通知某个任务必须先拿到它的句柄所以创建任务时传入了s_notifyTaskAHandlexTaskCreatePinnedToCore(notify_taskA,notify_taskA,2048,NULL,3,s_notifyTaskAHandle,1);notify_taskB 每秒把自增值直接投递给任务 AxTaskNotify(s_notifyTaskAHandle,notify_val,eSetValueWithOverwrite);notify_val;vTaskDelay(pdMS_TO_TICKS(1000));任务 A 阻塞等待并读出通知值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.hvTaskDelay()/pdMS_TO_TICKS()阻塞延时毫秒换算成 tickfreertos/task.hxQueueCreate()创建队列队列长度 × 单项字节数freertos/queue.hxQueueSend()向队尾发送数据等价于 xQueueSendToBackfreertos/queue.hxQueueReceive()从队头接收数据可阻塞等待freertos/queue.hxQueueSendFromISR()中断服务程序中的发送版本freertos/queue.hxSemaphoreCreateBinary()创建二值信号量数量最大为 1freertos/semphr.hxSemaphoreCreateCounting()创建计数信号量最大数、初始数freertos/semphr.hxSemaphoreCreateMutex()创建互斥量支持优先级继承freertos/semphr.hxSemaphoreTake()/xSemaphoreGive()获取 / 释放信号量freertos/semphr.hxSemaphoreTakeRecursive()/xSemaphoreGiveRecursive()递归互斥量的获取 / 释放freertos/semphr.hxEventGroupCreate()创建事件组freertos/event_groups.hxEventGroupWaitBits()等待一个或多个事件位freertos/event_groups.hxEventGroupSetBits()/xEventGroupClearBits()置位 / 清除事件位freertos/event_groups.hxTaskNotify()直达任务通知发送freertos/task.hxTaskNotifyWait()直达任务通知等待并读取通知值freertos/task.h六、编译与现象验证cdesp32-board/rtos idf.py build idf.py-pCOM端口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 semaphorertos_event_sample()先打印Event BIT0 set约 1 秒后打印Event BIT1 set然后循环rtos_notify_sample()每秒一行receive notify value:NN 递增。