本文将详细全方位的讲解FreeRTOS的消息队列,其实在FreeRTOS中消息队列的重要性也不言而喻,与FreeRTOS任务调度同等重要,因为后面的各种信号量基本都是基于消息队列的。
目录
一、消息队列的简介
1.1 产生的原因
1.2 消息队列的解决办法
1.3 消息队列的基本概念
1.4 队列的特点
1.5 队列的入队和出队
1.6 队列操作基本过程
二、队列结构体介绍
三、队列相关API函数介绍
3.1 消息队列的使用流程
3.2 创建消息队列API函数
3.3 往队列写入消息API函数
3.4 从队列读取消息API函数
四、队列操作实验
五、队列相关API函数解析(了解)
一、消息队列的简介
1.1 产生的原因
假设有一个全局变量a = 0,现有两个任务都在对变量a进行自增操作(写操作),如下图所示:
对于自增操作它不是原子操作,会经过一系列的步骤,最后才将自增后的结果写入到寄存器,如果任务2的优先级高于任务1,在任务1将自增后的2即将写入到a之前,任务2打断任务1进行自增操作,任务2拿到的是a为1,然后再进行的自增,两个任务执行完后自增其实只进行了一次,数据无保护,导致数据不安全,当多个任务同时对该全局变量操作时,数据易受损!!其实这就像是Linux中的由于线程的并发运行,导致线程对进程资源的竞争问题,不过在这里是由于任务的优先级影响的。
因此,使用全局变量并不是安全的,于是产生了消息队列。消息队列可以理解成带中断保护的全局数组,它是用来存放数据的,我们是将他设计成一个队列结构体。
1.2 消息队列的解决办法
使用队列的情况如下:
队列依旧是来存放数据的,可以看成是全局数组,
我们对于队列的读写封装好了函数,并且在这个函数里面进行了保护(关闭中断带来的),关闭中断后,管理范围内的中断不会再响应,并且也不会进行任务的切换(PendSV任务切换中断也被关闭),也就是中断和其他任务都不能打断当前任务向队列中写入数据!,同理从队列读取数据也一样,这样保证同一时刻只有一个任务向队列写入数据或者从队列读取数据!因此,他可以做到:防止多任务同时访问冲突;我们只需要直接调用API函数(可以理解Linux中的可重入函数/线程安全函数)即可,简单易用!
1.3 消息队列的基本概念
消息队列是任务到任务、任务到中断、中断到任务数据交流的一种机制(进行消息传递)FreeRTOS基于队列, 实现了多种功能,其中包括队列集、互斥信号量、计数型信号量、二值信号量、 递归互斥信号量,因此很有必要深入了解 FreeRTOS 的队列 。在消息队列中可以存储数量有限、大小固定的数据,可以将队列理解为一个全局数组。队列中的每一个数据叫做“队列项目”,队列能够存储“队列项目”的最大数量称为队列的长度。如下图所示:
我们在进行创建队列时,就必须要指定队列长度以及队列项目的大小!,然后操作系统就会为我们分配相应的内存空间!
1.4 队列的特点
当任务从队列中读取消息(出队)时,如果此时队列为空,则此任务将进入阻塞状态,等待消息队列不为空。 用户可以指定阻塞等待的时间,当等待的时间超过阻塞等待时间,任务将结束阻塞状态,转为就绪态。同理,入队也是如此。我们可以指定的阻塞时间如下:
①若阻塞时间为: 0 :代表直接返回不会等待;(不进行阻塞)②若阻塞时间为:0~port_MAX_DELAY (最大值0xffffffff) :代表等待设定的阻塞时间(一段时间内阻塞),若在该时间内还无法入队,超时后直接返回不再等待;③若阻塞时间为: port_MAX_DELAY(最大值0xffffffff) :代表死等,(一直阻塞)一直等到可以入队为止。
1.5 队列的入队和出队
注意:我们通过前面学习知道:每个任务对应一个任务控制块,它就是一个结构体,里面有状态列表项和事件列表项两个成员。
队列满了,此时写不进去数据:
①将该任务的状态列表项挂载在pxDelayedTaskList(阻塞状态);
②将该任务的事件列表项挂载在xTasksWaitingToSend(等待发送);
队列为空,此时读取不了数据:
①将该任务的状态列表项挂载在pxDelayedTaskList; (阻塞状态)
②将该任务的事件列表项挂载在xTasksWaitingToReceive(等待接收);
思考一个问题:
当多个任务写入消息给一个“满队列”时,这些任务都会进入阻塞状态,也就是说有多个任务 在等待同一个队列的空间。那当队列中有可以写入的空间时,哪个任务会进入就绪态?
答:
1、优先级最高的任务
2、如果大家的优先级相同,那等待时间最久的任务会进入就绪态
1.6 队列操作基本过程
二、队列结构体介绍
队列结构体如下:
可以看到:里面有一个联合体,我们知道后面学的各种信号量都是基于队列实现的,那这个联合体就根据不同的结构,使用不同的成员。
队列结构体整体示意图:主要有两部分组成:队列结构体/队列控制块和队列项(存储数据的)构成。
三、队列相关API函数介绍
3.1 消息队列的使用流程
在使用队列之前,我们一定要明确队列的使用流程,这样就会形成编码规范,不容易出错,使用队列的主要流程:创建队列 ——》写队列 ——》 读队列。
3.2 创建消息队列API函数
创建消息队列函数主要有以下两个:
函数 | 描述 |
xQueueCreate() | 动态方式创建队列 |
xQueueCreateStatic() | 静态方式创建队列 |
动态和静态创建队列之间的区别:动态创建队列所需的内存空间由 FreeRTOS 从 FreeRTOS 管理的堆中分配,而静态创建需要用户自行分配内存。我们通常用的是动态创建队列。
此函数用于使用动态方式创建队列,队列所需的内存空间由 FreeRTOS 从 FreeRTOS 管理的堆中分配 ,可以看到,真正起作用的函数是:xQueueGenericCreate()
形参 | 描述 |
uxQueueLength | 队列长度 |
uxItemSize | 队列项目的大小 |
返回值 | 描述 |
NULL | 队列创建失败 |
其他值 | 队列创建成功,返回队列句柄 |
xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), (queueQUEUE_TYPE_BASE ))
底层调用的是上述函数,前面两个参数很容易理解,但是最后这个呢?又该如何理解?前面说 FreeRTOS 基于队列实现了多种功能,每一种功能对应一种队列类型,队列类型的 queue.h 文件中有定义:
可以看到,使用哪个功能,第三个参数底层就会使用相应的宏定义。
从上面我们可以知道,引入队列的头文件,创建队列,我们需要提前定义好消息队列句柄,接收其返回值用于后续访问该消息队列,同时应该传入队列长度参数和队列项目大小参数。
3.3 往队列写入消息API函数
向队列中写入消息一共有以下函数:
往队列写入消息函数入口参数解析:
形参 | 描述 |
xQueue | 待写入的队列(它的类型是一个队列句柄) |
pvItemToQueue | 待写入消息 (传的是一个地址) |
xTicksToWait | 阻塞超时时间 |
xCopyPosition | 写入的位置 |
返回值 | 描述 |
pdTRUE | 队列写入成功 |
errQUEUE_FULL | 队列写入失败 |
从上面我们可以知道,往队列中写入消息,传入提前定义好消息队列句柄,以及写入消息的地址,和传入如果队列已经满时,该任务的阻塞时间即可!
3.4 从队列读取消息API函数
从队列读取消息API函数:
从上面我们可以知道,读取队列中消息,传入提前定义好消息队列句柄,以及读取消息存放的地址,和传入如果队列当前为空时,该任务的阻塞时间即可!
四、队列操作实验
创建任务文件
#include "stm32f4xx.h" // Device header
#include "stdio.h"
#include "FreeRTOS.h"
#include "task.h"
#include "dynamic.h"
#include "mydelay.h"
#include "mykey.h"
#include "queue.h"/**********************START_TASK任务配置******************************/
/***********包括任务堆栈大小、任务优先级、任务句柄、创建任务***********/#define START_TASK_STACK_SIZE 128 //定义堆栈大小为128字(1字等于4字节)
#define START_TASK_PRIO 1 //定义任务优先级,0-31根据任务需求
TaskHandle_t start_task_handler; //定义任务句柄(结构体指针)
void start_task(void* args);/**********************TASK1任务配置******************************/
/***********包括任务堆栈大小、任务优先级、任务句柄、创建任务***********/
#define TASK1_STACK_SIZE 128 //定义堆栈大小为128字(1字等于4字节)
#define TASK1_PRIO 2 //定义任务优先级,0-31根据任务需求
TaskHandle_t task1_handler; //定义任务句柄(结构体指针)
void task1(void* args);/**********************TASK2任务配置******************************/
/***********包括任务堆栈大小、任务优先级、任务句柄、创建任务***********/
#define TASK2_STACK_SIZE 128 //定义堆栈大小为128字(1字等于4字节)
#define TASK2_PRIO 3 //定义任务优先级,0-31根据任务需求
TaskHandle_t task2_handler; //定义任务句柄(结构体指针)
void task2(void* args);/**********************TASK3任务配置******************************/
/***********包括任务堆栈大小、任务优先级、任务句柄、创建任务***********/
#define TASK3_STACK_SIZE 128 //定义堆栈大小为128字(1字等于4字节)
#define TASK3_PRIO 4 //定义任务优先级,0-31根据任务需求
TaskHandle_t task3_handler; //定义任务句柄(结构体指针)
void task3(void* args);QueueHandle_t key_queue; //小数据队列句柄
QueueHandle_t big_date_queue; //大数据队列句柄
char buff[100]="我是大数组12345678911111111111";/*********开始任务用来创建一个任务,只创建一次,不能是死循环,创建完这个任务后删除开始任务本身***********/
void start_task(void* args)
{taskENTER_CRITICAL(); /*进入临界区*/xTaskCreate( (TaskFunction_t) task1,(char *) "task1", ( configSTACK_DEPTH_TYPE) TASK1_STACK_SIZE,(void *) NULL,(UBaseType_t) TASK1_PRIO ,(TaskHandle_t *) &task1_handler );xTaskCreate( (TaskFunction_t) task2,(char *) "task2", ( configSTACK_DEPTH_TYPE) TASK2_STACK_SIZE,(void *) NULL,(UBaseType_t) TASK2_PRIO ,(TaskHandle_t *) &task2_handler ); xTaskCreate( (TaskFunction_t) task3,(char *) "task3", ( configSTACK_DEPTH_TYPE) TASK3_STACK_SIZE,(void *) NULL,(UBaseType_t) TASK3_PRIO ,(TaskHandle_t *) &task3_handler ); vTaskDelete(NULL); //删除开始任务自身,传参NULLtaskEXIT_CRITICAL(); /*退出临界区*///临界区内不会进行任务的调度切换,出了临界区才会进行任务调度,抢占式
}/********任务1的任务函数,无返回值且是死循环***********//*****任务1:实现入队*******/
void task1(void* args)
{uint8_t key=0;char *buf=buff;BaseType_t xReturn;while(1){key=KEY_Scan(0); //按键扫描得到键值if(key==KEY0_PRES ||key==KEY1_PRES ){xReturn = xQueueSend( key_queue, &key, portMAX_DELAY ); //将键值写入队列if(xReturn !=pdTRUE ){printf("key_queue队列发送失败!\n");}}else if(key==WKUP_PRES){xReturn = xQueueSend( big_date_queue, &buf, portMAX_DELAY );//将键值写入队列if(xReturn !=pdTRUE )if(xReturn !=pdTRUE ){printf("big_date_queue队列发送失败!\n");}}vTaskDelay(10); //FreeRTOS自带的延时函数,延时10毫秒}
}/********任务2的任务函数,无返回值且是死循环***********//***任务2:实现小数据出队*******/
void task2(void* args)
{uint8_t key = 0;BaseType_t xReturn;while(1){xReturn = xQueueReceive( key_queue,&key,portMAX_DELAY);if(xReturn !=pdTRUE ){printf("key_queue队列读取失败!\n");} else{printf("key_queue队列读取成功,数据:%d\n",key);} //不需要延时,因为队列为空,该任务会直接会被阻塞,挂载到阻塞列表下面}
}/********任务3的任务函数,无返回值且是死循环***********//***任务3:实现大数据出队*******/
void task3(void* args)
{BaseType_t xReturn;char * buf;while(1){xReturn = xQueueReceive( big_date_queue,&buf,portMAX_DELAY);if(xReturn !=pdTRUE ){printf("big_date_queue队列读取失败!\n");} else{printf("big_date_queue队列读取成功,数据:%s\n",buf);} }
}/****如果按键未按下,消息队列未不会有数据,任务2和任务3都会进入阻塞态****///FreeRTO入口例程函数,无参数,无返回值
void freertos_demo(void)
{/***队列创建***/key_queue = xQueueCreate( 2, sizeof(uint8_t) );if(key_queue !=NULL){printf("key_queue队列创建成功!\n");}big_date_queue = xQueueCreate( 1, sizeof(char *) );if(big_date_queue !=NULL){printf("big_date_queue队列创建成功!\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_handler );vTaskStartScheduler(); //开启任务调度器}
主函数任务调度文件
#include "stm32f4xx.h" // Device header
#include "stdio.h"
#include "myled.h"
#include "myusart.h"#include "FreeRTOS.h"
#include "task.h"
#include "dynamic.h"extern TaskHandle_t Start_Handle;int main(void)
{//硬件初始化My_UsartInit();//调用入口函数freertos_demo();}
五、队列相关API函数解析(了解)
至此,已经讲解完毕!初次学习,循序渐进,一步步掌握即可!以上就是全部内容!请务必掌握,创作不易,欢迎大家点赞加关注评论,您的支持是我前进最大的动力!下期再见!