- tcp协议-socket通信采集见:基于ModbusTcp协议的Java Socket通信 报文编码格式与数据采集过程详解(上)
- 基于ModbusTcp协议的Java Socket通信 报文编码格式与数据采集过程详解(下)
Modbus协议使用串口传输时可以选择RTU或者ASCII模式,并规定了消息、数据结构、命令和应答方式,且需要对数据进行校验。ASCII模式采用LRC校验,RTU模式采用16位CRC校验。通过以太网传输时使用TCP,这种模式下不使用校验,因为TCP协议是一个面向连接的可靠协议。
ModbusRTU如何判断开始与结束
ModbusRTU协议中,需要用时间间隔来判断一帧报文的开始和结束,协议规定的时间为3.5个字符周期。在一帧报文开始前,必须有大于3.5个字符周期的空闲时间,一帧报文结束后,也必须要有3.5个字符周期的空闲时间,否则就会出现粘包的请况。3.5个字符周期是一个具体时间,与波特率有关。
整个报文帧必须以连续的字符流发送。如果两个字符之间的空闲间隔大于 1.5 个字符时间,则报文帧被认为不完整应该被接收节点丢弃。
CRC循环冗余校验
在 RTU 模式包含一个对全部报文内容执行的,基于循环冗余校验 (CRC - Cyclical Redundancy Checking) 算法的错误检验域。CRC 域检验整个报文的内容。不管报文有无奇偶校验,均执行此检验。
1)CRC有16位,由两个8字节组成
2)CRC附加在报文最后面,先附加低字节,再附加高字节。
3)附加在报文后面的 CRC 的值由发送设备计算。接收设备在接收报文时重新计算 CRC 的值,并将计算结果于实际接收到的CRC 值相比较。如果两个值不相等,则为错误。
ModbusRTU在串行链路中的报文格式
从站地址(1 byte)+功能码(1 byte)+数据区(N bytes)+校验码(2 bytes)
从站地址:一个字节,作用是索引
功能码:一个字节,表明读写功能
数据:通信所传输的数据,可以是多字节
校验:判断接收的数据在传输过程中是否有损失,两个字节
读取输出/保持寄存器:
CRC工具类:
public class CRC {
/**
* 获取源数据和验证码的组合byte数组
*/
public static byte[] getData(byte[] aa) {
byte[] bb = getCrc16(aa);
byte[] cc = new byte[aa.length+bb.length];
System.arraycopy(aa,0,cc,0,aa.length);
System.arraycopy(bb,0,cc,aa.length,bb.length);
return cc;
}
/**
* 获取验证码byte数组,基于Modbus CRC16的校验算法
*/
private static byte[] getCrc16(byte[] arr_buff) {
int len = arr_buff.length;
// 预置 1 个 16 位的寄存器为十六进制FFFF, 称此寄存器为 CRC寄存器。
int crc = 0xFFFF;
int i, j;
for (i = 0; i < len; i++) {
// 把第一个 8 位二进制数据 与 16 位的 CRC寄存器的低 8 位相异或, 把结果放于 CRC寄存器
crc = ((crc & 0xFF00) | (crc & 0x00FF) ^ (arr_buff[i] & 0xFF));
for (j = 0; j < 8; j++) {
// 把 CRC 寄存器的内容右移一位( 朝低位)用 0 填补最高位, 并检查右移后的移出位
if ((crc & 0x0001) > 0) {
// 如果移出位为 1, CRC寄存器与多项式A001进行异或
crc = crc >> 1;
crc = crc ^ 0xA001;
} else
// 如果移出位为 0,再次右移一位
crc = crc >> 1;
}
}
return intToBytes(crc);
}
/**
* 将int转换成byte数组,低位在前,高位在后
* 改变高低位顺序只需调换数组序号
*/
private static byte[] intToBytes(int value) {
byte[] src = new byte[2];
src[1] = (byte) ((value>>8) & 0xFF);
src[0] = (byte) (value & 0xFF);
return src;
}
/**
* 将字节数组转换成十六进制字符串
*/
public static String byteTo16String(byte[] data) {
StringBuffer buffer = new StringBuffer();
for (byte b : data) {
buffer.append(byteTo16String(b));
}
return buffer.toString();
}
/**
* 将字节转换成十六进制字符串
* int转byte对照表
* [128,255],0,[1,128)
* [-128,-1],0,[1,128)
*/
public static String byteTo16String(byte b) {
StringBuffer buffer = new StringBuffer();
int aa = (int)b;
if (aa<0) {
buffer.append(Integer.toString(aa+256, 16)+" ");
}else if (aa==0) {
buffer.append("00 ");
}else if (aa>0 && aa<=15) {
buffer.append("0"+Integer.toString(aa, 16)+" ");
}else if (aa>15) {
buffer.append(Integer.toString(aa, 16)+" ");
}
return buffer.toString();
}
public static byte[] hexStringToByteArray(String hexString) {
hexString = hexString.replaceAll(" ", "");
int len = hexString.length();
if (len%2 == 1){
len = len + 1;
hexString = "0"+ hexString;
}
byte[] bytes = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
// 两位一组,表示一个字节,把这样表示的16进制字符串,还原成一个字节
bytes[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character
.digit(hexString.charAt(i + 1), 16));
}
return bytes;
}
}
ModbusRtuEncoder类:
getReadCommand函数
private short address = 0X01;
public byte[] getReadCommand(byte stationNumber, short dataAddress, PLCAddressType addressType, int dataAddressLength) {
//byte[] headerBuffer = Converter.getBytes((short)id++);
byte[] dataAddressBuffer = Converter.getBytes(dataAddress);
byte addressByte = (byte)(addressType.ordinal() + 1);
//int[] data = new int[]{0x01, 0x03, 0x00, 0x65, 0x00, 0x02};
byte[] data = new byte[]{(byte)address, addressByte, dataAddressBuffer[1], dataAddressBuffer[0], 0x00, (byte)dataAddressLength,}; //小端模式
byte[] crcByte = CRC.getData(data);
//String str = CRC.byteTo16String(crcByte).toUpperCase();
return crcByte;
}
getWriteCommand函数(主要处理寄存器)
public <T> byte[] getWriteCommand(short stationNumber, short dataAddress, PLCAddressType addressType, T value) {
Converter aa = new Converter();
String hex = aa.getHex(value);
int dataLength = hex.length();
byte[] hexdata = CRC.hexStringToByteArray(hex);
byte[] dataAddressBuffer = Converter.getBytes(dataAddress);
byte[] hexBytes = new byte[]{};
byte[] data = new byte[]{};
int functionCode = 0;
if (dataLength == 8){
switch (addressType){
case CoilStatus: {
functionCode = 0x0F;
} case HoldingRegister: {
functionCode = 0x10;
hexBytes = new byte[]{ hexdata[1], hexdata[0],hexdata[3], hexdata[2]};
}
default:break;
}
data = new byte[]{(byte)address, (byte)functionCode, dataAddressBuffer[1], dataAddressBuffer[0], 0x00, (byte)(dataLength/4),(byte)(dataLength/2),}; //小端模式
}
else if (0 < dataLength && dataLength <= 4){
switch (addressType){
case CoilStatus: {
functionCode = 0x05;
} case HoldingRegister: {
functionCode = 0x06;
hexBytes = new byte[]{ hexdata[1], hexdata[0]};
}
default:break;
}
data = new byte[]{(byte)address, (byte)functionCode, dataAddressBuffer[1], dataAddressBuffer[0]};
}
//int[] data = new int[]{01 10 00 65 00 02 04 a6 42 99 9a 5d 1f};
byte[] tmp = new byte[data.length+hexBytes.length];
System.arraycopy(data,0,tmp,0,data.length);
System.arraycopy(hexBytes,0,tmp,data.length,hexBytes.length);
byte[] crcByte = CRC.getData(tmp);
return crcByte;
}
ModbusRtuDecoder类:
(不完全展示,其他还有检查id合法性之类的)
/**
* 获取内容长度-通过调试工具事先发现响应报文长度
* @param recvHeadByte 接收到的头指令
* @return
*/
public <T> short getContentLength(byte[] recvHeadByte,short dataAddressLength, Class<T> clazz,int WriteOrRead) {
if(WriteOrRead == 1){
return 8; //写入rtu
}
else if( WriteOrRead == 0 ){
short dataLen = (short)(5+dataAddressLength*2);
return dataLen; //读取rtu的两个寄存器
}
return 0;
}
/**
* 获取编码的值
* @param recvByte 接收到的指令
* @return
*/
public <T> String getValue(byte[] recvByte, short dataAddressLength, Class<T> clazz){
String result = null;
try {
switch (dataAddressLength){
case 2: {
byte[] dataBytes = new byte[]{ recvByte[5], recvByte[6], recvByte[3], recvByte[4]}; //小端模式
float value = Converter.getFloat(dataBytes);
result = String.valueOf(value);
break;
} case 1:{
byte[] dataBytes = new byte[]{recvByte[3], recvByte[4]}; //小端模式
short value = Converter.getShort(dataBytes);
result = String.valueOf(value);
break;
} case 3:{
byte[] dataBytes1 = new byte[]{recvByte[3], recvByte[4]}; //小端模式
byte[] dataBytes2 = new byte[]{recvByte[7], recvByte[8], recvByte[5], recvByte[6]};
short value1 = Converter.getShort(dataBytes1);
float value2 = Converter.getFloat(dataBytes2);
result = String.valueOf(value1)+" "+String.valueOf(value2);
break;
}
default:break;
}
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
ModbusRtuDeviceTest类:
运行函数
public static void main(String[] args) {
ModbusRtuDevice modbusRtu = new ModbusRtuDevice("127.0.0.1",502);
short dataAddress = 100; // 数据地址
short dataAddressLength = 3; // 寄存器长度
Class clazz = float.class; // 数据类型
Class clazzAll =long.class; // 代表一个混合数据类型
Object value=83.3f; //(short)950
while (true){
try {
/**
* 读取数据
*/
OperateResult<String> result1 = modbusRtu.read(dataAddress, PLCAddressType.HoldingRegister, dataAddressLength, clazzAll);
if(result1.isSuccess){
System.out.println("读取设备数据成功:"+result1.content);
}else{
System.out.println("读取设备数据失败:"+result1.message);
}
/**
* 写入数据
*/
// OperateResult<String> result2 = modbusRtu.write(dataAddress, PLCAddressType.HoldingRegister, value);
// if(result2.isSuccess){
// System.out.println("写入设备数据成功:"+result2.content);
// }else{
// System.out.println("写入设备数据失败:"+result2.message);
// }
} catch (Exception e) {
e.printStackTrace();
}
finally {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
整体代码框架如下: