没志青年
发布于 2025-06-16 / 68 阅读
0

FreeRTOS 任务间通信

  • 互斥:同一时刻,共享资源只能被一个任务所使用。

  • 同步:控制不同任务访问共享资源的顺序。

其实,就可以把通知理解为同步,它们的作用都是控制任务的执行顺序。

任务之间的通信,称为 IPC(Inner-Process Communication)

FreeRTOS中提供了队列、信号量、事件组、任务通知用于任务间的通信、互斥与同步,还是非常灵活的。

特性

任务通知

二进制信号量

计数信号量

互斥量

事件组

消息队列

速度

最快

很快

较快

稍快

内存占用

0(复用TCB字段)

大数据

数据类型

数值+位操作

数值

位操作

支持复杂数据

注意事项

只能通知指定的一个任务,无法通知多个任务。

注意不要出现多个任务等待同一个二进制信号量的情况,否则低优先级的任务可能一直无法执行。我也不太清楚

使用场景

任务之间一对一通信

(1)在中断中通知一个任务

(2)在任务中通知另一个任务

多个相同资源的并发访问控制

对共享资源(单资源)互斥保护

(1)任务通知

(2)任务同步

(1)用来传递任意数据类型的数据

(2)也能用来通知任务。但是何必呢,又慢又没有前面的方便。

优先级反转

不会,一对一直接操作TCB

??

会,但互斥量会自己解决

不会,本质是位操作,无资源占用

不会

很乱很乱,无法发理解。

数据传递

队列

创建队列

// 动态创建队列
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength,  // 队列长度,最多能存放多少个数据
                            UBaseType_t uxItemSize);    // 一个元素占用的空间(字节数)

// 静态创建队列,静态队列一旦创建无法删除
QueueHandle_t xQueueCreateStatic( UBaseType_t uxQueueLength,       // 队列长度,最多能存放多少个数据
                                  UBaseType_t uxItemSize,          // 一个元素占用的空间(字节数)
                                  uint8_t *pucQueueStorageBuffer,  // 队列存储区域的指针
                                  StaticQueue_t *pxQueueBuffer );   // 队列控制块(像任务控制块那样用于管理队列)

动态创建:

QueueHandle_t xQueue = xQueueCreate(10, sizeof(int));
if (xQueue == NULL) {
    // 队列创建失败
}

静态创建:

static uint8_t ucQueueStorage[5 * sizeof(int)];
static StaticQueue_t xStaticQueue;
QueueHandle_t xQueue;

xQueue = xQueueCreateStatic(5, sizeof(int), ucQueueStorage, &xStaticQueue);
if (xQueue == NULL) {
    // 队列创建失败
}

删除队列

仅能删除动态方式创建的队列。

void vQueueDelete(QueueHandle_t xQueue);

清空队列

不会释放队列的内存,只是清除队列中的所有元素。

BaseType_t xQueueReset(QueueHandle_t xQueue);

写队列

(1)写入到队列尾部

BaseType_t xQueueSend( QueueHandle_t xQueue,         // 队列句柄
                       const void *pvItemToQueue,    // 数据指针
                       TickType_t xTicksToWait );     // 超时时间:0 不等待;portMAX_DELAY 无限等待
                         

BaseType_t xQueueSendToBack( QueueHandle_t xQueue,
                             const void *pvItemToQueue,
                             TickType_t xTicksToWait );

BaseType_t xQueueOverwrite(QueueHandle_t xQueue, const void *pvItemToQueue);

xQueueSend 和 xQueueSendToBack 这俩是一个函数。

xQueueOverwrite 向队列尾部写入数据,如果队列满了,就覆盖最旧的数据(队列头部的数据)。

xQueueOverwrite 适用场景:队列中只需要保留最新的数据,过时的数据可以被覆盖。

(2)写入到队列头部,优先处理紧急数据。

BaseType_t xQueueSendToFront( QueueHandle_t xQueue,
                              const void *pvItemToQueue,
                              TickType_t xTicksToWait );

读队列

BaseType_t xQueueReceive( QueueHandle_t xQueue,   // 队列句柄
                          void * const pvBuffer,   // 接收数据缓冲区
                          TickType_t xTicksToWait );  // 超时时间:0 不等待;portMAX_DELAY 无限等待

中断安全版本:

BaseType_t xQueueReceiveFromISR(
                                 QueueHandle_t xQueue,
                                 void *pvBuffer,
                                 BaseType_t *pxTaskWoken   // 
                                 )

查询队列

队列中还有多少个元素未处理,即队列中元素的个数:

UBaseType_t uxQueueMessagesWaiting(QueueHandle_t xQueue);

返回队列的剩余空间:

UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );
// 0:满了
// >0:有剩余空间

偷看队列

从队列中获取数据,但是不移除它。

BaseType_t xQueuePeek(QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait);

共享资源互斥

互斥量

互斥量专门用于解决共享资源的互斥同时只能由一个任务获取,并且只能由获取它的任务释放

互斥量不能在中断程序中释放,因为中断中没有任务上下文,无法参与优先级继承逻辑。

互斥量默认支持优先级继承,不用担心优先级反转的问题。

信号量也能实现互斥,但可能发生优先级反转。

(1)互斥量和临界区函数的区别:

临界区

互斥量

自旋锁(ESP32)

作用

共享资源单核互斥

共享资源单核互斥

共享资源多核互斥

保护原理

通过禁止上下文切换(暂停调度器)和禁止中断打断,来保证只有当前任务执行,独占共享资源。

获得互斥量的任务执行,没获得互斥量的任务阻塞。

两个核心中的任务都需要某个共享资源,若核0的任务获得了共享资源,和1的任务只能一直等待,直到核0的任务释放。

这个是正在关中断中实现的,比原先的多了个自旋锁。

性能开销

低(无上下文切换)

中(可能任务切换)

高(CPU空转)

使用场景

短小代码,不可阻塞

单核长临界区,可阻塞

多核短临界区

注意

临界区过长会破坏系统实时性。

临界区过长会破坏系统实时性。

创建互斥量

SemaphoreHandle_t xSemaphoreCreateMutex( void );

递归互斥量

中断安全函数:

优先级反转

假设有三个任务:

  • Task_H(高优先级)

  • Task_M(中优先级)

  • Task_L(低优先级)

优先级反转过程:

  1. Task_L 获取了某个共享资源(如互斥量)。

  2. Task_H 就绪,抢占 Task_L,但尝试获取同一资源时被阻塞(因为资源被 Task_L 占用)。

  3. Task_M 就绪(不需要该资源),抢占 Task_L 并执行。

  4. Task_L 因被 Task_M 抢占,无法继续执行并释放资源,导致 Task_H 长期阻塞。

在这个过程中,Task_M 优先级比 Task_H 低,却比 Task_H 先执行,违反了优先级调度原则,称之为优先级反转。

优先级反转,增加了高优先级任务的响应时间,导致系统不能及时处理高优先级任务,系统的实时性变差。

优先级反转解决办法:

(一)优先级继承

当一个低优先级任务持有资源并被中等优先级任务抢占时,其优先级会被暂时提升到高优先级任务的优先级,直到它释放资源为止。

只有互斥量支持优先级继承,且创建时默认开启。

SemaphoreHandle_t xMutex;  // 互斥量句柄

void vTask1(void *pvParameters)
{
    while (1)
    {
        // 获取互斥量
        if (xSemaphoreTake(xMutex, portMAX_DELAY))  
        {
            // 执行任务操作
            printf("Task 1 is accessing shared resource\n");
            vTaskDelay(pdMS_TO_TICKS(1000));
            xSemaphoreGive(xMutex);  // 释放互斥量
        }
    }
}

void vTask2(void *pvParameters)
{
    while (1)
    {
        if (xSemaphoreTake(xMutex, portMAX_DELAY))  
        {
            // 执行任务操作
            printf("Task 2 is accessing shared resource\n");
            vTaskDelay(pdMS_TO_TICKS(1000));
            xSemaphoreGive(xMutex);  // 释放互斥量
        }
    }
}

int main(void)
{
    // 创建互斥量并启用优先级继承
    xMutex = xSemaphoreCreateMutex();

    if (xMutex != NULL)
    {
        // 创建任务
        xTaskCreate(vTask1, "Task 1", configMINIMAL_STACK_SIZE, NULL, 1, NULL);
        xTaskCreate(vTask2, "Task 2", configMINIMAL_STACK_SIZE, NULL, 2, NULL);

        // 启动调度器
        vTaskStartScheduler();
    }

    // 如果调度器启动失败,则进入死循环
    for (;;);
    return 0;
}

(二)优先级天花板

任务在获取资源时,自动提升自身优先级至预设的“天花板优先级”(所有可能访问该资源的任务中的最高优先级)。

xSemaphoreCreateMutexStatic()

总结:

  1. 优先级反转提醒我们使用锁的代码段应尽量短

  2. 如果锁保护的代码段很短,直接使用原子锁忙等也是一个选择

任务同步

信号量

信号量分为计数信号量和二进制信号量,计数信号量值任意,二进制信号量只有0和1两个值。

信号量可用于互斥和同步。

创建信号量

(1)动态创建信号量

// 动态创建二进制信号量,创建后就可take
SemaphoreHandle_t xSemaphoreCreateBinary(void);	

// 动态创建计数信号量
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount,       // 资源总数
                                           UBaseType_t uxInitialCount);  // 初始计数值

中断安全函数:

(2)静态创建信号量

// 静态创建二进制信号量,创建后必须先give才能take
SemaphoreHandle_t xSemaphoreCreateBinaryStatic(StaticSemaphore_t *pxSemaphoreBuffer);

// 静态创建计数信号量
SemaphoreHandle_t xSemaphoreCreateCountingStatic(
    UBaseType_t uxMaxCount,            // 资源总数
    UBaseType_t uxInitialCount,        // 初始计数值
    StaticSemaphore_t *pxSemaphoreBuffer  // 静态信号量的控制块
);

删除信号量

void vSemaphoreDelete(SemaphoreHandle_t xSemaphore);

中断安全函数:

P 操作

P操作即获取信号量,表示使用共享资源。

BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore,
                          TickType_t xTicksToWait);

中断安全函数:

BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore,
                                 BaseType_t *pxHigherPriorityTaskWoken);

V 操作

V操作即释放信号量,表示释放共享资源。

BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);

中断安全函数:

BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken );

信号量应用于通知示例:

SemaphoreHandle_t xSemaphore = xSemaphoreCreateBinary();

void vTask(void *pvParameters)
{
    for(;;)
    {
        if (xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE)
        {
            // 被中断通知了,开始工作
        }
    }
}

void ISR_Handler(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);   // 释放信号量
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

事件组

事件组就是一组二进制位,一位表示一个事件状态。

事件组用于任务间同步:

  • 多个任务可等待同一个事件

  • 一个任务可等待多个事件

#include "event_groups.h"

创建事件组

// 动态创建事件组
EventGroupHandle_t xEventGroupCreate( void );

// 静态创建事件组
EventGroupHandle_t xEventGroupCreateStatic( StaticEventGroup_t * pxEventGroupBuffer );

示例:

// 动态创建
EventGroupHandle_t xEventGroup = xEventGroupCreate();

// 静态创建

删除事件组

void vEventGroupDelete( EventGroupHandle_t xEventGroup );

设置事件位

定义事件位:

#define BIT_TASK_A   (1 << 0)
#define BIT_TASK_B   (1 << 1)
#define BIT_TASK_C   (1 << 2)
#define ALL_READY_BITS  (BIT_TASK_A | BIT_TASK_B | BIT_TASK_C)

EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,   // 哪个事件组
 								const EventBits_t uxBitsToSet );  // 设置哪些位

中断安全函数:

BaseType_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,    // pdTRUE:所有条件都得成立;pdFALSE:任何一个条件成立就行
                                 TickType_t xTicksToWait);            // 超时时间,portMAX_DELAY 永久,pdMS_TO_TICKS(n) 有超时时间



使用示例:
void ledc_task(void* param)
{
    EventBits_t ev;
    while(1)
    {
        ev = xEventGroupWaitBits(s_ledc_ev,LEDC_ON_EV|LEDC_OFF_EV,pdTRUE,pdFALSE,pdMS_TO_TICKS(5000));
        if(ev)
        {
            if(ev & LEDC_OFF_EV)
            {
               
            }
            else if(ev & LEDC_ON_EV)
            {
               
            }
        }
    }
}

中断安全函数:

清除事件位

手动清除事件位

xEventGroupClearBits(xEventGroup, BIT_0 | BIT_1);

事件同步

该函数结合了上面的设置函数和等待函数,用于简化代码编写。

EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup,     // 事件组句柄
                             const EventBits_t uxBitsToSet,      // 要设置的事件位
                             const EventBits_t uxBitsToWaitFor,  // 要等待的事件位
                             TickType_t xTicksToWait );          // 等待时间

xEventGroupSync 可实现复杂的同步,虽然用其它的方法也能实现,但是没这个简单。


(1)多任务协同启动

所有任务就绪后继续执行。

void vTaskA(void *pvParams) {
    xEventGroupSync(xEventGroup, TASK_A_READY_BIT,   // 任务各自设置
                    (TASK_A_READY_BIT | TASK_B_READY_BIT | TASK_C_READY_BIT),
                    portMAX_DELAY);

}

(2)分阶段启动

任务A采集数据后设置 DATA_COLLECTED_BIT,任务B处理数据后设置 DATA_PROCESSED_BIT,双方同步推进。

// 阶段1:等待数据采集完成
xEventGroupSync(xEventGroup, DATA_COLLECTED_BIT, 
                (DATA_COLLECTED_BIT | DATA_PROCESSED_BIT),
                pdMS_TO_TICKS(100));
// 阶段2:继续后续操作

中断安全函数:

任务通知

  • 指定任务通知

  • 中断中可以发送通知,但不能接收通知。

发送任务通知

// 发送通知,可传输32位整数数据
BaseType_t xTaskNotify(TaskHandle_t xTask, uint32_t ulValue, eNotifyAction eAction);


// 简化版发送通知(类似二进制信号量)
BaseType_t xTaskNotifyGive(TaskHandle_t xTask);

eAction 对值的处理:

行为

适用场景

eNoAction

仅通知不传值

轻量级事件触发

eSetBits

按位或(ulValue 为位掩码)

类似事件组(多事件标记)

eIncrement

通知值 +1,ulValue被忽略

计数信号量

eSetValueWithOverwrite

直接覆盖通知值

传递单一数值

eSetValueWithoutOverwrite

有旧值,不覆盖

避免数据丢失

中断安全函数:

BaseType_t xTaskNotifyFromISR( TaskHandle_t xTaskToNotify,
                                 uint32_t ulValue,
                                 eNotifyAction eAction,
                                 BaseType_t *pxHigherPriorityTaskWoken );

接收任务通知

// 等待通知并获取数值
BaseType_t xTaskNotifyWait(
        uint32_t ulBitsToClearOnEntry,   // 阻塞前清除哪些位
        uint32_t ulBitsToWaitFor,         // 退出后清除哪些位
        uint32_t *pulNotificationValue, // 通知值
        TickType_t xTicksToWait);    // 等待时间


// 仅等待通知,类似于二值信号量
BaseType_t ulTaskNotifyTake()

uint32_t notifyValue;

xTaskNotifyWait(
    0x00,          // 进入前不清,保留事件
    0xFFFFFFFF,    // 退出后清所有
    &notifyValue,
    portMAX_DELAY
);

清除的是内核中tcb中的任务通知的值,原来的值已经拷贝到 notifyValue 中了,所以后面的判断依然有效。

清除任务的通知状态

清除某个任务的通知状态,但不清除通知值。一般用不到。

BaseType_t xTaskNotifyStateClear(TaskHandle_t xTask);
// 传入NULL表示清除当前任务

最大的作用,就是确保任务只执行一次。

void vTaskProcess(void *pvParams) {
    uint32_t ulNotifiedValue;
    while (1) {
        xTaskNotifyWait(0x00, 0xFFFFFFFF, &ulNotifiedValue, portMAX_DELAY);
        
        // 处理通知值
        if (ulNotifiedValue & 0x01) {
            
        }
        if (ulNotifiedValue & 0x02) {
            
        }

        
       // xTaskNotifyStateClear(NULL); 
    }
}


void vISR_Handler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xTaskNotifyFromISR(xTaskProcessHandle, 0x03, eSetBits, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

使用:仅通知

使用:事件组





#define NOTIFY_UART_RX   (1UL << 0)
#define NOTIFY_TIMER     (1UL << 1)
#define NOTIFY_KEY       (1UL << 2)


xTaskNotify(AppTaskHandle,
            NOTIFY_UART_RX,
            eSetBits);




相当于 taskNotifyValue |= ulBitsToSet; 把指定位置1

void AppTask(void *arg)
{
    uint32_t notify;

    for (;;) {
        xTaskNotifyWait(
            0,
            0xFFFFFFFF,
            &notify,
            portMAX_DELAY
        );

        if (notify & NOTIFY_UART_RX) {
            Uart_Process();
        }

        if (notify & NOTIFY_TIMER) {
            Timer_Process();
        }

        if (notify & NOTIFY_KEY) {
            Key_Process();
        }
    }
}