
1. 为什么要在Cx-Programmer里用ST语言做PLC编程时间长了你会发现梯形图LD其实有点不够用。算个PID、捋一串数据处理算法、做配方管理这种带循环和分支的逻辑用梯形图写出来的东西又长又难懂——几百个触点和线圈摊在屏幕上改一个参数都要顺着网络翻半天。更别提那些复杂公式了梯形图根本没法直接写数学表达式只能拆成一个一个加法和乘法块看着都头疼。ST语言Structured Text结构化文本就是解决这个问题的。它长得像高级语言实际上是欧姆龙Cx-Programmer原生支持的IEC 61131-3标准语言之一。你在Cx-Programmer里建一个ST程序块就能直接用类似Pascal或者C的语句来写逻辑IF、CASE、FOR、WHILE、赋值、函数调用这些都能用。Cx-Programmer会把ST编译成和梯形图一样的指令表Instruction List再下载到PLC里执行所以实时性不受影响该快还是快。这篇内容会围绕Cx-Programmer里做ST编程的完整流程展开从最基础的数据类型、变量声明到IF/CASE/FOR这些控制结构再到怎么写函数和功能块、怎么和梯形图配合使用最后把我在实际项目中踩过的坑和调试技巧一并写出来。如果你之前一直用梯形图、想学ST或者刚接触Cx-Programmer想找一条能落地的路径这篇文章可以直接照着做。2. ST语言的项目准备与基础配置2.1 新建ST程序块的正确姿势在Cx-Programmer的工程树里找到“程序”节点右键选择“插入程序”类型选“ST”。这一步很关键如果你在梯形图程序里随便插个ST段那只是“梯形图程序里包含的ST文本段”两者编译和调用的方式都有差别。我个人的习惯是给ST程序单独建一个程序块比如叫“Algorithm”这样职责清晰后续维护也好找。建好之后Cx-Programmer会为你生成一个后缀为.st的空白源文件双击就能打开ST编程编辑器。这个编辑器自带语法高亮、关键词自动识别、括号匹配检查虽然没有现代化IDE那么智能但基本够用。有一点要注意Cx-Programmer老版本对ST编辑器的中文注释支持很一般建议代码注释统一用英文或者用拼音避免保存后再打开出现乱码。提示如果你用的是比较新的Sysmac StudioST编辑体验会好很多。但在Cx-Programmer环境下养成“先画流程、再写代码”的习惯比依赖编辑器更重要。ST代码在编译时才会报错写的时候可没有红波浪线提示。2.2 变量声明与数据类型这是ST的基石ST程序的变量声明区在“VAR”和“END_VAR”之间。和IEC标准一致Cx-Programmer的ST也支持三种作用域变量局部变量在程序块内部声明的外面访问不到全局变量在“符号表”或“全局变量表”里定义ST和梯形图都能用输入/输出变量用在功能块FB和函数FUN中分别在VAR_INPUT、VAR_OUTPUT、VAR_IN_OUT里声明我建议在使用ST之前先把Cx-Programmer支持的数据类型吃透。除了常见的BOOL、INT、DINT、REAL、WORD、DWORD之外还有几个ST里特别好用的INT16位整数范围-32768到32767做计数器、小范围运算够用DINT32位整数范围更大运算时不容易溢出我一般习惯用DINT替代INTREAL32位浮点注意它只有7位有效数字做高精度运算要小心STRING字符串类型在配方管理和报警文本拼接时非常有用ARRAY数组ST能直接循环遍历数组这是梯形图做不到的还有一点很多人不知道Cx-Programmer的ST里可以使用枚举间接寻址你可以定义一个数组用变量做下标去访问这在做配方切换时特别方便。公式上叫“查表法”本质就是用一个连续内存块映射不同的配方参数。变量声明时建议遵守几条规则名称要有含义变量名尽量用匈牙利命名法或下划线法比如“nCount”、“bIsRun”、“rPV_Temp”这样看代码就能猜到类型同一种用途的变量尽量集中放在一起加注释分组变量初始化很有必要BOOL默认是FALSEINT默认是0但REAL有时会出现未定义值强烈建议在声明时手动初始化2.3 从梯形图思维切换到ST思维很多从梯形图转ST的人最开始写出来的代码就是梯形图的翻译版——每个网络翻译成一行IF语句一段一段拼下来。这没错但你发现没有这样写出的ST代码特别啰嗦。基于我个人经验ST最大的优势是批量数据处理和复杂逻辑的扁平化表达。比如一个简单的启保停电路梯形图需要三个网络ST一行就能搞定bRun : (bStart OR bRun) AND NOT bStop;就这一行理解了逻辑的同事都能看懂。但要是把PID的误差累加、比例运算、微分运算全部展开你会看到几十行纯公式。这时候梯形图根本没法写ST的价值就体现出来了。所以我的建议是在开始写ST之前先把你的功能需求写成流程伪代码。比如如果设备处于自动模式 并且启动按钮被按下 并且没有急停信号 则启动电机 否则 停止电机然后把这个伪代码逐句转成ST逻辑不会乱代码也清晰。3. ST语言核心语法与实操注解3.1 赋值语句不要混淆“等于”和“赋值”ST里做赋值用的是“:”和比较用的“”是两个不同的符号。Cx-Programmer的ST继承了这个规则但新手经常在这上面栽跟头。nValue : 5; // 把数字5放进nValue变量 IF nValue 5 THEN // 比较nValue的值是否为5 ... END_IF;“:”是一个动作把右侧的值传入左侧的变量“”是一个判断返回TRUE或FALSE。这两个用错编译时Cx-Programmer不会报错但逻辑跑起来就完全不对了。比如你把“bReady TRUE”写到赋值位置等于把TRUE这个布尔值赋值给bReady变量语法合法但逻辑全乱。赋值是ST里最基础、最高频的操作。BOOL用TRUE/FALSE赋值数值直接填也可以把另一个变量的值赋过来Cx-Programmer会自动做隐式类型转换但注意精度损失。REAL赋给INT会丢小数DINT赋给INT可能溢出所以我建议养成显式转换的习惯用内置的转换函数nValue : REAL_TO_INT(rTemp); // 显式把REAL转成INT3.2 条件分支IF语句与CASE语句的选择IF语句应该是最常用的控制结构格式如下IF condition1 THEN // 条件1成立时的代码 ELSIF condition2 THEN // 条件2成立时的代码 ELSE // 所有条件都不成立时的代码 END_IF;注意Cx-Programmer的ST里ELSIF的拼写是“ELSIF”不是“ELSEIF”也不是“ELSE IF”。我有一段时间写得快老是敲成ELSEIF编译直接报错排查半天才发现是拼写问题。IF语句适合分支不太多的场景。如果分支超过三四个用CASE语句更加清晰CASE nMode OF 1: bOp1 : TRUE; 2: bOp2 : TRUE; 3, 4: bOp3 : TRUE; ELSE bOpDefault : TRUE; END_CASE;CASE语句的语法核心是“值匹配”它会把CASE后面的表达式这里是nMode和每个分支的常量值比较。它支持一个分支匹配多个值比如“3, 4”表示模式3或4都走同一段逻辑也有ELSE兜底。我在实际项目里发现一条经验状态机逻辑优先用CASE工艺流程分支优先用IF。因为状态机里一个时刻只能有一个状态CASE天然就是“多选一”代码像一张表一样清晰而工艺流程往往有多个条件交叉用IF更容易表达组合条件。3.3 循环结构FOR、WHILE与数据批处理这是ST相对梯形图的绝对优势。梯形图做批量数据处理要么展开多个重复网络要么用变址寄存器循环跳转指令代码特别绕。ST里直接写循环。FOR循环适合已知次数的重复操作FOR i : 0 TO 9 DO arrData[i] : arrData[i] * 2; END_FOR;这个例子把数组arrData的前10个元素全部乘以2。注意几点i不需要单独声明但Cx-Programmer要求i必须是已声明的INT/DINT变量FOR循环的循环变量在循环结束后会保留退出时的值比如上面例子里循环结束后i的值是10因为i自增到10时条件不满足退出。想让循环变量归零最好手动设置循环里如果有大量运算Cx-Programmer的扫描时间会被拉长。一个循环内有几万次浮点运算时要谨慎评估WHILE循环适合未知次数、靠条件判断退出的场景WHILE bCondition DO // 循环体 END_WHILE;WHILE循环有一个隐患如果bCondition永远为TRUE循环就永远不会结束这在PLC里会导致看门狗超时CPU直接报错停机。所以我加了条建议——WHILE循环里必须包含改变条件的语句并且要设最大循环次数保护nLoopCount : 0; WHILE (bCondition AND nLoopCount 1000) DO // 处理逻辑 nLoopCount : nLoopCount 1; END_WHILE;3.4 程序组织单元PROGRAM、FUNCTION与FUNCTION_BLOCKST代码可以组织成不同的“程序组织单元”POU即Program Organization Unit在Cx-Programmer里主要有三种PROGRAM程序由CPU调度的顶层执行单元类似C语言的main。Cx-Programmer的每个程序块本质上就是一个PROGRAM你可以在里面写ST代码也可以在里面调用函数和功能块。FUNCTION函数有输入、有返回值、没有内部记忆的功能块。典型的比如数学计算函数FUNCTION F_Multiply2 : INT VAR_INPUT x : INT; END_VAR F_Multiply2 : x * 2; END_FUNCTION调用方法很简单nResult : F_Multiply2(5);。注意函数里不能有静态变量或者不可复位的记忆它是纯计算的。FUNCTION_BLOCK功能块有输入、有输出、有内部状态记忆。比如一个电机控制块内部可以记住当前启停状态。这是我在工程中用得最多的组织方式。FUNCTION_BLOCK FB_MotorControl VAR_INPUT bStart : BOOL; bStop : BOOL; END_VAR VAR_OUTPUT bRun : BOOL; END_VAR VAR bInternal : BOOL : FALSE; END_VAR IF bStart THEN bInternal : TRUE; ELSIF bStop THEN bInternal : FALSE; END_IF; bRun : bInternal; END_FUNCTION_BLOCK然后在ST主程序里实例化并调用fbMotor1 : FB_MotorControl; fbMotor1(bStart : bBtnStart, bStop : bBtnStop); IF fbMotor1.bRun THEN // 后续逻辑 END_IF;有一个经验分享功能块实例化一次就占用一块固定的内存区域实例多了内存占用也会涨。但反过来它的好处是每个实例都有自己的内部状态记忆比如你用同一个功能块控制三台电机只需要实例化三个副本代码量一下子降下来。4. ST与梯形图LD的协同编程方案4.1 公司现有程序大多是梯形图怎么嵌ST在实际项目里很少会从零开始用纯ST重写整套控制程序。绝大多数场景是设备控制用梯形图数据处理和算法用ST。Cx-Programmer支持“程序段嵌入”——在梯形图程序里插入一个ST段梯形图网络和ST段落可以在同一个程序块里共存。这就意味着你可以在梯形图的某个网络后面右键选择“插入ST”段落然后写一段ST代码。这个ST段和梯形图网络共享同一个程序块的变量空间直接读写符号表里的变量。基于这个机制我的推荐做法是设备级逻辑电机启停、阀岛控制用梯形图因为电气工程师接手方便现场维护时看得懂算法级逻辑PID运算、数据滤波、配方映射、通讯协议解析用ST因为代码短、修改快模块化封装用功能块梯形图里也能调用你写好的功能块ST这样既有梯形图的直观性又有ST的灵活性和简洁性。Cx-Programmer自带功能块调用指令你可以在梯形图里直接插入FB_MotorControl的调用网络梯形图程序通过端子传入启动/停止信号功能块内部是ST逻辑两者毫无违和地协作。4.2 全局变量表让两种语言共享数据Cx-Programmer的“符号表”就是全局变量表。在ST里访问全局变量通过模块名来限定。比如你在符号表里定义了一个全局变量“g_bEmergencyStop”梯形图网络能访问ST代码里也能直接读IF g_bEmergencyStop THEN // 急停响应 END_IF;需要注意的是ST程序访问全局变量不用显式声明为EXTERNAL因为Cx-Programmer的编译模型是以程序块为单位的你只要确保符号表里这个名字存在且类型匹配即可。一个常见的坑是名字一样、类型不一样编译不报错但在线监控时数值永远是0或不正确。所以每次修改全局变量类型时要排查所有ST程序和梯形图引用处。4.3 在线监控与调试给ST调试开个“透视眼”Cx-Programmer对ST的在线监控比较朴素不像梯形图那么直观——程序块窗口里变量值会即时刷新当前执行的语句会被高亮显示。在线调试时Cx-Programmer的ST调试功能主要有两个好用的断点调试在ST编辑器左侧双击就能设置断点。PLC运行到那行时CPU会暂停执行这在工业环境里要小心设备会紧急停住Cx-Programmer会显示当前变量的值。我一般用断点在算法里验证中间计算结果比如写PID时设置断点看误差项和输出项是否在合理范围。监视窗口不用断点把关键变量编组放到监视窗口里在线刷新。我习惯在调试阶段把PID的PV、SV、输出值和偏差放进监视窗口调整参数时实时看曲线响应基本能定位问题。另外还要提一个Cx-Programmer特有的调试坑ST里对数组和结构体变量的在线监控不方便。数组变量的元素值只能在监视窗口里一个个手动填写下标去查看非常麻烦。我通常会在ST代码里写一小段临时逻辑把想要查看的数组元素赋值给几个单独的DINT/REAL变量再在监视窗口看这些临时变量实测很有效。5. ST编程进阶技巧与常见问题排查实录5.1 三角函数用什么指令在ST里Cx-Programmer内置了不少数学库函数基本覆盖工程常用SIN、COS、TAN求三角函数ASIN、ACOS、ATAN求反三角LN、LOG求对数EXP求指数ABS求绝对值。还有几个很实用的函数SQRT平方根要注意负数没法开方会得到错误结果、MOD取余运算、EXPT乘方运算。使用时注意单位Cx-Programmer的三角函数默认用弧度制不是角度。这在现场做运动控制时特别容易踩雷。解决方案要么在指令里加一个自定义的转换函数要么每次调用前把角度转成弧度rRad : rDeg * 3.14159265358979 / 180.0; rResult : SIN(rRad);好在Cx-Programmer支持你在ST里写自己的FUNCTION所以做一个角度转弧度的函数后续所有地方都能直接调用。5.2 数组与结构体配方管理的首选结构如果做多组工艺参数管理ARRAY和STRUCT是你躲不开的。结构体的定义在ST里很简单TYPE ST_Recipe : STRUCT nTempSet : INT; nTimeSet : INT; rSpeedSet : REAL; END_STRUCT END_TYPE然后在程序里定义实例VAR arrRecipe : ARRAY[0..9] OF ST_Recipe; END_VAR批量切换配方时ST代码可以通通用一组循环搞定FOR i : 0 TO 9 DO arrRecipe[i].nTempSet : 20 i * 5; END_FOR数组和结构体在Cx-Programmer ST里也可以定义多维数组访问方式arrData[3][2]和C语言差不多。但有一点要知道Cx-Programmer对结构体作为功能块输入的支持有限老版本甚至需要逐个成员传递不如Sysmac Studio方便。所以如果你要加强类型抽象还是建议用Sysmac Studio或者NJ/NX系列平台Cx-Programmer这边用简单的ARRAY和STRUCT就够了。5.3 注释不只是写给别人看ST代码支持两种注释风格//行注释和(* *)块注释。实际上我更推荐使用行注释因为块注释嵌套容易出错一旦嵌套错误编译报错的位置特别难找。注释内容除了描述功能最好把修改记录也写下// [2024-11-22] 修改滤波系数K解决温度波动大问题 - 某开发者这比在Word里维护变更记录好用得多因为注释跟着代码走不管工程传到谁的电脑上都能看到前人的修改历史。我吃了没有注释的亏——接手一个老项目ST程序3000多行没有任何注释光理解一个公式就花了两周。从那以后我都强烈建议团队ST代码不给注释就不要提交编译。5.4 常见错误速查与避坑经验我在多个Cx-Programmer项目里积累了一些ST编程常见错误整理成一张表希望能帮大家避开现象可能原因解决方法编译报错“Unknown identifier”变量未声明或拼写错误检查变量名Cx-Programmer区分大小写吗不区分但建议统一大小写风格数值总是溢出/错误类型不匹配DINT转INT导致溢出使用显式转换函数循环执行时间过长WHILE/FOR循环次数太多优化算法增加循环次数上限功能块输出不刷新功能块调用后忘了把输出传递给主程序变量在调用时使用输出变量接收在线监控时ST程序不执行相应的程序块未被调用或条件为FALSE检查程序块的任务周期设置和调用条件程序偶尔死机数组越界访问严格检查数组下标范围循环里与数组长度比较还有一个我个人最常见的问题忘了给FOR循环和WHILE循环设计退出条件。PLC扫描周期是固定的一旦循环体里有10万次运算这一周期就远远超过设定周期紧接着CPU的看门狗就会动作。我的经验是ST里的循环次数尽量控制在几千次内如果真要做大量数据搬运分段处理。5.5 提升ST代码可维护性的三条铁律写完第一版能跑的ST代码后我一般花半小时做重构。这个时间投入非常值得。第一一个功能块只做一件事。见过一个多功能块里既处理模拟量又做状态机又驱动电机输出代码460多行出了问题谁也不愿意改。拆成三个功能块每个80行调试验证都轻松得多。第二常量用宏/常量定义不要魔数。比如写出“IF nMode 3”这种代码你看代码的人完全不知道“3”代表什么。我习惯在程序最开始定义一组常量VAR_GLOBAL CONSTANT nMode_Auto : 1; nMode_Manual : 2; nMode_Remote : 3; END_VAR然后在ST代码里用常量名代替数字可读性瞬间提升。第三ST程序块要有一个清晰的主流程入口。Cx-Programmer的程序块一定会被调用执行但主流程如果淹没在大量辅助函数里读代码就很累。我会在ST顶部写几行主入口注释和一串跳转逻辑接下来是初始化分支、正常运行分支、异常处理分支。这种三层结构无论后期谁维护都能快速定位问题。6. 个人经验总结从踩坑到高效输出做了一段时间ST编程后我最大的体会是ST语言本身不难难的是从梯形图思维里跳出来。梯形图是“扫描网络”ST是“执行语句”两者的逻辑视角不同。早期我写ST时总是忍不住把梯形图中的中间继电器变量翻译成一个个BOOL变量后来才发现直接使用临时变量和函数返回值能让代码量少一半。调试ST代码时善用Cx-Programmer提供的在线监视和断点调试很多问题都能在一分钟内找到根源。但该离线模拟时还是先离线把数据想清楚再放PLC里跑。毕竟在线调试出问题整个产线可能都停下来等你。还有一点记得在完成一个功能模块后马上做单元验证不要攒到所有代码写完后才联调。我在一个项目里吃过亏三个功能块分头开发写完以后一联调发现变量命名冲突和类型不匹配整整花了三天时间排查。如果当时每个功能块写完都单独编一个临时测试程序验证十分钟就能发现问题。最后再分享一个实用技巧Cx-Programmer的ST编辑器和Excel结合能很快完成配方数组的初值录入。先在Excel里把数据整理好生成ST的数组赋值代码再粘贴到ST源文件里。这个方法比在编辑器里一行行手写快得多出错率也低。我每个配方管理项目都用这个方式屡试不爽。