你希望系统且详细地学习FreeRTOS中信号量的使用方法,我会从信号量的基本概念入手,区分不同类型的信号量,并结合具体的代码示例,讲解每种信号量的创建、获取、释放流程和实际应用场景,帮助你彻底掌握信号量的使用。

一、信号量的核心概念

信号量是FreeRTOS中实现任务间同步任务与中断同步资源互斥访问的核心机制,可通俗理解为:

  • 二进制信号量:像卫生间门锁,只有“锁上(0)”和“打开(1)”两种状态,用于简单同步/单资源互斥。
  • 计数信号量:像停车场车位,有N个可用车位(初始计数值),占用-1、释放+1,用于多资源管理。
  • 互斥信号量(互斥体):特殊的二进制信号量,通过“优先级继承”解决优先级反转问题,专用于资源互斥。

前置条件

使用信号量前需在FreeRTOS配置文件FreeRTOSConfig.h中开启对应宏:

#define configUSE_SEMAPHORES        1   // 开启二进制/计数信号量
#define configUSE_MUTEXES           1   // 开启互斥信号量
#define configUSE_RECURSIVE_MUTEXES 1   // 可选:开启递归互斥信号量

所有信号量API都包含在semphr.h头文件中。

二、不同类型信号量的使用方法

1. 二进制信号量(Binary Semaphore)

核心作用
  • 任务间同步:一个任务完成后通知另一个任务执行。
  • 中断-任务同步:中断中释放信号量,唤醒等待的任务(最常用场景)。
关键API
函数 作用 适用场景
xSemaphoreCreateBinary() 创建二进制信号量 任务中(返回句柄,失败为NULL)
xSemaphoreTake(句柄, 超时时间) 获取信号量(计数值→0) 任务中(portMAX_DELAY=永久等待)
xSemaphoreGive(句柄) 释放信号量(计数值→1) 任务中
xSemaphoreGiveFromISR(句柄, &xHigherPriorityTaskWoken) 释放信号量 中断中(ISR专用)
代码示例(中断与任务同步)

场景:按键中断触发后,释放信号量唤醒任务处理按键事件(基于STM32):

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

// 1. 定义二进制信号量句柄(全局/任务可访问)
SemaphoreHandle_t xBinarySem = NULL;

// 2. 按键中断服务函数(EXTI0为例)
void EXTI0_IRQHandler(void)
{
  BaseType_t xHigherPriorityTaskWoken = pdFALSE; // 标记是否需要切换任务
  
  if(EXTI_GetITStatus(EXTI_Line0) != RESET)
  {
    // 中断中释放信号量(必须用FromISR版本)
    if(xBinarySem != NULL)
    {
      xSemaphoreGiveFromISR(xBinarySem, &xHigherPriorityTaskWoken);
      // 若唤醒更高优先级任务,立即切换上下文
      portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }
    EXTI_ClearITPendingBit(EXTI_Line0); // 清除中断标志
  }
}

// 3. 按键处理任务
void vKeyTask(void *pvParameters)
{
  for(;;) // 任务无限循环
  {
    // 获取信号量:永久等待,直到中断释放
    if(xSemaphoreTake(xBinarySem, portMAX_DELAY) == pdPASS)
    {
      // 信号量获取成功,执行按键逻辑
      printf("按键按下!翻转LED\r\n");
      HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // 控制LED
    }
  }
}

// 4. 主函数初始化
int main(void)
{
  // 硬件初始化(GPIO、中断、串口等)
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  
  // 创建二进制信号量
  xBinarySem = xSemaphoreCreateBinary();
  
  if(xBinarySem != NULL)
  {
    // 创建按键任务:名称、栈大小、参数、优先级、任务句柄
    xTaskCreate(vKeyTask, "KeyTask", 128, NULL, 2, NULL);
    // 启动FreeRTOS调度器
    vTaskStartScheduler();
  }
  
  // 调度器启动失败(如栈不足),进入死循环
  for(;;);
}

2. 计数信号量(Counting Semaphore)

核心作用

管理多个相同资源(如3个串口缓冲区、5个打印队列),计数值代表可用资源数。

关键API
函数 作用
xSemaphoreCreateCounting(最大计数值, 初始计数值) 创建计数信号量
uxSemaphoreGetCount(句柄) 获取当前计数值(调试用)
xSemaphoreTake/Give 同二进制信号量(Take=资源-1,Give=资源+1)
代码示例(多资源管理)

场景:3个串口资源,4个任务竞争使用:

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

// 计数信号量句柄:最大3个资源,初始3个可用
SemaphoreHandle_t xCountSem = xSemaphoreCreateCounting(3, 3);

// 串口使用任务
void vUartTask(void *pvParameters)
{
  uint32_t taskID = (uint32_t)pvParameters; // 任务编号
  for(;;)
  {
    // 获取资源:等待500ms,超时则放弃
    if(xSemaphoreTake(xCountSem, pdMS_TO_TICKS(500)) == pdPASS)
    {
      // 成功获取资源
      printf("任务%d:占用串口,剩余资源:%d\r\n", 
             taskID, uxSemaphoreGetCount(xCountSem));
      vTaskDelay(pdMS_TO_TICKS(1000)); // 模拟使用串口
      
      // 释放资源
      xSemaphoreGive(xCountSem);
      printf("任务%d:释放串口,剩余资源:%d\r\n", 
             taskID, uxSemaphoreGetCount(xCountSem));
    }
    else
    {
      // 获取超时
      printf("任务%d:获取串口超时!\r\n", taskID);
    }
    vTaskDelay(pdMS_TO_TICKS(500)); // 任务延时,让出CPU
  }
}

// 主函数创建任务
int main(void)
{
  // 硬件初始化...
  
  if(xCountSem != NULL)
  {
    // 创建4个任务竞争3个资源
    xTaskCreate(vUartTask, "Uart1", 128, (void*)1, 1, NULL);
    xTaskCreate(vUartTask, "Uart2", 128, (void*)2, 1, NULL);
    xTaskCreate(vUartTask, "Uart3", 128, (void*)3, 1, NULL);
    xTaskCreate(vUartTask, "Uart4", 128, (void*)4, 1, NULL);
    
    vTaskStartScheduler();
  }
  
  for(;;);
}

3. 互斥信号量(Mutex Semaphore)

核心作用

解决优先级反转问题(低优先级任务占用资源,高优先级任务等待,中优先级任务抢占CPU),专用于独占式资源访问(如LCD、SPI总线)。

优先级继承:低优先级任务占用互斥体时,临时继承高优先级任务的优先级,避免中优先级任务抢占。

关键API
函数 作用 注意事项
xSemaphoreCreateMutex() 创建互斥信号量 无FromISR版本,不能在中断中使用
xSemaphoreTake/Give 获取/释放互斥体 必须在同一个任务中操作
代码示例(解决优先级反转)
#include "FreeRTOS.h"
#include "semphr.h"

// 互斥信号量句柄
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();

// 低优先级任务(优先级1)
void vLowTask(void *pvParameters)
{
  for(;;)
  {
    xSemaphoreTake(xMutex, portMAX_DELAY); // 获取互斥体
    printf("低优先级任务:占用LCD\r\n");
    vTaskDelay(pdMS_TO_TICKS(2000)); // 长时间占用资源
    xSemaphoreGive(xMutex); // 释放互斥体
    printf("低优先级任务:释放LCD\r\n");
    vTaskDelay(pdMS_TO_TICKS(1000));
  }
}

// 中优先级任务(优先级2)
void vMidTask(void *pvParameters)
{
  for(;;)
  {
    printf("中优先级任务:执行中...\r\n");
    vTaskDelay(pdMS_TO_TICKS(500));
  }
}

// 高优先级任务(优先级3)
void vHighTask(void *pvParameters)
{
  for(;;)
  {
    printf("高优先级任务:尝试获取LCD\r\n");
    xSemaphoreTake(xMutex, portMAX_DELAY); // 获取互斥体
    printf("高优先级任务:占用LCD\r\n");
    vTaskDelay(pdMS_TO_TICKS(500)); // 短时间使用
    xSemaphoreGive(xMutex); // 释放互斥体
    printf("高优先级任务:释放LCD\r\n");
    vTaskDelay(pdMS_TO_TICKS(1000));
  }
}

// 主函数创建任务
int main(void)
{
  // 硬件初始化...
  
  if(xMutex != NULL)
  {
    xTaskCreate(vLowTask, "LowTask", 128, NULL, 1, NULL);
    xTaskCreate(vMidTask, "MidTask", 128, NULL, 2, NULL);
    xTaskCreate(vHighTask, "HighTask", 128, NULL, 3, NULL);
    
    vTaskStartScheduler();
  }
  
  for(;;);
}

三、信号量使用注意事项

  1. 中断中仅能调用xSemaphoreGiveFromISR不能调用xSemaphoreTakeFromISR(中断不允许等待)。
  2. 互斥信号量仅能在任务中使用,且获取/释放必须在同一个任务中。
  3. 避免使用portMAX_DELAY永久等待,否则可能导致任务死锁,建议设置合理超时。
  4. 信号量句柄需全局或通过参数传递,确保任务/中断能访问到。

总结

  1. FreeRTOS信号量分三类核心类型:二进制信号量(同步)、计数信号量(多资源)、互斥信号量(解决优先级反转的互斥)。
  2. 中断操作信号量必须用FromISR后缀的API,并处理xHigherPriorityTaskWoken完成上下文切换。
  3. 互斥信号量是资源互斥的首选方案,二进制信号量更适合同步场景,计数信号量用于多相同资源管理。
Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐