二值信号量:任务同步的“开关”

二值信号量,名字听着挺唬人,其实说白了就是一个只有两个状态的“开关”。要么是1(有效),要么是0(无效)。在FreeRTOS里,它最核心的用途就是——任务同步。

我刚开始用RTOS那会儿,总觉得信号量是个很玄乎的东西。后来踩了几次坑才明白,二值信号量本质上就是个“通知机制”。一个任务负责“给”,另一个任务负责“等”。就这么简单。

工作原理:就像一把钥匙

想象一下这个场景:你有一把钥匙,这把钥匙只能被一个人拿着。当钥匙在桌上时,你可以拿走它(Take)。当你用完,你把它放回桌上(Give)。

二值信号量就是这把钥匙。它的值只有两个:

  • 1:信号量可用,任务可以获取
  • 0:信号量被占用,任务需要等待

当一个任务调用xSemaphoreTake()时,如果信号量是1,它就立刻拿到,并把值变成0。如果已经是0,任务就会被挂起,直到有人调用xSemaphoreGive()把它变回1。

核心要点:二值信号量不计数。它只记录“有没有”,不记录“有多少”。这和计数信号量有本质区别。

创建与删除:从生到死

创建二值信号量,我习惯用xSemaphoreCreateBinary()这个API。它会在堆上动态分配内存。

SemaphoreHandle_t xBinarySemaphore;

// 创建二值信号量
xBinarySemaphore = xSemaphoreCreateBinary();

// 检查是否创建成功
if (xBinarySemaphore != NULL) {
    // 创建成功,可以用了
} else {
    // 内存不足,创建失败
    // 我曾经在这里吃过亏,堆大小没配够
}

创建时,信号量的初始值是0。这意味着第一个调用xSemaphoreTake()的任务会阻塞,直到有人先Give一次。

删除就简单了:

// 删除信号量
vSemaphoreDelete(xBinarySemaphore);
xBinarySemaphore = NULL;  // 我习惯置空,防止野指针

注意:删除信号量时,如果有任务正在等待这个信号量,这些任务会永远等下去。我曾经在项目里犯过这个错,调试了一整天才发现是信号量被提前删了。

Give与Take操作:给与拿的艺术

这两个操作是二值信号量的灵魂。咱们一个一个说。

Give(给)

xSemaphoreGive()用于释放信号量。如果当前信号量是0,调用后变成1。如果有任务在等待,这个任务会被立即唤醒。

// 从中断中释放信号量
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(xBinarySemaphore, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);

嗯,这里要注意:中断里只能用GiveFromISR版本。我见过有人直接在中断里调xSemaphoreGive(),结果系统直接崩了。

Take(拿)

xSemaphoreTake()用于获取信号量。你可以设置超时时间:

// 无限等待,直到拿到信号量
xSemaphoreTake(xBinarySemaphore, portMAX_DELAY);

// 等待100个tick,拿不到就超时返回
if (xSemaphoreTake(xBinarySemaphore, pdMS_TO_TICKS(100)) == pdTRUE) {
    // 拿到了,干活
} else {
    // 超时了,处理超时逻辑
}

我的习惯:能用超时就用超时,别用portMAX_DELAY。万一信号量永远不来,任务就卡死了。调试的时候特别痛苦。

典型应用场景:任务同步

二值信号量最常见的用法就是“一个任务等另一个任务干完活”。我给你举个例子:

假设你有两个任务:一个负责采集数据(生产者),一个负责处理数据(消费者)。采集任务必须等处理任务完成上一批数据,才能开始下一批采集。

// 生产者任务
void vProducerTask(void *pvParameters) {
    for (;;) {
        // 采集数据
        collectData();
        
        // 通知消费者:数据准备好了
        xSemaphoreGive(xBinarySemaphore);
        
        // 等消费者处理完
        xSemaphoreTake(xBinarySemaphore, portMAX_DELAY);
    }
}

// 消费者任务
void vConsumerTask(void *pvParameters) {
    for (;;) {
        // 等数据
        xSemaphoreTake(xBinarySemaphore, portMAX_DELAY);
        
        // 处理数据
        processData();
        
        // 通知生产者:我处理完了,你可以继续了
        xSemaphoreGive(xBinarySemaphore);
    }
}

你看,这两个任务通过一个二值信号量,实现了“你等我,我等你”的同步。这就是所谓的“握手协议”。

避坑指南:我曾经在项目里用二值信号量做任务同步,结果发现两个任务都卡死了。查了半天才发现——我忘了在创建信号量后先Give一次。创建时信号量默认是0,两个任务都在等对方Give,结果谁也等不到。死锁了。

什么时候用二值信号量?

我总结了几条经验:

  • 任务同步:一个任务等另一个任务完成某个动作
  • 中断与任务同步:中断里Give,任务里Take
  • 一次性事件:比如外设初始化完成,通知主任务开始工作

但要注意,二值信号量不适合做资源管理。如果你有多个任务要访问同一个资源,用互斥量更合适。二值信号量没有优先级继承机制,容易出优先级反转的问题。

警告:别把二值信号量当互斥量用。我见过有人这么干,结果高优先级任务被低优先级任务堵死,系统响应时间直接爆炸。

好了,二值信号量就聊到这儿。说白了它就是一把钥匙,谁拿到谁干活。用好了是利器,用不好就是坑。你想想看,是不是这个理儿?