
做过单片机串口通信的朋友基本都绕不过RS485这根总线。老式仪表、变频器、传感器、PLC工业现场到处是它的影子。RS485用差分信号传输抗干扰能力强通信距离能到1200米一条总线上还能挂几十个设备这些特性让它到现在依然是工控领域最普及的物理层接口之一。但RS485有个让新手头疼的地方它是半双工的发送和接收共用一对差分线你得自己管理方向切换。传统方案要用单片机的GPIO去控制收发芯片的DE引脚时序稍微没算好就丢帧。这次我在一个环境监测项目里选了贴片封装的MAX13487收发器它内置自动换向功能配合MicroPython开发直接省掉了方向控制逻辑代码清爽很多。这篇文章就完整记录一下这套方案的硬件接法、协议设计、MicroPython主从站代码以及我在调试中踩过的坑项目不大但含金量不低适合正在搞RS485组网或者想用MicroPython做工业通信的朋友参考。1. 项目背景与整体方案设计1.1 为什么选RS485而不选RS232或CAN项目本身的需求是从机节点每隔一段时间上报一包环境数据温湿度、光照、设备状态主控节点统一汇总后上传到上位机。节点数量不多大概5到8个分布在一个厂房的几个房间里节点间距离从十几米到几十米不等。一开始我考虑过RS232但RS232是单端信号发送和接收各有独立的线用起来确实简单但抗干扰能力太差传输距离也就15米左右多点组网更是别想。CAN总线在汽车和工业控制里很强抗干扰和实时性都很好但需要一个CAN控制器很多单片机没有原生CAN外设MicroPython对CAN的支持深度也不如UART那么顺手入门成本高。RS485在这个场景几乎是标准答案。它是差分信号A和B两根线上的电压差代表逻辑电平共模干扰会被抵消掉所以抗干扰能力强焊接车间、电机附近这种电磁环境恶劣的地方也能稳定跑。RS485标准规定通信距离能达到1200米实际看波特率和线材几十到几百米的室内组网完全没压力。最重要的是单片机上的UART外设加一个便宜的收发器就能实现RS485通信MicroPython里直接操作现成的UART对象就行开发效率高。1.2 MAX13487自动换向带来的简化传统RS485方案里最常用的收发芯片是MAX485它有一个DE引脚控制驱动器使能有一个RE引脚控制接收器使能。半双工通信时发送数据前要把DE拉高发送完了再拉低DE、拉低RE或者直接把DE和RE接在一起用一个GPIO控制。这个方向切换用裸机C语言写倒还好但在MicroPython里就有点难受了因为MicroPython的解释执行有不确定性GPIO翻转的时机可能抖动一旦发送完还没来得及切换到接收从站回的数据就会丢。MAX13487最核心的特点就是把方向控制做进了芯片内部。它通过检测驱动器输入DI的电平变化来自动控制驱动器使能总线空闲时DI是空闲电平高电平芯片自动让驱动器处于禁用状态此时线路由接收器接管当检测到DI上出现起始位低电平时芯片在极短的时间内自动打开驱动器把数据发出去发送完成后又自动切回接收模式。这个特性带来的好处是实实在在的。首先MCU的GPIO少占用一个代码里完全不用管方向切换把UART当成普通的全双工串口用就行其次消除了软件切换方向带来的时序抖动问题MicroPython这种解释型语言也能放心用再者因为切换完全由硬件完成切换时间非常短可以支持更高的波特率。从数据手册来看MAX13487最高支持500kbps的速率室内环境跑个9600或者115200都绰绰有余。它还具备真正的失效保护功能也就是说总线开路、短路或者空闲时接收器输出确定的电平而不会乱跳这对空闲状态的总线稳定性很有帮助。另外它内部有热插拔保护电路节点带电插拔不容易损坏芯片。不过要提醒一下MAX13487是5V供电的芯片RO输出高电平接近5V接到3.3V的MCU比如ESP32引脚时务必确认引脚是否兼容5V输入。ESP32大部分GPIO是不耐5V的稳妥的做法是在RO到RX之间串联一个1kΩ电阻分压或者在RO上加一个3.3V稳压管做钳位。我实测下来简单串一个电阻就能稳定工作后面会细说。1.3 主从通信架构设计RS485总线上虽然可以挂多个节点但同一时刻只能有一个节点在发送数据这是由它的物理层特性决定的。所以实际项目里几乎都采用主从轮询的方式也就是主站Master依次给每个从站Slave发指令从站收到指令后回答数据谁拿到发送权谁才能往总线上写数据。这次项目的架构是这样设计的主站ESP32开发板负责定时轮询下辖的从站节点收到数据后通过串口或者WiFi上报。从站ESP32-C3或者ESP8266开发板接传感器等待主站指令收到对自己地址的查询帧后回传数据。总线所有节点通过两芯屏蔽双绞线手拉手连接总线两端各接一个120Ω终端电阻。通信流程说白了就是主站喊一圈名字每个被喊到的从站应答一声。主站广播一个查询帧帧里带着从站地址从站收到后判断地址是不是自己是就处理不是就忽略。协议采用1主N从、半双工、一问一答的方式简单可靠也不会产生总线冲突。我在协议设计上特意找了一个平衡点既要简单到MicroPython能轻松处理又要保证在噪声环境下能识别错误帧。前面加固定帧头中间字段定长末尾加CRC16校验这样解析逻辑清晰误码也能检测出来。2. 硬件准备与接线详解2.1 器件清单与选型说明这套方案用到的核心器件不多列个清单主控板ESP32开发板主站ESP32-C3开发板从站两者都原生支持MicroPythonUART资源也够用。RS485收发器MAX13487EESASOIC-8封装自动换向500kbps速率。注意别买成MAX13488那个是用于不同方向的芯片引脚定义和MAX13487有差异。两芯屏蔽双绞线若干用于总线连接。屏蔽层单点接地不接到设备端的信号地上。120Ω终端电阻两个焊接在总线两端的A、B之间。USB转RS485调试工具一个用于联调。我用的是最常见的CH340加SP485方案的小模块主要用来模拟主站或者从站验证链路。选型的时候我对比过MAX3485和MAX13487前者是3.3V供电虽然和MCU电平匹配但依然需要软件控制方向既然是MicroPython开发自动换向的优势太明显了所以最终选了MAX13487。如果你用3.3V供电的板子可以考虑带自动换向的3.3V型号不过市面上不太常见多半还是回到MAX3485手动切换的老路上去。2.2 芯片引脚定义与典型电路MAX13487是8脚封装引脚功能如下引脚名称功能说明1RO接收器输出接MCU的RX2RE#接收使能低电平有效。自动换向模式下直接接地3DE驱动器使能高电平有效。自动换向模式下悬空即可有些型号也可以直接接VCC按数据手册来4DI驱动器输入接MCU的TX5GND电源地6A总线非反相端7B总线反相端8VCC电源正极接5V旁边放0.1μF退耦电容典型接线示意图如下以ESP32为例MAX13487的VCC接5V电源GND和ESP32的GND共地。RO经1kΩ电阻接ESP32的RX引脚比如GPIO16如果不放心可以再并一个3.3V稳压管到地但实际上串联电阻已经够用。DI直接接ESP32的TX引脚GPIO17。RE#直接接地DE悬空。A和B接双绞线A接AB接B注意全总线统一不要有的节点A接B。这里有个容易忽略的细节RS485芯片的逻辑侧是5V电平的RO输出高电平时接近5V对3.3V的MCU来说超出了供电电压。长期超出数据手册绝对最大值容易损坏引脚所以RO串联电阻分压是必要的。1kΩ串在信号线上MCU引脚的输入阻抗很高实测分压后电平在3.3V左右能可靠识别高电平同时电流很小不会烧引脚。2.3 组网拓扑与终端电阻RS485组网最忌讳星型连接。正确的拓扑是一根主线从主站开始一个接一个地把所有节点并联在这根总线上每个节点的A/B线尽量短地接入主线像葡萄串一样。星型结构会让信号在分支末端反射长距离高速率时反射会叠加成误码。终端电阻的作用是吸收信号在总线末端产生的反射。在通信速率较高或者线路较长时信号到达导线末端会因为阻抗不匹配产生反射反射波和正向波叠加会让波形畸变导致接收端误判。标准做法是在总线最远的两端各接一个120Ω电阻。这里的两端是物理位置上的两端不是每个节点都接接多了会增加收发器负载每个节点处都接120Ω会直接把差分信号拉没。MAX13487内部有失效保护功能即使没有偏置电阻总线空闲时接收器输出也是确定的高电平所以这套方案里不接偏置电阻也能正常跑。如果现场干扰特别严重可以在主站端给A线上拉一个4.7kΩ电阻到5V给B线下拉一个4.7kΩ电阻到地这样总线空闲时A相对B有一个确定的正向压差抗干扰能力更好。接线顺序上我建议先把所有设备的A/B线都接好再统一检查极性最后上电。A/B接反的典型症状是通信完全不通或者主站发出去的命令从站能听到但老是乱码后面排查部分会详细展开。3. MicroPython程序实现主从通信3.1 通信协议帧格式设计通信协议是整套系统的灵魂。RS485本质上只负责把UART的字节流原样搬到总线上字节流里怎么组织数据完全由应用层协议决定。我设计的帧格式如下帧头两个固定字节0xAA 0x55用于接收端同步识别一条新帧的开始。地址1字节范围1~2470是广播地址。本项目不用广播从站地址从1开始排。功能码1字节。0x01表示查询数据。数据长度1字节表示后面Data字段的字节数。数据0~255字节。CRC162字节从地址字段开始到数据字段结束的CRC16-Modbus校验值低字节在前。主站发送的查询帧是固定长度的内容很短只有帧头、地址、功能码、长度为0、CRC几个字段。从站回的数据帧里Data字段放着传感器读数比如温度、湿度各占两个字节设备状态一个字节所以长度就是5。CRC16-Modbus是RS485通信中用的最多的校验方式工业Modbus-RTU协议用的就是它。它比简单的累加和校验强很多能检测出突发错误计算也不算复杂MicroPython跑起来没什么负担。下面是我封装好的CRC16计算函数def crc16_modbus(data: bytes) - int: crc 0xFFFF for byte in data: crc ^ byte for _ in range(8): if crc 0x0001: crc (crc 1) ^ 0xA001 else: crc 1 return crc使用的时候把从地址开始到数据结束的字节串传进去得到CRC值低字节在前放在帧尾。接收端对同样的范围重新算一次CRC和帧尾的CRC比对不一致就丢弃。帧头选择0xAA 0x55是有讲究的0xAA是101010100x55是01010101在二进制上刚好互补这种帧头在噪声环境下不容易被误识别。而且接收端同步的逻辑很简单连续收到0xAA 0x55就认为新帧开始了即使之前处于乱码状态也能在下一帧重新同步。3.2 主站程序编写主站的MicroPython程序逻辑比较直接核心是一个轮询循环遍历所有从站地址逐个发送查询帧并等待应答。先初始化UARTfrom machine import UART, Pin import time # ESP32的UART2TX17RX16 uart UART(2, baudrate9600, bits8, parityNone, stop1, txPin(17), rxPin(16), timeout200)配置里需要注意两点。第一波特率我选了9600这个速率在几十米的双绞线上非常稳抗干扰能力最强而且很多工业设备默认就是9600兼容性最好。如果追求速度115200也能跑但要求线材质量更好、走线更规整。第二UART的timeout参数单位是毫秒它决定了读不到数据时read操作的阻塞时间这个值要设得比从站响应时间大。主站发送查询帧def build_query_frame(addr: int, func: int 0x01) - bytes: frame bytearray() frame.append(0xAA) frame.append(0x55) frame.append(addr) frame.append(func) frame.append(0x00) # 数据长度查询帧没有数据 crc crc16_modbus(bytes(frame[2:])) frame.append(crc 0xFF) frame.append((crc 8) 0xFF) return bytes(frame)接收并解析从站响应def read_response(timeout_ms: int 200) - dict | None: start time.ticks_ms() buf bytearray() while time.ticks_diff(time.ticks_ms(), start) timeout_ms: chunk uart.read() if chunk: buf.extend(chunk) if len(buf) 4 and buf[0] 0xAA and buf[1] 0x55: # 尝试解析完整帧 msg parse_frame(bytes(buf)) if msg: return msg else: buf buf[1:] # 去掉一个字节继续找帧头 return None完整的主站轮询循环就是把这两步串起来遍历地址1到N每个地址发一帧等应答处理数据然后进入下一个周期。这里有一个关键经验轮询周期不能太短要给从站留出足够的处理时间。如果下一帧发得太快上一帧的总线冲突还没消散从站可能还在处理上一帧的数据新指令就到了半双工下很容易乱套。完整的轮询帧间隔我用的是每从站200ms也就是8个从站一轮下来差不多1.6秒对这个数据采集项目来说实时性足够。3.3 从站程序编写从站的MicroPython程序核心是循环读取UART数据按帧格式解析地址匹配后回发数据。from machine import UART, Pin import time uart UART(0, baudrate9600, bits8, parityNone, stop1, txPin(21), rxPin(20), timeout100) SLAVE_ADDR 1 def handle_frame(frame: bytes): # 校验CRC crc_received frame[-2] | (frame[-1] 8) crc_calc crc16_modbus(frame[2:-2]) if crc_received ! crc_calc: return False addr frame[2] func frame[3] if addr ! SLAVE_ADDR: return False if func 0x01: # 模拟传感器读数实际项目中换成读取ADC或I2C传感器 temp 25.3 humi 60.1 payload bytearray() payload.append(int(temp * 10) 0xFF) payload.append((int(temp * 10) 8) 0xFF) payload.append(int(humi * 10) 0xFF) payload.append((int(humi * 10) 8) 0xFF) payload.append(0x01) # 设备状态正常 resp bytearray() resp.append(0xAA) resp.append(0x55) resp.append(SLAVE_ADDR) resp.append(func) resp.append(len(payload)) resp.extend(payload) crc crc16_modbus(bytes(resp[2:])) resp.append(crc 0xFF) resp.append((crc 8) 0xFF) uart.write(bytes(resp)) return True主循环里做帧同步解析rx_buf bytearray() while True: data uart.read() if data: rx_buf.extend(data) # 查找帧头 while len(rx_buf) 2: if rx_buf[0] 0xAA and rx_buf[1] 0x55: break else: rx_buf.pop(0) # 至少要有帧头地址功能码长度才能判断总长 if len(rx_buf) 5: data_len rx_buf[4] total_len 5 data_len 2 if len(rx_buf) total_len: frame bytes(rx_buf[:total_len]) handle_frame(frame) del rx_buf[:total_len] time.sleep_ms(10)从站这里有个细节UART的read()在timeout时间里读不到数据会返回None所以主循环里加了个10ms的sleep避免空转消耗CPU太狠。MicroPython在ESP32-C3上跑这种循环10ms的轮询间隔完全跟得上9600波特率的节奏。如果你用115200帧间隔变短sleep可以改成2ms。3.4 半双工时序与几个关键经验参数半双工总线上的时序控制是RS485通信成败的核心。虽然是半双工但MAX13487自动换向帮我们省了方向控制的GPIO我们仍然需要在发送完后给总线留一点稳定时间让最后一位数据在总线上传完也让远端节点的自动换向逻辑完成切换。以9600波特率为例一个字节在8N1格式下要发10个bit也就是1.04ms。一帧查询帧总共9个字节2帧头1地址1功能码1长度2CRC2末尾大约9.4ms。从站收到后需要解析、组织数据、回发这个处理时间通常在几毫秒到几十毫秒之间。所以主站的接收超时时间设置成200ms很保守但很稳妥不会因为偶尔的处理延迟就误判为超时。关于波特率的选择我总结一下自己的经验50米以内的室内环境115200完全没问题超过100米或者过电机的强干扰区域建议降到9600或者19200稳定性优先。有些人喜欢一上来就设115200结果测试的时候隔三差五丢包排查了半天发现是线太差或者布线贴着动力线降到9600立马稳了。RS485的优势本来就在于抗干扰和高可靠没必要为了那点速度牺牲稳定性。4. 联调测试与故障排查实录4.1 先用USB转RS485工具验证链路在正式把主从站程序都跑起来之前强烈建议先用USB转RS485模块分两步验证链路。第一步只接一个主站和一个USB转RS485模块用电脑串口助手手动发送主站组装的查询帧看看能不能模拟出从站该有的响应逻辑。这个步骤主要是验证协议设计的合理性不用烧录从站程序。第二步把USB转RS485模块当成一个虚拟从站用电脑发数据验证主站的接收解析代码是否能正确处理真实总线上的数据。我调试的时候踩过一个比较典型的坑USB转RS485模块上电后它的A/B输出状态有时候会让总线处于不确定状态特别是当模块和实际从站都在总线上时多个收发器同时被使能会导致总线冲突表现为乱码。后来发现这类调试模块很多是带方向自动切换的但它切换的依据也是DI上的数据和MAX13487一样模块空闲时不会驱动总线所以问题不大。但要注意USB转RS485模块的VCC/GND和主站之间是否需要共地如果不共地差分信号虽然不要求共地但超过共模电压范围会烧芯片或导致通信异常实际使用中最好把GND连在一起。4.2 典型故障现象与原因分析把我在调试过程中遇到的一些典型问题整理成了一张排查表方便大家对照定位故障现象可能原因排查方向与解决办法完全不通主站发帧无任何响应A/B接反检查全总线A/B极性的统一性完全不通且主站报UART错误RX/TX接反核对MAX13487的DI接MCU的TXRO接MCU的RX能通但数据乱码波特率不一致检查主从站和调试工具的波特率设置能通但偶发乱码干扰、缺终端电阻、共地不良加终端电阻、检查屏蔽层接地、确认电源共地主站超时从站好像收到了但不回从站地址不匹配核对从站程序里的SLAVE_ADDR和主站轮询的地址多个节点时出现数据错乱总线拓扑有问题或帧间隔太短检查接线是否为菊花链增大轮询间隔短距离正常长距离时通时断终端电阻缺失或匹配不良确认总线两端各有一个120Ω终端电阻这里面电平和电平兼容的问题容易忽视。MAX13487的RO输出高电平接近5V接到ESP32的RX前串电阻这个前面提过。但还有另一种情况有些开发板的UART引脚内部有弱上拉电阻分压后高电平可能被拉低到2V以下导致高电平识别不了。如果串了1kΩ电阻后通信不稳定可以试试把电阻降到470Ω或者330Ω只要限流保护作用到位就行。4.3 实测中的独家技巧调试RS485时示波器和逻辑分析仪是最好用的工具。示波器看A和B之间的差分波形能直接看出信号幅度、上升沿质量和反射情况。如果波形在上升沿有明显的振铃说明终端电阻没接好或者分支线太长。没有示波器的话用带PWM捕获的逻辑分析仪也能凑合看但RS485是差分信号逻辑分析仪只能分别看A对地、B对地的波形参考意义有限。另一个独门技巧是用串口返回模式自测。很多USB转RS485模块支持自发自收也就是把模块的TX和RX短接然后电脑发什么就收什么。用这个功能可以快速判断模块本身是否正常。但要注意自发自收模式下模块的A/B内部是短路的还是不短路的不同模块不一样别拿这个结果去推断总线上的行为。实际项目中我还遇到过一种情况总线空载时波形正常一旦把设备全接上就出现偶发乱码。排查下来发现是有个从站节点没上电但它的MAX13487芯片还挂在总线上。MAX13487有热插拔保护这种情况下还算温和没有锁死总线。如果是老一些的MAX485芯片节点掉电后芯片引脚可能呈现低阻状态直接把总线拉死。所以从站节点如果可能断电最好在总线上加偏置电阻或者选用带热插拔保护的收发器MAX13487这一点做得不错。5. 项目扩展与后续设想5.1 从Modbus到更多节点的扩展路径这套主从通信协议其实已经是简化版的Modbus-RTU了。如果后续要把这个系统接入工业上位机、组态软件或者PLC直接把协议换成标准Modbus-RTU即可硬件的RS485链路完全复用Modbus-RTU的帧格式和我设计的协议在结构上高度相似区别主要在功能码的语义和帧头的处理方式上。标准Modbus-RTU没有帧头它靠帧间隔来区分一帧的开始和结束要求一帧之内字符间隔不能超过3.5个字符时间这在中断驱动的C语言程序里比较容易实现但在MicroPython的解释执行环境下要卡这么精准的时序就比较吃力了。所以MicroPython项目里自定义一套带帧头的协议反而更实际解析起来轻松很多。如果节点数量从8个扩展到30个以上还需要注意几个问题。第一每个节点的地址要重新规划确保不冲突。第二轮询周期会变长实时性要求高的场景得考虑用中断或者异步接收方式处理UART数据。第三总线的电气负载也要考虑虽然MAX13487是1/8单位负载理论上一根总线能挂256个节点但实际组网受线缆电阻、分布电容和终端匹配的影响超过50个节点后可靠性会下降。这种大节点场景一般要加RS485中继器把总线分成几段。5.2 结合USB Host与本地网关的设想这次项目用的是ESP32直接通过WiFi把数据上报到服务器不过RS485系统经常出现在一些封闭的局域网环境里设备不连外网这时候就需要一个本地网关来做数据汇聚和存储。我后来在另一个项目里尝试用支持USB Host的MicroPython固件跑在ESP32-S3上通过USB口挂U盘把RS485总线上采集到的数据实时写入U盘里相当于做了一个不依赖网络的本地数据记录仪。这个方案的思路是ESP32-S3作为主站定时轮询RS485总线上的从站节点把收到的数据同时写到两个地方一个通过WiFi发给内网服务器另一个写到U盘的CSV文件里。这样即使网络断了数据也不会丢事后还可以把U盘拔下来导出数据分析。支持USB Host的MicroPython固件现在已经有不少现成版本UART操作和标准MicroPython一致USB存储部分有对应的usbhost库实现起来不算复杂。5.3 几点实操感悟整个项目做下来我有一个比较深的感触RS485通信的稳定不取决于通信协议有多复杂而取决于硬件基础是否扎实。终端电阻有没有接对、A/B极性是否统一、节点之间是否共地、电源纹波大不大这些看起来不起眼的细节决定了系统是长期稳定运行还是三天两头出怪毛病。而MAX13487的自动换向功能帮我砍掉了最难调的方向切换时序问题特别是在用MicroPython这种脚本语言开发时这个优势被进一步放大了。最后再分享一个小技巧开发阶段可以把主站的轮询间隔调得短一点比如100ms用来快速暴露通信问题联调稳定后再根据实际需求把轮询间隔调到项目要求的周期。这样既能保证开发效率又不会因为通信压力过大而掩盖了潜在的硬件隐患。RS485这套东西方案成熟、资料多但自己动手把每一个细节都走一遍踩过的坑才会真正变成经验的积累。