2026-08-09 学习笔记:背钻检测核心概念
一、背钻在干什么
一块多层 PCB 板打了一个通孔(via),电镀后铜柱从板顶贯穿到板底,像一根吸管插穿蛋糕。
但高速信号只需要连到第 3 层。第 3 层以下那截铜柱没用,反而变成天线,反射信号。
解决办法:拿一把大一号的钻头,从板底反着往上钻,把多余铜掏掉。
板顶 ━━━━━━━━━━━━━━━
│ ██ 信号层 ██ │ ← 信号走这里
│ ██ 信号层 ██ │
─────┤──████████────├── 信号层
│ ████████ │ ← 残桩(留着,安全距离)
─────┤ ████████ ├── 钻头停在这
│ │
│ 多余铜柱 │ ← 这段被掏掉了
│ │
板底 ━━━━━━━━━━━━━━━ ← 钻头从这扎进去
铅笔比喻:
✏️ 笔尖 ━━━ 板顶
████ ← 保留的笔芯(信号层,有用)
████
───████─── ← 钻头停的位置
████ ← 残桩(留一点,安全)
───████─── ← 钻尖最深点
← 下半截笔芯被削掉了
════ ← 笔尾(板底)
| 术语 | 什么意思 | 比喻 |
|---|---|---|
| 信号层 | 信号走的那层铜 | 铅笔写字的那段 |
| 残桩 stub | 信号层到钻头的安全距离 | 写字段和削掉段之间留的一点芯 |
| 钻深 | 钻进去多深 | 削掉多长 |
| 钻头最深点 | 钻头停在哪 | 削到哪停了 |
二、CT 扫描 Z 范围:只扫关键区域
CT 不需要扫整板,只扫钻头停的位置上下那一段。检测目的:确认钻深对不对、残桩长不长。
板顶 ━━━━━━━━━━━━━━━━━━
│ ████████ │ ← 保留铜(不在扫描范围里)
│ ████████ │
─────┤────████████────├── ← topPos(扫描上边界)
│ ① ████████ ① │ ← 残桩,铜还在
─────┤ ████████ ├── ← 钻头最深点
│ ② ② │
│ ② 被掏空 ② │ ← 铜被钻掉了
│ ② ② │
板底 ━━━━━━━━━━━━━━━━━━
│ ③ ③ │ ← 冗余,板外空气
─────┴───────────────┴── ← bottomPos(扫描下边界)
| 区间 | 位置 | 有铜? | 为什么 |
|---|---|---|---|
| ① | topPos → 钻头最深处 | 有 | 残桩,故意留的安全距离 |
| ② | 钻头最深处 → 板底 | 没有 | 被钻头掏掉了,背钻的目的 |
| ③ | 板底 → bottomPos | 板外空气 | 多扫一点冗余,防漏 |
| CT 看到的情况 | 说明 |
|---|---|
| 钻太浅 | 残桩太长,残留铜变天线,信号烂 |
| 钻太深 | 伤到信号层的铜,断路 |
比喻:手机拍杯子 --- 不拍整个房间(整板),只拍杯子周围(Z 范围),稍微拉远一点(冗余量)。
语法 1:Math.Ceiling --- 向上取整
什么时候用:算"最少需要几个"的时候 --- 宁可多一个,不能少。
csharp
// 场景:蛋糕切成 3cm 一块,蛋糕长 10cm,要几块?
double cakeLength = 10;
double pieceSize = 3;
int pieces = (int)Math.Ceiling(cakeLength / pieceSize);
// 10 ÷ 3 = 3.33 → Ceiling → 4 块
// 3 块只能装 9cm,最后 1cm 不能扔 → 多加一块
Ceiling vs Floor vs Truncate:
csharp
double x = 3.3;
Math.Ceiling(x); // 4 向上:往更大的整数靠
Math.Floor(x); // 3 向下:往更小的整数靠
Math.Truncate(x); // 3 向零:直接砍掉小数部分
double y = -3.3;
Math.Ceiling(y); // -3 向上:数轴上 -3.3 往上走到 -3
Math.Floor(y); // -4 向下:数轴上 -3.3 往下走到 -4
Math.Truncate(y); // -3 向零:砍掉小数
常见坑:
- Ceiling 返回
double,存 int 要强转(int),否则类型不匹配 - 负数时别搞混:
Ceiling(-3.3)是-3不是-4
为什么用 Ceiling 不用 Floor:Floor 会"丢一段"。10 个学生、每车坐 3 人 → 需要 4 辆车,不是 3 辆。
三、三种眼睛:CCD / FPD / CT
3.1 同一个光源,三种看法
X-Ray 光管(唯一光源)
│
│ X 光打下去
▼
┌───── 板子 ─────┐
│ │
▼ ▼
普通光相机 X光探测器
(拍外观) (拍透视/CT)
| 方式 | 像什么 | 怎么拍 | 看到什么 |
|---|---|---|---|
| CCD | 📷 手机拍蛋糕表面 | 普通光/LED 照板面,相机接收反射光 | 2D 外观 |
| FPD | 🏥 拍胸片 | X 光穿透板子,探测器接收穿透光 | 所有层叠在一起的影子 |
| CT | 🏥 拍 CT | X 光绕板子转多角度各拍一张 | 3D 模型,能一层一层切着看 |
通俗理解:
- CCD = 用眼看蛋糕,只看到表面
- FPD = 手电筒从蛋糕底照,看到叠影(哪层有果酱?看不清)
- CT = 把蛋糕切成薄片,一层一层看(果酱在第 3 层,清清楚楚)
3.2 物理平台:两个探头共用一套轨道
┌────────────────────────────────┐
│ X/Y/Z 三轴运动平台 │
│ │
│ [CCD相机] ←── 偏移量 ──→ [FPD探测器] │
│ │
└────────────────────────────────┘
CCD 和 FPD 装在同一套轨道上,之间有固定物理偏移。
流程:轨道移到 CCD 位置拍外观照 → 平移固定偏移量 → FPD 对准同一位置拍 X 光。
比喻:同一辆滑板车上装两个摄像头,推到位置 A 用第一个拍,滑固定距离用第二个拍。
语法 2:switch 表达式 --- 比 if-else 更干净的多分支
为什么需要它:多个条件各返回不同值时,if-else 又长又啰嗦。
csharp
// ❌ 老写法(switch 语句):需要 break,不返回值
string result;
switch (mode)
{
case 0: result = "小焦斑"; break;
case 2: result = "大焦斑"; break;
default: result = "未知"; break;
}
// ✅ switch 表达式(C# 8+):一行赋值,自动返回
string result = mode switch
{
0 => "小焦斑",
2 => "大焦斑",
_ => "未知" // _ = discard 模式,兜底 default
};
语法拆解:
value switch { ... }--- 表达式不是语句,直接产生值,可以赋给变量pattern => result--- 匹配上了就返回 result,不需要 case 和 break_--- discard,匹配所有没列出来的情况,等价 default
什么时候用:条件 > 2 个、每个分支都是简单返回值 → switch 表达式。如果每个分支要做一堆事(调方法、改状态),用 if-else。
常见坑:
- 忘了加
_兜底 → 值不在列举范围内 → 运行时抛异常 - 把 switch 语句的
default:写成 switch 表达式 --- 语法不一样,编译不过
语法 3:Dictionary<K,V> --- 键值映射 O(1) 查找
为什么需要它:用"名字"找"东西"的场景 --- 不用遍历,直接拿。
csharp
// 场景:学生成绩查询
var scores = new Dictionary<string, int>
{
["张三"] = 95,
["李四"] = 87,
["王五"] = 92,
};
// 查张三的成绩 → 一步到位,不用遍历
int zhangScore = scores["张三"]; // 95
// ❌ 但是!"赵六"不在字典里 → 抛 KeyNotFoundException
// ✅ 安全写法:
if (scores.TryGetValue("赵六", out int zhaoScore))
{
Console.WriteLine($"成绩:{zhaoScore}");
}
else
{
Console.WriteLine("没有这个人");
}
和 List 对比:
| List 查找 | Dictionary 查找 | |
|---|---|---|
| 怎么找 | 从头一个个比对 | 算哈希码,直接定位 |
| 100 项 | 最多比 100 次 | 1 次 |
| 复杂度 | O(n) | O(1) |
什么时候用 Dictionary:知道 key 要取 value 的场景。key 是唯一的。
常见坑:
dict[key]--- key 不存在直接炸,用TryGetValue兜底- key 是引用类型 → 注意别用
null当 key - 同一个 key 存两次 → 第二次覆盖第一次,不报错
四、BGA 检测核心概念
4.1 桥接(Bridge)= 球挨太近了?
焊球 A 焊球 B
● ● ← 正常:两个球,中间有间隙
●~~~~~~~● ← NG:中间有锡连着,短路了
检测方法:膨胀法。
原始焊球框: 膨胀后(×1.5):
┌──┐ ┌──┐ ┌────┐┌────┐
│ A│ │ B│ → │ A ││ B │ ← 扩大后重叠 = 桥接!
└──┘ └──┘ └────┘└────┘
比喻:两个人站在地铁里,距离刚好不碰。但给他们各套一件羽绒服 --- 羽绒服碰上了,说明离得太近,有危险。
4.2 空洞率(Void)= 球里面有多少气泡
完美焊球 有空洞的焊球
┌─────┐ ┌─────┐
│█████│ │██ ██│
│█████│ │█ ○ █│ ← 中间有气泡
│█████│ │██ ██│
└─────┘ └─────┘
空洞率 0% 空洞率 20% → 超过 30% NG
比喻:掰开一个馒头,里面气孔太多 → 不瓷实 → 容易裂。
4.3 枕头效应(HIP)= 看起来粘住了其实没粘
焊球 ██
██ ← 挨上了
─ ─ ─ ─ ─ ← 接触面
██ ← 锡膏
焊盘 ██
两个表面挨上了但没有真正融合,像两块糖放在热锅里挨上了但没化在一起 --- 看着粘住了,一掰就开,时通时断。
产生原因:焊球熔了但锡膏没熔透 / 温度不够。回流焊 = BGA 贴 PCB 后进高温炉(预热→回流区 220-250°C 熔化→冷却凝固)。
4.4 五层切片
焊球在 CT 里不是只看一层,从上到下切 5 层:
Top ─── ○ ─── 顶层(贴芯片端)
TopComp ─── ○ ─── 顶层补偿
Center ─── ◎ ─── 中心层(最宽截面)
BotComp ─── ○ ─── 底层补偿
Bottom ─── ○ ─── 底层(贴 PCB 端)
每层各跑一套检测参数(空洞率、圆度、焊锡率),不合格就报。
语法 4:foreach vs for --- 遍历的选择
csharp
// 场景:打印班级花名册
string[] students = { "张三", "李四", "王五", "赵六" };
// ✅ foreach --- 不用管索引,直接拿元素
foreach (var name in students)
{
Console.WriteLine(name);
}
// for --- 需要知道"第几个",用索引
for (int i = 0; i < students.Length; i++)
{
Console.WriteLine($"第{i + 1}名:{students[i]}");
}
选择规则:
| 用 foreach | 用 for |
|---|---|
| 每个元素都要处理 | 需要知道序号 |
| 不需要修改集合 | 需要跳过/只处理一部分 |
| 代码更短更安全 | 需要反向遍历 |
常见坑:
- foreach 遍历期间不能修改集合 → 抛异常。方案:先
ToList()复制一份再遍历 - foreach 变量是只读的,不能给元素重新赋值(值类型)
五、焦斑与硬件控制
5.1 焦斑 = 手电筒调焦
电子束打在阳极靶上的点大小,决定光源大小:
小焦斑(mode=0): 大焦斑(mode=2):
┌──┐ ┌──────┐
│很│ ← 光源小 │ 很亮 │ ← 光源大
│清│ 边缘锐利 │ 但糊 │ 边缘模糊
│楚│ 但暗 │ │ 但亮
└──┘ └──────┘
比喻:手电筒聚光模式(光斑小,照得远但范围窄)vs 泛光模式(光斑大,照得近但范围广)。工厂检测优先亮 → 默认大焦斑。
5.2 开光顺序:先设参再开灯
SetFocus → SetVoltage → SetCurrent → 停上一轮 → 开光 → 等稳定
① ② ③ ④ ⑤ ⑥
比喻:开煤气灶 --- 先调火大小,再打火。不能先打着火再调大小。
语法 5:async/await --- 排队做事,不走神
csharp
// 场景:早上出门流程
async Task MorningRoutine()
{
BrushTeeth(); // 刷牙(同步,瞬间)
WashFace(); // 洗脸(同步,瞬间)
await BoilWater(); // 烧水(异步,要等5分钟)
// ↑ await = 站在这等着,水开了才往下走
await MakeCoffee(); // 冲咖啡(异步,要等1分钟)
// ↑ 必须水开了才能冲,顺序不能乱
Console.WriteLine("出门!");
}
// ❌ 错误:没有 await → BoilWater 还没开,MakeCoffee 就开始冲了
BoilWater(); // 发了指令就跑了
MakeCoffee(); // 水还凉着呢就开始冲 --- 崩
语法拆解:
async Task--- 声明"这个方法里有 await"await--- "这一步没做完之前,不准往下走"- 不加 await → 方法发了个指令就跑,不等结果
async ≠ 快。async 是"等的时候不卡住 UI",不是"跑的更快"。
常见坑:
- 写了
async但方法里没有await→ 编译器警告 await忘了写 → 异步变同步的假象,时序错乱Task.Result死锁 --- UI 线程上 await,别用 .Result
5.3 状态机:上楼不能跳级
Offline → OnlineLocal → OnlineRemote
(没上线) (本地控制) (远程遥控)
不能跳级。像进公司大楼:先刷卡进大门,再进办公室。
语法 6:守卫子句(Guard Clause)
为什么需要它:把"不合法的情况"拦在门口,后续代码只处理合法的。
csharp
// ❌ 层层嵌套,越读越深
void Process(User user)
{
if (user != null)
{
if (user.IsActive)
{
if (user.HasPermission)
{
DoWork(); // 三层嵌套才到正事
}
}
}
}
// ✅ 守卫子句:不合法就滚,合法的继续
void Process(User user)
{
if (user == null) return; // null 直接滚
if (!user.IsActive) return; // 不活跃滚
if (!user.HasPermission) return; // 没权限滚
DoWork(); // 走到这的一定是合法用户,放心做事
}
语法点:
if (不合法) return/throw--- 守卫子句的核心句式- 前面拦完,后面代码不用再判 null、不用再判状态
什么时候用:方法开头检查参数合法性、检查前置条件。
常见坑 :守卫子句处理完 null 了,后面别再用 ?. 画蛇添足。
5.4 轮询等稳定:隔一会看一眼
开光后电压不是瞬间到位 → 隔 100ms 看一次 → 到了继续、没到等、超时报错。
比喻:微波炉热牛奶 --- 隔一会打开看热了没,没热继续,超时还没热就是微波炉坏了。
语法 7:while 轮询 + Stopwatch 超时
csharp
// 场景:等水烧到 100°C,最多等 30 秒
async Task<bool> WaitForBoiling(int timeoutMs = 30000)
{
var timer = Stopwatch.StartNew(); // 秒表开始计时
while (timer.ElapsedMilliseconds < timeoutMs)
{
double temp = ReadTemperature();
if (temp >= 100)
{
return true; // 水开了!
}
await Task.Delay(500); // 没开,等半秒再测
}
return false; // 超时,水还是没开 → 可能壶坏了
}
语法拆解:
Stopwatch.StartNew()--- 启动秒表并返回,一行 = new + Startwhile (条件)--- 条件成立就继续循环Stopwatch.ElapsedMilliseconds--- 从 Start 到现在经过了多少毫秒Task.Delay(500)--- 等 500ms,不卡 UI 。和Thread.Sleep完全不同- 两个出口:①
return true成功 ②return false超时失败
Stopwatch vs DateTime.Now:
Stopwatch用硬件高精度计时器,误差微秒级DateTime.Now用系统时钟,误差可能十几毫秒,而且可能被用户改时间- 测性能、做超时 → 一律用 Stopwatch
常见坑:
Task.Delay不精确 --- 可能 501ms、510ms,看系统调度- while 忘了
await Task.Delay→ 死循环吃满 CPU,一个核 100% - while 条件只写
true不写超时 → 永远出不来
六、数据组织
6.1 超大元件切格子
元件比探测器视野大 → 一张拍不全 → 切成 N×M 个格子逐格拍:
┌────┬────┬────┐
│1,1 │1,2 │1,3 │ ← 每个格子是一个"虚拟元件"
├────┼────┼────┤ 标记 IsVirtual = true
│2,1 │2,2 │2,3 │
├────┼────┼────┤
│3,1 │3,2 │3,3 │
└────┴────┴────┘
格子数 = Math.Ceiling(元件宽 ÷ 视野宽) --- 向上取整,2.3 → 拍 3 格,不能漏。
比喻:A3 纸放不进扫描仪 → 折 4 张 A4 → 逐张扫 → 拼回去。
6.2 不同元件 → 不同处理器
元件类型 ──→ 字典查找 ──→ 对应的检测处理器
"A类" A处理器(检空洞、桥接)
"B类" B处理器(检分段、虚焊)
"C类" C处理器(2D单张即可,不用CT)
比喻:医院分诊台 --- 挂号时告诉护士症状,护士分到对应科室(内科/外科/骨科)。
附录:语法速查表
| # | 语法点 | 一句话 | 章节 |
|---|---|---|---|
| 1 | Math.Ceiling |
向上取整,2.3→3,宁可多不能少 | 二 |
| 2 | Math.Floor / Math.Truncate |
向下取整 / 向零截断 | 二 |
| 3 | switch 表达式 | value switch { pattern => result } 一行赋值 |
三 |
| 4 | Dictionary<K,V> |
O(1) 键值查找,用 TryGetValue 安全取值 | 三 |
| 5 | foreach vs for |
遍历选择:不管索引用 foreach,要序号用 for | 四 |
| 6 | async/await |
排队执行,await 不等完不走 | 五 |
| 7 | 守卫子句 | if(非法) return 把不合法的拦门口 |
五 |
| 8 | Stopwatch |
高精度计时,ElapsedMilliseconds 拿耗时 |
五 |
| 9 | while 轮询 + Task.Delay |
循环检查 + 超时保护 | 五 |
