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 生命周期细节 |
| 协议层 | 帧编解码、事务(读/写寄存器) | IProtocolClient、ModbusProtocolClient | 关心 TextBox 刷新 |
| HAL 层 | 字节收发、连接、超时 | IDeviceDriver、HalSerialPort | 理解「温度」含义 |
2.1 依赖方向(自上而下)
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 各写一个HalSerialPort、HalTcpStream,都实现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 工厂模式与设备驱动