【实时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; // 保守导向安全
}
#endif
5.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 功能——让工业现场因你的代码而更安全!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)