
1. 项目缘起与整体架构设计半导体晶圆和石墨岛的搬移听起来像是实验室里穿着无尘服的操作员用镊子夹来夹去的精细活但一旦放到量产线上这就是一套对精度、洁净度和节拍要求都极其苛刻的自动化流程。晶圆Wafer是芯片制造的基底材料石墨岛Graphite Island则常见于某些化合物半导体或特殊工艺的承载/散热结构两者在物理特性上差异很大——晶圆脆、怕静电、怕微尘石墨岛相对耐操但容易掉粉污染。把这两类物料从A点搬到B点中间涉及机械臂运动控制、真空吸附/静电吸附切换、视觉定位补偿、与MES/上位系统的数据交互任何一个环节掉链子轻则碎片重则整批报废。我接到的需求就是给这样一套搬移设备做上位机系统。技术栈定的是C# WPF这个组合在工业上位机领域算是“老搭档”了C#负责逻辑、通信、数据处理WPF负责界面呈现和交互。为什么不用WinForm因为这套系统需要展示实时运动轨迹、多轴状态、晶圆Map图、报警日志滚动、参数配置面板WinForm做复杂布局和动态可视化会很吃力而WPF的XAML数据绑定、样式模板、矢量渲染能力正好对上。至于为什么不选.NET MAUI工业现场基本都是Windows工控机MAUI的跨平台优势用不上反而在串口、OPC、相机SDK这些底层调用上不如WPF成熟稳定。整个系统的核心模块我拆成了五块设备通信层PLC、机械臂控制器、真空/静电发生器、视觉定位层相机取像、模板匹配、坐标偏移计算、运动调度层搬移流程状态机、多轴联动时序、数据持久层搬移记录、报警日志、配方参数、人机交互层WPF主界面、参数配置、手动调试、报表导出。这五层之间通过事件总线和接口抽象解耦方便后期换PLC品牌或加相机工位时不用大改。注意半导体设备上位机最忌讳“界面卡死”。所有通信和耗时操作必须走异步UI线程只做渲染。我见过太多项目因为串口读写在UI线程同步执行导致界面假死被操作员投诉。架构选型上我采用了MVVM模式。ViewModel持有设备状态和命令View通过Binding自动刷新Model层封装通信协议和数据实体。这样做的好处是调试时我可以单独跑一个“模拟设备”的Model不接真实PLC也能测界面逻辑后期如果客户要求换触摸屏操作View层重写即可ViewModel和Model基本不动。社区里常说的“Building Enterprise Applications with WPF and the MVVM Pattern”那套思路在工业场景里同样适用只是要把异步和线程安全考虑得更细。2. 核心通信与设备对接细节2.1 PLC通信为什么选OPC UA而不是直接TCP设备主控是一台西门子S7-1500系列PLC负责安全门锁、真空压力、气缸位置、急停链这些底层IO。上位机要和PLC交换的数据分两类周期性状态如各轴当前位置、真空值、报警字和事件性指令如启动搬移、切换配方、复位。最初考虑过直接用C#的Socket走TCP发自定义报文但实测下来有几个坑一是PLC侧要额外写通信处理块增加调试工作量二是字节序和数据类型对齐容易出错一个float高低位搞反真空值就变成天文数字三是后期加一台PLC就要改代码。后来改用西门子OPC UA服务器PLC侧启用OPC UA功能C#这边用Opc.Ua.Client库订阅节点。OPC UA的好处是数据类型自描述订阅模式支持数据变化上报不用轮询。配置时注意两点会话超时要设长一点我设的60秒工业现场网络抖动时自动重连订阅间隔根据数据重要性分级急停和真空值设100ms温度电流设1000ms避免带宽浪费。// OPC UA订阅核心代码片段 var session await Session.Create(config, endpoint, false, , 60000, null, null); var subscription new Subscription(session.DefaultSubscription) { PublishingInterval 100 }; session.AddSubscription(subscription); subscription.Create(); var item new MonitoredItem(subscription.DefaultItem) { StartNodeId ns3;s\DB_Motion\.\Axis1_Pos\, AttributeId Attributes.Value, SamplingInterval 100, QueueSize 10 }; item.Notification OnPlcDataChanged; subscription.AddItem(item); subscription.ApplyChanges();实操心得OPC UA节点名里的引号和分号很容易写错建议先用UaExpert工具连上PLC把节点浏览一遍直接复制NodeId别手敲。2.2 机械臂与真空/静电控制搬移动作由一台四轴SCARA机械臂执行控制器支持Modbus TCP。C#这边用NModbus4库读写保持寄存器。机械臂的坐标下发、速度设定、到位信号都走Modbus。这里有个细节Modbus寄存器是16位的而坐标值可能是32位浮点需要拆成两个寄存器发送并且要确认控制器侧的高低字顺序。我踩过的坑是控制器手册写“高字在前”实际测试发现是低字在前导致机械臂往反方向跑差点撞限位。真空吸附和静电吸附的切换由PLC的IO控制上位机只发指令字。晶圆用真空吸附避免静电损伤石墨岛用静电吸附表面粗糙真空吸不住。切换时序很关键必须先确认旧吸附关闭、新吸附建立再移动机械臂否则物料会在半空中掉落。我在状态机里加了互锁逻辑吸附状态反馈来自PLC的真空压力开关和静电电流检测。2.3 视觉定位补偿晶圆在载具里的位置不可能每次都完美居中偏差可能到±0.5mm直接抓取会碎片。所以加了一台海康威视工业相机做定位。C#通过海康的SDK取像用OpenCVSharp做模板匹配算出实际圆心与理论圆心的偏移量再把偏移量补偿到机械臂坐标里。这里涉及一个热词里提到的“C#调用C出现Access Violation c0000005”的问题。海康SDK底层是C的C#调用时如果回调函数里访问了已释放的内存或者图像数据指针在回调外使用就会崩。我的做法是在回调里只做数据拷贝不做任何耗时处理和UI更新把图像数据拷到托管数组后通过线程安全队列丢给后台线程处理。// 相机回调中只拷贝数据 private void OnImageCallback(IntPtr pData, ref MV_FRAME_OUT_INFO_EX pFrameInfo, IntPtr pUser) { int width pFrameInfo.nWidth; int height pFrameInfo.nHeight; byte[] buffer new byte[width * height]; Marshal.Copy(pData, buffer, 0, buffer.Length); _imageQueue.Enqueue(buffer); // 线程安全队列 }注意相机SDK的回调线程和WPF UI线程不是同一个任何界面更新都要用Dispatcher.Invoke但别在回调里直接Invoke会阻塞采集。正确做法是后台线程处理完再更新UI。3. 搬移流程状态机与运动调度实现3.1 状态机设计从“取”到“放”的完整闭环搬移流程看起来简单——去A点取料搬到B点放料——但实际要考虑的状态有十几个空闲、等待配方、取料定位、取料下降、真空开启、取料上升、搬移中、放料定位、放料下降、真空关闭、放料上升、回安全位、报警暂停、急停复位。我用了一个枚举加switch-case的状态机每个状态处理完调用NextState()推进。为什么不用现成的状态机框架因为工业流程的状态迁移条件往往和硬件信号强相关比如“取料下降”到“真空开启”的条件是“Z轴到位信号为真且真空压力小于阈值”这种逻辑用框架反而绕。手写状态机虽然土但调试直观加日志方便。private void StateMachineLoop() { while (!_cts.IsCancellationRequested) { switch (_currentState) { case MoveState.Idle: if (_startCommand) _currentState MoveState.PickLocate; break; case MoveState.PickLocate: if (VisionLocateDone()) _currentState MoveState.PickDown; break; case MoveState.PickDown: if (PlcSignals.ZAxisInPosition PlcSignals.VacuumPressure 10) _currentState MoveState.VacuumOn; break; // ... 其余状态 } Thread.Sleep(10); // 10ms周期兼顾响应和CPU占用 } }3.2 多轴联动时序与节拍优化四轴SCARA的X/Y/Z/R轴运动有先后依赖XY先定位到目标上方Z再下降R轴旋转对位。如果串行执行一个搬移周期要8秒以上产能上不去。我做了两处优化XY运动的同时预转R轴只要R轴旋转不会碰到周边结构以及Z轴下降时提前开启真空预吸真空阀打开但压力未完全建立等Z到位时压力刚好达标。优化后单周期降到5.2秒提升约35%。节拍计算过程原时序XY定位1.5s R旋转0.8s Z下降0.6s 真空建立0.5s Z上升0.6s XY搬移1.5s Z下降0.6s 真空释放0.4s Z上升0.6s 回安全位1.0s 8.1s。优化后R轴与XY并行省0.8s真空预吸省0.3s实际5.2s左右。这个数据是在空跑测试下测的带料后因真空建立时间略长约5.5s。实操心得节拍优化别一味压缩要留安全余量。我见过为了提速把Z轴下降速度调太高结果晶圆下压时产生气流把晶圆吹偏碎片率飙升。速度参数一定要在带料测试中逐步逼近别拍脑袋。3.3 异常处理与安全恢复搬移过程中最怕的是突然断电或急停。我的处理是每个状态迁移前先写“恢复点”到本地SQLite记录当前状态、目标坐标、物料ID。重新上电后系统读取恢复点提示操作员“上次在放料下降状态中断请确认物料是否在吸盘上”由人工确认后再继续或复位。这样避免自动恢复时把已掉落的物料当成还在吸盘上造成撞机。报警分级也做了区分致命报警急停、安全门开、真空丢失立即停机并锁定需手动复位警告真空建立慢、定位偏差大记录日志但继续运行达到连续3次才升级为致命。这个阈值可配不同产品要求不一样。4. 数据持久化与报表导出4.1 SQLite在工业上位机里的轻量应用搬移记录、报警日志、配方参数都存在本地SQLite里。选SQLite而不是SQL Server是因为工控机往往不装数据库服务SQLite单文件、零配置、C#用System.Data.SQLite或Microsoft.Data.Sqlite直接读写部署省心。表结构设计了三张核心表MoveRecord搬移记录、AlarmLog报警日志、Recipe配方参数。CREATE TABLE MoveRecord ( Id INTEGER PRIMARY KEY AUTOINCREMENT, WaferId TEXT NOT NULL, SourcePos TEXT, TargetPos TEXT, StartTime DATETIME, EndTime DATETIME, Result INTEGER, -- 0成功 1失败 OffsetX REAL, OffsetY REAL );写入频率上搬移记录每周期一条一天两班约2万条SQLite完全扛得住。但要注意批量提交如果每条都INSERT后立即Commit磁盘IO会很频繁。我改成每50条或每5秒提交一次用事务包裹写入性能提升明显。4.2 报表导出RDLC还是自己画热词里有人问“WPF RDLC ReportViewer是否能做复杂格式的报表”。我的经验是RDLC做固定格式的检验报告、出货单可以但半导体搬移数据往往需要动态列不同产品关注的参数不同和图表混合RDLC的表达式写起来很痛苦。我最后用的是EPPlus导出Excel加LiveCharts画趋势图操作员要报表就导Excel要在界面看趋势就LiveCharts。这样灵活度高也不用依赖ReportViewer的运行时。导出时注意Excel文件不要直接覆盖正在打开的文件会抛异常。我的做法是文件名带时间戳或者先写临时文件再File.Replace。另外大数据量导出用EPPlus的LoadFromCollection比逐行写快很多。5. 常见问题与排查技巧实录5.1 通信类问题速查现象可能原因排查步骤解决OPC UA连不上端点URL错、安全策略不匹配用UaExpert测试确认端点、关安全策略或导入证书Modbus读回值乱跳寄存器地址偏移、数据类型解析错用Modbus Poll对比核对手册地址、确认高低字相机回调崩溃c0000005回调中访问已释放内存看崩溃dump回调只拷贝数据不处理串口打开失败被其他程序占用设备管理器看端口关闭占用程序或换端口5.2 界面类问题WPF的DataGrid默认行高固定如果一格里内容多会显示不全。热词里有人问“WPF DataGrid一行变为两行显示”我的做法是设置RowHeightAuto并给单元格模板加TextWrappingWrap。但注意Auto行高在大数据量下性能差如果超过500行建议用固定行高加ToolTip显示完整内容。另一个常见问题是RichTextBox的Document不支持直接Binding。热词里“WPF如何在RichTextBox.Document上使用Binding”问的就是这个。我的方案是写一个附加属性监听Document的TextChanged事件手动同步到ViewModel的属性。虽然绕但能用。5.3 线程与异步的坑C#上位机里async/await用不好会死锁。典型场景UI线程调用SomeMethodAsync().Result而SomeMethodAsync内部又await了一个需要UI线程继续的操作互相等待就死锁。我的原则是UI事件处理里用async void仅限事件其他地方全用async Task绝不.Result或.Wait()。通信层的回调统一丢到Task.Run里处理。注意Dispatcher.Invoke是同步的会阻塞调用线程直到UI处理完。如果后台线程频繁InvokeUI线程会被压垮。高频更新用Dispatcher.BeginInvoke或者用CompositionTarget.Rendering做批量刷新。5.4 独家避坑技巧技巧一模拟器先行。在真实设备到位前我写了一个IDeviceSimulator接口用随机数和延时模拟PLC和机械臂的反馈。这样界面和状态机可以提前开发测试等设备到了只需切换实现类。省了至少两周的联调时间。技巧二日志分级且带上下文。用NLog配置FileTarget每条日志带ThreadId、StateName、WaferId。出问题时直接grep晶圆ID整条搬移链路一目了然。别用Console.WriteLine工业现场没人看控制台。技巧三参数配置加校验和版本。配方参数存SQLite时加一个Version字段和Checksum加载时校验。防止有人手动改数据库导致参数错乱。改参数必须通过界面界面里做范围校验比如速度不能超过机械臂额定值的80%。技巧四急停按钮的软件响应。硬件急停会切断动力电但上位机要第一时间感知并记录状态。我在PLC里把急停信号映射到一个OPC节点上位机订阅变化一旦为真立即停止状态机并弹窗。弹窗要用Topmost否则操作员可能看不到。6. 从项目里带走的经验这套系统从立项到交付用了四个月其中通信联调占了一个半月视觉定位调试占了一个月真正写业务逻辑的时间反而不多。半导体设备上位机的难点从来不在C#语法或WPF界面而在对工艺的理解和对异常场景的覆盖。你知道真空建立需要多久、机械臂加减速曲线怎么设、晶圆在什么情况下会滑片这些知识比会写多少行代码重要得多。如果让我重新做一遍我会更早地引入模拟器和自动化测试。搬移流程的状态迁移条件用单元测试覆盖比在现场一遍遍试要快得多。另外WPF的界面别追求花哨操作员要的是信息清晰、按钮大、报警醒目动画和渐变在工业场景里是减分项。最后分享一个调试小习惯每次改完通信或状态机代码先跑100次空搬移循环看内存有没有涨、日志有没有异常、节拍稳不稳定。工业软件不怕功能少就怕跑着跑着崩了。稳定永远排在功能前面。