简介:面向STM32F103平台的RS485通信参考工程,适合嵌入式开发入门者、工业自动化及远程监控项目技术人员,重点解决长距离多节点串行通信中的UART配置、485驱动器控制与收发切换问题。压缩包含122个文件,以C源码、H头文件和启动汇编文件为主,另有Keil工程文件、hex固件、使用说明txt、PDF文档与辅助脚本,整体826KB,目录结构清晰,便于按需查阅和快速定位。已有5507人学习、下载,兼具学习与复用价值。资料完整给出硬件连接注意事项、UART初始化流程、DE/RE数据使能控制、发送与接收中断处理、错误检测等核心代码,配合标准外设库和Keil工程可直接编译、烧录、验证。读者可借助串口助手进行通信调试,并能根据实际项目调整波特率、校验方式、终端电阻及多节点协议,快速移植到自己的设备中。
1. 485通信是什么,为什么工业现场离不开它
1.1 从一个“丢帧”的夜班说起
我先讲个真实经历。几年前做一套分布式温控系统,现场设备分散在车间四个角落,最近的两台设备也隔着十五米。当时图省事直接用STM32的TTL串口拉线,结果一上电就傻眼——数据只要线缆超过两米就开始乱码,电机一转更是直接瘫痪。后来老工程师路过,看了一眼说:“换485吧。”这就是我第一次接触RS-485,从那以后,但凡涉及长距离、多节点、强干扰的通信,485几乎成了我的默认答案。
RS-485(也叫TIA-485-A)是一种差分串行通信标准。相比STM32直接输出的TTL电平(0~3.3V单端信号),485用A、B两根线之间的电压差来传输逻辑状态:A比B高200mV以上代表逻辑1,反过来代表逻辑0。这种差分结构的好处非常直观——外部干扰对两根线的影响是共模的,相减之后基本被抵消,所以抗干扰能力比单端串口强得多。
1.2 485的关键技术指标
| 指标 | 典型值 | 说明 |
|---|---|---|
| 传输距离 | 理论1200米 | 实际建议500米内,取决于波特率和线缆质量 |
| 节点数量 | 标准32个 | 加中继器可扩展,部分芯片可接128个 |
| 通信模式 | 半双工 | 同一时刻只能收或发,需要方向切换 |
| 逻辑电平 | A-B电压差 | 大于+200mV为1,小于-200mV为0 |
| 常用波特率 | 9600/115200 | 距离越长,波特率要适当降低 |
半双工是485最需要适应的思维转变。以前写TTL串口代码,只管往USART_DR里丢数据,发完就完事。但485不一样,你用一根双绞线同时承担发送和接收,就必须通过收发器芯片的DE/RE引脚来控制方向:拉高DE进入发送模式,拉低RE进入接收模式。这个切换看似简单,实际操作中的坑多得让我怀疑人生——后面专门用一整章来讲。
所以这个项目适合谁?如果你准备用STM32接入工业设备(电表、伺服驱动器、PLC)、搭建多节点传感网络,或者只是想把数据传到几十米外的上位机,485都是绕不开的一课。本文会从硬件电路、CubeMX配置、HAL库代码到排查技巧一条龙讲完,直接给你一套能复用的方案。
2. 硬件设计:从收发器选型到总线拓扑
2.1 收发器芯片怎么选
STM32的USART外设输出的是3.3V TTL电平,直接怼到双绞线上是传不了多远的,必须经过收发器转换成差分信号。市面上最常见的几款收发器我列个表:
| 芯片 | 工作电压 | 节点数 | 速率上限 | 特点 |
|---|---|---|---|---|
| MAX485 | 5V | 32 | 2.5Mbps | 经典款,便宜,假货多 |
| SP3485 | 3.3V | 32 | 10Mbps | 3.3V系统首选 |
| ISL3170 | 3.3V | 256 | 20Mbps | 高速高节点,带失效保护 |
| MAX3485 | 3.3V | 32 | 12Mbps | 和MAX485引脚兼容 |
我的建议是,STM32系统用3.3V供电就选SP3485,5V系统选MAX485。这两个芯片广量最可靠。注意MAX485如果用在3.3V系统里虽然也能跑,但输入高电平阈值可能踩线,不建议冒险。我之前就吃过亏,MAX485在3.3V下偶尔出现接收错误,换SP3485后一切正常。
2.2 硬件电路的关键细节
很多教程给的参考电路就一颗收发器加两个电阻,实际部署时会发现不够用。我贴一下自己验证过的电路要点:
- 方向控制引脚:通常将DE和RE短接,用STM32的一个GPIO控制。拉高进入发送模式,拉低进入接收模式。
- 终端匹配电阻:总线最远两端各接一个120Ω电阻,用于消除信号反射。注意不是每个设备都接,只在物理链路的首尾两端接。
- 上下拉偏置电阻:在A线接上拉到VCC(通常4.7kΩ),B线下拉到GND。这是很多新手忽略的,它的作用是保证总线空闲时A-B电压差处于确定的状态,防止空闲时收到乱码。
- 保护器件:工业场景建议加TVS管(比如SMBJ6.0CA)和自恢复保险丝,防止静电和浪涌损坏收发器。
这个偏置电阻的细节我特别想强调。如果没有上下拉电阻,总线空闲时所有收发器都处于高阻态,A-B之间的电压差是浮动的,可能落在-200mV到+200mV之间,接收端就会输出随机数据。有了上下拉,空闲时A比B高,逻辑确定是1,接收端不会乱跳。我自己实测过,没加上下拉的485总线在接上设备后,串口助手偶尔会收到0x00或0xFF的毛刺,加上电阻之后这个问题彻底消失。
2.3 最小系统接线参考
以STM32F103C8T6为例,接SP3485的完整引脚关系:
// 引脚分配 PA2 -> USART2_TX -> SP3485 DI PA3 -> USART2_RX -> SP3485 RO PA1 -> GPIO_OUT -> SP3485 DE/RE(短接后控制)接好之后通电,用万用表测A、B之间的电压,空闲状态应该A比B高约0.2~0.5V。没有偏置电阻时两个引脚几乎等电位,有偏置后就能看到明显压差。这是一个很实用的硬件自检方法。
3. 软件实现:HAL库下的完整收发流程
3.1 CubeMX配置要点
用STM32CubeMX配置USART2,参数如下:
- Mode:Asynchronous(异步模式)
- Baud Rate:9600或115200,根据现场距离来定
- Word Length:8 Bits
- Parity:None
- Stop Bits:1
- USART2中断:启用全局中断
PA1配置为GPIO_Output,初始电平设为低(接收模式)。这里有个细节:很多人习惯把方向引脚初始化成高电平,结果设备一上电就先发送模式,如果总线上有其他设备正在通信,就会造成总线冲突,直接干扰正常帧。所以初始化为接收模式(低电平)是绝对必要的。
3.2 发送流程:先拉高DE再发数据
发送的核心顺序是:拉高DE进入发送模式,调用HAL_UART_Transmit发送,等待发送完成标志,再拉低DE切回接收。这个顺序不能乱,尤其不能发完立刻拉低DE,否则最后几个字节会丢。
// 485发送一帧数据 void RS485_SendData(uint8_t *data, uint16_t len) { // 1. 切换到发送模式 HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_SET); // 2. 发送数据,等待完成 HAL_UART_Transmit(&huart2, data, len, 100); // 3. 等待发送移位寄存器清空(关键!) while (__HAL_UART_GET_FLAG(&huart2, UART_FLAG_TC) == RESET); // 4. 切回接收模式 HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_RESET); }重点解释第3步。HAL_UART_Transmit返回只代表数据已经进入发送数据寄存器(TDR),但移位寄存器可能还没发完。如果这时候立刻拉低DE,最后一个字节的停止位可能才传输到一半,收发器就切到接收了,总线上这个不完整的字节直接变成噪声。加上等待TC(Transmission Complete)标志的循环,确保所有数据包括停止位全部发送完毕,再切换方向。
3.3 接收流程:中断接收 + 状态机
接收建议用中断或者DMA,不要在主循环里死等HAL_UART_Receive。我用的是经典的中断接收,配合一个简单的状态机来识别帧头帧尾:
uint8_t rx_buf[64]; uint8_t rx_index = 0; uint8_t rx_frame_ready = 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart == &huart2) { rx_buf[rx_index++] = rx_temp; // 简单帧处理:收到帧尾0xAA认为一帧结束 if (rx_temp == 0xAA) { rx_frame_ready = 1; rx_len = rx_index; } else if (rx_index >= 64) { rx_index = 0; // 防止溢出 } HAL_UART_Receive_IT(&huart2, &rx_temp, 1); } }收到完整帧后,在主循环里检查rx_frame_ready标志,处理完清零。这个思路简单可靠,而且不会阻塞其他任务。工业帧协议通常有CRC校验,我这里为了示例简化成帧尾判断,实际项目一定要加上校验。
3.4 带DMA的优化方案
如果数据量大,中断逐个字节接收会频繁打断CPU。可以改成DMA空闲中断方式:配置USART2的DMA接收,开启空闲中断(IDLE Interrupt),当一帧数据结束后触发空闲中断,DMA自动报告接收了多少字节。
// 开启DMA接收 HAL_UART_Receive_DMA(&huart2, rx_dma_buf, RX_BUF_SIZE); // 空闲中断处理函数 void USART2_IRQHandler(void) { if (USART2->SR & USART_SR_IDLE) { // 清除标志 USART2->SR = 0; __HAL_UART_CLEAR_IDLEFLAG(&huart2); // 计算接收长度 rx_len = RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart2_rx); // 处理帧... // 重新开启DMA接收 HAL_UART_Receive_DMA(&huart2, rx_dma_buf, RX_BUF_SIZE); } }这个方式的好处是CPU利用率极低,而且天然按帧处理数据。缺点是代码复杂度上来一些,而且DMA接收缓冲区的长度要足够大,防止一帧数据溢出。如果是Modbus RTU这种协议,还额外需要3.5个字符时间间隔来判断帧结束,用空闲中断是一个很好的前提。
4. 极容易踩的坑:总线冲突、数据错乱、丢帧
4.1 DE切换太快导致丢字节
这个坑我前面提到过,这里再详细说。现象是发送函数返回后,对方收到的数据总是少最后两个字节,或者最后一个字节数据错误。原因就是没有等待TC标志就拉低了DE。解决方法是加等待循环。还有一个隐藏原因:如果DE引脚挂了电容,电平翻转需要时间,拉高后立刻开始发送,第一个字节也可能被截断。稳妥的做法是拉高DE后加一个微秒级别的延时(比如1us),再开始发送。
4.2 收发器使能脚接反
有一次我画板子的时候把DE和RE分开接了,DE接到了普通GPIO上,RE直接接地。结果是收发器永远处于接收状态,发送的数据直接发不出去。这个问题最迷惑的地方在于,如果只测A/B波形,能看到收发器输入端有信号,但差分输出端就是没输出。查了半天才意识到是RE一直拉低,收发器的发送通道被禁用了。常规做法是把DE和RE短接到一个GPIO,这是最省事最不容易出错的接法。
4.3 胡乱加终端电阻导致信号衰减
终端电阻的作用是匹配线缆阻抗、消除反射,但只在总线的首尾两端加。有些新手在每个设备端都接了120Ω,导致总线等效阻抗变得很低,收发器输出电流加大,信号幅度下降,反而通信失败。一个简单判断方法:总线空闲时用万用表量A-B之间的电阻,如果设备都断电还是约60Ω,说明两端各有一个120Ω电阻,这是正确的。如果量出来更低,比如30Ω,说明某个点接多了。
4.4 波特率不同步导致数据错乱
这个看起来像是“低级错误”,但实际发生频率很高。尤其是用USB转485调试工具时,默认波特率9600,而STM32程序里配置的是115200,两边都没错,就是没对上。还有一个更隐蔽的情况:STM32的时钟树配置错误导致USART波特率实际值和理论值偏差过大。特别是外部晶振频率配错(比如实际是8M晶振,CubeMX里选的HSI内部时钟),115200的实际波特率可能偏到10万左右,近距离通信勉强能通,距离稍远就频繁错误。
排查方法很简单:先用示波器看USART_TX引脚的波形,数一下一个字节(比如0x55,波形是01010101)的位宽,能精确算出实际波特率。没有示波器的话,串口助手发一串1,捕捉接收数据计算对比。
4.5 总线空闲电平不稳导致乱码
前面讲过上下拉电阻的重要性。实际项目里如果设备数量多,上下拉电阻加在每个设备端,并联后等效阻值很小,也可能出问题。比如20个设备每个都接4.7kΩ上下拉,等效上拉约235Ω,总线空闲时电流达到十几毫安,部分质量差的收发器可能扛不住。解决办法是只在两个终端设备上接上下拉,中间设备只接收发器。这个在制定接线规范时要写清楚。
5. 一个完整的示例工程:轮询式多机通信
5.1 协议设计
我以实际做过的环境监测系统为例,设计一个简化版的主从协议。总线上一台STM32做主机,挂3台从机(从机也各用STM32实现),9600波特率,一主多从轮询:
帧格式:
| 帧头 | 地址 | 功能码 | 数据长度 | 数据 | CRC16 | 帧尾 |
|---|---|---|---|---|---|---|
| 0xAA | 1字节 | 1字节 | 1字节 | N字节 | 2字节 | 0x55 |
主机发送查询命令,地址匹配的从机回复数据帧。CRC16用Modbus的标准多项式0xA001,从地址字节开始计算,包含到数据结束。
5.2 主机轮询实现
// 主机查询从机地址为addr的设备 void Master_Query(uint8_t addr) { uint8_t cmd[8]; cmd[0] = 0xAA; // 帧头 cmd[1] = addr; // 从机地址 cmd[2] = 0x03; // 功能码:读数据 cmd[3] = 0x00; // 数据长度 cmd[4] = 0x00; cmd[5] = 0x00; // 预留 uint16_t crc = CRC16_Modbus(cmd + 1, 4); cmd[6] = crc & 0xFF; cmd[7] = crc >> 8; RS485_SendData(cmd, 8); // 等待从机回复,超时100ms uint32_t start = HAL_GetTick(); while (!rx_frame_ready) { if (HAL_GetTick() - start > 100) { printf("Device %d no response\r\n", addr); return; } } // 处理回复帧... rx_frame_ready = 0; }主循环里用for循环依次查询3个从机地址,加上适当的间隔时间,就构成了完整的轮询周期。实际工程里,如果某个从机掉线,要连续失败3次才判定离线,防止瞬时干扰误报。
5.3 从机响应实现
从机侧的核心是收到帧后校验地址和CRC,通过后执行命令并组织回复数据。地址匹配的关键点在于,从机接收完一帧后,在发送回复前,要先检查一下总线是否空闲(即接收模式下DE为低且无接收中断发生)。多数情况下这个检查是多余的,但万一主机同时向两个从机发出广播类命令(虽然我的设计里没有),从机们同时抢占总线就会冲突。
void Slave_Process_Frame(void) { // 检查地址是否匹配 if (rx_buf[1] != MY_ADDR) return; // CRC校验(略) if (CRC_Check() != OK) return; // 执行命令,组织回复数据 uint8_t resp[16]; resp[0] = 0xAA; resp[1] = MY_ADDR; resp[2] = 0x83; // 功能码+0x80,回复 resp[3] = sensor_count; // 填充数据... uint16_t crc = CRC16_Modbus(resp + 1, 4 + sensor_count); resp[4 + sensor_count] = crc & 0xFF; resp[5 + sensor_count] = crc >> 8; // 发送回复 RS485_SendData(resp, 6 + sensor_count); }这个架构扩展性很好,以后加设备只需要分配新地址,硬件不需要改动。我后来在这个基础上移植了FreeModbus协议栈,直接和组态软件对接,基本没改业务代码。关于FreeModbus移植,如果你是从零开始,建议先跑通裸机收发,再套协议栈,不然协议栈本身的各种异常处理会让排查难度翻倍。
6. 调试技巧与工具推荐
6.1 必备工具
调试485通信,示波器是排查疑难问题的首选。逻辑分析仪也可以,但示波器能直接查看A-B差分波形、判断信号质量。没有示波器的话,至少准备一个USB转485模块(比如CH340T+MAX485方案的),配合串口助手先验证单点通信是否正常。
调试思路分三步:
- 第一步:STM32自发自收(把收发器输出端短接A-B,去掉终端电阻),验证USART配置是否正确。
- 第二步:STM32通过485模块连接PC,用串口助手收发,验证硬件链路。
- 第三步:接入从机,跑协议,逐个排查帧内容。
6.2 排查流程速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 完全收不到数据 | 收发器方向脚接反 | 量DE引脚电平是否随收发切换 |
| 丢最后一个字节 | 发送后未等TC就切方向 | 加等待TC循环 |
| 收到乱码 | 波特率不匹配/时钟配置错误 | 示波器测量实际波特率 |
| 空闲时收到0x00/0xFF | 总线无上下拉偏置 | 在A/B上加4.7kΩ上下拉 |
| 距离稍远就出错 | 终端电阻缺失/线缆质量问题 | 首尾加120Ω电阻,换双绞线 |
| 多从机时偶尔冲突 | 从机回复太慢/总线驱动时间重叠 | 检查从机回复延时,加总线仲裁 |
6.3 一个我踩过的印象最深的坑
最后说一个让我折腾了两天的案例。现场三台STM32组网,主机偶尔读不到从机2的数据,但单测从机2又是正常的。用示波器抓波形发现,从机2回复时,总线上的信号有严重的过冲和振铃。排查到最后发现是从机2的线缆特别长(约200米),而且终端电阻接在了主机端和从机1端,从机2那端恰好是中间位置,没有终端匹配。线缆长加上无终端匹配,反射叠加导致信号变形,主机无法正确解码。把终端电阻移到从机2端后,问题立刻消失。
这个案例让我得出一个经验:终端电阻的物理位置比简单地“首尾各一个”更微妙,如果你的总线不是一条直线,或者有分叉(注意485总线是不允许有星型分叉的,必须手拉手串联),要考虑把终端电阻放在真正物理链路的两端。
7. 写在最后的一些心得
做485通信这几年,踩过的坑大多是同一个根源:对半双工的理解不够彻底。TTL串口是单工或者全双工思维,发完就完;485要求你时刻清楚总线当前谁在用、信号有没有真正传完、切换方向会不会撞车。把这一点想透了,很多莫名其妙的问题都能顺着这条主线找到根源。
另外建议刚入手的同学,不要一上来就追求复杂方案。先把最基础的单发单收跑通,再往上叠加多机协议、DMA、FreeModbus。485是可靠性优先的通信方式,稳定比花哨重要得多。方法没错的话,即使现场环境再恶劣,拔了线再插、断了电再开,通信依然能恢复,这才是我们要的效果。
本文还有配套的精品资源,点击获取