事件标志组

        事件标志组简介

事件标志组是FreeRTOS中用一组二进制位来表示多个事件状态的同步机制,当在嵌入式系统中运行多个任务时,这些任务可能需要互相通信,协调其操作,而事件标志组可以在任务之间传递信息和同步操作,比信号量更加了灵活,信号量只适合表达一个资源或者一个通知,事件标志组更适合描述多个条件是否满足,事件组中的所有事件位都 存储在 EventBits_t 类型的单个无符号整数变量中,事件位 0 存储在位 0 中,事件位 1 存储在位1 中,依此类推

总体来说事件标志组更侧重于任务间的事件通知和同步,而信号量更侧重于资源的控制和同步

        事件标志组API

函数 描述

xEventGroupCreate()

使用动态方式创建事件标志组

xEventGroupCreateStatic()

使用静态方式创建事件标志组

xEventGroupClearBits()

清零事件标志位

xEventGroupClearBitsFromISR()

在中断中清零事件标志位

xEventGroupSetBits()

设置事件标志位

xEventGroupSetBitsFromISR()

在中断中设置事件标志位

xEventGroupWaitBits()

等待事件标志位

xEventGroupSync()

设置事件标志位,并等待事件标志位

        事件标志组配置说明

事件组是由EventGroupHandle_t 类型的变量引用,在事件组中实现的位数(或标志数)取决于配置文件中的配置项

configUSE_16_BIT_TICKS 和configTICK_TYPE_WIDTH_IN_BITS 来控制 TickType_t 的类型会有不同的效果

  • 如果 configUSE_16_BIT_TICKS 设置为 1,则事件组内实现的位数(或标志数)为 8

  • 如果 configUSE_16_BIT_TICKS 设置为 0,则为 24位,注意在FreeRTOS中事件组理论上支持32个事件位,但是官方规定bit24~bit31不建议使用,FreeRTOS内部会在高8位存任务通知/内部状态

  • 还可以对configTICK_TYPE_WIDTH_IN_BITS来进行配置,但是是FreeRTOS V7.x版本后引入的这里不作讨论

        代码演示

#include "FreeRTOS_demo.h"
#include "FreeRTOS.h"//这两个头文件是必须要引入的
#include "task.h"
#include "LED.h"
#include "Key.h"
#include "stdio.h"
#include "event_groups.h"
//启动任务的配置
#define START_TASK_STACK 128
#define START_TASK_PRIORITY 1
TaskHandle_t start_task_handle;
void start_task(void * pvParameters);//启动任务

//任务1的配置
#define TASK1_STACK 128
#define TASK1_PRIORITY 2
TaskHandle_t task1_handle;
void task1(void * pvParameters);//启动任务

//任务2的配置
#define TASK2_STACK 128
#define TASK2_PRIORITY 3
TaskHandle_t task2_handle;
void task2(void * pvParameters);//启动任务


EventGroupHandle_t event_group_handle;
void freertos_start(void)//启动FreeRTOS
{
    event_group_handle = xEventGroupCreate();
    if(event_group_handle != NULL)
    {
        printf("创建成功");
    }
    //1.创建一个启动任务
    xTaskCreate((TaskFunction_t) start_task,                //任务函数地址,前面是类型强转
                (char*) "start_task",                       //取任务名
                (configSTACK_DEPTH_TYPE) START_TASK_STACK,  //任务栈大小,最小是128字节
                (void *) NULL,                              //不需要形参就给一个NULL
                (UBaseType_t) START_TASK_PRIORITY,          //任务优先级,这里的优先级默认为5,有5个优先级
                (TaskHandle_t *) &start_task_handle);        //任务句柄
                //任务的相关信息会保存到一个tcb地址,然后这个地址会赋值给这个任务句柄,所以这只用创建一个任务句柄即可
    //2.启动调度器
    vTaskStartScheduler();//这是必须要启动的,会自动创建空闲函数
}

void start_task(void * pvParameters)//启动任务,用来创建其他task
{
    
    //进入临界区,临界区里面的代码不会被打断
    taskENTER_CRITICAL();
    
    //创建3个任务
    xTaskCreate((TaskFunction_t) task1,                
                (char*) "task1",                       
                (configSTACK_DEPTH_TYPE) TASK1_STACK,  
                (void *) NULL,                            
                (UBaseType_t) TASK1_PRIORITY,          
                (TaskHandle_t *) &task1_handle);
    xTaskCreate((TaskFunction_t) task2,                
                (char*) "task2",                       
                (configSTACK_DEPTH_TYPE) TASK2_STACK,  
                (void *) NULL,                            
                (UBaseType_t) TASK2_PRIORITY,          
                (TaskHandle_t *) &task2_handle);
    //退出临界区
    taskEXIT_CRITICAL();

    //启动任务只需要执行一次,用完就删除自己
    vTaskDelete(NULL);
}
#define EVENTBIT_0 (1<<0)
#define EVENTBIT_1 (1<<1)
void task1(void * pvParameters)//任务1:读取按键按下键值,根据不同键值将事件标志组相应事件位置一,模拟事件发生
{
   uint8_t key = 0;
   while(1)
   {
        if(key == KEY1_PRESS)
        {
            /*key1按下,对bit0置1*/
            xEventGroupSetBits(event_group_handle, EVENTBIT_0);
            printf("按下key1对bit0置1");
        }
        else if(key == KEY2_PRESS)
        {
            /*key2按下,对bit1置1*/
            xEventGroupSetBits(event_group_handle, EVENTBIT_1);
            printf("按下key2对bit0置1");
        }
   }
   vTaskDelay(500);
}
void task2(void * pvParameters)//任务2:同时等待事件标志组中的多个事件位,当这些事件位都置 1 的话就执行相应的处理
{
   while(1)
   {
      uint32_t event_bits = 0;
      event_bits = xEventGroupWaitBits(event_group_handle,         //事件标志组对应的句柄
                            EVENTBIT_0 | EVENTBIT_1,    //等待的比特位
                            pdTRUE,                     //是否清除标志位,pdTRUE表示清除
                            pdTRUE,                     //是否等待所有位都置1,pdTRUE表示等待所有位都置1
                            portMAX_DELAY);             //等待时间
        printf("task2接收到的事件标志组=%#x.....\r\n",event_bits);
   }
}

任务通知

        任务通知简介

任务通知是FreeRTOS中一种用于任务间通信的机制,它允许一个任务向其他任务发送简单的通知或信号,以实现任务间的同步和协作,任务通知通常用于替代二值信号量或事件标志组,提供了更轻量级的任务间通信方式

向任务发送"任务通知"会将目标任务通知设为"挂起"状态,正如任务可以阻塞中间对象(如等待信号量可用的信号量),任务也可以阻塞任务通知,以等待通知状态变为"挂起"(即比如在一个接收方的TCB中调用一个获取通知值的函数,指定索引来表明读取哪一个通知值,如果还没有接收到通知,则就会设置为挂起状态,即阻塞等待通知到达)

向任务发送“任务通知”也可以更新目标通知的值(可选),可使用下列任一方法:

  • 覆盖原值(无论接收任务是否已读取被覆盖的值)
  • 覆盖原值(仅当接收任务已读取被覆盖的值时)
  • 在值中设置一个或多个位
  • 对值进行增量(添加 1)

RTOS 任务通知功能默认为启用状态,在配置文件中显式定configUSE_TASK_NOTIFICATIONS 设为0可以禁用

        任务通知、信号量、队列、事件组的关系

大多数任务间通信方式要通过中间对象,如队列、信号量、事件组,这些都是独立的内核对象,发送任务写入通信对象,接收任务从通信对象读取,当使用直接任务通知时,发送任务直接向接收任务发送通知,不需要中间对象,因为任务通知相当于是属于任务本身的,通知信息直接挂在任务控制块,每个任务内部自带一个通知值,在新版本里自带一个通知数组,任务通知只有在某些情况下会堵塞,发送数据的时候不会等待,要么覆盖要么丢失,但是TCB在接收方读取值时可以选择堵塞或不堵塞,即读取的时候没有值可以选择等待

每个RTOS任务都有一个任务通知组(即每个任务的TCB里存在一个任务通知的数组,这个数组可以配置里面有多少个空位,每个空位大小为多少,然后其他任务向该任务的通知就放在对应索引空位),每条通知均独立运行,都有"挂起"或"非挂起的通知状态",以及一个32通知量,常量 configTASK_NOTIFICATION_ARRAY_ENTRIES 可设置任务通知组中的索引数量,在 FreeRTOS V10.4.0 版本前,任务只有单条任务通知(即只能一对一),没有任务通知组

  • 在这里的任务通知组,我的第一反应也是觉得于事件标志组很类似,但是任务通知的通知值内嵌在任务控制块中,更适合一对一、轻量级的定向通知,而事件标志组是独立的内核对象,更适合管理多个任务共享

和信号量相比:

  • 任务通知经常可以替代信号量,对于一对一的通知比如中断通知某个任务,任务A唤醒任务B等,任务通知更省RAM更快而且不需要创建信号量对象,信号量更偏向于多个任务共同争抢、共享同步对象

和对列相比:

  • 对列主要是应用于传数据,比如是发送一个结构体、发送一串字符串、传递串口数据等,而任务通知主要是通知事件来了,告诉你来了几次,告诉你哪个bit被置位了

和事件标志组相比:

  • 任务通知虽然也可以按位使用但是它是绑定在某个任务上的私有通知值,而事件标志组是独立的共享事件对象,任务通知适合一对一、定向、轻量,事件组适合多任务围绕同一组事件协作

        任务通知API

函数 描述
xTaskNotify() 发送通知,带有通知值
xTaskNotifyAndQuery() 发送通知,带有通知值并且保留接收任务的原通知值(即会返回原来的通知值)
xTaskNotifyGive() 发送通知,不带通知值(没有具体的数据,只是表示有这个通知)
xTaskNotifyFromISR()
xTaskNotifyAndQueryFromISR() 在中断中发送任务通知
xTaskNotifyGiveFromISR()
ulTaskNotifyTake() 获取任务通知,可选退出函数时对通知值清零或减一(对整个值处理)
xTaskNotifyWait() 获取任务通知,可获取通知值和清除通知值的指定位(对每个位处理)
  • 注意:发送通知有相关的ISR函数,但是接收通知没有ISR函数,不能在ISR中接收任务通知,因为需要接收方有TCB任务块,在中断中没有所以没有接收通知的ISR函数

        代码演示

  • 这里演示的是普通的并行任务之间的任务间通讯,实际中更加常用的是在中断中唤醒任务,可以实现发生特定事件的时候才执行相关的任务,特别常见于DMA完成中断,串口接收中断,定时器中断,ADC转换完成中断
#include "FreeRTOS_demo.h"
#include "FreeRTOS.h"//这两个头文件是必须要引入的
#include "task.h"
#include "LED.h"
#include "Key.h"
#include "stdio.h"
//启动任务的配置
#define START_TASK_STACK 128
#define START_TASK_PRIORITY 1
TaskHandle_t start_task_handle;
void start_task(void * pvParameters);//启动任务

//任务1的配置
#define TASK1_STACK 128
#define TASK1_PRIORITY 2
TaskHandle_t task1_handle;
void task1(void * pvParameters);//启动任务

//任务2的配置
#define TASK2_STACK 128
#define TASK2_PRIORITY 3
TaskHandle_t task2_handle;
void task2(void * pvParameters);//启动任务


void freertos_start(void)//启动FreeRTOS
{
    //1.创建一个启动任务
    xTaskCreate((TaskFunction_t) start_task,                //任务函数地址,前面是类型强转
                (char*) "start_task",                       //取任务名
                (configSTACK_DEPTH_TYPE) START_TASK_STACK,  //任务栈大小,最小是128字节
                (void *) NULL,                              //不需要形参就给一个NULL
                (UBaseType_t) START_TASK_PRIORITY,          //任务优先级,这里的优先级默认为5,有5个优先级
                (TaskHandle_t *) &start_task_handle);        //任务句柄
                //任务的相关信息会保存到一个tcb地址,然后这个地址会赋值给这个任务句柄,所以这只用创建一个任务句柄即可
    //2.启动调度器
    vTaskStartScheduler();//这是必须要启动的,会自动创建空闲函数
}
void start_task(void * pvParameters)//启动任务,用来创建其他task
{
    //进入临界区,临界区里面的代码不会被打断
    taskENTER_CRITICAL();
    
    //创建3个任务
    xTaskCreate((TaskFunction_t) task1,                
                (char*) "task1",                       
                (configSTACK_DEPTH_TYPE) TASK1_STACK,  
                (void *) NULL,                            
                (UBaseType_t) TASK1_PRIORITY,          
                (TaskHandle_t *) &task1_handle);
    xTaskCreate((TaskFunction_t) task2,                
                (char*) "task2",                       
                (configSTACK_DEPTH_TYPE) TASK2_STACK,  
                (void *) NULL,                            
                (UBaseType_t) TASK2_PRIORITY,          
                (TaskHandle_t *) &task2_handle);  
    //退出临界区
    taskEXIT_CRITICAL();
    //启动任务只需要执行一次,用完就删除自己
    vTaskDelete(NULL);  
}

void task1(void * pvParameters)//任务1:用于按键扫描,当检测到按键KEY1被按下时,将发送任务通知
{
   uint8_t key = 0;
   BaseType_t res = 0;
   while(1)
   {
        key = Key_Detect();
        if(key == KEY1_PRESS)
        {
            /*发送任务通知*/
            res = xTaskNotifyGive(task2_handle);//通知的任务句柄
            //会发送一个0,然后状态行为是增量行为,每Give一次通知值就加一
            if(res == pdPASS)
            {
                printf("发送任务通知成功");
            }
        }
        vTaskDelay(500);
   }
}
void task2(void * pvParameters)//任务2:用于接收任务通知,并打印相关提示信息
{
    uint32_t notify_value = 0;
   while(1)
   {
        notify_value = ulTaskNotifyTake(pdTRUE,portMAX_DELAY);
        //要不要把通知值清零(pdTURE清零,pdFALSE通知值减一),阻塞时间
        //返回取出来的通知值
        printf("任务2接收到任务通知值为%d",notify_value);
        vTaskDelay(500);
    }
}

 

Logo

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

更多推荐