STM32F1+HAL库+FreeTOTS学习12——队列
- 1. 队列介绍
- 1.1 什么是队列?
- 1.2 队列的引入
- 1.3 队列的数据存储
- 1.4 多任务访问
- 1.5 队列读取阻塞
- 1.6 队列写入阻塞
- 1.7 数据传递方式
- 1.8 队列操作
- 2. 队列结构体
- 3. 队列相关API函数
- 3.1 队列创建
- 3.1.1 动态创建
- 3.1.2 静态创建
- 3.2 队列写入
- 3.3 队列读出
- 4. 队列操作实验
- 1. 实验内容
- 2. 代码实现:
- 3. 运行结果
上一期我们学习了延时函数API相关的使用,这一期我们学习FreeRTOS中另一个重要的内容:队列
1. 队列介绍
1.1 什么是队列?
队列是一种任务到任务、任务到中断、中断到任务数据交流的一种机制。在队列中可以存储数量有限、大小固定的多个数据,队列中的每一个数据叫做队列项目,队列能够存储队列项目的最大数量称为队列的长度,在创建队列的时候,就需要指定所创建队列的长度及队列项目的大小。因为队列是用来在任务与任务或任务于中断之间传递消息的一种机制,因此队列也叫做消息队列。
基于队列,FreeRTOS 实现了多种功能,其中包括队列集、互斥信号量、计数型信号量、二值信号量、递归互斥信号量,因此很有必要深入了解 FreeRTOS 的队列。在一期内容中,我们介绍队列的使用,其他的内容在后续更新。
1.2 队列的引入
我们知道,在单片机的内部一条指令一条指令的执行的,而我们通过编程成语写出的代码,往往会转化成几条指令的组合,举个简单的例子:
我们在FreeRTOS里面定义了一个全局变量a,在任务1里面执行a++;那么在单片机内部,就会先读取a的值到CPU寄存器(这里假设是r0
寄存器),然后对r0寄存器加1,最后将r0寄存器的值写入到a当中,才完成了a++。
但如果说现在有两个任务(任务1和任务2),都需要执行a++这条指令(假设初始 a = 1)。就有可能导致任务1a++的过程中(运行到第二部,第三步还未进行,这个时候r0 = 2),由于任务切换(任务切换,所以r0的值被保存起来),执行任务2(任务2完整运行完,a = 2)之后再切换回任务1(这个时候r0恢复,r0 = 2),继续执行第三步(导致a = r0),之后a的值还是2,就导致以下结果:经过两个任务执行a++,本来a应该等于3,实际上却等于2。
很明显这个情况是不被允许的,为了避免这种情况,我们有以下两个解决办法:
- 避免使用全局变量,从根源上避免问题,当然这个是不切实际的
- 引入消息队列,实现任务之间的信息传递,数据交流。这个是我们需要学习的
1.3 队列的数据存储
与堆栈不同,队列通常采用 FIFO(先进先出)的存储缓冲机制,当有新的数据被写入队列中时,永远都是写入到队列的尾部,而从队列中读取数据时,永远都是读取队列的头部数据。 总之记住四个字:先进先出!!!
- FreeRTOS的队列也支持将数据写入到队列的头部,并且还可以指定是否覆盖先前已经在队列头部的数据。
- 在队列中可以存储数量有限、大小固定的数据。队列中的每一个数据叫做“队列项目”,队列能够存储“队列项目”的最大数量称为队列的长度
- 在创建队列时,就要指定队列长度以及队列项目的大小!
1.4 多任务访问
队列不属于某个特定的任务,可以在任何的任务或中断中往队列中写入消息,或者从队列中读取消息。
- 一个队列可以被多个任务读取,如果有多个任务等待读取队列的的消息,一旦队列中有消息,将会按照任务优先级和阻塞的先后,决定那个任务优先读取。
- 同理也可以有多个任务写入队列,如果有多个任务等待写入队列(此时队列已满),一旦队列中有空位,将会按照任务优先级和阻塞的先后,决定那个任务优先写入。
1.5 队列读取阻塞
- 在任务从队列读取消息时,可以指定一个阻塞超时时间。如果任务在读取队列时,队列为空,这时任务将被根据指定的阻塞超时时间添加到阻塞态任务列表中进行阻塞,以等待队列中有可用的消息。
- 当有其他任务或中断将消息写入队列中,因等待队列而阻塞任务将会被添加到就绪态任务列表中,并读取队列中可用的消息。
- 如果任务因等待队列而阻塞的时间超过指定的阻塞超时时间,那么任务也将自动被转移到就绪态任务列表中,但不再读取队列中的数据。
1.6 队列写入阻塞
- 与队列读取一样,在任务往队列写入消息时,也可以指定一个阻塞超时时间。如果任务在写入队列时,队列已经满了,这时任务将被根据指定的阻塞超时时间添加到阻塞态任务列表中进行阻塞,以等待队列有空闲的位置可以写入消息。
- 指定的阻塞超时时间为任务阻塞的最大时间,如果在阻塞超时时间到达之前,队列有空闲的位置,那么队列写入阻塞任务将会解除阻塞,并往队列中写入消息。
- 如果达到指定的阻塞超时时间,队列依旧没有空闲的位置写入消息,那么队列写入阻塞任务将会自动转移到就绪态任务列表中,但不会往队列中写入消息。
1.7 数据传递方式
reeRTOS中队列采用实际值传递,即将数据拷贝到队列中进行传递, FreeRTOS采用拷贝数据传递,也可以传递指针,所以在传递较大的数据的时候采用指针传递
1.8 队列操作
下图描述了两个任务,任务A写入消息,任务B读出数据。
- 创建队列
- 写入第一个消息
- 写入第二个消息
- 读出第一个消息
2. 队列结构体
队列的结构体为 Queue_t,在 queue.c 文件中有定义,具体的代码如下所示:
typedef struct QueueDefinition
{int8_t * pcHead; /*< 指向队列项消息存储区的指针 */int8_t * pcWriteTo; /*< 指向队列项消息存储器下一个写入位置的指针 *//*信号量是由队列实现的,此结构体区别队列和信号量当用于队列时,使用联合体中的xQueue当用于信号量时,使用联合体中的xSemaphore*/union{QueuePointers_t xQueue; SemaphoreData_t xSemaphore;} u;List_t xTasksWaitingToSend; /* 写入阻塞任务列表 */List_t xTasksWaitingToReceive; /* 读取阻塞任务列表 */volatile UBaseType_t uxMessagesWaiting; /*< 非空闲项目的数量,当为0时,表示队列已满 */UBaseType_t uxLength; /* 队列的长度 */UBaseType_t uxItemSize; /*< 队列项的大小 *//*锁用于在任务因队列操作被阻塞之前,防止中断或其他任务操作队列上锁期间,队列可以写入和读取消息,但不会操作队列阻塞任务列表当有消息写入时,cTxLock加1,当有消息读出时,cRxLock加1在解锁时,会统一处理队列的阻塞任务列表*/volatile int8_t cRxLock; /*< 读取上锁计数器 */volatile int8_t cTxLock; /*< 写入上锁计数器 *//* 同时启用了静态和动态内存管理 */#if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )uint8_t ucStaticallyAllocated; /* 定义静态创建标志 */#endif/* 此宏用于使能启用队列集 */#if ( configUSE_QUEUE_SETS == 1 )struct QueueDefinition * pxQueueSetContainer; /* 指向队列所在队列集 */#endif/*用于调试*/#if ( configUSE_TRACE_FACILITY == 1 )UBaseType_t uxQueueNumber;/* 队列的类型* 0: 队列或队列集* 1: 互斥信号量* 2: 计数型信号量* 3: 二值信号量* 4: 可递归信号量*/uint8_t ucQueueType;#endif
} xQUEUE;
- 在队列结构体中存在一个联合u,当队列结构体用作队列时,使能联合体的xQueue,其数据类型为QueuePointers_t ,定义如下:
typedef struct QueuePointers
{int8_t * pcTail; /* 存储区域的结束地址 */int8_t * pcReadFrom; /* 最后一次读取队列的位置 */
} QueuePointers_t;
- 当队列结构体作为互斥信号量和递归信号量时,使能联合体的xSemaphore,其数据类型为SemaphoreData_t,定义如下:
typedef struct SemaphoreData
{TaskHandle_t xMutexHolder; /* 互斥信号量的持有者 */UBaseType_t uxRecursiveCallCount; /* 递归互斥信号量被递归获取计数器 */
} SemaphoreData_t;
3. 队列相关API函数
3.1 队列创建
3.1.1 动态创建
队列创建方式有两种,动态创建:xQueueCreate() 和 静态创建:xQueueCreateStatic(),我们这里主要介绍动态创建(方便,简单使用)
- 函数 xQueueCreate()
此函数用于动态创建队列,队列所需要的内存空间由FreeRTOS从管理的堆中分配,xQueueCreate() 实际上是一个宏定义,其具体代码如下:
#if ( configSUPPORT_DYNAMIC_ALLOCATION == 1 )#define xQueueCreate( uxQueueLength, uxItemSize ) xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )
可以看到:xQueueCreate函数实际上是调用了xQueueGenericCreate函数,参数 queueQUEUE_TYPE_BASE 表示创建的是队列我们来看一下xQueueGenericCreate函数的具体定义:
- 函数xQueueGenericCreate()
/*** @brief xQueueGenericCreate* @param uxQueueLength : 队列长度* @param uxItemSize : 队列项目的大小* @param ucQueueType : 队列类型,可决定创建的是队列、队列集还是信号量* @retval 返回值为NULL,表示创建失败,其他值则表示返回队列的句柄(起始位置)*/QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength,const UBaseType_t uxItemSize,const uint8_t ucQueueType )
前面有说到FreeRTOS基于队列实现了多种队列类型,至于创建的类型是什么,由xQueueGenericCreate函数的第三个参数ucQueueType决定,其参数与类型对应关系在 queue.h文件中有定义:
#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函数的内部实现
/*** @brief xQueueGenericCreate* @param uxQueueLength : 队列长度* @param uxItemSize : 队列项目的大小* @param ucQueueType : 队列类型,可决定创建的是队列、队列集还是信号量* @retval 返回值为NULL,表示创建失败,其他值则表示返回队列的句柄(起始位置)*/QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength,const UBaseType_t uxItemSize,const uint8_t ucQueueType ){Queue_t * pxNewQueue = NULL; /* 定义队列句柄,指向队列的起始位置 */size_t xQueueSizeInBytes; /* 列表项消息存储区大小 */uint8_t * pucQueueStorage;/* 参数检查,确保列表长度大于0、创建之后的队列大小在允许范围内 *//* 当队列作为信号量时,uxItemSize的大小为0 */if( ( uxQueueLength > ( UBaseType_t ) 0 ) &&/* Check for multiplication overflow. */( ( SIZE_MAX / uxQueueLength ) >= uxItemSize ) &&/* Check for addition overflow. */( ( SIZE_MAX - sizeof( Queue_t ) ) >= ( uxQueueLength * uxItemSize ) ) ){/*计算队列存储空间需要的字节大小*/xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize ); /*为队列申请内存空间 = 队列控制块 + 队列存储区域*/pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes );/* 内存申请成功!!! */if( pxNewQueue != NULL ){/* 获取队列存储区域的起始地址 = 申请完的地址+队列结构体存储区偏移 */pucQueueStorage = ( uint8_t * ) pxNewQueue;pucQueueStorage += sizeof( Queue_t ); /* 此宏用于启用支持静态内存管理 */#if ( configSUPPORT_STATIC_ALLOCATION == 1 ){/* 标记此队列为非静态申请内存 */pxNewQueue->ucStaticallyAllocated = pdFALSE;}#endif /* configSUPPORT_STATIC_ALLOCATION *//* 初始化队列 */prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue );}else{/* 用于调试 */traceQUEUE_CREATE_FAILED( ucQueueType );mtCOVERAGE_TEST_MARKER();}}else{configASSERT( pxNewQueue );mtCOVERAGE_TEST_MARKER();}return pxNewQueue;}
可以看到xQueueGenericCreate()函数里面主要负责计算队列大小和申请相应的空间,然后调用prvInitialiseNewQueue()对队列进行初始化。prvInitialiseNewQueue函数在queue.c文件中有定义,这里由于篇幅,就不做介绍,下图是创建完成之后的队列结构体成员示例图:
3.1.2 静态创建
- xQueueCreateStatic() 此函数用于静态方式创建队列,队列需要的内存空间,由用户自己手动分配及提供,函数xQueueCreateStatic()实际上是一个宏定义,具体代码如下:
#define xQueueCreateStatic( uxQueueLength, \uxItemSize, \pucQueueStorage, \pxQueueBuffer) \xQueueGenericCreateStatic( ( uxQueueLength ), \( uxItemSize ), \( pucQueueStorage ), \( pxQueueBuffer ), \( queueQUEUE_TYPE_BASE ))
可以看到xQueueCreateStatic()函数实际上是调用了xQueueGenericCreateStatic()函数,所以我们直接来看一下xQueueGenericCreateStatic函数的具体定义:
/*** @brief xQueueGenericCreateStatic* @param uxQueueLength: 队列长度* @param uxItemSize: 队列项目的大小* @param pucQueueStorage: 队列存储器的起始地址* @param pxStaticQueue:静态队列结构体的地址* @param ucQueueType : 队列类型,可决定创建的是队列、队列集还是信号量* @retval 返回值为NULL,表示创建失败,其他值则表示返回队列的句柄(起始位置)*/QueueHandle_t xQueueGenericCreateStatic( const UBaseType_t uxQueueLength,const UBaseType_t uxItemSize,uint8_t * pucQueueStorage,StaticQueue_t * pxStaticQueue,const uint8_t ucQueueType );
这里静态创建的方法,可以参考前面动态创建,这里不做过多介绍
3.2 队列写入
在FreeRTOS中,提供了多种队列写入消息的方式,头部写入,尾部写入,覆写,以及适用于中断场合的写入API函数,如下表:
函数 | 描述 |
---|---|
xQueueSend() | 往队列的尾部写入消息 |
xQueueSendToBack() | 同 xQueueSend() |
xQueueSendToFront() | 往队列的头部写入消息 |
xQueueOverwrite() | 覆写队列消息(只用于队列长度为 1 的情况) |
xQueueSendFromISR() | 在中断中往队列的尾部写入消息 |
xQueueSendToBackFromISR() | 同 xQueueSendFromISR() |
xQueueSendToFrontFromISR() | 在中断中往队列的头部写入消息 |
xQueueOverwriteFromISR() | 在中断中覆写队列消息(只用于队列长度为 1 的情况) |
其中:队列尾部写入消息( xQueueSend()、xQueueSendToBack() )、头部写入消息( xQueueSendToFront() )、覆写函数( xQueueOverwrite() )都是宏定义,内部都调用了一个相同的函数:xQueueGenericSend(),其具体代码如下:
#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()
此函数的作用是向队列中写入消息,在queue.c中有定义,函数原型如下:
/*** @brief xQueueGenericSend* @param xQueue: 需要写入消息的队列* @param pvItemToQueue:需要写入的消息* @param xTicksToWait: 阻塞超时时间* @param xCopyPosition :写入的位置* @retval 返回值为pdTRUE,表示队列写入成功,返回值为errQUEUE_FULL,表示队列写入失败*/
BaseType_t xQueueGenericSend( QueueHandle_t xQueue,const void * const pvItemToQueue,TickType_t xTicksToWait,const BaseType_t xCopyPosition )
需要注意以下两点
- xTicksToWait表示阻塞超时时间,当队列中消息已满时,会将需要写入消息的的任务挂入阻塞列表,最多等待xTicksToWait 个系统节拍后,会将需要写入消息的任务挂入就绪列表,不在等待写入消息。如果xTicksToWait的值为0,表示不等待,为portMAX_DELAY(0xfffffffff),表示死等,等不到就一直等。如果在0~portMAX_DELAY,则表示等待xTicksToWait个系统节拍后就不等了,
- xCopyPosition 表示需要写入消息的位置,具体定义如下:
#define queueSEND_TO_BACK ( ( BaseType_t ) 0 ) /* 写入队列尾部 */
#define queueSEND_TO_FRONT ( ( BaseType_t ) 1 ) /* 写入队列头部 */
#define queueOVERWRITE ( ( BaseType_t ) 2 ) /* 覆写队列 */
- 当需要对队列进行覆写时,队列的长度必须为1,否则不能使用。
同样的:在中断中向队列尾部写入消息、中断中向队列头部写入消息和中断中覆写队列,也是都在内部调用了一个相同的函数xQueueGenericSendFromISR(),具体代码如下:
#define xQueueSendFromISR( xQueue, \pvItemToQueue, \pxHigherPriorityTaskWoken) \xQueueGenericSendFromISR( ( xQueue ), \( pvItemToQueue ), \( pxHigherPriorityTaskWoken ), \queueSEND_TO_BACK)
#define xQueueSendToBackFromISR( xQueue, \pvItemToQueue, \pxHigherPriorityTaskWoken) \xQueueGenericSendFromISR( ( xQueue ), \( pvItemToQueue ), \( pxHigherPriorityTaskWoken ), \queueSEND_TO_BACK)
#define xQueueSendToFrontFromISR( xQueue, \pvItemToQueue, \pxHigherPriorityTaskWoken) \xQueueGenericSendFromISR( ( xQueue ), \( pvItemToQueue ), \( pxHigherPriorityTaskWoken ), \queueSEND_TO_FRONT)
#define xQueueOverwriteFromISR( xQueue, \pvItemToQueue, \pxHigherPriorityTaskWoken) \xQueueGenericSendFromISR( ( xQueue ), \( pvItemToQueue ), \( pxHigherPriorityTaskWoken ), \queueOVERWRITE)
- xQueueGenericSendFromISR()
此函数的作用是在中断中向队列中写入消息,在queue.c中有定义,用法和xQueueGenericSend类似,函数原型如下:
/*** @brief xQueueGenericSendFromISR* @param xQueue: 需要写入消息的队列* @param pvItemToQueue:需要写入的消息* @param pxHigherPriorityTaskWoken: 需要任务切换标记* @param xCopyPosition :写入的位置* @retval 返回值为pdTRUE,表示队列写入成功,返回值为errQUEUE_FULL,表示队列写入失败*/
BaseType_t xQueueGenericSendFromISR( QueueHandle_t xQueue,const void * const pvItemToQueue,BaseType_t * const pxHigherPriorityTaskWoken,const BaseType_t xCopyPosition )
关于xQueueGenericSendFromISR() 函数的具体使用,我们这里不做过多介绍,感兴趣的自己查阅资料。
3.3 队列读出
在FreeRTOS中,提供了两种队列读出消息的方式,以及适用于中断场景的API函数。具体如下表
函数 | 描述 |
---|---|
xQueueReceive() | 从队列头部读取消息,并删除消息 |
xQueuePeek() | 从队列头部读取消息 |
xQueueReceiveFromISR() | 在中断中从队列头部读取消息,并删除消息 |
xQueuePeekFromISR() | 在中断中从队列头部读取消息 |
- xQueueReceive()
此函数用于任务中,从队列的头部读取消息,并且读取成功之后,该消息将会从队列中移除,消息的读取时通过拷贝的形式传递的,拷贝数据的大小,为队列项目的大小。函数原型如下:
/*** @brief xQueueReceive* @param xQueue: 需要读出消息的队列* @param pvBuffer:信息读出缓冲区* @param xTicksToWait: 阻塞超时时间* @retval 返回值为pdTRUE,表示队列读取成功,返回值pdFALSE,表示队列读取失败*/
BaseType_t xQueueReceive( QueueHandle_t xQueue,void * const pvBuffer,TickType_t xTicksToWait);
- xQueuePeek()
此函数用于任务中,从队列的头部读取消息,并且读取成功之后,该消息不会从队列中移除,消息的读取时通过拷贝的形式传递的,拷贝数据的大小,为队列项目的大小。函数原型如下:
/*** @brief xQueueReceive* @param xQueue: 需要读出消息的队列* @param pvBuffer:信息读出缓冲区* @param xTicksToWait: 阻塞超时时间* @retval 返回值为pdTRUE,表示队列读取成功,返回值pdFALSE,表示队列读取失败*/
BaseType_t xQueuePeek( QueueHandle_t xQueue,void * const pvBuffer,TickType_t xTicksToWait);
- xQueueReceiveFromISR()
此函数用于任务中,从队列的头部读取消息,并且读取成功之后,该消息将会从队列中移除,消息的读取时通过拷贝的形式传递的,拷贝数据的大小,为队列项目的大小。函数原型如下:
/*** @brief xQueueReceiveFromISR* @param xQueue: 需要读出消息的队列* @param pvBuffer:信息读出缓冲区* @param pxHigherPriorityTaskWoken: 需要任务切换标记* @retval 返回值为pdTRUE,表示队列读取成功,返回值pdFALSE,表示队列读取失败*/
BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue,void * const pvBuffer,BaseType_t * const pxHigherPriorityTaskWoken);
- xQueuePeekFromISR()
此函数用于在中断中,从队列的头部读取消息,并且读取成功之后,该消息不会从队列中移除,消息的读取时通过拷贝的形式传递的,拷贝数据的大小,为队列项目的大小。函数原型如下:
/*** @brief xQueuePeekFromISR* @param xQueue: 需要读出消息的队列* @param pvBuffer:信息读出缓冲区* @retval 返回值为pdTRUE,表示队列读取成功,返回值pdFALSE,表示队列读取失败*/
BaseType_t xQueuePeekFromISR( QueueHandle_t xQueue,void * const pvBuffer);
上面就是队列的介绍以及队列相关函数API的原型,下面我基于上面的内容,完成一个简单的队列操作实验,包括队列创建、写入和读出:
4. 队列操作实验
1. 实验内容
在STM32F103RCT6上允许FreeRTOS,通过按键控制,完成对应的队列读写,具体要求如下:
- 定义两个队列,一个队列存放按键消息,一个队列存放大数据,队列长度为10。
- 定义三个按键,按键0和按键1负责写入消息,按键2负责读出 消息
- 按下按键0:向按键队列写入按键0消息,向大数据队列写入大数据1
- 按下按键1:向按键队列写入按键1消息,向大数据队列写入大数据2
- 按下按键2:分别向按键队列和大数据队列读出一个消息。
- 每次按键动作,通过串口打印相应动作和数据。
2. 代码实现:
- 由于本期内容涉及到按键扫描,会用到: STM32框架之按键扫描新思路 ,这里不做过多介绍。我们直接来看代码:
- freertos_demo.c
#include "freertos_demo.h"
#include "main.h"
#include "queue.h" //需要包含队列和任务相关的头文件
#include "task.h"
#include "key.h" //包含按键相关头文件
/*FreeRTOS*********************************************************************************************//******************************************************************************************************/
/*FreeRTOS配置*//* TASK1 任务 配置* 包括: 任务句柄 任务优先级 堆栈大小 */
#define TASK1_PRIO 1 /* 任务优先级 */
#define TASK1_STK_SIZE 128 /* 任务堆栈大小 */
TaskHandle_t Task1Task_Handler; /* 任务句柄 */
void task1(void *pvParameters); /*任务函数*////* TASK2 任务 配置
// * 包括: 任务句柄 任务优先级 堆栈大小
// */
//#define TASK2_PRIO 2 /* 任务优先级 */
//#define TASK2_STK_SIZE 128 /* 任务堆栈大小 */
//TaskHandle_t Task2Task_Handler; /* 任务句柄 */
//void task2(void *pvParameters); /*任务函数*////* TASK3 任务 配置
// * 包括: 任务句柄 任务优先级 堆栈大小
// */
//#define TASK3_PRIO 3 /* 任务优先级 */
//#define TASK3_STK_SIZE 128 /* 任务堆栈大小 */
//TaskHandle_t Task3Task_Handler; /* 任务句柄 */
//void task3(void *pvParameters); /*任务函数*/QueueHandle_t keyQueue; /*按键队列*/QueueHandle_t bigDataQueue; /*大数据队列*//******************************************************************************************************//*** @brief FreeRTOS例程入口函数* @param 无* @retval 无*/
void freertos_demo(void)
{taskENTER_CRITICAL(); /* 进入临界区,关闭中断,此时停止任务调度*/keyQueue = xQueueCreate( 10, sizeof(uint8_t) );if(keyQueue != NULL){printf("按键队列创建成功\r\n");}else{printf("按键队列创建失败\r\n");}bigDataQueue = xQueueCreate( 10, sizeof(char*) );if(keyQueue != NULL){printf("大数据队列创建成功\r\n");}else{printf("大数据队列创建失败\r\n");}/* 创建任务1 */xTaskCreate((TaskFunction_t )task1,(const char* )"task1",(uint16_t )TASK1_STK_SIZE,(void* )NULL,(UBaseType_t )TASK1_PRIO,(TaskHandle_t* )&Task1Task_Handler);
// /* 创建任务2 */
// xTaskCreate((TaskFunction_t )task2,
// (const char* )"task2",
// (uint16_t )TASK2_STK_SIZE,
// (void* )NULL,
// (UBaseType_t )TASK2_PRIO,
// (TaskHandle_t* )&Task2Task_Handler);
// /* 创建任务3 */
// xTaskCreate((TaskFunction_t )task3,
// (const char* )"task3",
// (uint16_t )TASK3_STK_SIZE,
// (void* )NULL,
// (UBaseType_t )TASK3_PRIO,
// (TaskHandle_t* )&Task2Task_Handler);taskEXIT_CRITICAL(); /* 退出临界区,重新开启中断,开启任务调度 */vTaskStartScheduler(); //开启任务调度
}/*** @brief task1* @param pvParameters : 传入参数(未用到)* @retval 无*/
void task1(void *pvParameters)
{while(1){Key_One_Scan(Key_Name_Key0,Key0_Up_Task,Key0_Down_Task); /*检测按键0,并调用相应函数*/Key_One_Scan(Key_Name_Key1,Key1_Up_Task,Key1_Down_Task); /*检测按键1,并调用相应函数*/Key_One_Scan(Key_Name_Key2,Key2_Up_Task,Key2_Down_Task); /*检测按键2,并调用相应函数*/}
}
/*** @brief task2* @param pvParameters : 传入参数(未用到)* @retval 无*/
//void task2(void *pvParameters)
//{
// //
//
//while(1)
// {// // }
//}
///**
// * @brief task3
// * @param pvParameters : 传入参数(未用到)
// * @retval 无
// */
//void task3(void *pvParameters)
//{
// //
//
//while(1)
// {// // }
//}
- key.c
/* USER CODE BEGIN 2 */#include "freertos_demo.h"
#include "key.h"
#include "usart.h"//定义按键信息
#define Key0Message 0x11 //16+1 = 17
#define Key1Message 0x22 //32+2 = 34extern QueueHandle_t keyQueue; //声明外部变量
extern QueueHandle_t bigDataQueue;char buff1[100] = {"我是一个大数组,大大大大大大!!!!"};
char buff2[100] = {"我是一个小数组,小小小小小小!!!!"};void Key0_Down_Task(void)
{BaseType_t errorMessage = 0;uint8_t keyMessage = Key0Message;char* pbuff ;pbuff = &buff1[0];errorMessage = xQueueSend(keyQueue,&keyMessage,100); //入队函数,将keyMessage入队到keyMessage队列,如果队列已满,则100ms后返回if(errorMessage != pdTRUE){printf("keyQueue队列按键0入队失败!!!\r\n");}else{printf("keyQueue队列按键0入队成功!!!,数据:%d\r\n",keyMessage);}errorMessage = xQueueSend(bigDataQueue,&pbuff,400); //入队函数,将keyMessage入队到keyMessage队列,如果队列已满,则400ms后返回if(errorMessage != pdTRUE){printf("bigDataQueue队列大数据入队失败!!!\r\n");}else{printf("bigDataQueue队列大数据入队成功!!!,数据:%s\r\n",buff1);}}
void Key0_Up_Task(void)
{}
void Key1_Down_Task(void)
{BaseType_t errorMessage = 0;uint8_t keyMessage = Key1Message;char* pbuff ;pbuff = &buff2[0];errorMessage = xQueueSend(keyQueue,&keyMessage,500); //入队函数,将keyMessage入队到keyMessage队列,如果队列已满,则500ms后返回if(errorMessage != pdTRUE){printf("keyQueue队列按键1入队失败!!!\r\n");}else{printf("keyQueue队列按键1入队成功!!!,数据:%d\r\n",keyMessage);}errorMessage = xQueueSend(bigDataQueue,&pbuff,400); //入队函数,将keyMessage入队到keyMessage队列,如果队列已满,则400ms后返回if(errorMessage != pdTRUE){printf("bigDataQueue队列大数据入队失败!!!\r\n");}else{printf("bigDataQueue队列大数据入队成功!!!,数据:%s\r\n",buff2);}
}
void Key1_Up_Task(void)
{}
void Key2_Down_Task(void)
{BaseType_t errorMessage = 0;uint8_t keyMessage;char* pbuff ;errorMessage = xQueueReceive(keyQueue,&keyMessage,100); //出队函数,将keyQueue中的数据出队,如果数据已空,则100ms后返回if(errorMessage != pdTRUE){printf("keyQueue队列出队失败!!!\r\n");}else{printf("keyQueue队列出队成功!!!,数据:%d\r\n",keyMessage); }errorMessage = xQueueReceive(bigDataQueue,&pbuff,400); //出队函数,将bigDataQueue中的数据出队,如果数据已空,则400ms后返回if(errorMessage != pdTRUE){printf("bigDataQueue队列大数据出队失败!!!\r\n");}else{printf("bigDataQueue队列大数据出队成功!!!,数据:%s\r\n",pbuff);}
}
void Key2_Up_Task(void)
{}
void WKUP_Down_Task(void)
{}
void WWKUP_Up_Task(void)
{}void Key_One_Scan(uint8_t KeyName ,void(*OnKeyOneUp)(void), void(*OnKeyOneDown)(void))
{static uint8_t Key_Val[Key_Name_Max]; //按键值的存放位置static uint8_t Key_Flag[Key_Name_Max]; //KEY0~2为0时表示按下,为1表示松开,WKUP反之Key_Val[KeyName] = Key_Val[KeyName] <<1; //每次扫描完,将上一次扫描的结果左移保存switch(KeyName){case Key_Name_Key0: Key_Val[KeyName] = Key_Val[KeyName] | (HAL_GPIO_ReadPin(KEY0_GPIO_Port, KEY0_Pin)); //读取Key0按键值break;case Key_Name_Key1: Key_Val[KeyName] = Key_Val[KeyName] | (HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin)); //读取Key1按键值break;case Key_Name_Key2: Key_Val[KeyName] = Key_Val[KeyName] | (HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin)); //读取Key2按键值break;
// case Key_Name_WKUP: Key_Val[KeyName] = Key_Val[KeyName] | (HAL_GPIO_ReadPin(WKUP_GPIO_Port, WKUP_Pin)); //读取WKUP按键值
// break; default:break;}
// if(KeyName == Key_Name_WKUP) //WKUP的电路图与其他按键不同,所以需要特殊处理
// {
// //WKUP特殊情况
// //当按键标志为1(松开)是,判断是否按下,WKUP按下时为0xff
// if(Key_Val[KeyName] == 0xff && Key_Flag[KeyName] == 1)
// {
// (*OnKeyOneDown)();
// Key_Flag[KeyName] = 0;
// }
// //当按键标志位为0(按下),判断按键是否松开,WKUP松开时为0x00
// if(Key_Val[KeyName] == 0x00 && Key_Flag[KeyName] == 0)
// {
// (*OnKeyOneUp)();
// Key_Flag[KeyName] = 1;
// }
// }
// else //Key0~2按键逻辑判断
// {//Key0~2常规判断//当按键标志为1(松开)是,判断是否按下if(Key_Val[KeyName] == 0x00 && Key_Flag[KeyName] == 1){(*OnKeyOneDown)();Key_Flag[KeyName] = 0;}//当按键标志位为0(按下),判断按键是否松开if(Key_Val[KeyName] == 0xff && Key_Flag[KeyName] == 0){(*OnKeyOneUp)();Key_Flag[KeyName] = 1;} }//}
/* USER CODE END 2 */
3. 运行结果
以上就是本期使用到的核心代码,其他部分我这里就不做展示,直接去看我往期的内容,源代码都有的。至于按键的配置,可以参考:夜深人静学32系列9——GPIO驱动数码管/蜂鸣器/按键/LED