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

张开发
2026/4/6 11:06:44 15 分钟阅读

分享文章

FreeRTOS:信号量与互斥量在DMA串口发送中的实战剖析
前言一、从问题出发多任务共享资源的困境1.1 场景描述1.2 为什么需要同步机制二、信号量Semaphore深度解析2.1 什么是信号量2.2 二值信号量的工作模型2.3 计数信号量的应用场景三、互斥量Mutex深度解析3.1 互斥量与二值信号量的区别3.2 互斥量的优先级继承机制3.3 互斥量 vs 二值信号量对比表四、实战案例DMA异步串口发送4.1 为什么需要DMA4.2 系统架构图4.3 核心代码实现步骤1创建同步原语步骤2DMA配置步骤3异步发送函数核心步骤4DMA中断处理步骤5发送任务实现4.4 完整执行流程时序图四、总结前言在嵌入式实时操作系统RTOS的开发中任务同步与资源共享是两个核心问题。信号量Semaphore和互斥量Mutex是解决这两类问题的最基础也是最强大的工具。然而很多初学者仅仅停留在信号量用于同步互斥量用于互斥的表面理解在实际项目中却不知如何正确运用。本文将从一个真实的STM32 FreeRTOS项目出发结合DMA实现的异步串口发送深入剖析信号量与互斥量的底层机制、使用场景以及常见的陷阱。通过本文你不仅会学会如何使用这些同步原语更会理解为什么要这样用。完整的示例代码基于STM32F4系列使用FreeRTOS V10.0DMA2串口1发送一、从问题出发多任务共享资源的困境1.1 场景描述假设我们有一个嵌入式系统需要三个任务通过同一个UART串口发送调试信息。每个任务每隔1秒发送一条消息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 什么是信号量信号量是一个非负整数的计数器支持两种原子操作TakeP操作如果信号量值 0将其减1并继续如果为0任务阻塞等待GiveV操作将信号量值加1如果有任务在等待唤醒其中一个信号量分为两种类型类型初始值典型用途特点二值信号量0事件通知值只有0或1计数信号量N 0资源管理可以管理多个相同资源2.2 二值信号量的工作模型二值信号量就像一把一次性钥匙// 创建二值信号量初始为0 SemaphoreHandle_t sem xSemaphoreCreateBinary(); // 任务A等待事件 xSemaphoreTake(sem, portMAX_DELAY); // 没钥匙那就等着 // 事件发生后才能执行到这里 // 中断服务函数事件发生给钥匙 xSemaphoreGiveFromISR(sem, NULL); // 给一把钥匙关键点如果Give时没有任务在等待信号量会保持为1下一个Take会立即成功。2.3 计数信号量的应用场景// 管理5个相同的硬件缓冲区 SemaphoreHandle_t bufferSem xSemaphoreCreateCounting(5, 5); // 任务获取缓冲区 xSemaphoreTake(bufferSem, portMAX_DELAY); // 使用缓冲区... // 使用完释放 xSemaphoreGive(bufferSem);注关于优先级反转和优先级继承可以参考下面的博客https://blog.csdn.net/qq_33775774/article/details/149491381?fromshareblogdetailsharetypeblogdetailsharerId149491381sharereferPCsharesourceweixin_45725144sharefromfrom_link三、互斥量Mutex深度解析3.1 互斥量与二值信号量的区别很多初学者会问既然二值信号量也能实现互斥为什么还要互斥量这是一个关键问题。看下面的例子// 场景低优先级任务持有锁高优先级任务等待 // 使用二值信号量 LowTask: xSemaphoreTake(sem); // 获得锁 // 执行长时间操作... MediumTask: // 抢占CPU执行无限循环 HighTask: xSemaphoreTake(sem); // 永远等不到问题优先级反转Priority Inversion3.2 互斥量的优先级继承机制互斥量内置了优先级继承机制可以有效缓解优先级反转// 使用互斥量 LowTask: xSemaphoreTake(mutex); // 获得锁 MediumTask: // 试图抢占 // 但此时LowTask临时继承了HighTask的优先级 // MediumTask无法抢占HighTask能更快获得锁优先级继承的规则当高优先级任务等待被低优先级任务持有的互斥量时低优先级任务临时提升到高优先级任务的优先级释放互斥量后恢复原始优先级3.3 互斥量 vs 二值信号量对比表特性互斥量二值信号量优先级继承支持❌ 不支持递归获取可配置❌ 不支持典型用途资源保护事件同步初始化状态已释放1未发生0谁可以Give只能由持有者任何任务/ISR四、实战案例DMA异步串口发送4.1 为什么需要DMA传统的轮询发送方式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字节需要约100msCPU完全被占用无法执行其他任务。解决方案DMA直接内存访问 中断 信号量4.2 系统架构图4.3 核心代码实现步骤1创建同步原语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(); }步骤2DMA配置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异步发送函数核心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步骤4DMA中断处理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发送任务实现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 完整执行流程时序图时间线 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从而实现了高效的并发执行

更多文章