STM32 USART串口通信:从协议原理到DMA实战应用

发布时间:2026/8/1 15:22:08
STM32 USART串口通信:从协议原理到DMA实战应用 1. 项目概述从零开始理解STM32的“嘴巴”和“耳朵”如果你刚开始接触STM32面对一堆外设名词感到头大那“USART”绝对是你绕不开的第一个坎。它不像GPIO点个灯那么直观也不像定时器那样抽象USART是微控制器与外界“对话”最基础、最常用的方式。你可以把它想象成STM32的“嘴巴”和“耳朵”通过TX发送引脚“说”出数据通过RX接收引脚“听”进数据。我当年调试第一个串口程序花了整整一下午才让电脑端的串口助手显示出“Hello World”那种从无到有的成就感至今记忆犹新。无论是调试时打印日志还是驱动GPS模块、蓝牙模块或是与另一个单片机“聊天”USART都是核心角色。网上资料虽多但往往要么过于晦涩直接甩寄存器要么过于简单只讲库函数调用让新手知其然不知其所以然。这篇内容我就结合自己踩过的坑和项目经验带你从电路原理、协议层到代码实现彻底搞懂STM32的USART让你不仅能“抄作业”跑通程序更能明白每一步背后的道理真正入门嵌入式通信。2. USART通信协议核心原理拆解2.1 物理层电平与硬件连接很多人一上来就写代码结果发现数据死活不对一半以上的问题出在物理连接和电平理解上。USART通信在物理层通常采用异步串行方式关键词是“异步”和“串行”。“串行”好理解就是数据一位一位地排队发送相对于并行的同时发送8位或更多节省了引脚但速度慢些。更关键的是“异步”这意味着通信双方没有统一的时钟线来同步节奏。想象一下两个人约好“每秒说一个字”但没有手表对时很容易说岔。为了解决这个问题USART协议规定了一套严格的“说话节奏”这就是波特率。双方必须约定相同的波特率比如9600bps即每秒传输9600个二进制位。每个位的持续时间是1/9600秒。发送方按这个节奏发送接收方也按这个节奏去采样监听从而实现同步。这里第一个坑就来了常见的STM32系统主频是72MHz或168MHz如何精准地产生1/9600秒这样的时间单位这依赖于芯片内部的波特率发生器一个由定时器驱动的分频器。配置波特率时实际上是在设置这个分频器的值。如果计算稍有偏差就会导致采样点偏移数据出错。STM32的USART波特率寄存器USART_BRR是一个浮点分频器配置时需要根据主频精确计算。硬件连接上记住一个原则交叉互联。MCU的TX引脚应连接对方设备的RX引脚MCU的RX连接对方的TX。直连的话双方都“说”不到对方“耳朵”里。电平标准方面在3.3V系统的STM32上逻辑1通常是3.3V逻辑0是0VTTL电平。若要与传统PC的RS232接口电平为±12V通信中间必须加一个电平转换芯片如MAX3232这是新手极易忽略的一点直接连接可能烧毁芯片。2.2 协议层数据帧格式与传输过程解决了物理上“怎么传”的问题接下来看“传什么”和“传成什么样”。USART的一帧数据绝不是简单地把8位数据扔出去那么简单它被包装在一个标准的“信封”里这个信封的格式是起始位 数据位 校验位 停止位。这是协议层的核心必须吃透。起始位这是一个固定的逻辑低电平0持续1个波特率周期。它的作用就像跑步时的“各就位预备——跑”告诉接收方“注意后面紧跟的就是有效数据了”接收端一直在监听线路当检测到从空闲高电平跳转到低电平时就启动内部定时器准备在后续的特定时刻点采样数据。数据位紧接起始位之后就是我们要传输的有效数据可以是5、6、7、8或9位最常用的是8位一个字节。数据位是低位LSB先发。例如发送字节0x55二进制01010101实际在线上出现的顺序是1LSB-0-1-0-1-0-1-0MSB。校验位这是一个可选的位用于简单的错误检测。常见的有偶校验保证数据位校验位中“1”的个数为偶数。奇校验保证“1”的个数为奇数。无校验不添加此位。始终为0/1较少用。 例如发送数据0x3100110001其中“1”的个数是3奇数。若使用偶校验则校验位应为1使总数为4偶数若使用奇校验则校验位应为0。接收方会按相同规则计算并比对若不匹配则提示校验错误。注意校验位只能检测奇数个位发生的错误对于偶数个位错误无效。停止位帧的结束标志必须是逻辑高电平1可以是1、1.5或2个波特率周期。它有两个作用一是保证帧之间有最起码的空闲时间高电平便于区分连续的两帧二是给接收方一点处理时间。通常使用1位停止位。一帧完整的传输过程如下线路空闲时为高电平。发送开始时TX引脚拉低一个位时间作为起始位然后依次送出数据位LSB first、校验位如果有最后拉高并保持至少一个位时间作为停止位。然后线路恢复高电平空闲等待下一帧。注意波特率误差是通信稳定的杀手。除了确保双方波特率设置一致还要注意STM32的时钟源HSE/HSI精度。使用内部RC振荡器HSI可能会有1%以上的误差在较高波特率如115200长距离通信时可能导致累积错误。对于要求高的场合务必使用外部晶振HSE。3. STM32 USART外设功能深度解析3.1 关键寄存器与功能模式理解了协议我们来看STM32如何用硬件实现它。STM32的USART外设功能相当强大远不止简单的发送接收。通过配置一系列寄存器我们可以灵活控制其行为。对于初学者理解几个核心寄存器至关重要USART_SR (状态寄存器)这是你了解USART“在干什么”的窗口。关键标志位有TXE发送数据寄存器空为1时表示发送数据寄存器TDR空了可以写入下一个要发送的数据。这是最常用的发送查询标志。TC发送完成为1时表示包括停止位在内的整个帧都已从移位寄存器发送出去线路完全空闲。在需要严格知道发送完毕的场景如关闭串口前使用。RXNE接收数据寄存器非空为1时表示接收数据寄存器RDR里有新数据可以读取。这是最常用的接收查询标志。ORE溢出错误、FE帧错误、NE噪声错误、PE校验错误这些错误标志位能帮你诊断通信问题在可靠性要求高的程序中需要定期检查并清除。USART_DR (数据寄存器)这是一个“双缓冲”寄存器。写操作是针对发送数据寄存器TDR读操作是针对接收数据寄存器RDR。你向USART_DR写入一个值硬件会将其加载到发送移位寄存器中然后按波特率一位一位地从TX引脚移出。同时RX引脚接收到的数据在完成一帧后会从接收移位寄存器转移到RDR你从USART_DR读出的就是这个值。USART_BRR (波特率寄存器)这是设置通信速度的关键。如前所述它是一个32位寄存器高16位用于整数部分DIV_Mantissa低4位用于小数部分DIV_Fraction。计算公式为波特率 f_PCLK / (16 * USARTDIV)其中USARTDIV就是BRR寄存器表示的浮点数值。例如在PCLK72MHz下要得到9600波特率USARTDIV 72000000 / (16 * 9600) 468.75。那么BRR应设置为整数部分4680x1D4小数部分0.75*16120xC所以BRR 0x1D4C。CubeMX等工具会自动帮你计算好这个值。USART_CR1 (控制寄存器1)功能开关大全。你需要在这里使能USARTUE位、使能发送TE位和接收RE位。还可以配置字长M位0代表8位数据1代表9位、校验控制PCE位和PS位、中断使能TXEIE,TCIE,RXNEIE等。3.2 三种编程方式查询、中断与DMA掌握了寄存器就可以选择如何“驱动”USART了。主要有三种方式适用不同场景查询方式最简单适合初学者理解和简单应用。程序不断轮询状态寄存器USART_SR的标志位。发送先检查TXE是否为1是则写入USART_DR。接收不断检查RXNE是否为1是则读取USART_DR。优点逻辑直观代码简单。缺点CPU被长时间占用在“等待”上效率极低。如果等待接收数据程序其他部分就无法执行系统实时性差。中断方式最常用平衡了效率与复杂度。当特定事件如发送寄存器空、接收完成发生时硬件自动触发中断CPU暂停当前工作去处理串口事务。发送中断通常使能TXE中断。当TXE1时进入中断服务函数填入下一个要发送的数据。注意发送完最后一字节后应关闭TXE中断转而使能TC中断在TC中断中完成发送完毕后的清理工作如关闭串口、切换状态等否则会不断进入TXE中断。接收中断使能RXNE中断。一旦收到一字节数据立即进入中断服务函数读取USART_DR并将数据存入自定义的缓冲区如数组或队列。优点CPU无需主动查询解放出来处理其他任务只在有数据时被“打断”一下效率高。缺点中断服务函数必须尽量简短快出避免嵌套或长时间占用影响其他中断。频繁的单字节中断在高速大数据量时仍有开销。DMA方式高效处理批量数据适合高速、大数据块传输。DMA直接存储器访问控制器可以在不打扰CPU的情况下在外设如USART的DR寄存器和内存如一个数组之间直接搬运数据。发送DMA设置DMA源地址为内存中的待发送数组目标地址为USART_DR。启动后DMA自动将数组中的数据逐个搬到USART_DR发送出去发送完成后产生DMA完成中断通知CPU。接收DMA设置DMA源地址为USART_DR目标地址为内存中的接收数组。DMA会自动将接收到的数据存入数组存满指定长度后产生中断。优点极致高效尤其适合连续数据流如GPS数据解析、文件传输。CPU完全解放。缺点配置相对复杂需要理解DMA通道、数据宽度、传输模式等概念。对于不定长数据接收传统DMA模式处理起来较麻烦需结合空闲中断。实操心得对于绝大多数应用**“接收用中断环形缓冲区发送用中断或DMA”**是黄金组合。接收中断保证数据不丢失存入环形缓冲区主程序从缓冲区取出处理。发送少量数据可用中断发送大量数据如图像、语音务必用DMA。新手可以从查询和中断入手熟练后再攻克DMA。4. 从零搭建USART通信工程以STM32F103为例4.1 硬件设计与软件环境准备我们以最经典的STM32F103C8T6蓝色pill开发板为例使用USART1与电脑通信。硬件连接非常简单STM32的PA9(USART1_TX) 接 USB转TTL模块的RX。STM32的PA10(USART1_RX) 接 USB转TTL模块的TX。两者共地GND相连。STM32和USB转TTL模块均由稳定的5V或3.3V电源供电注意电平兼容常见的USB转TTL模块多是3.3V/5V兼容跳线帽选择3.3V即可。软件环境我推荐两种标准库/HAL库 Keil MDK传统且资源丰富。HAL库封装程度高上手快但代码效率稍低。标准库更接近寄存器效率高但STM32已停止更新。STM32CubeMX HAL库 Keil/IAR当前主流。CubeMX图形化配置引脚、时钟、外设参数自动生成初始化代码极大降低入门门槛。我们以此为例。首先打开STM32CubeMX新建工程选择MCU型号。在Pinout Configuration视图左侧找到Connectivity-USART1。将模式Mode设置为Asynchronous异步。下方参数配置Parameter SettingsBaud Rate: 115200 (常用)Word Length: 8 BitsParity: NoneStop Bits: 1Over Sampling: 16 Samples (默认)此时右侧芯片图上PA9和PA10会自动被配置为USART1_TX和USART1_RX。接着配置时钟树Clock Configuration。对于F103通常使用外部8MHz晶振HSE经过PLL倍频到72MHz作为系统时钟SYSCLK。APB2总线时钟PCLK2也为72MHzUSART1挂载在APB2上。在Project Manager里设置好工程名、路径、选择IDEMDK-ARM在Code Generator中勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”这样代码更清晰。点击GENERATE CODE生成工程。4.2 基础收发功能代码实现与解析打开生成的Keil工程在main.c中CubeMX生成的初始化代码MX_USART1_UART_Init()已经完成了USART1的硬件配置。我们只需要调用HAL库的API进行收发。查询方式实现// 发送一个字符阻塞式 void USART1_SendByte(uint8_t data) { HAL_UART_Transmit(huart1, data, 1, 1000); // 超时1000ms } // 发送字符串 void USART1_SendString(char *str) { HAL_UART_Transmit(huart1, (uint8_t*)str, strlen(str), 1000); } // 接收一个字符阻塞式 uint8_t USART1_ReceiveByte(void) { uint8_t data; HAL_UART_Receive(huart1, data, 1, 1000); // 会一直等待直到收到数据或超时 return data; } // 在主循环中测试 char rxData; rxData USART1_ReceiveByte(); // 等待接收一个字符 USART1_SendByte(rxData); // 将收到的字符回传echo代码解析HAL_UART_Transmit/Receive是阻塞函数。Transmit会等待直到数据发送完毕或超时Receive会等待直到收到指定数量的数据或超时。这种方式代码简单但Receive会卡住整个程序。中断方式实现更实用首先在CubeMX中配置USART1的NVIC嵌套向量中断控制器使能它的全局中断。 在代码中// 启动中断接收通常在主初始化后调用一次 HAL_UART_Receive_IT(huart1, rxBuffer, 1); // 每次接收1字节到rxBuffer // 重写接收完成回调函数弱函数需要用户实现 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart-Instance USART1) { // 处理接收到的数据 rxBuffer // 例如存入环形队列或直接回传 HAL_UART_Transmit_IT(huart1, rxBuffer, 1); // 中断方式回传 // 重新启动中断接收准备接收下一个字节至关重要 HAL_UART_Receive_IT(huart1, rxBuffer, 1); } } // 中断方式发送字符串非阻塞 void USART1_SendString_IT(char *str) { HAL_UART_Transmit_IT(huart1, (uint8_t*)str, strlen(str)); }代码解析HAL_UART_Receive_IT启动一次中断接收收到一字节后触发UART_IRQHandler最终调用HAL_UART_RxCpltCallback。关键点必须在回调函数中再次调用HAL_UART_Receive_IT否则只会接收一次。发送中断HAL_UART_Transmit_IT会在发送完成后调用HAL_UART_TxCpltCallback。4.3 进阶应用DMA收发与不定长数据接收DMA发送在CubeMX中除了配置USART1还需在DMA Settings中添加发送和接收的DMA请求如USART1_TX到MemoryUSART1_RX到Memory。选择通道如DMA1 Channel4 for USART1_TX模式设为Normal发送一次或Circular循环数据宽度对齐。uint8_t txData[] Hello DMA!\r\n; // 使用DMA发送非阻塞 HAL_UART_Transmit_DMA(huart1, txData, sizeof(txData) - 1); // 减1排除字符串结束符\0 // 可以在发送完成回调函数中做后续处理 void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if(huart-Instance USART1) { // DMA发送完成可以点亮一个LED或准备下一包数据 } }不定长数据接收IDLE中断 DMA这是实际项目中最常用的技巧。例如接收一串以回车换行结尾的指令长度未知。单纯DMA需要知道长度单纯字节中断处理拼接又麻烦。结合**空闲中断IDLE**和DMA是完美方案。 空闲中断在线路从有数据变为空闲高电平持续一帧时间时触发。我们可以用DMA在后台连续接收数据存入缓冲区当一帧数据发送完毕线路空闲触发IDLE中断。在中断里通过查询DMA当前剩余传输数量就能计算出已经接收了多少个数据。CubeMX配置使能USART1的全局中断和DMA接收。在USART1的参数设置中Advanced Features下勾选USART1 global interrupt和DMA settings中添加RX的DMA流。代码实现#define RX_BUF_SIZE 256 uint8_t rxBuffer[RX_BUF_SIZE]; // DMA接收缓冲区 // 启动DMA接收循环模式持续接收 HAL_UART_Receive_DMA(huart1, rxBuffer, RX_BUF_SIZE); // 使能空闲中断 __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE); // 在USART1中断服务函数中stm32f1xx_it.c添加空闲中断处理 void USART1_IRQHandler(void) { HAL_UART_IRQHandler(huart1); // 处理HAL库定义的中断 // 用户自定义的空闲中断处理 if(__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE) ! RESET) { __HAL_UART_CLEAR_IDLEFLAG(huart1); // 清除空闲中断标志重要 // 计算接收到的数据长度 uint16_t rxLen RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx); if(rxLen 0) { // 此时rxBuffer[0] 到 rxBuffer[rxLen-1] 就是接收到的一帧数据 processReceivedData(rxBuffer, rxLen); // 处理数据 // 重新启动DMA接收因为DMA在循环模式下会自动继续但为了安全可重置 // HAL_UART_Receive_DMA(huart1, rxBuffer, RX_BUF_SIZE); } } }这个方案既能高效接收又能准确识别帧结束是工业级串口应用的标配。5. 调试技巧与常见问题实战排查5.1 硬件排查与软件调试方法通信不通首先进行硬件排查检查接线TX-RX是否交叉连接GND是否共地这是最低级也最常犯的错误。检查电源用万用表测量STM32和USB转TTL模块的VCC电压是否稳定3.3V或5V。检查引脚确认程序配置的引脚PA9/PA10与实际连接的引脚一致。用示波器或逻辑分析仪是最直观的探头接TX引脚应该能看到发送数据时规律的方波。如果没有波形说明程序可能没运行或USART未正确初始化。如果有波形但电脑端收不到检查波特率设置和电平转换。软件调试利用printf重定向这是最强大的调试工具。将printf函数输出重定向到串口就可以方便地打印变量值、程序状态。// 在工程中重写fputc函数 int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 1000); return ch; } // 然后在代码中直接使用printf printf(System Clock: %lu Hz\r\n, HAL_RCC_GetSysClockFreq());记得在Keil的Target选项里勾选Use MicroLIB这是一个针对嵌入式优化的小型C库。使用断点和变量观察在发送或接收函数前后设置断点观察缓冲区数据、状态寄存器标志位如USART_SR的值。串口助手设置确保电脑端串口助手如XCOM、SSCOM的端口号、波特率、数据位、停止位、校验位与STM32程序设置完全一致。特别是波特率115200和9600差很多。5.2 典型问题与解决方案速查表下表汇总了USART开发中最常见的“坑”及其解决办法问题现象可能原因排查步骤与解决方案完全收不到任何数据1. 硬件连接错误TX/RX接反、未共地2. 串口助手参数错误3. USART或时钟未使能4. 程序未运行复位电路、启动模式1. 用万用表通断档检查连线确保共地。2. 核对波特率、数据位等所有参数。3. 检查代码中HAL_UART_MspInit是否被调用__HAL_RCC_USART1_CLK_ENABLE()是否执行。4. 检查BOOT0/BOOT1引脚确保在用户闪存启动模式。点个LED灯测试程序是否运行。收到乱码1.波特率不匹配最常见2. 时钟源误差大使用了HSI3. 数据位、停止位、校验位设置不匹配1.重点检查双方波特率是否精确一致。用示波器测量一个位的时间计算实际波特率。2. 换用外部晶振HSE作为时钟源。3. 仔细核对串口助手和代码中的帧格式设置。只能发送不能接收或反之1. 发送或接收未使能TE/RE位2. 接收引脚配置错误应为浮空输入/上拉输入3. 中断未使能或中断服务函数未实现1. 检查USART_CR1寄存器或HAL初始化代码确认TE和RE位已置1。2. 检查RX引脚如PA10的GPIO模式应为GPIO_MODE_AF_INPUT或复用推挽但接收端内部需设置为浮空/上拉。3. 检查NVIC配置确认中断已开启并实现了对应的中断服务函数如USART1_IRQHandler。数据丢失偶尔丢字节1. 接收缓冲区溢出处理速度跟不上接收速度2. 中断优先级低被其他中断打断3. 发送函数阻塞时间过长1. 使用环形缓冲区在接收中断中快速将数据存入环缓冲区主循环再慢慢处理。2. 提高串口接收中断的NVIC优先级。3. 发送改用DMA或中断方式避免使用阻塞式的HAL_UART_Transmit长时间占用CPU。使用DMA时数据异常1. DMA缓冲区溢出或未对齐2. DMA传输未完成就被篡改数据3. 内存访问冲突Cache问题在带Cache的高端芯片中1. 确保DMA缓冲区大小足够地址对齐如4字节对齐。2. 在DMA传输完成回调函数中再处理数据或启动下一次传输避免数据竞争。3. 对于M7等内核注意配置DMA缓冲区的Cache策略如使用SCB_CleanDCache_by_Addr。空闲中断不触发1. 空闲中断未使能2. 空闲标志未清除3. 总线时钟或波特率异常导致空闲检测逻辑错误1. 确认调用了__HAL_UART_ENABLE_IT(huart, UART_IT_IDLE)。2.必须在中断服务函数中清除空闲标志调用__HAL_UART_CLEAR_IDLEFLAG(huart)。3. 检查系统时钟和波特率配置是否正确。5.3 稳定性与抗干扰经验谈在实际工业环境或长距离通信中稳定性至关重要。添加硬件滤波在RX/TX线上串联一个几十欧姆的电阻并并联一个几十皮法的小电容到地可以滤除部分毛刺。使用差分信号如果环境恶劣、距离远超过几米考虑使用RS485半双工或RS422全双工差分通信标准它们抗共模干扰能力强。STM32的USART支持通过控制DE引脚配合RS485收发器如MAX485工作。软件容错与协议设计帧校验除了可选的奇偶校验位在应用层数据包中一定要加入校验字段如CRC校验。简单的累加和Checksum也行但CRC更可靠。帧头帧尾设计数据包格式例如0xAA 0x55 [长度] [数据] [CRC16] 0x0D 0x0A。只有检测到正确的帧头帧尾和CRC才认为是一包有效数据。超时机制对于不定长接收除了空闲中断还应加入超时定时器。如果一段时间内没有收到新数据也判定为一帧结束防止因干扰导致空闲中断永不触发。数据重发在可靠通信协议中接收方收到数据后应回复确认ACK发送方在一定时间内未收到ACK则重发数据。最后关于性能优化的一点体会在资源紧张的项目中如果HAL库的中断处理显得臃肿可以考虑使用LL库Low-Layer甚至直接操作寄存器。LL库提供了更轻量级的封装代码效率更高但需要你对寄存器有更深的理解。对于简单的USART收发直接操作USART_DR、USART_SR寄存器配合精简的中断服务函数往往能获得最快的响应速度和最小的代码体积。这需要你在开发效率和运行效率之间做出权衡。