一、学习ESP32中FreeRTOS的必要性
- ESP-IDF中的examples都是使用的FreeRTOS;
- ESP-IDF框架的源码都是基于FreeRTOS进行开发的;
- 使用FreeRTOS便于模块化开发;
二、任务
1.任务的基本概念
- 任务的概念:使用FreeRTOS操作系统的实时应用程序可以被构建成一组独立的任务,每个任务在自己的上下文中执行不依赖于系统内的其他任务或RTOS调度器本身;
- 任务的状态
- 准备就绪:指能够执行(不处于阻塞或挂起状态),但由于高优先级任务或同等优先级但序号靠前的任务正在运行,目前没有执行的任务;
- 运行:正在执行的任务;
- 阻塞:正在等待时间或外部事件的任务;例如调用vTaskDelay(),任务将被阻塞;任务也可以通过阻塞等待队列、信号量、事件组、通知;
- 挂起 :处于挂起状态的任务,调度器不再对其进行调度。任务只有分别通过vTaskSuspend()和xTaskResume()才能进入或退出挂起状态
2.任务相关的API
-
创建任务API
BaseType_t xTaskCreatePinnedToCore( TaskFunction_t pxTaskCode, const char * const pcName, const uint32_t ulStackDepth, void * const pvParameters, UBaseType_t uxPriority, TaskHandle_t * const pxCreatedTask, const BaseType_t xCoreID ); // pxTaskCode:任务函数指针 // pcName:任务名 // ulStackDepth:任务栈大小(单位:字节) // pvParameters:任务参数 // uxPriority:任务优先级(值越大,优先级越高,取值范围0 ~ configMAX_PRIORITIES -1),(configMAX_PRIORITIES在FreeRTOSConfig.h中定义) // pxCreatedTask:传回来的任务句柄 // xCoreID:运行任务的内核ID // return: 成功返回pdPASS
- 该API是原生FreeRTOS中没有的,是乐鑫官方自己重新实现的一个函数,用于适配ESP32的双核;
-
任务延时API
void vTaskDelay( const TickType_t xTicksToDelay ); // xTicksToDelay:任务阻塞的时间,单位:系统滴答数; /* 任务中调用该API后会立即进入阻塞状态,但不意味着经过给定阻塞时间后,任务能立刻得到执行; 具体要看系统的调度情况,即经过阻塞时间后是否有更高优先级的任务在运行; */
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement ) // pxPreviousWakeTime:指向“任务最后处于非阻塞时刻”的指针 // xTimeIncrement:任务需要阻塞时间; // return:任务是否精确地阻塞了规定时间,是返回pdTRUE,否返回pdFALSE /* 该API用于设置任务精确的解除阻塞时间 */ portmacro.h #define portBASE_TYPE int typedef portBASE_TYPE BaseType_t;
3.示例代码
-
创建工程
-
添加ESP-IDF路径到工程中
#include <stdio.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_log.h" void taskA(void *param) { while(1) { ESP_LOGI("main", "hello world!"); vTaskDelay(pdMS_TO_TICKS(500)); } } void app_main(void) { xTaskCreatePinnedToCore(taskA, "taskA", 2048, NULL , 3, NULL, 1); }
三、消息队列(用于任务间同步)
1.任务间同步与互斥
- 任务同步,指确保不同任务之间或任务与外部事件之间按照预期的顺序或时机执行。其涉及到任务间的通信和协调机制;
- 任务互斥,确保同一时刻只有一个任务对临界资源进行访问;
2.消息队列的概念
- 消息队列遵循“先进先出”的原则传输数据,用于任务间的数据传输;
3.消息队列的API
- 创建队列
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize ); // UBaseType_t uxQueueLength: 队列项的数量 // UBaseType_t uxItemSize :单个队列项的大小,单位:字节 // QueueHandle_t :成功返回队列句柄
- 在任务中向队列中发送消息
BaseType_t xQueueSend( QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait ) // QueueHandle_t xQueue:队列句柄 // const void *pvItemToQueue:指向发送消息的指针; // TickType_t xTicksToWait:如果队列满了等待队列有空间的时间 // return: 成功发送返回pdTRUE
- 在中断中向队列中发送消息
BaseType_t xQueueSendFromISR( QueueHandle_t xQueue, const void *pvItemToQueue, BaseType_t *pxHigherPriorityTaskWoken ) // QueueHandle_t xQueue: 队列句柄 // const void *pvItemToQueue:指向发送消息的指针; // BaseType_t *pxHigherPriorityTaskWoken: 如果发送队列导致任务解除阻塞,且解除阻塞的任务的优先级高于当前运行的任务,则*pxHigherPriorityTaskWoken被置pdTRUE--->此时在退出中断前要执行请求上下文切换函数,即void taskYIELD(void); // return: pdPASS,即使队列满了,该API也会向队列写入内容
- 从队列中接收消息
BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer,TickType_t xTicksToWait ) // QueueHandle_t xQueue: 队列句柄 // void * const pvBuffer: 指向接收消息的指针 // TickType_t xTicksToWait: 如果队列为空,等待队列中有数据的时间 // return: 成功接收返回pdTRUE
4.示例代码
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_log.h"
QueueHandle_t queue_handle = NULL;
typedef struct
{
int value;
}test_queue;
void TaskA(void *param)
{
test_queue sd = {0};
while(1)
{
xQueueSend(queue_handle, &sd, pdTICKS_TO_MS(2000));
sd.value ++;
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void TaskB(void *param)
{
test_queue rd = {0};
while(1)
{
if(pdTRUE == xQueueReceive(queue_handle, &rd, pdMS_TO_TICKS(2000)))
{
ESP_LOGI("queue", "queue recv data:%d", rd.value);
}
}
}
void app_main(void)
{
queue_handle = xQueueCreate(4, sizeof(test_queue));
xTaskCreatePinnedToCore(TaskA, "TaskA", 2048, NULL, 3, NULL, 1);
xTaskCreatePinnedToCore(TaskB, "TaskB", 2048, NULL, 3, NULL, 1);
}
四、信号量与互斥锁(用于任务间同步与互斥)
1.信号量的作用
- 互斥:保护共享资源,避免多任务同时访问发生数据混乱;
- 同步:协调任务与任务、任务与中断的执行时序,确保事件按预期触发;
- 资源计数:记录有限资源数量,管理任务对资源的申请与释放;
2.信号量的分类
- 二值信号量:二值信号量最大值为1,即信号量个数只有1个,
- 计数信号量: 信号量的个数可以为多个;
3.互斥锁
- 互斥锁与二进制信号量极为相似,但互斥锁实现了优先级继承,因此,互斥锁常用于临界区资源的互斥访问;
(1)优先级继承
- 当低优先级任务占用高优先级任务所需资源时,系统临时将低优先级任务的优先级提升至高优先级任务的水平,确保高优先级任务能尽快获得资源并运行;
(2)优先级反转
- 假设应用程序中有三个任务A、B、C,优先级A>B>C;
当前任务C正在运行,且正在占用资源S;此时任务A需要执行,但任务A也需要使用资源S,那么任务A就只能阻塞等待任务C释放资源S;
当任务C还在运行时,任务B需要执行,由于任务B优先级>任务C,CPU会运行任务B,任务C处于就绪态。
此时出现了任务A优先级>任务B,但是任务B先于优先级A执行,这种情况就是优先级反转。
4.信号量/互斥锁的API
- 创建二值信号量
SemaphoreHandle_t xSemaphoreCreateBinary(void); //return:成功返回二值信号量句柄
- 创建计数信号量
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, UBaseType_t uxInitialCount); // UBaseType_t uxMaxCount: 计数信号量最大值; // UBaseType_t uxInitialCount: 计数信号量初始值 // return: 成功返回计数信号量句柄
- 创建互斥锁
SemaphoreHandle_t xSemaphoreCreateMutex(void); // return: 互斥锁的句柄
- 获取一个信号量/互斥锁
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait); // SemaphoreHandle_t xSemaphore: 信号量句柄; // TickType_t xTicksToWait: 等待时间 // return: 成功获取信号量返回pdTRUE,反之,pdFALSE
- 释放一个信号量/互斥锁
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore); // SemaphoreHandle_t xSemaphore: 信号量句柄; // return: 成功释放返回pdTRUE,反之,pdFALSE;
- 删除一个信号量/互斥锁
void vSemaphoreDelete(SemaphoreHandle_t xSemaphore); // SemaphoreHandle_t xSemaphore: 信号量句柄
5. 示例代码
-
信号量的示例代码
#include <stdio.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" #include "esp_log.h" SemaphoreHandle_t SemHandle = NULL; void TaskA(void *param) { while(1) { xSemaphoreGive(SemHandle); vTaskDelay(pdMS_TO_TICKS(1000)); } } void TaskB(void *param) { while(1) { if(pdTRUE == xSemaphoreTake(SemHandle, portMAX_DELAY)) { ESP_LOGI("sem", "task B take binsem success"); } } } void app_main(void) { SemHandle = xSemaphoreCreateBinary(); xTaskCreatePinnedToCore(TaskA, "TaskA", 2048, NULL, 3, NULL, 1); xTaskCreatePinnedToCore(TaskB, "TaskB", 2048, NULL, 3, NULL, 1); }
-
互斥锁示例代码
#include <stdio.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" #include "esp_log.h" SemaphoreHandle_t Mutex = NULL; int Soure = 10; void TaskA(void *param) { while(1) { // xSemaphoreTake(Mutex, portMAX_DELAY); Soure ++; ESP_LOGI("mutex", "TaskA--->Soure:%d", Soure); vTaskDelay(pdMS_TO_TICKS(1000)); // xSemaphoreGive(Mutex); vTaskDelay(pdMS_TO_TICKS(10)); } } void TaskB(void *param) { while(1) { // xSemaphoreTake(Mutex, portMAX_DELAY); Soure --; ESP_LOGI("mutex", "TaskB--->Soure:%d", Soure); vTaskDelay(pdMS_TO_TICKS(500)); // xSemaphoreGive(Mutex); vTaskDelay(pdMS_TO_TICKS(10)); } } void app_main(void) { Mutex = xSemaphoreCreateMutex(); xTaskCreatePinnedToCore(TaskA, "TaskA", 2048, NULL, 3, NULL, 1); xTaskCreatePinnedToCore(TaskB, "TaskB", 2048, NULL, 3, NULL, 1); }
五、事件组、任务通知
1. 事件组
- 事件位:用于指示事件是否发生,事件位通常称为事件标志;
- 事件组:一组事件位。事件组的事件位通过位编号来引用;
2.事件组的API
- 创建事件组;
EventGroupHandle_t xEventGroupCreate(void); // return: 成功,返回事件组句柄;失败返回NULL;
- 设置标志位;
EventBits_t xEventGroupSetBits(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet); // EventGroupHandle_t xEventGroup: 事件组句柄; // const EventBits_t uxBitsToSet: 按位值,指示要设置的位; /* return: 事件组的值。 返回值中指定的uxBitsToSet参数所对应的位可能被清除,原因如下: 1.设置某个位导致一个正在等待该位的任务退出阻塞状态,那么该位可能会被自动清除(即xEventGroupWaitBits中xClearOnExit被设置位pdTRUE); 2.优先级高于调用xEventGroupSetBits任务的任务执行,并在xEventGroupSetBits返回之前更改事件组的值; */
- 等待事件组中某个标志位,用返回值以确定哪些位已完成设置;
EventBits_t xEventGroupWaitBits(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToWaitFor, const BaseType_t xClearOnExit, const BaseType_t xWaitForAllBits, TickType_t xTicksToWait); // EventGroupHandle_t xEventGroup: 事件组句柄; // const EventBits_t uxBitsToWaitFor: 需要等待的事件位; // const BaseType_t xClearOnExit: 函数返回时,是否清除所有标志位; // const BaseType_t xWaitForAllBits: 是否等待所有标志位都被设置才返回; // TickType_t xTicksToWait: 最大阻塞时间 // return: 标志位被设置或阻塞时间到期时,事件组返回的值。注意:当xClearOnExit被设置位pdTRUE时,返回的值是被清除之前的事件组的值;
- 清除标志位;
EventBits_t xEventGroupClearBits(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToClear); // EventGroupHandle_t xEventGroup: 事件组句柄 // const EventBits_t uxBitsToClear: 按位置,要清除的位; // return: 指定的位被清除之前事件组的值;
3.事件组示例代码
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_log.h"
EventGroupHandle_t test_evgroup;
#define NUM0_BIT BIT0
#define NUM1_BIT BIT1
void TaskA(void *param)
{
while(1)
{
xEventGroupSetBits(test_evgroup, BIT0);
vTaskDelay(pdMS_TO_TICKS(1000));
xEventGroupSetBits(test_evgroup, BIT1);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void TaskB(void *param)
{
EventBits_t evgroup_value;
while(1)
{
evgroup_value = xEventGroupWaitBits(test_evgroup,NUM0_BIT | NUM1_BIT, pdTRUE, pdFALSE, portMAX_DELAY);
if(evgroup_value & NUM0_BIT)
{
ESP_LOGI("evgroup", "NUM0_BIT Set");
}
if(evgroup_value & NUM1_BIT)
{
ESP_LOGI("evgroup", "NUM1_BIT Set");
}
}
}
void app_main(void)
{
test_evgroup = xEventGroupCreate();
xTaskCreatePinnedToCore(TaskA, "TaskA", 2048, NULL, 3, NULL, 1);
xTaskCreatePinnedToCore(TaskB, "TaskB", 2048, NULL, 3, NULL, 1);
}
4.任务通知
- 任务通知概念
- 每个RTOS任务都有一个任务通知数组(数组元素个数默认为1,每个元素是一个32位的值);
- 任务通知是直接发送到任务的时间,不是通过中间对象(如队列、信号量、事件组)间接发送到任务的事件;
- 如果数组元素,即32位的值不为0表示有未处理完的通知,此时任务通知处于挂起状态;当32的值清除为0时,任务通知处于非挂起状态;
5.任务通知的API
- 发送任务通知
BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction ); // TaskHandle_t xTaskToNotify: 要通知的任务的任务句柄; // uint32_t ulValue; 携带的通知值 /* eNotifyAction eAction: 执行的操作 eSetBits:目标任务的任务通知值与ulValue进行按位或运算,这种情况下,总是返回pdPASS; eIncrement: 目标通知值加1,此时不使用ulValue,这种情况下,总是返回pdPASS; eSetValueWithOverwrite: 目标通知值被设置为ulValue的值,这种情况下总是返回pdPASS; eSetValueWithoutOverwrite: 如果目标任务通知处于非挂起状态,那么目标通知值将被设置为ulValue,并且返回pdPASS;如果目标任务通知处于挂起状态,那么不执行任何操作,并且返回pdFAIL; eNoAction: 目标任务接收事件,但通知值不更新,不适用ulValue,总是返回pdPASS; */
- 等待接收任务通知
BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry, uint32_t ulBitsToClearOnExit, uint32_t *pulNotificationValue, TickType_t xTicksToWait ); // uint32_t ulBitsToClearOnEntry:进入函数清除的通知位; // uint32_t ulBitsToClearOnExit: 退出函数清除的通知位; // uint32_t *pulNotificationValue: 接收到的通知值; // TickType_t xTicksToWait:等待时长 // return: 成功接收通知,返回pdPASS;等待超时或发生错误,返回pdFAIL;
6.任务通知示例代码
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
TaskHandle_t TaskA_Handle;
TaskHandle_t TaskB_Handle;
void TaskA(void *param)
{
uint32_t value = 0;
while(1)
{
xTaskNotify(TaskB_Handle, value, eSetValueWithOverwrite);
value ++;
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void TaskB(void *param)
{
uint32_t value;
while(1)
{
xTaskNotifyWait(0x00, ULONG_MAX, &value, portMAX_DELAY);
ESP_LOGI("notify", "TaskB recv Notify:%lu", value);
}
}
void app_main(void)
{
xTaskCreatePinnedToCore(TaskA, "TaskA", 2048, NULL, 3, &TaskA_Handle, 1);
xTaskCreatePinnedToCore(TaskB, "TaskB", 2048, NULL, 3, &TaskB_Handle, 1);
}
六、原生FreeRTOS和ESP-IDF中的FreeRTOS区别
- ESP32是双核架构,可以做到真正的并行;原生的FreeRTOS代码没有考虑双核的处理;(原生的FreeRTOS从v11.0开始支持AMP,即非对称多处理架构功能)
- ESP-IDF自动创建5种任务:空闲任务(每个核上都会创建一个,优先级为0)、定时器(优先级为1)、app_main(优先级为1)、IPC(每个核上都会创建一个,用于处理多核协调,优先级最高,为24)、ESP定时器(负责定时器的回调,优先级为22);
- ESP-IDF不使用原生的FreeRTOS的内存堆管理,实现了自己的堆;
- ESP-IDF中创建任务使用xTaskCreatePinnedToCore();
- ESP-IDF中删除任务时避免删除另一个核的任务;
- ESP-IDF为双核,单单禁用中断不能进入临界区,另外一个核还有任务运行,应该采用自旋锁的方式同步;
- 如果任务中用到浮点运算,创建任务时必须指定任务运行在哪个核上,不能由系统自动分配;