
1. 项目从哪来晶圆与石墨岛搬移的业务场景1.1 晶圆搬移到底在搬什么做半导体设备的工程师对“晶圆搬移”这四个字应该都不陌生。它涵盖的范围其实很广晶圆从晶圆盒FOUP里被取出来放到预对准器上找平再送入工艺腔体完成后取出放到下一站这是最常见的搬移动作。还有一类是把晶圆从一片载盘上转移到另一片载盘上或者在不同热处理炉管之间进行移送。无论是哪一种背后都需要一套能跟机械手、升降轴、气缸协同工作的控制系统。除了底层的机械结构和电气回路上位机软件就是操作员和设备之间的“翻译官”。这套系统的核心目标其实很朴实一不能碰伤晶圆二不能放错位置三要跑得足够稳定。晶圆这玩意儿太娇贵了正面布满电路结构边缘还有倒角机械手夹持的位置稍有偏差轻则产生颗粒污染重则直接造成机械损伤。一颗晶圆从前道到后道经历几十道工序任何一次搬移事故都可能导致整批产品报废损失不是几十块钱的事是几十万甚至上百万的成本。所以在设计上位机之前你得先明白一个底层逻辑搬移系统的第一考核指标不是速度而是安全和稳定。1.2 石墨岛为什么单独拎出来说很多做通用自动化的人对“石墨岛”这个概念比较陌生。它是碳化硅SiC外延、高温退火等工艺环节常用的一种承载工具用高纯石墨材料制成形状类似一个圆形或方形的岛台晶圆直接放在岛面上送入反应腔。石墨岛要求耐高温、热膨胀系数小而且表面要经过特殊处理防止在高温工艺中释放杂质污染晶圆。为什么在搬移系统中要把石墨岛单独作为一个对象来管理因为石墨岛跟晶圆不一样它本身是工艺耗材需要定期更换、清洗、进出腔体搬运。更关键的是石墨岛在高温工艺中反复热胀冷缩之后放置在机械手上的位置会发生轻微偏移。如果上位机只按固定坐标去取放时间一长必然偏位。我参与过的项目里专门针对石墨岛增加了位移补偿机制每次取放前先通过传感器或视觉做一次位置修正再执行搬运动作。这一块做得好不好直接决定了设备长时间运行的良率和稳定性。1.3 上位机系统在产线中的角色有人会把上位机理解成“一个界面”觉得界面好看就行这是最常见的误区。真正的上位机系统是整台设备的逻辑调度中枢。它要管配方下发、管机械手动作时序、管传感器信号校验、管数据记录、管异常报警还要预留跟MES系统对接的接口。尤其是在半导体产线里设备所有动作都要有日志、有追溯一旦出现异常你得能从日志里还原当时每一步发生了什么。从软件架构上看这套系统大致可以拆成四层人机交互层、业务逻辑层、设备通信层和数据处理层。人机交互层负责操作员看到的界面业务逻辑层负责动作流程和防呆判断设备通信层负责跟 PLC、运动控制卡、机械手控制器打交道数据处理层负责配方、报警记录、运行日志的存取。四层之间尽量解耦互相不掺和。这样无论是界面改版还是换一套PLC型号都不会牵一发动全身。2. 为什么是 C# WPF技术选型的取舍2.1 不是不能选C但C#的时程太适合设备软件做工控的老工程师很多习惯用 C 或 C 来写上位机理由是效率高、底层可控。这个观点在十年前完全正确但现在情况变了半导体设备软件的复杂度在快速上升界面要求越来越高产线又要求快速迭代、快速交付。用C从零搭一套MFC界面光是把按钮、表格、图表这些控件调顺手就得耗掉不少时间更别提后期维护的成本。C#在.NET生态下开发效率确实高尤其是内存管理、LINQ、异步编程这些机制能帮人省掉大量重复劳动。在工控场景里一次指令交互往往就是毫秒到百毫秒级别使用 C# 编写的上位机在响应性能上完全够用。真正到了微秒级实时运动控制那是运动控制卡和伺服驱动器该干的活上位机只需要发出目标位置和速度不需要参与每一微秒的插补运算所以把“实时性焦虑”放下就好。2.2 WPF对比WinFormUI和业务解耦的底气我刚做上位机那会儿大家基本都还在用WinForm拖控件。WinForm胜在简单直接但界面一旦复杂起来代码全堆在窗体的cs文件里改一个按钮逻辑都可能牵扯到一堆控件事件时间一长就成了“屎山”。而WPF最大的优势是引入了XAML描述界面界面和业务代码天然分离再配合MVVM模式ViewModel只管业务状态和命令View只管界面显示和用户交互两边通过数据绑定连起来。这套机制搬到工控软件上简直是量身定做的。比如设备运行状态WinForm的做法是你在PLC数据变化时手动去更新每个控件的Text而WPF的做法是把状态放到ViewModel里通过INotifyPropertyChanged自动推送界面每个区域各绑各的互不干扰。晶圆搬移系统的界面通常需要同时展示机械手坐标、轴状态、真空压力、工艺腔体门状态、报警列表等几十个数据点用MVVM管理这些状态比WinForm那套“控件名.属性值”的方式清爽太多。2.3 整体架构分层与模块化设计当时搭这套系统我没有一上来就写代码而是先花几天把架构定下来。最终采用的方案是基础设施层公共的日志组件、配置管理组件、异常处理组件设备通信层负责运动控制卡、PLC、机械手控制器的通信协议封装对外提供统一的接口业务服务层管理动作流程、状态机、配方校验、防呆逻辑UI表现层WPF界面 MVVM按功能拆模块例如操作主页、手动调试页、参数配置页、报警历史页。层与层之间只通过接口引用不允许跨层调用。例如UI层不能直接去实例化一个Modbus通信对象必须通过业务服务层提供的接口去操作。这样做的好处后来在现场调试中体现得很明显某个轴的动作逻辑改了不用动界面代码界面要加一个按钮也不用担心影响通信线程。3. 五大核心模块的落地实现3.1 运动控制指令下发控制卡的封装思路先纠正一个概念上位机通常不会直接进行伺服电机的插补运算电机实时运动是由运动控制卡或者伺服驱动器来完成的。上位机要做的是把“机械手去A点、速度为20mm/s、到位后夹爪闭合”这类命令翻译成控制卡能识别的指令再打包发送出去。项目中用的是市面上比较常见的PCIe运动控制卡。厂家提供了C语言的动态库我们需要在C#里用DllImport做一层封装。封装时我给自己定了几个纪律所有控制卡的初始化逻辑放在系统启动模块一次性执行不要在每个窗口里重复初始化每一条运动指令都必须有返回值检查和状态反馈不能发完指令就认为动作一定成功运动控制相关的调用统一走一个后台线程队列避免界面线程直接阻塞。举个简单例子调用动态库里单轴点位运动的函数封装后大概是这个样子[DllImport(gts.dll, EntryPoint GT_AxisMove)] private static extern int AxisMove(ushort axis, double dist, uint targetPos); public bool MoveAxis(ushort axis, double distance) { int result AxisMove(axis, distance, 0); if (result ! 0) { _logger.Error($轴 {axis} 运动指令执行失败错误码 {result}); return false; } return true; }注意这里返回true只代表指令被控制卡接收了并不代表轴真的移动到位。真正判断到位必须靠回读编码器位置或者等待到位信号。我见过不少新人犯这个错指令发出去就执行下一步结果轴还没到位真空已经打开了轻则把晶圆带偏重则撞机。所以每次运动后必须轮询轴状态确认到位后再进入下一步流程。3.2 坐标体系管理晶圆盒数据与石墨岛偏置搬移系统的坐标管理是绝对的核心。我采用的方式是建立一个坐标配方表按照设备工位来组织上料台、预对准器、工艺腔1、工艺腔2、冷却台、下料台等等每个工位记录机械手接近点、取片点、放片点、安全高度等关键坐标。晶圆盒FOUP比较特殊它有多层槽位每一层的Z轴高度不一样。所以配方里不能简单存一个坐标要把X、Y、Z、R旋转角度按槽位编号循环计算出来。例如29槽FOUP每隔10mm一个高度那么第N槽的取片Z坐标就是基准高度加上N乘以槽间距。这个逻辑很简单但坑在于不同厂家FOUP的槽间距有细微差别我一般会在调试时用一个标准片做全槽位验证把所有槽位的实测Z值记录下来再反推出槽间距而不是盲信说明书。石墨岛的偏置补偿就更讲究了。石墨岛本身比较重又经历高温循环机械手把它从腔体里取出来以后位置可能有1到3毫米的漂移。如果直接按原坐标放回载台很可能蹭到定位销。我们的做法是在载台上加装了两个对射传感器当石墨岛放下来时通过传感器信号计算偏移量然后把偏移量写入配方的修正项。这样每放一次系统都会学习一次位置偏移长期运行也不会越跑越偏。3.3 防呆互锁与权限管理半导体设备的安全性设计怎么强调都不过分互锁逻辑是上位机必须做严的一环。举几个典型场景门锁未到位工艺腔体门没关好机械手绝对不允许伸进去取放片真空信号异常真空吸住了晶圆但压力值达不到设定阈值这时不能执行搬运动作轴位置冲突两个轴在空间上会干涉软件上必须做禁区判断防止机械碰撞未回原点禁止自动运行设备上电后所有轴必须先完成回原点动作才能允许进自动流程。这些互锁逻辑如果放在上位机层必须注意一个问题上位机软件崩溃了怎么办所以真正的安全硬互锁必须在下位机PLC或硬件电路里做上位机只做软件层面的防呆和提示不能把安全责任全部交给上位机。我当时的做法是PLC里做硬互锁上位机里做状态预判两边同时判断任何一个不满足都不会触发动作。权限管理这块容易被忽视。半导体产线对操作权限管理很严格一般分为操作员、工艺工程师、设备工程师、管理员四档。操作员只能进行启停和监控工艺工程师可以修改配方参数设备工程师可以手动控制每个轴和I/O。这个权限控制在WPF里做起来不复杂用一个全局登录状态 按钮的Command做绑定即可但要注意一点权限校验不能只在前端做后台的命令执行入口也要校验以防有人绕过界面直接操作。3.4 数据追溯每片晶圆都要有“档案”半导体行业有个硬性要求每一片晶圆的所有工艺参数、设备状态、搬移记录都要可追溯。这套搬移系统虽然只负责运输但在追溯链路上不能少数据。我在系统里设计了一个搬运记录表每次晶圆从进入设备到离开设备都会生成一条跟踪数据。存储的信息包括晶圆ID、FOUP槽位号、工艺配方名、各轴运动到的坐标、真空压力值、搬运时间、操作员账号、搬移结果等。数据量其实不小一条数据几十个字段一天几千片晶圆下来就是几十万条记录所以我用了SqlSugar这个轻量级ORM底层接的是工业现场常用的SQL Server开个定时任务把当天的搬运记录归档到独立分区表避免后续查询卡顿。数据追溯的价值平时看不出来一旦出现产品质量问题就需要靠设备日志和技术追踪报告来排查。比如客户反馈某种工艺下芯片有划伤我们拿记录一查发现特定腔体、特定槽位的真空压力在某一时间段一直偏低顺着这条线索很快定位到真空管路堵塞。没有追溯数据这种问题基本只能靠猜。3.5 配方管理不同产品换线不换程序半导体设备的另一个特点是要适应多种产品。不同尺寸的晶圆2英寸、4英寸、6英寸不同厚度的衬底搬移参数都不一样。如果每次换产品都去改代码既不现实也容易出事。所以配方管理系统必须做进上位机里。配方的设计思路是把一套完整的动作参数打包成一个文件包含工位坐标、速度、加速度、真空延时、吹气时间、超时时间等所有跟搬移过程相关的参数。操作员切换产品时只需要加载对应配方文件系统会校验配方参数是否在合理范围内有问题直接拒绝加载。配方文件我用JSON格式存储结构清晰方便工艺工程师在界面上直接编辑也方便从设备导出到另一台设备做参数复制。配方管理里有个很关键的校验环节当设备正在运行中时不允许加载新配方。必须在设备停止状态才能执行配方切换否则可能出现前一片晶圆用的是旧参数后一片晶圆突然变成新参数工艺状态不连续容易出质量问题。4. WPF上位机开发实战要点4.1 MVVM模式用CommunityToolkit.Mvvm搭底子以前用WPF做MVVM很多人用MvvmLight但这个库已经不怎么维护了新项目我建议直接用官方的 CommunityToolkit.Mvvm。它在源生成器方面做得很好写起来非常干净。举个例子设备状态栏需要显示当前正在执行的动作在ViewModel里定义一个属性public partial class MainViewModel : ObservableObject { [ObservableProperty] private string currentAction 空闲; [ObservableProperty] private bool isRunning; }界面上直接绑定TextBlock Text{Binding CurrentAction} /只要在业务逻辑里赋值CurrentAction 正在取片界面就会自动更新完全不需要手写事件。这套机制在工控软件里太舒服了几十个状态量全部靠这种绑定驱动界面代码量能减少一半以上。命令绑定也是一样的套路。比如“启动自动流程”按钮对应ViewModel里的一个RelayCommand可以在CanExecute里绑定启动条件设备没回原点或者门没关好时按钮自动置灰操作员想点也点不了。4.2 设备通信层的封装细节设备通信层是我花心思最多的地方。上位机要同时跟运动控制卡、PLC、机械手控制器通信还有可能对接传感器和MES系统。如果每个模块都自己去写Socket或串口操作代码会非常混乱。我统一封装成了三个服务类PlcService、MotionService、RobotService。以PLC的Modbus TCP通信为例我用的是网上开源的EasyModbus库但直接裸用会踩不少坑。通信不稳定时一个ReadHoldingRegisters可能抛出异常或者长时间阻塞。我的做法是封装一层带超时控制、自动重连、日志记录的通信类public class PlcService { private static readonly object LockObj new object(); private ModbusClient _client; public bool ReadBool(string blockName, int address) { lock (LockObj) { try { if (!_client.Connected) { Reconnect(); } bool[] values _client.ReadCoils(address, 1); return values[0]; } catch (Exception ex) { _logger.Error($读取PLC线圈异常地址 {address}, ex); return false; } } } }这里有几个细节想提醒大家所有通信操作加上全局锁不要让多个线程同时调用通信库很多通信库不是线程安全的并发调用会直接导致数据错乱每一次通信都做异常捕获不要相信 “之前没出过错所以以后也不会出错”工控现场电磁干扰、网线松动都很常见连接断了不要马上报错退出写一个简单的重连机制最多重试三次如果三次都失败再弹报警。4.3 异常处理与日志记录设备软件最怕的就是没日志。现场设备出了故障操作员可能只告诉你“刚才机器咔的一声停了”你如果没有日志就只能对着设备发呆。所以从一开始我就要求所有核心代码必须有日志而且日志里必须带上上下文信息。我用的是NLog配置很简单日志输出到文件同时按天滚动保留30天。日志分四级Debug、Info、Error、Fatal。正常情况下只记录Info调试时才切到Debug。每条日志我都尽量带上设备号、动作名称、关键参数值比如2025-06-18 10:23:15.123 [Info] 取片流程开始 | 晶圆ID: A0123 | FOUP槽位: 5 | 坐标: (120.5, 340.2, 45.0) 2025-06-18 10:23:15.876 [Info] X轴运动启动 | 目标位置: 120.5 | 速度: 20mm/s 2025-06-18 10:23:17.432 [Error] 真空压力不足 | 设定阈值: -60kPa | 实测值: -35kPa | 已中止取片这样出了任何问题翻日志就能还原现场不用让操作员去回忆“刚才按了什么”。除了日志我还加了全局未处理异常捕获当代码里出现没有兜住的异常时系统会弹一个友好提示框并把完整堆栈写到专用崩溃日志不会直接闪退。半导体设备现场如果动不动闪退操作员会直接打电话过来骂人的。5. 现场常见问题排查实录5.1 通信偶发超时/卡死这个可以说是工控上位机最常见的毛病。系统跑着跑着界面某个数据一直不刷新点击按钮也没反应过几秒又恢复了或者干脆一直卡住。按照我的排查经验第一个怀疑对象就是UI线程被阻塞了。很多人写代码图省事直接在按钮点击事件里写PLC.ReadHoldingRegisters()这个操作如果通信超时设置为3秒那么这3秒钟界面就是死的。因为按钮点击事件跑在UI线程上UI线程被一个同步的耗时操作占住整个窗体就无响应了。解决办法是所有的耗时通信都走异步方法或者后台线程private async void OnReadDataButtonClick(object sender, RoutedEventArgs e) { var result await Task.Run(() _plcService.ReadFloat(ProcessTemperature)); TemperatureValue result; }还有一种情况是通信库内部的连接状态没有管理好。Modbus TCP断开后直接调用读取方法有时候会抛异常而不是返回超时。这时候要做连接状态检查断开了先重连再读重连失败就抛给上层处理不让异常留在通信层。5.2 坐标漂移导致晶圆位置偏移坐标漂移这个问题在现场调试阶段几乎必然遇到。一开始跑得好好的几十片晶圆跑完发现晶圆在某个工位上的放置位置明显偏了甚至有几片出现了边缘擦碰。排查思路分几步走。先确认机械结构有没有松动联轴器、皮带这些机械部件时间长了会磨损这是硬件层面的问题。如果机械没问题就是软件上对原点或者编码器位置的校准出了问题。最常见的原因是机械手靠近传感器或限位开关的位置有干扰导致回原点时每次停留的位置其实不一样。软件层面我采取的方案是三重保险每片晶圆取放前做一次基准位置校验靠传感器或基准块确认当前机械手零点没有漂移定期执行自动回原点操作将累计误差清零关键工位的坐标做成闭环比对放片前后读取编码器实际值与设定值比对偏差超过阈值就报警停机。5.3 界面卡顿与线程冲突WPF有一条铁律UI元素只能在UI线程中修改。一旦你在后台线程里直接改了一个控件的Text系统就会抛异常让你根本跑不下去。不过有些时候你不会马上发现比如用了一些带有异步回调的库回调跑在线程池上你在回调里改了界面WPF在某些情况下会有跨线程调用的异常。实际开发中我尽量不在后台线程碰界面元素所有的界面的更新都通过数据绑定来自动完成。如果必须在后台线程里触发某个界面操作就用Dispatcher.Invoke或者把逻辑抛回UI上下文private void UpdateStatusOnUiThread(string message) { Application.Current.Dispatcher.Invoke(() { CurrentStatus message; }); }界面卡顿还有一个隐藏原因实时刷新频率太高。我见过有人用一个20毫秒的定时器去刷新坐标显示结果CPU占用率直接飙到30%以上界面拖拽都卡。后来我把坐标刷新降到了100毫秒一次界面毫无压力。人的肉眼根本分不清20毫秒和100毫秒的刷新差异但CPU的负载差了5倍。5.4 C# 操作 UI 线程的规范这部分算是我给团队立的一个开发纪律写出来给大家参考所有界面绑定数据源的属性都必须实现INotifyPropertyChanged不要用普通属性直接绑后台线程不要引用任何界面控件变量即使通过Dispatcher也不要最好把界面通讯封装成统一的事件总线长时间运行的循环任务比如状态轮询优先用Task.RunCancellationToken不要用Thread.Sleep暴力延时定时器能合并就合并不要让三个定时器同时跑尤其是UI线程的DispatcherTimer能少用就少用。6. 最后分享几个我踩过的坑文章写到这里想多说几句路上的体会。第一个坑是太相信控制卡厂商的demo代码。市面上的运动控制卡demo基本上都是控制台程序功能调通了就算完事。直接搬到WPF项目里就会发现各种问题线程上下文、消息循环、异常处理完全不一样。我的建议是厂商demo只用来验证硬件上层一定要按自己的框架重新封装别偷懒。第二个坑是现场调试前没有准备好“最小可运行版本”。半导体设备现场调试环境很吵、时间很紧你不可能在现场慢慢改代码。我之前有过一次到了客户现场才发现某个通信库的版本和工控机的操作系统不兼容现场手忙脚乱地装环境耽误了整整一天。现在我的习惯是把所有依赖打包成离线安装包通信库、数据库驱动、日志组件、运行库一应俱全到现场装完就能跑。第三个坑是忽略了操作习惯。你写的上位机最终用的是车间里的操作员他们不是程序员。界面上的按钮文案要直白状态颜色要统一报警弹窗要醒目。有一次我设计了一个“复位”按钮操作员理解成把设备恢复出厂设置吓得不敢按。后来我把命名改成“复位当前报警”并加了二次确认问题才解决。软件好不好用不是你觉得好用就好用而是操作员觉得好用才算数。这套晶圆与石墨岛搬移上位机系统到今天已经在好几条产线上稳定跑了大半年累计搬移晶圆超过十万片最大的收获不是代码本身而是理解了半导体行业对设备软件的苛刻要求稳定、可追溯、易维护。做这行的都知道设备一天不宕机不算本事一年不宕机才算入门。希望这篇文章能给做工控上位机或者准备踏入半导体设备行业的朋友一些参考少走几步弯路。