简介:面向Codesys汇川AM600平台的面向对象编程PDF资料,适合工业自动化工程师与PLC进阶学习者。资源聚焦IEC 61131-3标准下的OOP实现,系统讲解对象、类、属性、方法及动作(Action)的概念,并结合汇川AM600环境演示继承与PUBLIC/PRIVATE访问修饰符的用法。内容还包含PROGRAM MAIN示例与CTD功能块调用语法,帮助读者掌握在函数块中扩展动作、封装数据的方法,从而编写模块化、可维护的控制程序。资源为单个PDF文件,大小约768KB,目前已有8137人学习下载,便于随时查阅。
1. 面向对象的 PLC 程序:Codesys 与 AM600 上动作、方法、属性到底能省多少事
在 Codesys 里给汇川 AM600 写面向对象程序,多数工程师第一反应是没必要:电机控制用功能块加全局变量也能转。但等你要把同一套逻辑复用到三条产线,每次调限幅都翻十几个变量时,动作、方法、属性这三个成员就是提前备好的后悔药。这份 PDF 讲的是 AM600 上 IEC 61131-3 的面向对象扩展:功能块改造成带封装的对象,方法替代重复运算,属性给内部变量加受控入口。适合能熟练写功能块、还没玩过高级编程的 PLC 工程师;不用先学 C++,但要愿意把设备 FB 拆出状态和动作。下面用一个电机控制对象串起这套特性,标出参数边界和踩坑点。
2. 动作、方法、属性的选型:三者的边界、一份对比参数表和一个最小对象模型
2.1 概念先立住:IEC 61131-3 为什么要把功能块拆成三个口
传统功能块的形态是一个接口区(VAR_INPUT / VAR_OUTPUT / VAR_IN_OUT)加一段执行体。问题出在两个地方:第一,执行体一旦超过三四十行,就变成一条谁都改不动的大面条;第二,外部程序想看内部状态,只能靠你预先定义的一堆输出变量。输出变量越多,调用方要维护的"假接口"就越多,改一个内部逻辑往往要同时改接口、改调用处、改上位机监控三个地方。
面向对象扩展给出的解法不是推翻功能块,而是在功能块内部增加三种命名成员,共享同一个实例的数据区,区别在调用方式和职责边界。属性(Property)是受控的访问口,读和写可以分别拦截校验;方法(Method)是带参数表、可返回值的命名操作,相当于把一段逻辑打包成"命令";动作(Action)是不带参数、共享实例数据的执行片段,适合挂到状态机或 SFC 步骤上按状态触发。AM600 的 Codesys V3 平台对这三类成员的建模是统一的,写法和库开发完全一致,学一次能用在所有支持该内核的控制器上。
2.2 三者的分工:一张选型表决定代码该怎么拆
| 成员类型 | 本质 | 参数与返回值 | 调用语法 | 典型场景 |
|---|---|---|---|---|
| 属性 Property | 对内部变量的受控读写 | 无参数 | fb.SetPoint := 50.0;/v := fb.SetPoint; | 数据校验、限幅、只读暴露 |
| 方法 Method | 命名操作 | 支持 VAR_INPUT / VAR_OUTPUT / VAR_IN_OUT,可带返回值 | ret := fb.Start(RampTime := T#3s); | 参数化命令、算法计算、状态切换 |
| 动作 Action | 无参执行片段 | 无参数无返回值 | 在 FB 主体内按状态调用,或挂到 SFC 步骤 | 状态步进、初始化段、故障处理段 |
选型判断顺序我一般这样走:先问外部要不要写这个量,要写就做成属性;再问这个操作要不要传参或返回结果,要就做成方法;剩下那些内部按状态拆开执行的段落,全部做成动作。最容易犯的错是把属性当成"高级变量"满处用,又在方法里塞进一大段与状态无关的初始化代码。属性只做校验和暴露,方法只做一个命令,动作只服务一个状态,这个边界守住,对象的结构基本不会乱。
2.3 最小对象骨架:能直接照抄的 FB 结构
先用一段可编译的结构化文本把骨架立出来,后面所有例子都长在这个框架上:
FUNCTION_BLOCK FB_Motor VAR_INPUT xEnable : BOOL; // 外部使能 END_VAR VAR _state : INT; // 0 Ready,1 Run,2 Fault _cmdRun : BOOL; // 运行指令 _bRunning : BOOL; // 运行反馈 _bFault : BOOL; END_VAR // 主体只做两件事:按状态分发动作、统一处理迁移 CASE _state OF 0: Exec_Ready(); 1: Exec_Run(); 2: Exec_Fault(); ELSE _state := 0; END_CASE // 状态迁移集中写,排障时只看这一段 IF _bFault THEN _state := 2; ELSIF xEnable AND _cmdRun THEN _state := 1; ELSIF NOT xEnable THEN _state := 0; END_IF这段代码的关键是"主体瘦身":FB 的执行体不再背着全部业务,只做状态分发和迁移判断,每个状态的具体行为被拆进同名动作里。参数说明:_state用整数承载,建议在 FB 的常量区定义_STATE_READY、_STATE_RUN、_STATE_FAULT这样的命名常量,千万别在 CASE 里裸写数字;动作名和状态名一一对应,看到调用链就知道当前在跑哪一段。这样新增一个状态只需要加一个动作和一行 CASE,旧状态完全不受影响。
提示:动作之间共享 FB 实例的全部数据,动作内不要重复定义同名变量;状态切换后,上一个动作遗留的中间量必须在进入新动作时自行复位,这点到避坑章再展开。
3. 把电机控制功能块改造成对象:属性封装与限幅、方法带参调用、动作配合状态机的完整代码
3.1 属性:先做限幅再放行,把裸变量变成受控接口
给 FB_Motor 增加属性成员 SetPoint,声明为 PUBLIC,勾选 GET 和 SET。先看 SET 侧:
// SET 访问器:写入前先限幅,负值直接钳到 0 IF SetPoint < 0.0 THEN _setPoint := 0.0; ELSIF SetPoint > _maxSpeed THEN _setPoint := _maxSpeed; ELSE _setPoint := SetPoint; END_IF _needRamp := TRUE; // 置位斜坡刷新标志,等运行态动作消费再看 GET 侧:
// GET 访问器:只读内部备份值,不在 GET 里改任何东西 SetPoint := _setPoint;调用侧就变成MotorA.SetPoint := 80.0;和v := MotorA.SetPoint;。这里要搞清楚执行时机:属性名在访问器里有特殊身份——SET 里它代表外部传入的新值,GET 里它代表要返回给调用方的值,两段代码分别在写入和读取时触发。参数边界:声明属性时只勾 GET 就是只读属性,对只读属性赋值会在编译期直接报错;只勾 SET 则是只写属性,读取一侧编译不过。限幅逻辑放在 SET 里,比调用方重复写十遍可靠得多,这是属性相对"输出变量加外部校验"最大的价值。
3.2 方法:把重复的命令序列收成一个带参操作
电机启动这段逻辑如果在三个程序里各写一遍,改一次斜坡时间就要改三处。用方法收口:
METHOD PUBLIC Start VAR_INPUT RampTime : TIME := T#2s; // 默认斜坡 2 秒 END_VAR VAR_OUTPUT bStarted : BOOL; // 是否真正下发成功 END_VAR // 方法体直接访问 FB 内部变量,不需要额外传引用 IF NOT _bFault AND xEnable THEN _cmdRun := TRUE; _rampTime := RampTime; bStarted := TRUE; ELSE _cmdRun := FALSE; bStarted := FALSE; END_IF调用写法是IF MotorA.Start(RampTime := T#5s) THEN ...。三个细节值得记:第一,方法有独立的输入输出区,默认值T#2s在调用处不传参时生效,传了则覆盖;第二,方法输出变量有两种消费方式,要么直接当返回值收ret := fb.Start(...),要么在调用后读MotorA.bStarted;第三,方法体里访问 FB 成员不需要THIS前缀,直接写变量名就行,但方法内声明的局部 VAR 是实例级持久数据,不是函数栈,这个坑很深,放第四章细讲。方法副作用也要约定:方法体里改 FB 成员就是副作用,命令类方法必须带输出确认位,测试时才知道命令有没有真正下发。
3.3 动作:让一个执行体按状态拆成多段,每段只做一件事
动作 Exec_Run 的内容如下:
// 动作 Exec_Run:运行态要做的全部事情 IF _needRamp THEN _ramp := 0.0; _needRamp := FALSE; END_IF _ramp := MIN(_ramp + 1.0, 100.0); // 斜坡爬满且指令有效才认为进入运行 _bRunning := (_ramp >= 100.0) AND _cmdRun; _actualFreq := _maxSpeed * _ramp / 100.0;调用点在第二章的 FB 主体里:状态为 1 时,主体每个周期调Exec_Run()。动作没有自己的参数表,读写全靠实例成员,所以进入动作前必须自己把上一状态的残留量清掉——上面_needRamp就是配合属性 SET 置位、动作消费的典型做法。动作也可以挂到 SFC 步骤上:编辑 SFC 时先选"调用动作",再指定 FB 实例的某个动作名,等于把状态机搬进 SFC,代码结构不用变。
3.4 组装:主程序里实例化与调用顺序
PROGRAM MAIN VAR MotorA : FB_Motor; v_cur : REAL; END_VAR // 先给使能,再写目标值,最后发命令 MotorA.xEnable := TRUE; MotorA.SetPoint := 80.0; v_cur := MotorA.SetPoint; // 读到的是钳位后的值 IF MotorA.Start(RampTime := T#3s) THEN // 命令已下发,此处可以联动上位机指示灯 END_IF这段的要点是"读写走属性、命令走方法、行为走动作"的三层约定:主程序不再碰任何下划线开头的内部变量,真正的状态机在 FB 主体里跑。实例化数量要心里有数——每个实例都会复制一份 FB 数据区,AM600 的内存有限;属性、方法、动作再多也只是代码,实例才是数据。一个任务里散着声明上千个大型 FB 会明显涨内存,能用数组管理实例时就别一个个声明。
4. 避坑实测:属性递归、参数传值、继承不绑定的五个典型翻车点
这一章的坑都是我在一个模拟项目X 上实际踩出来的,在线调试时的表现各不相同,根因却很集中。有些问题乍看像玄学,其实是绑定规则和变量作用域在作怪。
4.1 只读属性被赋值,编译期直接报错
现象:定义了一个只勾 GET 的属性,程序里写MotorA.SetPoint := 80.0;,编译报错,提示该表达式不可赋值。
原因:属性声明对话框里 GET / SET 两个勾选就是读写权限开关,只勾 GET 的属性在编译期被标记为只读。
解决:回属性编辑器勾上 SET;如果这个量确实只允许内部改,就改成方法调用,用带参数的命令入口接收外部输入。检查时先看属性上方的权限勾选,再查调用处语法,别急着改业务代码。
4.2 SET 访问器里给属性本身赋值,死循环直接拖停程序
现象:设备在线运行几分钟后任务看门狗触发,整个任务被强制停机,在线定位光标停在一个属性 SET 访问器里。
原因:SET 里写了类似IF SetPoint > 100 THEN SetPoint := 100;的代码,对属性名的赋值会再次触发 SET,形成无限递归。
解决:访问器里只操作内部备份变量,属性名只当边界用——SET 里读它拿新值,GET 里给它回传值,绝不在访问器里对属性名再赋值。我给所有内部变量统一加下划线前缀,就是为了在线调试时一眼分清哪边是备份、哪边是接口。
4.3 方法 VAR_INPUT 改不到调用处变量:默认传值不传址
现象:往方法里传了一个限幅目标值,方法内部把它改小了,调用处的原变量纹丝不动,后续逻辑拿到的还是旧值。
原因:VAR_INPUT 在 IEC 61131-3 里是按值传递,方法内部操作的是副本。
解决:需要回写调用处变量时,把参数声明为VAR_IN_OUT,IN_OUT 传的是引用,调用处不能传字面量,必须先放进一个变量。我现在的习惯是:方法里凡是要改外部状态,一律走 IN_OUT,或者干脆在方法内部直接操作 FB 成员,避免"传了等于没传"的错觉。
4.4 方法里的"局部"变量跨调用残留,计数器不归零
现象:方法内VAR iCnt : INT; END_VAR做自增,第二次调用 iCnt 从上一轮的值继续,而不是从 0 开始。
原因:Codesys 里方法声明的局部变量实际属于 FB 实例的数据区,两次调用之间保持,行为更像成员变量而不是高级语言的函数栈。
解决:需要每次清零的临时量在方法入口显式复位;VAR_TEMP依赖具体运行时的内存管理,不保证自动清零,我从来不做"默认为 0"的假设。排查办法:给方法加一个输入bInit,在线把两次调用的行为对照,很快能定位残留位置。
4.5 基类类型装子类实例,调用的还是父类方法
现象:FB_B 继承 FB_A 并重写了 DoWork,主程序里VAR fa : FB_A;,执行fa := fbB; fa.DoWork();,跑出来的结果还是父类那套逻辑。
原因:普通继承的方法调用按声明类型静态解析,fa 的声明类型是 FB_A,编译器看不到实际实例是子类。
解决:要拿真正的多态行为,改用接口分派:定义一个INTERFACE I_Work声明 DoWork,让 FB_A、FB_B 都 IMPLEMENTS,调用侧声明接口类型变量来持有实例。日常排查时看调用的目标类型就能预判绑定方式——类型是接口,动态分派;类型是具体 FB,静态绑定。这就是下一章要展开的接口玩法。
5. 继承与多态:用接口在 AM600 上实现可替换的驱动层
5.1 继承:把公共逻辑收进基类,子类只写差异
设备群里有多种电机,共同的"设置使能、判断故障"逻辑放基类:
FUNCTION_BLOCK FB_BaseMotor VAR _bEnable : BOOL; _cmdRun : BOOL; END_VAR METHOD PUBLIC SetEnable VAR_INPUT bEn : BOOL; END_VAR _bEnable := bEn; IF NOT bEn THEN _cmdRun := FALSE; END_IF子类通过 EXTENDS 继承,覆盖同名方法时先调父类再补差异:
FUNCTION_BLOCK FB_RampMotor EXTENDS FB_BaseMotor VAR _ramp : REAL; END_VAR METHOD PUBLIC SetEnable VAR_INPUT bEn : BOOL; END_VAR SUPER^.SetEnable(bEn := bEn); // 复用父类原生逻辑 IF bEn THEN _ramp := 0.0; // 子类独有的斜坡复位 END_IF这里有两个参数边界:第一,子类覆盖方法的签名要和父类完全一致,输入输出数量和类型不同就编译不过;第二,SUPER^.是显式调父类实现的唯一入口,忘记调用父类,公共逻辑就丢了。另外,父类的私有成员在子类里也访问不到,这跟很多人从 Java 带过来的直觉不一样。我的做法是给父类提供带参数的方法作为访问入口,而不是把成员改成全局可见。
5.2 接口:用一张契约换驱动层的可替换性
接口在 Codesys 里是一种特殊的类型声明,只写方法签名不写实现:
INTERFACE I_Drive METHOD PUBLIC Start : BOOL VAR_INPUT RampTime : TIME; END_VAR METHOD PUBLIC Stop END_INTERFACE实现接口的功能块都要声明 IMPLEMENTS,并按签名逐一实现:
FUNCTION_BLOCK FB_MotorDrive IMPLEMENTS I_Drive // 接口方法实现:签名必须完全一致 METHOD PUBLIC Start : BOOL VAR_INPUT RampTime : TIME; END_VAR Start := TRUE; // 返回值直接赋给方法名 _cmdRun := TRUE; _rampTime := RampTime;调用侧不再绑定具体类型,只认识接口:
VAR driveA : FB_MotorDrive; driveB : FB_VectorDrive; // 另一台驱动设备,同样实现了 I_Drive active : I_Drive; END_VAR active := driveA; // 换驱动只需要改这一行 active := driveB; IF active.Start(RampTime := T#2s) THEN // 实际调用的是 driveB 的实现 END_IF多态的价值在替换而不在省代码:新增一种驱动,实现一次 I_Drive 就能插进现有调用链,主程序一行不用改。需要留意的是接口调用多了一层间接寻址,有少量周期开销。AM600 上如果这段代码处于伺服刷新热路径,别把接口嵌套到三层以上;实测一到三层基本无感,再深就要用定时统计对比验证了。
注意:接口方法签名不能写默认参数值,实现侧也不写;两边必须完全一致,否则编译阶段就会报实现缺失。
5.3 分层习惯:驱动层、工艺层、应用层怎么配合
我在 AM600 工程里的组织方式:驱动层放实现 I_Drive 的 FB,工艺层把驱动对象聚合起来编排动作,应用层只做实例声明和参数下发。继承只用来抽公共数据,接口用来定义替换边界——能用接口解决的问题,不要叠加多层继承,否则在线调试时调用链会变得很难翻。这也是这份资源里反复强调的取舍:面向对象在 PLC 上是手段不是目的,三层以内最舒服,超出三层先怀疑建模错了而不是语言不够用。
6. 验证与习惯:用断点确认方法分派路径,附我写对象的五个固定动作
6.1 验证:断点加标记输出,确认走了哪个实现
多态代码最怕"我以为调的是 A,实际调的是 B"。我的验证办法分两步:第一步,在接口方法的两个实现方法里分别打断点,在线切换到对应对象后触发调用,看断点停在哪一侧;第二步,写一个什么都不做的测试方法 T_DispatchCheck,在两个实现里分别置位一个标记变量,触发一次后在上位机监控两个标记,哪个为真就是哪个实现被分派。这套办法每次改完继承或接口都强制走一遍,比盯着代码猜可靠得多。别把接口调用当黑匣子,调试器里调用栈和断点就是它的透视镜。
6.2 五个固定动作
我写对象前会过一遍这五条:属性名只做公开接口、内部备份一律下划线开头;方法入口显式复位所有临时量,不赌默认值;接口方法签名不写默认参数,避免实现侧误匹配;改完对象先重新编译库再刷新实例,防止在线更新后行为不一致;动手前把 PDF 里动作、方法、属性的边界和选型表再翻一遍。
这份《Codesys汇川AM600面向对象(动作方法属性)》我放在资料页,里面这套流程比我踩坑记的版本完整得多——尤其是继承和接口的配图说明,照着它把自己手上的电机或阀门 FB 改一版,一个下午就能走通。以前我省事跳过验证步骤,结果一次现场调试因为属性递归把任务拖停,从那以后每次碰对象,我都要走完断点和标记验证才收工,希望帮到你。
本文还有配套的精品资源,点击获取