理解内存结构是所有可编程逻辑控制器(PLC)的起点。这至关重要,因为我们编写的程序操作的是内存单元。即使是最简单的程序——直接将螺丝端子数字输入的值传输到螺丝端子数字输出——也是内存到内存的操作。它至少涉及两个内存位置和两个对这些内存位置进行操作的功能,包括读取和写入操作。在梯形图逻辑(LL)中,我们将这些功能分别识别为常开触点和线圈。阅读本文时请牢记这一区别。虽然我们看到的是触点和线圈的图像,但我们并不是直接操作硬件。相反,我们执行的是内存到内存的操作。
Arduino 回顾
本文将探讨全局与局部内存位置之间的关系。为缩小文章范围,我们将讨论限定为布尔值。
假定读者已有Arduino微控制器(C语言编程)的编程经验。但为了最大程度降低门槛,我们先简要回顾Arduino的重要概念,包括循环以及涉及局部和全局变量的内存空间。掌握这些知识后,您就能以最小的困惑和努力从Arduino过渡到LL。
准备好利用Arduino Opta PLC的强大功能与IEC 61131-3语言(如LL)了吗?让我们告别传统的Arduino集成开发环境(IDE),转向Arduino PLC IDE。
技术提示 :本文提供了开始为Arduino Opta进行LL编程所需的背景知识。对文章冗长且教科书式的写作风格表示歉意。但这是使用梯形图逻辑编程Opta必需的背景材料。衷心希望您能收藏本页,在开始编程Opta PLC时参考。您会发现这些概念简单但不直观,且通常未被记录——因为许多PLC采用较旧的固定内存模型,而Opta使用的是灵活的基于标签的方法。
Arduino 循环
每个Arduino程序都包含setup()和loop()函数。请注意,setup()函数中的语句仅在启动时执行一次。loop()中的语句会顺序执行并不断循环——loop()的最后动作就是跳回开头重新开始(简化解释,另见static关键字)。只要微控制器未遇到阻塞代码,这个循环每秒会执行成千上万次。
技术提示 :阻塞代码是指导致程序停止的一个或多个语句。例如tone()、delay()或while(WaitForEvent)等控制语句。在阻塞代码持续期间,微控制器将处于停滞状态。除非实现中断等机制,否则微控制器无法响应事件。需注意PLC代码专门设计用于避免阻塞情况,确保PLC能响应现实事件。对于从Arduino转向PLC的程序员,阻塞概念可能成为理解障碍。
Arduino 输入 / 输出
Arduino通过digitalRead()和digitalWrite()函数实现输入/输出(I/O) 操作。这是内存到内存传输的典型示例。digitalRead()函数读取输入引脚状态,并将数据传输到内存位置。digitalWrite()函数将布尔内存位置的值传输到物理输出引脚。
Arduino 内存作用域
C语言编程中较具挑战性的概念之一是内存作用域。当学习者首次接触函数时,这点尤为明显。典型错误是在全局、多个函数内及loop()中不当使用同名变量。例如程序员可能在所有三个位置使用索引变量"i":for(int i = 0; i < 10; i++)。编译器秉持"你清楚自己在做什么"的理念,会愉快地按照既定规则编译代码。这对不了解内存作用域规则的初学者并不友好。最终生成的代码可能难以调试。
虽然这些规则不能直接套用于PLC的LL语言,但重申概念仍有价值:
-
全局变量:定义在任何函数之外的变量。在Arduino草图中,全局变量通常定义在setup()函数之上和之外。同一标签页Arduino草图内的所有语句均可访问该全局变量(简化解释,另见extern关键字)。
-
局部变量:定义在函数内部的变量。仅能由该函数内的语句访问。局部变量的值可通过引用或传值方式传递给其他函数。但该函数无法直接访问原变量。
Arduino C 方言与梯形图的共通性
Arduino编程与采用梯形图的PLC编程存在诸多相似之处。循环、I/O操作和内存结构等概念对两种设备的编程都至关重要。下面我们将探讨这两种语言的相似点。请注意梯形图的实现方式并非通用标准。各家PLC制造商处理内存的方式各不相同。Arduino的实现则是基于现代化标签的复杂方案。
梯形图循环结构
程序扫描是PLC的基础程序结构。从编程哲学角度看,可将其视为使PLC能及时响应现实事件的循环结构。图1所示的时钟是个很好的类比。12点位置开始执行后台维护任务。3点位置读取螺丝端子输入并传输至内存。6点位置执行描述内存间操作的程序。最后在9点位置,将布尔内存内容传输至PLC的螺丝端子输出端。
该循环每秒执行约1000次。必须避免阻塞代码,否则会阻碍PLC对现实事件的响应。这一点在PLC定时器中体现得最为明显。它们被设计为非阻塞模式。例如,考虑一个5秒的延时接通定时器(TON)。该定时器在被判定为真之前,会经历数千次程序扫描的访问(进入)。定时器在任何时候都不会中断程序扫描。
技术提示 :PLC的看门狗定时器与程序扫描密切相关。作为正常12点钟的例行维护流程的一部分,PLC会象征性地“安抚”基于硬件的看门狗。如果循环时间过长,看门狗会变得焦虑,唤醒并将PLC置于硬(非运行)故障状态。与普遍看法相反,在梯形逻辑中执行类似while()或for()的耗时循环是可能的;例如用于遍历数组。这些6点钟的循环可能导致整个程序扫描速度减慢到足以唤醒看门狗。
图 1 :这个时钟类比代表了PLC程序扫描期间采取的动作。
梯形逻辑 I/O
本月早些时候,我进行了一次关于类似Arduino的编程语言用于PLC编程的讨论。这是我第一次听到“PLC风格”这个词,用于描述在编程循环中处理I/O的方式。这个想法与图1所示的时钟密切相关。
代码结构在loop()开始时捕获所有输入。然后执行各种内存到内存的操作,前提是影子寄存器在扫描期间保持稳定。在循环结束时,用我们的类比来说是9点钟,输出被更新。这与传统做法形成对比,传统做法是在程序中随时处理I/O。
另一种识别这种“PLC风格”的方法是使用“影子寄存器”的语言。在3点钟,螺丝端子输入被传输到影子寄存器,然后由你的6点钟代码使用。稍后在9点钟,输出影子寄存器被传输到螺丝端子输出。
技术提示 :影子寄存器标识了一个包含微控制器输入映像的内存位置。它也可以被解释为保存将要成为输出的内容的内存位置。在这两种情况下,你的代码操作的是影子寄存器,而不是直接读取或写入硬件。这与图1直接相关,在3点钟输入被传输到影子寄存器。然后你的PLC程序与影子寄存器交互,而不是螺丝端子上的实际信息。实际上,PLC在下次程序扫描前对任何输入变化都处于"盲视"状态。
梯形图逻辑内存作用域
我们终于抵达了梯形图逻辑讨论的核心。虽然花费了些时间铺垫,但必须先解释循环结构及处理螺丝端子I/O的方法。掌握这些知识后,我们就能探索如何读取输入、进行处理并将结果传递至输出。本文后续将证明Opta LL内存作用域与传统C程序密切相关。
基于标签的结构
Arduino Opta采用基于标签的内存系统。这在很多方面都是传统Arduino编程方式的延续。您可以像往常一样自由命名变量并定义其类型。定义布尔型变量甚至浮点型数组都很简单。
技术提示 :基于标签的PLC内存自由度并非通用标准。大量PLC具有固定内存位置,提供N个布尔类型或M个浮点类型空间。程序员可自由使用编号内存位置或其别名。程序员始终需要高度关注内存位置,这种单一变量存储方式形成了扁平化内存结构,所有内存位置都是全局的。与之形成对比的是基于标签的结构,程序员无需关注变量位置,只需关注名称和类型。
标签命名
与传统Arduino类似,您可以在限定范围内自由选择变量名称。作为聪明的程序员,您应该利用这点来创建具有描述性的标签。这样能大大增强代码的自解释性。例如gxStartPB这样的标签名,能清晰标识启动按钮的阴影寄存器。这个示例中我们使用了匈牙利命名法。前缀g表示该变量是全局作用域。前缀x代表布尔类型。
有些人会质疑如此讲究变量命名的必要性。他们并没有错。不过,我认为我们正处于另一种情境中:本应专注于学习时,却试图像专家一样编写代码。
打个比方,想想你如何通过自然拼读法学习阅读。要读"cat"这个词,先指向肩膀发出"c"(或"k")音,接着指向肘部强调"a"音,最后指向手部发出收尾的"t"音。熟练的阅读者不需要这些复杂步骤。同理,有充分理由认为经验丰富的程序员不需要匈牙利命名法。
但根据我的经验,添加匈牙利前缀能促使学习者思考变量的类型和作用域。它能减少那些难以定位的bug,比如变量名不当重复或变量错位。此外,在编程进阶之路上,对变量名的精确关注将有助于构建基于PLC的函数。
想了解更多匈牙利命名法及PLC通用准则,我推荐阅读PLCopen组织发布的这份《编码规范》文档。
标签作用域
与C语言类似,Opta梯形图逻辑实现包含局部和全局内存。全局标签对Opta内所有程序组织单元(POU)可用。局部变量的作用域仅限于定义它的POU内。图2展示了这种关系,局部变量封装在各自POU中。
技术提示 :POU是PLC中的代码块。在IEC 61131-3环境中,PLC程序由多个POU组成。一个密切相关且常被混淆的概念是PLC功能块(FB)或用户自定义功能块(UDFB)。所有函数都是POU,因为它们能帮助组织复杂程序。但并非所有POU都是函数。例如,假设我们编写了名为prgCtrl的10段梯形图程序。这个POU是程序,而非函数。为厘清细微差别,我们采用函数的传统定义:在程序内部被调用的内容。根据这个定义,我们的prgCtrl POU未被显式调用——因此不是函数。但它可能调用诸如绝对值这样的函数。
当我们有多个程序组织单元(POU)时,事情就变得有趣了。由于POU不是函数,我们需要一种在POU之间传递信息的方法。例如,考虑图2所示的结构。这里是一个基础的三程序解决方案,包含prgInMap、prgCtrl和prgOutMap。prgControl POU包含PLC的逻辑,而映射POU用于在PLC的螺丝端子与影子寄存器之间传输数据。由于这些不是函数,唯一传输数据的方法是使用全局变量。为了帮助说明,图3展示了Arduino PLC IDE视图,显示prgCtrl的局部变量。
技术提示 :询问任何计算机程序员,他们都会告诉你避免使用全局变量。他们甚至可能会讲述自己或认识的人花费数小时甚至数天排查一个隐藏得很深的bug的故事。这是合理的建议。我们应绝对最小化全局变量的数量,仅在绝对必要时使用它们。根据经验,它们应仅用于在POU之间传递I/O。随着我们学会编程FB和UDFB,这将变得更容易。
图 2 :全局变量对所有POU可用,而局部变量的作用域仅限于定义它们的POU。
图 3 :此图像显示与prgCtrl关联的局部变量。
结论
这是本文的一个合适的停止点。我们通过对比和比较著名的Arduino C编程结构,定义了Arduino Opta的内存结构。我们了解到Opta使用基于标签的内存分配,每个标签都用数据类型和描述性名称实例化。PLC使用一个循环进行操作,该循环在后台执行内务处理活动,并将螺丝端子I/O传输到影子寄存器。最后,我们了解到变量的作用域可以是局部的或全局的。这伴随着对学习者使用匈牙利表示法来帮助组织程序的建议。
我再次鼓励大家阅读PLCOpen组织提供的PLC编码指南。这与阅读GNU编码标准等文档同样重要。这些文档将帮助您理解程序是如何组装的,以及最佳实践如何使您的代码更具可读性和可维护性。
或许将来我们可以转向使用梯形逻辑开发程序。在撰写本文时,有一篇DigiKey技术论坛文章对初学Opta PLC编程者很有帮助。
该文章提供了一种实用技巧,可通过状态指示灯和红绿蓝LED查看Opta输入输出端的状态。这是区分硬件问题与软件问题的重要故障排除手段。


