FreeRTOS内部机制理解(事件组)(六)



摘要

四、事件组、任务通知

4.1 事件组

队列是数据

资源个数是信号量

互斥使用 互斥量并且包含优先级继承

如果一个任务是等待两个事件,那么就需要使用事件组,也就是需要两个事件触发。而刚好今天开发的程序就是这个样子,在表示烘干合作消毒健闪烁开始5秒闪烁的时候,就是使用了两个条件,一个是消毒工作标志位,一个是闪烁标志位,之所以是闪烁标志位是因为开始5秒是闪烁的,后面需要常量,因此就增肌了一个闪烁标志位,然后又会使用一个具体实现闪烁的标志位,也就是所有的闪烁都是基于这个标志位实现。所以这个里面就需要两个标志位一个是消毒工作标志位,一个是闪烁标志位,来完成前面5秒消毒或烘干的模式选择。 这不就是两个事件触发一个任务的典型代表,虽然我这个例子是在裸机开发实现的,但是这是一个实际的应用的例子,在做应用开发中,其实裸机和FreeRTOS中有些内容是相同的,但是执行的思路是不一样的。但是要思考应用场景。

接下来就实际说明事件组是什么。

事件是用位表示,不同位表示不用事件,那不就是我们的标志位。只不过如果在开发过程中全部使用标志位有点冗余,还会造成代码不方便阅读,这是很关键的,特别现在是模块化编程。

互斥量也可以看作一个最简单的事件组

在这里插入图片描述

需要注意的是在事件组中,左边的产生事件的任务永远不会阻塞,这是跟队列或者信号量有本质区别的,而右边消费这些事件的是会阻塞的,这是显而易见的,因为这些事件的到来肯定不是同时的呀,肯定有先来后到顺序,因此很正常。

typedef struct EventGroupDef_t

{

    EventBits_t uxEventBits;

    List_t xTasksWaitingForBits; /*< List of tasks waiting for a bit to be set. */

    #if ( configUSE_TRACE_FACILITY == 1 )

        UBaseType_t uxEventGroupNumber;

    #endif

    #if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )

        uint8_t ucStaticallyAllocated; /*< Set to pdTRUE if the event group is statically allocated to ensure no attempt is made to free the memory. */

    #endif

} EventGroup_t;

这是事件组的结构体,可以看出来还是简单的,没有那么复杂。

xTasksWaitingForBits 表示的是等待的事件到来的任务,

另外可以看出EventBits_t uxEventBits; 这个是一个32位的数据,因为我们每一位就要表示一个事件,因此我们需要尽可能大的数据来表示多种位,因此就直接使用一个单片机支持的最大的位。

1、创建结构体

2、等待(等待那些位,以及是与的关系还是或的关系 已经Timeout(用来设置是否等待))

a、关闭调度器

队列或者信号量的时候 是写数据的时候唤醒,并且还是需要关闭中断。

为什么关闭中断?

队列既可以是任务写,也可以是中断写,那么如果是避免中断写打断,那么就只能关闭中断,防止资源竞争。更深层就是防止数据在写的过程被打断,例如是信号量增加,导致两次增加只有一次增加有效,因此必须要关闭中断。

但是在事件组中,是关闭调度器。为什么不管中断?

因为事件是通过写队列去触发定时器任务做写事件操作,所以只有可能在任务重调用,所以没必要屏蔽中断。换句话说也就是在中断中不可能进行写事件。

b、当前事件位是否满足当前需要

不满足可以选择等待也可以选择不等待。如果是等待那么跟之前是一样的,放到源码中的那个核心链表,这样别人在写这个事件的时候就可以找到这个任务然唤醒了。并且还需要将自己移除ReadyList这才会释放CPU,而放到xTasksWaitingForBits 只是为了让别人找到你,不能起到休眠的作用。 这一部分其实是和队列,信号量是异曲同工之妙。

3、条件满足
一开始就满足或者被唤醒满足

成功返回OK。

设置事件:就是将某些位设置为1,并且还要唤醒与设置事件匹配的任务。是对应事件的任务,可能是多个事件才能唤醒一个任务,也可能是一个事件一个任务。

设置事件

唤醒等待链表中 “所有“ 满足条件的任务。这个就是广播机制?

在这里插入图片描述

EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,

                                 const EventBits_t uxBitsToWaitFor,

                                 const BaseType_t xClearOnExit,

                                 const BaseType_t xWaitForAllBits,

                                 TickType_t xTicksToWait )

在使用FreeRTOS的时候,我们其实还是需要弄明白,这些函数里面的参数是什么意义,这样才能不会使用错误,因为我们使用FreeRTOS最基本的能力就是如此,掌握各个API参数的含义,这也是日常基本功,或者说日常开发,因为很少会从0开始写,等我们熟练掌握了这些,才能进行系统的剪裁等,也就是步入大佬环节了,这都是后话。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

解释一个问题:

在事件组中,事件来临,可以触发跟这个事件所有相关的任务,但是如果我们在其中一个任务中清除了这个位,那么其他任务在执行的时候还会不会受到这个位影响,而不执行。

事件位的“清除”操作只影响后续的等待任务,不会影响已被唤醒或正在检查的任务

特性自动清除 (xClearOnExit=pdTRUE)手动清除/不自动清除 (xClearOnExit=pdFALSE)
行为任务成功等待事件位后,​自动清除这些位任务成功等待事件位后,​不自动清除这些位
对其他任务的影响后续等待相同事件位的任务不会立即满足条件(因为位已被清除)后续等待相同事件位的任务立即满足条件(因为位仍被置位)
适用场景常见场景。希望事件仅被一次性响应,避免任务被“残留”的事件位重复唤醒特殊场景。希望多个任务都能响应同一个事件,或事件状态需要持久化

一般来说都是要清除的吧。

假设有三个任务(TaskA, TaskB, TaskC)都在等待同一个事件位(例如 BIT0)。

  • 某个任务(或中断)设置了 BIT0​:事件组中 BIT0 的值变为 1。

  • 内核唤醒所有等待 BIT0 的任务​:TaskA, TaskB, TaskC 都会从阻塞态变为就绪态,等待调度器调度运行。

  • 任务调度执行与位清除​:

    • 如果 ​TaskA 的 xClearOnExit参数设置为 pdTRUE,那么当 TaskA ​成功获取到 BIT0 并开始执行后,它会自动清除 BIT0

    • 然而,​这个“清除”操作发生在 TaskA 成功等到事件之后。此时,TaskB 和 TaskC ​已经被唤醒并位于就绪队列中。因此,​BIT0 的清除不会影响已经被唤醒的 TaskB 和 TaskC,它们会正常执行。

    • 如果 ​TaskA 的 xClearOnExit参数设置为 pdFALSE,那么它执行后 BIT0 ​仍然为 1。如果之后还有其他任务(例如新创建的任务)也开始等待 BIT0,它会立刻满足条件并执行,因为事件位从未被清除。

你可以把事件位想象成一个大礼堂的公共广播开关。按下开关(置位)时,所有正在聆听(等待)的人(任务)都会听到(被唤醒)。其中一个人(TaskA)在听到后,可以选择是否关掉广播(清除事件位)。​关键在于,关广播的动作不会影响其他已经听到广播的人,他们该干嘛还是会干嘛;这只会影响之后进来聆听的人。​​ 因此,只要在任务等待事件时根据需求合理配置 xClearOnExit参数,就能协调好多个任务对同一事件的响应。

设计时明确事件生命周期​:在系统设计阶段,就要想清楚一个事件是希望被多个任务共同响应一次,还是希望被多个任务多次响应,或者是作为一个持久的状态标志。这将决定每个任务在等待时是否应自动清除事件位。

谨慎使用 pdFALSE ​:除非有明确需求(如状态标志),否则更推荐使用 pdTRUE。这样可以避免事件位残留导致的任务“虚假唤醒”(即任务因事件位仍为1而无需等待就直接执行),使系统行为更清晰可控。

手动清除​:除了在xEventGroupWaitBits中自动清除,你也可以在任何地方调用 xEventGroupClearBits()手动清除事件位。这给了你更大的灵活性,例如可以在任务处理完所有相关工作后,再主动清除事件位。

再次说明为什么不关闭中断?

在中断中不会设置bit,会唤醒守护函数在任务调度器中设置,关闭任务调度器就不会导致同时修改bit,所以没必要关闭中断。

这个就是之前将的守护任务,这样做的目的就是让中断快速相应,不会浪费太多时间执行逻辑,他有一个白手套那就是守护函数,守护函数就是执行中断想干的一切,然后守护函数还是使用任务调度器去设置事件位,这样的话我们只需要关闭任务调度器就行了。

中断使用该函数,只是写了一个队列,那就意味着有一个任务会被唤醒,那么这个任务就是守护者任务吧。

在这里插入图片描述

传进去一个函数,会执行函数vEventGroupSetBitsCallback

这个函数的作用就是设置事件位,所以并不是中断直接设置。

void vEventGroupSetBitsCallback( void * pvEventGroup,

                                 const uint32_t ulBitsToSet )

{

    ( void ) xEventGroupSetBits( pvEventGroup, ( EventBits_t ) ulBitsToSet ); 
}

在这里插入图片描述
这是正常的设置事件位,要处理很多事情,例如唤醒所有的任务,这个是需要时间的,

**==如果这些内容直接在中断中处理,那么就会导致我们的中断阻塞,所以就引入了守护函数,曲线救国。

在这里插入图片描述

所有等待事件组共用一个链表,不管是等待什么位。

设置事件的任务不会区分等待不同事件的任务。直接挨个对比就行了。

直接从链表中拿出这些任务,看当前的事件组是否满足当前链表中任务的要求,要求的就直接唤醒,不满足要求的就直接等待下一次。



结束语

如果觉得我的内容对您有帮助,希望不要吝啬您的赞和关注,您的赞和关注是我更新优质内容的最大动力。



专栏介绍

《嵌入式通信协议解析专栏》
《PID算法专栏》
《C语言指针专栏》
《单片机嵌入式软件相关知识》
《FreeRTOS源码理解专栏》
《嵌入式软件分层架构的设计原理与实践验证》



文章源码获取方式:
如果您对本文的源码感兴趣,欢迎在评论区留下您的邮箱地址。我会在空闲时间整理相关代码,并通过邮件发送给您。由于个人时间有限,发送可能会有一定延迟,请您耐心等待。同时,建议您在评论时注明具体的需求或问题,以便我更好地为您提供针对性的帮助。

【版权声明】
本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议。这意味着您可以自由地共享(复制、分发)和改编(修改、转换)本文内容,但必须遵守以下条件:
署名:您必须注明原作者(即本文博主)的姓名,并提供指向原文的链接。
相同方式共享:如果您基于本文创作了新的内容,必须使用相同的 CC 4.0 BY-SA 协议进行发布。

感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言,笔者一定知无不言,言无不尽。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

慈悲不渡自绝人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值