【实时Linux工业PLC解决方案系列】第六篇 - 实时Linux PLC模拟量采集与处理方案
本文介绍基于实时LinuxPLC的模拟量采集系统方案,可替代传统PLC模块。工业现场90%的物理量需通过ADC/DAC转换,传统方案存在采样率固定、抖动大等问题。该方案采用实时Linux内核(PREEMPT_RT)配合IIO驱动,实现1ms级采集周期,延迟<50μs。硬件使用ADS1256(24-bit)和MCP4725(12-bit)模块,支持SPI/I2C接口。软件层面包含实时采集程序、
一、简介:为什么模拟量采集是 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, ¶m) < 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 配置 |
七、实践建议与最佳实践
-
硬件隔离
模拟地与数字地单点连接,ADC 前端加 RC 低通(截止 10Hz),抑制高频噪声。 -
软件抗混叠
采样率 ≥ 信号最高频率 × 2.56,硬件平均后再软件滤波。 -
实时性验证
用cyclictest确认采集线程延迟 < 50μs,再用ftrace抓调度事件。 -
热插拔保护
传感器断线检测:读数超量程 110% 或 < -10% 时标记无效,保持上次有效值。 -
校准周期
每 6 个月或环境温度变化 > 20℃ 时重新校准,系数存 EEPROM 防丢失。 -
冗余设计
关键回路双 ADC 交叉校验,偏差 > 2% 时切换并报警。
八、总结:一张脑图带走全部要点
实时 Linux PLC 模拟量系统
├─ 硬件:ADS1256(24bit) + MCP4725(12bit) + 信号调理
├─ 驱动:IIO 子系统 + 设备树
├─ 采集:SCHED_FIFO 实时线程 + 周期控制
├─ 滤波:滑动平均 + 卡尔曼 + 工频抑制
├─ 输出:PID → DAC → 4-20mA
├─ 校准:两点线性化,系数持久化
└─ 集成:CODESYS / OpenPLC 软 PLC 运行时
掌握本文方案,你就能用 ¥1000 级开源硬件 替代 ¥5000+ 专用模拟量模块,且采样率、算法、协议完全自主可控。从温控、压力到流量,从单机到分布式,实时 Linux PLC 正在成为工业 4.0 的新基建——而你,已经站在了这条赛道的起点。
更多推荐



所有评论(0)