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 晶圆搬移的完整流程拆解
以晶圆从卡匣搬到检测工位为例,完整流程如下:
- 检查卡匣状态:读卡匣 RFID,确认型号和批次;读 PLC 的卡匣在位信号和料检测信号。
- 机械手取片:机械手移动到卡匣取片位,下降,真空开启,等待真空压力传感器达到阈值(通常 -60kPa 以上)。
- 真空确认:如果 500ms 内真空未建立,判定取片失败,机械手退回,报警提示“取片失败,请检查吸盘或晶圆位置”。
- 移动到视觉工位:机械手携带晶圆移动到相机上方,触发拍照。
- 视觉定位:相机计算晶圆圆心偏移和缺口角度,上位机把偏移量转换为机械手的补偿坐标。
- 移动到目标工位:机械手根据补偿后的坐标移动到放置位,下降,真空关闭,吹气释放(部分场景需要)。
- 放片确认:读目标工位的在位传感器,确认晶圆已放下。
- 机械手返回安全位:完成一次搬移,记录日志,状态回到 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=true7.2 现场调试的节奏
现场调试最忌讳一上来就全自动跑。我的节奏是:
- 单点测试:每个 IO、每个轴、每个通讯通道单独测。
- 手动流程:操作员手动一步步走流程,确认每个动作正确。
- 单步自动:自动模式下每步暂停,确认后再继续。
- 连续自动:小批量跑,观察稳定性。
- 批量验证:满负荷跑,记录节拍和异常。
每一步都要有记录,出问题能回退。调试期间我习惯在界面加一个“调试模式”开关,打开后显示更多中间变量和日志,关闭后界面干净给操作员用。
7.3 交付文档与培训
交付不是把软件装上就完了。我一般准备这几份文档:操作手册(图文并茂,操作员能看懂)、维护手册(常见问题排查)、通讯协议表(和 PLC、机械手、相机的接口定义)、备份恢复指南(数据库和配置怎么备份)。
培训分两场:操作员培训讲怎么用、怎么处理报警;工程师培训讲架构、怎么改配方、怎么查日志。培训时一定要让学员自己动手操作一遍,光看是记不住的。
实操心得:现场一定要留远程协助通道。我试过半夜被叫起来处理问题,如果现场能远程连上看日志,十分钟解决;不能远程,开车过去两小时。当然,远程通道的安全策略要严格,只允许特定 IP、特定账号、操作留痕。
8. 后续扩展方向
这套系统跑稳之后,可以往上加的东西不少。比如对接 MES 系统,把搬移记录和批次信息上传,实现全厂追溯;加 SPC 统计,对视觉定位偏差、真空建立时间做趋势分析,提前发现设备劣化;多工位并行,用多个机械手或者双工位结构提升产能;配方版本管理,不同产品对应不同参数,切换时自动校验。
界面方面,可以加大屏看板,用 WPF 或者单独做个 Web 页面,实时显示产量、良率、设备状态。如果产线有多台设备,还可以做个集中监控客户端,一台电脑看所有设备状态。
代码层面,我建议把通讯层和业务层进一步抽象成可配置的插件式架构。不同品牌的 PLC、机械手、相机,实现同一套接口,配置文件里切换。这样以后接新设备,不用改核心代码,加个插件就行。
最后分享一个我在这个项目里体会最深的一点:工业软件的价值不在于功能多炫,而在于稳定、可追溯、好维护。操作员不需要花哨的动画,他们需要的是按钮按下去有反应、报警能看懂、出了问题能查到原因。把这三件事做好,比加一百个功能都管用。