作为一名在工业自动化领域摸爬滚打多年的工程师,我深知Modbus协议在设备通信中的重要性。今天要分享的这个ESP32 Modbus RTU从站程序,是我在实际项目中反复打磨出来的成果,已经在气压检测、恒温控制等多个场景中稳定运行。与常见的库文件实现不同,这个程序采用纯子程序编写,代码结构清晰,注释详尽,特别适合需要深度定制或学习Modbus协议本质的开发者。
这个程序的核心价值在于:
提示:虽然ESP32有现成的Modbus库可用,但自己实现一遍能让你真正理解Modbus协议的精髓,在遇到复杂问题时也能更快定位和解决。
要运行这个Modbus RTU从站程序,你需要准备以下硬件:
硬件连接示意图:
code复制ESP32 MAX485模块
GPIO16 --- RO(接收输出)
GPIO17 --- DI(驱动输入)
GND --- GND
3.3V --- VCC(注意不是5V!)
A --- RS485总线A线
B --- RS485总线B线
code复制https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
程序开头定义了几个关键参数,需要根据实际应用调整:
cpp复制#define led 2 // 状态指示灯引脚
#define baudrate 19200 // Modbus通信波特率
#define slaveID 1 // 从站地址
#define modbusDataSize 101 // Modbus数据库大小
#define bufferSize 255 // 接收缓冲区大小
波特率选择建议:
初始化包括硬件初始化和Modbus协议栈初始化两部分:
cpp复制void setup() {
pinMode(led, OUTPUT); // 初始化LED引脚
Serial.begin(19200); // 调试串口
modbusRTU_INI(&Serial2); // 初始化Modbus通信端口
}
void modbusRTU_INI(HardwareSerial *SerialPort) {
mySerial = SerialPort;
mySerial->begin(baudrate, SERIAL_8N1, 16, 17); // 初始化Serial2
while(mySerial->available()) mySerial->read(); // 清空缓冲区
}
实际项目中的经验:
主循环承担着状态更新和数据处理的核心任务:
cpp复制void loop() {
unsigned long starttime = millis();
modbusRTU_slave(); // 处理Modbus通信
// 更新系统状态数据
lasttime = millis() - starttime;
modbusData[8] = lasttime; // 记录循环时间
modbusData[9] = ~modbusData[9]; // 状态标志取反
// 模拟数据变化
for(int i=0; i<100; i++) {
modbusData[i] = modbusData[i] + 1;
}
// LED控制逻辑
if(modbusData[9] > 5000) digitalWrite(led, HIGH);
else digitalWrite(led, LOW);
}
调试技巧:
Modbus RTU的时序控制是可靠通信的关键:
cpp复制// 计算1.5个字符时间
if(baudrate > 19200) intervalTime = 750;
else intervalTime = 15000000/baudrate;
// 数据接收逻辑
while(mySerial->available()) {
buffer[bufferCount] = mySerial->read();
bufferCount++;
lastReceiveTime = micros();
}
// 判断帧结束
if(micros() - lastReceiveTime > intervalTime && bufferCount > 0) {
processModbusFrame();
}
注意事项:
cpp复制if(function == 0x03) {
startAddress = (buffer[2] << 8) | buffer[3];
dataLength = (buffer[4] << 8) | buffer[5];
// 异常检查
if(dataLength > 125 || startAddress+dataLength > modbusDataSize) {
responseError(ID, function, 0x02);
return;
}
// 构造响应帧
response[0] = ID;
response[1] = function;
response[2] = dataLength * 2;
for(int i=0; i<dataLength; i++) {
response[3+i*2] = modbusData[startAddress+i] >> 8;
response[4+i*2] = modbusData[startAddress+i] & 0xFF;
}
// 计算并添加CRC
unsigned short crc = calculateCRC(response, 3+dataLength*2);
response[3+dataLength*2] = crc & 0xFF;
response[4+dataLength*2] = crc >> 8;
mySerial->write(response, 5+dataLength*2);
}
cpp复制if(function == 0x06) {
writeAddress = (buffer[2] << 8) | buffer[3];
writeValue = (buffer[4] << 8) | buffer[5];
if(writeAddress >= modbusDataSize) {
responseError(ID, function, 0x02);
return;
}
modbusData[writeAddress] = writeValue;
// 返回相同的帧作为应答
mySerial->write(buffer, bufferCount);
}
cpp复制if(function == 0x10) {
startAddress = (buffer[2] << 8) | buffer[3];
dataLength = (buffer[4] << 8) | buffer[5];
byteCount = buffer[6];
// 异常检查
if(dataLength > 123 || startAddress+dataLength > modbusDataSize
|| byteCount != dataLength*2) {
responseError(ID, function, 0x03);
return;
}
// 写入数据
for(int i=0; i<dataLength; i++) {
modbusData[startAddress+i] = (buffer[7+i*2] << 8) | buffer[8+i*2];
}
// 构造响应帧
response[0] = ID;
response[1] = function;
response[2] = buffer[2];
response[3] = buffer[3];
response[4] = buffer[4];
response[5] = buffer[5];
unsigned short crc = calculateCRC(response, 6);
response[6] = crc & 0xFF;
response[7] = crc >> 8;
mySerial->write(response, 8);
}
Modbus RTU采用CRC-16校验确保数据完整性:
cpp复制unsigned short calculateCRC(unsigned char* _regs, unsigned char arraySize) {
unsigned short _crc = 0xFFFF;
for(int pos = 0; pos < arraySize; pos++) {
_crc ^= (unsigned short)_regs[pos];
for(int i = 8; i != 0; i--) {
if((_crc & 0x0001) != 0) {
_crc >>= 1;
_crc ^= 0xA001;
} else {
_crc >>= 1;
}
}
}
return _crc;
}
调试中发现的关键点:
典型数据映射方案:
code复制Modbus地址 变量 功能
4x0001 currentTemp 当前温度(×10,保留1位小数)
4x0002 targetTemp 目标温度(×10)
4x0003 heaterState 加热器状态(0关1开)
4x0004 fanSpeed 风机转速(0-100%)
通信参数配置:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 通信完全无响应 | 接线错误/波特率不匹配 | 检查A/B线是否接反,确认主从站波特率一致 |
| CRC校验失败 | 线路干扰/时序问题 | 添加终端电阻,降低波特率,检查1.5字符时间计算 |
| 部分功能码不响应 | 功能码未实现/地址越界 | 检查程序支持的功能码,确认数据地址范围 |
| 随机通信中断 | 电源干扰/ESD问题 | 增加电源滤波,RS485模块加TVS二极管保护 |
对于大数据量传输:
提高实时性:
增强可靠性:
Modbus协议允许使用功能码65-72和100-110作为自定义功能码。例如实现一个设备重启功能:
cpp复制if(function == 0x41) { // 自定义功能码0x41
// 安全检查
if(buffer[2] == 0xDE && buffer[3] == 0xAD) {
ESP.restart();
}
}
通过修改slaveID处理逻辑,实现设备地址动态切换:
cpp复制// 在setup()中读取EEPROM存储的地址
slaveID = EEPROM.read(0);
if(slaveID == 0xFF) slaveID = 1; // 默认值
// 添加特殊功能码用于修改地址
if(function == 0x42 && buffer[2] == 0x55) {
newID = buffer[3];
if(newID >=1 && newID <=247) {
EEPROM.write(0, newID);
EEPROM.commit();
slaveID = newID;
}
}
利用ESP32的双核特性,可以同时实现RTU和TCP协议转换:
cpp复制// 在核心0运行Modbus RTU从站
void loop() {
modbusRTU_slave();
}
// 在核心1运行Modbus TCP服务器
TaskHandle_t tcpTask;
void tcpServer(void *pv) {
// 实现TCP服务器逻辑
}
void setup() {
xTaskCreatePinnedToCore(tcpServer, "TCP Server", 8192, NULL, 1, &tcpTask, 1);
}
这个ESP32 Modbus RTU从站程序虽然代码量不大,但包含了工业通信中的诸多关键细节。在实际项目中,通信稳定性往往取决于这些细节的正确处理。建议在理解基本原理后,根据具体应用场景调整参数和功能,必要时可以增加诊断寄存器、通信统计等功能,便于现场调试和维护。