- 1. 一、方案概述
- 2. 二、硬件准备
- 3. 三、开发环境与库安装
- 4. 四、基础版:Modbus RTU数据采集并发布MQTT
- 5. 五、工业级增强版
- 6. 六、完整的工业级网关代码框架
- 7. 七、MQTT服务器搭建与数据查看
- 8. 八、常见问题与排查
- 9. 九、进阶扩展
- 10. 十、学习资源
- 11. VIP专属:ESP32 Modbus RTU转MQTT网关代码包
ESP32作为一款性价比极高的物联网微控制器,内置WiFi和蓝牙,配合Modbus RTU主机库和MQTT客户端库,可以轻松实现RS485设备数据采集并上传到MQTTDer Server。本文详细介绍如何用ESP32搭建Modbus RTU转MQTT网关,包括硬件接线、库安装、ModbusDatenerfassung、MQTT发布订阅、OTA升级、断线重连以及完整的工业级网关代码。
一、方案概述
1.1 为什么用ESP32做Modbus网关
- 成本极低:ESP32开发板仅20-30元,RS485模块5-10元,整体成本远低于商业网关
- 内置WiFi:无需额外网络模块,直接连接WiFi上传数据
- 性能充足:双核240MHz,520KB SRAM,可同时处理Modbus采集和MQTTKommunikation
- Arduino生态:丰富的库支持,ModbusMaster、PubSubClient等成熟库直接可用
- 支持OTA:可远程升级固件,无需现场维护
- 多路串口:ESP32有3路硬件串口,可同时连接多条RS485总线
1.2 系统架构
┌──────────────────────────────────────────────────────────────┐
│ MQTT Broker(Der Server) │
│ EMQX / Mosquitto / 阿里云IoT │
└───────────────────────────┬──────────────────────────────────┘
│ MQTT (WiFi)
┌───────────────────────────▼──────────────────────────────────┐
│ ESP32 网关 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ WiFi管理 │ │ MQTT客户端 │ │ Modbus RTUHauptstation │ │
│ │ (Verbindung/重连) │ │(PubSubClient)│ │ (ModbusMaster) │ │
│ └─────────────┘ └──────┬──────┘ └──────────┬──────────┘ │
└───────────────────────────┼─────────────────────┼────────────┘
│ │ UART2 (TX2/RX2)
│ ▼
│ ┌──────────────┐
│ │ RS485模块 │
│ │ (MAX485/SP3485)│
│ └──────┬───────┘
│ │ A/B
│ ┌──────▼───────┐
│ │ RS485总线 │
│ │ ┌────────┐ │
│ │ │Temperatur Feuchtigkeit │ │
│ │ │Der Sensor │ │
│ │ └────────┘ │
│ │ ┌────────┐ │
│ │ │电力仪表 │ │
│ │ └────────┘ │
│ └──────────────┘
二、硬件准备
2.1 所需材料
- ESP32开发板:推荐ESP32-WROOM-32或ESP32-S3
- RS485模块:MAX485、SP3485或带隔离的RS485模块(推荐工业级带隔离)
- 杜邦线:若干
- 120Ω终端电阻:总线两端各一个(长距离必需)
- Modbus RTUAusrüstung:Temperatur- & Feuchtigkeitssensor、电力仪表等(测试用)
- 5V/3.3V电源:给ESP32和RS485模块供电
2.2 接线说明
| ESP32引脚 | RS485模块 | 说明 |
|---|---|---|
| GPIO17 (TX2) | DI | ESP32发送→RS485输入 |
| GPIO16 (RX2) | RO | RS485输出→ESP32接收 |
| GPIO4 | DE/RE | 方向控制(高=Senden Sie,低=Empfang) |
| 3.3V | VCC | 电源(注意模块电压,有些是5V) |
| GND | GND | 共地 |
| – | A | RS485 A线(接设备A) |
| – | B | RS485 B线(接设备B) |
Aufmerksamkeit:
- 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
// Werkzeuge → 开发板 → 开发板管理器 → Suche"ESP32" → 安装
// 需要安装的库(Werkzeuge → 管理库 → Suche):
// 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 // Porter Rate
#define MODBUS_SLAVE_ID 1 // Von der StationID
#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); // 切换到接收模式
}
// ====== WiFiVerbindung ======
void connectWiFi() {
Serial.print("VerbindungWiFi: ");
Serial.println(WIFI_SSID);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFibereits verbunden");
Serial.print("IPAdresse: ");
Serial.println(WiFi.localIP());
}
// ====== MQTTVerbindung ======
void connectMQTT() {
while (!mqtt.connected()) {
Serial.print("VerbindungMQTTDer Server...");
// VerbindungMQTT(带遗嘱消息:离线时通知)
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("bereits verbunden");
mqtt.publish("modbus/gateway/001/status", "online", true); // 上线通知
} else {
Serial.print("失败,rc=");
Serial.print(mqtt.state());
Serial.println(",5秒后重试");
delay(5000);
}
}
}
// ====== LesenModbus设备数据 ======
struct SensorData {
float temperature; // Temperatur
float humidity; // 湿度
uint16_t status; // 状态字
bool valid; // 数据是否有效
};
SensorData readModbusData() {
SensorData data = {0, 0, 0, false};
// Lesen und halten Register0-2(Temperatur、湿度、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("Temperatur: %.1f°C, 湿度: %.1f%%, Status: %d\n",
data.temperature, data.humidity, data.status);
} else {
Serial.printf("ModbusLesen gescheitert,错误码: %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);
// VerbindungWiFi
connectWiFi();
// 配置MQTT
mqtt.setServer(MQTT_SERVER, MQTT_PORT);
mqtt.setBufferSize(512); // 增大MQTT缓冲区
}
// ====== loop ======
void loop() {
// 确保WiFi和MQTTVerbindung
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-Adresse
uint8_t functionCode; // Funktionscode 3=保持寄存器, 4=输入寄存器
uint8_t count; // Lesen der Menge
float multiplier; // 乘数(转换系数)
float divider; // 除数
};
struct DeviceConfig {
uint8_t slaveId; // Von der StationID
const char* name; // Gerätname
RegisterConfig* registers; // 寄存器配置数组
uint8_t regCount; // Anzahl der Register
};
// Ausrüstung1:Temperatur- & Feuchtigkeitssensor
RegisterConfig sensor1Regs[] = {
{"temperature", 0, 3, 1, 10}, // Temperatur,Adresse0,除以10
{"humidity", 1, 3, 1, 10}, // 湿度,Adresse1,除以10
{"dew_point", 2, 3, 1, 10}, // 露点,Adresse2,除以10
};
// Ausrüstung2:电力仪表
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 MQTTFernsteuerung(订阅下行命令)
// 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();
// AnalyseJSONBefehl
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) {
// Funktionscode06:Schreiben eines einzelnen Registers
uint8_t result = node.writeSingleRegister(address, value);
Serial.printf("写寄存器 %d=%d, Ergebnis: %s\n",
address, value,
result == node.ku8MBSuccess ? "成功" : "失败");
}
else if (strcmp(function, "write_coil") == 0) {
// Funktionscode05:Schreiben Sie eine einzelne Schleife.
uint8_t result = node.writeSingleCoil(address, value ? 0xFF00 : 0x0000);
Serial.printf("写线圈 %d=%d, Ergebnis: %s\n",
address, value,
result == node.ku8MBSuccess ? "成功" : "失败");
}
else if (strcmp(function, "read_register") == 0) {
// Funktionscode03:读寄存器并回复
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("OTAFehler.[%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);
}
}
}
// ====== Datenerfassung ======
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服务器搭建与数据查看
# 方式一:使用公共MQTTDer Server(测试用)
# 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引脚是否连接并在代码中控制 |
| 偶尔读取失败 | 波特率不匹配或终端电阻缺失 | 核对波特率/Prüfstand.,总线两端加120Ω电阻 |
| 读取数据乱码 | 串口配置错误 | BestätigenSERIAL_8N1与设备一致,检查RX/TX是否接反 |
| MQTT连接失败 | WiFi未连接或服务器地址错误 | 检查串口日志中的IP和MQTT状态码 |
| MQTT消息丢失 | PubSubClient缓冲区太小 | mqtt.setBufferSize(1024)增大缓冲区 |
| ESP32频繁重启 | 看门狗超时或内存不足 | 检查loop中是否有长时间阻塞,减小JSON文档大小 |
| 多设备冲突 | Von der StationID重复或请求间隔太短 | 确保每个设备ID唯一,请求间加delay(30-50ms) |
| 32位数据不对 | 字序错误 | 尝试交换高低字:raw=(buf[1]<<16)|buf[0] |
| OTA升级失败 | 密码错误或网络不稳定 | BestätigenArduinoOTA密码,使用5GHz以下WiFi |
| RS485模块发热 | 电压不匹配或短路 | 确认模块供电电压,检查A/B是否短路 |
九、进阶扩展
- 双总线网关:使用UART1和UART2分别连接两条RS485总线,创建两个ModbusMaster对象
- Modbus TCPVon der Station:ESP32也可以作为Modbus TCPDer Server,用modbus-esp32库实现
- 数据本地缓存:网络中断时用SPIFFS/LittleFS缓存数据,恢复后补传
- NTP时间同步:通过NTP获取真实时间戳,替代millis()
- WiFiManager配网:首次启动启动AP模式,手机配置WiFi和MQTTParameter,无需硬编码
- MQTT TLS加密:使用WiFiClientSecureVerbindung8883端口,支持证书验证
- 低功耗模式:电池供电场景使用深度睡眠,定时唤醒采集上传
- Web管理界面:ESP32运行WebDer Server,浏览器查看实时数据和修改配置
十、学习资源
- ModbusMaster库:https://github.com/4-20ma/ModbusMaster
- PubSubClient库:https://github.com/knolleary/pubsubclient
- ESP32 Arduino核心:https://github.com/espressif/arduino-esp32
- EMQX MQTTDer Server: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升级、看门狗、低功耗。
Öffnen Sie VIP即可下载完整代码,同时解锁 30+ 工程实战资料包:调试脚本、速查表、项目模板、排查案例……
前往VIP资料库下载 → 月费仅9.9元 / 年费199元