尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32F103双串口收发实战:从引脚配置到中断调试的完整指南

STM32F103双串口收发实战:从引脚配置到中断调试的完整指南 简介STM32F103双串口收发调试通过的完整工程面向嵌入式开发者和STM32入门学习者解决多串口同时收发、数据交互及代码结构混乱的问题。资源共143个文件以34个.h头文件和31个.c源文件为主包含编译生成的.o、.crf、.axf、.hex等中间产物及Keil工程配置压缩包约2.21MB已有2688人学习下载。工程采用结构体方式封装串口参数USART_Config结构体统一定义串口外设、波特率、引脚配合初始化、发送、接收等函数便于同时管理两个串口并快速复用代码中还包含stm32f10x_tim.c等定时器相关源码及OLED等外设工程片段可供参考GPIO配置、时钟开启、中断使能等底层细节。底层开发者可直接基于工程修改引脚与波特率用于设备控制、数据监控或调试场景不必从零配置寄存器适合希望快速实现双串口应用的工程师与学习者。1. 双串口到底用来干嘛别一上来就写代码先说清楚一个核心问题很多人调双串口是因为板子上有两个串口闲着也是闲着这种思路容易跑偏。咱先想明白双串口收发在STM32F103上最常见的应用场景到底是什么。我自己做过的项目里双串口的需求大概分三类。第一类是数据透传比如一个串口接GPRS/4G模组或者WiFi模块另一个串口接PC或者主控上位机两边数据互转。第二类是协议桥接比如一个串口以Modbus RTU协议接485总线上的传感器另一个串口接调试终端打印日志或者下发指令两个串口是主—从关系而非对等关系。第三类是通信冗余或者分通道处理一个串口走实时性要求高的短消息另一个串口走大数据量低优先级的日志流避免互相阻塞。搞清楚需求再选串口。STM32F103的USART1挂在APB2总线上时钟是72MHzUSART2和USART3挂在APB1总线上时钟是36MHz。USART1比USART2/3多一路硬件流控的支持其实F103的USART2也有RTS/CTS但实际用的少而且同一波特率下分频误差更小所以高波特率通信优先用USART1。485自动收发电路那套东西建议只挂在USART2或USART3上原因后面调试章节细说。还有一点是引脚映射。F103的USART1默认在PA9/PA10重映射在PB6/PB7USART2在PA2/PA3基本没有重映射选项USART3在PB10/PB11重映射到PD8/PD9。做板子之前务必查好引脚占用别画完板才发现PA9被你拿去接LED了。选串口的原则总结一下高波特率、大数据量、和PC通信频繁USART1接485总线、走Modbus等外部设备协议USART2或USART3两个串口都要中断收发时中断优先级要区分别让低优先级长时间打断高优先级2. 标准库V3.5下的串口初始化GPIO、时钟、优先级一个都不能漏STM32F103的标准外设库V3.5是很多老项目的底子新项目用CubeMX的也很多但我今天还是拿标准库讲因为寄存器级别的理解对排查问题更有帮助。初始化串口的顺序有讲究建议按开时钟→配GPIO→配USART→配中断→使能来不要在顺序上偷懒。2.1 GPIO复用配置TXD和RXD别搞反先看代码。USART1的PA9是TXPA10是RXUSART2的PA2是TXPA3是RX。注意F103的GPIO配置里TX要配成复用推挽输出RX要配成浮空输入或上拉输入。很多新手在这里把TX配成普通推挽输出导致数据发不出去或者波形不对。void UART1_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); // USART1_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); // USART1_RX PA10 浮空输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); }USART2的GPIO配置完全同理只是引脚换成PA2和PA3时钟注意USART2的时钟是APB1使能但GPIOA的时钟还是APB2。这句话看起来简单实际很多人栽在这GPIOA时钟忘了开或者只开了USART2的时钟没开GPIOA的时钟串口什么都发不出来。2.2 USART初始化结构体里的门道USART_InitStructure有五个成员逐个说USART_BaudRate波特率。9600、115200用的最多。115200时USART1和USART2的误差都极小放心用。USART_WordLength数据位长度常规8位数据位就选USART_WordLength_8b。如果用了奇偶校验要选9位因为校验位占一位。USART_StopBits停止位常规USART_StopBits_1。USART_Parity校验位。RS232点对点通信可以不校验485总线上的Modbus RTU是8位数据、无校验、1位停止位这个和协议有关别乱加。USART_Mode收发模式只要不是纯发送或者纯接收都写成USART_Mode_Rx | USART_Mode_Tx。具体初始化函数void UART1_Init(u32 baudrate) { USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); UART1_GPIO_Config(); USART_InitStructure.USART_BaudRate baudrate; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_Init(USART1, USART_InitStructure); USART_Cmd(USART1, ENABLE); }USART2的初始化结构体内容一模一样只是把USART1换成USART2时钟使能函数换成RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2)。USART2的APB1时钟是36MHz库函数会自动根据RCC_ClocksTypeDef里的PCLK1值去算分频所以在系统时钟配置正确的前提下波特率误差没区别。2.3 NVIC中断优先级双串口收发最容易翻车的环节F103有两个中断控制器分组通常用NVIC_PriorityGroup_2即2位抢占优先级、2位子优先级。两个串口的中断要分主次比如USART1用来和PC通信数据量大优先级高USART2接传感器数据量小优先级低。void UART1_NVIC_Config(void) { NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); NVIC_InitStructure.NVIC_IRQChannel USART2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); }提示NVIC_PriorityGroupConfig只需调用一次。如果你在USART1初始化里调用它USART2里又调用一次虽然值一样但这种写法容易在某些封装好的底层库里埋雷。3. 收发逻辑中断接收、标志位驱动发送最省心串口收发有两种常见方案一种是纯轮询查询SR寄存器标志位另一种是中断缓冲。纯轮询的问题很明显如果主循环里在做延时或者等待某个外设数据就丢了。所以我推荐接收用中断、发送用查询的组合接收靠USART_IT_RXNE中断实时把数据搬进缓冲区发送在需要时调用阻塞函数写数据寄存器。3.1 接收中断串口助手发来的每一帧都别丢中断服务函数里做两件事读SR判断中断来源再读DR拿数据。读DR这个动作本身会自动清除RXNE标志位所以代码里不用手动清。#define UART1_RX_BUF_SIZE 256 #define UART2_RX_BUF_SIZE 256 volatile u8 uart1_rx_buf[UART1_RX_BUF_SIZE]; volatile u16 uart1_rx_head 0; volatile u16 uart1_rx_tail 0; void USART1_IRQHandler(void) { u8 data; if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { data USART_ReceiveData(USART1); uart1_rx_buf[uart1_rx_head] data; uart1_rx_head (uart1_rx_head 1) % UART1_RX_BUF_SIZE; // 如果head追上了tail说明缓冲区满了。简单处理丢弃最旧的数据 if (uart1_rx_head uart1_rx_tail) { uart1_rx_tail (uart1_rx_tail 1) % UART1_RX_BUF_SIZE; } } }环形缓冲区的两个指针一个写一个读这种结构在嵌入式里极其常见串口、SPI、DMA缓冲全用得上。初始化时把head和tail都清零读的时候判断head ! tail就读buf[tail]然后tail加一取模。USART2的中断服务函数结构一样中断函数名是USART2_IRQHandler。3.2 发送函数阻塞发送为什么够用发送用阻塞模式原因有两个一是串口发送本身很快115200波特率下1个字节约87us发几十个字节也就几毫秒二是阻塞发送代码简单出错概率低。真正需要大流量连续发送的场景通常直接上DMA而不是优化中断发送。void UART1_SendByte(u8 data) { USART_SendData(USART1, data); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); } void UART1_SendString(u8 *str) { while (*str) { UART1_SendByte(*str); str; } }注意这里等待的是TXE发送数据寄存器空标志不是TC发送完成标志。很多教程写等待TC在高波特率连续发送时会多出无谓的等待时间。想严格保证一帧发完再去切RS485收发引脚DE/RE控制的时候才等TC。3.3 两个串口的数据互通一个最简单的透传框架把两个串口接起来做透传主循环只需要做一件事查USART1的环形缓冲区有没有数据有就扔给USART2发出去反过来也一样。这个框架是双串口项目的骨架后面不管加协议、加解析、加逻辑都是在两个方向上插过滤函数。int main(void) { // 时钟、GPIO、USART、NVIC初始化 UART1_Init(115200); UART2_Init(9600); while (1) { while (uart1_rx_head ! uart1_rx_tail) { u8 data uart1_rx_buf[uart1_rx_tail]; uart1_rx_tail (uart1_rx_tail 1) % UART1_RX_BUF_SIZE; UART2_SendByte(data); } while (uart2_rx_head ! uart2_rx_tail) { u8 data uart2_rx_buf[uart2_rx_tail]; uart2_rx_tail (uart2_rx_tail 1) % UART2_RX_BUF_SIZE; UART1_SendByte(data); } } }这个框架跑起来之后串口助手A发的数据能由USART1进来、USART2出去如果你在两个串口之间加了个USB转TTL模块另一端再用一个串口助手收就能完整验证双向链路。4. 调试环节串口助手收不到数据时按这条链路去查标题写了调试通过但真正的调试过程往往是一路踩坑过来的。我在实测中大概遇到四类问题按出现频率排序分别是收不到数据、收到乱码、收发一帧丢一帧、双串口互相干扰。逐个排查思路如下。4.1 收不到数据先分硬件问题还是软件问题最有效的第一招是回环测试把该串口的TXD和RXD直接短接如果是通过USB转TTL模块连PC就在模块的TXD/RXD端短接然后用串口调试助手发一个字符。如果助手能收到自己发出去的内容说明串口驱动和助手配置没问题问题出在STM32这边如果收不到那就是USB转TTL模块、驱动或者接线的问题跟单片机无关。这一步能直接砍掉一半排查空间。排除了电脑端和USB转TTL后用示波器或者逻辑分析仪量STM32的TXD引脚。正常情况下发送数据时TXD引脚应该有明显的高低电平跳变。没有波形从这几个方向查GPIO时钟和USART时钟有没有都使能TX引脚模式是不是GPIO_Mode_AF_PP不是就改成它USART_Cmd有没有调用调用后有没有延时几个USART时钟周期再去发数据检查外部晶振是否起振如果用的HSI内部时钟但代码是按外部8M晶振配置的波特率会完全不对4.2 收到乱码波特率误差和电平不匹配是两大主凶乱码最常见的原因是波特率不对。F103的波特率寄存器USART_BRR的值由PCLK和波特率计算而来PCLK错了或者外部晶振不是8M导致系统时钟配置偏差都会乱码。用串口调试助手不断试9600、19200、115200、460800如果某个固定波特率下字符稳定但内容错那基本是晶振频率配置问题不是波特率设置问题。第二个原因是逻辑电平不匹配。STM32的TXD输出是3.3V TTL电平如果直接接了一个5V TTL电平的设备按5V识别的接收端可能把3.3V高电平判成有效也可能恰好能工作但通信不稳定。更常见的是USB转TTL模块本身有3.3V和5V跳线帽跳线帽位置错了就会收到乱码或者收不到。CH340模块上的跳线帽要拨到3.3V一侧再接F103。第三个是共地问题。两个设备之间只接了TXD和RXD没接GND。串口通信的电平参考是各自的地不共地时参考点漂移乱码、丢字节都是轻的严重时还可能烧IO。USB转TTL模块和STM32板子共地是调试串口的第一条铁律。4.3 双串口互相干扰中断优先级和缓冲区溢出排查两个串口同时收发时一种典型故障是只跑USART1收发一切正常只跑USART2也正常两个一起跑就出现USART1丢帧或者卡死。这种基本都是中断优先级的问题。USART2的中断如果抢占优先级和USART1相同或者更高且USART2中断服务函数里耗时较长比如在中断里做数据处理就会把USART1的RXNE中断堵住导致溢出错误。处理办法一是明确主从关系把数据量大的串口设为更高抢占优先级二是在中断服务函数里只做搬数据这一个动作解析和处理全部放到主循环三是如果溢出标志已经置位ORE读SR再读DR可以清除或者干脆在检测到ORE时重置接收缓冲区的读写指针。我用一张表总结双串口调试中常见的现象和排查方向方便你对照现象可能原因排查/解决方向完全收不到数据接线错误、模块损坏、GPIO配置错回环测试、检查TXD模式、示波器量波形收到乱码波特率误差、电平不匹配、未共地检查晶振频率配置、调整电平跳线、共地发一帧丢一帧发送时等待TC而非TXE、缓冲区溢出改等TXE标志、加大缓冲区或改DMA两个串口同时跑就丢数NVIC优先级配置不当、中断处理耗时过长区分抢占优先级、中断里只搬数据偶尔死机中断里操作耗时过长、共用全局变量竞态数据加volatile、用临界区保护读写指针4.4 485自动收发电路的一个提醒热词里出现了485自动收发电路如果你在双串口项目里接了485这里多说一句。485自动收发电路利用TX信号通过光耦或三极管自动切换DE/RE确实省一个GPIO但实测中在波特率高于57600时自动切换电路的翻转时间可能出现问题导致第一个字节或者最后一个字节异常。建议驱动485时用一个独立GPIO控制DE/RE发送前拉高DE等待TC标志再拉低这样最稳。F103的USART空闲时TXD为高电平某些自动收发电路会因为这个电平组合在空闲状态误触发发送也是要注意的点。5. 一个完整示例的实测记录最后分享一个我最近调通的方案F103C8T6最小系统板USART1接USB转TTL模块连PC波特率115200USART2接一个RS485收发器接Modbus传感器波特率9600。整个工程基于标准库V3.5MDK5编译下载到板子后用两个串口助手同时验证。我按下面的步骤做的联调你可以照抄先用USB转TTL模块只接USART1串口助手发AT字符串观察回显是否正常。这一步过了USART1收发就通了。USART2先不接485直接在板子上把PA2和PA3短接回环用另一个USB转TTL模块接到PC调试助手发一个字节确认USART2中断接收和阻塞发送都正常。回环通过再去接外部设备这样能快速定位问题是在MCU内部还是MCU到外部设备的链路上。去掉PA2和PA3的短接接上485收发器发送端用Modbus Poll工具发查询帧F103在USART2中断里收到后再通过USART1打印到串口助手验证数据内容。如果收到的帧前后多了字节或者少了字节优先查485收发器的DE/RE控制时序和收发切换延时。两个串口同时打数据用串口助手1向USART1连续发送1000字节预期USART2接了485回环到另一个USB转TTL能完整收到这1000字节。这个测试能暴露缓冲区溢出问题。这套流程跑下来大部分双串口问题都能定位。我也不太建议一口吃成胖子把两个串口同时调通再去做协议解析比在协议解析里排查串口底层问题要高效得多。6. 关于数据和调试工具的几个实操心得测试双串口时用串口调试助手的定时发送功能配合加时间戳功能能非常直观地判断丢帧和延迟。数据量测试时别只发几个字节一次性发几百上千字节才能暴露缓冲区问题。另外串口收到十六进制数据时记得勾选HEX显示不然你看到的都是乱码字符很难判断帧格式对不对。调试时有一个我反复用的技巧在每个串口的发送函数和接收中断里加一个短促的GPIO翻转用来显示活动状态外接逻辑分析仪能看到哪个串口在哪个时刻收发定位两个串口互相抢占的问题特别好使。比如USART1发送前拉高PB0发送完拉低USART2发送前拉高PB1逻辑分析仪看PB0/PB1波形重叠情况一眼就知道时序有没有冲突。这个方法在改完代码后验证改动是否正确也很管用比翻调试助手的日志更直接。我自己调完这套双串口后最大的体会是STM32F103的串口外设本身很皮实大多数问题出在配置顺序、时钟源和外部硬件连接上。回环测试加串口助手加示波器这三件套能解决九成以上的串口收发不通过问题。如果你正卡在双串口收发上先把两个串口分别跑通再合到一起联调这个顺序能省掉大量排查时间。本文还有配套的精品资源点击获取
返回列表