博图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功能块,因原文有些地方描述不是很详细,所以在调试时,花了写时间查找原因,我在原文的基础上,自己做了测试,并深化了细节.
__EOF__
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发中对象命名的一点思考
· .NET Core内存结构体系(Windows环境)底层原理浅谈
· C# 深度学习:对抗生成网络(GAN)训练头像生成模型
· .NET 适配 HarmonyOS 进展
· .NET 进程 stackoverflow异常后,还可以接收 TCP 连接请求吗?
· 本地部署 DeepSeek:小白也能轻松搞定!
· 如何给本地部署的DeepSeek投喂数据,让他更懂你
· 在缓慢中沉淀,在挑战中重生!2024个人总结!
· 从 Windows Forms 到微服务的经验教训
· 李飞飞的50美金比肩DeepSeek把CEO忽悠瘸了,倒霉的却是程序员