做CODESYS ST编程这些年我最大的感受是语法好学规范难养。同样是ST有人写出来的代码能让人一眼看穿整个流程有人写的程序运行起来没毛病改需求时却想砸键盘。CODESYS 3.5底层是IEC 61131-3ST只是其中一种文本语言可工程上的差距恰恰不在语言本身而在代码的骨架和习惯。这篇是ST编程规范的第三部分我不打算再讲基础语法专门聚焦四件事代码组织、SoftMotion运动控制、Modbus RTU和网口通信这类系统交互、任务调度与上线排错。适合已经用过CODESYS一阵子、想把手头ST代码往标准化方向推的人。你要是正在搞汇川或其他基于CODESYS的国产PLC或者接手六轴机械臂、多轴联动这类项目也可以直接对照这里的做法来打磨自己的工程模板。1. ST代码的组织骨架POU划分、命名与变量声明很多刚接触CODESYS的人会把ST理解成高级一点的梯形图写起来就是往一个大程序里堆代码。这恰恰是后面维护成本飙升的根源。编程规范第一层不是语法而是代码怎么切块、怎么命名、变量放在哪里这三件事决定了三个月后你还能不能顺畅地改这个工程。1.1 POU边界怎么划功能块才不背锅CODESYS里有三类可执行单元PROGRAM、FUNCTION_BLOCK、FUNCTION对应中文习惯里的程序、功能块、函数。很多人直接用PROGRAM一张大饼画到底真正工程里这样干会非常痛苦。我自己的划分原则就三条判断这段逻辑需不需要记住上一个周期的状态。如果只是把输入算成输出例如温度线性换算、CRC校验、速度前馈计算用FUNCTION如果这个逻辑要在多台设备上各自保有状态比如每台电机的使能管理、每个Modbus从站的轮询缓存用FUNCTION_BLOCK只有最顶层的启动、停止、模式切换那些系统级调度才用PROGRAM。举个例子做六轴机器人控制的时候我习惯把单轴使能管理抽成一个FB它内部接受轴引用、急停信号、使能请求输出轴状态和报警。每个轴实例化时各自带状态互不干扰。如果你非要把六根轴的使能判断全部写在一个PROGRAM里每加一根轴就要复制粘贴一段逻辑还要改几十个变量的名字迟早会改出问题。调用深度也要控制。我给自己定的规矩是一个FB内部对其它FB的调用最多三层。超过三层单步调试时你根本分不清当前返回值是底层哪一路逻辑造成的。真遇到复杂流程可以把状态机折叠成一级调用把动作执行放在下一级保证从程序入口能顺着链子一次捋到底。提示一个功能块最好只有一个责任。我见过有人把一个FB既做报警处理又做产量统计后来报警抖动的时候它和新加的统计逻辑搅在一起排查的体验非常酸爽。单一职责不只是面向对象语言的专利ST同样适用。1.2 命名、注释和版本没人看但必须有的东西命名规范这事团队里必须有一份强制约定否则每个人的代码风格拼在一起就是灾难。至少要把前缀规则定下来布尔量用x开头例如xAxisReady整数用di、n或i开头区分双精度和普通长度浮点用r开头字符串用s开头功能块实例用fb开头全局变量统一加g_前缀常量大写或者用C_前缀。这套习惯在IEC 61131-3体系里非常通用你以后读任何第三方的库源码都能快速上手。命名之外注释的重点不是翻译代码而是写为什么。下面两段对比感受一下// 不好这是注液速度系数 rCoeff : 2.5; // 好2.5来自现场三批试产的数据如果更换针头规格需要重新标定 rCoeff : 2.5;后面这种注释才能在设备改造时真正救命。版本方面CODESYS工程本身有SVN集成但实际现场用SVN的比例没那么高。我的建议是至少做到每次上线前导出一次带版本号的archive文件命名规则项目名_日期_版本号POU文件头写一段修改记录包括修改人、日期、改了哪个FB的哪个接口。说句难听的半年后出问题的时候这段代码是谁写的、当时为什么这么改比语法本身值钱得多。1.3 变量声明局部变量的黄金准则CODESYS的FB里有两类本地变量VAR和VAR_TEMP这个区别非常关键。VAR_TEMP是临时变量每个扫描周期重新初始化不保持上一次调用的值用来做中间计算没问题但千万别在里面存状态。我有一次就是把一个累计计数变量放在VAR_TEMP里结果数值永远归零查了一个小时才发现。全局变量能不用就不用。我见过有些项目的全局变量列表有几百个代码之间靠全局变量互相传话最后根本理不清谁写了谁读。规范做法是功能块之间通过输入输出参数传递数据只有真正的全局状态才放全局变量池。你可以给自己定一个硬指标5000行以内的工程全局变量不超过30个超过就要反思数据流设计。变量初始化也很容易被忽略。CODESYS里功能块实例的VAR变量默认会在冷启动时按声明初始值或零值初始化但如果你在声明里不写初始值后面改需求的时候很容易出现上电一瞬间变量是零后来却变成上次的保持值这类问题。我的习惯是所有BOOL清零所有数值写明确初值掉电保持的变量单独用VAR RETAIN声明并且只允许在特定初始化块里写一次。2. SoftMotion与六轴联动ST如何组织运动控制逻辑运动控制是ST里最容易被考验的地方。逻辑写不顺伺服一跑起来就是噪音和报警。SoftMotion库是CODESYS生态里做运动控制的主力现在有一个SoftMotion Light和一个完整版SoftMotion很多人刚接触时搞不明白该往工程里加哪个。简单说SoftMotion Light是入门版轴数少、功能受限胜在免费完整版支持CNC、Robotics、多轴插补那些高级功能。做六轴机器人类项目你基本逃不掉完整版。2.1 轴初始化和使能动作别在每条指令里重复SoftMotion的轴对象在设备树里配好后程序里要干的第一件事是使能。很多初学者在每个FB里都调用一次MC_Power结果多个FB抢同一根轴的控制权轴状态抖动、报警乱跳。正确的做法是做一个统一的轴管理FB把MC_Power、回零、限位状态全部收拢进去。我常用的轴初始化结构是这样程序里先等轴电源就绪然后使能使能完成后自动进入回零状态回零完成置位xAxisReady。主流程看到xAxisReady为TRUE才允许下发运动指令。这样写的好处是任何一根轴出问题你能从上位机的轴状态字一眼看出卡在哪一步。FUNCTION_BLOCK FB_AxisInit VAR_INPUT AxisRef : AXIS_REF; bEnable : BOOL; bResetError : BOOL; END_VAR VAR_OUTPUT bBusy : BOOL; bReady : BOOL; eErrorId : MC_ERROR; END_VAR VAR fbPower : MC_Power; fbHome : MC_Home; bPowerDone : BOOL; bHomeDone : BOOL; eState : INT; END_VAR这个FB内部的状态切换是第三部分要展开的但核心思想就是每个状态一个CASE分支状态转移条件必须是确定的BOOL信号不要让轴停留在中间态。等我把回零和使能合到一起后主程序的运动逻辑就只剩下等轴Ready、下发坐标、看Done、处理Error四件事清爽很多。2.2 六轴机器人控制任务分层和轨迹生成块六轴机器人的ST程序结构我习惯分成三层人机交互层、运动规划层、轴执行层。人机交互层只负责把HMI下发的目标坐标X、Y、Z、姿态角转换成运动规划层的数据运动规划层调用SoftMotion的机器人运动学库把笛卡尔坐标逆解成六个关节角度轴执行层才真正执行每个关节的MC_MoveAbsolute或MC_MoveVelocity。这三层之间用功能块接口隔离不允许越层调用。最忌讳的是在人机交互层直接拼一个MC_MoveAbsolute去控制关节这样一旦路径规划和安全逻辑耦合在一起后面加门区、加软限位就寸步难行。代码组织上我会单独建一个FB_RobotMove它接收目标坐标和运动速度内部完成逆解、限位校验、轨迹下发。这个FB的接口看起来是这样FUNCTION_BLOCK FB_RobotMove VAR_INPUT bExecute : BOOL; rTargetX, rTargetY, rTargetZ : LREAL; aTargetEuler : ARRAY[1..3] OF LREAL; rVelocity : LREAL; END_VAR VAR_OUTPUT bDone : BOOL; bBusy : BOOL; bError : BOOL; aJointAngles : ARRAY[1..6] OF LREAL; END_VAR调用方只看得到笛卡尔坐标和回读的关节角完全不关心中间的运动学算法。这既是规范也是为将来换不同型号机械臂留退路。只要接口不变运动学库内部怎么改都不影响上层程序。还有一点要提六轴项目里关节坐标和笛卡尔坐标必须区分清楚。很多事故就是把关节弧度当成角度直接下发或者把逆解出来的弧度值送给驱动前漏了转换。我的规范是所有角度量在接口命名上强制带_Rad或_Deg后缀否则评审直接打回。2.3 回零、限位和急停编程里最容易漏的一环运动控制的安全逻辑最容易被ST的功能实现掩盖。轴跑得起来不算完回零顺序、软限位、急停处理这三件事必须单独建立规范而不是散落在各个运动指令里。回零我坚持一条原则使能完成之前绝对不允许进入自动运动。SoftMotion的轴有全集成回零功能但不同型号伺服的回零方式不一样有的用原点开关有的用编码器Z相。在ST层面不要试图在业务代码里自己实现回零算法而是把MC_Home封装进轴初始化FB由参数选择回零模式。急停处理更要独立。急停信号不能放在某个运动FB内部串行判断否则这个FB没有被调用时急停等于失效。我实际项目里的做法是急停信号接入一个独立的高优先级任务这个任务只做三件事锁存急停状态、给所有轴下发MC_Stop、输出急停报警给HMI。任务的扫描周期和运动任务的扫描周期分开运动任务可以慢急停任务必须快。这里的MC_Stop要选对模式立即停止还是按减速度停止需要和机械设计人员提前确认我踩过减速比选错导致惯性过大的坑。软限位这一条很多人完全不写。等你真遇到设备来回撞限位撞坏丝杠的时候再补代码已经晚了。ST里的软限位应该在运动规划层做每次下发移动指令前先检查目标位置是不是在允许区间内。六轴机器人还要额外检查关节角限位因为笛卡尔空间在可达范围内也可能有奇异点。3. Modbus RTU、网口MAC地址与通信数据规范工业设备基本离不开通信而通信代码的混乱程度往往比运动逻辑还严重。CODESYS里做Modbus RTU串口通信、或者读PLC网口MAC地址这类系统操作时最常见的坑不是指令不会用而是没有统一的通信管理机制每个功能块各写各的最终总线上一片乱。3.1 Modbus RTU的ST通信约定Modbus RTU在这里指的是基于串口RS485、RS232的Modbus协议它和Modbus TCP的区别在于RTU直接操作串口帧CODESYS里通常是调用串口库或者经过串口服务器转成TCP后才进PLC。不管走哪条路ST层面的处理逻辑是一致的。我的通信管理规范是全局只放一个轮询FB每个从站、每个寄存器都配置成一张表FB按表挨个发起事务绝不允许业务代码里到处散落ReadRegister、WriteRegister这样的调用。原因很简单Modbus RTU是半双工同一时刻总线上只能有一个请求如果多个FB并发发帧帧碰撞、超时、重试全乱套。轮询FB大致这么写启动时计算请求次数和地址表长度每个周期只发一帧请求发出后等待从站回复收到回复就转下一项超时没回复就记录错误码、尝试重发最多三次三次都失败就把这个站标记为离线然后继续轮询其它站不让单站故障拖垮整条总线。这里要特别提醒超时时间不能设太短很多设备的响应时间在50到200毫秒你设个20毫秒超时明明是设备正常的结果被你误判成离线。// 伪代码示意实际项目按地址表循环 CASE eComState OF 0: // 空闲取下一笔请求 bSendRequest : TRUE; eComState : 1; 1: // 等待接收完成 IF xRxDone THEN bSendRequest : FALSE; eComState : 0; ELSIF bTimeout THEN iRetryCount : iRetryCount 1; IF iRetryCount 3 THEN xSlaveOffline : TRUE; iRetryCount : 0; eComState : 0; ELSE eComState : 0; // 重发当前请求 END_IF END_IF END_CASE这套状态机的价值在于业务逻辑不用关心通信过程本身只读接口上的数值和健康标志。现场排查时一眼就能看到是哪一站离线、重试了几次。3.2 读取PLC网口MAC地址的几种思路热词里反复出现CODESYS读取PLC网口MAC地址说明这个需求在设备授权、资产管理、在线诊断场景里确实常见。CODESYS 3.5本身没有直接给你一个GET_MAC的ST指令它需要依赖系统库或底层平台接口。不同PLC硬件平台的实现差别很大不能指望一套代码到处跑。我的处理方式是把MAC读取做进一个统一的系统信息功能块FB_SysInfo输出PLC名称、固件版本、IP地址、MAC地址。MAC获取分两层如果能找到CODESYS的系统库接口比如SysSockGetHostAddr或其它网口枚举函数就调用系统库如果目标平台没有暴露这个接口就在设备描述文件或寄存器层面想办法某些汇川PLC的网口MAC地址可以通过系统状态区读出来。下面是一个典型的伪代码写法思路是枚举网卡接口找到当前激活的以太网口// 伪代码具体函数随库版本变化 FOR i : 0 TO 3 DO IF SysSockGetInterfaceInfo(i, InterfaceInfo) THEN IF InterfaceInfo.bActive THEN sMacAddress : InterfaceInfo.sMac; EXIT; END_IF END_IF END_FOR这个功能做好后我一般只允许在设备上电初始化阶段调用一次把结果保存起来。因为频繁读取MAC会引起系统调用开销而且这个值在运行中基本不会变。做授权绑定的你一定注意PLC网口MAC地址不是所有现场都能稳定读到无线网卡、虚拟网口、USB转网口都会影响结果不要把它当成唯一的授权依据最好结合CPU序列号一起绑。3.3 数据区规划与字节序陷阱通信数据区最怕的是各写各的。做Modbus寄存器映射时我会在工程里专门建一个全局数据块文件把所有从站地址、寄存器起始地址、长度、缩放系数、数据类型做成一张表。代码里只引用这个表的常量不允许在功能块里直接写死地址。这样现场改仪表地址时只改一处全工程生效。字节序问题必须反复强调。CODESYS的PLC大多是小端模式而很多第三方仪表、变频器使用大端模式。同一个16位寄存器PLC读出来后高字节和低字节是反的数值直接对不上。处理办法有两个一是直接在通信映射层用WORD数组接收再手动高低字节交换二是用UNION把原始字节和数值做映射在ST里定义一个联合体用内存对齐的方式完成转换。TYPE U_ModbusInput : UNION abyRaw : ARRAY[1..4] OF BYTE; // 原始4字节 rValue : REAL; // 转换后的浮点数 END_UNION END_TYPE这种写法比手动移位和掩码直观得多也基本不出错。如果现场发现读回来的数是个天文数字第一个怀疑方向永远是字节序。4. 任务调度、实时性和ST必须避免的坏味道ST代码写得合规但如果任务调度乱来照样会出各种莫名其妙的问题。CODESYS 3.5的任务配置不是摆设它不仅决定程序执行周期还直接影响看门狗、数据一致性和多轴同步精度。4.1 任务周期、看门狗和任务间数据交换任务周期分配我一般分三档。运动控制任务跑1到10毫秒主要执行轴使能、运动指令、限位检查逻辑任务跑10到50毫秒处理模式切换、配方、报警累计通信任务跑50到200毫秒做Modbus RTU轮询、HMI数据交互。有人喜欢把所有东西塞进同一个10毫秒任务里图省事短期看没事等到后面加了大量通信处理任务周期被拉爆轴运动就开始一卡一卡。看门狗也不能敷衍。CODESYS里每个任务都可以配置看门狗超时时间任务执行时间超过设定值就会报错。我通常把看门狗设置为任务周期乘以3到5倍太紧容易误报太松等于没有。如果一个任务看门狗老报警问题根源往往是里面有FOR循环处理了大数组或者调用了等待类指令这类情况必须拆任务而不是靠调大看门狗来掩盖。任务间数据交换最隐蔽的问题是数据撕裂。一个任务写了半个结构体另一个任务在同一周期去读就可能读到一半新一半旧的数据。规范做法是跨任务传递的数据要么用单个原子变量比如一个32位整数或BOOL要么用双缓冲一个任务写后备区另一个任务读当前区再交换指针。数据量实在大也可以用Synch指令或全局变量加锁但CODESYS的ST里锁的粒度不太好控制我建议优先用双缓冲。4.2 常见ST坏味道这些坏味道我是在无数个调试现场攒出来的每一条都是踩过坑的产物。第一FOR循环里调用功能块实例。你想想FOR循环每转一圈调一次FBFB内部可能还有状态机一个周期内同一个实例被调了十几次它的内部状态到底执行到哪一步全靠运气整个代码的可预测性直接归零。正确的做法是循环只做数组搬运或求和功能块调用永远在循环外面一次调用处理一个批处理任务。第二使用STRING做频繁拼接。ST对STRING的支持本来就不算强反复拼接字符串会产生大量临时对象任务周期被拖慢。如果只是组报文建议直接用BYTE数组加偏移量填充最后统一作为数据块交给通信库。第三比较BOOL变量的写法。有些人写IF xReady TRUE THEN这也不是不行但完全等价于IF xReady THEN。关键是很多新手不知道BOOL还能AND、OR、XOR组合于是写出了IF (xReady TRUE) AND (xFault FALSE) THEN这种啰嗦的代码。声明了xFault这种带否定含义的变量更是坏味道直接改成xNoFault或者正逻辑会让状态判断清爽得多。第四CASE语句没有ELSE分支。ST的状态机里如果状态值意外跑到了没有分支的地方程序就凭空消失了。我要求所有CASE都要有ELSE哪怕只是把状态重置到初始值也必须让程序知道该怎么回家。第五通信和运动混在一个FB里。这是架构问题不是语法问题。通信的超时重试、重连机制会把运动逻辑的状态图搅乱。分开之后运动FB只管运动通信FB只管数据收各自的状态都简单才好维护。4.3 汇川和其他国产CODESYS平台差异点国内用CODESYS基本绕不开汇川它们的AM系列、H系列都是用CODESYS做底层的。ST语法本身没差别但工程上还是有些差异要适应。首先是库版本和功能范围。汇川的CODESYS工程打开时如果提示库版本冲突最常见原因是本机安装的库版本比工程创建时高。规范做法是团队内部固定一个CODESYS版本和库文件版本工程文件里用相对版本引用不要手贱去更新那些看起来能更新的系统库否则你上午还能打开的工程下午就报一串找不到库的错误。其次是系统功能接口的差异。读取MAC、读取CPU序列号、访问系统时间这些操作在汇川平台和纯CODESYS平台上的库函数可能名字都不同。我的习惯是把这些平台相关调用全部封装成一个独立的系统适配层FB主业务代码只调用适配层接口。换平台的时候只需要重写适配层业务逻辑一行不用动。最后是运动控制的同步周期。多轴联动时汇川的轴同步周期和SoftMotion的默认配置未必匹配轴参数里关于总线刷新周期和插补周期的设置有平台相关性。ST层面的规范很简单轴的使能和回零标准都写在轴管理FB里轴参数差异只保留在设备配置层业务代码不允许感知这些差异。5. 调试、排错与上线前检查清单ST代码写得再好也还要过现场调试这一关。调试阶段的习惯有时比编程习惯更重要因为现场发生的事情往往不按剧本来。5.1 在线监控、断点和强制变量的正确姿势CODESYS的在线功能很强但用错了反而误事。先说强制变量这个功能在调试伺服和模拟输入时非常好用你可以在线给变量写一个假值观察程序反应。但强制是有状态的强制过的变量在重新登录后可能仍然保持强制等你要真正跑I/O时它还是那个假值设备就会乱动。我的习惯是调试完毕必须在强制变量窗口里全部清除并核对一遍再运行。断点不要留在循环任务里。运动任务10毫秒跑一次你要是断在一个频繁调用的FB内部程序直接卡住伺服报警现场人会疯掉。正确的做法是把问题定位到某个周期用Latch工具或者Trace工具记录变量趋势而不是用断点把PLC停下来。SoftMotion的轴运动出问题时Trace轴位置和速度曲线比断点一眼看出来的信息量多得多。日志方面不要只靠HMI报警。我会在工程里做一个简单的日志FB它把关键事件、错误码、时间戳、当前模式追加到非易失存储区。现场出了偶发故障客户跟你说刚才报警了一下就消失了这时候唯一能还原现场的就是这份日志。5.2 常见问题速查表调试多了之后问题就成了套路。我把碰到的典型问题和排查顺序整理成一张表放到工程文档里团队新人照着查也能省不少时间。现象可能原因排查顺序规范预防轴使能后不动使能未完成、总线未同步先看轴Ready位再查驱动器报警轴管理FB统一处理轴跑了一段后抖动插补周期与伺服刷新周期不匹配检查SoftMotion周期配置和轴参数锁版本、锁周期Modbus偶发超时波特率错误、线路干扰、从站响应慢看日志里的超时时间和重试计数统一轮询FB超时重试三次PLC运行变慢、看门狗报警任务周期设计不合理循环内调FB查看任务实际执行时间拆分逻辑任务避免阻塞调用掉电后数据全丢变量没放在RETAIN区检查保持变量配置数据分类单独RETAIN声明六轴运行中丢失原点回零未完成就进入自动或累计误差查原点开关信号、回零完成位回零状态机强约束读回的寄存器数值不对字节序反了、地址偏移错了用原始字节数组对比统一UNION转换、地址表管理程序下载后第一次运行异常旧保持变量值污染新逻辑冷启动清洗保持区初始化块主动赋值这张表不是让你死记硬背而是提醒你大多数灵异问题背后都有确定性的工程原因排查顺序对了问题往往很快浮出来。5.3 交付前的代码评审检查清单工程交付前我会按一张固定清单过一遍。清单不是摆设是长期项目经验沉淀出来的把关线。第一结构层面。POU是不是按PROGRAM、FB、FUNCTION边界清晰划分全局变量是不是控制在允许数量内有没有把业务逻辑写在某个功能块的VAR_TEMP里有没有FB循环调用自己导致栈溢出第二接口层面。每个FB的输入输出量是否都声明了类型和注释有没有直接把全局变量塞给别人当输入功能块的输入参数是不是都在调用前做了合法性判断举个简单例子速度值如果接收外部HMI下来的是负数你有没有在运动指令里拦截没有的话反向冲击容易把机械搞坏。第三任务和安全层面。运动任务周期和看门狗是否匹配急停逻辑是不是独立任务软限位校验是否在每一笔移动指令前都生效回零状态是不是必须先于自动模式完成第四通信和持久化层面。Modbus寄存器地址是不是全部来自地址表通信超时重试是不是统一在轮询FB里掉电保持变量是否明确分类并初始化一次评审会一般花一到两个小时但能在现场省出几天时间。我宁可交付前被问得满头包也不愿意上线第二天接到半夜电话。再分享一个我的私人习惯吧。我评审别人的ST程序通常只花三十秒先扫一遍POU树命名乱不乱、文件分层清不清楚、核心FB的输出接口是不是一眼看得懂。如果这些骨架是通的后面细节再烂维护成本也有限如果骨架就是一锅粥局部写再规范也救不回来。编程规范说到底不是给机器人看的是给未来那个凌晨三点还在现场的自己留的线索。你在CODESYS里写的每一行ST都会在某个脆弱瞬间变成你判断现场状态的救命稻草。多花点时间把规范变成习惯值。