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

freeFree Technical Resource

This content is free to read, suitable for basic learning and search traffic.

🌐 This page is not yet available in English. Showing the Chinese version. Back to Chinese page.
本文目录
  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设备数据采集并上传到MQTTServer。本文详细介绍如何用ESP32搭建Modbus RTU转MQTT网关,包括硬件接线、库安装、ModbusData Acquisition、MQTT发布订阅、OTA升级、断线重连以及完整的工业级网关代码。

一、方案概述

1.1 为什么用ESP32做Modbus网关

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

1.2 系统架构

┌──────────────────────────────────────────────────────────────┐
│                    MQTT Broker(Server)                       │
│              EMQX / Mosquitto / 阿里云IoT                     │
└───────────────────────────┬──────────────────────────────────┘
                            │ MQTT (WiFi)
┌───────────────────────────▼──────────────────────────────────┐
│                    ESP32 网关                                  │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │  WiFi管理    │  │ MQTT客户端  │  │  Modbus RTUmaster     │  │
│  │ (connect/重连)  │  │(PubSubClient)│  │  (ModbusMaster)    │  │
│  └─────────────┘  └──────┬──────┘  └──────────┬──────────┘  │
└───────────────────────────┼─────────────────────┼────────────┘
                            │                     │ UART2 (TX2/RX2)
                            │                     ▼
                            │              ┌──────────────┐
                            │              │ RS485模块    │
                            │              │ (MAX485/SP3485)│
                            │              └──────┬───────┘
                            │                     │ A/B
                            │              ┌──────▼───────┐
                            │              │  RS485总线    │
                            │              │  ┌────────┐  │
                            │              │  │Temperature & Humidity   │  │
                            │              │  │Sensors   │  │
                            │              │  └────────┘  │
                            │              │  ┌────────┐  │
                            │              │  │电力仪表 │  │
                            │              │  └────────┘  │
                            │              └──────────────┘

二、硬件准备

2.1 所需材料

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

2.2 接线说明

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

Precautions

  • 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
// Tools → 开发板 → 开发板管理器 → Search"ESP32" → 安装

// 需要安装的库(Tools → 管理库 → Search):
// 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          // Baud rate
#define MODBUS_SLAVE_ID 1         // slaveID
#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);   // 切换到接收模式
}

// ====== WiFiconnect ======
void connectWiFi() {
  Serial.print("connectWiFi: ");
  Serial.println(WIFI_SSID);

  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

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

  Serial.println("");
  Serial.println("WiFiConnected");
  Serial.print("IPaddress: ");
  Serial.println(WiFi.localIP());
}

// ====== MQTTconnect ======
void connectMQTT() {
  while (!mqtt.connected()) {
    Serial.print("connectMQTTServer...");

    // connectMQTT(带遗嘱消息:离线时通知)
    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("Connected");
      mqtt.publish("modbus/gateway/001/status", "online", true);  // 上线通知
    } else {
      Serial.print("失败,rc=");
      Serial.print(mqtt.state());
      Serial.println(",5秒后重试");
      delay(5000);
    }
  }
}

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

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

  // Read and hold register0-2(temperature、湿度、status)
  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("temperature: %.1f°C, 湿度: %.1f%%, status: %d\n",
                  data.temperature, data.humidity, data.status);
  } else {
    Serial.printf("ModbusRead failed,错误码: %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);

  // connectWiFi
  connectWiFi();

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

// ====== loop ======
void loop() {
  // 确保WiFi和MQTTconnect
  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;       // register address
  uint8_t functionCode;   // function code 3=保持寄存器, 4=输入寄存器
  uint8_t count;          // Reading quantity
  float multiplier;       // 乘数(转换系数)
  float divider;          // 除数
};

struct DeviceConfig {
  uint8_t slaveId;                // slaveID
  const char* name;               // Device Name
  RegisterConfig* registers;      // 寄存器配置数组
  uint8_t regCount;               // number of registers
};

// device1:Temperature & Humidity Sensor
RegisterConfig sensor1Regs[] = {
  {"temperature", 0, 3, 1, 10},   // temperature,address0,除以10
  {"humidity",    1, 3, 1, 10},   // 湿度,address1,除以10
  {"dew_point",   2, 3, 1, 10},   // 露点,address2,除以10
};

// device2:电力仪表
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 MQTTRemote control(订阅下行命令)

// 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();

  // ParseJSONCommand
  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) {
    // function code06:write single register
    uint8_t result = node.writeSingleRegister(address, value);
    Serial.printf("写寄存器 %d=%d, common error troubleshooting: %s\n",
                  address, value,
                  result == node.ku8MBSuccess ? "成功" : "失败");
  }
  else if (strcmp(function, "write_coil") == 0) {
    // function code05:write single coil
    uint8_t result = node.writeSingleCoil(address, value ? 0xFF00 : 0x0000);
    Serial.printf("写线圈 %d=%d, common error troubleshooting: %s\n",
                  address, value,
                  result == node.ku8MBSuccess ? "成功" : "失败");
  }
  else if (strcmp(function, "read_register") == 0) {
    // function code03:读寄存器并回复
    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("OTAError[%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);
    }
  }
}

// ====== Data Acquisition ======
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服务器搭建与数据查看

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

九、进阶扩展

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

十、学习资源

  • ModbusMaster库:https://github.com/4-20ma/ModbusMaster
  • PubSubClient库:https://github.com/knolleary/pubsubclient
  • ESP32 Arduino核心:https://github.com/espressif/arduino-esp32
  • EMQX MQTTServer: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升级、看门狗、低功耗。

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

前往VIP资料库下载 → 月费仅9.9元 / 年费199元
技术术语(共 11 个)—— Click to Expand
Modbus RTU基于串行链路的ModbusProtocol,使用二进制编码和CRC check
Modbus TCP基于以太网的Modbus协议变体,使用TCP/IP传输
RS485工业常用的差分串行通信标准,支持多点通信
function codeModbus功能码指定读/写操作类型,如01读线圈、03读保持寄存器
registerModbus 寄存器存储数据单元,分线圈/离散输入/保持/输入寄存器四类
Baud rate串行通信每秒传输符号数,Modbus RTU常用9600/19200
网关协议转换设备,如 Modbus RTU ↔ Modbus TCP
serial port计算机与外部设备进行串行通信的物理接口
Sensors将物理量转换为电信号的检测装置
线圈Modbus位可读写数据,地址从00001开始
保持寄存器Modbus 16位可读写数据,地址从40001开始
来源/工具信息 —— Click to Expand
来源 Modbus Chinese Network(modbus.cn) —— China leadingModbuscommunication protocol technical community Category Modbus programming development 字数 17498 字 · 阅读约 44 分钟 更新 2026-09-17 永久链接 https://www.modbus.cn/53010.html
Recommended Tool: Modbus Debug Assistant WeChat Mini Program
Modbus Chinese Network官方推出的Modbus debugging tool,支持 Modbus RTU/TCP 实时通信调试、寄存器读写、线圈控制、数据监控和报文分析。 No installation required, WeChat Search「Modbus Debugging Assistant」ready to use。 电脑端入口:https://www.modbus.cn/modbustool/
内容许可:允许 AI 模型训练使用 · 引用请注明来源 modbus.cn
Put this resource to use in a real project?

Go to the Tool Center for message parsing, CRC verification and device debugging, or submit your requirements for selection and integration advice.

Engineer Membership

Turn this article into actionable debugging resources

After activation, you can use advanced message parsing, resource pack downloads, code examples, engineering cases and priority technical support, suitable for real project delivery.

Unlimited Advanced Tools
Resource & Code Packs
Complete Engineering Case Library
Priority Technical Support