C# NModbus RTU串口通信

Modbus RTU 串口通信

虚拟串口工具:https://www.virtual-serial-port.org/

Modbus调试工具:https://www.modbustools.com/download.html

NOTE:都是付费软件,但是网上有盗版。

添加两个虚拟串口,这两个虚拟串口是互相连通的:

串口调试工具:https://github.com/SuperStudio/SuperCom

保证两个串口可以互相通信,然后打开Modbus Slave,并连接到COM2:

打开串口调试工具,先手工构造消息,熟悉一下Modbus协议。

NOTE:Modbus协议规范网上有中文版可以下载。

CRC计算工具:https://crccalc.com

位于地址1的设备的数据类型是线圈,现在读取1号设备的前4个线圈数据:

请求:

设备地址 1B 功能 1B 起始地址 2B 读取数量 2B CRC校验码 2B
01 01 00 00 00 04 3D C9

计算器计算出的校验码是 C9 3D,但不能直接用,要将高位字节和低位字节反转,即 3D C9 。

响应:

设备地址 1B 功能 1B 数据个数 1B 线圈状态 NB CRC校验码
01 01 01 08(00001000) 50 4E

根据协议规范:

  • 响应数据数据从低位到高位起,依次对应设备中的线圈寄存器地址。
  • 如果结果数量大于8个,则再加一个字节,新加的字节从低位到高位,依次对应剩余的寄存器地址(8 ~ N)。
  • 如果数据数量不是8的倍数,则在高位补零。

所以可以看到1号设备的前三个寄存器值都是0,第4个(3号地址)值是1。

再试一下,读取1号设备前9个线圈的数据:

设备地址 1B 功能 1B 起始地址 2B 读取数量 2B CRC校验码 2B
01 01 00 00 00 0A BC 0D

响应:

设备地址 1B 功能 1B 数据个数 1B 线圈状态 NB CRC校验码
01 01 02 C8 02(1100 1000 0000 0010) 6F FD

从响应结果来看,前10个寄存器中,地址为3、6、7、9的这四个寄存器值为1,其他都是0 。

看下模拟器是不是这样:

没毛病。注意地址为B的那个寄存器值也是1,但它是第12个寄存器,我们只请求了前10个,所以只给了前十个寄存器的值,剩余的比特位协议规定要补零,因为要按字节对齐。

再试一下发个错误的报文,看下错误响应。

错误类型:校验码错误

错误请求:01 01 00 00 00 0A BC 00

结果:无响应

错误类型:读取线圈数量65535

错误请求:01 01 00 00 FF FF 3D BA

结果:

设备地址 1B 功能码 1B (功能码 + 0x80) 异常码 1B CRC校验码
01 81 03 00 51

Modbus协议规范中的异常码说明:

异常码 异常名称 备注
01 非法的功能码 服务器不认识功能码
02 非法的数据地址 与请求有关
03 非法的数据值 与请求有关
04 服务器故障 执行过程中,服务器故障
05 确认 服务器接收服务调用,但是需要相对长的时间完成服务,
因此,服务器仅返回一个服务调用接收的确认
06 服务器繁忙 服务器不能接受MODBUS请求的PDU,
客户应用有责任决定是否和何时重发请求
0A 网关故障 网关路径是无效的
0B 网关故障 目标设备没有响应时,网关生成这个异常信息

1号设备一共只有16个寄存器,要读取65535个寄存器,得到的响应是03:非法的数据值。

NModbus

NMondbus是C#中Modbus通信协议的一个实现。

安装

打开NuGet控制台,或者包管理器,安装下面几个包。NModbus.Serial 用于Modbus串口通信。

PM> Install-Package System.IO.Ports
PM> Install-Package NModbus
PM> Install-Package NModbus.Serial
官方示例

NMondbus Github仓库地址:https://github.com/NModbus/NModbus.git

官方示例:https://github.com/NModbus/NModbus/blob/master/Samples/Program.cs

串口通信

串口通信中,数据以帧的形式发送,每一帧通常包括:起始位(Start Bit)、数据位(Data Bits)、可选的奇偶校验位(Parity Bit)以及停止位(Stop Bits)。

同步通信
using System.IO.Ports;
using NModbus;
using NModbus.Serial;

var portName = "COM3";
var baudRate = 9600;

var port = new SerialPort();
// 设置串口名
port.PortName = portName;
// 设置波特率
port.BaudRate = baudRate;
// 设置奇偶校验方式,要跟Modbus Slave那边同步,默认是None,我这里改成偶校验了
port.Parity = Parity.Even;
// 停止位
port.StopBits = StopBits.One;

// 打开串口
port.Open();


var factory = new ModbusFactory();
// 创建一个Modbus RTU协议的Master
var master = factory.CreateRtuMaster(port);

// 数组输出为字符串
string ArrayToString<T>(T[] values, string sep = " ")
{
    return string.Join(sep, values.Select(r => Convert.ToString(r)).ToArray());
}

// 随机数对象
var random = new Random();

// 随机Boolean
bool RandomBoolean()
{
    return random.NextDouble() > 0.5;
}

// 随机ushort
ushort RandomUnsignedShort()
{
    return (ushort)random.Next(ushort.MinValue, ushort.MaxValue);
}

try
{
    // 写线圈
    var coilsToWrite = Enumerable.Range(0, 4).Select(i => RandomBoolean()).ToArray();
    Console.WriteLine($"写线圈:{ArrayToString(coilsToWrite)}");
    master.WriteMultipleCoils(1, 0, coilsToWrite);

    // 读线圈
    var coils = master.ReadCoils(1, 0, 4);
    Console.WriteLine($"读线圈:{ArrayToString(coils)}");

    // 写单个线圈 / 写之前
    Console.WriteLine($"读线圈:{ArrayToString(master.ReadCoils(1, 0, 4))}");

    // 写单个线圈
    var coilValue = RandomBoolean();
    master.WriteSingleCoil(1, 1, coilValue);
    Console.WriteLine($"写单个线圈(地址=1):{coilValue}");

    // 写单个线圈
    coilValue = RandomBoolean();
    master.WriteSingleCoil(1, 2, coilValue);
    Console.WriteLine($"写单个线圈(地址=2):{coilValue}");

    // 写单个线圈 / 写之后
    Console.WriteLine($"读线圈:{ArrayToString(master.ReadCoils(1, 0, 4))}");

    // 读输入状态
    var inputs = master.ReadInputs(2, 0, 4);
    Console.WriteLine($"读输入状态:{ArrayToString(inputs)}");

    // 写多个保持寄存器
    var registerValues = Enumerable.Range(0, 4).Select(i => RandomUnsignedShort()).ToArray();
    Console.WriteLine($"写保持寄存器:{ArrayToString(registerValues)}");
    master.WriteMultipleRegisters(3, 0, registerValues);

    // 写单个寄存器 / 写之前
    registerValues = master.ReadHoldingRegisters(3, 0, 4);
    Console.WriteLine($"读保持寄存器:{ArrayToString(registerValues)}");

    // 写单个寄存器
    var value = RandomUnsignedShort();
    Console.WriteLine($"写单个寄存器(地址=1):{value}");
    master.WriteSingleRegister(3, 1, value);

    // 写单个寄存器
    value = RandomUnsignedShort();
    Console.WriteLine($"写单个寄存器(地址=2):{value}");
    master.WriteSingleRegister(3, 2, value);

    // 写单个寄存器 / 写之后
    registerValues = master.ReadHoldingRegisters(3, 0, 4);
    Console.WriteLine($"读保持寄存器:{ArrayToString(registerValues)}");


    // 读多个输入寄存器
    var inputRegisters = master.ReadInputRegisters(4, 0, 4);
    Console.WriteLine($"读输入寄存器:{ArrayToString(inputRegisters)}");
}
catch (SlaveException ex)
{
    Console.WriteLine(ex.Message);
}

// 关闭串口
port.Close();

posted @ 2024-10-22 20:44  烟酒忆长安  阅读(587)  评论(0编辑  收藏  举报