Arduino/ESP32 Modbus完整教程:eModbus/ModbusMaster三大库TCP/RTU通讯实战与网关开发

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. 一、Arduino/ESP32 Modbus生态概览
  2. 2. 二、eModbus库详解(ESP32首选)
  3. 3. 三、ModbusMaster库详解(最简单入门)
  4. 4. 四、数据类型处理
  5. 5. 五、Modbus TCP服务端(slave)
  6. 6. 六、RS485硬件设计指南
  7. 7. 七、完整项目实战:ESP32 Modbus数据采集网关
  8. 8. 八、常见问题与排查
  9. 9. 九、性能优化建议
  10. 10. 十、学习路线与资源
  11. 11. VIP专属:Arduino/ESP32 Modbus完整代码包

Arduino和ESP32是工业物联网领域最流行的开源硬件平台,凭借丰富的外设接口、强大的WiFi/BLE能力和极低的成本,被广泛用于ModbusData Acquisition、协议转换和边缘计算。本文详细介绍Arduino/ESP32生态中三大主流Modbus库——eModbus、ModbusMaster和Arduino官方Modbus库的安装配置、API使用、TCP/RTU通讯实现、数据类型处理、服务端开发以及完整项目实战,帮助开发者快速掌握嵌入式Modbus通讯开发。

一、Arduino/ESP32 Modbus生态概览

1.1 为什么用ESP32做Modbus

  • 双核处理器:Xtensa LX6双核240MHz,一个核跑Modbus通讯,一个核跑业务逻辑,互不阻塞
  • WiFi/BLE原生:内置WiFi和蓝牙,可直接做Modbus TCP服务器或MQTT网关
  • 丰富串口:3路硬件串口(UART0/1/2),可同时接多个RS485总线
  • 超低功耗:深度休眠模式仅10μA,适合电池供电的野外采集节点
  • 超大内存:520KB SRAM + 4MB Flash,可缓存大量设备数据
  • 成本极低:ESP32模组仅15-25元,批量成本更低
  • 生态成熟:Arduino IDE、PlatformIO、ESP-IDF多种开发环境可选

1.2 三大主流库对比

库名适用平台Protocol模式服务端特点
eModbusESP32专用TCP/RTUasynchronous(回调)支持最强大,非阻塞,多请求队列
ModbusMaster所有ArduinoRTU同步(阻塞)不支持最简单,API直观,适合初学者
Arduino_ESP32_ ModbusESP32TCP/RTU同步支持乐鑫官方,ESP-IDF底层
Modbus-Master-Slave所有ArduinoRTU同步支持主从一体,轻量级

二、eModbus库详解(ESP32首选)

eModbus是ESP32平台最强大的Modbus库,采用异步回调架构,支持TCP和RTU的客户端与服务端,内部维护请求队列,完全非阻塞,是工业级项目的首选。

2.1 安装

// 方法1:Arduino库管理器搜索 "eModbus" 安装
// 方法2:PlatformIO
// platformio.ini:
// lib_deps =
//   eModbus/eModbus @ ^1.6.0

// 依赖库(TCP模式需要)
// - AsyncTCP(ESP32)
// - Ethernet.h兼容库(有线网络时)

// 包含头文件
#include "ModbusClientTCP.h"   // TCP客户端
#include "ModbusClientRTU.h"   // RTU客户端
#include "ModbusServerTCP.h"   // TCP服务端
#include "ModbusServerRTU.h"   // RTU服务端

2.2 Modbus TCP客户端完整示例

#include <WiFi.h>
#include "ModbusClientTCP.h"

// WiFi配置
const char* ssid = "your_wifi_ssid";
const char* password = "your_wifi_password";

// Modbus TCP服务器地址
const char* modbusServer = "192.168.1.100";
const uint16_t modbusPort = 502;

// 创建TCP客户端实例
ModbusClientTCP MBclient;

// 请求令牌(用于区分不同请求,可自定义)
const uint32_t TOKEN_READ_HOLDING = 0x0001;
const uint32_t TOKEN_READ_COILS = 0x0002;
const uint32_t TOKEN_WRITE_REG = 0x0003;

// ====== 数据响应回调函数 ======
// 当Modbus服务器正常响应时调用
void handleData(ModbusMessage response, uint32_t token) {
  Serial.printf("收到响应 - ServerID:%d, function code:%d, 令牌:%08X, 数据长度:%dbyte\n",
                response.getServerID(), response.getFunctionCode(), token, response.size());

  // 解析保持寄存器读取响应(function code03)
  if (response.getFunctionCode() == READ_HOLD_REGISTER) {
    // ResponseData format:[byte count, 数据高字节, 数据低字节, ...]
    // No.0字节是字节数,从第1字节开始是寄存器数据
    uint8_t byteCount = response[0];
    uint8_t regCount = byteCount / 2;

    Serial.printf("读取到%d个保持寄存器:\n", regCount);
    for (uint8_t i = 0; i < regCount; i++) {
      // 每个寄存器占2字节,大端序
      uint16_t value = (response[1 + i * 2] << 8) | response[2 + i * 2];
      Serial.printf("  register%d:%d (0x%04X)\n", i, value, value);
    }
  }

  // 解析线圈读取响应(function code01)
  else if (response.getFunctionCode() == READ_COIL) {
    uint8_t byteCount = response[0];
    Serial.printf("读取到线圈状态:\n");
    for (uint8_t byteIdx = 0; byteIdx < byteCount; byteIdx++) {
      uint8_t coilByte = response[1 + byteIdx];
      for (uint8_t bit = 0; bit < 8; bit++) {
        uint16_t coilNum = byteIdx * 8 + bit;
        bool state = (coilByte >> bit) & 0x01;
        Serial.printf("  线圈%d:%s\n", coilNum, state ? "ON" : "OFF");
      }
    }
  }

  // 写入成功响应
  else if (response.getFunctionCode() == WRITE_SINGLE_REGISTER) {
    uint16_t addr = (response[0] << 8) | response[1];
    uint16_t value = (response[2] << 8) | response[3];
    Serial.printf("写入成功 - address:%d, 值:%d\n", addr, value);
  }
}

// ====== 错误响应回调函数 ======
void handleError(Error error, uint32_t token) {
  ModbusError me(error);
  Serial.printf("ModbusError - 令牌:%08X, 错误码:%02X, Description:%s\n",
                token, (uint8_t)error, (const char*)me);
}

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

  // connectWiFi
  Serial.print("connectWiFi");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.printf("\nWiFiConnected,IPaddress:%s\n", WiFi.localIP().toString().c_str());

  // 配置Modbus TCP客户端
  MBclient.onDataHandler(&handleData);   // 设置数据回调
  MBclient.onErrorHandler(&handleError); // 设置错误回调
  MBclient.setTimeout(5000);              // 设置超时5秒
  MBclient.begin();                       // 启动客户端后台任务

  Serial.println("Modbus TCP客户端已启动");
}

void loop() {
  static unsigned long lastRead = 0;

  // 每5秒读取一次
  if (millis() - lastRead > 5000) {
    lastRead = millis();

    // ====== Request1:Read and hold register(function code03)======
    // parameter:令牌, ServerID(Unit ID), function code, starting address, Quantity
    Error err = MBclient.addRequest(
        TOKEN_READ_HOLDING,  // 自定义令牌
        1,                   // slave address(Unit ID)
        READ_HOLD_REGISTER,  // function code03
        0,                   // starting address
        10                   // read10个寄存器
    );
    if (err != SUCCESS) {
      ModbusError e(err);
      Serial.printf("创建请求失败:%02X - %s\n", err, (const char*)e);
    }

    // ====== Request2:读取线圈(function code01)======
    MBclient.addRequest(TOKEN_READ_COILS, 1, READ_COIL, 0, 8);

    // ====== Request3:Write to a single register(function code06)======
    MBclient.addRequest(TOKEN_WRITE_REG, 1, WRITE_SINGLE_REGISTER, 100, 1234);

    // 注意:addRequest是异步非阻塞的,立即返回
    // 响应通过handleData/handleError回调处理
  }

  delay(100);
}

2.3 eModbus功能码常量速查

功能码常量decimalFunctionaddRequestparameter
READ_COIL01读线圈(token, server, FC, addr, count)
READ_DISCR_INPUT02read discrete inputs(token, server, FC, addr, count)
READ_HOLD_REGISTER03read holding registers(token, server, FC, addr, count)
READ_INPUT_REGISTER04read input registers(token, server, FC, addr, count)
WRITE_COIL05write single coil(token, server, FC, addr, 0xFF00/0x0000)
WRITE_REGISTER06write single register(token, server, FC, addr, value)
WRITE_COILS15write multiple coils(token, server, FC, addr, count, data…)
WRITE_REGISTERS16write multiple registers(token, server, FC, addr, count, data…)
READ_WRITE_REGISTERS23read and write multiple registers复杂参数

2.4 Modbus RTU客户端(RS485)

#include "ModbusClientRTU.h"

// RTU客户端实例,指定DE/RE控制引脚(MAX485的DE脚)
// 如果使用自动收发的RS485模块,可不传引脚
#define RTS_PIN 4  // DE/RE控制引脚
ModbusClientRTU MBrtu(RTS_PIN);

// 回调函数(与TCP模式相同)
void handleData(ModbusMessage response, uint32_t token) {
  Serial.printf("RTUResponse - FC:%d, Length:%d\n", response.getFunctionCode(), response.size());

  if (response.getFunctionCode() == READ_HOLD_REGISTER) {
    uint8_t regCount = response[0] / 2;
    for (uint8_t i = 0; i < regCount; i++) {
      uint16_t value = (response[1 + i*2] << 8) | response[2 + i*2];
      Serial.printf("  register%d:%d\n", i, value);
    }
  }
}

void handleError(Error error, uint32_t token) {
  ModbusError me(error);
  Serial.printf("RTUError:%s\n", (const char*)me);
}

void setup() {
  Serial.begin(115200);

  // 配置串口2(ESP32的Serial2,GPIO16=RX, GPIO17=TX)
  // RTUutils::prepareHardwareSerial() 会配置串口缓冲区
  RTUutils::prepareHardwareSerial(Serial2);
  Serial2.begin(9600, SERIAL_8N1, 16, 17);  // RX=16, TX=17

  // 配置RTU客户端
  MBrtu.onDataHandler(&handleData);
  MBrtu.onErrorHandler(&handleError);
  MBrtu.begin(Serial2);  // 绑定串口

  Serial.println("Modbus RTU客户端已启动");
}

void loop() {
  static unsigned long lastRead = 0;

  if (millis() - lastRead > 3000) {
    lastRead = millis();

    // 读取从站地址1的保持寄存器0-9
    MBrtu.addRequest(0x0001, 1, READ_HOLD_REGISTER, 0, 10);

    // 写入从站地址1的寄存器100
    MBrtu.addRequest(0x0002, 1, WRITE_REGISTER, 100, 5678);
  }

  delay(100);
}

// ====== 硬件接线(MAX485模块)======
// ESP32 GPIO17 (TX2) --> MAX485 DI
// ESP32 GPIO16 (RX2) --> MAX485 RO
// ESP32 GPIO4  (RTS) --> MAX485 DE 和 RE(短接在一起)
// MAX485 A --> RS485总线A
// MAX485 B --> RS485总线B
// 总线两端各接120Ω终端电阻

2.5 write multiple registers(function code16)

// 写多个寄存器需要先构建ModbusMessage
ModbusMessage request;
request.add((uint8_t)1);              // slave address
request.add((uint8_t)WRITE_REGISTERS); // function code16 (0x10)
request.add((uint16_t)100);            // starting address
request.add((uint16_t)5);              // number of registers

// 添加数据值(每个uint16_t占2字节,大端序自动处理)
request.add((uint16_t)100);
request.add((uint16_t)200);
request.add((uint16_t)300);
request.add((uint16_t)400);
request.add((uint16_t)500);

// return response
Error err = MBclient.addRequest(0x0010, request);
if (err != SUCCESS) {
  Serial.printf("请求失败:%02X\n", err);
}

// write multiple coils(function code15)
ModbusMessage coilReq;
coilReq.add((uint8_t)1);
coilReq.add((uint8_t)WRITE_COILS);  // function code15 (0x0F)
coilReq.add((uint16_t)0);           // starting address
coilReq.add((uint16_t)8);           // 线圈数量
coilReq.add((uint8_t)1);            // byte count
coilReq.add((uint8_t)0xAA);         // Coil status:10101010
MBclient.addRequest(0x0011, coilReq);

三、ModbusMaster库详解(最简单入门)

ModbusMaster(4-20ma)是Arduino生态中最经典的Modbus RTU主站库,API简单直观,同步阻塞模式,适合初学者和简单项目。支持所有Arduino平台(Uno、Mega、ESP32、STM32等)。

3.1 安装与基础使用

// Arduino库管理器搜索 "ModbusMaster" 安装
// 或 PlatformIO: lib_deps = ModbusMaster

#include <ModbusMaster.h>

// 创建ModbusMaster实例(可创建多个实例对应不同从站)
ModbusMaster node;

void setup() {
  Serial.begin(115200);

  // 初始化:指定从站地址和串口
  // parameter1:From station address (1-247)
  // parameter2:用于通讯的Serial对象
  node.begin(1, Serial2);  // slave1,使用Serial2

  // 设置串口参数(必须与从站一致)
  Serial2.begin(9600, SERIAL_8N1);

  // 可选:设置超时(毫秒)
  node.setTimeout(1000);
}

void loop() {
  // ====== Read and hold register(function code03)======
  // parameter:starting address, Quantity
  // 返回:状态码(0=成功)
  uint8_t result = node.readHoldingRegisters(0, 10);

  if (result == node.ku8MBSuccess) {
    Serial.println("读取成功:");
    // 从响应缓冲区获取数据
    for (uint8_t i = 0; i < 10; i++) {
      uint16_t value = node.getResponseBuffer(i);
      Serial.printf("  register%d:%d\n", i, value);
    }
  } else {
    Serial.printf("Read failed,错误码:%02X\n", result);
  }

  // ====== Write to a single register(function code06)======
  result = node.writeSingleRegister(100, 1234);
  if (result == node.ku8MBSuccess) {
    Serial.println("写入寄存器成功");
  }

  // ====== Write into a single coil(function code05)======
  // 注意:线圈值用0xFF00表示ON,0x0000表示OFF
  result = node.writeSingleCoil(0, 0xFF00);  // 线圈0置ON
  result = node.writeSingleCoil(1, 0x0000);  // 线圈1置OFF

  delay(2000);
}

3.2 完整功能码示例

#include <ModbusMaster.h>
ModbusMaster node;

void setup() {
  Serial.begin(115200);
  Serial2.begin(9600, SERIAL_8N1);
  node.begin(1, Serial2);
}

void loop() {
  uint8_t result;

  // ====== function code01:读线圈 ======
  result = node.readCoils(0, 8);
  if (result == node.ku8MBSuccess) {
    // 线圈状态打包在寄存器缓冲区中
    // 每个uint16_t包含16个线圈状态
    uint16_t coilStates = node.getResponseBuffer(0);
    for (int i = 0; i < 8; i++) {
      Serial.printf("线圈%d:%s\n", i, (coilStates >> i) & 1 ? "ON" : "OFF");
    }
  }

  // ====== function code02:read discrete inputs ======
  result = node.readDiscreteInputs(0, 8);
  if (result == node.ku8MBSuccess) {
    uint16_t inputStates = node.getResponseBuffer(0);
    Serial.printf("离散输入:%04X\n", inputStates);
  }

  // ====== function code03:read holding registers ======
  result = node.readHoldingRegisters(0, 10);
  if (result == node.ku8MBSuccess) {
    for (int i = 0; i < 10; i++) {
      Serial.printf("保持寄存器%d:%d\n", i, node.getResponseBuffer(i));
    }
  }

  // ====== function code04:read input registers ======
  result = node.readInputRegisters(0, 4);
  if (result == node.ku8MBSuccess) {
    for (int i = 0; i < 4; i++) {
      Serial.printf("输入寄存器%d:%d\n", i, node.getResponseBuffer(i));
    }
  }

  // ====== function code05:write single coil ======
  node.writeSingleCoil(0, 0xFF00);  // ON
  node.writeSingleCoil(0, 0x0000);  // OFF

  // ====== function code06:write single register ======
  node.writeSingleRegister(50, 999);

  // ====== function code15:write multiple coils ======
  // 先设置发送缓冲区
  node.setTransmitBuffer(0, 0x00FF);  // 前8个线圈状态
  result = node.writeMultipleCoils(0, 8);  // starting address, Quantity

  // ====== function code16:write multiple registers ======
  node.setTransmitBuffer(0, 100);
  node.setTransmitBuffer(1, 200);
  node.setTransmitBuffer(2, 300);
  result = node.writeMultipleRegisters(100, 3);  // starting address, Quantity

  delay(3000);
}

3.3 ModbusMaster错误码

错误码常量名含义
0x00ku8MBSuccess成功
0x01ku8MBIllegalFunction非法功能码
0x02ku8MBIllegalDataAddressIllegal data address
0x03ku8MBIllegalDataValueIllegal data value
0x04ku8MBSlaveDeviceFailureSubstation equipment malfunction
0xE0ku8MBInvalidSlaveID无效从站ID
0xE1ku8MBInvalidFunction无效功能码
0xE2ku8MBResponseTimedOut响应超时
0xE3ku8MBInvalidCRCCRC checkError

四、数据类型处理

Modbus寄存器是16位的,但工业设备经常使用32位浮点数、32位整数等数据类型。以下提供完整的转换工具函数。

// ====== 32位浮点数解析(ABCD大端序,最常见)======
float parseFloatABCD(uint16_t regHigh, uint16_t regLow) {
  uint32_t raw = ((uint32_t)regHigh << 16) | regLow;
  float value;
  memcpy(&value, &raw, sizeof(float));
  return value;
}

// ====== 32位浮点数解析(CDAB字交换序)======
float parseFloatCDAB(uint16_t reg0, uint16_t reg1) {
  // reg0是低16位,reg1是高16位
  uint32_t raw = ((uint32_t)reg1 << 16) | reg0;
  float value;
  memcpy(&value, &raw, sizeof(float));
  return value;
}

// ====== 32位有符号整数(大端序)======
int32_t parseInt32ABCD(uint16_t regHigh, uint16_t regLow) {
  return (int32_t)(((uint32_t)regHigh << 16) | regLow);
}

// ====== 32位无符号整数(大端序)======
uint32_t parseUint32ABCD(uint16_t regHigh, uint16_t regLow) {
  return ((uint32_t)regHigh << 16) | regLow;
}

// ====== 16位有符号整数 ======
int16_t parseInt16(uint16_t reg) {
  return (int16_t)reg;
}

// ====== 带缩放的物理量 ======
float parseScaled(uint16_t reg, float scale) {
  return reg * scale;
}

// ====== 使用示例(ModbusMaster)======
void readTemperature() {
  uint8_t result = node.readHoldingRegisters(100, 2);  // 温度占2个寄存器
  if (result == node.ku8MBSuccess) {
    uint16_t reg0 = node.getResponseBuffer(0);
    uint16_t reg1 = node.getResponseBuffer(1);
    float temp = parseFloatABCD(reg0, reg1);
    Serial.printf("temperature:%.2f°C\n", temp);
  }
}

// ====== 使用示例(eModbus回调中)======
void handleData(ModbusMessage response, uint32_t token) {
  if (response.getFunctionCode() == READ_HOLD_REGISTER) {
    // 温度在寄存器100-101(响应中的第0-1个寄存器)
    uint16_t reg0 = (response[1] << 8) | response[2];
    uint16_t reg1 = (response[3] << 8) | response[4];
    float temp = parseFloatABCD(reg0, reg1);
    Serial.printf("temperature:%.2f°C\n", temp);
  }
}

五、Modbus TCP服务端(slave)

使用eModbus可以快速实现Modbus TCP从站设备,让ESP32模拟一个ModbusServer,供其他主站读取数据。

#include <WiFi.h>
#include "ModbusServerTCP.h"

ModbusServerTCP MBserver;

// 从站数据存储
#define MAX_REGISTERS 100
uint16_t holdingRegs[MAX_REGISTERS] = {0};
uint16_t inputRegs[MAX_REGISTERS] = {0};
bool coils[MAX_REGISTERS] = {false};
bool discreteInputs[MAX_REGISTERS] = {false};

// ====== 读保持寄存器回调 ======
ModbusMessage readHoldingRegs(ModbusMessage request) {
  uint16_t addr, count;
  request.get(2, addr);   // starting address(从第2字节开始)
  request.get(4, count);  // number of registers

  ModbusMessage response;
  response.add(request.getServerID());
  response.add((uint8_t)READ_HOLD_REGISTER);
  response.add((uint8_t)(count * 2));  // byte count

  for (uint16_t i = 0; i < count; i++) {
    if (addr + i < MAX_REGISTERS) {
      response.add(holdingRegs[addr + i]);
    } else {
      response.add((uint16_t)0);
    }
  }
  return response;
}

// ====== 写单个寄存器回调 ======
ModbusMessage writeSingleReg(ModbusMessage request) {
  uint16_t addr, value;
  request.get(2, addr);
  request.get(4, value);

  if (addr < MAX_REGISTERS) {
    holdingRegs[addr] = value;
    Serial.printf("写入寄存器%d:%d\n", addr, value);
  }

  // Response:回显地址和值
  ModbusMessage response;
  response.add(request.getServerID());
  response.add((uint8_t)WRITE_SINGLE_REGISTER);
  response.add(addr);
  response.add(value);
  return response;
}

// ====== 读线圈回调 ======
ModbusMessage readCoils(ModbusMessage request) {
  uint16_t addr, count;
  request.get(2, addr);
  request.get(4, count);

  uint8_t byteCount = (count + 7) / 8;
  ModbusMessage response;
  response.add(request.getServerID());
  response.add((uint8_t)READ_COIL);
  response.add(byteCount);

  for (uint8_t b = 0; b < byteCount; b++) {
    uint8_t coilByte = 0;
    for (uint8_t bit = 0; bit < 8; bit++) {
      uint16_t coilNum = addr + b * 8 + bit;
      if (coilNum < MAX_REGISTERS && coils[coilNum]) {
        coilByte |= (1 << bit);
      }
    }
    response.add(coilByte);
  }
  return response;
}

void setup() {
  Serial.begin(115200);

  // 初始化一些模拟数据
  holdingRegs[0] = 100;   // 温度设定值
  holdingRegs[1] = 250;   // 湿度设定值
  inputRegs[0] = 253;     // 当前温度(25.3°C)
  inputRegs[1] = 652;     // 当前湿度(65.2%)

  // connectWiFi
  WiFi.begin("your_ssid", "your_password");
  while (WiFi.status() != WL_CONNECTED) delay(500);
  Serial.printf("IPaddress:%s\n", WiFi.localIP().toString().c_str());

  // 注册功能码处理函数
  MBserver.registerWorker(1, READ_HOLD_REGISTER, &readHoldingRegs);
  MBserver.registerWorker(1, WRITE_SINGLE_REGISTER, &writeSingleReg);
  MBserver.registerWorker(1, READ_COIL, &readCoils);

  // 启动TCP服务端,监听502端口
  MBserver.start(502, 2, 2000);  // 端口, 最大连接数, 超时
  Serial.println("Modbus TCP服务端已启动,端口502");
}

void loop() {
  // 模拟传感器数据更新
  static unsigned long lastUpdate = 0;
  if (millis() - lastUpdate > 1000) {
    lastUpdate = millis();
    inputRegs[0] = 250 + random(-5, 5);  // 温度小幅波动
    inputRegs[1] = 650 + random(-10, 10); // 湿度小幅波动
  }
  delay(100);
}

六、RS485硬件设计指南

6.1 常用RS485模块对比

模块芯片自动收发隔离Price适用场景
MAX485模块MAX485否(需GPIO控制)2-5元简单实验、短距离
SP3485模块SP34853-6元3.3Vsystem、低功耗
隔离RS485模块ADM2483部分支持是(2500V)15-25元industrial site、强干扰
自动收发模块MAX485+硬件逻辑5-8元无需GPIO控制

6.2 接线规范

  • 总线拓扑:必须使用手拉手菊花链拓扑,禁止星型连接。分支线长度不超过0.3米
  • 终端电阻:总线两端各接一个120Ω电阻,中间节点不接。阻抗匹配可大幅减少信号反射
  • 线缆选择:使用屏蔽双绞线(STP),特性阻抗120Ω。A/B线必须绞合在一起
  • 接地:屏蔽层单端接地(通常在主控端),避免形成地环路
  • 共地:如果设备间地电位差超过1V,需要使用隔离模块或加共地线
  • 距离:9600波特率最大1200米,115200波特率最大100米。降低波特率可延长距离
  • 上拉下拉:A线接10kΩ上拉到VCC,B线接10kΩ下拉到GND,确保总线空闲时电平稳定

七、完整项目实战:ESP32 Modbus数据采集网关

以下是一个完整的工业数据采集网关项目:ESP32通过RS485读取多个温湿度传感器和电表数据,通过WiFi上传到MQTTServer,同时提供Modbus TCP服务供本地SCADAread。

#include <WiFi.h>
#include <PubSubClient.h>
#include "ModbusClientRTU.h"
#include "ModbusServerTCP.h"

// ====== 配置 ======
const char* WIFI_SSID = "your_ssid";
const char* WIFI_PASS = "your_password";
const char* MQTT_SERVER = "192.168.1.200";
const int MQTT_PORT = 1883;
const char* MQTT_TOPIC = "factory/sensor/data";

#define RTS_PIN 4
#define MODBUS_BAUD 9600

// ====== 设备定义 ======
struct Device {
  const char* name;
  uint8_t slaveAddr;
  uint16_t tempAddr;
  uint16_t humAddr;
};

Device devices[] = {
  {"车间A", 1, 0, 2},
  {"车间B", 2, 0, 2},
  {"仓库",  3, 0, 1},
};
const int DEVICE_COUNT = sizeof(devices) / sizeof(devices[0]);

// ====== 全局数据 ======
struct SensorData {
  float temperature;
  float humidity;
  bool valid;
};
SensorData sensorData[DEVICE_COUNT];

// ====== Modbus RTU客户端 ======
ModbusClientRTU MBrtu(RTS_PIN);
WiFiClient espClient;
PubSubClient mqtt(espClient);

// ====== 浮点数解析 ======
float parseFloat(uint16_t high, uint16_t low) {
  uint32_t raw = ((uint32_t)high << 16) | low;
  float f;
  memcpy(&f, &raw, 4);
  return f;
}

// ====== RTU数据回调 ======
void handleData(ModbusMessage response, uint32_t token) {
  int devIdx = token & 0xFF;  // 令牌低8位存设备索引

  if (response.getFunctionCode() == READ_HOLD_REGISTER) {
    uint16_t reg0 = (response[1] << 8) | response[2];
    uint16_t reg1 = (response[3] << 8) | response[4];

    // 判断是温度还是湿度(令牌高8位存类型:0=temperature, 1=湿度)
    uint8_t type = (token >> 8) & 0xFF;
    if (type == 0) {
      sensorData[devIdx].temperature = parseFloat(reg0, reg1);
    } else {
      sensorData[devIdx].humidity = reg0 * 0.1;  // 16位整数,分辨率0.1%
    }
    sensorData[devIdx].valid = true;
  }
}

void handleError(Error error, uint32_t token) {
  int devIdx = token & 0xFF;
  sensorData[devIdx].valid = false;
  ModbusError me(error);
  Serial.printf("device%dRead failed:%s\n", devIdx, (const char*)me);
}

// ====== MQTT重连 ======
void mqttReconnect() {
  while (!mqtt.connected()) {
    if (mqtt.connect("ESP32_Gateway")) {
      Serial.println("MQTTConnected");
    } else {
      delay(5000);
    }
  }
}

// ====== 上传数据 ======
void publishData() {
  String json = "{";
  for (int i = 0; i < DEVICE_COUNT; i++) {
    if (sensorData[i].valid) {
      if (i > 0) json += ",";
      json += "\"" + String(devices[i].name) + "\":{";
      json += "\"temp\":" + String(sensorData[i].temperature, 1);
      json += ",\"hum\":" + String(sensorData[i].humidity, 1);
      json += "}";
    }
  }
  json += "}";
  mqtt.publish(MQTT_TOPIC, json.c_str());
  Serial.printf("上传:%s\n", json.c_str());
}

void setup() {
  Serial.begin(115200);

  // WiFi
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) delay(500);
  Serial.printf("WiFiConnected:%s\n", WiFi.localIP().toString().c_str());

  // MQTT
  mqtt.setServer(MQTT_SERVER, MQTT_PORT);

  // Modbus RTU
  RTUutils::prepareHardwareSerial(Serial2);
  Serial2.begin(MODBUS_BAUD, SERIAL_8N1, 16, 17);
  MBrtu.onDataHandler(&handleData);
  MBrtu.onErrorHandler(&handleError);
  MBrtu.begin(Serial2);

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

void loop() {
  static unsigned long lastPoll = 0;
  static unsigned long lastPublish = 0;

  if (!mqtt.connected()) mqttReconnect();
  mqtt.loop();

  // 每3秒轮询所有设备
  if (millis() - lastPoll > 3000) {
    lastPoll = millis();
    for (int i = 0; i < DEVICE_COUNT; i++) {
      // 温度请求(令牌高8位=0,低8位=设备索引)
      MBrtu.addRequest((0 << 8) | i, devices[i].slaveAddr,
                       READ_HOLD_REGISTER, devices[i].tempAddr, 2);
      delay(100);  // RTU需要间隔
      // 湿度请求
      MBrtu.addRequest((1 << 8) | i, devices[i].slaveAddr,
                       READ_HOLD_REGISTER, devices[i].humAddr, 1);
      delay(100);
    }
  }

  // 每30秒上传一次
  if (millis() - lastPublish > 30000) {
    lastPublish = millis();
    publishData();
  }

  delay(100);
}

八、常见问题与排查

问题可能原因解决方法
RTU无响应A/B线接反交换A/B线,注意A对A、B对B
RTU无响应DE/RE引脚未控制ConfirmRTS_PIN接线正确,或使用自动收发模块
CRCError波特率不匹配确认主从站波特率、data bit、stop bit、校验位完全一致
CRCError串口缓冲区太小使用RTUutils::prepareHardwareSerial()增大缓冲区
TCP连接失败端口被占用ESP32作为客户端时确认服务器502端口开放
TCP连接失败防火墙拦截检查服务器防火墙,允许502端口入站
数据错乱字节序错误尝试ABCD/CDAB/DCBA三种浮点数解析方式
偶尔丢包总线干扰加终端电阻、使用屏蔽线、降低波特率
eModbus请求不发送队列满减少请求频率,addRequest之间加delay
ESP32重启看门狗超时避免在loop中长时间阻塞,使用异步回调

九、性能优化建议

  • 批量读取:一次读取连续的多个寄存器,比分多次读取快数倍。合理规划寄存器地址表
  • 请求间隔:RTU模式下,不同从站请求之间至少间隔50-100ms,避免总线冲突
  • 双核分配:ESP32可将Modbus通讯任务放在Core0,业务逻辑放在Core1,使用xTaskCreatePinnedToCore
  • 超时设置:eModbus默认超时可能过长,根据实际网络设置2-5秒
  • 数据缓存:变化缓慢的数据(如设备参数)可缓存,减少Modbus请求频率
  • 波特率选择:短距离(<100米)可用115200,长距离用9600。不要盲目追求高波特率
  • 队列深度:eModbus默认队列深度100,高频场景可适当增大
  • 内存优化:大量设备时避免使用String类,使用char数组减少内存碎片

十、学习路线与资源

阶段学习内容推荐库目标
入门Arduino基础、串口通讯、Modbus协议概念ModbusMaster能读取单个传感器
基础全部功能码、RS485接线、浮点数处理ModbusMaster能完成常见读写操作
进阶eModbus异步架构、TCP通讯、多设备轮询eModbus能开发稳定的采集系统
高级Modbus服务端、MQTT网关、OTA升级、工业级部署eModbus + ESP-IDF能开发工业级Modbus网关

10.1 推荐资源

  • eModbus官方文档:https://emodbus.github.io/
  • eModbussource code:https://github.com/eModbus/eModbus
  • ModbusMasterDocument:https://4-20ma.io/ModbusMaster/
  • Modbus协议规范:https://modbus.org/specs.php
  • ESP32官方文档:https://docs.espressif.com/
  • Arduino ModbusTutorial:https://docs.arduino.cc/tutorials/opta/getting-started-with-modbus-rtu/

Arduino和ESP32以其极低的成本和强大的生态,正在改变工业数据采集的格局。eModbus作为ESP32平台最强大的Modbus库,提供了异步非阻塞的TCP/RTU客户端和服务端实现,适合工业级项目;ModbusMaster则以其简单直观的API,是初学者的最佳选择。本文提供的工具函数和完整网关项目可直接用于实际开发,建议先在PC端用Modbus Slave模拟器测试,再接入真实设备。

📦

VIP专属:Arduino/ESP32 Modbus完整代码包

eModbus/ModbusMaster库完整示例:TCP/RTU主从站、网关机、WiFi配置,可直接烧录运行。

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

前往VIP资料库下载 → 月费仅9.9元 / 年费199元
技术术语(共 13 个)—— Click to Expand
Modbus RTU基于串行链路的ModbusProtocol,使用二进制编码和CRC check
Modbus TCP基于以太网的Modbus协议变体,使用TCP/IP传输
RS485工业常用的差分串行通信标准,支持多点通信
function codeModbus功能码指定读/写操作类型,如01读线圈、03读保持寄存器
registerModbus 寄存器存储数据单元,分线圈/离散输入/保持/输入寄存器四类
CRC check循环冗余校验,用于检测数据传输中的错误
SCADA数据采集与监视控制系统,用于远程监控工业过程
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 字数 18649 字 · 阅读约 47 分钟 更新 2026-09-17 永久链接 https://www.modbus.cn/53005.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