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 三大主流库对比
| 库名 | 适用平台 | 프로토콜 | 모드 | 服务端 | 特点 |
|---|---|---|---|---|---|
| eModbus | ESP32专用 | TCP/RTU | 异步(回调) | 支持 | 最强大,非阻塞,多请求队列 |
| ModbusMaster | 所有Arduino | RTU | 同步(阻塞) | 不支持 | 最简单,API直观,适合初学者 |
| Arduino_ESP32_ Modbus | ESP32 | TCP/RTU | 同步 | 支持 | 乐鑫官方,ESP-IDF底层 |
| Modbus-Master-Slave | 所有Arduino | RTU | 同步 | 支持 | 主从一体,轻量级 |
二、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)
// - 이더넷.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 handle데이터(ModbusMessage response, uint32_t token) {
Serial.printf("收到响应 - 服务器ID:%d, 기능 코드:%d, 令牌:%08X, 데이터길이:%d바이트\n",
response.getServerID(), response.get기능Code(), token, response.size());
// 분석유지 레지스터읽기响应(기능 코드03)
if (response.get기능Code() == 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.get기능Code() == 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.get기능Code() == 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 handle오답(오답 error, uint32_t token) {
Modbus오답 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.on데이터Handler(&handle데이터); // 设置데이터回调
MBclient.on오답Handler(&handle오답); // 设置错误回调
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:유지 레지스터 읽기 (FC03)(기능 코드03)======
// 매개변수:令牌, 服务器ID(Unit ID), 기능 코드, 시작주소, 数量
오답 err = MBclient.addRequest(
TOKEN_READ_HOLDING, // 自定义令牌
1, // 슬레이브 주소(Unit ID)
READ_HOLD_REGISTER, // 기능 코드03
0, // 시작주소
10 // 읽기10个레지스터
);
if (err != SUCCESS) {
Modbus오답 e(err);
Serial.printf("创建请求失败:%02X - %s\n", err, (const char*)e);
}
// ====== 请求2:읽기코일(기능 코드01)======
MBclient.addRequest(TOKEN_READ_COILS, 1, READ_COIL, 0, 8);
// ====== 请求3:단일 레지스터 쓰기 (FC06)(기능 코드06)======
MBclient.addRequest(TOKEN_WRITE_REG, 1, WRITE_SINGLE_REGISTER, 100, 1234);
// 注意:addRequest是异步非阻塞的,바로返回
// 响应通过handle데이터/handle오답回调处理
}
delay(100);
}
2.3 eModbus기능 코드常量速查
| 기능 코드常量 | 10진수 | 功能 | addRequest매개변수 |
|---|---|---|---|
| READ_COIL | 01 | 读코일 | (token, server, FC, addr, count) |
| READ_DISCR_INPUT | 02 | 读이산 입력 | (token, server, FC, addr, count) |
| READ_HOLD_REGISTER | 03 | 读유지 레지스터 | (token, server, FC, addr, count) |
| READ_INPUT_REGISTER | 04 | 读입력 레지스터 | (token, server, FC, addr, count) |
| WRITE_COIL | 05 | 写单个코일 | (token, server, FC, addr, 0xFF00/0x0000) |
| WRITE_REGISTER | 06 | 写单个레지스터 | (token, server, FC, addr, value) |
| WRITE_COILS | 15 | 写多个코일 | (token, server, FC, addr, count, 데이터...) |
| WRITE_REGISTERS | 16 | 写多个레지스터 | (token, server, FC, addr, count, 데이터...) |
| READ_WRITE_REGISTERS | 23 | 读写多个레지스터 | 复杂매개변수 |
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 handle데이터(ModbusMessage response, uint32_t token) {
Serial.printf("RTU响应 - FC:%d, 길이:%d\n", response.get기능Code(), response.size());
if (response.get기능Code() == 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 handle오답(오답 error, uint32_t token) {
Modbus오답 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.on데이터Handler(&handle데이터);
MBrtu.on오답Handler(&handle오답);
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);
// 보내기请求
오답 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() {
// ====== 유지 레지스터 읽기 (FC03)(기능 코드03)======
// 매개변수:시작주소, 数量
// 返回:状态码(0=成功)
uint8_t result = node.readHolding회원가입s(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);
}
// ====== 단일 레지스터 쓰기 (FC06)(기능 코드06)======
result = node.writeSingle회원가입(100, 1234);
if (result == node.ku8MBSuccess) {
Serial.println("쓰기레지스터成功");
}
// ====== 단일 코일 쓰기 (FC05)(기능 코드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.readHolding회원가입s(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.readInput회원가입s(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.writeSingle회원가입(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.writeMultiple회원가입s(100, 3); // 시작주소, 数量
delay(3000);
}
3.3 ModbusMaster错误码
| 错误码 | 常量名 | 含义 |
|---|---|---|
| 0x00 | ku8MBSuccess | 成功 |
| 0x01 | ku8MBIllegal기능 | 非法기능 코드 |
| 0x02 | ku8MBIllegal데이터주소 | 非法데이터주소 |
| 0x03 | ku8MBIllegal데이터Value | 非法데이터值 |
| 0x04 | ku8MBSlaveDeviceFailure | 슬레이브设备故障 |
| 0xE0 | ku8MBInvalidSlaveID | 无效슬레이브ID |
| 0xE1 | ku8MBInvalid기능 | 无效기능 코드 |
| 0xE2 | ku8MBResponseTimedOut | 响应超时 |
| 0xE3 | ku8MBInvalidCRC | CRC 검증错误 |
四、데이터유형处理
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.readHolding회원가입s(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 handle데이터(ModbusMessage response, uint32_t token) {
if (response.get기능Code() == 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);
}
}
응답 반환;
}
// ====== 写单个레지스터回调 ======
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);
응답 반환;
}
// ====== 读코일回调 ======
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);
}
응답 반환;
}
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.시작(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模块 | SP3485 | 否 | 否 | 3-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/데이터";
#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 Sensor데이터 {
float temperature;
float humidity;
bool valid;
};
Sensor데이터 sensor데이터[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 handle데이터(ModbusMessage response, uint32_t token) {
int devIdx = token & 0xFF; // 令牌低8位存设备索引
if (response.get기능Code() == 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) {
sensor데이터[devIdx].temperature = parseFloat(reg0, reg1);
} else {
sensor데이터[devIdx].humidity = reg0 * 0.1; // 16位整数,分辨率0.1%
}
sensor데이터[devIdx].valid = true;
}
}
void handle오답(오답 error, uint32_t token) {
int devIdx = token & 0xFF;
sensor데이터[devIdx].valid = false;
Modbus오답 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 publish데이터() {
String json = "{";
for (int i = 0; i < DEVICE_COUNT; i++) {
if (sensor데이터[i].valid) {
if (i > 0) json += ",";
json += "\"" + String(devices[i].name) + "\":{";
json += "\"temp\":" + String(sensor데이터[i].temperature, 1);
json += ",\"hum\":" + String(sensor데이터[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.on데이터Handler(&handle데이터);
MBrtu.on오답Handler(&handle오답);
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();
publish데이터();
}
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-시작ed-with-modbus-rtu/
Arduino和ESP32以其极低的成本和强大的生态,正在改变工业데이터采集的格局。eModbus作为ESP32平台最强大的Modbus库,提供了异步非阻塞的TCP/RTU클라이언트和服务端实现,适合산업용项目;ModbusMaster则以其简单直观的API,是初学者的最佳选择。本文提供的도구函数和完整게이트웨이项目可直接用于实际개발,建议先在PC端用Modbus Slave模拟器测试,再接入真实设备。
VIP专属:Arduino/ESP32 Modbus完整代码包
eModbus/ModbusMaster库完整예제:TCP/RTU主슬레이브、게이트웨이机、WiFi설정,可直接烧录运行。
开通VIP即可下载完整代码,同时解锁 30+ 엔지니어링实战자료包:디버깅스크립트、速查表、项目模板、排查案例……
前往VIP자료库下载 → 月费仅9.9元 / 年费199元