【FreeRTOS-消息队列】
参照正点原子以及以下gitee笔记整理本博客,并将实验结果附在文末。
https://gitee.com/xrbin/FreeRTOS_learning/tree/master
一、队列简介
1、FreeRTOS中的消息队列是什么
答:消息队列是任务到任务、任务到中断、中断到任务数据交流的一种机制(消息传递)。
2、消息队列和全局变量的区别
答:消息队列作用有点类似于全局变量,但消息队列在RTOS中比全局变量更安全可靠。
假设有一个全局变量a=0,现在有两个任务都要写这个变量a。
上图中任务1和任务2在RTOS中相互争取修改a的值,a的值容易受损错乱。
下图表示如果使用全局变量,当任务1运行到②时,更高优先级的任务2抢占并完全执行得到r0为1赋给a,然后回到任务1执行③,最终结果执行完任务1和任务2后,a=1
全局变量的弊端:数据无保护,导致数据不安全,当多个任务同时对该变量操作时,数据易受损。
3、使用队列的情况
答:使用队列的情况如下:
读写队列做好了保护,防止多任务或中断同时访问产生冲突。我们只需直接调用API函数即可,简单易用。
注意:FreeRTOS基于队列,实现了多种功能,其中包括队列集、互斥信号量、计数信号量、二值信号量、递归互斥信号量,因此很有必要深入了解FreeRTOS的队列。
补充:任务调度是由于PendSV引起的,他的中断优先级是最低的,关闭中断后,PendSV无法触发,因此任务A在写队列是,任务B是无法打断任务A的执行的。
4、队列项目和队列长度
答:在队列中可以存储数量有限、大小固定的数据。队列中的每个数据就叫做 “队列项目” ,队列能够存储 “队列项目” 的最大数量称为队列的长度。
在创建队列时,就要指定队列长度以及队列项目的大小!
5、FreeRTOS队列特点
答:
-
数据入队出队方式 : 队列通常采用 “先进先出(FIFO)” 的数据存储缓冲机制,即先入队的数据会先从队列中被读取,FreeRTOS中也可以配置为 “后进先出(LIFO)” 方式。
-
数据传递方式 : FreeRTOS中队列采用实际值传递,即将数据拷贝到队列中进行传递,FreeRTOS采用拷贝数据传递,也可以传递指针,所以在传递较大的数据的时候采用指针传递。
-
多任务访问 : 队列不属于某个任务,任何任务和中断都可以向队列写入/读取消息。
-
出队、入队阻塞 : 当任务向一个队列发送/读取消息时,可以指定一个阻塞时间,假设此时当队列已满无法入队。
- 若阻塞时间为0 :直接返回不会等待。
- 若阻塞时间为0~port_MAX_DELAY :等待设定阻塞时间,若在该时间内无法入队/出队,超时后直接返回不再等待。
- 若阻塞时间为port_MAX_DELAY :死等,一直等到可以入队/出队为止。
6、入队/出队阻塞过程
答:
入队阻塞:
队列满了,此时写不进去数据:
-
将该任务的状态列表项(就绪列表)挂载在pxDelayedTaskList(阻塞列表);
-
将该任务的事件列表项挂载在xTasksWaitingToSend(等待发送列表);
出队阻塞:
队列为空,此时读取不了数据:
-
将该任务的状态列表项(就绪列表)挂载在pxDelayedTaskList(阻塞列表);
-
将该任务的事件列表项挂载在xTasksWaitingToReceive(等待读取列表);
(当队列中有数据,那么解除xTasksWaitingToReceive,移除pxDelayedTaskList,放到就绪列表里,此时任务B就能够执行)
7、当多个任务写入消息给一个 “满队列” 时,这些任务都会进入阻塞状态,也就是说有多个任务在等待同一个队列的空间。那当队列有空间时,哪个任务会进入就绪态?
答:
-
优先级最高的任务
-
如果大家的优先级相同,那等待时间最久的任务进入就绪态。
8、队列创建、写入和读出过程
答:
二、队列结构体介绍
1、队列结构体
答:
typedef struct QueueDefinition
{int8_t * pcHead; /* 存储区域的起始地址 */int8_t * pcWriteTo; /* 下一个写入的位置 */union{QueuePointers_t xQueue;SemaphoreData_t xSemaphore; } u ;List_t xTasksWaitingToSend; /* 等待发送列表 */List_t xTasksWaitingToReceive; /* 等待接收列表 */volatile UBaseType_t uxMessagesWaiting; /* 非空闲队列项目的数量 */UBaseType_t uxLength; /* 队列长度 */UBaseType_t uxItemSize; /* 队列项目的大小 */volatile int8_t cRxLock; /* 读取上锁计数器 */volatile int8_t cTxLock; /* 写入上锁计数器 *//* 其他的一些条件编译 */
} xQUEUE;
当用于队列使用时:
typedef struct QueuePointers
{int8_t * pcTail; /* 存储区的结束地址 */int8_t * pcReadFrom; /* 最后一个读取队列的地址 */
} QueuePointers_t;
当用于互斥信号量和递归互斥信号量时:
typedef struct SemaphoreData
{TaskHandle_t xMutexHolder; /* 互斥信号量持有者 */UBaseType_t uxRecursiveCallCount; /* 递归互斥信号量的获取计数器 */
} SemaphoreData_t;
队列结构体示意图:
队列结构体存储区+队列项(消息)存储区
int8_t * pcHead; /* 存储区域的起始地址 */
int8_t * pcWriteTo; /* 下一个写入的位置 */
int8_t * pcTail; /* 存储区的结束地址 */
int8_t * pcReadFrom; /* 最后一个读取队列的地址 */
三、队列相关API函数介绍
1、队列使用流程
答:使用队列的主要流程:创建队列 —> 写队列 —> 读队列。
2、创建队列函数
答:
函数返回值:
3、各种功能所对应的队列
答:
#define queueQUEUE_TYPE_BASE ( ( uint8_t ) 0U ) /* 队列 */
#define queueQUEUE_TYPE_SET ( ( uint8_t ) 0U ) /* 队列集 */
#define queueQUEUE_TYPE_MUTEX ( ( uint8_t ) 1U ) /* 互斥信号量 */
#define queueQUEUE_TYPE_COUNTING_SEMAPHORE ( ( uint8_t ) 2U ) /* 计数型信号量 */
#define queueQUEUE_TYPE_BINARY_SEMAPHORE ( ( uint8_t ) 3U ) /* 二值信号量 */
#define queueQUEUE_TYPE_RECURSIVE_MUTEX ( ( uint8_t ) 4U ) /* 递归互斥信号量 */
4、队列写入消息函数
答:
任务级+中断级
#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait )xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )
#define xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait )xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )
#define xQueueSendToFront( xQueue, pvItemToQueue, xTicksToWait )xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_FRONT )
#define xQueueOverwrite( xQueue, pvItemToQueue )xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), 0, queueOVERWRITE )
可以看到这几个写入函数调用的是同一个函数xQueueGenericSend( ),只是指定了不同的写入位置!
队列一共有3种写入位置:
#define queueSEND_TO_BACK ( ( BaseType_t ) 0 ) /* 写入队列尾部 */
#define queueSEND_TO_FRONT ( ( BaseType_t ) 1 ) /* 写入队列头部 */
#define queueOVERWRITE ( ( BaseType_t ) 2 ) /* 覆写队列*/
注意:==覆写方式写入队列,只有在队列的队列长度为 1 时,才能够使用== 。往队列写入消息函数入口参数解析:
BaseType_t xQueueGenericSend( QueueHandle_t xQueue,const void * const pvItemToQueue,TickType_t xTicksToWait,const BaseType_t xCopyPosition );
函数参数:
函数返回值:
5、队列读出消息函数
答:
BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait )
函数参数:
函数返回值:
BaseType_t xQueuePeek( QueueHandle_t xQueue,void * const pvBuffer,TickType_t xTicksToWait )
此函数用于在任务中,从队列中读取消息, 但与函数 xQueueReceive()不同,此函数在成功读取消息后,并不会移除已读取的消息!
函数参数:
函数返回值:
四、队列操作实验
实验简介
实验现象
task2和task3无需调用延时函数,由于调用队列接收函数,如果队列无数据,表明无法接收数据,任务会自动进入阻塞态。
部分代码
/******************************************************************************************************* @file freertos.c* @author 正点原子团队(ALIENTEK)* @version V1.4* @date 2022-01-04* @brief FreeRTOS 移植实验* @license Copyright (c) 2020-2032, 广州市星翼电子科技有限公司***************************************************************************************************** @attention** 实验平台:正点原子 探索者F407开发板* 在线视频:www.yuanzige.com* 技术论坛:www.openedv.com* 公司网址:www.alientek.com* 购买地址:openedv.taobao.com******************************************************************************************************/#include "freertos_demo.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
#include "./SYSTEM/delay/delay.h"
#include "./MALLOC/malloc.h"
/*FreeRTOS*********************************************************************************************/
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"/******************************************************************************************************/
/*FreeRTOS配置*//* START_TASK 任务 配置* 包括: 任务句柄 任务优先级 堆栈大小 创建任务*/
#define START_TASK_PRIO 1
#define START_TASK_STACK_SIZE 128
TaskHandle_t start_task_handle;
void start_task( void * pvParameters );/* TASK1 任务 配置* 包括: 任务句柄 任务优先级 堆栈大小 创建任务*/#define TASK1_PRIO 2
#define TASK1_STACK_SIZE 128
TaskHandle_t task1_handle;
void task1(void * pvParameters);/* TASK2 任务 配置* 包括: 任务句柄 任务优先级 堆栈大小 创建任务*/#define TASK2_PRIO 3
#define TASK2_STACK_SIZE 128
TaskHandle_t task2_handle;
void task2(void * pvParameters);/* TASK3 任务 配置* 包括: 任务句柄 任务优先级 堆栈大小 创建任务*/#define TASK3_PRIO 4
#define TASK3_STACK_SIZE 128
TaskHandle_t task3_handle;
QueueHandle_t key_queue; /* 小数据 */
QueueHandle_t big_data_queue; /* 大数据 */
char big_buff[100] = {"我是大数组,大大的数组 12123dafenhagendasi"};
void task3(void * pvParameters);/******************************************************************************************************//*** @brief FreeRTOS例程入口函数* @param 无* @retval 无*/
void freertos_demo(void)
{ /* 队列创建 */key_queue = xQueueCreate( 2, sizeof(uint8_t) ); /* 由于有两个按键,因此队列项长度为2 ;按键键值的大小 */if(key_queue != NULL){printf("Key_queue队列创建成功!\r\n");}else{printf("Key_queue队列创建失败!\r\n");}big_data_queue = xQueueCreate( 1, sizeof(char *) ); /* 由于有大数据数组,因此队列项长度为2 ;数组首地址 */if(big_data_queue != NULL){printf("big_data_queue队列创建成功!\r\n");}else{printf("big_data_queue队列创建失败!\r\n");}xTaskCreate( (TaskFunction_t ) start_task,(char * ) "start_task",(configSTACK_DEPTH_TYPE) START_TASK_STACK_SIZE,(void * ) NULL, (UBaseType_t ) START_TASK_PRIO,(TaskHandle_t * ) &start_task_handle);//开启任务调度器vTaskStartScheduler();
}void start_task( void * pvParameters )
{taskENTER_CRITICAL(); /*进入临界区,任务切换不会进行*/xTaskCreate( (TaskFunction_t ) task1,(char * ) "task1",(configSTACK_DEPTH_TYPE) TASK1_STACK_SIZE,(void * ) NULL, (UBaseType_t ) TASK1_PRIO,(TaskHandle_t * ) &task1_handle);xTaskCreate( (TaskFunction_t ) task2,(char * ) "task2",(configSTACK_DEPTH_TYPE) TASK2_STACK_SIZE,(void * ) NULL, (UBaseType_t ) TASK2_PRIO,(TaskHandle_t * ) &task2_handle);xTaskCreate( (TaskFunction_t ) task3,(char * ) "task3",(configSTACK_DEPTH_TYPE) TASK3_STACK_SIZE,(void * ) NULL, (UBaseType_t ) TASK3_PRIO,(TaskHandle_t * ) &task3_handle);vTaskDelete( NULL );taskEXIT_CRITICAL(); /*退出临界区,才会开始任务切换*//*简单而言,临界区保护,就是保护那些不想被打断的从程序段 */ }/* 任务一,实现入队 */
void task1(void * pvParameters)
{uint8_t key_value = 0;char * buf;BaseType_t err = 0;buf = big_buff; /* buf = &buff[0] */while(1){ key_value = key_scan(0);if(key_value == KEY0_PRES || key_value == KEY1_PRES){err = xQueueSend(key_queue, &key_value, portMAX_DELAY);if(err != pdTRUE){printf("队列发送失败\r\n");}}else if(key_value == WKUP_PRES){err = xQueueSend(big_data_queue, &buf, portMAX_DELAY);if(err != pdTRUE){printf("队列发送失败\r\n");}}vTaskDelay(10);}
}/* 任务二,实现小数据出队 */
void task2(void * pvParameters)
{uint8_t key = 0;BaseType_t err = 0;while(1){err = xQueueReceive(key_queue,&key,portMAX_DELAY); /* 即使没有调用延时函数,由于队列接收函数接收不了数据而变为阻塞态 */if(err != pdTRUE){printf("key_queue队列读取失败\r\n");}else{printf("key_queue读取对列成功,数据%d\r\n",key);}}
}/* 任务二,实现大数据出队 */
void task3(void * pvParameters)
{char * buf;BaseType_t err = 0;while(1){err = xQueueReceive(big_data_queue,&buf,portMAX_DELAY);if(err != pdTRUE){printf("big_data_queue队列读取失败\r\n");}else{printf("big_data_queue读取对列成功,数据%s\r\n",buf);}}}