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;链表中

使用事件组

  1. 创建事件组

  1. 左边生产者set bits 设置事件的某个位

能够设置哪个事件,就去设置哪个位


  1. 右边消费者 wait bits 等待事件的某个位


  1. 同步点

假如有taskA、taskB、taskC

taskA做完某些事情后,设置bit0,等待三个任务的bit都设置为1后才可以做下一步
taskB同
taskC同
这三个task都可以调用这个函数表示完成了某件事情
三个bit位都被设置完后,这三个task都可以从这个函数里退出来

这就是同步点,可以使用这个函数来实现多个task,函数退出之后将会设置三个bit,清零

注意

事件组只起通知作用,要想把数据保存起来,就要另外使用其他方法保存数据

比如队列保存数据

keil里面的代码标准

keil默认使用的C语言标准是C89,必须这样子做
使用C99,变量的定义可以定义在任何地方
可以在Keil里使用C99标准

图片写错辽 是–c99

事件组的基本使用

1、创建事件组

事件组只能起通知作用,保存数据要采用其他的方法,这里采用队列



注意,使用事件组时,需要定义宏后,才能使用


#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(有数据了,待处理)

  1. 一开始创建taskA时,任务通知的状态的初始值为taskNOT_WAITING_NOTIFICATION,taskA没有在等待通知
  2. taskA想要等待通知的话,taskB可以调用ulTaskNotifyTake函数或 xTaskNotifyWait函数,将会进入taskWAITING_NOTIFICATION状态,taskA在等待通知
  3. taskB可以调用xTaskNotify或xTaskNotifyGive函数来通知taskA

此时taskA的任务状态为taskNOTIFICATION_RECEIVED,表示接收到了数据,待处理
同时还会将taskA,从阻塞状态变为就绪状态,taskA开始运行

任务通知的两类函数

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 );

任务通知的优缺点

  1. 传递数据/发送事件时,更快
  2. 更节省内存,因为无需创建通信对象
  3. 不能使用任务通知给ISR发送数据/发送事件,ISR不属于任务
  4. 接收方只有一个任务
  5. 无法缓冲多个数据,任务通知只能保存1个数据
  6. 无法向多个任务进行广播
  7. 发送方无法阻塞

任务通知的使用-轻量级信号量

uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit, TickType_t xTicksToWait);

xClearCountOnExit,函数返回前是否清零

  • pdTRUE,将通知值清零
  • pdFALSE,如果通知之大于0,就把通知值减1

ulTaskNotifyTake()函数的返回值有两种情况

  • 大于0,在超时之前,通知值被增加了,返回的是通知值
  • 等于0,一直没有其他task增加通知量,超时返回

信号量和使用任务通知实现信号量的辨析

信号量

  1. 创建信号量
  2. taskA完成事件后,give信号量,让信号量计数值+1
  3. taskB等待信号量,take信号量,让信号量计数值-1
  4. taskA可以give N 次,taskB可以take N次

任务通知实现信号量

  1. taskA发送任务通知给taskB
  2. taskB任务通知值++
  3. taskA类似于give操作,taskB类似于take操作
  4. taskB take几次,取决于xClearCountOnExit
    1. pdTRUE,退出时将通知值清零
    2. 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、区别

任务通知不需要创建结构体


同时

发送数据

BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify,uint32_t ulValue,eNotifyAction eAction);

第一个参数是任务句柄

第二个参数就是存入到任务通知值的值

第三个参数是选择实现 覆盖Or 不覆盖

接收数据

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之后,才被唤醒,进入运行状态

任务通知实现事件组

任务通知实现事件组并不能实现,等待某些事件或者某个事件

每被通知一次,将会被唤醒一次