目录

1、Binary Semaphores

1.1、Usage

1.2、APIs

1.2.1、xSemaphoreCreateBinary

1.2.2、xSemaphoreTake / xSemaphoreTakeFromISR

1.2.3、xSemaphoreGive / xSemaphoreGiveFromISR

2、Counting Semaphores

2.1、Usage

2.2、APIs

2.2.1、xSemaphoreCreateCounting


 

FreeRTOS 中使用信号量来做同步,信号量可以在任务中使用作为任务与任务间的同步,也可以在中断中使用(带 FromISR 的版本)中断与任务间的同步;

针对不同的应用场景,信号量分为两种:

1、二值信号量;

2、计数信号量;

 

1、Binary Semaphores

1.1、Usage

顾名思义,二值信号量只有两个值:0 和 1;它用于简单场景下的任务与任务、中断与任务之间的同步,比如:一个任务,在等待某个资源到位后,才能够继续执行,在得到这个资源之前,它处于阻塞状态,假如一个中断来了,给出了这个资源,那么等待这个资源的任务便可以投入运行,这种情况下就可以使用二值信号量来处理这个情况:

上面的情况,可以将二值信号量理解为长度为 1 的 Queue,实际上,它的实现,也是用 Queue;

操作二值信号量,分为两个行为:Give 和 Take:

1、Give:往二值信号量写 1;

2、Take:获取二值信号量;

上面的例子可以理解为下面的顺序:

首先初始化一个二值信号量,任务尝试获取二值信号量,但是获取失败,使得任务进入 Blocked 状态:

中断来了,执行 ISR,往这个二值信号量写 1;

此刻,等待在这个二值信号量上的任务会被解除阻塞,投入运行:

任务获取到二值信号量,开始执行任务:

执行完毕后,再次去 Take 失败,再次进入 Blocked:

1.2、APIs

1.2.1、xSemaphoreCreateBinary

应用代码使用 xSemaphoreCreateBinary 来创建二值信号量:

SemaphoreHandle_t xSemaphoreCreateBinary( void );

有一个返回值:

Return:创建成功返回一个信号量的句柄,失败返回 NULL;

 

1.2.2、xSemaphoreTake / xSemaphoreTakeFromISR

应用代码使用 xSemaphoreTake() 来获取一个二值信号量:

注意:不要在 ISR 中使用 xSemaphoreTake 应该使用对应的 xSemaphoreTakeFromISR;

BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait );

有两个入参,一个返回值:

xSemaphore:创建信号量的句柄;

xTicksToWait:如果获取不到信号量,最大阻塞的时间;如果设置为 0,那么立即返回(不阻塞),如果配置为 portMAX_DELAY,则无限制等待;

Return:返回 pdPASS 代表正常获取到信号量,返回 pdFALSE 代表获取失败;

 

1.2.3、xSemaphoreGive / xSemaphoreGiveFromISR

应用代码使用 xSemaphoreGive() 来设置一个二值信号量:

注意:不要在 ISR 中使用 xSemaphoreGive 应该使用对应的 xSemaphoreGiveFromISR

BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore,
                                  BaseType_t *pxHigherPriorityTaskWoken );

两个入参,一个返回值:

xSemaphore:信号量的句柄;

pxHigherPriorityTaskWoken :如果该信号量会导致一个比当前任务优先级更高的任务解除阻塞,那么返回 pdTRUE;

Return:返回是否设置信号量成功,成功返回 pdTRUE,否则返回 pdFALSE;

Example:

有一个周期性的任务,每隔 500ms 定时产生一个软件中断:

/* The number of the software interrupt used in this example. The code shown is from
the Windows project, where numbers 0 to 2 are used by the FreeRTOS Windows port
itself, so 3 is the first number available to the application. */

#define mainINTERRUPT_NUMBER 3
static void vPeriodicTask( void *pvParameters )
{
    const TickType_t xDelay500ms = pdMS_TO_TICKS( 500UL );
    /* As per most tasks, this task is implemented within an infinite loop. */
    for( ;; )
    {
        /* Block until it is time to generate the software interrupt again. */
        vTaskDelay( xDelay500ms );
        /* Generate the interrupt, printing a message both before and after
        the interrupt has been generated, so the sequence of execution is evident
        from the output.
        The syntax used to generate a software interrupt is dependent on the
        FreeRTOS port being used. The syntax used below can only be used with
        the FreeRTOS Windows port, in which such interrupts are only simulated. */
        vPrintString( "Periodic task - About to generate an interrupt.\r\n" );
        vPortGenerateSimulatedInterrupt( mainINTERRUPT_NUMBER );
        vPrintString( "Periodic task - Interrupt generated.\r\n\r\n\r\n" );
    }
}

下面是一个期望获取二值信号量的任务:

static void vHandlerTask( void *pvParameters )
{
    /* As per most tasks, this task is implemented within an infinite loop. */
    for( ;; )
    {
        /* Use the semaphore to wait for the event. The semaphore was created
        before the scheduler was started, so before this task ran for the first
        time. The task blocks indefinitely, meaning this function call will only
        return once the semaphore has been successfully obtained - so there is
        no need to check the value returned by xSemaphoreTake(). */
        xSemaphoreTake( xBinarySemaphore, portMAX_DELAY );
        /* To get here the event must have occurred. Process the event (in this
        Case, just print out a message). */
        vPrintString( "Handler task - Processing event.\r\n" );
    }
}

接下来是产生中断的那个 ISR,根据传入的 pxHigherPriorityTaskWoken 来判断是否要进行上下文切换:

static uint32_t ulExampleInterruptHandler( void )
{
    BaseType_t xHigherPriorityTaskWoken;
    
    /* The xHigherPriorityTaskWoken parameter must be initialized to pdFALSE as
    it will get set to pdTRUE inside the interrupt safe API function if a
    context switch is required. */
    xHigherPriorityTaskWoken = pdFALSE;
    
    /* 'Give' the semaphore to unblock the task, passing in the address of
    xHigherPriorityTaskWoken as the interrupt safe API function's
    pxHigherPriorityTaskWoken parameter. */
    xSemaphoreGiveFromISR( xBinarySemaphore, &xHigherPriorityTaskWoken );
    
    /* Pass the xHigherPriorityTaskWoken value into portYIELD_FROM_ISR(). If
    xHigherPriorityTaskWoken was set to pdTRUE inside xSemaphoreGiveFromISR()
    then calling portYIELD_FROM_ISR() will request a context switch. If
    xHigherPriorityTaskWoken is still pdFALSE then calling
    portYIELD_FROM_ISR() will have no effect. Unlike most FreeRTOS ports, the
    Windows port requires the ISR to return a value - the return statement
    is inside the Windows version of portYIELD_FROM_ISR(). */
    
    portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}

主函数为:

int main( void )
{
    /* Before a semaphore is used it must be explicitly created. In this example
    a binary semaphore is created. */
    xBinarySemaphore = xSemaphoreCreateBinary();
    /* Check the semaphore was created successfully. */
    if( xBinarySemaphore != NULL )
    {
        /* Create the 'handler' task, which is the task to which interrupt
        processing is deferred. This is the task that will be synchronized with
        the interrupt. The handler task is created with a high priority to ensure
        it runs immediately after the interrupt exits. In this case a priority of
        3 is chosen. */
        xTaskCreate( vHandlerTask, "Handler", 1000, NULL, 3, NULL );
        /* Create the task that will periodically generate a software interrupt.
        This is created with a priority below the handler task to ensure it will
        get preempted each time the handler task exits the Blocked state. */
        xTaskCreate( vPeriodicTask, "Periodic", 1000, NULL, 1, NULL );
        /* Install the handler for the software interrupt. The syntax necessary
        to do this is dependent on the FreeRTOS port being used. The syntax
        shown here can only be used with the FreeRTOS windows port, where such
        interrupts are only simulated. */
        vPortSetInterruptHandler( mainINTERRUPT_NUMBER, ulExampleInterruptHandler );
        /* Start the scheduler so the created tasks start executing. */
        vTaskStartScheduler();
    }
    /* As normal, the following line should never be reached. */
    for( ;; );
}

由于 vHandlerTask 优先级为最高,所以他会先运行,并阻塞在二值信号量的获取上;

vHandlerTask 进入阻塞后,vPeriodicTask 会周期性的去拉一个中断,导致进入 ISR;

在 ISR 中设置了信号量,导致 vHandlerTask 被解除阻塞,进入运行,抢占 vPeriodicTask;

vHandlerTask 运行完后,再次进入阻塞;

上述的场景中,二值信号量是可以胜任的,但是试想,真实的系统中,IRQ 是随时都可能来的,如果一种情况下,IRQ 来的比较频繁,当 Task 正在获得二值信号量处理的时候,又连续来了 2 个 IRQ,由于二值信号量只能存储一次事件,那么必然导致事件的丢失,如下所示:

此刻二值信号量显得有点力不从心,接下来就看计数信号量的了

 

2、Counting Semaphores

2.1、Usage

二值信号量可以看成是只有一个长度的 Queue,计数信号量就是多个长度的 Queue(只关心长度,不关心 Queue 内容);

要使用计数信号量,需要配置 configUSE_COUNTING_SEMAPHORES 为 1;

计数信号量主要可以用作如下两个方面:

1、事件计数:这种场景下,事件通过 Give 来往计数信号量中记录事件发生的次数,另一端的 Task 通过 Take 来进行每一次事件的处理;一般的,信号量的计数被初始化为 0;

2、资源管理:这种场景下,信号量代表可用资源的数目,一般的,这种情况将信号量初始化为一个资源的数目,任务每次获取资源,都将资源减一;如果信号量为 0 说明没有可用的资源了;一旦任务完成,便通过 Give 来释放资源,增加信号量的计数;

同样是之前的例子,当 IRQ 来的过快,任务来不及处理完的情况下,多余的 Event 会在计数信号量中保存,直到任务完成,再次进入阻塞:

 

2.2、APIs

2.2.1、xSemaphoreCreateCounting

创建一个计数信号量使用 xSemaphoreCreateCounting 接口:

SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount,
                                            UBaseType_t uxInitialCount );

两个入参,一个返回值:

uxMaxCount:指的是信号量的最大计数个数;

uxInitialCount:被初始化的个数;

Return:如果成功,返回信号量的句柄,否则返回 NULL;

其余的 Get 和 Take 和二值信号量一样,不在赘述;

 

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐