简介一套基于C#开发的大型自动化设备二次注液机上位机软件源码面向工业自动化上位机软件工程师及本专业学习者围绕二次注液机精确注液的控制场景系统展示如何借助C#构建人机交互界面、通过数据库持久化存储工艺参数与报警记录并实现与欧姆龙PLC的稳定通信。压缩包共二百零八个文件大小约二点五七兆字节以C#源文件为主体搭配动态链接库、SQL数据库脚本、界面资源文件以及配置类文件涵盖数据访问、界面显示、通信控制等工程模块目录结构清晰便于按模块研读。这份源码包已有三百三十二人学习适合具备一定C#基础、希望掌握工业设备上位机完整开发流程的读者。研读其中的解决方案与代码注释可深入借鉴数据库表结构设计、欧姆龙PLC通信协议封装、多线程调度与异常处理等关键点的落地写法理解从下位机数据采集到界面监控的完整链路对同类自动化设备软件开发具有直接参考价值。1. 二次注液机上位的 C# 源码包先搞清楚它能帮你落地什么一条产线的二次注液工位设备商交付时通常只给 PLC 程序和一张 IO 点表上位软件要么买成品充灌软件要么自己人从零写。多数团队卡在注液精度和追溯这两件事上工艺给了目标重量泵也标定了但批量跑起来总有几杯偏少查不到是哪一步丢的数据客户要溯源你只能翻 Excel。这套基于 C# 的二次注液机上位软件源码解决的就是中间那层配方管理、注液流程调度、称重数据采集、报警记录和 MES 对接也就是把工艺参数变成产线行为、把动作变成可追溯记录的那一层。适合正在做锂电、医疗或化工小容量注液自动化又偏 C# 上位机方向的工程师复现拿来改你自己的机型比从空白 WinForm 项目起步省非常多事。2. 先搞懂工艺再拆代码二次注液机上位软件的模块边界与工程结构二次注液机不是一台“注完就完”的机器。你打开源码包之前得先想清楚它在整条产线上站哪个位置否则代码里的多线程、状态机、报警恢复你全看不懂。2.1 工艺链路一次注液、静置等待、二次注液与称重复检二次注液的“二次”是指工艺上分两段加注。第一段把大部分电解液或药液注进去然后静置一段时间让液体渗透到极片或填充物里、排出气泡此时液位会掉下去或者称重发现实际重量低于目标下限再补第二段。如果一次直接注到位容易出现气泡、浸润不均最终称重不合格率偏高。这条链路里典型工位是我下面这张表的顺序工位执行部件上位机职责一次注液注液泵、注液阀下发目标重量/注液量记录实际注液量静置缓存输送线、夹具计时控制静置时间与流转节拍二次注液注液泵、精密控制阀按称重回差计算补注量再次注液称重复检称重模块、剔除机构读取稳定重量判定合格/不合格所以你拆这套源码时发现核心不在界面而在“一次注液→静置→称重→二次补注→再判定”这条链路的数据流转上。界面只是壳里面跑的是工艺队列。2.2 模块边界PLC 管动作上位机管配方、记录与追溯大型自动化设备上位机和 PLC 的活必须分清楚。PLC 负责气缸、电机、注液阀这些硬动作的时序上位机负责配方、参数下发、数据采集、报警和报表。实时性要求高的伺服插补、毫秒级阀动作一定放在 PLC 或运动控制卡里上位机介入实时闭环纯属给自己挖坑。我拆这类项目时会先画一张职责边界表再去看代码里的通信消息是怎么组织的层级负责内容典型模块底层执行气缸动作、电机启停、阀开关、安全互锁PLC、运动控制卡上位机配方选择、注液量计算、称重读取、报警判定、数据落库C# 后台服务、数据库系统对接条码扫描、MES 上传、SPC 统计C# 接口模块有了这个分层再看源码里的“启动注液”事件就很简单上位机给 PLC 发一个握手命令PLC 执行完回一个完成标志上位机再读称重值。一般约定长这样// 启动注液指令上位机写 PLC 线圈 M200 1 // 注液完成应答PLC 置位 M201上位机轮询读取 // 当前称重值PLC 写数据寄存器 D400单位 0.1g // 报警代码PLC 写 D410同时置位 M210这里参数差异是现场最容易打架的地方M 区地址、D 区地址、单位精度每个设备厂的习惯都不一样拿到源码后第一步就是把这份点位约定和你现场的点表对齐对不上后面全是乱码。2.3 拿到源码包先看什么五个关键文件与目录把 ZIP 解压后我建议你不要先打开 MainForm.cs 看界面而是按这个顺序把工程结构过一遍第一解决方案文件和工程文件也就是 .sln、.csproj。先确认目标框架是 .NET Framework 4.7.2 还是 .NET 6/8这决定了你能不能在本机直接编译。第二通信层目录一般叫 Communication、Serial 或 Modbus看它是用串口还是 TCP 跟 PLC 和仪表通信。第三流程调度目录可能叫 Flow、Task 或 StateMachine这是整个源码的灵魂。第四数据库脚本和配置文档常见是 SQLite 或 MySQL 的建表脚本以及配方表、报警表、产量统计表。第五协议说明书、IO 点表、部署文档这类 PDF/Word很多新手不看这个只盯着代码看结果连注液泵的寄存器地址都不知道去哪找。提示源码包最值钱的不一定是 .cs 文件而是那几份通信协议和 IO 点表。设备厂换个人就换个寄存器说法有文档才有后悔药。3. 通信层实现串口参数、Modbus CRC 校验与西门子 OPC 接入细节上位软件一半的坑在通信层。注液泵和称重仪表通常走 RS485 串口PLC 通常走以太网老产线还有 OPC 接入的需求。这一章把你大概率要改的三条通信链路讲透。3.1 注液泵与称重仪表通信串口参数与 CRC 校验的实现注液泵和流量计最常见的接口是 RS485 Modbus RTU。现场典型参数是波特率 9600、数据位 8、停止位 1、无校验但有些流量计厂家默认偶校验所以源码里串口参数必须是可配置的不能写死。读流量、写目标注液量本质是拼 Modbus 帧。下面这段代码是我常用的写入单路寄存器实现用来给泵下发启动指令private byte[] BuildModbusFrame(byte deviceAddr, byte functionCode, ushort startAddr, ushort value) { var frame new byte[8]; frame[0] deviceAddr; // 从站地址现场 1~247 frame[1] functionCode; // 0x06 写单个寄存器 frame[2] (byte)(startAddr 8); // 寄存器地址高字节 frame[3] (byte)(startAddr 0xFF); // 寄存器地址低字节 frame[4] (byte)(value 8); // 写入值高字节 frame[5] (byte)(value 0xFF); // 写入值低字节 ushort crc CalculateCrc16(frame, 6); frame[6] (byte)(crc 0xFF); // CRC 低字节在前 frame[7] (byte)(crc 8); // CRC 高字节在后 return frame; }这段代码的逻辑要点是Modbus RTU 帧在数据字节后面追加两个 CRC 校验字节而且低字节在前、高字节在后这是新手最容易翻车的地方。你如果只把计算出的 CRC 按高字节在前发出去仪表直接不回包而且没有任何报错提示。private ushort CalculateCrc16(byte[] data, int length) { ushort crc 0xFFFF; // 初始值固定为 0xFFFF for (int i 0; i length; i) { crc ^ data[i]; // 异或当前字节 for (int j 0; j 8; j) { if ((crc 0x0001) ! 0) crc (ushort)((crc 1) ^ 0xA001); // 多项式 0xA001 else crc 1; } } return crc; }CRC 的参数说明多项式 0xA001 是 0x8005 的反序写法这是 Modbus 标准规定不能改。计算的对象是除 CRC 本身以外的所有字节。现场排查时如果仪表时通时不通先用串口助手抓包把上位机发出去的原始帧和仪表手册的示例帧逐字节比对80% 是 CRC 字节顺序反了或者是功能码选错。3.2 与 PLC 的网口通路TCP 客户端、心跳保活与断线重连大型设备里 PLC 和上位机走以太网是主流。常见架构是上位机做 TCP 客户端PLC 做 TCP 服务器上位机主动连 PLC 的固定端口。源码里如果用了 TcpListener那通常是给模拟器或老式 PLC 做服务端才需要。要注意的是上位机单纯用 TcpClient 连接后不做心跳网络上任何一端重启连接状态都不会立刻反映到 Error 属性里你发数据时才抛异常甚至一直卡在阻塞发送里。下面是我常用的一个精简客户端结构包含心跳和重连public class PlcTcpClient { private TcpClient _client; private readonly string _ip; private readonly int _port; private readonly CancellationTokenSource _cts new(); public event Actionstring OnLog; // 日志回调界面用 public event Action OnConnectionLost; // 断线事件 public PlcTcpClient(string ip, int port) { _ip ip; _port port; } public async Task RunAsync() { while (!_cts.IsCancellationRequested) { try { _client new TcpClient(); await _client.ConnectAsync(_ip, _port); // 3 秒超时由外接 CancellationToken 控制 OnLog?.Invoke(PLC 连接成功); await HeartbeatLoopAsync(); } catch (Exception ex) { OnLog?.Invoke($连接异常: {ex.Message}); OnConnectionLost?.Invoke(); await Task.Delay(3000); // 3 秒后重连 } } } private async Task HeartbeatLoopAsync() { while (_client.Connected) { byte[] beat { 0x01, 0x06, 0x00, 0x00, 0x00, 0x01, 0x48, 0x0A }; await _client.GetStream().WriteAsync(beat); await Task.Delay(3000); // 心跳间隔现场常用 1~3 秒 } } }这段代码的逻辑说明RunAsync 里是一个带自动重连的循环PLC 短暂断电或网线松动后上位机每 3 秒尝试重新连接一次心跳循环每 3 秒发一次固定帧PLC 收到就当链路正常。参数说明心跳间隔不要小于 1 秒否则会给 PLC 增加没必要的负载重连间隔也不要太短现场网线松了不是一秒钟能插回去的3 秒左右比较均衡。3.3 C# 连接西门子 OPC 与调用 C 控制卡 DLL两种外部接入方式二次注液机现场遇到西门子 PLC 时接入方案常用的有两类。一类是 OPC DA通过 opcdaauto 接口访问适合老产线改造另一类是 Siemens S7 协议直连用 S7netplus 或 Sharp7 这类库不需要 OPC Server 环境部署简单适合新设备。如果源码里同时出现这两种接入方式说明作者做过多种机型的适配你按现场情况挑一种用就好。运动控制卡或称重模块的 C DLL 是另一个常见接入点DllImport 调用时最容易出问题的是委托签名不匹配。看下面这段典型代码[DllImport(MotionCtrl.dll, CallingConvention CallingConvention.Cdecl)] private static extern int OpenDevice(int deviceType, int linkId); [DllImport(MotionCtrl.dll, CallingConvention CallingConvention.Cdecl)] private static extern int RegisterCallback(CallbackFunc callback); public delegate void CallbackFunc(int axis, int eventType, IntPtr userData);这里的参数说明CallingConvention 必须和控制卡厂商头文件里声明的一致常见的是 Cdecl 或 StdCall写错了调用时栈被破坏轻则返回垃圾值重则直接崩溃。委托的签名要和 DLL 导出函数完全一致参数个数、类型、参数顺序都不能有偏差否则就是你后面会看到的 Access Violation。另外委托对象要保持引用不能让它被 GC 回收否则回调触发时程序直接崩溃这是 C# 调 C 最经典的坑放这里先留个印象第五章细讲。4. 把注液流程写成状态机配方切换、自动循环与报警恢复这一章是这个源码包最核心的部分也是上位软件和普通管理系统最大的区别所在。界面怎么写都行流程调度才是二次注液机的命根子。4.1 自动运行状态机待机、自动、暂停、报警与复归大型自动化设备的上位软件流程不能靠按钮事件里套 if else 写状态一多就乱成一锅粥。成熟的做法是显式状态机把设备的运行状态枚举出来public enum MachineState { Idle, // 待机允许编辑配方和参数 Ready, // 已就绪等待自动启动 Running, // 自动运行中 Paused, // 暂停等待继续或复位 Alarm, // 报警需人工干预 Aborted // 流程中止需复位 }每次状态切换时把旧状态、新状态、切换原因写到日志这是排查流程问题最重要的依据。比如状态从 Running 进了 Alarm日志里必须带上是哪个条件触发的是称重超差还是通信超时。状态转移条件可以整理成一张表源码里的逻辑也应该能对应上这张表当前状态目标状态触发条件IdleReady配方校验通过安全门关闭ReadyRunning收到自动启动命令RunningPaused操作员按暂停RunningAlarm注液量超差、通信超时、急停触发AlarmIdle故障复位且报警原因消除关键点在于上位机状态机里的 Alarm 和 PLC 里的急停要解耦PLC 急停后的状态恢复必须走专门流程不能简单粗暴地把上位机状态也复位成 Ready否则两边的动作时序就错位了。4.2 配方数据结构与注液量换算二次注液机一个很实际的场景是换型号就换配方。配方类基本应该长这样public class InjectRecipe { public string RecipeId { get; set; } // 配方号 public double TargetWeight { get; set; } // 目标总重量单位 g public double FirstInjectWeight { get; set; } // 一次注液量 public double SecondInjectWeight { get; set; } // 二次注液量 public double Density { get; set; } // 液体密度用于体积/重量换算 public double AllowedDeviation { get; set; } // 合格判定允许偏差单位 g public int SoakSeconds { get; set; } // 静置等待时间 }二次注液量的核心换算逻辑不是简单的“配方里写多少就注多少”而是按称重回差算。常见做法是一次注液结束后静置完成先称重如果实际重量和目标重量之间的差在允许偏差内直接判定合格如果差超了按差值除以密度换算成体积再二次注液补上去double actualWeight GetStableWeightFromScale(); // 取稳定后的称重值 double shortfall recipe.TargetWeight - actualWeight; if (shortfall recipe.AllowedDeviation) { double secondVolume shortfall / recipe.Density; // 重量差值折算体积 StartSecondInject(secondVolume, recipe.RecipeId); } else if (shortfall -recipe.AllowedDeviation) { SetAlarm(实际重量超出目标检查一次注液量); } else { MarkAsPass(actualWeight); }这段代码的参数说明shortfall 是称重值和目标值的差只有为正且超偏差时才补注为负说明注多了这通常是异常不能靠“倒吸”来处理二次注液机一般没有回抽功能。GetStableWeightFromScale 必须有滤波和稳定判断称重模块的数值会抖动连续读取 5 次都在 0.1g 范围内才认为稳定否则补注量算出来就是满瓶乱撞。4.3 与 PLC 的握手信号设计请求、应答与超时上位机和 PLC 之间最原始的配合方式就是“我发一个请求等你应答”。很多新手直接 Thread.Sleep 等固定时间结果现场节拍一变就出问题。更稳的是用请求号加超时机制下面是一个用 TaskCompletionSource 实现应答等待的例子private async Taskbool WaitForPlcAckAsync(int requestId, int timeoutMs) { var tcs new TaskCompletionSourcebool(); void Handler(int ackId) { if (ackId requestId) tcs.TrySetResult(true); } _plc.OnAck Handler; try { return await tcs.Task.WaitAsync(TimeSpan.FromMilliseconds(timeoutMs)); } finally { _plc.OnAck - Handler; // 无论成功失败都要解绑事件 } }这里 Handler 是一个局部函数每次请求分配一个新的避免多个请求之间事件串扰。timeoutMs 参数要由泵的响应速度决定柱塞泵响应快可以设 2 秒带预压的泵可能要 5 秒设太短正常流程会被误判为超时设太长故障时设备多余的等待时间会拖死整线节拍。finally 里解绑这是关键一步不解绑会造成委托堆积内存泄漏是小事老事件被重复触发才是大麻烦。5. 二次注液机上位软件避坑通信、时序与数据追溯的五个实战记录这一章是我实际跑现场遇到过的坑每一条都真实影响过产线。你照着源码改的时候大概率也会碰到其中的一两条。5.1 现象注液量偶发偏差称重显示时高时低但泵的执行看起来没问题原因串口数据读到了半包。串口是流式协议一次 Read 返回的数据不一定是一整帧可能只有半个帧也可能一次收到了两帧。上位机直接按固定长度解析就会拿到错位的字节把它当成称重值或注液量流程却继续往下走最后注液量自然就偏了。解决所有串口通信都要做分包和粘包处理。按 Modbus 帧的字节长度判断先收进缓冲区检查长度够了再解析解析完把剩余字节继续保留给下一帧。我一般会写一个 FrameBuffer 类专门做这件事而且每帧解析完要校验 CRCCRC 不过的帧直接丢弃并计数不进入流程逻辑。5.2 现象程序启动后界面卡死转圈任务管理器里显示未响应原因上位机在 UI 线程里做了串口打开、数据库连接或 PLC 连接操作。串口.Open() 和 TcpClient.ConnectAsync 在没有设备响应时可以卡好几秒UI 线程被阻塞界面就死了。大型自动设备界面上要显示实时状态卡一秒都受不了。解决所有可能阻塞的操作走 async/await 或后台 TaskUI 线程只负责绑定数据。用 async 时注意 ConfigureAwait(false) 的使用场景后台任务里别混抢 UI 上下文涉及第三方 SDK 的通信组件回调里不要直接操作控件先 Post 到界面线程再更新。5.3 现象PLC 断线重连后MES 记录不完整只有报警但数据不连续原因上位机往 MES 上报用的是 HTTP 短连接比如 RestClient。PLC 或者上位机网络闪断时上报请求抛出了“远程主机强迫关闭了一个现有的连接”之类的异常代码只 catch 了打印日志没有补发机制重连后流程恢复但断线期间的记录已经丢了。解决上报动作不能只在业务代码里同步调用一次要加“存储转发”机制。先写本地 SQLite 缓存表上报成功后再标记已上传失败则重试重试次数超过阈值才置为失败状态待人工处理。这样 PLC 断线、网络抖动都不影响数据完整性MES 那边查到的是最终一致的数据。5.4 现象C# 调用控制卡 C DLL 直接报 Access Violation错误码 0xC0000005原因这是调用约定不匹配或委托被 GC 回收。最常见的是厂商头文件里声明的是 cdecl你在 DllImport 里默认成了 StdCall或者注册回调时传了一个临时委托调用完局部变量就没了C 那边触发回调时访问的是已释放的内存地址直接崩溃。解决先核对头文件里的调用约定DllImport 里显式写 CallingConvention。委托要么作为类字段持有引用要么用 GCHandle.Alloc 固定住绝不能用局部临时变量。另外回调函数的参数类型必须和 DLL 文档一一对应IntPtr 就写 IntPtr别图省事写成 int在 64 位进程下这又是同一个崩溃。5.5 现象急停恢复后流程和 PLC 不同步自动启动一按就报警原因上位机状态机和 PLC 状态没有做同频。急停发生时PLC 侧动作立即停了但上位机进程可能还在 Running 状态里等应答。复位操作只清了 PLC 的故障没有把上位机状态拉回 Idle两边状态一错位再发启动指令PLC 的安全逻辑检测到条件不满足又立刻报警。解决急停信号在上位机里必须作为最高优先级事件处理不只置 Alarm 状态还要取消当前所有等待任务并把 PLC 侧状态清掉。恢复时走“复位”流程先读 PLC 当前状态上位机跟着 PLC 同步到 Ready再允许启动。我现在的习惯是状态切换的全部动作都留日志现场发生了就直接查最后一条状态迁移记录比看波形还快。6. 上真机前先过这一关模拟器联调与追溯数据交叉验证源码拿回来别急着往真机上跑真机一次动作就是一组机械磨损调试成本和风险都高。我现在的做法是先用模拟器把通信和流程跑通再上真机。6.1 串口/TCP 模拟器没有真机也能把流程完整跑一遍PLC 那边可以用 Python 写一个简单的 TCP 服务端模拟心跳应答和状态寄存器读写。上位机连这个模拟器就能验证连接、断线重连、心跳超时这些逻辑import socket, time HOST, PORT 127.0.0.1, 502 srv socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind((HOST, PORT)) srv.listen(5) print(模拟 PLC 已监听, PORT) while True: conn, addr srv.accept() print(上位机接入:, addr) while True: data conn.recv(1024) if not data: print(连接断开) break # 收到心跳帧后回一个相同长度的确认帧 conn.sendall(b\x01\x06\x00\x00\x00\x01\x48\x0A) conn.close()这段脚本的逻辑说明监听 502 端口收到任何数据都回一个固定确认帧上位机那边的显示就应该从“连接成功”变成“心跳正常”。参数说明端口可以随意改但要和上位机配置里保持一致这个模拟器还能扩展成根据寄存器地址返回不同的值用来测报警逻辑。6.2 追溯数据交叉验证清单数据追溯是客户验收的硬指标。我建议把配方表、注液记录表、称重记录表、报警表之间的关联关系预先定好每一杯产品有一个唯一的流水号加工序记录全部带着这个流水号。验证时跑一组模拟数据按产线批次反查SELECT r.RecipeId, r.TargetWeight, i.FirstInjectWeight, w.ActualWeight, w.JudgeResult FROM InjectRecord i JOIN WeighRecord w ON i.SerialNo w.SerialNo JOIN Recipe r ON i.RecipeId r.RecipeId WHERE i.SerialNo P20250115001;查询逻辑说明用 SerialNo 把注液记录和称重记录串起来能看到一次注液量、称重实际值和判定结果。参数说明如果跑模拟器时 SerialNo 是固定的那就会查出很多行这是正常现象因为模拟数据一直在覆盖同一批号真要上真机批号必须是条码枪扫出来的唯一值。6.3 源码包到产线之间最后再核对一遍 IO 点表带这个项目时我吃过一个亏源码里的注液完成信号地址和现场 PLC 程序里的地址差了 4 个字节上位机一直收不到完成应答整个自动流程瘫在那里。从那以后每次接产线我都强制先走一遍模拟器联调加 IO 点表逐点核对再上真机全流程空跑两次确认事件顺序、超时参数和追溯数据全部对上才放行。这套动作看着慢实际比到现场再改代码省一整天希望帮到你。本文还有配套的精品资源点击获取