TIA博途(V19)ModbusTCP仿真与S7NetPlus库的基本使用心得(三)

500 阅读6分钟
  • 对西门子PLC中Struct的读写

S7NetPlus库中也提供了.ReadStruct();方法来对PLC中的Struct进行读写操作:

结构体.png

//首先需要在C#中定义一个和PLC中的Struct对应的结构体
//结构体中就连字段的声明顺序,都必须要和PLC中保持一致
public struct PlcStruct{
    public bool varBool;
    public byte varByte;
    public ushort varWord;
    public uint varDW;
    public int varDInt;
    
    [S7String(S7StringType.S7String,254)]
    public string varS7String;
    
    [S7String(S7StringType.S7WString,254)]
    public string varS7WString;
    
    public double varLreal;
}

//之后就可以进行读写操作了
PlcStruct myStruct = new PlcStruct();
//参数1是DBid,参数2是开始字节地址
myStruct = (PlcStruct)plcObj.ReadStruct<PlcStruct>(3,806);

读结构体.png

但是在查看数据时,发现DWord数据发生了“错误”,PLC中明明是777_777_77(16#04A2_CB71),读取到的数据却是190_917_069_2(16#71CB_A204),转换成十六进制后发现数据的大小端颠倒了。当然可以通过把读取到的DWord数据转成字节数组再倒过来,来获得正确的数值,不过这不是重点;PLC中还有自定义的数据类型(UDT),使用.ReadStruct();是读不到的,而且.ReadClass();方法也读不到,在这里还是卡了很久,最终找到一个方法可以较方便的解决读Struct或UDT或“类”的问题:

plcObj.ReadBytes();只要知道从哪里开始读(startByteAdr)以及读多少个字节(Count),就可以把整个结构体或者自定义类型(UDT)获取到字节数组,之后再根据当中具体的地址进行数据类型转换,最后赋值到C#定义的struct/Class中即可:

//在我的PLC的DB3中,结构体的开始地址为806,最后一地址是1594(Lreal类型是64位占8个字节),所以一共要读取788个字节

int startByteAdr = 806;
var structByteArray = plcObj.ReadBytes(DataType.DataBlock,3,startByteAdr,788);
myStruct.varBool = Bit.FromByte(structByteArray[0], 0);  
myStruct.varByte = structByteArray[807 - startBtyeAdr];  
myStruct.varWord = Word.FromByteArray(structByteArray[(808 - startBtyeAdr)..(810 - startBtyeAdr)]);  
myStruct.vadDW = DWord.FromByteArray(structByteArray[(810 - startBtyeAdr)..(814 - startBtyeAdr)]);  
myStruct.varDInt = DInt.FromByteArray(structByteArray[(814 - startBtyeAdr)..(818 - startBtyeAdr)]);  
myStruct.varS7String = S7String.FromByteArray(structByteArray[(818 - startBtyeAdr)..(1074 - startBtyeAdr)]);  
myStruct.varS7WString = S7WString.FromByteArray(structByteArray[(1074 - startBtyeAdr)..(1586 - startBtyeAdr)]);  
myStruct.varLreal = LReal.FromByteArray(structByteArray[(1586 - startBtyeAdr)..(1594 - startBtyeAdr)]);

读结构数组.png

读取PLC中自定义类型UDT的时候,操作没多大差别,可以先在C#定义一个类(class),之后跟结构体一样的操作进行字节的读取。

C#8中提供了方便的通过数组索引来获取到数组范围的方式,但是在framework里不行,可以通过linq中的skip()+take()方法,例:structByteArray.Skip("PLC中偏移减去开始偏移地址").Take("该数据类型所占字节数").ToArray();

往PLC中写入struct就很简单,只需要实例化一个结构体,然后将其中的字段赋值,并调用.WriteStruct();方法即可,但要注意的是,C#中定义的struct里的字段,数据类型顺序一定要与PLC中结构体的数据类型排列顺序一致,否则执行写入时会使PLC中的数据混乱。

写入Class的时候,遇到了一些困难,S7NetPlus库中提供的ReadClass();与WriteClass();方法不知道为什么既读不出来,也写不进PLC中,压根用不了(也可能是我博途还不怎么会用的原因);根据上述读写struct的逻辑,只好自己写一个WriteClass的拓展方法:

//拓展方法---静态类中的静态方法
public static class S7NetClassOperationExtension{
    public static void WriteClassEx(this Plc plc,object classValue, DataType dataType,int DBid,int startByteAdr){
        //利用反射判断类中字段的数据类型,依据类型对字段的值进行与PLC数据类型相对应的字节数组转换
        //最后将这些数组拼接成一整个数组,通过S7NetPlus中WriteBytes();方法写入到博途中
        
        //创建List<byte[]>用于存储PLC数据类型的字节数组
        List<byte[]> listBytes = new List<byte[]>();
        
        var classFields = classValue.GetType().GetFields();
        foreach(var field in classFields)
        {
            //先获取该字节的值
            var varObj = field.GetValue(classValue);
            //每次遍历都声明一个新字节数组
            byte[] varBytes = new byte[]{};
            //判断字段的数据类型,再将字段的值转换成对应的字节数组
            swicth(field.FieldType.Name)
            {
                case "Double":
                   varBytes = Lreal.ToByteArray((double)varObj);
                   break;
                case "Single":
                   varBytes = Real.ToByteArray((float)varObj);
                   break;
                case "UInt32":
                   varBytes = DWord.ToByteArray((uint)varObj);
                   break;
                case "Uint16":
                   varBytes = Word.ToByteArray((ushort)varObj);
                   break;
                case "Int32":
                   varBytes = DInt.ToByteArray((int)varObj);
                   break;
                case "Int16":
                   varBytes = S7.Net.Types.Int.ToByteArray((short)varObj);
                   break;
                case "Byte":
                    //在PLC中要占两个字节的位置
                   varBytes = new byte[]{(byte)varObj,0};
                   break;
                case "Boolean":
                    //在PLC中要占两个字节的位置
                   varBytes = new byte[]{Convert.ToByte((bool)varObj)};
                   break;
                   
                //处理String类型时,还需要判断该字段的特性是S7String还是S7WString
                case "String":
                    var att = field.GetCustomAttribute<S7StringAttribute>();
                    swicth(att.Type.ToString())
                    {
                        case "S7String":
                            varBytes = S7String.ToByteArray(varObj.ToString(),254);
                            break;
                        case "S7WString":
                            varBytes = S7WString.ToByteArray(varObj.ToString(),254);
                            break;
                    }
                    break;
            }
            listBytes.Add(varBytes);
            
            //将list中的byte数组展平成一个字节数组
            byte[] combineClassBytes = listBytes.SelectMany(arr => arr).ToArray();
            
            //执行字节写入操作
            plc.WriteBytes(dataType,DBid,startByteAdr,combineClassBytes);
        }
    }
}

写入class.png


  • 上面的WriteClassEx();方法只是一个简单的,实际使用中,PLC的UDT自定义数据类型可能是复杂的(比如层层嵌套或包含数组),基于此,利用反射与递归,整了个相对完整版的方法,希望能够应对一些实际情况:

//拓展方法--静态类中的静态方法
public static class S7NetPlusExtension{
    public static void WriteClassEnhance(this Plc plc,DataType datatype,int DBid,int StartByteAdr,Object classValue){
    
        byte[] byteArrayFromClass = ToByteArrayForClass(classValue);
        plc.WriteBytes(datatype,DBid,startByteAdr,classValue);
    }
    
    private static byte[] ToByteArrayForClass(Object classValue){
        //创建一个List<Byte[]>存储各种PLC类型数据的字节数组
        List<byte[]> listBytes = new List<byte[]>();
        //获取类中的字段集合
        var classFields = classValue.GetType().GetFields();
        //遍历每一个字段
        foreach(var field in classFields){
            //获取当前字段的值
            var fieldValue = field.GetValue(classValue);
            //声明一个字节数组用来临时保存转换的字节数据
            byte[] varBytes = new byte[]{};
            //判断当前字段的值是否是基元类型或string类型,如果是就直接转换
            if(fieldValue.IsNullOrPrimitiveOrString()){
                varBytes = ByteArrayFromFieldInfo(field,fieldValue);
            }
            //判断是不是数组
            else if(field is Array arr){
                varBytes = ByteArrayFromArray(arr);
            }
            else
            {
                //既不是基元类型也不是数组,就只能还是class类了,递归
                varBytes = ToByteArrayForClass(fieldValue);
            }
            listBytes.Add(varBytes);
            
            //将List展开成字节数组
            var resBytes = listBytes.SelectMany(arr => arr).ToArray();
            return resBytes;
        }
    }
    
    //拓展方法,判断该数据类型是否为空,是否是基元类型,是否是string类型
    private static bool IsNullOrPrimitiveOrString(this object obj){
        if(obj == null) return true;
        Type ty = obj.GetType();
        return ty.IsPrimitive || ty == typeof(string);
    }
    
    //判断字段的类型,并且将值转换为相应的PLC类型需要的字节数组
    private static byte[] ByteArrayFromFieldInfo(FieldInfo field,object fieldValue){
        //临时数组
        byte[] varBytes = new byte[]{};
        switch(field.FieldType.Name){
            case "Double":  
                resBytes = LReal.ToByteArray((double)fieldValue);  
                break;  
            case "Single":  
                resBytes = Real.ToByteArray((float)fieldValue);  
                break;  
            case "UInt32":  
                resBytes = DWord.ToByteArray((uint)fieldValue);  
                break;  
            case "UInt16":  
                resBytes = Word.ToByteArray((ushort)fieldValue);  
                break;  
            case "Int32":  
                resBytes = DInt.ToByteArray((int)fieldValue);  
                break;  
            case "Int16":  
                resBytes = S7.Net.Types.Int.ToByteArray((short)fieldValue);  
                break;  
            case "Byte":  
                resBytes = new byte[] { (byte)fieldValue, 0 };  
                break;  
            case "Boolean":  
                resBytes = new byte[] { Convert.ToByte((bool)fieldValue), 0 };  
                break;  
            case "String":  
                var att = field.GetCustomAttribute<S7StringAttribute>();  
                switch (att.Type.ToString())  
                {  
                    case "S7String":  
                        resBytes = S7String.ToByteArray(fieldValue.ToString(), 254);  
                        break;  
                    case "S7WString":  
                        resBytes = S7WString.ToByteArray(fieldValue.ToString(), 254);  
                        break;  
                }  
                break;
        }
        return resBytes;
    }
    
    //判断数组的数据类型,并转换成PLC所对应的字节数组
    private static byte[] ByteArrayFromArray(Array arr){
        List<byte[]> listBytes = new List<byte[]>();
        foreach(var element in arr){
            byte[] varBytes = new byte[]{};
            //判断数组类型是否为基元类型或string,如果是就直接转换
            if(element.IsNullOrPrimitiveOrString()){
                switch (element.GetType().Name)  
                        {  
                            //考虑到空间占用,应该很少出现S7String/S7WString类型的数组,所以偷懒了
                            case "Double":  
                                varBytes = LReal.ToByteArray((double)element);  
                                break;  
                            case "Single":  
                                varBytes = Real.ToByteArray((float)element);  
                                break;  
                            case "UInt32":  
                                varBytes = DWord.ToByteArray((uint)element);  
                                break;  
                            case "UInt16":  
                                varBytes = Word.ToByteArray((ushort)element);  
                                break;  
                            case "Int32":  
                                varBytes = DInt.ToByteArray((int)element);  
                                break;  
                            case "Int16":  
                                varBytes = S7.Net.Types.Int.ToByteArray((short)element);  
                                break;  
                            case "Byte":  
                                varBytes = S7.Net.Types.Byte.ToByteArray((byte)element);  
                                break;  
                            case "Boolean":  
                                varBytes = new byte[] { Convert.ToByte((bool)element)};  
                                break;  
                        }
            }
            else
            {
                //如果是别的类的话,就递归
                varBytes = ToByteArrayForClass(element);
            }
            listBytes.Add(varBytes);
        }
        //在PLC中,byte与bit类型数据结尾还要加一个字节{0}
        if (arr.GetType().Name == "Byte[]" || arr.GetType().Name == "Boolean[]") {  
                listBytes.Add(new byte[] { 0 });  
        }
        var resBytes = listBytes.SelectMany(arr => arr).ToArray();
        return resBytes;
    }
}



  • 本节完

第二部分【S7NetPlus库的基本使用心得(二)】