2.1 概览
基于 FreeRTOS 的应用程序由一组独立的任务构成——每个任务都是具有独立权
限的小程序。这些独立的任务之间很可能会通过相互通信以提供有用的系统功能。
FreeRTOS 中所有的通信与同步机制都是基于队列实现的。
2.2队列的特性
数据存储
队列可以保存有限个具有确定长度的数据单元。队列可以保存的最大单元数目被称
为队列的“深度”。在队列创建时需要设定其深度和每个单元的大小。
通常情况下,队列被作为 FIFO(先进先出)使用,即数据由队列尾写入,从队列首读
出。当然,由队列首写入也是可能的。
往队列写入数据是通过字节拷贝把数据复制存储到队列中;从队列读出数据使得把
队列中的数据拷贝删除。 图 19 展现了队列的写入与读出过程,以及读写操作对队列中
数据的影响。
可被多任务存取
队列是具有自己独立权限的内核对象,并不属于或赋予任何任务。所有任务都可以
向同一队列写入和读出。一个队列由多方写入是经常的事,但由多方读出倒是很少遇到。
读队列时阻塞
当某个任务试图读一个队列时,其可以指定一个阻塞超时时间。在这段时间中,如
果队列为空,该任务将保持阻塞状态以等待队列数据有效。当其它任务或中断服务例程
往其等待的队列中写入了数据,该任务将自动由阻塞态转移为就绪态。当等待的时间超
过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转移为就绪态。
由于队列可以被多个任务读取,所以对单个队列而言,也可能有多个任务处于阻塞
状态以等待队列数据有效。这种情况下,一旦队列数据有效,只会有一个任务会被解除
阻塞,这个任务就是所有等待任务中优先级最高的任务。而如果所有等待任务的优先级
相同,那么被解除阻塞的任务将是等待最久的任务。
写队列时阻塞
同读队列一样,任务也可以在写队列时指定一个阻塞超时时间。这个时间是当被写
队列已满时,任务进入阻塞态以等待队列空间有效的最长时间。
由于队列可以被多个任务写入,所以对单个队列而言,也可能有多个任务处于阻塞
状态以等待队列空间有效。这种情况下,一旦队列空间有效,只会有一个任务会被解除
阻塞,这个任务就是所有等待任务中优先级最高的任务。而如果所有等待任务的优先级
相同,那么被解除阻塞的任务将是等待最久的任务。
2.3 使用队列
xQueueCreate() API 函数
队列在使用前必须先被创建。
队列由声明为 xQueueHandle 的变量进行引用。 xQueueCreate()用于创建一个队
列,并返回一个 xQueueHandle 句柄以便于对其创建的队列进行引用。
当创建队列时, FreeRTOS 从堆空间中分配内存空间。分配的空间用于存储队列数
据结构本身以及队列中包含的数据单元。如果内存堆中没有足够的空间来创建队列,
xQueueCreate()将返回 NULL。
xQueueSendToBack() 与 xQueueSendToFront() API 函数
如同函数名字面意思所期望的一样, xQueueSendToBack()用于将数据发送到队列
尾;而 xQueueSendToFront()用于将数据发送到队列首。
xQueueSend()完全等同于 xQueueSendToBack()。
但 切 记 不 要 在 中 断 服 务 例 程 中 调 用 xQueueSendToFront() 或
xQueueSendToBack()。系统提供中断安全版本的 xQueueSendToFrontFromISR()与
xQueueSendToBackFromISR()用于在中断服务中实现相同的功能。
xQueueReceive()与 xQueuePeek() API 函数
xQueueReceive()用于从队列中接收(读取)数据单元。接收到的单元同时会从队列中删除。
xQueuePeek()也是从从队列中接收数据单元,不同的是并不从队列中删出接收到
的单元。 xQueuePeek()从队列首接收到数据后,不会修改队列中的数据,也不会改变
数据在队列中的存储序顺。
切记不要在中断服务例程中调用 xQueueRceive()和 xQueuePeek()。
uxQueueMessagesWaiting() API 函数
uxQueueMessagesWaiting()用于查询队列中当前有效数据单元个数。
切记不要在中断服务例程中调用 uxQueueMessagesWaiting()。应当在中断服务中
使用其中断安全版本 uxQueueMessagesWaitingFromISR()。
使用队列传递复合数据类型
一个任务从单个队列中接收来自多个发送源的数据是经常的事。通常接收方收到数
据后,需要知道数据的来源,并根据数据的来源决定下一步如何处理。一个简单的方式
就是利用队列传递结构体,结构体成员中就包含了数据信息和来源信息。 图 23 对这一方案进行了展现。
从图 23 中可以看出:
• 创建一个队列用于保存类型为 xData 的结构体数据单元。结构体成员包括了一个数
据值和表示数据含义的编码,两者合为一个消息可以一次性发送到队列。
• *控制任务用于完成主要的系统功能。 其必须对队列中传来的输入和其它系统状
态的改变作出响应。
• CAN 总线任务用于封装 CAN 总线的接口功能。当 CAN 总线任务收到并解码一个消
息后,其将把解码后的消息放到 xData 结构体中发往控制任务。结构体的 iMeaning
成员用于让*控制任务知道这个数据是用来干什么的 — 从图中的描述可以看
出,这个数据表示电机速度。结构体的 iValue 成员可以让*控制任务知道电机的
实际速度值。
• 人机接口(HMI)任务用于对所有的人机接口功能进行封装。设备操作员可能通过各种
方式进行命令输入和参数查询,人机接口任务需要对这些操作进行检测并解析。当
接收到一个新的命令后,人机接口任务通过 xData 结构将命令发送到*控制任务。
结构体的 iMeaning 成员用于让*控制任务知道这个数据是用来干什么的 — 从
图中的描述可以看出,这个数据表示一个新的参数设置。结构体的 iValue 成员可以
让*控制任务知道具体的设置值。
工作于大型数据单元
如果队列存储的数据单元尺寸较大,那最好是利用队列来传递数据的指针而不是对
数据本身在队列上一字节一字节地拷贝进或拷贝出。传递指针无论是在处理速度上还是
内存空间利用上都更有效。但是,当你利用队列传递指针时,一定要十分小心地做到以下两点:
1. 指针指向的内存空间的所有权必须明确
当任务间通过指针共享内存时,应该从根本上保证所不会有任意两个任务同时
修改共享内存中的数据,或是以其它行为方式使得共享内存数据无效或产生一致性
问题。原则上,共享内存在其指针发送到队列之前,其内容只允许被发送任务访问;
共享内存指针从队列中被读出之后,其内容亦只允许被接收任务访问。
2. 指针指向的内存空间必须有效
如果指针指向的内存空间是动态分配的,只应该有一个任务负责对其进行内存
释放。当这段内存空间被释放之后,就不应该有任何一个任务再访问这段空间。
切忌用指针访问任务栈上分配的空间。因为当栈帧发生改变后,栈上的数据将不再有效。
这个时候,再回过头去看看上一篇随笔,为什么官方demo使用结构体指针,因为这样效率更高。