1. 概述
基于FreeRTOS 的应用程序由一组独立的任务构成——每个任务都是具有独立权限的小程序。这些独立的任务之间很可能会通过相互通信以提供有用的系统功能。FreeRTOS 中所有的通信与同步机制都是基于队列实现的。
2. 队列特性
数据存储 队列可以保存有限个具有确定长度的数据单元。队列可以保存的最大单元数目被称为队列的“深度”。在队列创建时需要设定其深度和每个单元的大小。 通常情况下,队列被作为FIFO(先进先出)使用,即数据由队列尾写入,从队列首读出。当然,由队列首写入也是可能的。 往队列写入数据是通过字节拷贝把数据复制存储到队列中;从队列读出数据使得把队列中的数据拷贝删除。 可被多任务存取 队列是具有自己独立权限的内核对象,并不属于或赋予任何任务。所有任务都可以向同一队列写入和读出。一个队列由多方写入是经常的事,但由多方读出倒是很少遇到。 读队列时阻塞 当某个任务试图读一个队列时,其可以指定一个阻塞超时时间。在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效。当其它任务或中断服务例程往其等待的队列中写入了数据,该任务将自动由阻塞态转移为就绪态。当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转移为就绪态。(这个是不是类似于linux中的poll机制,设定了超时时间。其实知识原理都是相通的。) 由于队列可以被多个任务读取,所以对单个队列而言,也可能有多个任务处于阻塞状态以等待队列数据有效。这种情况下,一旦队列数据有效,只会有一个任务会被解除阻塞,这个任务就是所有等待任务中优先级最高的任务。而如果所有等待任务的优先级相同,那么被解除阻塞的任务将是等待最久的任务。 写队列时阻塞 同读队列一样,任务也可以在写队列时指定一个阻塞超时时间。这个时间是当被写队列已满时,任务进入阻塞态以等待队列空间有效的最长时间。 由于队列可以被多个任务写入,所以对单个队列而言,也可能有多个任务处于阻塞状态以等待队列空间有效。这种情况下,一旦队列空间有效,只会有一个任务会被解除阻塞,这个任务就是所有等待任务中优先级最高的任务。而如果所有等待任务的优先级相同,那么被解除阻塞的任务将是等待最久的任务。 读写队列过程:
3. 使用队列
xQueueCreate() API 函数 队列在使用前必须先被创建。 队列由声明为xQueueHandle 的变量进行引用。xQueueCreate()用于创建一个队列,并返回一个xQueueHandle 句柄以便于对其创建的队列进行引用。 当创建队列时,FreeRTOS 从堆空间中分配内存空间。分配的空间用于存储队列数据结构本身以及队列中包含的数据单元。如果内存堆中没有足够的空间来创建队列,xQueueCreate()将返回NULL。 函数原型: xQueueHandle xQueueCreate( unsigned portBASE_TYPE uxQueueLength, unsigned portBASE_TYPE uxItemSize ); uxQueueLength 队列能够存储的最大单元数目,即队列深度。 uxItemSize 队列中数据单元的长度,以字节为单位。 返回值 NULL 表示没有足够的堆空间分配给队列而导致创建失败。非NULL 值表示队列创建成功。此返回值应当保存下来,以作为操作此队列的句柄。 xQueueSendToBack() 与 xQueueSendToFront() API 函数 xQueueSendToBack()用于将数据发送到队列尾;而xQueueSendToFront()用于将数据发送到队列首。 xQueueSend()完全等同于xQueueSendToBack()。 但切记不要在中断服务例程中调用xQueueSendToFront() 或xQueueSendToBack()。系统提供中断安全版本的xQueueSendToFrontFromISR()与xQueueSendToBackFromISR()用于在中断服务中实现相同的功能。
函数原型: portBASE_TYPE xQueueSendToFront( xQueueHandle xQueue, const void * pvItemToQueue, portTickType xTicksToWait ); portBASE_TYPE xQueueSendToBack( xQueueHandle xQueue, const void * pvItemToQueue, portTickType xTicksToWait ); xQueue 目标队列的句柄。这个句柄即是调用xQueueCreate()创建该队列时的返回值。 pvItemToQueue 发送数据的指针。其指向将要复制到目标队列中的数据单元。由于在创建队列时设置了队列中数据单元的长度,所以会从该指针指向的空间复制对应长度的数据到队列的存储区域。 xTicksToWait 阻塞超时时间。 (1)如果在发送时队列已满,这个时间即是任务处于阻塞态等待队列空间有效的最长等待时间。 (2)如果xTicksToWait设为0, 并且队列已满, 则xQueueSendToFront()与xQueueSendToBack()均会立即返回。阻塞时间是以系统心跳周期为单位的,所以绝对时间取决于系统心跳频率。常量portTICK_RATE_MS 可以用来把心跳时间单位转换为毫秒时间单位。 (3)如果把xTicksToWait 设置为portMAX_DELAY , 并且在FreeRTOSConig.h 中设定INCLUDE_vTaskSuspend 为1,那么阻塞等待将没有超时限制。(这里的意思就是: 永久等待,直到发送队列变为有效。) 返回值 有两个可能的返回值: 1. pdPASS 返回pdPASS 只会有一种情况,那就是数据被成功发送到队列中。 如果设定了阻塞超时时间(xTicksToWait,非0),在函数返回之前任务将被转移到阻塞态以等待队列空间有效,在超时到来前能够将数据成功写入到队列,函数则会返回pdPASS (意思就是:还没等超时时间到,数据写入成功,不然等到超时时间到了,数据才来的话,任务早已经超时返回了。)。 2. errQUEUE_FULL 如果由于队列已满而无法将数据写入, 则将返回errQUEUE_FULL。 如果设定了阻塞超时时间(xTicksToWait 非0),在函数返回之前任务将被转移到阻塞态以等待队列空间有效。但直到超时也没有其它任务或是中断服务例程读取队列而腾出空间,函数则会返回errQUEUE_FULL。(这是另一种情况) xQueueReceive()与xQueuePeek() API 函数 xQueueReceive()用于从队列中接收(读取)数据单元。接收到的单元同时会从队列中删除。 xQueuePeek()也是从从队列中接收数据单元,不同的是并不从队列中删出接收到的单元。xQueuePeek()从队列首接收到数据后,不会修改队列中的数据,也不会改变数据在队列中的存储顺序。 切记不要在中断服务例程中调用xQueueRceive()和xQueuePeek(),中断安全版本:xQueueReceiveFromISR()函数。 函数原型: portBASE_TYPE xQueueReceive( xQueueHandle xQueue, const void * pvBuffer, portTickType xTicksToWait ); portBASE_TYPE xQueuePeek ( xQueueHandle xQueue, const void * pvBuffer, portTickType xTicksToWait ); xQueue 被读队列的句柄。 这个句柄即是调用xQueueCreate()创建该队列时的返回值。 pvBuffer 接收缓存指针。其指向一段内存区域,用于接收从队列中拷贝来的数据。 数据单元的长度在创建队列时就已经被设定,所以该指针指向的内存区域大小应当足够保存一个数据单元。 xTicksToWait 阻塞超时时间。 (1)如果在接收时队列为空,则这个时间是任务处于阻塞状态以等待队列数据有效的最长等待时间。 (2)如果xTicksToWait 设为0,并且队列为空,则xQueueRecieve()与xQueuePeek()均会立即返回。阻塞时间是以系统心跳周期为单位的,所以绝对时间取决于系统心跳频率。常量portTICK_RATE_MS 可以用来把心跳时间单位转换为毫秒时间单位。 (3)如果把xTicksToWait 设置为portMAX_DELAY , 并且在FreeRTOSConig.h 中设定INCLUDE_vTaskSuspend 为1,那么阻塞等待将没有超时限制。 返回值 有两个可能的返回值: 1. pdPASS 只有一种情况会返回pdPASS,那就是成功地从队列中读到数据。 如果设定了阻塞超时时间(xTicksToWait 非0),在函数返回之前任务将被转移到阻塞态以等待队列数据有效, 在超时到来前能够从队列中成功读取数据,函数则会返回pdPASS。 2. errQUEUE_FULL 如果在读取时由于队列已空而没有读到任何数据,则将返回errQUEUE_FULL。 如果设定了阻塞超时时间(xTicksToWait 非0),在函数返回之前任务将被转移到阻塞态以等待队列数据有效。但直到超时也没有其它任务或是中断服务例程往队列中写入数据,函数则会返回errQUEUE_FULL。 uxQueueMessagesWaiting() API 函数 uxQueueMessagesWaiting()用于查询队列中当前有效数据单元个数。 切记不要在中断服务例程中调用uxQueueMessagesWaiting()。应当在中断服务中使用其中断安全版本uxQueueMessagesWaitingFromISR()。 函数原型: unsigned portBASE_TYPE uxQueueMessagesWaiting( xQueueHandle xQueue ); xQueue 被查询队列的句柄。这个句柄即是调用xQueueCreate()创建该队列时的返回值。 返回值 当前队列中保存的数据单元个数。返回0 表明队列为空。
例子10:
代码语言:javascript复制本例示范创建一个队列,由多个任务往队列中写数据,以及从队列中把数据读出。 这个队列创建出来保存long 型数据单元。往队列中写数据的任务没有设定阻塞超时时间,而读队列的任务设定了超时时间。 往队列中写数据的任务的优先级低于读队列任务的优先级。这意味着队列中永远不会保持超过一个的数据单元。因为一旦有数据被写入队列,读队列任务立即解除阻塞,抢占写队列任务,并从队列中接收数据,同时数据从队列中删除—队列再一次变为空队列。 写队列任务实现: 这个任务被创建了两个实例,一个不停地往队列中写数值100,而另一个实例不停地往队列中写入数值200。任务的入口参数被用来为每个实例传递各自的写入值。 这里:两个写任务是不停地写buffer的。除非时间片到,或者被高优先级任务抢占。 读队列任务实现: 读队列任务设定了100 毫秒的阻塞超时时间,所以会进入阻塞态以等待队列数据有效。一旦队列中数据单元有效,或者即使队列数据无效但等待时间超过100 毫秒,此任务将会解除阻塞。在本例中,将永远不会出现100 毫秒超时,因为有两个任务在不停地往队列中写数据。 注意:这里如果读任务不设置100ms阻塞超时时间,会发生什么呢? main()函数的实现。 其在启动调度器之前创建了一个队列和三个任务。尽管对任务的优先级的设计使得队列实际上在任何时候都不可能多于一个数据单元,本例代码还是创建了一个可以保存最多5 个long 型值的队列。 代码实现: 点击(此处)折叠或打开
#include "led.h"
#include "delay.h"
#include "sys.h"
#include "usart.h"
// FreeRTOS head file, add here.
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "list.h"
#include "portable.h"
#include "FreeRTOSConfig.h"
/* declare a queueHandle variable, using to save queue handler. */
xQueueHandle xQueue;
void vSendTask(void *pvParameters)
{
long valueToSend;
portBASE_TYPE status;
/* 该任务会被创建两个实例,所以写入队列的值通过任务入口参数传递 – 这种方式使得每个实例使用不同的
值。队列创建时指定其数据单元为long型,所以把入口参数强制转换为数据单元要求的类型 */
valueToSend = (long)pvParameters;
while(1)
{
/* 往队列发送数据
第一个参数是要写入的队列。队列在调度器启动之前就被创建了,所以先于此任务执行。
第二个参数是被发送数据的地址,本例中即变量lValueToSend的地址。
第三个参数是阻塞超时时间 – 当队列满时,任务转入阻塞状态以等待队列空间有效。本例中没有设定超
时时间,因为此队列决不会保持有超过一个数据单元的机会,所以也决不会满。
*/
status = xQueueSendToBack(xQueue, &valueToSend, 0);
if(status != pdPASS)
{
/* 发送操作由于队列满而无法完成 – 这必然存在错误,因为本例中的队列不可能满。 */
printf("could not send to the queue. rn");
}
/* 允许其它发送任务执行。 taskYIELD()通知调度器现在就切换到其它任务,而不必等到本任务的时间片耗尽 */
taskYIELD();
}
}
void vReceiveTask(void *pvParameters)
{
long lReceivedValue;
portBASE_TYPE status;
while(1)
{
/* 此调用会发现队列一直为空,因为本任务将立即删除刚写入队列的数据单元。 这里要问为什么??? */
if( uxQueueMessagesWaiting( xQueue ) != 0 )
{
printf( "Queue should have been empty! rn"); // 这句话不会得到执行,为什么??思考。。。。。
}
/* 从队列中接收数据
第一个参数是被读取的队列。队列在调度器启动之前就被创建了,所以先于此任务执行。
第二个参数是保存接收到的数据的缓冲区地址,本例中即变量lReceivedValue的地址。此变量类型与
队列数据单元类型相同,所以有足够的大小来存储接收到的数据。
第三个参数是阻塞超时时间 – 当队列空时,任务转入阻塞状态以等待队列数据有效。本例中常量
portTICK_RATE_MS用来将100毫秒绝对时间转换为以系统心跳为单位的时间值。
*/
status = xQueueReceive( xQueue, &lReceivedValue, 100 / portTICK_RATE_MS );
if( status == pdPASS )
{
printf( "Received = %ldrn", lReceivedValue );
}
else
{
/* 等待100ms也没有收到任何数据。
必然存在错误,因为发送任务在不停地往队列中写入数据 */
printf( "Could not receive from the queue.rn" );
}
}
}
int main(void)
{
// board initialize.
LED_Init();
uart_init(115200);
// create queue, can store 5 value which data type is long
xQueue = xQueueCreate(5, sizeof(long));
if(xQueue != NULL) // adjust the return value, to confirm whether create queue successful.
{
// Create two write queue task, priority = 1;
xTaskCreate(vSendTask, "SendTask1", configMINIMAL_STACK_SIZE, (void *)100, 1, NULL);
xTaskCreate(vSendTask, "SendTask2", configMINIMAL_STACK_SIZE, (void *)200, 1, NULL);
// create one read queue task, priority = 2;
xTaskCreate(vReceiveTask, "RecTask", configMINIMAL_STACK_SIZE, NULL, 2, NULL);
// start scheduler now
vTaskStartScheduler();
}
else
{
// queue create unsuccessful here. add your code.
}
return 0;
}
写队列任务在每次循环中都调用taskYIELD()。taskYIELD()通知调度器立即进行任务切换,而不必等到当前任务的时间片耗尽。某个任务调用taskYIELD()等效于其自愿放弃运行态。由于本例中两个写队列任务具有相同的任务优先级,所以一旦其中一个任务调用了taskYIELD(),另一个任务将会得到执行 — 调用taskYIELD()的任务转移到就绪态,同时另一个任务进入运行态。这样就可以使得这两个任务轮翻地往队列发送数据。
执行流程:
上面有一个思考问题: 在接收queue任务中:
代码语言:javascript复制 if( uxQueueMessagesWaiting( xQueue ) != 0 )
{
printf( "Queue should have been empty! rn"); // 这句话不会得到执行,为什么??思考。。。。。
}
注意到:接收任务是高优先的任务,首先得到运行,第一次运行的时候,queue还是空的,这句话不成立,不会执行; 然后,执行到读队列的函数,由于队列为空,则任务由运行态进入阻塞态,这时候退出CPU的占有权限,写队列的任务得到执行,一旦有写队列任务写进一个数据到队列中,将会唤醒读队列,因为读队列任务优先级高啊,读队列任务得到运行,读取队列,并且删除队列中的元素,导致队列继续为空,循环到上面的语句的时候,还是条件不成立,不会打印这句字符串。以此循环下去,该句话永远得不到执行。 上面的执行流程的问号: 为什么接收队列运行完了,还要执行一小块发送的任务呢?
代码语言:javascript复制void vSendTask(void *pvParameters)
{
long valueToSend;
portBASE_TYPE status;
valueToSend = (long)pvParameters;
while(1)
{
printf("enter rn"); // 这里加上打印语句
status = xQueueSendToBack(xQueue, &valueToSend, 0);
printf("exit rn"); // 这里加上打印语句
// 一旦这行到这里,队列里面有数据了,则将会唤醒读队列任务,从这里切换出去,知道读任务进入阻塞态,该任务继续占有CPU而执行未完成的代码。优先级高就是牛逼啊。。
----
if(status != pdPASS)
{
printf("could not send to the queue. rn");
}
taskYIELD();
}
}
这个问题可以测试一下,在status = xQueueSendToBack(xQueue, &valueToSend, 0); 的前面和后面各加上一句打印语句,就可以看出执行顺序了。 打印结果: