一、简介:为什么模拟量采集是 PLC 的核心能力?

  • 工业现场 90% 的物理量是模拟量:温度(4-20mA)、压力(0-10V)、流量(脉冲转模拟)、液位(电阻信号)……这些连续变化的信号必须被精准采集,才能形成闭环控制。

  • 痛点

    • 传统 PLC 采集频率固定(通常 100ms),无法适应高速变化过程;

    • 工控机 + 普通 Linux 抖动 > 10ms,导致 PID 输出震荡;

    • 量程未校准、未滤波,现场数据跳变引发误报警。

  • 实时 Linux PLC 优势

    • 采集周期可配置到 1ms 甚至 100μs;

    • 内核态驱动 + 用户态算法,延迟确定性 < 50μs;

    • 软件定义滤波算法,灵活替换,无需换硬件。

掌握本文方案,你就能替代西门子 S7-1200 模拟量模块,用开源栈搭建符合 IEC 61131-3 的 SoftPLC。


二、核心概念:6 个关键词先搞懂

关键词 一句话说明 本文出现场景
ADC 模数转换器,将电压/电流转为数字量 16-bit ADS1115、24-bit ADS1256 驱动
DAC 数模转换器,将数字量转为电压/电流输出 12-bit MCP4725、16-bit DAC8552
采样率 每秒采集次数,单位 SPS(Samples Per Second) 配置 860 SPS ~ 30k SPS trade-off
量化误差 ADC 分辨率限制导致的固有误差 16-bit 满量程 10V → 误差 ±0.15mV
数字滤波 软件算法消除噪声:滑动平均、卡尔曼、中值滤波 50Hz 工频干扰抑制
量程校准 两点校准(零点/满点)消除增益漂移 y = k*x + b 系数烧录 EEPROM

三、环境准备:15 分钟搭好实验台

3.1 硬件清单

组件 型号/规格 数量 参考价格
工业主板 x86_64 4核 + 实时内核 1 ¥800
ADC 模块 ADS1115(I2C,16-bit,4CH) 2 ¥30/片
高速 ADC ADS1256(SPI,24-bit,8CH) 1 ¥150
DAC 模块 MCP4725(I2C,12-bit,2CH) 2 ¥20/片
信号调理 4-20mA 转 0-3.3V 模块 4 ¥25/路
传感器 PT100 温度探头(0-200℃) 2 ¥50/支
压力变送器 0-1MPa,4-20mA 输出 1 ¥200

3.2 软件栈

层级 组件 版本
内核 PREEMPT_RT 5.15 linux-image-5.15.0-rt
驱动 Industrial I/O (IIO) 子系统 内核自带
libiio 0.24
运行时 Xenomai 3 / POSIX RT 可选
PLC 运行时 CODESYS Runtime / OpenPLC 3.5.19 / 1.0

3.3 一键安装脚本(可复制)

#!/bin/bash
# setup_ai_ao.sh
set -e

echo "=== 安装实时内核 ==="
sudo apt update
sudo apt install -y linux-image-5.15.0-rt-amd64 linux-headers-5.15.0-rt-amd64

echo "=== 安装 Industrial I/O ==="
sudo apt install -y libiio-dev libiio-utils iiod

echo "=== 启用 IIO 设备用户访问 ==="
sudo usermod -aG iio $USER

echo "=== 安装 Python 绑定 ==="
pip3 install pylibiio numpy scipy

echo "=== 重启后生效 ==="
sudo reboot

四、应用场景:智能温控系统

场景描述:某化工厂反应釜温度控制,要求:

  • 8 路 PT100 温度采集,精度 ±0.1℃;

  • 2 路 4-20mA 调节阀输出,控制蒸汽流量;

  • 采样周期 10ms,超温 150℃ 时 50ms 内切断加热;

  • 全年无故障运行,MTBF > 8760 小时。

方案架构

PT100 → 信号调理 → ADS1256 (SPI) → 实时 Linux (PREEMPT_RT)
                                              ↓
CODESYS PID → MCP4725 (I2C) → 4-20mA 输出 → 调节阀
                                              ↓
                    HMI (Modbus TCP) ← 报警/趋势记录

关键优化点:

  • ADS1256 配置为 30k SPS,硬件平均 4 次,有效分辨率 22-bit;

  • 软件 10 点滑动平均 + 卡尔曼滤波,抑制 50Hz 工频;

  • 量程校准:冰水浴 0℃、沸水 100℃ 两点标定。


五、实际案例与步骤:从零搭建 AI/AO 系统

5.1 步骤 1:硬件连接与设备树配置

ADS1256 SPI 接线(BCM2835 编号):

ADS1256 GPIO
SCLK GPIO11 (SCLK)
DIN GPIO10 (MOSI)
DOUT GPIO9 (MISO)
CS GPIO8 (CE0)
DRDY GPIO25
PDWN GPIO24

设备树覆盖(DTS)

// ads1256-overlay.dts
/dts-v1/;
/plugin/;

/ {
    compatible = "brcm,bcm2835";
    fragment@0 {
        target = <&spi0>;
        __overlay__ {
            status = "okay";
            spidev@0 { status = "disabled"; };
            ads1256@0 {
                compatible = "ti,ads1256";
                reg = <0>;
                spi-max-frequency = <1953000>;
                interrupts = <25 2>;  /* DRDY, falling edge */
                interrupt-parent = <&gpio>;
                #address-cells = <1>;
                #size-cells = <0>;
                vref-supply = <&vdd_5v0_reg>;
            };
        };
    };
};

编译并启用:

sudo dtc -I dts -O dtb -o /boot/overlays/ads1256.dtbo ads1256-overlay.dts
echo "dtoverlay=ads1256" | sudo tee -a /boot/config.txt
sudo reboot

验证:

ls /sys/bus/iio/devices/iio:device*/name
# 应显示 ads1256

5.2 步骤 2:实时采集程序(C + libiio)

/* ai_realtime.c - 实时模拟量采集 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sched.h>
#include <time.h>
#include <iio.h>

#define RT_PRIORITY 99
#define SAMPLE_RATE_HZ 100   /* 100 Hz = 10ms 周期 */
#define BUFFER_SIZE 100

static struct iio_context *ctx;
static struct iio_device *dev;
static struct iio_channel *ch[8];

void set_realtime(void) {
    struct sched_param param = { .sched_priority = RT_PRIORITY };
    if (sched_setscheduler(0, SCHED_FIFO, &param) < 0) {
        perror("sched_setscheduler");
        exit(1);
    }
}

double read_channel(int idx) {
    long long raw;
    iio_channel_attr_read_longlong(ch[idx], "raw", &raw);
    
    /* 量程转换:24-bit 补码 → 电压 → 温度(PT100 查表或线性) */
    double voltage = (raw * 5.0) / (1 << 23);  /* ADS1256 满量程 ±5V */
    double temp = voltage * 50.0 - 50.0;         /* 0-10V 对应 -50~200℃ */
    return temp;
}

int main(int argc, char **argv) {
    set_realtime();
    
    /* 连接本地 IIO 守护进程 */
    ctx = iio_create_default_context();
    dev = iio_context_find_device(ctx, "ads1256");
    if (!dev) { fprintf(stderr, "ADS1256 not found\n"); return 1; }
    
    /* 启用 8 个通道 */
    for (int i = 0; i < 8; i++) {
        ch[i] = iio_device_find_channel(dev, i, false);
        iio_channel_enable(ch[i]);
    }
    
    /* 配置采样率 */
    iio_device_attr_write_longlong(dev, "sampling_frequency", 30000);
    
    /* 创建缓冲区 */
    struct iio_buffer *buf = iio_device_create_buffer(dev, BUFFER_SIZE, false);
    
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    
    while (1) {
        /* 严格周期控制 */
        ts.tv_nsec += 1000000000 / SAMPLE_RATE_HZ;
        if (ts.tv_nsec >= 1000000000) {
            ts.tv_sec++;
            ts.tv_nsec -= 1000000000;
        }
        clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &ts, NULL);
        
        /* 采集 */
        iio_buffer_refill(buf);
        double temp[8];
        for (int i = 0; i < 8; i++) {
            temp[i] = read_channel(i);
            printf("CH%d: %.3f℃ ", i, temp[i]);
        }
        printf("\n");
        
        /* 超温报警 */
        for (int i = 0; i < 8; i++) {
            if (temp[i] > 150.0) {
                fprintf(stderr, "ALARM: CH%d overtemp %.1f℃\n", i, temp[i]);
                /* 触发 AO 切断或 GPIO 继电器 */
            }
        }
    }
    
    iio_buffer_destroy(buf);
    iio_context_destroy(ctx);
    return 0;
}

编译运行:

gcc ai_realtime.c -o ai_realtime -liio -lm -pthread
sudo chrt -f 99 ./ai_realtime

5.3 步骤 3:数字滤波算法(滑动平均 + 卡尔曼)

# filter.py - 实时滤波模块
import numpy as np
from collections import deque

class MovingAverage:
    """滑动平均滤波"""
    def __init__(self, window=10):
        self.buf = deque(maxlen=window)
    
    def update(self, value):
        self.buf.append(value)
        return sum(self.buf) / len(self.buf)

class KalmanFilter:
    """一维卡尔曼滤波,抑制随机噪声"""
    def __init__(self, process_variance=1e-5, measurement_variance=0.1**2):
        self.q = process_variance    # 过程噪声
        self.r = measurement_variance  # 测量噪声
        self.x = 0.0                 # 估计值
        self.p = 1.0                 # 估计误差协方差
        self.k = 0.0                 # 卡尔曼增益
    
    def update(self, measurement):
        # 预测
        self.p += self.q
        
        # 更新
        self.k = self.p / (self.p + self.r)
        self.x += self.k * (measurement - self.x)
        self.p = (1 - self.k) * self.p
        
        return self.x

# 使用示例
if __name__ == "__main__":
    import random
    
    kf = KalmanFilter()
    ma = MovingAverage(window=5)
    
    # 模拟带噪声的 50Hz 工频干扰信号
    for t in range(100):
        true_val = 25.0 + 5 * np.sin(2 * np.pi * 50 * t / 1000)
        noise = random.gauss(0, 2.0)  # 2℃ 标准差噪声
        raw = true_val + noise
        
        filtered_kf = kf.update(raw)
        filtered_ma = ma.update(raw)
        
        print(f"t={t:3d} raw={raw:6.2f} KF={filtered_kf:6.2f} MA={filtered_ma:6.2f}")

5.4 步骤 4:模拟量输出(AO)控制

/* ao_control.c - 实时模拟量输出 */
#include <stdio.h>
#include <iio.h>
#include <math.h>

#define DAC_RESOLUTION 4096  /* 12-bit MCP4725 */

struct iio_device *dac_dev;

void set_output(int channel, double mA) {
    /* 4-20mA 对应 0-3.3V → 0-4095 */
    int raw = (int)((mA - 4.0) / 16.0 * DAC_RESOLUTION);
    if (raw < 0) raw = 0;
    if (raw > DAC_RESOLUTION - 1) raw = DAC_RESOLUTION - 1;
    
    struct iio_channel *ch = iio_device_find_channel(dac_dev, channel, true);
    iio_channel_attr_write_longlong(ch, "raw", raw);
}

int main() {
    struct iio_context *ctx = iio_create_default_context();
    dac_dev = iio_context_find_device(ctx, "mcp4725");
    
    /* PID 输出 → 4-20mA 调节阀 */
    double pid_output = 12.5;  /* 示例:50% 开度 = 12mA */
    set_output(0, pid_output);
    
    iio_context_destroy(ctx);
    return 0;
}

5.5 步骤 5:量程校准程序

# calibration.py - 两点校准
import json

class Calibrator:
    def __init__(self):
        self.zero_raw = None   # 零点原始值(如 0℃)
        self.span_raw = None   # 满点原始值(如 100℃)
        self.k = 1.0           # 增益系数
        self.b = 0.0           # 偏移量
    
    def calibrate_zero(self, raw_value, true_value):
        self.zero_raw = raw_value
        self.b = true_value
    
    def calibrate_span(self, raw_value, true_value):
        self.span_raw = raw_value
        self.k = (true_value - self.b) / (raw_value - self.zero_raw)
    
    def convert(self, raw):
        return self.k * (raw - self.zero_raw) + self.b
    
    def save(self, filename):
        with open(filename, 'w') as f:
            json.dump({
                'zero_raw': self.zero_raw,
                'span_raw': self.span_raw,
                'k': self.k,
                'b': self.b
            }, f)
    
    def load(self, filename):
        with open(filename) as f:
            data = json.load(f)
            self.__dict__.update(data)

# 使用:冰水浴 0℃ 和沸水 100℃ 标定
cal = Calibrator()
cal.calibrate_zero(raw_zero=524288, true_value=0.0)    # ADS1256 24-bit 中点
cal.calibrate_span(raw_span=786432, true_value=100.0)  # 对应 100℃
cal.save('pt100_cal.json')

# 运行时加载
cal.load('pt100_cal.json')
temp = cal.convert(raw_reading)

六、常见问题与解答(FAQ)

问题 现象 解决
采集数据跳变剧烈 50Hz 工频干扰 硬件:屏蔽双绞线;软件:滑动平均 + 卡尔曼
实时任务延迟 > 1ms 未用 SCHED_FIFO chrt -f 99 或代码内 sched_setscheduler
ADS1256 读取失败 SPI 速率过高 降速到 1.9MHz,检查 DRDY 中断接线
4-20mA 输出不准 负载电阻不匹配 确保回路电阻 250Ω(4-20mA → 1-5V)
校准后仍有偏差 传感器非线性 改用多点分段线性化或查表法
CODESYS 无法识别 IIO 驱动未注册 检查 iiod 服务,/etc/iiod.ini 配置

七、实践建议与最佳实践

  1. 硬件隔离
    模拟地与数字地单点连接,ADC 前端加 RC 低通(截止 10Hz),抑制高频噪声。

  2. 软件抗混叠
    采样率 ≥ 信号最高频率 × 2.56,硬件平均后再软件滤波。

  3. 实时性验证
    cyclictest 确认采集线程延迟 < 50μs,再用 ftrace 抓调度事件。

  4. 热插拔保护
    传感器断线检测:读数超量程 110% 或 < -10% 时标记无效,保持上次有效值。

  5. 校准周期
    每 6 个月或环境温度变化 > 20℃ 时重新校准,系数存 EEPROM 防丢失。

  6. 冗余设计
    关键回路双 ADC 交叉校验,偏差 > 2% 时切换并报警。


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

实时 Linux PLC 模拟量系统
├─ 硬件:ADS1256(24bit) + MCP4725(12bit) + 信号调理
├─ 驱动:IIO 子系统 + 设备树
├─ 采集:SCHED_FIFO 实时线程 + 周期控制
├─ 滤波:滑动平均 + 卡尔曼 + 工频抑制
├─ 输出:PID → DAC → 4-20mA
├─ 校准:两点线性化,系数持久化
└─ 集成:CODESYS / OpenPLC 软 PLC 运行时

掌握本文方案,你就能用 ¥1000 级开源硬件 替代 ¥5000+ 专用模拟量模块,且采样率、算法、协议完全自主可控。从温控、压力到流量,从单机到分布式,实时 Linux PLC 正在成为工业 4.0 的新基建——而你,已经站在了这条赛道的起点。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐