信号量 Semaphore
1. Semaphore 是什么
Semaphore(信号量)本质上是一种用于任务同步和事件通知的内核对象。
它最重要的作用不是传递具体数据,而是表达:
某个资源是否可用,或者某个事件是否已经发生。
在 FreeRTOS 中,Semaphore 的底层实现与 Queue 有密切关系。
可以先建立一个总的认识:
事件 / 资源发生变化
↓
Give 信号量
↓
Semaphore状态发生变化
↓
等待中的Task被唤醒
↓
Take信号量
↓
Task处理事件
因此,Semaphore最核心的两个动作就是:
Give
↓
释放 / 发布信号
Take
↓
获取 / 等待信号
2. Semaphore 与 Queue 的关系
2.1 FreeRTOS 中的底层关系
FreeRTOS 的 Semaphore 并不是完全独立设计的一套底层数据结构。
它使用了 Queue 的底层机制。
可以理解为:
FreeRTOS
│
↓
Queue
│
┌─────────┴─────────┐
↓ ↓
Queue API Semaphore API
│
┌───────────┼───────────┐
↓ ↓ ↓
二值信号量 计数信号量 互斥量
所以前面学习 Queue 时看到的很多底层概念,在 Semaphore 中仍然存在。
2.2 为什么 Semaphore 可以使用 Queue 的底层逻辑
Queue关注:
有没有数据、数据是什么。
Semaphore关注:
有没有信号、资源数量是多少。
因此 Semaphore 不需要真正关心数据内容。
例如:
Queue:
发送:
数据A → Queue
接收:
Queue → 数据A
而二值信号量:
Semaphore:
Give
↓
有信号
Take
↓
获取信号
它不关心:
“这个信号里面到底存了什么数据”
2.3 二值信号量的特殊性
二值信号量只有两种状态:
0 = 不可用 / 没有信号
1 = 可用 / 有信号
从底层 Queue 的角度理解:
uxLength = 1
uxItemSize = 0
这里:
uxLength
表示只能容纳一个信号。
而:
uxItemSize = 0
表示它不真正存储数据项。
所以二值信号量可以理解为:
只借用了 Queue 的“有没有东西”的逻辑,而不需要真正保存数据。
3. Semaphore 的核心思想
Semaphore 最重要的不是 API,而是下面这个模型:
事件产生者
│
↓
Give
│
↓
Semaphore
│
↓
Take
│
↓
事件处理者
例如:
温度超过阈值
↓
Give
↓
温度处理任务
↓
Take
↓
执行故障处理
这里必须注意:
Semaphore 本身不是事件源。
真正的事件来源可能是:
按键
ADC
DMA
定时器
CAN接收
UART接收
硬件中断
其他任务
Semaphore 只是负责把:
“事件发生了”
这个信息传递给等待它的任务。
4. 二值信号量 Binary Semaphore
4.1 二值信号量的状态
二值信号量只有:
0
1
可以理解成:
0 → 没有信号
1 → 有信号
状态变化:
Give
0 ─────────────→ 1
│
│ Take
↓
0
因此它特别适合:
一个事件发生 → 通知另一个任务处理一次。
4.2 创建二值信号量
API:
// 创建一个二值信号量
SemaphoreHandle_t xSemaphoreCreateBinary(void);
典型代码:
// 创建二值信号量
SemaphoreHandle_t xSemaphore = xSemaphoreCreateBinary();
// 判断信号量是否创建成功
if (xSemaphore == NULL)
{
// 创建失败,进入错误处理
Error_Handler();
}
这里需要特别注意:
动态创建可能失败。
原因包括:
FreeRTOS Heap空间不足
动态内存分配失败
堆管理配置问题
所以工程代码不能默认:
创建一定成功
而应该检查:
// 判断信号量句柄是否为空
if (xSemaphore == NULL)
{
// 创建失败
Error_Handler();
}
5. 为什么创建后不能认为已经有信号
这是理解 Semaphore 的一个关键点。
创建:
// 创建二值信号量
xSemaphore = xSemaphoreCreateBinary();
并不等于:
事件已经发生
通常创建后:
Semaphore
↓
不可用
↓
count = 0
因此任务执行:
// 尝试获取信号量
xSemaphoreTake(xSemaphore, 0);
通常会:
Take
↓
没有信号
↓
返回失败
这正是合理的。
例如:
系统刚启动
↓
还没有发生按键事件
↓
按键任务等待
↓
Blocked
而不是一启动就让任务误认为:
“已经发生了一次按键事件”
6. 二值信号量的 Give
6.1 普通任务上下文 Give
API:
// 在任务上下文中释放一个信号量
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
例如:
// 释放二值信号量
BaseType_t result = xSemaphoreGive(xSemaphore);
// 判断释放是否成功
if (result == pdTRUE)
{
// 信号量释放成功
}
else
{
// 信号量释放失败
}
6.2 Give 到底发生了什么
假设:
Semaphore = 0
执行:
Give
结果:
0 → 1
如果此时有任务正在:
Take
并且因为没有信号而进入 Blocked,那么 Give 会使等待任务获得被唤醒的机会:
Task
Blocked
↓
Give
↓
Task
Ready
注意:
Blocked → Ready 不等于 Ready → Running。
真正是否马上运行,还需要经过调度器判断。
7. 为什么 Give 与任务之间能够联动
这是 Semaphore 最容易被误解的地方。
不能理解成:
Give
↓
莫名其妙
↓
另一个Task运行
正确过程是:
事件发生
↓
事件产生者执行Give
↓
Semaphore变为可用
↓
等待该Semaphore的Task
Blocked → Ready
↓
调度器重新选择任务
↓
如果满足运行条件
↓
Task Running
↓
Take成功
8. 一个真实的任务间同步例子
假设有两个任务。
ProducerTask
负责检测温度。
温度超过阈值
↓
Give
ConsumerTask
负责处理故障。
Take
↓
处理故障
整体流程:
ProducerTask
│
│ 检测温度
↓
温度超过阈值
│
↓
Give
│
↓
Semaphore
│
↓
ConsumerTask
│
│ Take
↓
处理故障
这里 Semaphore 的作用就是:
把“温度异常事件”从产生者传递给处理者。
9. 任务等待信号量
核心 API:
// 等待并尝试获取信号量
BaseType_t xSemaphoreTake(
SemaphoreHandle_t xSemaphore,
TickType_t xBlockTime
);
其中最重要的是:
xBlockTime
它决定:
如果当前没有信号,任务最多愿意等待多久。
10. xSemaphoreTake() 的三种等待方式
10.1 不等待
// 立即尝试获取信号量,不进行阻塞等待
xSemaphoreTake(xSemaphore, 0);
过程:
Take
↓
有信号?
├── 是 → 成功
└── 否 → 立即失败
任务不会进入 Blocked。
10.2 有限时间等待
例如:
// 最多等待100个Tick获取信号量
xSemaphoreTake(xSemaphore, 100);
如果当前没有信号:
Take
↓
没有信号
↓
进入Blocked
↓
等待
如果100 Tick内发生 Give:
Give
↓
Blocked → Ready
↓
调度器
↓
Task运行
↓
Take成功
如果100 Tick内一直没有 Give:
Blocked
↓
100 Tick超时
↓
Ready
↓
Take失败
↓
返回pdFALSE
10.3 无限等待
// 无限等待信号量
xSemaphoreTake(xSemaphore, portMAX_DELAY);
含义:
任务愿意长期等待这个事件。
不是:
CPU一直等待
而是:
Task
↓
Blocked
↓
CPU运行其他Ready任务
↓
事件发生
↓
Give
↓
Task Ready
所以:
portMAX_DELAY 描述的是任务的等待行为,不代表 CPU 忙等。
11. Take 的完整决策流程
xSemaphoreTake()
│
↓
Semaphore是否可用?
┌─────┴─────┐
│ │
是 否
│ │
↓ ↓
Take成功 检查BlockTime
│
┌────────┴────────┐
│ │
0 > 0
│ │
↓ ↓
立即失败 Blocked
│
┌──────────┴──────────┐
│ │
Give 超时
│ │
↓ ↓
Blocked→Ready Ready
│ │
└──────────┬──────────┘
↓
调度器
↓
Task运行
这是必须掌握的核心流程。
12. Semaphore 与任务状态
必须严格区分:
Blocked
Ready
Running
例如:
Task A
Blocked
因为:
Take
↓
没有信号
↓
允许阻塞
当另一个执行者 Give:
Blocked
↓
Ready
但:
Ready ≠ Running
只有调度器最终选择 Task A:
Ready
↓
调度
↓
Running
13. 为什么阻塞不会占用 CPU
错误理解:
Task A
↓
等待信号量
↓
CPU一直等Task A
正确理解:
Task A
↓
Take
↓
没有信号
↓
Blocked
此时:
CPU
↓
运行其他Ready任务
例如:
Task A → Blocked
Task B → Ready
Task C → Ready
CPU
↓
Task B
↓
Task C
↓
Task B
所以阻塞等待的意义就是:
让出CPU,而不是忙等。
14. Semaphore 的 Give / Take 速度不匹配
二值信号量只有一个有效状态。
因此它不能像 Queue 一样保存大量事件。
例如:
Semaphore = 0
第一次Give
0 → 1
第二次Give
已经是1
↓
不能继续累计
所以:
Give
Give
Give
Give
不能理解成:
4个事件全部排队等待
二值信号量只能表达:
“现在至少有一个事件尚未被消费”
因此如果:
生产速度 > 消费速度
就可能出现:
事件丢失 / 无法累计
如果需求是:
必须记录累计发生了多少次事件
那么二值信号量可能不合适。
15. Counting Semaphore
计数信号量用于表达:
当前累计有多少个可用资源 / 未处理事件。
它与二值信号量最大的区别:
Binary Semaphore:
0 / 1
Counting Semaphore:
0 / 1 / 2 / 3 / …
16. 创建计数信号量
API:
// 创建一个计数信号量
SemaphoreHandle_t xSemaphoreCreateCounting(
UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount
);
例如:
// 创建最大计数为5、初始计数为0的计数信号量
SemaphoreHandle_t xSemaphore =
xSemaphoreCreateCounting(5, 0);
这里:
uxMaxCount = 5
表示:
最大只能计数到5。
而:
uxInitialCount = 0
表示:
创建时当前没有可用信号。
17. Counting Semaphore 的计数变化
假设:
uxMaxCount = 3
count = 0
连续 Give:
第一次Give:
0 → 1
第二次Give:
1 → 2
第三次Give:
2 → 3
此时:
count = uxMaxCount = 3
继续 Give:
3 → ?
不能继续增加。
因此:
达到 uxMaxCount 后不能继续增加。
18. 为什么达到最大值后 Give 失败
原因不是:
FreeRTOS内存不足
而是:
计数信号量的逻辑计数已经达到上限。
例如:
uxMaxCount = 3
当前count = 3
此时:
Give
↓
发现已经达到最大计数
↓
无法增加
↓
Give失败
所以:
Give失败
不等于:
内存不足
这两个概念必须区分。
19. Counting Semaphore 的 Take
假设:
count = 3
连续 Take:
第一次:
3 → 2
第二次:
2 → 1
第三次:
1 → 0
第四次 Take:
0
↓
没有信号
如果允许阻塞:
Task
↓
Blocked
等待下一次 Give。
20. Counting Semaphore 的工程意义
假设生产者产生事件:
事件1
事件2
事件3
每发生一次:
Give
那么:
count:
0
↓
1
↓
2
↓
3
消费者每处理一个:
Take
那么:
3
↓
2
↓
1
↓
0
因此 Counting Semaphore 可以表示:
还有多少个事件 / 资源等待处理。
21. Binary Semaphore 与 Counting Semaphore 对比
| 状态 | 0 / 1 | 0 ~ uxMaxCount |
| 是否累计 | 不能累计多个 | 可以累计 |
| Give上限 | 1 | uxMaxCount |
| Take | 消耗一个信号 | 计数减1 |
| 典型用途 | 事件通知 | 资源/事件计数 |
| 是否保存数据 | 否 | 否 |
| 是否适合传递具体数据 | 否 | 否 |
简单记忆:
Binary:
“有没有?”
Counting:
“有几个?”
22. Semaphore 与 Queue 的核心区别
Queue
Queue主要解决:
数据传递。
例如:
Task A
↓
发送ADC数据
↓
Queue
↓
Task B
↓
接收ADC数据
Queue中真正存在:
数据
Semaphore
Semaphore主要解决:
同步 / 事件通知 / 资源计数。
例如:
DMA完成
↓
Semaphore Give
↓
ADC Task Take
Semaphore本身不负责保存ADC数据。
23. 数据与事件必须分开理解
这是工程中非常重要的一点。
例如 ADC + DMA:
ADC数据
↓
DMA
↓
ADC_Buffer[]
这是:
数据通道
而:
DMA完成
↓
Semaphore
这是:
事件通道
最终:
数据通道:
ADC → DMA → Buffer
事件通道:
DMA完成 → ISR → Semaphore → Task
所以:
Semaphore负责告诉任务“什么时候可以处理”,Buffer负责告诉任务“处理什么数据”。
24. ISR 释放信号量
在中断服务函数中不能简单按照普通任务代码思考。
任务上下文:
// 在任务上下文中释放信号量
xSemaphoreGive(xSemaphore);
ISR上下文:
// 在ISR上下文中释放信号量
xSemaphoreGiveFromISR(
xSemaphore,
&xHigherPriorityTaskWoken
);
核心区别:
任务上下文
↓
xSemaphoreGive()
ISR上下文
↓
xSemaphoreGiveFromISR()
25. 为什么 ISR 必须使用 FromISR 版本
ISR与普通Task的执行上下文不同。
FreeRTOS专门提供:
xxxFromISR()
形式的API用于中断环境。
因此:
DMA ISR
↓
xSemaphoreGiveFromISR()
而不是:
DMA ISR
↓
xSemaphoreGive()
这是 FreeRTOS API 使用中的基本规则。
26. xHigherPriorityTaskWoken
典型代码:
// 保存是否因为Give操作唤醒了更高优先级任务
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 在ISR中释放信号量
xSemaphoreGiveFromISR(
xSemaphore,
&xHigherPriorityTaskWoken
);
这个变量用于告诉 FreeRTOS:
这次ISR中的Give是否使一个更高优先级的任务从Blocked变成Ready。
如果是:
xHigherPriorityTaskWoken != pdFALSE
通常会:
// 如果唤醒了更高优先级任务
if (xHigherPriorityTaskWoken != pdFALSE)
{
// 请求在ISR退出后进行任务切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
其逻辑:
ISR
↓
GiveFromISR
↓
等待任务被唤醒
↓
Blocked → Ready
↓
发现它优先级更高
↓
请求任务切换
↓
ISR退出后调度
27. ISR 与 Task 的完整同步流程
这是 Semaphore 在实时系统中非常重要的模型:
硬件事件
│
↓
ISR
│
↓
xSemaphoreGiveFromISR()
│
↓
Semaphore
│
↓
等待中的Task被唤醒
│
Blocked → Ready
│
↓
调度器
│
↓
Running
│
↓
xSemaphoreTake()
│
↓
处理事件
28. ADC + DMA + Semaphore
这是本章最重要的工程应用之一。
假设:
ADC采集64个数据
DMA负责:
ADC
↓
DMA
↓
ADC_Buffer[64]
DMA完成后:
DMA Transfer Complete
↓
DMA ISR
↓
xSemaphoreGiveFromISR()
ADC处理任务:
xSemaphoreTake()
↓
等待
因此:
ADC Task
Blocked
DMA完成:
Blocked
↓
Ready
调度器选择该任务:
Ready
↓
Running
最终:
Take成功
↓
读取ADC_Buffer[]
↓
处理ADC数据
29. ADC DMA 完成通知完整流程图
┌──────────────┐
│ ADC │
│ 产生采样值 │
└──────┬───────┘
↓
┌──────────────┐
│ DMA │
│ 自动搬运数据│
└──────┬───────┘
↓
┌──────────────┐
│ ADC_Buffer[] │
│ 保存数据 │
└──────┬───────┘
↓
DMA传输完成
↓
┌──────────────┐
│ DMA ISR │
└──────┬───────┘
↓
GiveFromISR()
↓
┌──────────────┐
│ Semaphore │
│ 事件同步 │
└──────┬───────┘
↓
Blocked → Ready
↓
┌──────────────┐
│ ADC处理任务 │
└──────┬───────┘
↓
Take()
↓
读取ADC_Buffer[]
↓
处理ADC数据
30. ADC DMA 示例代码
30.1 定义 Buffer 和 Semaphore
// 定义ADC DMA缓冲区长度
#define ADC_BUFFER_SIZE 64
// 保存DMA采集的ADC数据
static uint16_t ADC_Buffer[ADC_BUFFER_SIZE];
// 保存ADC DMA完成信号量句柄
static SemaphoreHandle_t xADC_Semaphore = NULL;
30.2 创建信号量
// 创建一个二值信号量
xADC_Semaphore = xSemaphoreCreateBinary();
// 判断信号量是否创建成功
if (xADC_Semaphore == NULL)
{
// 信号量创建失败
Error_Handler();
}
30.3 ADC处理任务
// ADC数据处理任务
static void ADC_Process_Task(void *argument)
{
// 当前任务不使用传入参数
(void)argument;
// 任务持续运行
while (1)
{
// 无限等待ADC DMA完成事件
if (xSemaphoreTake(
xADC_Semaphore,
portMAX_DELAY) == pdTRUE)
{
// DMA已经完成数据搬运
uint32_t sum = 0;
// 遍历所有ADC采样值
for (uint32_t i = 0; i < ADC_BUFFER_SIZE; i++)
{
// 累加ADC采样值
sum += ADC_Buffer[i];
}
// 计算ADC平均值
uint16_t average = sum / ADC_BUFFER_SIZE;
// 对ADC平均值进行后续处理
BMS_ADC_Process(average);
}
}
}
30.4 DMA ISR
// DMA1 Channel1中断服务函数
void DMA1_Channel1_IRQHandler(void)
{
// 保存是否唤醒了更高优先级任务
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 判断DMA传输完成中断是否发生
if (DMA_GetITStatus(DMA1_IT_TC1) != RESET)
{
// 清除DMA传输完成中断标志
DMA_ClearITPendingBit(DMA1_IT_TC1);
// 通知ADC处理任务DMA已经完成
xSemaphoreGiveFromISR(
xADC_Semaphore,
&xHigherPriorityTaskWoken
);
}
// 判断是否唤醒了更高优先级任务
if (xHigherPriorityTaskWoken != pdFALSE)
{
// 请求在ISR退出后进行任务切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
31. 为什么 ISR 不应该负责复杂 ADC 数据处理
错误结构:
DMA ISR
↓
读取64个ADC值
↓
计算平均值
↓
滤波
↓
最大值/最小值计算
↓
复杂BMS算法
问题:
ISR执行时间变长
↓
其他中断响应受到影响
↓
系统实时性下降
更合理:
DMA ISR
↓
清中断标志
↓
GiveFromISR
↓
退出ISR
然后:
ADC Task
↓
Take
↓
复杂数据处理
32. Semaphore 并不能解决所有并发问题
这是工程中必须注意的边界。
Semaphore解决:
“什么时候可以处理?”
但不自动解决:
“共享Buffer如何保证数据一致?”
例如:
DMA
↓
正在写Buffer
↘
Task读取Buffer
如果DMA已经开始覆盖下一批数据,而Task还没有处理完上一批数据,就可能产生数据竞争。
因此实际工程可能进一步使用:
双缓冲
Ping-Pong Buffer
半传输/全传输机制
Buffer状态管理
这里需要记住:
Semaphore 是同步机制,不是万能的数据保护机制。
33. Binary Semaphore 的典型使用场景
适合:
一个事件
↓
通知一个任务
例如:
按键
↓
EXTI
↓
GiveFromISR
↓
KeyTask Take
DMA完成
↓
DMA ISR
↓
GiveFromISR
↓
ADC Task Take
温度超过阈值
↓
Give
↓
故障处理Task Take
34. Counting Semaphore 的典型使用场景
适合:
多个事件发生
↓
需要累计数量
例如:
事件1 → Give
事件2 → Give
事件3 → Give
得到:
count = 3
处理:
Take → 2
Take → 1
Take → 0
因此:
Binary Semaphore
→ “有没有事件?”
Counting Semaphore
→ “有多少个事件?”
35. Semaphore 不适合传递具体数据
如果需要:
ADC值
CAN帧
温度值
结构体
命令参数
那么应该考虑:
Queue
因为 Queue 的核心能力是:
数据生产者
↓
Queue
↓
数据消费者
而 Semaphore:
事件生产者
↓
Semaphore
↓
事件消费者
36. Semaphore 与 Queue 的选择
可以用下面的判断:
我要传递什么?
│
┌─────────┴─────────┐
↓ ↓
数据 事件/资源
│ │
↓ ↓
Queue Semaphore
│
┌─────────┴─────────┐
↓ ↓
是否累计? 只需0/1?
│ │
↓ ↓
Counting Binary
简单判断:
“我要把什么东西传过去?”
↓
Queue
“我只想告诉另一个任务事情发生了。”
↓
Binary Semaphore
“我还要知道累计发生了多少次。”
↓
Counting Semaphore
37. Semaphore 常见错误
37.1 创建后直接认为已经有信号
错误认识:
Create
↓
Semaphore已经有效
正确:
Create
↓
等待真正事件发生
↓
Give
↓
Semaphore有效
37.2 ISR中使用普通Give
错误:
// 错误示例:ISR中直接使用任务版本API
xSemaphoreGive(xSemaphore);
正确:
// 在ISR中使用ISR安全版本API
xSemaphoreGiveFromISR(
xSemaphore,
&xHigherPriorityTaskWoken
);
37.3 把Blocked理解成占用CPU
错误:
Task Blocked
↓
CPU一直等待
正确:
Task Blocked
↓
任务不参与CPU运行
↓
其他Ready任务获得CPU
37.4 把Blocked → Ready理解成马上运行
错误:
Blocked
↓
Ready
↓
立即Running
正确:
Blocked
↓
Ready
↓
调度器
↓
根据优先级等条件决定
↓
Running
37.5 用Binary Semaphore累计大量事件
例如:
Give
Give
Give
Give
不能理解成:
4个事件全部保存
二值信号量最多表达:
0 / 1
如果必须累计事件数量:
Counting Semaphore
或者根据具体数据需求考虑 Queue 等机制。
37.6 认为Semaphore可以传递数据
错误:
Semaphore → ADC数据
正确:
Buffer → ADC数据
Semaphore → ADC完成事件
37.7 ISR做大量工作
错误:
ISR
↓
复杂计算
↓
长时间占用CPU
更合理:
ISR
↓
快速处理
↓
GiveFromISR
↓
Task处理复杂业务
38. 一个完整的工程模型
以BMS中的ADC采集为例:
┌─────────────┐
│ ADC │
│ 采集电池数据 │
└──────┬──────┘
↓
┌─────────────┐
│ DMA │
│ 自动搬运数据 │
└──────┬──────┘
↓
┌─────────────┐
│ ADC_Buffer │
│ 保存数据 │
└──────┬──────┘
↓
DMA完成中断
↓
┌─────────────┐
│ ISR │
└──────┬──────┘
↓
GiveFromISR()
↓
┌─────────────┐
│ Semaphore │
│ 事件通知 │
└──────┬──────┘
↓
Task Ready
↓
调度器选择
↓
┌─────────────┐
│ ADC Task │
└──────┬──────┘
↓
Take()
↓
读取ADC_Buffer
↓
数据处理/计算
39. Semaphore 的核心知识体系
把整个章节压缩成一棵知识树:
Semaphore
│
├── 1. 本质
│ ├── 同步
│ ├── 事件通知
│ └── 资源计数
│
├── 2. 底层关系
│ └── 基于Queue机制
│
├── 3. Binary Semaphore
│ ├── 0 / 1
│ ├── Create
│ ├── Give
│ └── Take
│
├── 4. Counting Semaphore
│ ├── 0 ~ uxMaxCount
│ ├── Give → count + 1
│ └── Take → count – 1
│
├── 5. Task等待
│ ├── xBlockTime = 0
│ ├── 有限Tick
│ └── portMAX_DELAY
│
├── 6. ISR释放
│ ├── xSemaphoreGiveFromISR()
│ ├── xHigherPriorityTaskWoken
│ └── portYIELD_FROM_ISR()
│
├── 7. Task状态
│ ├── Blocked
│ ├── Ready
│ └── Running
│
├── 8. 工程应用
│ ├── 按键事件
│ ├── DMA完成
│ ├── ADC DMA
│ └── 任务间事件通知
│
└── 9. 与Queue区别
├── Queue → 数据
└── Semaphore → 事件/同步/资源
40. 面试高频问题
40.1 Semaphore和Queue有什么区别?
标准回答:
Queue主要用于任务之间的数据传递,能够保存具体的数据项;Semaphore主要用于任务同步、事件通知或者资源计数,本身不关注具体数据内容。FreeRTOS中Semaphore底层使用了Queue的相关机制。
40.2 二值信号量为什么只有0和1?
因为它只需要表达:
0 → 没有信号
1 → 有信号
不需要保存具体数据。
40.3 xSemaphoreTake() 阻塞的时候CPU是不是一直等?
不是。
任务进入:
Blocked
不会占用CPU。
调度器会运行其他Ready任务。
40.4 Give之后任务是不是马上运行?
不是必然。
流程:
Give
↓
Blocked → Ready
↓
调度器
↓
判断优先级等条件
↓
决定是否运行
40.5 为什么ISR中使用 xSemaphoreGiveFromISR()?
因为ISR属于中断上下文,FreeRTOS提供专门的 FromISR API供中断环境使用。
40.6 为什么不能在ISR中进行复杂计算?
因为ISR执行时间过长会增加中断延迟,影响系统实时性。
更合理:
ISR
↓
快速通知
↓
Task
↓
复杂处理
40.7 Binary Semaphore和Counting Semaphore区别?
核心区别:
Binary:
0 / 1
Counting:
0 ~ uxMaxCount
Binary适合:
是否发生事件。
Counting适合:
累计发生了多少事件 / 有多少资源。
40.8 ADC DMA为什么适合使用Semaphore?
因为DMA负责搬运数据,DMA完成后只需要通知任务:
“这一批数据已经准备好了。”
具体数据已经存储在ADC Buffer中,因此Semaphore负责事件同步,Buffer负责数据保存。
最终记忆模型
如果以后忘记Semaphore,不要从API开始回忆。
先问自己:
我要解决的是数据传输,还是事件同步?
如果是:
数据
↓
Queue
如果是:
事件
↓
Semaphore
然后继续判断:
只有“有没有事件”?
↓
Binary Semaphore
需要知道“有多少个事件”?
↓
Counting Semaphore
如果事件来自ISR:
ISR
↓
GiveFromISR
↓
Task Take
如果任务等待事件:
Take
↓
没有信号
↓
Blocked
↓
CPU运行其他任务
事件发生:
Give
↓
Blocked → Ready
↓
调度器
↓
Running
↓
Take成功
最终形成:
Semaphore
│
┌────────────────┼────────────────┐
↓ ↓ ↓
Binary Counting ISR同步
│ │ │
0 / 1 0~MaxCount GiveFromISR
│ │ │
└────────────────┼────────────────┘
↓
Take
↓
Task等待事件
↓
Blocked
↓
Give
↓
Ready
↓
调度器
↓
Running
一句话总结:
Semaphore不是用来传数据的,而是用来表达“事件发生了、资源可用了、还有多少个事件/资源”的同步机制;Task通过Take等待,事件产生者通过Give发布,ISR则使用FromISR版本进行通知。


