ESP32 Modbus RTU转MQTT게이트웨이 실전:RS485데이터采集上传 원격 제어OTA与산업용稳定性完整튜토리얼

무료무료 기술 자료

이 글은 바로 읽을 수 있으며 기초 학습과 검색 유입에 적합합니다.

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/데이터";  // 发布主题

// 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 Sensor데이터 {
  float temperature;   // 온도
  float humidity;      // 湿度
  uint16_t status;     // 状态字
  bool valid;          // 데이터是否有效
};

Sensor데이터 readModbus데이터() {
  Sensor데이터 데이터 = {0, 0, 0, false};

  // 유지 레지스터 읽기 (FC03)0-2(온도、湿度、状态)
  uint8_t result = node.readHolding회원가입s(0, 3);

  if (result == node.ku8MBSuccess) {
    // 읽기成功,从响应缓冲区获取데이터
    uint16_t rawTemp = node.getResponseBuffer(0);  // 온도原始值
    uint16_t rawHum = node.getResponseBuffer(1);   // 湿度原始值
    데이터.status = node.getResponseBuffer(2);       // 状态字

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

    Serial.printf("온도: %.1f°C, 湿度: %.1f%%, 状态: %d\n",
                  데이터.temperature, 데이터.humidity, 데이터.status);
  } else {
    Serial.printf("Modbus읽기失败,错误码: %d\n", result);
  }

  // 지우기响应缓冲区,准备下一次읽기
  node.clearResponseBuffer();

  return 데이터;
}

// ====== 发布데이터到MQTT ======
void publish데이터(const Sensor데이터& 데이터) {
  // 构建JSON消息
  char payload[128];
  snprintf(payload, sizeof(payload),
    "{\"temperature\":%.1f,\"humidity\":%.1f,\"status\":%d,\"ts\":%lu}",
    데이터.temperature, 데이터.humidity, 데이터.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();

    Sensor데이터 데이터 = readModbus데이터();
    if (데이터.valid && mqtt.connected()) {
      publish데이터(데이터);
    }
  }
}

五、산업용增强版

基础版功能可用,但工业시나리오还需要多设备支持、多레지스터 매핑、설정管理、看门狗、OTA等功能。以下是增强版的关键模块。

5.1 多设备多레지스터설정

// 定义设备和레지스터 매핑
struct 회원가입Config {
  const char* name;       // 데이터名称(MQTT필드名)
  uint16_t 주소;       // 레지스터주소
  uint8_t functionCode;   // 기능 코드 3=유지 레지스터, 4=입력 레지스터
  uint8_t count;          // 읽을 수량
  float multiplier;       // 乘数(转换스케일 계수)
  float divider;          // 除数
};

struct DeviceConfig {
  uint8_t slaveId;                // 슬레이브ID
  const char* name;               // 设备名称
  회원가입Config* registers;      // 레지스터설정数组
  uint8_t regCount;               // 레지스터 수량
};

// 设备1:온습도센서
회원가입Config sensor1Regs[] = {
  {"temperature", 0, 3, 1, 10},   // 온도,주소0,除以10
  {"humidity",    1, 3, 1, 10},   // 湿度,주소1,除以10
  {"dew_point",   2, 3, 1, 10},   // 露点,주소2,除以10
};

// 设备2:电力仪表
회원가입Config 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 데이터 = doc.createNestedObject("데이터");

  bool anySuccess = false;

  for (uint8_t i = 0; i < dev.regCount; i++) {
    회원가입Config& reg = dev.registers[i];
    node.setSlaveId(dev.slaveId);  // 设置슬레이브ID

    uint8_t result;
    if (reg.functionCode == 3) {
      result = node.readHolding회원가입s(reg.주소, reg.count);
    } else {
      result = node.readInput회원가입s(reg.주소, 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;
      }
      데이터[reg.name] = value;
      anySuccess = true;
    } else {
      데이터[reg.name] = nullptr;  // 읽기失败기호为null
    }

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

  if (!anySuccess) return "";

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

5.3 MQTT원격 제어(订阅下行命令)

// MQTT回调函数:处理下行控制命令
void mqtt호명back(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;
  Deserialization오답 error = deserializeJson(doc, payload, length);
  if (error) {
    Serial.println("JSON분석失败");
    return;
  }

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

  node.setSlaveId(slaveId);

  if (strcmp(function, "write_register") == 0) {
    // 기능 코드06:写单个레지스터
    uint8_t result = node.writeSingle회원가입(주소, value);
    Serial.printf("写레지스터 %d=%d, 结果: %s\n",
                  주소, value,
                  result == node.ku8MBSuccess ? "成功" : "失败");
  }
  else if (strcmp(function, "write_coil") == 0) {
    // 기능 코드05:写单个코일
    uint8_t result = node.writeSingleCoil(주소, value ? 0xFF00 : 0x0000);
    Serial.printf("写코일 %d=%d, 结果: %s\n",
                  주소, value,
                  result == node.ku8MBSuccess ? "成功" : "失败");
  }
  else if (strcmp(function, "read_register") == 0) {
    // 기능 코드03:读레지스터并回复
    uint8_t result = node.readHolding회원가입s(주소, 1);
    if (result == node.ku8MBSuccess) {
      uint16_t regVal = node.getResponseBuffer(0);
      char resp[64];
      snprintf(resp, sizeof(resp), "{\"주소\":%d,\"value\":%d}", 주소, regVal);
      mqtt.publish("modbus/gateway/001/response", resp);
      node.clearResponseBuffer();
    }
  }
}

// 在setup中회원가입回调和订阅
void setup() {
  // ... 其他初始化 ...
  mqtt.set호명back(mqtt호명back);
}

// 在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 handleModbus오답(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.on오답([](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["주소"] | 0;
  uint16_t val = doc["value"] | 0;

  node.setSlaveId(sid);
  if (!strcmp(func, "write_reg")) {
    node.writeSingle회원가입(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 데이터 = devObj.createNestedObject("데이터");

    for (uint8_t r = 0; r < dev.regCnt; r++) {
      RegDef& reg = dev.regs[r];
      uint8_t res = (reg.fc == 3) ?
        node.readHolding회원가입s(reg.addr, reg.cnt) :
        node.readInput회원가입s(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;
        }
        데이터[reg.name] = v;
      } else {
        데이터[reg.name] = nullptr;
      }
      node.clearResponseBuffer();
      delay(30);
    }
  }

  String output;
  serializeJson(doc, output);
  String topic = String("modbus/") + GATEWAY_ID + "/데이터";
  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.set호명back(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/데이터 보기데이터

# 방식二:本地搭建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/데이터" -v

# 用命令行보내기控制命令
mosquitto_pub -h broker.emqx.io -t "modbus/gw001/cmd" \
  -m '{"slave_id":1,"function":"write_register","주소":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元
실제 프로젝트에 이 자료를 활용하시겠습니까?

도구 센터에서 프레임 분석, CRC 검증, 장비 디버깅을 진행하거나 요청을 제출하여 선정 및 연동 제안을 받아보세요.

엔지니어 회원

이 글을 실행 가능한 디버깅 자료로 활용하세요

가입하면 고급 프레임 분석, 자료 패키지 다운로드, 코드 예제, 엔지니어링 사례, 우선 기술 지원을 이용할 수 있어 실제 프로젝트 납품에 적합합니다.

고급 도구 무제한 이용
자료 패키지와 코드 패키지
완전한 엔지니어링 사례 라이브러리
우선 기술 지원 채널