初始导入:PhoneRail 五路手机直供电源板
原理图/PCB 已在 EasyEDA 里做到 12 页跨页 0 漏接,固件刚完成骨架包 A1。 这次是把已有的一切一次性纳入版本控制,没有改动任何设计内容。 纳入: - 设计与需求文档、PROGRESS.md、小米8 参考原理图 PDF 与追脚脚本 - .claude/ 下的 EasyEDA 桥接工具链与几何/网表缓存(eda-parts.json、 pcb-parts.json、pcb-pads.json、p2-ch1.json 等,是重画各页的唯一事实来源) - 117 张验证截图(本项目所有方向问题都是靠人眼看截图发现的,属于证据不是垃圾) - firmware/ 的 A1 骨架与 site/ 文档站源码 .gitignore 排除:构建产物(firmware/build、host/build、sdkconfig)、 site/node_modules、__pycache__、桥接日志 _bridge.log、脚本自标的 *.stale。 .gitattributes 默认 `* -text`:现有 .py/.json 大多是 CRLF,原样存, 不让 git 重写行尾。唯一例外是 *.sh 强制 LF —— 带 \r 的 shell 脚本 bash 跑不了。 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QYuExfgyYa6DzRRcUso8M8
这个提交包含在:
@@ -0,0 +1,181 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""下板 PCB1:**每个还要接的焊盘,能不能走得出去**。
|
||||
|
||||
/c/Python314/python .claude/pcb1_escape_audit.py # 全部
|
||||
/c/Python314/python .claude/pcb1_escape_audit.py --net SDA # 只看这几个网络
|
||||
/c/Python314/python .claude/pcb1_escape_audit.py --why U8.3 # 单个焊盘,列出挡路的铜
|
||||
|
||||
## 为什么要单独有这一支
|
||||
|
||||
布线器报「布不通」的时候,看不出根因是**这一头出不来**还是**中间那段挤**。
|
||||
两者的处理办法完全不同:前者要动扇出过孔 / 摆位(几何上就没有出路,
|
||||
换多少种布线策略都一样),后者才是布线器的事。
|
||||
|
||||
判据:在焊盘周围开一个 8 x 8 mm 的窗,按 0.05 mm 栅格算「0.2 mm 的线能走的格」,
|
||||
从出线段末端泛洪,看能不能摸到窗边。摸不到 = **这个焊盘被围死了**。
|
||||
(窗开 8 mm 是因为 0.5 脚距 QFN 的死胡同都在 2~3 mm 以内;
|
||||
再大就分不清「出不来」和「出得来但走不远」了。)
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
from scipy import ndimage
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
|
||||
import pcb1_route_sig as R # noqa: E402
|
||||
|
||||
E, D = R.E, R.D
|
||||
WIN = 4.0 # 窗口半边长(mm)
|
||||
G = 0.05
|
||||
W = 0.20
|
||||
|
||||
|
||||
def audit(pads, obstacles, tags, verbose_tag=None):
|
||||
bytag = {p['tag']: p for p in pads}
|
||||
stubs = getattr(audit, 'stubs', {})
|
||||
alts = getattr(audit, 'alts', {})
|
||||
bad = []
|
||||
for tag in sorted(tags):
|
||||
q = bytag.get(tag)
|
||||
if q is None:
|
||||
continue
|
||||
# ⚠️ **每一个出线方向都要试。**只试首选方向会把
|
||||
# `U13.2` 这种“南边是死胡同、北边其实通”的误报进来 ——
|
||||
# `route_net()` 本来就会在失败时换方向重试。
|
||||
cands = list(alts.get(tag) or ([stubs[tag]] if tag in stubs else [(None, 0)]))
|
||||
best = None
|
||||
for st in cands:
|
||||
r = _breathe(q, st if st[0] else None, obstacles)
|
||||
if r is None:
|
||||
best = None
|
||||
break
|
||||
best = r if best is None else max(best, r)
|
||||
if best is None:
|
||||
continue
|
||||
bad.append((tag, q['net'], u'被围死:%d 个出线方向全试过,'
|
||||
u'最好的一个两层加起来只能走 %.2f mm²'
|
||||
% (len(cands), best)))
|
||||
if tag == verbose_tag:
|
||||
st = cands[0]
|
||||
sx, sy = st[0] if st[0] else (q['x'], q['y'])
|
||||
box = (q['x'] - WIN, q['y'] - WIN, q['x'] + WIN, q['y'] + WIN)
|
||||
f = E.Field(obstacles, box, skip_net=q['net'], grid=G)
|
||||
_why(f, q, sx, sy, obstacles, q['lay'] if q['lay'] != 12 else E.L_TOP)
|
||||
return bad
|
||||
|
||||
|
||||
def _breathe(q, st, obstacles):
|
||||
u"""从这个出线方向能不能走到窗边。能 → None;不能 → 能走的面积 mm²。否则"""
|
||||
if True:
|
||||
sx, sy = st[0] if st else (q['x'], q['y'])
|
||||
box = (q['x'] - WIN, q['y'] - WIN, q['x'] + WIN, q['y'] + WIN)
|
||||
f = E.Field(obstacles, box, skip_net=q['net'], grid=G)
|
||||
f.set_relax([E.sh_rect(cx, cy, hw, hh, R.pad_layers(q))
|
||||
for cx, cy, hw, hh in R.pad_rects(q)]
|
||||
+ ([E.sh_seg(q['x'], q['y'], sx, sy, st[1], q['lay'])] if st else []))
|
||||
lay = q['lay'] if q['lay'] != 12 else E.L_TOP
|
||||
# ⚠️ **两层一起泛洪,中间按 `via_ok()` 打通。**
|
||||
# 只在焊盘自己那一层上算的话,「出来一步就打孔翻到底层」这种最常见的逃法
|
||||
# 会被判成走不通 —— 第一版就是这样,报出 60 多个假阳性
|
||||
# (`U23.9` / `U6.*` 明明单独布都能通)。
|
||||
ok = {L: f.free(L, W) for L in (E.L_TOP, E.L_BOT)}
|
||||
vok = f.via_ok()
|
||||
i, j = f._idx(sx, sy)
|
||||
if not (0 <= i < f.nx and 0 <= j < f.ny):
|
||||
return None
|
||||
if not ok[lay][j, i]:
|
||||
return 0.0
|
||||
seed = {L: np.zeros_like(ok[L]) for L in (E.L_TOP, E.L_BOT)}
|
||||
seed[lay][j, i] = True
|
||||
oth = {E.L_TOP: E.L_BOT, E.L_BOT: E.L_TOP}
|
||||
for _it in range(8):
|
||||
grew = False
|
||||
for L in (E.L_TOP, E.L_BOT):
|
||||
r = ndimage.binary_propagation(seed[L], mask=ok[L])
|
||||
if (r != seed[L]).any():
|
||||
seed[L] = r
|
||||
grew = True
|
||||
for L in (E.L_TOP, E.L_BOT):
|
||||
cross = seed[L] & vok & ok[oth[L]]
|
||||
if (cross & ~seed[oth[L]]).any():
|
||||
seed[oth[L]] = seed[oth[L]] | cross
|
||||
grew = True
|
||||
if not grew:
|
||||
break
|
||||
# ⚠️ 环到哪一圈才算「出去了」:窗口外那圈墙本身是障碍,
|
||||
# 紧贴着它的十几格 `free()` 永远是假的(edt 不够)。
|
||||
# 拿自由区最外圈当判据会把每一个焊盘都报成“出不去”。
|
||||
nb = max(1, int(round(E.Field.RIM / G)))
|
||||
rim = nb + int(round((E.CLR + W / 2.0 + f.qerr) / G)) + 2
|
||||
edge = np.zeros(ok[lay].shape, bool)
|
||||
edge[rim, :] = True
|
||||
edge[-rim - 1, :] = True
|
||||
edge[:, rim] = True
|
||||
edge[:, -rim - 1] = True
|
||||
if any((seed[L] & edge).any() for L in (E.L_TOP, E.L_BOT)):
|
||||
return None
|
||||
return sum(int(seed[L].sum()) for L in (E.L_TOP, E.L_BOT)) * G * G
|
||||
|
||||
|
||||
def _why(f, q, sx, sy, obstacles, lay):
|
||||
u"""把挡在这个焊盘 3 mm 内的异网铜按距离列出来。"""
|
||||
print(u' —— %s 周围 3 mm 内的异网铜(按到出线段末端的距离)——' % q['tag'])
|
||||
rows = []
|
||||
for s, n in obstacles:
|
||||
if n == q['net'] or lay not in s[-1]:
|
||||
continue
|
||||
if s[0] == 'rect':
|
||||
d = D.seg_rect((sx, sy), (sx, sy), s[1], s[2], s[3], s[4])
|
||||
what = u'焊盘/铺铜 (%.2f,%.2f) %.2fx%.2f' % (s[1], s[2], s[3] * 2, s[4] * 2)
|
||||
elif s[0] == 'via':
|
||||
d = math.hypot(sx - s[1], sy - s[2]) - s[3]
|
||||
what = u'过孔 (%.2f,%.2f)' % (s[1], s[2])
|
||||
else:
|
||||
d = D.seg_pt(s[1], s[2], s[3], s[4], sx, sy) - s[5]
|
||||
what = u'走线 (%.2f,%.2f)-(%.2f,%.2f) w%.2f' % (s[1], s[2], s[3], s[4], s[5] * 2)
|
||||
if d < 3.0:
|
||||
rows.append((d, n, what))
|
||||
for d, n, what in sorted(rows)[:14]:
|
||||
print(u' %6.3f %-11s %s' % (d, n or u'(空)', what))
|
||||
|
||||
|
||||
def main():
|
||||
argv = sys.argv[1:]
|
||||
want = None
|
||||
if '--net' in argv:
|
||||
want = set(a for a in argv[argv.index('--net') + 1:] if not a.startswith('--'))
|
||||
why = None
|
||||
if '--why' in argv:
|
||||
why = argv[argv.index('--why') + 1]
|
||||
|
||||
pads, vias, lines = R.board()
|
||||
pours = R.pour_rects()
|
||||
obstacles = R.obstacles_of(pads, vias, lines, pours)
|
||||
js = R.jobs(pads, want, skip=() if want else R.SKIP)
|
||||
need = set()
|
||||
for net, frags in js:
|
||||
for fr in frags:
|
||||
if len(fr['pads']) == 1 and not fr['vias'] and not fr['lines']:
|
||||
need.add(fr['pads'][0]['tag'])
|
||||
order = [n for n, _f in js]
|
||||
stubs, alts = R.plan_stubs(order, pads, obstacles, need)
|
||||
audit.stubs = stubs
|
||||
audit.alts = alts
|
||||
if why:
|
||||
need = set([why])
|
||||
print(u'要接的孤立焊盘 %d 个,其中留到出线段的 %d 个' % (len(need), len(stubs)))
|
||||
bad = audit(pads, obstacles, need, verbose_tag=why)
|
||||
print(u'\n**被围死的焊盘 %d 个**:' % len(bad))
|
||||
for tag, net, msg in bad:
|
||||
print(u' [!!] %-10s %-12s %s' % (tag, net, msg))
|
||||
if not bad:
|
||||
print(u' (没有)')
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
在新工单中引用
屏蔽一个用户