在实际的应用中,常常会遇到一个任务或者中断服务需要和另外一个任务进行“沟通交流”, 这个“沟通交流”的过程其实就是消息传递的过程。在没有操作系统的时候两个应用程序进行消息传递一般使用全局变量的方式,但是如果在使用操作系统的应用中用全局变量来传递消息就会涉及到“资源管理”的问题。FreeRTOS 对此提供了一个叫做“队列”的机制来完成任务与任务、任务与中断之间的消息传递,由于队列用来传递消息的,所以也称为消息队列。
1、队列简介
队列是为了任务与任务、任务与中断之间的通信而准备的,可以在任务与任务、任务与中断之间传递消息,队列中可以存储有限的、大小固定的数据项目。任务与任务、任务与中断之间要交流的数据保存在队列中,叫做队列项目。队列所能保存的最大数据项目数量叫做队列的长度,创建队列的时候会指定数据项目的大小和队列的长度。
通常队列采用先进先出(FIFO)的存储缓冲机制,也就是往队列发送数据的时候(也叫入队)永远都是发送到队列的尾部,而从队列提取数据的时候(也叫出队)是从队列的头部提取的。但是也可以使用 LIFO 的存储缓冲,也就是后进先出,FreeRTOS 中的队列也提供了 LIFO 的存储缓冲机制。
数据发送到队列中会导致数据拷贝,也就是将要发送的数据拷贝到队列中,这就意味着在队列中存储的是数据的原始值,而不是原数据的引用(即只传递数据的指针),这个也叫做值传递。UCOS 的消息队列采用的是引用传递,传递的是消息指针。采用引用传递的话消息内容就必须一直保持可见性,也就是消息内容必须有效,那么局部变量这种可能会随时被删掉的东西就不能用来传递消息,但是采用引用传递会节省时间啊!因为不用进行数据拷贝。
采用值传递的话虽然会导致数据拷贝,会浪费一点时间,但是一旦将消息发送到队列中原始的数据缓冲区就可以删除掉或者覆写,这样的话这些缓冲区就可以被重复的使用。FreeRTOS中使用队列传递消息的话虽然使用的是数据拷贝,但是也可以使用引用来传递消息啊,我直接往队列中发送指向这个消息的地址指针不就可以了!这样当我要发送的消息数据太大的时候就可以直接发送消息缓冲区的地址指针,比如在网络应用环境中,网络的数据量往往都很大的,采用数据拷贝的话就不现实。
1、多任务访问
队列不是属于某个特别指定的任务的,任何任务都可以向队列中发送消息,或者从队列中提取消息。
2、出队阻塞
当任务尝试从一个队列中读取消息的时候可以指定一个阻塞时间,这个阻塞时间就是当任务从队列中读取消息无效的时候任务阻塞的时间。出队就是就从队列中读取消息,出队阻塞是针对从队列中读取消息的任务而言的。
比如任务 A 用于处理串口接收到的数据,串口接收到数据以后就会放到队列 Q 中,任务 A 从队列 Q 中读取数据。但是如果此时队列 Q 是空的,说明还没有数据,任务 A 这时候来读取的话肯定是获取不到任何东西,那该怎么办呢?任务 A 现在有三种选择,一:二话不说扭头就走,二:要不我在等等吧,等一会看看,说不定一会就有数据了,三:死等,死也要等到你有数据!选哪一个就是由这个阻塞时间决定的,这个阻塞时间单位是时钟节拍数。阻塞时间为 0 的话就是不阻塞,没有数据的话就马上返回任务继续执行接下来的代码,对应第一种选择。如果阻塞时间为 0~ portMAX_DELAY,当任务没有从队列中获取到消息的话就进入阻塞态,阻塞时间指定了任务进入阻塞态的时间,当阻塞时间到了以后还没有接收到数据的话就退出阻塞态,返回任务接着运行下面的代码,如果在阻塞时间内接收到了数据就立即返回,执行任务中下面的代码,这种情况对应第二种选择。当阻塞时间设置为portMAX_DELAY 的话,任务就会一直进入阻塞态等待,直到接收到数据为止!这个就是第三种选择。
3、入队阻塞
入队说的是向队列中发送消息,将消息加入到队列中。和出队阻塞一样,当一个任务向队列发送消息的话也可以设置阻塞时间。比如任务 B 向消息队列 Q 发送消息,但是此时队列 Q 是满的,那肯定是发送失败的。此时任务 B 就会遇到和上面任务 A 一样的问题,这两种情况的处理过程是类似的,只不过一个是向队列 Q 发送消息,一个是从队列 Q 读取消息而已。
4、队列操作过程图示
2、队列结构体
有一个结构体用于描述队列,叫做 Queue_t,这个结构体在文件 queue.c 中定义。
3、队列创建
在使用队列之前必须先创建队列,有两种创建队列的方法,一种是静态的,使用函数xQueueCreateStatic();另一个是动态的,使用函数 xQueueCreate()。这两个函数本质上都是宏,真正完成队列创建的函数是 xQueueGenericCreate()和 xQueueGenericCreateStatic(),这两个函数在文件 queue.c 中定义。
函数 prvInitialiseNewQueue() 用于队列的初始化,此函数在文件 queue.c 中定义。
函数 prvInitialiseNewQueue()中调用了函数 xQueueGenericReset()来复位队列。
比如我们创建一个有 4 个队列项,每个队列项长度为 32 个字节的队列 TestQueue,创建成功的队列如图所示:
在创建的时候需要指定此队列的用途,也就是队列类型,一共有六种类型:
queueQUEUE_TYPE_BASE 普通的消息队列
queueQUEUE_TYPE_SET 队列集
queueQUEUE_TYPE_MUTEX 互斥信号量
queueQUEUE_TYPE_COUNTING_SEMAPHORE 计数型信号量
queueQUEUE_TYPE_BINARY_SEMAPHORE 二值信号量
queueQUEUE_TYPE_RECURSIVE_MUTEX 递归互斥信号量
4、向队列发送消息
FreeRTOS 提供了 8 个向队列发送消息的 API 函数:
5、队列上锁和解锁
队列的上锁和解锁是两个 API 函数:prvLockQueue()和 prvUnlockQueue()。
6、从队列读取消息
有入队就有出队,出队就是从队列中获取队列项(消息),FreeRTOS 中出队函数如表示:
具体的函数用法大家可以在用到的时候百度,这里就不详细介绍了,大家知道有这些东西就行。其中最重要的是任务级和中断级不一样,在中断处理函数中是由一套自己的 API 用的。