二进制信号量定义:
c
#include "semphr.h" // SemaphoreHandle_t
// 二进制信号量(Binary Semaphore)
SemaphoreHandle_t xBinarySemaphore;
void vTaskA(void *pvParameters) {
while(1) {
// 获取信号量
if(xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdTRUE) {
printf("TaskA: Got semaphore, doing work...\r\n");
vTaskDelay(500 / portTICK_PERIOD_MS);
xSemaphoreGive(xBinarySemaphore); // 释放
}
}
}
使用示例:
c
/**
* @brief 主应用函数:二进制信号量
*
* @note None
* @param None
* @return None
* @warning 此函数不会返回,内部包含无限循环
* @remark 通过函数指针从main()调用,支持灵活的启动架构
*/
void Current_Program4(void)
{
LED_Init(); // 初始化LED设备
UART1_Config(); // UART1串口初始化
xBinarySemaphore = xSemaphoreCreateBinary();
if (xBinarySemaphore == NULL) {
// 创建失败(堆内存不足)
}
// 创建后信号量不可用(值为0),通常需要先 Give 一次
xSemaphoreGive(xBinarySemaphore); // 使其变为可用
//xSemaphoreGive(xBinarySemaphore); // 释放信号量
xTaskCreate(vTaskA, "vTaskA", 128, NULL, 1, NULL);
// 启动调度器(永远不会返回)
vTaskStartScheduler();
// 理论上程序不会执行到这里,但为了安全,可以加一个死循环
while(1){
}
}
下面详细解释二进制信号量(Binary Semaphore)代码所涉及的核心知识点。二进制信号量是 FreeRTOS 中用于任务同步 和资源共享控制的轻量级机制,可以把它看作一个只能取 0 或 1 的计数器(0 表示不可用,1 表示可用)。
一、二进制信号量的本质
- 计数范围:0 或 1。
- 操作 :
- Take(获取):如果信号量值为 1,则将其减为 0 并立即返回成功;如果为 0,则任务可阻塞等待。
- Give(释放):将信号量值从 0 变为 1(若已有任务等待该信号量,则唤醒其中一个)。
- 与队列的区别:队列可以携带数据,而信号量只传递"事件发生"或"资源可用"这一布尔信息,没有数据负载。
二、代码中涉及的 API 函数
1. xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait)
- 作用:获取(消耗)一个信号量。
- 参数 :
xSemaphore:信号量句柄。xTicksToWait:若信号量不可用,最多等待多少个 tick。portMAX_DELAY表示无限等待。
- 返回值 :
pdTRUE:成功获取信号量。pdFALSE:超时仍未获取到。
- 注意 :在中断服务函数中应使用
xSemaphoreTakeFromISR()。
2. xSemaphoreGive(SemaphoreHandle_t xSemaphore)
- 作用:释放(给出)一个信号量,使其值从 0 变为 1。
- 参数:信号量句柄。
- 返回值 :
pdTRUE:释放成功。pdFALSE:释放失败(例如信号量已经是 1,但一般不会失败)。
- 注意 :在中断中应使用
xSemaphoreGiveFromISR()。
三、代码运行流程分析
c
SemaphoreHandle_t xBinarySemaphore; // 全局句柄
void vTaskA(void *pvParameters) {
while(1) {
// 1. 尝试获取信号量(若为 0 则阻塞)
if(xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdTRUE) {
printf("TaskA: Got semaphore, doing work...\r\n");
// 2. 模拟占用资源的工作(持有信号量期间)
vTaskDelay(500 / portTICK_PERIOD_MS);
// 3. 工作完成,释放信号量
xSemaphoreGive(xBinarySemaphore);
}
}
}
执行逻辑:
- 假设初始信号量值为 1(可用)。
- 任务 A 调用
xSemaphoreTake成功,将信号量减为 0,然后打印并延时 500ms。 - 在延时期间,任务 A 仍然持有信号量(信号量值为 0),其他任何想获取该信号量的任务都会进入阻塞状态。
- 延时结束后,任务 A 调用
xSemaphoreGive,信号量恢复为 1。此时如果有其他任务正在等待该信号量,则会被唤醒并获得信号量。
四、典型应用场景
1. 任务同步(Task Synchronization)
一个任务给出信号量,另一个任务获取它,表示"事件已发生"。
- 例如:中断服务程序给出信号量,后台任务获取后处理数据。
2. 资源互斥(Resource Mutex)------ 有缺陷
上述代码将二进制信号量用于互斥访问共享资源(如打印机、缓冲区)。但二进制信号量不提供优先级继承 ,可能导致优先级反转 问题。对于互斥,FreeRTOS 专门提供了 xMutex(互斥量),支持优先级继承。
3. 单次通知
如果信号量初始为 0,一个任务 Give,另一个任务 Take,实现一次性通知。
五、二进制信号量 vs 互斥量
| 特性 | 二进制信号量 | 互斥量(Mutex) |
|---|---|---|
| 用途 | 同步、简单标志 | 互斥访问共享资源 |
| 优先级继承 | ❌ 不支持 | ✅ 支持(防止优先级反转) |
| 谁可以释放 | 任何任务/中断 | 只能由获取它的任务释放 |
| 递归获取 | ❌ 不能重复获取 | ✅ 支持(同一个任务可多次获取,需释放相同次数) |
| 创建函数 | xSemaphoreCreateBinary() |
xSemaphoreCreateMutex() |
因此 :如果 vTaskA 中的代码是在保护共享资源(如写一个全局变量),应使用互斥量而非二进制信号量。若只是用作任务间"干完活通知我"之类的一次性同步,二进制信号量就已足够。
六、注意事项
-
初始状态:
xSemaphoreCreateBinary()创建的信号量初始值为 0。如果希望一开始就可用,创建后需要先调用一次xSemaphoreGive()。- 而
xSemaphoreCreateMutex()创建的互斥量初始值为 1(可用)。
-
死锁风险:
- 如果获取信号量的任务在释放前被无限期挂起(如永循环),其他所有等待该信号量的任务都将永远阻塞。
-
中断中使用:
- 中断服务程序中只能使用
xSemaphoreGiveFromISR(),并且不能阻塞(不能调用Take带超时)。 - 示例给出信号量后,应检查
pxHigherPriorityTaskWoken并做任务切换。
- 中断服务程序中只能使用
-
所有权:
- 二进制信号量没有"所有者"概念,任何任务(包括中断)都可以释放它,这容易造成意外释放。互斥量则只有持有者才能释放,更安全。
-
递归获取:
- 二进制信号量不支持递归获取。同一个任务再次
Take同一个二进制信号量会阻塞(除非先Give)。而互斥量支持递归。
- 二进制信号量不支持递归获取。同一个任务再次
七、完善示例:正确的互斥使用(改用互斥量)
c
SemaphoreHandle_t xMutex;
void vTaskA(void *pvParameters) {
while(1) {
if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
// 临界区:访问共享资源
printf("TaskA: Accessing shared resource...\r\n");
vTaskDelay(500 / portTICK_PERIOD_MS);
xSemaphoreGive(xMutex);
}
}
}
创建:
c
xMutex = xSemaphoreCreateMutex();
八、总结
- 二进制信号量是 FreeRTOS 中最简单的同步工具,状态只有 0/1。
Take减少计数,Give增加计数。- 适用于任务间同步 (如中断通知任务)和简单的资源控制(注意优先级反转风险)。
- 对于保护共享资源的互斥场景,应优先使用
xSemaphoreCreateMutex()创建的互斥量。
你给出的代码展示了典型的信号量获取与释放模式,只要理解其适用边界,就能灵活运用。
附录
