一套设备能在实验室里“跑起来”,不代表它已经具备可交付的软件架构。相机、运动轴、传感器和 PLC 接入之后,最容易出现的问题不是少一个按钮,而是职责散落:界面直接调用厂商 SDK,流程代码同时处理通信重连,控制器与上位机各自保存一份状态,安全动作甚至依赖 Windows 程序及时响应。
本文面向刚进入上位机或设备软件开发的工程师,先回答一个决定后续设计的问题:什么应该由上位机负责,什么必须留在下位机或独立安全链中? 示例以 C#/.NET 10 为背景,但职责划分并不依赖某一种 UI 框架或控制器品牌。半导体场景下的领域化展开(状态机、Recipe、SECS/GEM、晶圆厂集成)见《半导体设备软件(一):设备软件的整体架构》系列,本组只讲与行业无关的通用方法。
1. “上位机”和“下位机”不是按机器外壳划分
上位机通常运行在工业 PC 上,负责操作界面、流程编排、数据管理、诊断和对外集成;下位机可能是 PLC、运动控制器、嵌入式控制板或仪器固件,负责贴近硬件的采样和控制。但这只是常见形态,不是定义。
更可靠的划分方法是看四个约束:
| 约束 | 更适合放在哪里 | 原因 |
|---|---|---|
| 必须在确定的短周期内闭环 | 实时控制器或设备固件 | 桌面操作系统的调度延迟不具备硬实时保证 |
| 断网、进程崩溃后仍必须生效 | 下位机或独立安全回路 | 不能把安全依赖于上位机存活 |
| 经常变化且需要复杂业务规则 | 上位机 | 更适合版本管理、测试和快速迭代 |
| 需要大量存储、分析和人机交互 | 上位机 | 工业 PC 的算力、存储和 UI 能力更合适 |
因此,“电机移动到 125.0 mm”可以由上位机发起,但位置环通常在运动控制器内闭合;“检测到门打开后切断危险输出”也不应等待 WPF 事件处理程序执行。上位机可以展示联锁状态、阻止流程继续并记录证据,却不应成为唯一的安全屏障。
边界不是越靠下越好。把 Recipe 校验、批次追踪和复杂调度全部塞进 PLC,同样会让系统难以演进。目标是让每项职责位于能够满足其时序、安全和维护要求的最低复杂度层级。
2. 从五类职责看完整设备
可以先把系统按职责分成五层。它们是逻辑边界,不要求对应五个进程或五个项目:
| |
最重要的依赖方向是:流程依赖“设备能力”,而不是依赖某个品牌的 DLL。厂商适配层实现能力接口,并把厂商类型、线程模型和错误码限制在边界内。这样更换相机或增加模拟器时,流程层不需要认识新的 SDK。
UI 也不是控制核心。按钮应提交一个带权限和前置条件的意图,由应用层决定能否执行;设备状态则通过只读快照或事件投影给 UI。若点击事件里同时出现 MessageBox、数据库事务和 VendorAxis.Move(),说明边界已经被穿透。
3. 命令、状态和事实要分开
设备软件里经常混淆三类信息:
- 命令(Command) 表达“希望系统做什么”,例如开始运行或移动轴;命令可能被拒绝、取消或失败。
- 状态(State) 表达系统当前允许什么,例如
Ready、Running、Faulted;状态是经过规则解释后的模型。 - 事实(Observation/Event) 表达实际发生了什么,例如限位输入变为有效、控制器返回故障码、图像采集完成。
命令成功返回也不一定代表物理动作已经完成。某些 SDK 的 Move 只表示命令已进入控制器队列,真正完成需要等待到位信号,并同时检查伺服报警、限位和超时。适配层必须把这种厂商语义翻译成统一契约,不能让每条业务流程自行猜测。
状态也不能由 UI 随意写入。较稳健的数据流是:
| |
这条单向链路能避免“界面显示 Ready,但控制器仍在初始化”这样的双重事实源。
4. 进程边界与架构边界不是一回事
小型设备完全可以先采用模块化单体:一个进程中包含 UI、流程、设备抽象和适配器,通过明确的项目引用与接口维持边界。把每个模块拆成服务并不会自动获得可靠性,反而会引入网络故障、部署协调和分布式状态一致性问题。
只有当故障隔离、权限边界、独立升级或资源占用确有需要时,才值得引入进程边界。例如:
- 不稳定的 32 位厂商 SDK 无法加载进 64 位主进程,可以用独立进程承载;
- 图像处理需要独立 GPU 进程并允许单独重启;
- 控制服务必须在无人登录时作为 Windows Service 运行,而 UI 只是客户端;
- 多个客户端需要通过受控 API 访问同一台设备。
即使拆成多个进程,设备的写控制权也应有唯一所有者。两个进程同时向同一控制器下发命令,通常比单进程故障更难诊断。
5. 用 Generic Host 管理进程生命周期
.NET Generic Host 把依赖注入、配置、日志、后台服务和优雅停止组合在统一生命周期中。它既能承载 Worker Service,也能作为 WinForms/WPF 应用中的组合根。下面只展示骨架,具体设备接口将在下一篇展开:
| |
对于桌面 UI,通常由应用启动代码调用 StartAsync,在退出流程中调用 StopAsync 并释放 Host,而不是同时调用会阻塞至关闭的 RunAsync。还要注意,优雅停止不是崩溃恢复机制:断电、进程被强制终止时,StopAsync 可能没有机会执行,所以安全状态必须由控制器、驱动器或独立回路兜底。
6. 用场景验证边界
架构评审不要只看方框图,可以逐个追问失败场景:
- UI 无响应时,正在运行的轴由谁停止?
- 网线拔掉后,控制器继续执行还是进入安全状态?这个策略由谁定义?
- 上位机重启后,如何确认设备真实位置和当前物料,而不是加载一份过期缓存?
- Recipe 更新到一半失败,下一次运行使用哪个版本?
- 两个客户端同时发送启动命令,谁拥有控制权?
- 厂商 SDK 卡死时,能否隔离、超时或重启,而不破坏物理安全?
如果答案是“UI 会处理”“正常不会发生”或“重启试试”,说明职责边界还没有真正落地。边界的价值正是在非正常路径上决定谁负责检测、谁负责处置、谁保存证据。
总结
上位机适合承担变化快、数据多、交互复杂的业务;下位机适合承担贴近硬件、时序确定的控制;独立安全链负责不能依赖通用软件存活的保护。逻辑分层不等于强行拆进程,但设备的事实源和写控制权必须清楚。
下一篇将沿着“流程只依赖设备能力”的原则,设计硬件抽象层和厂商适配器,并处理单位、错误语义、线程模型与资源所有权这些最容易泄漏的细节。