我们介绍了FreeRTOS的任务管理并与主流的几个系统进行了对比。在本讲中,我们将介绍一个新的主题--队列管理(Queue Management)。在以往的教程中,我们创建的任务都是相对独立的,无法互相通讯交换数据,而队列提供了一种任务间或者任务和中断间的通讯机制。
什么是队列
队列是一种数据结构,可以包含一组固定大小的数据。在创建队列的同时,队列的长度和所包含数据类型的大小就确认下来了。栈(Stack)也是一种数据结构,栈和队列的区别在于栈是后进先出(Last In First Out),而队列是先进先出(First In First Out)。先进先出表示的是每次写入队列的数据会放在队列的尾部,先写入的数据会先被读取,符合人类的直觉思维。
队列有两种实现方式
- 复制队列(Queue by copy) 表示写入队列的数据都被完整复制到队列中了
- 引用队列(Queue by reference)表示写入队列的是要写入数据的引用并不是数据本身
FreeRTOS采用是复制队列的实现方式,有如下优势:
- 有些栈变量是在函数运行结束后会被销毁,采用引用队列的话引用会失效
- 发送数据的函数可以重复使用变量,采用引用队列的话每发送一个数据需要一个新的变量
- 发送队列数据和接受队列数据的函数是没有耦合的,互相不影响
一个队列可以有多个写入数据的任务和多个读取数据的任务。当一个任务试图从队列读取数据的时候,它可以设置一个阻塞时间(block time)。这是当队列数据为空时,任务会进入阻塞状态的时间。当有数据在队列或者到达阻塞时间的时候,任务都会进入就绪状态。如果有多个任务同时在阻塞状态等待队列数据,优先级高的任务会在数据到达时进入就绪状态;在优先级相同的时候,等待时间长的任务会进入就绪状态。同理可以推及多个任务写入数据时候的运行状态。FreeRTOS还可以设置让任务等待一组队列(Sets of Queues)的任意队列的数据。
如何使用队列
下面将介绍创建和使用队列需要用到的几个函数
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize )
xQueueCreate()是创建队列用到的函数。函数的返回值是QueueHandle_t具柄类型,表示的是对所创建队列的一个引用。FreeRTOS从FreeRTOS的堆中指定一些内存空间给队列使用。如果堆中没有足够空间给队列使用的话函数的返回值会是NULL。
函数的几个参数介绍如下
- uxQueueLength 队列包含数据的最大长度
- uxItemSize 每个数据占用的字节大小
BaseType_t xQueueSend( QueueHandle_t xQueue, const void * pvItemToQueue,
TickType_t xTicksToWait )
xQueueSend()函数用于将数据发送到队列(具体一点就是队列的尾部)。如果要在中断程序调用的话需要使用xQueueSendFromISR()函数。
函数的几个参数介绍如下
- xQueue 队列的具柄,来自于xQueueCreate()的返回值
- pvItemToQueue 所发送数据的引用,然后这些数据会被复制到队列中
- xTicksToWait 队列如果满时发送任务的阻塞时间(block time),上文已经介绍过,可以通过pdMS_TO_TICKS()把时间转换成节拍数。如果设置为portMAX_DELAY的话任务将永远等待下去(需要FreeRTOSConfig.h头文件中INCLUDE_vTaskSuspend参数设置为1)
- 返回值 发送数据成功时返回pdPASS,失败时返回errQUEUE_FULL
下面两个函数用于明确指定发送数据到队列的头部还是尾部。
BaseType_t xQueueSendToFront( QueueHandle_t xQueue, const void * pvItemToQueue,
TickType_t xTicksToWait )
BaseType_t xQueueSendToBack( QueueHandle_t xQueue, const void * pvItemToQueue,
TickType_t xTicksToWait )
xQueueSend()函数和xQueueSendToBack()函数本质上是一样的,可以在下图xQueueSend()的宏定义中queueSEND_TO_BACK看到
BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer,
TickType_t xTicksToWait )
xQueueReceive()函数用于从队列中读取数据,同时读取到的数据会被从队列中移除。
函数的几个参数介绍如下
- xQueue 队列的具柄,来自于xQueueCreate()的返回值
- pvBuffer 指向内存空间的一个引用,读取的数据会被复制到这片内存
- xTicksToWait 队列如果空时接送任务的阻塞时间(block time),上文已经介绍过,可以通过pdMS_TO_TICKS()把时间转换成节拍数。如果设置为portMAX_DELAY的话任务将永远等待下去(需要FreeRTOSConfig.h头文件中INCLUDE_vTaskSuspend参数设置为1)
- 返回值 接收数据成功时返回pdPASS,失败时返回errQUEUE_EMPTY
UBaseType_t uxQueueMessagesWaiting( QueueHandle_t xQueue )
uxQueueMessagesWaiting()函数用于获得队列中数据的数量
队列的实例应用
当时我在第五章里说起定义全局变量其实不太安全。因为任何函数都可以随意修改全局变量的变量的值,对于程序的安全性和健壮性不太好。这里我打算把Task1和Task2的程序修改下,通过引入一个全局的队列xTemperatureQueue来使两个任务交换温度数据。
QueueHandle_t xTemperatureQueue;//定义一个全局队列
同时修改Task1和Task2,请参考程序的注释
void vTask1(void const * argument)//温度传感器数据获取任务
{
BaseType_t xStatus;
//把时间转换成
const TickType_t xTicksToWait = pdMS_TO_TICKS( 1000 );
int test = 0;
int temp = 0;
for(;;)
{
//模拟温度传感器数据
temp = 20+test%20;
//通过发送函数把温度发送到队列xTemperatureQueue中
xStatus = xQueueSend( xTemperatureQueue, &temp, xTicksToWait );
if( xStatus != pdPASS )
{
//如果发送数据失败在这里进行错误处理
}
test++;
if(test>30000)
{
test=0;
}
//模拟温度传感器的采样频率
osDelay(1000);
}
}
void vTask2(void const * argument)//数据处理以及屏幕显示
{
char text[]= "Temperature:";
char tempText[10];
BaseType_t xStatus;
int ReceivedValue = 0;
const TickType_t xTicksToWait = pdMS_TO_TICKS( 100 );
TM_LCD_SetFont(&TM_Font_11x18);
TM_FONT_GetStringSize(text, &FontSize, &TM_Font_11x18);
TM_LCD_Fill(COLOR_RED);
for(;;)
{
//通过接收函数从xTemperatureQueue队列中获取温度数据
xStatus = xQueueReceive( xTemperatureQueue, &ReceivedValue, xTicksToWait );
if(xStatus == pdPASS )
{
//在显示屏上显示数据
TM_LCD_SetXY((TM_LCD_GetWidth() - FontSize.Width) / 2, FontSize.Height * 5);
TM_LCD_Puts(text);
sprintf(tempText, "%d", ReceivedValue);
TM_LCD_SetXY((TM_LCD_GetWidth() - FontSize.Width) / 2, FontSize.Height * 7);
TM_LCD_Puts(strncat(tempText," Degree",7));
}
else
{
//如果接收数据失败在这里进行错误处理
}
}
}
同时在main()函数中建立队列并启动内核调度器运行程序
int main(void)
{
......
xTemperatureQueue = xQueueCreate( 10, sizeof( int32_t ) );
xTaskCreate( vTask1, "get sensor data", 200, NULL, 3, NULL );
xTaskCreate( vTask2, "show sensor data", 200, NULL, 2, NULL );
xTaskCreate( vTask3, "get user input", 200, NULL, 1, NULL );
/* Start scheduler */
vTaskStartScheduler();
while (1)
{
}
}
程序的运行效果和第五讲的运行效果是一致的,演示效果视频可以参考第五讲。
但通过使用队列任务间可以更加安全,方便地交互数据。通过队列不仅仅可以传递基本类型数据,也可以传递结构体。比如一个结构体里面可以包含发送命令的来源,要执行的命令,执行命令的附加参数等。接收函数通过接收结构体里的这些具体数据可以实现更强大的功能。
下面这个图展示一种复杂场景中的应用例子,在这个场景中不同的任务比如CAN总线任务,显示处理任务等把数据包装到结构体中并发送结构体数据到队列。结构体数据包含数据的类别eDataID和具体数据的值IDataValue。 Controller任务从队列中获取结构体数据,并对不同类别的数据进行相应的处理,可以通过组合实现复杂的功能。
队列的错误处理要重视,防止队列因为数据没有被及时处理使得队列为满造成后面的数据无法写入队列中,从而影响系统的性能。