当前位置: 首页 > news >正文

别再傻等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发送的可靠解决方案。

http://www.jsqmd.com/news/894653/

相关文章:

  • 2026年知名的海口汽车租赁租车/海口机场接送租车/海南租车服务型公司推荐 - 品牌宣传支持者
  • 别再死记硬背了!用UE4 DS做联机游戏,搞懂Role和Replicate才是王道
  • GEO不是新赛道,是你现有营销栈的“补丁“:2026年数字营销团队的整合指南
  • 土地利用优化配置的多目标人工免疫优化模型【附程序】
  • OK3588开发板多屏显示实战:如何用Uboot菜单灵活切换HDMI和LVDS输出(附飞凌手册避坑点)
  • 2026年热门的液冷电机/永磁同步电机/水冷电机可靠供应商推荐 - 行业平台推荐
  • 黑客松:从编程马拉松到组织创新催化剂的四大价值与落地实践
  • 网安副业单日入账 12k,到底是什么私活这么赚钱?
  • Flutter 国际化与本地化实战指南
  • 从修改器到Mod开发:如何利用dnSpy和Unity调试功能快速定位游戏核心逻辑
  • 构建FPI评级系统:多因子模型与自然语言生成在投资决策中的应用
  • 2026年热门的三亚中巴车出租/三亚会议车出租/三亚旅游车出租高评分公司推荐 - 行业平台推荐
  • 2026年4月大连味之母口碑好吗,大连味之母,大连味之母好不好 - 品牌推荐师
  • 基于AI的邮件HTML兼容性自动修复工具开发实践
  • ARM指令集解析:STC与STL指令深度剖析
  • AI智能体在电商中的角色探索:从“人找货”到“货找人”的交互新范式
  • AI生成代码中的CORS安全漏洞:从原理到修复的完整指南
  • 别再让SkinnedMeshRenderer拖垮你的游戏!Unity骨骼动画性能优化实战(BakeMesh + 动态合批)
  • 2026年知名的家具批发/酒店家具批发本地公司推荐 - 品牌宣传支持者
  • 构建会“说话”的智能体:从工具调用到记忆系统的工程实践
  • 从多仓库到pnpm workspace:前端Monorepo实战迁移与效率提升
  • CEO年度战略复盘:从数据叙事到战略聚焦的沟通艺术
  • 2026年热门的海口美兰机场租车/海口包年租车/海口租中巴租车/海口东站租车品牌公司推荐 - 行业平台推荐
  • STM32H743模拟SMBUS读取BQ40Z50电量,我踩过的三个坑(附完整代码与示波器波形)
  • AutoHotKey V2定时器(SetTimer)深度使用指南:从防抖连击到后台轮询,5个案例搞定
  • 大型语言模型压缩技术:SVD与DipSVD实践指南
  • Soul in Motion:用身体运动探索内在状态的身心实践框架
  • 别再手动调参了!用Python的sklearn一键找出最佳F1分数阈值(附完整代码)
  • Web应用API安全审计:从身份验证到输入验证的系统性加固实践
  • 从代码实现到系统设计:AI时代开发者的核心技能重构