news 2026/7/6 14:13:50

上位机分层架构设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
上位机分层架构设计

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 工厂模式与设备驱动

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/6 14:13:26

STM32L4与74HC32实现低功耗多键盘管理方案

1. 为什么选择74HC32与STM32L4A6RG组合&#xff1f;在嵌入式系统中管理多个按键时&#xff0c;传统矩阵键盘方案需要占用大量GPIO引脚。以2x2键盘为例&#xff0c;直接扫描需要4个GPIO&#xff08;2行2列&#xff09;&#xff0c;当系统需要管理多组键盘时&#xff0c;引脚资源…

作者头像 李华
网站建设 2026/7/6 14:04:45

问答解惑|okbiye AI写作,一站式搞定毕业论文所有难题

okbiye-免费查重复率aigc检测/开题报告/毕业论文/智能排版/文献综述/科研绘图毕业论文 - Okbiye智能写作https://www.okbiye.com/ai/bylw 1、为什么普通AI写论文&#xff0c;总过不了学校审核&#xff1f; 很多同学写毕业论文都会踩坑&#xff1a;通用AI写出来的内容空洞模板化…

作者头像 李华
网站建设 2026/7/6 14:04:12

终极指南:如何在PC上免费畅玩Switch游戏?yuzu模拟器完整教程

终极指南&#xff1a;如何在PC上免费畅玩Switch游戏&#xff1f;yuzu模拟器完整教程 【免费下载链接】yuzu 任天堂 Switch 模拟器 项目地址: https://gitcode.com/GitHub_Trending/yu/yuzu 想要在电脑上体验《塞尔达传说&#xff1a;旷野之息》、《超级马里奥&#xff1…

作者头像 李华
网站建设 2026/7/6 14:02:55

MAX9744与PIC18LF46K40的高效音频放大方案解析

1. 为什么选择MAX9744与PIC18LF46K40组合在音频功率放大领域&#xff0c;D类放大器因其高效率特性已成为主流选择。MAX9744作为Analog Devices推出的20W立体声D类音频功率放大器&#xff0c;其核心优势在于以D类能效实现了AB类放大器的音质表现。实测数据显示&#xff0c;在12V…

作者头像 李华
网站建设 2026/7/6 14:02:00

终极指南:如何使用BotW Save Manager轻松实现Switch与WiiU存档互转

终极指南&#xff1a;如何使用BotW Save Manager轻松实现Switch与WiiU存档互转 【免费下载链接】BotW-Save-Manager BOTW Save Manager for Switch and Wii U 项目地址: https://gitcode.com/gh_mirrors/bo/BotW-Save-Manager 你是否曾经在Switch和WiiU之间切换玩《塞尔…

作者头像 李华
网站建设 2026/7/6 14:01:03

ClaudeCode for VSCode:深度代码理解的AI编程协作者

1. 为什么是 ClaudeCode for VSCode&#xff0c;而不是 Copilot、Cursor 或其他&#xff1f; 如果你最近刷过技术社区、开发者群或者 GitHub Trending&#xff0c;大概率已经看到过“ClaudeCode”这个词被反复提起。它不是官方产品&#xff0c;没有 Anthropic 直接背书&#x…

作者头像 李华