【实时Linux工业PLC解决方案系列】第三十三篇 - 实时Linux PLC安全PLC功能实现
一、简介:为什么安全 PLC 是工业自动化的"生命线"?
事故代价:2019 年德国某汽车厂机械臂失控,致 1 死 2 伤,赔偿+停产损失超 2 亿欧元。根因:安全 PLC 响应延迟 120ms,超出设计阈值 50ms。
法规强制:欧盟机械指令 2006/42/EC、中国 GB/T 20438(等同 IEC 61508)明确要求:安全相关控制系统须通过独立功能安全认证。
技术趋势:传统专用安全 PLC(如 Pilz PSS、Siemens S7-F)成本高昂、封闭生态;基于实时 Linux 的软安全 PLC成为新选择——开源可控、硬件通用、认证路径清晰。
掌握安全 PLC 功能实现,是工业自动化工程师从"功能实现"迈向"安全设计"的关键跃迁,也是国产替代方案的核心竞争力。
二、核心概念:6 个关键词读懂安全 PLC
| 关键词 | 一句话说明 | 本文出现场景 |
|---|---|---|
| SIL | 安全完整性等级,SIL 1-4,数字越高失效率越低 | 定级、设计、验证全流程 |
| SIF | 安全仪表功能,如"紧急停机"是一个完整 SIF | 功能分解与实现 |
| PFD/PFH | 按需失效概率 / 每小时失效频率,量化安全指标 | 架构设计与计算 |
| 冗余架构 | 1oo1(单通道)、1oo2(双通道)、2oo3(三取二) | 硬件选型与软件表决 |
| 安全停车(Safe Stop) | 受控减速至静止,保持制动,非直接断电 | 运动控制安全功能 |
| 故障安全(Fail-Safe) | 任何故障导向安全状态,而非危险状态 | 设计原则与验证 |
三、环境准备:15 分钟搭好安全 PLC 开发台
3.1 硬件清单
| 组件 | 型号/规格 | 安全作用 |
|---|---|---|
| 工业主板 | 研华 AIMB-587(Intel i7,支持 PREEMPT_RT) | 主控制器 |
| 安全输入模块 | 双通道安全继电器(强制导向结构) | 急停按钮、安全门 |
| 安全输出模块 | 双通道固态继电器 + 反馈回路 | 接触器、电机驱动 |
| 编码器 | 双旋转变压器(Resolver) | 位置反馈冗余 |
| 看门狗 | 外部硬件 WDT(MAX6814) | 软件死机检测 |
3.2 软件环境
| 组件 | 版本 | 安装命令 |
|---|---|---|
| 实时内核 | linux-5.15.y-rt | 见下文脚本 |
| 安全运行时 | Xenomai 3.2 | 硬实时补丁 |
| 逻辑引擎 | PLCopen Runtime(C 语言实现) | 源码编译 |
| 认证文档模板 | IEC 61508-3 软件模板 | Git 下载 |
3.3 一键安装脚本(可复制)
#!/bin/bash # setup_safety_plc.sh set -e # 1. 安装依赖 sudo apt update sudo apt install -y build-essential git libncurses-dev flex bison \ libssl-dev libelf-dev bc dwarves # 2. 下载并打 RT 补丁 KERNEL_VER=5.15.71 RT_PATCH=patch-5.15.71-rt53.patch.xz wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-${KERNEL_VER}.tar.xz wget https://cdn.kernel.org/pub/linux/kernel/projects/rt/${KERNEL_VER}/${RT_PATCH} tar -xf linux-${KERNEL_VER}.tar.xz && cd linux-${KERNEL_VER} xzcat ../${RT_PATCH} | patch -p1 # 3. 配置安全相关内核选项 make x86_64_defconfig ./scripts/config --enable CONFIG_PREEMPT_RT ./scripts/config --enable CONFIG_LOCKDEP # 锁依赖检测 ./scripts/config --enable CONFIG_DEBUG_SPINLOCK # 自旋锁调试 ./scripts/config --enable CONFIG_FAULT_INJECTION # 故障注入 make -j$(nproc) && sudo make modules_install install echo "实时内核编译完成,重启选择新内核"3.4 安全开发目录结构
mkdir -p ~/safety-plc/{src,docs,tests,cert} cd ~/safety-plc四、应用场景:机械臂安全控制系统
在 6 轴协作机械臂场景中,安全 PLC 需实现以下功能:
安全停车(Safe Stop 2):收到停止指令后,沿规划路径减速至静止,保持伺服使能,确保不偏离轨迹。
紧急停机(Emergency Stop):双通道急停按钮触发,立即切断电机动力,制动器抱闸,机械臂 200ms 内静止。
故障隔离:单轴编码器故障时,自动切换至双通道中的备用通道,记录故障并限制该轴速度至 10%,维持产线运行而非全停。
该系统需通过 TÜV 的 SIL 2 认证,PFDavg < 10⁻²,响应时间 < 50ms。
五、实际案例与步骤:从需求到验证
5.1 步骤 1 - 安全需求规范(SRS)
SRS-001 紧急停机功能
功能描述:双通道急停信号任一触发,立即执行安全停机 SIL 等级:SIL 2 触发条件:ESTOP1=0 OR ESTOP2=0 响应时间:≤ 50ms(从信号变化到电机断电) 故障检测:双通道不一致持续 100ms 报故障追溯矩阵(Excel/GitLab 管理):
| 需求 ID | 设计元素 | 代码文件 | 测试用例 |
|---|---|---|---|
| SRS-001 | ESTOP 双通道采集 | safety_input.c | TC-ESTOP-01 |
5.2 步骤 2 - 安全架构设计
硬件架构:1oo2D(双通道,诊断)
[急停按钮 NC] ----+ +---- [安全输入模块] ---- [CPU 双核锁步] [急停按钮 NC] ----+ ↑ ↓ [反馈回路] <-------------- [安全输出模块] ---- [接触器线圈]软件架构:
/* safety_arch.h */ #ifndef SAFETY_ARCH_H #define SAFETY_ARCH_H #include <stdint.h> #include <stdbool.h> /* 安全通道状态 */ typedef enum { SAFETY_CH_HEALTHY = 0, SAFETY_CH_FAULT_1, // 通道 1 故障 SAFETY_CH_FAULT_2, // 通道 2 故障 SAFETY_CH_DISAGREE // 双通道不一致 } SafetyChannelState; /* 安全 PLC 核心结构 */ typedef struct { uint32_t cycle_time_us; // 周期 1ms SafetyChannelState estop_state; // 急停状态 bool output_safe_state; // 输出安全状态 uint32_t diag_counter; // 诊断计数器 } SafetyPLC; /* 表决函数:双通道一致性检查 */ static inline bool safety_vote_1oo2(bool ch1, bool ch2, SafetyChannelState *state) { if (ch1 && ch2) { *state = SAFETY_CH_HEALTHY; return true; // 安全状态 } if (!ch1 && !ch2) { *state = SAFETY_CH_HEALTHY; return false; // 触发停机 } // 不一致,启动定时器检测 *state = SAFETY_CH_DISAGREE; return false; // 保守导向安全 } #endif5.3 步骤 3 - 安全功能实现
A. 双通道采集与诊断
/* safety_input.c */ #include "safety_arch.h" #include <time.h> #include <gpiod.h> // libgpiod for GPIO #define ESTOP_PIN_1 17 #define ESTOP_PIN_2 18 #define DISAGREE_TIMEOUT_US 100000 // 100ms struct gpiod_line *line_estop1, *line_estop2; int safety_input_init(void) { struct gpiod_chip *chip = gpiod_chip_open("/dev/gpiochip0"); line_estop1 = gpiod_chip_get_line(chip, ESTOP_PIN_1); line_estop2 = gpiod_chip_get_line(chip, ESTOP_PIN_2); gpiod_line_request_input(line_estop1, "estop1"); gpiod_line_request_input(line_estop2, "estop2"); return 0; } /* 周期调用:1ms */ SafetyChannelState safety_input_read(bool *safe_state) { static uint64_t disagree_start = 0; static bool last_disagree = false; bool val1 = gpiod_line_get_value(line_estop1); bool val2 = gpiod_line_get_value(line_estop2); /* 低电平有效(NC 接法)*/ bool active1 = !val1; bool active2 = !val2; SafetyChannelState state; *safe_state = safety_vote_1oo2(!active1, !active2, &state); /* 不一致检测 */ if (state == SAFETY_CH_DISAGREE) { uint64_t now = clock_gettime_us(CLOCK_MONOTONIC); if (!last_disagree) disagree_start = now; if ((now - disagree_start) > DISAGREE_TIMEOUT_US) { state = SAFETY_CH_FAULT_1; // 或 FAULT_2,需进一步诊断 } last_disagree = true; } else { last_disagree = false; } return state; }B. 安全输出与故障安全
/* safety_output.c */ #include "safety_arch.h" #include <gpiod.h> #define OUT_SAFE_PIN 23 #define OUT_FEEDBACK_PIN 24 struct gpiod_line *line_out, *line_fb; int safety_output_init(void) { struct gpiod_chip *chip = gpiod_chip_open("/dev/gpiochip0"); line_out = gpiod_chip_get_line(chip, OUT_SAFE_PIN); line_fb = gpiod_chip_get_line(chip, OUT_FEEDBACK_PIN); gpiod_line_request_output(line_out, "safe_out", 0); gpiod_line_request_input(line_fb, "feedback"); return 0; } /* 设置安全输出:true=运行,false=安全状态 */ int safety_output_set(bool run, uint32_t *diag) { /* 故障安全:任何异常导向 0(安全状态)*/ if (run) { /* 自检:读取反馈确认当前状态 */ int fb = gpiod_line_get_value(line_fb); if (fb != 0) { *diag |= DIAG_FEEDBACK_FAULT; gpiod_line_set_value(line_out, 0); // 强制安全 return -1; } } gpiod_line_set_value(line_out, run ? 1 : 0); /* 验证写入 */ int fb = gpiod_line_get_value(line_fb); if (fb != (run ? 1 : 0)) { *diag |= DIAG_WRITE_VERIFY_FAIL; return -1; } return 0; }C. 主循环与周期保证
/* safety_main.c */ #include "safety_arch.h" #include <pthread.h> #include <signal.h> #define SAFETY_CYCLE_US 1000 // 1ms 周期 static volatile bool running = true; void sigint_handler(int sig) { running = false; } /* 硬实时线程 */ void *safety_thread(void *arg) { SafetyPLC plc = {0}; plc.cycle_time_us = SAFETY_CYCLE_US; struct timespec next; clock_gettime(CLOCK_MONOTONIC, &next); while (running) { /* 周期开始时间戳 */ uint64_t t_start = clock_gettime_us(CLOCK_MONOTONIC); /* 读取输入 */ bool safe_state; plc.estop_state = safety_input_read(&safe_state); /* 安全逻辑处理 */ if (!safe_state || plc.estop_state != SAFETY_CH_HEALTHY) { plc.output_safe_state = false; // 进入安全状态 } else { /* 正常运行业务逻辑 */ plc.output_safe_state = true; } /* 写入输出 */ uint32_t diag = 0; if (safety_output_set(plc.output_safe_state, &diag) < 0) { plc.output_safe_state = false; // 输出故障,导向安全 } plc.diag_counter |= diag; /* 周期结束,计算 jitter */ uint64_t t_end = clock_gettime_us(CLOCK_MONOTONIC); int32_t jitter = (t_end - t_start) - SAFETY_CYCLE_US; /* 周期控制:绝对时间等待 */ next.tv_nsec += SAFETY_CYCLE_US * 1000; while (next.tv_nsec >= 1000000000) { next.tv_sec++; next.tv_nsec -= 1000000000; } clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL); } /* 退出时强制安全状态 */ safety_output_set(false, NULL); return NULL; } int main(void) { signal(SIGINT, sigint_handler); /* 初始化硬件 */ safety_input_init(); safety_output_init(); /* 创建实时线程 */ pthread_t tid; pthread_attr_t attr; struct sched_param param = { .sched_priority = 99 }; pthread_attr_init(&attr); pthread_attr_setschedpolicy(&attr, SCHED_FIFO); pthread_attr_setschedparam(&attr, ¶m); pthread_create(&tid, &attr, safety_thread, NULL); pthread_join(tid, NULL); return 0; }编译运行:
gcc -o safety_plc safety_main.c safety_input.c safety_output.c \ -lgpiod -pthread -O2 -Wall -Wextra sudo ./safety_plc # 需 root 设置实时优先级5.4 步骤 4 - 故障注入与验证
/* fault_inject.c - 用于测试的故障注入模块 */ #include <fcntl.h> void inject_estop1_stuck_at_1(void) { /* 模拟通道 1 粘连在高电平(危险故障)*/ system("echo 1 > /sys/kernel/debug/fault_inject/gpio17/force_value"); } void inject_estop_disagree(void) { /* 模拟双通道 50ms 不一致 */ system("echo toggle_50ms > /sys/kernel/debug/fault_inject/estop/mode"); }测试用例 TC-ESTOP-01:
| 步骤 | 操作 | 预期结果 | 实测 | 通过 |
|---|---|---|---|---|
| 1 | 正常状态,双通道高电平 | 系统运行 | ||
| 2 | 按下急停,双通道变低 | 50ms 内进入安全状态 | ||
| 3 | 恢复急停,双通道变高 | 系统恢复运行 | ||
| 4 | 注入通道 1 粘连故障 | 100ms 内检测到不一致,报故障,保持安全 |
5.5 步骤 5 - 认证文档包
docs/ ├── 00_SafetyPlan.md # 安全计划 ├── 01_SRS.md # 安全需求规范 ├── 02_SAD.md # 安全架构设计 ├── 03_SoftwareDesign.md # 软件设计 ├── 04_TestReport.md # 测试报告(含故障注入) ├── 05_FMEA.xlsx # 失效模式分析 ├── 06_TraceabilityMatrix.xlsx # 追溯矩阵 └── 07_UserManual.md # 用户手册(含安全警示)六、常见问题与解答(FAQ)
| 问题 | 现象 | 解决 |
|---|---|---|
| 周期抖动 > 100μs | 非实时任务抢占 | 隔离 CPU:isolcpus=2,3+ 绑定安全线程 |
| 双通道不一致误报 | 机械抖动导致 | 增加 10ms 硬件消抖,或软件滤波 |
| 故障注入后无法恢复 | 安全状态锁死 | 设计"故障清除"流程,需人工确认复位 |
| 认证机构要求代码覆盖率 100% | 部分防御代码难触发 | 使用 GCC--coverage+ 故障注入强制触发 |
| SIL 2 与 SIL 3 选型犹豫 | 成本与复杂度差异大 | 一般机械臂 SIL 2 足够,核电/汽车选 SIL 3 |
七、实践建议与最佳实践
防御式编程:所有分支默认导向安全状态,
else必须处理。周期监控:主循环内置 watchdog,连续 3 周期超时强制复位。
版本锁定:编译器、内核、库版本写入《安全配置清单》,升级需安全评估。
双人审查:安全相关代码必须第二人签字,Git 提交附审查记录。
现场可维护:保留诊断串口,支持不拆机读取故障记录。
持续认证:每年监督审核,每 3 年换证,文档与代码同步更新。
八、总结:一张脑图带走全部要点
实时 Linux 安全 PLC ├─ 标准:IEC 61508 / GB/T 20438 ├─ 架构:1oo2D 双通道 + 诊断 ├─ 功能:安全停车 / 紧急停机 / 故障隔离 ├─ 实现:Xenomai 硬实时 + 周期 1ms + 故障安全 ├─ 验证:故障注入 + 100% 分支覆盖 + 追溯矩阵 └─ 认证:TÜV / CQC 第三方发证掌握安全 PLC 实现,你就拥有了:
合规竞争力:国产替代方案的核心差异化能力
系统可靠性:从"能用"到"敢用"的质变
职业护城河:功能安全工程师认证(CFSE)的实战基础
立刻搭建你的安全 PLC 实验台,从"急停按钮双通道采集"开始,跑通第一个 SIL 2 功能——让工业现场因你的代码而更安全!
