FreeRTOS(2):队列、信号量、互斥量

发布时间 2023-11-09 09:43:44作者: xsgcumt

1、队列

 1.1 数据传输方法

任务之间如何传输数据

  数据个数 互斥措施 阻塞-唤醒
全局变量 1
环形缓冲区 多个
队列 多个

队列又称消息队列,是一种常用于任务间通信的数据结构,队列可以在任务与任务间、中断和任务间传递信息。

为什么不使用全局变量?

如果使用全局变量,任务1修改了变量 a ,等待任务3处理,但任务3处理速度很慢,在处理数据的过程中,任务2有可能又修改了变量 a ,导致任务3有可能得到的不是正确的数据。

在这种情况下,就可以使用队列。任务1和任务2产生的数据放在流水线上,任务3可以慢慢一个个依次处理。

关于队列的几个名词:

队列项目:队列中的每一个数据;

队列长度:队列能够存储队列项目的最大数量;

创建队列时,需要指定队列长度及队列项目大小。

1.2 队列的特性

1.2.1常规操作

队列的简化操如入下图所示,从此图可知:

  ⚫ 队列可以包含若干个数据:队列中有若干项,这被称为"长度"(length)

  ⚫ 每个数据大小固定

  ⚫ 创建队列时就要指定长度、数据大小

  ⚫ 数据的操作采用先进先出的方法(FIFO,First In First Out):写数据时放到尾 部,读数据时从头部读

  ⚫ 也可以强制写队列头部:覆盖头部数据

 更详细的操作入下图所示:

 1.2.2 队列传输数据的两种方法

使用队列传输数据时有两种方法:

  ⚫ 拷贝:把数据、把变量的值复制进队列里

  ⚫ 引用:把数据、把变量的地址复制进队列里

FreeRTOS 使用拷贝值的方法,这更简单:

  ⚫ 局部变量的值可以发送到队列中,后续即使函数退出、局部变量被回收,也 不会影响队列中的数据

  ⚫ 无需分配 buffer 来保存数据,队列中有 buffer

  ⚫ 局部变量可以马上再次使用

  ⚫ 发送任务、接收任务解耦:接收任务不需要知道这数据是谁的、也不需要发 送任务来释放数据

  ⚫ 如果数据实在太大,你还是可以使用队列传输它的地址

  ⚫ 队列的空间有 FreeRTOS 内核分配,无需任务操心

  ⚫ 对于有内存保护功能的系统,如果队列使用引用方法,也就是使用地址,必 须确保双方任务对这个地址都有访问权限。使用拷贝方法时,则无此限制:内核 有足够的权限,把数据复制进队列、再把数据复制出队列。

1.2.3 队列的阻塞

只要知道队列的句柄,谁都可以读、写该队列。任务、ISR 都可读、写队列。可以多个 任务读写队列。

任务读写队列时,简单地说:如果读写不成功,则阻塞;可以指定超时时间。口语化地 说,就是可以定个闹钟:如果能读写了就马上进入就绪态,否则就阻塞直到超时。

队列:1、环形Buffer  2、两个链表:senderList和ReceiverList

如:一个任务B,它想读队列的时候,如果读不到数据并且它愿意等待的话,它将从就绪链表里移除,会放入这个队列的receiverList里,并且会放入一个Delayed链表里。然后A写队列,唤醒,它会去队列receiverList里看看是否为空,如果不空,则取出第一个任务唤醒,同时把B从receiverList和Delayed里面删除,重新放入ReadyList里。

1.3 队列函数

1.3.1、创建队列

队列的创建有两种方法:动态分配内存、静态分配内存

  ⚫ 动态分配内存:xQueueCreate,队列的内存在函数内部动态分配 函数原型如下:

QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );

  

参数 说明
uxQueueLength 队列长度,最多能存放多少个数据(item)
uxItemSize 每个数据(item)的大小:以字节为单位
返回值

非 0:成功,返回句柄,以后使用句柄来操作队列

NULL:失败,因为内存不足

  ⚫ 静态分配内存:xQueueCreateStatic,队列的内存要事先分配好

函数原型如下:

QueueHandle_t xQueueCreateStatic(
 UBaseType_t uxQueueLength,
UBaseType_t uxItemSize,
uint8_t *pucQueueStorageBuffer,
 StaticQueue_t *pxQueueBuffer
 );

  

参数 说明
uxQueueLength 队列长度,最多能存放多少个数据(item)
uxItemSize 每个数据(item)的大小:以字节为单位
pucQueueStorageBuffer

如果 uxItemSize 非 0,pucQueueStorageBuffer 必须 指向一个 uint8_t 数组,

此数组大小至少为"uxQueueLength * uxItemSize"

pxQueueBuffer 必须执行一个 StaticQueue_t 结构体,用来保存队列的数据结构
返回值

非 0:成功,返回句柄,以后使用句柄来操作队列

NULL:失败,因为 pxQueueBuffer 为 NULL

 

1.3.2、读队列

读队列总共有以下几个函数:

 

函数 描述
xQueueReceive() 从队列头部读取消息,并删除消息
xQueuePeek() 从队列头部读取消息,但是不删除消息
xQueueReceiveFromISR() 在中断中从队列头部读取消息,并删除消息
xQueuePeekFromISR() 在中断中从队列头部读取消息

 

BaseType_t xQueueReceive(QueueHandle_t xQueue,void *pvBuffer,TickType_t xTicksToWait);

  

参数 说明
xQueue 队列句柄,要读哪个队列
pvBuffer bufer 指针,队列的数据会被复制到这个 buffer 复制多大的数据?在创建队列时已经指定了数据大小
xTicksToWait

如果队列空则无法读出数据,可以让任务进入阻塞状态,

xTicksToWait 表示阻塞的最大时间(Tick Count)。

如果被设为 0,无法读出数据时函数会立刻返回;

如果被设为 portMAX_DELAY,则会一直阻塞直到有数据可写

返回值

pdPASS:从队列读出数据入

errQUEUE_EMPTY:读取失败,因为队列空了

 

1.3.3、写队列

写队列总共有以下几个函数:

函数 描述
xQueueSend() 往队列的尾部写入消息
xQueueSendToBack() 同 xQueueSend()
xQueueSendToFront() 往队列的头部写入消息
xQueueOverwrite() 覆写队列消息(只用于队列长度为 1 的情况)
xQueueSendFromISR() 在中断中往队列的尾部写入消息
xQueueSendToBackFromISR() 同 xQueueSendFromISR()
xQueueSendToFrontFromISR() 在中断中往队列的头部写入消息
xQueueOverwriteFromISR() 在中断中覆写队列消息(只用于队列长度为 1 的情况)

 

BaseType_t xQueueSend(QueueHandle_t xQueue,const void * pvItemToQueue,TickType_t xTicksToWait);

  

参数 说明
xQueue 队列句柄,要写哪个队列
pvItemToQueue

数据指针,这个数据的值会被复制进队列,

复制多大的数据?在创建队列时已经指定了数据大小

xTicksToWait

如果队列满则无法写入新数据,可以让任务进入阻塞状 态,

xTicksToWait 表示阻塞的最大时间(Tick Count)。

如果被设为 0,无法写入数据时函数会立刻返回;

如果被设为 portMAX_DELAY,则会一直阻塞直到有空间可写

返回值

pdPASS:数据成功写入了队列

errQUEUE_FULL:写入失败,因为队列满了。

 

2、信号量

2.1 信号量的特性

2.1.1 信号量的常规操作

信号量这个名字很恰当:

⚫ 信号:起通知作用

⚫ 量:还可以用来表示资源的数量

  ◼ 当"量"没有限制时,它就是"计数型信号量"(Counting Semaphores)

  ◼ 当"量"只有 0、1 两个取值时,它就是"二进制信号量"(Binary Semaphores)

⚫ 支持的动作:"give"给出资源,计数值加 1;"take"获得资源,计数值减 1

计数型信号量的典型场景是:

⚫ 计数:事件产生时"give"信号量,让计数值加 1;处理事件时要先"take"信号量,就是获得信号量,让计数值减 1。

⚫ 资源管理:要想访问资源需要先"take"信号量,让计数值减 1;用完资源后 "give"信号量,让计数值加 1。

信号量的"give"、"take"双方并不需要相同,可以用于生产者-消费者场合:

⚫ 生产者为任务 A、B,消费者为任务 C、D

⚫ 一开始信号量的计数值为 0,如果任务 C、D 想获得信号量,会有两种结果:

  ◼ 阻塞:买不到东西咱就等等吧,可以定个闹钟(超时时间)

  ◼ 即刻返回失败:不等

⚫ 任务 A、B 可以生产资源,就是让信号量的计数值增加 1,并且把等待这个资源的顾客唤醒

⚫ 唤醒谁?谁优先级高就唤醒谁,如果大家优先级一样就唤醒等待时间最长的人二进制信号量跟计数型的唯一差别,就是计数值的最大值被限定为1。

2.2.2 信号量跟队列的对比

队列 信号量

可以容纳多个数据,

创建队列时有 2 部分内存: 队列结构体、存储数据的空间

只有计数值,无法容纳其他数据。

创建信号量时,只需要分配信号量结构体

生产者:没有空间存入数据时可以阻塞 生产者:用于不阻塞,计数值已经达到最大时返回失败
消费者:没有数据时可以阻塞 消费者:没有资源时可以阻塞

 

2.2.3 两种信号量的对比

信号量的计数值都有限制:限定了最大值。如果最大值被限定为 1,那么它就是二值信号量;如果最大值不是 1,它就是计数型信号量。

二值信号量其实就是一个长度为1,大小为零的队列,只有0和1两种状态,通常情况下,我们用它来进行互斥访问或任务同步。

互斥访问:比如门钥匙,只有获取到钥匙才可以开门

任务同步:比如我录完视频你才可以看视频

计数型信号量相当于队列长度大于1 的队列,因此计数型信号量能够容纳多个资源,这在计数型信号量被创建的时候确定的

二值信号量 计数型信号量
被创建时初始值为 0 被创建时初始值可以设定
其他操作是一样的 其他操作是一样的

2.2 信号量函数

2.2.1 二值信号量相关API

函数 描述
xSemaphoreCreateBinary() 使用动态方式创建二值信号量
xSemaphoreCreateBinaryStatic() 使用静态方式创建二值信号量
xSemaphoreGive() 释放信号量
xSemaphoreGiveFromISR() 在中断中释放信号量
xSemaphoreTake() 获取信号量
xSemaphoreTakeFromISR() 在中断中获取信号量

(1).创建二值型信号量

SemaphoreHandle_t xSemaphoreCreateBinary( void )

  

参数
返回值

成功,返回对应二值信号量的句柄;

        失败,返回 NULL 。

 

(2).释放二值型信号量

BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore )

  

参数 说明
xSemaphore 要释放的信号量句柄
返回值

成功,返回 pdPASS ;

失败,返回 errQUEUE_FULL 

 

(3).获取二值信号量

BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore,TickType_t xTicksToWait );

  

参数 说明
 xSemaphore 要获取的信号量句柄
xTicksToWait 超时时间,0 表示不超时,portMAX_DELAY表示卡死等待;
返回值

成功,返回 pdPASS ;

失败,返回 errQUEUE_FULL 。

 

2.2.2 计数型信号量相关API

函数 描述
xSemaphoreCreateCounting() 使用动态方法创建计数型信号量。
xSemaphoreCreateCountingStatic() 使用静态方法创建计数型信号量
uxSemaphoreGetCount() 获取信号量的计数值

 

计数型信号量的释放和获取与二值信号量完全相同 !

SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount,UBaseType_t uxInitialCount);

 

参数 说明
 uxMaxCount 可以达到的最大计数值
uxInitialCount 创建信号量时分配给信号量的计数值
返回值

成功,返回对应计数型信号量的句柄;

失败,返回 NULL 。

 3、互斥量

3.1互斥量的使用场合

  在多任务系统中,任务 A 正在使用某个资源,还没用完的情况下任务 B 也来使用的话, 就可能导致问题。

  比如对于串口,任务A正使用它来打印,在打印过程中任务B也来打印,客户看到的结 果就是A、B的信息混杂在一起。

  这种现象很常见:

    ⚫ 访问外设:刚举的串口例子

    ⚫ 读、修改、写操作导致的问题

  

  在多数情况下,互斥型信号量和二值型信号量非常相似,但是从功能上二值型信号量用于同步,而互斥型信号量用于资源保护。

互斥型信号量和二值型信号量还有一个最大的区别,互斥型信号量可以有效解决优先级反转现象。

3.2 优先级反转

系统中有3个不同优先级的任务A、B、C,优先级依次升高。最高优先级任务C和最低优先级任务A通过信号量机制,共享资源。目前任务A占有资源,锁定了信号量。但是Task C在等待Task A释放信号量的过程中,中等优先级任务B抢占了任务A,从而延迟了信号量的释放时间,导致Task C阻塞了更长时间,这种现象称为优先级倒置或反转。

优先级继承:当一个互斥信号量正在被一个低优先级的任务持有时, 如果此时有个高优先级的任务也尝试获取这个互斥信号量,那么这个高优先级的任务就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级。

优先级继承并不能完全的消除优先级翻转的问题,它只是尽可能的降低优先级翻转带来的影响。
3.3 互斥量相关API

互斥信号量不能用于中断服务函数中!

 

函数 描述
xSemaphoreCreateMutex() 使用动态方法创建互斥信号量。
xSemaphoreCreateMutexStatic() 使用静态方法创建互斥信号量。

 

SemaphoreHandle_t xSemaphoreCreateMutex( void )

  

参数
返回值

成功,返回对应互斥量的句柄;

失败,返回 NULL 。

其他函数

/*
* xSemaphore: 信号量句柄,你要删除哪个信号量, 互斥量也是一种信号量
*/
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
/* 释放 */ BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
/* 释放(ISR 版本) */ BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken );
/* 获得 */ BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait );
/* 获得(ISR 版本) */ xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken );