C# / .NET 2.0 简单 Modbus 协议






4.91/5 (105投票s)
2007 年 10 月 18 日
7分钟阅读

888277

56208
对 .NET 2.0 的 SerialPort 类的简单实现,用于 Modbus 通信

引言
我工作中经常需要测试和调试使用 Modbus 协议通信的设备。为了节省时间,我尝试了几个可下载的应用程序,但总是得出同样的结论——“既然自己就能做出来,为什么要花钱买呢?” 而且,鉴于 Modbus 是一个有 30 年历史的标准,很难找到在当今流行语言中可用的最新代码。下面将简要介绍 Modbus 的原理以及我每天在测试 RS232 Modbus 设备时使用的一个小型应用程序。该代码远非健壮,但它实现了功能,并且易于使用和修改——这是我在实验室环境中认为最重要的两个特性。希望您觉得这个有帮助!
背景
Modbus 是一种串行通信协议,自 20 世纪 70 年代以来就被广泛应用于工业界。它在需要通过网络连接多个设备(或从设备)的场景中尤其有用,因为它支持多设备地址。但如果您看到这篇文章,很可能您已经知道 Modbus 是什么了,不想再被细节 boring 到死。对于那些想深入了解的人,这里是获取相关细节的地方。
在本文中,我将快速回顾所使用的 Modbus 实现以及涉及的功能。
Modbus 变体
Modbus 协议有两种形式:**RTU** 和 **ASCII**。RTU 是二进制实现,通常是最受欢迎的。因此,本文将仅讨论 RTU 标准。
Modbus 功能
Modbus 协议提供了许多功能,但我发现除了基本的读写命令外,大多数功能都没有什么用处。因此,我们只关注 **功能 3 - 读取多个寄存器** 和 **功能 16 - 写入多个寄存器** 命令的实现。其他命令可以通过本文“背景”部分提供的链接了解。
功能 3 - 读取多个寄存器消息帧
请求
- 地址(一个字节,表示从设备 ID)
- 功能码(一个字节,表示功能 ID,此处为“3”)
- 起始地址(两个字节,表示开始读取的 Modbus 寄存器)
- 寄存器数量(两个字节,表示要读取的寄存器数量)
- CRC(两个字节,包含出站消息的循环冗余校验校验和)
响应
- 地址(一个字节,包含响应的从设备 ID)
- 功能码(一个字节,表示从设备正在响应的功能,此处为“3”)
- 字节计数(一个字节,表示正在读取的字节数。每个 Modbus 寄存器由 2 个字节组成,因此该值将是 2 * N,其中 N 是正在读取的寄存器数量)
- 寄存器值(2 * N 字节,表示正在读取的值)
- CRC(两个字节,包含入站消息的 CRC 校验和)
功能 16 - 写入多个寄存器消息帧
请求
- 地址(一个字节,表示从设备 ID)
- 功能码(一个字节,表示功能 ID,此处为“16”)
- 起始地址(两个字节,表示开始写入的 Modbus 寄存器)
- 寄存器数量(两个字节,表示要写入的寄存器数量)
- 字节计数(一个字节,表示正在写入的字节数。每个 Modbus 寄存器由 2 个字节组成,因此该值将是 2 * N,其中 N 是正在写入的寄存器数量)
- 寄存器值(2 * N 字节,包含要写入的实际字节)
- CRC(两个字节,包含出站消息的循环冗余校验校验和)
响应
- 地址(一个字节,包含响应的从设备 ID)
- 功能码(一个字节,表示正在响应的功能,此处为“16”)
- 起始地址(两个字节,说明首先写入的起始寄存器地址)
- 寄存器数量(两个字节,表示被写入的 Modbus 寄存器的数量)
- CRC(两个字节,表示入站消息的 CRC 校验和)
错误和异常编码
Modbus 协议在其实现指南中描述了每种功能可用的多种异常代码。在此代码中处理的只有简单的 CRC 评估,因为这远非 Modbus 协议的完整实现。如果选择添加更多错误检查,可以轻松地将其他过程添加到提供的代码中。
Using the Code
类 modbus.cs 包含用于串行端口处理和数据传输的几个函数。所有函数都使用 .NET 2.0 的 **System.IO.Ports
** 命名空间中的 SerialPort
类。
SerialPort
类有一个特殊的 DataReceived
事件。正如本网站上的其他文章所记录的那样,DataReceived
事件本应在每次串行端口接收到传入事件时触发,但它“实际上并没有这样做”。这使得真正的事件驱动通信无法正常工作,需要额外的修复和变通方法。
幸运的是,使用 Modbus 协议这类方式,我们可以知道传入和传出的消息长度。这允许我们将 ReadByte
函数作为任何读取命令的基础,从而完全绕过该问题。理想情况下,这将是一个事件驱动的类——但在此期间,我们将通过自己告知端口何时读取来避免数据丢失。
SendFc3 - 读取多个寄存器
public bool SendFc3(byte address, ushort start,
ushort registers, ref short[] values)
{
//Ensure port is open:
if (sp.IsOpen)
{
//Clear in/out buffers:
sp.DiscardOutBuffer();
sp.DiscardInBuffer();
//Function 3 request is always 8 bytes:
byte[] message = new byte[8];
//Function 3 response buffer:
byte[] response = new byte[5 + 2 * registers];
//Build outgoing modbus message:
BuildMessage(address, (byte)3, start, registers, ref message);
//Send modbus message to Serial Port:
try
{
sp.Write(message, 0, message.Length);
GetResponse(ref response);
}
catch (Exception err)
{
modbusStatus = "Error in read event: " + err.Message;
return false;
}
//Evaluate message:
if (CheckResponse(response))
{
//Return requested register values:
for (int i = 0; i < (response.Length - 5) / 2; i++)
{
values[i] = response[2 * i + 3];
values[i] <<= 8;
values[i] += response[2 * i + 4];
}
modbusStatus = "Read successful";
return true;
}
else
{
modbusStatus = "CRC error";
return false;
}
}
else
{
modbusStatus = "Serial port not open";
return false;
}
}
此公共函数接受四个变量 - address
(从设备 ID),start
(开始读取的寄存器),registers
(要读取的寄存器数量)和 values
(用于存储读取值的字节数组的引用)。函数将返回 true
表示读取成功,或 false
表示读取错误。
BuildMessage()
函数只是将每个字节放入所需的 Modbus 消息格式,并将此消息传递给 CRC 计算器。GetResponse()
从串行端口读取固定长度的字节流,并将此结果放入 response[]
数组中。这些函数非常直接,可以通过下载示例代码进行检查。在读写例程之后,通过将请求的数据放入传递给此函数的引用 short[]
数组来处理。然后,这些值可供用户应用程序使用。
公共字符串 modbusStatus
在 Modbus 通信的各个阶段包含相关信息,并在程序执行期间将其提供给用户应用程序。这可以在示例应用程序的整个过程中看到。
SendFc16 - 写入多个寄存器
public bool SendFc16(byte address, ushort start,
ushort registers, short[] values)
{
//Ensure port is open:
if (sp.IsOpen)
{
//Clear in/out buffers:
sp.DiscardOutBuffer();
sp.DiscardInBuffer();
//Message is 1 addr + 1 fcn + 2 start + 2 reg + 1 count +
2 * reg vals + 2 CRC
byte[] message = new byte[9 + 2 * registers];
//Function 16 response is fixed at 8 bytes
byte[] response = new byte[8];
//Add bytecount to message:
message[6] = (byte)(registers * 2);
//Put write values into message prior to sending:
for (int i = 0; i < registers; i++)
{
message[7 + 2 * i] = (byte)(values[i] >> 8);
message[8 + 2 * i] = (byte)(values[i]);
}
//Build outgoing message:
BuildMessage(address, (byte)16, start, registers, ref message);
//Send Modbus message to Serial Port:
try
{
sp.Write(message, 0, message.Length);
GetResponse(ref response);
}
catch (Exception err)
{
modbusStatus = "Error in write event: " + err.Message;
return false;
}
//Evaluate message:
if (CheckResponse(response))
{
modbusStatus = "Write successful";
return true;
}
else
{
modbusStatus = "CRC error";
return false;
}
}
else
{
modbusStatus = "Serial port not open";
return false;
}
}
此函数的功能与 SendFc3
类似。我们再次将四个变量传递给函数,只是这次我们传递一个已填充值的数组来用于写入相关寄存器,而不是传递一个字节数组的引用来接收读取值。同样,我们收到一个 boolean
值,表示写入命令的成功或失败。
如前所述,CheckResponse
函数除了简单的 CRC 检查外,不做任何其他操作。CRC 会针对入站消息重新计算,并与消息本身中传递的值进行比较——从而检查响应的有效性。任何消息帧错误都应导致 CRC 异常。
关注点
同样,重要的是重申 SerialPort DataReceived
事件中事件处理的缺点。简单的谷歌搜索会找到许多沮丧的程序员解释类似的情况,但很少有解决方案被提出。本网站包含一个这样的解决方案,是否采用这种编码方式取决于个人。Modbus 协议更好的实现将包含该事件的重写以及更精细的数据接收函数。
请注意,modbus
类中的公共函数是按照可以在用户创建的应用程序的专用线程中使用的意图构建的。示例应用程序演示了在 System.Timers.Timer.Elapsed
事件创建的线程中使用这些函数。应该很容易找到适合您特定需求的用途。