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