目录
(1)二值信号量的创建 :xSemaphoreCreateBinary()
(4)中断中释放二值信号量 :xSemaphoreGiveFromISR()
(1)创建计数信号量 xSemaphoreCreateCounting
(1)创建互斥信号量 xSemaphoreCreateMutex()
一.信号量基础概念
信号量是一种任务间同步机制,用于协调多个任务或中断对共享资源的访问,防止资源冲突。它就像一个“通行证”,拿到信号量 = 拿到通行证,可以进入访问资源;如果没拿到,那就等待通行证释放。
信号量底层其实是使用 FreeRTOS 的队列(Queue)机制来实现的,我们可以把它理解为一种 “特殊配置的队列”,但是信号量本身不承载任何“有效数据”,它只表示一种状态(比如“我来了”“你可以动了”)。所以信号量不需要传输数据,只需要传达“信号”就行了;
FreeRTOS提供了3种主要类型的信号量:二值信号量、计数信号量、互斥信号量,下面具体展开了解;
二. 二值信号量(Binary Semaphore)
1.基础概念
它就像一个“标志位”或“开关”,只能是 0(无信号) 或 1(有信号)。就像一个盒子,只要有人放了东西(信号)(xSemaphoreGive()),它就是非空状态,但是有人把盒子里的东西拿走了(xSemaphoreTake()),这个盒子就是空的状态,所以这个盒子只有“空”或“非空”两个状态,代表这个房间能不能进。这个牌子就是“二值信号量”。二值信号量通常用于任务间同步,或者中断唤醒任务。
2.二值信号量相关的常用函数
(1)二值信号量的创建 :xSemaphoreCreateBinary()
初始状态为“空”,如果任务创建后要立刻执行一次,需要手动Give一次(xSemaphoreGive() ),否则一开始就会卡住;
如果信号量完全依赖于中断或者其他任务来Give,那无需手动初始化;
-
函数原型
SemaphoreHandle_t xSemaphoreCreateBinary(void);
-
返回值:
-
成功:返回一个信号量句柄(handle);
-
失败:返回 NULL(堆内存不足等)。
-
-
示例代码
SemaphoreHandle_t xSemaphore;
xSemaphore = xSemaphoreCreateBinary();//返回一个指向信号量对象的指针,成功时返回非 NULL,失败时返回 NULL
//现在假设任务创建成功,需要立刻执行一次,则需要手动give一次:
if (xSemaphore != NULL) {
xSemaphoreGive(xSemaphore); // 给一个初始信号量
}
(2)二值信号量的释放 :xSemaphoreGive()
释放一个信号量,通常用来通知其他任务或同步。
-
函数原型:
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
//参数1 xSemaphore:信号量句柄;
-
返回值
-
pdTRUE:成功释放;
-
pdFALSE:失败(比如队列满了);
-
-
示例代码
if (xSemaphoreTake(xSemaphore, pdMS_TO_TICKS(1000)))
{
// 成功拿到信号量,执行操作
} else
{
// 超时了,信号量没人给
}
(3)二值信号量的获取 :xSemaphoreTake()
-
函数原型
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
//参数1 xSemaphore:信号量句柄;
//参数2 xTicksToWait:最大等待时间,单位是tick
-
返回值
-
pdTRUE:成功获取;
-
pdFALSE:超时未获取。
-
-
示例代码
if (xSemaphoreTake(xSemaphore, pdMS_TO_TICKS(1000))) {
// 成功拿到信号量,执行操作
} else {
// 超时了,信号量没人给
}
二值型号量创建、释放与获取粗略的用法顺序大概是以下:
//1.创建二值信号量
SemaphoreHandle_t xSemaphore = xSemaphoreCreateBinary();
//2.获取信号量(拿“通行证”),如果成功获取信号量,任务立即获取成功;
//如果没获取成功,任务就会阻塞(挂起自己)等待直到可用或超时;
xSemaphoreTake(xSemaphore, TickType_t xTicksToWait);
//3.释放信号量(给“通行证”),通常是另一个任务或中断服务程序调用,表示资源/事件已准备好;
xSemaphoreGive(xSemaphore);
完整示例:
//任务:实现任务B每3秒通知一次任务A,任务A收到通知后立即执行自己的工作;
SemaphoreHandle_t xSemaphore;
void TaskA(void *pvParameters) {
while (1) {
if (xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE) {
//xSemaphoreTake拿走一个信号;
printf("Task A: Received signal! Start working...\n");
}//portMAX_DELAY 表示无限等下去;
}
}
void TaskB(void *pvParameters) {
while (1) {
vTaskDelay(pdMS_TO_TICKS(3000)); // 等 3 秒
xSemaphoreGive(xSemaphore); // 给一个信号,从而通知 Task A
printf("Task B: Sent signal to Task A\n");
}
}
void app_main() {
xSemaphore = xSemaphoreCreateBinary();//二值信号量的创建
xTaskCreate(TaskA, "TaskA", 2048, NULL, 2, NULL);//任务A创建
xTaskCreate(TaskB, "TaskB", 2048, NULL, 2, NULL);//任务B创建
}
-
Q&A:give了之后需要手动重置信号量为空吗?
-
二值信号量的使用流程是:Give → Take
-
xSemaphoreGive():让信号量“可用”;
-
xSemaphoreTake():消耗信号量 ;
-
以上这2个函数是配套使用的,给了信号量之后,要有任务调用 xSemaphoreTake() 来“消费”它;一旦被 Take,信号量状态从有→ 空,接下来就必须等待下一次 Give 才能再次使用;
-
-
Q&A:是谁要获取信号量?又是谁要释放信号量呢?
-
原则就是谁想要干活就要先“获取”(take)信号量;谁觉得别人可以开始干活了,就“释放”(give)信号量。
-
(4)中断中释放二值信号量 :xSemaphoreGiveFromISR()
由于中断里不能用 xSemaphoreGive(),所以这个是专门为**中断服务程序(ISR)**设计的版本,用来从中断中释放信号量。
xSemaphoreGiveFromISR() 是非阻塞的,在中断里不能等待!
xSemaphoreGiveFromISR()必须配合 portYIELD_FROM_ISR() 使用,确保中断退出时能切换到高优先级任务。
另外,需要注意,中断的本质是为了快速响应事件,比如按钮按下、串口收到数据,中断服务程序(ISR)执行时间必须非常短,所以与中断相关函数中只有Give函数,不能有等待接收的函数,所以,与中断有关的函数中,这里仅有xSemaphoreGiveFromISR(),不可以在中断中使用xSemaphoreTake();
如果你在中断中等待信号量(比如 xSemaphoreTake()),那么中断将会阻塞,而阻塞中断是非常严重的设计错误,可能会导致系统卡死或错过其他中断。
-
函数原型:
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken);
//参数1 xSemaphore:要给的信号量句柄
//参数2 pxHigherPriorityTaskWoken:用来通知 RTOS:是否需要在中断退出后立即进行一次任务切换;
//如果信号量触发导致某个任务解除阻塞,且解除阻塞的这个某任务优先级高于当前系统正在运行的任务,
//则 xSemaphoreGiveFromISR() 会将 pxHigherPriorityTaskWoken 设置为 pdTRUE,
//表示如果 xSemaphoreGiveFromISR() 将此值设置为 pdTRUE,则应在退出中断之前请求上下文切换,执行任务优先级更高的这个某任务;
-
返回值:
-
pdTRUE:成功释放;
-
pdFALSE:释放信号量失败。
-
-
示例代码
// 声明信号量
SemaphoreHandle_t xSemaphore;//句柄
// 中断服务程序(如 GPIO 中断)
void IRAM_ATTR gpio_isr_handler(void* arg)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 从中断中 Give 信号量
xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
// 如果唤醒了高优先级任务,要求立即切换
if (xHigherPriorityTaskWoken == pdTRUE) {
portYIELD_FROM_ISR();
}
}
(5)删除信号量:vSemaphoreDelete()
释放信号量占用的系统资源。当信号量不再需要时,应调用此函数进行清理,以防内存泄漏。
-
函数原型:
void vSemaphoreDelete(SemaphoreHandle_t xSemaphore);
//参数 xSemaphore:创建二值信号量时的句柄,相当于信号量的“身份证”或“控制指针”。
//无返回值(void 类型);
-
使用场景
1.动态创建的信号量(xSemaphoreCreateBinary() 或 xSemaphoreCreateMutex() 等)在不再使用时,应该手动删除;
2.比如某些临时功能、初始化流程中用完一次信号量后,及时释放资源;
3.【注意】静态信号量(使用 xSemaphoreCreateBinaryStatic() 创建的)不应使用这个函数删除。
-
示例代码
SemaphoreHandle_t xSemaphore;
void some_task(void *pvParameters)
{
// 创建信号量
xSemaphore = xSemaphoreCreateBinary();
if (xSemaphore == NULL) {
printf("信号量创建失败!\n");
vTaskDelete(NULL);
}
// 给一次信号量,让任务可以继续执行
xSemaphoreGive(xSemaphore);
// 使用信号量
if (xSemaphoreTake(xSemaphore, pdMS_TO_TICKS(1000))) {
printf("任务成功获取信号量\n");
}
// 使用完后删除
vSemaphoreDelete(xSemaphore);
// 删除任务
vTaskDelete(NULL);
}
(6)综合示例
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "driver/gpio.h"
#include "esp_log.h"
#define BUTTON_GPIO 0 // 假设按键接在 GPIO0
SemaphoreHandle_t xSemaphore = NULL;
// 中断服务函数
void IRAM_ATTR gpio_isr_handler(void* arg)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 给信号量,唤醒任务
xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
// 若需要上下文切换
if (xHigherPriorityTaskWoken == pdTRUE) {
portYIELD_FROM_ISR();
}
}
// 任务函数:等待按键信号量,执行操作
void sensor_task(void *arg)
{
while (1)
{
// 等待信号量,有效期为无限等待
if (xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE)
{
// 模拟读取传感器
ESP_LOGI("SENSOR", "读取传感器中...");
vTaskDelay(pdMS_TO_TICKS(500)); // 模拟耗时
// 模拟上传数据
ESP_LOGI("SENSOR", "上传数据中...");
vTaskDelay(pdMS_TO_TICKS(500));
ESP_LOGI("SENSOR", "操作完成,等待下一次中断触发...");
}
}
}
void app_main(void)
{
// 创建二值信号量
xSemaphore = xSemaphoreCreateBinary();
if (xSemaphore == NULL)
{
ESP_LOGE("MAIN", "创建信号量失败!");
return;
}
// 配置 GPIO 作为输入(上拉),设置中断
gpio_config_t io_conf = {
.pin_bit_mask = 1ULL << BUTTON_GPIO,
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.intr_type = GPIO_INTR_NEGEDGE // 下降沿触发
};
gpio_config(&io_conf);
// 安装中断服务并注册
gpio_install_isr_service(0);
gpio_isr_handler_add(BUTTON_GPIO, gpio_isr_handler, NULL);
// 创建任务
xTaskCreate(sensor_task, "SensorTask", 2048, NULL, 10, NULL);
// 手动 give 一次,首次运行后可以立即进入任务(可选)
xSemaphoreGive(xSemaphore);
}
三. 计数信号量(Counting Semaphore)
计数信号量是一种可以计数的信号量,用于管理“多个资源”的使用权限,或者“统计”多个事件的发生次数。它不像二值信号量只能是 0 或 1,而是可以有 多个“许可”数量。它可用于计数资源的使用情况,比如管理多个相同资源(如 N 个串口缓冲区、连接数、设备通道等)的访问。
1.核心原理
-
初始化时设置一个“可用资源数量”(最大计数值)。
-
每次任务调用 xSemaphoreTake() 时,计数值减 1。
-
每次调用 xSemaphoreGive() 时,计数值加 1。
-
当计数值为 0 时,再 Take() 的任务将被阻塞。
所以,从本质上看,它和二值信号量一样,底层是用队列实现的。
2.常用函数
(1)创建计数信号量 xSemaphoreCreateCounting
-
函数原型
SemaphoreHandle_t xSemaphoreCreateCounting(
UBaseType_t uxMaxCount, // 信号量最大计数值
UBaseType_t uxInitialCount // 初始可用资源数量
);
-
示例
SemaphoreHandle_t xSemaphore;
void app_main() {
// 创建最大为5、初始为2的计数信号量
xSemaphore = xSemaphoreCreateCounting(5, 2);
if (xSemaphore == NULL) {
printf("信号量创建失败!\n");
}
}
(2)获取计数信号量 xSemaphoreTake
- 从计数信号量中“获取”一个资源,也就是将信号量计数减1。
-
如果当前信号量计数大于0,函数立即成功返回,表示成功获取资源。
-
如果信号量计数为0,表示当前没有可用资源,调用该函数的任务会阻塞(等待)直到信号量计数变为大于0(即有资源释放),或者超时。
(3)释放计数信号量 xSemaphoreGive
释放一个资源,即使信号量计数加1。释放资源后,系统会检查是否有阻塞任务等待该信号量,如果有,唤醒优先级最高的任务。
-
示例
//有3台打印机共享给多个打印任务使用,每个任务打印时需要占用一台打印机;
//打印完成后释放打印机供其他任务使用。打印机资源有限,因此必须用计数信号量来管理访问。
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include <stdio.h>
#define PRINT_JOB_COUNT 5
// 创建计数信号量,代表3台打印机资源
SemaphoreHandle_t xPrinterSemaphore;
void print_task(void* arg) {
int task_id = (int)arg;
for (int i = 0; i < PRINT_JOB_COUNT; i++) {
printf("Task %d: Waiting for printer...\n", task_id);
// 请求一个打印机,等待最多1000 tick
if (xSemaphoreTake(xPrinterSemaphore, pdMS_TO_TICKS(1000)) == pdTRUE) {
printf("Task %d: Got a printer, printing job %d\n", task_id, i + 1);
// 模拟打印时间
vTaskDelay(pdMS_TO_TICKS(500));
printf("Task %d: Finished printing job %d, releasing printer\n", task_id, i + 1);
// 释放打印机资源
xSemaphoreGive(xPrinterSemaphore);
} else {
printf("Task %d: Timeout waiting for printer for job %d\n", task_id, i + 1);
}
// 等待一段时间后准备下一次打印
vTaskDelay(pdMS_TO_TICKS(200));
}
vTaskDelete(NULL); // 任务完成后自杀
}
void app_main(void) {
// 初始化计数信号量,3个打印机资源,初始可用3个
xPrinterSemaphore = xSemaphoreCreateCounting(3, 3);
if (xPrinterSemaphore == NULL) {
printf("Failed to create printer semaphore\n");
return;
}
// 创建多个打印任务,模拟多个任务共享3台打印机
for (int i = 1; i <= 5; i++) {
char task_name[16];
snprintf(task_name, sizeof(task_name), "PrintTask%d", i);
xTaskCreate(print_task, task_name, 2048, (void*)i, 2, NULL);
}
}
(4)中断中释放计数信号量 :xSemaphoreGiveFromISR()
释放信号量计数+1,同时可以通知系统是否需要从ISR直接切换任务
// 创建计数信号量,最大计数5,初始资源数5
SemaphoreHandle_t xSemaphore = xSemaphoreCreateCounting(5, 5);
void task(void* arg) {
while(1) {
// 请求一个资源,等待最多100个tick
if (xSemaphoreTake(xSemaphore, 100)) {
// 成功获取资源,执行任务代码
do_something();
// 任务结束释放资源
xSemaphoreGive(xSemaphore);
} else {
// 超时没获得资源,处理超时逻辑
handle_timeout();
}
}
}
// 中断服务函数释放资源示例
void IRAM_ATTR isr_handler(void* arg) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
if(xHigherPriorityTaskWoken) {
portYIELD_FROM_ISR();
}
}
四. 互斥信号量(Mutex Semaphore)
1. 基本概念
-
互斥信号量是一种特殊的二值信号量,设计目的是防止多个任务同时访问共享资源(如共享变量、外设等)。
-
它提供了优先级继承机制,用来解决任务优先级反转问题(Priority Inversion)。
-
互斥信号量必须由同一个任务“拿(take)”和“放(give)”,否则会产生错误。
2.常用函数
(1)创建互斥信号量 xSemaphoreCreateMutex()
-
函数原型
SemaphoreHandle_t xSemaphoreCreateMutex(void);//无参数
-
返回值:
-
成功:返回互斥信号量句柄(SemaphoreHandle_t 类型)
-
失败:返回 NULL(比如内存不足)
-
示例
SemaphoreHandle_t xMutex;
void app_main() {
xMutex = xSemaphoreCreateMutex();//无参数
if (xMutex == NULL) {
printf("互斥信号量创建失败!\n");
} else {
printf("互斥信号量创建成功!\n");
}
}
(2)释放互斥信号量 xSemaphoreGive()
-
函数原型
BaseType_t xSemaphoreGive(SemaphoreHandle_t xMutex);
//参数: xMutex:互斥信号量句柄
-
返回值:
-
pdTRUE:释放成功
-
pdFALSE:释放失败(常见于没有先 take 就 give)
-
示例
// 假设当前任务已经成功 xSemaphoreTake(xMutex)
xSemaphoreGive(xMutex);
-
必须是当前获取了该 mutex 的任务才能释放,否则行为未定义;
-
释放后,可能唤醒等待该 mutex 的任务;
(3)获取互斥信号量xSemaphoreTake()
-
函数原型
BaseType_t xSemaphoreTake(SemaphoreHandle_t xMutex, TickType_t xTicksToWait);
//参数1: xMutex:互斥信号量句柄(通过 xSemaphoreCreateMutex() 获得)
//参数2: xTicksToWait:最大等待时间(单位为 Tick) 0:不等待,立即返回 ;portMAX_DELAY:无限等待直到可用
-
返回值:
-
pdTRUE:成功获取互斥信号量
-
pdFALSE:超时未获取
-
示例
if (xSemaphoreTake(xMutex, pdMS_TO_TICKS(100))) {
// 成功获取互斥量
printf("进入临界区\n");
// 访问受保护资源...
xSemaphoreGive(xMutex); // 使用完毕,记得释放
} else {
printf("获取互斥量超时,无法进入临界区\n");
}
(4)综合示例
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
SemaphoreHandle_t xMutex;
void low_priority_task(void *param)
{
xSemaphoreTake(xMutex, portMAX_DELAY);
printf("低优先级任务 A 获取了互斥量,开始占用资源...\n");
// 模拟长时间占用资源
for (int i = 0; i < 5; i++) {
printf("A 正在使用资源 (%d/5)...\n", i+1);
vTaskDelay(pdMS_TO_TICKS(1000));
}
printf("A 释放互斥量,资源释放完毕\n");
xSemaphoreGive(xMutex);
vTaskDelete(NULL);
}
void medium_priority_task(void *param)
{
while (1) {
printf("中等优先级任务 C 正在运行(打断其他任务)...\n");
vTaskDelay(pdMS_TO_TICKS(500));
}
}
void high_priority_task(void *param)
{
// 等待一段时间让低优先级任务先运行并获取 mutex
vTaskDelay(pdMS_TO_TICKS(1000));
printf("高优先级任务 B 尝试获取互斥量...\n");
xSemaphoreTake(xMutex, portMAX_DELAY);
printf("高优先级任务 B 成功获取了互斥量,执行完成!\n");
xSemaphoreGive(xMutex);
vTaskDelete(NULL);
}
void app_main(void)
{
xMutex = xSemaphoreCreateMutex();
if (xMutex == NULL) {
printf("创建互斥量失败!\n");
return;
}
// 创建三个不同优先级的任务
xTaskCreate(low_priority_task, "Task A (Low)", 2048, NULL, 3, NULL);
xTaskCreate(medium_priority_task, "Task C (Medium)", 2048, NULL, 4, NULL);
xTaskCreate(high_priority_task, "Task B (High)", 2048, NULL, 5, NULL);
}
五.总结
1.三种信号量总结
信号量类型 | 描述 | 特点 | 适用场景 |
二值信号量 Binary Semaphore | 只有两个状态:“满”(1) 或 “空”(0) | - 不记录数量 - 类似事件通知 - 支持中断中释放 - 初始值为空 | - 任务同步 - 中断通知任务 - 实现简单的“事件到达” |
计数信号量 Counting Semaphore | 信号量可以有多个“资源单位” | - 支持计数(如5个资源) - 支持中断中释放- 初始值由你设置 | - 多资源池管理(如多个缓冲区) - 控制并发数 - 网络连接池 |
互斥信号量(互斥量) Mutex Semaphore | 只能被“获得它的任务”释放 | - 带优先级继承机制 - 同一任务重复获取会阻塞 (递归 mutex 除外) | - 保护临界资源 - 防止“优先级反转” - 用于串口、共享变量、IO口保护等 |
2.通用函数总结(适用于多种信号量)
函数名 | 说明 | 使用说明 |
xSemaphoreCreateBinary() | 创建二值信号量 | 信号量初始为空 |
xSemaphoreCreateCounting(max, init) | 创建计数信号量 | 可设置最大和初始计数值 |
xSemaphoreCreateMutex() | 创建互斥信号量 | 含优先级继承 |
xSemaphoreTake(xSemaphore, timeout) | 获取信号量(减1) | 适用于所有类型(互斥量也一样) |
xSemaphoreGive(xSemaphore) | 释放信号量(加1) | 所有类型通用(互斥量需自己释放) |
xSemaphoreGiveFromISR(xSemaphore, *pxHigherPriorityTaskWoken) | 在中断中释放信号量 | 仅适用于二值或计数信号量 |
vSemaphoreDelete(xSemaphore) | 删除信号量 | 所有类型都适用 |
3.使用场景举例对比
应用场景 | 推荐使用 |
中断通知任务 | 二值信号量(GiveFromISR) |
任务之间同步事件 | 二值信号量 |
管理多个连接/资源 |
计数信号量 |
控制并发执行上限 | 计数信号量 |
保护共享变量/设备 | 互斥信号量 |
防止优先级反转 | 互斥信号量(具备继承机制) |