一、什么是信号量?
信号量可以理解为 RTOS 里的一种"受保护的计数器":任务通过它申请资源、等待事件、协调执行顺序。
在 STM32 + FreeRTOS 里,最常用的是 二值信号量 和 计数信号量 ;如果只是为了保护共享资源不被多个任务同时访问,通常更推荐用 互斥信号量。
二、信号量的种类
| 类型 | 取值/特点 | 典型用途 |
|---|---|---|
| 二值信号量 | 0 / 1 | 任务同步、中断通知任务 |
| 计数信号量 | 0 ~ 最大值 | 管理多个同类资源、事件计数 |
| 互斥信号量 | 特殊二值信号量,带优先级继承 | 保护临界资源,避免优先级反转 |
| 递归信号量 | 同一任务可多次获取 | 递归调用或嵌套加锁场景 |
三、基本使用方法
FreeRTOS 中信号量常用 API:
xSemaphoreCreateBinary(); // 创建二值信号量
xSemaphoreCreateCounting(max, init);// 创建计数信号量
xSemaphoreTake(sem, timeout); // 获取信号量
xSemaphoreGive(sem); // 释放信号量
xSemaphoreGiveFromISR(sem, &hp); // 中断中释放信号量
使用流程一般是:
- 创建信号量:在调度器启动前创建。
- 任务获取 :调用
xSemaphoreTake(),获取不到就阻塞等待。 - 事件/资源释放 :调用
xSemaphoreGive()或xSemaphoreGiveFromISR()。 - 配对使用:Take 和 Give 必须成对出现,否则容易造成任务永久阻塞。
四、STM32 案例一:二值信号量做任务同步
场景: 传感器采集任务采集完数据后,通知数据处理任务开始处理。
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
SemaphoreHandle_t xDataReadySem;
void vSensorTask(void *pvParameters)
{
for (;;)
{
/* 采集传感器数据 */
// Sensor_Read();
/* 数据准备好,释放信号量 */
xSemaphoreGive(xDataReadySem);
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void vProcessTask(void *pvParameters)
{
for (;;)
{
/* 等待数据就绪 */
if (xSemaphoreTake(xDataReadySem, portMAX_DELAY) == pdTRUE)
{
/* 处理数据 */
// Data_Process();
}
}
}
int main(void)
{
/* 硬件初始化省略 */
xDataReadySem = xSemaphoreCreateBinary();
if (xDataReadySem != NULL)
{
xTaskCreate(vSensorTask, "Sensor", 128, NULL, 1, NULL);
xTaskCreate(vProcessTask, "Process", 128, NULL, 2, NULL);
vTaskStartScheduler();
}
for (;;);
}
这里 vSensorTask 负责释放信号量,vProcessTask 负责获取信号量。二值信号量创建后初始为空,所以处理任务会先阻塞,直到采集任务给出信号量。
五、STM32 案例二:中断通知任务
场景: 按键中断发生后,唤醒一个任务处理按键事件。
SemaphoreHandle_t xKeySem;
void vKeyTask(void *pvParameters)
{
for (;;)
{
if (xSemaphoreTake(xKeySem, portMAX_DELAY) == pdTRUE)
{
/* 处理按键 */
// Key_Handler();
}
}
}
void EXTI0_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
/* 清除中断标志省略 */
xSemaphoreGiveFromISR(xKeySem, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
int main(void)
{
/* 硬件初始化省略 */
xKeySem = xSemaphoreCreateBinary();
xTaskCreate(vKeyTask, "KeyTask", 128, NULL, 2, NULL);
vTaskStartScheduler();
for (;;);
}
注意:中断里不能用 xSemaphoreGive(),必须用 xSemaphoreGiveFromISR()。
六、STM32 案例三:计数信号量管理资源
场景: 系统有 3 个串口缓冲区,最多允许 3 个任务同时使用。
SemaphoreHandle_t xUartResSem;
void vUartUserTask(void *pvParameters)
{
for (;;)
{
/* 申请一个串口缓冲区资源 */
if (xSemaphoreTake(xUartResSem, portMAX_DELAY) == pdTRUE)
{
/* 使用串口缓冲区 */
// UART_Send();
vTaskDelay(pdMS_TO_TICKS(50));
/* 使用完毕,归还资源 */
xSemaphoreGive(xUartResSem);
}
vTaskDelay(pdMS_TO_TICKS(200));
}
}
int main(void)
{
/* 硬件初始化省略 */
xUartResSem = xSemaphoreCreateCounting(3, 3);
if (xUartResSem != NULL)
{
xTaskCreate(vUartUserTask, "UartUser1", 128, NULL, 1, NULL);
xTaskCreate(vUartUserTask, "UartUser2", 128, NULL, 1, NULL);
xTaskCreate(vUartUserTask, "UartUser3", 128, NULL, 1, NULL);
vTaskStartScheduler();
}
for (;;);
}
这里最大计数值是 3,初始值也是 3,表示一开始有 3 个可用资源。每调用一次 xSemaphoreTake(),计数值减 1;调用 xSemaphoreGive(),计数值加 1。
七、选型建议
- 任务/中断同步:优先用二值信号量。
- 管理多个同类资源:用计数信号量。
- 保护共享资源互斥访问:优先用互斥信号量,因为它支持优先级继承,能降低优先级反转风险。
- 中断里释放信号量 :必须使用
xSemaphoreGiveFromISR()。 - 简单事件通知:在 STM32 + FreeRTOS 里也可以考虑任务通知,开销通常更小。