STM32双机串口通信实战:从硬件连接到DMA空闲中断
简介这份资源面向正在学习STM32单片机、需要打通双板串口通信的嵌入式初学者与进阶开发者围绕mini板与精英板之间的USART3互连展开解决串口收发、数据打印与屏幕显示联动的实际问题。压缩包共321个文件约7.35MB以69个h头文件、66个c源文件为核心配合Keil工程文件、编译中间产物与hex烧录文件完整保留了可直接编译调试的工程结构。资源中已实现mini板USART3PB10、PB11发送、精英板USART3接收并通过USART1经CH340打印到串口调试助手同时支持LCD显示接收内容接收成功时LED10翻转、发送成功时LED1翻转便于直观判断通信状态。作者还给出无mini板时用串口助手替代、无LCD时注释相关初始化代码的适配思路降低了硬件门槛。目前已有4418人学习下载适合对照工程理解串口配置、中断收发与调试排错流程。1. 两块板子聊不上天多半不是代码写错了手里有两块 STM32 开发板想把其中一块采集到的传感器数据发给另一块或者让一块当主控、一块当执行器最省事的路子就是串口。可真动手时很多人卡在第一步线接好了程序也烧进去了串口助手却一个字节都收不到。STM32 串口通信这件事难点从来不在“会不会调库”而在引脚、时钟、波特率、收发时序这几处细节有没有对齐。这份资源围绕 STM32 之间的串口通信展开覆盖从硬件连线、USART 初始化、中断收发到双机联调的完整链路适合刚接触 STM32 外设、又急着把两块板子连起来跑通的新手也适合想回头把中断和 DMA 收发边界理清楚的熟手。下面按“先能通、再通得稳、最后知道哪里会翻车”的顺序拆。2. 先把物理层和时钟理顺串口不通先查这三处串口通信在 STM32 上是个典型的“软硬交界”外设软件配置再漂亮物理层和时钟没对上照样是黑匣子。我一般把排查顺序固定成三步引脚复用对不对、时钟开没开、电平共没共地。这三步过了再去动代码。2.1 引脚复用与交叉连线STM32 的 USART 引脚不是焊死在某个脚上的而是通过 GPIO 复用功能映射过去的。以常见的 USART1 为例PA9 是 TX、PA10 是 RX但换到 USART2 就可能是 PA2/PA3。两块板子互联时A 板的 TX 必须接 B 板的 RXA 板的 RX 接 B 板的 TX这是最基础也最容易接反的一步。接反的现象很典型发送端一切正常接收端永远进不了中断。配置引脚时要同时设置复用模式和电气属性。下面这段是 USART1 的 GPIO 初始化注意 TX 配成复用推挽、RX 配成浮空或上拉输入/* 使能 GPIOA 与 USART1 时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); GPIO_InitTypeDef gpio; /* TX: PA9 复用推挽输出 */ gpio.GPIO_Pin GPIO_Pin_9; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); /* RX: PA10 上拉输入避免悬空误触发 */ gpio.GPIO_Pin GPIO_Pin_10; gpio.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, gpio);逻辑说明TX 是芯片往外推信号必须走复用推挽普通推挽输出是驱动不了 USART 外设的RX 是接收外部信号配成上拉输入可以在对端未上电时保持一个确定电平减少乱码。参数上GPIO_Speed对串口这种低速外设选 50MHz 足够不必追高。2.2 时钟树与波特率误差USART 的波特率是从 APB 总线时钟分频出来的所以先确认 USART 挂在哪个总线上。USART1 在 APB2USART2/3 在 APB1两条总线频率常常不同。如果初始化时只开了外设时钟却算错了总线频率波特率就会偏表现为能收到数据但全是乱码。标准库的USART_Init会根据你填的USART_BaudRate自动算分频但前提是RCC_GetClocksFreq拿到的频率和实际一致。常见做法是初始化后回读一下USART_GetFlagStatus配合示波器或逻辑分析仪量一个字节的时间9600 波特率下一位约 104 微秒一个 8N1 帧约 1.04 毫秒量出来偏差超过 3% 就要回头查时钟。参数常用取值说明波特率9600 / 115200双机联调先用 9600稳了再提速数据位8与多数传感器模块一致停止位18N1 是默认组合校验位无需要校验时两端必须一致流控无双机短距离直连不需要两端参数必须完全一致任何一项不同都会导致整帧解析错位。这一点在资源里的双机示例中反复强调因为它是“能发不能收”的头号原因。2.3 共地与电平匹配两块板子各自供电时如果只连了 TX/RX 两根线而没共地参考电平不统一接收端判高低电平就会飘。血泪经验是只要两块板子不是同一路电源GND 必须连上。另外要确认双方都是 3.3V 逻辑若一端是 5V 器件直接对接可能损伤 RX 引脚中间要加电平转换。资源里的连线图把 GND 单独标了出来就是怕人漏。3. 初始化与中断收发把“能通”落到代码物理层理顺后进入软件配置。串口通信的代码骨架其实很固定时钟、GPIO、USART 参数、中断优先级、使能。真正拉开差距的是收发方式的选择——轮询简单但占 CPU中断灵活但要处理边界DMA 适合大批量但要管好缓冲区。3.1 USART 参数初始化接着 2.1 的 GPIO 配置往下写 USART 本体USART_InitTypeDef usart; usart.USART_BaudRate 9600; usart.USART_WordLength USART_WordLength_8b; usart.USART_StopBits USART_StopBits_1; usart.USART_Parity USART_Parity_No; usart.USART_HardwareFlowControl USART_HardwareFlowControl_None; usart.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, usart); /* 使能接收中断数据一到就进 ISR */ USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_Cmd(USART1, ENABLE);逻辑说明USART_Mode同时打开收发只做单向传输时可以只开一个方向省资源。USART_IT_RXNE是接收寄存器非空中断每收到一个字节触发一次这是中断收发的核心开关。参数上USART_WordLength_8b对应 8 数据位和 2.2 表格里的 8N1 对齐。3.2 中断服务函数与接收缓冲中断里最忌讳做耗时操作。正确姿势是只把数据搬进缓冲区、置个标志解析放到主循环。下面是一个环形缓冲的简化写法#define RX_BUF_SIZE 64 static volatile uint8_t rx_buf[RX_BUF_SIZE]; static volatile uint16_t rx_head 0, rx_tail 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART1); /* 读数据自动清标志 */ uint16_t next (rx_head 1) % RX_BUF_SIZE; if (next ! rx_tail) { /* 留一个空位判满 */ rx_buf[rx_head] data; rx_head next; } /* 缓冲区满则丢弃避免覆盖未处理数据 */ } }逻辑说明USART_ReceiveData读一次就会清掉 RXNE 标志不需要手动清。环形缓冲用 head/tail 两个指针留一个空位区分“满”和“空”这是嵌入式里最稳的写法。参数RX_BUF_SIZE按最大帧长乘 2 取64 字节对多数传感器帧够用帧长的场景要相应放大。主循环里这样取数据while (rx_tail ! rx_head) { uint8_t b rx_buf[rx_tail]; rx_tail (rx_tail 1) % RX_BUF_SIZE; /* 在这里做协议解析或转发 */ }3.3 双机联调的收发验证两块板子都烧好程序后先做单向验证A 板定时发固定字符串B 板收到后原样回发。资源里给的验证帧是0xAA 0x55加长度加数据的简单格式好处是能一眼看出帧头对不对。验证顺序建议是先单字节、再多字节、最后连续流。单字节通了说明引脚和波特率没问题多字节通了说明缓冲没溢出连续流通了说明中断处理跟得上。任何一步卡住回到第 2 章的三处排查。4. 避坑与排查五个让双机通信翻车的现场串口这东西跑通一次不代表稳定下面几条是我和身边人真实踩过的按“现象 → 原因 → 解决”记下来遇到时能少走弯路。4.1 能发不能收接收中断从不触发现象发送端逻辑分析仪能看到波形接收端主循环里缓冲区永远是空的。原因多半是 RX 引脚没配成输入模式或者 NVIC 里没使能 USART 中断。解决检查GPIO_Mode_IPU是否设置再确认NVIC_EnableIRQ(USART1_IRQn)有没有调用中断优先级分组是否配置过。4.2 收到数据但全是乱码现象字节数对得上内容却是随机值。原因通常是两端波特率不一致或者一端时钟源配置和实际晶振不符。解决两端统一 9600 先验证用逻辑分析仪量位宽反推实际波特率再回头核对SystemInit里的时钟配置。4.3 连续发送时丢字节现象单帧正常连续发几十帧后中间少几个字节。原因是中断里处理太慢RXNE 被新数据覆盖或者环形缓冲满了直接丢弃。解决中断里只搬数据不做解析缓冲开大必要时改用 DMA 接收。资源里对 DMA 模式有单独示例适合高帧率场景。4.4 上电瞬间收到一个错误字节现象每次复位后第一个字节总是 0x00 或 0xFF。原因是引脚上电瞬间电平未稳定或对端先上电导致起始位误判。解决协议里加帧头校验丢弃帧头前的无效字节硬件上给 RX 加上拉软件上初始化后先清一次接收寄存器。4.5 两块板子各自单独测试都正常连起来就不行现象A 板接串口助手正常B 板接串口助手也正常A 接 B 就失败。原因基本是 TX/RX 没交叉或者 GND 没共。解决按 2.1 的交叉规则重接万用表量一下两地之间的通断确认共地。提示排查时优先用逻辑分析仪看波形比反复改代码快得多。没有分析仪就用串口助手分别验证单板把问题范围缩到最小。5. 进阶用 DMA 加空闲中断收不定长帧轮询和逐字节中断在低速场景够用但一旦帧率上来或者帧长不固定逐字节中断的 CPU 开销就很可观。我后来固定用的一套组合是“DMA 接收 串口空闲中断”它能在不占 CPU 的情况下收完整帧特别适合两块板子之间传变长数据包。思路是DMA 把 USART 数据寄存器搬到内存缓冲区串口总线空闲时触发 IDLE 中断此时读出 DMA 已传输的字节数就知道这一帧有多长。核心配置如下#define DMA_RX_SIZE 128 static uint8_t dma_rx_buf[DMA_RX_SIZE]; /* DMA1 通道5 对应 USART1_RX按具体型号查手册 */ DMA_InitTypeDef dma; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); dma.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; dma.DMA_MemoryBaseAddr (uint32_t)dma_rx_buf; dma.DMA_DIR DMA_DIR_PeripheralSRC; dma.DMA_BufferSize DMA_RX_SIZE; dma.DMA_PeripheralInc DMA_PeripheralInc_Disable; dma.DMA_MemoryInc DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; dma.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; dma.DMA_Mode DMA_Mode_Circular; /* 循环模式免重装 */ DMA_Init(DMA1_Channel5, dma); DMA_Cmd(DMA1_Channel5, ENABLE); USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE); USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); /* 开空闲中断 */逻辑说明DMA_Mode_Circular让缓冲区收满自动回绕省去每帧重装USART_IT_IDLE是空闲中断一帧结束后总线静默触发。中断里这样取长度void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_IDLE) ! RESET) { /* 清 IDLE 标志先读 SR 再读 DR */ volatile uint32_t tmp USART1-SR; tmp USART1-DR; (void)tmp; uint16_t recv_len DMA_RX_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5); /* recv_len 即本帧字节数交给解析层 */ /* 处理完后重置 DMA 计数准备下一帧 */ DMA_Cmd(DMA1_Channel5, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel5, DMA_RX_SIZE); DMA_Cmd(DMA1_Channel5, ENABLE); } }参数上DMA_RX_SIZE要大于最大帧长循环模式下超出会覆盖旧数据所以协议层最好带长度字段做二次校验。清 IDLE 标志必须“先读 SR 再读 DR”这是手册里的固定动作漏了会导致中断反复触发。这套组合的边界是帧与帧之间必须有空闲间隔如果对端连续不断发、中间不留缝IDLE 就不会触发这时要退回定长 DMA 或加超时判断。从那以后我每次做双机串口联调都强制先跑一遍“单字节—多字节—连续流”三段验证再上 DMA省得在协议层和底层之间来回怀疑。希望帮到你。本文还有配套的精品资源点击获取