ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32F103 RS485通信实战:自动收发电路与协议组网调试

2026/9/16 11:46:57 拓冰建站 浏览量
STM32F103 RS485通信实战:自动收发电路与协议组网调试 简介面向嵌入式开发初学者与工程师的STM32F103 RS485串口通信示例工程解决在STM32F103上利用UART外设实现工业级RS485半双工通信的核心问题涵盖MAX485/SP3485转换芯片接线、DE/RE方向引脚控制、波特率与数据位配置、发送接收状态切换等关键知识点。压缩包共213个文件约3.27MB以C/C源码、启动文件、Keil工程配置及编译中间文件为主另附hex烧录文件和说明文档结构清晰便于对照学习。目前已有1011人学习下载是入门STM32串口通信与工业总线应用的高频参考资料。示例代码覆盖UART初始化、半双工收发方向切换并给出CRC校验、握手应答及多节点冲突避免等可靠性实现方法学习者可对照源码从原理到工程完整跑通RS485通信流程快速迁移到传感器网络、分布式数据采集等项目。1. RS485协议与STM32(F103)串口通信项目的真实起点RS485不是一串字符协议而是一对A/B差分导线上的电平规矩。STM32F103片内只有UART外设引脚出来的是3.3V单端TTL电平既推不动长线也没有抗共模干扰的能力所以任何一个“STM32(F103)RS485串口通信”项目真正的工程量都压在收发器芯片、方向控制和帧格式这三件事上。不少开发者在第一块板子上拿示波器看TXD引脚觉得波形完美接到总线上却一帧都收不到问题通常不在波特率而是收发器的DE/RE方向脚根本没被正确驱动。这篇会按物理层、控制时序、组网帧和验证手段四层展开最后给出一套可以直接改用的工程配置。2. RS485硬件电路收发器选型、自动收发电路与终端电阻计算2.1 F103的UART只输出TTLRS485总线靠A/B差分电平驱动STM32F103的USART外设负责把数据并转串输出的是对地3.3V逻辑电平空闲时TXD为高。RS485总线上的信号则定义在A、B两根线之间的电压差上A比B高200mV以上表示逻辑1B比A高200mV以上表示逻辑0。差分传输的好处是抗共模干扰、传输距离可达上千米但代价是F103的TXD/RXD根本接不进总线必须经过一个RS485收发器完成单端TTL与差分信号之间的变换。收发器选型直接决定项目能否稳定工作。常见的几款芯片对比如下型号供电电压逻辑电平典型场景MAX4855V与5V TTL匹配老系统升级5V侧接收端注意电平转换SP34853.3V与STM32F103直连与F103共用3.3V电源最常用ISL31703.3V3.3V CMOS高速场合支持20Mbps以上国产兼容型号3.3V或5V按手册确认封装替代时需要核对逻辑阈值给F103项目选芯片时我一般先看供电如果板上只有3.3V电源优先选SP3485或同类3.3V收发器省掉一组5V电源和电平匹配电路。MAX485在5V供电时RO输出高电平接近5V直接接到F103引脚前要确认该引脚是否5V容忍F103的普通IO不是所有引脚都能承受5V选型时容易在这里埋雷。2.2 RS485自动收发电路的原理与参数计算RS485收发器是半双工器件RE和DE两个引脚分别控制接收和发送使能通常把两个脚合并成一个方向引脚低电平时接收高电平时发送。方向控制有两种做法一种是用一个GPIO主动切换另一种就是热词里常出现的“RS485自动收发电路”。自动收发电路的思路是借用TXD信号自身来控制方向空闲时TXD为高电平经过三极管取反后把DE拉到接收态发送起始位时TXD拉低三极管截止DE被上拉到发送态。这样一个GPIO都不占用电路也简单但代价是波特率越高越容易出问题因为RC上升沿和下降沿的切换需要时间。典型参数如下// 自动收发电路常用阻容取值按3.3V系统计算 // R1限制基极电流R2决定空闲分压点 // R3是DE上拉电阻集电极输出控制DE/RE #define RS485_AUTO_R1 2200 // 基极限流电阻单位欧姆 #define RS485_AUTO_R2 10000 // 基极对地分压电阻 #define RS485_AUTO_R3 10000 // DE上拉电阻这段取值对应的是NPN三极管接法逻辑是TXD空闲为高时三极管饱和导通DE被拉到GND收发器处于接收状态TXD发起始位变成低电平三极管截止DE通过R3上拉到高电平收发器切换为发送。参数说明R1取2.2k时基极电流约0.9mA足够让普通小信号三极管饱和R2取10k时与R1分压空闲基极电压约2.7V能稳定导通R3决定DE上升沿速度太小会增加静态功耗太大则上升沿变慢。自动收发电路适合9600到57600波特率、线缆不长、节点不多的场合成品USB转485模块里很常见。数量密集的工业RS485自动收发电路图通常在传输线两端接收状态相同的情况下没问题但波特率到115200且总线长度超过几十米时建议直接用软件控制方向不要在自动收发电路上继续压榨时序余量。2.3 软件方向控制与自动收发电路怎么取舍软件方向控制的电路更简单只需要把DE/RE并接到一个MCU GPIO上在发送前拉高、发送完成后拉低。控制时序可以用代码直接表达#define RS485_DE_ENABLE() GPIO_SetBits(GPIOB, GPIO_Pin_6) // PB6高DE1发送态 #define RS485_DE_DISABLE() GPIO_ResetBits(GPIOB, GPIO_Pin_6) // PB6低DE0接收态两个宏只做了两件事把PB6置高切到发送态或者置低回到接收态。参数说明GPIO要配置成推挽输出初始电平为低保证上电后收发器处于接收状态不会在总线空闲时把一个乱电平发出去如果项目里有限位开关、按键等设备选一个默认低电平的引脚即可不必纠结具体编号。对比项软件方向控制自动收发电路GPIO占用1个0个最高可靠波特率921600以上9600~57600较稳发送最后一位风险需要等待TC标志依赖RC延时停止位易被削代码可控性完全可控靠硬件原理不可动态调整适用场景协议复杂、波特率高简单点对点、固定波特率3. STM32CubeMX配置USART方向引脚、阻塞发送与DMA接收的落地方案3.1 CubeMX里设置USART1的关键参数用STM32CubeMX生成工程时F103的USART1默认挂在APB2总线上时钟是72MHz这个信息在后面计算波特率和调参时经常用到。串口参数按RS485常见配置走配置项推荐值说明ModeAsynchronous异步UARTRS485走的就是UART帧Baud Rate9600或115200距离长优先9600调试用115200Word Length8 Bits标准数据位ParityNone通常由上层CRC保证完整性Stop Bits11位停止位别选2位方向引脚配置成GPIO_Output初始电平为低相当于系统上电后立刻处于接收态。这里容易被忽略的是APB时钟分频USART1挂在APB2上如果CubeMX里APB2分频系数被改成2USART1的实际输入时钟会从72MHz变成36MHz同一个波特率配置下实际误差会变大通信距离长了以后误码率明显上升。所以生成代码后第一件事是确认APB2分频是否满足设计预期。3.2 使用HAL_UART_Transmit实现RS485发送的最后一位保护软件方向控制的RS485发送函数最容易踩的坑是“最后一位被截断”。HAL_UART_Transmit在标准实现里会等发送寄存器空但返回时移位寄存器里的最后一位停止位不一定已经完整移出如果立刻把DE拉低接收端看到的最后一帧就是坏的。void RS485_SendFrame(uint8_t *buf, uint16_t len) { RS485_DE_ENABLE(); // 先切到发送态再发数据 HAL_UART_Transmit(huart1, buf, len, 30); // 阻塞发送超时30ms // 等待发送移位寄存器彻底移完最后一位 while (__HAL_UART_GET_FLAG(huart1, UART_FLAG_TC) RESET) { } RS485_DE_DISABLE(); // 最后一个停止位完整移出后才释放总线 }代码逻辑分成三段先拉高DE方向脚让收发器进入发送模式然后调用HAL阻塞发送最后显式等待TC标志。TC标志表示发送移位寄存器已经移完最后一位UART状态寄存器里的数据才算真正全部离开引脚。参数说明超时时间30ms不是波特率相关的固定值它的作用是防止UART异常时函数永远卡死如果帧长很长或者波特率是960030ms可能不太够需要把超时改成长度与波特率之比的1.5倍以上。显式等待TC这步即使在HAL内部已经等过一次也不会拖慢速度因为这个标志在函数返回前大概率已经置位。如果不用HAL而是直接操作寄存器核心代码是发送数据寄存器写完后循环等待TC置位再在DE释放前做一次短延时效果是相同的。寄存器写法比HAL少一层封装适合对代码体积敏感的场景。3.3 用IDLE中断加DMA接收变长RS485帧RS485从机接收的数据长度往往不固定接收办法需要按变长帧来设计。逐字节中断接收的代码简单但每来一个字节都进一次中断对帧频繁的工业场合不划算。常见做法是DMA加空闲中断DMA负责把数据搬到内存USART空闲中断负责告诉你“这一帧数据已经结束了”。uint8_t rs485_rx_buf[256]; void RS485_StartRxDMA(void) { HAL_UART_Receive_DMA(huart1, rs485_rx_buf, sizeof(rs485_rx_buf)); __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE); } void USART1_IRQHandler(void) { if (RESET ! __HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); // 清空闲标志是关键 uint16_t len sizeof(rs485_rx_buf) - __HAL_DMA_GET_COUNTER(hdma_usart1_rx); RS485_OnFrame(rs485_rx_buf, len); // 交给上层协议解析 HAL_UART_DMAStop(huart1); // 先停DMA防止数据重叠 RS485_StartRxDMA(); // 重新启动下一帧接收 } }这段代码的关键是清IDLE标志的顺序先读标志再清除再用DMA计数器计算实际收到的字节数。DMA计数器是剩余未传输数量用缓冲区总长减去剩余数量就是刚收到的这一帧长度。参数说明缓冲区256字节如果总线上一帧最长是64字节这个余量是足够的但DMA收到256字节后会回绕覆盖数据所以设计帧格式时要在协议层限制最大长度主站发送完一帧后要等从机应答帧间隔最好留几毫秒避免从机还在处理上一帧时下一帧已经到达。4. RS485一主多从组网地址帧、CRC16与超时重发机制4.1 半双工总线上为什么必须由主机轮询RS485是半双工总线同一时刻只能有一个节点往A/B线上驱动电平。如果两个从机同时在总线上发数据就会产生冲突轻则这一帧数据全是乱码重则反复拉低总线让收发器进入限流保护。为了避免冲突最常见的可靠做法是“一主多从轮询”主机是唯一主动发起通信的节点从机只能在收到发给自己的帧后应答从机之间不会直接对话。主机依次给每个从机发送查询指令从机在自己的地址匹配时才回复这样总线上的每个时间窗口都只有一个发送者。轮询机制天然避免了两个从机同时抢占总线的问题代价是实时性和通信效率受轮询周期限制。如果总线上有10个从机每个从机应答时间10ms那么主机至少需要100ms才能完整轮询一圈对控制类应用来说这个延迟要提前算清楚别等到现场发现响应慢了才回头改协议。4.2 自定义RS485协议帧结构与CRC16查表实现协议帧设计不必从零造一大堆花活直接参考Modbus RTU的地址、功能码、数据、CRC结构最实用。一个简单的RS485帧可以定义为字段长度说明地址1字节1~247是对应从机地址0是广播地址命令1字节0x03读寄存器0x06写寄存器可按需扩展数据长度1字节数据区字节数数据N字节实际读写内容N上限由缓冲区定CRC162字节从地址到数据区末尾的校验值低字节在前CRC16实现可以用查表法也可以直接按位计算。对F103来说查表更快但代码会多一张256字节的表按位循环实现更省空间计算时间在几十微秒级别对9600和115200波特率来说完全够用。uint16_t rs485_crc16(uint16_t crc, uint8_t data) { crc ^ data; for (int i 0; i 8; i) { if (crc 0x0001) { crc (crc 1) ^ 0xA001; // 反射多项式对应Modbus RTU } else { crc 1; } } return crc; } uint16_t rs485_frame_crc(uint8_t *buf, uint16_t len) { uint16_t crc 0xFFFF; // 初始值 for (uint16_t i 0; i len; i) { crc rs485_crc16(crc, buf[i]); } return crc; }参数说明0xA001是0x8005的反射形式对应Modbus RTU的多项式计算得到的校验字节顺序是低字节先发初始值必须是0xFFFF帧尾的CRC计算范围要从地址字节算到最后一个数据字节不能把CRC自身算进去。如果你的项目需要和变频器、仪表这类现成设备通信很多设备自带Modbus RTU协议这套CRC函数和帧结构可以直接拿来做底层通信不需要自己设计功能码。4.3 从机地址过滤、应答与主站超时重发从机收到一帧数据先做地址过滤再做CRC校验。地址不匹配的帧直接丢弃连CRC都不用算因为在总线节点多的时候这是最省CPU的做法。void RS485_OnFrame(uint8_t *frame, uint16_t len) { if (len 5) return; // 最少包含地址、命令、长度、2字节CRC if (frame[0] ! MY_RS485_ADDR) return; // 地址不匹配直接丢弃 uint16_t crc_calc rs485_frame_crc(frame, len - 2); // 校验范围不含CRC本身 uint16_t crc_recv (frame[len - 2] 8) | frame[len - 1]; if (crc_calc ! crc_recv) return; // CRC不对静默丢弃不上报错误 RS485_SendFrame(response, resp_len); // 校验通过组织应答帧返回 }代码说明地址过滤放在CRC之前因为从机只需要关心主机发给自己的消息其他节点的帧对当前从机没有意义提前丢弃能缩短中断处理时间。参数说明广播地址0不需要回复从机按地址过滤时会直接放行CRC通过后执行命令但不回帧这个行为要在设计轮询时序时提前想清楚否则主机会一直等待一个永远不会出现的广播应答。主站侧配套的逻辑是超时重发。主机发送一帧后启动超时计时超时时间建议设为从机正常响应时间的2到3倍例如从机10ms内能回完主站就设30ms超时。超时后先重发同一帧连续重发3次仍然超时再判定该从机离线。重发前必须保证总线上处于空闲状态重发间隔至少要大于一帧时间否则可能和从机刚刚发出的迟到的应答帧撞车。5. RS485链路验证与排错回环自测、DE时序抓波与低波特率失效5.1 没有第二块板时的回环自测只有一块F103开发板的时候可以先把DE引脚强制拉高让RS485收发器一直处于发送状态然后串口助手发给MCU一个字节MCU收到后原样发回去。如果串口助手能收到同样的字节说明UART、收发器、A/B接线、CP2102这类USB转485模块的链路基本是通的。这个自测能排除接线问题但验证不了DE方向切换的时序所以正式调试时还要看第二步。5.2 用逻辑分析仪量DE引脚与最后的停止位把逻辑分析仪的两个通道分别接到DE引脚和UART的RX引脚上发送一帧数据观察DE下降沿与最后一个字节停止位的位置。如果DE在停止位还没结束时就已经拉低说明发送函数在最后一字节的停止位没有完整移出前就切回了接收态接收端把这帧当成错误帧丢弃。解决办法是显式等待TC标志或者在关闭DE前加延时延时长度按“1/波特率 × 1.5位”估算。5.3 4800比115200更难跑通时先查时钟源再看偏置电阻有热词提到“串口波特率9600能通信4800没有数据”这个现象在F103上很典型。问题不在波特率数值本身而在STM32F103的HSI内部RC振荡器精度不够。4800波特率对应每位时长208.3微秒如果系统时钟用了内部8MHz HS而没启用外部晶振或者外部晶振的负载电容和芯片不匹配实际波特率误差超过2%以后接收端采样点会逐渐偏离数据位中心低波特率下误差累积更明显。排查时优先确认是否真的在跑8MHz外部晶振再用示波器量MCO引脚看时钟频率。另一个隐蔽原因是总线空闲电压不足A/B之间空闲差压低于200mV时接收端输出不定态485收发器检测到的反而是一些随机电平低波特率下更容易表现为“完全没数据”。用万用表直流档量A和B之间电压正常应大于200mV低于这个值就要检查偏置电阻是否取值过大或者上拉A、下拉B的电阻有没有焊上。本文还有配套的精品资源点击获取