前言
Modbus 是目前使用比较广泛的工业自动化协议。C#语言有一个框架 EasyModbus 对Modbus的底层协议进行了实现。大家可以直接用它,不用自己费脑筋写底层代码了。在之前的介绍 EasyModbus 文章中,我们重点介绍了如何处理中文字符。因为原框架不直接支持处理中文字符,需要对源代码稍加修改。有的小伙伴看了之后可能还是有些困惑,问那其它类型的数据怎么处理,比如小数啥的。
实际上常见数据类型的传输处理EasyModbus都已经涵盖到了,框架已经为我们封装好了,我们只要直接拿来用就行了。
这里我们给大家再梳理一下。
在使用之前,我们先回顾一下Modbus协议的一些基本知识。这里面有几个要点需要了解。
Modbus的基本知识
- Modbus 是按照二进制传输数据的,任何与上层的数据通讯都必须按照二进制处理。
- Modbus 协议的数据分两类,一类是实体数据,这种是用寄存器存取的,下层开发人员可以存取,上层软件通过Modbus协议也可以存取,这种寄存器叫做保持寄存器(HoldRegisters)
- 另外一种是开关量数据,读出来就是1和0,开关量数据保存在所谓的"线圈(Coils)"或"离散输入量(DiscreteInputs)"里,叫什么名字不重要,你只要记住"线圈"是可读可写,"离散输入量"只能读。
- Modbus 的数据是按照"位"来存的,一个位代表一个寄存器。对于Modbus里的寄存器,一个寄存器 位 占2个字节,了解这一点非常重要,这有助于我们后面理解第三方开发框架提供的一些基本方法的含义。
- Modbus 协议提供了一些功能码保存和读取数据。包括存取寄存器和线圈等。不过现在有了第三方的开发框架,这些协议的功能码我们可以不用记了,只要知道使用第三方框架的哪些方法干活就行。如果你有时间有兴趣仔细研究,一定要知其所以然,可以先通读协议文本。
EasyModbus里的"位数组"
在了解了Modbus基本知识后,我们再梳理EasyMobus的处理思路。
寄存器的存取稍微复杂一点,我们着重阐述这部分。
EasyModbus在处理数据时的基本套路如下:
如果是写,先将数据转为"位数组",就是整数数组,代码中用的int[],然后再将这个数组写入PLC。
上面我们介绍了,Modbus是按照位来存取数据的,因此EasyModbus这用一个 int 来代表一个 位,我们说了一个位代表2个字节的,而 int 是32位,占4个字节,那 EasyModbus 为什么要 int呢(其实用 short 更能表达一个位的含义)。其实也没什么原因,因为大家都习惯用int了,所以作者也就用int了 既然用的是int,而int有4个字节,那么框架里的这些"位数组"里的 int[] 实际上只用到低位2个字节。高位的2个作者是没有用到的,至于他具体怎么做的,我们不用管,框架里面自己处理了,无非就是加入一些移位处理了。我们只要知道是这么用的就行了。
读的时候也是先读到位数组里面,再把这个位数组转为最终结果。示意图如下:

了解了modbus的处理套路,我们再看看里面的一些方法怎么使用就豁然开朗了。
EasyModbus存取整数
写入整数:
先转为位数组
int myInt1 = 4444;
int[] values = ModbusClient.ConvertIntToRegisters(myInt1); //转位数组
有了上面的说明,你就不用感到奇怪或困惑:为什么本身是整数又要转为一个整数数组呢?
这里的整数数组,你就理解为一个short 数组好了。一个整数4个字节,要转为2个short的位置存。
然后写入PLC:
client.WriteMultipleRegisters(startingAddress, values); //写入PLC ,startAddress是从哪个位开始写,一般你跟下层约定的协议已经定好了的哪个变量占多少位,所以你的应用层协议已经确定了从哪个位置开始写,这个也不能搞错。下同。
读取整数:
int[] values = client.ReadHoldingRegisters(startingAddress, 2);//从PLC读位数组,这个2比较重要。整数2个位(4个字节),就是上面说的2个short位。
int myInt2= ModbusClient.ConvertRegistersToInt(values);//将位数组转为最终结果。
EasyModbus存取小数
写入小数:
先转为位数组:
double myDouble1 = 113.234567;//比如一个经度
int[] values = ModbusClient.ConvertDoubleToRegisters(myDouble1);//转位数组
然后写入PLC:
client.WriteMultipleRegisters(startingAddress, values);//写入PLC
读取小数:
int[] values = client.ReadHoldingRegisters(start, 4);//从PLC读位数组,double小数4个位(8个字节),就是上面说的4个short位。
double myDouble2 = ModbusClient.ConvertRegistersToDouble(values);//将位数组转为最终结果。
EasyModbus存取字符串
字符串的处理套路跟上面差不多,如果只是ASCII码,可以直接用已有的方法:
写入字符串:
string myString1 ="Hello World!";
int[] values = ModbusClient.ConvertStringToRegisters(myString1);
client.WriteMultipleRegisters(startingAddress, values);
读取字符串:
int num = (int)Math.Ceiling(myString1.Length * 1.0 / 2); //计算好用从PLC取的位数
int[] values = client.ReadHoldingRegisters(start, num);// 读字符串时,要先计算好取几个位,就是原来字符串的长度除以2后向上取整
string myString2 = ModbusClient.ConvertRegistersToString(values); //将位数组转为最终结果。
中文字符串处理
如果涉及到中文,EasyModbus存取字符串稍微复杂一点点,需要先对,两个Convert静态方法加以改造。
这里给出笔者针对中文处理,修改过之后的方法。可以直接替换原有方法。兼容ASCII文本。
public static int[] ConvertStringToRegisters(string stringToConvert, int registerNumber)
{
byte[] array = Encoding.UTF8.GetBytes(stringToConvert);
int len0 = array.Length / 2 + array.Length % 2;//字符串的字节数一半。
int len1 = Math.Min(len0, registerNumber);//实际占用的地址位,取小值,若字符串过长舍弃
int[] returnarray = new int[registerNumber];//占用寄存器位长度,字符串长度不足的默认0
for (int i = 0; i < len1; i++)
{
returnarray[i] = array[i * 2];
if (i*2 +1< array.Length)
{
returnarray[i] = returnarray[i] | (array[i * 2 + 1] << 8);
}
}
return returnarray;
}
public static string ConvertRegistersToString(int[] registers, int offset)
{
byte[] result = new byte[registers.Length * 2];
for (int i = 0; i < registers.Length; i++)
{
byte[] registerResult = BitConverter.GetBytes(registers[offset + i]);
result[i * 2] = registerResult[0];
result[i * 2 + 1] = registerResult[1];
}
string str = Encoding.UTF8.GetString(result).TrimEnd('\0');
int n = str.IndexOf('\0');
if (n >= 0)
{
str = str.Substring(0, n);
}
return str;
}
改良过的方法ConvertStringToRegisters,加一个寄存器个数入参,这个比较重要。
因为我们在与下层约定协议时,一般会在协议里定好这个字符串最大会占用多少个位。
如果字符串长度不够预留的长度,转换过的位数组自动填入0
读字符串的时候遇到\0就截止了,这样可以避免如果上一次写的字符串较长,第二次写的字符串较短会带出垃圾数据的问题。
使用方法如下:
string myString1 = "王老五你好啊";
int registerNumber = 10;
int[] values = ModbusClient.ConvertStringToRegisters(myString1,registerNumber);
client.WriteMultipleRegisters(startingAddress, values);
读取字符串:
int[] values = client.ReadHoldingRegisters(start, registerNumber);// 读字符串,使用应用层协议预先定好的寄存器个数。
string myString2 = ModbusClient.ConvertRegistersToString(values ,0); //将位数组转为最终结果。
在与下层约定协议时要正确估算好字符串可能需要占用PLC的最大位数。
一般一个中文的UTF8会占3个字节,举例说明,上面的 "王老五你好啊" 6个汉字需要18个字节, 9个PLC位。我们约定registerNumber为10是合理的。
开关量存取及其它
EasyModbus里面开关量的存取比较简单。直接用方法就行了。
WriteSingleCoil(int startingAddress, bool value)//写单个线圈
WriteMultipleCoils(int startingAddress, bool[] values)//写多个线圈
bool[] ReadCoils(int startingAddress, int quantity)//读多个线圈
ReadDiscreteInputs(int startingAddress, int quantity);//读离散输入量, 这个只读,没有写方法。
另外还有一种只读寄存器,只能读的,没有写方法,用的比较少,跟上面说的读法一样。
ReadInputRegisters(int startingAddress, int quantity)
总结
EasyModbus里面的保持寄存器的存取,是通过中间变量“位数组”来处理的,写入时先把内容转为“位数组”,然后写入;读取时先从PLC读取指定位置和数量的内容到位数组,位的数量跟数据类型相关,参见上文描述。然后将位数组转位最终结果。EasyModbus为我们已经构建好了这些基本转换方法,我们只要使用就行了。中文字符串处理需要对原有代码进行改良,笔者在文章中已经给出了改良的细节代码,直接使用即可。



402

被折叠的 条评论
为什么被折叠?



