Modbus ASCII

简介

Modbus ASCII 使用ASCII字符集传递消息,方便阅读和调试。Modbus ASCII相比于Modbus RTU,协议帧中添加了起始和结束,更换了校验算法。

Modbus网络模型

这张图比较简洁清晰。Modbus网络中,只有一个Master,Master可以向Slave发起请求并获取响应,Slave只能被动发送响应而不能主动请求。

Slave最多可达247个 . 每个Slave由1到247之间的地址标识,0地址用来广播,剩余地址保留。

帧格式

字段描述
名称 字节数 描述
Start 1 B 以冒号 : 开头,ASCII十六进制值为3A
Address 2 B 十六进制节点地址,字符表示
Function 2 B 十六进制功能码,字符表示
Data 2 * n B n是数据字节数,取决于功能码
LRC 2 B LRC冗余校验码
End 2 B CRLF

Modbus ASCII为了兼容Modbus RTU,是将二进制字节改用ASCII字符来表示,例如 0xFF 这个十六进制数,Modbus RTU中,使用二进制进行传输,传输的数据是 1111 1111 。Modbus ASCII中,传输数据就变成了 0100 0110 0100 0110 ,共两个字节,每个字节对应十进制70,是 F 的ASCII码。

传输示例

校验码计算

网上找不到计算工具,没办法,自己算吧。

校验算法叫LRC,纵向冗余校验。就是将所有字节相加(两个数字表示一个字节),截取最低的8位(忽略进位),再取补码。注意是十六进制数字相加,不是ASCII码。两行代码就可以算出来:

var msg = "020300030002";
// 两个十六进制数字表示一个字节,因此需要做一个转换,每两位字符转换为一个十六进制数,然后再计算LRC
// 
var bytes = Enumerable.Range(0, msg.Length / 2).Select(i => Convert.ToByte(msg.Substring(i * 2, 2), 16));
var lrc = ((bytes.Aggregate((a, b) => (byte)(a + b)) ^ 0xFF) + 1) & 0xFF;

Console.WriteLine(lrc.ToString("X2"));

下面的代码是NModbus库里面的:

/// <summary>
///     Converts a hex string to a byte array.
/// </summary>
/// <param name="hex">The hex string.</param>
/// <returns>Array of bytes.</returns>
public static byte[] HexToBytes(string hex)
{
    if (hex == null)
    {
        throw new ArgumentNullException(nameof(hex));
    }

    if (hex.Length % 2 != 0)
    {
        throw new FormatException(Resources.HexCharacterCountNotEven);
    }

    byte[] bytes = new byte[hex.Length / 2];

    for (int i = 0; i < bytes.Length; i++)
    {
        bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
    }

    return bytes;
}

/// <summary>
///     Calculate Longitudinal Redundancy Check.
/// </summary>
/// <param name="data">The data used in LRC.</param>
/// <returns>LRC value.</returns>
public static byte CalculateLrc(byte[] data)
{
    if (data == null)
    {
        throw new ArgumentNullException(nameof(data));
    }

    byte lrc = 0;
    
    foreach (byte b in data)
    {
        lrc += b;
    }

    lrc = (byte)((lrc ^ 0xFF) + 1);

    return lrc;
}

打开Modbus Slave模拟器,打开虚拟串口工具,串口调试工具,手动构造请求消息试一下。

读取2号设备的3号和4号寄存器

功能码0x03(读保持寄存器)的格式:

功能码 起始地址 读取数量
1 字节 / 2 字符 2 字节 / 4 字符 2 字节 / 4 字符

构造请求:

帧头 设备地址 功能码 起始地址 读取数量 校验码 结束符
: 02 03 00 03 00 02 F6 CRLF
:020300030002F6\r\n

得到响应:

:02030400070006EA\r\n

响应格式:

功能码 字节数 寄存器值
1 字节 / 2 字符 1 字节 / 2 字符 N * 2 字节 / N * 4 字符(N 为读取数量)

响应解析:

设备地址 功能码 字节数 寄存器值 校验码
02 03 04 00 07 00 06 EA

一个寄存器16位,一个字节8位,一个十六进制数4位。所以我们请求了两个寄存器的值,响应给了八个字符,一共四个字节的数据,没问题。再看数据对不对:

3号寄存器值为7,4号寄存器值为6,没毛病。

将2号设备的4号和5号寄存器置为1

功能码0x10(写多个寄存器)的格式:

功能码 起始地址 寄存器数量 字节数 寄存器值
1 字节 / 2 字符 2 字节 / 4 字符 2 字节 / 4 字符 1 字节 / 2 字符 N * 2 字节 / N * 4 字符(N 为寄存器数量)

构造请求:

设备地址 功能码 起始地址 寄存器数量 字节数 寄存器值 LRC校验码
02 10 00 04 00 02 04 00 01 00 01 F5
:0210000400020400010001E2

获得响应:

:021000040002E8

响应格式:

功能码 起始地址 寄存器数量
1 字节 / 2 字符 2 字节 / 4 字符 2 字节 / 4 字符

解析响应:

设备地址 功能码 起始地址 寄存器数量 LRC校验码
02 10 00 04 00 02 E8

看下结果:

4号和5号寄存器的值现在都是1,没毛病。

使用NModbus

使用NModbus库,编码时除了一开始的配置,基本上无需关心用的什么协议,库都帮你封装好了。

var factory = new ModbusFactory();
// 使用NModbus RTU,传进去一个SerialPort对象
var master = factory.CreateRtuMaster(port);
// 使用Modbus ASCII,传进去一个SerialPort对象
var master = factory.CreateAsciiMaster(port);
// 使用Modbus TCP,传进去一个TcpClient对象
var master = factory.CreateMaster(tcpClient);

// 其他API都一样,直接用就行了
posted @ 2024-10-24 15:34  烟酒忆长安  阅读(68)  评论(0编辑  收藏  举报