C# WPF半导体设备晶圆与石墨岛搬移上位机系统实战

C# WPF半导体设备晶圆与石墨岛搬移上位机系统实战 做半导体设备上位机这几年碰过最多的设备不是那种花里花哨的桌面软件而是看起来平平无奇、实际上承担整条产线节拍的搬移类机台。这里说的搬移不只是普通传送带上的物料流转而是晶圆和石墨岛之间的精确取放、传输和回传。我在接手这类 C# WPF 上位机项目前也以为无非是“读传感器、发脉冲、显示状态”真正做完一版能稳定跑产线的系统后才发现坑远比想象中多。这篇文章就把我搭建这套半导体晶圆与石墨岛搬移上位机系统时的需求拆解、架构取舍、关键实现和实战踩坑完整记录下来给正在做同类设备的软件工程师一点参考。1. 先拆业务晶圆/石墨岛搬移系统的真实工况与上位机职责1.1 搬什么、从哪里到哪里设备动作流程晶圆在热处理、薄膜沉积这类工艺里不是一片一片单独进出腔室的通常会把一批晶圆放在石墨舟或者石墨载盘里一起送入工艺炉管。有些设备的载具设计得像一个小岛中间是石墨基座四周有支撑柱业内干脆叫它石墨岛。上位机要管的就是在上下料台、缓存工位、工艺腔室、冷却工位之间把晶圆和石墨岛按流程搬来搬去。一套典型的搬移动作大概是这样的人工或者上游设备把装有晶圆的石墨岛放到上料台上位机确认物料到位后通知机械手夹具夹取石墨岛上升到安全高度水平移动到工艺腔室门口等待腔室门打开再下降、放置、退回。工艺完成后机械手再反向把石墨岛从腔室搬运到下料台。中间还可能经过冷却台、缓存台、翻转台每个工位都有传感器、气缸、真空吸盘、升降机构要配合。做这类系统的上位机核心职责不是去控制每一个电机而是把设备当成一个有“状态”的有机体来管理知道物料在哪、要去哪、现在能不能动、动了之后有没有异常。动作之间的时序和互锁比单纯画界面要重要得多。1.2 上位机、PLC、运动控制卡三者如何分工很多刚入行的人会问为什么不用 PLC 一把梭核心原因是节拍和点位精度。石墨岛的定位精度通常是毫米级甚至亚毫米级体积又大需要多个轴协同运动用通用 PLC 写点位插补不是不行但开发效率低后期示教点位也麻烦。我这里采用的运动控制方案是“上位机 运动控制卡 远程 IO 传感器”PLC 只做一部分安全回路和气缸阀岛控制。三者分工大概是这样的模块负责内容为什么这么分上位机C#流程调度、状态管理、点位管理、报警处理、MES 交互、UI逻辑复杂、变更多适合用高级语言快速迭代运动控制卡直线轴/旋转轴的插补、回零、限位、到位判断实时性强供应商 SDK 里都封装好了PLC / IO 模块气缸、真空、门开关、光幕、急停等开关量安全链路相对固定交给可靠逻辑控制这样分之后上位机通过调用运动控制卡的 SDK 来发运动指令通过 Modbus TCP 或 TCP Socket 去读写 PLC 的寄存器来获取气缸和传感器状态再把这些信息汇总成一个统一的设备模型。1.3 需求里的隐藏点节拍、追溯、联锁需求文档上写“实现晶圆与石墨岛搬移”这十几个字拆开之后全是细节。第一个隐藏点是节拍。产线上整个工艺循环大概多长时间机械手搬一次要多久上下料台能不能提前准备好这些直接决定流程能不能并行。第二个隐藏点是追溯。石墨岛用了多少次、是哪一批晶圆、在哪个腔室做过工艺都要记录否则出了问题查不到批次。第三个隐藏点是联锁。腔室门没开到位绝对不能伸进去真空没建立绝对不能松开夹具光幕被遮挡绝对不能让轴动这些不只是写在代码 if 里还要有独立的安全回路。所以在动手写第一行代码之前我习惯先画一张“工位-动作-条件”表格把每个动作的前提条件、完成后的变化都列出来后面写状态机的时候直接照着抄。2. 架构与技术选型为什么 C# WPF 适合这类系统2.1 上位机软件的分层界面、业务、驱动三层的边界项目一开始最容易犯的错误是把所有代码堆在窗口的按钮点击事件里。比如点击“自动运行”按钮里面直接写打开控制卡、读传感器、发运动指令、更新 TextBox。看起来很快但产线上一出问题根本定位不到是界面问题还是运动逻辑问题。我做这类系统时固定分成三层驱动层、业务层、界面层。驱动层只做一件事封装硬件通信向上层提供“MoveAbs”“ReadInput”“WriteOutput”这类方法不包含任何流程判断。业务层负责状态机、动作序列、防碰撞逻辑、报警判定它只面向驱动层的接口不关心界面怎么显示。界面层只负责展示和接收用户操作把业务层抛出来的状态和事件绑定到 WPF 控件上。这样做最大的好处是可以单独测试业务层。我用一个模拟驱动类去替代真实控制卡在不上机台的情况下就能把搬移流程的时序逻辑跑一遍。等到了现场只替换驱动层的实现上面的流程代码一行不用改。2.2 实时通信选型控制卡 SDK、TCP/Modbus、OPC UA半导体设备现场通信协议远比想象中杂。运动控制卡一般自带 Windows 动态库C# 里头用 P/Invoke 调用PLC 和远程 IO 模块多数支持 Modbus TCP有些新设备支持 EtherCAT但上位机要拿到 EtherCAT 数据通常还是通过控制卡或者网关转成 TCP/Modbus再往上MES 系统的交互经常走 OPC UA 或者 HTTP REST。我的选型原则是能用标准协议尽量用标准协议少碰厂商私有协议。比如远程 IO 模块我优先选支持 Modbus TCP 的这样用一个通用的 Modbus 客户端库就能搞定不依赖厂商 SDK。如果某些特殊的真空计、质量流量计只有串口协议就单独写一个驱动服务统一暴露成接口。下面是一段简化后的 Modbus TCP 读写封装思路public class ModbusTcpClient { private readonly IModbusClient _client; public async Taskbool ReadCoilAsync(byte unitId, ushort address, CancellationToken ct) { // 根据所选库调用例如 NModbus4 var result await _client.ReadCoilsAsync(unitId, address, 1, ct); return result.First(); } public async Task WriteCoilAsync(byte unitId, ushort address, bool value, CancellationToken ct) { await _client.WriteSingleCoilAsync(unitId, address, value, ct); } }因为通信的异常是不可避免的所以驱动层所有方法都要考虑超时和重试而且要把异常往上抛让业务层决定是暂停流程还是进入安全状态。2.3 框架选择Prism/MVVM 的实践尺度WPF 本身没有强制 MVVM但这类多界面、多状态、频繁更新的上位机如果不用 MVVM代码很快就变成一团乱麻。我用过 Prism 也用过 CommunityToolkit.Mvvm实际项目里更推荐后者轻量没有太多魔法容易控制。不过我要强调一点上位机不是所有地方都适合纯 MVVM。比如实时监控画面里机械手的坐标位置每 20 毫秒更新一次如果通过 ViewModel 绑定的方式刷新会频繁触发属性通知效率并不高。我的做法是常规按钮、状态文字、报警列表用 MVVM高频坐标和示教操作允许在 View 的后台代码里直接调用业务层接口再用手动方式写入绘图元素。这样既保住了界面逻辑的可维护性又不会把性能浪费在绑定管道上。3. 运动控制与流程调度的关键实现3.1 点位管理示教、映射、坐标系变换石墨岛搬移系统最基础的数据就是各个工位的取放料点位。实践中点位不能写死在代码里因为机械安装有误差传感器位置可能微调产线工程师需要在不改软件的情况下重新示教。我的做法是用点位表来管理。每个点位包含轴位置、移动到该点位的速度、加速度以及一个文本描述。点位表存在一个 JSON 或者数据库中界面上提供示教模式操作员选定某轴用点动按钮移动到合适位置点“保存”就把当前坐标写回点位表。更复杂的情况是机械手夹具具有多个吸盘石墨岛放在不同高度的工位上。这时候点位不只是 XYZ 坐标还要加上夹具旋转角度、Z 轴下探距离。为了减少示教工作量我会按“目标工位”而不是“目标坐标”来组织点位每个工位有一组取料点和放料点业务层根据工位号自动查表。public class PointTableItem { public string Id { get; set; } public string Name { get; set; } public double X { get; set; } public double Y { get; set; } public double Z { get; set; } public double Rot { get; set; } public long Speed { get; set; } // 单位 mm/min public long Accel { get; set; } }坐标系变换也是这里容易忽略的点。机械手的基准点、夹具中心、石墨岛中心可能不重合如果不做位置补偿就会出现“看起来放到位了实际上把石墨岛别住了”。我通常会预先在软件里定义工具坐标系和工件坐标系示教时直接示教工件坐标运行时通过矩阵变换换算成各轴的实际目标位置。这样产线只关心工装摆放不关心机械结构细节。3.2 轴指令封装与 PLC 交互代码示例运动控制卡的 SDK 往往是 C 接口要把它封装成 C# 服务。比如某款控制卡的移动指令是int move_abs(int axis, double pos, int speed, int accel)我封装一层public class MotionService { [DllImport(MotionLib.dll, CallingConvention CallingConvention.Cdecl)] private static extern int move_abs(int axis, double pos, int speed, int accel); [DllImport(MotionLib.dll, CallingConvention CallingConvention.Cdecl)] private static extern int get_position(int axis); public void MoveToPosition(string pointId, CancellationToken ct) { var p _pointTable.GetPoint(pointId); CheckCanMove(); // 业务层联锁判断 foreach (var axis in p.Axes) { int ret move_abs(axis.AxisId, axis.Position, axis.Speed, axis.Accel); if (ret ! 0) throw new MotionException($轴 {axis.AxisId} 启动失败: {ret}); } // 等待所有轴到位 WaitAllInPosition(_pointTable.GetAxes(pointId), 5000, ct); } }这里的WaitAllInPosition不只是简单轮询位置还要同时监测报警状态。如果某轴处于限位、跟丢或驱动器报警必须立刻取消流程。和 PLC 的交互我通常把“条件是否满足”放到 PLC 里去判断因为安全回路的响应时间要远快于软件轮询。比如“腔室门未关到位”PLC 直接切断运动使能上位机只要读达成状态即可。但有些气缸没有硬互锁就需要上位机自己判断顺序这类逻辑我是通过 Modbus 读写线圈实现的public async Taskbool IsVacuumReadyAsync(CancellationToken ct) { return await _plc.ReadCoilAsync(unitId: 1, address: 0x100, ct); } public async Task EngageGripperAsync(CancellationToken ct) { await _plc.WriteCoilAsync(unitId: 1, address: 0x101, true, ct); // 等待到位反馈而不是立即返回 for (int i 0; i 10; i) { if (await _plc.ReadCoilAsync(unitId: 1, address: 0x102, ct)) return; await Task.Delay(200, ct); } throw new TimeoutException(夹具真空反馈超时); }这段代码里有个容易被新手的忽略点发出气缸动作指令后不能马上开始下一步必须等待反馈信号返回而且要有超时机制。否则气缸还没到位机械手就已经开始运动轻则剐蹭重则撞坏石墨岛。3.3 多工位状态机与防碰撞调度搬移系统往往不止一个机械手在干活。比如上料台侧有机器人 A腔室侧有机器人 B中间通过缓存台交接。还有的机台是一个龙门机械手负责多个腔室。这时候就需要用状态机把每个工位、每个机械手的状态管理起来。我把每个物理实体建模为一个状态对象状态可以从“空闲”“运行中”“等待条件”“报警”“急停”等状态之间迁移。搬移任务进入一个调度队列由调度器统一分配。调度器要把“工位是否空闲”和“机械手是否可达”作为两个独立条件来检查。比如一个典型的“从缓存台取石墨岛到工艺腔室”的任务业务层处理逻辑大致是public async Task ExecuteTransferAsync(CancellationToken ct) { await _scheduler.WaitSlotAsync(CancellationToken.None); // 限制并发任务数 try { if (!await IsChamberDoorClosedAsync(ct)) throw new SafetyException(腔室门未关禁止搬入); await _motion.MoveToPoint(CachePickReady, ct); // 气缸到位、真空吸附 await EngageGripperAsync(ct); await _motion.MoveToPoint(CachePickAbove, ct); // 移动到腔室门口 await _motion.MoveToPoint(ChamberWait, ct); if (!await IsChamberDoorOpenAsync(ct)) throw new SafetyException(腔室门未打开); await _motion.MoveToPoint(ChamberPlace, ct); await ReleaseGripperAsync(ct); await _motion.MoveToPoint(ChamberRetract, ct); } finally { _scheduler.Release(); } }这里的_scheduler我用SemaphoreSlim实现保证同一时间进入危险区域的搬移任务只有配置好的数量。防碰撞不只是软件层面还要在点位层做“占用区”判断一个工位如果是“占用中”其他任务就不能把它作为目标或途经点。3.4 异步、超时与异常处理让搬移动作“知道什么时候失败”工业软件最怕的不是报错而是“卡住不动却没有提示”。我刚开始写的时候很多函数是同步阻塞的一旦控制卡通信卡死整个软件就假死操作员不知道是设备坏了还是程序死了。后来我把所有运动交互改成异步并且给关键动作设置超时。比如“等待轴到位”这个操作在标准情况下应该在 3 秒内完成如果超过 5 秒还没到位就取消当前任务并弹报警。使用CancellationTokenSource可以很方便地实现超时取消using var cts new CancellationTokenSource(TimeSpan.FromSeconds(5)); try { await _motion.WaitAllInPosition(axisIds, 5000, cts.Token); } catch (OperationCanceledException) { await _motion.StopAllAxesAsync(); _alarmService.Raise(轴到位超时已停止所有运动); }异常处理也不能只弹一个 MessageBox而是要把异常对象、当时的步骤、设备状态一起记录到日志里方便后面查原因。4. WPF 界面让操作员一眼看清当前物料在哪台设备4.1 上位机界面的设计原则高频信息一目了然很多设备软件界面花花绿绿反而不好用。搬移设备操作员最关心的是三件事当前是什么模式、物料在哪个工位、有没有报警。我把主界面设计成三栏结构左侧是设备状态总览中间是物料跟踪图右侧是操作按钮和报警摘要。所有颜色遵循一个原则正常是绿色或灰色动作中是黄色报警是红色急停是红色闪烁。背景色用深色或者浅色都可以但要注意产线环境光线较强界面对比度要高。字号也要偏大因为操作员往往是隔着一米多看的不能按办公室软件的默认 12 号字设计。这个细节经常被人忽略但操作员用下来的反馈都很好。4.2 物料位置实时跟踪Canvas Binding 实现车间布局图要直观展示石墨岛和晶圆的位置我用 WPF 的 Canvas 画了一个设备俯视图按照真实比例放置上料台、缓存台、腔室、机械手活动范围。每个物理对象对应一个自定义控件位置绑定到 ViewModel 里的坐标属性。由于位置更新频率不高我直接使用普通属性加INotifyPropertyChanged就够了。如果机械手移动轨迹要动态显示就在后台代码里每隔几十毫秒读取一次实时坐标然后更新 Canvas 元素的位置。这里不要把所有坐标变化都通过 MVVM 绑定否则会产生大量垃圾对象。简单直接的后台写法是private void OnMotionPositionUpdated(double x, double y, double z) { // 更新机械手图标在 Canvas 上的位置 ManipulatorIcon.SetValue(Canvas.LeftProperty, x); ManipulatorIcon.SetValue(Canvas.TopProperty, y); }物料跟踪图还有一个作用操作员不需要打开数据报表只看图就能知道某个石墨岛在哪是否需要人工干预。4.3 报警与操作权限安全互锁的人机交互报警不能只是界面变红还要把报警分类、解除方式和是否需要确认区分开。比如“真空压力不足”这种报警需要操作员确认真空恢复正常后再手工清除“光幕触发”报警则要求操作员离开光幕区域并且重新按下复位按钮才能继续。我会把报警做成一个列表每一行记录报警时间、报警代码、中文描述、当前状态并提供“报警复位”按钮只有满足条件时才允许点击。权限管理也是必需品。操作员只能执行“启动自动流程”“暂停流程”工程师可以进入示教模式、修改点位参数管理员才能修改配方和系统配置。WPF 里我简单地做一个登录窗口登录后把当前用户的角色放到全局 session 里按钮和菜单绑定到权限规则。这个逻辑不难难的是界面上每个可操作控件都要有权限校验不能只隐藏不校验因为隐藏的按钮也可以通过后台代码被调用。4.4 UI 性能优化大数据量刷新不卡顿的实践WPF 上位机经常会遇到“日志列表刷新过快导致界面卡顿”“示教画面拖动卡顿”这类问题。我踩过一个大坑把收到的每一条实时数据都直接放到 ObservableCollection结果 UI 线程忙不过来连急停操作都响应慢。后来我做了三层处理日志和传感器数据先进入内存队列使用定时器每 100 毫秒批量刷新一次 UI。列表控件使用虚拟化VirtualizingStackPanel.IsVirtualizingTrue。高频属性通知只更新必要控件不再触发整棵可视化树刷新。实测下来界面响应速度明显提升CPU 占用也降了不少。5. 数据追溯与 MES 对接5.1 晶圆 ID 与石墨岛身份绑定石墨岛在设备里会被反复使用每批晶圆是哪几个石墨岛承载的、工艺参数是什么都需要记录。上位机通过扫码枪或者 RFID 读头获取晶圆盒和石墨岛的身份信息建立绑定关系。我用的方案是在上料台装一个扫码枪加 RFID 天线物料到位后自动读取 ID。读取成功后软件把“石墨岛 ID 晶圆批次号 当前时间”写入当前批次记录。后续每一次搬移动作都把动作类型、目标工位、时间戳、操作模式追加到这条记录里。这样出了品质问题可以倒查每一个环节。public class MoveRecord { public long Id { get; set; } public string CarrierId { get; set; } public string LotId { get; set; } public string ActionType { get; set; } // Load / Unload / Transfer public string FromStation { get; set; } public string ToStation { get; set; } public DateTime Timestamp { get; set; } }5.2 本地数据库与 MES 接口的设计本地数据我先用 SQLite 记录所有搬移记录因为部署简单、不需要额外安装数据库服务。MES 交互如果有实时性要求就通过 HTTP REST 接口推送没有实时要求就在本地记录后定时批量上报。为了不让本地数据库成为瓶颈我使用了异步写入队列。业务层只把记录放到内存队列后台线程负责批量写入 SQLite并处理写入失败后的重试。这样即使 MES 临时故障也不会影响设备本体的自动化流程。5.3 报表与历史数据导出产线工程师经常要按批次查询搬移历史或者按时间导出报警记录。我在 WPF 里做了一个报表页面支持按时间范围、批次号、工位号筛选结果可以导出为 CSV方便在 Excel 里进一步分析。报表查询因为数据量不大直接使用 SQL 查询内存表即可。这里要注意的一个细节是SQLite 的时间字段最好存 UTC 而不是本地时间否则跨班次查询会有 8 小时偏差。6. 实战踩坑记录从调试机到产线哪些问题最消耗时间6.1 与运动控制卡交互的 P/Invoke 坑控制卡 SDK 是 C 接口我在 C# 里 DllImport 调用时遇到过两个典型案例。一个是字符集问题控制卡回传错误信息用的 char 数组如果 C# 侧默认使用 Unicode 封送读出来的字符串就会乱码。解决方法是在 DllImport 声明里指定CharSet CharSet.Ansi。另一个是回调函数控制卡在某个轴到位后会触发回调这个回调线程不是 UI 线程如果直接在回调里操作控件就会抛异常。我的做法是回调里只做一件事——把事件放进ConcurrentQueue由专门的事件分发线程负责处理。6.2 线体联锁的时序冲突传感器反馈与指令完成的竞态有一次调试时机械手在腔室里放完石墨岛后立刻收到“真空松开”指令但此时机械手其实还没有完全退出腔室结果石墨岛被带偏了一点。调查发现问题出在我的代码里放料动作完成后我直接执行了松开夹具的指令没有等待机械手移动到安全高度。而在前一个工位因为这个安全高度条件刚好满足所以没暴露。后来我在所有“松开夹具”之前都加了明确的位置校验先让 Z 轴抬起到安全高度再执行 ReleaseGripper。如果当前位置不在安全区宁可报错停下也不能继续走。这个经验后来也延续到其他动作上上一个动作是下一个动作的前提但前提不能只依赖“指令完成”还要依赖“空间位置安全”。6.3 软件升级与配置管理不要让产线工程师拿着 U 盘拷贝配置刚交付第一台设备时我在现场改了两次点位重新编译上位机然后复制整个发布目录过去。结果第二次复制后操作员发现之前示教的点位都没了因为发布目录里的点位文件被我重置了。产线工程师只好想办法找回旧配置文件折腾了很久。后来我做了两件事一是把配置文件和软件程序分离开点位表、配方、日志都放在独立的配置文件夹发布程序时不会覆盖二是给点位表增加版本号软件启动时如果发现配置版本与程序不一致会弹出提示让工程师决定加载哪一份。这样升级软件时不会因为疏忽把产线参数冲掉。6.4 急停恢复后的状态清理急停不是简单按一下按钮就能继续生产的事。急停触发时机械手可能正停在半路气缸可能没有到位真空可能已经泄掉。如果直接按复位就继续执行原流程大概率会撞机或掉片。我的方案是急停解除后上位机进入“恢复确认”状态先手动把所有轴回到原点或者回到安全位置再逐项检查气缸和真空状态全部确认完成后才允许进入自动模式。这个流程不能自动跳过每一步都要有操作员确认。虽然看起来多花了一点时间但相比一次撞机带来的停机损失这点时间完全可以接受。6.5 从“跑通”到“稳定”的最后一公里代码能跑通和能在产线 24 小时稳定运行差距很大。稳定性靠的不是某个大招而是很多小细节通信异常有没有重试超时时间是否合理日志是否记录了关键现场报警复位会不会留下隐患我的习惯是每次现场问题处理后都追加一条视频级复盘问题现象、定位路径、根因、修复方案、同类问题如何避免。半年下来这些复盘记录比任何文档都有价值。最后分享一个小技巧如果你也在做这类搬移上位机一定要在开发环境里把“模拟模式”做完整。用模拟驱动替代真实硬件把所有状态迁移和报警分支在电脑上先跑一遍到了现场再逐步替换真实 IO 和运动控制。这样既能缩短现场调试时间也能让产线工程师提前熟悉软件操作真正把精力花在解决实际问题上。