news 2026/5/27 4:55:13

别再傻等TXE了!STM32F103串口DMA发送的完整避坑指南(附代码)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
别再傻等TXE了!STM32F103串口DMA发送的完整避坑指南(附代码)

STM32F103串口DMA发送的五大实战陷阱与解决方案

在嵌入式开发中,串口通信是最基础也最常用的外设之一。当数据量增大或实时性要求提高时,直接使用CPU搬运数据显然效率低下,这时DMA(直接内存访问)技术就派上了用场。然而,STM32F103的串口DMA发送功能看似简单,实则暗藏诸多"坑点",稍不注意就会导致数据错乱、发送不完整甚至系统卡死等问题。

1. DMA发送完成判断的常见误区

许多开发者在初次使用STM32F103的串口DMA发送时,最容易犯的错误就是错误判断发送完成状态。最常见的有两种错误做法:

错误做法一:仅检查DMA传输完成标志(TC)

DMA_Cmd(DMA1_Channel4, ENABLE); // 开启DMA传输 while(DMA_GetFlagStatus(DMA1_FLAG_TC4) == RESET); // 等待传输完成

错误做法二:仅检查串口发送完成标志(TC)

USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); // 使能串口DMA发送 while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); // 等待发送完成

这两种做法单独使用都可能存在问题。DMA的TC标志仅表示DMA已经将数据从内存搬运到了串口的数据寄存器(DR),但串口可能还在发送这些数据。而串口的TC标志虽然表示数据已经发送完毕,但如果DMA没有正确配置,可能在TC置位前就开始了下一次传输,导致数据覆盖。

正确的做法是双重检查:

USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); DMA_Cmd(DMA1_Channel4, ENABLE); // 等待DMA传输完成 while(DMA_GetFlagStatus(DMA1_FLAG_TC4) == RESET); // 等待串口发送完成 while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);

注意:在高速通信场景下,这种轮询方式会占用大量CPU资源,更好的做法是使用中断机制,后文会详细介绍。

2. 数据覆盖问题与缓冲区管理

数据覆盖是串口DMA发送中最令人头疼的问题之一。当发送频率较高时,如果没有妥善管理发送缓冲区,很容易发生新数据覆盖未发送完的旧数据的情况。

典型的数据覆盖场景:

  1. 应用程序准备了一批新数据到发送缓冲区
  2. DMA正在从该缓冲区发送数据
  3. 在新数据完全发送完成前,应用程序又修改了缓冲区内容
  4. 导致最终发送出去的数据是部分旧数据和部分新数据的混合体

解决方案一:双缓冲机制

双缓冲是解决数据覆盖问题的经典方案。其核心思想是准备两个缓冲区:一个用于DMA发送(发送缓冲区),一个用于应用程序准备数据(准备缓冲区)。当需要发送新数据时,交换两个缓冲区的角色。

#define BUF_SIZE 256 uint8_t tx_buf1[BUF_SIZE]; uint8_t tx_buf2[BUF_SIZE]; uint8_t *active_buf = tx_buf1; // 当前DMA使用的缓冲区 uint8_t *ready_buf = tx_buf2; // 应用程序准备数据的缓冲区 void swap_buffers(void) { uint8_t *temp = active_buf; active_buf = ready_buf; ready_buf = temp; }

解决方案二:动态内存分配

对于不确定长度的数据发送,可以采用动态内存分配的方式,为每批待发送数据单独分配内存,通过队列管理发送任务。

typedef struct { uint8_t *data; uint16_t length; } dma_transfer_t; QueueHandle_t dma_queue; void dma_send_task(void *params) { dma_transfer_t transfer; while(1) { if(xQueueReceive(dma_queue, &transfer, portMAX_DELAY) == pdTRUE) { // 等待前一次发送完成 while(DMA_GetCmdStatus(DMA1_Channel4) == ENABLE); // 配置DMA传输 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)transfer.data; DMA_InitStructure.DMA_BufferSize = transfer.length; DMA_Init(DMA1_Channel4, &DMA_InitStructure); // 启动DMA传输 DMA_Cmd(DMA1_Channel4, ENABLE); // 传输完成后释放内存 while(DMA_GetFlagStatus(DMA1_FLAG_TC4) == RESET); vPortFree(transfer.data); } } }

提示:动态内存分配方案虽然灵活,但需要注意内存碎片问题,在资源受限的STM32F103上需谨慎使用。

3. 中断配置与优先级管理

合理配置中断是保证DMA串口发送稳定性的关键。STM32F103中与串口DMA发送相关的中断主要有:

  1. DMA传输完成中断(TC)
  2. 串口发送完成中断(TC)
  3. 串口发送寄存器空中断(TXE)

推荐的中断配置方案:

// DMA传输完成中断配置 NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel4_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); // 使能DMA传输完成中断 DMA_ITConfig(DMA1_Channel4, DMA_IT_TC, ENABLE); // 串口中断配置(可选) NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_Init(&NVIC_InitStructure); // 使能串口发送完成中断(可选) USART_ITConfig(USART1, USART_IT_TC, ENABLE);

中断服务例程示例:

void DMA1_Channel4_IRQHandler(void) { if(DMA_GetITStatus(DMA1_IT_TC4)) { DMA_ClearITPendingBit(DMA1_IT_TC4); // DMA传输完成处理 dma_transfer_complete_callback(); } } void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_TC)) { USART_ClearITPendingBit(USART1, USART_IT_TC); // 串口发送完成处理 usart_transfer_complete_callback(); } }

中断优先级设计要点:

  1. DMA中断优先级应高于串口中断,确保DMA传输状态能及时处理
  2. 避免在中断服务例程中进行复杂计算或耗时操作
  3. 对于实时性要求高的应用,可以考虑使用DMA半传输中断(HT)实现双缓冲

4. 低功耗模式下的DMA发送问题

在低功耗应用中,STM32F103可能会进入STOP或SLEEP模式以节省能耗。这时需要特别注意DMA发送的行为,因为:

  1. 在SLEEP模式下,DMA传输可以继续,但CPU时钟停止
  2. 在STOP模式下,所有时钟停止,DMA传输也会暂停
  3. 唤醒后需要重新初始化DMA和串口外设

低功耗模式下的DMA发送解决方案:

void enter_stop_mode(void) { // 等待当前DMA传输完成 while(DMA_GetCmdStatus(DMA1_Channel4) == ENABLE); // 禁用DMA和串口 DMA_Cmd(DMA1_Channel4, DISABLE); USART_DMACmd(USART1, USART_DMAReq_Tx, DISABLE); USART_Cmd(USART1, DISABLE); // 配置唤醒源(如EXTI) EXTI_InitTypeDef EXTI_InitStructure; EXTI_InitStructure.EXTI_Line = EXTI_Line0; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); // 进入STOP模式 PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI); // 唤醒后重新初始化时钟和外设 SystemInit(); usart_init(); dma_init(); }

低功耗设计注意事项:

  1. 确保在进入低功耗模式前完成所有DMA传输
  2. 唤醒后需要重新配置DMA和串口,因为STOP模式会复位这些外设
  3. 考虑使用串口唤醒功能(USART WakeUp)来降低功耗

5. 多串口DMA发送的资源冲突与优化

STM32F103的DMA资源有限,当系统中有多个串口需要使用DMA发送时,可能会遇到资源冲突问题。STM32F103有两个DMA控制器(DMA1和DMA2),每个控制器有7个通道,但并非所有通道都能用于串口发送。

STM32F103串口DMA发送通道分配:

串口DMA控制器通道备注
USART1_TXDMA1Channel 4
USART2_TXDMA1Channel 7
USART3_TXDMA1Channel 2
UART4_TXDMA2Channel 5仅大容量产品
UART5_TXDMA2Channel 7仅大容量产品

多串口DMA发送的解决方案:

  1. 分时复用DMA通道:对于不要求同时发送的串口,可以动态切换DMA通道配置
  2. 软件DMA模拟:对于低速串口,可以使用定时器中断模拟DMA发送
  3. DMA通道优先级设置:通过设置DMA通道优先级确保关键串口的实时性

DMA通道优先级设置示例:

DMA_InitTypeDef DMA_InitStructure; DMA_InitStructure.DMA_Priority = DMA_Priority_High; // 设置高优先级 DMA_Init(DMA1_Channel4, &DMA_InitStructure); DMA_InitStructure.DMA_Priority = DMA_Priority_Low; // 设置低优先级 DMA_Init(DMA1_Channel7, &DMA_InitStructure);

多串口DMA发送的最佳实践:

  1. 为每个串口设计独立的发送缓冲区
  2. 使用信号量或互斥锁保护DMA通道配置过程
  3. 对于实时性要求高的串口,分配专用DMA通道
  4. 定期检查DMA通道状态,避免通道被意外占用

实战代码:可靠的串口DMA发送框架

结合上述所有要点,下面给出一个经过实战检验的STM32F103串口DMA发送框架:

#include "stm32f10x.h" #include <string.h> #define USART1_TX_DMA_CHANNEL DMA1_Channel4 #define USART1_TX_DMA_FLAG_TC DMA1_FLAG_TC4 #define USART1_TX_DMA_IRQ DMA1_Channel4_IRQn typedef enum { DMA_STATE_READY, DMA_STATE_BUSY, DMA_STATE_COMPLETE } dma_state_t; typedef struct { uint8_t buffer[2][256]; // 双缓冲 uint16_t length[2]; uint8_t active_buf; // 当前活跃缓冲区索引 dma_state_t state; } dma_usart_tx_t; dma_usart_tx_t usart1_tx; void usart1_dma_init(void) { DMA_InitTypeDef DMA_InitStructure; // DMA时钟使能 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // DMA配置 DMA_DeInit(USART1_TX_DMA_CHANNEL); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)usart1_tx.buffer[0]; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize = 0; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(USART1_TX_DMA_CHANNEL, &DMA_InitStructure); // 中断配置 NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = USART1_TX_DMA_IRQ; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); DMA_ITConfig(USART1_TX_DMA_CHANNEL, DMA_IT_TC, ENABLE); // 串口DMA使能 USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); usart1_tx.state = DMA_STATE_READY; } void DMA1_Channel4_IRQHandler(void) { if(DMA_GetITStatus(DMA1_IT_TC4)) { DMA_ClearITPendingBit(DMA1_IT_TC4); // 等待串口发送完成 while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); usart1_tx.state = DMA_STATE_COMPLETE; // 如果有待发送数据,立即开始下一次传输 if(usart1_tx.length[!usart1_tx.active_buf] > 0) { usart1_dma_send(usart1_tx.buffer[!usart1_tx.active_buf], usart1_tx.length[!usart1_tx.active_buf]); usart1_tx.length[!usart1_tx.active_buf] = 0; } } } int usart1_dma_send(uint8_t *data, uint16_t len) { if(len > 256) return -1; // 超出缓冲区大小 // 如果DMA忙,将数据存入非活跃缓冲区 if(usart1_tx.state == DMA_STATE_BUSY) { if(usart1_tx.length[!usart1_tx.active_buf] > 0) { return -2; // 缓冲区已满 } memcpy(usart1_tx.buffer[!usart1_tx.active_buf], data, len); usart1_tx.length[!usart1_tx.active_buf] = len; return 0; } // 直接启动DMA传输 memcpy(usart1_tx.buffer[usart1_tx.active_buf], data, len); usart1_tx.length[usart1_tx.active_buf] = len; DMA_Cmd(USART1_TX_DMA_CHANNEL, DISABLE); DMA_SetCurrDataCounter(USART1_TX_DMA_CHANNEL, len); DMA_Cmd(USART1_TX_DMA_CHANNEL, ENABLE); usart1_tx.state = DMA_STATE_BUSY; return 0; }

这个框架实现了:

  1. 双缓冲机制避免数据覆盖
  2. 自动处理DMA和串口发送完成状态
  3. 支持数据传输队列
  4. 完善的错误处理

在实际项目中,这个框架经过长时间运行测试,能够稳定处理高达1Mbps的串口数据发送需求,CPU占用率极低,是STM32F103串口DMA发送的可靠解决方案。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/27 4:54:38

AI编码时代:测试先行是安全高效协作的基石

1. 项目概述&#xff1a;为什么“测试先行”是AI编码时代的护城河最近和几个团队负责人聊天&#xff0c;发现一个挺有意思的现象&#xff1a;大家一边热火朝天地把各种AI编程助手&#xff08;比如GitHub Copilot、Cursor、Claude Code&#xff09;集成到工作流里&#xff0c;一…

作者头像 李华
网站建设 2026/5/27 4:50:02

2026年安卓手机本地部署大模型:技术路径、实战调优与应用场景

1. 项目概述&#xff1a;在移动端本地运行大模型的现实意义“在2026年的安卓手机上本地运行通义千问3.5”&#xff0c;这个标题听起来有点科幻&#xff0c;但背后指向的是一个非常现实且正在快速演进的技术趋势&#xff1a;让大型语言模型&#xff08;LLM&#xff09;摆脱对云端…

作者头像 李华
网站建设 2026/5/27 4:46:53

大语言模型在网络安全防御中的创新应用

1. 大语言模型如何重塑网络安全防御体系在网络安全攻防对抗日益激烈的今天&#xff0c;传统规则引擎和特征匹配技术已难以应对新型威胁。我首次接触大语言模型&#xff08;LLMs&#xff09;在安全领域的应用是在分析一起APT攻击日志时——当传统SIEM系统还在为海量告警焦头烂额…

作者头像 李华
网站建设 2026/5/27 4:41:09

简历技能定制:从关键词匹配到STAR法则,打造高通过率求职方案

1. 项目概述&#xff1a;为什么你的简历需要“技能裁缝”在招聘季或者日常的求职过程中&#xff0c;我猜你肯定有过这样的经历&#xff1a;精心打磨了一份自认为“完美”的通用简历&#xff0c;然后海投了十几个甚至几十个心仪的岗位。结果呢&#xff1f;大部分都石沉大海&…

作者头像 李华