目录

一、先搞懂基本概念:什么是二值信号量?什么是互斥量?

二、核心区别:从 “行为特性” 看差异

关键区别详解:

a.“所有权” 是核心差异

b.优先级反转问题的处理(重点)

1.使用二值信号量

2.使用互斥量

三、使用示例

3.1二值信号量的使用方法

1.创建二值信号量

2.1 任务中获取信号量

2.2 中断中获取信号量

3.1 任务中释放信号量

3.2 中断中释放信号量

3.2互斥量的使用方法

1.创建互斥量

2.获取互斥量(加锁)

3.释放互斥量(解锁)

3.3使用时的核心注意事项

1.二值信号量的注意事项

2.互斥量的注意事项

3.两者共有的注意事项

总结

四、怎么选?用场景判断


对于学习FreeRTOS 来说,二值信号量和互斥量确实容易混淆,因为它们底层机制相似,但用途和行为有本质区别。我们可以从 “功能用途”“核心特性”“使用场景” 三个维度来理解它们的区别和联系。

一、先搞懂基本概念:什么是二值信号量?什么是互斥量?

1. 二值信号量(Binary Semaphore

可以理解为一个 “开关”,只有两种状态:“有信号”(值为 1)和“无信号”(值为 0)。

  • 当一个任务调用 xSemaphoreGive() 时,信号量变为 “有信号”(值 1);

  • 当一个任务调用 xSemaphoreTake() 时,信号量变为 “无信号”(值 0);

  • 核心作用:“同步”(让一个任务等待另一个任务 / 事件的发生)。

2. 互斥量(Mutex

可以理解为一个 “钥匙”,用于保护共享资源 (比如一个传感器、一段内存、一个外设)。

  • 当一个任务需要访问共享资源时,先 “拿钥匙”(xSemaphoreTake());

  • 用完后必须 “还钥匙”(xSemaphoreGive());

  • 核心作用:“互斥”(确保同一时间只有一个任务能访问共享资源)。

二、核心区别:从 “行为特性” 看差异

关键区别详解:

a.“所有权” 是核心差异

  • 二值信号量:比如任务 A 调用 xSemaphoreGive() 使信号量变为 1,任务 B 可以调用 xSemaphoreTake() 拿走(变为 0),之后任务 C 也可以调用 xSemaphoreGive() 再次使它变为 1—— 谁都可以 “给”,谁都可以 “拿”,没有归属。

  • 互斥量:比如任务 A 调用 xSemaphoreTake() 拿到互斥量(“拿到钥匙”),那么只有任务 A能调用 xSemaphoreGive() 释放它。如果任务 B 试图释放任务 A 持有的互斥量,会导致错误(行为未定义)。

b.优先级反转问题的处理(重点)

这是互斥量最关键的特性,也是最难理解的点,举个例子:

  1. 假设有高优先级任务 H、中优先级任务 M、低优先级任务 L。

三个任务在 “竞争访问同一个共享资源” 时,采用了不同的 “资源保护工具”—— 要么用二值信号量,要么用互斥量。

1.使用二值信号量

这里的逻辑是:

为了防止多个任务同时访问共享资源(比如一块内存、一个传感器),三个任务(L、M、H)约定用二值信号量作为 “资源占用的标志”:

  • 谁要访问资源,先调用 xSemaphoreTake() 拿信号量(拿到了就表示 “资源可用”,没拿到就等);

  • 用完资源后,调用 xSemaphoreGive() 释放信号量(告诉其他任务 “资源空闲了”)。

但因为二值信号量没有 “优先级继承” 特性,就会出现问题:

  1. 低优先级任务 L 先拿到二值信号量,开始访问资源;

  2. 此时高优先级任务 H 就绪,也想拿信号量访问资源,但信号量被 L 占用,H 只能等待;

  3. 中优先级任务 M(优先级比 L 高、比 H 低)也就绪了 —— 因为 M 的优先级比 L 高,会直接抢占 L 的 CPU(L 被打断,暂停访问资源);

  4. M 运行时,H 依然在等信号量,但 L 被 M 打断后迟迟不能释放信号量,导致 H(最高优先级)反而被 M(中优先级)和 L(低优先级)“拖累”,这就是优先级反转。

一句话:三个任务都用二值信号量来 “抢资源”,但二值信号量保护不了优先级,导致反转。

2.使用互斥量

这里的逻辑是:

三个任务(L、M、H)约定改用互斥量作为 “资源占用的标志”,规则和信号量类似(拿锁 -> 用资源 -> 放锁),但互斥量有 “优先级继承” 特性,能解决问题:

  1. 低优先级任务 L 先拿到互斥量,开始访问资源;

  2. 高优先级任务 H 就绪,想拿互斥量但被 L 占用,H 开始等待;

  3. 此时 FreeRTOS 检测到 “高优先级任务 H 在等低优先级任务 L 持有的互斥量”,会临时把 L 的优先级提升到和 H 一样高;

  4. 中优先级任务 M 就绪后,因为 M 的优先级低于 “被临时提升后的 L”,所以 M 无法抢占 L,L 能继续不受干扰地访问资源,用完后释放互斥量;

  5. 互斥量释放后,H 就能立刻拿到互斥量访问资源,避免了优先级反转。

一句话:三个任务都用互斥量来 “抢资源”,互斥量通过 “临时提升低优先级任务 L 的优先级”,阻止了 M 抢占 L,让 H 能及时拿到资源。

三、使用示例

二值信号量和互斥量在 FreeRTOS 底层都基于信号量机制实现,它们的核心操作(xSemaphoreTake() 获取、xSemaphoreGive() 释放)的函数接口完全相同。

可以理解为:

  • 二值信号量是 “最简单的信号量”(计数只能是 0 或 1);

  • 互斥量是 “增强版的二值信号量”,增加了 “所有权” 和 “优先级继承” 特性,专门用于互斥场景。

3.1二值信号量的使用方法

二值信号量核心用于同步(如 “事件通知”“任务唤醒”),使用流程分为 3 步:创建、获取(等待信号)、释放(发送信号)。

1.创建二值信号量

使用 xSemaphoreCreateBinary() 函数创建,返回信号量句柄(SemaphoreHandle_t)。

#include "FreeRTOS.h"
#include "semphr.h"

// 定义二值信号量句柄(全局或任务间可见)
SemaphoreHandle_t xBinarySemaphore;

void vInitSemaphores(void) {
    // 创建二值信号量(初始状态为0:无信号)
    xBinarySemaphore = xSemaphoreCreateBinary();
    
    // 检查创建是否成功(内存不足可能失败)
    if (xBinarySemaphore == NULL) {
        // 错误处理:如日志输出、系统复位等
    }
}

注意:二值信号量创建后默认状态为 “无信号”(值为 0),需通过xSemaphoreGive()手动设置为 “有信号”(值为 1)。

2.1 任务中获取信号量

使用 xSemaphoreTake() 函数等待信号量变为 “有信号”(值 1),获取后信号量自动变为 “无信号”(值 0)。

FreeRTOS源代码:

/**
 * semphr. h
 * <pre>xSemaphoreTake(
 *                   SemaphoreHandle_t xSemaphore,
 *                   TickType_t xBlockTime
 *               )</pre>
 *
 * <i>Macro</i> to obtain a semaphore.  The semaphore must have previously been
 * created with a call to xSemaphoreCreateBinary(), xSemaphoreCreateMutex() or
 * xSemaphoreCreateCounting().
 *
 * @param xSemaphore A handle to the semaphore being taken - obtained when
 * the semaphore was created.
 *
 * @param xBlockTime The time in ticks to wait for the semaphore to become
 * available.  The macro portTICK_PERIOD_MS can be used to convert this to a
 * real time.  A block time of zero can be used to poll the semaphore.  A block
 * time of portMAX_DELAY can be used to block indefinitely (provided
 * INCLUDE_vTaskSuspend is set to 1 in FreeRTOSConfig.h).
 *
 * @return pdTRUE if the semaphore was obtained.  pdFALSE
 * if xBlockTime expired without the semaphore becoming available.
 *
 * Example usage:
 <pre>
 SemaphoreHandle_t xSemaphore = NULL;

 // A task that creates a semaphore.
 void vATask( void * pvParameters )
 {
    // Create the semaphore to guard a shared resource.
    xSemaphore = xSemaphoreCreateBinary();
 }

 // A task that uses the semaphore.
 void vAnotherTask( void * pvParameters )
 {
    // ... Do other things.

    if( xSemaphore != NULL )
    {
        // See if we can obtain the semaphore.  If the semaphore is not available
        // wait 10 ticks to see if it becomes free.
        if( xSemaphoreTake( xSemaphore, ( TickType_t ) 10 ) == pdTRUE )
        {
            // We were able to obtain the semaphore and can now access the
            // shared resource.

            // ...

            // We have finished accessing the shared resource.  Release the
            // semaphore.
            xSemaphoreGive( xSemaphore );
        }
        else
        {
            // We could not obtain the semaphore and can therefore not access
            // the shared resource safely.
        }
    }
 }
 </pre>
 * \defgroup xSemaphoreTake xSemaphoreTake
 * \ingroup Semaphores
 */
#define xSemaphoreTake( xSemaphore, xBlockTime )                xQueueSemaphoreTake( ( xSemaphore ), ( xBlockTime ) )

示例:任务等待中断通知(如按键按下)

// 等待信号量的任务
void vWaitTask(void *pvParameters) {
    for (;;) {
        // 等待信号量(无限期等待,直到有信号)
        if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdPASS) {
            // 信号量获取成功:处理事件(如按键逻辑)
            printf("收到信号,执行操作...\n");
        }
    }
}

2.2 中断中获取信号量

FreeRTOS源代码:

/**
 * semphr. h
 * <pre>
 xSemaphoreTakeFromISR(
                          SemaphoreHandle_t xSemaphore,
                          BaseType_t *pxHigherPriorityTaskWoken
                      )</pre>
 *
 * <i>Macro</i> to  take a semaphore from an ISR.  The semaphore must have
 * previously been created with a call to xSemaphoreCreateBinary() or
 * xSemaphoreCreateCounting().
 *
 * Mutex type semaphores (those created using a call to xSemaphoreCreateMutex())
 * must not be used with this macro.
 *
 * This macro can be used from an ISR, however taking a semaphore from an ISR
 * is not a common operation.  It is likely to only be useful when taking a
 * counting semaphore when an interrupt is obtaining an object from a resource
 * pool (when the semaphore count indicates the number of resources available).
 *
 * @param xSemaphore A handle to the semaphore being taken.  This is the
 * handle returned when the semaphore was created.
 *
 * @param pxHigherPriorityTaskWoken xSemaphoreTakeFromISR() will set
 * *pxHigherPriorityTaskWoken to pdTRUE if taking the semaphore caused a task
 * to unblock, and the unblocked task has a priority higher than the currently
 * running task.  If xSemaphoreTakeFromISR() sets this value to pdTRUE then
 * a context switch should be requested before the interrupt is exited.
 *
 * @return pdTRUE if the semaphore was successfully taken, otherwise
 * pdFALSE
 */
#define xSemaphoreTakeFromISR( xSemaphore, pxHigherPriorityTaskWoken )        xQueueReceiveFromISR( ( QueueHandle_t ) ( xSemaphore ), NULL, ( pxHigherPriorityTaskWoken ) )

3.1 任务中释放信号量

  • 任务中释放:xSemaphoreGive()

  • 中断中释放:必须用 xSemaphoreGiveFromISR()(ISR 安全版本)

FreeRTOS源代码:

/**
 * semphr. h
 * <pre>xSemaphoreGive( SemaphoreHandle_t xSemaphore )</pre>
 *
 * <i>Macro</i> to release a semaphore.  The semaphore must have previously been
 * created with a call to xSemaphoreCreateBinary(), xSemaphoreCreateMutex() or
 * xSemaphoreCreateCounting(). and obtained using sSemaphoreTake().
 *
 * This macro must not be used from an ISR.  See xSemaphoreGiveFromISR () for
 * an alternative which can be used from an ISR.
 *
 * This macro must also not be used on semaphores created using
 * xSemaphoreCreateRecursiveMutex().
 *
 * @param xSemaphore A handle to the semaphore being released.  This is the
 * handle returned when the semaphore was created.
 *
 * @return pdTRUE if the semaphore was released.  pdFALSE if an error occurred.
 * Semaphores are implemented using queues.  An error can occur if there is
 * no space on the queue to post a message - indicating that the
 * semaphore was not first obtained correctly.
 *
 * Example usage:
 <pre>
 SemaphoreHandle_t xSemaphore = NULL;

 void vATask( void * pvParameters )
 {
    // Create the semaphore to guard a shared resource.
    xSemaphore = vSemaphoreCreateBinary();

    if( xSemaphore != NULL )
    {
        if( xSemaphoreGive( xSemaphore ) != pdTRUE )
        {
            // We would expect this call to fail because we cannot give
            // a semaphore without first "taking" it!
        }

        // Obtain the semaphore - don't block if the semaphore is not
        // immediately available.
        if( xSemaphoreTake( xSemaphore, ( TickType_t ) 0 ) )
        {
            // We now have the semaphore and can access the shared resource.

            // ...

            // We have finished accessing the shared resource so can free the
            // semaphore.
            if( xSemaphoreGive( xSemaphore ) != pdTRUE )
            {
                // We would not expect this call to fail because we must have
                // obtained the semaphore to get here.
            }
        }
    }
 }
 </pre>
 * \defgroup xSemaphoreGive xSemaphoreGive
 * \ingroup Semaphores
 */
#define xSemaphoreGive( xSemaphore )                xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )

3.2 中断中释放信号量

使用 xSemaphoreGive() 函数将信号量设置为 “有信号”(值 1),唤醒等待的任务。

FreeRTOS源代码:

/**
 * semphr. h
 * <pre>
 xSemaphoreGiveFromISR(
                          SemaphoreHandle_t xSemaphore,
                          BaseType_t *pxHigherPriorityTaskWoken
                      )</pre>
 *
 * <i>Macro</i> to  release a semaphore.  The semaphore must have previously been
 * created with a call to xSemaphoreCreateBinary() or xSemaphoreCreateCounting().
 *
 * Mutex type semaphores (those created using a call to xSemaphoreCreateMutex())
 * must not be used with this macro.
 *
 * This macro can be used from an ISR.
 *
 * @param xSemaphore A handle to the semaphore being released.  This is the
 * handle returned when the semaphore was created.
 *
 * @param pxHigherPriorityTaskWoken xSemaphoreGiveFromISR() will set
 * *pxHigherPriorityTaskWoken to pdTRUE if giving the semaphore caused a task
 * to unblock, and the unblocked task has a priority higher than the currently
 * running task.  If xSemaphoreGiveFromISR() sets this value to pdTRUE then
 * a context switch should be requested before the interrupt is exited.
 *
 * @return pdTRUE if the semaphore was successfully given, otherwise errQUEUE_FULL.
 *
 * Example usage:
 <pre>
 \#define LONG_TIME 0xffff
 \#define TICKS_TO_WAIT        10
 SemaphoreHandle_t xSemaphore = NULL;

 // Repetitive task.
 void vATask( void * pvParameters )
 {
    for( ;; )
    {
        // We want this task to run every 10 ticks of a timer.  The semaphore
        // was created before this task was started.

        // Block waiting for the semaphore to become available.
        if( xSemaphoreTake( xSemaphore, LONG_TIME ) == pdTRUE )
        {
            // It is time to execute.

            // ...

            // We have finished our task.  Return to the top of the loop where
            // we will block on the semaphore until it is time to execute
            // again.  Note when using the semaphore for synchronisation with an
                        // ISR in this manner there is no need to 'give' the semaphore back.
        }
    }
 }

 // Timer ISR
 void vTimerISR( void * pvParameters )
 {
 static uint8_t ucLocalTickCount = 0;
 static BaseType_t xHigherPriorityTaskWoken;

    // A timer tick has occurred.

    // ... Do other time functions.

    // Is it time for vATask () to run?
        xHigherPriorityTaskWoken = pdFALSE;
    ucLocalTickCount++;
    if( ucLocalTickCount >= TICKS_TO_WAIT )
    {
        // Unblock the task by releasing the semaphore.
        xSemaphoreGiveFromISR( xSemaphore, &xHigherPriorityTaskWoken );

        // Reset the count so we release the semaphore again in 10 ticks time.
        ucLocalTickCount = 0;
    }

    if( xHigherPriorityTaskWoken != pdFALSE )
    {
        // We can force a context switch here.  Context switching from an
        // ISR uses port specific syntax.  Check the demo task for your port
        // to find the syntax required.
    }
 }
 </pre>
 * \defgroup xSemaphoreGiveFromISR xSemaphoreGiveFromISR
 * \ingroup Semaphores
 */
#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken )        xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ), ( pxHigherPriorityTaskWoken ) )

示例:中断中释放信号量(通知任务)

// 按键中断服务程序(ISR)
void EXTI0_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    
    // 检查中断标志(硬件相关)
    if (EXTI_GetITStatus(EXTI_Line0) != RESET) {
        // 释放二值信号量(通知等待任务)
        xSemaphoreGiveFromISR(xBinarySemaphore, &xHigherPriorityTaskWoken);
        
        // 清理中断标志
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
    
    // 若需要,触发任务切换(高优先级任务被唤醒时)
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

3.2互斥量的使用方法

互斥量核心用于保护共享资源(确保同一时间只有一个任务访问),使用流程也是 3 步:创建、获取(加锁)、释放(解锁)。

1.创建互斥量

使用 xSemaphoreCreateMutex() 函数创建,返回互斥量句柄(SemaphoreHandle_t)。

// 定义互斥量句柄(保护共享资源,如串口)
SemaphoreHandle_t xMutex;

void vInitMutex(void) {
    // 创建互斥量(初始状态为“有信号”:可被获取)
    xMutex = xSemaphoreCreateMutex();
    
    // 检查创建是否成功
    if (xMutex == NULL) {
        // 错误处理
    }
}

注意:互斥量创建后默认状态为 “有信号”(值 1),表示 “资源可用”。

2.获取互斥量(加锁)

同样使用 xSemaphoreTake() 函数,获取后互斥量变为 “无信号”(值 0),表示 “资源被占用”。 示例:任务访问共享资源(如串口打印)前加锁

// 任务1:使用串口打印
void vTask1(void *pvParameters) {
    for (;;) {
        // 获取互斥量(最多等待100ms,超时则放弃)
        if (xSemaphoreTake(xMutex, pdMS_TO_TICKS(100)) == pdPASS) {
            // 成功获取:安全访问共享资源(串口)
            printf("任务1:正在使用串口...\n");
            vTaskDelay(pdMS_TO_TICKS(50)); // 模拟使用资源
            
            // 用完后释放互斥量(解锁)
            xSemaphoreGive(xMutex);
        } else {
            // 获取失败(超时):处理错误(如重试或放弃)
            printf("任务1:获取互斥量超时!\n");
        }
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

// 任务2:也需要使用串口(与任务1互斥)
void vTask2(void *pvParameters) {
    // 逻辑与任务1相同,省略...
}

3.释放互斥量(解锁)

使用 xSemaphoreGive() 函数,释放后互斥量变为 “有信号”(值 1),表示 “资源可用”。 核心规则:只有获取互斥量的任务才能释放它(所有权特性)。 如果任务 A 获取互斥量后,任务 B 试图释放,会导致未定义行为(可能系统崩溃)。

3.3使用时的核心注意事项

1.二值信号量的注意事项

  • 初始化状态:创建后默认 “无信号”(值 0),需首次调用xSemaphoreGive()才能让xSemaphoreTake()获取成功。

  • 中断中必须用FromISR版本:释放信号量时,ISR 中必须用xSemaphoreGiveFromISR(),不能用普通xSemaphoreGive()(否则可能导致调度错误)。

  • 不适合保护共享资源:二值信号量没有 “所有权” 和 “优先级继承”,若用于互斥,可能导致优先级反转(高优先级任务被低优先级任务阻塞)。

  • 避免长期持有:xSemaphoreTake()获取后,应尽快用xSemaphoreGive()释放,避免其他等待任务长期阻塞。

2.互斥量的注意事项

  • 严格的所有权:必须由获取互斥量的任务释放,其他任务释放会导致错误(FreeRTOS 不检查,可能引发系统异常)。

  • 禁止在 ISR 中使用:互斥量的xSemaphoreTake()可能阻塞(xTicksToWait非 0),而 ISR 不能阻塞;且互斥量的优先级继承机制在 ISR 中无效。

  • 避免嵌套死锁:同一任务多次获取同一个互斥量会导致死锁(第一次获取成功,第二次获取时因互斥量已被自己占用而阻塞,永远无法返回)。

// 错误示例:同一任务嵌套获取互斥量
void vBadTask(void *param) {
    xSemaphoreTake(xMutex, portMAX_DELAY); // 第一次获取成功
    xSemaphoreTake(xMutex, portMAX_DELAY); // 第二次获取:阻塞,死锁!
}
  • 持有时间要短:获取互斥量后,应仅执行 “访问共享资源” 的必要操作,尽快释放,避免其他任务长时间等待。

  • 优先级继承的限制:互斥量的优先级继承只能 “减轻” 优先级反转,不能完全消除。例如,若低优先级任务 L 持有互斥量,高优先级任务 H 等待,L 会被提升到 H 的优先级,但如果此时有中优先级任务 M,M 仍可能被 L(临时高优先级)阻塞。

3.两者共有的注意事项

  • 必须检查返回值:xSemaphoreTake()xSemaphoreGive()的返回值需判断(如超时、释放失败),否则可能因资源未获取 / 未释放导致逻辑错误。

  • 句柄有效性:使用前必须确保信号量 / 互斥量创建成功(句柄非 NULL),否则调用相关函数会导致崩溃。

  • 内存管理:信号量 / 互斥量依赖 FreeRTOS 堆内存,若堆内存不足,创建会失败(返回 NULL),需确保堆大小足够(FreeRTOSConfig.h中的configTOTAL_HEAP_SIZE)。

总结

  • 二值信号量:用于 “同步”(事件通知),无所有权,可在 ISR 中用FromISR版本,注意初始化和及时释放。

  • 互斥量:用于 “互斥”(保护共享资源),有所有权和优先级继承,禁止在 ISR 中使用,严格避免嵌套死锁。

四、怎么选?用场景判断

  • 用二值信号量:当你需要 “通知” 或 “同步” 时。 例 1:按键中断触发后,用 xSemaphoreGiveFromISR() 释放信号量,唤醒等待的任务处理按键事件。 例 2:任务 A 完成数据采集后,释放信号量,通知任务 B 开始处理数据。

  • 用互斥量:当你需要 “保护共享资源” 时。 例 1:多个任务需要向同一个串口打印日志,用互斥量确保同一时间只有一个任务能操作串口。 例 2:多个任务需要读写同一块内存中的配置参数,用互斥量避免数据错乱。

总结

  • 二值信号量:像 “门铃”,谁都可以按(释放),谁都可以听(获取),用于 “通知对方做事”。

  • 互斥量:像 “厕所钥匙”,谁拿到谁用,用完必须自己还,用于 “保证只有一个人用厕所”,还能避免 “急事的人(高优先级)被慢腾腾的人(低优先级)耽误”。

记住核心原则:同步用信号量互斥用互斥量,并始终检查返回值,就能避免大部分使用错误。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐