将之前的画面和代码用复制粘贴的方法复制四份,就完成了整个主画面和主程序的基本构建。

我用PYQT5做的第一个实用的上位机项目(六)-LMLPHP

下面的工作是关于PLC和通信。 

        上位机项目,其与PLC通信的模式很多都是这样的:在没有操作和设置的平常显示界面,按照预定周期从PLC读取当前页面需要刷新的部件的数据字节,并对字节进行解码和刷新。当有了设置和改变的动作,需要改写PLC的内部数据时,把需要改写的地址和数据发送给PLC。

        查阅了很多资料,关于上位机与PLC通信这块,大多数都是针对西门子的,并且使用了Snap7。使用Snap7确实可以使问题简化,但是如果PLC不是西门子品牌就无能为力了。即使是西门子PLC,还要对PLC的内部寄存器地址、指针、语句编程这些知识进行深入学习才能灵活应用。像我这样的以搞控制为主的,PLC和上位机编程都只是简单用用的人,就感觉很吃力。一句题外话,玩了三十年工控,到目前为止PLC程序也做过上千个了,大多数时候都还是梯形图和直接地址访问,每每看到那些大公司的专业PLC编程工程师熟练地使用间接寻址和语句编程,都觉得很汗颜。所以,这里采用了通用的tcp ip的socket通信模式,自定义报文,直接地址读写,好处是提高了通用性、降低了技术难度,坏处是PLC端需要额外编写一些通信程序,不过PLC的程序全部都是直接地址访问,用不到考虑指针和寄存器地址映射之类的,编程也可以用梯形图,属于简单劳动。

        既然是自定义报文,每个人完全可以根据PLC的品牌型号以及自己的习惯来约定报文的内容。我这里的范例,上位机发送到PLC的报文采用固定长度,每个报文长度12个字节,前6个字节是功能码,后面4个字节是数据内容,最后2个字节与第一第二个字节内容相同,作为结束符用来断帧和校验,防止TCP数据粘连。

        1、对PLC的例行的周期查询,报文第1、2字节为字符“A0”,第3字符是画面编号,第4、5是对应的周期,第6是周期时间的单位,7-10字节用字符“0”填充,11、12与第1、2字节相同为字符“A0”。例如:报文A0101S0000A0,其含义是编号1的画面一秒周期的查询。至于本次需要查询哪些部件的变量,这些变量的类型和字节长度都是提前规划好并在PLC端写好了对应的子程序,当PLC端收到报文并且报文头6字节与字符串“A0101S”比对成功,报文的第11、12字节与字符串“A0”比对成功,就执行相应的子程序把上位机需要的地址的内容复制到PLC的通信发送缓冲区打包并发送至上位机,上位机收到返回的数据后,按照提前规划的变量的类型和字节长度解码并刷新显示界面。

        总之,所有的需要查询的内容都是已知和提前规划的,PLC端只需要比对收到的报文字符串前6个字节和第11、12 字节,调用对应的子程序将需要的地址的数据复制到送缓冲区打包发送。

        举例:画面2,每10秒钟需要读取PLC的MW10、VD100、QB0这几个变量,那么每隔10秒钟,发送报文字符串“A0210S0000A0”到PLC,PLC收到报文,前6个字节与“A0210S”比对成功,第11、12字节与字符串“A0”比对成功,执行对应的子程序将MW10、VD100、QB0这几个变量的值写入发送缓冲区打包并发送至上位机。上位机收到返回数据,按照提前规划的“整数、实数、字节”(即MW10、VD100、QB0这几个变量的类型)的顺序解码数据并分配刷新各个部件。由于采用了约定自定义报文的方式,PLC端对所有地址的操作都是直接的地址访问,用不着考虑寄存器实际地址和指针,不同的PLC,上位机的报文也是通用的(唯一需要考虑大小端编码)。

        另,各个周期的查询内容是长周期包含短周期,例如10秒周期的查询包含了1秒周期的查询内容。

        2、对PLC的写操作,由于本例项目很小,需要写的数据并不多,所以也采用了约定报文的方式,如果数据量较大,这种方式就不太方便,必须用到指针了。话又说回来,之所以费劲巴交地自己写代码,就是因为项目小,利润低,没法用正版scada,如果是数据很多的大项目,有足够的利润支撑就直接用正版wincc、组态王了,那功能又强大,使用又简便。至于盗版嘛,看您高不高兴了,不在讨论范围内。

        言归正传,本例中,对PLC的写操作,第1、2字节为字符“B0”,第3、4字节为事件编号,例如可以将“启动/停止缓凝剂“事件定义为01事件,5、6字节用字符0填充,7、8、9、10字节为数据内容,这里的数据内容之所以用了4个字节是因为有可能传输的最长数据格式是浮点数,长度为4个字节。对于长度小于4个字节的数据类型,靠前的字节为有效数据,后面的用b'\x00'填充。第11、12字节为字符“B0”,与第1、2字节相同,作为结束符用来断帧和校验,防止TCP数据粘连。

        当PLC收到报文后,并且第1、2字节和第11、12字节与字符”B0“比对成功,就读取第3、4字节的事件编号,执行相应编号的子程序,执行约定的操作。

        举例:上位机发送报文b'B001\x00\x00\x01\x00\x00\x00B0',这个报文的开头和结尾为B0,表示要对PLC写操作,事件编号为第3、4字节01,5、6字节无意义,第7字节为数据位b'\x01',将其约定为”启动缓凝剂“,在PLC程序里,实现该事件的的方法是将Q0.0置位,编写并执行该子程序(或网络)即可。

        至此,通信报文的基本规划就完成了,还可以加入一些别的约定报文,例如报警、故障什么的,看个人需要了。总结一下,本例采用了基于tcp ip协议的socket通信(这个叫法貌似不是很专业,管他呢,我也不是专业的程序员,能用就好),PLC作为服务器,上位机作为客户端。所有的通信均由上位机发起,上位机的程序中有系统定时器,周期地查询PLC中的数据,PLC根据收到的报文内容做出响应,并根据约定代码的不同执行不同的子程序或网络,对PLC内部数据进行读写和发送。在任何时候,上位机发给PLC的报文都是固定长度的,报文的首2和末2字节内容相同,作为事件定义和断帧校验。tcp通信是基于”流“的,就没有严格意义上的”帧“,tcp通信的最大隐患就是数据帧长度的不确定性,很容易造成接收缓冲区数据排序混乱,这也是我之前做过的很多PLC通信项目遇到过的问题,固定PLC接收通信数据的长度并把通信报文中加入断帧和校验字符,就可以很好地解决这个问题。

        下面的工作就是编写实际的程序了。

10-11 00:05