FreeRTOS学习笔记(4、事件组、任务通知)
前言
这是第四弹,由于CSDN长度的限制,所以把FreeRTOS学习分为几部分来发,这是第四部分
主要包括
事件组、任务通知
等
往期学习笔记链接
第一弹
:FreeRTOS学习笔记(1、FreeRTOS初识、任务的创建以及任务状态理论、调度算法等)
第二弹
: FreeRTOS学习笔记(2、同步与互斥通信、队列、队列集的使用)
第三弹
: FreeRTOS学习笔记(3、信号量、互斥量的使用)
第四弹
: FreeRTOS学习笔记(4、事件组、任务通知)
第五弹
: FreeRTOS学习笔记(5、定时器、中断管理、调试与优化)
学习工程
所有学习工程
oufen / FreeRTOS学习
都在我的Gitee工程当中,大家可以参考学习
事件组 event group
可以通过队列传送数据
可以通过信号量,来传递状态信息
还可以使用互斥量来实现临界资源的互斥访问(独占)
但是上述都没办法解决事件组,事件组包含多个事件
事件组,右边可以等
- 若干个事件中的某个事件
- 某个事件
- 若干个事件中的所有事件
从学习此知识点开始,使用Source Ingsight编辑代码,不再使用VScode
事件组的创建函数
事件组的结构体
EventBits_t 代表一个整数,每一位bit代表一件事件
当生产者,生产完后就可以设置这个EventGroup的某一位,表示这个事件我做完了
生产者1和生产者2所做的事情不一样,所设置的位也不一样
如果事件组里没有东西,那么消费者任务就会等待
这些任务存放在List_t xTasksWaitingForBits
;链表中
使用事件组
- 创建事件组
- 左边生产者set bits 设置事件的某个位
能够设置哪个事件,就去设置哪个位
- 右边消费者 wait bits 等待事件的某个位
- 同步点
假如有taskA、taskB、taskC
taskA做完某些事情后,设置bit0,等待三个任务的bit都设置为1后才可以做下一步
taskB同
taskC同
这三个task都可以调用这个函数表示完成了某件事情
三个bit位都被设置完后,这三个task都可以从这个函数里退出来
这就是同步点,可以使用这个函数来实现多个task,函数退出之后将会设置三个bit,清零
注意
事件组只起通知作用,要想把数据保存起来,就要另外使用其他方法保存数据
比如队列保存数据
keil里面的代码标准
keil默认使用的C语言标准是C89,必须这样子做
使用C99,变量的定义可以定义在任何地方
可以在Keil里使用C99标准
图片写错辽 是--c99
事件组的基本使用
1、创建事件组
事件组只能起通知作用,保存数据要采用其他的方法,这里采用队列
注意,使用事件组时,需要定义宏后,才能使用
c
#define configSUPPORT_DYNAMIC_ALLOCATION 1 /* 使用事件组头文件*/
2、设置事件
计算完成后,向队列中写入数据,并且设置事件组的bit0位
3、等待事件
等待事件组的bit0位|bit1位后,将数据从队列中读取出来
可以看到,计算结束后,设置事件组的bit0|bit1位,等待事件组的两位设置位1后,然后就从队列中将数据读取出来
事件组的使用-同步点
同步函数,有三个功能
- 设置事件,表示自己完成了某个或者某些事件
- 等待事件,和别的task同步
- 成功返回后,清除等待的事件
同步函数退出后
- 成功退出的话,会清除事件
- 成功退出的话,等待哪些事件,就会清除哪些事件
1、创建事件组
2、等待同步
当三个bit都被设置为1之后,就可以同步继续,打印等待同步之后的内容
任务通知 task notification
任务通知的基础知识
使用队列、信号量、事件组等,我们都需要事先创建对应的结构体,对方通过中间的结构体进行通信
而使用任务通知,任务结构体TCB,中就包含了内部对象,可以直接接收别人发送过来的通知
使用任务通知时,只能通知指定的task,所以是多对1的关系
TCB结构体中包含了内部对象
一个是uint8_t 类型的,表示通知的状态
一个是uint32_t类型的,表示通知的值
一个TCB结构体代表一个task,别的task可以去通知它,可以往TCB结构体中放入值,通知它,进而放一个通知值
发送task往TCB结构体中放入值的时候,要么成功,要么失败,都不会进入到阻塞状态
目标task可以等待,无数据时,可以阻塞等待
有数据时,即刻返回
在TCB结构体中只有一个通知状态,并没有List列表存放阻塞的task
并不像队列一样让其存放在链表中,从而阻塞等待
当发送task将通知值放入TCB结构体中时,将会改变任务通知的状态,并且将目标task唤醒
所以是多对1的关系
任务通知状态的取值
ucNotifyState 通知状态的取值为
taskNOT_WAITING_NOTIFICATION
:任务没有在等待通知taskWAITING_NOTIFICATION
:任务在等待通知taskNOTIFICATION_RECEIVED
:任务接收到了通知,也被称为pending(有数据了,待处理)
- 一开始创建taskA时,任务通知的状态的初始值为
taskNOT_WAITING_NOTIFICATION
,taskA没有在等待通知 - taskA 想要
等待通知
的话,taskB 可以调用ulTaskNotifyTake
函数或xTaskNotifyWait
函数,将会进入taskWAITING_NOTIFICATION状态
,taskA在等待通知 - taskB可以调用
xTaskNotify或xTaskNotifyGive函数
来通知taskA
此时taskA的任务状态为taskNOTIFICATION_RECEIVED
,表示接收到了数据,待处理
同时还会将taskA,从阻塞状态变为就绪状态,taskA开始运行
任务通知的两类函数
c
BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait );
BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction );
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t *pulNotificationValue,
TickType_t xTicksToWait );
任务通知的优缺点
- 传递数据/发送事件时,更快
- 更节省内存,因为无需创建通信对象
- 不能使用任务通知给ISR发送数据/发送事件,ISR不属于任务
- 接收方只有一个任务
- 无法缓冲多个数据,任务通知只能保存1个数据
- 无法向多个任务进行广播
- 发送方无法阻塞
任务通知的使用-轻量级信号量
c
uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait);
xClearCountOnExit,函数返回前是否清零
- pdTRUE,将通知值清零
- pdFALSE,如果通知之大于0,就把通知值减1
ulTaskNotifyTake()函数的返回值有两种情况
- 大于0,在超时之前,通知值被增加了,返回的是通知值
- 等于0,一直没有其他task增加通知量,超时返回
信号量和使用任务通知实现信号量的辨析
信号量
- 创建信号量
- taskA完成事件后,give信号量,让信号量计数值+1
- taskB等待信号量,take信号量,让信号量计数值-1
- taskA可以give N 次,taskB可以take N次
任务通知实现信号量
- taskA发送任务通知给taskB
- taskB任务通知值++
- taskA类似于give操作,taskB类似于take操作
- taskB take几次,取决于xClearCountOnExit
- pdTRUE,退出时将通知值清零
- pdFALSE,退出时如果通知之大于0,就把通知值减1
xClearCountOnExit采用pdFALSE,时,和一般的信号量是一样的
如果采用pdTRUE,只能take一次,因为在take后就将任务通知值-1了
1/通知其他任务
使用xTaskNotifyGive
函数通知其他任务,使其他任务,通知值+1
2/等待任务通知
两者区别
任务通知不需要创建结构体,直接使用TCB结构体中的任务通知值和任务状态即可
如果任务通知想向信号量一样使用
那么xClearCountOnExit
参数的选择需要选择pdFALASE,否则选择pdTRUE的话,任务通知,give++,任务接收时只能接收一次
任务通知的使用-轻量级队列
队列和任务通知实现队列的区别
1、队列
task可以向队列中写入数据,也可以向队列中接收数据,在写入数据时可以等待,在接收数据时也要可以等待
并且在队列的创建时,可以指定队列的长度和大小(字符串、结构体、整数等)
2、任务通知
而对于任务通知
TCB结构体中的任务通知值和任务状态,
任务通知值只能够存放一个数据,且是32位的
发送通知task,有两种情况,要么覆盖,要么不覆盖数据
覆盖指的是,发送第一个数据,存放至通知值中,发送第二个数据时,覆盖第一次的数据,此时,通知值中是第二次的数据
不覆盖的话,存入数据时将会不成功
接收任务通知,可以读取任务通知值
3、区别
任务通知不需要创建结构体
同时
发送数据
c
BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify,uint32_t ulValue,eNotifyAction eAction);
第一个参数是任务句柄
第二个参数就是存入到任务通知值的值
第三个参数是选择实现 覆盖Or 不覆盖
接收数据
c
BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry,uint32_t ulBitsToClearOnExit,uint32_t *pulNotificationValue,TickType_t xTicksToWait);
- 队列的长度和大小可以指定
- 任务通知只有1个数据,通知值,数据是32位的
- 队列,向队列写数据或者读数据时,可以阻塞
- 任务通知,写队列时不可以阻塞
- 队列,如果队列的长度是1,可以选择覆盖队列
- 任务通知,可以覆盖也可以不覆盖
这个轻量级的队列的长度只有1
队列实现
task1向队列中写入了十个数据,task2就可以从队列中取出这十个数据
任务通知实现
1、通知值不覆盖
2、通知值覆盖
这里注意了,任务通知,任务通知写入多次,但是等待任务通知只能读一次
但是邮箱,一旦邮箱中有数据,可以多次读,都会成功
任务通知的使用-轻量级事件组
可以通过设置xTaskNotify()
函数的eNotifyAction
参数,设置为eSetBits
时,即可实现轻量级事件组
事件组和任务通知实现事件组
事件组
任务完成后,可以设置事件组中的某一位
等待事件组的某一位或某几位或者所有位
假如,taskA,完成事件后,设置bit0,taskB完成事件后,设置bit1
这是等待事件时,有两种情况
- 等待bit0或bit1
- 等待bit0和bit1
当等待的事件为或的时候,每设置一位,就会被唤醒一次,比如设置了bit0,将会被唤醒一次,设置了bit1,将会被唤醒第二次
当等待的事件为和的时候,只有当两个事件位都被设置,才会被唤醒
任务通知实现事件组
通过设置xTaskNotify()
函数的eNotifyAction
参数,设置为eSetBits
时,即可实现轻量级事件组
此时ulvalue将会等于val = val | ulValue
此时一旦taskA,调用
**xTaskNotify()**
函数,就会唤醒目标任务taskB
但是事件组,设置了某些位后,要等待这些位满足条件才能被唤醒
发送方可以设置事件,但是接收方并不能等待指定的事件,不能等待若干个事件中的任意一个或多个
一旦有事件,总会唤醒task
事件组实现
可以看到事件组,task3在等待两位都被设置为1之后,才被唤醒,进入运行状态
任务通知实现事件组
任务通知实现事件组并不能实现,等待某些事件或者某个事件
每被通知一次,将会被唤醒一次