文章目录
- 信号量的引入
- [信号 VS 信号量](#信号 VS 信号量)
- 二值信号量
- 计数信号量(了解)
- 互斥信号量
-
- 互斥信号量的本质
- 互斥锁的实现原理
- 优先级反转问题
- 互斥信号量的重要机制:优先级继承
- 互斥信号量的操作API
- 一个很重要的经验总结
- 优先级继承:代码演示
- [互斥信号量 VS 计数信号量](#互斥信号量 VS 计数信号量)
- 其余需要了解的细节
- 扩展问题:如果中断当中需要保护共享资源怎么办?
- 临界区和互斥信号量的案例对比
- 信号量总结
信号量的引入
在前面学习 FreeRTOS 的任务通信机制时,我们已经学习了 消息队列(Queue)。
消息队列主要用于 任务之间传递数据,也是任务间通信最常用的手段之一。
例如:
任务A采集数据,将数据发送到队列:
c
xQueueSend(QueueHandle, &Value, 0);
任务B从队列中读取数据:
c
xQueueReceive(QueueHandle, &Value, portMAX_DELAY);
这种方式解决的是:
任务之间如何传递数据的问题。
但在实际系统中,还有另外一种非常常见的需求:任务同步。
这里有一个小问题:什么叫做任务同步?
很多同学第一次听到这个词时,往往会觉得有点抽象。
其实所谓的 任务同步,本质上就是一句话:
让多个任务按照某种顺序或条件进行执行。
换句话说就是:
某个任务必须等待某个事件发生,才能继续执行。
这种"等待条件满足再继续执行"的行为,就称为 任务同步。
当然如果两个任务之间没有任何关系,它们就可以各自独立运行,这种互不依赖的执行方式称为任务异步。
举一个非常简单的例子,假设系统中有两个任务:
任务A:读取传感器数据
任务B:处理传感器数据
显然:
任务B必须等待任务A采集完成之后,才能开始处理。
如果任务B提前执行,就会出现问题:此时任务B得到的数据可能是 错误的、旧的或者空的。
所以我们需要一种机制,让任务B:
等待任务A完成后再继续执行。
这就是一个典型的 任务同步问题。
在这种场景下:我们并不需要传递数据。只需要告诉对方:某个事件发生了。
为了解决这种问题,RTOS操作系统通常会提供一种机制:信号量(Semaphore)。
在FreeRTOS中,若想使用信号量,首先需要包含一个头文件:
c
#include "semphr.h"
否则相关的声明定义、函数API等都是无法使用的。
信号 VS 信号量
在讲信号量之前,我们先简单区分两个非常容易混淆的概念:信号(Signal) 和 信号量(Semaphore)。
这两个名字非常像,但实际上是 两种完全不同的机制。
很多初学者在第一次接触操作系统时,经常会把这两个概念混淆。
信号(Signal)
信号主要出现在 Linux / Unix 操作系统 中。
信号是一种 异步通知机制,用于通知进程某个事件发生。
例如:
- 程序按下
Ctrl + C - 子进程退出
- 定时器到期
- 程序非法访问内存
这些事件都会触发一个 Signal 信号。
程序可以通过 信号处理函数(Signal Handler) 来处理这些事件。
简单来说:
Signal 本质是一种简单的事件通知机制,只用于通知某个事件已经发生。
需要注意的是:
FreeRTOS 中并没有 Signal 这种机制。
信号量(Semaphore)
信号量是一种 用于任务同步和资源管理的机制。
它主要用于解决:多个任务之间的协作问题。
例如:
任务A等待某个事件发生:
等待信号量
任务B完成某项工作后:
释放信号量
这样任务A就会被唤醒继续执行。
可以理解为:
信号量是一种任务之间的事件通知,实现任务同步的工具。
从这个角度来看:
Signal 和 Semaphore 都可以用于事件通知。
但除此之外,信号量还兼具资源计数管理、共享资源保护等功能,这些能力是 Signal 机制所不具备的。
简单对比一下:
| 机制 | 主要用途 | 使用场景 |
|---|---|---|
| Signal(信号) | 事件通知 | Linux / Unix 通用操作系统 |
| Semaphore(信号量) | 事件通知 / 资源管理 / 资源保护 | RTOS 实时操作系统 |
接下来我们将学习 FreeRTOS 中的三种信号量,它们的作用是完全不同的:
- 二值信号量(Binary Semaphore) ,用于 事件通知,任务同步。
- 计数信号量(Counting Semaphore) ,用于 资源计数管理。
- 互斥信号量(Mutex) ,用于 保护共享资源 ,并解决 优先级反转问题。
接下来我们先来看最简单的一种信号量:二值信号量(Binary Semaphore)。
当然,在具体讲解这些信号量之前,我们先要明确一个大前提:
所有信号量的本质都是消息队列,所有信号量的底层实现都是一个Queue_t类型的结构体。
二值信号量
在 FreeRTOS 中,最简单的一种信号量类型是:二值信号量(Binary Semaphore)
从名字就可以看出,它有两个关键词:二值(Binary) 、信号量(Semaphore)
所谓 二值 ,意思就是,这个信号量的值 只有两种状态:
0
1
可以理解为:
0 表示没有信号
1 表示有信号
也可以理解成:
0 表示资源不可用
1 表示资源可用
或者更直观一点,它就像一个开关:
0 关
1 开
从本质上理解二值信号量
从具体讲解二值信号量的操作 API,以及如何使用它之前,我们不妨先从 源码层面 来理解一下:
二值信号量到底是什么东西?
实际上,FreeRTOS当中所有的信号量,包括二值信号量,本质上都是基于消息队列来实现的。
换句话说:信号量,实际上是一种特殊的消息队列。
就二值信号量来说,它本质上是一个 长度为 1 的消息队列,也就是说:
创建二值信号量的函数是:xSemaphoreCreateBinary()
它本质上是一个宏函数,源码如下:
c
// 源码截取自 semphr.h 的第166行
#if ( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
#define xSemaphoreCreateBinary() \
xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )
#endif
....
// 源码截取自 semphr.h 的第41行
#define semSEMAPHORE_QUEUE_ITEM_LENGTH ( ( uint8_t ) 0U )
可以看到,创建二值信号量和创建消息队列时,最终调用的都是同一个函数:
c
xQueueGenericCreate()
也就是说:
二值信号量的底层实现,其实就是创建了一个消息队列。
不过,这个队列和普通队列有两个非常明显的区别:
- 元素的个数只有1个,队列的长度是1
- 每个元素的大小是0
这代表什么意思呢?
很简单:
二值信号量确实是一个消息队列,但实际上不能存储任何元素,不能存储任何有效数据。
那么问题来了:
既然这个消息队列 不存储任何数据 ,那么它又是如何表示 信号存在(1) / 信号不存在(0) 这两种状态的呢?
答案其实就藏在 消息队列控制结构体 当中。
FreeRTOS 中消息队列的核心控制结构体定义如下(前面已经学过了):
c
typedef struct QueueDefinition{
// 第一部分四个成员
int8_t *pcHead;
int8_t *pcTail;
int8_t *pcWriteTo;
int8_t *pcReadFrom;
// 第二部分两个成员
List_t xTasksWaitingToSend;
List_t xTasksWaitingToReceive;
// 第三部分三个成员
volatile UBaseType_t uxMessagesWaiting;
UBaseType_t uxLength;
UBaseType_t uxItemSize;
} xQUEUE;
关键就在 xQUEUE 结构体里的这个成员:
c
uxMessagesWaiting
这个成员表示:当前消息队列中有多少个元素,实际存储元素的个数
对于二值信号量来说:
- uxMessagesWaiting取值是0,消息队列为空,信号不存在,即二值信号量的0。
- uxMessagesWaiting取值是1,消息队列中有1个长度为0的元素,信号存在,即二值信号量的1。
理解了二值信号量的本质后,再来学习二值信号量的一系列操作函数,就会变得 非常简单、非常清晰。
二值信号量的操作API
首先,二值信号量机制在FreeRTOS中是可裁剪配置的,它的使用基于下列宏的开启:
c
#define configSUPPORT_DYNAMIC_ALLOCATION 1
这个宏我们很熟悉了,它的默认值就是1。
所以二值信号量是FreeRTOS默认开启使用的任务间通信机制。
从 API 使用角度 来看,二值信号量常用的函数其实并不多,核心操作API还是只需要学习 4个:
- 创建二值信号量
- 获取信号量(Take)
- 释放信号量(Give)
- 在中断环境下释放信号量(GiveFromISR)
下面,逐一介绍一下这些函数。
创建二值信号量:xSemaphoreCreateBinary()
创建一个二值信号量,其函数原型如下:
SemaphoreHandle_t xSemaphoreCreateBinary(void);
函数的作用是:用于 创建一个二值信号量对象。
函数的返回值含义是:
- 如果创建成功,返回二值信号量句柄。创建完成后,就可以通过这个句柄操作二值信号量。
- 如果FreeRTOS内核堆空间不足创建失败,函数返回NULL**(很显然,这是一种动态创建手段,依赖FreeRTOS内核堆空间)**。
代码示例:
c
SemaphoreHandle_t BinarySem;
BinarySem = xSemaphoreCreateBinary();
需要注意的是:
刚创建出来的二值信号量 默认是空的,默认是0,或者说默认事件未发生。
也就说,如果创建完二值信号量后,立刻去获取信号量(Take),是无法获取信号量的。
如果使用二值信号量来表示 资源的可用状态 ,那么有些场景下我们希望:系统启动时资源就是可用的。
此时通常的做法是:
在创建二值信号量后,手动释放一次信号量。
当然,是否需要在创建后立即释放信号量,取决于具体的使用场景和设计需求。
如果二值信号量用于 事件通知(例如中断通知任务),那么通常 不需要提前释放信号量,而是等待事件真正发生时再释放信号量。
创建二值信号量的本质就是:
- 创建一个长度为1,元素大小为0的消息队列
- 在创建之初,队列控制结构体中的成员
uxMessagesWaiting取值是0,表示没有信号。
释放二值信号量:xSemaphoreGive()
该函数用于 释放信号量。
也可以理解为:发送信号量、发送通知。
该函数原型如下:
c
BaseType_t xSemaphoreGive(
SemaphoreHandle_t xSemaphore // 要操作的二值信号量句柄
);
当任务调用该函数时:信号量的值变为 1,从无到有,从不满足条件到满足条件
如果此时有任务正在 等待该信号量:
那么系统会:唤醒等待的任务。
其返回值,表示释放信号的两种结果:
- pdTRUE,释放成功
- pdFALSE,释放失败
什么时候会释放成功,什么时候会释放失败呢?
- 当二值信号量已经为 1 时,再次调用
xSemaphoreGive()会失败并返回pdFALSE,因为信号量已经处于满状态。 - 如果二值信号量还是0,调用此函数,就会释放信号成功,并返回值
pdTRUE。
为什么这个函数不需要设置阻塞时间?
释放信号不会阻塞任务,因为释放信号本身不需要等待条件成立,没有信号时就释放成功,有信号时直接释放失败。
释放二值信号量的本质可以理解为:改变队列中"元素数量"的状态。
在调用 xSemaphoreGive() 释放信号量时:
- 如果uxMessagesWaiting的取值是0,表示当前没有信号,此时函数会将其设置为 1 ,表示信号产生,并返回 pdTRUE。
- 如果uxMessagesWaiting的取值已经是1,表示信号已经存在,此时函数不会阻塞,也不会改变状态,直接返回 pdFALSE。
获取二值信号量:xSemaphoreTake()
该函数用于 获取信号量。
也可以理解为:等待接收信号量、等待事件发生、等待通知。
函数原型如下:
c
BaseType_t xSemaphoreTake(
SemaphoreHandle_t xSemaphore, // 要操作的二值信号量句柄
TickType_t xTicksToWait // 等待的最大阻塞时间
);
其函数中的参数,和消息队列中xQueueReceive函数的参数类似:
- xSemaphore,表示要操作的 信号量句柄。
- xTicksToWait,表示等待信号量的最大阻塞时间。其取值分为三种:
- 等于0,不等待,立刻返回获取结果,即有信号或无信号。
- 大于0,表示最多阻塞等待xTicksToWait个Tick,如果超时后仍无信号,则会返回获取失败的结果。
portMAX_DELAY,表示无限期阻塞等待信号,不会因超时返回获取失败。
其返回值表示获取信号的两种结果:
- pdTRUE,获取成功
- pdFALSE,获取失败
看到这里,你应该已经很熟悉了,这不是和消息队列差不多?不着急,我们继续看。
获取二值信号量的本质,也可以理解为:改变队列中"元素数量"的状态。
在调用 xSemaphoreTake() 获取信号量时:
- 如果uxMessagesWaiting的取值是0,表示当前没有信号,此时函数会根据 xTicksToWait 的取值决定是否进入阻塞等待,或者直接返回失败。
- 如果uxMessagesWaiting的取值是1,表示信号存在,此时函数会 获取该信号 ,并将
uxMessagesWaiting的值 设置为 0 ,然后返回 pdTRUE。
在中断环境释放信号量:xSemaphoreGiveFromISR()
xSemaphoreGive() 只能在任务环境中调用,如果在 中断服务函数(ISR) 中调用该函数,是不允许的。
FreeRTOS 专门提供了一个 中断版本的释放函数:
c
BaseType_t xSemaphoreGiveFromISR(
SemaphoreHandle_t xSemaphore, // 要操作的二值信号量句柄
BaseType_t *pxHigherPriorityTaskWoken // 是否需要在ISR结束后,触发高优先级任务切换
);
这个函数的作用是:
在中断服务函数中释放信号量。
该函数和在中断中发送数据到消息队列函数,使用起来完全是一致的。
如果需要要在ISR结束后,立刻触发高优先级任务切换,则需要按照下列方式进行函数调用:
c
// 某个中断的ISR
void XXXX_IRQHandler(void){
BaseType_t xFlag = pdFALSE;
xSemaphoreGiveFromISR(BinarySem, &xFlag);
portYIELD_FROM_ISR(xFlag);
}
采用上面的调用方式时,如果随着信号释放,某个高优先级的任务被唤醒,那么:
ISR结束后,系统会 立即切换到该任务执行。
这么做可以尽早让信号接收者任务响应,提升系统的实时性。
简单代码示例
先给出一个简单的代码示例,演示一下上述二值信号量API函数的使用。
如下:
c
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "DebugUSART1.h"
static SemaphoreHandle_t BinarySem = NULL;
/* -------------------- 任务1:释放信号量任务 -------------------- */
void TaskGive(void *Argument) {
while (1) {
// 间隔1s 释放一次信号量
vTaskDelay(1000);
BaseType_t Ret = xSemaphoreGive(BinarySem);
if (Ret == pdTRUE) {
printf1("[Give] Binary Semaphore Give Success \n");
} else {
printf1("[Give] Binary Semaphore Give Failed \n");
}
}
}
/* -------------------- 任务2:等待信号量任务 -------------------- */
void TaskTake(void *Argument) {
while (1) {
printf1("[Take] Waiting Semaphore... \n");
BaseType_t Ret = xSemaphoreTake(BinarySem, portMAX_DELAY);
if (Ret == pdTRUE) {
printf1("[Take] Got Semaphore! \n");
}else{
printf1("[Take] Got Semaphore failed! \n");
}
// 获取信号处理后,延时5s
vTaskDelay(5000);
}
}
int main(void) {
DebugUSART1_Init();
/* 创建二值信号量 */
BinarySem = xSemaphoreCreateBinary();
if (BinarySem == NULL) {
printf1("Binary Semaphore Create Failed! \n");
while (1) {}
}
/*
* 刚创建出来的二值信号量默认是空的,Take 会阻塞等待
* 释放信号量的任务优先级更高,只要不阻塞,会获取CPU执行权释放信号
*/
xTaskCreate(TaskGive,
"TaskGive",
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 2,
NULL);
xTaskCreate(TaskTake,
"TaskTake",
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 1,
NULL);
vTaskStartScheduler();
while (1) {}
}
通过该示例,可以观察到二值信号量具有以下几个特点:
- 二值信号量只有两种状态:可用(1)或不可用(0)。
- 刚创建出来的二值信号量默认是空的(不可用状态)。因此如果任务立即执行 xSemaphoreTake(),任务会进入阻塞等待。
- 当信号量为空时,执行 xSemaphoreTake() 的任务会被阻塞。任务会从就绪态进入阻塞态,等待信号量被释放。
- 当其他任务执行
xSemaphoreGive()释放信号量后,等待该信号量的任务会被唤醒继续运行。 - 二值信号量最多只能保存一个"信号"。如果信号量已经处于可用状态,再次执行 xSemaphoreGive() 会失败。
- 二值信号量常用于实现任务同步或事件通知,而不是用于传递数据。
串口接收数据的优化
在前面使用 FreeRTOS 消息队列 的例子中,我们已经实现了这样一个功能:
通过 USART1 串口接收数据,再由任务从消息队列中取出数据进行解析,最终控制 LED 的点亮与熄灭。
这个程序在正常情况下当然是没有问题的。
但是现在,我们还需要进一步思考一个问题:如果系统运行过程中出现了异常情况怎么办?
比如说:
- USART1 接收中断持续不断地收到数据,发送到消息队列的速度太快
- 负责接收队列数据的任务,因为某些原因没有及时运行
- 消息队列中的数据越积越多,最终把队列塞满
- 此时中断中继续向队列发送数据,就会发送失败,导致数据丢失
这就是一种很典型的 系统运行时异常。
如果出现这些异常情况,怎么办呢?
这里要注意:
我们现在暂时先不去讨论:
出现异常以后,到底该怎么恢复、怎么补救、怎么重试。
因为在真正处理异常之前,还有一个更基础、更重要的问题:
程序必须先知道异常已经发生了。
如果程序连异常发生了都不知道,后面自然也就谈不上做任何处理。
所以这里的第一步,不是"处理异常",而是:
先把异常上报出来,通知系统:现在出问题了。
而在 FreeRTOS 中,二值信号量 就非常适合完成这样的工作。
因为二值信号量本身并不负责传递具体数据, 它更适合表达这样一种含义:
某个事件发生了,请通知另外一个任务立即去处理。
这里的"事件",就可以是:
消息队列已满,串口中断发送队列失败,系统发生了异常。
因此,我们完全可以这样设计:
- USART1 中断 仍然负责接收串口数据,并尝试把数据发送到消息队列中
- 如果发送成功,说明一切正常,程序继续运行
- 如果发送失败,说明消息队列可能已经满了,也就是系统发生了异常情况
- 此时中断中不直接做复杂处理,而是 释放一个二值信号量
- 随后,由一个 更高优先级的异常处理任务 去等待这个二值信号量
- 一旦信号量被释放,说明系统出现了异常,该任务就会被立即唤醒,进行后续处理
这样一来,程序的结构就会变得比较清晰:
消息队列负责传递正常数据,二值信号量负责上报异常事件。
也就是说:
消息队列传数据,信号量传异常通知。
下面给出代码示例:
USART.h头文件:
c
#ifndef __USART1_H__
#define __USART1_H__
#include "stm32f10x.h"
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include "FreeRTOS.h"
#include "queue.h"
#include "semphr.h"
/* main.c 里创建的队列句柄 */
extern QueueHandle_t UartRxQueue;
extern SemaphoreHandle_t UartErrSem;
/**
* @brief 初始化串口 USART1
*/
void USART1_Init(void);
/**
* @brief 调试打印函数(串口版 printf)
*/
void printf1(const char *format, ...);
#endif
USART.c源文件:
c
#include "USART1.h"
/* ================= 内部函数(不对外暴露) ================= */
/**
* @brief USART1 发送单字节(阻塞式)
* @note 仅供本文件内部使用
*/
static void DebugUSART1_SendByte(uint8_t Byte) {
/* 等待发送数据寄存器为空 */
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, Byte);
}
/**
* @brief 初始化 USART1 串口
*/
void USART1_Init(void) {
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
/* PA9 -> USART1_TX:复用推挽输出 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* PA10 -> USART1_RX:上拉输入 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* USART 参数配置:115200, 8N1 */
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, ENABLE);
// 以下为USART1全局中断配置
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
// 使用FreeRTOS时
// 中断优先级一律建议采用分组4,即4位优先级寄存器全部配置给抢占优先级
// 抢占优先级设置为12,不建议高于11(可以设置为11)
// 原因请看文档或者询问老师
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 12;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
USART_Cmd(USART1, ENABLE);
}
/**
* @brief 调试打印函数(串口版 printf)
* @param format: 格式化字符串,和printf函数的第一个参数完全一致
* @note
* 1. 用法与 printf 类似
* 2. 实际输出通过 USART1 完成
* 3. 用于 FreeRTOS 任务调试与运行观测
*/
void printf1(const char *format, ...) {
char buffer[100];
va_list list;
va_start(list, format);
vsprintf(buffer, format, list);
va_end(list);
for (uint16_t i = 0; buffer[i] != '\0'; i++) {
if (buffer[i] == '\n') {
DebugUSART1_SendByte('\r');
}
DebugUSART1_SendByte((uint8_t)buffer[i]);
}
}
/**
* @brief USART1中断:收到 1 字节就塞进 FreeRTOS 队列
*/
void USART1_IRQHandler(void) {
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) {
char ch = (char)USART_ReceiveData(USART1);
BaseType_t xFlag = pdFALSE;
BaseType_t Ret = xQueueSendFromISR(UartRxQueue, &ch, &xFlag);
if (Ret != pdPASS) {
/* 通知高优先级异常处理任务 */
if (UartErrSem != NULL) {
xSemaphoreGiveFromISR(UartErrSem, &xFlag);
}
}
// 如果xFlag在上述函数中被设置为pdTRUE
// 则下面的宏函数会触发PendSV,使得ISR结束后触发任务切换
portYIELD_FROM_ISR(xFlag);
}
}
main.c源文件:
c
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"
#include "LED.h"
#include "USART1.h"
#define BUFFER_SIZE 20
QueueHandle_t UartRxQueue;
SemaphoreHandle_t UartErrSem = NULL;
/* -------------------- 任务:接收并解析串口命令 -------------------- */
void TaskRecv(void *Argument) {
char Buffer[BUFFER_SIZE] = { 0 }; // 局部变量必须手动初始化0值
uint8_t BufferIndex = 0;
while (1) {
char ch;
/* 无限阻塞等待接收队列数据,也就是等待串口发数据 */
if (xQueueReceive(UartRxQueue, &ch, portMAX_DELAY) == pdPASS) {
/* 如果接收到的是换行符相关的,直接丢弃 */
if (ch == '\r' || ch == '\n') {
continue;
}
/* 拼接字符串,永远保证 '\0' 结尾 */
if (BufferIndex < BUFFER_SIZE - 1) {
Buffer[BufferIndex++] = ch;
Buffer[BufferIndex] = '\0';
} else {
// 如果接收过长的数据导致Buffer溢出
// 那么多余数据直接用空字符拼在Buffer末尾
Buffer[BUFFER_SIZE - 1] = 0;
}
if (strcmp(Buffer, "OK") == 0) {
printf1("Light On \r\n");
LED_On();
memset(Buffer, 0, sizeof(Buffer));
BufferIndex = 0;
} else if (strcmp(Buffer, "ERROR") == 0) {
printf1("Light Off \r\n");
LED_Off();
memset(Buffer, 0, sizeof(Buffer));
BufferIndex = 0;
}
/* 非法指令输入,清空Buffer */
if (BufferIndex > 0 && Buffer[0] != 'O' && Buffer[0] != 'E') {
memset(Buffer, 0, sizeof(Buffer));
BufferIndex = 0;
}
/* 长度超过 ERROR(5) 就清空Buffer */
if (BufferIndex > 5) {
memset(Buffer, 0, sizeof(Buffer));
BufferIndex = 0;
}
}
}
}
/* -------------------- 任务2:异常处理任务 -------------------- */
void TaskUartError(void *Argument) {
while (1) {
/* 阻塞等待异常事件通知 */
if (xSemaphoreTake(UartErrSem, portMAX_DELAY) == pdTRUE) {
printf1("Error: USART RX Queue Overflow! \n");
/*
* 这里可以继续扩展异常处理逻辑,例如:
* 1. 点亮告警灯
* 2. 让蜂鸣器工作
* 其他处理....
*/
}
}
}
int main(void) {
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
LED_Init();
USART1_Init();
/* 创建消息队列:长度 20,每个元素 1 字节 */
UartRxQueue = xQueueCreate(20, sizeof(char));
if (UartRxQueue == NULL) {
printf1("Error: UartRxQueue create failed. \n");
while (1) {}
}
/* 创建二值信号量 */
UartErrSem = xSemaphoreCreateBinary();
if (UartErrSem == NULL) {
printf1("Error: UartErrSem create failed. \n");
while (1) {}
}
xTaskCreate(TaskRecv,
"TaskRecv",
configMINIMAL_STACK_SIZE + 128,
NULL,
tskIDLE_PRIORITY + 1,
NULL);
/* 高优先级异常处理任务 */
xTaskCreate(TaskUartError,
"TaskUartError",
configMINIMAL_STACK_SIZE + 128,
NULL,
tskIDLE_PRIORITY + 2,
NULL);
vTaskStartScheduler();
while (1) {}
}
如此,我们就在原本 "串口中断 + 消息队列 + 接收任务解析" 的基础上,又补充了一层 异常事件通知机制。
整个程序的运行逻辑,就可以概括为下面这样:
- USART1 接收到 1 个字节数据,进入接收中断
- 中断中尝试将该字节发送到
UartRxQueue消息队列 - 如果发送成功,说明系统当前处理能力正常,程序继续按照原来的逻辑运行
- 如果发送失败,说明消息队列可能已经满了,也就是系统当前出现了异常情况
- 此时中断中不去直接做复杂处理,而是释放二值信号量
UartErrSem - 高优先级任务
TaskUartError一直阻塞等待该信号量 - 一旦信号量被释放,说明异常发生,
TaskUartError会被立即唤醒并执行相应处理逻辑
这样写有一个非常明显的好处:
中断中只负责"发现异常并上报异常",而不负责"详细处理异常"。
计数信号量(了解)
除了二值信号量,FreeRTOS 还提供 计数信号量(Counting Semaphore) 作为一种任务间通信的手段。
计数信号量在实际项目中 使用频率并不算高,一般了解其原理和基本使用方式即可。
从功能上来说,计数信号量大体可以实现两类用途:
- 计数功能,释放一次信号,计数器的值就会 + 1:
- 比如某个中断每触发一次,就释放一次信号量,那么信号量的计数值就可以用来表示"事件发生了多少次"。
- 但这种用途完全可以用消息队列来实现,而且消息队列还能携带实际数据,比计数信号量好用。
- 因此,在实际工程中,如果只是为了统计事件次数,通常直接使用消息队列即可。
- 计数信号量真正比较适合的场景,是:系统中存在多个相同资源,而且存在多个任务同时竞争,此时需要进行资源数量管理。
例如:
系统中有多个缓冲区Buffer,此时多个任务都可能申请这些资源。
如果资源已经被全部占用,那么新的任务就需要 等待资源释放。
在这种场景下,就可以使用计数信号量来管理资源数量。
因此可以总结为一句话:
计数信号量的核心作用是:管理"多个相同资源"的数量。
理解这一点之后,计数信号量的使用方式其实就非常简单了。
而且:
计数信号量对于 "多个相同资源" 的管理问题,具有非常独特的作用,可以很好地表示 当前系统中还剩多少可用资源。
FreeRTOS 的其他任务间通信机制,一般很难直接实现类似的功能。(多任务竞争多个资源)
但可惜的是:
FreeRTOS更适用于资源紧张的嵌入式环境中,这种多资源竞争管理的场景,确实很少见。
连带着,计数信号量也就不太常用了。
但如果真的涉及到这种场景,还是可以考虑使用计数信号量。
计数信号量的本质
计数信号量和二值信号量,本质上是同一种东西。
甚至可以说:
计数信号量只是"容量更大的二值信号量"。
在 FreeRTOS 中:
所有的信号量,本质上都是基于消息队列实现的,计数信号量当然也不例外。
计数信号量在创建时,最终依然会调用xQueueGenericCreate()函数。
如下图所示:

这意味着:
- 计数信号量在底层仍然是一个消息队列结构体(Queue_t / xQUEUE)。
- 队列长度不再是 1,而是 uxMaxCount,元素大小仍然是 0
举个例子:
在创建计数信号量时,我们指定uxMaxCount的值是5,那么实际上等价于:
创建一个长度为 5 的消息队列,每个元素大小为 0 字节。
由于元素大小为 0 字节 ,因此这个队列仍然 不存储任何实际数据。
但是:
队列中"元素的个数",就可以表示当前信号量的计数值。
这个数量,仍然保存在 Queue 控制结构体 中的成员:
c
uxMessagesWaiting
因此:
计数信号量的计数值,其实就是uxMessagesWaiting的当前取值。
从底层结构来看,可以用一句话总结二值信号量和计数信号量:
- 二值信号量,本质是一个长度为1的"0字节消息队列",用uxMessagesWaiting的1和0表示两种不同的状态。
- 计数信号量,本质是一个长度为N的"0字节消息队列",把uxMessagesWaiting的具体值作为一个计数器。
理解了这一点之后,下面来学习计数信号量的操作API就非常简单了。
计数信号量的操作API
首先,计数信号量机制在 FreeRTOS 中是 可裁剪配置的功能。
它的使用依赖于下面这个宏是否开启:
c
#define configUSE_COUNTING_SEMAPHORES 1
如果该宏被设置为 1,则表示 开启计数信号量功能。
如果该宏为 0,则表示 关闭计数信号量功能。
需要注意的是:
这个宏的默认值是 0,也就是说:计数信号量在 FreeRTOS 中默认是关闭的任务间通信机制。
如果需要在工程中使用计数信号量,就需要手动在 FreeRTOSConfig.h 文件中显式开启该宏。
现在,你已经知道计数信号量和二值信号量的区别:
计数信号量无非就是消息队列元素数量是N,uxMessagesWaiting的最大值是N的二值信号量。
所以计数信号量的操作 API 与二值信号量基本一致,只是 创建函数不同。
创建计数信号量xSemaphoreCreateCounting()
创建计数信号量的函数:xSemaphoreCreateCounting()
其函数原型如下:
c
SemaphoreHandle_t xSemaphoreCreateCounting(
UBaseType_t uxMaxCount, // 底层消息队列的元素数量,也就是信号量允许的最大计数值,也就是uxMessagesWaiting的最大值
UBaseType_t uxInitialCount // 计数值的初始值,也就是uxMessagesWaiting的初始值
);
举一个例子:
c
SemaphoreHandle_t Sem;
Sem = xSemaphoreCreateCounting(5, 5);
函数的返回值和二值信号量并无不同,都是返回信号量句柄。
表示:
- uxMessagesWaiting成员的最大值是5
- uxMessagesWaiting成员的初始值是5
如果用计数信号量来表示可用资源数量:
- 当前最大可用资源数量为 5
- 当前可用资源数量为 5
获取信号量:xSemaphoreTake()
用于 获取信号量(表示获取消耗一个资源)。
该函数和二值信号量的获取信号是同一个函数:
c
BaseType_t xSemaphoreTake(
SemaphoreHandle_t xSemaphore, // 要操作的信号量句柄
TickType_t xTicksToWait // 最大等待阻塞时间
);
返回值:
- pdTRUE,获取信号成功(资源获取成功)
- pdFALSE,获取信号失败(在规定时间内,获取资源失败)
对于计数信号量来说,xSemaphoreTake()函数调用一次,本质上是:uxMessagesWaiting--
释放信号量:xSemaphoreGive()
用于 释放信号量(表示释放,结束占用一个资源)。
该函数和二值信号量的释放信号是同一个函数:
c
BaseType_t xSemaphoreGive(
SemaphoreHandle_t xSemaphore // 要操作的信号量句柄
);
返回值:
- pdTRUE,释放信号成功(资源释放成功,解除占用)
- pdFALSE,释放信号失败(对于资源管理来说,只要正确编写代码不存在消息队列满,导致资源释放失败的情况,因为只要总是先占用再释放)
对于计数信号量来说,xSemaphoreGive()函数调用一次,本质上是:uxMessagesWaiting++
在中断环境释放信号量:xSemaphoreGiveFromISR()
用于 在中断中,释放信号量(表示释放,结束占用一个资源)。
该函数和二值信号量的在中断中释放信号是同一个函数:
c
BaseType_t xSemaphoreGiveFromISR(
SemaphoreHandle_t xSemaphore, // 要操作的二值信号量句柄
BaseType_t *pxHigherPriorityTaskWoken // 是否需要在ISR结束后,触发高优先级任务切换
);
返回值:
- pdTRUE,释放成功(资源释放成功,解除占用)
- pdFALSE,释放失败(对于资源管理来说,只要正确编写代码不存在消息队列满,导致资源释放失败的情况)
对于计数信号量来说,xSemaphoreGiveFromISR()函数调用一次,本质上是:uxMessagesWaiting++
只不过是在中断环境下完成这个事情。
计数信号量在资源管理上的约定
在使用计数信号量进行资源管理时,有一个非常重要的约定:
计数信号量的数值表示的是"当前可用资源数量"。
而不是:当前已使用资源数量。
因此在资源管理场景中,应当遵循如下规则:
- xSemaphoreCreateCounting(3, 3) → 共有3个可用资源,且一开始都处于可用状态。uxMessagesWaiting的初始值是3
- xSemaphoreTake() → 获取信号量 → 获取占用一个资源 → uxMessagesWaiting--
- xSemaphoreTake() → 释放信号量 → 释放,解除占用一个资源 → uxMessagesWaiting++
一旦计数变为:0
即说明:系统已经没有可用资源
新的任务调用 xSemaphoreTake() 想要获取资源时,就会 阻塞等待资源释放,如此就实现了对资源的管理。
代码示例
如果系统中有些资源需要多任务竞争使用,那么我们就可以使用 计数信号量 来实现这种资源管理场景。
参考代码示例如下:
c
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "DebugUSART1.h"
/* 计数信号量:表示可用资源个数 */
SemaphoreHandle_t ResourceSem;
/* -------------------- 通用任务函数 -------------------- */
void TaskResource(void *argument) {
char *taskName = (char *)argument;
while (1) {
/* 循环尝试获取资源 */
while (xSemaphoreTake(ResourceSem, 500) != pdTRUE) {
printf1("%s waiting resource\r\n", taskName);
}
printf1("%s get resource\r\n", taskName);
/* 模拟占用资源 */
vTaskDelay(10000);
printf1("%s release resource\r\n", taskName);
/* 释放资源 */
xSemaphoreGive(ResourceSem);
/* 稍微延时一下,方便观察现象 */
vTaskDelay(1000);
}
}
/* -------------------- main -------------------- */
int main(void) {
DebugUSART1_Init();
/*
* 创建计数信号量
* 最大计数值 = 2
* 初始计数值 = 2
* 表示系统当前有 2 个可用资源
*/
ResourceSem = xSemaphoreCreateCounting(2, 2);
if (ResourceSem == NULL) {
while (1);
}
xTaskCreate(TaskResource,
"TaskA",
configMINIMAL_STACK_SIZE,
"TaskA",
tskIDLE_PRIORITY + 1,
NULL);
xTaskCreate(TaskResource,
"TaskB",
configMINIMAL_STACK_SIZE,
"TaskB",
tskIDLE_PRIORITY + 1,
NULL);
xTaskCreate(TaskResource,
"TaskC",
configMINIMAL_STACK_SIZE,
"TaskC",
tskIDLE_PRIORITY + 1,
NULL);
vTaskStartScheduler();
while (1);
}
注:
如果printf1输出结果很乱,可以使用临界区加锁保障输出时不会发生任务切换。
通过这个示例可以清楚地看到:计数信号量的核心作用,就是管理"有限数量的资源"。
但这种场景,并不多见,了解即可。
互斥信号量
在之前的学习中,我们已经学习过 临界区(Critical Section)。
临界区的作用是:
在一小段代码执行期间,暂时禁用低优先级中断或禁止任务调度。
典型形式如下:
c
taskENTER_CRITICAL();
/* 访问共享资源 */
taskEXIT_CRITICAL();
通过这种方式,可以确保:
在执行临界区代码期间,不会发生任务切换,也不会被低优先级中断打断。
这样就可以保证 共享资源访问的原子性,从而保证线程安全。
但是,临界区也存在明显的局限性。
临界区的特点是:执行期间 CPU 不能被打断,无论中断亦或者其他任务,都无法获得CPU执行权。
基于这样的特点,临界区的使用必须做到:
临界区的代码必须 尽快执行完成并退出,避免持续占用CPU导致破坏系统的实时性,甚至整个系统运行出现问题。
因此:
临界区只能用于保护非常短小的、执行迅速的代码片段。
但在实际系统中,还有另一种情况:某个资源需要被任务长时间占用。
例如:
- 串口发送数据
- 访问 I2C 总线
- 访问 SPI Flash
- 操作 OLED(本质上还是通信)
- ...
这些操作往往需要:
- 几百微秒
- 几毫秒
- 甚至更长时间
如果使用临界区保护这些操作,就会出现严重问题:整个系统在这段时间内 无法进行任务切换,也不能响应中断。
这显然是不可接受的。
因此,FreeRTOS 提供了另一种机制:
互斥信号量(Mutex)。
互斥信号量的核心思想是:
允许任务在等待资源时进入阻塞态,而不是一直占用 CPU。
当任务获取互斥信号量时:
- 如果共享资源空闲,任务会立即获得资源
- 如果资源被其他任务占用,当前任务会进入 阻塞态,主动放弃CPU执行权。
- 当共享资源释放后,阻塞任务会被唤醒。
这样:
在共享资源被占用的时候,CPU无需等待资源释放,还可以继续执行其他任务。
系统的 实时性和效率 都不会受到影响。
互斥信号量的本质
互斥信号量,本质上是一种特殊的二值信号量。
在 FreeRTOS 中:
互斥信号量同样是基于 消息队列机制实现的。
在创建互斥信号量时,内部最终同样会调用:
c
xQueueGenericCreate()
源码如下图所示:

因此,从底层实现结构来看:
互斥信号量和二值信号量在消息队列层面是完全一致的。
具体表现为:
- 队列长度为 1
- 队列元素大小为 0 字节
同时,互斥信号量的状态同样是通过队列结构体中的成员 uxMessagesWaiting 的取值来表示。
从当前这些内容来看,互斥信号量和二值信号量几乎没有区别。
但是需要注意:
虽然二者使用的是同一个成员变量 uxMessagesWaiting,但在两种信号量中的 语义含义是完全不同的。
在 二值信号量 中:
成员 uxMessagesWaiting 的取值表示 信号是否存在:
- 当
uxMessagesWaiting = 1时,表示 信号存在 - 当
uxMessagesWaiting = 0时,表示 信号不存在
而在 互斥信号量 中:
成员 uxMessagesWaiting 的取值表示 资源是否被占用:
- 当uxMessagesWaiting = 1时,表示 互斥信号量可用(即资源空闲)
- 当uxMessagesWaiting = 0时,表示 互斥信号量已被占用(即资源不可用)
因此:
当某个任务成功获取互斥信号量后,uxMessagesWaiting 会从 1 变为 0,表示资源已经被占用。
此时如果其他任务再次尝试获取该互斥信号量:
由于信号量已经不可用,这些任务就会进入 阻塞状态,等待资源释放。
只有当当前持有资源的任务调用 xSemaphoreGive() 释放互斥信号量后,其他等待的任务才有机会再次获取该资源。
因此,互斥信号量的主要作用是:
当多个任务需要访问同一个共享资源时,通过互斥信号量保证同一时刻只有一个任务能够访问该资源,从而实现对共享资源的线程安全访问。
从这个角度出发,在很多情况下,我们也会把 互斥信号量 直接称为:
FreeRTOS 中的"互斥锁"(Mutex Lock)。
当某个任务成功获取互斥信号量后,就相当于 获得了这把锁,并且 独占对应的共享资源。
只要该任务 不主动释放这把锁,其他任务就无法获取该互斥信号量,也就无法进入对共享资源的访问代码。
互斥锁可以保证某一段代码在同一时刻只会被一个任务执行。
这段被保护的代码区域,也可以称之为 临界区(Critical Section)。(思考一下和之前的临界区有啥区别)
例如,下列伪代码就展示了多个任务通过互斥锁访问共享资源的过程:
c
// 任务A
获取互斥锁 // 成功获取锁,独占共享资源
.... // 对共享资源进行访问
释放互斥锁 // 资源使用完毕,释放锁
// 任务B
获取互斥锁 // 若此时锁已被任务A占用,则任务B会阻塞等待
.... // 成功获取锁后,访问共享资源
释放互斥锁
当 任务A获取互斥锁后 ,如果 任务B尝试获取该锁 ,由于资源已经被占用,任务B就会进入 阻塞状态,直到任务A释放互斥锁为止。
反之亦然。
互斥锁的实现原理
上面我们讲到:
互斥信号量本质上可以看作是一种 互斥锁。
如果某个任务已经获取了互斥锁,那么:
其余任务再次尝试获取该锁时,就无法成功获取锁,只能 进入阻塞状态,等待锁被释放。
但是这里就会产生一个非常重要的问题:
如果互斥信号量只是一个简单的二值信号量结构,那么它显然无法实现真正的"互斥锁"机制。
原因其实很简单:
系统必须知道:这把锁当前到底是被哪个任务持有。
如果系统不知道锁的持有者是哪个任务,那么系统甚至无法判断当前任务是否持有锁,无法决定当前任务能否访问共享资源。
因此,在 FreeRTOS 中:
互斥信号量在普通信号量的基础上,额外增加了用于记录锁持有者的信息。
在源码中可以看到如下结构:


其中最关键的成员是:xMutexHolder
它用于记录:当前持有互斥锁的任务句柄(TaskHandle_t)。
此后,如果其他任务再次尝试获取该互斥锁,而锁已经被占用,则该任务会进入阻塞状态,等待锁释放。
下面总结一下互斥锁的实现原理:
- 通过 uxMessagesWaiting 表示互斥锁的占用状态。
- 当
uxMessagesWaiting = 1时,表示锁未被占用(资源可用); - 当
uxMessagesWaiting = 0时,表示锁已被占用(资源不可用)。
- 当
- 通过 xMutexHolder 记录当前持有互斥锁的任务。
- 系统可以根据该成员判断 锁当前的持有者是谁,从而确定当前任务是否具有共享资源的访问权。
- 如果当前任务不具有共享资源的访问权,那么当前任务会进入阻塞状态,主动下CPU。
可以看到,二值配合,才真正实现了 互斥锁的机制。
优先级反转问题
FreeRTOS是一种多任务环境,优先级抢占式实时操作系统。
这意味着,正常情况下,优先级越高的任务越先执行,而且只要该任务不主动放弃CPU,那么它会一直执行下去。
但系统中一旦引入互斥信号量,使用互斥锁,就不可避免的会带来------优先级反转问题(Priority Inversion)
什么是优先级反转?
高优先级任务反而被低优先级任务"间接阻塞",明明高优先级任务处于就绪态,却始终无法上CPU,这种异常现象就是优先级反转。
下面具体来看一个场景(只是为了演示优先级反转问题,实际上FreeRTOS中不会这么运行)。
假设系统中有三个任务:
- TaskHigh(高优先级5)
- TaskMedium(中优先级3)
- TaskLow(低优先级1)
同时有一个共享资源,例如 串口,使用互斥信号量进行保护。
系统执行过程可能如下:
第一步:
系统启动时,只有低优先级任务 TaskLow 处于就绪态,其余任务还没有进入就绪状态。
于是低优先级的任务,先执行。
于是低优先级的任务,获取互斥锁,共享资源被TaskLow任务使用。
第二步:
高优先级任务 TaskHigh 变为就绪态,并尝试获取同一个 互斥锁。
但是:
互斥锁 已经被 TaskLow 占用,且没有被释放。
因此:
TaskHigh 只能进入阻塞态等待。
第三步:
这时 TaskMedium 变为就绪态。
由于:
TaskMedium 的优先级 高于 TaskLow ,因此调度器会让 TaskMedium 运行。
而且任务 TaskMedium 不会主动放弃CPU,它会一直运行下去。
此时整个系统中的三个任务,它们的运行情况是:
- TaskHigh → 等待获取互斥锁,等待共享资源(阻塞)
- TaskMedium → 一直运行,一直占着CPU不放。
- TaskLow → 处于就绪态,但优先级低,无法获取CPU,没有执行权。
结果就是:
真正持有互斥锁的 TaskLow 无法运行,自然也无法释放 Mutex。
而 TaskHigh 又必须等待 Mutex。
于是就出现了一种奇怪的情况:高优先级任务,反而被中优先级任务间接阻塞了,高优先级的任务反而没有机会执行了。
这就是:优先级反转问题。
简单一句话总结:
高优先级任务因为资源竞争,被更低优先级任务间接阻塞执行。
这种情况显然是不合理的,那么如何解决这种优先级反转问题呢?
互斥信号量的重要机制:优先级继承
为了解决 优先级反转问题,FreeRTOS 在 互斥信号量 中实现了一种重要机制:优先级继承(Priority Inheritance)。
其核心思想其实非常简单:
当高优先级任务因等待互斥锁而进入阻塞状态时,系统会 临时提升当前锁持有任务的优先级,
使其优先级 提升到与等待该锁的最高优先级任务相同。
我们仍然用刚才的三个任务来举例:
- TaskHigh(优先级5)
- TaskMedium(优先级3)
- TaskLow(优先级1)
系统的真实运行过程如下:
第一步:
系统启动时,只有低优先级任务 TaskLow 处于就绪态,其余任务还没有进入就绪状态。
于是低优先级的任务,先执行。
于是低优先级的任务,获取互斥锁,共享资源被TaskLow任务使用。
第二步:
高优先级任务 TaskHigh 变为就绪态,并尝试获取同一个 互斥锁。
但是:互斥锁 已经被 TaskLow 占用,且没有被释放。
因此:TaskHigh 只能进入阻塞态等待。
这时 FreeRTOS 会发现:
高优先级任务正在等待一个被低优先级任务占用的 互斥锁。
于是系统会执行 优先级继承机制:
临时把 TaskLow 的优先级提升到 TaskHigh 的优先级。
也就是说,原本的优先级关系是:
TaskHigh 5
TaskMedium 3
TaskLow 1
触发优先级继承后变成:
TaskHigh 5 (阻塞)
TaskLow 5 (临时提升)
TaskMedium 3
第三步:
此时即使 TaskMedium 进入就绪态,由于 TaskLow 的优先级已经被提升到 5 ,因此 TaskLow 会优先运行。
这样 TaskLow 执行完共享资源访问操作,并释放 互斥锁。
第四步:
互斥锁 释放后:
- TaskHigh 立即被唤醒
- TaskHigh 立刻抢占 CPU(优先级最高)
与此同时:
释放互斥锁后,TaskLow 的优先级会恢复为原来的优先级。
系统优先级关系恢复正常。
因此可以用一句话总结 优先级继承机制:
当高优先级任务等待互斥资源时,系统会临时提升资源持有者的优先级,使其尽快释放资源。
需要特别注意的一点是:
优先级继承只存在于互斥信号量中,其余信号量没有这样的机制。
所以我们可以得出一个结论:
如果需求是针对单一共享资源的保护,推荐使用互斥信号量。
互斥信号量的操作API
首先,互斥信号量机制在 FreeRTOS 中是 可裁剪配置的功能。
它的使用依赖于下面这个宏是否开启:
c
#define configUSE_MUTEXES 1
如果该宏被设置为 1,则表示 开启互斥信号量功能。
如果该宏为 0,则表示 关闭互斥信号量功能。
需要注意的是:
这个宏的默认值是 0,也就是说:互斥信号量在 FreeRTOS 中默认是关闭的任务间通信机制。
如果需要在工程中使用计数信号量,就需要手动在 FreeRTOSConfig.h 文件中显式开启该宏。
互斥信号量的API只需要学习一个新的。
创建互斥信号量:
c
SemaphoreHandle_t xSemaphoreCreateMutex(void);
该函数用于 创建一个互斥信号量(互斥锁),返回值是一个互斥信号量句柄。
需要注意的是:
互斥信号量在创建成功后,其内部状态默认是 可用状态,也就是:
c
uxMessagesWaiting = 1
表示当前没有任务占用该锁。
当任务需要访问共享资源时,可以调用如下函数 获取互斥锁:
c
BaseType_t xSemaphoreTake(
SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait
);
如果互斥锁当前 没有被其他任务占用,那么任务就可以成功获取锁,并进入临界区访问共享资源。
如果互斥锁 已经被其他任务持有 ,那么当前任务就会根据 xTicksToWait 的设置:
- 立即返回失败
- 或进入 阻塞状态等待锁释放
当任务使用完共享资源后,需要调用以下函数 释放互斥锁:
c
BaseType_t xSemaphoreGive(
SemaphoreHandle_t xSemaphore
);
释放互斥锁后,系统会:
- 将锁状态恢复为 可用
- 并唤醒等待该锁的任务
需要特别注意的是:
互斥锁必须由"锁的持有者任务"进行释放。
也就是说:
获取互斥锁的任务,必须由 同一个任务调用 xSemaphoreGive() 释放锁。
否则将会导致系统行为异常。
最后总结一下互斥信号量的基本使用流程:
c
创建互斥锁 (xSemaphoreCreateMutex)
↓
任务获取互斥锁 (xSemaphoreTake)
↓
访问共享资源
↓
释放互斥锁 (xSemaphoreGive)
通过这种方式,就可以在多任务环境下实现 对共享资源的互斥访问。
一个很重要的经验总结
到这里为止,三种常见的信号量机制:二值信号量、计数信号量、互斥信号量,我们已经全部学完了。
我们可以对 信号量相关的操作 API 做一个简单的总结。
有以下几条:
- 三种信号量的创建函数各不相同,但本质上都是创建消息队列,返回消息队列句柄。
- 释放信号量都使用xSemaphoreGive()函数:
- 对于二值信号量而言,释放信号量表示信号状态从无到有,从0到1。
- 对于计数信号量和互斥信号量而言,释放信号量表示资源被释放,可用资源增加。
- 本质上,都是uxMessagesWaiting++
- 获取信号量都使用xSemaphoreTake()函数:
- 对于二值信号量而言,获取信号量表示信号状态从有到无,从1到0。
- 对于计数信号量和互斥信号量而言,获取信号量表示资源被获取占用,可用资源减少。
- 本质上,都是uxMessagesWaiting--
- 牢牢记住:Give 表示释放,Take 表示获取,三种信号量使用同样的函数进行获取和释放。
记住这几点,在理解和使用信号量机制时,很多问题都会变得更加清晰。
优先级继承:代码演示
对于下列演示代码:
Buzzer.h头文件:
c
#ifndef __BUZZER_H
#define __BUZZER_H
#include "stm32f10x.h"
/* 初始化蜂鸣器 IO口接入PA0引脚 低电平工作 */
void Buzzer_Init(void);
/* 翻转蜂鸣器状态 */
void Buzzer_Toggle(void);
#endif
Buzzer.c实现源文件:
c
#include "Buzzer.h"
/**
* @brief 初始化蜂鸣器 GPIO
* @note 蜂鸣器接在 PA0,低电平响
*/
void Buzzer_Init(void) {
/* 1. 开启 GPIOA 时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
/* 2. 配置 PA0 为推挽输出 */
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* 默认关闭蜂鸣器 */
GPIO_SetBits(GPIOA, GPIO_Pin_0);
}
/**
* @brief 翻转蜂鸣器状态
*/
void Buzzer_Toggle(void) {
if (GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_0) == Bit_SET) {
GPIO_ResetBits(GPIOA, GPIO_Pin_0);
} else {
GPIO_SetBits(GPIOA, GPIO_Pin_0);
}
}
main.c源文件:
c
#include "stm32f10x.h"
#include "Delay.h"
#include "Buzzer.h"
#include "DebugUSART1.h"
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
/* 串口互斥信号量 */
SemaphoreHandle_t UartMutex;
/*
任务优先级关系:
TaskLow 低优先级,先获取互斥锁
TaskMedium 中优先级,模拟普通任务不断运行
TaskHigh 高优先级,后续尝试获取互斥锁
用于演示:
1. 低优先级任务先持有锁
2. 高优先级任务随后申请锁,被阻塞
3. 中优先级任务本来会抢占低优先级任务
4. 但由于使用的是 Mutex,具有优先级继承机制,低优先级任务会临时提升优先级
*/
/* -------------------- 低优先级任务:先持有互斥锁,使用共享串口 -------------------- */
void TaskLow(void *argument) {
while (1) {
printf1("\r\n[TaskLow] try take mutex\r\n");
/* 低优先级任务先获取互斥锁 */
xSemaphoreTake(UartMutex, portMAX_DELAY);
printf1("[TaskLow] take mutex\r\n");
/*
模拟持锁工作较长时间
这里故意分多次延时,
让调度器有机会切换到别的任务,
方便观察优先级继承现象。
*/
for (int i = 1; i < 6; i++) {
printf1("[TaskLow] working %d\r\n", i);
Delay_Ms(1000); // TIM2实现的延时,会占用CPU进行延时
}
printf1("[TaskLow] give mutex\r\n");
/* 释放互斥锁 */
xSemaphoreGive(UartMutex);
}
}
/* -------------------- 高优先级任务:后续申请互斥锁,使用共享串口 -------------------- */
void TaskHigh(void *argument) {
/*
先延时一段时间,
确保 TaskLow 先上CPU拿到锁
*/
vTaskDelay(500);
while (1) {
printf1("\r\n[TaskHigh] try take mutex\r\n");
/*
此时锁通常还在 TaskLow 手里,
所以 TaskHigh 会进入阻塞态等待。
*/
xSemaphoreTake(UartMutex, portMAX_DELAY);
printf1("[TaskHigh] take mutex\r\n");
for (int i = 1; i < 6; i++) {
printf1("[TaskHigh] working %d\r\n", i);
Delay_Ms(1000); // TIM2实现的延时,会占用CPU进行延时
}
printf1("[TaskHigh] give mutex\r\n");
xSemaphoreGive(UartMutex);
// 延时主动让出CPU
// 否则高优先级任务会一直获取锁
vTaskDelay(1000);
}
}
/* -------------------- 中优先级任务:不使用共享串口,使用一个蜂鸣器代表工作 -------------------- */
void TaskMedium(void *argument) {
/*
先延时一段时间,
确保 TaskLow 先上CPU拿到锁
*/
vTaskDelay(1000); // 注意这里的延时时间更长
while (1) {
/*
中优先级任务不访问共享串口资源,
只做蜂鸣器周期工作。
*/
Buzzer_Toggle();
vTaskDelay(200);
//Delay_Ms(200);
}
}
/* -------------------- main -------------------- */
int main(void) {
// 初始化串口和TIM2定时器
DebugUSART1_Init();
Delay_Init();
Buzzer_Init();
/* 创建互斥锁 */
UartMutex = xSemaphoreCreateMutex();
/* 创建低优先级任务 */
xTaskCreate(TaskLow,
"TaskLow",
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 1,
NULL);
/* 创建中优先级任务 */
xTaskCreate(TaskMedium,
"TaskMedium",
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 2,
NULL);
/* 创建高优先级任务 */
xTaskCreate(TaskHigh,
"TaskHigh",
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 3,
NULL);
/* 启动调度器 */
vTaskStartScheduler();
while (1);
}
其中Delay_Ms是前面定时器阶段讲过,写过的,基于TIM2定时器实现的CPU阻塞式忙等待延时。
也就是说:使用Delay_Ms进行延时,会占用CPU进行阻塞。
这段代码的设计是这样的:
- 高优先级和低优先级任务竞争同一个串口资源。
- 中等优先级的任务,控制蜂鸣器翻转工作。
通过这段代码,你可以自行测试一下互斥信号量的工作机制原理。
互斥信号量 VS 计数信号量
在 FreeRTOS 中,互斥信号量 和 计数信号量 在底层实现上其实非常相似,它们本质上都是基于 消息队列机制 实现的。
而且看起来,它们都和资源管理、保护有关系。
但实际上,两者有明显区别。
互斥信号量(Mutex) 用于 保护单一共享资源。
其核心特点是:
- 同一时刻 只允许一个任务获取资源
- 内核会记录 当前资源持有者
- 支持 优先级继承机制,用于解决优先级反转问题
因此,互斥信号量通常用于:
- 保护串口
- 保护 SPI / I2C 总线
- 保护 OLED 的操作
- 保护一个全局数据结构,比如一个全局共享的Buffer
本质上,它解决的是:多个任务竞争同一个资源的线程安全问题。
计数信号量(Counting Semaphore) 则用于 管理多个相同资源的数量。
其核心特点是:
- 同时允许 多个任务获取资源
- 资源数量由 计数值控制
- 没有 优先级继承机制
例如:
如果系统中有 3 个相同资源 ,那么最多允许 3 个任务同时获取资源,当资源耗尽时,新的任务必须等待。
因此,计数信号量解决的是:
多个任务竞争多个同类资源的问题。
可以用一句话简单总结两者的区别:
互斥信号量:保护一个共享资源。
计数信号量:管理多个同类型资源,避免在资源已经耗尽的情况下,任务仍然继续去抢占资源。
在嵌入式开发这种资源紧张的前提背景下,互斥信号量显然是更常用的。
互斥信号量是学习信号量的重点。
其余需要了解的细节
在 优先级反转 和 优先级继承 讲完之后,关于互斥信号量,其实还有几个比较实用的注意点,可以简单了解一下。
第一,互斥信号量必须由持有者释放
互斥信号量有一个基本规则:谁获取,谁释放。
例如:
c
TaskA xSemaphoreTake(Mutex);
TaskB xSemaphoreGive(Mutex); // 不允许,不允许
这种用法会导致系统误以为锁被释放,从而破坏互斥访问,可能导致A和B两个任务同时访问共享资源。
同时这种做法还会破坏优先级继承机制。
总之,互斥锁必须遵循 "谁加锁,谁解锁" 的原则。
第二,互斥信号量不能在中断中使用(重点!!!!!)。
互斥信号量 只能在任务环境中使用,不能在中断服务函数(ISR)中使用。
原因是:
互斥信号量会记录锁持有者的任务句柄,而中断显然不是任务,没有任务句柄。
而且互斥信号量涉及 优先级继承机制,而中断并不是任务,没有任务优先级,也不存在任务调度关系。
因此:不要在中断中尝试使用互斥信号量。
第三,递归和函数嵌套中使用互斥信号量(了解)。
FreeRTOS 还提供了一种特殊的互斥信号量:
递归互斥信号量(Recursive Mutex)
创建函数:
c
xSemaphoreCreateRecursiveMutex()
它允许 同一个任务多次获取同一个 Mutex,但释放时也必须释放相同次数。
通过下面的一个成员实现:

这种机制主要用于 函数嵌套调用时的资源保护,实际项目中使用较少,了解即可。
扩展问题:如果中断当中需要保护共享资源怎么办?
这是一个很好的问题,甚至很值得在面试中问一问。
在一个嵌入式系统中,共享资源大多是:
- 某个外设资源
- 某个缓冲区Buffer
- 某个全局变量
- ...
如果这些共享资源,需要同时被中断和任务访问,甚至被两个中断同时访问。
看起来,如果不采取任何保护机制,就非常有可能出现。
所以,理论上来说,中断访问共享资源时,也需要保护。
但从实际工程角度出来,我们不应该这么设计系统,也就是说:
在实际工程中,优良的设计是:不要在中断中处理复杂共享资源。
设计良好的嵌入式系统中,应当遵循一个原则:尽量不要让中断去操作复杂的共享资源。
具体来说:
- 中断只负责产生事件,产生信号,发送通知。
- 而不要让中断去处理事件,处理信号,处理通知。
- 中断只负责产生数据或事件,而任务负责消费和处理这些数据。
这样设计至少有三点好处:
- 中断可以更加短平快,执行时间更短,这符合中断的设计哲学。
- 避免复杂的同步问题,而不处理这些问题就可以更好的实现第一条。
- 系统实时性更好,这是第一条和第二条带来的好处。
FreeRTOS官方也更推荐使用这种设计方式。
也就是说:解决问题,不仅可以选择解决问题本身,还可以解决问题的提出者。
记住:系统设计远比单纯技术实现更重要。
当然,如果某些情况下 中断确实需要访问共享资源,那么也可以使用最简单的保护方法:
短暂关闭中断,也就是使用临界区。
c
// 中断ISR当中
UBaseType_t uxSavedInterruptStatus;
uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();
/* 访问共享资源 */
taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus );
但需要注意:临界区必须短平快。
所以总结来说:
如果中断中需要保护共享资源,可以使用临界区。
但更好的系统设计方式是:
让中断只产生事件,由任务去处理资源。
这样既简单,也更安全,也是 FreeRTOS 推荐的设计模式。
总结如下:
- 如果共享资源只是 全局变量或 Buffer 缓冲区 ,在中断中使用 临界区简单保护 一下通常问题不大。
- 但如果共享资源是 串口、I2C 等重量级通信外设 资源,一般 不建议在中断中直接处理数据。
- 更合理的做法是:中断只负责产生事件,任务负责处理数据。也就是:中断做通知,任务做处理。
临界区和互斥信号量的案例对比
如果是对 轻量级资源 的共享竞争,比如竞争某个全局变量、某个缓冲区等,此时使用 临界区 是完全可以的。
因为这类资源的临界区代码通常 非常短、执行很快。
虽然理论上临界区会 短时间禁止任务切换,对系统实时性有一定影响,但为了保证数据访问的正确性,这种开销是完全可以接受的。
但如果是对 重量级资源 的共享竞争,比如:
多个任务竞争 某个外设资源(串口、I2C、SPI 等)
此时就更建议使用 互斥信号量 来进行访问控制,从而提升整体系统的运行效率。
你可以这样理解这两种机制的区别:
- 临界区的含义是:在临界区代码执行期间,调度器被暂时锁定,其它任务无法进行任务切换,必须等待临界区执行结束。
- 互斥信号量的含义是:只有需要竞争该资源的任务才会等待锁,而不涉及该资源的任务仍然可以正常被调度执行。
再用一个更通俗的类比来理解:
- 临界区就像是:
- 所有人都必须排队竞争同一个资源
- 后面的人,都必须等待前面的人完成操作之后,才能得以运行。
- 如果每个人的操作都很快,那问题不大,即便后面的人也能很快得到资源。
- 但如果把每个人操作的时间都延长,那后面的人想要获取资源就非常困难了。
- 互斥信号量则像是:
- 愿意竞争资源的人就排队。
- 既然你愿意竞争资源,愿意排队,那么即便等得久一些也是你乐意的。
- 而不需要这个资源的人,可以继续去做自己的事情,不会受到影响。
经过多次讲解,相信大家已经对临界区和互斥信号量的区别,有了一定的了解。
下面我们再通过两段代码加深对它们区别的理解。
首先,我们仍然继续使用上面代码中写过的蜂鸣器模块代码。
随后我们在系统中,创建3个优先级一致的任务:
- 任务A和任务B竞争,同一个串口资源,进行串口输出。
- 任务C只负责翻转蜂鸣器状态。
- 由于三者优先级一致,采用时间片轮转调度,理论上蜂鸣器能够正常工作。
如果采用临界区来实现:
c
#include "stm32f10x.h"
#include "Delay.h"
#include "FreeRTOS.h"
#include "task.h"
#include "Buzzer.h"
#include "DebugUSART1.h"
/* -------------------- 任务Beep:周期性蜂鸣器节拍 -------------------- */
void TaskBeep(void *argument) {
while (1) {
/* 翻转蜂鸣器的工作状态 */
Buzzer_Toggle();
/* 间隔 200ms */
vTaskDelay(200);
}
}
/* -------------------- 任务A:发送大量串口数据 -------------------- */
void TaskA(void *argument) {
while (1) {
/*
进入临界区
这样做可以保证:
当前任务在整个打印期间,不会被其它任务打断,
从而避免 TaskA 和 TaskB 的输出内容交叉混乱。
但问题是:
临界区期间,系统调度会受到影响。
*/
taskENTER_CRITICAL();
printf1("---------- TaskA START ----------\n");
for (int i = 0; i < 20; i++) {
printf1("TaskA running %d : 1234567890 ABCDEFGHIJKLMNOPQRSTUVWXYZ\r\n", i);
}
printf1("---------- TaskA END ----------\n\n");
/* 退出临界区,恢复正常调度 */
taskEXIT_CRITICAL();
/* 延时一段时间 */
vTaskDelay(500);
}
}
/* -------------------- 任务B:发送大量串口数据 -------------------- */
void TaskB(void *argument) {
while (1) {
/*
TaskB 也采用临界区保护串口发送
*/
taskENTER_CRITICAL();
printf1("---------- TaskB START ----------\n");
for (int i = 0; i < 20; i++) {
printf1("TaskB running %d : abcdefghij klmnopqrstuvwxyz 1234567890\r\n", i);
}
printf1("---------- TaskB END ----------\n\n");
taskEXIT_CRITICAL();
vTaskDelay(500);
}
}
int main(void) {
DebugUSART1_Init();
Buzzer_Init();
/* 串口任务 */
xTaskCreate(TaskA,
"TaskA",
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 1,
NULL);
xTaskCreate(TaskB,
"TaskB",
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 1,
NULL);
/* 蜂鸣器节拍任务 */
xTaskCreate(TaskBeep,
"TaskBeep",
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 1,
NULL);
vTaskStartScheduler();
while (1);
}
在这个实现中:
蜂鸣器任务实际上 和串口资源毫无关系,它根本不需要参与串口资源的竞争。
但由于 临界区会锁定调度器,在任务A或任务B执行串口输出期间,其它任务都无法进行任务切换。
而串口打印本身是一个 相对耗时的操作,因此临界区会持续较长时间。
最终就会导致:
蜂鸣器任务无法按时获得CPU执行时间,表现为 断断续续、不规律、甚至卡顿式的工作状态。
如果采用互斥信号量来实现:
c
#include "stm32f10x.h"
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "DebugUSART1.h"
#include "Buzzer.h"
/* 串口互斥信号量句柄 */
SemaphoreHandle_t UartMutex;
/* -------------------- 任务A:发送大量串口数据 -------------------- */
void TaskA(void *argument) {
while (1) {
/*
申请串口资源
如果串口当前被其它任务占用,
当前任务会进入阻塞态等待,
不会一直占着 CPU 不放。
*/
xSemaphoreTake(UartMutex, portMAX_DELAY);
printf1("---------- TaskA START ----------\n");
for (int i = 0; i < 20; i++) {
printf1("TaskA running %d : 1234567890 ABCDEFGHIJKLMNOPQRSTUVWXYZ\r\n", i);
}
printf1("---------- TaskA END ----------\n\n");
/* 释放串口资源 */
xSemaphoreGive(UartMutex);
// vTaskDelay(500);
}
}
/* -------------------- 任务B:发送大量串口数据 -------------------- */
void TaskB(void *argument) {
while (1) {
/* 申请串口资源 */
xSemaphoreTake(UartMutex, portMAX_DELAY);
printf1("---------- TaskB START ----------\n");
for (int i = 0; i < 20; i++) {
printf1("TaskB running %d : abcdefghij klmnopqrstuvwxyz 1234567890\r\n", i);
}
printf1("---------- TaskB END ----------\n\n");
/* 释放串口资源 */
xSemaphoreGive(UartMutex);
// vTaskDelay(500);
}
}
/* -------------------- 任务Beep:周期性蜂鸣器节拍 -------------------- */
void TaskBeep(void *argument) {
while (1) {
/* 翻转蜂鸣器的工作状态 */
Buzzer_Toggle();
/* 间隔 200ms */
vTaskDelay(200);
}
}
/* -------------------- main -------------------- */
int main(void) {
/* 初始化硬件 */
DebugUSART1_Init();
Buzzer_Init();
/* 创建互斥信号量 */
UartMutex = xSemaphoreCreateMutex();
/* 创建任务A */
xTaskCreate(TaskA,
"TaskA",
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 1,
NULL);
/* 创建任务B */
xTaskCreate(TaskB,
"TaskB",
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 1,
NULL);
/* 创建蜂鸣器任务 */
xTaskCreate(TaskBeep,
"TaskBeep",
configMINIMAL_STACK_SIZE,
NULL,
tskIDLE_PRIORITY + 1,
NULL);
/* 启动调度器 */
vTaskStartScheduler();
while (1);
}
在这种实现方式中:
只有 任务A 和 任务B 需要竞争串口资源,因此它们会通过 互斥信号量 进行访问控制。
当某个任务获取不到互斥信号量时,它会 进入阻塞态等待,不会持续占用 CPU。
而 蜂鸣器任务并不参与串口资源的竞争,因此它不会被迫等待阻塞。
蜂鸣器任务仍然可以按照 时间片轮转调度 正常获得执行机会,从而保持稳定的节拍工作。
信号量总结
到这里,我们已经把 FreeRTOS 中最常用的三类信号量机制全部讲完了,分别是:
- 二值信号量(Binary Semaphore)
- 计数信号量(Counting Semaphore)
- 互斥信号量(Mutex)
虽然这三种机制在使用形式上比较相似,都是通过 Take / Give 进行操作,但它们在 设计目的和使用场景 上是有所区别的。
简单总结如下:
| 类型 | 本质 | 主要作用 |
|---|---|---|
| 二值信号量 | 长度为1、元素大小为0的消息队列 | 任务同步 / 事件通知 |
| 计数信号量 | 长度>1、元素大小为0的消息队列 | 管理同类型的多个资源 |
| 互斥信号量 | 记录持有者,实现优先级继承机制的特殊二值信号量 | 保护重量级共享资源 |
从使用角度来看,可以这样理解:
- **二值信号量,**常用于 任务同步,例如:中断通知任务、任务之间的事件触发。
- 计数信号量 ,用于 管理同类型的多个资源,例如:多个缓冲区、多个连接、多个设备资源等。
- 互斥信号量 ,用于 保护重量级共享资源,例如:串口、I2C、SPI 等外设资源访问控制。
不过在实际的嵌入式开发中,真正 最重要、也最常使用的,往往是 互斥信号量(Mutex),也就是我们常说的 互斥锁。
计数信号量 用于管理多个同类型资源,在嵌入式系统中这样的场景很少见,因此计数信号量的使用频率也很低。
而 二值信号量 在很多场景下,已经被 任务通知(Task Notification)机制 所替代。
这是 FreeRTOS 官方非常力推的一种任务间通信方式。
下一章节,我们就来详细看看 任务通知 是如何工作的。