基于QtPy (PySide6) 的PLC-HMI工程实战记录(一)创建PLC工程模板项目
一个PLC-HMI实战工程的过程记录。
一、新建TIA工程,组态PLC时可以直接指定PLC型号,也可以不指定CPU,使用unspecified CPU:
这样做的好处是保持模板的通用性,并且在硬件确定之后不用手动逐个输入组态硬件,连接到已通电的CPU,使用“在线”、“硬件检测”就可以直接把实际的硬件读取到项目中。
使用这种读取在线PLC的方式组态,有一个坑点是:必须给在线硬件分配PROFINET 设备名称,并与程序中的名称一致!否则会组态失败。
二、新建程序分组,命名为“系统OB”:
添加OB30、OB82、OB100、OB86、OB122等必要和有可能用到的OB系统块。
三、新建PLC数据类型
这个命名为“AreaVar”的数据类型,包含了数据分区(I/QM/DB)、DB编号、数据起点、数据字节长度等信息,可用来进行各种以字节为单位的块间操作。这个数据类型是本工程与上位机通信的基础类型,它指定了数据报文的位置和长度。
四、导入之前导出保存的CRC校验表DB文件:
“添加新的外部文件”、“从源生成块”,这样就把外部的db文件转换为项目中的DB块。
- 新建分组“背景类DB”,并将生成的DB块拖入:
五、创建TCP连接并组态和编程连接:
创建连接之前需要将CPU指定为具有网络通信能力的型号,这里指定为1513-1PN。
1、新建程序块分组:功能块
2、在分组中新建FB,命名为“上位通信”,此功能块负责与上位机通信。
3、打开上位通信FB,开始编程。
4、从指令库中将TCON拖入FB的网络,会自动创建一个DB块:TCON_DB。
5、点击“开始组态”按钮,对连接进行组态:
在打开的组态窗口中:
- 伙伴:未指定
- 伙伴主动建立连接
- 连接数据:新建
- 作为一个模板,无需为PLC特意指定IP地址,当在实际的项目中改变了CPU的IP地址后,连接参数中的地址会自动随之改变为新的。
这样就将PLC创建为TCP主站。
可以看到,在系统块的程序资源中自动生成了2个数据块:
PLC_1_Connection_DB,用于存储连接参数。
TCON_DB则是TCON指令的背景数据块:
这些都是自动生成和分配的,无需人工参与,只要知道其用途即可。
6、编译一下,确认生成无误:
7、为上位通信FB块创建一个数据块:上位通信_工作DB,并在DB中定义一个Struct,作为TCON指令的参数容器:
8、在上位通信FB中为TCON指令分配参数:
9、在OB1中调用上位通信FB
调用时会自动创建一个背景DB,将其命名为:上位通信_背景DB,并拖入“背景类DB”分组中。这个分组中的DB全部为背景类的DB,无需人工干预和编辑。
10、在上位通信_工作DB中补充发送和接收的参数容器:
11、在上位通信FB中继续编程,拖入发送和接收指令,增加接收和发送功能
12、在创建两个相同的DB块,用来存放上行和下行报文
13、继续完善上位通信FB,把发送和接收指令的DATA端口指向新建的两个报文DB
14、创建FC块,命名为“_build_AreaVal”
功能是由Area、DBnum、offset、byteCount这几个参数来生成一个自定义的区域变量:AreaVal
接口:
Input 数据类型
Area Byte
DBNum UInt
Offset UInt
byteCount UInt
InOut
destAreaVar "AreaVar"
scl:
#destAreaVar.Area := #Area; #destAreaVar.DBNum := #DBNum; #destAreaVar.Offset := #Offset; #destAreaVar.byteCount := #byteCount;15、创建FC块,命名为“组态上行报文”,用来组态上行报文
接口:
Input
frameHeader1 Byte 帧头字节1
frameHeader2 Byte 帧头字节2
frameEnd1 Byte 帧尾字节1
frameEnd2 Byte 帧尾字节2
srcAreaVars Array[*] of "AreaVar" 源数据区域,支持多区域输入
DB_CRC_TABLE Array[0..255] of Word CRC查表运算的表格
Output
InOut
destArea "AreaVar" 目标区域,存放上行报文的字节
Temp
Temp_1 "AreaVar"
wCRC Word
i DInt
pos DInt 字节临时指针
bTemp Byte
wTemp Word
nIndex DInt
varTemp "AreaVar"
tmp_DI1 DInt
tmp_DI2 DInt
Constant
Return
组态上行报文 Void
scl:
////////////////////////////组态帧头/////////////////////////// POKE(area := #destArea.Area, dbNumber := #destArea.DBNum, byteOffset := 0, value := #frameHeader1); //帧头1 POKE(area := #destArea.Area, dbNumber := #destArea.DBNum, byteOffset := 1, value := #frameHeader2); //帧头2 #pos := 2; //#tmp_DI1 := LOWER_BOUND(ARR := #srcAreaVars, DIM := 1); //#tmp_DI2 := UPPER_BOUND(ARR := #srcAreaVars, DIM := 1); FOR #i := LOWER_BOUND(ARR := #srcAreaVars, DIM := 1) TO UPPER_BOUND(ARR := #srcAreaVars, DIM := 1) DO #varTemp := #srcAreaVars[#i]; ////////组态区域变量的信息(本帧数据的区域和起点、长度)////// POKE(area := #destArea.Area, //本帧数据的区域 dbNumber := #destArea.DBNum, byteOffset := #pos, value := #varTemp.Area); #pos := #pos + 1; POKE(area := #destArea.Area, //字节占位 dbNumber := #destArea.DBNum, byteOffset := #pos, value := #varTemp.Area); #pos := #pos + 1; POKE(area := #destArea.Area, //本帧数据的DB号 dbNumber := #destArea.DBNum, byteOffset := #pos, value := #varTemp.DBNum); //value := DINT_TO_BYTE(IN:=#varTemp.DBNum)); #pos := #pos + 4; POKE(area := #destArea.Area, //本帧数据的起始字节偏移 dbNumber := #destArea.DBNum, byteOffset := #pos, value := #varTemp.Offset); #pos := #pos + 4; POKE(area := #destArea.Area, //本帧数据的字节数 dbNumber := #destArea.DBNum, byteOffset := #pos, value := #varTemp.byteCount); #pos := #pos + 4; ///////////////////////////////组态帧数据内容///////////////////////// POKE_BLK(area_src := #varTemp.Area, dbNumber_src := #varTemp.DBNum, byteOffset_src := #varTemp.Offset, area_dest := #destArea.Area, dbNumber_dest := #destArea.DBNum, byteOffset_dest := #pos, count := #varTemp.byteCount); #pos := #pos + #varTemp.byteCount; END_FOR; ///////////////////////////////组态帧尾//////////////////////////// POKE(area := #destArea.Area, //帧尾1 dbNumber := #destArea.DBNum, byteOffset := #pos, value := #frameEnd1); #pos := #pos + 1; POKE(area := #destArea.Area, //帧尾2 dbNumber := #destArea.DBNum, byteOffset := #pos, value := #frameEnd2); #pos := #pos + 1; /////////////////////////////////CRC16校验和////////////////////////////// #destArea.byteCount := #pos; #wCRC := 16#FFFF; FOR #i := 0 TO #destArea.byteCount - 1 DO #bTemp := PEEK(area := #destArea.Area, byteOffset := #i, dbNumber := #destArea.DBNum); #wTemp := #wCRC XOR BYTE_TO_WORD(IN := #bTemp); #wTemp := #wTemp AND 16#00FF; #nIndex := WORD_TO_INT(IN := #wTemp); #wCRC := SHR(IN := #wCRC, N := 8) XOR #DB_CRC_TABLE[#nIndex]; END_FOR; //写入CRC字 POKE(area := #destArea.Area, dbNumber := #destArea.DBNum, byteOffset := #pos, value := #wCRC); #pos := #pos + 2; #destArea.byteCount := #pos;16、在“上位通信_工作DB”中增加帧头和帧尾的定义:
17、在“上位通信”FB中编程,组态上行报文:
上行报文的区域暂时不填,具体到实际的PLC再填。
18、创建FC块,命名为“解析下行数据”:
接口:
Input
frameHeader1 Byte 帧头字节1
frameHeader2 Byte 帧头字节2
frameEnd1 Byte 帧尾字节1
frameEnd2 Byte 帧尾字节2
rsv_area_var "AreaVar" 接收下行报文的DB块
DB_CRC_TABLE Array[0..255] of Word
Output
DONE Bool
InOut
Temp
i DInt 0.0
pos DInt 4.0 字节临时指针
tmp_head Byte 8.0
tmp_end Byte 9.0
tmp_areaVar "AreaVar" 10.0
bTemp Byte 28.0
wTemp Word 30.0
wCRC Word 32.0
nIndex Int 34.0
Constant
Return
解析下行数据 Void
scl:
#DONE := FALSE; ////////////////////////////解析帧头/////////////////////////// #pos := 0; // 字节指针 #tmp_head := PEEK(area := #rsv_area_var.Area, dbNumber := #rsv_area_var.DBNum, byteOffset := #pos); //帧头1 IF #frameHeader1 <> #tmp_head THEN #DONE := FALSE; RETURN; END_IF; #pos := #pos + 1; #tmp_head := PEEK(area := #rsv_area_var.Area, dbNumber := #rsv_area_var.DBNum, byteOffset := #pos); //帧头2 IF #frameHeader1 <> #tmp_head THEN #DONE := FALSE; RETURN; END_IF; #pos := #pos + 1; IF PEEK(area := #rsv_area_var.Area, dbNumber := #rsv_area_var.DBNum, byteOffset := #pos) = #frameEnd1 THEN IF PEEK(area := #rsv_area_var.Area, dbNumber := #rsv_area_var.DBNum, byteOffset := #pos + 1) = #frameEnd2 THEN #DONE := TRUE; RETURN; END_IF; END_IF; /////////////////////////解析区域变量的参数//////////////////// #tmp_areaVar.Area := PEEK(area := #rsv_area_var.Area, dbNumber := #rsv_area_var.DBNum, byteOffset := #pos); //帧区域 #pos := #pos + 2; #tmp_areaVar.DBNum := DWORD_TO_DINT(IN := PEEK_DWORD(area := #rsv_area_var.Area, dbNumber := #rsv_area_var.DBNum, byteOffset := #pos)); //帧DB号 #pos := #pos + 4; #tmp_areaVar.Offset := DWORD_TO_DINT(PEEK_DWORD(area := #rsv_area_var.Area, dbNumber := #rsv_area_var.DBNum, byteOffset := #pos)); //帧字节偏移 #pos := #pos + 4; #tmp_areaVar.byteCount := DWORD_TO_DINT(PEEK_DWORD(area := #rsv_area_var.Area, dbNumber := #rsv_area_var.DBNum, byteOffset := #pos)); //帧字节长度 #pos := #pos + 4; #tmp_areaVar.bit := DWORD_TO_DINT(PEEK_DWORD(area := #rsv_area_var.Area, dbNumber := #rsv_area_var.DBNum, byteOffset := #pos)); //帧位(bit) #pos := #pos + 4; //pos目前位于数据区之前 ////////////////////////////解析数据内容和CRC////////////////////////// //解析CRC #wCRC := 16#FFFF; FOR #i := 0 TO #pos + #tmp_areaVar.byteCount + 1 DO //读取接收DB从0到数据长度,再多读4位(帧尾) #bTemp := PEEK(area := #rsv_area_var.Area, byteOffset := #i, dbNumber := #rsv_area_var.DBNum); #wTemp := #wCRC XOR BYTE_TO_WORD(IN := #bTemp); #wTemp := #wTemp AND 16#00FF; #nIndex := WORD_TO_INT(IN := #wTemp); #wCRC := SHR(IN := #wCRC, N := 8) XOR #DB_CRC_TABLE[#nIndex]; END_FOR; #wCRC := #wCRC; #pos := #pos + #tmp_areaVar.byteCount; //pos目前位于数据区之后,帧尾之前 IF #wCRC <> PEEK_WORD(area := #rsv_area_var.Area, byteOffset := #pos + 2, dbNumber := #rsv_area_var.DBNum) THEN //CRC失败 #DONE := FALSE; RETURN; END_IF; ////////////////////////////解析帧尾/////////////////////////////////// #tmp_end := PEEK(area := #rsv_area_var.Area, dbNumber := #rsv_area_var.DBNum, byteOffset := #pos); //帧尾1 IF #frameEnd1 <> #tmp_end THEN #DONE := FALSE; RETURN; END_IF; #pos := #pos + 1; #tmp_end := PEEK(area := #rsv_area_var.Area, dbNumber := #rsv_area_var.DBNum, byteOffset := #pos); //帧尾2 IF #frameEnd2 <> #tmp_end THEN #DONE := FALSE; RETURN; END_IF; #pos := #pos + 1; //此时的pos位于帧尾后,CRC前 #pos := #pos - #tmp_areaVar.byteCount - 2; //把pos移回到数据区的起始 /////////////////////////////////如果是bool////////////////////////////// IF #tmp_areaVar.bit <> 16#FFFFFFFF THEN POKE_BOOL(area := #tmp_areaVar.Area, ////写一个bool位 dbNumber := #tmp_areaVar.DBNum, byteOffset := #tmp_areaVar.Offset, bitOffset := DINT_TO_INT(IN := #tmp_areaVar.bit), value := PEEK_BOOL(area := #rsv_area_var.Area, dbNumber := #rsv_area_var.DBNum, byteOffset := #pos, bitOffset := #tmp_areaVar.bit)); /////////////////////////////////如果不是bool////////////////////////////// ELSE //写块数据 POKE_BLK(area_src := #rsv_area_var.Area, dbNumber_src := #rsv_area_var.DBNum, byteOffset_src := #pos, area_dest := #tmp_areaVar.Area, dbNumber_dest := #tmp_areaVar.DBNum, byteOffset_dest := #tmp_areaVar.Offset, count := #tmp_areaVar.byteCount); END_IF; #DONE := TRUE;