目录
1. 队列简介
1.1 数据存储
1.2 多任务访问
1.3 出队阻塞
1.4 入队阻塞
1.5 队列操作过程
1.5.1 创建队列
1.5.2 向队列发送第一个消息
1.5.3 向队列发送第二个消息
1.5.4 从队列中读取消息
2. 队列结构体
3. 队列创建
3.1 函数原型
3.1.1 函数 xQueueCreate()
3.1.2 函数 xQueueCreateStatic()
3.1.3 函数 xQueueGenericCreate()
3.1.4 函数 xQueueGenericCreateStatic()
3.2 队列创建函数
3.3 初始化队列函数
3.4 队列复位函数
4. 向队列发送消息
4.1 函数原型
4.1.1 函数 xQueueSend()、xQueueSendToBack() 和 xQueueSendToFront()
4.1.2 函数 xQueueOverwrite()
4.1.3 函数 xQueueGenericSend()
4.1.4 函数 xQueueSendFromISR()、xQueueSendToBackFromISR() 和 xQueueSendToFrontFromISR()
4.1.5 函数 xQueueOverwriteFromISR()
4.1.6 函数 xQueueGenericSendFromISR()
4.2 任务级通用入队函数
4.3 中断通用入队函数
4.4 队列上锁和解锁
4.5 从队列读取消息
4.5.1 函数 xQueueReceive()
4.5.2 函数 xQueuePeek()
4.5.3 函数 xQueueGenericReceive()
4.5.4 函数 xQueueReceiveFromISR()
4.5.5 函数 xQueuePeekFromISR()
4.6 队列操作实验
4.6.1 实验设计
4.6.2 实验程序
4.6.2.1 USART.c
4.6.2.2 Timer.c
4.6.2.3 main.c
在实际的应用中,常常会遇到一个任务或者中断服务需要和另外一个任务进行 “沟通交流”,这个 “沟通交流” 的过程其实就是消息传递的过程。在没有操作系统的时候两个应用程序进行消息传递一般使用全局变量的方式,但是如果在使用操作系统的应用中用全局变量来传递消息就会涉及到 “资源管理” 的问题。对此 FreeRTOS 提供了一个叫做 “队列” 的机制来完成任务与任务、任务与中断之间的消息传递。
这里解释一下为什么在操作系统中使用全局变量会造成 “资源管理” 的问题,也可以说是产生资源受损的现象?
首先我们肯定全局变量确实可以在不同任务中进行消息传递。
但是,假设现在有一个全局变量 a = 0,现在有两个任务中都使用到了这个变量 a ;
任务一:实现 a++;
任务二:实现 a++;
正常来说在操作系统中,两个任务会交替进行,那么运行完两个任务之后,a 的值应该是 2 才对;
但是我们知道,操作系统中 a++ 的底层逻辑首先会将 a 的值保存在系统寄存器 r0 中,然后在寄存器中实现 r0 = r0 + 1;最后再将寄存器的值 r0 重新写回 a ,但是不同任务之间存在优先级关系,如果任务一进行到了 r0 = r0 + 1 这一步,此时任务二的优先级更高,就会抢占 CPU 的使用权,那么此时 a 的值还是 0 ,经过任务二 a 的值变为 1 ,此时就会产生资源受损的现象!
全局变量的弊端:数据无保护,导致数据不安全,当多个任务同时对该变量进行操作时,数据易受损!
1. 队列简介
队列是为了任务与任务、任务与中断之间的通信而准备的,可以在任务与任务、任务与中断之间传递消息,队列中可以存储有限的、大小固定的数据项目。任务与任务、任务与中断之间要交流的数据保存在队列中,叫做队列项目。队列所能保存的最大数据项目数量叫做队列的长度,创建队列的时候会指定数据项目的大小和队列的长度。由于队列是用来传递消息的,所以也称为消息队列。
1.1 数据存储
通常队列采用先进先出(FIFO)的存储缓冲机制,也就是往队列发送数据的时候(也叫做入队)永远都是发送到队列的尾部,而从队列提取数据的时候(也叫做出队)是从队列的头部提取的。但是也可以使用 LIFO(后进先出)的存储缓冲,FreeRTOS 中的队列同样也提供了 LIFO 的存储缓冲机制。
数据发送到队列中会导致数据拷贝,也就是将要发送的数据拷贝到队列中,这就意味着在队列中存储的是数据的原始值,而不是原数据的引用(即只传递数据的指针),这个也叫做值传递。学习过 UCOS 的应该知道,UCOS 的消息队列采用的是引用传递,传递的是消息指针。采用引用传递的话消息内容就必须一直保持可见性,也就是消息内容必须有效,这样一来局部变量这种可能会被随时删除掉的东西就不能用来传递消息,但是引用传递也有其优点,引用传递会节省时间,不用进行数据拷贝!
采用值传递的话虽然会导致数据拷贝,会浪费一点时间,但是一旦将消息发送到队列中,原始的数据缓冲区就可以被删除掉或者被覆写,这样的话这些缓冲区就可以被重复的使用。
FreeRTOS 中使用队列传递消息虽然使用的是数据拷贝(数据拷贝就是 Ctrl + x,剪切,而不是复制),但是同样也可以使用引用来传递消息,直接往队列里发送指向这个消息的地址指针就可以!当我要发送的消息数据太大的时候就可以直接发送消息缓冲区的地址指针,比如在网络应用环境中,网络的数据量往往都是很大的,采用数据拷贝的话就不现实。
这里再次解释一下,为什么使用队列就不会造成资源受损的现象?
在使用到队列进行消息传递时,会调用函数,比如说 xQueueSend(),函数内部首先就会进入临界区,也就是我们提及的关中断,这样中断就不会干涉到消息传递,也就不会造成资源受损的现象!
1.2 多任务访问
队列不是属于某个特别指定的任务的,任何任务都可以向队列中发送消息,或者从队列中提取消息。
1.3 出队阻塞
当任务尝试从一个队列中读取消息的时候可以指定一个阻塞时间,这个阻塞时间就是当任务从队列中读取消息无效的时候任务阻塞的时间。出队就是从队列中读取消息,出队阻塞是针对从队列中读取消息的任务(阻塞阻塞是说任务阻塞的)而言的。比如说任务 A 用于处理串口接收到的数据,串口接收到数据以后就会放到队列 Q 中,任务 A 从队列 Q 中读取数据。但是如果此时队列 Q 是空的,说明还没有数据,任务 A 这个时候来读取的话肯定是获取不到任何东西的,试想一下此时该怎么办呢?
任务 A 此时有三种选择,一:二话不说扭头就走;二:再等等,等一会看看,说不定一会就有数据了;三:死等,死也要等到数据到来!
选哪一个是由这个阻塞时间来决定的,这个阻塞时间单位是时钟节拍数。阻塞时间为 0 的话就是不阻塞,没有数据的话就马上返回任务继续执行接下来的代码,对应于第一种选择。如果阻塞时间为 0 ~ portMAX_DELAY,当任务没有从队列中获取到消息的话就进入阻塞态,阻塞时间指定了任务进入阻塞态的时间,当阻塞时间到了以后还没有接收到数据的话就退出阻塞态,返回任务接着运行下面的代码,如果在阻塞时间内接收到了数据就立即返回,执行任务中下面的代码,这种情况对应第二种选择。当阻塞时间设置为 portMAX_DELAY 的话,任务就会一直进入阻塞态等待,直到接收到数据为止!这个就是第三种选择;
1.4 入队阻塞
入队说的是向队列中发送消息,将消息加入到队列中。和出队阻塞一样,当一个任务向队列发送消息的话也可以设置阻塞时间。比如任务 B 向消息队列 Q 发送消息,但是此时队列 Q 是满的,那肯定是发送失败了。此时任务 B 就会遇到和上面任务 A 一样的问题,这两种情况的处理过程是类似的,只不过一个是向队列 Q 发送消息,一个是从队列 Q 读取消息而已。
1.5 队列操作过程
1.5.1 创建队列
如上图所示,任务 A 要向任务 B 发送消息,这个消息的内容是 x 变量的值。此时首先需要创建一个队列,并且指定队列的长度和每条消息的长度。这里我们创建一个长度为 4 的队列,因为要传递的是 x 的值,而 x 是个 int 类型的变量,所以每条消息的长度就是 int 类型的长度,在 STM32 中就是 4 字节,即每条消息是 4 个字节的。
1.5.2 向队列发送第一个消息
如上图所示,任务 A 的变量 x 的值为 10,将这个值发送到消息队列中。此时队列剩余长度就是 3 了。在之前的学习过程中,我们已经了解到了向队列发送消息采用的是拷贝的方式,所以一旦消息发送完成变量 x 就可以再次被使用,赋其他的值。
1.5.3 向队列发送第二个消息
如上图所示,任务 A 又向队列发送了一个消息,即新的 x 的值,这里是 20。此时队列剩余长度为 2。
1.5.4 从队列中读取消息
如上图所示,任务 B 从队列中读取消息,并将读取到的消息值赋值给 y,这样 y 就等于 10 了。任务 B 从队列中读取消息完成以后可以选择清除掉这个消息或者不清除。当选择清除这个消息的话其他任务或中断就不能获取这个消息了,而且队列剩余大小就会加一,变为 3。如果不清除的话其他任务或中断也可以获取这个消息,而队列剩余大小依旧是 2。
2. 队列结构体
有一个结构体用于描述队列,叫做 Queue_t ,这个结构体在文件 queue.c 中定义如下:
typedef struct QueueDefinition
{ int8_t *pcHead; //指向队列存储区开始地址。 int8_t *pcTail; //指向队列存储区最后一个字节。 int8_t *pcWriteTo; //指向存储区中下一个空闲区域。 union { int8_t *pcReadFrom; //当用作队列的时候指向最后一个出队的队列项首地址 UBaseType_t uxRecursiveCallCount;//当用作递归互斥量的时候用来记录递归互斥量被 //调用的次数。 } u; List_t xTasksWaitingToSend; //等待发送任务列表,那些因为队列满导致入队失败而进 //入阻塞态的任务就会挂到此列表上。 List_t xTasksWaitingToReceive; //等待接收任务列表,那些因为队列空导致出队失败而进 //入阻塞态的任务就会挂到此列表上。 volatile UBaseType_t uxMessagesWaiting; //队列中当前队列项数量,也就是消息数 UBaseType_t uxLength; //创建队列时指定的队列长度,也就是队列中最大允许的 //队列项(消息)数量 UBaseType_t uxItemSize; //创建队列时指定的每个队列项(消息)最大长度,单位字节 volatile int8_t cRxLock; //当队列上锁以后用来统计从队列中接收到的队列项数 //量,也就是出队的队列项数量,当队列没有上锁的话此字 //段为 queueUNLOCKED volatile int8_t cTxLock; //当队列上锁以后用来统计发送到队列中的队列项数量, //也就是入队的队列项数量,当队列没有上锁的话此字 //段为 queueUNLOCKED #if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) &&\ ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) ) uint8_t ucStaticallyAllocated; //如果使用静态存储的话此字段设置为 pdTURE。 #endif #if ( configUSE_QUEUE_SETS == 1 ) //队列集相关宏 struct QueueDefinition *pxQueueSetContainer; #endif #if ( configUSE_TRACE_FACILITY == 1 ) //跟踪调试相关宏 UBaseType_t uxQueueNumber; uint8_t ucQueueType; #endif } xQUEUE; typedef xQUEUE Queue_t;
3. 队列创建
3.1 函数原型
在使用队列之前必须先创建队列,有两种创建队列的方法,一种是静态的,使用函数 xQueueCreateStatic();另一个是动态的,使用函数 xQueueCreate()。这两个函数本质上都是宏,真正完成队列创建的函数是 xQueueGenericCreate() 和 xQueueGenericCreateStatic(),这两个函数在文件 queue.c 中有定义;
3.1.1 函数 xQueueCreate()
此函数本质上是一个宏,用来动态的创建队列,此宏最终调用的是函数 xQueueGenericCreate(),函数原型如下:
QueueHandle_t xQueueCreate(UBaseType_t uxQueueLength,UBaseType_t uxItemSize)
参数:
uxQueueLength:要创建的队列的队列长度,这里是队列的项目数。
uxItemSize:队列中每个项目(消息)的长度,单位是字节。
返回值:
其他值:队列创建成功以后返回的队列句柄!
NULL:队列创建失败!
3.1.2 函数 xQueueCreateStatic()
此函数也是用于创建队列的,但是使用的静态方法创建队列,队列所需要的内存由用户自行分配,此函数本质上也是一个宏,此宏最终调用的是函数 xQueueGenericCreateStatic():
QueueHandle_t xQueueCreateStatic(UBaseType_t uxQueueLength,UBaseType_t uxItemSize,uint8_t* pucQueueStorageBuffer,StaticQueue_t* pxQueueBuffer)
参数:
uxQueueLength:要创建的队列的队列长度,这里是队列的项目数。
uxItemSize:队列中每个项目(消息)的长度,单位是字节。
pucQueueStorage:指向队列项目的存储区,也就是消息的存储区,这个存储区需要用户自行分配。此参数必须指向一个 uint8_t 类型的数组。这个存储区要大于等于(uxQueueLength* uxItemsSize)字节。
pxQueueBuffer:此参数指向一个 StaticQueue_t 类型的变量,用来保存队列结构体。
返回值:
其他值:队列创建成功以后返回的队列句柄!
NULL:队列创建失败!
3.1.3 函数 xQueueGenericCreate()
函数 xQueueGenericCreate() 用于动态创建队列,创建队列过程中需要的内存均通过 FreeRTOS 中的动态内存管理函数 pvPortMalloc() 分配,函数原型如下:
QueueHandle_t xQueueGenericCreate(const UBaseType_t uxQueueLength,const UBaseType_t uxItemSize,const uint8_t ucQueueType)
参数:
uxQueueLength:要创建的队列的队列长度,这里是队列的项目数。
uxItemSize:队列中每个项目(消息)的长度,单位是字节。
ucQueueType:队列类型,由于 FreeRTOS 中的信号量等也是通过队列来实现的,创建信号量的函数最终也是使用此函数的,因此在创建的时候需要指定此队列的用途,也就是队列类型,一共有六种类型:
queueQUEUE_TYPE_BASE 普通的消息队列
queueQUEUE_TYPE_SET 队列集
queueQUEUE_TYPE_MUTEX 互斥信号量
queueQUEUE_TYPE_COUNTING_SEMAPHORE 计数型信号量
queueQUEUE_TYPE_BINARY_SEMAPHORE 二值信号量
queueQUEUE_TYPE_RECURSIVE_MUTEX 递归互斥信号量
返回值:
其他值:队列创建成功以后返回的队列句柄!
NULL:队列创建失败!
3.1.4 函数 xQueueGenericCreateStatic()
此函数用于静态创建队列,创建队列过程中需要的内存需要由用户自行分配好,函数原型如下:
QueueHandle_t xQueueGenericCreateStatic(const UBaseType_t uxQueueLength,const UBaseType_t uxItemSize,uint8_t* pucQueueStorage,StaticQueue_t* pxStaticQueue,const uint8_t ucQueueType)
参数:
uxQueueLength:要创建的队列的队列长度,这里是队列的项目数。
uxItemSize:队列中每个项目(消息)的长度,单位是字节。
pucQueueStorage:指向队列项目的存储区,也就是消息的存储区,这个存储区需要用户自行分配。此参数必须指向一个 uint8_t 类型的数组。这个存储区要大于等于(uxQueueLength* uxItemsSize)字节。
pxStaticQueue:此参数指向一个 StaticQueue_t 类型的变量,用来保存队列结构体。
ucQueueType:队列类型。
返回值:
其他值:队列创建成功以后返回的队列句柄!
NULL:队列创建失败!
3.2 队列创建函数
最终完成队列创建的函数有两个,一个是静态方法的 xQueueGenericCreateStatic(),另外一个是动态方法的 xQueueGenericCreate()。动态创建函数 xQueueGenericCreate() 定义如下:
QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, const uint8_t ucQueueType )
{ Queue_t *pxNewQueue; size_t xQueueSizeInBytes; uint8_t *pucQueueStorage; configASSERT( uxQueueLength > ( UBaseType_t ) 0 ); if( uxItemSize == ( UBaseType_t ) 0 ) //第二个参数是每个队列项的大小,单位是字节{ //队列项大小为 0,那么就不需要存储区。 xQueueSizeInBytes = ( size_t ) 0; } else //否则意味着队列项的大小不为 0{ //分配足够的存储区,确保随时随地都可以保存所有的项目(消息), xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize ); (1) } //动态分配长度为 uxQueueLength ,每个队列项的大小为 uxItemSize 的空间;pxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes ); (2) //动态开辟队列空间,开辟空间的大小为 队列结构体的大小加上队列中所需存储区的大小//内存申请成功 if( pxNewQueue != NULL ) { pucQueueStorage = ( ( uint8_t * ) pxNewQueue ) + sizeof( Queue_t ); (3) #if( configSUPPORT_STATIC_ALLOCATION == 1 ) { //队列是使用动态方法创建的,所以队列字段 ucStaticallyAllocated 标 //记为 pdFALSE。 pxNewQueue->ucStaticallyAllocated = pdFALSE; } #endif prvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, \ (4) ucQueueType, pxNewQueue ); } return pxNewQueue;
}
(1)、队列是要存储消息的,所以必须要有消息的存储区,函数的参数 uxQueueLength 和 uxItemSize 指定了队列中最大队列项目(消息)数量和每个消息的长度,两者相乘就是消息存储区的大小。
(2)、调用函数 pvPortMalloc() 给队列分配内存,注意这里申请的内存大小是队列结构体和队列中消息存储区的总大小。
(3)、计算出消息存储区的首地址,(2)中申请到的内存是队列结构体和队列中存储区的总大小,队列结构体内存在前,紧跟在后面的就是消息存储区内存。
(4)、调用函数 prvInitialiseNewQueue() 初始化队列。
可以看出函数 xQueueGenericCreate() 重要的工作就是给队列分配内存,当内存分配成功以后调用函数 prvInitialiseNewQueue() 来初始化队列。
3.3 初始化队列函数
队列初始化函数 prvInitialiseNewQueue() 用于队列的初始化,此函数在文件 queue.c 中有定义,函数代码如下:
static void prvInitialiseNewQueue( const UBaseType_t uxQueueLength, //队列长度 const UBaseType_t uxItemSize, //队列项目长度 uint8_t * pucQueueStorage, //队列项目存储区 const uint8_t ucQueueType, //队列类型 Queue_t * pxNewQueue ) //队列结构体
{ //防止编译器报错 ( void ) ucQueueType; if( uxItemSize == ( UBaseType_t ) 0 ) //如果队列项目长度为 0,{ //队列项(消息)长度为 0,说明没有队列存储区,这里将 pcHead 指向队列开始地址 pxNewQueue->pcHead = ( int8_t * ) pxNewQueue; } //队列头指针指向队列开始地址else //否则意味着队列项目不为 0{ //设置 pcHead 指向队列项存储区首地址 pxNewQueue->pcHead = ( int8_t * ) pucQueueStorage; (1) } //队列头指针指向队列存储区首地址//初始化队列结构体相关成员变量 pxNewQueue->uxLength = uxQueueLength; (2) pxNewQueue->uxItemSize = uxItemSize; ( void ) xQueueGenericReset( pxNewQueue, pdTRUE ); (3) #if ( configUSE_TRACE_FACILITY == 1 ) //跟踪调试相关字段初始化 { pxNewQueue->ucQueueType = ucQueueType; } #endif /* configUSE_TRACE_FACILITY */ #if ( configUSE_QUEUE_SETS == 1 ) //队列集相关字段初始化 { pxNewQueue->pxQueueSetContainer = NULL; } #endif /* configUSE_QUEUE_SETS */ traceQUEUE_CREATE( pxNewQueue );
}
(1)、队列结构体中的成员变量 pcHead 指向队列存储区中首地址。
(2)、初始化队列结构体中的成员变量 uxQueueLength 和 uxItemSize,这两个成员变量保存队列的最大队列项目和每个队列项大小。
(3)、调用函数 xQueueGenericReset() 复位队列。
3.4 队列复位函数
队列初始化函数 prvInitialiseNewQueue() 中调用了函数 xQueueGenericReset() 来复位队列,函数 xQueueGenericReset() 代码如下:
BaseType_t xQueueGenericReset( QueueHandle_t xQueue, BaseType_t xNewQueue )
{ Queue_t * const pxQueue = ( Queue_t * ) xQueue; configASSERT( pxQueue ); taskENTER_CRITICAL(); //进入临界区{ //初始化队列相关成员变量 pxQueue->pcTail = pxQueue->pcHead + ( pxQueue->uxLength * pxQueue->\ (1) uxItemSize ); //pcTail 指针指向队列尾部的下一个位置,通过计算队列长度和每个元素的大小得到pxQueue->uxMessagesWaiting = ( UBaseType_t ) 0U; //uxMessagesWaiting 表示队列的消息数量,初始化为 0;pxQueue->pcWriteTo = pxQueue->pcHead; //pcWriteTo 指向队列的头部,表示写操作时的起始位置;pxQueue->u.pcReadFrom = pxQueue->pcHead + ( ( pxQueue->uxLength - \ ( UBaseType_t ) 1U ) * pxQueue->uxItemSize ); //pcReadFrom 指向队列的尾部,表示读操作时的起始位置;pxQueue->cRxLock = queueUNLOCKED; pxQueue->cTxLock = queueUNLOCKED; //cRxLock 和 cTxLock 是用于实现队列的读写锁。if( xNewQueue == pdFALSE ) (2) { //由于复位队列以后队列依旧是空的,所以对于那些由于出队(从队列中读取消 //息)而阻塞的任务就依旧保持阻塞壮态。但是对于那些由于入队(向队列中发送 //消息)而阻塞的任务就不同了,这些任务要解除阻塞壮态,从队列的相应列表中 //移除。 if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE ) { if( xTaskRemoveFromEventList( &( pxQueue->\ xTasksWaitingToSend ) ) != pdFALSE ) { queueYIELD_IF_USING_PREEMPTION(); } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } else { //初始化队列中的列表vListInitialise( &( pxQueue->xTasksWaitingToSend ) ); (3) vListInitialise( &( pxQueue->xTasksWaitingToReceive ) ); } } taskEXIT_CRITICAL(); return pdPASS;
}
(1)、初始化队列中相关成员变量。
(2)、根据参数 xNewQueue 确定要复位的队列是否是新创建的队列,如果不是的话还需要做其他的处理。
(3)、初始化队列中的列表 xTasksWaitingToSend 和 xTasksWaitingToReceive。
经过上述的一系列努力,队列创建成功;
比如说我们创建了一个有 4 个队列项,每个队列项长度为 32 个字节的队列 TestQueue,创建成功的队列如下图所示:
队列头指针 pcHead 指向队列项目一,表示队列的开始项;队列尾指针 pcTail 指向队列项目四,表示队列的结束项;pcWriteTo 指向队列的头部,表示写操作时的起始位置;pcReadFrom 指向队列的尾部,表示读操作的起始位置。uxMessagesWaiting 表示队列中的消息数量,初始时为 0;uxLength 表示队列的长度,此队列的长度为 4,分别是队列项目一、二、三、四;uxItemSize 表示队列项的长度,设置为 32 ,表示每项长度为 32 个字节;cRxLock = queueUNLOCKED 表示队列的读锁是未加锁状态,也就是说可以进行读操作;cTxLock = queueUNLOCKED 表示队列的写锁是未加锁状态,也就是说可以进行写操作;
4. 向队列发送消息
4.1 函数原型
创建好队列以后就可以向队列发送消息了,FreeRTOS 提供了 8 个向队列发送消息的 API 函数:
任务级入队函数:
xQueueSend() 发送消息到队列尾部(后向入队)
xQueueSendToBack() 发送消息到队列尾部(后向入队)
xQueueSendToFront() 发送消息到队列头(前向入队)
xQueueOverwrite() 发送消息到队列,带覆写功能,当队列满了以后自动覆盖掉旧的消息。
中断级入队函数:
xQueueSendFromISR() 发送消息到队列尾部(后向入队),用于中断服务函数
xQueueSendToBackFromISR() 发送消息到队列尾部(后向入队),用于中断服务函数
xQueueSendToFrontFromISR() 发送消息到队列头(前向入队),用于中断服务函数
xQueueOverwriteFromISR() 发送消息到队列,带覆写功能,当队列满了以后自动覆盖掉旧的消息,用于中断服务函数
4.1.1 函数 xQueueSend()、xQueueSendToBack() 和 xQueueSendToFront()
这三个函数都是用于向队列中发送消息的,这三个函数的本质都是宏,其中函数 xQueueSend() 和 xQueueSendToBack() 是一样的,都是后向入队,也就是将新的消息插入到队列的后面。函数 xQueueSendToFront() 是前向入队,也就是将新消息插入到队列的前面。然而!这三个函数最后都是调用同一个函数:xQueueGenericSend() 。这三个函数只能用于任务函数中,不能用于中断服务函数,中断服务函数有专用的函数。三个函数的函数定义如下:
BaseType_t xQueueSend( QueueHandle_t xQueue, const void * pvItemToQueue, TickType_t xTicksToWait); BaseType_t xQueueSendToBack(QueueHandle_t xQueue, const void* pvItemToQueue, TickType_t xTicksToWait); BaseType_t xQueueSendToToFront(QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait);参数:
xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的队列句柄
pvItemToQueue: 指向要发送的消息,发送时会将这个消息拷贝到队列中。
xTicksToWait: 阻塞时间,此参数指示当队列满的时候任务进入阻塞态等待队列空闲的最大时间。如果为 0 的话当队列满的时候就立即返回;当为 portMAX_DELAY 的话就会一直等待,直到队列有空闲的队列项,也就是死等,但是宏 INCLUDE_vTaskSuspend 必须为 1.返回值:
pdPASS: 向队列发送消息成功!
errQUEUE_FULL: 队列已经满了,消息发送失败。
4.1.2 函数 xQueueOverwrite()
此函数也是向队列发送数据的,当队列满了以后会覆写掉旧的数据,不管这个旧数据有没有被其他任务或中断取走。这个函数常用于向那些长度为 1 的队列发送消息,此函数也是一个宏,最终调用的函数也是 xQueueGenericSend(),函数原型如下:
BaseType_t xQueueOverwrite(QueueHandle_t xQueue,const void* pvItemToQueue);参数:xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的队列句柄。pvItemToQueue: 指向要发送的消息,发送的时候会将这个消息拷贝到队列中。返回值:pdPASS: 向队列发送消息成功与否,此函数只会返回 pdPASS!因为此函数执行过程中不在乎队列满不满,满了的话就覆盖掉旧的数据,总之发送数据肯定能成功!
4.1.3 函数 xQueueGenericSend()
此函数才是真正实现上述功能的函数,上面提到的任务级入队函数最终都是调用此函数,函数原型如下:
BaseType_t xQueueGenericSend( QueueHandle_t xQueue, const void * const pvItemToQueue, TickType_t xTicksToWait, const BaseType_t xCopyPosition ) 参数:
xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的队列句柄
pvItemToQueue: 指向要发送的消息,发送的过程中会将这个消息拷贝到队列中
xTicksToWait: 阻塞时间
xCopyPosition: 入队方式,有三种入队方式:queueSEND_TO_BACK: 后向入队queueSEND_TO_FRONT: 前向入队queueOVERWRITE: 覆写入队入队 API 函数就是通过此参数来决定采用哪种入队方式的;返回值:
pdTURE: 向队列发送消息成功!
errQUEUE_FULL: 队列已经满了,消息发送失败!
4.1.4 函数 xQueueSendFromISR()、xQueueSendToBackFromISR() 和 xQueueSendToFrontFromISR()
这三个函数也是向队列中发送消息的,这三个函数用于中断服务函数中。这三个函数本质也是宏,其中函数 xQueueSendFromISR() 和 xQueueSendToBackFromISR() 是一样的,都是后向入队,也就是新的消息插入到队列的后面。函数 xQueueSendToFrontFromISR() 是前向入队,也就是将新的消息插入到队列的前面。这三个函数同样调用一个函数 xQueueGenericSendFromISR() 。这三个函数的函数原型如下:
BaseType_t xQueueSendFromISR(QueueHandle_t xQueue, const void * pvItemToQueue, BaseType_t * pxHigherPriorityTaskWoken); BaseType_t xQueueSendToBackFromISR(QueueHandle_t xQueue, const void * pvItemToQueue, BaseType_t * pxHigherPriorityTaskWoken); BaseType_t xQueueSendToFrontFromISR(QueueHandle_t xQueue, const void * pvItemToQueue, BaseType_t * pxHigherPriorityTaskWoken); 参数:
xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后就会返回此队列的队列句柄
pvItemToQueue: 指向要发送的消息,发送的时候会将这个消息拷贝到队列中。
pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换,这个变量的值由这三个函数来设置用户不用进行设置,用户只需要提供一个变量来保存这个值就行了。当此值 为 pdTURE 的时候在退出中断服务函数之前一定要进行一次任务切换。返回值:
pdTURE: 向队列中发送消息成功!
errQUEUE_FULL: 队列已经满了,消息发送失败。
通过观察可以发现,上述三个函数都没有设置阻塞时间值。因为这些函数都是在中断服务函数中调用的,并不是在任务中,所以就没有阻塞这一说了!
4.1.5 函数 xQueueOverwriteFromISR()
此函数是 xQueueOverwrite() 的中断级版本,用在中断服务函数中,在队列满的时候自动覆写掉旧的数据,此函数也是一个宏,实际调用的也是函数 xQueueGneericSendFromISR(),此函数原型如下:
BaseType_t xQueueOverwriteFromISR(QueueHandle_t xQueue, const void * pvItemToQueue, BaseType_t * pxHigherPriorityTaskWoken); 参数:
xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后就会返回此队列的队列句柄
pvItemToQueue: 指向要发送的消息,发送的时候会将这个消息拷贝到队列中。
pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换,这个变量的值由这三个函数来设置用户不用进行设置,用户只需要提供一个变量来保存这个值就行了。当此值 为 pdTURE 的时候在退出中断服务函数之前一定要进行一次任务切换。返回值:pdPASS: 向队列发送消息成功与否,此函数只会返回 pdPASS!因为此函数执行过程中不在乎队列满不满,满了的话就覆盖掉旧的数据,总之发送数据肯定能成功!
4.1.6 函数 xQueueGenericSendFromISR()
这个函数是入队级函数最终被调用的函数 xQueueGenericSendFromISR(),函数原型如下:
BaseType_t xQueueGenericSendFromISR(QueueHandle_t xQueue, const void* pvItemToQueue, BaseType_t* pxHigherPriorityTaskWoken, BaseType_t xCopyPosition); 参数:
xQueue: 队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的队列句柄
pvItemToQueue: 指向要发送的消息,发送的过程中会将这个消息拷贝到队列中
pxHigherPriorityTaskWoken: 标记退出此函数以后是否进行任务切换,这个变量的值由这三个函数来设置,用户不用进行设置,用户只需要提供一个变量来保存这个值就行了。当这个值为 pdTURE 的时候在退出中断服务函数之前一定要进行一次任务切换xCopyPosition: 入队方式,有三种入队方式:queueSEND_TO_BACK: 后向入队queueSEND_TO_FRONT: 前向入队queueOVERWRITE: 覆写入队返回值:
pdTRUE: 向队列发送消息成功!
errQUEUE_FULL: 队列已经满了,消息发送失败;
4.2 任务级通用入队函数
不管是后向入队、前向入队还是覆写入队,最终调用的都是通过入队函数 xQueueGenericSend(),这个函数在文件 queue.c 文件中定义,函数代码如下:
BaseType_t xQueueGenericSend( QueueHandle_t xQueue, const void * const pvItemToQueue, TickType_t xTicksToWait, const BaseType_t xCopyPosition )
{ BaseType_t xEntryTimeSet = pdFALSE, xYieldRequired; TimeOut_t xTimeOut; Queue_t * const pxQueue = ( Queue_t * ) xQueue; for( ;; ) { taskENTER_CRITICAL(); //进入临界区 { //查询队列现在是否还有剩余存储空间,如果采用覆写方式入队的话那就不用在 //乎队列是不是满的啦。 if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) ||\ (1) ( xCopyPosition == queueOVERWRITE ) ) { traceQUEUE_SEND( pxQueue ); xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue,\ (2) xCopyPosition );
/**************************************************************************/
/**************************省略掉与队列集相关代码**************************/
/**************************************************************************/ { //检查是否有任务由于等待消息而进入阻塞态 if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) ==\(3) pdFALSE ) { if( xTaskRemoveFromEventList( &( pxQueue->\ (4) xTasksWaitingToReceive ) ) != pdFALSE ) { //解除阻塞态的任务优先级最高,因此要进行一次任务切换 queueYIELD_IF_USING_PREEMPTION(); (5) } else { mtCOVERAGE_TEST_MARKER(); } } else if( xYieldRequired != pdFALSE ) { queueYIELD_IF_USING_PREEMPTION(); } else { mtCOVERAGE_TEST_MARKER(); } } taskEXIT_CRITICAL(); return pdPASS; (6) } else { if( xTicksToWait == ( TickType_t ) 0 ) (7) { //队列是满的,并且没有设置阻塞时间的话就直接返回 taskEXIT_CRITICAL(); traceQUEUE_SEND_FAILED( pxQueue ); return errQUEUE_FULL; (8) } else if( xEntryTimeSet == pdFALSE ) (9) { //队列是满的并且指定了任务阻塞时间的话就初始化时间结构体 vTaskSetTimeOutState( &xTimeOut ); xEntryTimeSet = pdTRUE; } else { //时间结构体已经初始化过了, mtCOVERAGE_TEST_MARKER(); } } } taskEXIT_CRITICAL(); //退出临界区 vTaskSuspendAll(); (10) prvLockQueue( pxQueue ); (11) //更新时间壮态,检查是否有超时产生 if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE ) (12) { if( prvIsQueueFull( pxQueue ) != pdFALSE ) (13) { traceBLOCKING_ON_QUEUE_SEND( pxQueue ); vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToSend ), \ (14) xTicksToWait ); prvUnlockQueue( pxQueue ); (15) if( xTaskResumeAll() == pdFALSE ) (16) { portYIELD_WITHIN_API(); } } else { //重试一次 prvUnlockQueue( pxQueue ); (17) ( void ) xTaskResumeAll(); } } else { //超时产生 prvUnlockQueue( pxQueue ); (18) ( void ) xTaskResumeAll(); traceQUEUE_SEND_FAILED( pxQueue ); return errQUEUE_FULL; (19) } }
}
4.3 中断通用入队函数
中断级入队函数 xQueueGenericSendFromISR() 的定义如下:
BaseType_t xQueueGenericSendFromISR( QueueHandle_t xQueue, const void * const pvItemToQueue, BaseType_t * const pxHigherPriorityTaskWoken, const BaseType_t xCopyPosition )
{ BaseType_t xReturn; UBaseType_t uxSavedInterruptStatus; Queue_t * const pxQueue = ( Queue_t * ) xQueue; portASSERT_IF_INTERRUPT_PRIORITY_INVALID(); uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR(); { if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) ||\ (1) ( xCopyPosition == queueOVERWRITE ) ) { const int8_t cTxLock = pxQueue->cTxLock; (2) traceQUEUE_SEND_FROM_ISR( pxQueue ); ( void ) prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition ); (3) //队列上锁的时候就不能操作事件列表,队列解锁的时候会补上这些操作的。 if( cTxLock == queueUNLOCKED ) (4) { /**************************************************************************/
/**************************省略掉与队列集相关代码**************************/
/**************************************************************************/ if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == \ (5) pdFALSE ) { if( xTaskRemoveFromEventList( &( pxQueue->\ (6) xTasksWaitingToReceive ) ) != pdFALSE ) { //刚刚从事件列表中移除的任务对应的任务优先级更高,所以标记要进行任务切换 if( pxHigherPriorityTaskWoken != NULL ) { *pxHigherPriorityTaskWoken = pdTRUE; (7) } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } } else { //cTxLock 加一,这样就知道在队列上锁期间向队列中发送了数据 pxQueue->cTxLock = ( int8_t ) ( cTxLock + 1 ); (8) } xReturn = pdPASS; (9) } else { traceQUEUE_SEND_FROM_ISR_FAILED( pxQueue ); xReturn = errQUEUE_FULL; (10) }
} portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus ); return xReturn;
}
4.4 队列上锁和解锁
队列的上锁和解锁是两个 API 函数:prvLockQueue() 和 prvUnlockQueue() 。
上锁函数:#define prvLockQueue( pxQueue ) taskENTER_CRITICAL(); { if( ( pxQueue )->cRxLock == queueUNLOCKED ) { ( pxQueue )->cRxLock = queueLOCKED_UNMODIFIED; } if( ( pxQueue )->cTxLock == queueUNLOCKED ) { ( pxQueue )->cTxLock = queueLOCKED_UNMODIFIED; } } taskEXIT_CRITICAL() prvLockQueue() 函数就是将队列中的成员变量 cRxLock 和 cTxLock 设置为
queueLOCKED_UNMODIFIED 就可以;
解锁函数:static void prvUnlockQueue( Queue_t * const pxQueue )
{ //上锁计数器(cTxLock 和 cRxLock)记录了在队列上锁期间,入队或出队的数量,当队列 //上锁以后队列项是可以加入或者移除队列的,但是相应的列表不会更新。 taskENTER_CRITICAL(); { //处理 cTxLock。 int8_t cTxLock = pxQueue->cTxLock; while( cTxLock > queueLOCKED_UNMODIFIED ) (1) { /**************************************************************************/ /**************************省略掉与队列集相关代码**************************/
/**************************************************************************/ { //将任务从事件列表中移除 if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == \ (2) pdFALSE ) { if( xTaskRemoveFromEventList( &( pxQueue->\ (3) xTasksWaitingToReceive ) ) != pdFALSE ) { //从列表中移除的任务优先级比当前任务的优先级高,因此要 //进行任务切换。 vTaskMissedYield(); (4) } else { mtCOVERAGE_TEST_MARKER(); } } else { break; } } --cTxLock; (5) } pxQueue->cTxLock = queueUNLOCKED; (6) } taskEXIT_CRITICAL(); //处理 cRxLock。 taskENTER_CRITICAL(); { int8_t cRxLock = pxQueue->cRxLock; while( cRxLock > queueLOCKED_UNMODIFIED ) (7) { if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE ) { if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) !=\ pdFALSE ) { vTaskMissedYield(); } else { mtCOVERAGE_TEST_MARKER(); } --cRxLock; } else { break; } } pxQueue->cRxLock = queueUNLOCKED; } taskEXIT_CRITICAL();
}
4.5 从队列读取消息
有入队就会有出队,出队就是从队列中获取队列项(消息),FreeRTOS 中出队函数如下表所示:
任务级出队函数:
xQueueReceive() 从队列中读取队列项(消息),并且读取完以后删除掉队列项(消息)
xQueuePeek() 从队列中读取队列项(消息),并且读取完以后不删除队列项(消息)
中断级出队函数:
xQueueReceiveFromISR() 从队列中读取队列项(消息),并且读取完以后删除掉队列项(消息),用于中断服务函数中
xQueuePeekFromISR() 从队列中读取队列项(消息),并且读取完以后不删除队列项(消息),用于中断服务函数中
4.5.1 函数 xQueueReceive()
此函数用于在任务中从队列读取一条(请求)消息,读取成功以后就会将队列中的这条数据删除,此函数的本质是一个宏,真正执行的函数是 xQueueGneericReceive() 。此函数在读消息的时候是采用拷贝方式的,所以用户需要提供一个数组或缓冲区来保存读取到的数据,所读取的数据长度是创建队列的时候所设定的每个队列项目的长度,函数原型如下:
BaseType_t xQueueReceive(QueueHandle_t xQueue, void * pvBuffer, TickType_t xTicksToWait); 参数:
xQueue: 队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的队列句柄
pvBuffer: 保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中
xTicksToWait: 阻塞时间。此参数指示当队列空的时候任务进入阻塞态等待队列有数据的最大时间。如果为 0 的话当队列空的时候就立即返回;当为 portMAX_DELAY 的话就会一直等待,直到队列有数据,也就是死等,但是宏 INCLUDE_vTaskSuspend 必须为 1.返回值:
pdTRUE: 从队列中读取数据成功
pdFALSE: 从队列中读取数据失败
4.5.2 函数 xQueuePeek()
此函数用于在任务中从队列读取一条(请求)消息,读取成功以后不会将队列中的这条数据删除,此函数的本质是一个宏,真正执行的函数是 xQueueGneericReceive() 。此函数在读消息的时候是采用拷贝方式的,所以用户需要提供一个数组或缓冲区来保存读取到的数据,所读取的数据长度是创建队列的时候所设定的每个队列项目的长度,函数原型如下:
BaseType_t xQueuePeek(QueueHandle_t xQueue, void * pvBuffer, TickType_t xTicksToWait); 参数:
xQueue: 队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的队列句柄
pvBuffer: 保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中
xTicksToWait: 阻塞时间。此参数指示当队列空的时候任务进入阻塞态等待队列有数据的最大时间。如果为 0 的话当队列空的时候就立即返回;当为 portMAX_DELAY 的话就会一直等待,直到队列有数据,也就是死等,但是宏 INCLUDE_vTaskSuspend 必须为 1.返回值:
pdTRUE: 从队列中读取数据成功
pdFALSE: 从队列中读取数据失败
4.5.3 函数 xQueueGenericReceive()
不管是函数 xQueueReceive() 还是 xQueuePeek(),最终都是通过调用函数 xQueueGenericReceive(),此函数是真正起作用的,函数原型如下:
BaseType_t xQueueGenericReceive(QueueHandle_t xQueue, void* pvBuffer, TickType_t xTicksToWait BaseType_t xJustPeek) 参数:
xQueue: 队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的队列句柄
pvBuffer: 保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中
xTicksToWait: 阻塞时间。此参数指示当队列空的时候任务进入阻塞态等待队列有数据的最大时间。如果为 0 的话当队列空的时候就立即返回;当为 portMAX_DELAY 的话就会一直等待,直到队列有数据,也就是死等,但是宏 INCLUDE_vTaskSuspend 必须为 1.
xJustPeek: 标记当读取成功以后是否删除掉队列项,当为 pdTURE 的时候就不用删除,也就是说如果后面再调用函数 xQueueReceive() 获取到的队列项是一样的。当为 pdFALSE 的时候就会删除掉这个队列项。返回值:
pdTRUE: 从队列中读取数据成功
pdFALSE: 从队列中读取数据失败
4.5.4 函数 xQueueReceiveFromISR()
此函数是 xQueueReceive() 的中断版本,用于在中断服务函数中从队列中读取(请求)一条消息,读取成功以后就会将队列中的这条数据删除。此函数在读取消息的时候是采用拷贝方式的, 所以需要用户提供一个数组或缓冲区来保存读取到的数据,所读取的数据长度是创建队列的时 候所设定的每个队列项目的长度,函数原型如下:
BaseType_t xQueueReceiveFromISR(QueueHandle_t xQueue, void* pvBuffer, BaseType_t * pxTaskWoken); 参数:
xQueue: 队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的队列句柄。
pvBuffer: 保存数据的缓存区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中
pxTaskWoken: 标记退出此函数以后是否进行任务切换,这个变量的值是由函数来设置的,用户不用进行设置,用户只需要提供一个变量来保存这个值就行了。当此值为 pdTRUE 的时候在退出中断服务函数之前一定要进行一次任务切换。返回值:
pdTRUE: 从队列中读取数据成功。
pdFALSE: 从队列中读取数据失败。
4.5.5 函数 xQueuePeekFromISR()
此函数是 xQueuePeek() 的中断版本,此函数在读取成功以后不会将消息删除,此函数的原型为:
BaseType_t xQueuePeekFromISR(QueueHandle_t xQueue, void * pvBuffer)参数:
xQueue: 队列句柄,指明要读取哪个队列的数据,创建队列成功以后会返回此队列的队列句柄
pvBuffer: 保存数据的缓冲区,读取队列的过程中会将读取到的数据拷贝到这个缓冲区中返回值:
pdTRUE: 从队列中读取数据成功。
pdFALSE: 从队列中读取数据失败。
4.6 队列操作实验
4.6.1 实验设计
本实验设计三个任务:start_task、task1_task、Keyprocess_task 这三个任务的任务功能如下:
start_task:用来创建其他 2 个任务。
task1_task:读取按键的键值,然后将键值发送到队列 Key_Queue 中,并且检查队列的剩余容量等信息。
Keyprocess_task:按键处理任务,读取队列 Key_Queue 中的消息,根据不同的消息值做相应的处理。
实验还需要三个按键 KEY_UP、KEY2 和 KEY0,不同的按键对应不同的按键值,任务 task1_task 会将这些值发送到队列 Key_Queue 中。
实验中还创建了两个队列 Key_Queue 和 Message_Queue,队列 Key_Queue 用于传递按键值,队列 Message_Queue 用于传递串口发送过来的消息。
实验还需要两个中断:一个是串口 1 接收中断,一个是定时器 9 中断,他们的作用如下:
串口 1 接收中断:接收串口发送过来的数据,并将接收到的数据发送到队列 Message_Queue 中。
定时器 9 中断:定时周期设置为 500ms,在定时中断中读取队列 Message_Queue 中的消息,并将其显示在 LCD 上。
4.6.2 实验程序
4.6.2.1 USART.c
extern QueueHandle_t Message_Queue; //信息队列句柄
//QueueHandle_t queue.h 中定义void USART1_IRQHandler(void) //串口1中断服务程序
{u8 Res;//xHigherPriorityTaskWoken:用来标记退出此函数以后是否进行任务切换,这个变量的值由三个函数来设置,用户不再进行设置//用户只需要提供一个变量来保存这个值就可以了。//但是切记要注意:当此值为 pdTURE 的时候在退出中断服务函数之前一定要进行一次任务切换。BaseType_t xHigherPriorityTaskWoken; //BaseType_t 也在 queue.h 中定义if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是0x0d 0x0a结尾){Res =USART_ReceiveData(USART1);//(USART1->DR); //读取接收到的数据if((USART_RX_STA&0x8000)==0)//接收未完成{if(USART_RX_STA&0x4000)//接收到了0x0d{if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始else USART_RX_STA|=0x8000; //接收完成了 }else //还没收到0X0D{ if(Res==0x0d)USART_RX_STA|=0x4000;else{USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;USART_RX_STA++;if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收 } }} }//串口1中断要做的事:接收串口发送过来的消息,并且将消息发送到队列Message_Queue中//向队列发送串口接收到的数据if((USART_RX_STA&0X8000)&&(Message_Queue!=NULL)) //串口接收状态位 USART_RX_STA 的最高位为 1,表示接收到了数据,并且 Message_Queue 队列不为空{xQueueSendFromISR(Message_Queue,USART_RX_BUF,&xHigherPriorityTaskWoken); //向队列中发送数据// 第一个参数队列句柄,指明要向哪个队列发送数据,创建队列成功以后会返回此队列的队列句柄// 第二个参数指明要发送的消息,发送的时候会将这个消息拷贝到队列中;发送的消息就是串口接收缓存区的消息// 第三个参数标记退出此函数以后是否需要进行任务切换;当这个值为pdTURE的时候在退出中断服务函数之前一定要进行一次任务切换USART_RX_STA = 0; // 清空接收数据状态位memset(USART_RX_BUF,0,USART_REC_LEN); //清除数据接收缓存区 USART_RX_BUF,用于下一次数据接收portYIELD_FROM_ISR(xHigherPriorityTaskWoken); //如果需要的话进行一次任务切换}
}
4.6.2.2 Timer.c
#include "timer.h"
#include "led.h"
#include "usart.h"
#include "Malloc.h"
#include "usart.h"
#include "string.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"//FreeRTOS时间统计所用的节拍计数器
volatile unsigned long long FreeRTOSRunTimeTicks;//初始化定时器3使其为FreeRTOS时间统计提供时基
void ConfigureTimeForRunTimeStats(void)
{//定时器3初始化,定时器时钟为84M,分频系数为84-1,所以定时器3的频率为84M/84=1M//自动重装载为50-1,那么定时器周期就是50us 50/1000=0.05ms=50usFreeRTOSRunTimeTicks=0; //0.05ms清零重新计时一次TIM3_Int_Init(50-1,84-1); //初始化TIM3
}//通用定时器3中断初始化
//arr:自动重装值。
//psc:时钟预分频数
//定时器溢出时间计算方法:Tout=((arr+1)*(psc+1))/Ft us.
//Ft=定时器工作频率,单位:Mhz
//这里使用的是定时器3!
void TIM3_Int_Init(u16 arr,u16 psc)
{TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;NVIC_InitTypeDef NVIC_InitStructure;RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); ///使能TIM3时钟TIM_TimeBaseInitStructure.TIM_Period = arr; //自动重装载值TIM_TimeBaseInitStructure.TIM_Prescaler=psc; //定时器分频TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);//初始化TIM3TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE); //允许定时器3更新中断TIM_Cmd(TIM3,ENABLE); //使能定时器3NVIC_InitStructure.NVIC_IRQChannel=TIM3_IRQn; //定时器3中断NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0x01; //抢占优先级1NVIC_InitStructure.NVIC_IRQChannelSubPriority=0x00; //子优先级0NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;NVIC_Init(&NVIC_InitStructure);
}//通用定时器9中断初始化
//arr:自动重装值。
//psc:时钟预分频数
//定时器溢出时间计算方法:Tout=((arr+1)*(psc+1))/Ft us.
//Ft=定时器工作频率,单位:Mhz
//这里使用的是定时器9!
void TIM9_Int_Init(u16 arr,u16 psc)
{TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;NVIC_InitTypeDef NVIC_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM9,ENABLE); //使能TIM9时钟TIM_TimeBaseInitStructure.TIM_Period = arr; //自动重装载值TIM_TimeBaseInitStructure.TIM_Prescaler=psc; //定时器分频TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1; TIM_TimeBaseInit(TIM9,&TIM_TimeBaseInitStructure); //初始化TIM9TIM_ITConfig(TIM9,TIM_IT_Update,ENABLE); //允许定时器9更新中断TIM_Cmd(TIM9,ENABLE); //使能定时器9NVIC_InitStructure.NVIC_IRQChannel=TIM1_BRK_TIM9_IRQn; //定时器9中断NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0x08; //抢占优先级8NVIC_InitStructure.NVIC_IRQChannelSubPriority=0x00; //子优先级0NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;NVIC_Init(&NVIC_InitStructure);
}//定时器3中断服务函数
void TIM3_IRQHandler(void)
{if(TIM_GetITStatus(TIM3,TIM_IT_Update)==SET) //溢出中断{FreeRTOSRunTimeTicks++;}TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //清除中断标志位
}extern QueueHandle_t Message_Queue; //信息队列句柄
extern void disp_str(u8* str); // LCD屏上显示接收到的队列消息//定时器9中断服务函数
//定时器9中断要做的事情:每500ms产生一次定时器9中断,在定时器中断中读取Message_Queue中的消息,并将读取到的消息显示在LCD上
void TIM1_BRK_TIM9_IRQHandler(void)
{u8* buffer;BaseType_t xTaskWokenByReceive = pdFALSE; //参数为pdTURE的时候进行一次任务切换BaseType_t err;if(TIM_GetITStatus(TIM9,TIM_IT_Update)==SET) //获取中断状态,产生溢出中断{buffer = mymalloc(SRAMIN,USART_REC_LEN); //动态开辟内存,返回分配到的内存首地址,也就是buffer指针指向分配到的内存首地址if(Message_Queue!=NULL) //如果队列不为空,意味着一定是在串口中断中接收到了消息,并且将消息存储到了Message_Queue中{memset(buffer,0,USART_REC_LEN); //清除缓存区,把buffer指针指向的缓存区设置为0err = xQueueReceiveFromISR(Message_Queue,buffer,&xTaskWokenByReceive); //中断中从队列中读消息,读取成功以后就会将队列中的这条数据删除// 返回值为pdTURE或者pdFALSE,读取成功或者读取失败if(err==pdTRUE) //从队列中读取到了消息{//用于在LCD上显示接收到的队列的消息//参数str: 要显示的字符串(接收到的消息)disp_str(buffer); //在LCD上显示接收到的消息,也就是指针buffer指向分配内存上存储的消息}}myfree(SRAMIN,buffer); //释放指针指向位置上分配的内存portYIELD_FROM_ISR(xTaskWokenByReceive); //如果需要的话进行一次任务切换//在FreeRTOS中,portYIELD_FROM_ISR是一个宏定义,用于在中断服务例程(ISR)中发起上下文切换。}TIM_ClearITPendingBit(TIM9,TIM_IT_Update); //清除中断标志位
}
4.6.2.3 main.c
#include "stm32f4xx.h"
#include "FreeRTOS.h" //这里注意必须先引用FreeRTOS的头文件,然后再引用task.h
#include "task.h" //存在一个先后的关系
#include "LED.h"
#include "LCD.h"
#include "Key.h"
#include "usart.h"
#include "delay.h"
#include "string.h"
#include "beep.h"
#include "malloc.h"
#include "timer.h"
#include "queue.h"//任务优先级
#define START_TASK_PRIO 1 //用于创建其他两个任务
//任务堆栈大小
#define START_STK_SIZE 256
//任务句柄
TaskHandle_t StartTask_Handler;
//任务函数
void start_task(void *pvParameters);//任务优先级
#define TASK1_TASK_PRIO 2 //读取按键的键值,将键值发送到队列Key_Queue中,检查队列的剩余容量;相当于写队列
//任务堆栈大小
#define TASK1_STK_SIZE 256
//任务句柄
TaskHandle_t Task1Task_Handler;
//任务函数
void task1_task(void *pvParameters);//任务优先级
#define KEYPROCESS_TASK_PRIO 3 //相当于读队列,读取队列Key_Queue中的消息,根据不同的消息值做出相应的处理
//任务堆栈大小
#define KEYPROCESS_STK_SIZE 256
//任务句柄
TaskHandle_t Keyprocess_Handler;
//任务函数
void Keyprocess_task(void *pvParameters);//按键 消息队列 的数量
#define KEYMSG_Q_NUM 1 //按键消息队列的数量
#define MESSAGE_Q_NUM 4 //发送数据的消息队列的数量
QueueHandle_t Key_Queue; //按键值消息队列句柄
QueueHandle_t Message_Queue; //信息队列句柄//LCD刷屏时使用的颜色
int lcd_discolor[14]={ WHITE, BLACK, BLUE, BRED,GRED, GBLUE, RED, MAGENTA,GREEN, CYAN, YELLOW, BROWN,BRRED, GRAY};//用于在LCD上显示接收到的队列的消息
//str: 要显示的字符串(接收到消息)
void disp_str(u8* str)
{LCD_Fill(5,230,110,245,WHITE); //首先清空要显示的区域LCD_ShowString(5,230,100,16,16,str);
}//加载主页面
void freertos_load_main_ui(void)
{POINT_COLOR = RED;LCD_ShowString(10,10,200,16,16,"ATK STM32F407");LCD_ShowString(10,30,200,16,16,"FreeRTOS Example");LCD_ShowString(10,50,200,16,16,"Message Queue");LCD_ShowString(10,70,200,16,16,"KEY_UP:LED1 KEY0:Refresh LCD");LCD_ShowString(10,90,200,16,16,"KEY1:SendMsg KEY2:BEEP");POINT_COLOR = BLACK;LCD_DrawLine(0,107,239,107); //画线LCD_DrawLine(119,107,119,319); //画线LCD_DrawRectangle(125,110,234,314); //画矩形POINT_COLOR = RED;LCD_ShowString(0,130,120,16,16,"DATA_Msg Size:");LCD_ShowString(0,170,120,16,16,"DATA_Msg rema:");LCD_ShowString(0,210,100,16,16,"DATA_Msg:");POINT_COLOR = BLUE;
}//查询Message_Queue队列中的总队列数量和剩余队列数量
void check_msg_queue(void)
{u8 *p;u8 msgp_remain_size; //消息队列剩余大小u8 msgp_total_size; //消息队列总大小taskENTER_CRITICAL(); //进入临界区msgp_remain_size = uxQueueSpacesAvailable(Message_Queue); //得到队列的剩余大小msgp_total_size = uxQueueMessagesWaiting(Message_Queue) + uxQueueSpacesAvailable(Message_Queue); //得到队列总大小,总大小等于使用的加上剩余的p = mymalloc(SRAMIN,20); //申请内存,指针p指向申请内存空间的起始位置sprintf((char*)p,"Total Size:%d",msgp_total_size); //显示DATA_Message消息队列总的大小LCD_ShowString(10,150,100,16,16,p);sprintf((char*)p,"Remain Size:%d",msgp_remain_size); //显示DATA_Message消息队列剩余的大小LCD_ShowString(10,190,100,16,16,p);myfree(SRAMIN,p); //释放内存taskEXIT_CRITICAL(); //退出临界区
}int main(void)
{NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); //设置中断优先级分组为4delay_init(168);uart_init(115200);LED_Init();KEY_Init();BEEP_Init();LCD_Init();TIM9_Int_Init(5000,16800-1); //初始化定时器9,500ms// 这里计算方式是 TIM9 定时器来自于APB2,频率为168MHz,也就是168000,168000/16800=10,5000是计时器的数值,要达到5000,那么需要5000/10=500msmy_mem_init(SRAMIN); //初始化内部内存池freertos_load_main_ui(); //加载主ui//创建开始任务xTaskCreate((TaskFunction_t )start_task, //任务函数(const char* )"start_task", //任务名称(uint16_t )START_STK_SIZE, //任务堆栈大小(void* )NULL, //传递给任务函数的参数(UBaseType_t )START_TASK_PRIO, //任务优先级(TaskHandle_t* )&StartTask_Handler);//任务句柄vTaskStartScheduler(); //开启任务调度
}//开始任务的任务函数
void start_task(void *pvParameters)
{taskENTER_CRITICAL();//创建消息队列Key_Queue = xQueueCreate(KEYMSG_Q_NUM,sizeof(u8)); //创建消息队列Key_QueueMessage_Queue = xQueueCreate(MESSAGE_Q_NUM,USART_REC_LEN); //创建消息队列Message_Queue,队列长度是串口接收缓冲区的长度//xQueueCreate的第一个参数是队列的长度,也就是队列的项目数,按键消息一个,发送消息4个//第二个参数是每个项目的长度,单位是字节,按键消息是unsigned char类型;消息队列是串口接收到的字节长度//创建Task1任务xTaskCreate((TaskFunction_t )task1_task, //任务函数(const char* )"task1_task", //任务名称(uint16_t )TASK1_STK_SIZE, //任务堆栈大小(void* )NULL, //传递给任务函数的参数(UBaseType_t )TASK1_TASK_PRIO, //任务优先级(TaskHandle_t* )&Task1Task_Handler);//任务句柄//创建Task2任务xTaskCreate((TaskFunction_t )Keyprocess_task, //任务函数(const char* )"Keyprocess_task", //任务名称(uint16_t )KEYPROCESS_STK_SIZE, //任务堆栈大小(void* )NULL, //传递给任务函数的参数(UBaseType_t )KEYPROCESS_TASK_PRIO, //任务优先级(TaskHandle_t* )&Keyprocess_Handler);//任务句柄vTaskDelete(StartTask_Handler); //删除开始任务taskEXIT_CRITICAL(); //退出临界区
}//task1任务函数
//读取按键的键值,然后将键值发送到队列Key_Queue中,检查队列的剩余容量等信息
void task1_task(void *pvParameters)
{u8 key,i=0;BaseType_t err;while(1){key=KEY_Scan(0); //按键扫描 KEY0返回1,KEY1返回2,KEY2返回3,KEY_UP返回4,无按键按下返回0if((Key_Queue!=NULL)&&(key)) //按键队列不为空,证明队列创建成功,并且有按键被按下{err=xQueueSend(Key_Queue,&key,10); //向队列发送消息//第一个参数是要发送队列的队列句柄//第二个参数指向要发送的消息,这里取地址,获取按键值//第三个参数设置阻塞时间//函数返回值发送消息成功或者队列已满,发送消息失败if(err==errQUEUE_FULL) //#define errQUEUE_FULL ( ( BaseType_t ) 0 ){printf("队列Key_Queue已满,数据发送失败!\r\n");}}i++;if(i%10==0){check_msg_queue(); //检查Message_Queue队列的容量}if(i==50){i=0;LED0=!LED0;}vTaskDelay(10); //延时10ms,也就是10个时钟节拍 }
}//KeyProcess_task函数
//读取队列中的消息,根据不同的消息做出对应的处理
void Keyprocess_task(void *pvParameters)
{u8 num,key;while(1){if(Key_Queue!=NULL) //按键队列不为空{if(xQueueReceive(Key_Queue,&key,portMAX_DELAY)) //从消息队列中读取消息,并且if判断语句为真,表示读取成功//第一个参数为要读取队列的队列句柄//第二个参数为将读取到的数据保存在key缓存区中//第三个参数表示阻塞时间,这里设置为portMAX_DELAY表示如果从队列中读取不到数据,那么就一直读//并且读取完数据后删除缓存区{//按键的队列长度为1,所以每次只会case其中的一种情况switch(key) //这里的key只会为1 2 3 4 也就是KEY0 KEY1 KEY2 KEY_UP的按键键值大小{case WKUP_PRES: //KEY_UP控制LED1LED1=!LED1;break;case KEY2_PRES: //KEY2控制蜂鸣器BEEP=!BEEP;break;case KEY0_PRES: //KEY0刷新LCD背景num++;LCD_Fill(126,111,233,313,lcd_discolor[num%14]);break;}}}vTaskDelay(10); //延迟10ms,也就是10个时钟节拍}
}