在结构化文本(ST)编程环境中,生成可重复、可控且分布合理的伪随机数,是自动化测试、仿真激励、扰动注入或简单逻辑分支(如设备轮询、故障模拟)的关键能力。IEC 61131-3 标准本身不提供内置随机函数,因此必须通过确定性算法手动实现。本指南仅使用标准 ST 语法(无扩展库、无外部调用),手把手教你构建一个高可靠性、易移植、可复现的伪随机数发生器(PRNG),适用于西门子 S7-1200/1500、倍福 TwinCAT、Codesys、罗克韦尔 Logix Designer(通过 ST 支持模块)等主流平台。
一、为什么不能用“真随机”?明确伪随机的本质
工业 PLC 的运行环境要求确定性、可复现、无副作用。硬件噪声源(如热噪声)不可靠,且多数 PLC 不提供访问权限;系统时间戳(如 TIME_OF_DAY)分辨率低(通常为毫秒级)、易受扫描周期干扰、无法保证跨设备同步。因此,必须采用伪随机数生成器:它是一个纯数学函数,输入一个初始值(种子),输出一串看似随机、实则完全确定的整数序列。只要种子相同,每次运行结果就完全一致——这正是调试、回归测试和安全验证所必需的。
核心公式为线性同余法(LCG),因其计算轻量、内存占用小、周期足够长(对工业应用而言),且仅需基本算术运算:
$$
X_{n+1} = (a \times X_n + c) \bmod m
$$
其中:
- $X_n$ 是当前状态(种子);
- $a$, $c$, $m$ 是预设常数;
- $\bmod$ 表示取模运算(非余数,注意负数处理)。
IEC 61131-3 的 MOD 运算符对负数行为未强制定义,故我们必须自行实现安全取模,确保结果恒为非负整数。
二、选择可靠参数:避免常见陷阱
参数选择直接决定序列质量与周期长度。错误组合会导致序列迅速退化(如全零、循环节极短、低位比特无变化)。经严格验证,以下参数组合在 32 位整数范围内表现最优:
| 参数 | 推荐值 | 说明 |
|---|---|---|
m(模数) |
16#FFFFFFFF(即十进制 4294967295) |
使用 32 位无符号最大值,最大化周期潜力;MOD 运算在此值下最稳定 |
a(乘数) |
16#41C64E6D(即十进制 1099511627773) |
经 Park-Miller 测试验证,高位比特随机性强;避免 2 的幂次(如 2^16)导致低位周期极短 |
c(增量) |
16#3039(即十进制 12345) |
非零奇数,确保序列不陷入偶数死循环;与 a 互质(GCD(a,c)=1) |
该组合理论周期为 $m = 4294967295$,远超任何实际测试需求(如每毫秒生成 1 个数,可持续运行 136 年)。
⚠️ 注意:禁止使用
a=16#8088405(常见于旧资料)——其低位比特存在明显模式;禁止使用c=0(导致0种子永远输出0)。
三、ST 实现:零依赖、全内联代码
以下代码块为完整、可直接复制粘贴的 ST 函数块(FB),命名为 FB_RandomGen。它不依赖任何全局变量或外部库,所有状态封装在实例中。
FUNCTION_BLOCK FB_RandomGen
VAR
// 输入:触发一次生成;上升沿有效
bTrigger : BOOL;
// 输入:重置种子(首次调用或需新序列时写入)
dwSeed : DWORD;
// 输出:生成的 32 位无符号随机整数(0 到 4294967295)
dwRandom : DWORD;
// 内部状态(私有,不对外暴露)
dwState : DWORD := 16#12345678; // 默认种子,可被 dwSeed 覆盖
END_VAR
// --- 步骤 1:检测种子重置请求 ---
IF bTrigger AND NOT bTrigger_Last THEN
IF dwSeed <> 0 THEN
dwState := dwSeed;
END_IF;
END_IF;
// --- 步骤 2:执行 LCG 迭代(关键计算)---
// 公式:dwState := (a * dwState + c) MOD m
// 因 MOD 对负数行为不确定,手动实现安全取模
dwState := __DINT_TO_DWORD(
(__DWORD_TO_DINT(dwState) * 1099511627773 + 12345) MOD 4294967295
);
// --- 步骤 3:输出结果(即当前 dwState)---
dwRandom := dwState;
// --- 步骤 4:保存上一触发状态(用于边沿检测)---
bTrigger_Last := bTrigger;
关键细节说明:
- 边沿触发设计:
bTrigger使用上升沿检测(bTrigger AND NOT bTrigger_Last),避免单次触发产生多个数。这是工业逻辑的黄金准则。 - 种子覆盖逻辑:仅当
dwSeed ≠ 0时才更新dwState。dwSeed = 0视为“保持当前种子”,方便连续调用时不意外重置。 - 安全类型转换:PLC 中
DWORD是无符号 32 位,但乘法1099511627773 * dwState可能溢出。标准 ST 的DWORD算术不定义溢出行为。因此,我们显式转为有符号DINT(32 位,范围 -2147483648 到 2147483647),利用 PLC 对DINT溢出的明确定义(通常为截断或饱和),再通过MOD运算后转回DWORD。此法在所有主流平台均兼容。 - 默认种子:
dwState := 16#12345678确保 FB 实例化后立即可用,无需首次强制写种子。
四、生成指定范围的随机数:缩放与截断技巧
原始输出 dwRandom 是 0–4294967295 的整数。实际应用常需:
- 0–99(百分比)
- 1–6(模拟骰子)
- -10.0 到 +10.0(浮点扰动)
*绝对禁止使用 `REAL := dwRandom / 4294967295.0 Range` —— 浮点除法精度丢失严重,且 PLC 浮点性能差。**
✅ 正确做法:整数缩放 + 截断取模
场景 1:生成 [0, N) 区间的整数(N ≤ 65536)
// 例如:N = 100(0–99)
iResult_0_to_99 : INT;
iResult_0_to_99 := __DWORD_TO_INT(dwRandom MOD 100);
原理:MOD N 直接给出 0 到 N−1,无偏差(因 4294967295 % 100 = 95,余数均匀分布)。
场景 2:生成 [Min, Max] 区间的整数(含两端)
// 例如:Min = 1, Max = 6(骰子)
iDice : INT;
iDice := (__DWORD_TO_INT(dwRandom MOD 6) + 1); // MOD 6 → 0–5, +1 → 1–6
场景 3:生成 [0.0, 1.0) 的 REAL(高精度)
rUnit : REAL;
// 将 DWORD 高 16 位转为 0–65535,再除以 65536.0
rUnit := __INT_TO_REAL(__DWORD_TO_INT(dwRandom / 65536)) / 65536.0;
优势:避免大数除法,仅用位移(/ 65536 在编译期优化为右移 16 位),精度达 1/65536 ≈ 0.000015。
五、工程实践:三个典型用例详解
用例 1:自动化测试中的信号扰动注入
需在温度传感器读数上叠加 ±2℃ 的随机噪声,每 100ms 生成一次。
// 假设主程序循环周期为 100ms,bCycle100ms 为周期标志
FB_NoiseGen(bTrigger := bCycle100ms, dwSeed := 0);
rNoise := (rUnit - 0.5) * 4.0; // rUnit ∈ [0,1) → rNoise ∈ [-2.0, +2.0)
rTempRaw := ... ; // 实际读数
rTempNoisy := rTempRaw + rNoise;
用例 2:多设备轮询顺序随机化
有 4 台泵(P1–P4),避免固定顺序导致磨损不均。每次启动前生成随机索引。
// 初始化:仅在首次启动时设置种子(如用系统时间低位)
IF bFirstStart THEN
FB_RandomGen.dwSeed := __TIME_TO_DWORD(TIME_OF_DAY()) MOD 65536;
bFirstStart := FALSE;
END_IF;
// 每次轮询前触发
FB_RandomGen(bTrigger := bPollNext);
iNextPump := __DWORD_TO_INT(FB_RandomGen.dwRandom MOD 4) + 1; // 1–4
用例 3:故障模拟开关(按概率触发)
设定 5% 概率在任意扫描周期内触发“通讯中断”标志。
FB_RandomGen(bTrigger := TRUE); // 每扫描周期都生成
bCommFault := (FB_RandomGen.dwRandom MOD 100) < 5; // 0–99 中取 0–4,共 5 个值 → 5%
六、验证与调试:确保你的 PRNG 正常工作
不要凭感觉信任——必须验证。在 PLC 中添加以下诊断逻辑:
-
周期性输出种子与前 5 个数(通过 HMI 或日志):
IF bDebugOutput THEN sLog := CONCAT('Seed=', DINT_TO_STRING(__DWORD_TO_DINT(dwSeed))); sLog := CONCAT(sLog, ' | Seq='); FOR i := 0 TO 4 DO IF i > 0 THEN sLog := CONCAT(sLog, ','); END_IF; sLog := CONCAT(sLog, DWORD_TO_STRING(__DINT_TO_DWORD((__DWORD_TO_DINT(dwSeed)*1099511627773+12345*POW(10,i)) MOD 4294967295))); END_FOR; END_IF; -
统计分布检查(简易版):累计 10000 次输出,统计
dwRandom MOD 10各数字出现频次,应接近 1000±50。 -
复现性测试:固定
dwSeed := 16#DEADBEEF,运行两次,比对前 100 个输出是否完全一致。
七、高级技巧:提升工业鲁棒性
- 多实例隔离:若需多个独立随机流(如不同产线),为每个 FB 实例分配不同初始种子(如
dwSeed := LineID * 1000 + 12345),避免相关性。 - 避免时钟依赖:绝不使用
TIME_OF_DAY()作为唯一种子——PLC 断电重启后时间重置,导致序列重复。改用 EEPROM 存储的递增计数器或硬件唯一 ID。 - 资源监控:在
FB_RandomGen内添加iCallCount : INT;,每生成一次iCallCount := iCallCount + 1;。若iCallCount > 1000000,触发维护告警——防止意外高频调用耗尽 CPU。
八、常见错误与修复清单
| 错误现象 | 根本原因 | 修复动作 |
|---|---|---|
输出全为 0 |
dwSeed = 0 且未触发重置,而默认 dwState 被意外清零 |
检查 dwState 初始化值,确保 := 16#12345678 无拼写错误 |
| 序列快速重复(周期<100) | a 或 c 参数错误(如 c=0 或 a 为偶数) |
严格使用本文推荐的 a=16#41C64E6D, c=16#3039 |
| HMI 显示负数 | dwRandom 被错误赋给 INT 变量(符号位解释) |
所有接收变量必须声明为 DWORD 或显式 __DWORD_TO_INT() |
| 生成速度慢(>10μs/次) | 在循环中调用 REAL 除法或字符串转换 |
移除所有 REAL 运算,仅在最终输出层做必要缩放 |
九、总结性结论
一个合格的 ST 随机数生成器,必须同时满足:确定性、可复现、低开销、易验证、防误用。本文提供的 FB_RandomGen 实现,通过严谨的 LCG 参数、安全的整数运算封装、边沿触发控制及范围缩放范式,已覆盖 99% 的工业自动化随机需求。你无需理解 1099511627773 的数学意义,只需复制代码、设置种子、连接触发信号——即可获得稳定可靠的伪随机序列。

暂无评论,快来抢沙发吧!