10. FreeRTOS的信号量

一、FreeRTOS的信号量简介

  信号量是一种解决同步问题的机制,可以实现对共享资源的有序访问。其中,“同步”指的是任务间的同步,即信号量可以使得一个任务等待另一个任务完成某件事情后,才继续执行;而“有序访问”指的是对被多任务或中断访问的共享资源(如全局变量)的管理,当一个任务在访问(读取或写入)一个共享资源时,信号量可以防止其他任务或中断在这期间访问(读取或写入)这个共享资源。

  信号量用于管理共享资源的场景相当于对共享资源上了个锁,只有任务成功获取到了锁的要是,才能够访问这个共享资源,访问完共享资源后还得归还要是,当然钥匙可以不只一把,即信号量可以有多个资源。

二、二值信号量

2.1、二值信号量简介

  二值信号量实际上就是只有两种情况的信号量(一个队列长度为 1 的队列),二值信号量只有两种情况,分别为有资源和无资源,这也就意味着,如果把二值信号量比喻成一把锁,那么这把锁只有一把钥匙,当一个任务获取了这个二值信号量之后,在这个任务释放这个二值信号量值之前,其它任务都是获取不到这个二值信号量的。二值信号量通常用于互斥访问或任务同步。

二值信号量

  二值信号量有可能会导致严重的 优先级翻转问题优先级翻转问题 指的是,当一个高优先级任务因获取一个被低优先级任务获取而处于没有资源状态的二值信号量时,这个高优先级的任务将被阻塞,直到低优先级的任务释放这个二值信号量,而在这之前,如果有一个优先级介于高优先级任务和低优先级任务之间的任务就绪,那么这个中等优先级的任务就会抢占低优先级任务的运行,这么一来,这三个任务中优先级最高的任务反而要最后才能运行。

  和队列一样,在获取二值信号量的时候,允许设置一个阻塞超时时间,阻塞超时时间是当任务获取二值信号量时,由于二值信号量处于没有资源的状态,而导致任务进入阻塞状态的最大系统时钟节拍数。如果多个任务同时因获取同一个处于没有资源状态的二值信号量而被阻塞,那么在二值信号量有资源的时候,这些阻塞任务中优先级高的任务将优先获得二值信号量的资源并解除阻塞。

2.2、二值信号量相关的API函数

2.2.1、创建二值信号量

2.2.1.1、动态方式创建二值信号量

// 返回NULL,创建失败,其它值表示创建成功并返回二值信号量的句柄
#define xQueueCreate( uxQueueLength, uxItemSize )    xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )

  此函数用于使用 动态方式创建二值信号量,创建二值信号量所需的内存,由 FreeRTOS 从 FreeRTOS 管理的堆中进行分配。该函数实际上是一个宏定义,在 semphr.h 文件中有定义。从上面的代码中可以看出,函数 xSemaphoreCreateBinary() 实际上是调用了函数 xQueueGenericCreate() 创建了一个队列长度为 1 且队列项目大小为信号量队列项目大小的二值信号量类型队列。

2.2.1.2、静态方式创建二值信号量

#define xSemaphoreCreateBinaryStatic( pxStaticSemaphore )    xQueueGenericCreateStatic( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, NULL, ( pxStaticSemaphore ), queueQUEUE_TYPE_BINARY_SEMAPHORE )

  此函数用于使用 静态方式创建二值信号量,创建二值信号量所需的内存,需要由用户手动分配并提供。该函数实际上是一个宏定义,在 semphr.h 文件中有定义。从上面的代码中可以看出,函数 xSemaphoreCreateStatic() 实际上是调用了函数 xQueueGenericCreateStatic() 创建了一个队列长度为 1 且队列项目大小为信号量队列项目大小的二值信号量类型队列。

2.2.2、删除二值信号量

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

  此函数用于删除已创建二值信号量。该函数实际上是一个宏定义,在 semphr.h 文件中有定义。从上面的代码中可以看出,函数 vSemaphoreDelete() 实际上是调用了函数 vQueueDelete() 删除已创建的二值信号量队列。

2.2.3、释放二值信号量

2.2.3.1、在任务中释放二值信号量

// 形参xSemaphore是要释放的信号量句柄
// 返回pdPASS表示释放信号量成功,返回errQUEUE_FULL表示释放信号量失败
#define xSemaphoreGive( xSemaphore )    xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )

  此函数用于释放信号量,如果信号量处于资源满的状态,那么此函数可续选择将任务进行阻塞,如果成功释放了信号量,那信号量的资源数将会加 1。该函数实际上是一个宏定义,在 semphr.h 文件中有定义。从上面的代码中可以看出,函数 xSemaphoreGive() 实际上是调用了函数 xQueueGenericSend()

2.2.3.2、在中断中释放二值信号量

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

  此函数用于在中断中释放信号量。该函数实际上是一个宏定义,在 semphr.h 文件中有定义。从上面的代码中可以看出,函数 xSemaphoreGiveFromISR() 实际上是调用了函数 xQueueGiveFromISR()。函数 xQueueGiveFromISR() 在 queue.c 文件中有定义。

BaseType_t xQueueGiveFromISR( QueueHandle_t xQueue,
                              BaseType_t * const pxHigherPriorityTaskWoken )
{
    BaseType_t xReturn;
    UBaseType_t uxSavedInterruptStatus;
    Queue_t * const pxQueue = xQueue;

    traceENTER_xQueueGiveFromISR( xQueue, pxHigherPriorityTaskWoken );
    configASSERT( pxQueue );
    configASSERT( pxQueue->uxItemSize == 0 );
    configASSERT( !( ( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX ) && ( pxQueue->u.xSemaphore.xMutexHolder != NULL ) ) );

    portASSERT_IF_INTERRUPT_PRIORITY_INVALID();                                 // 有受FreeRTOS 管理的中断才能调用该函数

    // 屏蔽受 FreeRTOS 管理的中断,并保存,屏蔽前的状态,用于恢复
    uxSavedInterruptStatus = ( UBaseType_t ) taskENTER_CRITICAL_FROM_ISR();
    {
        const UBaseType_t uxMessagesWaiting = pxQueue->uxMessagesWaiting;       // 获取信号量的资源数

        if( uxMessagesWaiting < pxQueue->uxLength )                             // 判断信号量是否有资源
        {
            const int8_t cTxLock = pxQueue->cTxLock;                            // 获取任务的写入上锁计数器

            traceQUEUE_SEND_FROM_ISR( pxQueue );                                // 用于调试

            // 更新信号量资源数
            pxQueue->uxMessagesWaiting = ( UBaseType_t ) ( uxMessagesWaiting + ( UBaseType_t ) 1 );

            if( cTxLock == queueUNLOCKED )                                      // 判断信号量队列的写入是否未上锁
            {
                #if ( configUSE_QUEUE_SETS == 1 )                               // 此宏定义用于启用队列集
                {
                    if( pxQueue->pxQueueSetContainer != NULL )                  // 判断队列是否在队列集中
                    {
                        if( prvNotifyQueueSetContainer( pxQueue ) != pdFALSE )  // 通知队列集
                        {
                            if( pxHigherPriorityTaskWoken != NULL )             // 判断是否接收需要任务切换标记
                            {
                                *pxHigherPriorityTaskWoken = pdTRUE;            // 标记要进行任务切换
                            }
                            else
                            {
                                mtCOVERAGE_TEST_MARKER();
                            }
                        }
                        else
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                    }
                    else                                                        // 队列不在队列集
                    {
                        // 判断队列的读取阻塞任务列表是否不为空
                        if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
                        {
                            // 将任务从队列的读取阻塞任务列表中移除
                            if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
                            {
                                if( pxHigherPriorityTaskWoken != NULL )         // 判断是否接收需要任务切换标记
                                {
                                    *pxHigherPriorityTaskWoken = pdTRUE;        // 标记需要进行任务切换
                                }
                                else
                                {
                                    mtCOVERAGE_TEST_MARKER();
                                }
                            }
                            else
                            {
                                mtCOVERAGE_TEST_MARKER();
                            }
                        }
                        else
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                    }
                }
                #else /* configUSE_QUEUE_SETS */
                {
                    // 判断队列的读取阻塞任务列表是否不为空
                    if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToReceive ) ) == pdFALSE )
                    {
                        // 将任务从队列的读取阻塞任务列表中移除
                        if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToReceive ) ) != pdFALSE )
                        {
                            if( pxHigherPriorityTaskWoken != NULL )             // 判断是否接收需要任务切换标记
                            {
                                *pxHigherPriorityTaskWoken = pdTRUE;            // 标记需要进行任务切换
                            }
                            else
                            {
                                mtCOVERAGE_TEST_MARKER();
                            }
                        }
                        else
                        {
                            mtCOVERAGE_TEST_MARKER();
                        }
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
                #endif /* configUSE_QUEUE_SETS */
            }
            else                                                                // 队列写入上锁,无需处理阻塞列表
            {
                prvIncrementQueueTxLock( pxQueue, cTxLock );                    // 更新队列写入上锁计数器
            }

            xReturn = pdPASS;
        }
        else                                                                    // 信号量没有资源
        {
            traceQUEUE_SEND_FROM_ISR_FAILED( pxQueue );                         // 用于调试
            xReturn = errQUEUE_FULL;
        }
    }
    taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus );                       // 恢复屏蔽中断前的中断状态

    traceRETURN_xQueueGiveFromISR( xReturn );

    return xReturn;
}

  函数 xQueueGiveFromISR() 只能用于释放二值信号量和计数型信号量,而不能用于获取互斥信号量,因为互斥信号量会有优先级继承的处理,而中断不属于任务,没法进行优先级继承。

2.2.4、获取二值信号量

2.2.4.1、在任务中获取信号量

// 形参xSemaphore是要获取的信号量句柄,形参xBlockTime是阻塞时间
// 返回pdTRUE表示获取信号量成功,返回pdFALSE表示超时,获取信号量失败
#define xSemaphoreTake( xSemaphore, xBlockTime )    xQueueSemaphoreTake( ( xSemaphore ), ( xBlockTime ) )

  此函数用于 获取信号量,如果信号量处于没有资源的状态,那么此函数可以选择将任务进行阻塞,如果成功获取了信号量,那信号量的资源数将会减 1。该函数实际上是一个宏定义,在 semphr.h 文件中有定义。从上面的代码中可以看出,函数 xSemaphoreTake() 实际上是调用了函数 xQueueSemaphoreTake() 来获取信号量,函数 xQueueSemaphoreTake() 在 queue.c 文件中有定义,具体的代码如下所示:

BaseType_t xQueueSemaphoreTake( QueueHandle_t xQueue,
                                TickType_t xTicksToWait )
{
    BaseType_t xEntryTimeSet = pdFALSE;
    TimeOut_t xTimeOut;
    Queue_t * const pxQueue = xQueue;

    #if ( configUSE_MUTEXES == 1 )
        BaseType_t xInheritanceOccurred = pdFALSE;
    #endif

    traceENTER_xQueueSemaphoreTake( xQueue, xTicksToWait );
    configASSERT( ( pxQueue ) );
    configASSERT( pxQueue->uxItemSize == 0 );                                   // 信号量类型队列的项目大小为 0

    #if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) )
    {
        configASSERT( !( ( xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED ) && ( xTicksToWait != 0 ) ) );
    }
    #endif

    for( ; ; )
    {
        taskENTER_CRITICAL();                                                   // 进入临界区
        {
            const UBaseType_t uxSemaphoreCount = pxQueue->uxMessagesWaiting;    // 获取信号量的资源数

            if( uxSemaphoreCount > ( UBaseType_t ) 0 )                          // 判断信号量是否有资源
            {
                traceQUEUE_RECEIVE( pxQueue );                                  // 用于调试

                // 更新信号量的资源数
                pxQueue->uxMessagesWaiting = ( UBaseType_t ) ( uxSemaphoreCount - ( UBaseType_t ) 1 );

                #if ( configUSE_MUTEXES == 1 )                                  // 此宏用于启用互斥信号量
                {
                    if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )           // 判断队列的类型是否为互斥信号量
                    {
                        // 设置互斥信号量的持有者,并更新互斥信号量的持有次数
                        pxQueue->u.xSemaphore.xMutexHolder = pvTaskIncrementMutexHeldCount();
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
                #endif /* configUSE_MUTEXES */

                // 判断信号量的获取阻塞任务列表中是否有任务
                if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
                {
                    // 将阻塞任务从信号量获取阻塞任务列表中移除
                    if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
                    {
                        queueYIELD_IF_USING_PREEMPTION();                       // 根据需要进行任务切换
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }

                taskEXIT_CRITICAL();                                            // 退出临界区

                traceRETURN_xQueueSemaphoreTake( pdPASS );

                return pdPASS;
            }
            else                                                                // 信号量没有资源
            {
                if( xTicksToWait == ( TickType_t ) 0 )                          // 判断是否不选择阻塞等待信号量
                {
                    taskEXIT_CRITICAL();                                        // 退出临界区

                    // 用于调试
                    traceQUEUE_RECEIVE_FAILED( pxQueue );
                    traceRETURN_xQueueSemaphoreTake( errQUEUE_EMPTY );

                    return errQUEUE_EMPTY;
                }
                else if( xEntryTimeSet == pdFALSE )                             // 选择阻塞等待信号量
                {
                    // 队列满,任务需要阻塞,记录下此时系统节拍计数器的值和溢出次数,用于下面对阻塞时间进行补偿
                    vTaskInternalSetTimeOutState( &xTimeOut );
                    xEntryTimeSet = pdTRUE;
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
        }
  
        // 退出临界区,退出临界区后系统时钟节拍会发生更新,因此任务如果需要阻塞的话,需要对阻塞时间进行补偿
        taskEXIT_CRITICAL();

        vTaskSuspendAll();                                                      // 挂起任务调度器
        prvLockQueue( pxQueue );                                                // 信号量队列上锁

        if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )       // 判断阻塞时间补偿后,是否还需要阻塞
        {
            if( prvIsQueueEmpty( pxQueue ) != pdFALSE )                         // 判断队列是否为空
            {
                traceBLOCKING_ON_QUEUE_RECEIVE( pxQueue );                      // 用于调试

                #if ( configUSE_MUTEXES == 1 )                                  // 此宏用于启用互斥信号量
                {
                    if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )           // 判断队列类型是否为互斥信号量
                    {
                        taskENTER_CRITICAL();                                   // 进入临界区
                        {
                            // 进行优先级继承,这是互斥信号量用于解决优先级翻转问题的
                            xInheritanceOccurred = xTaskPriorityInherit( pxQueue->u.xSemaphore.xMutexHolder );
                        }
                        taskEXIT_CRITICAL();                                    // 退出临界区
                    }
                    else
                    {
                        mtCOVERAGE_TEST_MARKER();
                    }
                }
                #endif /* if ( configUSE_MUTEXES == 1 ) */

                // 将任务添加到队列写入阻塞任务列表中进行阻塞
                vTaskPlaceOnEventList( &( pxQueue->xTasksWaitingToReceive ), xTicksToWait );
                prvUnlockQueue( pxQueue );                                      // 解锁队列

                if( xTaskResumeAll() == pdFALSE )                               // 恢复任务调度器
                {
                    taskYIELD_WITHIN_API();                                     // 根据需要进行任务切换
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            }
            else                                                                //  队列不为空
            {
                prvUnlockQueue( pxQueue );                                      // 解锁队列
                ( void ) xTaskResumeAll();                                      // 恢复任务调度器
            }
        }
        else                                                                    // 阻塞时间补偿后,不需要进行阻塞
        {
            prvUnlockQueue( pxQueue );                                          // 解锁队列
            ( void ) xTaskResumeAll();                                          // 恢复任务调度器

            if( prvIsQueueEmpty( pxQueue ) != pdFALSE )                         // 判断队列是否为空
            {
                #if ( configUSE_MUTEXES == 1 )                                  // 此宏用于启用互斥信号量
                {
                    if( xInheritanceOccurred != pdFALSE )                       // 判断任务是否发生优先级继承
                    {
                        taskENTER_CRITICAL();                                   // 进入临界区
                        {
                            UBaseType_t uxHighestWaitingPriority;

                            // 恢复任务优先级
                            uxHighestWaitingPriority = prvGetDisinheritPriorityAfterTimeout( pxQueue );
                            vTaskPriorityDisinheritAfterTimeout( pxQueue->u.xSemaphore.xMutexHolder, uxHighestWaitingPriority );
                        }
                        taskEXIT_CRITICAL();                                    //  退出临界区
                    }
                }
                #endif /* configUSE_MUTEXES */

                // 用于调试
                traceQUEUE_RECEIVE_FAILED( pxQueue );
                traceRETURN_xQueueSemaphoreTake( errQUEUE_EMPTY );

                return errQUEUE_EMPTY;
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
    }
}

  从上面的代码中可以看出,函数 xQueueSemaphoreTake() 不仅仅是用于获取二值信号量,还有计数型信号量,互斥信号量的获取,都是通过宏定义间接地调用了此函数。

2.2.4.2、在中断中获取信号量

#define xSemaphoreTakeFromISR( xSemaphore, pxHigherPriorityTaskWoken )    xQueueReceiveFromISR( ( QueueHandle_t ) ( xSemaphore ), NULL, ( pxHigherPriorityTaskWoken ) )

  此函数用于在中断中获取信号量。该函数实际上是一个宏定义,在 semphr.h 文件中有定义。从上面的代码中可以看出,函数 xSemaphoreTakeFromISR() 实际上是调用了函数 xQueueReceiveFromISR() 来获取信号量。要特别注意的是,函数 xSemaphoreTakeFromISR() 于函数 xSemaphoreTake() 不同,函数 xSemaphoreTakeFromISR() 只能用于获取二值信号量和计数型信号量,而不能用于获取互斥信号量。

2.3、实验例程

  main() 函数内容如下:

int main(void)
{
    HAL_Init();
    System_Clock_Init(8, 336, 2, 7);
    Delay_Init(168);

    UART_Init(&g_usart1_handle, USART1, 115200);
    Key_Init();

    freertos_demo();
  
    return 0;
}

  FreeRTOS 例程入口函数:

QueueHandle_t binary_semphore_handle;

/**
 * @brief FreeRTOS的入口函数
 * 
 */
void freertos_demo(void)
{
    binary_semphore_handle = xSemaphoreCreateBinary();                          // 创建二值信号量
    if (binary_semphore_handle != NULL)
    {
        printf("二值信号量创建成功\r\n");
    }

    xTaskCreate((TaskFunction_t        ) start_task,                            // 任务函数
                (char *                ) "start_task",                          // 任务名
                (configSTACK_DEPTH_TYPE) START_TASK_STACK_SIZE,                 // 任务栈大小
                (void *                ) NULL,                                  // 入口参数
                (UBaseType_t           ) START_TASK_PRIORITY,                   // 任务优先级
                (TaskHandle_t *        ) start_task_handle);                    // 任务句柄

    vTaskStartScheduler();                                                      // 开启任务调度器
}

  START_TASK 任务配置:

/**
 * START_TASK 任务配置
 * 包括: 任务优先级 任务栈大小 任务句柄 任务函数
 */
#define START_TASK_PRIORITY     1
#define START_TASK_STACK_SIZE   128

TaskHandle_t start_task_handle;

void start_task(void *pvParameters );

/**
 * @brief 开始任务的任务函数
 * 
 * @param pvParameters 任务函数的入口参数
 */
void start_task(void *pvParameters)
{
    taskENTER_CRITICAL();                                                       // 进入临界区,关闭中断

    xTaskCreate((TaskFunction_t        ) task1,                                 // 任务函数
                (char *                ) "task1",                               // 任务名
                (configSTACK_DEPTH_TYPE) TASK1_STACK_SIZE,                      // 任务栈大小
                (void *                ) NULL,                                  // 入口参数
                (UBaseType_t           ) TASK1_PRIORITY,                        // 任务优先级
                (TaskHandle_t *        ) &task1_handle);                        // 任务句柄

    xTaskCreate((TaskFunction_t        ) task2,                                 // 任务函数
                (char *                ) "task2",                               // 任务名
                (configSTACK_DEPTH_TYPE) TASK2_STACK_SIZE,                      // 任务栈大小
                (void *                ) NULL,                                  // 入口参数
                (UBaseType_t           ) TASK2_PRIORITY,                        // 任务优先级
                (TaskHandle_t *        ) &task2_handle);                        // 任务句柄

    vTaskDelete(NULL);                                                          // 删除任务自身

    taskEXIT_CRITICAL();                                                        // 退出临界区,重新开启中断
}

  TASK1 任务配置:

/**
 * TASK1 任务配置
 * 包括: 任务优先级 任务栈大小 任务句柄 任务函数
 */
#define TASK1_PRIORITY     2
#define TASK1_STACK_SIZE   128

TaskHandle_t task1_handle;

void task1(void *pvParameters);

/**
 * @brief 任务1的任务函数
 * 
 * @param pvParameters 任务函数的入口参数
 */
void task1(void *pvParameters)
{
    uint8_t key = 0;
    BaseType_t result = 0;

    while (1)
    {
        key = Key_Scan(0);
        switch (key)
        {
        case KEY1_PRESS:
            if (binary_semphore_handle != NULL)
            {
                result = xSemaphoreGive(binary_semphore_handle);                // 释放二值信号量
                if (result == pdPASS)
                {
                    printf("二值信号量释放成功\r\n");
                }
                else
                {
                    printf("二值信号量释放失败\r\n");
                }
            }
            break;

        default:
            break;
        }

        vTaskDelay(10);
    }
}

  TASK2 任务配置:

/**
 * TASK2 任务配置
 * 包括: 任务优先级 任务栈大小 任务句柄 任务控制块  任务栈 任务函数
 */
#define TASK2_PRIORITY     3
#define TASK2_STACK_SIZE   128

TaskHandle_t task2_handle;

void task2(void *pvParameters);

/**
 * @brief 任务2的任务函数
 * 
 * @param pvParameters 任务函数的入口参数
 */
void task2(void *pvParameters )
{
    BaseType_t result = 0;
    while (1)
    {
        result = xSemaphoreTake(binary_semphore_handle, portMAX_DELAY);         // 获取二值信号量
        if (result == pdTRUE)
        {
            printf("二值信号量获取成功\r\n");
        }
        else
        {
            printf("二值信号量获取失败\r\n");
        }
    }
}

三、计数型信号量

3.1、计数型信号量简介

  计数型信号量与二值信号量是很相似的 ,但是计数型信号量的资源数大于1(队列长度大于 0 的队列),也就是它的资源不止 0 和 1。因此计数型信号量能够容纳多个资源,这是在计数型信号量被创建的时候确定的。计数型信号量适用以下场合:

  • 事件计数:每次事件发生后,在事件处理函数中释放计数型信号量(计数型信号量的资源数加 1),其它等待事件发生的任务获取计数型信号量(计数型信号量的资源数减 1),这么一来等待事件发生的任务就可以在成功获取到计数型信号量之后执行相应的操作。在这种场合下,计数型信号量的资源数一般在创建时设置为 0。
  • 资源管理:计数型信号量的资源数代表着共享资源的可用数量。一个任务想要访问共享资源,就必须先获取这个共享资源的计数型信号量,之后在成功获取了计数型信号量之后,才可以对这个共享资源进行访问操作,当然,在使用完共享资源后也要释放这个共享资源的计数型信号量。在这种场合下,计数型信号量的资源数一般在创建时设置为受其管理的共享资源的最大可用数量。

3.2、计数型信号量相关的API函数

  计数型信号量除了创建的 API 函数与二值信号量不同,删除、释放和获取的 API 函数与二值信号量一致。

3.2.1、创建计数型信号量

3.2.1.1、动态方式创建计数型信号量

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

  此函数用于使用动态方式创建计数型信号量,创建计数型信号量所需的内存,由 FreeRTOS从 FreeRTOS 管理的堆中进行分配。该函数实际上是一个宏定义,在 semphr.h 中有定义。从上面的代码中可以看出,函数 xSemaphoreCreateCounting() 实际上是调用了函数 xQueueCreateCountingSemaphore(),函数 xQueueCreateCountingSemaphore() 在 queue.c 文件中有定义,具体的代码如下所示:

QueueHandle_t xQueueCreateCountingSemaphore( const UBaseType_t uxMaxCount,
                                                const UBaseType_t uxInitialCount )
{
    QueueHandle_t xHandle = NULL;

    traceENTER_xQueueCreateCountingSemaphore( uxMaxCount, uxInitialCount );

    // 计数型信号量的最大资源数必须大于0,数型信号量的初始资源数不能超过最大资源数
    if( ( uxMaxCount != 0U ) &&
        ( uxInitialCount <= uxMaxCount ) )
    {
        // 创建一个队列,队列长度为计数型信号量的最大资源数,队列类型为计数型信号量
        xHandle = xQueueGenericCreate( uxMaxCount, queueSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_COUNTING_SEMAPHORE );

        if( xHandle != NULL )                                                   // 判断队列是否创建成功
        {
            ( ( Queue_t * ) xHandle )->uxMessagesWaiting = uxInitialCount;      // 队列的非空闲项目数量即为计数型信号量的资源数

            traceCREATE_COUNTING_SEMAPHORE();                                   // 用于调试
        }
        else
        {
            traceCREATE_COUNTING_SEMAPHORE_FAILED();                            // 用于调试
        }
    }
    else
    {
        configASSERT( xHandle );
        mtCOVERAGE_TEST_MARKER();
    }

    traceRETURN_xQueueCreateCountingSemaphore( xHandle );

    return xHandle;
}

  从上面的代码中可以看出,计数型信号量的就是一个队列长度为计数型信号量最大资源数的队列,而队列的非空闲项目数量就是用来记录计数型信号量的可用资源的。

3.2.1.2、静态方式创建计数型信号量

#define xSemaphoreCreateCountingStatic( uxMaxCount, uxInitialCount, pxSemaphoreBuffer )    xQueueCreateCountingSemaphoreStatic( ( uxMaxCount ), ( uxInitialCount ), ( pxSemaphoreBuffer ) )

  此函数用于使用静态方式创建计数型信号量,创建计数型信号量所需的内存,需要由用户手动分配并提供。该函数实际上是一个宏定义,在 semphr.h 中有定义。从上面的代码中可以看出,函数 xSemaphoreCreateCountingStatic() 实际上是调用了函数 xQueueCreateCountingSemaphoreStatic(),函数 xQueueCreateCountingSemaphoreStatic() 在 queue.c文件中有定义,其函数内容与函数 xQueueCreateCountingSemaphore() 类似,只是动态创建队列的函数替换成了静态创建队列的函数。

3.2.2、获取信号量计数值

#define uxSemaphoreGetCount( xSemaphore )           uxQueueMessagesWaiting( ( QueueHandle_t ) ( xSemaphore ) )

  从上面的代码中可以看出,函数 uxSemaphoreGetCount() 实际上是调用了函数 uxQueueMessagesWaiting(),函数 uxQueueMessagesWaiting() 在 queue.c文件中有定义。

3.3、实验例程

  修改 FreeRTOS 例程入口函数:

QueueHandle_t count_semphore_handle;

/**
 * @brief FreeRTOS的入口函数
 * 
 */
void freertos_demo(void)
{
    count_semphore_handle = xSemaphoreCreateCounting(30, 0);                    // 创建计数型信号量
    if (count_semphore_handle != NULL)
    {
        printf("计数型信号量创建成功\r\n");
    }

    xTaskCreate((TaskFunction_t        ) start_task,                            // 任务函数
                (char *                ) "start_task",                          // 任务名
                (configSTACK_DEPTH_TYPE) START_TASK_STACK_SIZE,                 // 任务栈大小
                (void *                ) NULL,                                  // 入口参数
                (UBaseType_t           ) START_TASK_PRIORITY,                   // 任务优先级
                (TaskHandle_t *        ) start_task_handle);                    // 任务句柄

    vTaskStartScheduler();                                                      // 开启任务调度器
}

  修改 task1() 函数:

/**
 * @brief 任务1的任务函数
 * 
 * @param pvParameters 任务函数的入口参数
 */
void task1(void *pvParameters)
{
    uint8_t key = 0;
    BaseType_t result = 0;

    while (1)
    {
        key = Key_Scan(0);
        switch (key)
        {
        case KEY1_PRESS:
            if (count_semphore_handle != NULL)
            {
                result = xSemaphoreGive(count_semphore_handle);                 // 释放计数型信号量
                if (result == pdPASS)
                {
                    printf("计数型信号量量释放成功\r\n");
                }
                else
                {
                    printf("计数型信号量释放失败\r\n");
                }
            }
            break;

        default:
            break;
        }

        vTaskDelay(10);
    }
}

  修改 task2() 函数:

/**
 * @brief 任务2的任务函数
 * 
 * @param pvParameters 任务函数的入口参数
 */
void task2(void *pvParameters )
{
    BaseType_t result = 0;
    UBaseType_t count = 0;

    while (1)
    {
        result = xSemaphoreTake(count_semphore_handle, 1000);
        if (result == pdTRUE)
        {
            count = uxSemaphoreGetCount(count_semphore_handle);                 // 获取当前计数值
            printf("计数型信号量获取成功,信号量计数值为:%ld\r\n", count);
        }
        vTaskDelay(1000);
    }
}

计数型信号量和二值信号量的 API 函数是公用的。

四、优先级翻转

  优先级翻转 是指高优先级的任务反而慢执行,低优先级的任务反而优先执行。在使用二值信号量和计数型信号量的时候,会经常地遇到优先级翻转的问题,优先级翻转的问题在抢占式内核中是很常见的,但是在实时操作系统中是不允许出现优先级翻转的现象的,因为优先级翻转会破坏任务执行的预期顺序,可能会导致未知的严重后果。

优先级翻转

  如上图所示,定义:任务 H 为优先级最高的任务,任务 L 为优先级最低的任务,任务 M 为优先级介于任务 H 与任务 L 之间的任务。

  1. 任务 H 和任务 M 为挂起状态,等待某一事件发生,此时任务 L 正在运行。
  2. 此时任务 L 要访问共享资源,因此需要获取信号量。
  3. 任务 L 成功获取信号量,并且此时信号量已无资源,任务 L 开始访问共享资源。
  4. 此时任务 H 就绪,抢占任务 L 运行。
  5. 任务 H 开始运行。
  6. 此时任务 H 要访问共享资源,因此需要获取信号量,但信号量已无资源,因此任务 H 挂起等待信号量资源。
  7. 任务 L 继续运行。
  8. 此时任务 M 就绪,抢占任务 L 运行。
  9. 任务 M 正在运行。
  10. 任务 M 运行完毕,继续挂起。
  11. 任务 L 继续运行。
  12. 此时任务 L 对共享资源的访问操作完成,释放信号量,虽有任务 H 因成功获取信号量,解除挂起状态并抢占任务 L 运行。
  13. 任务 H 得以运行。

  从上面优先级翻转的示例中,可以看出,任务 H 为最高优先级的任务,因此任务 H 执行的操作需要有较高的实时性,但是由于优先级翻转的问题,导致了任务 H 需要等到任务 L 释放信号量才能够运行,并且,任务 L 还会被其他介于任务 H 与任务 L 任务优先级之间的任务 M 抢占,因此任务 H 还需等待任务 M 运行完毕,这显然不符合任务 H 需要的高实时性要求。

  修改 FreeRTOS 例程入口函数:

QueueHandle_t semphore_handle;

/**
 * @brief FreeRTOS的入口函数
 * 
 */
void freertos_demo(void)
{
    semphore_handle = xSemaphoreCreateBinary();                                // 创建信号量
    if (semphore_handle != NULL)
    {
        printf("信号量创建成功\r\n");
    }

    xTaskCreate((TaskFunction_t        ) start_task,                            // 任务函数
                (char *                ) "start_task",                          // 任务名
                (configSTACK_DEPTH_TYPE) START_TASK_STACK_SIZE,                 // 任务栈大小
                (void *                ) NULL,                                  // 入口参数
                (UBaseType_t           ) START_TASK_PRIORITY,                   // 任务优先级
                (TaskHandle_t *        ) start_task_handle);                    // 任务句柄

    vTaskStartScheduler();                                                      // 开启任务调度器
}

  修改 START_TASK 任务配置:

/**
 * START_TASK 任务配置
 * 包括: 任务优先级 任务栈大小 任务句柄 任务函数
 */
#define START_TASK_PRIORITY     1
#define START_TASK_STACK_SIZE   128

TaskHandle_t start_task_handle;

void start_task(void *pvParameters );

/**
 * @brief 开始任务的任务函数
 * 
 * @param pvParameters 任务函数的入口参数
 */
void start_task(void *pvParameters)
{
    taskENTER_CRITICAL();                                                       // 进入临界区,关闭中断

    xTaskCreate((TaskFunction_t        ) task1,                                 // 任务函数
                (char *                ) "task1",                               // 任务名
                (configSTACK_DEPTH_TYPE) TASK1_STACK_SIZE,                      // 任务栈大小
                (void *                ) NULL,                                  // 入口参数
                (UBaseType_t           ) TASK1_PRIORITY,                        // 任务优先级
                (TaskHandle_t *        ) &task1_handle);                        // 任务句柄

    xTaskCreate((TaskFunction_t        ) task2,                                 // 任务函数
                (char *                ) "task2",                               // 任务名
                (configSTACK_DEPTH_TYPE) TASK2_STACK_SIZE,                      // 任务栈大小
                (void *                ) NULL,                                  // 入口参数
                (UBaseType_t           ) TASK2_PRIORITY,                        // 任务优先级
                (TaskHandle_t *        ) &task2_handle);                        // 任务句柄

    xTaskCreate((TaskFunction_t        ) task3,                                 // 任务函数
                (char *                ) "task3",                               // 任务名
                (configSTACK_DEPTH_TYPE) TASK3_STACK_SIZE,                      // 任务栈大小
                (void *                ) NULL,                                  // 入口参数
                (UBaseType_t           ) TASK3_PRIORITY,                        // 任务优先级
                (TaskHandle_t *        ) &task3_handle);                        // 任务句柄

    taskEXIT_CRITICAL();                                                        // 退出临界区,重新开启中断

    vTaskDelete(NULL);                                                          // 删除任务自身
}

  修改 TASK1 任务配置:

/**
 * TASK1 任务配置
 * 包括: 任务优先级 任务栈大小 任务句柄 任务函数
 */
#define TASK1_PRIORITY     2
#define TASK1_STACK_SIZE   128

TaskHandle_t task1_handle;

void task1(void *pvParameters);

/**
 * @brief 任务1的任务函数
 * 
 * @param pvParameters 任务函数的入口参数
 */
void task1(void *pvParameters)
{
    while (1)
    {
        printf("低优先级的任务正在获取二值信号量\r\n");
        xSemaphoreTake(semphore_handle, portMAX_DELAY);                         // 获取信号量
        printf("低优先级的任务获取成功二值信号量\r\n");

        printf("低优先级的任务正在运行\r\n");
        Delay_ms(3000);

        printf("低优先级的任务正在释放二值信号量\r\n");
        xSemaphoreGive(semphore_handle);                                        // 释放信号量
        printf("低优先级的任务释放成功二值信号量\r\n");

        vTaskDelay(1000);
    }
}

  修改 TASK2 任务配置:

/**
 * TASK2 任务配置
 * 包括: 任务优先级 任务栈大小 任务句柄 任务控制块  任务栈 任务函数
 */
#define TASK2_PRIORITY     3
#define TASK2_STACK_SIZE   128

TaskHandle_t task2_handle;

void task2(void *pvParameters);

/**
 * @brief 任务2的任务函数
 * 
 * @param pvParameters 任务函数的入口参数
 */
void task2(void *pvParameters )
{
    while (1)
    {
        printf("中等优先级的任务正在运行\r\n");
        vTaskDelay(1000);
    }
}

  新建 TASK3 任务配置:

/**
 * TASK3 任务配置
 * 包括: 任务优先级 任务栈大小 任务句柄 任务函数
 */
#define TASK3_PRIORITY     4
#define TASK3_STACK_SIZE   128

TaskHandle_t task3_handle;

void task3(void *pvParameters);

/**
 * @brief 任务3的任务函数
 * 
 * @param pvParameters 任务函数的入口参数
 */
void task3(void *pvParameters )
{
    while (1)
    {
        printf("高优先级的任务正在获取二值信号量\r\n");
        xSemaphoreTake(semphore_handle, portMAX_DELAY);                         // 获取信号量
        printf("高优先级的任务获取成功二值信号量\r\n");

        printf("高优先级的任务正在运行\r\n");
        Delay_ms(1000);

        printf("高优先级的任务正在释放二值信号量\r\n");
        xSemaphoreGive(semphore_handle);                                        // 释放信号量
        printf("高优先级的任务释放成功二值信号量\r\n");

        vTaskDelay(1000);
    }
}

五、互斥信号量

5.1、互斥信号量简介

  互斥信号量 也叫 互斥锁,可以理解为是一种特殊的二值信号量,互斥信号量拥有优先级继承的机制使得互斥信号量能够在一定的程度上解决优先级翻转的问题。互斥信号量一般用于那些需要互斥访问的应用中。在互斥访问的应用中,互斥信号量就相当于是一把钥匙,当任务想要访问共享资源的时候,就必须先获取到这把钥匙,当任务访问完共享资源后,就必须归还这把钥匙,这样其他任务就可以拿着这把钥匙再去访问这个共享资源。

  互斥信号量的优先级继承机制体现在,当一个互斥信号量正被一个低优先级的任务持有时,如果此时有一个高优先级的任务也尝试获取这个互斥信号量,那么这个高优先级的任务就会因获取不到互斥锁而被挂起,不过接下来,高优先级的任务会将持有互斥信号量的低优先级任务的任务优先级提成到与高优先级任务的任务优先级相同的任务优先级,这个过程就是 优先级继承。优先级继承可以尽可能地减少高优先级任务挂起等待互斥锁的时间,并且将优先级翻转问题带来的影响降到最低。

  但是优先级继承并不是能完全解决优先级翻转带来的问题,因为优先级继承仅仅是将持有互斥信号量的低优先级任务的任务优先级提高的与高优先级任务相同的任务优先级,而非直接将互斥信号量直接从低优先级的任务手上 “抢” 过来,因此高优先级的任务还是需要等待低优先级的任务释放互斥信号量,高优先级的任务才能够获取到互斥信号量。

优先级继承

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

互斥信号量不能用于中断服务函数中,原因如下:

  • 互斥信号量有任务优先级继承的机制, 但是中断不是任务,没有任务优先级, 所以互斥信号量只能用于任务中,不能用于中断服务函数。
  • 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。

5.2、互斥信号量相关 API 函数

  互斥信号量除了创建函数之外,其余的获取、释放等信号量操作函数,都与二值信号量相同。使用互斥信号量时,首先将宏 configUSE_MUTEXES 置 1。

5.2.1、创建互斥信号量

  我们可以通过 xSemaphoreCreateMutex() 函数使用动态方式创建互斥信号量,创建互斥信号量所需的内存,由 FreeRTOS 从FreeRTOS 管理的堆中进行分配。该函数实际上是一个宏定义,在 semphr.h 中有定义,具体的代码如下所示:

#define xSemaphoreCreateMutex()    xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )

  从上面的代码中可以看出,函数 xSemaphoreCreateMutex() 实际上是调用了函数 xQueueCreateMutex(),函数 xQueueCreateMutex() 在 queue.c 文件中有定义,具体的代码如下所示:

QueueHandle_t xQueueCreateMutex( const uint8_t ucQueueType )
{
    QueueHandle_t xNewQueue;
    const UBaseType_t uxMutexLength = ( UBaseType_t ) 1, uxMutexSize = ( UBaseType_t ) 0;

    traceENTER_xQueueCreateMutex( ucQueueType );

    // 创建一个队列,队列长度为 1,队列项目大小为 0
    xNewQueue = xQueueGenericCreate( uxMutexLength, uxMutexSize, ucQueueType );
    prvInitialiseMutex( ( Queue_t * ) xNewQueue );                              // 初始化互斥信号量

    traceRETURN_xQueueCreateMutex( xNewQueue );

    return xNewQueue;
}

  从上面的代码中可以看出,互斥信号量的就是一个队列长度为 1 的队列,且队列项目的大小为 0,而队列的非空闲项目数量就是互斥信号量的资源数。函数 xQueueCreateMutex() 还会调用函数 prvInitialiseMutex() 对互斥信号量进行初始化,函数 prvInitialiseMutex() 在 queue.c 文件中有定义,具体的代码如下所示:

static void prvInitialiseMutex( Queue_t * pxNewQueue )
{
    if( pxNewQueue != NULL )
    {
        pxNewQueue->u.xSemaphore.xMutexHolder = NULL;                           // 互斥信号量的持有者初始化为空
        pxNewQueue->uxQueueType = queueQUEUE_IS_MUTEX;                          // 互斥信号量的持有者初始化为空

        pxNewQueue->u.xSemaphore.uxRecursiveCallCount = 0;                      // 互斥信号量的资源数初始化为〇

        traceCREATE_MUTEX( pxNewQueue );                                        // 用于调试

        // 新建的互斥信号量是有资源的
        ( void ) xQueueGenericSend( pxNewQueue, NULL, ( TickType_t ) 0U, queueSEND_TO_BACK );
    }
    else
    {
        traceCREATE_MUTEX_FAILED();                                             // 新建的互斥信号量是有资源的
    }
}

  我们还可以通过 xSemaphoreCreateMutexStatic() 函数使用静态方式创建互斥信号量,创建互斥信号量所需的内存,需要由用户手动分配并提供。该函数实际上是一个宏定义,在 semphr.h 中有定义,具体的代码如下所示:

#define xSemaphoreCreateMutexStatic( pxMutexBuffer )    xQueueCreateMutexStatic( queueQUEUE_TYPE_MUTEX, ( pxMutexBuffer ) )

  从上面的代码中可以看出,函数 xSemaphoreCreateMutexStatic() 实际上是调用了函数 xQueueCreateMutexStatic(),而函数 xQueueCreateMutexStatic() 在 queue.c 文件中有定义,其函数内容与函数 xQueueCreateMutex() 是类似的,只是将动态创建队列的函数替换成了静态创建队列的函数。

创建互斥信号量时,会主动释放一次信号量。

5.3、实验例程

  修改 FreeRTOS 例程入口函数:

QueueHandle_t semphore_handle;

/**
 * @brief FreeRTOS的入口函数
 * 
 */
void freertos_demo(void)
{
    semphore_handle = xSemaphoreCreateMutex();                                  // 创建互斥信号量
    if (semphore_handle != NULL)
    {
        printf("互斥信号量创建成功\r\n");
    }

    xTaskCreate((TaskFunction_t        ) start_task,                            // 任务函数
                (char *                ) "start_task",                          // 任务名
                (configSTACK_DEPTH_TYPE) START_TASK_STACK_SIZE,                 // 任务栈大小
                (void *                ) NULL,                                  // 入口参数
                (UBaseType_t           ) START_TASK_PRIORITY,                   // 任务优先级
                (TaskHandle_t *        ) start_task_handle);                    // 任务句柄

    vTaskStartScheduler();                                                      // 开启任务调度器
}
posted @ 2024-03-17 19:27  星光映梦  阅读(580)  评论(0编辑  收藏  举报