🔌 ESP32 RS485通信实战:Modbus工业协议从入门到设备互联

📅 2026-06-02 · 📂 硬件编程 · ⏱ 阅读约 9 分钟

📺 B站推荐视频:单片机应用实践篇之RS485通信及MODBUS通信协议 | 金善愚老师主讲

📖 概述

RS485 是工业现场最主流的串行通信标准,采用差分信号传输(A/B双线),抗共模干扰能力极强,最大传输距离可达 1200 米,支持最多 32 个节点挂载在同一条总线上。上至PLC、变频器,下至温湿度传感器、电表,几乎所有的工业设备都配备 RS485 接口。

在 RS485 物理层之上,运行着工业界的事实标准协议——Modbus。它采用简单高效的主从架构:主机发出查询帧(功能码+寄存器地址),从机解析后返回数据。上一讲我们学了 CAN总线——如果说 CAN 是汽车和机器人的「神经系统」,那么 RS485+Modbus 就是工厂自动化和楼宇控制的「循环系统」。

今天我们用 ESP32 外接一颗 MAX485 收发芯片,亲手搭建一个 Modbus 主从通信系统——主机读取从机的温湿度传感器数据,打通从「单片机实验」到「工业设备互联」的关键一步。

💡 为什么学 RS485/Modbus? 国内 90% 以上的工业传感器、PLC、电表、变频器都通过 RS485 Modbus 通信。学会它,你就能让 ESP32 与真实工业设备对话——这是从创客走向工程师的必经之路。

🛒 物料清单

物料型号/参数数量参考价格
主控板ESP32 DevKit V1 (WROOM-32)2¥25 ×2
RS485 收发器MAX485 模块 (5V供电, SOP-8)2¥5 ×2
温湿度传感器DHT22 (AM2302) 精度 ±0.5℃1¥12
OLED显示屏SSD1306 0.96寸 128×64 I2C1¥10
120Ω 终端电阻120Ω ±5%, 1/4W 直插2¥0.1 ×2
双绞线22AWG 双绞信号线(可用网线替代)~3m¥5
面包板 + 杜邦线830孔 + 公母各20根2套¥15 ×2
合计约 ¥112
选型提醒: MAX485 是 5V 器件,但信号电平与 ESP32 的 3.3V 兼容(逻辑高电平阈值 ≥2.0V)。如果追求完美,可以用 SP3485(3.3V 版本),接线完全一样。另外 必须在 A/B 线两端各并一个 120Ω 终端电阻,否则高速/长距离通信会因信号反射而出错。

🔌 接线图

【主机端 — ESP32 #1 + MAX485 + SSD1306】
──────────────────────────────────────────
ESP32 #1         →  MAX485 模块
3.3V             →  VCC (部分MAX485模块支持3.3V)
5V (VIN)         →  VCC (若模块仅支持5V)
GND              →  GND
GPIO 17 (TX2)    →  DI (驱动输入)
GPIO 16 (RX2)    →  RO (接收输出)
GPIO 5           →  DE + RE (短接,发送/接收使能)

ESP32 #1         →  SSD1306 OLED
3.3V             →  VCC
GND              →  GND
GPIO 21 (SDA)    →  SDA
GPIO 22 (SCL)    →  SCL

【从机端 — ESP32 #2 + MAX485 + DHT22】
──────────────────────────────────────────
ESP32 #2         →  MAX485 模块
3.3V/5V          →  VCC
GND              →  GND
GPIO 17 (TX2)    →  DI
GPIO 16 (RX2)    →  RO
GPIO 5           →  DE + RE

ESP32 #2         →  DHT22
3.3V             →  VCC
GND              →  GND
GPIO 4           →  DATA

【RS485 总线连接】
──────────────────────────────────────────
主机 MAX485 A  ───┬─── 从机 MAX485 A
                  │
                  ├── 120Ω 终端电阻 ──┐
                  │                   │
主机 MAX485 B  ───┴─── 从机 MAX485 B ─┘ (远端120Ω)
        

MAX485 模块的 DE(Driver Enable,发送使能)和 RE(Receiver Enable,接收使能)短接后由 GPIO5 控制——高电平=发送模式,低电平=接收模式。RS485 是半双工的,同一时刻只能发送或接收,这在代码中通过切换 DE/RE 引脚来实现。

💻 基础代码:Modbus RTU 主从通信

以下代码使用 ArduinoModbus 库(Arduino IDE 库管理搜索安装),实现主机通过 RS485 读取从机的 DHT22 温湿度数据,并在 OLED 上显示。

从机代码(ESP32 #2 + DHT22)

// ===== Modbus RTU 从机 — ESP32 + MAX485 + DHT22 ===== #include <ArduinoRS485.h> #include <ArduinoModbus.h> #include <DHT.h>#define DHTPIN 4 #define DHTTYPE DHT22 #define DE_RE_PIN 5 DHT dht(DHTPIN, DHTTYPE); void setup() { Serial.begin(115200); dht.begin(); // 初始化 RS485:Serial2, DE/RE引脚, 波特率9600 RS485.begin(9600, 16, 17, DE_RE_PIN); // 参数: 波特率, RX引脚, TX引脚, 收发使能引脚// 启动 Modbus RTU 从机,地址=1 ModbusRTUServer.begin(1, 9600); // 配置保持寄存器: 地址0=温度(×10), 地址1=湿度(×10) ModbusRTUServer.configureHoldingRegisters(0, 2); Serial.println("Modbus Slave #1 Ready"); } void loop() { static unsigned long lastRead = 0; // 每2秒更新一次寄存器数据if (millis() - lastRead > 2000) { lastRead = millis(); float t = dht.readTemperature(); float h = dht.readHumidity(); if (!isnan(t) && !isnan(h)) { ModbusRTUServer.holdingRegisterWrite(0, (int)(t * 10)); ModbusRTUServer.holdingRegisterWrite(1, (int)(h * 10)); Serial.printf("更新寄存器: T=%.1f℃ H=%.1f%%\n", t, h); } } // 轮询 Modbus 请求(必须频繁调用) ModbusRTUServer.poll(); }

主机代码(ESP32 #1 + SSD1306 OLED)

// ===== Modbus RTU 主机 — ESP32 + MAX485 + SSD1306 ===== #include <ArduinoRS485.h> #include <ArduinoModbus.h> #include <Wire.h> #include <Adafruit_SSD1306.h>#define DE_RE_PIN 5 Adafruit_SSD1306 display(128, 64, &Wire, -1); void setup() { Serial.begin(115200); display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.clearDisplay(); display.setTextSize(1); display.setTextColor(SSD1306_WHITE); RS485.begin(9600, 16, 17, DE_RE_PIN); ModbusRTUClient.begin(9600); // 设置超时时间(ms) ModbusRTUClient.setTimeout(500); Serial.println("Modbus Master Ready"); } void loop() { static unsigned long lastPoll = 0; if (millis() - lastPoll > 2000) { lastPoll = millis(); // 读取从机#1的保持寄存器 0-1 (温度×10, 湿度×10)if (!ModbusRTUClient.requestFrom(1, HOLDING_REGISTERS, 0, 2)) { Serial.print("Modbus 读取失败: "); Serial.println(ModbusRTUClient.lastError()); displayOLED(0, 0, true); return; } int rawTemp = ModbusRTUClient.read(); int rawHum = ModbusRTUClient.read(); float temp = rawTemp / 10.0; float hum = rawHum / 10.0; Serial.printf("从机#1 → 温度: %.1f℃, 湿度: %.1f%%\n", temp, hum); displayOLED(temp, hum, false); } } void displayOLED(float temp, float hum, bool error) { display.clearDisplay(); display.setCursor(0, 0); display.println("Modbus RTU Master"); display.println("----------------"); if (error) { display.println("Slave #1: OFFLINE!"); } else { display.printf("Temp: %.1f C\n", temp); display.printf("Hum : %.1f %%\n", hum); } display.display(); }
🔧 代码要点: Modbus 寄存器值用整数传输(避免浮点精度问题),约定温度×10、湿度×10 存入寄存器。主机读取后除以10还原。从机的 poll() 必须在主循环中频繁调用,否则会丢帧。DE/RE 引脚的自动切换由 ArduinoRS485 库内部完成,无需手动操作。

🚀 进阶应用

1. 多从机轮询网络

RS485 总线上最多可挂 32 个节点,每个从机配置不同的 Modbus 地址(1-247)。主机只需改变 requestFrom() 的第一个参数即可切换目标:

// 轮询多个从机 int slaveIDs[] = {1, 2, 3}; for (int id : slaveIDs) { if (ModbusRTUClient.requestFrom(id, HOLDING_REGISTERS, 0, 2)) { float t = ModbusRTUClient.read() / 10.0; float h = ModbusRTUClient.read() / 10.0; Serial.printf("从机#%d: %.1f℃ %.1f%%\n", id, t, h); } delay(50); }

2. 功能码实战:读写线圈和寄存器

Modbus 定义了标准功能码,最常用的四种:

功能码名称作用典型应用
01读线圈读取开关量输出读继电器状态
03读保持寄存器读取16位数据读温度/压力值
05写单线圈控制单个开关开关继电器
06写单寄存器写入16位数据设定目标温度
// 主机控制从机#1的继电器(线圈地址0) ModbusRTUClient.coilWrite(1, 0, true); // 闭合继电器 delay(2000); ModbusRTUClient.coilWrite(1, 0, false); // 断开继电器// 主机写从机#1保持寄存器(设定目标温度 35.5℃ → 寄存器值355) ModbusRTUClient.holdingRegisterWrite(1, 10, 355);

3. Modbus RTU 帧格式详解

理解底层帧格式有助于用逻辑分析仪调试。一帧 Modbus RTU 报文的结构如下:

主机查询帧(读保持寄存器 0-1,从机地址=1):
┌────────┬──────┬──────────┬──────────┬──────────┬──────────┐
│ 地址   │ 功能码│ 起始地址  │ 寄存器数  │ CRC校验   │ 帧间隔   │
│ 0x01   │ 0x03 │ 0x00 0x00│ 0x00 0x02│ 0xC4 0x0B│ ≥3.5字符 │
└────────┴──────┴──────────┴──────────┴──────────┴──────────┘

从机响应帧(温度=25.0℃=0xFA, 湿度=60.0%=0x258):
┌────────┬──────┬──────┬──────────────┬──────────┬──────────┐
│ 地址   │ 功能码│ 字节数│ 数据          │ CRC校验   │ 帧间隔   │
│ 0x01   │ 0x03 │ 0x04 │0x00 0xFA 0x02│ 0x?? 0x??│ ≥3.5字符 │
│        │      │      │      0x58     │           │          │
└────────┴──────┴──────┴──────────────┴──────────┴──────────┘
        

4. WiFi 网关:RS485 → MQTT 上云

结合第5/6篇 WiFi 和第5/22篇 MQTT 知识,将 RS485 网络采集的工业数据推送到云端:

// RS485-MQTT 网关:轮询从机 → 发布到 MQTT #include <WiFi.h> #include <PubSubClient.h> WiFiClient wifiClient; PubSubClient mqtt(wifiClient); void uploadToCloud(int slaveID, float temp, float hum) { char topic[64], payload[128]; sprintf(topic, "factory/rs485/slave%d", slaveID); sprintf(payload, "{\"temp\":%.1f,\"hum\":%.1f}", temp, hum); mqtt.publish(topic, payload); Serial.printf("MQTT发布: %s → %s\n", topic, payload); }

🎯 实战场景

场景一:机房环境监控系统

需求: 数据中心机房有 8 个温湿度采集点,需要集中监控。布线距离 50-80 米,I2C/SPI 完全不可行。

方案: 每个采集点用 ESP32 + DHT22 + MAX485 做 Modbus 从机,手拉手串联在 RS485 总线上。主机 ESP32 轮询 8 个从机,OLED 本地显示 + WiFi 推送到云平台。RS485 1200 米的通信距离轻松覆盖整个机房楼层。

物料扩展: 工业场景建议升级为 SHT30(I2C接口,精度 ±0.3℃),配合防水外壳和 24V 供电(加 DC-DC 降压模块)。

联动模块: HC-SR04 超声波做机柜门禁检测,FreeRTOS 多任务分离采集和通信任务,低功耗模式做电池供电节点。

场景二:智能农业大棚控制器

需求: 大棚内分布多个土壤湿度传感器、CO₂传感器和遮阳帘电机,需要总线式集中控制。

方案: RS485 作为大棚内部通信骨干,各传感器节点通过 Modbus 上报数据。主机根据土壤湿度自动通过 Modbus 写线圈控制电磁阀(灌溉)和电机(卷帘)。结合 PID 温控实现温室环境闭环调节。

场景三:工业电表数据采集

需求: 工厂配电柜里多个智能电表通过 RS485 输出电压、电流、功率因数,需要统一采集上传。

方案: ESP32 做 Modbus 主机,轮询各电表的电压(寄存器 0x0000)、电流(0x0008)、有功功率(0x000C)等标准寄存器。配合 HTTP 客户端推送到能耗管理平台。这是典型的工业物联网(IIoT)数据采集架构。

⚠️ 常见问题

❌ 通信完全不通,主机一直超时
先做硬件排查:①万用表量 A/B 线间直流电压——空闲时应为 0.2V~0.3V(差分偏置);②检查 120Ω 终端电阻是否两端都接了;③检查 DE/RE 引脚接线(注意有些 MAX485 模块 DE 和 RE 是分开的,必须短接或分别控制);④确认 A 接 A、B 接 B,不要交叉。如果以上都对,用 USB 转 RS485 工具 + Modbus Poll 软件单独测试从机是否正常。
❌ 偶尔能读到数据,但经常丢包
典型症状:①波特率不匹配——主从必须严格一致(9600 是最稳妥的起点);②没有终端电阻或只接了一端——挂上示波器看 A/B 波形,有振铃/反射就是缺电阻;③总线过长或分支线(stub)太长——RS485 要求菊花链拓扑,分支线不超过 30cm;④双绞线质量差——用网线里的双绞线对比普通杜邦线,效果天差地别。
❌ Modbus 返回错误码 0x02(非法数据地址)
功能码+错误码 0x83 表示请求的寄存器地址超出范围。检查从机的 configureHoldingRegisters() 配置——寄存器起始地址和数量是否覆盖了主机的请求范围。Modbus 从机地址从 0 开始,与 PLC 的 40001 等编号体系不同,注意转换。
❌ 多个从机同时在线时通信混乱
Modbus 是严格的主从协议——从机从不主动发送,只在收到属于自己的查询帧后才响应。如果出现乱码/冲突:①确认所有从机地址唯一(不要有重复 ID);②帧与帧之间必须保持 ≥3.5 字符时间的静默(9600bps 约 3.6ms),ArduinoModbus 库自动处理,但自己写裸协议时必须注意;③检查总线偏置电阻——某些 RS485 中继器或隔离器需要额外偏置。
❌ 刷入代码后 ESP32 不断重启
常见原因:MAX485 模块从 ESP32 的 3.3V 引脚取电导致供电不足(MAX485 峰值电流 ~1mA,但模块上可能有其他元件)。解决方案:从 VIN(5V)或外部电源给 MAX485 供电。另外 DHT22 和 MAX485 共用 GPIO 时要检查引脚冲突。

📊 总结

RS485 + Modbus 是工业通信的「普通话」——学会了它,你的 ESP32 就不再是孤立的开发板,而是一台能与 PLC、变频器、传感器、电表对话的工业级数据终端

✅ 理解 RS485 差分传输的物理优势(抗干扰、长距离、多节点)
✅ 掌握 Modbus RTU 主从通信编程(ArduinoModbus 库)
✅ 学会硬件报文过滤、多从机轮询、WiFi 网关等进阶用法
✅ 具备排查 RS485 通信故障的调试思路(示波器/逻辑分析仪/ModbusPoll)

下一讲,我们将进入蓝牙 Mesh 组网——用 ESP32 搭建去中心化的低功耗传感器网络,敬请期待!