跳到主要内容

PLC 编程

编译报错「未定义的变量」怎么办?

常见原因:

  1. 变量未声明 — 检查 VAR...END_VAR 中是否包含该变量
  2. 作用域问题 — 局部变量不能在其他程序中访问,需使用全局变量 (GVL)
  3. 拼写错误 — SCL 语言对大小写不敏感,但变量名必须完全匹配声明
(* 错误: motorSpeed 未声明 *)
motorSpeed := 100;

(* 正确: 在 VAR 中声明 *)
VAR
MotorSpeed : REAL;
END_VAR
MotorSpeed := 100.0;

SCL 支持哪些数据类型?

类型大小范围示例
BOOL1 bitTRUE / FALSETRUE
BYTE8 bit0 ~ 25516#FF
WORD16 bit0 ~ 6553516#FFFF
DWORD32 bit0 ~ 429496729516#FFFFFFFF
SINT8 bit-128 ~ 127-100
INT16 bit-32768 ~ 327671000
DINT32 bit-2^31 ~ 2^31-1100000
REAL32 bitIEEE 754 单精度3.14
LREAL64 bitIEEE 754 双精度3.14159265
STRING可变最大 255 字符'Hello'
TIME32 bit时间T#1s500ms
DATE32 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 没有断点/单步

工业 PLC 扫描周期是硬实时的, 不能暂停。DarraRT 不支持断点、单步执行等桌面调试器功能。调试手段全部基于"运行时监视 + 日志 + 报警"。

在线监视 (不暂停程序)

不暂停程序的情况下, 实时查看变量值:

  1. 连接到运行中的 Runtime
  2. 打开变量监视面板
  3. 添加要观察的变量
  4. 变量值以扫描周期刷新

强制写 (Force Write)

在线临时覆盖某个变量的计算值, 用于现场试验:

  1. 变量监视面板 → 右键变量 → 强制写
  2. 输入目标值, 勾选"锁定"后每个扫描周期强制回写
  3. 调试完成后必须取消强制, 否则会持续覆盖程序逻辑

趋势图

对实时变化的模拟量, 用趋势图观察波形:

  1. HMI → 添加趋势图控件
  2. 绑定变量, 设置采样率 (默认 100 Hz)
  3. 存储深度可选 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: 配方保存后重启丢失

排查:

  1. 配方目录权限

    icacls "$env:AppData\DarraPLC\recipes\"
    # Service 运行账号需有"修改"权限
  2. 磁盘空间

    Get-PSDrive C | Select Name, @{n='FreeGB';e={[math]::Round($_.Free/1GB,1)}}

    少于 500 MB 时写入可能失败。

  3. 保存 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 停机时:

  1. 打开报警历史, 找到 WDT 告警时间
  2. 查看同时刻的 service_*.log, 看最后一行执行到哪个 FB
  3. 常见原因:
    • 某 FB 内部死循环 (FOR/WHILE 未退出)
    • 通讯 FB 未用异步调用, 阻塞主循环
    • 大数组操作 (例如 MEMCPY 100KB) 超时
  4. 修改后冷启动恢复

Q: 已有程序怎么在线更新不停机

DarraRT 支持在线下载 (Online Update):

变化类型在线可行行为
代码逻辑修改扫描周期间隙切换新代码
新增变量变量分配空间, 初值 0
删除变量空间回收
变量类型变化需冷启动
任务周期变化需冷启动
EtherCAT 配置变化需冷启动

在线下载流程:

  1. IDE → 下载 → 选择"在线下载 (不停机)"
  2. IDE 编译新程序, 与运行版本做兼容性校验
  3. 下载增量到 Runtime
  4. 主扫描末尾的空闲窗口 (Idle Gap) 切换新代码
  5. 新代码从下一周期开始执行

冗余配置下的在线更新见 冗余功能 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