ESP32 Modbus RTU转MQTT网关实战:RS485数据采集上传 远程控制OTA与工业级稳定性完整教程

免费免费技术资料

这篇内容可直接阅读,适合用于基础学习和搜索引流。

本文目录
  1. 1. 一、方案概述
  2. 2. 二、硬件准备
  3. 3. 三、开发环境与库安装
  4. 4. 四、基础版:Modbus RTU数据采集并发布MQTT
  5. 5. 五、工业级增强版
  6. 6. 六、完整的工业级网关代码框架
  7. 7. 七、MQTT服务器搭建与数据查看
  8. 8. 八、常见问题与排查
  9. 9. 九、进阶扩展
  10. 10. 十、学习资源
  11. 11. VIP专属:ESP32 Modbus RTU转MQTT网关代码包

ESP32作为一款性价比极高的物联网微控制器,内置WiFi和蓝牙,配合Modbus RTU主机库和MQTT客户端库,可以轻松实现RS485设备数据采集并上传到MQTT服务器。本文详细介绍如何用ESP32搭建Modbus RTU转MQTT网关,包括硬件接线、库安装、Modbus数据采集、MQTT发布订阅、OTA升级、断线重连以及完整的工业级网关代码。

一、方案概述

1.1 为什么用ESP32做Modbus网关

  • 成本极低:ESP32开发板仅20-30元,RS485模块5-10元,整体成本远低于商业网关
  • 内置WiFi:无需额外网络模块,直接连接WiFi上传数据
  • 性能充足:双核240MHz,520KB SRAM,可同时处理Modbus采集和MQTT通信
  • Arduino生态:丰富的库支持,ModbusMaster、PubSubClient等成熟库直接可用
  • 支持OTA:可远程升级固件,无需现场维护
  • 多路串口:ESP32有3路硬件串口,可同时连接多条RS485总线

1.2 系统架构

┌──────────────────────────────────────────────────────────────┐
│                    MQTT Broker(服务器)                       │
│              EMQX / Mosquitto / 阿里云IoT                     │
└───────────────────────────┬──────────────────────────────────┘
                            │ MQTT (WiFi)
┌───────────────────────────▼──────────────────────────────────┐
│                    ESP32 网关                                  │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │  WiFi管理    │  │ MQTT客户端  │  │  Modbus RTU主站     │  │
│  │ (连接/重连)  │  │(PubSubClient)│  │  (ModbusMaster)    │  │
│  └─────────────┘  └──────┬──────┘  └──────────┬──────────┘  │
└───────────────────────────┼─────────────────────┼────────────┘
                            │                     │ UART2 (TX2/RX2)
                            │                     ▼
                            │              ┌──────────────┐
                            │              │ RS485模块    │
                            │              │ (MAX485/SP3485)│
                            │              └──────┬───────┘
                            │                     │ A/B
                            │              ┌──────▼───────┐
                            │              │  RS485总线    │
                            │              │  ┌────────┐  │
                            │              │  │温湿度   │  │
                            │              │  │传感器   │  │
                            │              │  └────────┘  │
                            │              │  ┌────────┐  │
                            │              │  │电力仪表 │  │
                            │              │  └────────┘  │
                            │              └──────────────┘

二、硬件准备

2.1 所需材料

  • ESP32开发板:推荐ESP32-WROOM-32或ESP32-S3
  • RS485模块:MAX485、SP3485或带隔离的RS485模块(推荐工业级带隔离)
  • 杜邦线:若干
  • 120Ω终端电阻:总线两端各一个(长距离必需)
  • Modbus RTU设备:温湿度传感器、电力仪表等(测试用)
  • 5V/3.3V电源:给ESP32和RS485模块供电

2.2 接线说明

ESP32引脚RS485模块说明
GPIO17 (TX2)DIESP32发送→RS485输入
GPIO16 (RX2)RORS485输出→ESP32接收
GPIO4DE/RE方向控制(高=发送,低=接收)
3.3VVCC电源(注意模块电压,有些是5V)
GNDGND共地
ARS485 A线(接设备A)
BRS485 B线(接设备B)

注意事项

  • RS485模块如果是5V供电,DI引脚需要电平转换(ESP32是3.3V),或直接用3.3V兼容的模块
  • DE和RE引脚短接在一起,由ESP32一个GPIO控制方向
  • 总线两端必须接120Ω终端电阻(A-B之间)
  • 工业环境推荐使用带隔离的RS485模块,防止地环路损坏ESP32
  • 多条总线可使用ESP32的UART1和UART2分别连接

三、开发环境与库安装

// Arduino IDE中安装ESP32开发板支持:
// 文件 → 首选项 → 附加开发板管理器网址:
// https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
// 工具 → 开发板 → 开发板管理器 → 搜索"ESP32" → 安装

// 需要安装的库(工具 → 管理库 → 搜索):
// 1. ModbusMaster by Doc Walker (4-20ma)
//    - Modbus RTU主站库,支持全部功能码
// 2. PubSubClient by Nick O'Leary
//    - MQTT客户端库,轻量稳定
// 3. ArduinoJson by Benoit Blanchon
//    - JSON序列化/反序列化(可选,用于结构化数据)
// 4. WiFiManager by tzapu (可选)
//    - WiFi配置门户,无需硬编码WiFi密码

// PlatformIO用户在platformio.ini中添加:
// [env:esp32dev]
// platform = espressif32
// board = esp32dev
// framework = arduino
// lib_deps =
//     4-20ma/ModbusMaster@^2.0.1
//     knolleary/PubSubClient@^2.8
//     bblanchon/ArduinoJson@^6.21.0

四、基础版:Modbus RTU数据采集并发布MQTT

// esp32_modbus_mqtt_basic.ino - 基础版Modbus RTU转MQTT网关
#include <WiFi.h>
#include <PubSubClient.h>
#include <ModbusMaster.h>

// ====== 配置区 ======
const char* WIFI_SSID = "your_wifi_ssid";
const char* WIFI_PASSWORD = "your_wifi_password";

const char* MQTT_SERVER = "broker.emqx.io";  // MQTT服务器地址
const uint16_t MQTT_PORT = 1883;              // MQTT端口
const char* MQTT_CLIENT_ID = "esp32-gateway-001";
const char* MQTT_USER = "";                   // MQTT用户名(无则留空)
const char* MQTT_PASSWORD = "";               // MQTT密码(无则留空)
const char* MQTT_TOPIC = "modbus/gateway/001/data";  // 发布主题

// Modbus配置
#define MODBUS_BAUD 9600          // 波特率
#define MODBUS_SLAVE_ID 1         // 从站ID
#define MODBUS_RX_PIN 16          // RX引脚
#define MODBUS_TX_PIN 17          // TX引脚
#define MODBUS_DE_PIN 4           // DE/RE控制引脚

// 采集间隔
const uint32_t POLL_INTERVAL = 5000;  // 5秒采集一次

// ====== 全局对象 ======
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
ModbusMaster node;  // Modbus主站对象

// ====== Modbus方向控制回调 ======
// ModbusMaster库需要一个函数来控制RS485的收发方向
void modbusPreTransmission() {
  digitalWrite(MODBUS_DE_PIN, HIGH);  // 切换到发送模式
  delayMicroseconds(10);              // 等待RS485芯片切换
}

void modbusPostTransmission() {
  delayMicroseconds(10);              // 等待最后一个字节发送完毕
  digitalWrite(MODBUS_DE_PIN, LOW);   // 切换到接收模式
}

// ====== WiFi连接 ======
void connectWiFi() {
  Serial.print("连接WiFi: ");
  Serial.println(WIFI_SSID);

  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi已连接");
  Serial.print("IP地址: ");
  Serial.println(WiFi.localIP());
}

// ====== MQTT连接 ======
void connectMQTT() {
  while (!mqtt.connected()) {
    Serial.print("连接MQTT服务器...");

    // 连接MQTT(带遗嘱消息:离线时通知)
    bool connected;
    if (strlen(MQTT_USER) > 0) {
      connected = mqtt.connect(
        MQTT_CLIENT_ID, MQTT_USER, MQTT_PASSWORD,
        "modbus/gateway/001/status", 1, true, "offline"
      );
    } else {
      connected = mqtt.connect(
        MQTT_CLIENT_ID,
        NULL, NULL,
        "modbus/gateway/001/status", 1, true, "offline"
      );
    }

    if (connected) {
      Serial.println("已连接");
      mqtt.publish("modbus/gateway/001/status", "online", true);  // 上线通知
    } else {
      Serial.print("失败,rc=");
      Serial.print(mqtt.state());
      Serial.println(",5秒后重试");
      delay(5000);
    }
  }
}

// ====== 读取Modbus设备数据 ======
struct SensorData {
  float temperature;   // 温度
  float humidity;      // 湿度
  uint16_t status;     // 状态字
  bool valid;          // 数据是否有效
};

SensorData readModbusData() {
  SensorData data = {0, 0, 0, false};

  // 读取保持寄存器0-2(温度、湿度、状态)
  uint8_t result = node.readHoldingRegisters(0, 3);

  if (result == node.ku8MBSuccess) {
    // 读取成功,从响应缓冲区获取数据
    uint16_t rawTemp = node.getResponseBuffer(0);  // 温度原始值
    uint16_t rawHum = node.getResponseBuffer(1);   // 湿度原始值
    data.status = node.getResponseBuffer(2);       // 状态字

    // 转换为实际值(假设分辨率0.1)
    data.temperature = rawTemp / 10.0;
    data.humidity = rawHum / 10.0;
    data.valid = true;

    Serial.printf("温度: %.1f°C, 湿度: %.1f%%, 状态: %d\n",
                  data.temperature, data.humidity, data.status);
  } else {
    Serial.printf("Modbus读取失败,错误码: %d\n", result);
  }

  // 清空响应缓冲区,准备下一次读取
  node.clearResponseBuffer();

  return data;
}

// ====== 发布数据到MQTT ======
void publishData(const SensorData& data) {
  // 构建JSON消息
  char payload[128];
  snprintf(payload, sizeof(payload),
    "{\"temperature\":%.1f,\"humidity\":%.1f,\"status\":%d,\"ts\":%lu}",
    data.temperature, data.humidity, data.status, millis());

  // 发布到MQTT(QoS=0,retain=false)
  mqtt.publish(MQTT_TOPIC, payload);
  Serial.printf("已发布到 %s: %s\n", MQTT_TOPIC, payload);
}

// ====== setup ======
void setup() {
  Serial.begin(115200);
  delay(100);

  // 初始化RS485方向控制引脚
  pinMode(MODBUS_DE_PIN, OUTPUT);
  digitalWrite(MODBUS_DE_PIN, LOW);  // 默认接收模式

  // 初始化Modbus(使用UART2)
  Serial2.begin(MODBUS_BAUD, SERIAL_8N1, MODBUS_RX_PIN, MODBUS_TX_PIN);
  node.begin(MODBUS_SLAVE_ID, Serial2);

  // 注册方向控制回调
  node.preTransmission(modbusPreTransmission);
  node.postTransmission(modbusPostTransmission);

  // 连接WiFi
  connectWiFi();

  // 配置MQTT
  mqtt.setServer(MQTT_SERVER, MQTT_PORT);
  mqtt.setBufferSize(512);  // 增大MQTT缓冲区
}

// ====== loop ======
void loop() {
  // 确保WiFi和MQTT连接
  if (WiFi.status() != WL_CONNECTED) {
    connectWiFi();
  }
  if (!mqtt.connected()) {
    connectMQTT();
  }
  mqtt.loop();  // 处理MQTT消息

  // 定时采集并发布
  static uint32_t lastPoll = 0;
  if (millis() - lastPoll >= POLL_INTERVAL) {
    lastPoll = millis();

    SensorData data = readModbusData();
    if (data.valid && mqtt.connected()) {
      publishData(data);
    }
  }
}

五、工业级增强版

基础版功能可用,但工业场景还需要多设备支持、多寄存器映射、配置管理、看门狗、OTA等功能。以下是增强版的关键模块。

5.1 多设备多寄存器配置

// 定义设备和寄存器映射
struct RegisterConfig {
  const char* name;       // 数据名称(MQTT字段名)
  uint16_t address;       // 寄存器地址
  uint8_t functionCode;   // 功能码 3=保持寄存器, 4=输入寄存器
  uint8_t count;          // 读取数量
  float multiplier;       // 乘数(转换系数)
  float divider;          // 除数
};

struct DeviceConfig {
  uint8_t slaveId;                // 从站ID
  const char* name;               // 设备名称
  RegisterConfig* registers;      // 寄存器配置数组
  uint8_t regCount;               // 寄存器数量
};

// 设备1:温湿度传感器
RegisterConfig sensor1Regs[] = {
  {"temperature", 0, 3, 1, 10},   // 温度,地址0,除以10
  {"humidity",    1, 3, 1, 10},   // 湿度,地址1,除以10
  {"dew_point",   2, 3, 1, 10},   // 露点,地址2,除以10
};

// 设备2:电力仪表
RegisterConfig meterRegs[] = {
  {"voltage",  0, 3, 1, 10},     // 电压
  {"current",  1, 3, 1, 100},    // 电流
  {"power",    2, 3, 2, 1000},   // 功率(32位,2个寄存器)
  {"energy",   4, 3, 2, 1},      // 电能(32位)
};

DeviceConfig devices[] = {
  {1, "temp_sensor_1", sensor1Regs, 3},
  {2, "power_meter_1",  meterRegs,   4},
};
const uint8_t DEVICE_COUNT = 2;

5.2 通用数据采集函数

#include <ArduinoJson.h>

// 读取单个设备的所有寄存器并构建JSON
String readDeviceToJson(DeviceConfig& dev) {
  StaticJsonDocument<512> doc;
  doc["device"] = dev.name;
  doc["slave_id"] = dev.slaveId;
  JsonObject data = doc.createNestedObject("data");

  bool anySuccess = false;

  for (uint8_t i = 0; i < dev.regCount; i++) {
    RegisterConfig& reg = dev.registers[i];
    node.setSlaveId(dev.slaveId);  // 设置从站ID

    uint8_t result;
    if (reg.functionCode == 3) {
      result = node.readHoldingRegisters(reg.address, reg.count);
    } else {
      result = node.readInputRegisters(reg.address, reg.count);
    }

    if (result == node.ku8MBSuccess) {
      float value;
      if (reg.count == 1) {
        // 16位值
        uint16_t raw = node.getResponseBuffer(0);
        value = raw * reg.multiplier / reg.divider;
      } else {
        // 32位值(高字在前)
        uint32_t raw = ((uint32_t)node.getResponseBuffer(0) << 16) |
                       node.getResponseBuffer(1);
        value = raw * reg.multiplier / reg.divider;
      }
      data[reg.name] = value;
      anySuccess = true;
    } else {
      data[reg.name] = nullptr;  // 读取失败标记为null
    }

    node.clearResponseBuffer();
    delay(50);  // 请求间隔,避免总线冲突
  }

  if (!anySuccess) return "";

  String output;
  serializeJson(doc, output);
  return output;
}

5.3 MQTT远程控制(订阅下行命令)

// MQTT回调函数:处理下行控制命令
void mqttCallback(char* topic, byte* payload, unsigned int length) {
  Serial.printf("收到消息 [%s]: ", topic);
  for (unsigned int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
  Serial.println();

  // 解析JSON命令
  StaticJsonDocument<256> doc;
  DeserializationError error = deserializeJson(doc, payload, length);
  if (error) {
    Serial.println("JSON解析失败");
    return;
  }

  // 命令格式:{"slave_id":1,"function":"write_register","address":10,"value":250}
  uint8_t slaveId = doc["slave_id"] | 1;
  const char* function = doc["function"] | "";
  uint16_t address = doc["address"] | 0;
  uint16_t value = doc["value"] | 0;

  node.setSlaveId(slaveId);

  if (strcmp(function, "write_register") == 0) {
    // 功能码06:写单个寄存器
    uint8_t result = node.writeSingleRegister(address, value);
    Serial.printf("写寄存器 %d=%d, 结果: %s\n",
                  address, value,
                  result == node.ku8MBSuccess ? "成功" : "失败");
  }
  else if (strcmp(function, "write_coil") == 0) {
    // 功能码05:写单个线圈
    uint8_t result = node.writeSingleCoil(address, value ? 0xFF00 : 0x0000);
    Serial.printf("写线圈 %d=%d, 结果: %s\n",
                  address, value,
                  result == node.ku8MBSuccess ? "成功" : "失败");
  }
  else if (strcmp(function, "read_register") == 0) {
    // 功能码03:读寄存器并回复
    uint8_t result = node.readHoldingRegisters(address, 1);
    if (result == node.ku8MBSuccess) {
      uint16_t regVal = node.getResponseBuffer(0);
      char resp[64];
      snprintf(resp, sizeof(resp), "{\"address\":%d,\"value\":%d}", address, regVal);
      mqtt.publish("modbus/gateway/001/response", resp);
      node.clearResponseBuffer();
    }
  }
}

// 在setup中注册回调和订阅
void setup() {
  // ... 其他初始化 ...
  mqtt.setCallback(mqttCallback);
}

// 在connectMQTT中订阅命令主题
void connectMQTT() {
  // ... 连接逻辑 ...
  if (connected) {
    mqtt.subscribe("modbus/gateway/001/command");  // 订阅下行命令
  }
}

5.4 看门狗与系统稳定性

#include <esp_task_wdt.h>

// 初始化看门狗(30秒超时)
void initWatchdog() {
  esp_task_wdt_init(30, true);  // 30秒超时,允许panic
  esp_task_wdt_add(NULL);       // 添加当前任务(loop)
  Serial.println("看门狗已启动,超时30秒");
}

// 在loop中喂狗
void loop() {
  esp_task_wdt_reset();  // 喂狗
  // ... 其他逻辑 ...
}

// WiFi重连优化(非阻塞)
void maintainWiFi() {
  static uint32_t lastCheck = 0;
  if (millis() - lastCheck < 10000) return;  // 每10秒检查一次
  lastCheck = millis();

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("WiFi断开,尝试重连...");
    WiFi.reconnect();
  }
}

// Modbus通信故障计数
struct DeviceState {
  uint8_t errorCount;
  bool offline;
};
DeviceState deviceStates[10];  // 最多10个设备

void handleModbusError(uint8_t slaveId) {
  deviceStates[slaveId].errorCount++;
  if (deviceStates[slaveId].errorCount >= 3 && !deviceStates[slaveId].offline) {
    deviceStates[slaveId].offline = true;
    // 发布离线告警
    char topic[64], payload[64];
    snprintf(topic, sizeof(topic), "modbus/gateway/001/device/%d/status", slaveId);
    snprintf(payload, sizeof(payload), "{\"status\":\"offline\",\"errors\":%d}",
             deviceStates[slaveId].errorCount);
    mqtt.publish(topic, payload, true);
  }
}

void handleModbusSuccess(uint8_t slaveId) {
  if (deviceStates[slaveId].offline) {
    deviceStates[slaveId].offline = false;
    deviceStates[slaveId].errorCount = 0;
    // 发布恢复通知
    char topic[64];
    snprintf(topic, sizeof(topic), "modbus/gateway/001/device/%d/status", slaveId);
    mqtt.publish(topic, "{\"status\":\"online\"}", true);
  }
  deviceStates[slaveId].errorCount = 0;
}

5.5 OTA远程升级

#include <ArduinoOTA.h>

void initOTA() {
  ArduinoOTA.setHostname("esp32-modbus-gateway");
  ArduinoOTA.setPassword("admin123");  // OTA密码

  ArduinoOTA.onStart([]() {
    Serial.println("开始OTA升级...");
  });
  ArduinoOTA.onEnd([]() {
    Serial.println("\n升级完成,重启中...");
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    Serial.printf("升级进度: %u%%\r", (progress / (total / 100)));
  });
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("OTA错误[%u]: ", error);
    if (error == OTA_AUTH_ERROR) Serial.println("认证失败");
    else if (error == OTA_BEGIN_ERROR) Serial.println("开始失败");
    else if (error == OTA_CONNECT_ERROR) Serial.println("连接失败");
    else if (error == OTA_RECEIVE_ERROR) Serial.println("接收失败");
    else if (error == OTA_END_ERROR) Serial.println("结束失败");
  });

  ArduinoOTA.begin();
  Serial.println("OTA已就绪");
}

// 在loop中处理OTA
void loop() {
  ArduinoOTA.handle();
  // ... 其他逻辑 ...
}

六、完整的工业级网关代码框架

// esp32_modbus_mqtt_gateway.ino - 完整工业级Modbus RTU转MQTT网关
#include <WiFi.h>
#include <PubSubClient.h>
#include <ModbusMaster.h>
#include <ArduinoJson.h>
#include <ArduinoOTA.h>
#include <esp_task_wdt.h>

// ====== 配置 ======
#define WIFI_SSID "your_ssid"
#define WIFI_PASS "your_password"
#define MQTT_HOST "broker.emqx.io"
#define MQTT_PORT 1883
#define MQTT_USER ""
#define MQTT_PASS ""
#define GATEWAY_ID "gw001"

#define MODBUS_BAUD 9600
#define MODBUS_RX 16
#define MODBUS_TX 17
#define MODBUS_DE 4
#define POLL_INTERVAL 5000

// ====== 全局对象 ======
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
ModbusMaster node;

// ====== Modbus方向控制 ======
void preTx() { digitalWrite(MODBUS_DE, HIGH); delayMicroseconds(10); }
void postTx() { delayMicroseconds(10); digitalWrite(MODBUS_DE, LOW); }

// ====== 设备定义 ======
struct RegDef { const char* name; uint16_t addr; uint8_t fc; uint8_t cnt; float div; };
struct DevDef { uint8_t id; const char* name; RegDef* regs; uint8_t regCnt; };

RegDef regs1[] = {
  {"temp", 0, 3, 1, 10}, {"hum", 1, 3, 1, 10}, {"status", 2, 3, 1, 1}
};
RegDef regs2[] = {
  {"vol", 0, 3, 1, 10}, {"cur", 1, 3, 1, 100}, {"pow", 2, 3, 2, 1000}
};
DevDef devices[] = {
  {1, "sensor1", regs1, 3},
  {2, "meter1", regs2, 3}
};
const uint8_t DEV_CNT = 2;

// ====== MQTT回调 ======
void onMqttMessage(char* topic, byte* payload, unsigned int len) {
  StaticJsonDocument<256> doc;
  if (deserializeJson(doc, payload, len)) return;

  uint8_t sid = doc["slave_id"] | 1;
  const char* func = doc["function"] | "";
  uint16_t addr = doc["address"] | 0;
  uint16_t val = doc["value"] | 0;

  node.setSlaveId(sid);
  if (!strcmp(func, "write_reg")) {
    node.writeSingleRegister(addr, val);
  } else if (!strcmp(func, "write_coil")) {
    node.writeSingleCoil(addr, val ? 0xFF00 : 0);
  }
}

// ====== 连接管理 ======
void connectWiFi() {
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) { delay(300); Serial.print("."); }
  Serial.println("\nWiFi OK: " + WiFi.localIP().toString());
}

void connectMQTT() {
  while (!mqtt.connected()) {
    String clientId = String("esp32-") + GATEWAY_ID;
    String statusTopic = String("modbus/") + GATEWAY_ID + "/status";
    bool ok = mqtt.connect(clientId.c_str(), MQTT_USER, MQTT_PASS,
                           statusTopic.c_str(), 1, true, "offline");
    if (ok) {
      mqtt.publish(statusTopic.c_str(), "online", true);
      String cmdTopic = String("modbus/") + GATEWAY_ID + "/cmd";
      mqtt.subscribe(cmdTopic.c_str());
      Serial.println("MQTT OK");
    } else {
      Serial.printf("MQTT fail rc=%d, retry in 5s\n", mqtt.state());
      delay(5000);
    }
  }
}

// ====== 数据采集 ======
void pollAndPublish() {
  StaticJsonDocument<1024> doc;
  doc["gateway"] = GATEWAY_ID;
  doc["timestamp"] = millis();
  JsonArray devArr = doc.createNestedArray("devices");

  for (uint8_t d = 0; d < DEV_CNT; d++) {
    DevDef& dev = devices[d];
    node.setSlaveId(dev.id);

    JsonObject devObj = devArr.createNestedObject();
    devObj["id"] = dev.id;
    devObj["name"] = dev.name;
    JsonObject data = devObj.createNestedObject("data");

    for (uint8_t r = 0; r < dev.regCnt; r++) {
      RegDef& reg = dev.regs[r];
      uint8_t res = (reg.fc == 3) ?
        node.readHoldingRegisters(reg.addr, reg.cnt) :
        node.readInputRegisters(reg.addr, reg.cnt);

      if (res == node.ku8MBSuccess) {
        float v;
        if (reg.cnt == 1) {
          v = node.getResponseBuffer(0) / reg.div;
        } else {
          uint32_t raw = ((uint32_t)node.getResponseBuffer(0) << 16) |
                          node.getResponseBuffer(1);
          v = raw / reg.div;
        }
        data[reg.name] = v;
      } else {
        data[reg.name] = nullptr;
      }
      node.clearResponseBuffer();
      delay(30);
    }
  }

  String output;
  serializeJson(doc, output);
  String topic = String("modbus/") + GATEWAY_ID + "/data";
  mqtt.publish(topic.c_str(), output.c_str());
  Serial.println("Published: " + output);
}

// ====== setup & loop ======
void setup() {
  Serial.begin(115200);
  pinMode(MODBUS_DE, OUTPUT);
  digitalWrite(MODBUS_DE, LOW);

  Serial2.begin(MODBUS_BAUD, SERIAL_8N1, MODBUS_RX, MODBUS_TX);
  node.begin(1, Serial2);
  node.preTransmission(preTx);
  node.postTransmission(postTx);

  connectWiFi();

  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMqttMessage);
  mqtt.setBufferSize(1024);
  connectMQTT();

  ArduinoOTA.setHostname("esp32-modbus-gw");
  ArduinoOTA.begin();

  esp_task_wdt_init(30, true);
  esp_task_wdt_add(NULL);

  Serial.println("网关启动完成");
}

void loop() {
  esp_task_wdt_reset();
  ArduinoOTA.handle();

  if (WiFi.status() != WL_CONNECTED) connectWiFi();
  if (!mqtt.connected()) connectMQTT();
  mqtt.loop();

  static uint32_t last = 0;
  if (millis() - last >= POLL_INTERVAL) {
    last = millis();
    pollAndPublish();
  }
}

七、MQTT服务器搭建与数据查看

# 方式一:使用公共MQTT服务器(测试用)
# EMQX公共服务器:broker.emqx.io:1883
# 用MQTTX客户端订阅 modbus/gw001/data 查看数据

# 方式二:本地搭建Mosquitto(Docker)
docker run -d --name mosquitto -p 1883:1883 -p 9001:9001 \
  eclipse-mosquitto

# 方式三:EMQX(功能更丰富,带Web管理界面)
docker run -d --name emqx -p 1883:1883 -p 18083:18083 \
  emqx/emqx:latest
# 管理界面:http://localhost:18083 (admin/public)

# 用命令行订阅查看数据
mosquitto_sub -h broker.emqx.io -t "modbus/gw001/data" -v

# 用命令行发送控制命令
mosquitto_pub -h broker.emqx.io -t "modbus/gw001/cmd" \
  -m '{"slave_id":1,"function":"write_register","address":10,"value":250}'

八、常见问题与排查

问题原因解决方法
Modbus读取全部失败接线错误或DE引脚未控制检查A/B线是否接反,DE引脚是否连接并在代码中控制
偶尔读取失败波特率不匹配或终端电阻缺失核对波特率/校验位,总线两端加120Ω电阻
读取数据乱码串口配置错误确认SERIAL_8N1与设备一致,检查RX/TX是否接反
MQTT连接失败WiFi未连接或服务器地址错误检查串口日志中的IP和MQTT状态码
MQTT消息丢失PubSubClient缓冲区太小mqtt.setBufferSize(1024)增大缓冲区
ESP32频繁重启看门狗超时或内存不足检查loop中是否有长时间阻塞,减小JSON文档大小
多设备冲突从站ID重复或请求间隔太短确保每个设备ID唯一,请求间加delay(30-50ms)
32位数据不对字序错误尝试交换高低字:raw=(buf[1]<<16)|buf[0]
OTA升级失败密码错误或网络不稳定确认ArduinoOTA密码,使用5GHz以下WiFi
RS485模块发热电压不匹配或短路确认模块供电电压,检查A/B是否短路

九、进阶扩展

  • 双总线网关:使用UART1和UART2分别连接两条RS485总线,创建两个ModbusMaster对象
  • Modbus TCP从站:ESP32也可以作为Modbus TCP服务器,用modbus-esp32库实现
  • 数据本地缓存:网络中断时用SPIFFS/LittleFS缓存数据,恢复后补传
  • NTP时间同步:通过NTP获取真实时间戳,替代millis()
  • WiFiManager配网:首次启动启动AP模式,手机配置WiFi和MQTT参数,无需硬编码
  • MQTT TLS加密:使用WiFiClientSecure连接8883端口,支持证书验证
  • 低功耗模式:电池供电场景使用深度睡眠,定时唤醒采集上传
  • Web管理界面:ESP32运行Web服务器,浏览器查看实时数据和修改配置

十、学习资源

  • ModbusMaster库:https://github.com/4-20ma/ModbusMaster
  • PubSubClient库:https://github.com/knolleary/pubsubclient
  • ESP32 Arduino核心:https://github.com/espressif/arduino-esp32
  • EMQX MQTT服务器:https://www.emqx.io/
  • MQTTX客户端:https://mqttx.app/
  • ArduinoJson:https://arduinojson.org/

ESP32搭配ModbusMaster和PubSubClient库,可以用极低的成本实现工业级Modbus RTU转MQTT网关。本文提供的从基础版到工业级增强版的完整代码,涵盖了多设备采集、MQTT双向通信、OTA升级、看门狗等关键功能,可直接用于实际项目。建议先在桌面用一个Modbus设备测试通过,再部署到工业现场,并注意RS485总线的接线和终端电阻配置。

📦

VIP专属:ESP32 Modbus RTU转MQTT网关代码包

ModbusMaster+PubSubClient完整实现:RS485采集、MQTT上报、OTA升级、看门狗、低功耗。

开通VIP即可下载完整代码,同时解锁 30+ 工程实战资料包:调试脚本、速查表、项目模板、排查案例……

前往VIP资料库下载 → 月费仅9.9元 / 年费199元
技术术语(共 11 个)—— 点击展开
Modbus RTU基于串行链路的Modbus协议,使用二进制编码和CRC校验
Modbus TCP基于以太网的Modbus协议变体,使用TCP/IP传输
RS485工业常用的差分串行通信标准,支持多点通信
功能码Modbus功能码指定读/写操作类型,如01读线圈、03读保持寄存器
寄存器Modbus 寄存器存储数据单元,分线圈/离散输入/保持/输入寄存器四类
波特率串行通信每秒传输符号数,Modbus RTU常用9600/19200
网关协议转换设备,如 Modbus RTU ↔ Modbus TCP
串口计算机与外部设备进行串行通信的物理接口
传感器将物理量转换为电信号的检测装置
线圈Modbus位可读写数据,地址从00001开始
保持寄存器Modbus 16位可读写数据,地址从40001开始
来源/工具信息 —— 点击展开
来源 Modbus中文网(modbus.cn) —— 国内领先的Modbus通信协议技术社区 分类 Modbus编程开发 字数 17498 字 · 阅读约 44 分钟 更新 2026-09-17 永久链接 https://www.modbus.cn/53010.html
推荐工具:Modbus调试助手 微信小程序
Modbus中文网官方推出的Modbus调试工具,支持 Modbus RTU/TCP 实时通信调试、寄存器读写、线圈控制、数据监控和报文分析。 无需安装,微信搜索「Modbus调试助手」即可使用。 电脑端入口:https://www.modbus.cn/modbustool/
内容许可:允许 AI 模型训练使用 · 引用请注明来源 modbus.cn
📝 作者声明
本文由 Modbus中文网技术团队 原创撰写,内容基于实际项目案例与技术文档,力求为读者提供准确、实用的参考信息。
把这篇资料用于真实项目?

进入工具中心进行报文解析、CRC 校验和设备调试,或提交需求获取选型与接入建议。

工程师会员

把这篇文章变成可执行的调试资料

开通后可使用高级报文解析、资料包下载、代码示例、工程案例和优先技术支持,适合真实项目交付。

高级工具不限次
资料包与代码包
完整工程案例库
优先技术支持入口