FreeRTOS-5-信号量

这是FreeRTOS系列学习文章的第五篇,主要介绍信号量。

前言

在FreeRTOS中,信号量是使用队列来实现的。我们后面会看到所有的信号量的相关函数都是对于队列函数的封装。

信号量的分类

在FreeRTOS中有四种信号量:

  • 二值信号量
  • 互斥信号量
  • 递归信号量
  • 计数信号量

每一种信号量都有其使用的范围。

二值信号量

其实就是Linux中的二元信号量,在FreeRTOS中又称之为二进制信号量。在概念上,可以将二进制信号量理解为一个长度为一的队列。该队列可以在任何时候都最多包含一个项,所以总是要么是空的,要么是满的(hence, binary)。

二值信号量和互斥信号量十分相似,但是二值信号量具有优先级的继承功能,而互斥信号量没有。所以二值信号量常常用于任务于任务或者是任务与中断之间的同步;而互斥信号量则用作对临界区访问的保护。

计数信号量

二进制信号量可以看作长度为1的队列,而计数信号量则可以看作长度大于1的队列,信号量使用者依然不必关心存储在队列中的消息,只需要关心队列中是否有消息即可。

在实际使用中,我们常将计数信号量用于事件计数与资源管理。每当某个事件发生时,任务或者中断将释放一个信号量(信号量计数值加1),当事件被处理时(一般在任务中处理),处理任务会取走该信号量(信号量计数值减1)。信号量的计数值表示还有多少个事件未被处理。此外,系统中还有很多资源,我们也可以使用计数信号量进行资源管理。信号量的计数值表示系统中可用的资源数目,任务必须先获取到信号量才能获取资源访问权,当信号量的计数值为0时,表示系统没有可用的资源,但是要注意,在使用完资源时必须归还信号量,否则当计数值为0时,任务就无法访问该资源了。

递归信号量

这个信号量的不同在于,其他信号量都是只能够读或者不读二选一。但是递归信号量可以多次读取。

按照信号量的特性,每获取一次,可用信号量个数就会减少一个,但是递归则不然,已经获取递归互斥量的任务可以重复获取该递归互斥量,该任务拥有递归信号量的所有权。任务成功获取几次递归互斥量,就要返还几次,在此之前,递归互斥量都处于无效状态,其他任务无法获取,只有持有递归信号量的任务才能获取与释放。

互斥信号量

互斥信号量无法在中断中使用。其他几个都可以。

互斥信号量其实是特殊的二值信号量,其特有的优先级继承机制使它更适用于简单互锁,也就是保护临界资源(关于优先级继承将在后文中详细讲解)。用作互斥时,信号量创建后可用信号量个数应该是满的,任务在需要使用临界资源(临界资源是指任何时刻只能被一个任务访问的资源)时,先获取互斥信号量,使其为空,这样其他任务需要使用临界资源时就会因为无法获取信号量而进入阻塞,从而保证了临界资源的安全。

信号控制块

信号量的API函数实际上都是宏,使用现有的队列机制,这些宏在semphr.h文件中定义,如果使用信号量或者互斥量,则需要包含semphr.h头文件,所以FreeRTOS的信号量控制块结构体与消息队列结构体是一样的,只不过结构体中某些成员变量代表的含义不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 1 typedefstruct QueueDefinition {
2 int8_t *pcHead;
3 int8_t *pcTail;
4 int8_t *pcWriteTo;
5
6 union {
7 int8_t *pcReadFrom;
8 UBaseType_t uxRecursiveCallCount;
9 } u;
10
11 List_t xTasksWaitingToSend;
12 List_t xTasksWaitingToReceive;
13
14 volatile UBaseType_t uxMessagesWaiting;(1)
15 UBaseType_t uxLength;(2)
16 UBaseType_t uxItemSize;(3)
17
18 volatile int8_t cRxLock;
19 volatile int8_t cTxLock;
20
21 #if( ( configSUPPORT_STATIC_ALLOCATION == 1 )
22 && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
23 uint8_t ucStaticallyAllocated;
24 #endif
25
26 #if ( configUSE_QUEUE_SETS == 1 )
27 struct QueueDefinition *pxQueueSetContainer;
28 #endif
29
30 #if ( configUSE_TRACE_FACILITY == 1 )
31 UBaseType_t uxQueueNumber;
32 uint8_t ucQueueType;
33 #endif
34
35 } xQUEUE;
36
37 typedef xQUEUE Queue_t;

(1):如果控制块结构体是用于消息队列,则uxMessagesWaiting用来记录当前消息队列的消息个数;

如果控制块结构体被用于信号量时,则这个值表示有效信号量个数,有以下两种情况:

  • 如果信号量是二值信号量、互斥信号量,这个值为1则表示有可用信号量,为0则表示没有可用信号量。
  • 如果是计数信号量,这个值表示可用的信号量个数,在创建计数信号量时会被初始化一个可用信号量个数uxInitialCount,最大不允许超过创建信号量的初始值uxMaxCount

(2):如果控制块结构体是用于消息队列,则uxLength表示队列的长度,也就是能存放多少消息;

如果控制块结构体被用于信号量时,则uxLength表示最大的信号量可用个数,会有以下两种情况:

  • 如果信号量是二值信号量、互斥信号量,uxLength最大为1,因为信号量要么是有效的,要么是无效的。
  • 如果是计数信号量,这个值表示最大的信号量个数,在创建计数信号量时将由用户指定uxMaxCount。

(3):如果控制块结构体是用于消息队列,则uxItemSize表示单个消息的大小;

如果控制块结构体被用于信号量时,则无须分配存储空间,为0即可。

相关函数

创建、删除、发送数据(任务和中断两种)、接受数据(任务和中断两种)

其中创建又有

  • 创建一个二元信号量
  • 创建一个计数信号量
  • 创建一个递归信号量
  • 创建一个

创建一个二元信号量

函数xSemaphoreCreateBinary()可以完成。

1
2
#define xSemaphoreCreateBinary()    xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )

这个函数实际上是对于xQueueGenericCreate()函数封装,我们后面的创建信号量的函数都差不多。

下面是创建的二元信号量

创建一个计数信号量

创建计数信号量xSemaphoreCreateCounting()

1
2
#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount )    xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) )

这个函数也是对与其他队列函数的封装。在xQueueCreateCountingSemaphore()这个函数中也使用到了函数xQueueGenericCreate()

下面是创建的计数信号量

其他两个信号量

互斥信号量(互斥量)和递归信号量会在下一篇文章中进行总结。

删除函数

vSemaphoreDelete()用于删除一个信号量,包括二值信号量、计数信号量、互斥量和递归互斥量。如果有任务阻塞在该信号量上,那么不要删除该信号量。

创建信号量需要多个不同的函数,但是删除信号量都是同一个函数。

1
#define vSemaphoreDelete( xSemaphore )    vQueueDelete( ( QueueHandle_t ) ( xSemaphore ) )

对于队列删除函数的封装。

信号量的释放

这个信号量的释放实际上就是队列入队。所以这个函数应该要先于获取函数。

与消息队列的操作一样,信号量的释放可以在任务、中断中使用,所以需要有不一样的API函数在不一样的上下文环境中调用。

FreeRTOS提供了信号量释放函数,每调用一次该函数就释放一个信号量。但是有一个问题,能不能一直释放?很显然是不能的,无论是二值信号量还是计数信号量,都要注意可用信号量的范围,当用作二值信号量时,必须确保其可用值在0~1范围内;而如果用作计数信号量,其范围是由用户在创建时指定uxMaxCount,其最大可用信号量不允许超出uxMaxCount,这说明我们不能一直调用信号量释放函数来释放信号量,其实一直调用也是无法释放成功的,在写代码时要注意代码的严谨性。

在任务中使用的释放函数

xSemaphoreGive()是一个用于释放信号量的宏,真正的实现过程是调用消息队列通用发送函数。就是入队

通过消息队列入队过程分析,我们可以将释放一个信号量的过程简化:如果信号量未满,控制块结构体成员uxMessageWaiting就会加1,然后判断是否有阻塞的任务,如果有就会恢复阻塞的任务,然后返回成功信息(pdPASS);如果信号量已满,则返回错误代码(err_QUEUE_FULL)

这个函数是:

1
#define xSemaphoreGive( xSemaphore )    xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )

对于队列发送函数的封装。

适用范围:

释放的信号量对象必须是已经被创建的,可以用于二值信号量、计数信号量、互斥量的释放,但不能释放由函数xSemaphoreCreateRecursiveMutex()创建的递归互斥量。此外,该函数不能在中断中使用。

在中断中使用释放函数

适用于中断处理函数中。这个函数也是对于队列相关函数的包装:

1
#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken )    xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ), ( pxHigherPriorityTaskWoken ) )

使用范围

xSemaphoreGiveFromISR()用于释放一个信号量,带中断保护。被释放的信号量可以是二元信号量和计数信号量。和普通版本的释放信号量API函数有些许不同,xSemaphoreGiveFromISR()不能释放互斥量,这是因为互斥量不可以在中断中使用。互斥量的优先级继承机制只能在任务中起作用,而在中断中毫无意义。

信号量的获取函数

同样的也是分为适用于任务中的和中断中的。其实就是出队列。

在任务中使用获取函数

这个函数的定义:

1
#define xSemaphoreTake( xSemaphore, xBlockTime )    xQueueSemaphoreTake( ( xSemaphore ), ( xBlockTime ) )

如果有可用信号量,控制块结构体成员uxMessageWaiting就会减1,然后返回获取成功信息(pdPASS);如果信号量无效并且阻塞时间为0,则返回错误代码(errQUEUE_EMPTY);如果信号量无效并且用户指定了阻塞时间,则任务会因为等待信号量而进入阻塞状态,并被挂接到延时列表中。

适用范围

xSemaphoreTake()函数用于获取信号量,不带中断保护。获取的信号量对象可以是二值信号量、计数信号量和互斥量,但是递归互斥量并不能使用这个API函数获取。

在中断中使用获取函数

适用范围

xSemaphoreTakeFromISR()是函数xSemaphoreTake()的中断版本,用于获取信号量,是一个不带阻塞机制获取信号量的函数,获取对象必须是已经创建的信号量,信号量类型可以是二值信号量和计数信号量。xSemaphoreTakeFromISR()与xSemaphoreTake()函数不同,它不能用于获取互斥量,因为互斥量不可以在中断中使用,并且互斥量特有的优先级继承机制只能在任务中起作用,而在中断中毫无意义。


FreeRTOS-5-信号量
https://ysc2.github.io/ysc2.github.io/2024/01/18/FreeRTOS-5-信号量/
作者
Ysc
发布于
2024年1月18日
许可协议