赞
踩
在实际的项目开发中,经常会遇到在任务于任务之间或任务于中断之间需要进行“沟通交流”,即消息传递的过程。在不使用操作系统的情况下,函数与函数,或函数与中断之间的“沟通交流”一般使用一个或多多个全局变量来完成,但是在操作系统中,因为会涉及“资源管理”的问题,比方说读写冲突,因此使用全局变量在任务于任务或任务于中断之间进行消息传递,并不是很好的解决方案。队列读写队列做好了保护,防止多任务同时访问冲突;我们只需要直接调用API函数即可,简单易用。
队列是任务到任务、任务到中断、中断到任务数据交流的一种机制(消息传递)。任务和ISR统称为进程,任务与任务之间,或任务与ISR之间有时需要进行通讯或同步,称为进程间通信。
在创建队列时,就要指定队列长度以及队列项目的大小!
队列操作的过程:包括创建队列、往队列中写入消息、从队列中读取消息等操作
(1)创建队列
创建了一个用于任务 A 与任务 B 之间“沟通交流”的队列,这个队列最大可容纳 5 个队列项目,即队列的长度为 5。刚创建的队列是不包含内容的,因此这个队列为空。
(2)写入队列
任务 A 将一个私有变量写入队列的尾部。由于在写入队列之前,队列是空的,因此新写入的消息,既是是队列的头部,也是队列的尾部。
任务 A 改变了私有变量的值,并将新值写入队列。现在队列中包含了队列 A写入的两个值,其中第一个写入的值在队列的头部,而新写入的值在队列的尾部。 这时队列还有 3 个空闲的位置。
(3)读取队列
任务 B 从队列中读取消息,任务 B 读取的消息是处于队列头部的消息,这是任务 A 第一次往队列中写入的消息。在任务 B 从队列中读取消息后,队列中任务 A 第二次写入的消息,变成了队列的头部,因此下次任务 B 再次读取消息时,将读取到这个消息。此时队列中剩余 4 个空闲的位置。
使用队列的主要流程:创建队列 ->写队列 -> 读队列。
队列创建时被分配固定个数的存储单元,每个存储单元存储
固定大小的数据,进程间传递的数据就保存在队列的存储单元里
1、xQueueCreate()创建队列
函数xQueueCreate()以动态分配内存的方式创建队列,队列需要用的存储空间由FreeRTOS从堆空间自动分配
#define xQueueCreate( uxQueueLength, uxItemSize )
xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ),
( queueQUEUE_TYPE_BASE ) )
//参数1:队列的长度
//参数2:每个存储单元的大小
//返回值:创建队列的句柄,若返回NULL表示创建失败
函数xQueueGenericCreate()是创建队列、信号量、互斥量等对象的通用函数。函数原型是:
QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength,
const UBaseType_t uxItemSize,
const uint8_t ucQueueType )
//参数1:队列长度,也就是存储单元的个数
//参数2:每个存储单元的字节数
//参数3:表示创建对象的类型
//返回值:表示创建队列的句柄
ucQueueType表示创建对象的类型一般有以下几种:
通过参数3来决定是创建的类型:创建队列函数
创建队列官方代码示例:参数1是数据个数,参数2是每个数据的存储空间字节数
xQueue1 = xQueueCreate( 10, sizeof( uint32_t ) );
创建了一个具有10个存储单元的队列,每个单元占用sizeof(uint32_t)个字节,也就是4个字节。
struct AMessage
{
char ucMessageID;
char ucData[ 20 ];
};
// Create a queue capable of containing 10 pointers to AMessage structures.
// These should be passed by pointer as they contain a lot of data.
xQueue2 = xQueueCreate( 10, sizeof( struct AMessage * ) );
创建一个包含10个指向结构体AMessageID的队列,每个队列大小也就是sizeof( struct AMessage * ) 的大小
注意:
一个任务或ISR向队列写入数据称为发送消息。队列是一个共享的存储区域,可以被多个进程写入,也可以被多个进程读取
可以发现几个写入函数调用的是同一个函数xQueueGenericSend( ),只是指定了不同的写入位置!
BaseType_t xQueueGenericSend( QueueHandle_t xQueue,
const void * const pvItemToQueue,
TickType_t xTicksToWait,
const BaseType_t xCopyPosition )
参数1:队列句柄
参数2:待写入消息
参数3:阻塞超时时间
参数4:表示数据写入队列的位置,有3种常数定义
返回值:pdTRUE表示写入成功,反之失败
写入队列的位置:
注意:覆写方式写入队列,只有在队列的队列长度为 1 时,才能够使用
//向队列尾部写入数据
#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 )
函数在队列未满时能正常向队列写入数据,函数返回值为pdTRUE;
如果队列已满,这两个函数不能再向队列写入数据,函数返回值为errQUEUE_FULL;
//以覆写的形式写入数据(队列长度为1的队列)
#define xQueueOverwrite( xQueue, pvItemToQueue )
xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), 0, queueOVERWRITE )
xQueueOverwrite()也可以用于向队列写入数据,但是这个函数只用于队列长度为1的队列,在队列已满时,它会覆盖队列原来的数据。
只是比任务版本函数多一个是否需要进行上下文切换的申请
例如:
//任务版本
参数1:写入的队列
参数2:要写入的数据
参数3:阻塞超时时间
#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait )
xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )
//ISR版本
//参数1:写入的队列
//参数2:要写入的数据
//参数3:是否进行上下文切换
#define xQueueSendFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken )
xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )
可以在任务或ISR里读取队列的数据,称为接收消息。总是从队列头读取数据,读出后删除这个单元的数据,后面的数据前移
如果设置了等待节拍数,但是队列中没有数据,任务会进入阻塞状态等待指定的时间。如果在等待时间内,队列里有了数据就会退出阻塞状态进入就绪状态,随后调度进入运行状态后从队列里读取数据。反之超过了等待时间队列里还是没有数据函数就是返回pdfalse,任务退出阻塞状态,进入就绪状态。
BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait ) //参数1:读取的队列句柄 //参数2:数据缓冲区(存储的位置) //参数3:阻塞超时时间 //返回值是pdTRUE或pdFALSE //ISR版本: BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue, void * const pvBuffer, BaseType_t * const pxHigherPriorityTaskWoken ) //参数1:读取的队列句柄 //参数2:数据缓冲区(存储的位置) //参数3:是否进行上下文切换
官方demo
// Task to receive from the queue. void vADifferentTask( void *pvParameters ) { struct AMessage *pxRxedMessage; if( xQueue != 0 ) { // Receive a message on the created queue. Block for 10 ticks if a // message is not immediately available. if( xQueueReceive( xQueue, &( pxRxedMessage ), ( TickType_t ) 10 ) ) { // pcRxedMessage now points to the struct AMessage variable posted // by vATask. } } // ... Rest of task code. }
//任务版本
BaseType_t xQueuePeek( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait )
//ISR版本
BaseType_t xQueuePeekFromISR( QueueHandle_t xQueue,
void * const pvBuffer )
将队列复位为空的状态,队列内的所有数据都被丢弃
#define xQueueReset( xQueue ) xQueueGenericReset( xQueue, pdFALSE )
删除一个队列,也可以用于删除一个信号量
void vQueueDelete( QueueHandle_t xQueue )
获取队列的名称,也就是创建队列时设置的队列名称字符串
#if( configQUEUE_REGISTRY_SIZE > 0 )
const char *pcQueueGetName( QueueHandle_t xQueue ) PRIVILEGED_FUNCTION; /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
#endif
为队列设置一个编号,这个编号由用户设置并使用
void vQueueSetQueueNumber( QueueHandle_t xQueue, UBaseType_t uxQueueNumber )
UBaseType_t uxQueueGetQueueNumber( QueueHandle_t xQueue )
获取队列剩余空间个数,也就是还可以写入的消息个数
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue ) PRIVILEGED_FUNCTION;
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue )
查询队列是否为空,返回值为pdTRUE表示队列为空
BaseType_t xQueueIsQueueEmptyFromISR( const QueueHandle_t xQueue )
查询队列是否满了,返回值为pdTRUE表示队列满了
BaseType_t xQueueIsQueueFullFromISR( const QueueHandle_t xQueue )
(1)创建三个任务,创建两个队列
(2)任务1:负责按键扫描,根据按下的不同按键值执行不同的写入操作
按键1按下或按键2按下将按键值拷贝到队列Queue_keys中
唤醒按键按下将提前准备好的字符串拷贝到队列Queue_string中
(3)任务2:读取队列Queue_keys中的按键值,并在LCD上显示按下的键值
(4)任务3:队列队列Queue_string中传输的字符串,并在LCD上显示传输的字符串数据
(1)复制已经配置好LCD、和按键引脚的stm32cube工程修改名字
(2)RCC部分保持原来配置
(3)SYS选择TIM6作为FreeRTOS基础时钟源
(3)GPIO引脚是配置好的LCD背光和按键引脚
(4)启动FreeRTOS并选择V2版本,创建三个任务(动态形式创建)和两个队列
创建3个任务,设置不同的任务优先级(按键检测任务优先级可以设置最大),占空间大小都设置为128字,动态形式创建3个任务。
第一个任务:实现按键检测,将按键值或字符串写入到相关的队列中
第二个任务:读取队列数据获取按键按下的值,并在lcd上显示数据
第三个任务:读取队列数据获取字符串数据,并在LCD上显示数据
队列1用于传输按键按下的值,队列长度为10,项的大小是sizeof(uint8_t)个字节。
队列2用于传输字符串数据,队列长度为10,项的大小为sizeof(char*)个字节。
(5)clock保持原来配置不变
(6)工程名和存储地址无需配置(就是刚开始打开时的工程名和地址)-无法修改
如果是新建的工程可以随意填写(不要包含中文路径和汉字)
生成代码
(1)复制BSP文件夹到工程目录下,并添加相对应路径,添加头文件实现LCD的数据显示
(2)main.h中添加头文件
/* USER CODE BEGIN Includes */
#include "cmsis_os.h"
#include "gpio.h"
#include "fsmc.h"
#include"tftlcd.h"
#include "keyled.h"
/* USER CODE END Includes */
(3)main.c文件主函数中LCD初始化和LCD显示测试
(4)FreeRTOS.c文件添加两个头文件
/* USER CODE BEGIN Includes */
#include"queue.h"
#include "stdio.h"
/* USER CODE END Includes */
(5)按键扫描任务
void AppTask_keyscan(void *argument) { /* USER CODE BEGIN AppTask_keyscan */ //按键扫描 KEYS key_value=KEY_NONE; //无按键按下 char *str="abcdefgh"; //要发送的字符串 // str=&buf[0]; BaseType_t result=pdFALSE; //函数返回值初始化 /* Infinite loop */ for(;;) { key_value=key_scan(0);//按键扫描 if(key_value==KEY_1||key_value==KEY_2)//key1或者key2按下 { //参数1:要写入的队列句柄,参数2:是按键值,参数3是一直等待 result=xQueueSend( Queue_keysHandle, &key_value, portMAX_DELAY ); uint8_t data[20]; //缓冲数组 if(result==pdTRUE)//表明已经成功写入到队列中 { sprintf(data,"key%d_write_ok ",key_value); LCD_ShowString(10, 10+2*20, tftlcd_data.width, tftlcd_data.height, 12, data); } //格式化字符串(key1_press或key2_press格式化存储到字符数组data中) sprintf(data,"key%d_press ",key_value); //LCD显示哪个按键按下 LCD_ShowString(10, 10+1*20, tftlcd_data.width, tftlcd_data.height, 12, data); } else if(key_value==KEY_UP)//唤醒按键按下将字符串写入到Queue_stringHandle队列中 { result=xQueueSend( Queue_stringHandle, &str , portMAX_DELAY ); LCD_ShowString(10, 10+1*20, tftlcd_data.width, tftlcd_data.height, 12, "key_up_press"); if(result==pdTRUE) { LCD_ShowString(10, 10+2*20, tftlcd_data.width, tftlcd_data.height, 12, "string_write_ok"); } } vTaskDelay(20); } /* USER CODE END AppTask_keyscan */ }
(6)读取按键队列,将接收到的按键值在lcd显示
void AppTask_keyshow(void *argument) { /* USER CODE BEGIN AppTask_keyshow */ uint8_t key; /* Infinite loop */ for(;;) { //参数1是读取的队列句柄,参数2:读取到的数值存放的变量,参数3:一直等到,返回值:是否读取成功 BaseType_t result= xQueueReceive( Queue_keysHandle, &key,portMAX_DELAY ); if(result=pdTRUE) { uint8_t str[20]; sprintf(str,"press key is %d ",key);//显示接收到的按键值 LCD_ShowString(10, 10+4*20, tftlcd_data.width, tftlcd_data.height, 12, str); } } /* USER CODE END AppTask_keyshow */ }
(7)读取字符串队列,将接收到的数据在lcd上显示
void AppTask_stringshow(void *argument) { /* USER CODE BEGIN AppTask_stringshow */ char* str;//存放接收的字符串数据 /* Infinite loop */ for(;;) { BaseType_t result= xQueueReceive( Queue_stringHandle, &str,portMAX_DELAY ); if(result=pdTRUE) { uint8_t data[20]; sprintf(data,"RXstr is %s ",str); LCD_ShowString(10, 10+4*20, tftlcd_data.width, tftlcd_data.height, 12, data); } } /* USER CODE END AppTask_stringshow */ }
编译下载即可
例如:要传输一个字符串长度为10的数据,设置不同的队列长度和不同的项大小有啥区别,要传输的数据和队列的创建上有啥关系?
在定义队列和项的大小时,不同的配置会对数据传输和处理产生不同的影响。
综上所述,选择哪种配置取决于具体的应用需求:
(1)如果需要逐字符或逐对字符处理,第一项和第二项配置可能更合适。
(2)如果需要并行处理或传输多个部分,第三项配置可能更好。
(3)如果整个字符串作为一个单元被处理或传输,并且不需要进一步分割,那么第四项配置可能最简单。
在实际应用中,还需要考虑网络带宽、延迟、内存使用和其他系统资源来做出最佳决策。
(1)按键扫描任务:向队列中写入数据
void AppTASK_Scankeys(void *argument) { /* USER CODE BEGIN AppTASK_Scankeys */ //扫描键�?�并写入到队列中 GPIO_PinState keystate=GPIO_PIN_SET;//按键是低电平有效,初始化为高电平 KEYS key=KEY_NONE;//按键值暂时初始化为按键按�? /* Infinite loop */ for(;;) { key=KEY_NONE; keystate=HAL_GPIO_ReadPin(KEY0_GPIO_Port, KEY0_Pin); if(keystate==GPIO_PIN_RESET)//表明按键0按下 { key=KEY_0;//赋�?�为枚举型变�? } keystate=HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin); if(keystate==GPIO_PIN_RESET)//表明按键0按下 { key=KEY_1;//赋�?�为枚举型变�? } keystate=HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin); if(keystate==GPIO_PIN_RESET)//表明按键0按下 { key=KEY_2;//赋�?�为枚举型变�? } keystate=HAL_GPIO_ReadPin(KEY_UP_GPIO_Port, KEY_UP_Pin); if(keystate==GPIO_PIN_SET)//表明按键0按下 { key=KEY_UP;//赋�?�为枚举型变�? } //如果按键按下了,则向队列中写�? if(key!=KEY_NONE) { //队列句柄,放进队列的项(把key的地�?放进去)�?50ms阻塞延时 BaseType_t err= xQueueSend(Queue_keysHandle,&key,50);//返回数据类型为BaseType_t if(err==errQUEUE_FULL)//如果队列已经满了 { //队列复位 xQueueReset(Queue_keysHandle); } vTaskDelay(300);//�?300ms只允许传入一个键值(起到�?个消抖的作用�? } else//没有扫描到按键的情况 { //如果不写这一条其他任务无法进行调�? vTaskDelay(5);//5ms的延迟(进入阻塞状�?�让其他任务得到调度�? } osDelay(1); } /* USER CODE END AppTASK_Scankeys */ }
(2)画线任务:读取队列数据
四个按键分别代表上下左右方向,根据不同的按键值往不同的方向画线
void AppTASK_Draw(void *argument) { /* USER CODE BEGIN AppTASK_Draw */ //读取队列数据并在lcd操作 //const char *pcQueueGetName( QueueHandle_t xQueue ) //先测试获取任务名称函�? uint8_t queueName[30]; sprintf(queueName,"queue name =%s",pcQueueGetName(Queue_keysHandle)); LCD_ShowString(10, 10+1*20,tftlcd_data.width,tftlcd_data.height, 12, queueName); //打印队列的大�? uint8_t queue_size[30]; sprintf(queue_size,"queue size =%d",uxQueueSpacesAvailable(Queue_keysHandle)); LCD_ShowString(10, 10+2*20,tftlcd_data.width,tftlcd_data.height, 12, queue_size); KEYS keyvalue; /* Infinite loop */ for(;;) { //等待读取的消息个�? uint8_t tempStr[30]; sprintf(tempStr,"queue wait number:%d",uxQueueMessagesWaiting(Queue_keysHandle)); LCD_ShowString(10, 10+3*20,tftlcd_data.width,tftlcd_data.height, 12, tempStr); //剩余的空间个�?(当前) sprintf(tempStr,"queue size =%d",uxQueueSpacesAvailable(Queue_keysHandle)); LCD_ShowString(10, 10+4*20,tftlcd_data.width,tftlcd_data.height, 12, tempStr); //下面获取任务高水位�?�来判断是否会发生栈空间溢出(如果某个任务高位�?�接�?0,就有栈溢出的风险,�?要增加栈空间大小�? sprintf(tempStr,"TASK_Draw HighWaterMark =%d",uxTaskGetStackHighWaterMark( TASK_DrawHandle )); LCD_ShowString(10, 10+5*20,tftlcd_data.width,tftlcd_data.height, 12, tempStr); sprintf(tempStr," TASK_Scankeys HighWaterMark=%d",uxTaskGetStackHighWaterMark( TASK_ScankeysHandle )); LCD_ShowString(10, 10+6*20,tftlcd_data.width,tftlcd_data.height, 12, tempStr); // //读取消息队列 if(xQueueReceive(Queue_keysHandle,&keyvalue,50)!=pdTRUE) { continue;//继续运行,跳�? } if(keyvalue==KEY_0)//�? { curScreenX+=10; } else if(keyvalue==KEY_2) { curScreenX-=10; } else if(keyvalue==KEY_1) { curScreenY+=10; } else if(keyvalue==KEY_UP) { curScreenY-=10; } //如果坐标大于屏幕尺寸320*480则进行处�? if(curScreenX>tftlcd_data.width)//限制x坐标 { curScreenX=tftlcd_data.width; } if(curScreenY>tftlcd_data.height)//限制y坐标 { curScreenY=tftlcd_data.height; } LCD_DrawLine(lastScreenX, lastScreenY, curScreenX, curScreenY); // LCD_DrawLine_Color(lastScreenX, lastScreenY, curScreenX, curScreenY,RED); //记录�?下当前的变量 lastScreenX=curScreenX; lastScreenY=curScreenY; vTaskDelay(400); } /* USER CODE END AppTASK_Draw */ }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。