MicroPython实战:用MAX13487实现RS485一主多从通信

发布时间:2026/9/9 4:10:38
MicroPython实战:用MAX13487实现RS485一主多从通信 前阵子做一个温室环境监测的小项目好几路传感器分布在四十多米长的大棚里Wi-Fi信号飘忽不定蓝牙距离又不够最后老老实实选了RS485总线。RS485这个几十年的老协议到现在依然是工业控制、楼宇自控、环境采集领域的主力原因就一个字皮实。更让我省心的是项目里用了MicroPython做逻辑验证配上MAX13487这种自动收发方向的收发器整个开发过程几乎不需要关心底层寄存器。这篇就完整复盘一下我是怎么用MicroPython驱动MAX13487实现一主多从通信的内容涵盖芯片选型、硬件接线、协议设计、完整代码以及几个调试过程中差点把我逼疯的坑。如果你是刚接触RS485的嵌入式开发者或者想用MicroPython快速做多机通信验证这篇文章可以让你少走很多弯路。1. 项目背景与通信方案选型1.1 为什么我的串口项目最终选了 RS485做多设备通信很多人第一反应是TTL串口直接怼懒得搞转换。两个设备之间距离很近、环境干净的场景用TTL没有问题。一旦设备分布范围超过几米甚至几十米或者现场有电机、变频器、继电器在频繁开关TTL这种单端电平的劣势就暴露得淋漓尽致电平摆幅固定共模干扰一来就容易误码地线电位差还会导致数据完全乱掉。RS485解决的就是这几个问题。它的物理层采用差分信号传输用A、B两根线之间的电压差表示逻辑0和1不再是某一根线相对于地的电平而是两根线之间的差模电压。这种方式天生就有很强的共模抑制能力干扰同时叠加在两根线上差模电压不会变接收端自然不容易解码错误。再加上RS485接口芯片的接收灵敏度一般在±200mV以内信号幅度还能保持在1.5V以上所以二三十米的距离跑个9600波特率非常轻松长距离应用场景下RS485几乎是默认选项。另一个选择RS485的原因是它支持多点组网。RS232只有一对收发线只能点对点RS485的收发器支持总线挂载标准的MAX13487可以挂128个节点普通项目里一主多从轻轻松松。虽然半双工意味着同一时刻只能有一个节点发送但在轮询机制下这种吞吐量对传感器采集、设备控制这类低频应用完全够用。1.2 MAX13487 相比 MAX485 的优势以及它在 MicroPython 场景下的意义提到RS485收发芯片大家最熟悉的是MAX485。MAX485需要MCU用两个引脚分别控制发送使能和接收使能发送数据前拉高DE发送完再拉低DE并拉高RE时序控制非常讲究。如果切换太早最后一个字节会被截掉一半切换太晚接收状态没建立又可能漏掉响应帧。在STM32这类用寄存器和硬件定时器的平台上还好处理但在MicroPython这种高层API环境下控制GPIO电平做精确时序切换就非常难受了。MAX13487最核心的改进就是内部集成了自动收发方向控制。它的引脚仍然有RO、DI、RE、DE但正常使用时不需要MCU去操作RE和DE芯片内部根据DI端是否有数据自动管理驱动器输出。这样从代码角度来看UART的TX引脚直接接DIRX引脚接RO剩下的交给芯片完全不用管方向切换。对于MicroPython开发者来说这等于把最麻烦的时序问题直接抹掉了。MAX13487还带有开路失效保护和总线短路保护。当总线开路、短路或总线空闲时接收端输出电平是确定的不会出现随机跳变导致UART收到一堆乱码。项目里我把一个节点断电后主站那边只是检测到超时不会收到乱帧排查起来干净利落。另外MAX13487内部还有热关断保护总线长时间短路时芯片温度升高到一定程度会自动关断输出对工业现场来说这个保护很重要。2. 硬件连接MAX13487 引脚与接线要点2.1 芯片引脚功能与自动收发原理MAX13487是标准8引脚SO封装引脚布局和MAX485基本一致。实际接线时重点关注以下几根VCC和GND芯片供电。MAX13487标准版本用5V供电也有3.3V的低压版本MAX13487E选型时看清后缀。我这边的开发板是3.3V逻辑但总线收发器这边直接用5V供电因为RS485差分信号电平范围更大抗干扰能力更好A、B引脚和MCU之间并不直接连所以电平匹配没问题。DI和RODI接MCU的UART_TXRO接MCU的UART_RX。这是芯片与MCU之间仅有的两根信号线数据方向由DI往A/B总线发送由A/B总线往RO接收。MAX13487自动收发逻辑就在这两根线内部完成当DI出现下降沿开始发送一帧数据时芯片自动将驱动器切入发送模式当最后一个停止位结束后芯片检测到总线空闲一段时间又自动切回接收模式。这个切换时间通常在几百纳秒到几微秒级别对9600波特率来说完全没有压力。A和B接RS485总线的两根差分线。注意A是非反相端B是反相端接线时整个网络保持一致如果某个节点A和B接反会导致该节点完全无法通信。RE和DEMAX13487的这两个引脚内部已经接好逻辑常规应用下直接悬空即可。我见过有些板子习惯性在RE和DE上做了跳线和上下拉电阻如果下拉电阻强行把DE拉低可能会干扰芯片自动收发逻辑实际使用时如果发现发不出去优先检查这个位置。表1 MAX13487关键引脚对照引脚名称连接目标说明1ROMCU_RX接收数据输出2RE悬空自动控制禁止外部强制拉低3DE悬空自动控制禁止外部强制拉低4DIMCU_TX发送数据输入5GND电源地与MCU逻辑地共地6A总线A非反相端7B总线B反相端8VCC5V供电注意丝印方向2.2 实际接线方案与终端电阻选择一个典型的一主两从RS485网络节点之间用双绞线连接A和B。双绞线的特征阻抗通常在120Ω左右为了消除信号在长线末端的反射标准做法是在总线物理两端各并联一个120Ω终端电阻。注意是“物理两端”不是“主站和最后一个从站”。如果主站恰好在一端从站依次挂下去那终端电阻就接在主站的A/B之间和最后一个从站的A/B之间如果主站在中间反而要让两端的从站各接一个。终端电阻的作用是让信号到达线缆末端时能量被电阻吸收而不是反射回来叠加在原始信号上。总线分支越长、波特率越高反射问题越严重。实际项目里如果只有两个节点且距离很短不接终端电阻也能工作但距离超过二三十米一定要把两个120Ω电阻加上。我踩过的坑是只在一端接了电阻结果30米线缆上出现明显的波形振铃误码率直接飙升。另外空闲状态下的总线电平也需要处理。RS485规定A高于B为逻辑1很多收发器在没有数据时总线处于高阻状态电平不确定。MAX13487虽然内部有失效保护在总线开路时输出高电平但当总线上挂了多个节点且线缆较长时仍然建议在总线两端额外加偏置电阻把空闲电平钳在确定状态。常用的做法是在A线上拉到VCCB线上拉到GND阻值选取范围在470Ω到10kΩ之间。这个偏置同时会提高接收端的抗干扰能力减少总线空闲时的误码。供电方面MAX13487的5V电源建议单独用稳压模块供电不要直接从MCU的3.3V LDO上拉。RS485总线一旦遭受外部干扰瞬态电流可能很大和MCU共用电源容易把逻辑电路搞复位。我项目里就是给每个节点单独配了一个B0505S隔离电源模块收发器电源和MCU电源之间用磁珠加电容做了隔离效果明显改善。如果预算紧张至少也要在VCC引脚附近并接一个0.1μF陶瓷电容和一个10μF电解电容保证电源纹波尽量小。3. 开发环境准备与 MicroPython UART 配置3.1 固件与开发板选择MicroPython生态里ESP32和RP2040是玩RS485最顺手的两个平台。ESP32自带多个UART外设硬件资源丰富固件稳定RP2040的UART也很干净核心板价格便宜。这次项目我用了ESP32主要原因是它除了跑RS485协议栈还要同时处理几个模拟量采集任务ESP32的CPU频率和内存空间更宽裕。固件方面直接在MicroPython官网下载对应开发板的官方固件即可。网上有时候能搜到一些第三方修改版比如支持USB Host的定制固件如果你准备同时挂U盘记录数据可以考虑这类变体。但对绝大多数RS485应用来说标准固件完全够用没必要为了花哨功能引入不稳定因素。烧录方式各家开发板有差异ESP32用esptool把固件写入0x1000地址RP2040直接拖拽UF2文件步骤网上都有这里不展开。3.2 UART 参数初始化的几个坑MicroPython里初始化UART非常直接但参数选择上有一些细节直接影响RS485通信质量。我实际用的是9600波特率8位数据位无校验1位停止位也就是常说的9600 8N1代码是这样写的from machine import UART, Pin import time # 根据自己板子实际引脚调整这里以ESP32 DevKitC为例 uart UART(1, baudrate9600, bits8, parityNone, stop1, tx17, rx16)波特率为什么选9600而不是115200RS485的传输距离和波特率成反比。9600波特率在普通双绞线上跑几百米都没问题115200虽然速度快但信号上升沿更陡对线缆分布电容、终端匹配和现场干扰都更敏感距离稍微一远就容易误码。如果是机柜内几米的短距离用115200也无妨一旦跨车间、跨楼层老老实实回到9600。另外有些RS485设备只支持固定波特率选型和联调前先确认两端都在同一速率。MicroPython的UART是异步收发调用uart.write()时函数并不会等所有字节从TX引脚送完才返回它只是把数据扔进发送缓冲区。对于MAX13487这种自动方向控制的芯片来说这个行为会造成一个隐蔽的问题如果写入数据后立刻调uart.read()而总线上从站的响应又回来得很快芯片可能还没完成从发送模式到接收模式的切换导致响应帧的前几个字节被吃掉。解决办法是在发送后加一个小的延时让芯片完成方向切换。这个时间怎么给一个字符在9600波特率下大约占1.04ms一帧数据如果是8字节整个发送过程约8.3ms我习惯在write()之后加time.sleep_ms(2)确保最后一个停止位已经稳定送出再给芯片几百微秒切到接收。如果你用更快的波特率这个延时可以适当缩短但不要完全去掉。读取端也很容易踩坑。MicroPython的uart.read(n)如果没有指定n会尝试读取接收缓冲区里的所有数据如果指定n它会一直等直到凑够n个字节或超时。RS485的响应帧长度不固定直接read(预期长度)容易卡死更好的做法是配合uart.any()做非阻塞读取循环轮询缓冲区。关于这段处理逻辑我在后面的代码实现部分会给出完整写法。4. 通信协议设计别让数据在总线上裸奔4.1 帧格式与地址分配RS485物理层只负责把字节从一端搬到另一端它不关心字节的含义。如果直接把原始数据扔到总线上主从之间很容易出现错帧、粘包、数据错位的问题。举个最简单的例子主站连续发送两条指令从站接收端还没来得及处理第一条第二条的数据已经追加到缓冲区里如果不做帧边界划分从站根本分不清哪个字节属于哪条指令。所以组网通信的第一步就是定义帧格式。我项目里用的是下面这套结构字节偏移字段名长度说明0FRAME_HEAD11帧头1固定0xAA1FRAME_HEAD21帧头2固定0x552ADDR1从站地址1~1273CMD1功能码4LEN1数据段长度5DATALEN数据段末尾CRC2CRC16校验帧头用两个字节0xAA 0x55目的是让接收端快速识别帧起始。地址字段直接对应从站编号主站发请求时填目标从站地址从站回复时填自己的地址。功能码表示这条指令要干什么比如0x01读传感器数据0x02控制继电器0x81表示读数据的响应0x82表示控制指令的响应最高位用来区分请求和响应从站回包时在请求功能码基础上置最高位这样主站收到回包后能立刻知道这是对哪条指令的应答。地址分配建议做成配置文件不要把地址写死在代码逻辑里。项目大了以后节点增多每个节点的地址、设备类型、寄存器映射都统一管理后续维护会轻松很多。对于支持拨码开关的硬件直接用硬件地址代码里读取引脚状态构建地址变量改地址不需要重新烧录固件。4.2 CRC16 校验实现数据在总线上传输电磁干扰、电平抖动都可能造成字节翻转。如果接收端不校验一个错字节可能让主站把错误数据当成真实数据轻则显示异常重则触发设备误动作。因此帧末尾一定要加校验字段。最简单的校验是累加和把前面所有字节加到一起取低8位。但在工业和强干扰场景下累加和会漏掉不少错误模式。更可靠的方案是CRC16。MicroPython的标准库binascii里自带crc_hqx()函数计算的是CRC-CCITT多项式0x1021初值0xFFFF用在RS485这类数据量不大的帧上非常合适。计算方式很简单从地址字节开始一直计算到数据段结束两个帧头字节不参与计算。这样可以避免帧头被干扰后仍然有可能被误判的问题。CRC结果按高位在前、低位在后的顺序附加到帧尾。import binascii def crc16(data): crc binascii.crc_hqx(data, 0xFFFF) return crc接收端的校验就是取出帧尾两字节和重新计算的结果比对不一致就丢弃整帧。这里有一个实际中的细节两端必须约定好CRC算的是哪些字节。发送端如果从帧头开始算接收端也必须从帧头开始算发送端如果跳过帧头接收端也要跳过。两边算法不一致联调时会出现正常数据却被校验失败的问题排查起来非常头疼。4.3 轮询与超时机制RS485是半双工总线同一时刻只能有一个节点占用总线发送数据所以必须要有一套介质访问规则。我的项目采用最经典的主从轮询机制主站按顺序向每个从站发送请求帧从站收到后立即回复主站收到回复则处理数据超时未收到回复则记录该从站通信失败继续轮询下一个节点。轮询周期要根据业务实时性要求来定。环境监测场景下传感器数据几秒钟更新一次完全够用轮询周期设为1秒每个从站的响应等待时间设为200毫秒整条总线最多挂十来个节点也能在一个周期内全部扫完。如果业务需要更快的响应速度可以缩短等待时间但要保证大于从站处理请求并组帧回包的最坏耗时否则主站容易误判超时。从站自身还要有防误响应机制。如果某条指令的CRC校验失败从站应当直接丢弃不能回复任何内容。如果从站错误地回复了主站和其他从站都会把这条乱帧当成真实数据总线立刻陷入混乱。这一点在从站代码里我会用代码逻辑强制实现。5. 主从通信代码实现与关键细节5.1 主站代码结构与过程先写主站。主站负责周期轮询所有从站发送请求帧、接收响应帧、校验CRC并打印解析结果。项目里主站往往会接显示屏或者上传云端代码里我把解析结果统一交给一个handle_report()函数处理实际项目只需要改这一个函数就行。import machine import time import binascii # 初始化UARTTX17RX16根据自己的板子调整 uart machine.UART(1, baudrate9600, bits8, parityNone, stop1, tx17, rx16) SLAVE_COUNT 2 SLAVE_ADDR_LIST [0x01, 0x02] TIMEOUT_MS 200 def crc16(data): return binascii.crc_hqx(data, 0xFFFF) def build_frame(addr, cmd, payload): frame bytes([0xAA, 0x55, addr, cmd, len(payload)]) bytes(payload) crc crc16(frame) frame bytes([(crc 8) 0xFF, crc 0xFF]) return frame def read_response(): buf bytearray() start_ms time.ticks_ms() while time.ticks_diff(time.ticks_ms(), start_ms) TIMEOUT_MS: if uart.any(): chunk uart.read(uart.any()) if chunk: buf.extend(chunk) # 至少收到帧头地址功能码长度2字节CRC 7字节 if len(buf) 7: break time.sleep_ms(2) return bytes(buf) def handle_report(addr, cmd, data): # 按业务需求处理解析后的数据 print(从站%d 功能码0x%02X 数据: %s % (addr, cmd, data.hex())) while True: for addr in SLAVE_ADDR_LIST: payload bytes([0x00]) # 示例读取指令数据段为空 frame build_frame(addr, 0x01, payload) uart.write(frame) time.sleep_ms(5) # 等待发送完成MAX13487自动切换回接收模式 resp read_response() if not resp: print(从站0x%02X 无响应记录故障 % addr) continue # 简单校验帧头与CRC if len(resp) 7 or resp[0] ! 0xAA or resp[1] ! 0x55: print(收到非法响应帧:, resp.hex()) continue resp_body resp[2:-2] if crc16(resp[2:-2]) ! (resp[-2] 8 | resp[-1]): print(CRC校验失败丢弃) continue handle_report(resp[2], resp[3], resp[5:5 resp[4]]) time.sleep_ms(1000)这里有一个细节值得专门解释帧头校验。主站判断响应是否合法第一步就是看前两个字节是否为0xAA 0x55。有些读者可能会问如果干扰把地址字节变成0xAA 0x55怎么办这里就要靠后面的CRC校验兜底。CRC漏检概率极低两个机制叠加起来几乎不会把错误数据交到业务层。5.2 从站状态机解析与响应从站代码相比主站复杂不少。从站不能像主站那样主动睡眠它要时刻监听总线判断有没有发往自己的合法指令。很多新手写从站时喜欢用uart.read()配合time.sleep()去阻塞等数据但这样会严重影响MCU处理其他任务。更好的方案是用uart.any()做非阻塞轮询把接收到的数据放进一个接收缓冲区再通过解析函数按帧提取。5.3 从站代码实现import machine import time import binascii uart machine.UART(1, baudrate9600, bits8, parityNone, stop1, tx17, rx16) MY_ADDR 0x01 rx_buf bytearray() def crc16(data): return binascii.crc_hqx(data, 0xFFFF) def process_rx(): global rx_buf # 查找帧头 head_idx -1 for i in range(len(rx_buf) - 1): if rx_buf[i] 0xAA and rx_buf[i1] 0x55: head_idx i break if head_idx -1: # 没找到帧头保留最后一个字节防止帧头跨包 if len(rx_buf) 1: rx_buf rx_buf[-1:] return # 丢弃帧头之前的垃圾数据 if head_idx 0: del rx_buf[:head_idx] # 需要至少 2帧头 1地址 1功能码 1长度 2CRC 7字节 if len(rx_buf) 7: return plen rx_buf[4] total 7 plen if len(rx_buf) total: return frame bytes(rx_buf[:total]) del rx_buf[:total] # 地址过滤 if frame[2] ! MY_ADDR: return # CRC校验 crc_recv frame[-2] 8 | frame[-1] crc_calc crc16(frame[2:-2]) if crc_recv ! crc_calc: print(CRC错误丢弃) return # 解析 cmd frame[3] data frame[5:5 plen] print(收到主站指令: 功能码0x%02X, 数据%s % (cmd, data.hex())) # 响应 payload bytes([0x11, 0x22, 0x33]) # 示例响应数据 resp bytes([0xAA, 0x55, MY_ADDR, cmd | 0x80, len(payload)]) payload crc crc16(resp[2:]) resp bytes([(crc 8) 0xFF, crc 0xFF]) uart.write(resp) time.sleep_ms(2) while True: if uart.any(): chunk uart.read(uart.any()) if chunk: rx_buf.extend(chunk) process_rx() # 其他业务逻辑可以放在这里 time.sleep_ms(1)从站的缓冲区解析逻辑看起来简单实际上已经处理了三个最麻烦的问题粘包、半包、帧头跨包。粘包是指主站连续发了两条指令第二次解析时缓冲区里可能出现两条帧半包是指网络延迟导致一帧数据分批到达帧头跨包是指上一包的最后一个字节恰好是下一帧的0xAA。缓冲区查找帧头、按总长度切帧的做法把这三种情况都覆盖到了。5.4 发送切换与接收时序处理对于MAX13487这种自动方向控制的芯片代码层面最重要的就是发送后的延时等待。我花了整整一个晚上才定位到这个问题主站发送请求后如果没有延时直接进入接收循环即使从站回的帧已经到了主站也会漏掉前面几个字节。后来逻辑分析仪一测才发现问题出在芯片自动收发切换上。用逻辑分析仪观察总线波形会发现MAX13487在DI输入最后一个停止位之后并不会立刻切回接收模式而是需要一小段稳定时间。如果MCU在这段时间内就开始读UART寄存器RO端输出的数据可能还没稳定读到的全是乱码或者直接丢字节。我在主站和从站的write()之后都加了time.sleep_ms(2)问题立刻消失。这个延时可以根据波特率调节9600波特率下2ms绰绰有余115200下0.5ms也够用但留着不碍事省得到时候换环境又出问题。另一个容易被忽略的细节是轮询节奏。主站发完一帧请求、等待响应、处理完响应后再轮询下一个从站这中间不要删掉time.sleep_ms(1)。哪怕只是1毫秒的空白也能让总线从上一帧的忙碌状态彻底回到空闲避免两个不同从站的响应帧在总线上首尾相连。工程上宁可慢一点也要保证每一帧都有清晰的时间边界。6. 调试实录与常见问题排查6.1 一主一从调试的流程和工具调试RS485通信最忌讳一上来就接整个网络。我第一次做多机通信时就犯了这个错误把所有从站全部接入总线出问题时根本分不清是哪个节点发错了数据排查效率极低。正确做法是先用“一主一从”的最小系统验证物理层和协议层确认无误后再挂第二个从站、第三个从站逐步扩大范围。调试工具方面USB转RS485模块是必需品。市面上常见的CH340或FT232芯片的USB转485模块都很实用把模块的A、B接到总线配合串口助手软件就能直接看到总线上所有节点的通信数据。我通常把USB转485模块当作“总线窃听器”挂在旁边观察波形和数据内容比用示波器便捷多了。串口助手要打开HEX显示看原始十六进制数据不要只看ASCII字符串否则帧边界和CRC校验值很难看出问题。6.2 常见问题速查表现象可能原因排查建议从站完全无响应主站发送后收不到任何数据A/B接反或收发器供电异常用USB转485模块挂在总线上观察是否有波形确认A/B方向统一测量VCC是否正常主站能收到数据但内容乱码波特率不一致或共模干扰过大确认两端均为9600 8N1检查总线屏蔽层是否单端接地地线是否连通发送正常但响应帧末尾少几个字节发送后未等待芯片切换回接收模式在write()后加2ms以上延时确保芯片稳定切换总线空闲时收到乱码总线出现振铃或偏置不足在总线两端加120Ω终端电阻必要时加偏置电阻多从站场景中一个从站异常导致总线瘫痪某个从站发送了乱帧或长时间占用总线逐个断开从站观察总线恢复情况检查该从站程序是否误触发发送逻辑从站重复响应或不响应特定指令缓冲区解析逻辑存在粘包处理bug将主机发送节奏调慢确认帧头解析逻辑是否遗漏半包场景6.3 几个容易踩的隐蔽坑先说说终端电阻。很多人在只有两个节点且距离不到十米时选择不接终端电阻实测发现也能通信但这会让你误以为这个项目“不需要终端电阻”。等设备正式安装在现场线缆走向和测试环境完全不同反射问题立刻爆发。我的建议是不管距离多近只要波特率超过9600或线缆长度超过十米终端电阻一定接上。如果距离很短终端电阻可以只在一端接两端都接也没问题不会产生明显信号衰减。然后是屏蔽层的接地问题。RS485用双绞线时屏蔽层接地的位置有讲究。屏蔽层如果两端都接地会因为地电位差产生环流反而引入干扰。正确做法是单端接地一般选择在主站一侧接地。如果现场有强电磁干扰源可以先用一段屏蔽层不接地的双绞线测试再用单端接地的方案对比效果差异非常明显。还有一个非常容易被忽略的坑是热插拔。虽然MAX13487本身有热插拔保护和失效保护但在总线正在通信时直接插拔某个节点的A/B线会产生非常大的瞬态干扰可能导致总线上其他节点同时触发接收错误。现在我的项目里所有从站接线端子都做了明确标识运维人员必须先断电再插拔总线避免通信中直接操作。最后说说从站的供电问题。我之前做一个环境采集节点从站程序在调试台上一切正常装到现场后每隔几分钟就掉线。排查到最后发现从站MCU用的是和传感器共用的3.3V LDO传感器瞬间启动电流把MCU电压拉到3.0V以下导致UART外设寄存器复位通信中断。后来我把从站供电改成5V宽压输入MCU单独用DCDC降压供电问题彻底消失。RS485总线通信对供电稳定性其实非常敏感MCU电源纹波大或者瞬态跌落都会直接反映在误码率上。调试这类多机通信我自己最大的体会是先保证一帧数据能干净地跑通再加业务逻辑。RS485本身不难难的是把“发送”、“接收”、“时序”、“异常处理”这几个环节都考虑周全。如果你现在正在调试一主多从通信建议不要急着把所有功能写进代码先把主从点对点通信跑通确认每一帧都能稳定收发再逐步扩展节点数量这个项目基本就成功一大半了。