第20章 FreeRTOS消息队列
本章节为大家讲解FreeRTOS的一个重要的通信机制----消息队列,初学者要熟练掌握,因为消息队列在实际项目中应用较多。
本章教程配套的例子含Cortex-M3内核的STM32F103和Cortex-M4内核的STM32F407以及F429。
20.1 消息队列
20.2 消息队列API函数
20.3 实验例程说明(任务间通信)
20.4 实验例程说明(中断方式通信)
20.5 总结
20.1 消息队列
20.1.1 消息队列的概念及其作用
消息队列就是通过RTOS内核提供的服务,任务或中断服务子程序可以将一个消息(注意,FreeRTOS消息队列传递的是实际数据,并不是数据地址,RTX,uCOS-II和uCOS-III是传递的地址)放入到队列。同样,一个或者多个任务可以通过RTOS内核服务从队列中得到消息。通常,先进入消息队列的消息先传给任务,也就是说,任务先得到的是最先进入到消息队列的消息,即先进先出的原则(FIFO),FreeRTOS的消息队列支持FIFO和LIFO两种数据存取方式。
也许有不理解的初学者会问采用消息队列多麻烦,搞个全局数组不是更简单,其实不然。在裸机编程时,使用全局数组的确比较方便,但是在加上RTOS后就是另一种情况了。相比消息队列,使用全局数组主要有如下四个问题:
u 使用消息队列可以让RTOS内核有效地管理任务,而全局数组是无法做到的,任务的超时等机制需要用户自己去实现。
u 使用了全局数组就要防止多任务的访问冲突,而使用消息队列则处理好了这个问题,用户无需担心。
u 使用消息队列可以有效地解决中断服务程序与任务之间消息传递的问题。
u FIFO机制更有利于数据的处理。
20.1.2 FreeRTOS任务间消息队列的实现
任务间消息队列的实现是指各个任务之间使用消息队列实现任务间的通信。下面我们通过如下的框图来说明一下FreeRTOS消息队列的实现,让大家有一个形象的认识。
运行条件:
u 创建消息队列,可以存放10个消息。
u 创建2个任务Task1和Task2,任务Task1向消息队列放数据,任务Task2从消息队列取数据。
u FreeRTOS的消息存取采用FIFO方式。
运行过程主要有以下两种情况:
u 任务Task1 向消息队列放数据,任务Task2从消息队列取数据,如果放数据的速度快于取数据的速度,那么会出现消息队列存放满的情况,FreeRTOS的消息存放函数xQueueSend支持超时等待,用户可以设置超时等待,直到有空间可以存放消息或者设置的超时时间溢出。
u 任务Task1 向消息队列放数据,任务Task2从消息队列取数据,如果放数据的速度慢于取数据的速度,那么会出现消息队列为空的情况,FreeRTOS的消息获取函数xQueueReceive支持超时等待,用户可以设置超时等待,直到消息队列中有消息或者设置的超时时间溢出。
上面就是一个简单的FreeRTOS任务间消息队列通信过程,FIFO方式数据存取过程的动态演示看官方地址:http://www.freertos.org/Embedded-RTOS-Queues.html 里面的GIF图片。
20.1.3 FreeRTOS中断方式消息队列的实现
FreeRTOS中断方式消息队列的实现是指中断函数和FreeRTOS任务之间使用消息队列。下面我们通过如下的框图来说明一下FreeRTOS消息队列的实现,让大家有一个形象的认识。

运行条件:
u 创建消息队列,可以存放10个消息。
u 创建1个任务Task1和一个串口接收中断。
u FreeRTOS的消息存取采用FIFO方式。
运行过程主要有以下两种情况:
u 中断服务程序向消息队列放数据,任务Task1从消息队列取数据,如果放数据的速度快于取数据的速度,那么会出现消息队列存放满的情况。由于中断服务程序里面的消息队列发送函数xQueueSendFromISR不支持超时设置,所以发送前要通过函数xQueueIsQueueFullFromISR检测消息队列是否满。
u 中断服务程序向消息队列放数据,任务Task1从消息队列取数据,如果放数据的速度慢于取数据的速度,那么会出现消息队列存为空的情况。在FreeRTOS的任务中可以通过函数xQueueReceive获取消息,因为此函数可以设置超时等待,直到消息队列中有消息存放或者设置的超时时间溢出。
上面就是一个简单的FreeRTOS中断方式消息队列通信过程。实际应用中,中断方式的消息机制要注意以下四个问题:
u 中断函数的执行时间越短越好,防止其它低于这个中断优先级的异常不能得到及时响应。
u 实际应用中,建议不要在中断中实现消息处理,用户可以在中断服务程序里面发送消息通知任务,在任务中实现消息处理,这样可以有效地保证中断服务程序的实时响应。同时此任务也需要设置为高优先级,以便退出中断函数后任务可以得到及时执行。
u 中断服务程序中一定要调用专用于中断的消息队列函数,即以FromISR结尾的函数。
u 在操作系统中实现中断服务程序与裸机编程的区别。
l 如果FreeRTOS工程的中断函数中没有调用FreeRTOS的消息队列API函数,与裸机编程是一样的。
l 如果FreeRTOS工程的中断函数中调用了FreeRTOS的消息队列的API函数,退出的时候要检测是否有高优先级任务就绪,如果有就绪的,需要在退出中断后进行任务切换,这点与裸机编程稍有区别,详见20.4小节实验例程说明(中断方式):
l 另外强烈推荐用户将Cortex-M3内核的STM32F103和Cortex-M4内核的STM32F407,F429的NVIC优先级分组设置为4,即:NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);这样中断优先级的管理将非常方便。
l 用户要在FreeRTOS多任务开启前就设置好优先级分组,一旦设置好切记不可再修改。
20.2消息队列API函数
使用如下23个函数可以实现FreeRTOS的消息队列:
(1) xQueueCreateStatic()
(2) vQueueDelete()
(3) xQueueSend()
(4) xQueueSendFromISR()
(5) xQueueSendToBack()
(6) xQueueSendToBackFromISR()
(7) xQueueSendToFront()
(8) xQueueSendToFrontFromISR()
(9) xQueueReceive()
(10) xQueueReceiveFromISR()
(11) uxQueueMessagesWaiting()
(12) uxQueueMessagesWaitingFromISR()
(13) uxQueueSpacesAvailable()
(14) xQueueReset()
(15) xQueueOverwrite()
(16) xQueueOverwriteFromISR()
(17) xQueuePeek()
(18) xQueuePeekFromISR()
(19) vQueueAddToRegistry()
(20) vQueueUnregisterQueue()
(21) pcQueueGetName()
(22) xQueueIsQueueFullFromISR()
(23) xQueueIsQueueEmptyFromISR()
关于这23个函数的讲解及其使用方法可以看FreeRTOS在线版手册:
这里我们重点的说以下4个函数:
(1) xQueueCreate ()
(2) xQueueSend ()
(3) xQueueSendFromISR ()
(4) xQueueReceive ()
因为本章节配套的例子使用的是这4个函数。
20.2.1 函数xQueueCreate
函数原型:
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength,
UBaseType_t uxItemSize );
函数描述:
函数xQueueCreate用于创建消息队列。
u 第1个参数是消息队列支持的消息个数。
u 第2个参数是每个消息的大小,单位字节。
u 返回值,如果创建成功会返回消息队列的句柄,如果由于FreeRTOSConfig.h文件中heap大小不足,无法为此消息队列提供所需的空间会返回NULL。
使用这个函数要注意以下问题:
1. FreeRTOS的消息传递是数据的复制,而不是传递的数据地址,这点要特别注意。每一次传递都是uxItemSize个字节。
使用举例:
static QueueHandle_t xQueue1 = NULL;
static QueueHandle_t xQueue2 = NULL;
static void AppObjCreate (void)
{
xQueue1 = xQueueCreate(10, sizeof(uint8_t));
if( xQueue1 == 0 )
{
}
xQueue2 = xQueueCreate(10, sizeof(struct Msg *));
if( xQueue2 == 0 )
{
}
}
20.2.2函数xQueueSend
函数原型:
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait
);
函数描述:
函数xQueueSend用于任务中消息发送。
u 第1个参数是消息队列句柄。
u 第2个参数要传递数据地址,每次发送都是将消息队列创建函数xQueueCreate所指定的单个消息大小复制到消息队列空间中。
u 第3个参数是当消息队列已经满时,等待消息队列有空间时的最大等待时间,单位系统时钟节拍。
u 返回值,如果消息成功发送返回pdTRUE,否则返回errQUEUE_FULL。
使用这个函数要注意以下问题:
1. FreeRTOS的消息传递是数据的复制,而不是传递的数据地址。
2. 此函数是用于任务代码中调用的,故不可以在中断服务程序中调用此函数,中断服务程序中使用的是
xQueueSendFromISR。
3. 如果消息队列已经满且第三个参数为0,那么此函数会立即返回。
4. 如果用户将FreeRTOSConfig.h文件中的宏定义INCLUDE_vTaskSuspend配置为1且第三个参数配置为portMAX_DELAY,那么此发送函数会永久等待直到消息队列有空间可以使用。
5. 消息队列还有两个函数xQueueSendToBack和xQueueSendToFront,函数xQueueSendToBack实现的是FIFO方式的存取,函数xQueueSendToFront实现的是LIFO方式的读写。我们这里说的函数xQueueSend等效于xQueueSendToBack,即实现的是FIFO方式的存取。
使用举例:
static QueueHandle_t xQueue1 = NULL;
static QueueHandle_t xQueue2 = NULL;
typedef struct Msg
{
uint8_t ucMessageID;
uint16_t usData[2];
uint32_t ulData[2];
}MSG_T;
MSG_T g_tMsg;
static void vTaskTaskUserIF(void *pvParameters)
{
MSG_T *ptMsg;
uint8_t ucCount = 0;
uint8_t ucKeyCode;
ptMsg = &g_tMsg;
ptMsg->ucMessageID = 0;
ptMsg->ulData[0] = 0;
ptMsg->usData[0] = 0;
while(1)
{
ucKeyCode = bsp_GetKey();
if (ucKeyCode != KEY_NONE)
{
switch (ucKeyCode)
{
case KEY_DOWN_K2:
ucCount++;
if( xQueueSend(xQueue1,
(void *) &ucCount,
(TickType_t)10) != pdPASS )
{
printf("K2键按下,向xQueue1发送数据失败,即使等待了10个时钟节拍\r\n");
}
else
{
printf("K2键按下,向xQueue1发送数据成功\r\n");
}
break;
case KEY_DOWN_K3:
ptMsg->ucMessageID++;
ptMsg->ulData[0]++;;
ptMsg->usData[0]++;
if(xQueueSend(xQueue2,
(void *) &ptMsg,
(TickType_t)10) != pdPASS )
{
printf("K3键按下,向xQueue2发送数据失败,即使等待了10个时钟节拍\r\n");
}
else
{
printf("K3键按下,向xQueue2发送数据成功\r\n");
}
default:
break;
}
}
vTaskDelay(20);
}
}
20.2.3函数xQueueSendFromISR
函数原型:
BaseType_t xQueueSendFromISR
(
QueueHandle_t xQueue, /* 消息队列句柄 */
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
函数描述:
函数xQueueSendFromISR用于中断服务程序中消息发送。
u 第1个参数是消息队列句柄。
u 第2个参数要传递数据地址,每次发送都是将消息队列创建函数xQueueCreate所指定的单个消息大小复制到消息队列空间中。
u 第3个参数用于保存是否有高优先级任务准备就绪。如果函数执行完毕后,此参数的数值是pdTRUE,说明有高优先级任务要执行,否则没有。
u 返回值,如果消息成功发送返回pdTRUE,否则返回errQUEUE_FULL。
使用这个函数要注意以下问题:
1. FreeRTOS的消息传递是数据的复制,而不是传递的数据地址。正因为这个原因,用户在创建消息队列时单个消息大小不可太大,因为一定程度上面会增加中断服务程序的执行时间。
2. 此函数是用于中断服务程序中调用的,故不可以在任务代码中调用此函数,任务代码中使用的是xQueueSend。
3. 消息队列还有两个函数xQueueSendToBackFromISR和xQueueSendToFrontFromISR,函数xQueueSendToBackFromISR实现的是FIFO方式的存取,函数xQueueSendToFrontFromISR实现的是LIFO方式的读写。我们这里说的函数xQueueSendFromISR等效于xQueueSendToBackFromISR,即实现的是FIFO方式的存取。
使用举例:
static QueueHandle_t xQueue1 = NULL;
static QueueHandle_t xQueue2 = NULL;
typedef struct Msg
{
uint8_t ucMessageID;
uint16_t usData[2];
uint32_t ulData[2];
}MSG_T;
MSG_T g_tMsg;
static uint32_t g_uiCount = 0;
static void TIM1_IRQHandler(void)
{
BaseType_t xResult;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
……
g_uiCount++;
xQueueSendFromISR(xQueue1,
(void *)&g_uiCount,
&xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
static void TIM2_IRQHandler (void)
{
MSG_T *ptMsg;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
……
ptMsg = &g_tMsg;
ptMsg->ucMessageID++;
ptMsg->ulData[0]++;
ptMsg->usData[0]++;
xQueueSendFromISR(xQueue2,
(void *)&ptMsg,
&xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
20.2.4函数xQueueReceive
函数原型:
BaseType_t xQueueReceive(
QueueHandle_t xQueue, /* 消息队列句柄 */
void *pvBuffer,
TickType_t xTicksToWait
);
函数描述:
函数xQueueReceive用于接收消息队列中的数据。
u 第1个参数是消息队列句柄。
u 第2个参数是从消息队列中复制出数据后所储存的缓冲地址,缓冲区空间要大于等于消息队列创建函数xQueueCreate所指定的单个消息大小,否则取出的数据无法全部存储到缓冲区,从而造成内存溢出。
u 第3个参数是消息队列为空时,等待消息队列有数据的最大等待时间,单位系统时钟节拍。
u 返回值,如果接到到消息返回pdTRUE,否则返回pdFALSE。
使用这个函数要注意以下问题:
1. 此函数是用于任务代码中调用的,故不可以在中断服务程序中调用此函数,中断服务程序使用的是xQueueReceiveFromISR。
2. 如果消息队列为空且第三个参数为0,那么此函数会立即返回。
3. 如果用户将FreeRTOSConfig.h文件中的宏定义INCLUDE_vTaskSuspend配置为1且第三个参数配置为portMAX_DELAY,那么此函数会永久等待直到消息队列有数据。
使用举例:
static QueueHandle_t xQueue1 = NULL;
static void vTaskMsgPro(void *pvParameters)
{
BaseType_t xResult;
const TickType_t xMaxBlockTime = pdMS_TO_TICKS(300);
uint8_t ucQueueMsgValue;
while(1)
{
xResult = xQueueReceive(xQueue1,
(void *)&ucQueueMsgValue,
(TickType_t)xMaxBlockTime);
if(xResult == pdPASS)
{
printf("接收到消息队列数据ucQueueMsgValue = %d\r\n", ucQueueMsgValue);
}
else
{
bsp_LedToggle(1);
bsp_LedToggle(4);
}
}
}
20.3实验例程说明(任务间通信)
20.3.1STM32F103开发板实验
配套例子:
V4-315_FreeRTOS实验_消息队列
实验目的:
1. 学习FreeRTOS的消息队列。
实验内容:
1. K1按键按下,串口打印任务执行情况(波特率115200,数据位8,奇偶校验位无,停止位1)。
2. K2键按下,向消息队列xQueue1发送数据。
3. K3键按下,向消息队列xQueue2发送数据。
4. 各个任务实现的功能如下:
vTaskUserIF任务 :按键消息处理。
vTaskLED任务 :使用函数xQueueReceive接收任务vTaskTaskUserIF发送的消息队列数据(xQueue2)。
vTaskMsgPro任务:使用函数xQueueReceive接收任务vTaskTaskUserIF发送的消息队列数据(xQueue1)。
vTaskStart任务 :启动任务,也是最高优先级任务,这里实现按键扫描。
FreeRTOS的配置:
FreeRTOSConfig.h文件中的配置如下:
#if defined(__ICCARM__) || defined(__CC_ARM) || defined(__GNUC__)
#include
extern volatile uint32_t ulHighFrequencyTimerTicks;
#endif
#define configUSE_PREEMPTION 1
#define configUSE_IDLE_HOOK 0
#define configUSE_TICK_HOOK 0
#define configCPU_CLOCK_HZ ( ( unsigned long ) 72000000 )
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
#define configMAX_PRIORITIES ( 5 )
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 )
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) )
#define configMAX_TASK_NAME_LEN ( 16 )
#define configUSE_TRACE_FACILITY 1
#define configUSE_16_BIT_TICKS 0
#define configIDLE_SHOULD_YIELD 1
#define configGENERATE_RUN_TIME_STATS 1
#define configUSE_STATS_FORMATTING_FUNCTIONS 1
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() (ulHighFrequencyTimerTicks = 0ul)
#define portGET_RUN_TIME_COUNTER_VALUE() ulHighFrequencyTimerTicks
//#define portALT_GET_RUN_TIME_COUNTER_VALUE 1
#define configUSE_CO_ROUTINES 0
#define configMAX_CO_ROUTINE_PRIORITIES ( 2 )
#define INCLUDE_vTaskPrioritySet 1
#define INCLUDE_uxTaskPriorityGet 1
#define INCLUDE_vTaskDelete 1
#define INCLUDE_vTaskCleanUpResources 0
#define INCLUDE_vTaskSuspend 1
#define INCLUDE_vTaskDelayUntil 1
#define INCLUDE_vTaskDelay 1
#ifdef __NVIC_PRIO_BITS
#define configPRIO_BITS __NVIC_PRIO_BITS
#else
#define configPRIO_BITS 4
#endif
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 0x0f
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 0x01
FreeRTOS任务调试信息(按K1按键,串口打印):
上面截图中打印出来的任务状态字母B, R, D, S对应如下含义:
#define tskBLOCKED_CHAR ( 'B' ) 任务阻塞
#define tskREADY_CHAR ( 'R' ) 任务就绪
#define tskDELETED_CHAR ( 'D' ) 任务删除
#define tskSUSPENDED_CHAR ( 'S' ) 任务挂起
程序设计:
u 任务栈大小分配:
vTaskUserIF任务 :2048字节
vTaskLED任务 :2048字节
vTaskMsgPro任务 :2048字节
vTaskStart任务 :2048字节
任务栈空间是在任务创建的时候从FreeRTOSConfig.h文件中定义的heap空间中申请的
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 17 * 1024 ) )
u 系统栈大小分配:
u FreeROTS初始化:
int main(void)
{
__set_PRIMASK(1);
bsp_Init();
vSetupSysInfoTest();
AppTaskCreate();
AppObjCreate();
vTaskStartScheduler();
while(1);
}
u 硬件外设初始化
硬件外设的初始化是在bsp.c文件实现:
void bsp_Init(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
bsp_InitUart();
bsp_InitLed();
bsp_InitKey();
}
u FreeRTOS任务创建:
static void AppTaskCreate (void)
{
xTaskCreate( vTaskTaskUserIF,
"vTaskUserIF",
512,
NULL,
1,
&xHandleTaskUserIF ); /* 任务句柄 */
xTaskCreate( vTaskLED,
"vTaskLED",
512,
NULL,
2,
&xHandleTaskLED );
xTaskCreate( vTaskMsgPro,
"vTaskMsgPro",
512,
NULL,
3,
&xHandleTaskMsgPro ); /* 任务句柄 */
xTaskCreate( vTaskStart,
"vTaskStart",
512,
NULL,
4,
&xHandleTaskStart );
}
u FreeRTOS消息队列创建:
static void AppObjCreate (void)
{
xQueue1 = xQueueCreate(10, sizeof(uint8_t));
if( xQueue1 == 0 )
{
}
xQueue2 = xQueueCreate(10, sizeof(struct Msg *));
if( xQueue2 == 0 )
{
}
}
u 四个FreeRTOS任务的实现:
static void vTaskTaskUserIF(void *pvParameters)
{
MSG_T *ptMsg;
uint8_t ucCount = 0;
uint8_t ucKeyCode;
uint8_t pcWriteBuffer[500];
ptMsg = &g_tMsg;
ptMsg->ucMessageID = 0;
ptMsg->ulData[0] = 0;
ptMsg->usData[0] = 0;
while(1)
{
ucKeyCode = bsp_GetKey();
if (ucKeyCode != KEY_NONE)
{
switch (ucKeyCode)
{
case KEY_DOWN_K1:
printf("=================================================\r\n");
printf("任务名 任务状态 优先级 剩余栈 任务序号\r\n");
vTaskList((char *)&pcWriteBuffer);
printf("%s\r\n", pcWriteBuffer);
printf("\r\n任务名 运行计数 使用率\r\n");
vTaskGetRunTimeStats((char *)&pcWriteBuffer);
printf("%s\r\n", pcWriteBuffer);
break;
/* K2键按下,向xQueue1发送数据 */
case KEY_DOWN_K2:
ucCount++;
if( xQueueSend(xQueue1,
(void *) &ucCount,
(TickType_t)10) != pdPASS )
{
printf("K2键按下,向xQueue1发送数据失败,即使等待了10个时钟节拍\r\n");
}
else
{
printf("K2键按下,向xQueue1发送数据成功\r\n");
}
break;
case KEY_DOWN_K3:
ptMsg->ucMessageID++;
ptMsg->ulData[0]++;;
ptMsg->usData[0]++;
if(xQueueSend(xQueue2,
(void *) &ptMsg,
(TickType_t)10) != pdPASS )
{
printf("K3键按下,向xQueue2发送数据失败,即使等待了10个时钟节拍\r\n");
}
else
{
printf("K3键按下,向xQueue2发送数据成功\r\n");
}
default:
break;
}
}
vTaskDelay(20);
}
}
static void vTaskLED(void *pvParameters)
{
MSG_T *ptMsg;
BaseType_t xResult;
const TickType_t xMaxBlockTime = pdMS_TO_TICKS(200);
while(1)
{
xResult = xQueueReceive(xQueue2,
(void *)&ptMsg,
(TickType_t)xMaxBlockTime);
if(xResult == pdPASS)
{
printf("接收到消息队列数据ptMsg->ucMessageID = %d\r\n", ptMsg->ucMessageID);
printf("接收到消息队列数据ptMsg->ulData[0] = %d\r\n", ptMsg->ulData[0]);
printf("接收到消息队列数据ptMsg->usData[0] = %d\r\n", ptMsg->usData[0]);
}
else
{
bsp_LedToggle(2);
bsp_LedToggle(3);
}
}
}
static void vTaskMsgPro(void *pvParameters)
{
BaseType_t xResult;
const TickType_t xMaxBlockTime = pdMS_TO_TICKS(300);
uint8_t ucQueueMsgValue;
while(1)
{
xResult = xQueueReceive(xQueue1,
(void *)&ucQueueMsgValue,
(TickType_t)xMaxBlockTime);
if(xResult == pdPASS)
{
printf("接收到消息队列数据ucQueueMsgValue = %d\r\n", ucQueueMsgValue);
}
else
{
bsp_LedToggle(1);
bsp_LedToggle(4);
}
}
}
static void vTaskStart(void *pvParameters)
{
while(1)
{
bsp_KeyScan();
vTaskDelay(10);
}
}