PLC 编程
编译报错「未定义的变量」怎么办?
常见原因:
- 变量未声明 — 检查
VAR...END_VAR中是否包含该变量 - 作用域问题 — 局部变量不能在其他程序中访问,需使用全局变量 (GVL)
- 拼写错误 — SCL 语言对大小写不敏感,但变量名必须完全匹配声明
(* 错误: motorSpeed 未声明 *)
motorSpeed := 100;
(* 正确: 在 VAR 中声明 *)
VAR
MotorSpeed : REAL;
END_VAR
MotorSpeed := 100.0;
SCL 支持哪些数据类型?
| 类型 | 大小 | 范围 | 示例 |
|---|---|---|---|
BOOL | 1 bit | TRUE / FALSE | TRUE |
BYTE | 8 bit | 0 ~ 255 | 16#FF |
WORD | 16 bit | 0 ~ 65535 | 16#FFFF |
DWORD | 32 bit | 0 ~ 4294967295 | 16#FFFFFFFF |
SINT | 8 bit | -128 ~ 127 | -100 |
INT | 16 bit | -32768 ~ 32767 | 1000 |
DINT | 32 bit | -2^31 ~ 2^31-1 | 100000 |
REAL | 32 bit | IEEE 754 单精度 | 3.14 |
LREAL | 64 bit | IEEE 754 双精度 | 3.14159265 |
STRING | 可变 | 最大 255 字符 | 'Hello' |
TIME | 32 bit | 时间 | T#1s500ms |
DATE | 32 bit | 日期 | D#2026-01-01 |
扫描周期太长怎么优化?
扫描周期过长的常见原因和解决方案:
1. 程序逻辑过于复杂
(* 不推荐: 主循环中执行大量数学运算 *)
FOR i := 0 TO 999 DO
result[i] := SIN(angle[i]) * COS(angle[i]);
END_FOR;
(* 推荐: 拆分到多个周期执行 *)
result[batchIndex] := SIN(angle[batchIndex]) * COS(angle[batchIndex]);
batchIndex := (batchIndex + 1) MOD 1000;
2. 字符串操作频繁
字符串拼接和格式化比数值运算慢 10~100 倍,避免在主循环中频繁使用。
3. 通讯操作阻塞
Modbus/TCP 等软件协议的通讯操作应放在异步 OB 中,不占用主循环时间。
4. IO 点位过多
使用 IO 地址打包优化 (AddressOptimizer),合并相邻地址为批量读写。
使用性能面板的「扫描周期分析」功能,可以看到每个阶段 (IO 输入读取 / PLC扫描 / IO 输出写入) 的耗时分解,快速定位瓶颈。
如何使用定时器?
IEC 61131-3 定义了三种标准定时器:
TON (延时接通)
VAR
Timer1 : TON;
END_VAR
Timer1(IN := StartSignal, PT := T#2s);
IF Timer1.Q THEN
(* 2秒后执行 *)
END_IF;
TOF (延时断开)
VAR
Timer2 : TOF;
END_VAR
Timer2(IN := StartSignal, PT := T#3s);
(* IN 变为 FALSE 后, Q 保持 TRUE 3秒才变 FALSE *)
TP (脉冲定时器)
VAR
Timer3 : TP;
END_VAR
Timer3(IN := TriggerSignal, PT := T#1s);
(* IN 上升沿触发 1秒脉冲输出 *)
如何调试 PLC 程序?
工业 PLC 扫描周期是硬实时的, 不能暂停。DarraRT 不支持断点、单步执行等桌面调试器功能。调试手段全部基于"运行时监视 + 日志 + 报警"。
在线监视 (不暂停程序)
不暂停程序的情况下, 实时查看变量值:
- 连接到运行中的 Runtime
- 打开变量监视面板
- 添加要观察的变量
- 变量值以扫描周期刷新
强制写 (Force Write)
在线临时覆盖某个变量的计算值, 用于现场试验:
- 变量监视面板 → 右键变量 → 强制写
- 输入目标值, 勾选"锁定"后每个扫描周期强制回写
- 调试完成后必须取消强制, 否则会持续覆盖程序逻辑
趋势图
对实时变化的模拟量, 用趋势图观察波形:
- HMI → 添加趋势图控件
- 绑定变量, 设置采样率 (默认 100 Hz)
- 存储深度可选 1 分钟 / 1 小时 / 1 天
日志 + 报警
程序内主动输出日志:
(* 关键状态变化时打日志 *)
IF State <> LastState THEN
LogInfo('state-machine', 'state=', State);
LastState := State;
END_IF;
(* 异常触发报警 *)
IF ErrorCode <> 0 THEN
RaiseAlarm(AlarmID := 100, Severity := ALARM_HIGH,
Text := 'Motor fault', ErrorCode := ErrorCode);
END_IF;
报警会写入报警历史数据库, 也会通过 HMI 实时推送。
如何实现 PID 控制?
Darra PLC 内置 PID 功能块:
VAR
PID1 : PIDController;
SetPoint : REAL := 50.0; (* 设定值: 50度 *)
Feedback : REAL; (* 温度反馈 *)
Output : REAL; (* 加热输出 0~100% *)
END_VAR
Feedback := INT_TO_REAL(AI_Temperature) * 0.01; (* 读取温度 *)
PID1(
SP := SetPoint,
PV := Feedback,
Kp := 2.0,
Ki := 0.5,
Kd := 0.1,
OutMin := 0.0,
OutMax := 100.0
);
Output := PID1.Out;
AO_Heater := REAL_TO_INT(Output * 327.67); (* 写入输出 *)
功能块和函数有什么区别?
| 特性 | 函数 (FUNCTION) | 功能块 (FUNCTION_BLOCK) |
|---|---|---|
| 返回值 | 有返回值 | 无返回值 (通过输出变量) |
| 内部状态 | 无 (每次调用独立) | 有 (保持上次状态) |
| 实例 | 不需要 | 需要声明实例 |
| 典型用途 | 数学运算、数据转换 | 定时器、计数器、PID |
部署运维层常见问题
Q: 下载程序到 PLC 后不运行
排查:
| 检查 | 正常值 |
|---|---|
| Runtime 模式开关 | 应为 RUN, 不是 STOP |
| 程序签名验证 | IDE 编译时签名通过 |
| 授权状态 | 激活且未过期 |
| 开机自启动 | 项目配置中勾选 |
| Q 区未被锁 | 检查安全 PLC 回路未置位 Disable |
查看 Runtime 当前状态:
# Service 日志会打印启动时的程序加载状态
Get-Content "$env:AppData\DarraPLC\logs\service_*.log" -Tail 50
典型失败日志:
[ERROR] 项目加载失败: 签名无效 (项目文件可能被篡改)
[ERROR] 授权校验失败: 许可证已过期
[INFO] 项目加载成功, 但当前为 STOP 模式, 等待 RUN 指令
Q: 冷启动 vs 热启动差别
| 启动类型 | 触发条件 | RETAIN 变量 | PERSISTENT 变量 | 普通 VAR |
|---|---|---|---|---|
| 冷启动 (Cold) | 手动冷启 / 首次下载 | 按初值 | 按初值 | 按初值 |
| 暖启动 (Warm) | 正常重启 / 断电恢复 | 保持上次值 | 保持上次值 | 按初值 |
| 热启动 (Hot) | 在线下载 / 冗余切换 | 保持上次值 | 保持上次值 | 保持上次值 |
IDE 中可配置每次下载采用哪种启动方式:
- 调试试验: 冷启动 (所有变量重置)
- 生产更新: 热启动 (不打断生产)
- 故障恢复: 暖启动 (安全默认)
Q: RETAIN 变量断电后丢了
RETAIN 变量存储在 %ProgramData%\DarraRT\retain\ 文件中, 每次变量变化异步刷盘 (默认 100 ms)。
故障场景:
| 现象 | 原因 | 处理 |
|---|---|---|
| 断电瞬间丢一部分 | 刷盘周期内未落盘 | 缩短 retain.flushIntervalMs, 或配 UPS |
| 全部丢失 | 持久化文件损坏 | 检查磁盘健康, 换 SSD |
| 每次开机都是初值 | 变量未标记 RETAIN | 在变量声明加 VAR RETAIN |
| 部分保留部分丢 | 变量新增但未重新下载 | 冷启一次重建持久化 |
VAR RETAIN
ProductCount : DINT; (* 累计产量, 断电保留 *)
LastRecipeId : INT;
END_VAR
VAR PERSISTENT
CalibrationData : ARRAY[0..99] OF REAL; (* 标定数据, 极长期保留 *)
END_VAR
Q: 配方保存后重启丢失
排查:
-
配方目录权限
icacls "$env:AppData\DarraPLC\recipes\"
# Service 运行账号需有"修改"权限 -
磁盘空间
Get-PSDrive C | Select Name, @{n='FreeGB';e={[math]::Round($_.Free/1GB,1)}}少于 500 MB 时写入可能失败。
-
保存 API 调用错误 确认调用
RecipeManager.Save(recipeId)后检查返回值:IF NOT RecipeManager.Save(101) THEN
LogError('recipe', 'Save failed: ', RecipeManager.LastError);
END_IF;
Q: 任务周期 WDT 超时导致 PLC 停机
Watchdog Timer (WDT) 保护机制: 主扫描任务超过设定周期未完成, PLC 自动进入 STOP, 输出全部清零 (安全状态)。
处理:
(* 项目设置 → 任务配置 *)
MainTask.CycleTime := T#1ms;
MainTask.WatchdogTime := T#2ms; (* 超时阈值, 一般是周期 2-3 倍 *)
MainTask.Priority := TASK_HIGH;
出现 WDT 停机时:
- 打开报警历史, 找到 WDT 告警时间
- 查看同时刻的
service_*.log, 看最后一行执行到哪个 FB - 常见原因:
- 某 FB 内部死循环 (FOR/WHILE 未退出)
- 通讯 FB 未用异步调用, 阻塞主循环
- 大数组操作 (例如
MEMCPY100KB) 超时
- 修改后冷启动恢复
Q: 已有程序怎么在线更新不停机
DarraRT 支持在线下载 (Online Update):
| 变化类型 | 在线可行 | 行为 |
|---|---|---|
| 代码逻辑修改 | 是 | 扫描周期间隙切换新代码 |
| 新增变量 | 是 | 变量分配空间, 初值 0 |
| 删除变量 | 是 | 空间回收 |
| 变量类型变化 | 否 | 需冷启动 |
| 任务周期变化 | 否 | 需冷启动 |
| EtherCAT 配置变化 | 否 | 需冷启动 |
在线下载流程:
- IDE → 下载 → 选择"在线下载 (不停机)"
- IDE 编译新程序, 与运行版本做兼容性校验
- 下载增量到 Runtime
- 主扫描末尾的空闲窗口 (Idle Gap) 切换新代码
- 新代码从下一周期开始执行
冗余配置下的在线更新见 冗余功能 Q6。
Q: 程序文件在哪, 怎么备份
项目工程文件存放位置:
| 文件 | 位置 |
|---|---|
| IDE 项目工程 | 用户自选 (建议 D:\DarraPLC\Projects\<name>\) |
| 编译产物 | <project>\bin\ |
| Runtime 已加载程序 | C:\ProgramData\DarraRT\projects\<name>\ |
| 配方 | %AppData%\DarraPLC\recipes\ |
| RETAIN 持久化 | C:\ProgramData\DarraRT\retain\ |
备份脚本:
# backup.ps1
$date = Get-Date -Format "yyyyMMdd-HHmm"
$dest = "\\fileserver\backup\DarraPLC-$date"
New-Item -ItemType Directory $dest
Copy-Item -Recurse "D:\DarraPLC\Projects" $dest
Copy-Item -Recurse "C:\ProgramData\DarraRT\retain" $dest
Copy-Item -Recurse "$env:AppData\DarraPLC\recipes" $dest
Compress-Archive "$dest\*" "$dest.zip"
Remove-Item -Recurse $dest
建议每周全量备份 + 每天增量 (RETAIN/recipe), 备份保留 90 天。
Q: 程序版本管理怎么做
DarraRT 项目工程文件是 JSON 结构, 可直接纳入 Git:
<project>/
├── project.darraproj ← 主工程描述 (JSON)
├── src/
│ ├── *.st ← SCL 源码
│ ├── *.cfc ← CFC (XML)
│ └── *.lad ← LAD (XML)
├── hmi/
│ └── pages/*.xml ← HMI 页面
└── bin/ ← 编译产物, 可加 .gitignore
.gitignore 建议:
bin/
*.log
*.user
Q: 多人协作时怎么避免冲突
- 按功能模块分文件 (一个
.st文件一人负责) - 全局变量表 (GVL) 冲突风险高, 建议单人维护, 其他人通过 PR
- IDE 保存时自动 LF 换行 + UTF-8 BOM, 与 Git 配合较好
- 二进制 HMI 资源 (图片) 用 Git LFS