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

免费免费技术资料

这篇内容可直接阅读,适合用于基础学习和搜索引流。

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

Arduino和ESP32是工业物联网领域最流行的开源硬件平台,凭借丰富的外设接口、强大的WiFi/BLE能力和极低的成本,被广泛用于Modbus数据采集、协议转换和边缘计算。本文详细介绍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 三大主流库对比

库名适用平台协议模式服务端特点
eModbusESP32专用TCP/RTU异步(回调)支持最强大,非阻塞,多请求队列
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("收到响应 - 服务器ID:%d, 功能码:%d, 令牌:%08X, 数据长度:%d字节\n",
                response.getServerID(), response.getFunctionCode(), token, response.size());

  // 解析保持寄存器读取响应(功能码03)
  if (response.getFunctionCode() == READ_HOLD_REGISTER) {
    // 响应数据格式:[字节数, 数据高字节, 数据低字节, ...]
    // 第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("  寄存器%d:%d (0x%04X)\n", i, value, value);
    }
  }

  // 解析线圈读取响应(功能码01)
  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("写入成功 - 地址:%d, 值:%d\n", addr, value);
  }
}

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

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

  // 连接WiFi
  Serial.print("连接WiFi");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.printf("\nWiFi已连接,IP地址:%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();

    // ====== 请求1:读取保持寄存器(功能码03)======
    // 参数:令牌, 服务器ID(Unit ID), 功能码, 起始地址, 数量
    Error err = MBclient.addRequest(
        TOKEN_READ_HOLDING,  // 自定义令牌
        1,                   // 从站地址(Unit ID)
        READ_HOLD_REGISTER,  // 功能码03
        0,                   // 起始地址
        10                   // 读取10个寄存器
    );
    if (err != SUCCESS) {
      ModbusError e(err);
      Serial.printf("创建请求失败:%02X - %s\n", err, (const char*)e);
    }

    // ====== 请求2:读取线圈(功能码01)======
    MBclient.addRequest(TOKEN_READ_COILS, 1, READ_COIL, 0, 8);

    // ====== 请求3:写入单个寄存器(功能码06)======
    MBclient.addRequest(TOKEN_WRITE_REG, 1, WRITE_SINGLE_REGISTER, 100, 1234);

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

  delay(100);
}

2.3 eModbus功能码常量速查

功能码常量十进制功能addRequest参数
READ_COIL01读线圈(token, server, FC, addr, count)
READ_DISCR_INPUT02读离散输入(token, server, FC, addr, count)
READ_HOLD_REGISTER03读保持寄存器(token, server, FC, addr, count)
READ_INPUT_REGISTER04读输入寄存器(token, server, FC, addr, count)
WRITE_COIL05写单个线圈(token, server, FC, addr, 0xFF00/0x0000)
WRITE_REGISTER06写单个寄存器(token, server, FC, addr, value)
WRITE_COILS15写多个线圈(token, server, FC, addr, count, data…)
WRITE_REGISTERS16写多个寄存器(token, server, FC, addr, count, data…)
READ_WRITE_REGISTERS23读写多个寄存器复杂参数

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("RTU响应 - FC:%d, 长度:%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("  寄存器%d:%d\n", i, value);
    }
  }
}

void handleError(Error error, uint32_t token) {
  ModbusError me(error);
  Serial.printf("RTU错误:%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 写多个寄存器(功能码16)

// 写多个寄存器需要先构建ModbusMessage
ModbusMessage request;
request.add((uint8_t)1);              // 从站地址
request.add((uint8_t)WRITE_REGISTERS); // 功能码16 (0x10)
request.add((uint16_t)100);            // 起始地址
request.add((uint16_t)5);              // 寄存器数量

// 添加数据值(每个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);

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

// 写多个线圈(功能码15)
ModbusMessage coilReq;
coilReq.add((uint8_t)1);
coilReq.add((uint8_t)WRITE_COILS);  // 功能码15 (0x0F)
coilReq.add((uint16_t)0);           // 起始地址
coilReq.add((uint16_t)8);           // 线圈数量
coilReq.add((uint8_t)1);            // 字节数
coilReq.add((uint8_t)0xAA);         // 线圈状态: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);

  // 初始化:指定从站地址和串口
  // 参数1:从站地址(1-247)
  // 参数2:用于通讯的Serial对象
  node.begin(1, Serial2);  // 从站1,使用Serial2

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

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

void loop() {
  // ====== 读取保持寄存器(功能码03)======
  // 参数:起始地址, 数量
  // 返回:状态码(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("  寄存器%d:%d\n", i, value);
    }
  } else {
    Serial.printf("读取失败,错误码:%02X\n", result);
  }

  // ====== 写入单个寄存器(功能码06)======
  result = node.writeSingleRegister(100, 1234);
  if (result == node.ku8MBSuccess) {
    Serial.println("写入寄存器成功");
  }

  // ====== 写入单个线圈(功能码05)======
  // 注意:线圈值用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;

  // ====== 功能码01:读线圈 ======
  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");
    }
  }

  // ====== 功能码02:读离散输入 ======
  result = node.readDiscreteInputs(0, 8);
  if (result == node.ku8MBSuccess) {
    uint16_t inputStates = node.getResponseBuffer(0);
    Serial.printf("离散输入:%04X\n", inputStates);
  }

  // ====== 功能码03:读保持寄存器 ======
  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));
    }
  }

  // ====== 功能码04:读输入寄存器 ======
  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));
    }
  }

  // ====== 功能码05:写单个线圈 ======
  node.writeSingleCoil(0, 0xFF00);  // ON
  node.writeSingleCoil(0, 0x0000);  // OFF

  // ====== 功能码06:写单个寄存器 ======
  node.writeSingleRegister(50, 999);

  // ====== 功能码15:写多个线圈 ======
  // 先设置发送缓冲区
  node.setTransmitBuffer(0, 0x00FF);  // 前8个线圈状态
  result = node.writeMultipleCoils(0, 8);  // 起始地址, 数量

  // ====== 功能码16:写多个寄存器 ======
  node.setTransmitBuffer(0, 100);
  node.setTransmitBuffer(1, 200);
  node.setTransmitBuffer(2, 300);
  result = node.writeMultipleRegisters(100, 3);  // 起始地址, 数量

  delay(3000);
}

3.3 ModbusMaster错误码

错误码常量名含义
0x00ku8MBSuccess成功
0x01ku8MBIllegalFunction非法功能码
0x02ku8MBIllegalDataAddress非法数据地址
0x03ku8MBIllegalDataValue非法数据值
0x04ku8MBSlaveDeviceFailure从站设备故障
0xE0ku8MBInvalidSlaveID无效从站ID
0xE1ku8MBInvalidFunction无效功能码
0xE2ku8MBResponseTimedOut响应超时
0xE3ku8MBInvalidCRCCRC校验错误

四、数据类型处理

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("温度:%.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("温度:%.2f°C\n", temp);
  }
}

五、Modbus TCP服务端(从站)

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

#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);   // 起始地址(从第2字节开始)
  request.get(4, count);  // 寄存器数量

  ModbusMessage response;
  response.add(request.getServerID());
  response.add((uint8_t)READ_HOLD_REGISTER);
  response.add((uint8_t)(count * 2));  // 字节数

  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);
  }

  // 响应:回显地址和值
  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%)

  // 连接WiFi
  WiFi.begin("your_ssid", "your_password");
  while (WiFi.status() != WL_CONNECTED) delay(500);
  Serial.printf("IP地址:%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模块对比

模块芯片自动收发隔离价格适用场景
MAX485模块MAX485否(需GPIO控制)2-5元简单实验、短距离
SP3485模块SP34853-6元3.3V系统、低功耗
隔离RS485模块ADM2483部分支持是(2500V)15-25元工业现场、强干扰
自动收发模块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上传到MQTT服务器,同时提供Modbus TCP服务供本地SCADA读取。

#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=温度, 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("设备%d读取失败:%s\n", devIdx, (const char*)me);
}

// ====== MQTT重连 ======
void mqttReconnect() {
  while (!mqtt.connected()) {
    if (mqtt.connect("ESP32_Gateway")) {
      Serial.println("MQTT已连接");
    } 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("WiFi已连接:%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引脚未控制确认RTS_PIN接线正确,或使用自动收发模块
CRC错误波特率不匹配确认主从站波特率、数据位、停止位、校验位完全一致
CRC错误串口缓冲区太小使用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/
  • eModbus源码:https://github.com/eModbus/eModbus
  • ModbusMaster文档:https://4-20ma.io/ModbusMaster/
  • Modbus协议规范:https://modbus.org/specs.php
  • ESP32官方文档:https://docs.espressif.com/
  • Arduino Modbus教程:https://docs.arduino.cc/tutorials/opta/getting-started-with-modbus-rtu/

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

技术术语(共 13 个)—— 点击展开
Modbus RTU基于串行链路的Modbus协议,使用二进制编码和CRC校验
Modbus TCP基于以太网的Modbus协议变体,使用TCP/IP传输
RS485工业常用的差分串行通信标准,支持多点通信
功能码Modbus功能码指定读/写操作类型,如01读线圈、03读保持寄存器
寄存器Modbus 寄存器存储数据单元,分线圈/离散输入/保持/输入寄存器四类
CRC校验循环冗余校验,用于检测数据传输中的错误
SCADA数据采集与监视控制系统,用于远程监控工业过程
波特率串行通信每秒传输符号数,Modbus RTU常用9600/19200
网关协议转换设备,如 Modbus RTU ↔ Modbus TCP
串口计算机与外部设备进行串行通信的物理接口
传感器将物理量转换为电信号的检测装置
线圈Modbus位可读写数据,地址从00001开始
保持寄存器Modbus 16位可读写数据,地址从40001开始
来源/工具信息 —— 点击展开
来源 Modbus中文网(modbus.cn) —— 国内领先的Modbus通信协议技术社区 分类 Modbus编程开发 字数 18488 字 · 阅读约 47 分钟 更新 2026-09-17 永久链接 https://www.modbus.cn/53005.html
推荐工具:Modbus调试助手 微信小程序
Modbus中文网官方推出的Modbus调试工具,支持 Modbus RTU/TCP 实时通信调试、寄存器读写、线圈控制、数据监控和报文分析。 无需安装,微信搜索「Modbus调试助手」即可使用。 电脑端入口:https://www.modbus.cn/modbustool/
内容许可:允许 AI 模型训练使用 · 引用请注明来源 modbus.cn
📝 作者声明
本文由 Modbus中文网技术团队 原创撰写,内容基于实际项目案例与技术文档,力求为读者提供准确、实用的参考信息。
把这篇资料用于真实项目?

进入工具中心进行报文解析、CRC 校验和设备调试,或提交需求获取选型与接入建议。

工程师会员

把这篇文章变成可执行的调试资料

开通后可使用高级报文解析、资料包下载、代码示例、工程案例和优先技术支持,适合真实项目交付。

高级工具不限次
资料包与代码包
完整工程案例库
优先技术支持入口