FreeRTOS与信号量(四)
FreeRTOS与硬件定时器中断(二)-CSDN博客
上面这篇文章中我已经介绍过了二值信号量的使用方法。
在后续我会专门出一期介绍信号量与互斥锁、与事件标志组等之间的关系,以及三者之间在现实场景开发中是如何选择并且协调使用的文章。
文章(二)已经介绍过二值信号量的使用方法,这篇文章我只简单补充一下计数信号量和互斥信号量的内容,以及介绍二值信号量与互斥信号量的关系。
一、信号量介绍
在 RTOS(实时操作系统)中,信号量(Semaphore) 是一种用于协调多任务协作的机制,本质上是一个 “计数器”+“等待队列” 的组合,核心作用是解决任务同步和资源共享问题。
1. 信号量核心作用是解决任务同步,那么,什么是任务同步?
“任务同步” 指的是:通过信号量协调两个任务(或事件)的执行时序,让一个任务的动作(如定时器触发的事件)成为另一个任务执行特定操作的 “开关”,确保两者在时间上 “步调一致”。
简单说,就是 “你不触发,我就不执行;你一触发,我就立即响应”—— 这就是 “同步” 的核心;而 “触发” 则是指通过信号量的 “释放” 动作,启动后续任务的操作。
在文章(二)中使用的二值信号量例程就是例子,就是解决了任务的同步触发,实现了在不影响任务调度运行的条件下,使用硬件定时器中断。
2. 信号量的三种类型
二值信号量:状态只有 0 或 1,像 “开关”,用于任务间同步(比如 A 做完通知 B 开始)。
计数信号量:计数器范围 0 到 N,像 “资源计数器”,控制最多 N 个任务同时使用有限资源(如 5 个缓冲区)。
互斥信号量:特殊二值信号量,带优先级继承机制,专门保护独占资源(如传感器),避免多个任务同时访问,还能解决优先级反转问题。
二、信号量例程
接下来,我将用相同的场景,用三个例程,介绍三个类型的信号量
1. 二值信号量例程
场景说明:二值信号量例程的和文章(二)的例程一致
定义结构体变量
#include "FreeRTOS.h"
#include "task.h"
#include <stdio.h>
#include "key.h"
#include "usart.h"#include "semphr.h"//信号量需引用此头文件SemaphoreHandle_t xBinarySemaphore;//定义二值信号量句柄
创建两个任务
void Task1(void *pvParams)// 任务1:按键检测,释放信号量(生产者)
{while(1){uint8_t KeyNum = Key_GetNum(0); // 获取按键状态printf("生产者准备释放信号量\r\n");if(KeyNum == 1)//按键按下{if(xSemaphoreGive(xBinarySemaphore) == pdTRUE)// 释放信号量(0->1){printf("信号量释放成功\r\n");}else{printf("信号量释放失败(当前已为1)\r\n");}}vTaskDelay(1000); // 1秒检测一次按键}
}
void Task2(void *pvParams)// 任务2:等待信号量,执行消费操作(消费者)
{while(1){// 等待信号量(1->0),永久阻塞直到获取到信号量xSemaphoreTake(xBinarySemaphore, portMAX_DELAY);// 获取到信号量后执行消费操作printf("消费者接收到信号量,开始消费\r\n");vTaskDelay(500); // 模拟消费过程(500ms)printf("消费者消费完成\r\n"); }
}
main函数
int main(void)
{// 硬件初始化USART_Init(); Key_Init(); // 创建二值信号量(初始0)xBinarySemaphore = xSemaphoreCreateBinary();// 创建任务xTaskCreate(Task1, "Task1", 128, NULL, 2, NULL); // 优先级2xTaskCreate(Task2, "Task2", 128, NULL, 1, NULL); // 优先级1vTaskStartScheduler();return 0;
}
2.计数信号量例程
场景说明:计数信号量用于管理多个相同资源(如允许累计多次按键事件,消费者按顺序处理)。此处设置最大计数为 5(最多累计 5 次按键),初始计数为 0,每次按键释放信号量时计数 + 1,消费者每次获取信号量时计数 - 1。
定义结构体变量
#include "FreeRTOS.h"
#include "task.h"
#include <stdio.h>
#include "key.h"
#include "usart.h"#include "semphr.h"//信号量需引用此头文件SemaphoreHandle_t xCountingSemaphore;// 定义计数信号量句柄
创建两个任务
void Task1(void *pvParams)// 任务1:按键检测,释放计数信号量(累计事件)
{while(1){uint8_t KeyNum = Key_GetNum(0);printf("生产者准备释放计数信号量\r\n");if(KeyNum == 1){// 释放信号量(计数加多一个,最大不超过5)if(xSemaphoreGive(xCountingSemaphore) == pdTRUE){// 获取当前信号量计数(调试用)UBaseType_t uxCount = uxSemaphoreGetCount(xCountingSemaphore);printf("信号量释放成功,当前计数: %u\r\n", uxCount);}else{printf("信号量释放失败(已达最大计数5)\r\n");}}vTaskDelay(1000); // 1秒检测一次按键}
}
void Task2(void *pvParams)// 任务2:等待计数信号量,逐个处理事件
{while(1){// 等待信号量(计数减少一个,若计数为0则阻塞)xSemaphoreTake(xCountingSemaphore, portMAX_DELAY);// 获取当前剩余计数(调试用)UBaseType_t uxCount = uxSemaphoreGetCount(xCountingSemaphore);printf("消费者接收到信号量,开始处理(剩余计数: %u)\r\n", uxCount);vTaskDelay(500); // 模拟处理过程printf("消费者处理完成\r\n"); }
}
main函数
int main(void)
{USART_Init();Key_Init();// 创建计数信号量(最大计数5,初始计数0)xCountingSemaphore = xSemaphoreCreateCounting(5, 0);// 创建任务(优先级:Task1=2,Task2=1)xTaskCreate(Task1, "Task1", 128, NULL, 2, NULL);xTaskCreate(Task2, "Task2", 128, NULL, 1, NULL);vTaskStartScheduler();return 0;
}
3.互斥信号量例程
场景说明:互斥信号量用于保护共享资源(如全局变量),防止多个任务同时访问。此处用全局变量g_key_press_count
记录按键次数,Task1 更新变量,Task2 读取变量,通过互斥信号量保证访问互斥。
定义结构体变量
#include "FreeRTOS.h"
#include "task.h"
#include <stdio.h>
#include "key.h"
#include "usart.h"#include "semphr.h"//信号量需引用此头文件SemaphoreHandle_t xMutexSemaphore;// 定义互斥信号量句柄
uint32_t g_key_press_count = 0;// 共享资源:按键按下次数计数器
创建两个任务
void Task1(void *pvParams)// 任务1:按键检测,更新共享资源(需互斥保护)
{while(1){uint8_t KeyNum = Key_GetNum(0);printf("生产者准备访问共享资源\r\n");if(KeyNum == 1){// 获取互斥信号量(若被占用则阻塞,最长等待100ms)if(xSemaphoreTake(xMutexSemaphore, 100 / portTICK_PERIOD_MS) == pdTRUE){// 临界区:安全更新共享资源g_key_press_count++;printf("共享资源更新:按键次数 = %u\r\n", g_key_press_count);// 释放互斥信号量(必须由获取它的任务释放)xSemaphoreGive(xMutexSemaphore);}else{printf("获取互斥信号量失败(资源被占用)\r\n");}}vTaskDelay(1000);}
}
void Task2(void *pvParams)// 任务2:读取共享资源(需互斥保护)
{while(1){// 获取互斥信号量(永久阻塞直到获取)xSemaphoreTake(xMutexSemaphore, portMAX_DELAY);// 临界区:安全读取共享资源printf("消费者读取共享资源:当前按键次数 = %u\r\n", g_key_press_count);vTaskDelay(500); // 模拟处理// 释放互斥信号量xSemaphoreGive(xMutexSemaphore);printf("消费者释放互斥信号量\r\n");vTaskDelay(100); // 短暂延迟,让Task1有机会获取资源}
}
main函数
int main(void)
{USART_Init();Key_Init();// 创建互斥信号量(初始值为1,代表资源可用)xMutexSemaphore = xSemaphoreCreateMutex();// 创建任务(优先级:Task1=2,Task2=1)xTaskCreate(Task1, "Task1", 128, NULL, 2, NULL);xTaskCreate(Task2, "Task2", 128, NULL, 1, NULL);vTaskStartScheduler();return 0;
}
三、二值信号量与互斥信号量的关系
互斥信号量是特殊的二值信号量,二者都只有 “0” 和 “1” 两种状态,互斥信号量的设计目的主要是为了解决资源共享的问题。
二值信号量:用于任务间同步(事件通知),比如 “一个任务触发事件,另一个任务等待事件”。
互斥信号量:用于保护共享资源,确保同一时间只有一个任务访问资源(避免并发冲突)。
在互斥信号量例程中:
-
共享资源就是全局变量
g_key_press_count
(记录按键次数)。 -
Task1 需要更新这个变量,Task2 需要读取这个变量,两者都可能并发访问。
-
互斥信号量通过
xSemaphoreTake()
和xSemaphoreGive()
“上锁” 和 “解锁”,确保同一时间只有一个任务能操作g_key_press_count
(比如避免 Task1 更新到一半时,Task2 读取到不完整的数据)
仔细观察这两个例程:
二值信号量Task1任务与互斥信号量Task1任务对比


可以明显看到,二值信号量只需释放信号量就行了,而互斥信号量需要先获取信号量,然后再释放信号量。互斥信号量的xSemaphoreTake函数与xSemaphoreGive函数中间的临界区保护了全局变量的g_key_press_count。
二值信号量Task2任务与互斥信号量Task2任务对比


同理,互斥信号量的xSemaphoreTake函数与xSemaphoreGive函数中间的临界区保护了printf打印的全局变量g_key_press_count。
二值信号量的初始值与互斥信号量的初始值
值得注意,二值信号量的初始值是0,互斥信号量的初始值是1。
四、二值信号量与互斥信号量总结
互斥信号量除了能保护共享资源,还能解决二值信号量不能解决的优先级反转的问题(后续文章会介绍优先级反转)。
如何选择二值信号量或者互斥信号量?
若需要传递事件(如 “按键按下通知处理任务”“中断完成通知应用任务”),用二值信号量,它更轻量,无所有权限制。
若需要保护共享资源(如全局变量、硬件寄存器、文件),用互斥信号量,它通过所有权和优先级继承机制确保资源安全访问。
结合例程来看:二值信号量例程的核心是 “按键事件同步”,互斥信号量例程的核心是 “按键计数变量的安全访问”