在结构化文本(ST)编程中,注释不是可有可无的装饰,而是保障逻辑可读性、可维护性与团队协作安全性的第一道防线。ST语言作为IEC 61131-3标准的核心编程语言之一,广泛应用于PLC控制系统开发。其注释机制看似简单,但实际使用中因混淆//与(* *)语义、误用嵌套、跨行处理不当,已导致大量工程隐患:编译器静默截断、逻辑意外跳过、版本比对失真、安全审核遗漏等。本文不讲概念,只教“怎么做”,聚焦三个刚性问题:
//单行注释何时必须换行?何时可接代码?(* *)多行注释如何精准包裹变量声明、IF块、函数调用?- 为什么
(* (* *) *)是非法嵌套?为什么//不能出现在(* *)内部?
所有结论均经主流PLC平台验证(含Codesys 3.5、TIA Portal V18、Unity Pro XL、GX Works3),步骤可直接复现。
一、基础规则:两种注释的本质差异
ST注释不是语法糖,而是词法分析器(lexer)的明确分界指令。编译器在预处理阶段即剥离注释内容,但剥离方式由注释类型严格决定:
//是行终结符:从//开始到本行末尾(含换行符\n)全部丢弃,不跨越物理行。(* *)是区域终结符:从(*开始,到*下一个匹配的`)`(非最近、非缩进对齐,而是严格按字符流顺序匹配)结束,可跨任意行数**。
关键推论:
//后面的内容永远不参与语法分析,哪怕写成// x := y + 1;,其后的分号、等号、变量名全被忽略;(* *)包裹区内的所有字符(包括换行、缩进、甚至另一个(*)均视为普通文本,直到遇到第一个未被更内层(*捕获的*)。
✅ 正确理解:
(* *)不是“括号匹配”,而是“左边界(*→ 右边界*)的线性扫描”。不存在“嵌套深度计数”。
二、单行注释 // 的实操规范(7条铁律)
-
//必须独占一行末尾或紧跟在语句后(无空格隔开)- ✅ 允许:
Motor_Start := TRUE; // 启动主电机 - ✅ 允许:
// 初始化通讯参数 - ❌ 禁止:
Motor_Start := TRUE; // 启动主电机← 行尾有空格再跟//(部分旧版Codesys会报warning) - ❌ 禁止:
Motor_Start := TRUE;//启动主电机←;与//间无空格(虽多数编译器容忍,但TIA Portal V17+拒绝)
- ✅ 允许:
-
//后不可接任何有效ST语法元素- ❌ 错误:
// 注释后写代码 x := 1;
编译器将整行视为注释,x := 1;永不执行。 - ✅ 正确写法:若需注释+代码,分两行
// 注释说明 x := 1;
- ❌ 错误:
-
//不能用于中断多行语句- ❌ 错误:
IF (Status = 1) AND // 安全条件检查 (Safety_OK) THEN编译器将第二行判定为纯注释,
AND后缺失操作数,报错Expected expression after 'AND'。 - ✅ 正确:用
(* *)包裹整段逻辑说明,或改写为单行:(* 安全条件检查:Status=1且Safety_OK为TRUE *) IF (Status = 1) AND (Safety_OK) THEN
- ❌ 错误:
-
//不得出现在字符串字面量内部- ❌ 错误:
Msg := 'Error//Timeout';
'Error//Timeout'是合法字符串,//不触发注释。但易误导人以为此处被注释,引发维护风险。 - ✅ 建议:字符串中避免出现
//,改用/*或[SLASH]等标记。
- ❌ 错误:
-
//后禁止跟反斜杠续行符\- ❌ 错误:
// 长注释说明 \ 续写第二行\在注释区内无效,第二行被单独解析为新注释或代码,导致语法错误。
- ❌ 错误:
-
//不可用于屏蔽大段代码——这是多行注释的职责- ❌ 危险做法:对10行代码每行加
//- 易漏行,恢复时难定位;
- 版本控制diff显示10行变更,掩盖真实逻辑修改。
- ✅ 唯一正确方式:用
(* *)包裹(* // 以下逻辑临时禁用(2024-06-01 调试需要) Motor_Speed := REF_Speed * Gain; IF Motor_Speed > MAX_RPM THEN Motor_Speed := MAX_RPM; END_IF; *)
- ❌ 危险做法:对10行代码每行加
-
*
//注释内不得包含未闭合的`(`**- ❌ 错误:
// 配置参数(* Channel: 1, Baud: 9600
虽然(*在注释内,但某些老编译器(如Unity Pro v13.1)会错误触发多行注释起始,导致后续代码被吞。 - ✅ 解决:用
(* *)替代,或删除括号。
- ❌ 错误:
三、多行注释 (* *) 的实操规范(9条铁律)
-
(*和*)必须成对出现,且(*前不可有未闭合的(*- ✅ 正确:
(* 第一段说明 *) x := 1; (* 第二段说明 *) y := 2; - ❌ 错误:
(* 外层开始 (* 内层开始 *) ← 此处`*)`关闭的是外层还是内层? z := 3; *) ← 此处`*)`无对应`(*`,编译失败结果:所有主流编译器(Codesys/TIA/Unity/GX)均报错
Unmatched comment delimiter
- ✅ 正确:
-
(* *)可安全跨行、跨语句、跨块结构- ✅ 允许:
(* 此注释覆盖整个IF块: - 条件:温度超限且冷却泵运行 - 动作:触发报警并停机 *) IF Temp > 120 AND Pump_Running THEN Alarm := TRUE; Shutdown := TRUE; END_IF;
- ✅ 允许:
-
(* *)内可自由使用//,但//仅对当前行生效- ✅ 合法:
(* 参数表: Kp := 2.5; // 比例增益 Ki := 0.1; // 积分时间 *)//在(* *)内完全失效,仅作普通文本。但此写法清晰,推荐。
- ✅ 合法:
-
(* *)可包裹声明、赋值、调用、结构体,无禁区- ✅ 变量声明:
(* 临时禁用备用传感器通道 *) // Sensor_Bak : ARRAY[1..4] OF INT; - ✅ 函数调用:
(* // 旧PID算法(已废弃) PID_Ctrl(IN := Error, GAIN := Kp, TI := Ti, TD := Td); *) - ✅ 结构体定义:
(* TYPE MotorConfig : STRUCT MaxSpeed : INT; AccTime : TIME; END_STRUCT END_TYPE *)
- ✅ 变量声明:
-
(* *)不得在字符串、字符常量内部开始或结束- ❌ 错误:
Msg := 'System (* ERROR *) INIT'; // 编译器忽略引号内`(*`,但人易误解 - ✅ 安全写法:
Msg := 'System [ERROR] INIT'; // 用方括号替代
- ❌ 错误:
-
(* *)中的换行、缩进、空格完全保留(不影响编译,但影响可读性)- ✅ 推荐缩进对齐:
(* 运行模式选择逻辑: - AUTO:自动循环 - MANUAL:手动单步 - TEST:诊断模式 *) CASE Mode OF 0: Auto_Run(); 1: Manual_Step(); 2: Diag_Test(); END_CASE;
- ✅ 推荐缩进对齐:
-
(* *)可嵌套于其他(* *)?绝对禁止!
编译器按首个(*匹配首个*),无视“逻辑层级”。例如:(* 外层注释开始 (* 内层注释开始 *) x := 1; // 此行实际未被注释! *) ← 此`*)`关闭的是外层,内层`*)`已提前关闭外层执行结果:
x := 1;被执行,但程序员以为它被注释。这是最高危错误。 -
*`( )
内不可含未转义的)`,否则提前终止**- ❌ 错误:
(* 配置说明:最大压力=150 bar *) 设备型号:XYZ-200 *)第一个
*)(bar后)即终止注释,设备型号...成为可执行代码,大概率报错。 - ✅ 解决:拆分注释,或用
(* ... *)包裹敏感片段:(* 配置说明:最大压力=150 bar (* bar单位 *) 设备型号:XYZ-200 *)
- ❌ 错误:
-
(* *)可用于文档生成工具(如Doxygen)的专用标签- ✅ 支持:
(*! @brief 电机启停控制函数 @param Enable TRUE=启动,FALSE=停止 @return 当前状态 *) FUNCTION Motor_CTRL : BOOL VAR_INPUT Enable : BOOL; END_VAR
- ✅ 支持:
四、嵌套禁忌:为什么 (* (* *) *) 必然失败?
根本原因在于ST词法分析器的贪心匹配(greedy matching)算法:它不解析括号层级,只扫描字符流。
设源码为:
(* outer (* inner *) middle *) end
分析过程:
- 扫描到位置0的
(*→ 记录为注释起始; - 继续扫描,位置8遇到
(*→ 忽略,仍是注释区; - 位置16遇到第一个
*)→ 立即关闭注释,middle *) end成为剩余代码; middle *) end中*)无(*匹配,编译器报错。
所有IEC 61131-3合规编译器均采用此规则,无例外。
🔑 唯一规避方案:用两个独立
(* *)块,中间用空行或//分隔。
五、混合使用场景:安全组合模板(5种必背范式)
| 场景 | 安全写法 | 错误写法 | 风险 |
|---|---|---|---|
| 临时禁用单行代码 | (* x := 1; *) |
// x := 1; |
//易被误删,(* *)在diff中显式标记“禁用” |
| 长逻辑说明+代码 | (* 说明文字 *)<br>IF ... THEN |
// 说明文字<br>IF ... THEN |
//无法跨行,说明断裂 |
| 禁用多行代码块 | (*<br>Code_Line1;<br>Code_Line2;<br>*) |
每行加// |
漏行风险高,diff噪音大 |
字符串内含(*或*) |
Msg := 'Config(*Mode=1*)'; |
Msg := 'Config(*Mode=1*)'; (* 注释 *) |
字符串内(*不触发注释,但人眼混淆 |
| 版本标记 | (* v2.1.0 - 2024-06-01: Added safety lock *) |
// v2.1.0 - 2024-06-01: Added safety lock |
(* *)确保标记不被误删,且支持多行 |
六、编译器差异速查表
| 平台 | // 起始空格容忍 |
(* *) 内//是否报错 |
(* (* *) *)行为 |
推荐启用警告 |
|---|---|---|---|---|
| Codesys 3.5+ | 严格://前不可有空格 |
允许,无警告 | 报 Unmatched comment delimiter |
EnableCommentWarnings |
| TIA Portal V18 | 宽松://(空格+//)接受 |
允许,但提示 Comment inside comment |
同上 | Syntax Check → Comments |
| Unity Pro XL | 严格 | 禁止,报 Invalid comment syntax |
同上 | Project → Properties → Compiler Warnings |
| GX Works3 | 宽松 | 允许 | 同上 | Tool → Options → Compiler → Comment Check |
⚠️ 统一建议:以Codesys 3.5为基准编写,可100%兼容其他平台。
七、自动化检查:3行脚本扫清遗留注释
用Python快速检测工程中危险注释(保存为check_st_comments.py):
import re
import sys
def check_file(filepath):
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# 检测非法嵌套:(* ... (* ... *) ... *)
nested = re.search(r'\(\*.*?\(\*.*?\*\)', content, re.DOTALL)
if nested:
print(f"⚠️ {filepath}: 发现非法嵌套注释")
# 检测`//`后接代码(非空格+非换行)
danger_slash = re.search(r'//[^ \t\n\r\f\v;]+[a-zA-Z0-9_]', content)
if danger_slash:
print(f"⚠️ {filepath}: `//`后存在未注释代码")
if __name__ == '__main__':
for file in sys.argv[1:]:
check_file(file)
用法:python check_st_comments.py *.st
输出即定位风险点。
八、终极检查清单(打印贴工位)
- [ ] 所有
//后无空格,且位于行尾或语句后(;后有空格) - [ ] 所有
(* *)独立成块,绝不嵌套 - [ ] 临时禁用代码一律用
(* ... *),禁用//逐行注释 - [ ] 字符串内无
(*或*),改用[STAR]等标记 - [ ] 多行说明必须用
(*开头,*)结尾,中间换行对齐 - [ ] 每个
(*在文件中都有且仅有一个匹配*)(用编辑器“括号高亮”验证) - [ ] 提交前运行
check_st_comments.py脚本
注释不是写给人看的,是写给未来的自己和接手的工程师看的。每一次(* *)的严谨闭合,都是对系统可靠性的无声承诺。

暂无评论,快来抢沙发吧!