设备软件架构与控制模型(一):上位机与下位机的职责边界

一套设备能在实验室里“跑起来”,不代表它已经具备可交付的软件架构。相机、运动轴、传感器和 PLC 接入之后,最容易出现的问题不是少一个按钮,而是职责散落:界面直接调用厂商 SDK,流程代码同时处理通信重连,控制器与上位机各自保存一份状态,安全动作甚至依赖 Windows 程序及时响应。

本文面向刚进入上位机或设备软件开发的工程师,先回答一个决定后续设计的问题:什么应该由上位机负责,什么必须留在下位机或独立安全链中? 示例以 C#/.NET 10 为背景,但职责划分并不依赖某一种 UI 框架或控制器品牌。半导体场景下的领域化展开(状态机、Recipe、SECS/GEM、晶圆厂集成)见《半导体设备软件(一):设备软件的整体架构》系列,本组只讲与行业无关的通用方法。

1. “上位机”和“下位机”不是按机器外壳划分

上位机通常运行在工业 PC 上,负责操作界面、流程编排、数据管理、诊断和对外集成;下位机可能是 PLC、运动控制器、嵌入式控制板或仪器固件,负责贴近硬件的采样和控制。但这只是常见形态,不是定义。

更可靠的划分方法是看四个约束:

约束更适合放在哪里原因
必须在确定的短周期内闭环实时控制器或设备固件桌面操作系统的调度延迟不具备硬实时保证
断网、进程崩溃后仍必须生效下位机或独立安全回路不能把安全依赖于上位机存活
经常变化且需要复杂业务规则上位机更适合版本管理、测试和快速迭代
需要大量存储、分析和人机交互上位机工业 PC 的算力、存储和 UI 能力更合适

因此,“电机移动到 125.0 mm”可以由上位机发起,但位置环通常在运动控制器内闭合;“检测到门打开后切断危险输出”也不应等待 WPF 事件处理程序执行。上位机可以展示联锁状态、阻止流程继续并记录证据,却不应成为唯一的安全屏障。

边界不是越靠下越好。把 Recipe 校验、批次追踪和复杂调度全部塞进 PLC,同样会让系统难以演进。目标是让每项职责位于能够满足其时序、安全和维护要求的最低复杂度层级。

2. 从五类职责看完整设备

可以先把系统按职责分成五层。它们是逻辑边界,不要求对应五个进程或五个项目:

1
2
3
4
5
6
7
8
9
操作与集成层       UI、权限、MES/上层系统接口
流程与领域层       状态机、任务编排、Recipe、业务规则
设备能力层         相机采集、轴运动、光源控制、传感器读取
厂商适配与通信层   SDK、串口、TCP、现场总线、错误码映射
控制器与安全层     实时闭环、硬联锁、安全 PLC、驱动器保护

最重要的依赖方向是:流程依赖“设备能力”,而不是依赖某个品牌的 DLL。厂商适配层实现能力接口,并把厂商类型、线程模型和错误码限制在边界内。这样更换相机或增加模拟器时,流程层不需要认识新的 SDK。

UI 也不是控制核心。按钮应提交一个带权限和前置条件的意图,由应用层决定能否执行;设备状态则通过只读快照或事件投影给 UI。若点击事件里同时出现 MessageBox、数据库事务和 VendorAxis.Move(),说明边界已经被穿透。

3. 命令、状态和事实要分开

设备软件里经常混淆三类信息:

  • 命令(Command) 表达“希望系统做什么”,例如开始运行或移动轴;命令可能被拒绝、取消或失败。
  • 状态(State) 表达系统当前允许什么,例如 ReadyRunningFaulted;状态是经过规则解释后的模型。
  • 事实(Observation/Event) 表达实际发生了什么,例如限位输入变为有效、控制器返回故障码、图像采集完成。

命令成功返回也不一定代表物理动作已经完成。某些 SDK 的 Move 只表示命令已进入控制器队列,真正完成需要等待到位信号,并同时检查伺服报警、限位和超时。适配层必须把这种厂商语义翻译成统一契约,不能让每条业务流程自行猜测。

状态也不能由 UI 随意写入。较稳健的数据流是:

1
2
3
4
5
操作意图 → 命令处理器 → 设备能力接口 → 适配器/控制器
                      观测值与结果事件
                         状态投影 → UI

这条单向链路能避免“界面显示 Ready,但控制器仍在初始化”这样的双重事实源。

4. 进程边界与架构边界不是一回事

小型设备完全可以先采用模块化单体:一个进程中包含 UI、流程、设备抽象和适配器,通过明确的项目引用与接口维持边界。把每个模块拆成服务并不会自动获得可靠性,反而会引入网络故障、部署协调和分布式状态一致性问题。

只有当故障隔离、权限边界、独立升级或资源占用确有需要时,才值得引入进程边界。例如:

  • 不稳定的 32 位厂商 SDK 无法加载进 64 位主进程,可以用独立进程承载;
  • 图像处理需要独立 GPU 进程并允许单独重启;
  • 控制服务必须在无人登录时作为 Windows Service 运行,而 UI 只是客户端;
  • 多个客户端需要通过受控 API 访问同一台设备。

即使拆成多个进程,设备的写控制权也应有唯一所有者。两个进程同时向同一控制器下发命令,通常比单进程故障更难诊断。

5. 用 Generic Host 管理进程生命周期

.NET Generic Host 把依赖注入、配置、日志、后台服务和优雅停止组合在统一生命周期中。它既能承载 Worker Service,也能作为 WinForms/WPF 应用中的组合根。下面只展示骨架,具体设备接口将在下一篇展开:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddSingleton<IMachineController, MachineController>();
builder.Services.AddSingleton<IAxis, SimulatedAxis>();
builder.Services.AddHostedService<DeviceSupervisor>();

using IHost host = builder.Build();
await host.RunAsync();

对于桌面 UI,通常由应用启动代码调用 StartAsync,在退出流程中调用 StopAsync 并释放 Host,而不是同时调用会阻塞至关闭的 RunAsync。还要注意,优雅停止不是崩溃恢复机制:断电、进程被强制终止时,StopAsync 可能没有机会执行,所以安全状态必须由控制器、驱动器或独立回路兜底。

6. 用场景验证边界

架构评审不要只看方框图,可以逐个追问失败场景:

  1. UI 无响应时,正在运行的轴由谁停止?
  2. 网线拔掉后,控制器继续执行还是进入安全状态?这个策略由谁定义?
  3. 上位机重启后,如何确认设备真实位置和当前物料,而不是加载一份过期缓存?
  4. Recipe 更新到一半失败,下一次运行使用哪个版本?
  5. 两个客户端同时发送启动命令,谁拥有控制权?
  6. 厂商 SDK 卡死时,能否隔离、超时或重启,而不破坏物理安全?

如果答案是“UI 会处理”“正常不会发生”或“重启试试”,说明职责边界还没有真正落地。边界的价值正是在非正常路径上决定谁负责检测、谁负责处置、谁保存证据。

总结

上位机适合承担变化快、数据多、交互复杂的业务;下位机适合承担贴近硬件、时序确定的控制;独立安全链负责不能依赖通用软件存活的保护。逻辑分层不等于强行拆进程,但设备的事实源和写控制权必须清楚。

下一篇将沿着“流程只依赖设备能力”的原则,设计硬件抽象层和厂商适配器,并处理单位、错误语义、线程模型与资源所有权这些最容易泄漏的细节。

参考资料

Licensed under CC BY-NC-SA 4.0