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) | DI | ESP32보내기→RS485입력 |
| GPIO16 (RX2) | RO | RS485출력→ESP32수신 |
| GPIO4 | DE/RE | 方向控制(高=보내기,低=수신) |
| 3.3V | VCC | 电源(注意模块电压,有些是5V) |
| GND | GND | 共地 |
| - | A | RS485 A线(接设备A) |
| - | B | RS485 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元