ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

C# WPF构建半导体晶圆搬移上位机:状态机、防错与架构实践

C# WPF构建半导体晶圆搬移上位机:状态机、防错与架构实践 接到这个项目的那一刻我就知道这活儿不是普通的“做界面”。产线节拍卡得死严机械手每动一次上位机状态机就得刷新一整轮。晶圆这东西你碰一下就是一个创收事故石墨岛又是高温工艺里反复进出的载具价值高还不经撞。整个系统最紧张的不是“动得多快”而是“不能错”。而所有防错、追溯、动作编排、设备联动的逻辑最终都落在上位机这一层。这篇文章我就把整个系统的设计过程和实现细节完整复盘一遍为什么选C#和WPF搬移任务怎么拆成状态机分层架构怎么搭防错机制怎么落地以及现场联调时踩过的一堆坑。1. 为什么这条产线需要一个上位机而且选了C#和WPF1.1 晶圆和石墨岛在产线上是什么关系先说场景。半导体前道工序里晶圆不是一直孤零零地在设备间传送的。像扩散、退火、镀膜这类工艺晶圆通常要装进石墨材质的载具里一起进出高温腔体。这种载具有的叫石墨舟有的现场干脆叫石墨岛本质上就是一块高强度、耐高温、经过精密加工的石墨托盘上面开好槽位一片片晶圆插进去整体搬运到工艺位。为什么不能让人手去拿因为12寸晶圆标准厚度只有775微米减薄片甚至能到100多微米比一张纸还脆。边缘磕一下、表面划一道整片就废了而一片晶圆从光刻到薄膜沉积流经这么多道工序之后的价值已经相当高。石墨岛也一样加工精度高、材料成本贵一旦被撞伤或者表面污染影响的不只是载具本身还可能在高温工艺里把整批晶圆带坏。所以搬移设备必须做到两点一是重复定位精度要稳二是任何异常都要能当场发现、当场停住。前者靠机械和运动控制后者靠传感器加上位机逻辑。这也解释了为什么一台看似简单的搬移机台最后要配一个功能完整的上位机软件。1.2 为什么不是单片机或者嵌入式方案有人会问搬个东西而已PLC加触摸屏不就行了确实很多简单设备靠PLC加HMI就能搞定。但这个项目的需求远远超过“按下启动走完流程”要跟MES系统对接实时上报每片晶圆的批次、槽位、动作结果要保存海量历史记录出货后出问题能倒查要支持配方管理不同产品、不同尺寸的晶圆对应不同的搬移参数要有多级权限、报警记录、数据曲线这些交互复杂的功能现场需求经常变今天加一个等待条件明天加一道复查动作。用梯形图或者HMI脚本去堆这些功能开发和维护成本会高到离谱。嵌入式裸机开发更是没必要——PC上位机在这个数据量和交互复杂度下开发效率、界面表现、二次迭代能力都是最优解。1.3 C#和WPF在同类方案里的位置我把当时考虑过的几条技术路线放在一起比过方案开发效率UI与数据绑定运动控制/PLC生态部署与维护C# WPF高强MVVM分离干净很成熟各类SDK都有简单可单文件发布C# WinForms中弱复杂绑定时代码量大成熟也简单但界面老C MFC低差强但写界面极痛苦依赖多维护累LabVIEW中一般仪器生态强PLC弱运行时庞大版本管理头疼Python PyQt中中上依赖第三方库实时性看场景打包麻烦现场环境易翻车C#赢在综合体验。语言本身有足够强的表达力写业务逻辑不罗嗦.NET的运行时和垃圾回收特性让程序在长期运行时更稳WPF的MVVM模式天然适合把“界面展示”和“设备控制逻辑”分开这在工控项目里特别重要因为界面经常要改底层逻辑最好纹丝不动。另外半导体设备商内部上位机采用C#的比例这几年明显增高社区里能参考的现成方案也多。设备联调遇到问题随便一搜就能找到前人踩坑的记录这一点对交付周期紧张的项目来说非常加分。1.4 最终采用的技术栈清单我最终敲定的技术栈是这样的开发环境Visual Studio 2022.NET 6后续升级到.NET 8UI框架WPF使用Prism做MVVM和模块化通信与PLC之间走Modbus TCP用NModbus4库与运动控制卡通过官方SDK调用曲线与图表OxyPlot用来显示真空度、位置偏差、温湿度等实时数据日志Serilog文件按天滚动保留至少90天数据存储SQLite存配置和历史记录JSON存配方文件部署单文件发布配合Windows服务做看门狗这个组合在后续几个月的开发、调试、验收里被验证是稳的。下面我按模块把核心设计讲清楚。2. 把“搬晶圆”这件小事拆成状态机2.1 一次搬移任务的动作序列很多人第一次做上位机最容易犯的错误是把工艺流程写成一大坨if else。比如“如果位置到了就开真空如果真空到位就抬升”写到后面自己都捋不清。搬移类任务虽然看起来动作简单但它是一个严格串行的过程每一步都以“上一动作的结果”作为前提。我实机上的一次完整搬移长这样开机回原点各轴归零装载位检测到片盒放置到位扫码枪读入批次号上位机根据配方判断取哪一片晶圆、放到石墨岛的哪个槽位龙门架移动到片盒目标片上方下降真空打开读取真空度模拟量判断吸嘴确实吸住晶圆抬升移动到石墨岛目标槽位上方下降到目标高度真空关闭二次判断晶圆确实释放抬升回到安全高度上报MES配方槽位标记为已放置。这8步里任何一步没有达到条件整个流程必须停下来报警提示操作员处理。如果中途真空吸住了但没吸稳或者放下之后没放到位后面全部白干。用状态机来建模这种流程再自然不过。2.2 状态机怎么设计才不臃肿我定义了一个枚举来列所有步骤public enum ProcessStep { Idle, HomeCheck, ReadWaferInfo, MoveToPickPosition, LowerToWafer, VacuumOn, CheckVacuum, LiftWithWafer, MoveToPlacePosition, LowerToSlot, VacuumOff, CheckRelease, LiftAfterPlace, UpdateMES, Completed }执行核心是一个简单但可靠的状态循环public async TaskStepResult ExecuteAsync(CancellationToken token) { while (_current ! ProcessStep.Completed !token.IsCancellationRequested) { var result await ExecuteStepAsync(_current); if (!result.Success) { Log.Error(步骤失败: {Step}, 原因: {Reason}, _current, result.Reason); return result; } _current result.Next; } return StepResult.Success(); }每个步骤的实际动作放在独立方法里返回成功或失败失败时必须带原因字符串方便报警界面直接显示。这里我特别强调一点不要为了状态机去引入重型工作流框架。搬移任务就十来步用最朴素的switchasync就能写得清晰可控。重框架反而会让现场维护的人看不懂。另外很重要的一环是超时看门狗。每一步执行前拿到一个超时时间如果动作发起后超过时限还没返回结果状态机要主动中断并进入急停联动流程。比如真空打开后正常1秒内真空度就应该下降到位如果3秒还没到位说明吸嘴堵了、密封圈漏气或者晶圆没对准。没有超时机制设备就会卡在那个动作上看着像“死机”了。2.3 与PLC和运动控制卡的通信设计这套系统里伺服轴的运动控制我走的是运动控制卡SDK而气缸、真空发生器、传感器这些IO通过PLC采集。上位机与PLC之间用Modbus TCP通信。我对Modbus的封装只有一个要求必须带超时、断线重连和日志。public class ModbusTcpClient { private readonly string _ip; private readonly int _port; private ModbusTcpMaster _master; private TcpClient _tcp; public async Taskbool ReadCoilAsync(int address, int timeoutMs 500) { try { var result await _master.ReadCoilsAsync(address, 1).WaitAsync(TimeSpan.FromMilliseconds(timeoutMs)); return result.First(); } catch (Exception ex) { Log.Error(读取Coil失败, 地址{Address}, 错误{Message}, address, ex.Message); return false; } } }WaitAsync这个API在.NET 6之后特别好用避免了老式ManualResetEvent那种手写超时的痛苦。我还会定时给PLC发心跳寄存器PLC那边在固定周期内没收到心跳值变化就把设备置为“通信故障”状态同时点亮急停灯。这样即使上位机进程崩溃设备侧也知道通信断了不会继续盲目动作。3. 分层架构让开发、调试、维护三方都不难受3.1 硬件抽象层所有设备都变成接口做过工控上位机的人都知道最怕的不是硬件坏而是硬件厂商的SDK换来换去。今天这家运动卡明天那家PLC如果业务代码直接调厂商SDK那换一次硬件就等于重写一遍软件。我的做法是加一层硬件抽象层HAL。运动控制卡、PLC、传感器全部抽象成接口public interface IMotionController { Taskbool MoveToPointAsync(string axis, double position, int timeoutMs); Taskbool HomeAsync(string axis); Taskbool IsInPositionAsync(string axis, double tolerance); } public interface IVacuumController { Taskbool TurnOnAsync(int channel); Taskbool TurnOffAsync(int channel); Taskdouble ReadPressureAsync(int channel); }界面上层只认接口。开发阶段我用一个模拟器实现这些接口鼠标点一下就走完整个流程UI和业务逻辑可以提前全部调通。等到硬件到场把模拟器换成真实驱动类改一行依赖注入注册系统直接跑起来。这个收益在项目后期特别明显因为硬件调试时间很短大部分软件问题早就被模拟环境过滤掉了。3.2 业务层动作编排与防错校验的落点业务层是状态机的家。它接收一个任务描述比如“取第5片晶圆放到石墨岛第3槽”然后内部按配方数据、当前设备状态决定走哪条动作链。我把所有防错校验也都放在这一层动作前检查对应轴是否回零、目标位置是否被占用、真空通道是否空闲动作中传感器读数是否在合理范围超时判断动作后把结果写入内存队列供界面展示和追溯模块消费。这样界面层永远不会因为一个“真空度异常”去自己判断该怎么做——它只需要把异常显示给操作员。真正的决策逻辑全部收敛在业务层。3.3 界面层与线程模型UI线程不卡顿的关键WPF的UI线程必须保持响应这是铁律。设备运动过程中操作员要能随时看到状态、点暂停、点急停。如果界面卡住哪怕只是半秒操作员的应急反应都会慢半拍。我定了几条规矩事件处理器里禁用Thread.Sleep需要等待一律用await Task.Delay所有硬件轮询放进后台任务结果通过事件或INotifyPropertyChanged通知界面长时间运行的设备动作全部支持CancellationToken操作员点停止时能及时退出异步事件方法必须try/catch否则异常会直接崩掉进程现场没法忍。一个典型的点击启动写法private async void StartButton_Click(object sender, RoutedEventArgs e) { try { StartButton.IsEnabled false; await _processService.ExecuteAsync(Cts.Token); } catch (OperationCanceledException) { StatusText.Text 用户主动停止; } catch (Exception ex) { Log.Error(ex, 启动流程异常); StatusText.Text 流程异常请查看日志; } finally { StartButton.IsEnabled true; } }数据绑定上我用Prism的BindableBase处理属性通知界面上几百个状态值都靠绑定自动刷新。这里有一个经验不要在一个ViewModel里塞太多属性按功能拆成多个小ViewModel再用聚合根组装。否则一个页面三五十个属性写起来很爽后期查问题能查到怀疑人生。4. 防错设计是上位机的灵魂4.1 软件参与防错的三个层次防错不是靠某一个功能而是分三层协同第一层是硬件信号互锁。急停按钮、安全光栅、门锁开关这些必须硬接线进PLC不依赖任何软件逻辑。这个没得商量哪怕上位机崩溃、通信中断设备都必须能停下来。第二层是软件状态校验。这是上位机的主场。每一步动作之前我都要求读回实际状态而不是“发了指令就当执行了”。比如气缸伸出不是上位机写一个线圈就完事而是等待气缸磁性开关反馈再等一个稳定时间才算动作完成。真空更严格不能只看真空开关通没通要读模拟量真空度连续0.5秒低于阈值才算吸稳。第三层是数据追溯和异常分析。每片晶圆的完整搬移记录、设备参数、报警事件、操作员账号全部存下来出问题之后能还原当时的现场。4.2 真空异常案例分析险情是怎么被软件拦下的项目调试过程中真出过一起险情。夜班操作员报告机械手把晶圆放到石墨岛上之后下一片晶圆已经吸起来了真空传感器才在界面弹了一个“真空度偏低”的报警。排查时我先看了日志发现现场并不是真空度直接掉到零而是吸嘴在放片后的短暂瞬间真空度掉得不够彻底数值介于“真空开启”和“真空关闭”之间而老代码只判断开关量“真空开关是否接通”于是把“已经不吸了”误判成“还吸着”。根因清楚了我做了两处修改一是增加稳定时间窗真空度必须连续500毫秒超过阈值才算释放确认二是从“单一开关量判断”改成“模拟量数值状态判断”两者共同决定结果。这个改动上线后再没出过同类问题。这类排查有一个通用思路不要只看界面报警要去日志里看原始数值曲线。上位机记录的数据越细排查越快。这也是为什么我后来把OxyPlot曲线直接放到了界面上操作员能实时看到真空度变化波形比看抽象的状态文字直观太多。4.3 数据追溯出问题之后能说得清半导体行业客户对追溯的要求非常高。每片晶圆的批次号、片号、放置槽位、操作员、设备状态、真空度曲线、关键动作时间戳这些数据必须能够随时导出来形成报告。我用的方案是SQLite存结构化数据日志用Serilog存文本两者通过一个全局的批次号关联。报告导出用了iText7把文本、图片、曲线分层输出到PDF的指定区域。这里有个小提示iText7的坐标是左下角原点跟WPF的左上角原点完全相反刚开始写标注位置的代码大概率会画反先把坐标换算函数写好再动手。另外所有关键操作都有操作员权限校验和二次确认。取消、回退、手动模式切换这类动作必须刷工卡或者输入密码。操作日志记录的不只是“谁做了什么”还有“谁试图做什么但被拒绝了”后者在调查争议时尤其有用。5. 现场高频问题排查实录5.1 VS2022里WPF模板不见了项目期间同事就遇到过这个事Visual Studio 2022装好了新建项目的时候搜索WPF结果一个模板都没有。这个问题基本不是VS坏了而是安装时没有勾选“.NET 桌面开发”工作负载。解决办法很简单打开Visual Studio Installer点“修改”勾选“.NET 桌面开发”然后等它装完重启VS模板就回来了。如果你要建的是.NET 6/7/8的WPF项目还要确认对应版本的.NET SDK已经安装。另外提醒一下.NET Framework版的WPF模板和.NET版模板是分开列出的搜索时不要只看“WPF应用程序”这一个名字看清后缀再说。5.2 Prism框架下按钮命令不触发另一个高频问题是WPF按钮绑定了DelegateCommand但点了没反应。我排查过好几次原因基本集中在三类第一DataContext没设置。这个最常见界面元素拿不到ViewModel对象命令绑定自然不生效。调试时看输出窗口有没有“BindingExpression path error”的提示一抓一个准。第二CanExecute返回false而且没刷新。Prism的DelegateCommand如果CanExecute一直返回false按钮就是灰的或者点了没反应。关键是当状态变化时要调用RaiseCanExecuteChanged()。有些场景是异步状态变化比如后台线程更新了某个属性这时候命令刷新和界面更新都要通过UI线程。第三CommandParameter为null。有的命令逻辑会判断参数参数没传进来就直接返回看着就像“命令没触发”。我的建议是先在命令方法第一行打日志看它到底有没有进来。日志会告诉你问题出在“绑定层”还是“业务层”省得瞎猜。5.3 UI线程卡死的经典成因上位机卡死是现场最不能忍的问题。我接手过一个模块现象是界面点了“启动”之后整个窗口拖不动过了几秒才好。一查代码有人在事件处理里写了while(true){ Thread.Sleep(50); }轮询传感器状态。这种写法在控制台程序里没事在WPF里就是灾难。UI线程被Sleep占住消息泵没法处理鼠标键盘消息界面自然卡死。正确做法是把轮询放到后台循环UI线程只负责接收通知、更新界面。还要注意后台任务不要无限while循环里直接操作控件——跨线程访问控件必须走Dispatcher。5.4 加分功能3D界面与实时曲线客户如果想让设备看起来更有科技感WPF的Viewport3D可以做简易3D机台预览。有人问“用滚动条控制PerspectiveCamera”其实就是在滚动条事件里改相机位置坐标做法不复杂但3D模型和工艺位的精确对应比较费时间要提前评估投入产出比。实时曲线用OxyPlot就够了。我做了真空度、轴位置偏差、温度三个趋势图刷新周期200毫秒运行半年下来非常稳定。注意OxyPlot的更新时间要放在UI线程大批量数据点更新时要做抽样不然图形会卡。6. 一套可以拿回去直接改的骨架代码6.1 主界面与ViewModel的骨架这里给一个最小可用的结构。主界面不放业务逻辑只有几个按钮、状态文本和DataGridStackPanel Button Content启动流程 Command{Binding StartCommand}/ Button Content停止 Command{Binding StopCommand}/ TextBlock Text{Binding StatusText}/ DataGrid ItemsSource{Binding WaferRecords} AutoGenerateColumnsFalse DataGridColumn Header晶圆ID Binding{Binding WaferId}/ DataGridColumn Header槽位 Binding{Binding SlotNo}/ DataGridColumn Header结果 Binding{Binding Result}/ /DataGrid /StackPanelViewModel侧用Prism的BindableBase和DelegateCommandpublic class MainViewModel : BindableBase { private readonly IProcessService _processService; public DelegateCommand StartCommand { get; } public DelegateCommand StopCommand { get; } public MainViewModel(IProcessService processService) { _processService processService; StartCommand new DelegateCommand(Start, CanStart); StopCommand new DelegateCommand(Stop, CanStop); } private async void Start() { ... } }在Prism里注册依赖关系之后ViewModel的构造由容器解析不需要自己new。项目入口重写CreateShell和RegisterTypes即可。6.2 状态机执行核心状态机的核心就是“当前步 - 执行 - 返回下一步”我把它简化成下面这个模式private async TaskStepResult ExecuteStepAsync(ProcessStep step) { switch (step) { case ProcessStep.HomeCheck: return await SafeExecuteAsync(async () { return await _motion.HomeAsync(X) await _motion.HomeAsync(Y); }, ProcessStep.ReadWaferInfo); case ProcessStep.VacuumOn: return await SafeExecuteAsync(async () { await _vacuum.TurnOnAsync(1); await Task.Delay(500); var pressure await _vacuum.ReadPressureAsync(1); return pressure 30; // 阈值具体值按实际真空发生器标定 }, ProcessStep.CheckVacuum); default: return StepResult.Fail(未知步骤); } }SafeExecuteAsync统一处理异常和日志让步进代码保持干净private async TaskStepResult SafeExecuteAsync(FuncTaskbool action, ProcessStep next) { try { var success await action(); return success ? StepResult.Next(next) : StepResult.Fail(动作执行失败); } catch (Exception ex) { Log.Error(ex, 步骤执行异常); return StepResult.Fail(ex.Message); } }这套代码在实际项目里被验证是足够稳的。当流程步骤有增删时只需要改枚举和对应case不会牵连其他模块。6.3 ModbusTCP通信封装通信层的封装核心是超时、日志、断线重连public class PlcService { private readonly ModbusTcpClient _client; public async Taskbool WriteRegisterAsync(ushort address, ushort value) { try { await _client.GetMaster().WriteSingleRegisterAsync(address, value) .WaitAsync(TimeSpan.FromSeconds(2)); return true; } catch (Exception ex) { Log.Error(ex, 写寄存器失败); await ReconnectAsync(); return false; } } }断线重连一定要做因为现场工业网偶尔会抖动。但要注意重连频率不能一掉线就疯狂重连搞成自旋。我用的策略是第一次重连等500ms连续失败每次翻倍最大间隔10秒同时上报状态给界面“PLC通信异常”变成红色背景提示。7. 从联调到交付我总结的几条“不要”7.1 不要相信“偶尔丢失的帧”工业通信里最坑的就是“偶尔”。Modbus TCP偶尔超时一次Profibus偶尔丢一帧串口偶尔乱码。不要抱着“这个概率很低出了事重启就好”的心态。所有通信必须有应答、有重试、有超时。我把每次通信都打了日志上线第一个月就抓到过两次网络波动导致的瞬时断连后来加自动重连之后现场再也没有因为通信问题停机。7.2 不要拍脑袋设延时吸真空要延时多久气缸伸出要等多久这些数字必须来自实测不是拍脑袋。我的做法是测10次取最大值的1.5倍再加200ms余量同时把延时做成配方参数现场微调不需要重新编译。这样做的另一个好处是当你发现某个延时要频繁增大时大概率是气缸或者真空回路开始磨损了提前预警。7.3 不要把操作员当不会点错的人只要是人操作就一定会点错。关键按钮要有确认弹窗权限分级要落实操作日志要记录。但更重要的一点是权限不能妨碍应急处置。急停、复位、手动模式应该在任何情况下都可用。设计上要把“安全优先级高于权限优先级”作为铁律。7.4 交付之后留下的文件软件交付不只是代码库。I/O点表、通信寄存器表、配方参数说明、操作手册、软件架构说明这五样东西缺一不可。很多上位机项目后期维护困难不是代码写得烂而是没有文档。尤其是I/O点表半年后你去排查一个传感器无信号的问题没有点表就只能现场一根根线去对。这套系统上线之后我最深的体会是上位机在一个搬移设备里的角色远比“操作界面”要大它其实是整台设备的逻辑中枢和记忆库。把状态机理清、把防错做足、把数据存好比任何花哨的界面都重要。如果你也在做类似的搬移类上位机项目希望这份复盘能帮你少踩几个坑。
返回列表