☰
STM32串口通信入门:从单板发送到双板互发数据
2026/10/26 4:36:14 网站建设 项目流程

简介:这份资源面向正在学习STM32单片机、需要打通双板串口通信的嵌入式初学者与进阶开发者,基于正点原子mini板与精英板搭建了一套可实际运行的USART通信方案:mini板经USART3(PB10、PB11)发送数据,精英板经USART3接收,并通过USART1(CH340下载口)打印到串口调试助手,同时支持LCD屏实时显示接收内容,接收成功时LED10翻转、发送成功时LED1翻转,便于直观验证通信状态。资源包共321个文件、约7.35MB,以69个h头文件、66个c源文件为核心,配合Keil工程文件(uvprojx、uvoptx)、编译产物(o、axf、hex、crf)及少量说明文档,工程结构完整、代码调试可用。目前已有4418人学习下载。读者可据此掌握双板串口收发配置、中断与轮询处理、串口打印调试及LCD显示联动等关键环节,并借助LED翻转快速定位收发是否成功,对缺少mini板或LCD屏的情况也预留了替代与裁剪思路,适合作为串口通信实验的参考工程。

1. 两块板子怎么“说话”:STM32 串口通信到底是怎么回事

刚摸到单片机的新手,最容易卡在一个特别具体的场景里:手里有两块 STM32 开发板,想让它们互相传个数据,结果线接好了、程序也烧进去了,串口助手却一个字都不显示。这时候网上一搜“stm32 串口通信”,出来的全是寄存器、波特率、中断、DMA,越看越觉得自己不适合干这行。

其实串口通信没那么玄乎。你可以把它想成两个人隔着一条走廊喊话:双方得先说好语速(波特率),得约定一句话说几个字(数据位),还得知道什么时候算一句话说完(停止位)。STM32 的串口外设,干的就是把你要发的字节,按这套约定一位一位推出去,再把对方推过来的位拼回字节。它解决的是“两块芯片之间怎么低成本、少接线地交换数据”这个问题,适合所有刚接触嵌入式、要做板间通信、要接传感器或调试输出的新手。

2. 为什么新手第一课就该学串口:它是最便宜的“对话通道”

2.1 串口是嵌入式的“普通话”,不会它寸步难行

学 STM32 的人常有个误区,觉得串口太基础,不如直接上 SPI、I2C、CAN 显得高级。但真实情况是,串口几乎是所有调试手段的底座。你程序跑飞了,第一反应是打印一行日志出来看看,这行日志走的就是串口;你买了个温湿度模块,说明书上写的通信方式大概率还是串口;你要把板子上的数据传到电脑上存起来,最省事的还是串口。

我第一次学的时候,导师只说了一句话:先把串口调通,你才有资格说自己在“调试”,否则你只是在“猜”。因为串口是唯一一个你能亲眼看到字节流动的通道,发送什么、收到什么,串口助手上一目了然。其他总线要么速度太快抓不到,要么需要逻辑分析仪,对零基础的人来说门槛高得多。所以入门路径上,串口不是可选项,是必经的第一站。

从资源角度看,串口通信的硬件成本也最低。两根线(TX 接对方 RX,RX 接对方 TX),再共地,就能通信。不需要额外的时钟线,不需要上拉电阻,不需要复杂的地址分配。对新手来说,接线越少,翻车点越少,你才能把注意力放在“数据怎么发、怎么收”这件事本身,而不是先跟硬件打架。

2.2 先搞懂三个参数,比背寄存器有用得多

新手看串口代码,最容易被一堆初始化结构体吓住。其实你只需要先记住三个参数,剩下的都是围绕它们转的。

第一个是波特率,单位是 bps,意思是每秒传多少位。常见的有 9600、115200。它就像两个人说话的语速,你说太快对方听不清,说太慢又浪费时间。双方必须设成一样的值,差一点都会变成乱码。第二个是数据位,通常是 8 位,也就是一个字节。第三个是停止位,通常是 1 位,用来告诉对方“这个字节说完了”。

还有一个校验位,新手阶段可以先设成无校验。这四个东西合起来叫“帧格式”,常见写法是 115200-8-N-1,翻译过来就是:每秒 115200 位、8 个数据位、无校验、1 个停止位。你在串口助手里看到的那些下拉框,选的就是这套东西。只要两边帧格式一致,通信就成功了一半。

提示:新手最常犯的错不是代码写错,而是串口助手那边波特率选错了。程序里写 115200,助手里选 9600,屏幕上就是一堆问号和方块,别急着怀疑人生。

2.3 第一个能跑的例子:把一句话从板子发到电脑

下面这段是轮询方式发送字符串的最小例子,用的是 STM32 标准外设库的写法思路,重点看每一步在干什么,不用纠结具体型号的寄存器名。

// 串口初始化:以 USART1 为例,PA9 为 TX,PA10 为 RX void UART1_Init(void) { // 1. 开时钟:串口和对应 GPIO 端口的时钟都要开 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); // 2. 配置 TX 引脚为复用推挽输出,数据才能从芯片推出去 GPIO_InitTypeDef gpio; gpio.GPIO_Pin = GPIO_Pin_9; gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); // 3. 配置 RX 引脚为浮空输入,用来接收对方发来的电平 gpio.GPIO_Pin = GPIO_Pin_10; gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &gpio); // 4. 串口核心参数:115200-8-N-1,收发模式都打开 USART_InitTypeDef uart; uart.USART_BaudRate = 115200; uart.USART_WordLength = USART_WordLength_8b; uart.USART_StopBits = USART_StopBits_1; uart.USART_Parity = USART_Parity_No; uart.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; uart.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_Init(USART1, &uart); // 5. 最后使能串口,前面配的参数才真正生效 USART_Cmd(USART1, ENABLE); } // 发送一个字节:等发送寄存器空,再写数据 void UART1_SendByte(uint8_t data) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); USART_SendData(USART1, data); } // 发送字符串:逐个字节发出去 void UART1_SendString(char *str) { while (*str != '\0') { UART1_SendByte((uint8_t)*str); str++; } }

这段代码的逻辑很直白:先给串口和引脚“通电”(开时钟),再告诉引脚该干什么活(TX 输出、RX 输入),然后约定好说话规则(115200-8-N-1),最后打开开关。发送部分是一个字节一个字节地等、一个字节一个字节地写,TXE这个标志位的意思是“发送寄存器空了,可以塞下一个了”,不等到它置位就写,数据会丢。

参数上你只需要改两处就能适配自己的板子:一是波特率,电脑端串口助手要跟它保持一致;二是引脚号,不同型号的 STM32 串口对应的引脚不一样,查一下自己板子的原理图,别照着抄。烧进去之后,打开串口助手,选对 COM 口和 115200,就能看到板子发来的字符串了。第一次看到屏幕上跳出自己写的那行字,那种感觉比点亮 LED 还实在。

3. 两块板子互发数据:从“能发”到“能收”

3.1 接线别接反:TX 对 RX 是血泪经验

单板发数据到电脑,你只需要接 TX、RX 和 GND 三根线到 USB 转串口模块。但一旦变成两块 STM32 互连,接线就成了第一个翻车点。记住一句话:一块板子的 TX 接另一块板子的 RX,RX 接对方的 TX,GND 必须共地。

为什么不能 TX 接 TX?因为 TX 是“说”的引脚,两个都在说,没人听,数据自然对不上。GND 不共地更隐蔽,两块板子各自的地电位不一样,电平判断就会出错,表现是时好时坏、偶尔收到乱码,这种玄学问题最折磨人。我第一次做双板通信,就是忘了共地,调了一下午,最后拿万用表一量才发现两块板子的地根本没连上。

接线顺序建议是:先断电接线,接完检查一遍再上电。TX 和 RX 交叉、GND 相连,这两条确认了,硬件层面就八九不离十。剩下的问题基本都在软件配置上,排查范围一下就缩小了。

3.2 接收端怎么写:轮询收一个字节

发送会了,接收其实是对称的。下面这段是轮询方式接收一个字节,配合上面的初始化直接用。

// 接收一个字节,返回 1 表示收到,0 表示超时没收到 uint8_t UART1_ReceiveByte(uint8_t *data, uint32_t timeout) { // 等接收寄存器非空,说明有数据到了 while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET) { if (timeout-- == 0) { return 0; // 等太久,认为对方没发,直接返回 } } // 把收到的字节读出来,读操作会自动清标志位 *data = USART_ReceiveData(USART1); return 1; }

RXNE这个标志位的意思是“接收寄存器里有新数据了”。轮询就是死等它置位,等到了就把数据读走。timeout参数是给死等加个保险,不然对方一直不发,程序就卡死在这里,别的活都干不了。这个写法适合入门理解流程,但真实项目里一般不会这么干,因为主循环会被拖住。

两块板子联调时,建议先让 A 板只发、B 板只收,B 板收到后通过它自己的串口转发到电脑上打印出来。这样你能同时看到“发了什么”和“收到了什么”,对不上就说明中间某一环有问题。等单向通了,再改成双向互发,难度会低很多。

3.3 中断方式:别让等数据把主程序卡死

轮询的毛病很明显:等数据的时候,CPU 什么也干不了。中断就是来解决这个的——数据到了,硬件自动打断主程序,跳去执行接收函数,处理完再回来。对新手来说,中断的概念比轮询绕一点,但用起来其实更省心。

// 串口1中断服务函数,名字要和启动文件里的一致 void USART1_IRQHandler(void) { // 判断是不是“收到数据”触发的中断 if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { uint8_t data = USART_ReceiveData(USART1); // 读走数据 // 这里可以做简单处理,比如回显或者存进缓冲区 UART1_SendByte(data); // 收到什么原样发回去,方便验证 USART_ClearITPendingBit(USART1, USART_IT_RXNE); // 清中断标志 } }

用中断之前,初始化里要多做两步:一是打开串口的接收中断USART_ITConfig(USART1, USART_IT_RXNE, ENABLE),二是在 NVIC 里给串口中断分配优先级并使能。中断函数里不要写耗时操作,读走数据、做个标记就够了,复杂的处理丢回主循环去做。这是新手容易忽略的规矩,中断里拖太久,整个系统的实时性都会受影响。

4. 新手最容易踩的五个坑:现象、原因、解决

4.1 串口助手一片乱码

现象:板子确实在发,但电脑上显示的是问号、方块或者一堆看不懂的符号。原因九成是波特率不一致,程序里设的 115200,助手里选的 9600,或者反过来。解决:先核对两边波特率,再核对数据位、停止位、校验位是否都是 8-N-1。如果参数全对还是乱码,检查一下晶振配置,时钟不对会导致实际波特率偏移。

4.2 一个字节都收不到

现象:发送端程序在跑,接收端毫无反应。原因通常是接线问题:TX/RX 没交叉、GND 没共地,或者引脚选错了。解决:拿万用表量一下 TX 引脚在发送时有没有电平变化,没有就说明引脚配置错了;有变化但对方收不到,重点查交叉和共地。还有一种情况是接收端没使能串口,USART_Cmd忘了调用。

4.3 收到第一个字节后就卡死

现象:能收到数据,但只收到一个就再也不动了。原因多半是中断标志没清,硬件一直认为“还有中断没处理”,反复进中断或者干脆卡住。解决:确认中断服务函数里读了数据寄存器,并且清了对应的中断标志位。读USART_ReceiveData本身会清RXNE,但如果你用的是别的标志,就得手动清。

4.4 数据偶尔丢一两个字节

现象:大部分数据对,但隔一段时间少一个字节。原因常见于轮询方式下主循环太忙,数据到了没及时读走,被下一个字节覆盖了。解决:要么改用中断接收,要么提高读取频率,要么加一个接收缓冲区先把数据存起来。串口速度不快的时候问题不明显,一旦波特率上到 115200 以上,轮询丢字节的概率会明显上升。

4.5 两块板子单独都正常,连起来就不行

现象:各自跟电脑通信都没问题,一互连就出问题。原因往往是电平或共地问题,也可能是两边帧格式没统一。解决:先确认两块板子的 GND 确实连在一起,再确认两边波特率、数据位完全一致。如果还不行,把其中一块的 TX 断开,只做单向通信,排除双向同时发导致的冲突。

5. 下一步学什么:从会发字节到会传数据

串口调通之后,你其实已经站在嵌入式通信的门口了。接下来最该补的不是更多外设,而是“怎么把一串字节变成有意义的数据”。因为真实场景里,你发的不会是一个字符,而是一帧一帧的数据包,里面可能有帧头、长度、内容、校验。这就引出了两个方向:一是协议设计,二是接收缓冲。

协议设计入门可以先从最简单的做起:约定一个帧头(比如 0xAA),后面跟数据长度,再跟数据本身,最后加一个校验字节。接收端先找帧头,再按长度收,最后校验,对上了才算一帧有效数据。这套思路不复杂,但能让你从“收到什么打印什么”升级到“收到一帧完整数据再处理”。我第一次写这种解析的时候,没加帧头,结果数据里恰好出现和长度一样的字节,整个包就错位了,从那以后我每次设计帧格式都强制先定帧头和校验,这个习惯救了我很多次。

接收缓冲则建议了解一下环形缓冲区。中断里只管往缓冲区塞数据,主循环慢慢从里面取,两边互不干扰。这是串口接收的标准做法,代码不长,但能彻底解决丢字节的问题。学完这两个,你再去看 DMA 串口收发,会发现它只是把“搬运数据”这件事交给了硬件,思路是一脉相承的。

学习路径上,我的建议是:先把轮询收发跑通,再换中断,然后加环形缓冲,最后碰 DMA。每一步都只改一个变量,出了问题才知道是哪一步引入的。别一上来就抄一份带 DMA 和协议解析的完整工程,那样跑通了也不知道为什么通,跑不通更不知道从哪查。

如果你手里还没有一份能直接对照的串口通信代码和接线说明,这份资源就是按“先单板发、再双板收、最后加中断”的顺序组织的,拿来跟着敲一遍,比只看文章快得多。入门阶段最怕的不是学得慢,是一直不敢动手。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询