ESP32的FreeRTOS框架重点

本笔记为作者再学习ESP32的一些心得体会,如有不对的地方,请包含与谅解!

​ ————by wsoz

FreeRTOS框架

FreeRTOS是一个实时操作系统 (Real-Time Operating System, RTOS)。多任务,多线程执行轮询使用CPU完成任务。标准的FreeRTOS是为单核MCU设计的。但ESP32(及后续S2, S3, C3等)很多型号是双核的。乐鑫对原本的RTOS进行了修改支持双核:

  • ESP-IDF FreeRTOS 可以自动地将您的任务(任务A, B, C, D…)分配到两个CPU核心 (Core 0 和 Core 1) 上去真正地并行运行

  • 默认情况下,ESP-IDF的系统服务(如Wi-Fi和蓝牙协议栈)运行在 Core 0 上。

  • 您自己的应用程序代码(比如 app_main 和您创建的其他任务)默认运行在 Core 1 上。

我们的头文件包含在:

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

线程创建和删除

线程是最小的调度单位,我们本身的main_app也是一个线程主线程,我只有创建了线程对线程进行任务编辑才可以让我们的线程进入到系统的一个调度。

线程创建

线程创建用的有两个api分别是

BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,	//线程回调函数
                            const char * const pcName, 	//线程名
                            const configSTACK_DEPTH_TYPE usStackDepth,	//线程栈大小
                            void * const pvParameters,	//参数
                            UBaseType_t uxPriority,	//优先级
                            TaskHandle_t * const pxCreatedTask 	//线程句柄
                      )

BaseType_t xTaskCreatePinnedToCore( TaskFunction_t pxTaskCode,	//线程回调函数
                                    const char * const pcName,	//线程名
                                    const uint32_t usStackDepth,	//线程栈大小
                                    void * const pvParameters,	//参数
                                    UBaseType_t uxPriority,	//优先级
                                    TaskHandle_t * const pxCreatedTask,	//线程句柄
                                    const BaseType_t xCoreID	//指定运行核
                                  )

和RTT不一样的是我们创建了线程就默认启动了线程不需要额外的api来开启线程

线程删除

当我们需要线程的时候我门就需要删除线程使用下面的api

void vTaskDelete( TaskHandle_t xTaskToDelete )

直接传入我们的线程句柄就可以实现删除线程了

示例代码

我们主线程就作为单独的一个执行一次的初始化线程,当然也可以当作一个任务线程在while中进行任务但是务必要有让出CPU的操作不然会发生断言错误

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h" 
#include "key_app.h"
#include "esp_log.h"

TaskHandle_t test_thred=NULL;
uint16_t time;
void test_run(void *pvParameters)
{
    while (1)
    {
        printf("test_run\n");
        time++;
        printf("time=%d\n",time);
        vTaskDelay(1000 / portTICK_PERIOD_MS);  // 延时1秒
    }
}

void app_main(void)
{
    xTaskCreate(test_run, "test_run", 2048, NULL, 10, &test_thred); // 创建按键任务
}

线程间通信

信号量互斥量事件组用于线程间同步的,为了避免不同线程对于同一资源区进行资源请求导致死锁导致系统出现问题。

下面是信号量和互斥量的对比:

特性 互斥量 (Mutex) 信号量 (Semaphore)
核心目的 互斥 (Mutual Exclusion) - 保护资源 同步/通知 (Synchronization) - 事件发信号
工作模型 钥匙 - 只有一个,用了要还 计数器 - 可以有多个,用于通知或资源池管理
所有权 - 谁上锁,谁解锁 没有 - 任何任务/ISR都可以释放信号
优先级继承 - 解决优先级反转 没有
典型场景 保护全局变量、I2C/SPI总线、打印函数 中断(ISR)通知任务、任务A通知任务B、管理N个缓冲区

值得注意的是互斥量只能在该线程上锁该线程解锁,而且互斥量最大的特点就是避免优先级反转问题(通过优先级继承协议实现),信号量就是每一个线程都可以释放信号量和获取信号量,常用来做计数器。如果具体想了解的话可以看我的RTT的笔记。

FreeRTOS 在设计上非常统一,互斥量、二值信号量、计数信号量的核心操作API(获取/释放/删除)是完全相同的! 它们都使用 SemaphoreHandle_t 这个句柄类型。

优先级反转问题

  1. 当一个高优先级线程试图通过信号量机制访问共享资源时,如果该信号量已被一低优先级线程持有,而这个低优先级线程在运行过程中可能又被其它一些中等优先级的线程抢占,因此造成高优先级线程被许多具有较低优先级的线程阻塞,实时性难以得到保证
  2. 优先级继承是通过在线程 A 尝试获取共享资源而被挂起的期间内,将线程 C 的优先级提升到线程 A 的优先级别,从而解决优先级翻转引起的问题。
  3. 具体解释一下第二点就是提高某个占有某种资源的低优先级线程的优先级,使之与所有等待该资源的线程中优先级最高的那个线程的优先级相等,然后执行,而当这个低优先级线程释放该资源时,优先级重新回到初始设定

然后我们的事件组主要是解决的一对多或多对一的情况下的线程间通信,通过不同的位数来表示不同的事件从而实现类似于多个信号量的情况

信号量和互斥量的头文件都包含在

#include "freertos/semphr.h"

事件组的头文件包含在

#include "freertos/event_groups.h"
信号量

信号量比互斥量更通用。它本质上是一个受保护的计数器。常用做:二值信号量(用来锁区) 计数器(传递信息)

  • 创建二值信号量

    SemaphoreHandle_t xSemaphoreCreateBinary( void );
    
  • 创建计数信号量

    SemaphoreHandle_t xSemaphoreCreateCounting( uxMaxCount, uxInitialCount );	//最大计数  初始计数值
    
  • 获取信号量

    BaseType_t xSemaphoreTake( xSemaphore, xBlockTime );	//信号量句柄 参数
    
  • 释放信号量

    BaseType_t xSemaphoreGive( xSemaphore );	//信号量句柄
    
  • 删除信号量

    void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );	//信号量句柄
    

下面是一个利用二值信号量锁区的例子

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h" 
#include "freertos/semphr.h"
#include "key_app.h"
#include "esp_log.h"

SemaphoreHandle_t  test_sem=NULL;
TaskHandle_t test_thred=NULL;
TaskHandle_t test2_thred=NULL;
uint16_t time;
void test_run(void *pvParameters)
{
    while (1)
    {
        xSemaphoreTake(test_sem,portMAX_DELAY); //获取信号量
        printf("test_run\n");
        time--;
        printf("time=%d\n",time);
        xSemaphoreGive(test_sem);   //释放信号量
        vTaskDelay(1000 / portTICK_PERIOD_MS);  // 延时1秒
    }
}

void test2_run(void *pvParameters)
{
    while (1)
    {
        xSemaphoreTake(test_sem,portMAX_DELAY); //获取信号量
        printf("test2_run\n");
        time+=3;
        printf("time=%d\n",time);
        xSemaphoreGive(test_sem);   //释放信号量
        vTaskDelay(1000 / portTICK_PERIOD_MS);  // 延时1秒
    }
}

void app_main(void)
{
    test_sem=xSemaphoreCreateBinary();
    xSemaphoreGive(test_sem);   //释放信号量
    if(test_sem==NULL)
    {
       printf("test_sem is null\n");
       return;
    }
    xTaskCreate(test_run, "test_run", 2048, NULL, 10, &test_thred); // 创建按键任务
    xTaskCreate(test2_run, "test2_run", 2048, NULL, 11, &test2_thred); // 创建按键任务
}

互斥量

互斥量说白了就类似于二值信号量但是多了一个避免优先级反转的问题,这也是最重要的点。

  • 互斥量创建

    SemaphoreHandle_t xSemaphoreCreateMutex( void );
    
  • 获取互斥量(上锁)

    BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait );	//互斥量句柄 等待时间
    
  • 释放互斥量(开锁)

    BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );	//互斥量句柄
    
  • 删除互斥量

    void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );	//信号量句柄
    

由于使用方法和信号量类似故此处不做演示,只需要注意==互斥量由谁释放就需要由谁开启==

事件组

事件组专门用于**“一对多”或“多对多”的复杂同步场景。说白了就相当于是这个事件集合的每一位都可以作为一个信号量**,然后主要是线程可以主动去等待多个事件比如或事件和事件。

  • 创建事件组

    EventGroupHandle_t xEventGroupCreate( void );
    
  • 设置事件标志位 (从任务调用)

    EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet ); //事件组句柄, 要设置为'1'的一个或多个位 (例如 BIT_0 | BIT_1)
    
  • 设置事件标志位 (从中断ISR调用)

    aseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet, BaseType_t *pxHigherPriorityTaskWoken ); //事件组句柄, 要设置的位, 是否需要任务切换
    
  • **等待事件标志位 **

    EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToWaitFor, const BaseType_t xClearOnExit, const BaseType_t xWaitForAllBits, TickType_t xTicksToWait ); //事件组句柄, 要等待的位, 退出时是否清除, 是否等待所有位(AND/OR), 最大等待时间
    

    第四个参数:

    • pdTRUE = 等待所有位 (AND 逻辑)

    • pdFALSE = 等待任意位 (OR 逻辑)

    第五个参数:

    • pdTRUE = 清除
    • pdFALSE =不清除
  • 清除事件标志位

    EventBits_t xEventGroupClearBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToClear ); //事件组句柄, 要清除为'0'的一个或多个位
    
  • 删除事件组

    void vEventGroupDelete( EventGroupHandle_t xEventGroup ); //事件组句柄
    

下面是一个利用事件组进行唤醒的例子

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h" 
#include "freertos/semphr.h"
#include "freertos/event_groups.h"
#include "key_app.h"
#include "esp_log.h"

TaskHandle_t test_thred=NULL;
TaskHandle_t test2_thred=NULL;
TaskHandle_t test3_thred=NULL;
EventGroupHandle_t test_event_group = NULL;

// 定义清晰的位宏
#define BIT_0 (1 << 0) // 代表 1
#define BIT_1 (1 << 1) // 代表 2


void test_run(void *pvParameters)
{
     while (1)
     {
         printf("test_run\r\n");
         xEventGroupSetBits(test_event_group, BIT_0); // 设置 bit 0
         vTaskDelay(1000 / portTICK_PERIOD_MS);  // 延时1秒
     }
}


void test2_run(void *pvParameters)
{
     while (1)
     {
         // 等待 bit 0
         xEventGroupWaitBits(test_event_group, BIT_0, pdFALSE, pdTRUE, portMAX_DELAY);
         printf("test2_run\r\n");
         xEventGroupSetBits(test_event_group, BIT_1); // 设置 bit 1
         
         vTaskDelay(1000 / portTICK_PERIOD_MS);  // 延时1秒
     }
}


void test3_run(void *pvParameters)
{
     while (1)
     {
         xEventGroupWaitBits(test_event_group, BIT_1|BIT0, pdTRUE, pdTRUE, portMAX_DELAY);
         printf("test3_run\r\n");
         vTaskDelay(1000 / portTICK_PERIOD_MS);  // 延时1秒
     }
}

void app_main(void)
{
     test_event_group = xEventGroupCreate();
     if(test_event_group == NULL)
     {
         printf("test_event_group create fail\n");
     }
     
     xTaskCreate(test_run, "test_run", 2048, NULL, 12, &test_thred); 
     xTaskCreate(test2_run, "test2_run", 2048, NULL, 11, &test2_thred); 
     xTaskCreate(test3_run, "test3_run", 2048, NULL, 10, &test3_thred);
}

队列

队列 (Queue) 是 FreeRTOS 中最常用、也是最核心的线程(任务)间通信方式。相当于就是我们在裸机时利用全局变量传递数据,可能会导致出现死锁的状态,我们直接使用队列来进行数据的传递就可以避免这一点。

队列相关的头文件包含在下面的头文件:

#include "freertos/queue.h"
  • 队列创建

    QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize ); //队列能容纳的最大项目数 (长度), 队列中 *每个项目* 的大小 (字节为单位)
    
  • 向队列发送数据 (发送到队尾)

    BaseType_t xQueueSend( QueueHandle_t xQueue, const void * pvItemToQueue, TickType_t xTicksToWait ); //队列句柄, 指向要发送数据的 *指针*, 如果队列满了最大等待时间
    
  • 从队列接收数据 (从队头读取)

    BaseType_t xQueueReceive( QueueHandle_t xQueue, void * pvBuffer, TickType_t xTicksToWait ); //队列句柄, 用于 *存放* 接收到数据的缓冲区指针, 如果队列空了最大等待时间
    
  • 删除队列

    void vQueueDelete( QueueHandle_t xQueue ); //队列句柄
    
  • 插队进入队列头(紧急数据)

    BaseType_t xQueueSendToFront( QueueHandle_t xQueue, const void * pvItemToQueue, TickType_t xTicksToWait ); //(参数同 xQueueSend, 但数据会插到队头)
    

队列的传递很灵活的并不是单纯只是传递一个数值,它可以传递数据指针,就比如我们可以使用定义结构体数据包,然后将我们的结构体数据包地址传递过去

下面为简单示例:

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h" 
#include "freertos/semphr.h"
#include "freertos/event_groups.h"
#include "freertos/queue.h"
#include "key_app.h"
#include "esp_log.h"

TaskHandle_t test_thred=NULL;
TaskHandle_t test2_thred=NULL;
QueueHandle_t test_queue=NULL;

typedef struct {
    char*string;
}mydata;

mydata mydata_temp;
mydata mydata_temp_receive;

char*mydata_string1 = "hello world!";
char*mydata_string2 = "hello ESP32!";
uint8_t count=0; 

void test_run(void *pvParameters)
{
     while (1)
     {
        if(++count>1)
        {
            count=0;
        }
        if(count==0)
        {
            mydata_temp.string=mydata_string1;
        }else
        {
            mydata_temp.string=mydata_string2;
        }
        xQueueSend(test_queue, &mydata_temp, portMAX_DELAY);
        printf("send:%s\r\n",mydata_temp.string);  
         printf("test_run...\r\n");
         vTaskDelay(1000 / portTICK_PERIOD_MS);  // 延时1秒
     }
}


void test2_run(void *pvParameters)
{
     while (1)
     {
         xQueueReceive(test_queue, &mydata_temp, portMAX_DELAY);
         printf("receive:%s\r\n",mydata_temp.string);
         printf("test2_run...\r\n");
         vTaskDelay(1000 / portTICK_PERIOD_MS);  // 延时1秒
     }
}


void app_main(void)
{
     test_queue = xQueueCreate(10, sizeof(mydata)); //创建一个队列,最多存放10个mydata结构体
     xTaskCreate(test_run, "test_run", 2048, NULL, 12, &test_thred); 
     xTaskCreate(test2_run, "test2_run", 2048, NULL, 10, &test2_thred); 
}
一点建议

关于 ESP32的操作系统框架大致只需要了解这一点重点即可,后续在使用过程中如果碰上了需要的我们再去学习即可,基本上掌握线程的创建和使用以及线程间同步线程间同步的队列就已经足够支持我们完成大多数任务。

如果需要详细了解操作系统可以参考我主页的RT-Thread操作的系统,它们的本质是不变的,只是接口的名字有不同。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐