zoukankan      html  css  js  c++  java
  • 博图TIA中ModbusRTU Over TCP/IP通讯的实现

    博图TIA中ModbusRTU Over TCP/IP通讯的实现

    在学习使用SCL通信时,查看了博途SCL实现自定义ModbusRtu Over TCP功能块这个文档,主要使用了IP解析部分的程序.

    后来想着研究一下ModbusRTU Over TCP/IP通讯,所以在TIA V16中按照教程做了一遍,因理解能力与作者的有些出入,所以重新做个笔记.

    在照着做的过程中,主要实现过程包括IP地址字符串解析函数封装、ModbusCRC校验算法函数封装、Socket发送、接收、报文拼接、报文解析等。具体步骤如下:

    设备组态

    IP地址解析FC函数

    IP地址解析FC函数SCL源

    FUNCTION "IpStringParse" : Void
    { S7_Optimized_Access := 'TRUE' }
    AUTHOR : bootloader
    VERSION : 0.1
    //IP地址解析FC函数
       VAR_INPUT 
          IP : String;
       END_VAR
    
       VAR_OUTPUT 
          iparr : Array[0..3] of Byte;
       END_VAR
    
       VAR_TEMP 
          pos : Int;
          ip_temp : String;
          len_temp : Int;
          index : UInt;
       END_VAR
    
    
    BEGIN
    	REGION 处理IP地址字符串
    	    FILL_BLK(IN := 0,
    	             COUNT := 20,
    	             OUT => #iparr[0]);
    	    #ip_temp := #IP;
    	    //查询第一个'.'的位置
    	    #pos := FIND(IN1 := #ip_temp, IN2 := '.');
    	    WHILE #pos <> 0 AND #index < 3 DO
    	        //截取第一个'.'之前的字符串,并转换为数值
    	        #iparr[#index] := UINT_TO_BYTE(STRING_TO_UINT(LEFT(IN := #ip_temp, L := #pos - 1)));
    	        #len_temp := LEN(#ip_temp);
    	        //去除第一个.之前的字符,得到新的字符串
    	        #ip_temp := MID(IN := #ip_temp, L := #len_temp - #pos, P := #pos + 1);
    	        //截取新字符串中查询第一个'.'的位置
    	        #pos := FIND(IN1 := #ip_temp, IN2 := '.');
    	        #index := #index + 1;
    	    END_WHILE;
    	    
    	    //将最后一部分转换为数值存在入
    	    #iparr[#index] := UINT_TO_BYTE(STRING_TO_UINT(IN := #ip_temp));
    	END_REGION
    END_FUNCTION
    

    CrcModbus校验FC函数

    CrcModbus校验FC函数SCL源

    FUNCTION "CrcModbusFun" : Void
    { S7_Optimized_Access := 'TRUE' }
    AUTHOR : bootloader
    VERSION : 0.1
    //CRCMODBUS校验FC函数
       VAR_INPUT 
          Command : Variant;
          dataLen : Int;
       END_VAR
    
       VAR_TEMP 
          buffer : Array[0..#MaxLen] of Byte;
          i : Int;
          j : Int;
          Crcreg : Word;
          Len : Int;
       END_VAR
    
       VAR CONSTANT 
          MaxLen : Int := 255;
       END_VAR
    
    
    BEGIN
    	#Crcreg := 16#FFFF;
    	IF #dataLen = 0 OR #dataLen > CountOfElements(IN := #Command) - 2 THEN
    	    //#Status := 01;
    	    RETURN;
    	ELSE
    	    //#Status := 00;
    	    #Len := #dataLen;
    	END_IF;
    	
    	//将数据转到缓冲区
    	VariantGet(SRC := #Command,
    	           DST => #buffer);
    	
    	//计算CRC校验码
    	FOR #i := 0 TO (#Len - 1) DO
    	    #Crcreg := #Crcreg XOR #buffer[#i];
    	    FOR #j := 0 TO 7 DO
    	        IF (#Crcreg AND 16#1) = 1 THEN
    	            #Crcreg := SHR_WORD(IN := #Crcreg, N := 1);
    	            #Crcreg := #Crcreg XOR 16#A001;
    	        ELSE
    	            #Crcreg := SHR_WORD(IN := #Crcreg, N := 1);
    	        END_IF;
    	    END_FOR;
    	END_FOR;
    	#buffer[#Len + 1] := SHR_WORD(IN := #Crcreg, N := 8);
    	#buffer[#Len] := #Crcreg AND 16#ff;
    	
    	//将缓冲区数据再写入到指针所指向的区域
    	VariantPut(SRC := #buffer,
    	           DST := #Command);
    	
    END_FUNCTION
    

    轮询令牌分发功能块FB

    轮询令牌分发功能块FB SCL源

    FUNCTION_BLOCK "token"
    { S7_Optimized_Access := 'TRUE' }
    AUTHOR : bootloader
    VERSION : 0.1
    //轮询令牌分发功能块
       VAR_INPUT 
          interval : Time := T#50ms;
          MultReqNums : Int;
          TurnArr : Variant;
       END_VAR
    
       VAR_OUTPUT 
          Status : Int;
       END_VAR
    
       VAR 
          token : Word;
          IntervalTon {InstructionName := 'TON_TIME'; LibVersion := '1.0'} : TON_TIME;
       END_VAR
    
       VAR_TEMP 
          tempArr : Array[0..#MaxReqNums] of Bool;
          i : Int;
          turnTriger : Bool;
       END_VAR
    
       VAR CONSTANT 
          MaxReqNums : Int := 20;
       END_VAR
    
    
    BEGIN
    	#turnTriger := #IntervalTon.Q;
    	#IntervalTon(IN := NOT #IntervalTon.Q,
    	             PT := #interval);
    	
    	//检查输入参数是否正确
    	IF #MultReqNums > UDINT_TO_INT( CountOfElements(#TurnArr)) OR #MultReqNums > #MaxReqNums THEN
    	    RETURN;
    	    #Status := 8001;
    	END_IF;
    	
    	//检查外部指针是不是布尔数组
    	IF (TypeOfElements(#TurnArr) <> Bool) THEN
    	    #Status := 8002;
    	    RETURN;
    	END_IF;
    	
    	IF #turnTriger THEN
    	    #token := #token + 1;
    	    IF #token >= #MultReqNums THEN
    	        //Statement section IF
    	        #token := 0;
    	    END_IF;
    	END_IF;
    	
    	//先将所有的复归
    	FOR #i := 0 TO #MultReqNums DO
    	    #tempArr[#i] := FALSE;
    	END_FOR;
    	
    	//把当前token置1
    	#tempArr[#token] := TRUE;
    	
    	VariantPut(SRC:=#tempArr,
    	           DST:=#TurnArr);
    	#Status := 0;
    	
    END_FUNCTION_BLOCK
    

    ModbusRTUOverTCP功能块FB

    ModbusRTUOverTCP功能块FB SCL源

    FUNCTION_BLOCK "ModbusRtuOverTcp"
    { S7_Optimized_Access := 'TRUE' }
    AUTHOR : bootloader
    VERSION : 0.1
    //ModbusRTUOverTCP功能块
       VAR_INPUT 
          Start : UInt;
          Length : UInt;
          IpAddr : String;
          Reg : Bool;
          ConnectID : CONN_OUC;
          Deviceld : Byte;
          timeOut : Time := T#50ms;
       END_VAR
    
       VAR_IN_OUT 
          Outdate : Variant;
       END_VAR
    
       VAR 
          ConnectParams {InstructionName := 'TCON_IP_v4'; LibVersion := '1.0'; S7_SetPoint := 'False'} : TCON_IP_v4 := (64, (), (), true, ([()]), 502, ());
          TSEND_C_Instance {InstructionName := 'TSEND_C'; LibVersion := '3.2'} : TSEND_C;
          TRCV_C_Instance {InstructionName := 'TRCV_C'; LibVersion := '3.2'; S7_SetPoint := 'False'} : TRCV_C;
          CommandBytes : Array[0..11] of Byte;
          start_recv : Bool;
          RecBuffer : Array[0..255] of Byte;
          index : Int;
          step : Int;
          R_TRIG_Instance {InstructionName := 'R_TRIG'; LibVersion := '1.0'; S7_SetPoint := 'False'} : R_TRIG;
          startSend : Bool;
          timeOutResponseTon {InstructionName := 'TON_TIME'; LibVersion := '1.0'; S7_SetPoint := 'False'} : TON_TIME;
          xTimeOutResPonse : Bool;
          init : Bool;
       END_VAR
    
       VAR_TEMP 
          ipArrayTemp : Array[0..3] of Byte;
          count : Int;
       END_VAR
    
    
    BEGIN
    	(* 
    	输入参数说明:
    	Start:读取保持寄存器的起始地址
    	Length:读取保持寄存器的个数
    	IPAddr:IP 地址字符串
    	Req:请求指令(只接受边沿信号)
    	DeviceID: 设备单元ID
    	ConnectID:网络连接资源ID(背景数据块不同时,需要保证唯一性)
    	输入输出参数:
    	Outdata:指向读取的数据保存区域的指针 *)
    	
    	//除始化IP地址,相同背景DB的功能块,只需要解析一次
    	//
    	
    	// ---------------------------------------------------------------------------------------------------------
    	// ConnectParams 静态变量 类型 TCON_IP_v4
    	// ConnectParams 设置硬件接口 InterfaceId
    	// ConnectParams 连接ID赋值 ID
    	// ConnectParams 设置连接类型 ConnectionType
    	// ConnectParams 设置主动连接 ActiveEstablished
    	// ConnectParams IP地址解析 RemoteAddress
    	// ConnectParams 设置远程设备端口 RemotePort
    	// 
    	IF NOT #init THEN
    	    
    	    #ConnectParams.ID := #ConnectID;
    	    #ConnectParams.ActiveEstablished := TRUE;
    	    "IpStringParse"(IP := #IpAddr,
    	                    iparr => #ipArrayTemp);
    	    #ConnectParams.RemoteAddress.ADDR[1] := #ipArrayTemp[0];
    	    #ConnectParams.RemoteAddress.ADDR[2] := #ipArrayTemp[1];
    	    #ConnectParams.RemoteAddress.ADDR[3] := #ipArrayTemp[2];
    	    #ConnectParams.RemoteAddress.ADDR[4] := #ipArrayTemp[3];
    	    #ConnectParams.RemotePort := 502;
    	    #init := TRUE;
    	END_IF;
    	// ---------------------------------------------------------------------------------------------------------
    	//拼写ModbusRTU报文 03功能码
    	#CommandBytes[0] := #Deviceld;
    	#CommandBytes[1] := 3;
    	#CommandBytes[2] := UINT_TO_BYTE(#Start / 256);
    	#CommandBytes[3] := UINT_TO_BYTE(#Start MOD 256);
    	#CommandBytes[4] := UINT_TO_BYTE(#Length / 256);
    	#CommandBytes[5] := UINT_TO_BYTE(#Length MOD 256);
    	
    	//计算CRC校验
    	"CrcModbusFun"(Command:=#CommandBytes,
    	               dataLen:=6);
    	// ---------------------------------------------------------------------------------------------------------
    	//检测到发送指令
    	#R_TRIG_Instance(CLK := #Reg);
    	IF #R_TRIG_Instance.Q AND NOT #TSEND_C_Instance.BUSY THEN
    	    #step := 0;
    	    #startSend := TRUE;
    	ELSE
    	    #startSend := FALSE;
    	END_IF;
    	
    	#TSEND_C_Instance(REQ := #startSend,
    	                  CONT := 1,
    	                  LEN := 8,
    	                  CONNECT := #ConnectParams,
    	                  DATA := #CommandBytes);
    	
    	#timeOutResponseTon(IN := #xTimeOutResPonse,
    	                    PT := #timeOut);
    	
    	CASE #step OF
    	    0:
    	        //等待发送完成
    	        IF #TSEND_C_Instance.DONE THEN
    	            #start_recv := TRUE;
    	            #step := 10;
    	        END_IF;
    	        //等待接收
    	    10:
    	        #xTimeOutResPonse := TRUE;
    	        //接收指令为一个异步指令,需多个扫描周期完成
    	        #TRCV_C_Instance(EN_R := #start_recv,
    	                         CONT := TRUE,
    	                         LEN := #Length * 2 + 5,
    	                         CONNECT := #ConnectParams,
    	                         DATA := #RecBuffer);
    	        //等待接收完成
    	        IF #TRCV_C_Instance.DONE THEN
    	            #start_recv := FALSE;
    	            //将数据移动到指针所指的区域
    	            #count := MOVE_BLK_VARIANT(SRC := #RecBuffer, COUNT := #Length * 2, SRC_INDEX := 3, DEST_INDEX := 0, DEST => #Outdate);
    	            #step := 20;
    	        ELSIF #TRCV_C_Instance.ERROR OR #timeOutResponseTon.Q THEN
    	            //至少有一套出现过故障
    	            #xTimeOutResPonse := 0;
    	            #step := 30;
    	        END_IF;
    	    20:
    	        //接收成功也要复归计时器
    	        #xTimeOutResPonse := 0;
    	END_CASE;
    	
    END_FUNCTION_BLOCK
    

    data数据块DB

    data数据块DB SCL源

    DATA_BLOCK "data"
    { S7_Optimized_Access := 'TRUE' }
    AUTHOR : bootloader
    VERSION : 0.1
    NON_RETAIN
       VAR 
          interval : Time;
          turn : Array[0..10] of Bool;
          turn_Rtrig : Array[0..10] of Bool;
          turn_Rtrig_1 : Array[0..10] of Bool;
          outdata10 : Array[0..19] of Byte;
          outdata11 : Array[0..19] of Byte;
          outdata20 : Array[0..19] of Byte;
          outdata21 : Array[0..19] of Byte;
       END_VAR
    
    
    BEGIN
       interval := T#50ms;
    
    END_DATA_BLOCK
    

    多重背景块FB

    主程序

    轮询、并发模拟

    S7-PLCSIM AdvanceV3.0 可以支持通信模拟.

    Modbus 从站或服务器可以用modbus slave软件模拟.在客户机中分别利用Modbus slave 模拟两个支持ModbusRTU 串口服务器IP地址分别为192.168.159.1 和192.168.159.2,每个服务器创建2个设备,协议选择ModbusRtu over TCP,并取消勾选忽略设备ID 选项.

    收发报文监视如下:

    数据解析

    接收到的数据保存在字节数组中,具体的数据类型取决于协议对寄存器的约定,如果需要批量解析为整形或浮点型,可以新建一个大小一致的存储区,数组中元素数据类型为协议约定的数据类型,然后可以用POKE_BLK 指令完成,这里浮点数并没有考虑大小端的问题.

    POKE_BLK(area_src:=16#84,
             dbNumber_src:=1,
             byteOffset_src:=50,
             area_dest:=16#84,
             dbNumber_dest:=1,
             byteOffset_dest:=90,
             count:=20);
    
    POKE_BLK(area_src := 16#84,
             dbNumber_src := 1,
             byteOffset_src := 50,
             area_dest := 16#84,
             dbNumber_dest := 1,
             byteOffset_dest := 110,
             count := 20);
    

    总结

    ModbusRTU Over TCP/IP通讯就是通过TCP 传输ModbusRTU 报文,其中ModbusRTU 报文格式可以查询相关文档,CRC校验分为查表法和计算法,两者各有优缺点,在程序块编写过程中,对于重复逻辑应采用循环结构如WHILE、FOR 等;对于输入参数为不定长数组的,形参需要设置为Variant 指针,对于内存区的批量读写操作,可以使用PEEK 和POKE 指令、Move_BLK、Move_BLK_Variant、Fill_BLK、VariantPut、VariantGet等指令.以上功能块部分程序仅为了强化博途间接寻址、程序结构、SCL、以及程序封装应用,实际工程应用时,可以适当修改.

    声明

    本文主要内容及思路来源于博途SCL实现自定义ModbusRtu Over TCP功能块,因原文有些地方描述不是很详细,所以在调试时,花了写时间查找原因,我在原文的基础上,自己做了测试,并深化了细节.

  • 相关阅读:
    动词 + to do、动词 + doing
    图像直线检测——霍夫线变换
    x=min(x, y)
    x=min(x, y)
    算法 Tricks(三)—— 数组(序列)任意区间最小(大)值
    算法 Tricks(三)—— 数组(序列)任意区间最小(大)值
    分治法求解切割篱笆
    分治法求解切割篱笆
    GMM的EM算法实现
    秒杀多线程第二篇 多线程第一次亲热接触 CreateThread与_beginthreadex本质差别
  • 原文地址:https://www.cnblogs.com/guyk/p/15169789.html
Copyright © 2011-2022 走看看