目录
(1)RCC、SYS、Code Generator、USART3、NVIC
进程间同步与通信是一个操作系统的基本功能,FreeRTOS提供了完善的进程间通信功能:包括消息队列、信号量、互斥量、事件组、任务通知等。其中,消息队列是信号量和互斥量的基础,研究进程间通信要先从进程间通信的基本概念以及消息队列的原理和使用开始,再逐步研究信号量、互斥量等其他进程间通信方式。
一、进程间通信技术
在使用RTOS的系统中,有多个任务,还可以有多个中断的ISR,任务和ISR可以统称为进程(process)。任务与任务之间,或任务与ISR之间,有时需要进行通信或同步,这称为进程间通信(Inter-Process Communication,IPC)。下图中所示的是使用RTOS和进程间通信时,ADC连续数据采集与处理的一种工作方式示意图,这个图中各个部分的功能解释如下:
- ADC中断ISR负责在ADC完成一次转换触发中断时,读取转换结果,然后写入数据缓冲区。
- 数据处理任务负责读取数据缓冲区里的ADC转换结果数据,然后进行处理,例如,进行滤波、频谱计算,或保存到SD卡上。
- 数据缓冲区负责临时保存ADC转换结果数据。在实际的ADC连续数据采集中,一般使用双缓冲区,一个缓冲区存满之后,用于读取和处理,另一个缓冲区继续用于保存ADC转换结果数据。两个缓冲区交替使用,以保证采集和处理的连续性。
- 进程间通信就是ADC中断ISR与数据处理任务之间的通信。在ADC中断ISR向缓冲区写入数据后,如果发现缓冲区满了,就可以发出一个标志信号,通知数据处理任务,一直在阻塞状态下等待这个信号的数据处理任务就可以退出阻塞状态,被调度为运行状态后,就可以及时读取缓冲区的数据并处理。
FreeRTOS提供了多种进程间通信技术,各种技术有各自的特点和用途:
- 队列(queue)。队列就是一个缓冲区,用于在进程间传递少量的数据,所以也称为消息队列。队列可以存储多个数据项,一般采用先进先出(FIFO)的方式,也可以采用后进先出(LIFO)的方式。
- 信号量(semaphore),分为二值信号量(binary semaphore)和计数信号量(counting semaphore )。二值信号量用于进程间同步,计数信号量一般用于共享资源的管理。二值信号量没有优先级继承机制,可能出现优先级翻转问题。
- 互斥量(mutex),分为互斥量(mutex)和递归互斥量(recursive mutex)。互斥量可用于互斥性共享资源的访问。互斥量具有优先级继承机制,可以减轻优先级翻转的问题。
- 事件组(event group)。事件组适用于多个事件触发一个或多个任务的运行,可以实现事件的广播,还可以实现多个任务的同步运行。
- 任务通知(task notification)。使用任务通知不需要创建任何中间对象,可以直接从任务向任务,或从ISR向任务发送通知,传递一个通知值。任务通知可以模拟二值信号量、计数信号量,或长度为1的消息队列。使用任务通知,通常效率更高,消耗内存更少。
- 流缓冲区(stream buffer)和消息缓冲区(message buffer)。流缓冲区和消息缓冲区是FreeRTOS V10.0.0版本新增的功能,是一种优化的进程间通信机制,专门应用于只有一个写入者(writer)和一个读取者(reader)的场景,还可用于多核CPU的两个内核之间的高效数据传输。
二、队列的特点和基本操作
1、队列的创建和存储
队列是FreeRTOS中的一种对象,可以使用函数xQueueCreate()或xQueueCreateStatic()创建。创建队列时,会给队列分配固定个数的存储单元,每个存储单元可以存储固定大小的数据项,进程间需要传递的数据就保存在队列的存储单元里。
函数xQueueCreate()是以动态分配内存方式创建队列,队列需要用的存储空间由FreeRTOS自动从堆空间分配。函数xQueueCreateStatic()是以静态分配内存方式创建队列,静态分配内存时,需要为队列创建存储用的数组,以及存储队列信息的结构体变量。在FreeRTOS中创建对象,如任务、队列、信号量等,都有静态分配内存和动态分配内存两种方式。
函数xQueueCreate()实际上是一个宏函数,其原型定义如下:
/**
* \defgroup xQueueCreate xQueueCreate
* \ingroup QueueManagement
*/
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
#define xQueueCreate( uxQueueLength, uxItemSize ) xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )
#endif
xQueueCreate()调用了函数xQueueGenericCreate(),这个是创建队列、信号量、互斥量等对象的通用函数。xQueueGenericCreate()的原型定义如下:
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, const uint8_t ucQueueType )
{
Queue_t *pxNewQueue;
size_t xQueueSizeInBytes;
uint8_t *pucQueueStorage;
//此处省略1万字
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
其中,参数uxQueueLength表示队列的长度,也就是存储单元的个数;参数uxItemSize是每个存储单元的字节数;参数ucQueueType表示创建的对象的类型,有以下几种常数取值:
#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) //递归互斥量
函数xQueueGenericCreate()的返回值是QueueHandle_t类型,是所创建队列的句柄,这个类型实际上是一个指针类型,定义如下:
typedef void *QueueHandle_t;
函数xQueueCreate()调用xQueueGenericCreate()时,传递了类型常数queueQUEUE_TYPE _BASE,所以创建的是一个基本的队列。调用函数xQueueCreate()的示例如下:
Queue_KeysHandle = xQueueCreate(5,sizeof(uint16_t));
这行代码创建了一个具有5个存储单元的队列,每个单元占用sizeof(uint16_t)字节,也就是2字节。
队列的存储单元可以设置任意大小,因而可以存储任意数据类型,例如,可以存储一个复杂结构体的数据。队列存储数据采用数据复制的方式,如果数据项比较大,复制数据会占用较大的存储空间。所以,如果传递的是比较大的数据,例如,比较长的字符串或大的结构体,可以在队列的存储单元里存储需要传递数据的指针,通过指针再去读取原始数据。
2、向队列写入数据
一个任务或ISR向队列写入数据称为发送消息,可以FIFO方式写入,也可以LIFO方式写入。
队列是一个共享的存储区域,可以被多个进程写入,也可以被多个进程读取。下图所示的是多个进程以FIFO方式向队列写入消息的示意图,先写入的靠前,后写入的靠后。
向队列后端写入数据(FIFO模式)的函数是xQueueSendToBack(),它是一个宏函数,其原型定义如下:
#define xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait )
xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )
宏函数xQueueSendToBack()调用了函数xQueueGenericSend(),这是向队列写入数据的通用函数,其原型定义如下:
BaseType_t xQueueGenericSend( QueueHandle_t xQueue, const void * const pvItemToQueue, TickType_t xTicksToWait, const BaseType_t xCopyPosition )
{
//
}
其中,参数xQueue是所操作队列的句柄;参数pvItemToQueue是需要向队列写入的一个项的数据;参数xTicksToWait是阻塞方式等待队列出现空闲单元的节拍数,为0时,表示不等待,为常数portMAX_DELAY时,表示一直等待,为其他的数时,表示等待的节拍数;参数xCopyPosition表示写入队列的位置,有3种常数定义。
#define queueSEND_TO_BACK ((BaseType_t)0) //写入后端,FIFO方式
#define queueSEND_TO_FRONT ((BaseType_t)1) //写入前段,LIFO方式
#define queueOVERWRITE ((BaseType_t)2) //尾端覆盖,在队列满时
向队列前端写入数据(LIFO方式),就使用函数xQueueSendToFront(),它也是一个宏函数,在调用函数xQueueGenericSend()时,为参数xCopyPosition传递值queueSEND_TO_FRONT。
#define xQueueSendToFront( xQueue, pvItemToQueue, xTicksToWait )
xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_FRONT )
在队列未满时,函数xQueueSendToBack()和xQueueSendToFront()能正常向队列写入数据,函数返回值为pdTRUE;在队列已满时,这两个函数不能再向队列写入数据,函数返回值为errQUEUE_FULL。
还有一个函数xQueueOverwrite()也可以向队列写入数据,但是这个函数只用于队列长度为1的队列,在队列已满时,它会覆盖队列原来的数据。xQueueOverwrite()是一个宏函数,也是调用函数xQueueGenericSend(),其原型定义如下:
#define xQueueOverwrite( xQueue, pvItemToQueue )
xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), 0, queueOVERWRITE )
3、从队列读取数据
可以在任务或ISR里读取队列的数据,称为接收消息。图4-4所示的是一个任务从队列读取数据的示意图。读取数据总是从队列首端读取,读出后删除这个单元的数据,如果后面还有未读取的数据,就依次向队列首端移动。
从队列读取数据的函数是xQueueReceive(),其原型定义如下:
BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait )
{
//
}
其中,xQueue是所操作的队列句柄;pvBuffer是缓冲区,用于保存从队列读出的数据;xTicksToWait是阻塞方式等待节拍数,为0时,表示不等待,为常数portMAX_DELAY时,表示一直等待,为其他数时,表示等待的节拍数。
函数的返回值为pdTRUE时,表示从队列成功读取了数据,返回值为pdFALSE时,表示读取不成功。
在一个任务里执行函数xQueueReceive()时,如果设置了等待节拍数并且队列里没有数据,任务就会转入阻塞状态并等待指定的时间。如果在此等待时间内,队列里有了数据,这个任务就会退出阻塞状态,进入就绪状态,再被调度进入运行状态后,就可以从队列里读取数据了。如果超过了等待时间,队列里还是没有数据,函数xQueueReceive()会返回pdFALSE,任务退出阻塞状态,进入就绪状态。
还有一个函数xQueuePeek()也是从队列里读取数据,其功能与xQueueReceive()类似,只是读出数据后,并不删除队列中的数据。
4、队列操作相关函数
除了在任务函数里操作队列,用户在ISR里也可以操作队列,但是在ISR里操作队列,必须使用相应的中断级函数,即带有后缀"FromISR"的函数。
FreeRTOS中队列操作的相关函数见表:
功能分组 | 函数名 | 功能描述 |
队列管理 | xQueueCreate() | 动态分配内存方式创建一个队列 |
xQueueCreateStatic() | 静态分配内存方式创建一个队列 | |
xQueueReset() | 将队列复位为空的状态,丢弃队列内的所有数据 | |
vQueueDelete() | 删除一个队列,也可用于删除一个信号量 | |
获取队列 | pcQueueGetName() | 获取队列的名称,也就是创建队列时设置的队列名称 |
vQueueSetQueueNumber() | 为队列设置一个编号,这个编号由用户设置并使用 | |
uxQueueGetQueueNumber() | 获取队列的编号 | |
uxQueueSpacesAvailable() | 获取队列剩余空间个数,也就是还可以写入的消息个数 | |
uxQueueMessagesWaiting() | 获取队列中等待被读取的消息个数 | |
uxQueueMessagesWaitingFromISR() | uxQueueMessagesWaiting()的ISR版本 | |
xQueueIsQueueEmptyFromISR() | 查询队列是否为空,返回值为pdTRUE表示队列为空 | |
xQueueIsQueueFullFromISR() | 查询队列是否已满,返回值为pdTRUE表示队列已满 | |
写入消息 | xQueueSend() | 将一个消息写到队列的后端(FIFO方式),这个函数 |
xQueueSendFromISR() | xQueueSend()的ISR版本 | |
xQueueSendToBack() | 与xQueueSend()功能完全相同,建议使用这个函数 | |
xQueueSendToBackFromISR() | xQueueSendToBack()的ISR版本 | |
xQueueSendToFront() | 将一个消息写到队列的前端(LIFO方式) | |
xQueueSendToFrontFromISR() | xQueueSendToFront()的ISR版本 | |
xQueueOverwrite() | 只用于长度为1的队列,如果队列已满,会覆盖原来 | |
xQueueOverwriteFromISR() | xQueueOverwrite()的ISR版本 | |
读取消息 | xQueueReceive() | 从队列中读取一个消息,读出后删除队列中的这个消息 |
xQueueReceiveFromISR() | xQueueReceive()的ISR版本 | |
xQueuePeek() | 从队列中读取一个消息,读出后不删除队列中的这个 | |
xQueuePeekFromISR() | xQueuePeek()的ISR版本 |
表中有一组函数是用于获取队列信息的,例如:
- 函数pcQueueGetName()返回队列的字符串名称;
- 函数uxQueueSpacesAvailable()返回队列剩余空间个数;
- 函数uxQueueMessagesWaiting()返回队列中等待被读取的消息的个数。
三、队列使用示例
1、示例功能和CubeMX项目设置
该示例用来演示队列的使用。依然使用旺宝红龙开发板STM32F407ZGT6 KIT V1.0。
该示例的主要功能是创建一个队列和两个任务:一个任务负责查询4个按键的状态,某个按键被按下时就向队列中写入代表此按键的值;另外一个任务负责读取队列的数据,计数,并在串口助手上显示统计数字。
本示例要用到4个按键,引用KEYLED文件夹里的文件。一些设置可以参考本文作者发布的其他文章:细说STM32单片机FreeRTOS中断管理及其应用方法-优快云博客 https://wenchm.blog.youkuaiyun.com/article/details/147326700?spm=1011.2415.3001.5331
(1)RCC、SYS、Code Generator、USART3、NVIC
与参考文章相同。NVIC默认。
其中,USART3和串口助手的波特率设置为1200, 为的是获得更好的显示效果。默认的波特率,数据更新太快了。
(2)GPIO
使用了开发板上的4个按键。
(3) FreeRTOS
建立2个任务、1个消息队列。其它的内容选择默认。
其中关于队列需要设置的属性,解析如下:
- Queue Name,队列名称。队列的字符串名称,这个名称可以通过函数pcQueueGetName()获取。
- Queue Size,队列大小。这个值是队列能存储的消息个数。
- Item Size,每个项的大小。也就是每个消息所占存储单元的大小,单位是字节。如果项是标准的数据类型,如uint8_t、uint16_t等,可以直接用数据类型表示;如果项是结构体等复杂的数据类型,可以直接填写字节数。
- Allocation,内存分配方式。可以设置为Dynamic(动态)或Static(静态)。设置为Dynamic时,队列占用的内存空间由FreeRTOS自动分配,后面的3个参数无须设置;设置为Static时,需要设置后面的Buffer Name和Control Block Name这两个参数。
- Buffer Name,缓冲区名称。静态分配内存时缓冲区数组的名称,缓冲区用于存储消息的数据。
- Buffer Size,缓冲区大小。静态分配内存时,自动根据队列长度和每个项的大小计算出的缓冲区大小,单位是字节。
- Control Block Name,控制块名称。静态分配内存时,需要定义一个结构体变量作为队列的控制块。
本示例以动态分配内存方式创建队列,队列长度为10,每个消息是一个uint8_t的数据,所以每个项的大小是1字节。
2、软件设计
(1)main.c
完成设置后,在CubeMX中自动生成代码。添加的用户代码如下:
/* USER CODE BEGIN 2 */
uint8_t startstr[] = "Demo4-1_Queue: test queue.\r\n\r\n";
HAL_UART_Transmit(&huart3,startstr,sizeof(startstr),0xFFFF);
/* USER CODE END 2 */
(2)freertos.c 创建任务和队列
/* Includes ------------------------------------------------------------------*/
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "keyled.h"
#include "queue.h"
#include <stdio.h>
#include "usart.h"
/* USER CODE END Includes */
自动创建任务和队列的相关定义:
/* Definitions for Task_Count */
osThreadId_t Task_CountHandle;
const osThreadAttr_t Task_Count_attributes = {
.name = "Task_Count",
.stack_size = 128 * 4,
.priority = (osPriority_t) osPriorityBelowNormal,
};
/* Definitions for Task_ScanKeys */
osThreadId_t Task_ScanKeysHandle;
const osThreadAttr_t Task_ScanKeys_attributes = {
.name = "Task_ScanKeys",
.stack_size = 128 * 4,
.priority = (osPriority_t) osPriorityNormal,
};
/* Definitions for Queue_Keys */
osMessageQueueId_t Queue_KeysHandle;
const osMessageQueueAttr_t Queue_Keys_attributes = {
.name = "Queue_Keys"
};
自动生成任务函数和队列的原型:
/* Create the queue(s) */
/* creation of Queue_Keys */
Queue_KeysHandle = osMessageQueueNew (10, sizeof(uint8_t), &Queue_Keys_attributes);
/* USER CODE BEGIN RTOS_QUEUES */
/* add queues, ... */
/* USER CODE END RTOS_QUEUES */
/* Create the thread(s) */
/* creation of Task_Count */
Task_CountHandle = osThreadNew(AppTask_Count, NULL, &Task_Count_attributes);
/* creation of Task_ScanKeys */
Task_ScanKeysHandle = osThreadNew(AppTask_ScanKeys, NULL, &Task_ScanKeys_attributes);
自动生成任务函数的框架,手工填写函数体,在任务函数体中读写消息队列:
/* USER CODE BEGIN Header_AppTask_Count */
/**
* @brief Function implementing the Task_Count thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_AppTask_Count */
void AppTask_Count(void *argument)
{
/* USER CODE BEGIN AppTask_Count */
// Read queue information
const char* qName=pcQueueGetName(Queue_KeysHandle);
printf("Queue Name = %s.\r\n",qName);
UBaseType_t qSpaces=uxQueueSpacesAvailable(Queue_KeysHandle);
printf("Queue Size = %ld.\r\n\r\n",qSpaces);
UBaseType_t msgCount=0, freeSpace=0;
uint8_t count_keyleft_pressed=0;
uint8_t count_keyright_pressed=0;
uint8_t count_keyup_pressed=0;
uint8_t count_keydown_pressed=0;
KEYS keyCode;
/* Infinite loop */
for(;;)
{
msgCount=uxQueueMessagesWaiting(Queue_KeysHandle); //messages Count
printf("Number of messages in the queue = %ld.\r\n",msgCount);
freeSpace=uxQueueSpacesAvailable(Queue_KeysHandle); //freeSpace
printf("free space in the queue = %ld.\r\n",freeSpace);
// read
BaseType_t result=xQueueReceive(Queue_KeysHandle, &keyCode, pdMS_TO_TICKS(50));
if (result != pdTRUE)
continue;
// read messages and process.
if (keyCode==KEY_LEFT){
count_keyleft_pressed++;
printf("count_keyleft_pressed = %d.\r\n",count_keyleft_pressed);}
else if (keyCode==KEY_RIGHT){
count_keyright_pressed++;
printf("count_keyright_pressed = %d.\r\n",count_keyright_pressed);}
else if (keyCode==KEY_UP){
count_keyup_pressed++;
printf("count_keyup_pressed = %d.\r\n",count_keyup_pressed);}
else if (keyCode==KEY_DOWN){
count_keydown_pressed++;
printf("count_keydown_pressed = %d.\r\n",count_keydown_pressed);}
vTaskDelay(pdMS_TO_TICKS(500));
}
/* USER CODE END AppTask_Count */
}
/* USER CODE BEGIN Header_AppTask_ScanKeys */
/**
* @brief Function implementing the Task_ScanKeys thread.
* @Scan the key and write to the queue.
* @After writen, vTaskDelay(300) is called,
* @to be used to de-jitter, and at the same time turn the task into blocking state.
* @Low priority tasks into the running state, read and process messages in the queue timely.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_AppTask_ScanKeys */
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;
//S5=KeyRight
keyState=HAL_GPIO_ReadPin(KeyRight_GPIO_Port, KeyRight_Pin);
if (keyState==GPIO_PIN_RESET)
key=KEY_RIGHT;
//S3=KeyDown
keyState=HAL_GPIO_ReadPin(KeyDown_GPIO_Port, KeyDown_Pin);
if (keyState==GPIO_PIN_RESET)
key=KEY_DOWN;
//S4=KeyLeft
keyState=HAL_GPIO_ReadPin(KeyLeft_GPIO_Port, KeyLeft_Pin);
if (keyState==GPIO_PIN_RESET)
key=KEY_LEFT;
//S2=KeyUp
keyState=HAL_GPIO_ReadPin(KeyUp_GPIO_Port, KeyUp_Pin);
if (keyState==GPIO_PIN_RESET)
key=KEY_UP;
//key pressed.
if (key != KEY_NONE)
{
BaseType_t err= xQueueSendToBack(Queue_KeysHandle, &key, pdMS_TO_TICKS(50));
if (err == errQUEUE_FULL) //Reset once the queue is full.
xQueueReset(Queue_KeysHandle);
vTaskDelay(pdMS_TO_TICKS(300)); //It will take at least 300 to de-jitter and perform task scheduling.
}
else
vTaskDelay(pdMS_TO_TICKS(5)); //vTaskDelay() in for(;;) loop , turned into blocking state.
}
/* USER CODE END AppTask_ScanKeys */
}
增加的私有应用,用于实现串口打印到串口助手:
/* Private application code --------------------------------------------------*/
/* USER CODE BEGIN Application */
int __io_putchar(int ch)
{
HAL_UART_Transmit(&huart3,(uint8_t*)&ch,1,0xFFFF);
return ch;
}
/* USER CODE END Application */
3、运行与调试
构建完成后,发现默认的串口波特率数据更新太快了,不利于观察。为了改善观察效果,修改串口波特率为1200。
开发板上的按键S2\S3\S4\S5,分别对应按键定义上、下、左、右。
如果不按任何键 ,串口助手连续显示“消息数=0”和“可用空间=10”,说明只有低优先级的任务AppTask_Count()在运行,高优先级的任务AppTask_ScanKeys()超过pdMS_TO_TICKS(50)的50ms的阻塞等待时间后,没有按下任何键,退出阻塞状态,进入就绪状态,时刻等待RTOS调度进入运行状态,只要这时按下任何一键,AppTask_ScanKeys()就会运行,并向队列写入消息。
(1)扫描按键和发送消息
任务Task_ScanKeys的功能是扫描按键,将被按下的按键值写入队列。开发板上的4个按键都是低输入有效。表示按键的枚举类型KEYS是在文件keyled.h中定义的,这个枚举类型定义如下:
//表示4个按键的枚举类型
typedef enum {
KEY_NONE = 0, //没有按键被按下
KEY_LEFT, //KeyLeft键
KEY_RIGHT, //KeyRight键
KEY_UP, //KeyUp键
KEY_DOWN, //KeyDown键
}KEYS;
程序检测到某个键被按下后,将按键类型值赋值给变量key。如果检测到有键按下,调用函数xQueueSendToBack(),将按键代码写入队列:
//key pressed.
if (key != KEY_NONE)
{
BaseType_t err= xQueueSendToBack(Queue_KeysHandle, &key, pdMS_TO_TICKS(50));
if (err == errQUEUE_FULL) //Reset once the queue is full.
xQueueReset(Queue_KeysHandle);
vTaskDelay(pdMS_TO_TICKS(300)); //It will take at least 300 to de-jitter and perform task scheduling.
}
else
vTaskDelay(pdMS_TO_TICKS(5)); //vTaskDelay() in for(;;) loop , turned into blocking state.
程序在调用xQueueSendToBack()时设置了阻塞等待时间,但是队列长度是10,一般总是有剩余空间,所以该函数会立刻返回。在执行完写入队列后,又调用函数vTaskDelay()延时300ms,这是用软件延时的方式消除按键抖动的影响,同时又使任务Task_ScanKeys进入阻塞状态,让低优先级的任务Task_Count可以进入运行状态,及时读取队列里的消息并处理。
(2)读取消息并计数
任务Task_Count的主要功能是读取队列里的按键代码并记录按键被按下的次数,然后在串口助手上显示该数。程序在进入for循环之前,调用函数pcQueueGetName()获取了队列的名称,调用函数uxQueueSpacesAvailable()获取队列的剩余空间个数。在程序刚运行起来时,没有消息进入队列,这个剩余空间就是队列的大小。
在for循环内,调用函数uxQueueMessagesWaiting()读取队列中等待读取的消息条数,调用函数uxQueueSpacesAvailable()读取剩余空间个数。在程序运行时,按下某个按键,或连续快速按下多个按键,会看到串口助手上显示的这两个数是变化的。
程序使用函数xQueueReceive()读取队列中的消息,调用的语句如下:
//读取队列里的消息, 阻塞式等待
BaseType_t result=xQueueReceive(Queue_KeysHandle, &keyCode, pdMS_TO_TICKS(50));
这里设置了等待时间为50ms,在执行这条语句时,如果队列中没有消息,任务Task_Count就会进入阻塞状态,等待时间最多为50ms。如果队列中有了消息,就会将读取的消息数据保存到变量keyCode中,任务Task_Count退出阻塞状态,进入就绪状态。如果函数xQueueReceive()的返回值不是pdTRUE,表示超过了阻塞等待时间,仍然没有消息可读。
如果成功读取了一条消息,消息的数据就是一个按键的枚举数值,程序根据按键码计数,然后在串口助手上显示数值。
for循环的最后调用函数vTaskDelay()延时500ms,是为了人为地造成比较大的延时。这样,在快速连续按下按键时,会看到串口助手上待读取消息条数可以达到2或3。如果将这个延时减小,可以使串口助手显示更新的响应变得更快。