【RTOS】任务间通信IPC

一壶水为什么还没有"呜呜"叫?是因为还没烧开,烧开和呜呜叫是同步

运动员为什么还没有起跑?是因为他还没听见枪响信号,枪响信号和起跑是同步的

任务是解耦的,但又是相互同步的

一个任务的开始可能需要别的任务的支持

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 核心逻辑

  1. 系统启动后创建「任务创建任务」(task_create_entry),优先级最高(12);
  2. 5 秒后,该任务创建「信号量释放任务」(give_sem_entry)、「LED 翻转任务」(led_toggle_entry)和「计数信号量」(最大计数 255,初始 0);
  3. 信号量释放任务:每 100ms 释放 1 次信号量,累计释放 30 次后停止;
  4. LED 翻转任务:阻塞等待信号量,获取到信号量后翻转 PA8 引脚电平(LED 亮灭),延时 500ms;
  5. 任务创建任务完成创建后自动删除自身。

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:创建互斥锁(任务创建前初始化)
cpp 复制代码
SemaphoreHandle_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:启动任务与调度器
cpp 复制代码
int 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()里的内容

相关推荐
电化学仪器白超3 小时前
《可编程固定阻值电子负载的制作与自动化标定技术》
python·单片机·嵌入式硬件·自动化
三佛科技-134163842123 小时前
LP3799FAES-B 反激式电源控制器芯片 典型应用电路
单片机·嵌入式硬件·物联网·智能家居·pcb工艺
黑客思维者4 小时前
XGW-9000 网关 DDR4/LPDDR4 内存子系统信号完整性仿真细化设计
开发语言·python·嵌入式硬件·ddr4·信号仿真
风行男孩4 小时前
stm32基础学习——按键的使用
stm32·嵌入式硬件·学习
小热茶5 小时前
浮点数计算专题【五、 IEEE 754 浮点乘法算法详解---基于RISCV的FP32乘法指令在五级流水线的运行分析与SystemC实现】
人工智能·嵌入式硬件·算法·systemc
brave and determined5 小时前
传感器学习(day09):三维手势识别:人机交互的未来革命
嵌入式硬件·手势识别·传感器·tof·嵌入式设计·多角成像技术·光飞时间技术
恒锐丰小吕5 小时前
无锡黑锋 HF5903 40V热插拔、50V耐压、可调限流保护开关技术解析
嵌入式硬件·硬件工程
国科安芯14 小时前
AS32S601型MCU芯片电源管理(PMU)模块详解
单片机·嵌入式硬件·性能优化·架构·risc-v
Jack电子实验室17 小时前
【杭电HDU】校园网(DeepL/Srun)自动登录教程
python·嵌入式硬件·计算机网络·自动化