18.5 信号量控制块

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

  1. typedef struct QueueDefinition
  2. {
  3. int8_t *pcHead; // pcHead 指向队列消息存储区起始位置,即第一个消息空间。
  4. int8_t *pcTail; // pcTail 指向队列消息存储区结束位置地址。
  5. int8_t *pcWriteTo; // pcWriteTo 指向队列消息存储区下一个可用消息空间。
  6. union
  7. {
  8. int8_t *pcReadFrom;
  9. UBaseType_t uxRecursiveCallCount;
  10. } u;
  11. List_t xTasksWaitingToSend;
  12. List_t xTasksWaitingToReceive;
  13. volatile UBaseType_t uxMessagesWaiting; //1
  14. UBaseType_t uxLength; //2
  15. UBaseType_t uxItemSize; //3
  16. volatile int8_t cRxLock;
  17. volatile int8_t cTxLock;
  18. #if( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
  19. uint8_t ucStaticallyAllocated;
  20. #endif
  21. #if ( configUSE_QUEUE_SETS == 1 )
  22. struct QueueDefinition *pxQueueSetContainer;
  23. #endif
  24. #if ( configUSE_TRACE_FACILITY == 1 )
  25. UBaseType_t uxQueueNumber;
  26. uint8_t ucQueueType;
  27. #endif
  28. } xQUEUE;
  29. typedef xQUEUE Queue_t;
  1. 如果控制块结构体是用于消息队列: uxMessagesWaiting 用来记录当前消息队列的消息个数; 如果控制块结构体被用于信号量的时候, 这个值就表示有效信号量个数,有以下两种情况:

    • 如果信号量是二值信号量、互斥信号量,这个值是 1 则表示有可用信号量,如果是 0 则表示没有可用信号量。
    • 如果是计数信号量,这个值表示可用的信号量个数,在创建计数信号量的时候会被初始化一个可用信号量个数 uxInitialCount,最大不允许超过创建信号量的初始值 uxMaxCount
  2. 果控制块结构体是用于消息队列: uxLength 表示队列的长度,也就是能存放多少消息; 如果控制块结构体被用于信号量的时候, uxLength 表示最大的信号量可用个数, 会有以下两种情况:

    • 如果信号量是二值信号量、互斥信号量, uxLength 最大为 1,因为信号量要么是有效的,要么是无效的
    • 如果是计数信号量,这个值表示最大的信号量个数,在创建计数信号量的时候将由用户指定这个值 uxMaxCount。
  3. 如果控制块结构体是用于消息队列: uxItemSize 表示单个消息的大小; 如果控制块结构体被用于信号量的时候,则无需存储空间,为 0 即可。

18.6 常会有信号量函数接口详解

18.6.1 创建信号量函数

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

xSemaphoreCreateBinary()用于创建一个二值信号量, 并返回一个句柄。 其实二值信号量和互斥量都共同使用一个类型 SemaphoreHandle_t 的句柄(.h 文件 79 行) , 该句柄的原型 是 一 个 void 型 的 指 针。
使 用 该 函数 创 建 的 二 值信 号 量 是 空的 , 在 使 用函 数xSemaphoreTake()获取之前必须先调用函数 xSemaphoreGive()释放后才可以获取。 如果是使用老式的函数 vSemaphoreCreateBinary()创建的二值信号量,则为 1, 在使用之前不用先释放。
configSUPPORT_DYNAMIC_ALLOCATION 定义为 1,即开启动态内存分配。

  1. #if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
  2. #define xSemaphoreCreateBinary() \
  3. xQueueGenericCreate( ( UBaseType_t ) 1, \
  4. semSEMAPHORE_QUEUE_ITEM_LENGTH, \
  5. queueQUEUE_TYPE_BINARY_SEMAPHORE )
  6. #endif

实 际 使 用 的 函 数 就 是xQueueGenericCreate()函数,和队列使用的是一样的,但是参数不同。

  1. QueueHandle_t xQueueGenericCreate(const UBaseType_t uxQueueLength,
  2. const UBaseType_t uxItemSize,
  3. const uint8_t ucQueueType)

const UBaseType_t uxQueueLength:为 1 表示创建的队列长度为 1,其实用作信号量就表示信号量的最大可用个数
const UBaseType_t uxItemSize:emSEMAPHORE_QUEUE_ITEM_LENGTH 其实是一个宏定义,其值为 0,
const uint8_t ucQueueType:ucQueueType 表示的是创建消息队列的类型, 在 queue.h 中有定义。

  1. #define queueQUEUE_TYPE_BASE ( ( uint8_t ) 0U )
  2. #define queueQUEUE_TYPE_SET ( ( uint8_t ) 0U )
  3. #define queueQUEUE_TYPE_MUTEX ( ( uint8_t ) 1U )
  4. #define queueQUEUE_TYPE_COUNTING_SEMAPHORE ( ( uint8_t ) 2U )
  5. #define queueQUEUE_TYPE_BINARY_SEMAPHORE ( ( uint8_t ) 3U )
  6. #define queueQUEUE_TYPE_RECURSIVE_MUTEX ( ( uint8_t ) 4U )

二值信号量的释放和获取都是通过操作队列结控制块构体成员 uxMessageWaiting 来实现的,它表示信号量中当前可用的信号量个数。 在信号量创建之后,变量 uxMessageWaiting 的值为 0,这说明当前信号量处于无效状态, 此时的信号量是无法被获取的, 在获取信号之前,应先释放一个信号量。
image.png

2. 创建计数信号量xSemaphoreCreateCounting()

xSemaphoreCreateCounting ()用于创建一个计数信号量。 和上面创建类似的。
image.png

  1. #if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
  2. #define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) \
  3. xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) )
  4. #endif
  1. #if( ( configUSE_COUNTING_SEMAPHORES == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
  2. QueueHandle_t xQueueCreateCountingSemaphore( const UBaseType_t uxMaxCount, const UBaseType_t uxInitialCount )
  3. {
  4. QueueHandle_t xHandle;
  5. configASSERT( uxMaxCount != 0 );
  6. configASSERT( uxInitialCount <= uxMaxCount );
  7. xHandle = xQueueGenericCreate( uxMaxCount,
  8. queueSEMAPHORE_QUEUE_ITEM_LENGTH,
  9. queueQUEUE_TYPE_COUNTING_SEMAPHORE );
  10. if( xHandle != NULL )
  11. {
  12. ( ( Queue_t * ) xHandle )->uxMessagesWaiting = uxInitialCount;
  13. traceCREATE_COUNTING_SEMAPHORE();
  14. }
  15. else
  16. {
  17. traceCREATE_COUNTING_SEMAPHORE_FAILED();
  18. }
  19. return xHandle;
  20. }
  21. #endif

仍然是调用函数xQueueGenericCreate()来创建一个计数信号量,信号量最大个数由参数 uxMaxCount 指定。
每个消息空间的大小由宏 queueSEMAPHORE_QUEUE_ITEM_LENGTH 指定,这个宏被定义为0。

如果创建成功,还会将消息队列控制块中的 uxMessagesWaiting 成员变量赋值为用户指定的初始可用信号量个数 uxInitialCount,如果这个值大于 0,则表示此时有 uxInitialCount 个计数信号量是可用的,这点与二值信号量的创建不一样,二值信号量在创建成功的时候是无效的。

二值信号量创建函数 xSemaphoreCreateBinary()使用实例:

  1. SemaphoreHandle_t xSemaphore = NULL;
  2. void vATask( void * pvParameters )
  3. {
  4. /* 尝试创建一个信号量 */
  5. xSemaphore = xSemaphoreCreateBinary();
  6. if ( xSemaphore == NULL ) {
  7. /* 内存不足,创建失败 */
  8. } else {
  9. /* 信号量现在可以使用,句柄存在变量 xSemaphore 中
  10. 这个时候还不能调用函数 xSemaphoreTake()来获取信号量
  11. 因为使用 xSemaphoreCreateBinary()函数创建的信号量是空的
  12. 在第一次获取之前必须先调用函数 xSemaphoreGive()先提交*/
  13. }
  14. }

计数信号量创建函数 xSemaphoreCreateCounting()使用实例:

  1. void vATask( void * pvParameters )
  2. {
  3. SemaphoreHandle_t xSemaphore;
  4. xSemaphore = xSemaphoreCreateCounting( 5, 5 );
  5. if ( xSemaphore != NULL ) {
  6. /* 计数信号量创建成功 */
  7. }
  8. }

18.6.2 信号量删除函数vSemaphoreDelete()

vSemaphoreDelete()用于删除一个信号量,包括二值信号量,计数信号量,互斥量和递归互斥量。
注意:如果有任务阻塞在该信号量上,那么不要删除该信号量。
image.png
删除信号量过程其实就是删除消息队列过程, 因为信号量其实就是消息队列, 只不过是无法存储消息的队列而已。

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

18.6.3 信号量释放函数

信号量的释放可以在任务、中断中使用,需要调用不同的API。
让信号量有效:

  1. 创建的时候进行初始化,将它可用的信号量个数设置一个初始值
  2. FreeRTOS 提供了信号量释放函数,每调用一次该函数就释放一个信号量

注意:

  • 要注意可用信号量的范围,当用作二值信号量的时候,必须确保其可用值在 0~1 范围内
  • 用作计数信号量的话,其范围是由用户在创建时指定 uxMaxCount,其最大可用信号量不允许超出uxMaxCount,
  • 可以调用,但是无法释放成功。要注意代码的严谨。

    1.xSemaphoreGive()(任务)

    xSemaphoreGive()是一个用于释放信号量的宏, 真正的实现过程是调用消息队列通用发送函数。
    释放的信号量对象必须是已经被创建的, 可以用于二值信号量、 计数信号量、 互斥量的释放,但不能释放由函数xSemaphoreCreateRecursiveMutex()创建的递归互斥量
    1. #define xSemaphoreGive( xSemaphore ) \
    2. xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), \
    3. NULL, \
    4. semGIVE_BLOCK_TIME, \
    5. queueSEND_TO_BACK)
    释放信号量实际上是一次入队操作,并且是不允许入队阻塞,因为阻塞时间为 semGIVE_BLOCK_TIME,该宏的值为 0。
    简化过程:如果信号量未满, 控制块结构体成员 uxMessageWaiting 就会加 1, 然后判断是否有阻塞的任务, 如果有的话就会恢复阻塞的任务,然后返回成功信息(pdPASS);如果信号量已满, 则返回错误代码(err_QUEUE_FULL) 。

使用示例:

  1. static void Send_Task(void* parameter)
  2. {
  3. BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为 pdPASS */
  4. while (1) {
  5. /* K1 被按下 */
  6. if ( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) {
  7. xReturn = xSemaphoreGive( BinarySem_Handle );//给出二值信号量
  8. if ( xReturn == pdTRUE )
  9. printf("BinarySem_Handle 二值信号量释放成功!\r\n");
  10. else
  11. printf("BinarySem_Handle 二值信号量释放失败!\r\n");
  12. if ( Key_Scan(KEY2_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON ) {
  13. xReturn = xSemaphoreGive( BinarySem_Handle );//给出二值信号量
  14. if ( xReturn == pdTRUE )
  15. printf("BinarySem_Handle 二值信号量释放成功!\r\n");
  16. else
  17. printf("BinarySem_Handle 二值信号量释放失败!\r\n");
  18. }
  19. }


2. xSemaphoreGiveFromISR()(中断)

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

  1. #define xSemaphoreGiveFromISR( xSemaphore, \
  2. pxHigherPriorityTaskWoken ) \
  3. xQueueGiveFromISR(( QueueHandle_t ) \
  4. ( xSemaphore ), \
  5. ( pxHigherPriorityTaskWoken ) )

一个或者多个任务有可能阻塞在同一个信号量上,调用函数 xSemaphoreGiveFromISR()可能会唤醒阻塞在该信号量上的任务,如果被唤醒的任务的优先级大于当前任务的优先级,那么形参 pxHigherPriorityTaskWoken 就会被设置为 pdTRUE, 然后在中断退出前执行一次上下文切换。

使用示例:

  1. void vTestISR( void )
  2. {
  3. BaseType_t pxHigherPriorityTaskWoken;
  4. uint32_t ulReturn;
  5. /* 进入临界段,临界段可以嵌套 */
  6. ulReturn = taskENTER_CRITICAL_FROM_ISR();
  7. /* 判断是否产生中断 */
  8. {
  9. /* 如果产生中断,清除中断标志位 */
  10. //释放二值信号量,发送接收到新数据标志,供前台程序查询
  11. xSemaphoreGiveFromISR(BinarySem_Handle,&
  12. pxHigherPriorityTaskWoken);
  13. //如果需要的话进行一次任务切换,系统会判断是否需要进行切换
  14. portYIELD_FROM_ISR(pxHigherPriorityTaskWoken);
  15. }
  16. /* 退出临界段 */
  17. taskEXIT_CRITICAL_FROM_ISR( ulReturn );
  18. }

18.6.4 信号量的获取

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

1.xSemaphoreTake()(任务)

image.png
从该宏定义可以看出释放信号量实际上是一次消息出队操作, 阻塞时间由用户指定xBlockTime。

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

使用实例:

  1. static void Receive_Task(void* parameter)
  2. {
  3. BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为 pdPASS */
  4. while(1) {
  5. //获取二值信号量 xSemaphore,没获取到则一直等待
  6. xReturn = xSemaphoreTake(BinarySem_Handle,/* 二值信号量句柄 */
  7. portMAX_DELAY); /* 等待时间 */
  8. if (pdTRUE == xReturn)
  9. printf("BinarySem_Handle 二值信号量获取成功!\n\n");
  10. LED1_TOGGLE;
  11. }
  12. }

2 xSemaphoreTakeFromISR()(中断)

它不能用于获取互斥,因为互斥量不可以在中断中使用,并且互斥量特有的优先级继承机制只能在任务中起
作用, 而在中断中毫无意义。
image.png