第一次在客户现场打开别人的Codesys工程,屏幕弹出一句版本不兼容的提示,而产线正停在那里等料——那种感觉,做过现场调试的人都懂。供应商留的程序是旧版本做的,我这台机器装的是新版本,双击直接卡在迁移对话框,点"是"怕改坏,点"否"又进不去。从那之后我养成了一个习惯:每次动Codesys,不管多小的改动,都往一个文本文件里记一笔,记版本、记配置、记报错、记最后是怎么解决的。这份记录攒了一年多,跨过三四个项目,从简单的逻辑控制到带总线伺服的运动控制都有。下面这些内容,基本就是把那份流水账按主题重排了一遍,删掉了具体项目名,保留了能复现的操作和踩过的坑。
如果你刚接触这套平台,需要知道它是什么:Codesys是一个遵循IEC 61131-3标准的软PLC开发环境,写好的程序可以跑在自家软PLC上,也可以部署到几十种不同品牌的控制器硬件里。它解决的是一件事——让梯形图、结构化文本、功能块图这些不同写法在同一套工程里共存,让同一份代码尽可能少改地换硬件。适合谁看?已经在做电气或自动化、手上开始接Codesys项目的工程师,以及被"设备描述文件""PDO映射""SoftMotion"这些名词卡住过的人。
1. 装完软件到跑通第一个工程,中间隔着的不是一步
1.1 版本号这件事,追新是最容易吃亏的选择
Codesys的版本号看上去是3.5后面跟一个SP,实际上每跳一个SP,工程文件格式、库版本、设备描述都可能动。我见过最典型的情况是:A用SP17建工程,B用SP19打开,迁移完成后回不去SP17了,而现场那台工控机里装的还是SP17的运行时。结果就是程序在笔记本上编译为零错误,下载到设备上直接报版本不匹配。
我的做法是把版本当成项目资产来管,具体是三条:
- 工程建立前先确认目标硬件的运行时版本,以它为准,不要以自己笔记本上的最新版为准。
- 团队内统一到同一个SP号,装包放在共享目录,新人不自己下载。
- 工程属性里的"编译版本"和库管理器里的库版本一起看,库版本不一致导致的行为差异比语法错误更难查。
安装时也别一路下一步点到底。安装向导里的组件勾选有讲究,默认勾选的只是基础部分。如果项目要用总线伺服,SoftMotion和对应的驱动组件得手动勾上;要用Modbus,得把Modbus相关的库和设备描述一起装上;做界面的话,可视化组件也得选。我吃过一次亏:装的时候没勾SoftMotion,工程里一插入运动控制功能块就报"库未找到",重新装一遍覆盖安装才解决,中间浪费了半天。
提醒一句:同一台机器上装多个SP版本是可行的,但要注意安装路径分开,且不要在同一时间同时运行两个版本的工程,否则后台的网关服务会打架,表现为"扫描不到设备"。
1.2 设备描述文件:看不见的地基
Codesys的"设备"不是一个抽象概念,它背后对应一个XML格式的设备描述文件,里面写清楚了这个控制器有几个任务、支持哪些总线、IO点怎么排。工程里设备树最上面那一项,就是靠这个文件撑起来的。
新设备第一次接入的流程,我固定成这个顺序:
- 拿到厂家提供的设备描述文件,通常是
.devdesc.xml后缀,少数是打包好的库文件。 - 打开工具菜单里的设备存储库管理器,导入这个文件。
- 新建标准工程时,在设备选择窗口里搜到对应型号,选中。
- 编译一次空工程,确认设备树能正常展开、没有红色叹号。
第4步别省。我遇到过一次导入成功、选中也正常,但一编译就提示"设备描述文件缺少XX通道定义",原因是厂家给的文件和运行时固件版本对不上。这种情况在空工程阶段就暴露,比写完两千行逻辑再发现要好得多。
设备树的层级也值得说清楚。典型结构是:最上面是PLC设备,下面挂Application,Application里挂任务配置和程序,再往下是各种总线主站和从站。初学者常犯的错是把程序直接挂在设备下而不是Application下,编译能过但下装后不执行。为什么?因为任务的调度单位是Application里的任务配置,挂在外面的程序没有被任何任务调用,自然不跑。
1.3 第一个工程:骨架比代码重要
我建议第一个工程不要写任何业务逻辑,只做骨架验证。骨架包含四件事:一个全局变量列表、一个程序、一个任务、一个可视化页面(哪怕只有一个按钮)。
程序里先放三行:一个布尔变量自增计数、一个整型变量累加、一个把计数值写进可视化元素的赋值。任务配置里确认这个程序被循环任务调用,周期设成10毫秒。可视化页面上放一个数值显示控件,绑定那个整型变量。然后编译、下载、登录、运行。
这套流程跑通,说明工具链是通的:编译没问题、网关能连、运行时能执行、可视化能刷新。后面所有复杂功能都建立在这四件事都正常的前提上。反过来,如果一上来就写完整逻辑,出错时你分不清是逻辑问题、任务配置问题还是通信问题,排查成本会成倍上升。
骨架验证阶段还有两个细节要注意。一是下载和登录的区别:下载是把程序写到设备里,登录是建立在线调试连接,两步都要做,且下载后必须重新登录,否则看到的还是旧程序。二是"在线修改"选项,第一次下载时如果勾了它,程序改动后不用重新下载,但某些结构性改动(比如新增功能块实例、改任务周期)无法在线修改,会强制要求完整下载。我一般第一次完整下载,调试期用在线修改,功能定型后再完整下载一次。
2. 变量、任务和扫描周期,跑起来之后才真正理解的三件事
2.1 四种变量作用域,选错了排查会非常痛苦
Codesys的变量声明位置决定了作用域,这块的坑不在于语法,而在于"看起来能跑但结果是错的"。
| 声明位置 | 关键字 | 作用范围 | 生命周期 | 典型误用 |
|---|---|---|---|---|
| 程序/功能块内部 | VAR | 当前POU | 随实例 | 想在两个程序间共享,结果各存各的 |
| 全局变量列表 | VAR_GLOBAL | 全工程 | 随运行时 | 到处用,最后没人知道谁改的 |
| 功能块输入输出 | VAR_INPUT / VAR_OUTPUT | 调用方与实例之间 | 随调用 | 用输入变量做内部状态保存 |
| 掉电保持 | VAR RETAIN / 持久变量列表 | 按配置 | 跨复位 | 混淆RETAIN和PERSISTENT |
最典型的一次排查:两台设备的同一个功能块实例,参数在各自主程序里都设了,但运行起来有一台参数始终是默认值。查了两个小时,最后发现那个功能块内部用了VAR声明的一个中间变量去缓存输入,而这个缓存只在首次调用时赋值一次。功能块实例是静态的,那个VAR变量保留了上一次的值,导致后续输入变化不生效。改成每次调用都重新赋值就好了。
再说RETAIN和PERSISTENT的区别,这个必须搞清楚:
VAR RETAIN声明的变量,在热复位和冷复位后保留,但在"原始下载"(也就是带清除的完整下载)后清零。- 持久变量列表里的变量,即使做了原始下载也保留,只有显式清除持久变量才会没了。
所以配方参数、累计产量这类东西放持久变量列表,而运行中间状态放RETAIN就够了。搞反的后果是:一次调试时的完整下载,把辛苦标定的零点参数全清了,得重新标一遍。
2.2 任务配置决定程序什么时候跑,跑多久算超时
任务配置界面上有几个参数:类型、优先级、周期、看门狗。类型常见的是循环任务(固定周期)和自由运行任务(跑完立刻下一轮),还有事件任务(由变量上升沿触发)。
周期不是越小越好。设成1毫秒听着很厉害,实际会让CPU占用飙高,尤其是程序里带浮点运算或者字符串处理的时候。我一般的取值是:普通逻辑10毫秒,需要快速响应的输入采集2到5毫秒,总线通信跟随总线周期,不单独设。判断依据很简单,看任务的实际执行时间占周期的比例,超过50%就要警惕,超过80%随时可能触发看门狗。
看门狗时间默认值往往偏小,一旦程序里有个循环写得不严谨,或者调用了耗时的通信功能块,任务超时就会让PLC直接停机,输出全部复位。车间里表现为设备突然停住、指示灯闪烁。我遇到过一回,一个Modbus读功能块在从站掉线时阻塞了将近200毫秒,正好超过了看门狗,整机停机。解决办法不是简单把看门狗调大,而是把通信调用挪到独立的低优先级任务里,主任务只读结果,这样通信卡住也不影响主逻辑。
优先级数字越小优先级越高,这点和很多人的直觉相反。高优先级任务会抢占低优先级任务,所以把耗时长的任务放高优先级是自找麻烦。
2.3 结构体、数组越界和指针,编译器不会全帮你兜住
Codesys对数组越界的检查,取决于编译选项和运行时的配置。默认情况下,越界写数组不一定报错,可能悄悄改掉了相邻变量的值。这种问题在现场的表现是"某个不相干的变量莫名其妙变了",极难定位。
我给自己的规矩是:所有数组访问前先做下标判断,尤其是下标来自外部输入或者计算结果的时候。写个简单的判断就行:
IF nIndex >= 0 AND nIndex < MAX_ITEMS THEN aItems[nIndex] := nValue; ELSE nErrorCount := nErrorCount + 1; END_IF结构体用起来方便,但要注意它的内存布局。如果结构体要用来和外部设备交换数据(比如映射到Modbus寄存器或者总线PDO),成员顺序和数据类型必须和对方严格一致,且最好显式处理对齐问题。我见过一次结构体映射后数据错位,原因是中间插了一个布尔成员,对方按字节对齐,这边按位对齐,差了半个字节。解决办法是把要交换的结构体单独定义,成员全部用标准类型,布尔量统一用字节表示,别偷懒。
指针在Codesys里能用,但我不建议在业务逻辑里用,除非确实需要动态内存或者和C写的库交互。用指针出的问题往往是野指针访问,运行时的表现是随机崩溃,日志里什么也留不下。
3. 通信配置:地址映射和字节序是两道绕不过去的坎
3.1 Modbus TCP:从站地址从0还是从1开始
这是我在Modbus上踩过最多次的坑,没有之一。设备手册上写着"寄存器40001对应温度",而Codesys里配置通道时填的地址到底是40001、40000还是0?
先把概念理清:Modbus有四种数据区,线圈、离散输入、输入寄存器、保持寄存器。手册上的40001是一种老式表示法,4代表保持寄存器区,后面的0001是区内偏移加一。也就是说,40001对应的实际协议地址偏移是0。而有些设备手册直接给协议偏移,写的就是0。
我的做法是不猜,用两步验证:
- 先只配一个通道,地址填手册上的原始数字,读一个已知会变化的值(比如设备运行时间或者温度)。
- 读不到就换另一种解释,偏移减一或者加一,再看。
配置界面里如果通道配置有"偏移"选项,通常填的是协议偏移,也就是从0开始。填完不要急着写业务逻辑,先在监视表里盯着这个变量看几秒,值在合理范围内跳动才算通。
还有一个高频问题:字节序和字序。32位浮点数在Modbus上占两个寄存器,先发高字还是先发低字,各家不一样。Codesys的通道配置里一般有交换选项,如果没有,就得自己在程序里拼。我一般这么处理:
// 假设 rHigh 是先到的寄存器,rLow 是后到的 fValue := DWORD_TO_REAL( SHL(DWORD#16#0 + WORD_TO_DWORD(wHigh), 16) OR WORD_TO_DWORD(wLow) );拼完之后对照设备面板上的实际显示值核对,别信计算,信面板。
连接参数上,超时和重试次数要设合理。默认超时往往偏长,从站掉线时主站会卡住很久,影响任务周期。我一般把超时设成300到500毫秒,重试1到2次,同时程序里做一个通信状态位,掉线时明确报警而不是让数据保持旧值假装正常。
3.2 EtherCAT:扫描、PDO映射和分布式时钟
EtherCAT的配置流程比Modbus直观,但细节更多。基本顺序是:在设备树里添加EtherCAT主站,右键扫描从站,从站被识别出来后逐个检查型号和站地址,然后配置PDO映射。
扫描这一步有个前提:网卡要选对。Codesys主站绑定的网卡必须是连接从站的那块物理网卡,如果机器上有多个网卡,选错了就扫描不到任何东西,而且不报错,只是列表空的。我一般先在系统层面确认网卡名称和连接状态,再回来选。
PDO映射是把从站的输入输出数据映射到程序的变量上。这里的关键是理解"过程数据"的概念:每个总线周期,主站和从站之间交换固定长度的数据帧,映射就是告诉主站,帧里的第几个字节对应哪个变量。映射界面里能看到每个通道的位偏移和长度,如果从站有多个PDO,要注意选的是一致的那一组,选错会出现数据位错位。
分布式时钟(DC)开启后,所有从站的时间基准同步到主站,对于多轴联动是必须的。但开启DC后如果某个从站不支持或者配置不对,整个总线可能起不来。排查顺序是:先关DC确认总线能跑,再逐个开启看是哪个从站的问题。
总线周期和任务周期的关系也值得说。总线周期设1毫秒,任务周期设10毫秒,意味着10个总线周期里主站只处理一次应用层数据,中间的数据是主站自己缓存的。这对一般逻辑没影响,但对需要精确同步的场景要注意,最好让任务周期等于或整除总线周期。
3.3 OPC UA:安全策略选错,客户端连不上还查不出原因
OPC UA服务端的配置在设备树里加一个服务端对象,然后配置端点。最容易出问题的是安全策略和用户认证。
安全策略有None、Basic128Rsa15、Basic256Sha256几种。选None最省事,任何客户端都能连,但生产环境不建议。选Basic256Sha256需要证书交换,客户端首次连接时要信任服务端证书,服务端也要信任客户端证书,任何一边没信任都会连接失败,而失败信息往往只显示"安全通道建立失败",不说具体是哪边的问题。
我的排查套路是:先用一个通用的OPC UA客户端工具,以None策略连一次,确认地址、端口、节点路径都对;然后再切到加密策略,逐项确认证书。这样能把"地址错"和"证书错"分开,不至于混在一起查。
节点命名空间也容易踩坑。工程里定义的变量,在OPC UA里看到的节点ID带一个命名空间索引,这个索引在每次重新下载后可能会变。如果上位机是写死节点ID的,重下程序后就失联了。稳妥做法是给要对外暴露的变量在符号配置里明确勾选,并尽量用稳定的节点路径而不是索引。
4. SoftMotion:从轴配置到动起来的那几步
4.1 虚拟轴是最好的练手对象,也是最快的验证手段
第一次配运动控制,我建议先不接真实伺服,用虚拟轴把整条链路跑通。虚拟轴不需要硬件,但走的功能块、状态机、单位换算和真实轴完全一样。
配置步骤大概是:在设备树里添加SoftMotion通用轴,设定单位(毫米或者度)、每单位的脉冲数、软限位、最大速度加速度。然后用MC_Power使能,用MC_Reset复位,用MC_MoveAbsolute给一个目标位置。
状态机的顺序不能乱:先Power使能,状态变到standstill,才能发运动指令。没使能就发运动指令,功能块返回错误,而这个错误码在文档里往往写着"无效状态转换",第一次看根本不知道啥意思。我养成的习惯是每个运动功能块都接上Error和ErrorID两个输出,出问题先看错误码。
单位换算这块要说清楚。轴的"每单位脉冲数"决定了位置值的物理含义。如果设成1000,那么位置值1.0对应1000个脉冲。设错了不会报错,只会表现为走的位置和预期差一个系数。验证方法很简单:给一个目标位置,量一下实际走了多少,反推系数。
4.2 单轴功能块的取舍:MoveAbsolute还是MoveVelocity
这两个功能块的适用场景完全不同,用错会带来控制上的麻烦。
MC_MoveAbsolute是按目标位置走,走完停在目标点。适合定点定位,比如取放料。它的好处是位置闭环,不会累积误差,坏处是每次都要知道目标位置,且中途改目标要重新触发。
MC_MoveVelocity是按速度走,一直走到收到停止指令。适合连续送料、传送带。它的坏处是没有位置闭环,长时间运行会有累积偏差,需要配合原点回归校正。
实际项目里经常两个都用:粗定位用速度模式快速接近,接近目标点时切到绝对定位精确定位。切换的时候要注意上一个功能块的Execute信号必须先复位再置位,否则新指令不触发。这个"先复位再置位"的动作,用代码写就是一个扫描周期的脉冲,很多人图省事直接用常TRUE,结果功能块只执行一次,后面的指令全被忽略。
回零也是必须配的。回零方式有好几种,按开关、按力矩、按绝对编码器。前两种要接传感器,最后一种要求伺服带绝对值编码器。选哪种取决于硬件,不是软件能决定的。回零完成后轴的位置值被设为原点值,后续所有绝对定位都基于这个点。
4.3 电子凸轮:凸轮表设计比编程更花时间
电子凸轮用于主从同步,比如飞剪、追剪、旋转贴标。软件上的操作是:定义主轴和从轴,建立凸轮表,用MC_CamIn把从轴耦合到主轴。
凸轮表本身是一组点和斜率,描述主轴位置和从轴位置的对应关系。设计凸轮表的时候,重点是保证加速度连续,否则从轴会在某些点产生冲击,机械上表现为抖动和异响。软件里一般有平滑选项,但平滑解决不了设计本身的问题。
调试电子凸轮,我的顺序是:先让主轴和从轴都单独能正常运行,再用很低的速度耦合,观察从轴是否跟着主轴按预期比例动,确认无误后再提速。中间用示波器或者软件里的追踪功能记录主轴位置、从轴位置、从轴速度三条曲线,看有没有突变。追踪功能的采样周期要和任务周期一致,否则曲线会失真。
解除耦合用MC_CamOut,要注意解除的时机。如果在从轴还在高速运行的时候解除,从轴会以解除瞬间的速度继续走,可能撞限位。稳妥做法是先让从轴减速到零,再解除耦合。
5. 在线调试和报错排查,车间里最耗时的部分
5.1 编译错误和运行时错误的区别,决定了你该往哪查
编译错误是语法和类型的问题,编译器直接拦住不让下载。这类错误好办,消息窗口里双击就跳到出错行。真正麻烦的是运行时错误,程序能下载能运行,但行为不对或者中途停机。
运行时错误的第一现场是日志。Codesys的运行时会把异常记录在设备的日志里,包含错误码、发生时间、任务名。我遇到过的几类典型错误:
- 数组越界:日志里提示访问地址超出范围,往往伴随某个变量被莫名改写。
- 除零:整型除零直接触发异常,浮点除零得到无穷大但不报错,后面计算全变成无效值。
- 看门狗超时:日志里明确写哪个任务超时,配合任务的实际执行时间看。
看日志之前先确认日志级别。默认可能只记录严重错误,一些警告被过滤掉了。调试期把日志级别调低一档,能看到更多线索,功能定型后再调回去,避免日志刷屏占存储。
5.2 在线修改不是万能的,哪些改动必须完整下载
在线修改(Online Change)省时间,但它有边界。可以改的:程序内部的逻辑、变量初值、部分常量。不能改的:新增或删除功能块实例、修改任务周期、修改全局变量列表的结构、更改设备配置、修改库版本。
遇到不能在线修改的情况,软件会提示需要完整下载。完整下载会中断运行,输出复位,对于正在生产的设备是大事。所以我的习惯是:结构性改动集中在停机窗口做,运行期间的临时调整只用在线修改。
还有一点容易忽略:在线修改后,如果没做"创建启动应用",设备重启后会回到上一次完整下载的版本。这个坑我踩过,调试完当场运行正常,第二天开机全变回旧逻辑。所以在调试完成后一定要执行一次"创建启动应用"并完整下载一次。
5.3 监视表、强制值和追踪,三件套的用法
监视表就是把你关心的变量拖进去看实时值。用法上有个小技巧:不要一次拖几十个变量,按功能分组,每组一个监视表,切换着看。变量太多刷新会拖慢在线连接,而且眼睛也看不过来。
强制值(Force)是强行给变量赋一个值,程序逻辑改不动它。用在调试传感器替代、模拟输入的时候很方便。但强制值不会自动清除,调试完忘了取消,后面逻辑怎么跑都不对。我一般调试结束前专门检查一遍强制列表,全部清除。
追踪(Trace)是记录变量随时间的变化曲线,比监视表强的地方是能看到历史。配置的时候注意采样周期和记录时长,采样太密记录时间就短,可能刚好错过问题发生的时刻。我一般先设一个较长的记录时长跑一轮,定位到大致时间点后再缩小范围加密采样。
6. 工程文件、版本管理和跨机器迁移
6.1 工程文件本质是个压缩包,能拆开看
.project文件其实是一个压缩容器,里面装着各个对象的源码、设备配置、库引用信息。正常情况下不需要拆,但在极端情况下有用:工程打不开、怀疑文件损坏时,把后缀改成.zip解压,能看到里面的XML文件,至少能确认工程结构还在不在。
不过我不建议日常靠拆包来管理版本。工程里的对象结构、库版本、设备描述都是耦合的,手工改XML很容易出问题。
6.2 PLCopenXML导出,做版本对比的正确姿势
工程文件是二进制压缩的,没法直接对比差异。要做版本管理,导出成PLCopenXML格式,它是纯文本,能看出两个版本之间改了哪些程序、哪些变量。
我的做法是每次提交前导出一份XML,和上一版放在一起对比。对比工具用普通的文本对比就行,重点看三样:程序逻辑有没有非预期改动、全局变量有没有增删、库版本有没有变。前两样是人改的,第三样往往是打开工程时被自动升级的,需要特别警惕。
导出的XML也可以用来做备份和迁移。换电脑、换版本的时候,导入XML比直接拷工程文件更稳,因为它不依赖原来的库缓存。
6.3 库管理:版本锁定比升级更重要
库管理器里能看到工程引用的所有库及其版本。默认情况下,新建工程会引用最新版本的库,这在单机开发时没问题,但团队协作时是隐患:A的机器上库更新了,B没有,同一份代码编译结果可能不同。
我的规矩是:工程定型前,把所有库的版本显式锁定,写进工程说明。升级库要单独做一次回归测试,不能顺手升级。库升级带来的行为变化往往很隐蔽,比如某个功能块的默认参数变了,或者错误码含义调整了,表面上看代码没动,实际运行结果不一样。
还有一个细节:库的存储位置。库可以装在系统目录,也可以随工程放在本地目录。随工程放的好处是换机器不用重新装库,坏处是工程体积变大。我倾向随工程放,尤其是在交付给客户的场合,避免对方环境缺库打不开。
7. 一些零散但很有用的记录
前面按主题整理完了,最后补几条不成体系但确实救了急的记录。
第一条是关于仿真。Codesys自带仿真模式,不需要真实硬件就能跑逻辑。仿真模式下,所有IO和总线都是模拟的,适合验证逻辑流程,但不适合验证时序和通信。我一般用仿真做逻辑预演,真实时序问题还是得上硬件。仿真和真实模式切换时,要注意有些功能块在仿真下行为不同,比如跟硬件相关的诊断功能块会直接返回成功。
第二条是关于中文路径。工程放在中文路径下,某些版本的编译器在处理库引用时会出问题,表现是编译报"文件找不到",但文件明明在。解决办法是把工程放到纯英文路径,这是个很老的问题,但每次换新环境都可能再遇到。
第三条是关于注释和文档。Codesys支持在程序里写注释,也支持给变量加描述。我给自己的要求是每个功能块实例旁边写清楚它的用途和调用条件,每个全局变量写清楚谁写谁读。这些描述在跨人交接的时候价值极大,比单独写一份文档更不容易丢。
第四条是关于备份频率。在线修改之后、完整下载之前、库升级之前,三个时间点各存一份工程快照,命名带上日期和改动摘要。这个习惯让我在两次误操作后都只花了几分钟就恢复了。
第五条,关于屏幕分辨率。Codesys的开发界面元素多,在低分辨率屏幕上,某些对话框的按钮会被裁掉,看着像是软件卡死。换一台高分辨率机器或者调缩放比例就好,这问题排查起来特别冤枉,第一次遇到会以为是软件故障。
这些记录没有什么体系,但每一条背后都是一次实打实的时间损失。写下来不是为了给别人看,是为了下次自己别再犯。工程越做越大,能省下来的排查时间,基本都藏在这些细节里。