文章目录
- 何为任务通知?
- 任务通知使用例子
- 任务通知的优势以及劣势
- 优势
- 劣势
- 深入源码看看API函数内部干了什么
- 函数的种类
- 函数都做了啥?
- 软件定时器
- 软件定时器的作用
- 软件定时器内部到底做了什么实现了“闹钟”功能
- 引入守护任务,守护任务做了啥?
- 守护任务的调度
- 简单深入源码学习软件定时器
何为任务通知?
所谓"任务通知",你可以反过来读"通知任务"。就是就是一个简单唤醒其他任务的功能吗?那这样我队列、信号量、互斥量一样可以啊!那为什么要引进任务通知呢?那么我们就可以去看看任务通知到底有什么宇宙不同。
任务通知的通信是没有间隔的:
使用队列、信号量、事件组时,我们都要事先创建对应的结构体,双方通过中间的结构体通信:
使用任务通知时,任务结构体TCB中就包含了内部对象,可以直接接收别人发过来的"通知":
就是说创建任务的时候,任务里面创建的TCB结构体里面的某些变量就足以使用任务通知了
通知状态:
任务通知就是根据这个TCB里面的通知状态进行通信的。
任务通知使用例子
被通知的任务TCB结构体的通信状态的变换:
任务通知的优势以及劣势
优势
明确性、效率高:我们使用队列、信号量、事件组等等方法时,并不知道对方是谁。使用任务通知时,可以明确指定:通知哪个任务。
节省资源:相信深入学习过队列、信号量、互斥量的都知道它们在使用之前都需要创建,创建的时候里面都会分配一个结构体空间,里面会有链表、以及其他数据,队列传递数据时更多成了一个Buffer缓冲区,这些都是需要空间的,对于队列的细节可以看看我的这个文章:队列内部机制,在嵌入式里面,所谓是寸土寸金,对于空间利用来说所谓是斤斤计较,所以能省则省!任务通知则是不用创建,节省了很多空间!
劣势
不能发送数据给ISR:
ISR并没有任务结构体,所以无法使用任务通知的功能给ISR发送数据。但是ISR可以使用任务通知的功能,发数据给任务。
数据只能给该任务独享
使用队列、信号量、事件组时,数据保存在这些结构体中,其他任务、ISR都可以访问这些数据。
使用任务通知时,数据存放入目标任务中,只有它可以访问这些数据。
在日常工作中,这个限制影响不大。因为很多场合是从多个数据源把数据发给某个任务,而不是把一个数据源的数据发给多个任务。
无法缓冲数据
使用队列时,假设队列深度为N,那么它可以保持N个数据。
使用任务通知时,任务结构体中只有一个任务通知值,只能保持一个数据。
无法广播给多个任务
使用事件组可以同时给多个任务发送事件。
使用任务通知,只能发个一个任务。
如果发送受阻,发送方无法进入阻塞状态等待
假设队列已经满了,使用 xQueueSendToBack() 给队列发送数据时,任务可以进入阻塞状态等待发送完成。
使用任务通知时,即使对方无法接收数据,发送方也无法阻塞等待,只能即刻返回错误。
深入源码看看API函数内部干了什么
函数的种类
任务通知有2套函数,简化版、专业版,列表如下:
简化版函数的使用比较简单,它实际上也是使用专业版函数实现的
专业版函数支持很多参数,可以实现很多功能
具体的API函数使用方法以及参数可以看看这个手册:FreeRTOS开发手册
函数都做了啥?
这些函数都无非和队列它们一样都是唤醒任务或者阻塞本身,任务通知就是有一点不一样,就是要修改任务里面的TCB里面的通知状态,仅此而已!关于任务的唤醒以及阻塞可以看看我之前写的文章:任务的调度机制
就可以把他们的作为概括为这样:
挑出其中一个函数来分析:
软件定时器
软件定时器的作用
软件定时器就是"闹钟",你可以设置闹钟,
1、在30分钟后让你起床工作
2、每隔1小时让你例行检查机器运行情况
软件定时器也可以完成两类事情:
1、在"未来"某个时间点,运行函数
2、周期性地运行函数
日常生活中我们可以定无数个"闹钟",这无数的"闹钟"要基于一个真实的闹钟。
在FreeRTOS里,我们也可以设置无数个"软件定时器",它们都是基于系统滴答中断(Tick Interrupt)。
软件定时器内部到底做了什么实现了“闹钟”功能
前面所学的什么队列、信号量啊等等,处理任务通知,其他都需要创建,这个软件定时器也是需要创建的,那它的创建都创建了些什么呢?毫无疑问,还是那些结构体,以及=链表!那么它们的结构体里面的内容成员是什么呢?
在人类世界中,我们都是以时分秒为单位计算的,比如一个小时后去上班,等我五分钟,我上个厕所…等等。那么软件定时器是怎么样的呢?它们的单位是什么呢?其实它们的单位是Tick,学习任务调度的时候,任务调度也是由Tick Interrupt控制调度的,那么,对于它们来说就是,多少个Tick后我就来运行!还有就是我们人类世界是有一个表来计数时间的,那它们是通过什么来计数时间的呢?它们是在Tick中断里面进行计数的:
那时间到了,我要去哪里找到那些任务到点了需要运行呢?这时候就需要链表上场表演了,创建的定时器的时候创建的各个结构体都会储存在一个链表里面,到时候我就会里面找,比对一下就知道了谁时间到了,周期性的还会更新里面的启动时间和到点时间,方便下次运行!
引入守护任务,守护任务做了啥?
在事件组中我们看到FreeRTOS的作者担心的问题,就是唤醒任务,你不知道要唤醒的任务的个数,具体可以看看我写的这个文章:事件组内部机制学习,害怕唤醒所需的时间过长,影响中断的性能,那么定时器也会有类似的担忧问题,讨论这个担忧的问题是什么之前,==我们来看看在哪里执行定时器回调函数呢?==第一印象就是在Tick中断中执行:
1、在Tick中断中判断定时器是否到时间了
2、如果时间到了,就调用软件定时器的回调函数
3、回调函数里面不得影响到别的任务,需要尽快执行:
不要调用会导致阻塞的API函数,比如 vTaskDelay()
可以调用 xQueueReceive() 之类的函数,但是超时时间要设为0:即刻返回,不可阻塞
但是,在FreeRTOS实时操作系统中,它想做到实时,就不可能允许在内核,在中断中执行不确定的大小,万一定时器函数非常的长(运行的时间非常长),就会导致Tick中断迟迟无法退出,影响任务的调度,进入影响整个系统!
所以在FreeRTOS中,不再Tick中断中执行软件定时器的回调函数。
那么在哪里调用定时器回调函数呢?它的做法和事件组的做法相同!都是利用队列唤醒一个任务来执行其他的工作的,软件定时器中的这个任务叫“守护任务”!以前被称为"Timer server",但是这个任务要做并不仅仅是定时器相关,所以改名为:RTOS Damemon Task。因为这个做法使用了队列,而不是在Tick中断中调用回调函数的,所以软件定时器的启动、停止函数的参数有一个超时时间也就不足为怪了!
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );
BaseType_t xTimerStop( TimerHandle_t xTimer, TickType_t xTicksToWait );
当FreeRTOS的配置项 configUSE_TIMERS 被设置为1时,在启动调度器时,会自动创建RTOSDamemon Task。
守护任务的优先级为:configTIMER_TASK_PRIORITY;定时器命令队列的长度为configTIMER_QUEUE_LENGTH。
守护任务不是专为某个定时器服务的,它还要处理其他定时器
守护任务的调度
守护任务的调度,跟普通的任务并无差别。当守护任务是当前优先级最高的就绪态任务时,它就可以运行。它的工作有两类:
1、处理命令:从命令队列里取出命令、
2、处理执行定时器的回调函数
能否及时处理定时器的命令、能否及时执行定时器的回调函数,严重依赖于守护任务的优先级。下面使用2个例子来演示。
例子1:守护任务的优先性级较低
t1:Task1处于运行态,守护任务处于阻塞态。
守护任务在这两种情况下会退出阻塞态切换为就绪态:命令队列中有数据、某个定时器超时了。至于守护任务能否马上执行,取决于它的优先级。
t2:Task1调用 xTimerStart()
要注意的是, xTimerStart() 只是把"start timer"的命令发给"定时器命令队列",使得守护任务退出阻塞态。
在本例中,Task1的优先级高于守护任务,所以守护任务无法抢占Task1。
t3:Task1执行完 xTimerStart()
但是定时器的启动工作由守护任务来实现,所以 xTimerStart() 返回并不表示定时器已经被启动了。
t4:Task1由于某些原因进入阻塞态,现在轮到守护任务运行。
守护任务从队列中取出"start timer"命令,启动定时器。
t5:守护任务处理完队列中所有的命令,再次进入阻塞态。
Idel任务时优先级最高的就绪态任务,它执行。
注意:假设定时器在后续某个时刻tX超时了,超时时间是"tX-t2",而非"tX-t4",从xTimerStart() 函数被调用时算起。
例子2:守护任务的优先性级较高
t1:Task1处于运行态,守护任务处于阻塞态。
守护任务在这两种情况下会退出阻塞态切换为就绪态:命令队列中有数据、某个定时器超时了。
至于守护任务能否马上执行,取决于它的优先级。
t2:Task1调用 xTimerStart()
要注意的是, xTimerStart() 只是把"start timer"的命令发给"定时器命令队列",使得守护任务退出阻塞态。
在本例中,守护任务的优先级高于Task1,所以守护任务抢占Task1,守护任务开始处理命令队列。
Task1在执行 xTimerStart() 的过程中被抢占,这时它无法完成此函数。
t3:守护任务处理完命令队列中所有的命令,再次进入阻塞态。
此时Task1是优先级最高的就绪态任务,它开始执行。
t4:Task1之前被守护任务抢占,对 xTimerStart() 的调用尚未返回。现在开始继续运行次函数、返回。
==t5:Task1由于某些原因进入阻塞态,进入阻塞态。==Idel任务时优先级最高的就绪态任务,它执行。
注意,定时器的超时时间是基于调用 xTimerStart() 的时刻tX,而不是基于守护任务处理命令的时刻tY。假设超时时间是10个Tick,超时时间是"tX+10",而非"tY+10"。
简单深入源码学习软件定时器
#define xTimerstart(xTimer,xTicksTowait ) \
xTimerGenericCommand(( xTimer ), tmrCONWMAND_START,( xTaskGetTickCount()),NULL,( xTicksTowait ) )
函数内部都做了什么?
那么毫无疑问肯定是守护函数在接收队列!那么守护函数里面具体做了什么呢?