博图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功能块,因原文有些地方描述不是很详细,所以在调试时,花了写时间查找原因,我在原文的基础上,自己做了测试,并深化了细节.

posted @ 2021-08-21 16:05  生命在等待中延续  阅读(3436)  评论(0编辑  收藏  举报