Modbus RTU通讯实例
前言
前文与完整代码:
C#上位机:串口通讯
本文是对前文的补充,主要是针对上位机的串口通讯在Modbus RTU协议方面的运用。在前文中有详细代码描述了如何搭建一个串口通讯上位机模板,在本文中,给出一些在前文中代码基础上的Modbus RTU的实际操作范例。
关于Modbus RTU
作为工业中最常用的通讯协议,常作为上位机与各下位机交流的媒介。详细介绍在这不赘述,网上资料都有。本文的核心主要是通过操作实例,分析使用机制与报文结构。一般来说,上位机作为主站,各下位机作为从站,对应Modbus 协议的一个主/从(Master/Slave)以及客户 端/服务器(Client/Server)架构。机制是上位机通过串口通讯发送Modbus RTU报文,从站回复报文(执行操作)。
本文中使用的是Modbus Slave作为从站模拟硬件。下载去百度下都有。
报文分析
打开Modbus Slave,我们可以看到Connection,点进去设置模拟串口的参数:
然后点击Set up下的Slave Definition,在此处我们将切换模拟地址:
点击ok后设置完毕,可以开始模拟测试。
首先来看一组报文
01 06 00 01 00 01 19 CA
其中: 01 (从机地址) 06 (功能号) 00 01 (数据地址) 00 01 (作用数据) 19 CA(CRC校验码)
此时的Slave ID,便是报文中的从机地址,对应01。
而后的四个Function,可以理解为一长串的连续寄存器地址:
线圈寄存器 | 地址位 |
---|---|
01 Coil Status 线圈(可读写) | 00001-09999 |
02 Input Status 离散输入(只读) | 10001-19999 |
03 Holding Register 保持寄存器(可读写) | 40001-49999 |
04 Input Registers 输入寄存器(只读) | 30001-39999 |
其中,当我们打开01的地址会发现,线圈是一位二进制状态
而寄存器则是整数变量。
因为可读写性不同,功能号其实同时决定了是在哪个寄存器进行哪一种操作,也就是说,报文中的地址只是寄存器内部的地址(1-9999),但是在哪个寄存器(线圈),由功能号做了区分。例如上述报文:01 06 00 01 00 01 19 CA,其实是作用于03 保持寄存器的第01位地址并写入01(如下)
自此,通过串口发送Modbus RTU报文的操作就完成了。附录全功能码:
01 (0x01) 读线圈
02 (0x02) 读离散量输入
03 (0x03) 读保持寄存器
04(0x04) 读输入寄存器
05 (0x05) 写单个线圈
06 (0x06) 写单个寄存器
15 (0x0F) 写多个线圈
16 (0x10) 写多个寄存器
我们再以01 05 00 01 FF 00 XXXX(此处省略校验码)为例
Slave ID为01,写线圈(也就是Coil Status 0X)地址01,FF 00 ,即更改状态为1。
如果是读取状态,那么就看报文反馈,往从机发送的报文都是有报文回复的,以功能码01 02 00 01 00 01 XXXX(此处同样省略CRC)为例,我们先去输入寄存器的01位修改值为X,发送后接收报文,16进制下读取报文,再切换为十进制,即可得到查询结果X。
注意事项:查明具体的地址与寄存器再进行操作实验。
生成CRC校验算法
如若需要查询报文对应的CRC校验码,可在此网页http://www.metools.info/code/c15.html选取CRC-16Modbus查询。
同时补充一个自动生成CRC校验码的算法:
private static byte[] GetModbusCrc16(byte[] bytes)
{
byte crcRegister_H = 0xFF, crcRegister_L = 0xFF;
byte polynomialCode_H = 0xA0, polynomialCode_L = 0x01;
for (int i = 0; i < bytes.Length; i++)
{
crcRegister_L = (byte)(crcRegister_L ^ bytes[i]);
for (int j = 0; j < 8; j++)
{
byte tempCRC_H = crcRegister_H;
byte tempCRC_L = crcRegister_L;
crcRegister_H = (byte)(crcRegister_H >> 1);
crcRegister_L = (byte)(crcRegister_L >> 1);
if ((tempCRC_H & 0x01) == 0x01)
{
crcRegister_L = (byte)(crcRegister_L | 0x80);
}
if ((tempCRC_L & 0x01) == 0x01)
{
crcRegister_H = (byte)(crcRegister_H ^ polynomialCode_H);
crcRegister_L = (byte)(crcRegister_L ^ polynomialCode_L);
}
}
}
return new byte[] { crcRegister_L, crcRegister_H };
}