阀门控制系统:开一个阀门到底有多复杂
这是「从零搭建工业控制系统」系列第11篇。上篇聊了权限控制,这篇讲阀门——工业系统里最频繁的操作对象,也是最容易出事的地方。
一个阀门引发的真空事故
第一次写阀门控制的时候,我想得太简单了。开阀门嘛,写个1到寄存器不就完了?
await _modbusService.WriteSingleCoilAsync(slaveAddress, address, true);
结果现场测试时,节流阀还没开到安全角度,闸阀就直接打开了。腔体瞬间从大气压冲到高真空,晶圆飞起来了。
问题出在没有联锁。闸阀打开前应该先确认节流阀已经开到指定角度,但我当时只写了一句WriteSingleCoil就完事。
后来重构了整个阀门执行器,加了预检查、联锁、前导动作、状态等待。一个"开阀门"的操作,背后有六七层逻辑。
阀门执行器架构
ValveCommandExecutor 是所有阀门操作的入口。核心方法 ExecuteAsync 接收阀门ID和动作(开/关):
public class ValveCommandExecutor
{
public async Task<CommandResult> ExecuteAsync(
ValveId valveId,
ValveAction action,
CancellationToken ct = default)
{
var config = _registry.GetValveConfig(valveId);
if (config == null)
return CommandResult.Fail("CONFIG_NOT_FOUND", $"未找到阀门配置: {valveId}");
var commandId = action == ValveAction.Open
? config.OpenCommandId
: config.CloseCommandId;
// 1. 切换阀互锁检查
if (action == ValveAction.Open)
{
var interlockResult = await CheckChangeoverInterlockAsync(valveId, ct);
// ...
}
// 2. 前置检查
var preChecks = action == ValveAction.Open
? config.OpenPreChecks
: config.ClosePreChecks;
var checkResult = await ExecutePreChecksAsync(preChecks, ct);
// 3. 前导动作
if (action == ValveAction.Open && config.OpenPreActions?.Count > 0)
{
var preActionResult = await ExecutePreActionsAsync(config, ct);
}
// 4. 关闭依赖:关阀门前先关其他阀门
if (action == ValveAction.Close && config.CloseDependencies.Count > 0)
{
var depResult = await ExecuteDependenciesAsync(config.CloseDependencies, ct);
}
// 5. 执行命令(单阀门或组合命令)
// 6. 等待状态变化
}
}
每一步失败都会中断整个流程,返回 CommandResult.Fail。

预检查:开门之前先看路
预检查(PreCheck)是阀门操作的安全门。比如开闸阀之前要确认腔体盖子关了(状态点位11),开真空阀之前要确认 foreline 状态正常(状态点位20)。
配置文件里这样写:
{
"valveId": "GateValve",
"openCommandId": "CMD_OPEN",
"openPreChecks": [
{
"type": "StatusProperty",
"statusProperty": "SP_01",
"expectedValue": 1,
"description": "腔体盖关闭"
}
]
}
执行时逐条检查:
private async Task<CommandResult> ExecutePreChecksAsync(
List<PreCheck> preChecks, CancellationToken ct)
{
foreach (var check in preChecks)
{
var actualValue = await ReadStatusAsync(check.StatusProperty, ct);
if (actualValue != check.ExpectedValue)
{
return CommandResult.Fail(
"PRE_CHECK_FAILED",
$"前置检查失败: {check.Description} (期望{check.ExpectedValue}, 实际{actualValue})");
}
}
return CommandResult.Success();
}
切换阀互锁:不能同时开
系统里有三个切换阀(Changer1、Changer2、Changer3),物理上不能同时打开——同时开会造成气路短路。
互锁逻辑在开阀门前先检查:

private async Task<CommandResult> CheckChangeoverInterlockAsync(
ValveId valveId, CancellationToken ct)
{
// 切换阀1和切换阀2不能同时开
// 切换阀3和切换阀1/2不能同时开
if (valveId == ValveId.Changer1)
{
var changer2Open = await IsValveOpenAsync(ValveId.Changer2, ct);
if (changer2Open)
return CommandResult.Fail("INTERLOCK", "切换阀2已打开,不能开切换阀1");
}
// ... 其他互锁组合
}
前导动作:开闸阀之前先开节流阀
最经典的场景:开闸阀之前,节流阀要先开到30度,否则气流冲击太大。
{
"openPreActions": [
{
"type": "Command",
"commandId": "CMD_THROTTLE_30",
"description": "节流阀开到30度"
},
{
"type": "Delay",
"milliseconds": 10000
}
]
}
执行顺序:下发节流阀30度指令 → 等待10秒 → 开闸阀 → 等闸阀状态确认 → 节流阀全开指令。

if (action == ValveAction.Open && config.OpenPreActions?.Count > 0)
{
var preActionResult = await ExecutePreActionsAsync(config, ct);
if (!preActionResult.IsSuccess)
return preActionResult;
}
两种执行模式:命令模式 vs 寄存器模式
阀门配置里有两种寄存器模式:
private async Task<CommandResult> ExecuteSingleValveCommandAsync(
ValveConfig config, ValveAction action,
Stopwatch sw, CancellationToken ct)
{
if (config.Register.IsCommandMode)
{
// 命令模式:通过命令索引执行
var cmdIndex = action == ValveAction.Open
? config.Register.OpenCmdIndex
: config.Register.CloseCmdIndex;
await ExecuteCommandByIndexAsync(config.Register.Type, cmdIndex.Value);
}
else
{
// 寄存器模式:直接写入寄存器
var writeValue = action == ValveAction.Open
? config.Register.OpenValue
: config.Register.CloseValue;
await WriteRegisterAsync(config.Register, writeValue);
}
// 等待状态变化
var expectedStatus = (int)action;
var waitResult = await WaitForStatusAsync(
config.GetStatusProperty(),
expectedStatus,
config.TimeoutMs,
ct);
if (waitResult)
return CommandResult.Success();
else
return CommandResult.Timeout($"阀门 {config.ValveId} 状态变化超时");
}
命令模式通过下发命令索引让下位机执行预设动作,寄存器模式直接写线圈值。前者更安全(下位机有自己的逻辑),后者更快。

状态等待:写了不代表成功
写完寄存器不代表阀门真的动了。必须等待状态反馈:
var waitResult = await WaitForStatusAsync(
config.GetStatusProperty(), // SP_01, SP_02...
expectedStatus, // 1=开, 0=关
config.TimeoutMs, // 超时时间
ct);
WaitForStatusAsync 内部轮询读取状态寄存器,直到值匹配或超时。超时就返回 CommandResult.Timeout。
调试模式下有个特殊处理——节流阀打开需要额外延迟:
if (Global.IsDevDebug)
{
if (config.ValveId == ValveId.ThrottleValve && action == ValveAction.Open)
{
_logger.Debug("调试模式,节流阀打开需要延迟1秒,否则会报:阀门状态变化超时");
await Task.Delay(1000, ct);
}
}
模拟器响应比真实硬件快,反而导致状态读取时序错位。这种坑只有踩过才知道。
关闭依赖:关我之前先关你
有些阀门关闭前需要先关闭下游阀门。比如关切换阀之前要先关所有依赖它的阀门:
if (action == ValveAction.Close && config.CloseDependencies.Count > 0)
{
var depResult = await ExecuteDependenciesAsync(config.CloseDependencies, ct);
if (!depResult.IsSuccess)
return depResult;
}
还有 CloseParents——关完当前阀门后顺带关父阀门:
if (singleResult.IsSuccess && action == ValveAction.Close
&& config.CloseParents?.Count > 0)
{
await ExecuteCloseParentsAsync(config.CloseParents, ct);
}
阀门踩坑清单
| 坑 | 现象 | 解决 |
|---|---|---|
| 不做联锁 | 切换阀同时开气路短路 | CheckChangeoverInterlockAsync |
| 不等状态 | 写了1但阀门没动 | WaitForStatusAsync轮询 |
| 不做前导 | 闸阀直接开气流冲击 | PreActions先开节流阀到30度 |
| 模拟器时序 | 节流阀状态变化超时 | 调试模式加1秒延迟 |
| 不关依赖 | 关切换阀后下游还通着 | CloseDependencies先关下游 |
本篇小结
| 知识点 | 关键做法 |
|---|---|
| 执行流程 | 互锁→预检查→前导动作→执行→状态等待 |
| 两种模式 | 命令模式(下位机执行) vs 寄存器模式(直接写) |
| 联锁 | 切换阀互斥,不能同时开 |
| 前导动作 | 开闸阀前先开节流阀到30度,等10秒 |
| 状态等待 | 轮询状态寄存器直到匹配或超时 |
| 关闭依赖 | 关阀门前先关下游阀门 |
阀门控制的核心不是"写值",而是"写值之前确认安全,写值之后确认生效"。
下期预告
第12篇:灯塔与蜂鸣器控制
阀门说完了,下篇看灯塔——蓝绿橙红四色LED加蜂鸣器,怎么根据系统状态自动控制。
:阀门控制系统——开一个阀门到底有多复杂&spm=1001.2101.3001.5002&articleId=163967024&d=1&t=3&u=342faea3564f431cbb03fefe8639fbe9)
1049

被折叠的 条评论
为什么被折叠?



