资讯动态

STM32F103标准库USART3 DMA收发实战:彻底解放CPU

发布时间:2026/10/20 11:48:01 来源:尧图企业网站定制
简介这份资源面向嵌入式开发初学者与中级工程师聚焦STM32F103标准库下USART3的DMA收发实现帮助解决串口通信中CPU占用高、实时性不足的问题。包内共171个文件以c源码、h头文件、s汇编启动文件为主辅以o目标文件、crf编译中间文件、uvproj与uvopt工程配置、hex烧录文件及map映射文件等压缩包约3.41MB构成一套可直接编译的Keil工程模板。内容围绕DMA通道选择、传输方向与源目标地址配置、USART3时钟与波特率设置、DMA请求使能、传输完成与错误中断处理等关键环节展开并给出初始化、启动传输与中断回调的软件流程。已有3312人学习下载适合希望掌握标准库DMA串口收发、对照工程模板快速搭建实验环境并排查传输异常的开发者参考。1. 串口收包还在死等中断这套 DMA 方案让 CPU 彻底解放如果你用 STM32F103 做过串口通信大概率经历过这样的场景主循环里塞了一个while(USART_GetFlagStatus(USART3, USART_FLAG_RXNE) RESET)波特率一高、数据一多CPU 就被拖死在轮询里稍微加点别的任务就开始丢字节。更别提用中断逐字节收每来一个字节进一次中断115200 波特率下每秒上万次中断主循环基本别想干正事了。这个项目就是冲着这个痛点来的在 STM32F103 上用标准库Standard Peripheral Library配置 USART3收发全部走 DMA 通道CPU 只在数据搬完之后处理一次中间完全不参与字节搬运。它适合正在做多串口数据采集、Modbus 通信、上位机指令解析的嵌入式开发者尤其是那些主控资源紧张、又不想上 RTOS 的场景。整套代码基于标准库而非 HAL对习惯寄存器级控制的开发者更友好移植到同系列芯片也直接可用。2. 架构拆解USART3 与 DMA1 的通道映射关系2.1 为什么是 USART3 配 DMA1 的这两个通道STM32F103 的 DMA 控制器分 DMA1 和 DMA2其中 DMA1 有 7 个通道每个通道固定映射到特定的外设请求。USART3 的收发请求在 DMA1 上的映射是硬连线的不能随便换通道外设请求DMA 控制器通道传输方向USART3_TXDMA1Channel 2存储器 → 外设USART3_RXDMA1Channel 3外设 → 存储器这个映射关系在参考手册的 DMA 请求映射表里写死了配错通道 DMA 请求根本不会触发现象就是「代码看着没问题但一个字节都收不到」。我见过有人把 RX 配到 Channel 2调了一下午以为是波特率问题血泪经验。发送方向是「存储器到外设」DMA 从你给的缓冲区里读数据逐个写到USART3-DR接收方向是「外设到存储器」每收到一个字节USART3 向 DMA 发一次请求DMA 把USART3-DR的值搬到你的缓冲区。两个方向互不干扰可以同时开启。2.2 整体数据流与中断分工整个方案的数据流是这样的发送链路应用层把待发数据放进USART3_TX_BUF调用发送函数 → 配置 DMA 传输长度 → 使能 DMA 通道 → DMA 自动把数据搬到 USART3 数据寄存器 → 搬完后触发 DMA 传输完成中断 → 在中断里关闭通道、置发送完成标志。接收链路USART3 每收到一个字节触发 DMA 请求 → DMA 把字节搬到USART3_RX_BUF→ 收满预设长度后触发 DMA 传输完成中断 → 在中断里置接收完成标志应用层轮询这个标志来处理数据。关键点在于CPU 只在「一整包数据搬完」时进一次中断而不是每个字节进一次。这就是 DMA 方案相比中断收发的核心优势。提示接收用 DMA 传输完成中断前提是你知道每包数据的固定长度。如果是变长协议比如 Modbus RTU 靠 3.5 字符间隔断帧需要额外配合 USART 的空闲中断IDLE来判断一帧结束这个后面在改造章节会讲。2.3 缓冲区设计双缓冲与长度约束缓冲区大小直接决定了单次能收发的最大数据量。DMA 的传输长度寄存器DMA_CNDTRx是 16 位的所以单次传输最大 65535 字节。实际项目里一般不会开这么大按协议最大帧长来定就行。#define USART3_TX_BUF_SIZE 256 #define USART3_RX_BUF_SIZE 256 u8 USART3_TX_BUF[USART3_TX_BUF_SIZE]; u8 USART3_RX_BUF[USART3_RX_BUF_SIZE]; volatile u8 usart3_tx_done 1; // 发送完成标志1 表示空闲 volatile u8 usart3_rx_done 0; // 接收完成标志这里usart3_tx_done初值设为 1表示上电后发送通道空闲可以直接发第一包。usart3_rx_done初值 0等 DMA 收满一包后在中断里置 1。两个标志都用volatile修饰防止编译器优化掉中断里的修改。缓冲区建议用全局数组而不是局部变量因为 DMA 传输是异步的函数返回后局部数组就失效了DMA 还在往里搬数据这就是典型的踩坑点。3. 初始化实战从 GPIO 到 DMA 的完整配置链路3.1 GPIO 与 USART3 复用配置USART3 的默认引脚是 PB10TX和 PB11RX。F103 上 USART3 还可以重映射到 PC10/PC11 或 PD8/PD9这里用默认引脚。void USART3_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; // 使能 GPIOB 和 USART3 时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE); // PB10 - TX复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // PB11 - RX浮空输入或上拉输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, GPIO_InitStructure); }TX 引脚必须配成GPIO_Mode_AF_PP复用推挽配成普通推挽输出的话 USART 外设根本控制不了这个引脚。RX 配浮空输入即可如果线路干扰大可以改成上拉输入。时钟这里有个容易翻车的点USART3 挂在 APB1 上要用RCC_APB1PeriphClockCmd而 GPIOB 挂在 APB2 上用RCC_APB2PeriphClockCmd。两个总线搞混了外设就是不动。3.2 USART3 参数初始化void USART3_Config(void) { USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART3, USART_InitStructure); // 使能 USART3 的 DMA 发送和接收请求 USART_DMACmd(USART3, USART_DMAReq_Tx | USART_DMAReq_Rx, ENABLE); // 使能 USART3 USART_Cmd(USART3, ENABLE); }USART_DMACmd这一句是很多人漏掉的关键步骤。它负责打开 USART3 内部的 DMA 请求开关不调用的话USART3 收到数据也不会向 DMA 发请求DMA 自然一动不动。参数USART_DMAReq_Tx | USART_DMAReq_Rx表示收发两个方向的 DMA 请求都使能。波特率 115200 是常见默认值改波特率直接改USART_BaudRate字段即可标准库会根据 APB1 时钟自动算分频。注意 APB1 时钟默认是 36MHz如果你改了系统时钟树要确认RCC_GetClocksFreq算出来的 APB1 频率是对的否则波特率会有偏差。3.3 DMA1 通道配置收发分开DMA 的配置分发送通道Channel 2和接收通道Channel 3两者结构类似但方向和外设地址不同。void USART3_DMA_Config(void) { DMA_InitTypeDef DMA_InitStructure; // 使能 DMA1 时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 发送通道 DMA1_Channel2 DMA_DeInit(DMA1_Channel2); DMA_InitStructure.DMA_PeripheralBaseAddr (u32)USART3-DR; DMA_InitStructure.DMA_MemoryBaseAddr (u32)USART3_TX_BUF; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; // 存储器 - 外设 DMA_InitStructure.DMA_BufferSize USART3_TX_BUF_SIZE; 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_Medium; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel2, DMA_InitStructure); // 接收通道 DMA1_Channel3 DMA_DeInit(DMA1_Channel3); DMA_InitStructure.DMA_PeripheralBaseAddr (u32)USART3-DR; DMA_InitStructure.DMA_MemoryBaseAddr (u32)USART3_RX_BUF; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; // 外设 - 存储器 DMA_InitStructure.DMA_BufferSize USART3_RX_BUF_SIZE; DMA_Init(DMA1_Channel3, DMA_InitStructure); // 配置 DMA 传输完成中断接收通道 DMA_ITConfig(DMA1_Channel3, DMA_IT_TC, ENABLE); NVIC_Configuration(); }逐项说明关键参数DMA_PeripheralBaseAddr固定为USART3-DR因为收发都是通过这一个数据寄存器。DMA_DIR是方向控制发送用DMA_DIR_PeripheralDST外设做目的地接收用DMA_DIR_PeripheralSRC外设做源。DMA_PeripheralInc必须Disable外设地址固定不变DMA_MemoryInc必须Enable存储器地址要递增否则所有字节都堆在缓冲区第一个位置。DMA_Mode_Normal表示搬完设定长度就停适合定长收发。如果要循环接收比如环形缓冲改成DMA_Mode_Circular。DMA_M2M_Disable关闭存储器到存储器模式串口通信必须关掉。接收通道的中断配置里DMA_ITConfig(DMA1_Channel3, DMA_IT_TC, ENABLE)打开传输完成中断这样收满一包后能通知应用层。3.4 NVIC 中断优先级配置void NVIC_Configuration(void) { NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitStructure.NVIC_IRQChannel DMA1_Channel3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); }DMA1_Channel3 的中断向量是DMA1_Channel3_IRQn别写成 Channel2 的。优先级分组用NVIC_PriorityGroup_2抢占优先级 1、子优先级 0这个优先级在大多数项目里够用。如果你的系统里还有更紧急的中断比如电机控制 PWM把 DMA 接收中断的抢占优先级调低一些。4. 收发核心逻辑中断服务函数与状态机4.1 发送函数启动 DMA 并等待完成u8 USART3_SendData(u8 *buf, u16 len) { if (len USART3_TX_BUF_SIZE) return 1; // 超长直接拒绝 if (usart3_tx_done 0) return 2; // 上一包还没发完 usart3_tx_done 0; // 拷贝数据到发送缓冲区 memcpy(USART3_TX_BUF, buf, len); // 关闭通道后才能改传输长度 DMA_Cmd(DMA1_Channel2, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel2, len); DMA_Cmd(DMA1_Channel2, ENABLE); return 0; }这个函数做了三件事长度校验、状态检查、启动 DMA。返回值 0 表示成功1 表示数据超长2 表示发送通道忙。调用方根据返回值决定是重试还是丢弃。关键细节修改DMA_CNDTRx之前必须先关闭通道。DMA 通道在使能状态下传输长度寄存器是只读的直接写不进去。所以顺序一定是「关通道 → 设长度 → 开通道」。这个顺序搞反了现象是发送长度永远是上次的值或者干脆发不出去。4.2 发送完成中断处理void DMA1_Channel2_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC2) ! RESET) { DMA_ClearITPendingBit(DMA1_IT_TC2); DMA_Cmd(DMA1_Channel2, DISABLE); // 搬完关通道 usart3_tx_done 1; // 置空闲标志 } }发送完成中断里做两件事关通道、置标志。关通道是为了下次发送时能重新设置长度。标志置 1 后应用层就知道可以发下一包了。注意这里用的是DMA1_IT_TC2对应 Channel 2 的传输完成标志。中断服务函数名DMA1_Channel2_IRQHandler必须和启动文件里的向量表名字完全一致拼错了就进不了中断。4.3 接收完成中断与数据处理void DMA1_Channel3_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC3) ! RESET) { DMA_ClearITPendingBit(DMA1_IT_TC3); DMA_Cmd(DMA1_Channel3, DISABLE); usart3_rx_done 1; // 通知应用层有完整一包数据 } }接收中断和发送中断结构一样区别在于接收完成后置的是usart3_rx_done。应用层在主循环里轮询这个标志int main(void) { USART3_GPIO_Config(); USART3_DMA_Config(); USART3_Config(); // 启动接收 DMA等待第一包数据 DMA_Cmd(DMA1_Channel3, ENABLE); while (1) { if (usart3_rx_done) { usart3_rx_done 0; // 处理 USART3_RX_BUF 里的数据 ProcessPacket(USART3_RX_BUF, USART3_RX_BUF_SIZE); // 重新启动接收 DMA_SetCurrDataCounter(DMA1_Channel3, USART3_RX_BUF_SIZE); DMA_Cmd(DMA1_Channel3, ENABLE); } // 其他任务照常跑CPU 不被串口占用 } }主循环里处理完一包数据后要重新设置接收长度并重新使能通道准备接下一包。这个「处理完再重启」的模式适合定长协议。如果是变长协议处理逻辑要配合空闲中断下一章会讲。注意ProcessPacket里如果处理时间较长期间来的数据会丢失因为 DMA 通道还没重新使能。对实时性要求高的场景建议用双缓冲或者环形缓冲 空闲中断。5. 避坑与排查那些让 DMA 串口「装死」的细节5.1 一个字节都收不到现象代码编译通过串口助手发数据接收缓冲区始终是初始值usart3_rx_done永远为 0。原因最常见的是 DMA 通道映射搞错。USART3_RX 必须用 DMA1_Channel3配成 Channel2 或别的通道DMA 请求根本不会触发。其次是漏了USART_DMACmd(USART3, USART_DMAReq_Rx, ENABLE)USART 内部的 DMA 请求开关没打开。解决对照参考手册的 DMA 请求映射表确认通道号检查USART_DMACmd是否调用检查DMA_Cmd(DMA1_Channel3, ENABLE)是否执行。5.2 只能收到第一个字节现象发一串数据只有第一个字节正确后面的全是 0 或者乱码。原因DMA_MemoryInc配成了DisableDMA 每次搬运都写到缓冲区同一个地址后面的字节把第一个覆盖了。或者DMA_PeripheralDataSize和DMA_MemoryDataSize配成了 HalfWord/Word而串口是 8 位数据。解决确认DMA_MemoryInc DMA_MemoryInc_Enable两个 DataSize 都配成DMA_PeripheralDataSize_Byte和DMA_MemoryDataSize_Byte。5.3 发送一次后再也发不出去现象第一包数据发送正常第二包调用发送函数返回 2忙或者干脆没反应。原因发送完成中断没进usart3_tx_done一直是 0。可能是中断向量名写错、NVIC 没使能、或者忘了在中断里清标志位导致反复进中断但逻辑不对。解决检查DMA1_Channel2_IRQHandler拼写确认NVIC_Init里DMA1_Channel2_IRQn使能确认中断里调用了DMA_ClearITPendingBit。5.4 数据错位或偶尔丢包现象大部分数据正确偶尔出现字节错位或整包丢失。原因接收处理期间 DMA 通道关闭新来的数据无人接收。或者发送时没等上一包完成就修改了缓冲区。解决接收侧改用双缓冲或环形缓冲配合 USART 空闲中断发送侧严格检查usart3_tx_done标志不要在忙的时候 memcpy 覆盖发送缓冲区。5.5 波特率偏差导致乱码现象数据能收到但全是乱码或者高波特率下误码率明显。原因系统时钟配置和标准库计算的 APB1 频率不一致导致 USART 分频值算错。解决用示波器量一下 TX 引脚的实际波特率或者打印RCC_GetClocksFreq的结果确认 APB1 频率。F103 在 72MHz 系统时钟下 APB1 是 36MHz如果外部晶振不是 8MHz要在system_stm32f10x.c里改HSE_VALUE。6. 二次开发改造变长协议与环形缓冲的落地思路定长收发在实际项目里其实不多见大部分协议是变长的。比如 Modbus RTU 靠帧间隔断帧自定义协议靠帧头帧尾。这时候纯 DMA 传输完成中断就不够用了需要引入USART 空闲中断IDLE。思路是这样的DMA 配成DMA_Mode_Circular或者保持 Normal 但缓冲区开大同时打开 USART3 的 IDLE 中断。每收到一帧数据总线空闲时触发 IDLE 中断在中断里读取 DMA 剩余传输数量DMA_GetCurrDataCounter(DMA1_Channel3)用「缓冲区总长 - 剩余数量」算出这一帧的实际长度。void USART3_IRQHandler(void) { if (USART_GetITStatus(USART3, USART_IT_IDLE) ! RESET) { // 清除 IDLE 标志先读 SR 再读 DR volatile u16 tmp; tmp USART3-SR; tmp USART3-DR; (void)tmp; // 计算本帧长度 u16 remain DMA_GetCurrDataCounter(DMA1_Channel3); u16 recv_len USART3_RX_BUF_SIZE - remain; if (recv_len 0) { // 标记本帧有效交给应用层处理 usart3_rx_len recv_len; usart3_rx_done 1; } // 重启 DMA 接收 DMA_Cmd(DMA1_Channel3, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel3, USART3_RX_BUF_SIZE); DMA_Cmd(DMA1_Channel3, ENABLE); } }这里有个细节清 IDLE 标志必须「先读 SR 再读 DR」这是 F103 的硬件要求只读 SR 清不掉。我一般会用一个volatile临时变量接一下防止编译器把读操作优化掉。再进一步如果数据量大、处理耗时可以把接收缓冲区做成环形缓冲DMA 配成循环模式应用层按读指针取数据。这样 DMA 永远在收不会因为处理慢而丢包。环形缓冲的读写指针要注意原子性单生产者DMA 中断单消费者主循环的场景下读写指针各自只被一方修改不需要加锁但指针类型要用volatile并且保证读写顺序。改造完之后验证方法很简单用串口助手发不同长度的数据帧看usart3_rx_len是否和实际发送长度一致再连续快速发多帧看有没有丢帧。我习惯在ProcessPacket里加一个帧计数器跑一晚上看计数和发送端是否对得上。从那以后我每次配 DMA 串口都强制走一遍「时钟 → GPIO → USART → DMA 通道 → NVIC → 中断向量名」的检查清单尤其是通道映射和USART_DMACmd这两步翻车太多次了。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑