ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

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

1. 项目缘起与整体设计思路

半导体制造车间里的晶圆搬移和石墨岛转运,听起来像是两个八竿子打不着的工序,但在化合物半导体、功率器件以及部分先进封装的产线上,它们经常出现在同一个自动化工作站里。晶圆需要从卡匣(Cassette)里一片片取出,经过对准、检测、缓存,再送回或流转到下一站;石墨岛则作为承载盘或者散热基板,需要在不同工位之间精准取放。这两类物料的共同点是:价值高、易碎、对颗粒污染和位置精度极其敏感,人工搬运基本等于报废率上升。

我接到的这个项目,就是给一条中小尺寸产线做一套搬移上位机系统。需求方给的原话很朴素:“要能控制机械手把晶圆和石墨岛从A点搬到B点,能看状态,能报警,能记录。”但真正落地的时候,你会发现这句话背后藏着一整套运动控制、视觉定位、通讯协议、数据追溯和异常处理的工程问题。

这套系统我选的是C# + WPF的技术栈。为什么不用 WinForm?WinForm 做工业上位机确实快,拖控件、写事件、跑起来就能用,但一旦界面涉及多轴状态实时刷新、动画示意、复杂数据绑定和高分屏适配,WinForm 的控件刷新机制和布局能力就会拖后腿。WPF 的数据绑定、依赖属性、模板化控件和硬件加速渲染,在工业 HMI 场景里优势非常明显。尤其是 MVVM 模式下,界面和逻辑解耦,后期改需求不用把代码翻个底朝天。

整体架构我分成四层:设备通讯层、运动控制层、业务逻辑层、界面展示层。设备通讯层负责和 PLC、机械手控制器、视觉相机、RFID 读写器打交道;运动控制层封装轴运动、点位管理、速度规划和安全互锁;业务逻辑层处理搬移流程、状态机、配方管理和数据记录;界面展示层就是 WPF 做的操作台,包含手动调试、自动运行、参数配置、报警查询和日志追溯几个大模块。

这个分层不是为了好看,而是因为半导体设备的上位机经常要面对“今天加一个传感器,明天换一种机械手”的情况。如果通讯和业务揉在一起,改一处动全身。分层之后,只要接口定义清楚,底层换品牌、换协议,上层业务基本不用动。

提示:工业上位机项目里,“能跑”和“能稳定跑三个月”是两回事。前期架构上多花两天做解耦,后期维护能省两个月。

2. 核心硬件选型与通讯方案拆解

2.1 为什么选西门子 PLC 做底层控制

这套系统里,PLC 负责的是实时性要求最高的部分:气缸动作、真空吸盘开关、安全门锁、急停链路、轴使能和限位信号。上位机再快也是 Windows 系统,调度延迟不可控,把安全相关的东西交给 PLC 是基本原则。

我选的是西门子 S7-1200 系列,原因很实际:产线现有设备大多是西门子生态,备件通用,电气工程师熟悉,调试配合成本低。上位机通过S7 通讯协议和 PLC 交换数据,读写 DB 块里的状态字和控制字。

C# 这边连接西门子 PLC,常用的库有 S7.NetPlus 和 HslCommunication。我最终用的是S7.NetPlus,轻量、开源、API 直观,读写 DB 块和 M 区都很方便。核心代码大概长这样:

using S7.Net; private Plc _plc; public bool ConnectPlc(string ip, int rack, int slot) { _plc = new Plc(CpuType.S71200, ip, rack, slot); _plc.Open(); return _plc.IsConnected; } public ushort ReadDbWord(int db, int startByte) { var bytes = _plc.ReadBytes(DataType.DataBlock, db, startByte, 2); return (ushort)(bytes[0] << 8 | bytes[1]); } public void WriteDbBit(int db, int byteAddr, int bitAddr, bool value) { _plc.Write(DataType.DataBlock, db, byteAddr * 8 + bitAddr, value); }

这里有个坑:S7 协议的数据是大端序,而 C# 在 x86 平台是小端序。读 16 位、32 位数据的时候必须手动做字节序转换,否则读出来的速度值、位置值全是乱的。我一开始没注意,调试的时候发现轴速度显示成 65535,查了半天才发现是字节序问题。

2.2 机械手和视觉的通讯方式

机械手用的是 EtherCAT 总线控制器,上位机不直接控制关节,而是通过TCP Socket 发送点位指令和动作命令。控制器那边定义了一套简单的 ASCII 协议,比如MOV P1, SPEED=50表示以 50% 速度移动到 P1 点,GRIP ON表示吸盘开启。上位机收到OK或者ERR,code作为应答。

视觉相机负责晶圆圆心定位和缺口(Notch)角度检测。相机厂商提供了 C# SDK,回调里返回图像和计算结果。这里我遇到过一个典型问题:回调线程和 UI 线程冲突。相机 SDK 的回调是在非 UI 线程触发的,如果直接在回调里更新 WPF 界面,会抛InvalidOperationException。解决办法是用Dispatcher.Invoke或者Dispatcher.BeginInvoke把更新操作切回 UI 线程。

private void OnVisionResult(VisionResult result) { Application.Current.Dispatcher.BeginInvoke(new Action(() => { WaferCenterX = result.CenterX; WaferCenterY = result.CenterY; NotchAngle = result.Angle; })); }

RFID 读写器用于识别卡匣和石墨岛托盘的身份,走的是串口通讯。WPF 里串口操作我封装了一个SerialPortService,打开、关闭、发送、接收事件都在里面处理,业务层只订阅解析后的数据。

2.3 通讯异常的处理策略

工业现场最怕的不是设备坏,而是通讯断了但界面还显示正常。我的做法是给每个通讯通道加心跳检测和超时重连。PLC 每 500ms 读一次心跳字,连续三次读不到就标记为断线,界面变灰并弹报警。Socket 连接设置ReceiveTimeout,超时后自动重连,重连成功前不允许下发新指令。

注意:重连逻辑一定要加退避策略。我见过有人写while(true) { try connect },结果网络故障时 CPU 直接跑满。正确做法是失败后等待 1s、2s、4s、8s 递增,上限 30s。

3. WPF 界面架构与 MVVM 落地细节

3.1 为什么工业 HMI 也要认真做 MVVM

很多人觉得工业界面能看就行,MVVM 是互联网那套花架子。但实际项目里,工业界面的状态复杂度一点不比业务系统低:轴状态、IO 状态、报警列表、配方参数、流程步骤、日志信息,全部要实时刷新。如果全写在代码后台里,一个按钮事件里改十几个控件属性,后期加一个状态就得翻遍所有事件处理函数。

我用的是CommunityToolkit.Mvvm(以前叫 MVVM Toolkit),轻量、源生成器写起来舒服,不用像 Prism 那样引入一大堆依赖。核心 ViewModel 大概有这几个:

  • MainViewModel:主窗口,管理导航和全局状态
  • ManualViewModel:手动调试,轴点动、气缸控制、吸盘开关
  • AutoViewModel:自动运行,流程启动、暂停、停止、状态显示
  • RecipeViewModel:配方管理,晶圆尺寸、速度、延时参数
  • AlarmViewModel:报警查询和处理
  • LogViewModel:操作日志和搬移记录

每个 ViewModel 通过构造函数注入服务接口,比如IPlcService、IRobotService、IVisionService。这样单元测试的时候可以 mock 掉硬件,直接测业务逻辑。

3.2 数据绑定中的实时刷新与性能平衡

工业界面里最典型的需求是:轴当前位置每 100ms 刷新一次,报警状态变化立即刷新,配方参数只在加载时刷新。如果所有数据都用INotifyPropertyChanged全量通知,界面会卡。

我的做法是分频处理:

数据类型刷新频率实现方式
轴位置/速度100ms定时器批量更新,UI 线程合并
IO 状态200ms位运算打包,一次通知
报警状态事件驱动PLC 报警字变化触发
配方参数手动加载只在切换配方时通知
日志列表追加式ObservableCollection 增量添加

轴位置刷新我用了一个DispatcherTimer,每 100ms 从 PLC 读一次所有轴的位置,然后一次性更新 ViewModel 里的属性。注意不要在定时器里直接await异步读,否则定时器会重入。我的做法是加一个_isReading标志位,上一次没读完就跳过这一次。

private bool _isReading; private async void OnTimerTick(object sender, EventArgs e) { if (_isReading) return; _isReading = true; try { var positions = await _plcService.ReadAllAxisPositionsAsync(); AxisXPosition = positions.X; AxisYPosition = positions.Y; AxisZPosition = positions.Z; } finally { _isReading = false; } }

3.3 界面布局与 HandyControl 的使用

WPF 原生控件做工业界面,样式确实朴素。我引入了HandyControl,它提供了一套现代化的控件库,按钮、文本框、卡片、步骤条、消息提示都有现成的工业风格样式。安装方式很简单,NuGet 搜HandyControl安装,然后在App.xaml里合并资源字典:

<Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"/> <ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources>

主界面布局我分成三块:左侧导航栏、中间主操作区、右侧状态面板。中间主操作区用ContentControl配合 DataTemplate,根据当前 ViewModel 自动切换视图。这样手动、自动、配方、报警几个页面切换时,不需要手动创建和销毁控件。

状态面板常驻显示:当前登录用户、设备模式(手动/自动)、PLC 连接状态、机械手连接状态、相机连接状态、当前配方名称、系统时间。这些信息用颜色区分状态,绿色正常、黄色警告、红色故障,操作员扫一眼就知道设备能不能动。

3.4 字体图标与视觉细节

工业界面里图标比文字更直观。我用了FontAwesome的 WPF 封装,通过 NuGet 安装FontAwesome.WPF,然后在 XAML 里直接使用:

<fa:ImageAwesome Icon="Play" Foreground="Green" Width="24" Height="24"/> <fa:ImageAwesome Icon="Stop" Foreground="Red" Width="24" Height="24"/>

按钮上配合图标和文字,操作员不用记哪个按钮是干什么的。另外,深色主题在工业现场更实用,因为车间灯光复杂,浅色界面反光严重。HandyControl 支持主题切换,我在设置里留了一个选项,默认用深色。

实操心得:WPF 界面在高分屏上如果没做 DPI 适配,字会糊。在app.manifest里加上<dpiAware>true</dpiAware>和<dpiAwareness>PerMonitorV2</dpiAwareness>,然后在App.xaml.cs里设置RenderOptions.ProcessRenderMode,基本能解决大部分模糊问题。

4. 搬移流程的状态机设计与实现

4.1 为什么必须用状态机

晶圆和石墨岛的搬移流程,表面上是“取-移-放”三步,但实际要考虑:卡匣有没有料、吸盘真空是否建立、机械手是否在安全位、目标工位是否空闲、视觉定位是否成功、放料后真空是否释放、是否要记录追溯数据。这些条件组合起来,用if-else写会变成几百行嵌套,改一个条件就提心吊胆。

我用的是有限状态机(FSM),每个状态只关心自己的进入条件、执行动作和退出条件。状态之间通过事件驱动切换,比如StartCommand、VacuumOk、VisionDone、PlaceComplete、ErrorOccurred。

核心状态定义:

public enum TransferState { Idle, CheckingCassette, PickingWafer, VacuumChecking, MovingToVision, VisionAligning, MovingToTarget, PlacingWafer, ReleasingVacuum, Returning, Completed, Error, Paused }

状态机引擎我写了一个简单的StateMachine<TState, TEvent>泛型类,支持注册状态转换和进入/退出动作。这样业务逻辑里只需要配置状态图,不用写一堆switch-case。

4.2 晶圆搬移的完整流程拆解

以晶圆从卡匣搬到检测工位为例,完整流程如下:

  1. 检查卡匣状态:读卡匣 RFID,确认型号和批次;读 PLC 的卡匣在位信号和料检测信号。
  2. 机械手取片:机械手移动到卡匣取片位,下降,真空开启,等待真空压力传感器达到阈值(通常 -60kPa 以上)。
  3. 真空确认:如果 500ms 内真空未建立,判定取片失败,机械手退回,报警提示“取片失败,请检查吸盘或晶圆位置”。
  4. 移动到视觉工位:机械手携带晶圆移动到相机上方,触发拍照。
  5. 视觉定位:相机计算晶圆圆心偏移和缺口角度,上位机把偏移量转换为机械手的补偿坐标。
  6. 移动到目标工位:机械手根据补偿后的坐标移动到放置位,下降,真空关闭,吹气释放(部分场景需要)。
  7. 放片确认:读目标工位的在位传感器,确认晶圆已放下。
  8. 机械手返回安全位:完成一次搬移,记录日志,状态回到 Idle。

石墨岛的搬移流程类似,但多了重量检测和放置平整度检测。石墨岛比晶圆重,吸盘选型不同,真空建立时间更长,释放时可能需要延时吹气确保脱离。

4.3 状态机的异常处理与恢复

工业现场最怕的是流程卡在中间状态。比如机械手取片到一半,真空建立了但通讯断了,恢复通讯后机械手不知道自己在哪。我的做法是每个状态进入时,把当前状态和关键参数写入 PLC 的保持寄存器,断电重启后上位机先读这些寄存器,判断是否需要回原点或者继续。

异常处理分三级:

  • 一级异常:可自动恢复,比如真空建立慢了一点,重试一次。
  • 二级异常:需要操作员确认,比如视觉定位失败,界面弹出对话框让操作员选择重试或跳过。
  • 三级异常:必须停机,比如安全门打开、急停按下、机械手碰撞报警。

注意:状态机里不要做耗时操作。我见过有人在状态执行动作里直接Thread.Sleep(5000),结果整个界面卡死。正确做法是用async/await加Task.Delay,或者把耗时操作放到后台线程,状态机只负责发指令和等回调。

5. 数据追溯与日志系统的落地

5.1 为什么半导体设备必须做数据追溯

半导体行业对追溯的要求近乎苛刻。一片晶圆从进站到出站,谁操作的、什么时候、用了什么配方、视觉结果是什么、有没有报警、最终放到哪个位置,全部要记录。出了问题要能倒查到具体环节,否则整批产品都可能被客户拒收。

我用的是SQLite做本地数据库,轻量、免安装、单文件,适合工控机环境。表结构大概有这几张:

表名用途关键字段
TransferRecord搬移记录Id, WaferId, Source, Target, StartTime, EndTime, Result
AlarmRecord报警记录Id, AlarmCode, AlarmText, OccurTime, AckTime, AckUser
OperationLog操作日志Id, User, Action, Detail, Time
RecipeData配方数据Id, RecipeName, Parameter, Value
VisionResult视觉结果Id, WaferId, CenterX, CenterY, Angle, Time

写入用SQLite 的事务批量提交,不要每条记录都开一次连接。我封装了一个DataRepository,内部维护一个队列,后台线程每 500ms 或队列满 100 条时批量写入。这样既保证数据不丢,又不会因为频繁写库影响界面响应。

5.2 日志分级与界面展示

日志分四个级别:Debug、Info、Warn、Error。Debug 只在调试模式记录,Info 记录正常流程节点,Warn 记录可恢复异常,Error 记录故障。界面上的日志列表用ObservableCollection,支持按级别过滤、按时间排序、关键字搜索。

日志文件同时写一份到本地文本文件,按天分割,保留 30 天。这样即使数据库损坏,也能从文本文件里查。文件路径放在D:\Logs\下,按yyyyMMdd建文件夹。

public void Log(LogLevel level, string message) { var entry = new LogEntry { Level = level, Message = message, Time = DateTime.Now }; Application.Current.Dispatcher.BeginInvoke(() => { LogCollection.Insert(0, entry); if (LogCollection.Count > 1000) LogCollection.RemoveAt(LogCollection.Count - 1); }); _fileLogger.Write(entry); _dbQueue.Enqueue(entry); }

5.3 数据导出与报表

操作员和工程师经常需要把搬移记录导出成 Excel 或 CSV 做分析。我用的是ClosedXML库,直接生成.xlsx文件,支持格式化和公式。导出时让用户选择时间范围,后台线程生成文件,完成后提示保存路径。

using ClosedXML.Excel; public void ExportToExcel(List<TransferRecord> records, string filePath) { using var workbook = new XLWorkbook(); var worksheet = workbook.Worksheets.Add("搬移记录"); worksheet.Cell(1, 1).Value = "晶圆ID"; worksheet.Cell(1, 2).Value = "来源"; worksheet.Cell(1, 3).Value = "目标"; worksheet.Cell(1, 4).Value = "开始时间"; worksheet.Cell(1, 5).Value = "结束时间"; worksheet.Cell(1, 6).Value = "结果"; for (int i = 0; i < records.Count; i++) { worksheet.Cell(i + 2, 1).Value = records[i].WaferId; worksheet.Cell(i + 2, 2).Value = records[i].Source; worksheet.Cell(i + 2, 3).Value = records[i].Target; worksheet.Cell(i + 2, 4).Value = records[i].StartTime; worksheet.Cell(i + 2, 5).Value = records[i].EndTime; worksheet.Cell(i + 2, 6).Value = records[i].Result; } workbook.SaveAs(filePath); }

实操心得:导出大量数据时,不要在主线程做。我试过导出 5 万条记录,主线程直接卡了十几秒,操作员以为死机了。后来改成Task.Run后台导出,界面显示进度条,体验好很多。

6. 常见问题与排查技巧实录

6.1 通讯类问题速查

现象可能原因排查方法解决方式
PLC 连接失败IP 不通、机架槽号错ping 测试、核对硬件组态修正 IP 和 rack/slot
读到的数据乱码字节序问题对比 PLC 监控值和上位机值手动做大小端转换
通讯间歇性断开网线干扰、交换机问题看断线频率、换网线加屏蔽、换工业交换机
Socket 收不到应答机械手忙、协议格式错抓包看发送内容加超时重发、核对协议
串口打开失败被占用、波特率错设备管理器看端口关闭占用程序、核对参数

6.2 WPF 界面类问题

界面卡顿是最常见的抱怨。原因通常有三个:一是 UI 线程做了耗时操作,二是数据绑定刷新太频繁,三是控件数量太多。我的排查顺序是:先看有没有在按钮事件里直接读 PLC(改成异步),再看定时器频率(降到 100ms 以上),最后看列表控件有没有虚拟化(VirtualizingStackPanel.IsVirtualizing="True")。

内存泄漏在长时间运行的工业软件里很致命。WPF 里常见的是事件订阅没取消、定时器没停、大对象没释放。我习惯在 ViewModel 的Dispose里统一取消订阅和停止定时器,窗口关闭时调用。

界面假死但后台还在跑,通常是Dispatcher被阻塞了。检查有没有在 UI 线程里Thread.Sleep或者Wait()。所有等待改成await Task.Delay。

6.3 运动控制类问题

机械手取片失败,先看真空压力值。如果压力没到阈值,可能是吸盘老化、晶圆位置偏、真空管路漏气。我遇到过吸盘用了半年后弹性下降,换新吸盘就好了。

视觉定位偏差大,先看相机标定有没有过期,再看光源亮度是否稳定。车间灯光变化、相机镜头污染都会影响。我一般建议每班次开机做一次标定验证。

轴运动超时,检查限位开关、伺服使能、急停回路。有时候是 PLC 程序里轴使能没给,上位机发指令机械手不动,等超时报警。

注意:调试阶段一定要把速度降到 10% 以下。我见过新手直接全速跑,机械手撞到夹具,维修花了两天。低速验证流程正确后再逐步提速。

6.4 数据类问题

SQLite 写入慢,通常是没加事务或者没建索引。批量写入用BEGIN TRANSACTION和COMMIT,查询字段建索引,速度能提升几十倍。

数据库文件损坏,一般是断电时正在写入。解决方式是启用 WAL 模式,并且关键数据同时写文本日志。WAL 模式下 SQLite 的并发和抗损坏能力都更好。

using var connection = new SQLiteConnection("Data Source=data.db"); connection.Open(); using var command = connection.CreateCommand(); command.CommandText = "PRAGMA journal_mode=WAL;"; command.ExecuteNonQuery();

导出 Excel 乱码,检查编码和字体。ClosedXML 默认 UTF-8,一般没问题。如果是 CSV,记得加 BOM 头,否则 Excel 打开中文会乱。

7. 部署与现场调试经验

7.1 工控机环境准备

工控机我一般选研华或西门子的无风扇机型,i5 以上、8GB 内存、SSD 硬盘。系统装 Windows 10 LTSC,不要装家庭版,LTSC 没有商店和一堆后台服务,稳定得多。装完系统后关闭自动更新、关闭休眠、设置高性能电源计划。

.NET 运行时我选.NET 6 或 .NET 8,不要用 .NET Framework 4.x 了,新项目没必要。发布用自包含发布,把运行时打包进去,现场不用单独装 .NET,减少环境问题。

dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true

7.2 现场调试的节奏

现场调试最忌讳一上来就全自动跑。我的节奏是:

  1. 单点测试:每个 IO、每个轴、每个通讯通道单独测。
  2. 手动流程:操作员手动一步步走流程,确认每个动作正确。
  3. 单步自动:自动模式下每步暂停,确认后再继续。
  4. 连续自动:小批量跑,观察稳定性。
  5. 批量验证:满负荷跑,记录节拍和异常。

每一步都要有记录,出问题能回退。调试期间我习惯在界面加一个“调试模式”开关,打开后显示更多中间变量和日志,关闭后界面干净给操作员用。

7.3 交付文档与培训

交付不是把软件装上就完了。我一般准备这几份文档:操作手册(图文并茂,操作员能看懂)、维护手册(常见问题排查)、通讯协议表(和 PLC、机械手、相机的接口定义)、备份恢复指南(数据库和配置怎么备份)。

培训分两场:操作员培训讲怎么用、怎么处理报警;工程师培训讲架构、怎么改配方、怎么查日志。培训时一定要让学员自己动手操作一遍,光看是记不住的。

实操心得:现场一定要留远程协助通道。我试过半夜被叫起来处理问题,如果现场能远程连上看日志,十分钟解决;不能远程,开车过去两小时。当然,远程通道的安全策略要严格,只允许特定 IP、特定账号、操作留痕。

8. 后续扩展方向

这套系统跑稳之后,可以往上加的东西不少。比如对接 MES 系统,把搬移记录和批次信息上传,实现全厂追溯;加 SPC 统计,对视觉定位偏差、真空建立时间做趋势分析,提前发现设备劣化;多工位并行,用多个机械手或者双工位结构提升产能;配方版本管理,不同产品对应不同参数,切换时自动校验。

界面方面,可以加大屏看板,用 WPF 或者单独做个 Web 页面,实时显示产量、良率、设备状态。如果产线有多台设备,还可以做个集中监控客户端,一台电脑看所有设备状态。

代码层面,我建议把通讯层和业务层进一步抽象成可配置的插件式架构。不同品牌的 PLC、机械手、相机,实现同一套接口,配置文件里切换。这样以后接新设备,不用改核心代码,加个插件就行。

最后分享一个我在这个项目里体会最深的一点:工业软件的价值不在于功能多炫,而在于稳定、可追溯、好维护。操作员不需要花哨的动画,他们需要的是按钮按下去有反应、报警能看懂、出了问题能查到原因。把这三件事做好,比加一百个功能都管用。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进