一壶水为什么还没有"呜呜"叫?是因为还没烧开,烧开和呜呜叫是同步
运动员为什么还没有起跑?是因为他还没听见枪响信号,枪响信号和起跑是同步的
任务是解耦的,但又是相互同步的
一个任务的开始可能需要别的任务的支持
1. PV操作
1.1 概念
PV 操作是操作系统 / RTOS 中用于 进程 / 任务同步与互斥 的核心原语(不可被打断的操作),由荷兰计算机科学家 Dijkstra 提出,本质是通过 "信号量 "(Semaphore)的两个原子操作(P 操作、V 操作),解决多任务间的资源竞争和时序同步问题。
信号量
信号量是 PV 操作的 "载体",本质是一个整数变量 S,附加两个规则:
仅能通过 PV 操作修改 S(不允许任务直接读写);
当 S ≤ 0 时,执行 P 操作的任务会阻塞(等待资源);S > 0 时,任务可获取资源继续执行。
信号量分两类,对应 PV 操作的两种核心用途:
- 二进制信号量(S 仅为 0 或 1):用于 "互斥"(保护共享资源,同一时间仅 1 个任务访问);
- 计数信号量(S 为非负整数,如 0~255):用于 "同步"(控制任务执行顺序)或 "资源池管理"(如允许 3 个任务同时访问某资源)。
P 操作(申请资源)
中文含义:申请资源(Task 向系统请求一个资源);
操作逻辑:S = S - 1;
若 S ≥ 0:申请成功,任务继续执行;
若 S < 0:申请失败,任务进入阻塞态(加入等待队列),直到有其他任务执行 V 操作释放资源。
V 操作(释放资源)
中文含义:释放资源(Task 向系统归还一个资源);
操作逻辑:S = S + 1;
若 S > 0:释放成功,任务继续执行(无等待任务);
若 S ≤ 0:说明有任务在等待资源,唤醒等待队列中优先级最高的任务,让其获取资源执行。
1.2 用途
用途 1:实现互斥(保护共享资源)
cpp
Semaphore S = 1; // 二进制信号量,初始值1(串口资源可用)
// 任务1:串口打印
void task1(void) {
while(1) {
P(S); // 申请串口资源(S=1→0,申请成功)
printf("Task1: 访问串口\r\n"); // 共享资源操作
V(S); // 释放串口资源(S=0→1,资源归还)
delay(100);
}
}
// 任务2:串口打印
void task2(void) {
while(1) {
P(S); // 申请串口资源(若S=0,阻塞等待)
printf("Task2: 访问串口\r\n");
V(S); // 释放串口资源
delay(100);
}
}
两个任务不会同时访问串口,避免打印乱码(和互斥锁的核心作用一致,本质互斥锁就是二进制信号量的增强版)。
用途 2:实现同步(控制任务时序)
解决 "任务 A 必须在任务 B 完成某操作后才能执行" 的问题(如 "传感器采集数据完成后,才能处理数据"),本质是用 计数信号量(S=0)传递 "完成信号"。
cpp
Semaphore S = 0; // 计数信号量,初始值0(无数据可用)
// 任务A:数据处理(依赖任务B的采集结果)
void task_process(void) {
while(1) {
P(S); // 申请数据(S=0→-1,阻塞等待)
printf("数据处理完成\r\n"); // 采集完成后才执行
delay(200);
}
}
// 任务B:传感器采集
void task_collect(void) {
while(1) {
printf("传感器采集数据\r\n"); // 采集操作
V(S); // 释放数据(S=-1→0,唤醒任务A)
delay(1000); // 每秒采集1次
}
}
任务 A 不会 "提前执行",严格在任务 B 采集完成后才处理数据,实现时序同步。
1.3 PV 操作与 FreeRTOS API 的对应关系

- 二进制信号量、计数信号量、互斥锁的 xSemaphoreTake/xSemaphoreGive 本质都是 PV 操作;
- 互斥锁是 "增强版二进制信号量",额外支持 优先级继承(解决优先级反转),但 PV 操作的核心逻辑不变;
- 中断服务函数(ISR)中需用 "ISR 版本 API":xSemaphoreTakeFromISR/xSemaphoreGiveFromISR,本质也是 PV 操作。
2. IPC
裸机前后台是如何同步的?
前台大循环和后台中断
通常会使用全局变量flag实现前后台状态的同步
任务间的信息传递
和上面PV操作很像,全局变量就称为信号量
信号量被置为1的过程------SemaphoreGive
等待信号重置1的过程------SemaphoreTake
任务之间的通信同步,我们称之为IPC
IPC(Inter-Process Communication,进程间通信)是 多任务 / 多进程系统 中,不同任务 / 进程之间传递数据、同步行为的核心机制。在嵌入式场景(尤其是 FreeRTOS、RT-Thread 等 RTOS 中),IPC 更多是 "任务间通信"(因嵌入式多为单进程多任务),但核心思想与通用操作系统(Linux/Windows)一致。

3. 二值信号量、计数信号量
二值信号量(Binary Semaphore)和计数信号量(Counting Semaphore)是 FreeRTOS 中最基础、最常用的 IPC 同步机制,核心都是通过「信号量计数器」实现任务间协同,本质区别在于 计数器的取值范围------ 二值信号量仅支持 0/1,计数信号量支持 0~N(N 为最大计数,如 255)。
二值信号量------最简单的IPC工具
3.1 二值信号量:仅允许 1 个任务 "占用" 资源
3.1.1 核心特性
- 计数器 S 只有两个状态:0(资源被占用 / 无同步信号)、1(资源空闲 / 有同步信号);
- 核心用途:两种场景(互斥 / 同步),但更适合「简单同步」(互斥场景优先用互斥锁,支持优先级继承);
- 关键规则:同一时间仅 1 个任务能获取信号量(S=1→0),释放后(S=0→1)其他任务才能获取。
3.1.2 常用 API(FreeRTOS)

3.2 计数信号量:允许 N 个任务 "同时占用" 资源
3.2.1 核心特性
- 计数器 S 取值范围:0 ≤ S ≤ max_count(max_count 为创建时配置的最大计数,如 3、10、255);
- 核心用途:「资源池管理」(如 N 个串口、N 个缓存块)或「批量同步」(如收集 N 个任务的完成信号);
- 关键规则:最多允许 max_count 个任务同时获取信号量,释放一个则 S+1,直到回到 max_count。
3.2.2 常用 API(FreeRTOS)

3.3 实验
创建两个任务:
任务1每秒释放一次二值信号量
任务2等待二值信号量,等到之后翻转一下LED1
3.3.1 核心逻辑
- 系统启动后创建「任务创建任务」(task_create_entry),优先级最高(12);
- 5 秒后,该任务创建「信号量释放任务」(give_sem_entry)、「LED 翻转任务」(led_toggle_entry)和「计数信号量」(最大计数 255,初始 0);
- 信号量释放任务:每 100ms 释放 1 次信号量,累计释放 30 次后停止;
- LED 翻转任务:阻塞等待信号量,获取到信号量后翻转 PA8 引脚电平(LED 亮灭),延时 500ms;
- 任务创建任务完成创建后自动删除自身。
3.3.2 基础初始化(主函数入口)
cpp
int main(void)
{
HAL_Init(); // 初始化HAL库(系统定时器、中断等)
SystemClock_Config(); // 配置系统时钟(HSE+PLL,最终时钟=HSE×2)
MX_GPIO_Init(); // 初始化GPIO(PA8=LED输出,PE6=备用输出)
MX_USART1_UART_Init(); // 初始化串口1(115200波特率,收发模式)
shell_init(); // 初始化Shell调试(支持串口输入命令调试)
// 创建「任务创建任务」:优先级12(最高),栈大小128字(512字节)
xTaskCreate(task_create_entry,"task_create_task",128,(void *)0,12,&xHandle);
vTaskStartScheduler(); // 启动FreeRTOS任务调度器(开始任务执行)
while (1) {} // 调度器启动后不会执行到这里,仅为语法兼容
}
关键说明:
FreeRTOS 中,vTaskStartScheduler() 会启动任务调度,按优先级抢占式执行任务;
栈大小 128 单位是「字(4 字节)」,实际栈空间 = 128×4=512 字节,需根据任务复杂度调整(避免栈溢出)。
3.3.3 核心组件:计数信号量(Counting Semaphore)
cpp
SemaphoreHandle_t CountSemaphore; // 计数信号量句柄(全局变量,供所有任务访问)
// 在task_create_entry中创建信号量
CountSemaphore = xSemaphoreCreateCounting(255, 0);
计数信号量特性:允许同时有 N 个任务获取信号量(N = 最大计数),此处最大计数 255,初始计数 0;
3.3.4 创建任务(task_create_entry)
cpp
void task_create_entry(void *p)
{
TaskHandle_t tmp_handle;
TaskHandle_t xHandle;
TaskHandle_t xHandle2;
vTaskDelay(5000); // 延迟5秒(FreeRTOS延时函数,单位ms,基于系统滴答定时器)
printf("start to create semaphore\r\n");
CountSemaphore = xSemaphoreCreateCounting(255,0); // 创建计数信号量(最大255,初始0)
printf("start to create tasks\r\n");
printf("create 1st task\r\n");
// 创建「信号量释放任务」:优先级10,栈128字
xTaskCreate(give_sem_entry,"give_sem_task",128,(void *)0,10,&xHandle);
printf("create 2nd task\r\n");
// 创建「LED翻转任务」:优先级10,栈128字
xTaskCreate(led_toggle_entry,"led_toggle_task",128,(void *)0,10,&xHandle2);
tmp_handle = xTaskGetHandle("task_create_task"); // 获取自身任务句柄
vTaskDelete(tmp_handle); // 删除自身(完成创建使命,释放资源)
}
延迟 5 秒是为了让系统稳定启动后再创建核心任务;
自身优先级最高(12),创建其他任务后立即删除,避免占用 CPU 资源;
两个核心任务优先级相同(10),FreeRTOS 会按时间片轮转调度(默认配置)。
3.3.5 任务 1:信号量释放任务(give_sem_entry)
cpp
void give_sem_entry(void *p)
{
uint8_t count = 0;
while(1)
{
if(count++<30) // 累计释放30次信号量
{
xSemaphoreGive(CountSemaphore); // 释放信号量(计数+1)
}
vTaskDelay(100); // 每100ms释放1次
}
}
xSemaphoreGive():信号量计数 + 1,若有任务阻塞等待该信号量,会唤醒优先级最高的等待任务;
仅释放 30 次后停止,之后不再操作信号量,任务进入 "空循环 + 100ms 延时" 状态。
3.3.6 任务 2:LED 翻转任务(led_toggle_entry)
cpp
void led_toggle_entry(void *p)
{
while(1)
{
// 阻塞等待信号量:无限等待(portMAX_DELAY),直到获取到信号量
xSemaphoreTake(CountSemaphore,portMAX_DELAY);
HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_8); // 翻转PA8电平(LED亮→灭/灭→亮)
vTaskDelay(500); // 保持当前电平500ms
}
}
xSemaphoreTake():尝试获取信号量,若信号量计数 > 0,计数 - 1 并返回成功;若计数 = 0,任务进入阻塞态,直到有其他任务释放信号量;
初始信号量计数 = 0,LED 任务会先阻塞,直到信号量释放任务释放第 1 次信号量;
释放 30 次信号量后,LED 任务会被唤醒 30 次,之后因无信号量释放,再次进入永久阻塞态。
3.3.7 基础硬件配置
(1) GPIO 初始化(MX_GPIO_Init)
cpp
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOE_CLK_ENABLE(); // 使能GPIOE时钟
__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能GPIOA时钟
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_6, GPIO_PIN_SET); // PE6初始拉高
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET); // PA8初始拉高(LED灭)
// PE6配置:推挽输出+上拉+低速
GPIO_InitStruct.Pin = GPIO_PIN_6;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);
// PA8配置:推挽输出+上拉+低速(LED控制引脚)
GPIO_InitStruct.Pin = GPIO_PIN_8;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
(2) 系统时钟配置(SystemClock_Config)
cpp
void SystemClock_Config(void)
{
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; // 外部晶振(HSE)
RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 开启HSE
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; // 开启PLL倍频
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; // PLL源=HSE
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL2; // HSE×2=系统时钟
// 总线时钟配置:AHB/APB1/APB2均不分频,与系统时钟一致
}
若 HSE 为 8MHz,系统时钟 = 8MHz×2=16MHz;
FreeRTOS 的系统滴答定时器(SysTick)由 HAL 库初始化,默认滴答周期 1ms(适配 vTaskDelay)。
(3) 串口 1 初始化(MX_USART1_UART_Init)
cpp
static void MX_USART1_UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
}
波特率 115200,8 位数据位,1 位停止位,无校验,收发模式;
4. 互斥锁
4.1 理解互斥锁
互斥锁(Mutex,全称 Mutual Exclusion)是 FreeRTOS 中用于 保护共享资源、避免任务间冲突 的核心同步机制,本质是 "二进制信号量的增强版",专门解决 "多个任务独占式访问同一资源" 的问题(如串口、SPI、全局变量等)。
互斥锁的核心作用:给共享资源 "加一把锁",任一任务要访问资源,必须先 "开锁"(获取锁),访问完再 "上锁"(释放锁),确保资源的 "原子访问"(不可被打断)。
互斥锁是二进制信号量(计数仅 0/1)的特殊形式,但多了两个关键特性,使其更适合 "资源保护":
关键特性 1:优先级继承(最核心优势)
解决 "优先级反转" 问题(高优先级任务等待低优先级任务的资源,导致高优先级任务被阻塞):
例:任务 A(高优先级)要访问串口,任务 B(低优先级)已持有串口互斥锁;
无优先级继承:任务 B 可能被中等优先级任务抢占,导致任务 A 长时间阻塞;
有优先级继承:任务 B 会临时继承任务 A 的高优先级,快速执行完并释放锁,避免任务 A 长时间等待。
关键特性 2:释放者必须是持有者
普通二进制信号量可由任意任务释放(如任务 A 获取,任务 B 释放),但互斥锁只能由获取它的任务释放------ 避免 "任务 A 获取锁后崩溃,任务 B 误释放" 导致的资源混乱。
4.2 API(FreeRTOS)
4.2.1 普通互斥锁

4.2.2 递归互斥锁

4.3 实验
先看一个场景:两个任务同时调用 printf 打印数据(串口是 "共享资源",同一时间只能发 1 个字节)。
cpp
#include "main.h"
#include "cmsis_os.h"
#include "stdio.h"
#include "task_init.h"
UART_HandleTypeDef huart1;
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART1_UART_Init(void);
int fputc(int ch,FILE *f)
{
while((USART1->SR & 0X40) == 0);
USART1->DR = (uint8_t)ch;
return ch;
}
void print1(void *p)
{
while(1)
{
printf("HELLO RTOS! HELLO BAICHENXI! HELLO MCU!\r\n");
}
}
void print2(void *p)
{
while(1)
{
printf("hello rtos! hello baichenxi! hello mcu!\r\n");
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
task_init();
while (1)
{
}
}
cpp
#include "task_init.h"
extern void print1(void *p);
extern void print2(void *p);
/* 创建任务的函数 */
void task_init( void )
{
BaseType_t xReturned;
TaskHandle_t xHandle = NULL;
/* 创建任务,存储句柄 */
xReturned = xTaskCreate(
print1, /* 执行任务的函数 */
"print1Task", /* 任务名称 */
STACK_SIZE, /* 堆栈大小,单位为字 */
( void * ) 1, /* 传递给任务的参数 */
tskIDLE_PRIORITY,/* 创建任务的优先级 */
&xHandle ); /* 用于传递创建的任务句柄 */
xReturned = xTaskCreate(
print2, /* 执行任务的函数 */
"print2Task", /* 任务名称 */
STACK_SIZE, /* 堆栈大小,单位为字 */
( void * ) 1, /* 传递给任务的参数 */
tskIDLE_PRIORITY,/* 创建任务的优先级 */
&xHandle ); /* 用于传递创建的任务句柄 */
vTaskStartScheduler();
}
实验现象(无互斥锁时)

打印是混乱的,因为两个任务在争抢资源
我们加入临界区,解决这个问题

实验现象

互斥锁使用步骤:
步骤 1:创建互斥锁(任务创建前初始化)
cppSemaphoreHandle_t MutexSemaphore; // 互斥锁句柄(全局变量,供所有任务访问) // 在 main 函数/初始化任务中创建 MutexSemaphore = xSemaphoreCreateMutex(); if(MutexSemaphore == NULL) { // 创建失败(内存不足),需处理错误 }步骤 2:任务中获取 + 释放互斥锁(访问共享资源)
cpp// 任务1:访问串口打印 void task1(void *p) { while(1) { // 等待获取互斥锁(无限等待) if(xSemaphoreTake(MutexSemaphore, portMAX_DELAY) == pdPASS) { // 成功获取锁,访问共享资源(串口打印) printf("Task1: Access shared resource!\r\n"); vTaskDelay(100); // 模拟资源访问耗时 // 释放互斥锁 xSemaphoreGive(MutexSemaphore); } vTaskDelay(200); // 任务循环延时 } } // 任务2:与任务1竞争同一互斥锁 void task2(void *p) { while(1) { if(xSemaphoreTake(MutexSemaphore, portMAX_DELAY) == pdPASS) { printf("Task2: Access shared resource!\r\n"); vTaskDelay(100); xSemaphoreGive(MutexSemaphore); } vTaskDelay(200); } }步骤 3:启动任务与调度器
cppint main(void) { // 硬件初始化(略) MutexSemaphore = xSemaphoreCreateMutex(); // 创建互斥锁 // 创建两个任务(优先级相同) xTaskCreate(task1, "Task1", 128, NULL, 10, NULL); xTaskCreate(task2, "Task2", 128, NULL, 10, NULL); vTaskStartScheduler(); // 启动调度器 while(1); }
还用之前的场景,我们在printf上面尝试获取互斥锁,之后再释放

执行打印任务前获取一下互斥锁,任务执行完释放互斥锁
注意区分xSemaphoreGive&xSemaphoreTake 和 xSemaphoreGiveRecursive&xSemaphoreTakeRecursive
等待到互斥锁后,互斥锁被print任务给锁上了,使用后把锁解开重新释放
实验现象(有互斥锁时)

临界区和互斥锁有点分不清?
临界区只是屏蔽一些低优先级任务,也就是RTOS系统所能管理的所有中断
4.4 互斥锁的递归与优先级继承
两个任务(print1/print2)优先级相同,循环尝试获取互斥信号量;
任一任务获取信号量后,调用 myfun 执行两次 "打印 + 信号量释放 / 重新获取" 的操作;
互斥信号量保证:同一时间只有一个任务能进入 "myfun 中的打印逻辑",避免串口打印冲突。
4.4.1 死锁------互斥锁的嵌套

FreeRTOS 的互斥信号量 不支持递归获取(同一任务不能连续两次 xSemaphoreTake,会导致死锁);
代码中:print1 外层已获取互斥量,进入 myfun 后第 1 次 xSemaphoreTake 会 阻塞自身(因互斥量已被当前任务持有,计数为 0),导致任务卡死;
正常现象应该是来回调用
看main函数中,创建task_create_entry任务

之前写了task_create_entry()任务函数

实验现象(死锁),没打印出myfun()里的内容:

4.4.2 解决死锁------递归互斥锁
法① 告诉写myfun的人,我外面写过了互斥锁,你就不要写互斥锁了
法② 递归互斥锁------记住加了几层锁,就解几层锁
加上Recursive

实验现象:打印出了myfun()里的内容
