前言
Modbus协议广泛用于自动化和工业制造中的智能设备监控,底层传感器/PLC与上层系统间的通信。关于协议本身这里不做赘述,网上的相关资料琳琅满目。
鉴于协议是二进制协议,理论上可以处理任何数据。也可以用市面上的任一种主流开发语言编写实际的数据通讯程序。
为了方便起见,本文以微软的C#语言为例说明。
作为一个通用的底层通讯协议,世界上早就有成千上万的人在用了,我们一般不需要从最基础的底层写起,就是常言说的不重复造轮子,因为即使很牛逼的程序员第一次写这种协议实现,难免有bug,需要反复修改完善。当然如果你有足够的时间和精力一步步钻研学习和研究,另当别论。
我们的目标是怎么尽快把项目完成,老板或客户根本不会关心你底层怎么实现的,写了多牛逼的代码。
EasyModbus的使用
这里我们找一个用的比较多的第三方库,EasyModbus,你问我怎么知道别人用的多,我也是瞎猜的。在Github上下载下来,大概浏览一下项目目录:
一看我的乖乖,作者很牛逼,花了很大功夫自己写了服务端和客户端,并且自己编写了一个模拟器。这个模拟器实在太方便 了,这样我们就不用等下层开发人员做好了再联合调试修改完善,太耽误时间了。上层开发人员可以独立把程序直接调通。一般来说,我们上层应用只用到客户端功能,可以把PLC当作一个微型服务器吧。
先运行模拟器ServerSimulator, 然后把启动项目设为ClientExample跑样例,界面如下。
先点Connect连接模拟器,然后点击各个按钮做测试。如果已经了解了协议本身,左边的按钮的含义就一目了然了,
这里把右下角的写入内容解释一下:
clear entry, 删除待写入列表中的一项,
clear all 删除待写入列表的全部内容,
下面两个输入框可以填入布尔值和整数值(这里的整数是实际上是ushort,范围0~65535)
填好后点左箭头移入待写入清单,一定要先点左箭头移入待写入清单,点左边的按钮发送的是待写入清单内容,如果为空会报错。如果写多个,必须是相同类型的内容,线圈为布尔值,寄存器为整数值。
Write Multiple Coils 写入多个线圈(布尔值)
Write Multiple Registers 写入多个寄存器(整数ushort),多个寄存器的个数就是界面上待写入列表的行数。
最下面的清单是二进制发送和接收内容,可以清楚的看到底层的传输数据。
常见问题点
需要注意的是,EasyModbus代码里的modbus位置和值都是用的int类型, 实际上这并不符合modbus的协议定义,int 是32位有符号整数,最准确的做法,无论是地址位置还是值,都应该用ushort,
好在EasyModbus里面自己处理了大部分的限制和转换,但是并没有覆盖全。比如,你要传输一个32768, 先用Write Single Register写入这个值,
再用Read Holding Register 读取会得到 -32768, 这就是作者使用了的int的原因。
由于源码都在,你可以直接修改Read Holding Register按钮的代码,和ModbusClient.ReadHoldingRegisters,修改值的类型由int改为ushort即可。
这种修改比较简单我们就不做赘述。
字符串传输问题及处理
EasyModbus里的客户端ModbusClient 内置了很多静态方法,ConvertXxx系列,可以方便的为我们完成各类数据输入输出的解析工作,包括完成大小端的识别和转换。
大部分数据类型都不会有什么问题,少量溢出的改写下值的类型为ushort即可,这样已经足够应付绝大部分场景了,不过虽然很少用到,但有的时候我们确实需要传输字符串。作为测试用例,这里我们先写入字符串然后读取字符串。
发送字符串我们需要先用ConvertStringToRegisters转换成待写入寄存器值,然后读出来后用ConvertRegistersToString转换成最终结果。
如果是ASCII码,EasyModbus默认的接口是没问题的,但是传输中文是有问题的,会报错。
由于作者是西方人,没碰到这个问题。笔者仔细分析了ModbusClient,发现问题出在上面提到的这2个方法上。
先看ConvertStringToRegisters
public static int[] ConvertStringToRegisters(string stringToConvert)
{
byte[] array = System.Text.Encoding.ASCII.GetBytes(stringToConvert);
int[] returnarray = new int[stringToConvert.Length / 2 + stringToConvert.Length % 2];
for (int i = 0; i < returnarray.Length; i++)
{
returnarray[i] = array[i * 2];
if (i*2 +1< array.Length)
{
returnarray[i] = returnarray[i] | ((int)array[i * 2 + 1] << 8);
}
}
return returnarray;
}
首先这里的ASCII是一定有问题的,不支持中文,需要改为UTF8(这里一定要用UTF8, 因为是世界通用标准,不要使用GB2312/GBK/GB18030等等),然后下面一行的
int[] returnarray = new int[stringToConvert.Length / 2 + stringToConvert.Length % 2];
也是有问题的,作者的本意是定义字节数除以2 作为寄存器的个数
如果是ASCII字符串没问题,但是中文字符串就有问题了,中文字符串UTF8转码后字节数不好确定,有的汉字是2字节编码,有的汉字是3字节编码,个别4字节的也有。比如“你真牛逼”这4个汉字用UTF8编码后是12个字节,因此中括号内应该用实际的字节数除以2,bytes.Length / 2 + bytes.Length % 2,后面的百分号表示如果是单数个字节,也要算一个modbus地址位置。因为modbus的一个地址位占2个字节。
另外按照我们上面说的,我们顺便也把值类型改为ushort,修改后的完整的方法如下:
public static ushort[] ConvertStringToRegisters(string stringToConvert)
{
byte[] bytes = Encoding.UTF8.GetBytes(stringToConvert);
ushort[] array = new ushort[bytes.Length / 2 + bytes.Length % 2];
for (int i = 0; i < array.Length; i++)
{
array[i] = bytes[i * 2];
if (i * 2 + 1 < bytes.Length)
{
array[i] = (ushort)(array[i] | (bytes[i * 2 + 1] << 8));
}
}
return array;
}
同样的我们把ConvertRegistersToString方法也做类似修改。
public static string ConvertRegistersToString(ushort[] registers, int offset, int stringLength)
{
byte[] array = new byte[stringLength];
for (int i = 0; i < stringLength / 2; i++)
{
byte[] bytes = BitConverter.GetBytes(registers[offset + i]);
array[i * 2] = bytes[0];
array[i * 2 + 1] = bytes[1];
}
return Encoding.UTF8.GetString(array);
}
改好了这2个方法我们再在UI上加2个按钮做一个简单测试。
int len = 0;
//写入寄存器
private void button5_Click(object sender, EventArgs e)
{
if (!modbusClient.Connected)
{
return;
}
string msg = textBox2.Text;
ushort[] content = ModbusClient.ConvertStringToRegisters(msg);
modbusClient.WriteMultipleRegisters(1, content);
len = content.Length;
}
//读取寄存器
private void button6_Click(object sender, EventArgs e)
{
if (!modbusClient.Connected)
{
return;
}
ushort[] content = modbusClient.ReadHoldingRegisters(1, len);
string msg = ModbusClient.ConvertRegistersToString(content, 0, content.Length * 2);
textBox3.Text = msg;
}
ConvertStringToRegisters可以得到待写入的寄存器值的内容和个数。临时存入变量len,
读取时modbusClient.ReadHoldingRegisters(1, len); 用到这个寄存器个数len即可。
测试成功。
总结
总的使用方法: EasyModbus使用时先用ConvertXXtoRegister 转为待发送寄存器值,然后读取后用ConvertRegisterToxxx转为实际值。对于中文字符串的传入需要对库代码做适当修改。