一、简介:为什么安全 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, &param);
    
    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

七、实践建议与最佳实践

  1. 防御式编程:所有分支默认导向安全状态,else 必须处理。

  2. 周期监控:主循环内置 watchdog,连续 3 周期超时强制复位。

  3. 版本锁定:编译器、内核、库版本写入《安全配置清单》,升级需安全评估。

  4. 双人审查:安全相关代码必须第二人签字,Git 提交附审查记录。

  5. 现场可维护:保留诊断串口,支持不拆机读取故障记录。

  6. 持续认证:每年监督审核,每 3 年换证,文档与代码同步更新。


八、总结:一张脑图带走全部要点

实时 Linux 安全 PLC
├─ 标准:IEC 61508 / GB/T 20438
├─ 架构:1oo2D 双通道 + 诊断
├─ 功能:安全停车 / 紧急停机 / 故障隔离
├─ 实现:Xenomai 硬实时 + 周期 1ms + 故障安全
├─ 验证:故障注入 + 100% 分支覆盖 + 追溯矩阵
└─ 认证:TÜV / CQC 第三方发证

掌握安全 PLC 实现,你就拥有了:

  • 合规竞争力:国产替代方案的核心差异化能力

  • 系统可靠性:从"能用"到"敢用"的质变

  • 职业护城河:功能安全工程师认证(CFSE)的实战基础

立刻搭建你的安全 PLC 实验台,从"急停按钮双通道采集"开始,跑通第一个 SIL 2 功能——让工业现场因你的代码而更安全!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐