STM32串口通信实战:从原理到DMA应用与调试技巧

📅 发布时间:2026/8/7 7:41:30
STM32串口通信实战:从原理到DMA应用与调试技巧 1. 项目概述从零上手STM32串口通信搞嵌入式开发尤其是玩STM32的串口通信绝对是绕不开的第一个“硬骨头”。它不像点个LED灯那么简单但又是后续所有复杂通信比如网络、蓝牙的基础。很多新手包括当年的我在配置串口发送一个“Hello World”或者稳定接收一串数据时都会遇到各种稀奇古怪的问题为什么发送的数据在调试助手上是乱码为什么接收总是丢字节或者卡死DMA又是啥怎么用这个项目我们就来彻底盘一盘STM32的串口USART/UART发送与接收。我会基于最常用的标准外设库虽然HAL库是趋势但标准库更能让你理解底层寄存器操作手把手带你从原理到配置从查询方式到中断再到DMA这种高效玩法最后把那些实际调试中踩过的坑和应对技巧都分享出来。无论你是刚开始接触STM32还是已经用过但想更深入地理解这篇文章都能给你提供一份可以直接“抄作业”的实操指南。2. 核心原理与硬件基础扫盲2.1 USART vs UART别再傻傻分不清在STM32的数据手册里你经常会看到USART和UART这两个词。它们有联系也有区别搞清楚这个对后续配置和理解通信模式很重要。简单来说UART是通用异步收发传输器。它只负责异步串行通信也就是我们常说的“串口”。通信双方没有统一的时钟线完全依靠事先约定好的波特率来同步数据位。因此UART最少只需要两根线TX发送和RX接收。USART是通用同步异步收发传输器。顾名思义它比UART多了一个“S”Synchronous即同步功能。在同步模式下USART可以额外提供一根时钟线CK在发送数据的同时输出时钟信号供接收方采样常用于一些需要高速、可靠时钟同步的场合比如连接某些特定类型的SPI设备或智能卡。但在绝大多数我们的应用场景中比如连接电脑串口、GPS模块、蓝牙模块等使用的都是它的异步UART模式。所以你可以粗略地认为在STM32上我们用的“串口”指的就是USART外设的异步工作模式。注意STM32的USART外设功能非常强大除了基本收发还支持硬件流控制RTS/CTS、多处理器通信、LIN总线、IrDA红外编码等。对于入门我们先聚焦最核心的异步全双工收发。2.2 关键参数解析通信的“暗号”要让两个设备通过串口成功对话必须事先对好“暗号”也就是配置相同的通信参数。主要包含以下几个波特率这是最重要的参数表示每秒传输的符号个数。常见的波特率有9600 115200 460800等。波特率越高传输速度越快但对时钟精度和线路抗干扰能力要求也越高。发送和接收设备的波特率必须严格一致否则必然出现乱码。STM32的波特率由外设时钟如APB2通过一个分频器计算得到配置时需要根据你的系统时钟精确计算。数据位指每个字符数据由多少位组成。通常是8位这也是最常用的因为一个字节刚好是8位。有时也会见到7位用于某些老式设备或特定协议。停止位用于表示一个字符数据的结束。可以是1位、1.5位或2位。绝大多数情况下1位停止位是标准配置。奇偶校验位用于简单的错误检测。可以是奇校验、偶校验或无校验。如果选择奇/偶校验数据位校验位的总和中“1”的个数将为奇数或偶数。在要求不高的场合通常选择无校验。硬件流控制通过RTS请求发送和CTS清除发送两根额外的信号线来控制数据流防止缓冲区溢出。在高速通信或与某些老式调制解调器连接时会用到。对于连接PC调试助手或大多数模块通常禁用硬件流控制。我们最常用的配置组合是波特率115200 8位数据位 1位停止位无奇偶校验无硬件流控制。这个配置在绝大多数串口调试助手和模块上都是默认选项兼容性最好。2.3 硬件连接三线制是最简模型要实现最基本的双向通信你需要连接三根线STM32的TX-对方设备的RXSTM32的RX-对方设备的TXGND-GND这里最容易出错的就是TX和RX的交叉连接。记住一个口诀发送对接收接收对发送。STM32要发送数据就从它的TX引脚输出这个信号应该连接到对方设备的RX引脚让对方“听”见。反之亦然。如果你使用的是USB转TTL串口模块比如CH340、CP2102、PL2303模块连接电脑连接方式如下STM32的TX 接 模块的RXSTM32的RX 接 模块的TXSTM32的GND 接 模块的GND模块的VCC3.3V或5V接STM32的供电务必确认电压匹配STM32通常是3.3V电平3. 软件驱动配置详解基于标准库理解了原理我们开始动手配置。这里以STM32F103系列最常用的USART1为例它通常挂在APB2总线上时钟频率最高引脚是PA9(TX)和PA10(RX)。3.1 初始化流程六步法标准库的配置有清晰的步骤我把它总结为“六步法”第一步开启时钟任何外设使用前必须先开启其对应的时钟。USART1挂在APB2总线上GPIOA也挂在APB2上。RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);第二步配置GPIO将USART对应的TX引脚配置为复用推挽输出RX引脚配置为浮空输入或上拉输入抗干扰更好。GPIO_InitTypeDef GPIO_InitStructure; // 配置TX引脚 (PA9) GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 配置RX引脚 (PA10) GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; // 浮空输入 // GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; // 或者上拉输入 GPIO_Init(GPIOA, GPIO_InitStructure);第三步配置USART基本参数设置波特率、数据位、停止位、校验位等。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(USART1, USART_InitStructure);第四步使能USART配置好后需要使能USART外设才能开始工作。USART_Cmd(USART1, ENABLE);到这一步其实你已经可以用查询方式发送数据了。但为了高效接收我们通常会启用中断。第五步配置NVIC中断控制器如果要用中断接收必须配置好中断优先级。NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 设置优先级分组整个项目统一 NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; // 抢占优先级 NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; // 子优先级 NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure);第六步使能接收中断告诉USART当收到数据时产生中断。USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 接收缓冲区非空中断 // 如果需要发送完成中断用于DMA或特定流程也可以使能 // USART_ITConfig(USART1, USART_IT_TC, ENABLE);3.2 发送数据的三种姿势姿势一查询发送阻塞式这是最简单直接的方式。不断检查发送数据寄存器是否为空TXE为空则填入数据。void USART_SendByte(USART_TypeDef* USARTx, uint8_t data) { USART_SendData(USARTx, data); while(USART_GetFlagStatus(USARTx, USART_FLAG_TXE) RESET); // 等待发送完成 } void USART_SendString(USART_TypeDef* USARTx, char *str) { while(*str ! \0) { USART_SendByte(USARTx, *str); } }优点代码简单逻辑清晰。缺点while循环等待会阻塞CPU如果波特率低或者发送数据量大会严重影响程序其他部分的执行效率。姿势二中断发送通常不单独为发送使能中断因为发送的时机是程序主动控制的。中断发送更多用于配合DMA或者在发送完成TC标志后通知程序进行后续操作如关闭DMA、切换缓冲区等。单纯的字节中断发送效率提升有限反而增加中断开销。姿势三DMA发送推荐用于大数据量这是最高效的方式。CPU只需要配置好DMA的源地址内存数组、目标地址USART数据寄存器、数据长度然后启动DMA。DMA控制器会在后台自动将数据“搬运”到串口发送整个过程不占用CPU时间。配置相对复杂我们会在后面DMA章节详细讲。3.3 接收数据的三种姿势与经典框架接收是串口的难点因为数据到来的时间是未知的。姿势一查询接收不推荐不断轮询接收数据寄存器非空RXNE标志。uint8_t USART_ReceiveByte(USART_TypeDef* USARTx) { while(USART_GetFlagStatus(USARTx, USART_FLAG_RXNE) RESET); // 等待收到数据 return USART_ReceiveData(USARTx); }缺点严重阻塞如果一直没数据程序就卡死了。实战中几乎不用。姿势二中断接收最常用、最灵活这是处理接收的黄金标准。数据一到触发中断在中断服务函数里快速把数据读走并存入缓冲区。核心是设计一个环形缓冲区。环形缓冲区实现要点定义缓冲区数组和头尾指针。接收中断服务函数里读取数据放入尾指针位置尾指针循环后移。主循环里判断头尾指针是否不同如果不同则从头部取出数据处理。关键操作移动指针、判断空/满都需要考虑临界保护因为中断可能在任何时候发生。一个简化的示例框架#define RX_BUF_SIZE 256 uint8_t usart_rx_buf[RX_BUF_SIZE]; volatile uint16_t usart_rx_head 0; // 主循环读取位置 volatile uint16_t usart_rx_tail 0; // 中断写入位置 void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART1); uint16_t next_tail (usart_rx_tail 1) % RX_BUF_SIZE; // 简单的缓冲区满判断丢弃最早的数据或新数据根据需求定 if(next_tail ! usart_rx_head) { usart_rx_buf[usart_rx_tail] data; usart_rx_tail next_tail; } // 更复杂的可以在这里判断帧尾如\n设置帧接收完成标志 USART_ClearITPendingBit(USART1, USART_IT_RXNE); } } // 在主循环中处理数据 void ProcessRxData(void) { while(usart_rx_head ! usart_rx_tail) { uint8_t data usart_rx_buf[usart_rx_head]; usart_rx_head (usart_rx_head 1) % RX_BUF_SIZE; // 对data进行处理例如解析命令 // ... } }姿势三DMA接收推荐用于高速、大数据流DMA接收可以做到“无感”接收。配置DMA循环模式指向一个足够大的缓冲区。数据会源源不断地自动存入缓冲区DMA的“半传输完成”和“传输完成”中断可以通知CPU去处理前半段或后半段数据实现“乒乓操作”几乎零丢失。同样配置放在后面DMA章节。4. 进阶实战DMA在串口收发中的应用当你需要高速比如921600波特率传输或者传输大量数据如图片、音频帧时查询和中断方式都会因为频繁进出中断而成为瓶颈。此时DMA就是你的救星。4.1 DMA发送配置与注意事项假设我们使用USART1对应DMA1的通道4对于STM32F103具体通道查数据手册。配置步骤开启DMA和USART时钟。配置DMA通道外设地址是USART1-DR内存地址是你的数据数组传输方向是内存到外设传输数据量循环模式通常关闭发送完即停内存地址递增外设地址不变。配置USART的DMA使能USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);启动DMA传输。关键技巧发送完成判断启动DMA后不能立即认为发送完成了。需要等待DMA传输完成标志或者更常见的等待USART的发送完成TC标志。因为DMA只负责把数据搬到USART的数据寄存器USART自己还需要时间把数据一位一位地发出去。// 启动DMA发送 DMA_Cmd(DMA1_Channel4, ENABLE); // 等待DMA传输完成 while(DMA_GetFlagStatus(DMA1_FLAG_TC4) RESET); DMA_ClearFlag(DMA1_FLAG_TC4); // 等待USART发送真正完成可选但推荐 while(USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); USART_ClearFlag(USART1, USART_FLAG_TC);内存数据有效性DMA传输是独立进行的。你必须确保在DMA传输期间源数据数组的内容不能被修改比如放在全局数组或动态分配的内存中且不被其他代码访问。如果数据需要更新要在确认上一次DMA传输完成后更新数据再启动下一次。4.2 DMA接收配置与“乒乓操作”DMA接收通常配置为循环模式这样缓冲区满了之后会自动从头开始覆盖实现不间断接收。配置步骤开启DMA和USART时钟。配置DMA通道外设地址是USART1-DR内存地址是接收缓冲区传输方向是外设到内存传输数据量设为缓冲区大小开启循环模式内存地址递增外设地址不变。配置USART的DMA使能USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);启动DMA传输。“乒乓操作”实现为了能及时处理接收到的数据而不丢失我们可以利用DMA的“半传输完成”HT和“传输完成”TC中断。将接收缓冲区分为大小相等的A、B两半。使能DMA的HT和TC中断。当DMA接收到半缓冲区数据时触发HT中断此时可以处理A半区的数据。当DMA接收满整个缓冲区时触发TC中断此时可以处理B半区的数据。因为DMA是循环的处理完A半区后DMA可能正在向B半区写入新数据反之亦然。这样就实现了接收和处理的并行。获取当前接收数据量在循环模式下DMA通道的CNDTR寄存器会实时记录剩余未传输的数据量。用设定的缓冲区大小减去CNDTR就能得到当前已经接收到的数据量从而知道该从哪里读取有效数据。uint16_t Get_USART_DMA_Rx_Count(void) { return RX_BUF_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5); // 假设是通道5 }5. 调试技巧与常见问题排查实录理论配置都懂了一上电调试问题就来了。下面是我多年调试串口总结的“血泪”经验和常见问题排查表。5.1 硬件排查三板斧查接线这是最低级也最常犯的错误。反复确认TX-RX是否交叉连接GND是否共地。可以用万用表通断档测量。查电平STM32是3.3V电平。如果你的USB转串口模块是5V电平直接连接可能会损坏STM32的IO口。务必使用3.3V电平的模块或者中间加电平转换芯片。查驱动电脑端设备管理器里你的USB转串口模块是否被正确识别端口号如COM3是多少串口调试助手选择的端口号对吗5.2 软件配置常见坑问题现象可能原因排查方法完全没数据1. USART或GPIO时钟未开启。2. GPIO模式配置错误TX未配成复用推挽。3. 波特率计算错误与实际偏差太大。4. 硬件连接错误或接触不良。1. 检查RCC配置代码。2. 用逻辑分析仪或示波器抓TX引脚波形看是否有数据信号。3. 核对波特率计算公式波特率 fCK / (16 * USARTDIV)确保分频值USARTDIV设置正确。4. 重新检查硬件。发送数据为乱码1.波特率不匹配最常见。2. 数据位、停止位、校验位配置与接收方不一致。3. 系统时钟HCLK配置错误导致所有外设时钟都不对。1.重点检查发送端和接收端如串口调试助手的波特率是否完全一致。2. 核对双方的数据格式设置。3. 检查SystemInit()函数或手动配置的系统时钟树确认APB总线时钟频率是否正确。只能发送不能接收1. RX引脚模式配置错误应为浮空/上拉输入。2. 接收中断未使能或NVIC未配置。3. 中断服务函数名写错或未清除中断标志。1. 检查GPIO初始化代码。2. 检查USART_ITConfig和NVIC_Init。3. 检查启动文件.s中的中断向量表确保函数名与向量表内名称一致。在中断函数里务必清除对应的中断标志位如USART_ClearITPendingBit。接收数据不完整或随机出错1. 中断服务函数处理太慢导致溢出ORE标志置位。2. 缓冲区太小或未使用缓冲区。3. 中断优先级被其他高优先级中断抢占。4. 电源噪声或地线干扰。1. 在中断函数开始读取数据前先检查USART_GetFlagStatus(USARTx, USART_FLAG_ORE)如果溢出先读USART_ReceiveData一次来清除ORE标志。2. 增大环形缓冲区并确保中断内只做最简单的存数据操作。3. 适当提高串口接收中断的优先级抢占优先级。4. 在RX引脚对地加一个20-50pF的电容滤波并确保电源稳定。使用DMA时数据错误1. DMA通道映射错误。2. 内存/外设地址配置错误。3. 数据宽度不匹配如内存是字节外设是半字。4. DMA传输未完成就修改了源数据。1. 查阅数据手册确认USART对应的DMA请求通道。2. 仔细检查DMA_Init结构体中的DMA_PeripheralBaseAddr和DMA_MemoryBaseAddr。3. 检查DMA_PeripheralDataSize和DMA_MemoryDataSize。4. 确保在DMA传输完成中断或查询标志确认完成后再更新源数据缓冲区。5.3 调试心得与高级技巧活用printf重定向将printf函数重定向到串口是调试输出的神器。通过实现fputc或使用ARMCC的MicroLIB库可以方便地打印变量值、程序状态。但注意printf是阻塞式发送且效率较低不适合在实时性要求高的中断或主循环中频繁调用。设计简单的通信协议如果只是乱发字节很难区分数据的开始和结束。可以设计一个简单的帧结构例如帧头如0xAA 0x55 数据长度 数据内容 校验和如累加和或CRC8。在接收中断中使用状态机来解析帧可以大大提高通信的可靠性。注意中断嵌套与优先级如果系统中有多个中断一定要规划好优先级。串口接收中断的优先级应该设置得相对较高确保数据来了能及时响应防止因被其他中断长时间阻塞而丢失数据。但也不要设为最高避免影响更紧急的系统任务如看门狗。低功耗下的串口唤醒在一些低功耗应用中MCU处于睡眠模式可以通过串口接收数据的起始位来唤醒MCU。这需要配置串口工作在特定模式如使能静默模式下的唤醒功能并配置好相应的中断。这是一个高级话题但在电池供电设备中很实用。使用硬件流控制当通信波特率非常高1Mbps或者接收端处理不过来时可以考虑启用RTS/CTS硬件流控制。它能由硬件自动控制数据流从根本上避免因缓冲区满导致的数据丢失但需要多连接两根线。串口通信是嵌入式工程师的必备技能从简单的参数配置到复杂的DMA中断环形缓冲区框架其背后体现的是对MCU外设、中断系统和数据流的深刻理解。我个人的经验是初期一定要把标准库的查询和中断方式吃透自己动手实现一个稳定的环形缓冲区接收框架。之后再去攻克DMA理解其“搬运工”的本质和内存-外设的协作关系。调试时耐心和逻辑是关键按照“硬件-基础配置-中断/DMA-应用逻辑”的顺序层层排查大部分问题都能迎刃而解。最后别忘了给你的通信数据穿上“协议”的外衣这是工程化、产品化不可或缺的一步。