干PLC这行的人,大概都有过这样的经历:程序写多了之后回头翻看,发现最耗时间的往往不是那些复杂的算法逻辑,而是“这个常数当初为什么设成12.5?”“MOTOR_HOLD_TIME到底是哪条产线定的参数?”之类的问题。我自己从做数控设备转做CoDeSys平台以来,交了不少学费才明白:一段ST程序能不能在多年后被自己和同事一眼看懂、安稳改对,常常取决于最基础的关键字和常数用得规不规范。这一篇继续“PLC编程公用元素”系列,专门把关键字与常数这两个“程序的核心词汇与固定值”掰开揉碎地讲一遍。
这两个东西看似基础,其实是整套IEC 61131-3标准里最容易被忽略、又最影响工程质量的部分。关键字规定了你“能说什么”,常数规定了你“定得死不死”。所以我一直对刚入门的工程师强调:先不急着学一堆高级功能块,把VAR声明写规范了,把常量规划好了,你的程序就成功了一半。这篇的内容适合正在学CoDeSys标准编程语法、想从“抄程序”过渡到“写程序”的同行,也适合准备在团队里统一编程规范的同学参考。
1. 关键字与常数:PLC程序的两类“基础设施”
1.1 关键字是什么——编译器眼中“有名有姓”的词
关键字在CoDeSys里就是一组预先定义好的保留字,比如IF、WHILE、VAR、BOOL、TRUE这些,它们构成了ST语言的语法框架。你可以把它类比成说话时的“连词、标点、时态标记”——没有这些词,句子就组不起来;反过来,你也绝对不能把“我”这个人名改成“了”这个助词,否则整句话就乱套了。
从编译器的视角看,所有ST源代码都要经历分词和语法解析的流程:先拆成一个个最小的词法单元,再按语法规则检查结构是否合法。关键字就是这棵语法树的固定分支节点,编译器看到IF就知道后面必须跟一个条件表达式,看到END_IF就知道这个判断语句收尾了。所以关键字是不能被重新定义的,你声明一个变量叫IF,编译器会直接报错,因为它无法区分这是语法结构还是变量名。
CoDeSys的关键字实际上包含两部分:第一部分是IEC 61131-3标准定义的公用元素,比如VAR、IF、CASE、FOR这些;第二部分是CoDeSys自己预留的扩展关键字,比如RETAIN、PERSISTENT、AT这类用于内存保持和地址绑定的词。这两类统称为保留字,规则都一样——只能按固定含义使用,不能另做他用。
有个非常实用的鉴别技巧:在CoDeSys的编辑环境里,默认会把关键字高亮显示。我在工程实战中几乎每天都用这个特征——写代码时只要看到一个标识符被系统自动变了颜色,立刻就知道这是保留字,不会再把它当变量名去用。不同CoDeSys版本的高亮配色会略有差别,但逻辑一致,本质就是编译器在用颜色悄悄告诉你红线范围。
1.2 常数为什么是程序质量的“隐形分水岭”
常数分两类:一类是字面常数,就是直接在代码里写的数字、TRUE、T#2s这种;另一类是命名常数,也就是先在声明区里给一个名字、再关联一个固定值。初学者往往会觉得字面常数写起来更快,但随着程序规模变大,字面常数到处散落会成为维护的噩梦。
打个生活化的比方:常数就像你家墙上贴的电器标签,写清楚“空调插座220V”当然能用,但如果在每个插座旁边都只写个“220V”,等你要插电饭煲的时候,还得逐个回忆哪一路是厨房的。同理,代码里到处写1500、3s、32这种“魔法数字”,三个月后你自己回来看都会发懵,更别说交接给同事了。
命名常数真正解决的是“一改全改”的问题。现场调试时经常要调延时时间、限位行程、报警阈值,如果你把这些值用命名常数统一收口在一处声明区,改的时候只需要动一个地方,逻辑代码完全不用碰。这个概念贯穿整个IEC 61131-3的公共元素体系,也是这一篇把关键字和常数放在一起讲的原因——二者一个管语法骨架,一个管数据取值,配合起来才能写出真正规整的PLC程序。
2. CoDeSys关键字全景盘点与使用边界
2.1 声明类关键字:搭起程序的骨架
声明区是ST程序的入口,也是初学者最容易小看的部分。VAR关键字用于声明局部变量,在功能块或程序内部有效,每次调用时默认不保持;VAR_INPUT声明输入变量,相当于功能块的“入口参数”,调用方只能从外部给它赋值;VAR_OUTPUT声明输出变量,相当于功能块的“返回值通道”;VAR_IN_OUT则比较特殊,它是输入输出双向的引用参数,传递的是变量本身而不是拷贝,使用时需要特别谨慎,避免在不经意间修改了外部实参。
VAR_GLOBAL用于声明全局变量,可以在多个POU之间共享数据。这里我必须强调一句:PLC编程里滥用全局变量是程序腐化的最快路径。全局变量破坏了模块的封装性,A程序改了某个全局量,B程序的行为可能莫名其妙跟着变,排查难度成倍上升。我个人的习惯是能不用就不用,跨POU数据优先通过输入输出参数接口传递。
VAR_TEMP声明临时变量,在每个扫描周期开始时会使用未初始化的内存区(一个周期内有效),常用于功能块内部的中间计算。VAR_CONSTANT声明命名常数,这是后面第3章要详细展开的内容。VAR_EXTERNAL用于引用外部变量,常用于跨工程文件或者库访问。
组织层面还有几个关键字需要记住:PROGRAM声明一段主程序;FUNCTION_BLOCK声明功能块;FUNCTION声明函数;METHOD和PROPERTY用于面向对象扩展,定义功能块的方法和属性;TYPE和STRUCT配合用于自定义数据类型和结构体。这一整套声明关键字构成了CoDeSys程序的基本骨架,也是后续架构设计的基础。
2.2 流程控制类关键字:指挥程序的走向
流程控制关键字是ST语言的核心执行逻辑骨架。IF-THEN-ELSIF-ELSE-END_IF是最常用的条件判断结构,注意ELSIF不是ELSEIF,这是IEC标准与很多通用语言不同的拼写,踩坑频率极高。CASE-OF-END_CASE适合对整数或枚举值做多分支选择,比连续嵌套多个IF要清晰得多。
循环方面,FOR-TO-BY-DO-END_FOR是确定性循环,适合已知循环次数的情况;WHILE-DO-END_WHILE是前置判断循环,先判断条件再执行;REPEAT-UNTIL-END_REPEAT是后置判断循环,至少执行一次。这三种循环在CoDeSys的扫描周期模型下都需要谨慎使用,尤其是WHILE和REPEAT,一旦条件写错,程序可能在一个扫描周期内死循环,导致整个PLC任务看门狗超时。EXIT关键字用于强制跳出当前循环,RETURN则用于提前结束当前POU的执行,相当于函数返回。
逻辑运算关键字也归在流程控制的大范畴里:AND、OR、XOR、NOT是基础逻辑运算,还有两个容易忽略的短路关键字AND_THEN和OR_ELSE。普通AND会计算两侧的所有表达式,而AND_THEN在左侧条件已经不满足时会直接跳过右侧计算。这个特性在防止数组越界、除数非零判断时尤为关键。比如要判断“索引在有效范围内且对应元素大于5”,用普通AND的话即使索引无效,元素仍然会被访问——这可能导致运行时错误或读取脏数据;用AND_THEN就能实现真正的短路保护。
2.3 容易踩线的关键字边界与命名规范
CoDeSys中IEC 61131-3标准明确关键字不区分大小写,这意味着你不能用IF、If、if三种写法规避保留字限制——无论怎么变体,编译器都识别为同一个关键字。这一点和C语言不同,很多从C转来的工程师会在这上面栽跟头。
| 关键字类别 | 常见保留字示例 |
|---|---|
| 数据类型 | BOOL, BYTE, WORD, DWORD, INT, DINT, REAL, LREAL, TIME, STRING |
| 声明区 | VAR, VAR_INPUT, VAR_OUTPUT, VAR_IN_OUT, VAR_GLOBAL, VAR_TEMP, VAR_CONSTANT |
| 流程控制 | IF, THEN, ELSIF, ELSE, END_IF, CASE, OF, FOR, TO, BY, DO, WHILE, REPEAT |
| 运算逻辑 | AND, OR, XOR, NOT, MOD, AND_THEN, OR_ELSE |
| 程序组织 | PROGRAM, FUNCTION_BLOCK, FUNCTION, METHOD, PROPERTY |
| 特殊值 | TRUE, FALSE, NULL |
实际编程时的命名规范,我建议遵循行业里比较通用的匈牙利前缀法:布尔变量用b开头,如bMotorOn;整数用i开头,如iCounter;浮点用r或f开头,如rTemperature;时间变量用t开头,如tDelay;字符串用str开头,如strProductName;常量则统一用大写全称加下划线,如MOTOR_RUN_HOLD_TIME。这样的好处是扫一眼变量名就能知道类型和用途,逻辑错误在阅读时就能被肉眼拦截大半。
还有一类容易踩的坑是CoDeSys扩展保留字,比如AT(地址分配)、RETAIN(保持区变量)、PERSISTENT(持久化变量)。这些词在标准里没有,但CoDeSys编译器识别它们。有些人是第一次接触就拿来当变量名,然后百思不得其解为什么编译报错。遇到这种情况,最快的方法是右键该标识符查看系统提示,看它是不是被当作保留字高亮了。
3. 常数的定义方式与CoDeSys特有语法
3.1 数值常数:进制、浮点与类型前缀
数值常数是程序里最常见的一类固定值。IEC 61131-3标准支持多种进制,CoDeSys中十进制直接写即可,十六进制用16#前缀,二进制用2#前缀,八进制用8#前缀。例如16#FF表示255,2#1010表示10,8#17表示15。值得一提的是CoDeSys允许用下划线分组提高可读性,比如2#1010_1010,这一点在配置IO地址掩码时非常实用。
浮点常数默认按REAL类型处理,也可以写成科学计数法,比如1.5E3表示1500.0。这里有一个隐蔽的坑:直接写的整数常数默认按最小适配类型处理,但如果你把它直接赋值给一个LREAL或DINT变量,可能会触发隐式类型转换警告。要避免这个问题,CoDeSys提供了类型化常数的语法,在数值前面加上类型前缀并配合#符号,例如INT#5、DINT#10、REAL#3.14、LREAL#2.71。这种写法让常数自带类型信息,赋值时编译器可以精确匹配,极大减少隐式转换带来的精度损失和编译告警。
还要特别提醒一句:在CoDeSys里把浮点常数赋给整型变量,或者反过来把数值常数赋给TIME类型变量,都会出现类型不匹配错误。很多新手在定时器参数那里直接写5,系统报错后一脸茫然——因为定时器需要的是TIME类型字面量,正确写法是T#5s。
3.2 布尔、时间与字符串常数
布尔常数很简单,就是TRUE和FALSE两个关键字,但要注意它们也是保留字,不能用作变量名。
时间常数是PLC编程里最常用、也最容易出格式问题的一类。CoDeSys使用T#或TIME#作为前缀,后面跟时间单位组合。毫秒写ms,秒写s,分钟写m,小时写h,天写d。例如T#500ms、T#3s、T#2m、T#1h30m20s。组合时间常数可以直连多个单位,比如T#1d2h3m4s5ms,系统会精确计算出总时长并存储为TIME类型。要注意单位后不加空格,小时部分不要写超过23的大数字——正确表达25小时应该用T#1d1h而不是T#25h,后者虽然可能被某些版本接受,但可读性极差。
字符串常数用单引号或双引号包裹。CoDeSys中STRING类型的字面量通常用单引号,例如'Running'和'Stopped',这样便于在HMI或数组IO中使用。字符串常数在报警信息处理、设备名牌传递、配方名称管理等场景下非常重要。我还见过有人把字符串常数和CHAR类型混淆:CHAR是单个字符,STRING才是字符串,两者声明和赋值语法不同,需要区分清楚。
3.3 命名常数:从魔法数字到工程可维护性
命名常数是通过VAR CONSTANT或VAR_GLOBAL CONSTANT声明的常量集合。代码写起来是:
VAR CONSTANT MOTOR_RUN_HOLD_TIME : TIME := T#3s; MAX_PRODUCT_COUNT : INT := 500; ALARM_MAX_TEMP : REAL := 85.5; END_VAR在VAR CONSTANT块里,每个名称一旦被赋予固定值,程序运行期间就不能再修改。如果试图在逻辑代码里对它赋值,编译器会直接报错。命名常数的好处前面已经提过:可读性、可维护性、一处修改全局生效。
需要补充的一个机制是常量折叠。CoDeSys编译器在编译阶段会把只包含常量的表达式提前算好结果。比如你在代码里写MAX_PRODUCT_COUNT - 1,只要MAX_PRODUCT_COUNT是常量,编译器就会直接编译成499,运行时没有额外计算消耗。这算是白送的性能优化,同时还能让代码语义更清晰。
不过硬币的另一面是:如果某处本应写成变量的数据被误定义成了常量,程序运行过程中该值永远不会变化,这种故障排查起来非常隐蔽。所以我的建议是——规则类、配置类的固定值才用命名常数;运行期可调、要跟配方走的数据,就用变量加HMI绑定,千万不要图省事把所有参数都写成常量。
4. 联动实战:用关键字和常数重写一个电机启保停程序
4.1 先看一个“裸写”版本的问题
拿一个最常见的电机启保停控制来做实例。原始版本可能是网上抄来的,逻辑长这样:
VAR bStart : BOOL; bStop : BOOL; bRun : BOOL; END_VAR IF bStart THEN bRun := TRUE; END_IF; IF bStop THEN bRun := FALSE; END_IF;这段代码的问题是显而易见的:第一,两个IF是顺序关系,同时按启动和停止时,后执行的IF bStop会把bRun清零,逻辑上“停止优先”。这个行为本身也许是对的,但代码表面上根本看不出来,语义全靠藏在执行顺序里的副作用。第二,整个逻辑没有互锁机制,如果启动按钮粘连或者程序被误写,电机可能无法正常停机。第三,所有条件都是布尔裸变量,没有任何滤波和延时缓冲。
对照这篇讲的关键字和常数,可以发现这段代码至少应该用CASE或显式互锁结构来重构,并且把延时参数提取成命名常数。这就是关键字和常数在实际程序里发挥作用的地方——不只是语法规则,更是程序结构的约束。
4.2 用声明关键字和常数重构规范版本
我在现场写这类逻辑时,会先考虑控制需求,把参数和状态提炼出来。假定需求是:电机上电后按启动按钮运行,按停止按钮停止,同时要求启动后至少保持运行3秒才能被停止,防止频繁启停损坏设备。
重构后的代码分成两个部分。第一段是声明区,把定时器值和按钮滤波时间全部用命名常数定义:
VAR CONSTANT MIN_RUN_TIME : TIME := T#3s; BUTTON_FILTER_TIME : TIME := T#50ms; END_VAR VAR bStartRaw : BOOL; bStopRaw : BOOL; bRun : BOOL; tRunTimer : TON; tBtnFilter : TON; END_VAR第二段是核心逻辑,用TON定时器实现按钮滤波,用时间常数做保持计时。TON功能块的PT端直接填MIN_RUN_TIME常数,这样将来现场觉得3秒太短,只需要改声明区一行,逻辑部分完全不用动。
tBtnFilter(IN := bStartRaw, PT := BUTTON_FILTER_TIME); IF tBtnFilter.Q THEN bRun := TRUE; tRunTimer(IN := bRun, PT := MIN_RUN_TIME); END_IF; tRunTimer(IN := bRun, PT := MIN_RUN_TIME); IF bStopRaw THEN IF tRunTimer.Q THEN bRun := FALSE; END_IF; END_IF;这里有几个细节值得说明:第一,bRun被置TRUE后,tRunTimer的IN就为TRUE,T#3s后Q输出为TRUE,此时如果再来停止信号,bRun才能被清掉。第二,启动用滤波后的按钮信号,避免现场抖动造成误触发。第三,停止信号直接取原始值,保证急停按钮的响应速度不受滤波影响——这是工程上的一个常见决策。
对照原来的裸写版本,新版本多用了四个命名常数、一个TON功能块、一个略显复杂的IF嵌套结构。代码行数增加了,但逻辑清晰度提升了一个档次。尤其是MIN_RUN_TIME这个常数,它把“为什么程序要让它多转几秒”这个业务规则直接摆在了声明区,后续维护的人一眼就能看到,不需要去猜测3秒是哪来的。这正是这个实战想传达的核心思想:关键字决定代码能不能跑,常数决定程序好不好改。
4.3 编译验证与在线调试看板
写完代码后在CoDeSys里点编译,如果一切正常,信息窗口只会有零条错误零条警告。我建议把编译选项里的“严格模式”打开,这样所有隐式转换都会被列为警告或错误,逼着你把常数类型写规范。比如延迟PT的值如果不写T#3s而直接写3,严格模式下立刻会提示类型不匹配,从根源堵住隐患。
在线调试时,我在监视窗口同时观察bStartRaw、bStopRaw、tRunTimer.Q和bRun几个关键变量。启动瞬间看滤波定时器的Q是否按预期延迟,停止时看tRunTimer的当前值和Q翻转是否一致。这个调试手法屡试不爽——很多看似莫名其妙的启停故障,其实问题都出在定时器还没计时完成就被反复重启上。这个时候也能体会到定义MIN_RUN_TIME常数的好处:一旦确定了要延时,就不用在多个功能块里到处找时间参数,监视窗口一目了然。
5. 常见报错与排查技巧实录
5.1 关键字相关的编译错误速查
| 错误现象 | 可能原因 | 解决办法 |
|---|---|---|
| 提示“未知标识符” | 变量名使用了保留字 | 修改变量名,使用前缀命名法 |
| 提示“语法错误”或“意外的关键字” | IF/END_IF不配对,或ELSIF拼错 | 检查结构化语句是否成对,建议使用代码折叠辅助 |
| 提示“需要表达式” | THEN后面没有写判断结果 | 检查条件表达式是否完整 |
| 声明区重复定义同名变量 | 同名标识符在多个声明区出现 | 统一声明到最合适的VAR块下 |
| 逻辑运算符号不匹配 | 普通AND与AND_THEN混用 | 明确是否要短路,优先用短路版本规避风险 |
ELSIF拼错成ELSEIF是我见过最多的一类编译错误。在CoDeSys里正确拼法是ELSIF,中间没有E。每次报“意外的关键字”时,先检查是不是这里写错了,往往能省下十分钟。
还有一个和关键字强相关的坑是:变量名虽然不撞任何一个保留字,但命名风格和关键字看起来高度相似,比如把变量命名为TIMER、IO_CHANNEL。虽然编译器不报错,但阅读时极容易和关键字TIME、IO变量混淆。团队协作时最好在命名规范里明确:所有自定义标识符必须带类型前缀且避免全大写词。
5.2 常数使用中的隐蔽陷阱
常数类型不匹配是编译期最常见的警告来源。默认的整数字面量在CoDeSys中被视为INT类型,如果你把它喂给DINT变量,会有隐式转换警告;喂给REAL类型,则会提示REAL与INT转换可能损失精度。解决办法就是前面说的类型化常数:DINT#100、REAL#2.5,明确告诉编译器你要的是什么类型。
时间常数格式的坑也很隐蔽。T#3s没问题,但T#0.5s里的小数点在某些版本里会引发异常,正确写法是T#500ms。还有人在表达式里写T#1.5m表示1.5分钟,这也是有风险的——应该写成T#1m30s。总之时间常数千万不要在小数和进制上做文章,老老实实用整数加多个单位组合表达。
然后是共享常量的波及范围问题。项目跨多个POU引用同一个TL等全局常量,改动时确实只改一处,但这“一处”改完后,所有引用点都会悄悄变化。我在实际项目中遇到过:把某个延时常量从5s改成8s后,产线上有一个吹气阀的动作时间跟着变了,当时排查了很久才定位根因。所以命名常量最好在声明处写明影响范围注释,改动前先用交叉引用列表查看所有使用位置。
5.3 我踩过的最贵的一个坑
最后分享一个我自己早年踩过的坑:在CoDeSys的FOR循环里,把循环变量声明成WORD类型,结果循环次数超过32767之后出现了隐式转换问题,程序跑到半夜才爆出偶发故障。后来把所有数组下标和循环控制变量统一改成DINT或UDINT,并配合常量折叠和显式类型转换,这个隐患才算彻底排除。
在CoDeSys这个平台上,关键字的规则是死的,但人的使用习惯可以相差十万八千里。我在团队里推行的习惯是:常量必须在声明处写清楚“含义、单位、允许范围、修改影响范围”四要素,开头命名用“对象_属性”全大写,例如MOTOR_RUN_HOLD_TIME、ALARM_MAX_TEMP。这样任何同事接手都能快速定位。另外坚持一个字面常数出现次数超过两次就转成命名常量的原则,配合代码折叠注释,程序维护成本会大幅下降。做设备维护时最怕的不是程序跑飞,而是别人改了一个全局常量导致整个生产节奏变化,却没有改注释。基础的东西守规矩,复杂的东西才有机会不翻车。