博圖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功能塊,因原文有些地方描述不是很詳細,所以在調試時,花了寫時間查找原因,我在原文的基礎上,自己做了測試,並深化了細節.
