在`Winform`中读取晶圆测试档案并绘制晶圆图

本文最后更新于:2025年4月10日 晚上

在 Winform 中读取晶圆测试档案并绘制晶圆图

写在最前面

在摸鱼的时候想到什么就写什么的一个小程序,功能并不完善,以及 Bug 频发(总之先叠甲吧)。主要是为以后设计此类程序提供一个大致思路。(?

仓库地址:DrawWaferMapApp(由于某些原因仓库已经更改为 Private,源码请联系邮箱:Centaurea_G@hotmail.com)

本文比较有用的部分:晶圆图控件 WaferMap 介绍

(但是写完本文后我发现本文起到的作用更多是文档而不是教程,呃,这就有点尴尬了)

CSV 处理工具介绍

Coordinate

用于表示坐标的类,跟 Point 类没啥区别。

Constructor Description
Coordinate 初始化一个 Coordinate 类的新实例
Coordinate(int, int) 使用指定的值初始化一个 Coordinate 类的新实例
Property Description
X 表示平面直角坐标系横坐标
Y 表示平面直角坐标系纵坐标

CsvTemplate

帮助解析 csv 文件用,它告诉 CsvProcessTool 该如何解析 csv 文件。

Property Description
HeaderRowStartNumber 获取或设置表头行起始行号, 行号从 1 开始计数
HeaderRowEndNumber 获取或设置表头行结束行号, 行号从 1 开始计数
HeaderKeyColumnNumber 获取或设置表头信息列号, 列号从 1 开始计数, 默认为 1
HeaderValueColumnNumber 获取或设置表头信息值起始列号, 列号从 1 开始计数, 默认为 2
ColumnNameRowNumber 获取或设置数据列列名行号, 行号从 1 开始计数
DataRowStartNumber 获取或设置数据行起始行号, 行号从 1 开始计数
XCoordinateColumnNumber 获取或设置横坐标列号, 列号从 1 开始计数
YCoordinateColumnNumber 获取或设置纵坐标列号, 列号从 1 开始计数
ColumnNames 获取或设置文件列名
ColumnsMap 获取或设置列名映射,用于将列名映射到该列的索引

针对不同的测试档,可以写一个派生类继承此类。

CsvDetail

用来存放解析后的 csv 数据,支持存入到 Dictionary 或是 Matrix 中。

Property Description
BodyInfo 获取或设置测试档中的数据,以 Dictionary 形式存储
BodyInfo_Matrix 获取或设置测试档中的数据,以矩阵形式存储
DataType 获取或设置数据存储的形式,DictionaryMatrix
XMax 获取或设置晶圆测试档中横坐标的最大值
XMin 获取或设置晶圆测试档中横坐标的最小值
YMax 获取或设置晶圆测试档中纵坐标的最大值
YMin 获取或设置晶圆测试档中纵坐标的最小值

其中,BodyInfo 的类型是 Dictionary<Coordinate, string[]>,以晶圆上的每个 Die 的坐标为 Key,其 Value 按顺序存储所读取的 csv 测试档案中的电性数据。而 BodyInfo_Matrix 的类型是 string[,][],是一个多维交错数组,其将晶圆上每个 Die 的坐标存储在二维数组 [,] 中,而对应的电性测试数据则存储在一维数组 [] 中。

针对不同的测试档,可以写一个派生类继承此类。

CsvProcessTool

用于处理 csv 文件,提供各种处理 csv 文件的方法。

Constructor Description
CsvProcessTool() 初始化一个 Coordinate 类的新实例
CsvProcessTool(string) 初始化一个 Coordinate 类的新实例,并设置指定的 Pattern
Property Description
Pattern 获取或设置正则表达式的 Pattern,默认为 “,(?=(?:[^”]*“[^]*)*[^”]*$)"
SplitChar 获取或设置 csv 文件的分隔符,默认为 “,”
Method Description
ReadCsvFile(string) 读取指定的 CSV 文件, 并将文件每一行的内容根据 SplitChar 指定的分隔符进行分隔,产生的字符串数组将被放入 List 中。
ReadCsvFile(string, bool) 读取指定的 CSV 文件, 第二个参数指定根据 SplitChar 指定的分隔符进行分隔,还是使用指定的正则表达式进行分隔。结果将被放入 List 中。
ReadCsvFileToDictionary(string, CsvTemplate, CsvDetail) 读取指定的 CSV 文件, 并将信息存入到 CsvDetailBodyInfo 属性中
ReadCsvFileToMatrix(string, CsvTemplate, CsvDetail) 读取指定的 CSV 文件, 并将信息存入到 CsvDetailBodyInfo_Matrix 属性中
ReadCsvFileAsync(string) ReadCsvFile(string) 的异步版本
GetHeaderInfoToDictionary(List<string[]>, CsvTemplate) 提取已解析的 CSV 文件中的表头信息
GetBodyInfoToDictionary(List<string>[], CsvTemplate) 提取已解析的 CSV 文件中的表身(单身)信息
IsInRange<T>(T, T, T) 判断给定的 T 是否在范围内
IsOutOfRange<T>(T, T, T) 判断给定的 T 是否不在范围内
ParseCsvLine(string, char) 根据指定的分隔符对传入的 string 进行解析
ParseCsvLineByRegex(string) 根据指定的正则表达式对传入的 string 进行解析

针对不同的测试档,可以写一个派生类继承此类。

晶圆图控件介绍

WaferMap

用于显示晶圆图的自定义控件。

Constructor Description
WaferMap() 初始化一个 WaferMap 类的新实例
Property Description
XMax 获取或设置测试档中的最大横坐标
XMin 获取或设置测试档中的最小横坐标
YMax 获取或设置测试档中的最大纵坐标
YMin 获取或设置测试档中的最小纵坐标
TranslationX 获取横坐标上的偏移量
TranslationY 获取纵坐标上的偏移量
Zoom 获取缩放比例
ScaleFactor 获取或设置缩放系数
DieSize 获取或设置绘制 Die 的尺寸
Detail 获取或设置 CsvDetail
Colors 获取或设置 Bin 的颜色
DrawCross 获取或设置是否在晶圆图上绘制十字线。
Field Description
_waferWidth 实际晶圆图的宽度
_waferHeight 实际晶圆图的长度
_isDrawBin 当前是否处于画 Bin 模式,画 Bin 模式下不允许除了描点以外的操作
_canModifyBin 是否可以修改 Bin
_squareSize 右键固定后绘制的小正方形方框的大小
_opacity 设置右键固定后出来的正方形框的透明度,0-255之间
_squareCenter 右键固定后出来的正方形框的中心点
_fixed 固定某一个点
lastX 记录上次鼠标点击的位置
lastY 记录上次鼠标点击的位置
drawBinPoints 记录画 Bin 的点
drawBinPen 画 Bin 时使用的画笔
binGraphics 画 Bin 时使用的 Graphics 对象
Event Description
WaferMapMouseMove 当鼠标在控件上移动时发生
Method Description
OnPaint(PaintEventArgs) 重载的绘制方法,将绘制晶圆图的逻辑放在此方法中
WaferMap_Load(object, MouseEventArgs) 控件Load事件触发时调用此方法
WaferMap_MouseDown(object, MouseEventArgs) 鼠标按键按下事件触发时调用此方法,用于判断拖拽动作及记录拖拽起点
WaferMap_MouseMove(object, MouseEventArgs) 鼠标移动事件触发时调用此方法,用于 WaferMapMouseMove.Invoke 以及在拖动时触发重绘
WaferMap_MouseUp(object, MouseEventArgs) 鼠标按键抬起事件触发时调用此方法,用于判断拖拽动作
WaferMap_MouseWheel(object, MouseEventArgs) 鼠标滚轮事件触发时调用此方法,处理晶圆图的放大缩小
WaferMap_MouseClick(object, MouseEventArgs) 鼠标按键点击事件触发时调用此方法,处理画 Bin 模式下的绘制及右键菜单的显示
RegisterEvents() 为控件注册事件的方法
tsmiFixed_CheckedChanged(object, EventArgs) 右键菜单中的固定选项的 Checked 属性的值发生更改时调用此方法,根据 Checked 的值决定是否绘制小正方形框
tsmiFixed_Click(object, EventArgs) 右键菜单中的固定选项被点击时调用此方法,用于切换其 Checked 属性的值
RedrawWaferMap() 令控件执行重绘,是个没什么用的方法
SetBinColor(Colors[]) 根据传入的 Colors[] 设置各个 Bin 等级的颜色
SetBinColorDefault() 用控件设定的默认值设置各个 Bin 等级的颜色
SetPosition(int, int) 根据传入的坐标去定位 Wafer 上的某个点,将该点移至控件的中心
DrawBin() 用于进入和退出画 Bin 模式
DrawBinUndo() 用于画 Bin 模式下的撤销
CleanDrawBinHistory() 清理画 Bin 记录
ModifyBin() 画 Bin 完成后修改 Bin 值(此方法并没有完成)
CrossProduct(Point, Point, Point) 计算叉积 (P2 - P1) x (P3 - P1)
IsPointOnSegment(Point, Point, Point) 判断点 P3 是否在线段 P1P2 上(注意:调用此方法判断的前提是三点共线,即它们的叉积为 0)
AreSegmentsIntersecting(Point, Point, Point, Point) 判断线段 P1P2 和 P3P4 是否相交
IsValidPolygon(List<Points>) 判断一组点是否构成有效的多边形
IsInPolygon2(Point, List<Point>) 判断点是否在多边形内
GetBinColor(int) 返回传入的 Bin 等级对应的 Color
GetBinColor(string) 返回传入的 Bin 等级对应的 Color

这里对该控件的部分功能的实现做一下详细解析。

晶圆图的绘制

我选择直接在控件上的 Graphics 进行绘制,而不是使用绘制在 BitMap 上,然后以 PictureBox 作为载体进行绘制。这么做有几个原因:

  1. BitMap 需要消耗额外内存。
  2. 方便实现晶圆图的放大、缩小、移动等功能(坐标、偏移量等容易计算出来)。

那么要如何绘制晶圆图呢?首先要清楚控件的坐标系,是以左上角为原点 (0, 0) 的,且向右和向下分别为横坐标与纵坐标的正增量,并非我们熟知的向右和向上。其次,我们需要在控件的范围内来绘制整张晶圆图,这就涉及到绘制的比例,这个比例也很好算,“控件的大小 / 晶圆的大小”。有了比例之后,就能够算出晶圆的某个 Die 在控件上的位置了,Die 对应的横纵坐标减去晶圆的最小横纵坐标,再乘上比例,就得到了 Die 在控件上的坐标。

缩放与移动

介绍完了基本的绘制,接下来再讲讲缩放和移动这些功能是如何实现的。

先来说说移动,其实移动很简单,我们只需要知道“偏移量(Translation)”即可,举个例子会好理解很多。比如,在下面这张图中,我们将鼠标从 A 点移动到了 B 点,鼠标的移动方向是左上方,所以晶圆图的移动方向也应该是左上方,这样才符合直觉。而本次移动的“偏移量”其实就是 “B 点 - A 点”,所以本次移动在横纵坐标上的“偏移量”都是负的。那么在绘制的时候,我们就需要将偏移后的坐标作为Graphics的原点,如图所示,偏移后的原点实际上就是当前原点 + 偏移量。为什么我们只用Graphics.TranslateTransform改动了原点,没有改动其他代码,也能正确绘制呢? 你可以理解成我们现在所有绘制的内容都自动加上了这个偏移量。下面来看看相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected virtual void WaferMap_MouseMove(object sender, MouseEventArgs e)
{
...
// 检查鼠标是否在移动,如果位置没有变化,直接返回
if (e.Location == _lastMousePosition)
return;

// 更新鼠标上次位置
_lastMousePosition = e.Location;

...
if (_isDragging && !_isDrawBin)
{
TranslationX += e.X - _dragStart.X;
TranslationY += e.Y - _dragStart.Y;

_dragStart = e.Location; // 更新拖动起点
Invalidate(); // 重新绘制
}
}

如果侦测到了用户正在拖动鼠标,控件便会更新TranslationXTranslationY,并执行重绘。在重绘方法中,将使用新的TranslationXTranslationY来重设原点,并计算偏移后的坐标系上限,这样在检查到某一个 Die 的坐标超出了当前控件的坐标系范围时,可以直接跳过绘制(因为就算画了也看不见,但是资源却实实在在地消耗了)(你问为什么能画控件客户端区域外的东西?我也不知道)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private void PaintByBin(PaintEventArgs e)
{
...
e.Graphics.Clear(Color.White); // 清除所有绘制内容,并用白色填充背景

// 对于 Graphics 而言,新的原点 = (TranslationX, TranslationY);对于控件而言,新的原点 = (0, 0) - (TranslationX, TranslationY)
e.Graphics.TranslateTransform(TranslationX, TranslationY); // 重设原点

...
// 记录偏移后的坐标系上限
float translationWidth = Width - (TranslationX);
float translationHeight = Height - (TranslationY);

// 遍历 bodyInfo,绘制每个数据点
if (Detail.DataType == DataStorageType.Dictionary)
{
foreach (var entry in Detail.BodyInfo)
{
...
// 不需要绘制控件外的点
if (xPos < 0 - TranslationX || yPos < 0 - TranslationY || xPos > translationWidth || yPos > translationHeight)
{
continue;
}
...
}
}
...
}

然后是缩放。其实缩放也很简单,缩放要处理的其实就是一个“缩放比例(Zoom)”,然后在控件重绘计算绘制比例的时候,乘以缩放比例即可。难的地方是计算缩放后的位置,因为我想做到晶圆图缩放后,鼠标指向的 Die 是同一个 Die。先从处理鼠标滚轮事件的相关代码说起吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
protected virtual void WaferMap_MouseWheel(object sender, MouseEventArgs e)
{
...
// 记录旧的缩放比例
float oldZoom = Zoom;

// 更新 Zoom
if (e.Delta > 0)
{
Zoom += ScaleFactor; // 放大
}
else if (e.Delta < 0)
{
Zoom = Math.Max(1.0f, Zoom - ScaleFactor); // 缩小,确保缩放比例不小于1.0
}

// 计算缩放前的鼠标相对于整个wafer图的坐标
float oldXPos = (e.X - TranslationX) / (oldZoom * ((float)Width / _waferWidth));
float oldYPos = (e.Y - TranslationY) / (oldZoom * ((float)Height / _waferHeight));

// 计算缩放后的鼠标相对于整个wafer图的坐标
float newXPos = (e.X - TranslationX) / (Zoom * ((float)Width / _waferWidth));
float newYPos = (e.Y - TranslationY) / (Zoom * ((float)Height / _waferHeight));

// 通过平移调整,使鼠标相对位置不变
TranslationX += (newXPos - oldXPos) * Zoom * ((float)Width / _waferWidth);
TranslationY += (newYPos - oldYPos) * Zoom * ((float)Height / _waferHeight);

// 请求重绘
Invalidate();

// 获取当前wafer的X和Y位置
int waferX = (int)((e.X - TranslationX) / (Zoom * ((float)Width / _waferWidth)) + XMin);
int waferY = (int)((e.Y - TranslationY) / (Zoom * ((float)Height / _waferHeight)) + YMin);

// 触发鼠标移动事件
WaferMapMouseMove?.Invoke(this, new WaferMapMouseMoveEventArgs(waferX.ToString(), waferY.ToString()));
...
}

可以发现这段代码中有个常出现的计算量:Zoom * ((float)Width / _waferWidth)Zoom * ((float)Height / _waferHeight),可以理解为“Die 的绘制比例”,因为缩放后,每颗 Die 的尺寸也会跟着变化。稍后你会发现这个计算量同样用于重绘方法中。

保持缩放前后鼠标指向同一个 Die 的代码可能难以理解,可以结合下面的示意图来理解:

缩放定位示意图

接着就是将相关参数合并到绘制方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
private void PaintByBin(PaintEventArgs e)
{
...
e.Graphics.Clear(Color.White); // 清除所有绘制内容,并用白色填充背景

// 对于 Graphics 而言,新的原点 = (TranslationX, TranslationY);对于控件而言,新的原点 = (0, 0) - (TranslationX, TranslationY)
e.Graphics.TranslateTransform(TranslationX, TranslationY); // 重设原点
Console.WriteLine(TranslationX + " " + TranslationY);
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; // 抗锯齿

// 计算图像的绘制比例
float xScale = (float)Width / _waferWidth * Zoom;
float yScale = (float)Height / _waferHeight * Zoom;

// 记录偏移后的坐标系上限
float translationWidth = Width - (TranslationX);
float translationHeight = Height - (TranslationY);

// 遍历 bodyInfo,绘制每个数据点
if (Detail.DataType == DataStorageType.Dictionary)
{
foreach (var entry in Detail.BodyInfo)
{
Coordinate coord = entry.Key;
string[] data = entry.Value;

// 根据 x 和 y 计算位置
float xPos = (coord.X - XMin) * xScale;
float yPos = (coord.Y - YMin) * yScale;

// 不需要绘制控件外的点
if (xPos < 0 - TranslationX || yPos < 0 - TranslationY || xPos > translationWidth || yPos > translationHeight)
{
continue;
}

// 绘制每个点(椭圆)
if (_canModifyBin && !IsInPolygon2(new Point((int)xPos, (int)yPos), _drawBinPoints))
{
e.Graphics.FillEllipse(new SolidBrush(Color.Pink), xPos, yPos, DieSize.Width * xScale, DieSize.Height * yScale);
}
else
{
e.Graphics.FillEllipse(new SolidBrush(GetBinColor(data[2])), xPos, yPos, DieSize.Width * xScale, DieSize.Height * yScale);
}
}
}
}

可以发现各个功能都挺简单的,最重要的是将他们一一拆开来实现,最后再组合在一起(然而并非简单,我再回头看都要看半天)。

WaferMapMouseMoveEventArgs

用于向其他控件传递当前鼠标所处的晶圆的坐标。

Property Description
WaferX 晶圆图横坐标
WaferY 晶圆图纵坐标

画 Bin

由于本功能并未全部完成所以我决定先摆烂不写嘿嘿嘿~

建议直接看代码~

MiniWaferMap

相当于放大镜,用来看晶圆图的某一块区域的 Bin 分布情况。

Constructor Description
MiniWaferMap() 初始化一个 MiniWaferMap 类的新实例
Property Description
Detail 获取或设置 CsvDetail
Colors 获取或设置 Bin 的颜色
X 中心 Die 在晶圆上的实际横坐标
Y 中心 Die 在晶圆上的实际纵坐标
HalfOfTheSide 要显示的中心点周边的 Die 的个数
Method Description
SetBinColor() 设置各个 Bin 等级的颜色
SetBinColorDefault() 用程序默认值设置各个 Bin 等级的颜色
GetBinColor(int) 返回传入的 Bin 等级对应的 Color
GetBinColor(string) 返回传入的 Bin 等级对应的 Color
DrawWhitePicture(Graphics) 绘制白布
DrawMiniMap(Graphics) 绘制迷你晶圆图
Redraw() 提供给外部使用,调用控件的 Invalidate() 方法进行重绘

你可能会在代码中发现一些额外的此处没有列出的属性或方法等,无需在意,因为那些八成是我没删掉的无用代码。

这个实现就没什么难点了,直接根据数据画图就行了。(这可比那WaferMap好写多了!)

ElectricalMap

是一个通过 PictureBoxBitmap 来展示抽/全测某个电性参数的测试数据的晶圆图的通用(并非)控件。这个时候就有吴彦祖要问了:主包主包,你不是不用 Bitmap 吗?确实,但是这抽全测看图它不需要花里胡哨的缩放和拖动功能,所以最适合展示的方式又变成了绘制 Bitmap 并用 PictureBox 进行展示。

由于赶工的原因,一些地方可能做了写死处理,不过都是小问题,图(及核心功能)没问题就是了!

Constructor Description
ElectricalMap() 初始化一个 ElectricalMap 类的新实例
Property Description
Detail 获取或设置 CsvDetail
Colors 获取或设置 Bin 的颜色
ElectricalMapName 所显示的图的电性参数的名称
MinValue 电性参数范围设置的最小值
MaxValue 电性参数范围设置的最大值
StepValue 电性参数范围设置的步长
DrawBorder 是否为 Die 绘制白色边框,可以使得每个抽测点变得更清楚,不建议全测使用
Field Description
_bitmap 晶圆图
_lastMousePosition 记录上次的鼠标位置
_sendMouseInfo 是否回传坐标信息
Event Description
ElectricalMapMouseMove 当鼠标在控件上移动时发生
ElectricalMapDoubleClick 当鼠标在控件上双击时发生
Method Description
DrawMap(string, int, CsvDetail, int, float, int) 绘制指定的电性参数的 Map
DrawMiniMap(string, int, CsvDetail, int, int, int, int, int, bool) 绘制指定的 (x,y) 旁的指定的 X 颗 Die
LoadMap(Bitmap, string) 直接加载传入的位图
GetBinColor(double) 根据传入的值,在指定的最大最小值及步长,找到其对应的颜色并返回

其实画图方法没什么好讲的,我已经在代码里面用注释写得很清楚了(连公式也给你交代了!),建议直接看代码再配合画图工具理解。(其实是因为要说明的东西太多了,而且有很多相关逻辑放在了调用该控件的窗体上!另外这是后面完成的所以我开摆了。说不定什么时候心情好就再写写。

ElectricalMapMoveEventArgs

用于向其他控件传递当前鼠标所处的晶圆的坐标。

Property Description
WaferX 晶圆图横坐标
WaferY 晶圆图纵坐标

ElectricalMapDoubleClickEventArgs

用于向其他控件传递电性参数的名称以及对应的位图。

Property Description
ElectricalName 当前 ElectricalMap 显示的电性参数名称
Map 当前 ElectricalMap 显示的晶圆图

两个界面的简单介绍

用户界面1(Form1

用来读档案的界面。单独写一个界面只是为了方便自己测试什么的。

用户界面1

用户界面2(WaferMapDisplayForm

展示 AOI 图的。

用户界面2

实际看图效果展示

AOI

读档完成并绘制完毕后的画面:

AOI看图效果

放大和拖动效果:

AOI看图缩放及拖动

可以放得很大很大!

AOI看图缩放及拖动2

画 Bin

目前只是简单地完成了多边形的判断已经写死的改 Bin 方法。点击 Draw Bin(Start) 就可以开始绘制多边形了,同时按钮会变为 Draw Bin(End)Draw Bin UndoDraw Bin Clean 按钮分别提供了撤销与清除功能。绘制完毕后,点击 Draw Bin(End) 按钮,会自动判断是否能够形成多边形并连接初始点与最后一个点。

绘制多边形

接着点击 Modify Bin 即可改 Bin。

将某个Bin转换为指定Bin后

全测

九宫格

全测九宫格效果图

大图,全测档案右下角会有放大镜(迷你晶圆图)

全测大图效果图

抽测

九宫格

抽测九宫格效果图

大图

抽测大图效果图

展望

这部分主要记录想要实现的其他功能。

  1. 改一下在控件中绘制晶圆的区域,目前是占满了整个控件,看看能不能改成可以手动设定什么的。
  2. 电性资料的显示。
  3. 描 Bin 功能并没有做完(说是没做完实际上核心功能如描点已经完成了,只剩下修改 Bin 的部分没完成罢了)。
  4. 设置跟随鼠标的半透明正方形,以方便确认🔍的位置。(直接在原有的 Graphics 上进行绘制并不方便,因为貌似无法进行局部重绘…)(Ok 我放弃了 由于 Winform 的渲染机制无论如何都无法避免重绘整个控件 所以我决定改成右键点击固定的时候显示)

其他想说的:项目结构管理得也太烂了,为什么我当时把所有东西都写在一个 Project 里了我请问了?拆都难拆。还有就是这代码写的也太烂了,回过头一看设计得是真烂,感觉还是过于耦合了,而且可扩展性并没有看上去的那么高。为什么这么多东西出不来一个接口?好吧实际上是因为我想到什么就写什么而且也并不清楚用户的实际需求导致最后实际需求与我所做的偏差有点大但是这东西不应该本身就由开发者来定制规范吗kuso。罚重新读一遍框架设计指南!


这里有一只爱丽丝

希望本文章能够帮到您~


在`Winform`中读取晶圆测试档案并绘制晶圆图
https://map1e-g.github.io/2025/04/10/在Winform中读取晶圆测试档案并绘制晶圆图/
作者
MaP1e-G
发布于
2025年4月10日
许可协议