一、Modbus 基础
(一)、串行链路上的两种不同模式
特性 | RTU模式 | ASCII模式 |
---|---|---|
编码 | 二进制 | ASCII |
每个字符位数 | 起始位:1 Bit | 起始位:1 Bit |
数据位:8 Bit | 数据位:7 Bit | |
奇偶校验:1位 | 奇偶校验:1位 | |
停止位:1 或 2 | 停止位:1 或 2 | |
报文校验 | CRC(循环冗余校验) | LRC(纵向冗余校验) |
(二)、串行链路上的通用报文格式
报文前后各需要大于3.5个字符的报文间隔时间,否则会被判断为一帧报文
(3.5个字节的时间主要由波特率决定的,在9600的波特率下大概是4ms)
地址 | 功能码 | 数据 | CRC校验 |
---|---|---|---|
1 Byte | 1 Byte | N Byte | 2 Byte |
(三)、存储区
存储区标识 | 名称 | 类型 | 读 / 写 | 存储单元地址 |
---|---|---|---|---|
0XXXX | 线圈 | 位 | 读 / 写 | 00001 ~ 0XXXX |
1XXXX | 输入线圈 | 位 | 只读 | 10001 ~ 1XXXX |
3XXXX | 输入寄存器 | 字 | 只读 | 30001 ~ 3XXXX |
4XXXX | 保持 / 输出寄存器 | 字 | 读 / 写 | 40001 ~ 4XXXX |
【注】:存储单元数量与实际设备相关
(四)、Modbus大小端
众所周知,计算机底层都是二进制代码,但在实际应用中,我们却经常和浮点数、整数或者字符串打交道,在进行赋值运算或者算术运算时,必须要保证参与运算的数据类型保持一致,如果不一致,就必须进行数据转换。
数据类型 | c#简称 | 数据长度(位) | 数据范围 |
---|---|---|---|
位 | Bit | 1 | 0-1 |
字节 | Byte | 8 | 0-255 |
有符号16位整数 | Short | 16 | -32768-32767 |
无符号16位整数 | UShort | 16 | 0-65535 |
有符号32位整数 | Int | 32 | -2E31-2E31 |
无符号32位整数 | UInt | 32 | 0-2E32 |
单精度浮点数 | Float | 32 | -3.4E38-3.4E38 |
有符号64位整数 | Long | 64 | -2E63-2E63 |
无符号64位整数 | ULong | 64 | 0-2E64 |
双精度浮点数 | Double | 64 | -1.79E308-1.79E308 |
字符串 | String | 64 | 无 |
字节顺序简单来说,就是指超过一个字节的数据类型在内存中的存储顺序,如果只有1个字节就不存在顺序的说法了,一般来说,字节顺序会分两类,一种叫做大端字节顺序,一种叫做小端字节顺序。
大端字节顺序是指高位字节存储在低位地址,低位字节存储在高位地址
小端字节顺序则反之,高位字节存储在高位地址,低位字节存储在低位地址
如果一个Int类型数组,占用4个字节,4个字节顺序为ABCD,那么采用big-endian大端字节顺序,那么在内存中即为ABCD,如果采用small-endian小端字节顺序,那么在内存中存储即为DCBA,但是在实际应用中,还有可能出现BADC或者CDAB的情况,因此我们在大小端的基础上做了一下扩展,定义了4种不同字节顺序,采用枚举类型表示
public enum DataFormat
{
ABCD = 0, // 大端形式
BADC = 1, // 大端单字反转
CDAB = 2, // 小端单字反转
DCBA = 3, // 小端形式
}
二、调试
(一)、软件介绍
名称 | 描述 |
---|---|
Modbus Slave | 一款用于仿真 Modbus Rtu 从站或 Modbus TCP 服务器的软件 |
Modbus Poll | 一款用于仿真 Modbus Rtu 主站或 Modbus TCP 客户端的软件 |
VSPD | 一款用于虚拟电脑串口软件(Configure Virtual Serial Port Driver) |
- 利用VSPD虚拟出两个串口(把本文为COM3 和 COM7)
- Function:选择的存储区需要相同
(二)、Modbus Slave 软件 —— Rtu从站 / TCP 服务器
Connection >> Connection Setup,设置如下
Setup >> Slave Definition,设置如下
Slave ID:从站的ID
Function:选择的存储区
(三)、Modbus Poll 软件 —— Rtu 主站 \ TCP 客户端
Connection >> Connection Setup,设置如下
Setup >> Read/Write Definition,设置如下
Slave ID:需要注意从站的ID
Function:选择的存储区
三、数据读取
(一)、读取输出线圈 (功能码:01H)
主站询问报文格式:
从站地址 | 功能码 | 起始地址(高位) | 起始地址(低位) | 线圈数量(高位) | 线圈数量(低位) | CRC |
---|---|---|---|---|---|---|
0x11 | 0x01 | 0x00 | 0x13 | 0x00 | 0x1B | XXXX |
含义:
读取11H(17)号从站输出线圈
起始地址 = 0013H(19),对应地址是00020;
线圈数量 = 001BH(27),结束地址是 = 00020 + 27 - 1 = 00046
即从11H(17)号从站读取00020 - 00046 共27个线圈状态。
从站应答报文格式:
从站地址 | 功能码 | 字节计数 | 线圈状态20-27 | 线圈状态28-35 | 线圈状态36-43 | 线圈状态44-46 | CRC |
---|---|---|---|---|---|---|---|
0x11 | 0x01 | 0x04 | 0xCD | 0x6B | 0xB2 | 0x05 | XXXX |
含义:来自11H(17)号从站输出线圈 00020-00046,共27个线圈状态,分别为 CD 6B B2 05
CD = 1100 1101 对应00020 - 00027
6B = 0110 1011 对应00028 - 00035
B2 = 1011 0010 对应00036 - 00043
05 = 0000 0101 对应00044 - 00046
【注】:状态从右往左进行对应低位到高位的线圈
/*
* 将byte数组转化为获取二进制
*/
var result = modbusRtu.ReadKeepRegister(1, 0, Convert.ToUInt16(this.textBox1.Text));
if (result.IsSuccess)
{
string binaryString = string.Empty;
foreach (var item in result.Content)
{
// 反转数组
char[] charArray = Convert.ToString(item, 2).PadLeft(8, '0').ToCharArray();
Array.Reverse(charArray);
binaryString += new string(charArray);
}
binaryString = binaryString.Substring(0, Convert.ToUInt16(this.textBox1.Text));
Console.WriteLine(binaryString);
}
else
{
Console.WriteLine(result.Message);
}
(二)、读取输入线圈 (功能码:02H)
主站询问报文格式:
从站地址 | 功能码 | 起始地址(高位) | 起始地址(低位) | 线圈数量(高位) | 线圈数量(低位) | CRC |
---|---|---|---|---|---|---|
0x11 | 0x02 | 0x00 | 0xC4 | 0x00 | 0x1D | XXXX |
含义:
读取11H(17)号从站输入线圈
起始地址 = 00C4H(196),对应地址是10197;
线圈数量 = 001DH(29),结束地址是 = 10197 + 29 - 1 = 10225
即从11H(17)号从站读取10197 - 10225 共29个线圈状态。
从站应答报文格式:
从站地址 | 功能码 | 字节计数 | 线圈状态10197-10204 | 线圈状态10205-10212 | 线圈状态10213-10220 | 线圈状态10221-10228 | CRC |
---|---|---|---|---|---|---|---|
0x11 | 0x02 | 0x04 | 0xCD | 0x6B | 0xB2 | 0x05 | XXXX |
含义:来自11H(17)号从站输入线圈 10197-10228,共32个线圈状态,分别为 CD 6B B2 05
CD = 1100 1101 对应10197 - 10204
6B = 0110 1011 对应10205 - 10212
B2 = 1011 0010 对应10213 - 10220
05 = 0000 0101 对应10221 - 10228
【注】:状态从右往左进行对应低位到高位的线圈,当然也不是绝对的,和上文所讲到的大小端相关
/*
* 将byte数组转化为获取二进制
*/
var result = modbusRtu.ReadKeepRegister(1, 0, Convert.ToUInt16(this.textBox1.Text));
if (result.IsSuccess)
{
string binaryString = string.Empty;
foreach (var item in result.Content)
{
// 反转数组
char[] charArray = Convert.ToString(item, 2).PadLeft(8, '0').ToCharArray();
Array.Reverse(charArray);
binaryString += new string(charArray);
}
binaryString = binaryString.Substring(0, Convert.ToUInt16(this.textBox1.Text));
Console.WriteLine(binaryString);
}
else
{
Console.WriteLine(result.Message);
}
(三)、读取保持寄存器 (功能码:03H)
主站询问报文格式:
从站地址 | 功能码 | 起始寄存器(高位) | 起始寄存器(低位) | 寄存器数量(高位) | 寄存器数量(低位) | CRC |
---|---|---|---|---|---|---|
0x11 | 0x03 | 0x00 | 0x6B | 0x00 | 0x02 | XXXX |
含义:
读取11H(17)号从站保持寄存器
起始地址 = 006BH(107),对应地址是40107;
寄存器数量 = 0002H(2),结束地址是 = 40108 + 2 - 1 = 40109
即从11H(17)号从站读取40108 - 40109 共2个寄存器的值。
从站应答报文格式:
从站地址 | 功能码 | 字节计数 | 40108高位 | 40108低位 | 40109高位 | 40109低位 | CRC |
---|---|---|---|---|---|---|---|
0x11 | 0x03 | 0x04 | 0x02 | 0x2B | 0x01 | 0x06 | XXXX |
含义:
来自11H(17)号从站保持寄存器的值 40108-40109,共2个寄存器的值,分别为 CD 6B B2 05
保持寄存器 40108 的值为 CD6BH
保持寄存器 40109 的值为 B205H
(四)、读取输入寄存器 (功能码:04H)
主站询问报文格式:
从站地址 | 功能码 | 起始寄存器(高位) | 起始寄存器(低位) | 寄存器数量(高位) | 寄存器数量(低位) | CRC |
---|---|---|---|---|---|---|
0x11 | 0x04 | 0x00 | 0x6B | 0x00 | 0x02 | XXXX |
含义:
读取11H(17)号从站输入寄存器
起始地址 = 006BH(107),对应地址是40107;
寄存器数量 = 0002H(2),结束地址是 = 40108 + 2 - 1 = 40109
即从11H(17)号从站读取40108 - 40109 共2个寄存器的值。
从站应答报文格式:
从站地址 | 功能码 | 字节计数 | 40108高位 | 40108低位 | 40109高位 | 40109低位 | CRC |
---|---|---|---|---|---|---|---|
0x11 | 0x04 | 0x04 | 0x02 | 0x2B | 0x01 | 0x06 | XXXX |
含义:
来自11H(17)号从站输入寄存器的值 40108-40109,共2个寄存器的值,分别为 CD 6B B2 05
输入寄存器 40108 的值为 CD6BH
输入寄存器 40109 的值为 B205H
(五)、强制单个线圈 (功能码:05H)
主站询问报文格式:
从站地址 | 功能码 | 线圈地址(高位) | 线圈地址(低位) | 断通标志 | 断通标志 | CRC |
---|---|---|---|---|---|---|
0x11 | 0x05 | 0x00 | 0xAC | 0xFF | 0x02 | XXXX |
含义:
强制 11H (17)号从站某个线圈的值
线圈地址=00ACH=172,对应地址 00173
断通标志为FF00H表示置位,断通标志为0000H表示复位
即置位 11H (17)号从站输出线圈00173
从站应答报文格式:
从站地址 | 功能码 | 线圈地址(高位) | 线圈地址(低位) | 断通标志 | 断通标志 | CRC |
---|---|---|---|---|---|---|
0x11 | 0x05 | 0x00 | 0xAC | 0xFF | 0x02 | XXXX |
含义:
强制 11H (17)号从站输出线圈00173为ON后原文返回
(六)、强制多个线圈 (功能码:0FH)
主站询问报文格式:
从站地址 | 功能码 | 起始(高位) | 起始(低位) | 线圈数(高位) | 线圈数(低位) | 字节计数 | 字节1 | 字节2 | CRC |
---|---|---|---|---|---|---|---|---|---|
0x11 | 0x0F | 0x00 | 0x13 | 0x00 | 0x0A | 0x02 | 0xCD | 0x00 | XXXX |
含义:
预置 11H (17)号从站多个线圈的值,
线圈起始地址=0013H=19,对应地址 00020;
线圈数=0x000A=10,结束地址为00020+10-1=00029,
写入值为0xCD00,
即预置 11H (17)号从站线圈:
00020-00027=0xCD=1100 1101
00028-00029=0x00=0000 0000
【注】:状态从右往左进行对应低位到高位的线圈
从站应答报文格式:
从站地址 | 功能码 | 起始(高位) | 起始(低位) | 线圈数(高位) | 线圈数(低位) | CRC |
---|---|---|---|---|---|---|
0x11 | 0x0F | 0x00 | 0x13 | 0x00 | 0x0A | XXXX |
含义:
预置 11H (17)号从站线圈:
00020-00027=0xCD=1100 1101
00028-00029=0x00=0000 0000
(七)、预置单个寄存器 (功能码:06H)
主站询问报文格式:
从站地址 | 功能码 | 寄存器地址(高位) | 寄存器地址(低位) | 写入值(高位) | 写入值(低位) | CRC |
---|---|---|---|---|---|---|
0x11 | 0x06 | 0x00 | 0x87 | 0x03 | 0x9E | XXXX |
含义:
预置 11H (17)号从站某个寄存器的值
寄存器地址=0087H=135,对应地址 40136
写入值为0x039E,
即预置 11H (17)号从站保存寄存器40136值为0x039E。
从站应答报文格式:
从站地址 | 功能码 | 寄存器地址(高位) | 寄存器地址(低位) | 写入值(高位) | 写入值(低位) | CRC |
---|---|---|---|---|---|---|
0x11 | 0x06 | 0x00 | 0x87 | 0x03 | 0x9E | XXXX |
含义:
预置 11H (17)号从站保存寄存器40136值为0x039E后原文返回
(八)、预置多个寄存器 (功能码:10H)
主站询问报文格式:
从站地址 | 功能码 | 起始(高位) | 起始(低位) | 数量(高位) | 数量(低位) | 字节计数 | 字节1 | 字节2 | 字节3 | 字节4 | CRC |
---|---|---|---|---|---|---|---|---|---|---|---|
0x11 | 0x10 | 0x00 | 0x87 | 0x00 | 0x02 | 0x04 | 0x01 | 0x05 | 0x0A | 0x10 | XXXX |
含义:
预置 11H (17)号从站多个寄存器的值,
寄存器起始地址=0087H=135,对应地址 40136;
寄存器数=0x0002=2,结束地址为40136+2-1=40137,
写入值为0x0105,0x0A10,
即预置 11H (17)号从站寄存器:
40136=0x0105
40137=0x0A10
从站应答报文格式:
从站地址 | 功能码 | 起始(高位) | 起始(低位) | 数量(高位) | 数量(低位) | CRC |
---|---|---|---|---|---|---|
0x11 | 0x10 | 0x00 | 0x87 | 0x00 | 0x02 | XXXX |
含义:
预置 11H (17)号从站寄存器:40136=0x0105 40137=0x0A10
写在最后
本博文只是我在学习c#的过程中所做的笔记,方便以后查阅实现过程。资料均来自网上,如果有侵权请联系我删除,谢谢。