基于 FreeRTOS 的应用程序由一组独立的任务构成——每个任务都是具有独立权限的小程序。这些独立的任务之间很可能会通过相互通信以提供有用的系统功能。FreeRTOS 中所有的通信与同步机制都是基于队列实现的。
通常情况下,队列被作为 FIFO(先进先出)使用,即数据由队列尾写入,从队列首读出。当然,由队列首写入也是可能的。
往队列写入数据是通过字节拷贝把数据复制存储到队列中;从队列读出数据使得把队列中的数据拷贝删除。
队列的写入和读取
使用队列
队列在使用前必须先被创建。
队列由声明为 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 阻塞超时时间。如果在发送时队列已满,这个时间即是任务处于
阻塞态等待队列空间有效的最长等待时间。
如 果 xTicksToWait 设 为 0 ,并且队列已满,则
xQueueSendToFront()与 xQueueSendToBack()均会立即返回。
阻塞时间是以系统心跳周期为单位的,所以绝对时间取决于系统
心跳频率。常量 portTICK_RATE_MS 可以用来把心跳时间单位
转换为毫秒时间单位。
如果把 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()。中断安全版本的替代 API 函数 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 阻塞超时时间。如果在接收时队列为空,则这个时间是任务处于
阻塞状态以等待队列数据有效的最长等待时间。
如果 xTicksToWait 设为 0,并且队列为空,则 xQueueRecieve()
与 xQueuePeek()均会立即返回。
阻塞时间是以系统心跳周期为单位的,所以绝对时间取决于系统
心跳频率。常量 portTICK_RATE_MS 可以用来把心跳时间单位转
换为毫秒时间单位。
如果把 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 表明队列为空。
程序实例
static void vReceiverTask( void *pvParameters );
static void vSenderTask( void *pvParameters ) ;
xQueueHandle xQueue;
int main( void )
{
/* 创建的队列用于保存最多5个值,每个数据单元都有足够的空间来存储一个long型变量 */
xQueue = xQueueCreate( 5, sizeof( long ) );
if( xQueue != NULL )
{
/* 创建两个写队列任务实例,任务入口参数用于传递发送到队列的值。所以一个实例不停地往队列发送
100,而另一个任务实例不停地往队列发送200。两个任务的优先级都设为1。 */
xTaskCreate( vSenderTask, "Sender1", 1000, ( void * ) 100, 1, NULL );
xTaskCreate( vSenderTask, "Sender2", 1000, ( void * ) 200, 1, NULL );
/* 创建一个读队列任务实例。其优先级设为2,高于写任务优先级 */
xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 2, NULL );
/* 启动调度器,任务开始执行 */
vTaskStartScheduler();
}
else
{
/* 队列创建失败*/
}
/* 如果一切正常,main()函数不应该会执行到这里。但如果执行到这里,很可能是内存堆空间不足导致空闲
任务无法创建。第五章有讲述更多关于内存管理方面的信息 */
for( ;; );
}
static void vSenderTask( void *pvParameters )
{
long lValueToSend;
portBASE_TYPE xStatus;
/* 该任务会被创建两个实例,所以写入队列的值通过任务入口参数传递 – 这种方式使得每个实例使用不同的
值。队列创建时指定其数据单元为long型,所以把入口参数强制转换为数据单元要求的类型 */
lValueToSend = ( long ) pvParameters;
/* 和大多数任务一样,本任务也处于一个死循环中 */
for( ;; )
{
/* 往队列发送数据
第一个参数是要写入的队列。队列在调度器启动之前就被创建了,所以先于此任务执行。
第二个参数是被发送数据的地址,本例中即变量lValueToSend的地址。
第三个参数是阻塞超时时间 – 当队列满时,任务转入阻塞状态以等待队列空间有效。本例中没有设定超
时时间,因为此队列决不会保持有超过一个数据单元的机会,所以也决不会满。
*/
xStatus = xQueueSendToBack( xQueue, &lValueToSend, 0 );
if( xStatus != pdPASS )
{
/* 发送操作由于队列满而无法完成 – 这必然存在错误,因为本例中的队列不可能满。 */
vPrintString( "Could not send to the queue.\r\n" );
}
/* 允许其它发送任务执行。 taskYIELD()通知调度器现在就切换到其它任务,而不必等到本任务的时
间片耗尽 */
taskYIELD();
}
}
static void vReceiverTask( void *pvParameters )
{
/* 声明变量,用于保存从队列中接收到的数据。 */
long lReceivedValue;
portBASE_TYPE xStatus;
const portTickType xTicksToWait = 100 / portTICK_RATE_MS;
/* 本任务依然处于死循环中。 */
for( ;; )
{
/* 此调用会发现队列一直为空,因为本任务将立即删除刚写入队列的数据单元。 */
/*读的优先级高于写的的优先级,所以这个队列会一直为空,当读队列发现没用的东西可以读的时候,就会去调用写*/
if( uxQueueMessagesWaiting( xQueue ) != 0 )
{
vPrintString( "Queue should have been empty!\r\n" );
}
/* 从队列中接收数据
第一个参数是被读取的队列。队列在调度器启动之前就被创建了,所以先于此任务执行。
第二个参数是保存接收到的数据的缓冲区地址,本例中即变量lReceivedValue的地址。此变量类型与
队列数据单元类型相同,所以有足够的大小来存储接收到的数据。
第三个参数是阻塞超时时间 – 当队列空时,任务转入阻塞状态以等待队列数据有效。本例中常量
portTICK_RATE_MS用来将100毫秒绝对时间转换为以系统心跳为单位的时间值。
*/
xStatus = xQueueReceive( xQueue, &lReceivedValue, xTicksToWait );
if( xStatus == pdPASS )
{
/* 成功读出数据,打印出来。 */
vPrintStringAndNumber( "Received = ", lReceivedValue );
}
else
{
/* 等待100ms也没有收到任何数据。
必然存在错误,因为发送任务在不停地往队列中写入数据 */
vPrintString( "Could not receive from the queue.\r\n" );
}
}
}
写队列任务在每次循环中都调用 taskYIELD()。taskYIELD()通知调度器立即进行任务切换,而不必等到当前任务的时间片耗尽。某个任务调用 taskYIELD()等效于其自愿放弃运行态。由于本例中两个写队列任务具有相同的任务优先级,所以一旦其中一个任务调用了 taskYIELD(),另一个任务将会得到执行 — 调用 taskYIELD()的任务转移到就绪态,同时另一个任务进入运行态。这样就可以使得这两个任务轮翻地往队列发送数据。
这个图详细写到了发送过程
结果:
使用队列传递复合数据类型可以通过变量识别传输过来的是什么数据
一个任务从单个队列中接收来自多个发送源的数据是经常的事。通常接收方收到数据后,需要知道数据的来源,并根据数据的来源决定下一步如何处理。一个简单的方式就是利用队列传递结构体,结构体成员中就包含了数据信息和来源信息。
• 创建一个队列用于保存类型为 xData 的结构体数据单元。结构体成员包括了一个数
据值和表示数据含义的编码,两者合为一个消息可以一次性发送到队列。
• 中央控制任务用于完成主要的系统功能。其必须对队列中传来的输入和其它系统状
态的改变作出响应。
• CAN 总线任务用于封装 CAN 总线的接口功能。当 CAN 总线任务收到并解码一个消
息后,其将把解码后的消息放到 xData 结构体中发往控制任务。结构体的 iMeaning
成员用于让中央控制任务知道这个数据是用来干什么的 — 从图中的描述可以看
出,这个数据表示电机速度。结构体的 iValue 成员可以让中央控制任务知道电机的
实际速度值。
• 人机接口(HMI)任务用于对所有的人机接口功能进行封装。设备操作员可能通过各种
方式进行命令输入和参数查询,人机接口任务需要对这些操作进行检测并解析。当
接收到一个新的命令后,人机接口任务通过 xData 结构将命令发送到中央控制任务。
结构体的 iMeaning 成员用于让中央控制任务知道这个数据是用来干什么的 — 从
图中的描述可以看出,这个数据表示一个新的参数设置。结构体的 iValue 成员可以
让中央控制任务知道具体的设置值。
demo
/* 定义队列传递的结构类型。 */
typedef struct
{
unsigned char ucValue;
unsigned char ucSource;
} xData;
/* 声明两个xData类型的变量,通过队列进行传递。 */
static const xData xStructsToSend[ 2 ] =
{
{ 100, mainSENDER_1 }, /* Used by Sender1. */
{ 200, mainSENDER_2 } /* Used by Sender2. */
};
xQueueHandle xQueue;
int main( void )
{
/* 创建队列用于保存最多3个xData类型的数据单元。 */
xQueue = xQueueCreate( 3, sizeof( xData ) );
if( xQueue != NULL )
{
/* 为写队列任务创建2个实例。 The
任务入口参数用于传递发送到队列中的数据。因此其中一个任务往队列中一直写入
xStructsToSend[0],而另一个则往队列中一直写入xStructsToSend[1]。这两个任务的优先级都设为2,高于读队列任务的优先级
*/
xTaskCreate( vSenderTask, "Sender1", 1000, &( xStructsToSend[ 0 ] ), 2, NULL );
xTaskCreate( vSenderTask, "Sender2", 1000, &( xStructsToSend[ 1 ] ), 2, NULL );
/* 创建读队列任务。读队列任务优先级设为1,低于写队列任务的优先级。 */
xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, NULL );
/* 启动调度器,创建的任务得到执行。 */
vTaskStartScheduler();
}
else
{
/* 创建队列失败。 */
}
/* 如果一切正常,main()函数不应该会执行到这里。但如果执行到这里,很可能是内存堆空间不足导致空闲任务无法创建。第五章将提供更多关于内存管理方面的信息
*/
for( ;; );
}
static void vSenderTask( void *pvParameters )
{
portBASE_TYPE xStatus;
const portTickType xTicksToWait = 100 / portTICK_RATE_MS;
/* As per most tasks, this task is implemented within an infinite loop. */
for( ;; )
{
/* 往队列发送数据
第一个参数是要写入的队列。队列在调度器启动之前就被创建了,所以先于此任务执行。
第二个参数是被发送数据的地址,本例中即变量lValueToSend的地址。
第三个参数是阻塞超时时间 – 当队列满时,任务转入阻塞状态以等待队列空间有效。本例中没有设定超时时间,因为此队列决不会保持有超过一个数据单元的机会,所以也决不会满。
*/
xStatus = xQueueSendToBack( xQueue, pvParameters, xTicksToWait );
if( xStatus != pdPASS )
{
vPrintString( "Could not send to the queue.\r\n" );
}
}
}
static void vReceiverTask( void *pvParameters )
{
/* 声明结构体变量以保存从队列中读出的数据单元 */
xData xReceivedStructure;
portBASE_TYPE xStatus;
/* This task is also defined within an infinite loop. */
for( ;; )
{
/* 读队列任务的优先级最低,所以其只可能在写队列任务阻塞时得到执行。而写队列任务只会在队列写
满时才会进入阻塞态,所以读队列任务执行时队列肯定已满。所以队列中数据单元的个数应当等于队列的
深度 – 本例中队列深度为3 ,里面的元素会在1-2-1,2-1-2,1-2-1,这样循环着*/
if( uxQueueMessagesWaiting( xQueue ) != 3 )
{
vPrintString( "Queue should have been full!\r\n" );
}
/* Receive from the queue.
第二个参数是存放接收数据的缓存空间。本例简单地采用一个具有足够空间大小的变量的地址。
第三个参数是阻塞超时时间 – 本例不需要指定超时时间,因为读队列任会只会在队列满时才会得到执行,
故而不会因队列空而阻塞
*/
xStatus = xQueueReceive( xQueue, &xReceivedStructure, 0 );
if( xStatus == pdPASS )
{
/* 数据成功读出,打印输出数值及数据来源。 */
if( xReceivedStructure.ucSource == mainSENDER_1 )
{
vPrintStringAndNumber( "From Sender 1 = ", xReceivedStructure.ucValue );
}
else
{
vPrintStringAndNumber( "From Sender 2 = ", xReceivedStructure.ucValue );
}
}
else
{
/* 没有读到任何数据。这一定是发生了错误,因为此任务只支在队列满时才会得到执行 */
vPrintString( "Could not receive from the queue.\r\n" );
}
}
}
t1 写队列任务 1 得到执行,并往队列中发送数据.
t2 写队列任务 1 切换到写队列任务 2。写队列任务 2 往队列中发送数据。
t3 写队列任务 2 又切回写队列任务 1。写队列任务 1 再次将数据写入队列,导致
队列满。
t4 写队列任务 1 切换到写队列任务 2。
t5 写队列任务 2 试图往队列中写入数据。但由于队列已满,所以写队列任务 2 转
入阻塞态以等待队列空间有效。这使得写队列任务 1 再次得到执行。
t6 写队列任务 1 试图往队列中写入数据。但由于队列已满,所以写队列任务 1 也
转入阻塞态以等待队列空间有效。此时写队列任务均处于阻塞态,这才使得被
赋予最低优先级的读队列任务得以执行。
t7 读队列任务从队列读取数据,并把读出的数据单元从队列中移出。一旦队列空
间有效,写队列任务 2 立即解除阻塞,并且因为其具有更高优先级,所以抢占
读队列任务。写队列任务 2 又往队列中写入数据,填充到刚刚被读队列任务腾
出的存储空间,使得队列再一次变满。写队列发送完数据后便调用 taskYIELD(),
但写队列任务 1 尚还处理阻塞态,所以写队列任务 2 并未被切换出去,继续执
行。
t8 写队列任务 2 试图往队列中写入数据。但队列已满,所以写队列任务 2 转入阻
塞态。两个写队列任务再一次同时处于阻塞态,所以读队列任务得以执行。
t9 读队列任务从队列读取数据,并把读出的数据单元从队列中移出。一旦队列空
间有效,写队列任务 1 立即解除阻塞,并且因为其具有更高优先级,所以抢占
读队列任务。写队列任务 1 又往队列中写入数据,填充到刚刚被读队列任务腾
出的存储空间,使得队列再一次变满。写队列发送完数据后便调用 taskYIELD(),
但写队列任务 2 尚还处理阻塞态,所以写队列任务 1 并未被切换出去,继续执
行。写队列任务 1 试图往队列中写入数据。但队列已满,所以写队列任务 1 转
入阻塞态。
两个写队列任务再一次同时处于阻塞态,所以读队列任务得以执行
工作于大型数据单元
如果队列存储的数据单元尺寸较大,那最好是利用队列来传递数据的指针而不是对
数据本身在队列上一字节一字节地拷贝进或拷贝出。传递指针无论是在处理速度上还是
内存空间利用上都更有效。但是,当你利用队列传递指针时,一定要十分小心地做到以
下两点:
- 指针指向的内存空间的所有权必须明确
当任务间通过指针共享内存时,应该从根本上保证所不会有任意两个任务同时
修改共享内存中的数据,或是以其它行为方式使得共享内存数据无效或产生一致性
问题。原则上,共享内存在其指针发送到队列之前,其内容只允许被发送任务访问;
共享内存指针从队列中被读出之后,其内容亦只允许被接收任务访问。 - 指针指向的内存空间必须有效
如果指针指向的内存空间是动态分配的,只应该有一个任务负责对其进行内存
释放。当这段内存空间被释放之后,就不应该有任何一个任务再访问这段空间。
切忌用指针访问任务栈上分配的空间。因为当栈帧发生改变后,栈上的数据将不再
有效。