1. 从总线上聊起:485为什么到现在还没退休
在STM32的学习路线里,串口UART基本是入门第一关,而485则是出门左转就会撞上的实际工程题。做嵌入式这几年,我调485踩的坑,比SPI和I2C加起来都多。这不是说485原理有多绕,而是它牵扯的点太杂:硬件上要看懂差分波形,软件上要掐准收发方向的切换时机,协议层还要自己拼帧、算校验。所以这篇学习心得,我想把485的完整链路串一遍,从原理到硬件,再到STM32的发送接收代码和配套实验,最后把调试时遇到的真问题整理成一份速查表。
这篇内容适合三类人:一是学完STM32串口、正在纠结下一步学什么的人;二是手上有一块需要做远距离通信的板子、发现普通串口距离不够的人;三是想搞懂Modbus RTU底层到底跑的是什么的工程师。我会先把原理讲透,再给出一套可以直接抄作业的硬件连接和代码,然后记录一次完整的两机和多机通信实验过程。你照着做一遍,比死磕十篇理论文章都有用。
1.1 单端信号变差分:485信号到底怎么传
常规UART是TTL电平,发高电平就是3.3V,发低电平就是0V,这叫单端信号。单端信号最大的问题是:地线电位只要有一点偏差,接收端判断逻辑高低就可能出错,而且线拉长了之后,线缆本身的电阻和外界干扰都会叠加到信号上。
485做的事情,就是把单端信号变成差分信号。它用的是A、B两根线,不再以"对地电压"为判断标准,而是看A线对B线的电压差。按惯例,A比B高200mV以上代表逻辑1,A比B低200mV以上代表逻辑0。换句话说,收发器只关心两根线之间的差值,外部干扰如果同时叠加到A和B上,两者的差值并不会变。这就像两个人手拉手站在晃动的车上,车再晃,他们俩的相对位置不变,依然能感知彼此的动作。
这个特性给485带来了两个杀手锏优势:一是抗共模干扰能力强,哪怕电机、变频器在旁边猛转,总线依然稳;二是传输距离远,理论极限能到1200米,普通TTL串口拉个两三米就开始冒汗了。工业现场为什么到现在还大量用485,核心原因就是这两点。
1.2 半双工、多节点:这些关键词怎么理解
RS-232是点对点通信,一个发一个收,全双工,简单粗暴。485则是半双工:同一时刻总线上只能有一个发送方,其他节点都在听。想说话,得先抢到总线。所以设计通信协议时必须有"轮询"的概念,主机逐个点名,从机被点到才应答,从机之间绝不能主动开口。
485还支持在一条总线上挂多个节点。标准负载下,一条总线最多能挂32个节点(有些低负载芯片能挂64甚至128个)。各个节点通过地址区分,就像教室里老师点名,叫到谁谁才站起来回答。半双工加多节点,让485特别适合做一主多从的采集系统:温度传感器、电表、PLC、变频器,全挂一条线上,主机统一调度。
这里要补一个常见误区:距离和速率是互相牵制的。同一个485网络,波特率越低,能传的距离越远;波特率越高,距离越短。比如9600bps下跑几百米很轻松,但跑到115200bps,距离通常就缩到几十米了。具体能跑多远,和线材质量、终端电阻匹配、现场干扰强度都有关系,不是表上查一个数就万事大吉。
1.3 收发芯片怎么选:SP3485和MAX485那些事
STM32的串口引脚输出的是TTL电平,不可能直接往A、B线上怼,中间必须加一个485收发器。这芯片干的活就是:发送时把TTL的0和1转换成A、B之间的差分电压,接收时把总线上的差分电压翻译回TTL电平。
常见的收发器芯片有MAX485、SP3485、ISL83485等。MAX485是5V供电的经典老将,SP3485则是3.3V版本,很多STM32开发板上直接焊的就是SP3485,原因很简单:STM32是3.3V系统,用SP3485不用做电平转换。芯片内部主要关心的引脚就四个:DI(发送数据输入)、RO(接收数据输出)、RE#(接收使能,低有效)、DE(发送使能,高有效)。DE和RE#其实是一对反向控制脚,实际电路里通常把这两个引脚短接,用一个GPIO来控制:置高就是进入发送模式,置低就是进入接收模式。
选隔离型还是非隔离型,取决于现场环境。简单实验板用非隔离的够了;如果是在强干扰环境,或者总线节点之间地电位差异很大,建议选带隔离的收发器,或者外部加数字隔离器。这个后面接硬件时细说。
2. 硬件电路设计:这几个细节千万别偷懒
讲完原理,直接进入动手环节。我用的是STM32F103C8T6这个最小系统板,外接一个SP3485模块。你把原理图摊开看,本质上就三部分:STM32的串口引脚连到收发器的TTL侧,收发器的A、B连到总线上,中间夹着一个GPIO控制收发方向。
2.1 最小连接:一根TX、一根RX、一根方向控制脚
STM32的USART1默认引脚是PA9(TX)和PA10(RX)。PA9接SP3485的DI,PA10接SP3485的RO。方向控制脚我随便选一个普通GPIO,比如PA8,接到SP3485的DE/RE#短接点上。为了方便代码里操作,可以定义两组宏:
#define RS485_DE_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET) #define RS485_DE_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET)PA8输出高电平,SP3485进入发送状态;输出低电平,回到接收状态。整个硬件连接就这些,没有多余的东西。
2.2 总线偏置:空闲时别让A、B悬空
很多人在这一步会踩坑。SP3485模块一般会预留A和B两个接口,有些模块上已经带了偏置电阻和终端电阻,有些则是裸的。所谓偏置,就是在A线上拉到VCC、B线上拉到GND,各串一个10kΩ电阻。作用是什么呢?保证总线上没有任何设备发送时,A对B的电压差仍然稳定在逻辑1,也就是空闲状态。如果总线上没有偏置,所有收发器都处于接收态,A、B之间是高阻,一点小噪声就能让总线上的数据乱跳,你会看到串口助手疯狂收到0xFF或者乱码。
实验阶段的建议:如果手上是裸模块,自己加两颗10kΩ电阻,A接VCC,B接GND。如果模块已经内置偏置,那就什么都不用动。自己画板子时,偏置电阻一定不要省。
2.3 终端电阻:是不是每次都接,得分情况
485标准推荐在总线物理最远的两个端点各接一个120Ω电阻,目的是匹配传输线阻抗,减少信号反射。信号在线上高速传输时,如果末端阻抗不匹配,能量会反射回来,造成波形畸变,表现就是高速通信时偶尔乱码。
问题是很多新手听了"必须接120Ω"就盲接,结果在短距离实验时反而把自己搞懵了。我自己的经验是:实验距离在几米以内,不接终端电阻一般也能跑;距离超过几十米,或者波特率上了57600,必须把两端120Ω接上。注意是两端,不是每个节点都接。如果一条总线挂了一主两从,总共有三个设备,只在总线物理两端各接一个电阻,中间节点不要接。全部接上会导致负载过重,信号幅度被压低,反而出错。
2.4 共地问题:差分不是免死金牌
这一点我吃了大亏。485虽然只用A、B两根信号线,但所有节点之间最好还是把GND连一起。有人想:"差分信号不看地电位,是不是不用共地?"理论上是这样,但实际收发器的输入范围是有限度的。A、B线上的电压如果超出芯片允许的共模范围(通常是-7V到+12V),芯片就会损坏,或收发状态错乱。
只要是把多块独立供电的板子接到同一总线上,它们的GND电位可能相差好几伏,轻则通信异常,重则烧芯片。所以工程上要么拉一条GND线把所有节点统一,要么用隔离型485收发器,让总线部分和MCU部分彻底分开。实验室怎么选?我建议最省心的方案是三线互联:A、B、GND各接一根,简单可靠,也不要额外花钱。
3. 软件实现:让STM32把485跑起来
硬件焊好了,接下来是软件。485通信的软件核心只有一个字:等。发送时要等数据真正从引脚上发完再切换方向,接收时要等完整的一帧数据到齐再做解析。下面我会从最常用的查询发送和中断接收讲起,再给一个更实用的一主多从框架。
3.1 发送方向切换的时机:TC标志位比延时靠谱
很多人初学485,写出来的发送函数长这样:
RS485_DE_HIGH(); HAL_UART_Transmit(&huart1, data, len, 1000); RS485_DE_LOW();单独跑一下,好像正常。但如果在高速或者连续发送时,对端就会经常丢最后一两个字节。原因在于HAL_UART_Transmit返回时,数据只是放进了UART的发送移位寄存器,并不代表已经全部从引脚上送出去了。如果立刻把DE拉低,最后一个字节可能只发了一半,总线就被你切回接收模式了。
正确做法是等TC(发送完成)标志位置位,再切方向。用STM32标准库或者HAL库都可以这样写:
uint8_t RS485_SendFrame(uint8_t *buf, uint16_t len) { RS485_DE_HIGH(); HAL_UART_Transmit(&huart1, buf, len, 1000); while (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TC) == RESET); RS485_DE_LOW(); return 1; }__HAL_UART_GET_FLAG这个宏就是读串口状态寄存器里的TC位。TC置1,说明最后一个停止位都已经送出,这时代DE拉低才安全。这里再提一个小技巧:如果用的不是HAL库,而是在中断或者RTOS环境里,可以用发送完成中断来做切换,但核心判断标准是一样的,就是TC位。
3.2 接收配置:中断接收才是正常姿势
接收端相对简单。先把485方向引脚拉低,进入接收模式,然后配置串口接收中断。我习惯用HAL库的接收函数:
uint8_t rx_buf[64]; HAL_UART_Receive_IT(&huart1, rx_buf, 64);每收到一个字节,HAL都会调用一次中断回调。在回调里把数据存进自己的缓冲区:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { // 这里处理收到的字节 rx_buf[0],再重新开启一次接收 HAL_UART_Receive_IT(&huart1, rx_buf, 64); } }中断接收的好处是不占CPU,MCU可以在接收期间干别的事。等一帧数据到齐后,再统一解析。帧什么时候算到齐?最简单的方式是固定帧长,收满这不长,刚好是一帧。如果需要接收不定长数据,后面我会讲空闲中断+DMA的玩法。
3.3 一主多从的帧格式设计
485协议层没有统一标准,除非你跑Modbus,否则帧格式基本是自定义。我经过几个项目数据统计,比较稳妥的帧结构是下面这样:
| 字段 | 长度 | 说明 |
|---|---|---|
| 帧头 | 1字节 | 固定0xAA,用于同步 |
| 从机地址 | 1字节 | 0x01~0xF7,0x00为广播地址 |
| 命令码 | 1字节 | 读写命令定义 |
| 数据长度 | 1字节 | 后面的有效数据字节数 |
| 数据域 | N字节 | 具体载荷 |
| 校验 | 1字节 | CRC8或累加和校验 |
| 帧尾 | 1字节 | 固定0x55 |
从机收到一帧数据后,先看帧头帧尾对不对,再看校验和,然后判断地址是不是自己,最后才解析命令。这样做的好处是:总线上的节点都能收到合法帧,但只有地址匹配的节点才会应答,不会造成总线冲突。
举个简单例子。主机要查从机0x01的温度,可以发送:AA 01 03 00 00 55,其中03是读温度命令,数据长度0。从机收到后回复:AA 01 03 01 25 56 55,其中25是温度数据,56是校验和。这个协议没有用CRC16那么复杂,实验阶段够用了。
3.4 主从机的完整收发配合
主机发送后,马上要切回接收模式,等待从机回包。这里有个关键点:切换方向之后,要稍微等一会儿,不能立刻就去读接收缓冲区里有没有数据。从机收到命令后需要时间解析,再组织应答,总线上数据回来还有个传播时延。一般做法是在发送完成后加一个小延时,比如1毫秒,或者直接等中断事件来临。更好的做法是收到完整帧后,用帧尾或者校验值来触发处理,而不是靠延时猜测。
从机的代码结构就三件事:初始化时方向引脚拉低;中断里收字节;攒够一帧后校验并判断是否是给自己的命令,是的话就置一个处理标志,主循环里写应答。
4. 实验记录:从点对点到多机轮询
软件代码在手,接下来就是上电实测了。我这里记录一次完整的实验过程:先是两块板子点对点通信,然后加第三个节点做多机轮询,全程用串口助手和逻辑分析仪观察现象。
4.1 点对点实验:两块板子互发
实验用的材料很简单:两块STM32F103C8T6最小系统板,两块SP3485模块,若干杜邦线。接线图如下:
- 主机:PA9 -> 模块DI,PA10 -> 模块RO,PA8 -> 模块DE/RE#
- 从机:同样接法
- 两个模块:A接A,B接B,GND接GND
- 两个模块的总线收发使能,默认处于能收能发的状态(通过单片机GPIO控制)
波特率两边都设置成9600,数据格式8N1。从机侧我写了一段简单的代码:收到一字节就在LED上翻转一下。主机用一个按键触发,每按一次按键,就通过485发送一帧测试数据。
第一次上电时,我犯了个典型错误:主机发送完立刻把DE拉低,从机偶尔能收到,偶尔丢字节。后来改成等待TC标志位再切方向,问题立刻消失。这说明实验中的丢帧十有八九是方向切换太早,不是硬件问题。
4.2 用示波器和逻辑分析仪看总线状态
调485这类差分信号,示波器是神器。把探头夹在A线和B线之间,看的其实是A-B的电压差。空闲状态时,A-B应该是正电压,大约在1V到2V之间(芯片不同略有差异)。发送数据时,波形会从空闲电平拉低,表示逻辑0,然后按位跳变。
我在实验里抓了一帧波特率9600的波形:起始位先拉低,然后从最低位开始逐位发送,最后停止位拉回高电平。这个波形和普通UART的波形本质一样,只不过普通UART的参考地是GND,而485的参考基准是另一根线。逻辑分析仪抓A、B两根线,再开差分运算,也能看到一样的时序。
这里提供一个小经验:如果示波器只有单通道,可以把A和B分别抓两个通道,然后用数学通道做减法,也能还原出差分信号。没有示波器的情况下,逻辑分析仪的差分模式也一样用,前提是设备支持。
4.3 多机轮询实验:三块板子挂在一条总线上
点对点通了之后,我加了一块板子,三条总线并联,还是A接A、B接B、GND接GND。地址规划如下:
| 设备 | 地址 | 作用 |
|---|---|---|
| 主机 | 0x00 | 轮询发起者 |
| 从机1 | 0x01 | 模拟温度采集 |
| 从机2 | 0x02 | 模拟湿度采集 |
主机每隔500毫秒轮询一次:先问从机1要温度,再问从机2要湿度。从机1和从机2只处理地址等于自己的命令,收到其他地址的帧直接忽略。观察串口助手的输出,每隔一段就出现两条应答数据,说明轮询已经跑通。
从这个实验里能明显感觉到485的一个特性:从机是"哑巴",不点名不说话。如果两个从机的地址设成一样的,数据就会在总线上打架,主机这边收到的全是乱码。这个现象后面会专门讲排查方法。
5. 常见问题与排查技巧实录
实验过程中遇到问题的概率,比想象中大得多。这里把我在485实验中遇到过的典型问题整理成速查表,附带排查思路。这张表可以直接存在手机里,去现场调试时照着查。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全收不到数据 | 接线错误或方向引脚一直停在发送态 | 先用万用表量A、B是否接反,再用逻辑分析仪看DI和RO引脚上有无信号 |
| 能发不能收 | DE/RE#一直为高,芯片锁在发送模式 | 软件里检查是否发送完没及时拉低DE,或在初始化时把方向脚设成了高电平 |
| 接收乱码,频繁收到0xFF | 总线空闲时A、B没有稳定偏置 | 在A线加上拉、B线加下拉,确保空闲态是逻辑1 |
| 距离一远通信就不稳 | 总线末端没有终端电阻 | 在总线的两个物理端点分别接120Ω电阻 |
| 多机同时通信时数据冲突 | 从机地址重复或从机主动发数据 | 检查地址分配,并确保伪从机只在收到主机命令后才应答 |
| 间歇性丢最后一字节 | 发送后DE拉低太早,停止位没发完 | 等待TC标志位置位后再切回接收模式 |
| 两块独立供电板子互相烧摸接口 | 没有共地,共模电压超限 | 连接GND线,或换隔离型485收发器 |
| 下载程序后串口无法打印 | 485芯片接在调试串口上,方向脚初始状态异常 | 如果调试串口和485复用,确保MCU上电后初始化为接收模式,再考虑打印 |
5.1 乱码的根源:总线空闲态不稳定
这是新手最常遇到,也是最容易误判的问题。现象是上电之后什么都不发,串口助手也一直收到乱码。把示波器夹上去看,A、B之间的电压差在0V上下晃。这种情况几乎都是总线偏置缺失。
解决方案就是在A上加10kΩ上拉,B上加10kΩ下拉。加了之后,空闲时A-B会被拉到正电压,不再乱跳。有些成品485模块自带这个电路,买的时候留意一下参数。自己画板子的话,永远把这组电阻画上去,带来的成本不到一毛钱,却能省掉大量排查时间。
5.2 收发切换的时序:不是快了就灵
我一开始也以为切换越快越好,实际上太快会切掉尾巴,太慢会错过从机回包。正确思路不是靠延时去猜,而是以标志位为准。发送侧等TC,接收侧等帧完整。如果用的是RTOS,把这块封装成一个互斥操作就更好,避免两个任务同时操作485导致总线竞争。
在不同波特率下,单字节的传输时间也可以算一下。9600bps下,一帧10位(起始位1+数据8+停止位1)差不多1.04毫秒。如果发送4字节的数据,整个过程大约4.2毫秒。这个数字在做延时会比较有用:方向切换的延时设置不能小于这个值,否则肯定把尾巴切掉。
5.3 地址冲突和总线冲突的诡异现象
多机通信时,一旦地址配错,现象比单机问题更玄幻。最典型的是:主机发查询命令,总线上多个从机同时应答,两路信号叠在一起,主机收到的不只是乱码,有时候还把不同从机的数据拼成"假帧",甚至校验都能意外通过。这种情况非常隐蔽,排查手段是逐个断开从机,或者给每个从机单独烧录不同地址再观察。还有一种常见情况是:总线上的某个从机上电瞬间,其485芯片的A、B引脚处于高阻态,外部偏置电阻上拉到高电平,实际上它会产生一次短暂的数据扰动,如果主机刚好在那个瞬间发命令,命令也会变成乱码。
我通常会这样处理:所有从机上电时不主动发送任何数据,只是等主机轮询;主机启动后先延时几百毫秒再发起第一帧查询,让总线上所有节点的收发器都稳定下来。
6. 最后说两句自己的经验
写这篇心得的时候,我又把事情从头到尾自己走了一遍,最大的感触是:485通信看起来简单,但真正要把它做稳,靠的是对细节的尊重。硬件上,A/B/GND三根线、终端电阻、偏置电阻、方向引脚的初始状态,一个都不能想当然;软件上,发送完成标志、接收中断、帧协议、地址分配,一环扣一环,任何一环松了都会在调试时给你一记闷棍。
项目里面很多问题不是技术能力不行,是缺少系统性的排查思路。我自己现在的习惯是:遇到485问题,先拿示波器看A-B波形,再看收发的切换时序,最后查协议和地址配置。按照这个顺序来,绝大多数问题都能在半小时之内定位。另外一个个人习惯是,所有485相关的模块上电默认都设成接收模式,然后发送函数里做成"发完必等TC再拉低",这两条规则救了我很多次。
最后有一个使用技巧想分享:在调试485网络时,如果手头没有双通道示波器,可以先用USB转485工具接上去,用上位机软件监控总线数据。这个方法虽然看不到波形,但能快速判断"消息到底有没有发出去""回的帧结构对不对"。等故障范围缩到物理层,再上示波器也不迟。这套组合拳,比一上来就盲目改代码有效率得多。