什么是队列
在实际的应用中,常会遇到一个任务或者中断服务需要和另外一个任务进行“沟通交流”。这个“沟通交流”的过程其实就是消息传递的过程。
在没有操作系统的时候两个应用程序进行消息传递一般使用全局变量的方式,但是如果在使用操作系统的应用中用全局变量来传递消息就会涉及到“资源管理”的问题。FreeRTOS对此提供了一个叫做“队列”的机制来完成任务与任务,任务与中断之间的消息传递。
队列可以在任务与任务,任务与中断之间传递消息,队列中可以存储有限的,大小固定的数据项目。任务与任务,任务与中断之间的要交流的数据保存在队列中,叫队列项目。
队列所能保存的最大数据项目数量叫做队列的长度,创建队列的时候会指定数据项目的大小和队列的长度。由于队列是用来传递消息的,所以又被称为消息队列。同时FreeRTOS中的信号量也是依据消息队列实现的。
1. 数据存储
通常队列采用先进先出(FIFO)的存储缓冲的存储缓冲机制,也就是往队列发送数据的时候(也叫入队)永远都是发送到队列的尾部,而从队列提取数据的时候(也叫出队)是从队列的头部提取的。但是也可以使用LIFO的存储缓冲,也就是后入先出,FreeRTOS中的队列也提供了这种机制。
数据发送到队列中会导致数据拷贝,也就是将要发送的数据拷贝到队列中,这意味着在队列中存储的是数据的原始值,而不是原数据的引用(即只传递数据的指针),这种数据发送形式就是值传递。
FreeRTOS这种采用值传递的方式必然会浪费一些时间,但是这样一来就算是局部变量也可以进行数据的传递了,并且这种值传递的方式也可以将指针传入,这样一来即使要传递的消息数据太大也不用担心了。
2. 多任务访问
队列不是属于某个特定任务的,任何任务都可以向队列中发送消息,或者从队列中提取消息
3. 出队阻塞
当任务尝试从一个队列中读取消息的时候可以指定一个阻塞时间,这个阻塞时间就是当任务从队列中读取消息无效的时候任务阻塞的时间。
出队就是从队列中读取消息,出队阻塞是针对从队列中读取消息的任务而言的。比如A任务用于处理串口接收到的数据,串口接收到数据以后就会放到队列Q中,任务A从队列Q中读取数据。但如果此时队列Q是空的,说明还没有数据,任务A这时候来读取的话肯定是获取不到任何东西,那该怎么办?任务A现在有三种选择:
二话不说扭头就走
要不再等等,说不定一会就有数据传递过来了
死等,直到等到数据传过来为止
选择哪一个就是由这个阻塞时间来决定的,这个阻塞时间单位是时钟节拍数。
- 阻塞时间为0的话就是不阻塞没有数据的话马上返回来继续进行接下来的代码操作
- 如果阻塞时间为0 ~ portMAX_DELAY,当任务没有从队列中获取到消息的话就进入阻塞态,当阻塞时间到了以后还没有接收到数据的话就退出阻塞态,返回任务接着运行下面的代码,如果在阻塞时间之内接收到了数据就立即返回,执行任务中下面的代码
- 如果阻塞时间为portMAX_DELAY的话,任务就会一直处于阻塞态等待,知道接收到数据为止
4. 入队阻塞
入队说的是向队列中发送消息,将消息加入到队列中。和出队阻塞一样,当一个任务向队列发送消息的话也可以设置阻塞时间。比如任务B向消息队列Q发送消息,但是此时队列Q是满的,那肯定是发送失败的。此时任务B就会遇到和上面任务A一样的问题,二者处理方式类似,只不过一个是向队列Q读取消息,一个是向队列Q发送消息。
#
5. 队列操作过程图示
图13.1.1中任务A要向任务B发送消息,这个消息是X变量的值。首先创建一个队列,并指定队列的长度和每条消息的长度。这里创建了一个长度为4的队列,因要要传递的是X值,而X是个int类型的变量,所以每条消息的长度就是int类型的长度,在STM32中就是4个字节,即每条消息是四个字节
图13.1.2中任务A的变量X值为10,将这个值发送到消息队列中。此时队列剩余长度就是3了。前面说了向队列中发送消息是采用拷贝的方式,所以一旦消息发送完成变量X就可以再次被使用,赋其他的值
图13.1.3中任务A又向队列发送了一个消息,即新的X的值,这里是20。此时队列的长度为2
图13.1.4中任务B从队列中读取消息,并将读到的消息值赋值给y,这样y就等于10了。任务B从队列读取消息完成之后可以选择清除或者不清除掉这个消息。当选择清除这个消息的话其他任务或中断就不能获取这个消息了,而且队列剩余大小就会加1,变成3.如果不清除的话其他任务或中断也可以获取这个消息,而且队列的剩余大小依旧是2。
相关的队列操作函数
入队函数
/* 任务级入队函数 */
-------------------------------------------------------------------
/* 发送消息到队列尾部(后向入队),这两个函数是一样的 */
xQueueSend()
xQueueSendToBack()
/* 发送消息到队列头 */
xQueueSendToFront()
/* 发送消息到队列,带覆写功能,当队列满了之后自动覆盖掉旧的消息 */
xQueueOverwrite()
-------------------------------------------------------------------
/* 中断级入队函数 */
xQueueSendFromISR()
xQueueSendToBackFromISR()
xQueueSendToFrontFromISR()
xQueueOverwriteFromISR()
函数参数
BaseType_t xQueueSend( QueueHandle_t xQueue, const void * pvItemToQueue, TickType_t xTicksToWait );
BaseType_t xQueueSendToBack(QueueHandle_t xQueue, const void* pvItemToQueue, TickType_t xTicksToWait);
BaseType_t xQueueSendToToFront(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait);
参数
- xQueue: 队列句柄,指要向那个队列发送数据
- pvItemToQueue: 指向要发送的消息,发送时候会将这个消息自动拷贝到队列中
- xTicksToWait: 阻塞时间
返回值
- pdPASS:向队列发送消息成功 !
- errQUEUE_FULL: 队列已经满了,消息发送失败。
BaseType_t xQueueOverwrite(QueueHandle_t xQueue, const void * pvItemToQueue);
此函数也是用于向队列发送数据的,当队列满了之后会覆盖掉旧的数据,不管这个旧的数据有没有被其他任务或者中断取走。该函数常用于向那些长度为1的队列发送消息。
参数
- xQueue:队列句柄,指要向那个队列发送数据
- pvItemToQueue: 指向要发送的消息,发送时候会将这个消息自动拷贝到队列中
返回值
- pdPASS:向队列发送消息成功 !(因为这个函数是强行覆写,所以一定是成功的)
BaseType_t xQueueSendFromISR(QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t * pxHigherPriorityTaskWoken);
BaseType_t xQueueSendToBackFromISR(QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t * pxHigherPriorityTaskWoken);
BaseType_t xQueueSendToFrontFromISR(QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t * pxHigherPriorityTaskWoken);
参数
- xQueue:队列句柄,指要向那个队列发送数据
- pvItemToQueue: 指向要发送的消息,发送时候会将这个消息自动拷贝到队列中
- pxHigherPriorityTaskWoken:标记退出此函数以后是否进行任务切换,这个变量的值由这三个函数来设置的,用户不进行设置,用户只需要提供一个变量来保存这个值就好了。
返回值
pdPASS:向队列发送消息成功!
errQUEUE_FULL: 队列已经满了,消息发送失败。
这些函数都没有设置阻塞时间。原因就是这些函数是在中断中调用的,并不是在任务中,所以也就没有了阻塞一说了
BaseType_t xQueueOverwriteFromISR(QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t * pxHigherPriorityTaskWoken);
此函数的返回值与上面三个相同
队列读取函数
/* 任务级出队函数 */
-------------------------------------------------------------------
/* 从队列中读取队列项并且删除 */
xQueueReceive()
/* 从队列中读取队列项但不删除 */
xQueuePeek()
-------------------------------------------------------------------
/* 中断级出队函数 */
xQueueReceiveFromISR()
xQueuePeekFromISR ()
函数参数
BaseType_t xQueueReceive(QueueHandle_t xQueue,
void * pvBuffer,
TickType_t xTicksToWait);
此函数在读取消息的时候采用拷贝的方式,所以用户需要提供一个数组或缓冲区来保存读取到的数据,所读取数据的长度是创建队列的时候所设定的每个队列的长度
参数
- xQueue:队列句柄,指要读取哪个队列的数据
- pvBuffer:保存数据的缓冲区
- xTicksToWait:阻塞时间
返回值
- pdTRUE:从队列中读取数据成功
- pdFALSE:从队列中读取数据失败
BaseType_t xQueuePeek(QueueHandle_t xQueue,
void * pvBuffer,
TickType_t xTicksToWait);
参数
- xQueue:队列句柄,指要读取哪个队列的数据
- pvBuffer:保存数据的缓冲区
- xTicksToWait:阻塞时间
返回值
- pdTRUE:从队列中读取数据成功
- pdFALSE:从队列中读取数据失败
读取队列函数的中断模式同理
相关函数使用注意事项
- 关于消息的发送与接收是以队列的1个长度为单位的,需要搬运数组等类型数据时需要多次循环使用消息队列收发函数
- 如果需要在搬运串口接收到的数据的话,串口中断优先级需要在FreeRTOS设置的优先级之内,否则无法使用中断类型函数,并且还要注意任务切换
- 队列的长度单位需要注意,需要与要搬运的数据长度一致