小编正在学习嵌入式软件,目前建立了一个交流群,可以留下你的评论,我拉你进群
一、简介
队列是为了任务与任务、任务与中断之间的通信而准备的,可以在任务与任务、任务与中断之间消息传递,队列中可以存储有限的、大小固定的数据项目。
通常队列采用先进先出的存储缓冲机制(FIFO)
往队列中发送数据称为入队,从队列中读取数据称为出队
FreeRTOS中队列是通过值传递(默认)进行存储的,如果大数据的时候,也可以通过数据引用(只传递数据的指针)进行存储
队列不属于某个任务,任何任务与中断都可以向队列读取/发送数据
注意:在出队,入队时候代码是进入临界区的,也就是说,在向队列读写数据时不可被比FreeRTOS可管理的最高中断优先级低的中断所打断
出队阻塞:当一个任务要从一个队列中读数据时,但这个队列是空的,这时任务该怎么办呢?一是立刻返回任务继续执行接下来的代码,二是等一段时间后再返回任务,三是一直等待,直到队列中有数据,读到数据再返回任务;这三种方式取决于用户设置的阻塞时间,如果阻塞时间为0就是第一种情况,若是阻塞时间为portMAX_DELAY就是第三种情况,若阻塞时间在0~portMAX_DELAY就是第二种情况
当阻塞时,将该任务的状态列表项挂载到pxDelayTaskList;将该任务的事件列表项挂载到List_t xTasksWaitingToReceive;
入队阻塞:当任务向队列中发数据,而此时队列是满的,没有多余的空间给了,这时任务该怎么办?这与出对遇到的情况类似,程序可以根据有用设定的阻塞时间进行等待
当阻塞时,将该任务的状态列表项挂载到pxDelayTaskList;将该任务的事件列表项挂载到List_t xTasksWaitingToSend;
当多个任务往一个已经满了的队列中写入时,任务均会进入阻塞状态,当队列有空间时,哪个任务先往队列中写入呢?
这里遵循两个原则:1、优先级最高的任务先写入 ;2、若优先级相同,等待时间最长的任务写入
二、队列结构体 /API
2.1 结构体
typedef struct QueueDefinition
{
int8_t * pcHead; /*存储区域的起始地址,也就是队列项的起始地址 */
int8_t * pcWriteTo; /*下一个队列项写入的位置 */union
{
QueuePointers_t xQueue; /* 该结构体用于队列的选项*/
SemaphoreData_t xSemaphore; /*该结构体用于互斥信号量/递归信号量的选项*/
} u;List_t xTasksWaitingToSend; /* 等待发送列表 */
List_t xTasksWaitingToReceive; /*等待接收列表 */volatile UBaseType_t uxMessagesWaiting; /*非空闲队列项目的数量,队列中已使用的*/
UBaseType_t uxLength; /*队列长度 */
UBaseType_t uxItemSize; /*对列项目的大小*/volatile int8_t cRxLock; /*读取上锁计数器,当使用时操作不了等待接收列表*/
volatile int8_t cTxLock; /*写入上锁计数器,当使用时操作不了等待发送列表 *//*其他的一些条件编译*/
} xQUEUE;
2.2 API
创建队列API:
使用队列的主要流程:创建队列->写入队列->读取队列
函数 | 描述 |
xQueueCreate() | 动态方式创建队列 (队列所需内存空间由FreeRTOS从FreeRTOS管理的堆中分配) |
xQueueCreateStatic() | 静态方式创建队列 (需要用户自行分配内存) |
#define xQueueCreate( uxQueueLength, uxItemSize ) xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )
创建队列API函数xQueueCreate()相当于API函数xQueueGenericCreate(),但后者多了一个参数queueQUEUE_TYPE_BASE
/* For internal use only. These definitions *must* match those in queue.c. */
#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 ) //递归互斥信号量
创建队列API流程:
创建队列:xQueueCreat() ——>实际执行的是xQueueGenericCreat()
1、计算队列需要多大内存xQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize );
2、为队列申请内存,申请大小为sizeof( Queue_t ) + xQueueSizeInBytes,前面部分存放结构体成员,后面存放队列项
3、判断内存是否申请成功,成功即计算出队列项存储的首地址pucQueueStorage = ( uint8_t * ) pxNewQueue;
4、调用prvInitialiseNewQueue()初始化新队列pxNewQueue
<1>初始化队列结构体成员变量
<2>调用xQueueGenericReset()复位队列
①初始化其他队列结构体成员变量
②判断要复位的队列是否为新创建的队列
若不是新创建的队列,那就复位它,将列表xTasksWaitingToSend移出
若是新创建的队列,那就初始化xTasksWaitingToSend,xTasksWaitingToReceive这两个列表
入队API:

带有覆写功能的函数只适用于队列项为1的队列
四个任务级入队函数调用的其实是相同的一个函数
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: 覆写队列(仅用于队列的队列长度为1)返回值:
pdTRUE :向队列发送消息成功
errQUEUE_FULL:队列已满,发送消息失败

出队API

BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer, TickType_t xTicksToWait )
BaseType_t xQueuePeek( QueueHandle_t xQueue,
void * const pvBuffer, TickType_t xTicksToWait )
@xQueue:待读取的队列
@pvBuffer:消息读取缓冲区
@xTicksToWait:阻塞时间
返回值:读取成功与失败

三、信号量
3.1 简介
信号量是一种解决同步问题的机制,可以实现对共享资源的有序访问其中,
“同步”指的是任务间的同步,即信号量可以使得一个任务等待另一个任务完成某件事情后才继续执行;
“有序访问”指的是对被多任务或中断访问的共享资源(如全局变量)的管理,当一个任务在访问(读取或写入)一个共享资源时,信号量可以防止其他任务或中断在这期间访问(读取或写入)这个共享资源
举一个例子,假设某个停车场有 100 个停车位(共享资源),这个 100 个停车位对所有人(访问共享资源的任务或中断)开放。如果有一个人要在这个停车场停车,那么就需要先判断这个停车场是否还有空车位(判断信号量是否有资源), 如果此时停车场正好有空车位(信号量有资源),那么就可以直接将车开入空车位进行停车(获取信号量成功), 如果此时停车场已经没有空车位了(信号量没有资源),那么这个人可以选择不停车(获取信号量失败),也可以选择等待(任务阻塞)其他人将车开出停车场(释放信号量资源), 让后再将车停入空车位。
在上面的这个例子中,空车位的数量相当于信号量的资源数,获取信号量相当于占用了空车位,而释放信号量就相当于让出了占用的空车位。信号量用于管理共享资源的场景相当于对共享资源上了个锁,只有任务成功获取到了锁的钥匙,才能够访问这个共享资源,访问完共享资源后还得归还钥匙,当然钥匙可以不只一把,即信号量可以有多个资源

3.2 二值信号量
3.2.1简介
二值信号量实际上就是一个队列长度为 1 的队列,在这种情况下,队列就只有空(0)和满(1)两种情况,二值信号量通常用于互斥访问或任务同步, 与互斥信号量比较类似,但是二值信号量有可能会导致优先级翻转的问题。
互斥访问:相当于有一扇门,两个任务,谁获得钥匙谁进入,只有一个任务可以打开门
互斥信号量:二值信号量存在优先级翻转,互斥信号存在优先级继承

释放信号量相当于将该标志位置满(1),获取标志相当于将标志位置空(0)
3.2.2相关API函数
使用二值信号量的过程:创建二值信号量-->释放二值信号量(相当于入队)-->获取二值信号量(相当于出队)
函数 | 描述 |
xSemaphoreCreateBinary() | 使用动态方式创建二值信号量,创建成功的返回值为二值信号量的句柄 |
xSemaphoreCreateBinaryStatic() | 使用静态方式创建二值信号量 |
xSemaphoreTake() | 获取信号量 |
xSemaphoreTakeFromISR(). | 在中断中获取信号量 |
xSemaphoreGive() | 释放信号量 |
xSemaphoreGiveFromISR() | 在中断中释放信号量 |
vSemaphoreDelete() | 删除信号量 |
创建二值信号量函数xSemaphoreCreateBinary(void)其实是调用函数xQueueGenericCreate( 1, ( semSEMAPHORE_QUEUE_ITEM_LENGTH ), (queueQUEUE_TYPE_BINARY_SEMAPHORE ) ),
@1:队列长度为 1
@semSEMAPHORE_QUEUE_ITEM_LENGTH:空
@queueQUEUE_TYPE_BINARY_SEMAPHORE:二值信号量
返回值:
创建成功返回句柄
创建失败
释放信号量函数xSemaphoreGive(SemaphoreHandle_t xSemaphore)其实调用函数
xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK);
@xSemaphore:句柄
@NULL:要传入的数据
@semGIVE_BLOCK_TIME:阻塞时间为0,不支持阻塞
@queueSEND_TO_BACK:尾部插入
返回值:释放信号量成功;释放信号量失败
获取信号量函数xSemaphoreTake(xSemaphore,xBlockTime)其实调用函数xQueueSemaphoreTake()来实现的
@xSemaphore:句柄
@xBlockTime:阻塞时间,用户可设置
返回值:获取信号量成功;超时,获取信号量失败
3.3 计数型信号量
3.3.1简介
计数型信号量相当于队列长度大于1的队列,因此计数型信号量能够容纳多个资源,这在计数信号量被创建的时候确定的
使用场景一,时间计数:在这种场合下,每次事件发生后,在事件处理函数中释放计数型信号量(计数型信号量的资源数加 1),其他等待事件发生的任务获取计数型信号量(计数型信号量的资源数减 1),这么一来等待事件发生的任务就可以在成功获取到计数型信号量之后执行相应的操作。在这种场合下,计数型信号量的资源数一般在创建时设置为 0。
使用场景二,资源管理:在这种场合下,计数型信号量的资源数代表着共享资源的可用数量一个任务想要访问共享资源,就必须先获取这个共享资源的计数型信号量,之后在成功获取了计数型信号量之后,才可以对这个共享资源进行访问操作,当然,在使用完共享资源后也要释放这个共享资源的计数型信号量。在这种场合下,计数型信号量的资源数一般在创建时设置为受其管理的共享资源的最大可用数量
3.3.2相关API函数
使用计数型信号量的过程:创建计数型信号量-->释放计数型信号量(相当于入队)-->获取计数型信号量(相当于出队)
计数型信号量的就是一个队列长度为计数型信号量最大资源数的队列,而队列的非空闲项目数量就是用来记录计数型信号量的可用资源的
函数 | 描述 |
xSemaphoreCreateCounting() | 使用动态方式创建计数型信号量 |
xSemaphoreCreateCountingStatic() | 使用静态方式创建计数型信号量 |
xSemaphoreTake() | 获取信号量 |
xSemaphoreTakeFromISR() | 在中断中获取信号量 |
xSemaphoreGive() | 释放信号量 |
xSemaphoreGiveFromISR() | 在中断中释放信号量 |
vSemaphoreDelete() | 删除信号量 |
uxSemaphoreGetCount | 获取信号量的计数值 |
创建计数型信号量:xSemaphoreCreateCounting(uxMaxCount, uxInitialCount)其实调用函数xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ))
@ uxMaxCount:计数值的最大限定
@uxInitialCount:计数值的初始值
返回值:NULL:失败;创建成功返回计数型信号量句柄
获取信号量的计数值:uxSemaphoreGetCount()其实调用函数uxQueueMessagesWaiting(QueueHandle_t xSemaphore)
@xSemaphore:句柄
返回值:当前信号量计数值大小
3.4 优先级翻转
优先级翻转:当一个高优先级任务因获取一个被低优先级任务获取而处于没有资源状态的二值信号量时,这个高优先级的任务将被阻塞,直到低优先级的任务释放二值信号量,而在这之前,如果有一个优先级介于这个高优先级任务和低优先级任务之间的任务就绪,那么这个中等优先级的任务就会抢占低优先级任务的运行, 这么一来,这三个任务中优先级最高的任务反而要最后才运行
在使用二值信号量和计数型信号量的时候,经常会遇到优先级翻转的问题,优先级在抢占式内核中是非常常见的,但是在实时操作系统中是不允许出现优先级翻转的,因为优先级翻转会破坏任务的预期顺序,可能会导致未知的严重后果,下面展示了一个优先级翻转的例子
例如:
前提条件:
任务H优先级最高,其先进行阻塞,获取信号量,释放信号量
任务M优先级中等,其先进行阻塞,循环打印
任务L优先级最低,其先运行,获取信号量,释放信号量
任务L先运行,获取信号量后,信号量为0;此时任务H就绪,抢占任务L,任务H进行获取信号量,但是由于信号量由任务L获取后未释放,所以目前H无法获得信号量而进行阻塞,此时任务L就绪运行,运行过程中任务M就绪对任务L进行抢占并运行,运行完毕后阻塞,由于任务L没有释放信号量,任务H一直被阻塞,任务L运行,释放信号量,任务H立刻获取信号量;也即是说当任务L阻塞时,优先级最高的任务L应该时执行的,但是由于任务L把持这信号量,导致任务H因等待信号量被阻塞,被任务M抢占运行。
高优先级任务被低优先级任务阻塞,导致高优先级任务迟迟得不到调度,但其他中等优先级的任务可以抢占CPU资源,从现象看就像中等优先级的任务比高优先级的任务有更高的优先权(即优先级翻转)
3.5 互斥信号量
3.5.1 简介
互斥信号量其实就是一个拥有优先级继承的二值信号量,在同步的应用中(任务与任务或中断与任务之间的同步)二值信号量最适合。互斥信号量适合用于那些需要互斥访问的应用中。在互斥访问中互斥信号量相当于一把钥匙, 当任务想要访问共享资源的时候就必须先获得这把钥匙,当访问完共享资源以后就必须归还这把钥匙,这样其他的任务就可以拿着这把钥匙去访问资源。
当一个互斥信号量正在被一个低优先级的任务持有时, 如果此时有个高优先级的任务也尝试获取这个互斥信号量,那么这个高优先级的任务就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级,这个过程就是优先级继承。
优先级继承尽可能的减少了高优先级任务处于阻塞态的时间,并且将“优先级翻转”的影响降到最低。
优先级继承并不能完全的消除优先级翻转的问题,它只是尽可能的降低优先级翻转带来的影响。实时应用应该在设计之初就要避免优先级翻转的发生
互斥信号量不能用于中断服务函数中,原因如下:
(1) 互斥信号量有任务优先级继承的机制, 但是中断不是任务,没有任务优先级, 所以互斥信号量只能用与任务中,不能用于中断服务函数。
(2) 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。
3.5.2 相关API
使用流程(与二值信号量有区别):创建互斥信号量-->获取信号量-->释放信号量
注意:创建互斥信号量是,函数内部会主动释放信号量
函数 | 描述 |
xSemaphoreCreateMutex() | 使用动态方式创建互斥信号量 |
xSemaphoreCreateMutexStatic() | 使用静态方式创建互斥信号量 |
xSemaphoreTake() | 获取信号量 |
xSemaphoreGive() | 释放信号量 |
vSemaphoreDelete() | 删除信号量 |
互斥信号量的就是一个队列长度为 1 的队列, 且队列项目的大小为 0, 而队列的非空闲项目数量就是互斥信号量的资源数