FreeRTOS:信号量与互斥量在DMA串口发送中的实战剖析

前言

一、从问题出发:多任务共享资源的困境

[1.1 场景描述](#1.1 场景描述)

[1.2 为什么需要同步机制?](#1.2 为什么需要同步机制?)

二、信号量(Semaphore)深度解析

[2.1 什么是信号量?](#2.1 什么是信号量?)

[2.2 二值信号量的工作模型](#2.2 二值信号量的工作模型)

[2.3 计数信号量的应用场景](#2.3 计数信号量的应用场景)

三、互斥量(Mutex)深度解析

[3.1 互斥量与二值信号量的区别](#3.1 互斥量与二值信号量的区别)

[3.2 互斥量的优先级继承机制](#3.2 互斥量的优先级继承机制)

[3.3 互斥量 vs 二值信号量对比表](#3.3 互斥量 vs 二值信号量对比表)

四、实战案例:DMA异步串口发送

[4.1 为什么需要DMA?](#4.1 为什么需要DMA?)

[4.2 系统架构图](#4.2 系统架构图)

[4.3 核心代码实现](#4.3 核心代码实现)

步骤1:创建同步原语

步骤2:DMA配置

步骤3:异步发送函数(核心)

步骤4:DMA中断处理

步骤5:发送任务实现

[4.4 完整执行流程时序图](#4.4 完整执行流程时序图)

四、总结


前言

在嵌入式实时操作系统(RTOS)的开发中,任务同步与资源共享是两个核心问题。信号量(Semaphore)和互斥量(Mutex)是解决这两类问题的最基础也是最强大的工具。然而,很多初学者仅仅停留在"信号量用于同步,互斥量用于互斥"的表面理解,在实际项目中却不知如何正确运用。

本文将从一个真实的STM32 + FreeRTOS项目出发,结合DMA实现的异步串口发送,深入剖析信号量与互斥量的底层机制、使用场景以及常见的陷阱。通过本文,你不仅会学会如何使用这些同步原语,更会理解为什么要这样用。

完整的示例代码基于STM32F4系列,使用FreeRTOS V10.0+,DMA2串口1发送

一、从问题出发:多任务共享资源的困境

1.1 场景描述

假设我们有一个嵌入式系统,需要三个任务通过同一个UART串口发送调试信息。每个任务每隔1秒发送一条消息

XML 复制代码
Task1(优先级5):发送 "Task1 is running..."
Task2(优先级6):发送 "Task2 is running..."
Task3(优先级7):发送 "Task3 is running..."

这就是典型的竞争条件(Race Condition)。

1.2 为什么需要同步机制?

在裸机编程中,我们通常通过关中断或使用标志位来保护临界区。但在RTOS中,任务可能会被阻塞、挂起,简单的关中断无法解决多任务并发访问的问题。我们需要更高级的同步原语:

  • 互斥量(Mutex):保证同一时刻只有一个任务访问共享资源;

  • 信号量(Semaphore):实现任务间的同步,通知某个事件已经发生;

二、信号量(Semaphore)深度解析

2.1 什么是信号量?

信号量是一个非负整数的计数器,支持两种原子操作:

  • Take(P操作):如果信号量值 > 0,将其减1并继续;如果为0,任务阻塞等待

  • Give(V操作):将信号量值加1,如果有任务在等待,唤醒其中一个

信号量分为两种类型:

类型 初始值 典型用途 特点
二值信号量 0 事件通知 值只有0或1
计数信号量 N > 0 资源管理 可以管理多个相同资源

2.2 二值信号量的工作模型

二值信号量就像一把"一次性钥匙":

cpp 复制代码
// 创建二值信号量,初始为0
SemaphoreHandle_t sem = xSemaphoreCreateBinary();

// 任务A:等待事件
xSemaphoreTake(sem, portMAX_DELAY);  // 没钥匙?那就等着
// 事件发生后才能执行到这里

// 中断服务函数:事件发生,给钥匙
xSemaphoreGiveFromISR(sem, NULL);    // 给一把钥匙

关键点:如果Give时没有任务在等待,信号量会保持为1,下一个Take会立即成功。

2.3 计数信号量的应用场景

cpp 复制代码
// 管理5个相同的硬件缓冲区
SemaphoreHandle_t bufferSem = xSemaphoreCreateCounting(5, 5);

// 任务获取缓冲区
xSemaphoreTake(bufferSem, portMAX_DELAY);
// 使用缓冲区...
// 使用完释放
xSemaphoreGive(bufferSem);

注:关于优先级反转和优先级继承可以参考下面的博客https://blog.csdn.net/qq_33775774/article/details/149491381?fromshare=blogdetail&sharetype=blogdetail&sharerId=149491381&sharerefer=PC&sharesource=weixin_45725144&sharefrom=from_link

三、互斥量(Mutex)深度解析

3.1 互斥量与二值信号量的区别

很多初学者会问:"既然二值信号量也能实现互斥,为什么还要互斥量?"

这是一个关键问题。看下面的例子:

cpp 复制代码
// 场景:低优先级任务持有锁,高优先级任务等待
// 使用二值信号量
LowTask:    xSemaphoreTake(sem);  // 获得锁
            // 执行长时间操作...
MediumTask: // 抢占CPU,执行无限循环
HighTask:   xSemaphoreTake(sem);  // 永远等不到!

问题优先级反转(Priority Inversion)

3.2 互斥量的优先级继承机制

互斥量内置了优先级继承机制,可以有效缓解优先级反转:

cpp 复制代码
// 使用互斥量
LowTask:    xSemaphoreTake(mutex);  // 获得锁
MediumTask: // 试图抢占
            // 但此时LowTask临时继承了HighTask的优先级!
            // MediumTask无法抢占,HighTask能更快获得锁

优先级继承的规则

  1. 当高优先级任务等待被低优先级任务持有的互斥量时

  2. 低优先级任务临时提升到高优先级任务的优先级

  3. 释放互斥量后,恢复原始优先级

3.3 互斥量 vs 二值信号量对比表

特性 互斥量 二值信号量
优先级继承 支持 ❌ 不支持
递归获取 可配置 ❌ 不支持
典型用途 资源保护 事件同步
初始化状态 已释放(1) 未发生(0)
谁可以Give 只能由持有者 任何任务/ISR

四、实战案例:DMA异步串口发送

4.1 为什么需要DMA?

传统的轮询发送方式:

cpp 复制代码
void uart_send_polling(uint8_t *data, uint32_t len) {
    for (uint32_t i = 0; i < len; i++) {
        while (!(USART->SR & TXE));  // CPU空转等待
        USART->DR = data[i];
    }
}

问题:发送1000字节需要约100ms,CPU完全被占用,无法执行其他任务。

解决方案:DMA(直接内存访问)+ 中断 + 信号量

4.2 系统架构图

4.3 核心代码实现

步骤1:创建同步原语

cpp 复制代码
static SemaphoreHandle_t uart_tx_done_semphr;  // 传输完成信号量
static SemaphoreHandle_t uart_tx_busy_mux;     // UART访问互斥量

static void uart_init(void)
{
    // 创建二值信号量,初始为0(表示"未完成")
    uart_tx_done_semphr = xSemaphoreCreateBinary();
    configASSERT(uart_tx_done_semphr);
    
    // 创建互斥量,初始为1(表示"可用")
    uart_tx_busy_mux = xSemaphoreCreateMutex();
    configASSERT(uart_tx_busy_mux);
    
    // 硬件初始化...
    uart_pin_init();
    uart_lowlevel_init();
    uart_dma_init();
}

步骤2:DMA配置

cpp 复制代码
static void uart_dma_init(void)
{
    // 配置DMA中断
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = DMA2_Stream7_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 7;
    NVIC_Init(&NVIC_InitStructure);
    
    // 配置DMA流
    DMA_InitTypeDef DMA_InitStructure;
    DMA_InitStructure.DMA_Channel = DMA_Channel_4;              // USART1_TX通道
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
    DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;     // 内存→外设
    DMA_InitStructure.DMA_BufferSize = 0;                       // 稍后设置
    DMA_Init(DMA2_Stream7, &DMA_InitStructure);
    
    // 使能传输完成中断
    DMA_ITConfig(DMA2_Stream7, DMA_IT_TC, ENABLE);
}

步骤3:异步发送函数(核心)

cpp 复制代码
static void uart_write(const uint8_t *data, uint32_t length)
{
    // ① 获取互斥量:确保只有一个任务使用UART
    xSemaphoreTake(uart_tx_busy_mux, portMAX_DELAY);
    
    // ② 配置DMA传输
    DMA2_Stream7->M0AR = (uint32_t)data;  // 源地址
    DMA2_Stream7->NDTR = length;           // 传输长度
    DMA_Cmd(DMA2_Stream7, ENABLE);         // 启动DMA
    
    // ③ 等待传输完成信号量(任务进入阻塞状态)
    xSemaphoreTake(uart_tx_done_semphr, portMAX_DELAY);
    
    // ④ 释放互斥量
    xSemaphoreGive(uart_tx_busy_mux);
}

关键点分析

  • 互斥量的作用:防止多个任务同时配置DMA寄存器

  • 信号量的作用:让任务在DMA传输期间让出CPU,而不是空转等待

  • 阻塞等待xSemaphoreTake 在信号量不可用时会让任务进入阻塞状态,不消耗CPU

步骤4:DMA中断处理

cpp 复制代码
void DMA2_Stream7_IRQHandler(void)
{
    if (DMA_GetFlagStatus(DMA2_Stream7, DMA_FLAG_TCIF7) != RESET)
    {
        DMA_ClearFlag(DMA2_Stream7, DMA_FLAG_TCIF7);
        DMA_Cmd(DMA2_Stream7, DISABLE);
        
        BaseType_t pxHigherPriorityTaskWoken = pdFALSE;
        
        // 从ISR中释放信号量,唤醒等待的任务
        xSemaphoreGiveFromISR(uart_tx_done_semphr, &pxHigherPriorityTaskWoken);
        
        // 如果唤醒的任务优先级更高,立即切换
        portYIELD_FROM_ISR(pxHigherPriorityTaskWoken);
    }
}

步骤5:发送任务实现

cpp 复制代码
static void uart_send_task(void *args)
{
    char buff[128];
    while (1)
    {
        vTaskDelay(pdMS_TO_TICKS(1000));
        sprintf(buff, "[%lu] %s is running...\r\n", 
                xTaskGetTickCount(), pcTaskGetName(NULL));
        uart_write((uint8_t *)buff, strlen(buff));
    }
}

4.4 完整执行流程时序图

html 复制代码
时间线    Task1(pri5)    DMA硬件      中断        Task2(pri6)   信号量值  互斥量状态
─────────────────────────────────────────────────────────────────────────────
0ms      Take互斥量 ✓                          阻塞(等互斥量)    0        持有者=T1
        启动DMA
        Take信号量(阻塞)  开始传输                           
        ↓                 ↓                                    
1ms      阻塞(等信号量)   传输中              阻塞(等互斥量)    0        持有者=T1
2ms      阻塞            传输中               阻塞             0        持有者=T1
3ms      阻塞            传输完成   ➡ Give信号量               1        持有者=T1
                         触发中断                               
4ms      被唤醒                              阻塞             0        持有者=T1
        Give互斥量                                             0        释放
        循环重新开始                         获得互斥量 ✓      0        持有者=T2
        Take互斥量(等待)                     启动DMA...        0        持有者=T2

四、总结

本文从一个实际的多任务UART发送场景出发,深入剖析了FreeRTOS中信号量与互斥量的本质区别与应用技巧。我们首先理解了为什么在多任务系统中需要同步机制------没有保护的共享资源会导致数据混乱和系统崩溃;接着详细对比了二值信号量与互斥量的核心差异:互斥量通过优先级继承机制有效缓解了优先级反转问题,适用于保护临界资源,而二值信号量更擅长于任务间的事件通知与同步,典型的应用场景就是本文中的DMA传输完成通知。在实战部分,我们实现了一个完整的STM32 + FreeRTOS + DMA的异步串口发送系统,通过互斥量uart_tx_busy_mux确保同一时刻只有一个任务访问UART硬件,通过二值信号量uart_tx_done_semphr让任务在DMA传输期间进入阻塞状态、主动让出CPU,从而实现了高效的并发执行

相关推荐
hughnz2 小时前
钻头技术持续突飞猛进:地热钻探领域的创新
人工智能·算法
xiaoye-duck2 小时前
《算法题讲解指南:动态规划算法--子数组系列》--21.乘积最大子数组,22.乘积为正数的最长子数组
c++·算法·动态规划
MicroTech20252 小时前
突破非幺正动力学瓶颈:MLGO微算法科技量子虚时演化赋能开放量子系统模拟
科技·算法·量子计算
计算机安禾2 小时前
【数据结构与算法】第24篇:哈夫曼树与哈夫曼编码
c语言·开发语言·数据结构·c++·算法·visual studio
wsoz2 小时前
Leetcode双指针-day2
算法·leetcode
郝学胜-神的一滴2 小时前
[力扣 20] 栈解千愁:有效括号序列的优雅实现与深度解析
java·数据结构·c++·算法·leetcode·职场和发展
AlenTech2 小时前
128. 最长连续序列 - 力扣(LeetCode)
算法·leetcode·职场和发展
田梓燊2 小时前
leetcode 无重复字符的最长子串
算法·leetcode·职场和发展
Yzzz-F3 小时前
Problem - 2148F - Codeforces[字符串后缀排序]
数据结构·算法