摘要
四、事件组、任务通知
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 协议进行发布。
感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言,笔者一定知无不言,言无不尽。
500

被折叠的 条评论
为什么被折叠?



