上位机分层架构设计

上位机分层架构设计

C# 上位机开发 - 上位机分层架构设计(HAL+协议层+业务层 一篇搞定 建议收藏)

作者:萨达大 | 标签:C#、上位机、分层架构、HAL、Modbus
导读:很多新手把SerialPort写在按钮事件里,换一台 PLC 就要改几十个窗体。本文用四层架构把 UI、业务、协议、硬件拆开,并给出可复制的接口与完整示例代码。


一、为什么上位机必须分层?

现场设备千差万别:串口 RS485、以太网 Modbus TCP、自定义 ASCII 帧……界面却要统一展示温度、压力、报警灯。若不分层:

  • 改协议:全项目搜0x03、改 N 个窗体。
  • 改硬件:USB 转串口换品牌,驱动层到处补丁。
  • 测试困难:没有 Mock,单元测试只能连真机。

工业软件常见做法是:UI 只谈业务语义,不谈字节流。下面四层就是 WinForms/WPF 上位机最常用的一种切分。


二、四层架构总览

层次职责典型类/接口禁止做的事
UI 层展示、交互、简单校验Form、BindingSource直接操作SerialPort、拼 Modbus 帧
业务层设备语义、轮询策略、报警逻辑DeviceService解析 CRC、管理 Socket 生命周期细节
协议层帧编解码、事务(读/写寄存器)IProtocolClientModbusProtocolClient关心 TextBox 刷新
HAL 层字节收发、连接、超时IDeviceDriverHalSerialPort理解「温度」含义

2.1 依赖方向(自上而下)

UI 层 WinForms/WPF

业务层 DeviceService

协议层 ModbusProtocolClient

HAL HalSerialPort / TcpClient

物理设备

ASCII 等价示意:

[ Form 界面 ] --> [ DeviceService ] --> [ ModbusProtocolClient ] --> [ HalSerialPort ]

铁律:HAL 不知道 Modbus;协议层不知道 Grid 有几列;UI 不知道寄存器地址 0x1000 还是 40001。


三、推荐项目结构

UpperComputer.sln ├── UpperComputer.Core │ ├── Abstractions/IDeviceDriver.cs │ ├── Abstractions/IProtocolClient.cs │ └── Models/DeviceTag.cs ├── UpperComputer.HAL │ └── Serial/HalSerialPort.cs ├── UpperComputer.Protocol │ └── Modbus/ModbusProtocolClient.cs ├── UpperComputer.Business │ └── Services/DeviceService.cs └── UpperComputer.UI.WinForms └── MainForm.cs

类库之间引用:UI → Business → Protocol → HAL → Core禁止反向引用


四、核心接口设计

4.1 IDeviceDriver(HAL)

硬件抽象只表达「连接 + 收发字节 + 状态」,与 Modbus 无关。

namespaceUpperComputer.Core.Abstractions;publicinterfaceIDeviceDriver:IAsyncDisposable{boolIsConnected{get;}TaskConnectAsync(CancellationTokencancellationToken=default);TaskDisconnectAsync(CancellationTokencancellationToken=default);Task<byte[]>TransceiveAsync(byte[]request,inttimeoutMs,CancellationTokencancellationToken=default);}

设计要点:

  • TransceiveAsync:请求-响应模型,适合 Modbus RTU/TCP;纯推送流可另加ReadAvailableAsync
  • 超时在 HAL 实现:协议层只关心「这次调用是否在时限内返回」。
  • IAsyncDisposable:窗体关闭、服务停止时统一释放端口。

4.2 IProtocolClient(协议层)

namespaceUpperComputer.Core.Abstractions;publicinterfaceIProtocolClient{Task<ushort[]>ReadHoldingRegistersAsync(byteslaveId,ushortstartAddress,ushortcount,CancellationTokenct=default);TaskWriteSingleRegisterAsync(byteslaveId,ushortaddress,ushortvalue,CancellationTokenct=default);}

业务层调用的是「读保持寄存器」,而不是「发 8 字节」。


五、HAL 实现:HalSerialPort

usingSystem.IO.Ports;usingUpperComputer.Core.Abstractions;namespaceUpperComputer.HAL.Serial;publicsealedclassHalSerialPort:IDeviceDriver{privatereadonlySerialPort_port;publicboolIsConnected=>_port.IsOpen;publicHalSerialPort(stringportName,intbaudRate=9600){_port=newSerialPort(portName,baudRate,Parity.Even,8,StopBits.One){ReadTimeout=500,WriteTimeout=500};}publicTaskConnectAsync(CancellationTokencancellationToken=default){if(!_port.IsOpen)_port.Open();returnTask.CompletedTask;}publicTaskDisconnectAsync(CancellationTokencancellationToken=default){if(_port.IsOpen)_port.Close();returnTask.CompletedTask;}publicasyncTask<byte[]>TransceiveAsync(byte[]request,inttimeoutMs,CancellationTokencancellationToken=default){_port.Write(request,0,request.Length);varbuffer=newbyte[256];vardeadline=DateTime.UtcNow.AddMilliseconds(timeoutMs);usingvarms=newMemoryStream();while(DateTime.UtcNow<deadline){cancellationToken.ThrowIfCancellationRequested();varn=awaitTask.Run(()=>_port.Read(buffer,0,buffer.Length),cancellationToken);if(n>0)ms.Write(buffer,0,n);if(n>0&&ms.Length>=5)break;// 简化:Modbus RTU 最小响应awaitTask.Delay(10,cancellationToken);}returnms.ToArray();}publicasyncValueTaskDisposeAsync(){awaitDisconnectAsync();_port.Dispose();}}

现场提示:RTU 需处理「帧间隔」「3.5 字符时间」;量产代码建议独立ModbusRtuTransceiver,HAL 只保留Write/Read


六、协议层:ModbusProtocolClient(节选)

usingUpperComputer.Core.Abstractions;namespaceUpperComputer.Protocol.Modbus;publicsealedclassModbusProtocolClient:IProtocolClient{privatereadonlyIDeviceDriver_driver;publicModbusProtocolClient(IDeviceDriverdriver)=>_driver=driver;publicasyncTask<ushort[]>ReadHoldingRegistersAsync(byteslaveId,ushortstartAddress,ushortcount,CancellationTokenct=default){varpdu=BuildReadHoldingRegistersPdu(slaveId,startAddress,count);varframe=ModbusRtuFrame.Wrap(pdu);// CRC16varresponse=await_driver.TransceiveAsync(frame,1000,ct);returnModbusRtuFrame.ParseRegisters(response,count);}publicTaskWriteSingleRegisterAsync(byteslaveId,ushortaddress,ushortvalue,CancellationTokenct=default){varpdu=BuildWriteSingleRegisterPdu(slaveId,address,value);varframe=ModbusRtuFrame.Wrap(pdu);return_driver.TransceiveAsync(frame,1000,ct).ContinueWith(_=>Array.Empty<byte>(),ct);}// BuildPdu / CRC 实现见 Modbus 通信专题,此处省略细节privatestaticbyte[]BuildReadHoldingRegistersPdu(byteslaveId,ushortstart,ushortcount)=>Array.Empty<byte>();}

协议层集中处理:功能码、异常码、字节序(Big-Endian),业务层拿到的已是ushort[]


七、业务层:DeviceService

usingUpperComputer.Core.Abstractions;namespaceUpperComputer.Business.Services;publicsealedclassDeviceService{privatereadonlyIProtocolClient_protocol;publiceventAction<double>?TemperatureChanged;publicDeviceService(IProtocolClientprotocol)=>_protocol=protocol;publicasyncTaskPollTemperatureAsync(CancellationTokenct=default){varregs=await_protocol.ReadHoldingRegistersAsync(1,0,1,ct);varraw=regs[0];vartemp=raw/10.0;// 工程量换算放在业务层TemperatureChanged?.Invoke(temp);}}

业务层负责:从寄存器到物理量、报警阈值、轮询间隔策略。同一协议客户端可被多个 Service 组合(温湿度、电机状态)。


八、WinForms 集成示例

publicpartialclassMainForm:Form{privatereadonlyDeviceService_service;privatereadonlyCancellationTokenSource_cts=new();publicMainForm(DeviceServiceservice){_service=service;_service.TemperatureChanged+=t=>BeginInvoke(()=>lblTemp.Text=$"{t:F1}°C");}privateasyncvoidMainForm_Load(objectsender,EventArgse){await_service.ConnectIfNeededAsync();// 可在 Service 内封装 Driver.Connect_=Task.Run(async()=>{while(!_cts.IsCancellationRequested){await_service.PollTemperatureAsync(_cts.Token);awaitTask.Delay(500,_cts.Token);}},_cts.Token);}protectedoverridevoidOnFormClosed(FormClosedEventArgse){_cts.Cancel();base.OnFormClosed(e);}}

窗体只做:订阅事件、BeginInvoke 刷新控件。若换 WPF,业务层DeviceService可原样复用。


九、分层与测试

测试对象方式
协议层MockIDeviceDriver,注入固定响应字节
业务层MockIProtocolClient,验证换算与报警
UI人工 / UI 自动化,不连真机
// 示例:假驱动返回固定 Modbus 响应publicsealedclassFakeDriver:IDeviceDriver{publicboolIsConnected=>true;publicTaskConnectAsync(CancellationTokencancellationToken=default)=>Task.CompletedTask;publicTaskDisconnectAsync(CancellationTokencancellationToken=default)=>Task.CompletedTask;publicTask<byte[]>TransceiveAsync(byte[]request,inttimeoutMs,CancellationTokencancellationToken=default)=>Task.FromResult(Convert.FromHexString("010302006400B9"));// 寄存器值 100publicValueTaskDisposeAsync()=>ValueTask.CompletedTask;}

十、常见问题 FAQ

Q1:四层会不会太「重」?小项目也要吗?
小 demo 可以两层(UI+协议),但一旦要上现场、要维护两年以上,四层边际成本很低,收益是换设备不改 UI。

Q2:TCP 和串口怎么统一?
HAL 各写一个HalSerialPortHalTcpStream,都实现IDeviceDriver。协议层 Modbus RTU/TCP 可共用一个IProtocolClient或拆两个实现。

Q3:异步会不会把 WinForms 搞复杂?
UI 线程只BeginInvoke;轮询放Task.Run+CancellationToken。比Timer里同步Read卡界面更安全。

Q4:寄存器地址 40001 和 0 怎么统一?
在业务层或配置里用「协议地址 0」,文档与 HMI 显示「40001」仅展示层转换。

Q5:和 MVVM 什么关系?
WPF 的 ViewModel 相当于本文的 UI 逻辑 + 部分业务;DeviceService 仍应独立于 ViewModel,便于单元测试。MVVM 专题见博客单独模块。

Q6:多设备多线程怎么分层?
每个设备一条「Driver + Protocol + Service」实例;UI 用字典或列表绑定。线程安全在 Service 或 DataHub 用锁/Channel,见本模块 03 篇。


十一、小结

  • UI:展示与交互,订阅业务事件。
  • 业务:工程量、轮询、报警。
  • 协议:Modbus/自定义帧。
  • HAL:串口/TCP 字节流。

把接口钉死后,换 PLC 改配置 + 换 Driver,窗体一行不动——这才是上位机架构的终极目标。


上一篇:02-communication / 08 TCP Socket 通信
下一篇:02 工厂模式与设备驱动