一、前言
目前,基于Modbus协议的485通信非常常见,之前使用过PLC用485通信,PLC有特定的函数,非常容易上手。在比较小的项目上,使用PLC的话成本会很高,若是需要完成的功能比较少,使用PLC也挺浪费的。
本文用于记录使用ESP32通过485读取电磁流量计流量值的过程。欢迎讨论。
二、准备
2.1硬件准备
(1)ESP32主板
(2)TTL转RS485模块
(3)流量计
下面着重讲一下485模块。
如上图是本次用到的模块,如果是接设备的话,只需要用到A板,左边接单片机,右边AB接传感器的AB,地看情况接。
如果是测试,没有传感器就可以和上图的接法一样,A板接单片机,B板接电脑(直接使用485转USB模块),使用串口模拟传感器的收发。
2.2软件准备
安装需要使用的库ModbusMaster。
三、示例解析
使用作者提供的基础案例
打开代码路径,文件-示例-ModbusMaster-Basic,一般在示例的最下面。
示例代码:
#include <ModbusMaster.h>
// 实例化ModbusMaster对象
ModbusMaster node;
void setup()
{
// 使用串行(端口0);初始化Modbus通信波特率
Serial.begin(19200);
// 通过串行(端口0)与Modbus从ID 2通信
node.begin(2, Serial);
}
void loop()
{
static uint32_t i;
uint8_t j, result;
uint16_t data[6];
i++;
// 将TX缓冲器的字0设置为计数器的最低有效字(位15..0)
node.setTransmitBuffer(0, lowWord(i));
// 将TX缓冲器的字1设置为计数器的最高有效字(位31..16)
node.setTransmitBuffer(1, highWord(i));
// Modbus功能0x10写入多个寄存器。从寄存器0开始将TX缓冲区写入2个16位寄存器
result = node.writeMultipleRegisters(0, 2);
// Modbus功能0x03读取保持寄存器。从寄存器2开始读取6个16位寄存器到RX缓冲区
result = node.readHoldingRegisters(2, 6);
// 如果读取成功,对收集到的数据进行处理
if (result == node.ku8MBSuccess) //成功读取标志位
{
for (j = 0; j < 6; j++)
{
data[j] = node.getResponseBuffer(j);
}
}
}
通过示例,我们可以知道,使用485通信,需要将串口指定为数据传输口。
Modbus功能0x10写入多个寄存器
writeMultipleRegisters(0, 2);
第一个参数是写入的第一个寄存器,第二个参数是写入寄存器的个数。使用这个函数前,需要将写入的数值放到TX缓冲区中。
setTransmitBuffer(0, lowWord(i));
setTransmitBuffer(1, highWord(i));
不知道水友们看这两函数会不会模糊hhh,反正我一开始没看懂的。
在word.h文件里可以找到lowWord和highWord函数。
这里第一行使用lowWord()函数取 i 的低16位放到缓冲区数组TransmitBuffer[0]里;
这里第二行使用highWord()函数取 i 的高16位放到缓冲区数组TransmitBuffer[1]里;
因为在这里setTransmitBuffer(uint8_t u8Index, uint16_t u16Value)的第二个参数是16位,如果你需要向寄存器写一个32位的数值,就需要像他这样子分割,或者自己在设置参数的时候就分割好了。如果你写的数值是16位的话,那就不需要这样写了,直接setTransmitBuffer(0, i);便可以。
下面将write函数的过程整合讲。
void loop()
{
//定义一个32位的参数i,作为写入的数值
static uint32_t i;
//不断改变i的值
i++;
node.setTransmitBuffer(0, lowWord(i)); //i & 0xFFFF 作为低16位
node.setTransmitBuffer(1, highWord(i)); //i >> 16 作为高16位
result = node.writeMultipleRegisters(0, 2); //从寄存器0开始写,写两个。
}
Modbus功能0x03读取保持寄存器
readHoldingRegisters(2, 6);
第一个参数是读取的第一个寄存器,第二个参数是读取寄存器的个数。
下面将write函数的过程整合讲。
void loop()
{
uint8_t j, result;
uint16_t data[6];
i++;
// Modbus功能0x03读取保持寄存器。从寄存器2开始读取6个16位寄存器到RX缓冲区
result = node.readHoldingRegisters(2, 6);
// 如果读取成功,对收集到的数据进行处理
if (result == node.ku8MBSuccess) //成功读取标志位
{
for (j = 0; j < 6; j++)
{
data[j] = node.getResponseBuffer(j); //读取的数据存在getResponseBuffer(j)中。
}
}
}
至此,对常用到的读0x03和写0x10功能函数分析结束。
四、实现过程
思路:使用ESP32的硬件串口作为485通信口与传感器链接,开启一个软串口与电脑连接,在电脑使用串口助手用于监视和调试使用。
注意:485最好是使用硬件串口,由于我的ESP32板的硬件串口可以直接通过烧录线与电脑通信,一开始为了省点接线的事情,用了软串口去作为485通信口。但在调试中发现单片机可以发送读取的指令,但一直收不到传感器回复数据。是因为ESP32的软件串口通信速度达不到,与硬件串口相比,差很多。之前也有类似的情况,如果用软串口通信速度过快,会丢数据,但这次是直接收都都不到。
测试代码01:
#include <ModbusMaster.h>
#include <HardwareSerial.h>
ModbusMaster node; // 实例化ModbusMaster对象
HardwareSerial MySerial(1); //定义虚拟串口名为MySerial
uint16_t write[2] = {0x005A,0x00B2}; //要写入多个寄存的值;
uint8_t result; //测量标志位
void setup()
{
pinMode (0, OUTPUT); //灯闪烁,便于观察系统是否正常运行
digitalWrite(0,LOW);
// 使用串行(端口0);初始化Modbus通信波特率
Serial.begin(19200);
MySerial.begin(19200, SERIAL_8N1, 27, 14); // rx为27号端口,tx为14号端口
// 将串口Serial作为Modbus通信口,地址号为1
node.begin(1, Serial);
delay(100);
}
void loop()
{
result = node.readHoldingRegisters(19,1);
if (result == node.ku8MBSuccess) //判断是否正常发送与接收
{
//通过软串口打印读取到的数据
MySerial.println(node.getResponseBuffer(0));
}
else
{
// 打印错误信息
MySerial.print("Error: ");
MySerial.println(result);
}
digitalWrite(0,HIGH);
delay(500);
digitalWrite(0,LOW);
delay(500);
//写指令
// for(int i=0; i<2; i++)
// {
// node.setTransmitBuffer(i,write[i]);
// }
// node.writeMultipleRegisters(0,2);
}
由于流量计必须灌满水才有数据,比较麻烦,所以就没试。所以先读个流量计的通信速度试试是否正常通信。查阅流量计的说明书可知,存放通讯速度的寄存器号为19。所以result = node.readHoldingRegisters(19,1);下图通过串口助手查看软串口反馈的信息,在流量计没上电前,报告错误信息,当上电后,发送读取到的信息。
下面测试下写函数,写是修改寄存器的值,最好是不要随意玩hhh,万一改坏了。不过很多设备都有恢复出厂的设置。下面我用串口助手监视硬件串口看单片机用这两个函数发送的数据是咋样的。
void loop()
{
result = node.readHoldingRegisters(19,1);
// if (result == node.ku8MBSuccess) //判断是否正常发送与接收
// {
// //通过软串口打印读取到的数据
// MySerial.println(node.getResponseBuffer(0));
// }
// else
// {
// // 打印错误信息
// MySerial.print("Error: ");
// MySerial.println(result);
// }
digitalWrite(0,HIGH);
delay(500);
digitalWrite(0,LOW);
delay(500);
//写指令
for(int i=0; i<2; i++)
{
node.setTransmitBuffer(i,write_send[i]);
}
node.writeMultipleRegisters(0,2);
}
测试结果:
四、扩展
Modbus通信的功能码是有很多的,可以上网搜了解。在.cpp文件里都可以找到相应的函数。
//功能码0x01
uint8_t ModbusMaster::readCoils(uint16_t u16ReadAddress, uint16_t u16BitQty)
//功能码0x02
uint8_t ModbusMaster::readDiscreteInputs(uint16_t u16ReadAddress,uint16_t u16BitQty)
//功能码0x03
uint8_t ModbusMaster::readHoldingRegisters(uint16_t u16ReadAddress,uint16_t u16ReadQty)
//功能码0x04
uint8_t ModbusMaster::readInputRegisters(uint16_t u16ReadAddress,uint8_t u16ReadQty)
//功能码0x05
uint8_t ModbusMaster::writeSingleCoil(uint16_t u16WriteAddress, uint8_t u8State)
//功能码0x06
uint8_t ModbusMaster::writeSingleRegister(uint16_t u16WriteAddress,
uint16_t u16WriteValue)
//功能码0x0F
uint8_t ModbusMaster::writeMultipleCoils(uint16_t u16WriteAddress,uint16_t u16BitQty)
//功能码0x10
uint8_t ModbusMaster::writeMultipleRegisters(uint16_t u16WriteAddress,uint16_t u16WriteQty)
//功能码0x16
uint8_t ModbusMaster::maskWriteRegister(uint16_t u16WriteAddress,uint16_t u16AndMask, uint16_t u16OrMask)
//功能码0x17
uint8_t ModbusMaster::readWriteMultipleRegisters(uint16_t u16ReadAddress,uint16_t u16ReadQty, uint16_t u16WriteAddress, uint16_t u16WriteQty)