简介:这份 Word 版 ST 语言编程手册面向工业自动化、机器人控制、数据采集与处理领域的初学者和工程技术人员,系统解决从语法入门到实际编程应用的学习需求。内容以语法图为主线,逐层展开语言描述、语法块与语义规则,并覆盖字符组、标识符命名规则、保留与保护标识符、整数浮点数与布尔值、字符串等基本元素,进而讲解基本数据类型与范围限制、用户自定义的枚举、结构、数组等派生类型,以及变量声明语法、初始值设定、赋值与表达式、运算符、顺序选择循环三类控制结构、函数与数组字符串处理,目录层次完整,便于按章节检索对照。资源包共 1 个 doc 文件,压缩后约 5.63MB,单文件即涵盖全部章节,适合离线阅读与打印。目前已有 235 人学习下载,可作为系统自学或查阅速查的入门参考。
1. 语法只是显性规则,真正的规则在变量寿命里
翻开一份 ST 语言编程手册,前三十页几乎都在讲语法图、标识符、数据类型,读起来像一本 Pascal 教材的翻版,很容易得出一个结论:ST 就是把 Pascal 的边角料裁掉,塞进控制器的壳子里。这个判断偏差很大。
真正让 ST 程序“看着对、跑起来错”的,恰恰不是语法没过关,而是每行代码背后绑着的两个隐性约定:变量能活多久,参数在哪个瞬间被拷贝。前者由变量模型决定,后者由 FC/FB 的调用约定决定。这两样东西语法图上一个字都不会写,却决定了你写的x := x + 1是累加、是重置,还是读到一个根本没人赋过值的随机数。搞懂这两层,再看那些语法规则,才会明白为什么手册要把“变量初始化时间”单独列一节、把“参数访问时间”写在调用规则里。
2. 语法图:为什么 ST 的正式定义是一张图,而不是一段 EBNF
2.1 四种结构拆开所有语句
ST 的正式语言描述用的是语法图,不是 BNF。图从左往右读,所有规则只用四种结构拼出来:
- 序列:块一个接一个,必须全走完;
- 选项:可以跳过的语句,对应“0 次或 1 次”;
- 迭代:一个或多个语句重复,对应“1 次或多次”;
- 替代:走这条或走那条,对应分支。
这套拆法的价值不在于描述得漂亮,而在于它是一把可复用的螺丝刀。以IF语句为例,IF ... THEN之后的语句序列是序列,ELSIF ... THEN这一段是迭代(零到多个),ELSE分支是选项(零到一个),最后END_IF和分号是固定的收尾。把这四个结构套上去,一种语句能写出哪些合法形态,一眼就清楚了。
对比 EBNF 就更明显:EBNF 用{ }、[ ]、|表达的是同样四件事,紧凑,但嵌套一深就得靠数括号。语法图占篇幅,却把“我能不能这么写”变成一个视觉问题,工程师扫一眼图就知道自己漏了哪个分支。副作用是它没法被工具直接解析,所以很多实现会在文档之外再维护一份文本文法,两套东西对不上的时候,以哪套为准往往要靠编译器实测。
更实用的一点是:遇到平台自己扩展的语句(比如等待条件的语句),你能自己把它的图补出来,也就顺带推出了它能写在哪些位置、能不能嵌套。
2.2 终端与复合单元:圆形、椭圆和长方形
语法图里的块分两类,这个分层是整套描述能收敛的关键。
终端用圆形或椭圆表示,它不再需要别的规则去展开——ST 字符组、预留标识符都属于这一类。你没法再问“IF这个词由什么构成”,它就是它。复合单元用长方形表示,意思是“这块另有图解释”,比如表达式、赋值、变量声明,都是长方形,点进去是另一张图。
分层带来的好处是可复用:<表达式>这个长方形在赋值语句、IF条件、CASE选择器、FOR边界里反复出现,改一处规则,所有引用点同步生效。它也带来一个读文档的技巧——先看长方形,把整个语句的骨架记住,需要细节时再逐层钻进去。反过来,如果一上来就从头读终端定义,很容易在字符集和特殊符号里绕不出来,忘了自己本来想查的是CASE能不能带范围标签。
顺带说一句,这套“圆形/长方形”的符号体系并不只是画法问题。终端与复合单元的边界,本质上就是词法分析和语法分析的边界,编译器在第一层拒绝非法字符,在第二层拒绝非法结构。
2.3 图里没有语义:为什么规则旁边必须贴注释
规则只定义格式结构,语义不在图里。手册里给了两个很硬的证据。
第一个是“同名不同义要额外命名”。日期拆出来的年、月、日都是十进制字符串,语法上完全一样,图里画不出区别,只能靠旁边的注释标明每个位置代表什么。第二个是“限制要额外列出”。整数规则里的负号,只能出现在SINT、INT、DINT这几类有符号类型之前,图里那个减号符号本身不会告诉你这一点。
这意味着语法通过只是最低要求。下面这段代码每一行都合法:
FOR i := 1 TO 10 BY 1 DO total := total + i; END_FOR;图能告诉你的只有:FOR后面跟赋值、TO后面跟表达式、BY是可选项、DO之后是语句序列、END_FOR收尾。但“终止值在什么时候求值”这件事,图里没有。常见的实现思路是:进入循环时对初值、终止值、步长各求值一次并锁住,循环体里再改这几个变量不影响循环次数。如果实现成每次迭代重新求值,i在循环体内被改写就会变成死循环——这种差异,只能靠语义说明和实测确认,语法图帮不上忙。
所以读手册的正确顺序是:语法图确认能不能写,紧随其后的规则说明确认写了会怎样。跳过后者,就等于只学会了拼写。
3. 词法层:编译器在第一行就能拒绝你的四种写法
3.1 大小写不敏感,以及它带来的连锁反应
ST 的字符集来自 ASCII:A到Z、a到z、0到9,加上一批有特定含义的特殊字符,下划线是其中之一。所有标识符都由这三类东西拼出来。
关键规则是大小写不敏感。Anna和AnNa在编辑器眼里是同一个东西,编译器在词法阶段就把它们映射到同一个符号表条目。好处很直接:工程师不用记某个变量当初是大写开头还是小写开头,HMI 组态、库导入导出、交叉引用表之间不会因为大小写对不上而丢引用。
代价也很直接:你没法用大小写区分两个东西。value和Value不能共存,SetPoint和setpoint只能是同一个变量。这在多人协作里是常见的问题——两个人各自建了一个“看起来不一样”的变量,编译时才发现撞名。更隐蔽的是跨工具场景:外部编辑器、版本管理工具、某些组态软件对大小写是敏感的,符号在 ST 侧不敏感、在对端敏感,就会出现“引用看起来在,编译就是不认”。
3.2 两条下划线的禁令与“合法但要避开”的命名
标识符的规则只有几条,但每一条都能卡住人:首字符必须是字母或下划线;后续字符是字母、数字或下划线;不允许连续出现两个或以上的下划线;不允许空格和除下划线之外的任何特殊字符。
| 写法 | 是否合法 | 原因 |
|---|---|---|
4ter | 否 | 首字符是数字 |
*#AB | 否 | 含特殊字符 |
RR__20 | 否 | 连续两个下划线 |
S value | 否 | 中间有空格 |
Array | 合法但不可用 | 是预留标识符 |
Array那一行最值得注意。它在词法上完全合法,但它是预留标识符,不能拿来命名变量或数据类型。预留标识符分成两类:一类是“保护标识符”,即语言本身已经占用的关键字;另一类是“额外预留标识符”,为将来扩展留的坑位。为什么要提前占坑?因为语言要向下兼容——今天让你把ARRAY用成自己的变量名,明天标准里加了新关键字,所有老程序集体编译失败。占坑是为了将来不破坏兼容性。
还有一条手册专门提醒、但很多人会忽略的建议:尽量避免用下划线、struct、enum、command这类词作为标识符开头。它们合法,但系统内部以及后续加载的技术包里,命令字、参数名、数据类型很可能也是这么开头的。今天不冲突,装个扩展包就冲突了,报错信息还往往指向一个你根本没写过的符号。这类问题的排查成本极高,命名时避开是最省事的做法。
3.3 进制前缀与字面量:写16#F而不是猜
数字字面量的规则不复杂,但细节密集:数字里不能出现逗号和空格;下划线可以作为视觉分隔符;可以带正号或负号,不带符号视为正;不能超过对应类型的最大最小值。
整数支持三种进制,靠前缀切换:2#二进制、8#八进制、16#十六进制。十进制 15 可以写成2#1111、8#17、16#F,编译期折叠成同一个常量。
VAR _enable : BOOL; (* 合法:下划线开头 *) axis1Pos : DINT := 16#0000_FFFF; (* 十六进制,下划线仅作分隔 *) ioPattern : WORD := 2#1111_0000_1111_0000; (* 二进制,看位序最直观 *) limitVal : INT := 8#17; (* 八进制 *) scale : REAL := 60_000.15; (* 下划线可出现在整数部分 *) END_VAR这段声明里,16#0000_FFFF和2#1111_0000_1111_0000是同一个值的两种写法,前者方便对字长,后者方便对位序。下划线不参与求值,编译器在词法阶段就把它剔掉了,所以60_000.15和60000.15完全等价。limitVal那行看起来奇怪,但8#17就是十进制 15,写八进制通常是为了和硬件侧的八进制地址对齐。
参数说明上的坑在类型边界:limitVal : INT := 8#17没问题,但如果你写limitVal : INT := 16#FFFF,字面量本身是 65535,塞进 16 位有符号整数就溢出了。手册专门写了“基础数据类型的值的范围限制”一节,就是为这个——字面量的合法性和它能装进哪个类型,是两件事。
浮点数还有额外的形态:可以带小数点,可以带指数,正负号规则和整数不完全一样。布尔值只有TRUE和FALSE,字符串则是一串字符。这些基本元素看着琐碎,但它们是词法分析器的全部输入,写错一个字符,后面的语法分析根本不会启动。
4. 类型与存储:变量活多久,比变量是什么类型更要命
4.1 从基本类型到派生类型:类型系统的三层
ST 的类型体系大致分三层。
最底层是基本数据类型和它们的取值范围限制。整型按位宽和有无符号分,浮点按精度分,此外还有布尔、字符串、时间这类。这一层的意义不只是“能装多大的数”,更是溢出的判定依据——两个INT相加,结果超出 16 位有符号范围,行为取决于实现:有的平台回绕,有的平台饱和,有的平台直接报错。手册把范围限制单列一节,说明这不是可以含糊过去的地方。
中间层是用户定义的数据类型,包括派生类型:ARRAY(同类型元素的定长集合)、枚举(一组具名常量)、STRUCT(把多个不同类型的字段打包)。它们由类型声明语法定义,可以层层嵌套——数组的元素可以是结构,结构的字段可以是枚举。
最上层是平台提供的系统数据类型和“技术目标数据类型”,也就是把轴、凸轮、跟随轴这类工艺对象直接建模成类型。这一层的设计动机值得多说两句。
4.2 技术目标数据类型:把轴当类型,而不是当一堆变量
传统写法里,一根轴对应一组散落的变量:位置一个、速度一个、加速度一个、使能一个、故障码一个,命名靠前缀区分,Axis1_Pos、Axis2_Pos、Axis1_Vel……轴一多,声明区就变成前缀的海洋,改一个属性要改几十行。
技术目标数据类型换了个思路:轴就是一个类型,位置、速度、加速度是这个类型的字段,而且字段通过继承关系层层派生——基础轴类型定义通用属性,具体轴类型继承并追加自己的属性。声明一根轴,就是声明一个该类型的变量:
TYPE T_AxisCfg : STRUCT maxVel : REAL; acc : REAL; enabled : BOOL; END_STRUCT; T_Mode : (M_IDLE, M_RUN, M_FAULT); END_TYPE VAR axisCfg : T_AxisCfg; (* 结构:字段成组访问 *) mode : T_Mode; (* 枚举:只能取三个值之一 *) END_VAR这么设计的好处有三层。第一,属性访问变成axisCfg.maxVel,语义自解释,不需要靠命名约定。第二,继承让平台可以在不改用户代码的前提下扩展属性——基础类型加了字段,派生类型的变量自动带上。第三,配置和代码解耦:轴的参数在工程组态里配,代码里只管引用,换一根轴只换变量名,逻辑不动。
代价是类型系统的复杂度上来了。你得知道哪些属性是继承来的、哪些是本层定义的,调试时看到一个不认识的字段,得往上翻继承链。
4.3 变量声明的四象限:保留/非保留 × 全局/本地
变量声明看着只是“起个名字、标个类型”,真正决定行为的是它落在哪个象限里。两个维度:是否掉电保留,以及作用域是全局还是本地。
| 变量类别 | 作用域 | 掉电保留 | 典型用途 |
|---|---|---|---|
| 保留的单元变量 | 全局 | 是 | 累计产量、配方参数 |
| 非保留的单元变量 | 全局 | 否 | 中间结果、状态标志 |
| 静态本地变量 | POU 内 | 是(随实例) | FB 内部状态 |
| 临时本地变量 | POU 内 | 否 | 循环下标、临时缓冲 |
“单元变量”指的是挂在源文件单元上的变量,跨 POU 可见;“本地变量”只在声明它的 POU 内可见,又分静态和临时两种。静态变量跟着实例走,跨调用周期保留;临时变量每次调用重新分配,用完即弃。
这张表最关键的一列是“掉电保留”。保留变量在设备重启后还是原值,非保留变量会被重新初始化。很多“怎么重启之后计数清零了”的问题,答案就在这一列——变量声明成了非保留,却当保留在用。
4.4 初始化发生在什么时候:五个不同的时间点
初始化是这套变量模型里最容易被忽略的部分,因为它不是“一次”发生的,是分五个时间点发生的:
保留的全局变量在启动时初始化(从保持区恢复);非保留的全局变量在启动或下载时初始化;本地变量每次调用时初始化;功能块的实例在实例化时初始化;技术目标的系统变量由平台在组态生效时初始化。此外还有一层保护机制:全局变量带版本标识,下载时比对版本,版本变了才重新初始化,没变就保留原值。
这五条合起来解释了一类非常典型的现象:改了一行代码下载进去,某个变量的值没变,你以为是代码没生效,其实是版本没变、初始化没触发,变量还带着上一次运行的值。反过来,如果你依赖“下载后一定归零”来做安全逻辑,那在版本未变的下载里就会失效。
临时变量的初始化最需要警惕。它分配在栈上,每次调用重新分配,语言不保证清零。上一轮调用留下的值、别的 POU 压栈留下的值,都可能出现在里面。手册把“本地数据栈变量的存储要求”单列一节,就是在提示这件事的底层原因。
5. 调用约定:FC 和 FB 差的不是一个关键字
5.1 运算符优先级与隐式转换的联动
表达式这块,语法只规定形状,真正决定结果的是优先级和求值顺序。典型的优先级从高到低是:括号、一元运算、乘除取模、加减、关系比较、逻辑与、逻辑或。注意逻辑与的优先级高于逻辑或,这一点和某些语言的直觉不同,a OR b AND c等价于a OR (b AND c),不是(a OR b) AND c。
优先级本身不难,难的是它和隐式类型转换叠加。算术表达式里混入不同类型时,编译器会做隐式转换:窄类型往宽类型靠,整型往浮点靠。方向大体合理,但有两处后果很严重:一是精度丢失,大整数转浮点后低位被截掉;二是溢出,两个INT相乘,结果还没转成DINT就先在 16 位里溢出了。
VAR a, b : INT := 300; r1 : INT; r2 : DINT; END_VAR r1 := a * b; (* 先在 INT 里算,溢出 *) r2 := DINT(a) * DINT(b); (* 显式转换后再算,结果正确 *)这两行的差别不在运算符,在求值时机。第一行先按INT算乘法,结果已经溢出,再赋给谁都没用;第二行先把操作数提升到DINT,乘法在 32 位里做。显式转换的意义就在这里——它不只是“换个类型”,而是把转换点提前到运算之前。手册把数据类型转换分成隐式和显式两类,并额外补了一节“补充的转换”,原因就是隐式转换在边界上的行为不够可预期。
5.2 控制语句的边界:EXIT、RETURN、GOTO 各自的出口
IF、CASE负责分支,FOR、WHILE、REPEAT负责循环,这五个是主干。真正容易出错的是三个出口语句。
EXIT只跳出当前一层循环,嵌套循环里它不会带你跳出两层。RETURN直接结束当前 POU 的执行,在功能块里意味着本次调用到此为止,后面的语句不执行,输出参数的拷贝照常发生——这一点很关键,RETURN不是“什么都没发生”,它是“提前收工但账还是要结”。GOTO配合标签做无条件跳转,能实现任意控制流,也能把程序跳成一团乱麻,手册把它单列一节而不是放进控制语句主干,态度已经很明确了。
还有一类平台特有的等待语句,语义是“条件不成立就停在这里”。在循环执行的模型下,它不可能真的阻塞——常见的实现是把当前 POU 的执行位置记下来,本周期直接返回,后续周期反复求值条件,成立才继续往下走。这意味着等待语句前后的代码可能跨多个扫描周期,中间所有变量的值都可能被别人改掉。把它当普通语句用,是典型的出错方式。
5.3 FC 与 FB:无状态和有状态的调用约定
功能(FC)和功能块(FB)在语法上长得像,本质差别是状态。
FC 没有实例,没有内部状态,同样的输入必然得到同样的输出。它的输出通过返回值或输出参数给出,调用结束即结束。FB 有背景数据(实例),内部变量存在实例里,这次调用的结果会成为下次调用的起点。
FUNCTION FC_Scale : REAL VAR_INPUT raw : REAL; gain : REAL; END_VAR FC_Scale := raw * gain; (* 无状态:只依赖入参 *) END_FUNCTION FUNCTION_BLOCK FB_Ramp VAR_INPUT setpoint : REAL; END_VAR VAR_OUTPUT out : REAL; END_VAR VAR cur : REAL; (* 存在实例里,跨周期保留 *) END_VAR IF cur < setpoint THEN cur := cur + 0.1; END_IF; out := cur; END_FUNCTION_BLOCKFC_Scale每调用一次都是全新的计算,FB_Ramp每调用一次都是“接着上次的cur继续爬”。这个差别决定了它们的使用场景:纯计算用 FC,需要记住上一拍状态的用 FB。用 FC 做斜坡,每次调用都从零开始;用 FB 做纯计算,白占一块实例内存,还会因为残留状态产生意料之外的结果。
5.4 参数在什么时刻被拷贝
参数传递的规则比“传值还是传引用”更细,它按参数方向分别定义时机:输入参数在调用开始时读入;输入输出参数在调用前读入、调用后写出;输出参数只在调用结束时写出。
这套时机规则解释了一个高频现象:在 FB 外面读它的输出参数,读到的是上一次调用结束时的值。如果这个 FB 本周期还没被调用,你读到的是上一周期的结果;如果它被调用了但执行到一半RETURN了,你读到的仍然是上一次的值。HMI 上显示“慢一拍”,根子通常在这里。
在 FB 外面写输入参数同理:你写进去的值,会在下一次调用开始时被调用方传入的实参覆盖。想改 FB 的行为,正确做法是改实参,不是去改形参。
FB 调用还有几个经典错误源:实例还没被调用就读它的输出;同一个实例在两个地方被调用,两处逻辑互相踩状态;以及递归调用自己导致实例重入。这些都不是语法错误,编译器不会拦你,但运行结果一定不对。
6. 避坑:五条让程序“看着对、跑起来错”的记录
6.1 五条现场记录
其一,临时变量没清零。现象是模拟器上跑一百遍都对,真机第一次上电就出错,重启一次又好了。原因在于临时变量分配在栈上,语言不保证初值,上一次调用残留的值恰好让逻辑走对了分支。解决办法是声明时给初值,或者干脆改用静态变量——如果这个值本来就该跨周期保留,用临时变量本身就是错的。
其二,命名撞上了系统符号。现象是装完一个扩展包之后编译报错,提示符号重复,但报错指向的那个名字自己根本没写过。原因是标识符以下划线或struct、enum、command开头,和系统内部命令字撞了。解决办法是统一命名规范,禁用这几类前缀;已经写了的,全局替换。
其三,读到 FB 的旧输出。现象是 HMI 上的数值总比实际慢一拍,或者某个状态位偶尔闪一下又回去。原因是输出参数只在 FB 调用结束时写出,在调用之前或执行中途读到的都是上一次的值。解决办法是把读取动作放到调用之后,或者直接读实例内部变量。
其四,一个实例两处调用。现象是两台设备的动作互相干扰,一台启动另一台跟着动。原因是两处逻辑共用同一个 FB 实例,背景数据只有一份。解决办法是每个调用点声明独立实例,实例名带上设备编号。
其五,直接访问绕过了过程图像。现象是同一个扫描周期里,两处代码读同一个输入点,读到的值不一样。原因是直接访问读到的是端口的实时值,过程图像访问读到的是本周期开始时锁定的快照。解决办法是统一走过程图像;确实需要实时值的场合,明确接受它和快照不一致这件事,并在注释里写清楚。
6.2 排查路径:从 watch 表走到调用栈
这五条有个共同点:都不是编译期能发现的。所以排查手段比编码技巧更重要,而手册给的调试工具是有层次的,按成本从低到高走。
第一层是符号浏览器和 watch 表。符号浏览器列出所有可见符号及其属性,用来确认“我以为存在的变量到底存不存在、是什么类型、在哪个作用域”。watch 表把关心的变量钉在一起,按周期刷新,用来确认“值到底有没有变、什么时候变的”。
第二层是程序状态。它能显示当前停在哪个代码位置、这个位置是通过哪条调用路径进来的。对于“这段代码到底有没有被执行”这类问题,比加打印语句快得多。
第三层是断点。调试模式下可以按任务组设断点,命中之后看调用栈——栈上每一层是谁调的、参数是什么。前面说的“一个实例两处调用”,在调用栈里会表现成两个不同的上层路径落到同一个实例,一眼就能看出来。
第四层是追溯。它记录一段时间内的变量变化曲线,用来处理“偶发”“闪一下”这类抓不住的现象。设置一次,等它复现,回头看波形。
还有一条容易被忽略的:生命周期监视。它检测程序是否还在正常跑、扫描周期有没有异常拉长。程序卡死和程序跑飞,在这项监视上的表现不一样,先分清是哪一种,再决定往哪个方向查。
7. 设计取舍:为什么这套语言把“周期”焊在语法之外
7.1 四个绕不开的取舍
第一个取舍是“图定义语法,注释定义语义”。好处是实现自由,各家平台可以在同一套语法图上做出不同的运行时行为;坏处是可移植性靠人肉保证,同一段代码换个平台,语法全过、行为不同。这不是疏忽,是标准留给实现的余地。
第二个取舍是大小写不敏感加严格的下划线规则。对写代码的人友好,对工具链不友好——所以才有交叉对照表、命名空间、库的 know-how 保护这些东西来补位。命名空间的存在,本质就是给“大家都想用同一个好名字”这件事一个技术出口。
第三个取舍是过程图像与直接访问并存。过程图像保证一个周期内输入一致,代价是延迟一个周期;直接访问拿到实时值,代价是一致性没了。两者都留着,是因为这两类需求真实存在,非要二选一,总有一半人要绕路。
第四个取舍是保留变量加版本标识的下载初始化。保留变量让设备重启后能接着干,版本标识避免每次下载都把参数冲掉。代价是“下载后变量归零”这个假设不成立,安全逻辑必须自己显式复位,不能指望下载动作。
| 取舍点 | 选了一边 | 放弃的一边 | 谁承担代价 |
|---|---|---|---|
| 语法图 vs 文本文法 | 可读性 | 工具可解析性 | 工具链开发者 |
| 大小写不敏感 | 编码效率 | 命名空间粒度 | 大型项目 |
| 过程图像 | 周期内一致性 | 实时性 | 高实时场合 |
| 保留变量 | 停机恢复能力 | 下载即初始化的直觉 | 调试期 |
7.2 验证一个 ST 程序的最小动作集
我这几年养成一个习惯:任何一段 ST 代码,上线前至少走完四步,一步都不省。
第一步,把变量声明按四象限过一遍,逐个确认“这个变量该不该保留、该不该是静态”。这一步能挡掉后面一半的问题。第二步,把所有隐式转换的地方改成显式,尤其是乘法和加法之前——不是不信任编译器,是不信任自己记得住类型边界。第三步,在 watch 表里把关键变量钉住,跑一个完整周期,看值的变化时机对不对,特别是 FB 输出参数,确认读的位置在调用之后。第四步,故意制造一次异常(拔输入、给超范围值、中途复位),看程序停在哪儿、调用栈长什么样。
这四步做完,剩下的问题基本都能靠追溯和断点定位。至于那份手册,我的用法是把它当两本书使:语法图和基本元素那几章当字典查,变量模型和调试那几章当教材读——前者查一次就够,后者每换一个平台都得重读一遍,因为语法是标准的,变量活多久从来不是。
从那以后我每次新建一个 POU,都会先把实例、静态、临时这三类变量分清楚再动手写第一行逻辑,宁可多花十分钟,也不愿意在调试台上花一晚上。希望帮到你。
本文还有配套的精品资源,点击获取