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 依赖方向自上而下UI 层 WinForms/WPF业务层 DeviceService协议层 ModbusProtocolClientHAL 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 IDeviceDriverHAL硬件抽象只表达「连接 收发字节 状态」与 Modbus 无关。namespaceUpperComputer.Core.Abstractions;publicinterfaceIDeviceDriver:IAsyncDisposable{boolIsConnected{get;}TaskConnectAsync(CancellationTokencancellationTokendefault);TaskDisconnectAsync(CancellationTokencancellationTokendefault);Taskbyte[]TransceiveAsync(byte[]request,inttimeoutMs,CancellationTokencancellationTokendefault);}设计要点TransceiveAsync请求-响应模型适合 Modbus RTU/TCP纯推送流可另加ReadAvailableAsync。超时在 HAL 实现协议层只关心「这次调用是否在时限内返回」。IAsyncDisposable窗体关闭、服务停止时统一释放端口。4.2 IProtocolClient协议层namespaceUpperComputer.Core.Abstractions;publicinterfaceIProtocolClient{Taskushort[]ReadHoldingRegistersAsync(byteslaveId,ushortstartAddress,ushortcount,CancellationTokenctdefault);TaskWriteSingleRegisterAsync(byteslaveId,ushortaddress,ushortvalue,CancellationTokenctdefault);}业务层调用的是「读保持寄存器」而不是「发 8 字节」。五、HAL 实现HalSerialPortusingSystem.IO.Ports;usingUpperComputer.Core.Abstractions;namespaceUpperComputer.HAL.Serial;publicsealedclassHalSerialPort:IDeviceDriver{privatereadonlySerialPort_port;publicboolIsConnected_port.IsOpen;publicHalSerialPort(stringportName,intbaudRate9600){_portnewSerialPort(portName,baudRate,Parity.Even,8,StopBits.One){ReadTimeout500,WriteTimeout500};}publicTaskConnectAsync(CancellationTokencancellationTokendefault){if(!_port.IsOpen)_port.Open();returnTask.CompletedTask;}publicTaskDisconnectAsync(CancellationTokencancellationTokendefault){if(_port.IsOpen)_port.Close();returnTask.CompletedTask;}publicasyncTaskbyte[]TransceiveAsync(byte[]request,inttimeoutMs,CancellationTokencancellationTokendefault){_port.Write(request,0,request.Length);varbuffernewbyte[256];vardeadlineDateTime.UtcNow.AddMilliseconds(timeoutMs);usingvarmsnewMemoryStream();while(DateTime.UtcNowdeadline){cancellationToken.ThrowIfCancellationRequested();varnawaitTask.Run(()_port.Read(buffer,0,buffer.Length),cancellationToken);if(n0)ms.Write(buffer,0,n);if(n0ms.Length5)break;// 简化Modbus RTU 最小响应awaitTask.Delay(10,cancellationToken);}returnms.ToArray();}publicasyncValueTaskDisposeAsync(){awaitDisconnectAsync();_port.Dispose();}}现场提示RTU 需处理「帧间隔」「3.5 字符时间」量产代码建议独立ModbusRtuTransceiverHAL 只保留Write/Read。六、协议层ModbusProtocolClient节选usingUpperComputer.Core.Abstractions;namespaceUpperComputer.Protocol.Modbus;publicsealedclassModbusProtocolClient:IProtocolClient{privatereadonlyIDeviceDriver_driver;publicModbusProtocolClient(IDeviceDriverdriver)_driverdriver;publicasyncTaskushort[]ReadHoldingRegistersAsync(byteslaveId,ushortstartAddress,ushortcount,CancellationTokenctdefault){varpduBuildReadHoldingRegistersPdu(slaveId,startAddress,count);varframeModbusRtuFrame.Wrap(pdu);// CRC16varresponseawait_driver.TransceiveAsync(frame,1000,ct);returnModbusRtuFrame.ParseRegisters(response,count);}publicTaskWriteSingleRegisterAsync(byteslaveId,ushortaddress,ushortvalue,CancellationTokenctdefault){varpduBuildWriteSingleRegisterPdu(slaveId,address,value);varframeModbusRtuFrame.Wrap(pdu);return_driver.TransceiveAsync(frame,1000,ct).ContinueWith(_Array.Emptybyte(),ct);}// BuildPdu / CRC 实现见 Modbus 通信专题此处省略细节privatestaticbyte[]BuildReadHoldingRegistersPdu(byteslaveId,ushortstart,ushortcount)Array.Emptybyte();}协议层集中处理功能码、异常码、字节序Big-Endian业务层拿到的已是ushort[]。七、业务层DeviceServiceusingUpperComputer.Core.Abstractions;namespaceUpperComputer.Business.Services;publicsealedclassDeviceService{privatereadonlyIProtocolClient_protocol;publiceventActiondouble?TemperatureChanged;publicDeviceService(IProtocolClientprotocol)_protocolprotocol;publicasyncTaskPollTemperatureAsync(CancellationTokenctdefault){varregsawait_protocol.ReadHoldingRegistersAsync(1,0,1,ct);varrawregs[0];vartempraw/10.0;// 工程量换算放在业务层TemperatureChanged?.Invoke(temp);}}业务层负责从寄存器到物理量、报警阈值、轮询间隔策略。同一协议客户端可被多个 Service 组合温湿度、电机状态。八、WinForms 集成示例publicpartialclassMainForm:Form{privatereadonlyDeviceService_service;privatereadonlyCancellationTokenSource_ctsnew();publicMainForm(DeviceServiceservice){_serviceservice;_service.TemperatureChangedtBeginInvoke(()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{publicboolIsConnectedtrue;publicTaskConnectAsync(CancellationTokencancellationTokendefault)Task.CompletedTask;publicTaskDisconnectAsync(CancellationTokencancellationTokendefault)Task.CompletedTask;publicTaskbyte[]TransceiveAsync(byte[]request,inttimeoutMs,CancellationTokencancellationTokendefault)Task.FromResult(Convert.FromHexString(010302006400B9));// 寄存器值 100publicValueTaskDisposeAsync()ValueTask.CompletedTask;}十、常见问题 FAQQ1四层会不会太「重」小项目也要吗小 demo 可以两层UI协议但一旦要上现场、要维护两年以上四层边际成本很低收益是换设备不改 UI。Q2TCP 和串口怎么统一HAL 各写一个HalSerialPort、HalTcpStream都实现IDeviceDriver。协议层 Modbus RTU/TCP 可共用一个IProtocolClient或拆两个实现。Q3异步会不会把 WinForms 搞复杂UI 线程只BeginInvoke轮询放Task.RunCancellationToken。比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 工厂模式与设备驱动