R2 底板:按用户布局重布风扇窗口 + L1 走廊重开 + 位号丝印重排

布线(窗口整体重布,已下发):
- 拆窗口内 43 段(用户手布的那批,用户选「拆掉重来」),补 139 段 / 7 孔,
  GND 回流孔 10 个;fan_check 对板上真实快照逐项净距达标(含「新孔 vs 内层线」)。
- ⛔ 根因:pcb_base.PD_L1 里的 L1 走廊是按**旧摆位**开的(注释坐标全是旧的),
  用户重摆后与件对不上 ⇒ 四条长途网必然布不通。重开六条走廊后未完成 4 → 2。
  走廊必须盖住焊盘正上方,只盖沿途不行。断面:几何 11.80 / 铺铜后实测 9.82(要 8.0)。
- 与拆线前逐网对比:没引入任何新的连通性问题(GND 未接 58→48、+3V3 2→0、
  两路 TACH 2/1→0、+12V 一字不差)。

定版:板上铜以**用户自己布好的那版**为准,不再改动。

丝印(silk_base.py,已下发并读回核对 150/150):
- 压焊盘 128→4、压过孔 48→8、越框 8→3、互相重叠 34 对→0;字号不变。
- 三条坑:rotation 是度且不归一化(900=180°);别拿「本体≥5mm」当保留白名单
  (会豁免掉 R9A/D3/U13 这些该重排的件);「离本体」必须设硬上限 + 降级策略。

新工具:silk_base.py(丝印重排)、l1_section.py(L1 12V 主块逐 y 断面核算)、
fan_rip.py(按窗口拆线,拆前后比对全板线/孔总数防连带删除)。

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
这个提交包含在:
zhou yang
2026-09-12 12:02:32 +08:00
共同撰写人 Claude Opus 5
父节点 51b570a59f
当前提交 312089326e
共修改 15 个文件,包含 5053 行新增和 1139 行删除
文件差异因一行或多行过长而隐藏
文件差异因一行或多行过长而隐藏
文件差异内容过多而无法显示 加载差异
+321 -165
查看文件
@@ -1,10 +1,4 @@
{ {
"C1": [
10.25,
18.75,
270,
2
],
"C10": [ "C10": [
12.5, 12.5,
71.75, 71.75,
@@ -17,17 +11,95 @@
0, 0,
2 2
], ],
"C12": [
26.9,
15.5,
270,
2
],
"C13": [
24.85,
23.86,
180,
2
],
"C14": [
2.95,
28.45,
0,
2
],
"C15": [
8.25,
28.45,
0,
2
],
"C16": [
13.55,
28.45,
0,
2
],
"C17": [
18.85,
28.45,
0,
2
],
"C18": [
19.2,
24.85,
0,
2
],
"C19": [
19.75,
23.6,
0,
2
],
"C2": [ "C2": [
5.5, 5.5,
35.0, 35.0,
180, 180,
1
],
"C20": [
19.75,
17.8,
0,
2
],
"C21": [
19.2,
15.1,
90,
2
],
"C22": [
23.9,
15.0,
0,
2
],
"C23": [
23.9,
26.3,
0,
2
],
"C24": [
23.9,
28.3,
0,
2 2
], ],
"C3": [ "C3": [
19.5, 19.5,
35.0, 35.0,
0, 0,
2 1
], ],
"C4": [ "C4": [
17.5, 17.5,
@@ -36,9 +108,9 @@
2 2
], ],
"C5": [ "C5": [
9.0, 1.5,
28.5, 21.75,
90, 270,
2 2
], ],
"C6": [ "C6": [
@@ -156,9 +228,9 @@
2 2
], ],
"C738": [ "C738": [
8.5, 7.9248,
83.5, 78.1729,
180, 270,
2 2
], ],
"C739": [ "C739": [
@@ -168,75 +240,75 @@
2 2
], ],
"C740": [ "C740": [
38.0, 42.926,
43.75, 41.578,
180, 90,
2 2
], ],
"C741": [ "C741": [
36.25, 41.148,
40.5, 37.338,
270, 270,
2 2
], ],
"C742": [ "C742": [
58.25, 57.7,
38.25, 38.2,
270, 0,
2 2
], ],
"C743": [ "C743": [
42.0, 45.3,
38.5, 41.64,
270, 180,
2 2
], ],
"C744": [ "C744": [
43.5, 43.18,
34.5, 36.449,
270, 180,
2 2
], ],
"C745": [ "C745": [
42.0, 43.18,
36.5, 37.465,
270, 180,
2 2
], ],
"C746": [ "C746": [
40.25, 45.3,
41.0, 44.06,
270, 180,
2 2
], ],
"C747": [ "C747": [
58.25, 52.95,
43.0, 45.25,
270, 90,
2 2
], ],
"C748": [ "C748": [
58.25, 57.7,
40.25, 39.41,
90, 0,
2 2
], ],
"C749": [ "C749": [
44.75, 48.75,
34.25, 32.42,
90, 270,
2 2
], ],
"C750": [ "C750": [
40.25, 49.2,
43.0, 45.91,
270, 90,
2 2
], ],
"C751": [ "C751": [
43.75, 65.5,
37.5, 36.5,
270, 0,
2 2
], ],
"C752": [ "C752": [
@@ -270,33 +342,33 @@
2 2
], ],
"D2": [ "D2": [
20.75, 12.192,
94.25, 77.724,
270, 270,
2 2
], ],
"DZ1": [ "D3": [
16.25, 19.25,
21.75, 4.0,
90, 270,
2 2
], ],
"DZ3": [ "DZ3": [
25.75, 1.397,
90.25, 82.296,
90, 90,
2 2
], ],
"F1": [ "F1": [
12.5, 12.5,
17.75, 11.8,
90, 90,
2 2
], ],
"J1": [ "J1": [
12.5, 12.5,
10.63, 5.25,
90, 180,
2 2
], ],
"J23": [ "J23": [
@@ -306,22 +378,22 @@
2 2
], ],
"J26": [ "J26": [
13.5, 12.446,
94.5, 94.289,
0, 0,
2 1
], ],
"J27": [ "J27": [
13.5, 7.112,
88.0, 84.443,
0, 0,
2 1
], ],
"J33": [ "J33": [
80.0, 80.0,
95.5, 95.5,
0, 0,
2 1
], ],
"J34": [ "J34": [
49.0, 49.0,
@@ -365,15 +437,15 @@
0, 0,
2 2
], ],
"Q1": [ "L4": [
12.5, 10.4,
24.5, 20.7,
180, 180,
2 2
], ],
"Q10": [ "Q10": [
26.0, 17.018,
81.75, 81.153,
180, 180,
2 2
], ],
@@ -384,33 +456,51 @@
2 2
], ],
"Q6": [ "Q6": [
21.25, 22.1032,
88.25, 86.3586,
0, 270,
2 2
], ],
"Q7": [ "Q7": [
24.75, 4.318,
95.75, 79.629,
270, 180,
2 2
], ],
"Q8": [ "Q8": [
14.0, 4.2148,
83.0, 76.2031,
0, 180,
2 2
], ],
"Q9": [ "Q9": [
26.0, 13.208,
77.25, 89.916,
90, 180,
2 2
], ],
"R1": [ "R10": [
8.75, 24.85,
21.75, 22.64,
270, 0,
2
],
"R11": [
24.85,
21.42,
180,
2
],
"R12": [
24.2,
16.58,
0,
2
],
"R13": [
24.2,
17.8,
0,
2 2
], ],
"R2": [ "R2": [
@@ -426,20 +516,20 @@
2 2
], ],
"R4": [ "R4": [
1.25, 3.937,
42.5, 42.418,
180, 0,
2 2
], ],
"R5": [ "R5": [
4.0, 4.318,
42.25, 45.085,
90, 270,
2 2
], ],
"R6": [ "R6": [
6.75, 3.937,
43.25, 41.402,
0, 0,
2 2
], ],
@@ -545,6 +635,12 @@
0, 0,
2 2
], ],
"R7": [
19.0,
11.25,
90,
2
],
"R706": [ "R706": [
16.75, 16.75,
71.25, 71.25,
@@ -558,39 +654,39 @@
2 2
], ],
"R708": [ "R708": [
40.25, 45.3,
38.0, 42.85,
270, 180,
2 2
], ],
"R709": [ "R709": [
35.75, 51.181,
37.25, 29.845,
270, 90,
2 2
], ],
"R710": [ "R710": [
41.75, 50.165,
34.0, 29.845,
270, 270,
2 2
], ],
"R711": [ "R711": [
39.5, 54.229,
34.25, 29.845,
90, 270,
2 2
], ],
"R712": [ "R712": [
38.0, 53.213,
37.25, 29.845,
90, 270,
2 2
], ],
"R716": [ "R716": [
37.25, 52.197,
34.25, 29.845,
90, 270,
2 2
], ],
"R717": [ "R717": [
@@ -606,15 +702,15 @@
2 2
], ],
"R719": [ "R719": [
64.75, 55.245,
34.25, 29.845,
90, 270,
2 2
], ],
"R720": [ "R720": [
38.5, 49.071,
41.0, 29.847,
90, 270,
2 2
], ],
"R721": [ "R721": [
@@ -629,88 +725,136 @@
90, 90,
2 2
], ],
"R723": [
42.926,
39.751,
270,
2
],
"R724": [
43.2,
35.35,
180,
2
],
"R725": [
43.2,
34.15,
180,
2
],
"R726": [
45.3,
33.6,
180,
2
],
"R727": [
57.7,
34.18,
180,
2
],
"R728": [
57.7,
35.4,
180,
2
],
"R729": [
57.7,
42.45,
180,
2
],
"R733": [ "R733": [
22.25, 24.5752,
85.0, 84.9776,
0, 90,
2 2
], ],
"R734": [ "R734": [
22.25, 22.5712,
82.75, 83.6586,
180, 180,
2 2
], ],
"R746": [ "R746": [
19.75, 4.064,
78.5, 82.042,
180, 180,
2 2
], ],
"R747": [ "R747": [
25.75, 5.9356,
86.0, 78.0532,
180, 90,
2 2
], ],
"R748": [ "R748": [
17.0, 5.08,
77.0, 73.914,
90, 180,
2 2
], ],
"R749": [ "R749": [
17.5, 3.175,
83.5, 73.914,
90, 0,
2 2
], ],
"R750": [ "R750": [
7.0, 9.906,
89.25, 91.313,
270, 180,
2 2
], ],
"R751": [ "R751": [
4.75, 10.541,
89.25, 89.408,
90, 90,
2 2
], ],
"R752": [ "R752": [
2.5, 12.7,
88.75, 87.884,
90, 180,
2 2
], ],
"R753": [ "R753": [
22.75, 15.621,
76.25, 89.535,
270, 90,
2 2
], ],
"R754": [ "R754": [
17.75, 12.573,
80.75, 82.55,
0, 180,
2 2
], ],
"R755": [ "R755": [
14.75, 14.5288,
79.75, 80.645,
0, 90,
2 2
], ],
"R756": [ "R756": [
14.25, 16.51,
77.5, 78.867,
0, 180,
2 2
], ],
"R757": [ "R757": [
22.5, 18.542,
80.0, 83.566,
90, 180,
2
],
"R8": [
24.85,
20.2,
180,
2 2
], ],
"R9A": [ "R9A": [
@@ -750,9 +894,9 @@
2 2
], ],
"RT602": [ "RT602": [
19.5, 26.9,
8.0, 19.3,
0, 90,
2 2
], ],
"Rs6": [ "Rs6": [
@@ -780,9 +924,9 @@
2 2
], ],
"U29": [ "U29": [
2.25, 2.032,
45.75, 45.085,
180, 270,
2 2
], ],
"U30": [ "U30": [
@@ -809,6 +953,18 @@
0, 0,
2 2
], ],
"U36": [
23.7,
10.4,
270,
2
],
"U37": [
21.0,
20.7,
270,
2
],
"U6": [ "U6": [
56.0, 56.0,
59.16, 59.16,
@@ -822,8 +978,8 @@
2 2
], ],
"Y1": [ "Y1": [
42.8, 39.878,
41.68, 40.64,
270, 270,
2 2
] ]
文件差异因一行或多行过长而隐藏
文件差异内容过多而无法显示 加载差异
+100
查看文件
@@ -0,0 +1,100 @@
{
"window": [
0.5,
73.5,
26.84,
97.09
],
"nets": [
"$6N45",
"$6N48",
"$6N49",
"$6N55",
"$6N64",
"$6N74",
"+3V3",
"FAN_12V_SW",
"FAN_EN",
"FAN_IN_TACH_RAW",
"FAN_OUT_TACH_RAW",
"FAN_PWM",
"FAN_PWM_L",
"FAN_TACH_IN",
"FAN_TACH_OUT"
],
"out_pen": 12,
"priority": [
"$6N49",
"$6N64",
"$6N45",
"$6N74",
"$6N48",
"$6N55",
"FAN_IN_TACH_RAW",
"FAN_OUT_TACH_RAW",
"FAN_PWM_L",
"FAN_12V_SW",
"FAN_TACH_IN",
"FAN_TACH_OUT",
"FAN_PWM",
"FAN_EN",
"+3V3"
],
"_note": "插槽方向在 repair_window._pen 里是 block_rect 封死,不是过路费;slot_pen 已证明无效(A* 宁可付 200 也抄近路),故删除 | pd_l1_extra = 按用户 2026-09-12 新摆位重开的 L1 走廊(旧的四条是按旧摆位开的,与件对不上 ⇒ 长途网必然布不通);逐 y 断面 11.80 ≥ 8.0,见 l1_section.py",
"pd_l1_extra": [
[
"FAN_EN",
[
12.0,
76.1,
27.5,
77.1
]
],
[
"FAN_TACH_IN",
[
16.0,
75.15,
27.5,
76.05
]
],
[
"FAN_TACH_IN",
[
13.7,
76.05,
16.2,
90.4
]
],
[
"FAN_TACH_OUT",
[
17.4,
80.7,
27.5,
81.7
]
],
[
"FAN_TACH_OUT",
[
17.4,
81.7,
18.8,
84.1
]
],
[
"FAN_PWM_L",
[
10.3,
83.9,
22.5,
87.3
]
]
]
}
文件差异因一行或多行过长而隐藏
+97
查看文件
@@ -0,0 +1,97 @@
# -*- coding: utf-8 -*-
u"""按坐标**拆掉窗口内待布网的旧铜**(下发新布线之前的第一步)。
/c/Python314/python .claude/r2/fan_rip.py # 只列清单,不动板
/c/Python314/python .claude/r2/fan_rip.py --push # 真删
⛔ 2026-09-12 实测:**删外层段会连带清掉过孔和它在内层的延续** —— 点名删 6 段,读回发现线少 12 段、
孔少 8 个,多掉的全在 L15/L16。⇒ 拆前后都要比对**全板**线 / 孔总数,不能只数自己点名的那几段。
这里每次都先读回全板、删完再读回,逐项打印差值。
判据与 `repair_window` 同口径:`_base_window.json` 的窗口 + 网名,**两个端点都在窗口内**才算窗口内的铜
(跨窗边界的段是主干接口,必须留着)。
"""
import io
import json
import math
import os
import sys
sys.stdout.reconfigure(encoding='utf-8')
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(HERE))
sys.path.insert(0, HERE)
from eda import run # noqa: E402
import pcb_base as B # noqa: E402
MIL = 0.0254
CFG = json.load(io.open(os.path.join(HERE, '_base_window.json'), encoding='utf-8'))
WIN = CFG['window']
NETS = set(n.replace('@', '$') for n in CFG['nets'])
OPEN = ("await eda.dmt_EditorControl.openDocument('%s');"
"await new Promise(r=>setTimeout(r,2500));" % B.PCB)
READ = OPEN + """
const L=(await eda.pcb_PrimitiveLine.getAll()||[]).filter(l=>[1,2,15,16].includes(l.layer))
.map(l=>[l.primitiveId,l.net,l.layer,l.startX,l.startY,l.endX,l.endY,l.lineWidth]);
const V=(await eda.pcb_PrimitiveVia.getAll()||[]).map(v=>[v.primitiveId,v.net,v.x,v.y]);
return {L, V};
"""
def inw(x, y):
return WIN[0] <= x <= WIN[2] and WIN[1] <= y <= WIN[3]
def read():
r = run(READ, timeout=300)
assert r.get('success'), json.dumps(r, ensure_ascii=False)[:400]
return r['result']
def main():
d = read()
L, V = d['L'], d['V']
outer = [l for l in L if l[2] in (1, 2)]
inner = [l for l in L if l[2] in (15, 16)]
print(u'板上:外层 %d 段 / 内层 %d 段 / 过孔 %d' % (len(outer), len(inner), len(V)))
kill = []
for pid, net, lay, x1, y1, x2, y2, w in outer:
if net not in NETS:
continue
if inw(x1 * MIL, y1 * MIL) and inw(x2 * MIL, y2 * MIL):
kill.append((pid, net, lay, x1 * MIL, y1 * MIL, x2 * MIL, y2 * MIL))
killv = [(v[0], v[1]) for v in V if v[1] in NETS and inw(v[2] * MIL, v[3] * MIL)]
print(u'要拆:%d 段 / %d 孔' % (len(kill), len(killv)))
by = {}
for k in kill:
by[k[1]] = by.get(k[1], 0) + 1
for n in sorted(by):
print(u' %-18s %d 段' % (n, by[n]))
io.open(os.path.join(HERE, '_base_fan_ripped.json'), 'w', encoding='utf-8').write(
json.dumps({'segs': [[k[2], k[3], k[4], k[5], k[6], 0, k[1]] for k in kill],
'vias': []}, ensure_ascii=False))
if '--push' not in sys.argv:
print(u'(只列清单;加 --push 才真删)')
return
ids = [k[0] for k in kill] + [v[0] for v in killv]
r = run(OPEN + """
const ids = %s;
let n = 0;
for (const id of ids) { try { await eda.pcb_PrimitiveLine.delete(id); n++; } catch (e) {
try { await eda.pcb_PrimitiveVia.delete(id); n++; } catch (e2) {} } }
await eda.pcb_Document.save();
return n;
""" % json.dumps(ids), timeout=600)
print(u'删除返回:%s' % json.dumps(r.get('result'), ensure_ascii=False))
d2 = read()
o2 = [l for l in d2['L'] if l[2] in (1, 2)]
i2 = [l for l in d2['L'] if l[2] in (15, 16)]
print(u'拆后:外层 %d 段(−%d,点名 %d)/ 内层 %d 段(−%d)/ 过孔 %d(−%d)'
% (len(o2), len(outer) - len(o2), len(kill), len(i2), len(inner) - len(i2),
len(d2['V']), len(V) - len(d2['V'])))
if len(outer) - len(o2) != len(kill) or len(inner) != len(i2):
print(u'⛔ 连带删除!实际掉的和点名的对不上,检查内层 / 过孔')
if __name__ == '__main__':
main()
+71
查看文件
@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
u"""L1 `+12V` 主块的**逐 y 水平断面**核算(§6.4 要 ≥ 8.0 mm)。
/c/Python314/python .claude/r2/l1_section.py # 核 pcb_base.PD_L1 现状
/c/Python314/python .claude/r2/l1_section.py --cfg # 核 _base_window.json 里的 pd_l1_extra
主块 x 0.5~27.8(宽 27.30),电流南北流 ⇒ 断面是水平线,线上被走廊占掉的 x 跨度要扣掉。
横带(沿 x 长)很贵、竖带(沿 y 长)几乎免费 —— 旧注释「一根 0.15 的线占约 0.5」说的就是竖线。
槽指头在 x ≥ 27.5,走廊 x 一律 ≤ 27.5 ⇒ 天然不碰任何一槽的供电路径。
"""
import io
import json
import os
import sys
sys.stdout.reconfigure(encoding='utf-8')
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(HERE))
sys.path.insert(0, HERE)
import pcb_base as B # noqa: E402
MIN_SECTION = 8.0
MAIN = B.l1_12v_rects()[0]
WIDTH = MAIN[2] - MAIN[0]
def check(corridors, step=0.05):
worst = (1e9, None)
y = MAIN[1]
while y <= MAIN[3]:
iv = []
for _n, r in corridors:
if r[1] - 1e-9 <= y <= r[3] + 1e-9:
iv.append((max(r[0], MAIN[0]), min(r[2], MAIN[2])))
iv.sort()
used, spans = 0.0, []
for a, b in iv:
if b <= a:
continue
if spans and a <= spans[-1][1]:
spans[-1] = (spans[-1][0], max(spans[-1][1], b))
else:
spans.append((a, b))
for a, b in spans:
used += b - a
left = WIDTH - used
if left < worst[0]:
worst = (left, y)
y += step
return worst
def main():
cor = [(n, r) for n, r in B.PD_L1 if n.startswith('FAN')]
if '--cfg' in sys.argv:
cfg = json.load(io.open(os.path.join(HERE, '_base_window.json'), encoding='utf-8'))
cor = [(n, tuple(r)) for n, r in (cfg.get('pd_l1_extra') or [])]
print(u'主块 x %.2f~%.2f(宽 %.2f);走廊 %d 条' % (MAIN[0], MAIN[2], WIDTH, len(cor)))
for n, r in cor:
kind = u'横带' if (r[2] - r[0]) >= (r[3] - r[1]) else u'竖带'
print(u' %-14s (%6.2f, %6.2f, %6.2f, %6.2f) %5.2f×%5.2f %s'
% (n, r[0], r[1], r[2], r[3], r[2] - r[0], r[3] - r[1], kind))
left, y = check(cor)
ok = left >= MIN_SECTION
print(u'%s 逐 y 最窄断面 %.2f mm(y=%.2f),判据 ≥ %.1f' %
(u'[OK]' if ok else u'[X]', left, y, MIN_SECTION))
return 0 if ok else 1
if __name__ == '__main__':
sys.exit(main())
+614 -556
查看文件
文件差异内容过多而无法显示 加载差异
+303
查看文件
@@ -0,0 +1,303 @@
# -*- coding: utf-8 -*-
u"""**窗口内整体重布**:把一块矩形区域里的几个网连同周边 `+3V3` / `GND` 局部一起重布。
bash .claude/r2/remote_repair_window.sh # 入口(本机)
DCDC_ROUTE_SERVER=1 pypy3 .claude/r2/repair_window.py
## 和 `repair_base.py` 的区别(为什么要单独一份)
`repair_base` 是**按网**补:给它 `+3V3`,它就在**全板**范围重布这个网 —— 2026-09-12 实测,
它把新的 `+3V3` 线从 **J34~J37 插槽针排的焊盘中间穿过去**(`fan_check` 十几处 −0.2,
还压 U13.7 −0.449)。插槽 1 mm 间距、手焊,那是致命的。
这一份改成**按窗口**补,三条约束缺一不可:
1. **`bypad` 只装窗口内的脚**。窗口外的同网脚一概不参与 ⇒ 布线器不会去动全板的主干。
2. **窗口外的已有铜照常留在占用图里**。它们是「本网已有的铜」,A* 从窗口内的脚出发,
碰到就算到达 —— 这就是和外面主干的接口,不需要显式指定接口点。
3. **窗口外加高过路费**(`OUT_PEN`)。不是封死:万一窗口内绕不出去,它还能出去一点点,
但代价高到不会乱跑。封死会让本来能绕的网直接报「没布通」。
拆线(窗口内目标网的旧铜)由本机 `remote_repair_window.sh` 先做掉,这里只读拆完的板面快照。
输入:`_base_fan_input.json`(板上现有铜)+ `_base_window.json`(窗口与网名,本机写)。
输出:`_base_window_repair.json`(只有新增的段与孔)。
"""
import io
import json
import math
import os
import sys
sys.stdout.reconfigure(encoding='utf-8')
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(HERE))
sys.path.insert(0, HERE)
if not os.environ.get('DCDC_ROUTE_SERVER'):
sys.exit(u'⛔ 布线(A*)只在 10.5.5.13 上跑(CLAUDE.md):bash .claude/r2/remote_repair_window.sh')
import router as RT # noqa: E402
import route_base as RB # noqa: E402
CFG = json.load(io.open(os.path.join(HERE, '_base_window.json'), encoding='utf-8'))
WIN = tuple(CFG['window']) # (x0, y0, x1, y1)
NETS = [n.replace('@', '$') for n in CFG['nets']]
OUT_PEN = int(CFG.get('out_pen', 40))
# 朝插槽那一侧的过路费:必须高到 A* 不会为了抄近路钻进槽区(槽体 x 28~70,手焊 1 mm 针排)
SLOT_PEN = int(CFG.get('slot_pen', 200))
# 虚拟落点沿主干往窗内退多少(避开压在主干线上导致起步格点为 0)
RETREAT = float(CFG.get('retreat', 0.75))
# 窗口内「别的网」的焊盘外扩多少封死(收费挡不住,见 _pen 的注释)
PAD_KEEP = float(CFG.get('pad_keep', 0.10))
cp = json.load(io.open(os.path.join(HERE, '_base_fan_input.json'), encoding='utf-8'))
# ── L1 走廊(`pcb_base.PD_L1` 的运行时追加)─────────────────────────────────
# ⛔ A6 那一带 L1 整片是 `+12V` 主块(x 0.5~27.8)⇒ 过孔上得去但走不动,等于单层,
# 而 L4 已被 22 件占满。`PD_L1` 里那几条走廊是**按旧摆位**开的(注释里写的坐标就是旧的),
# 用户重摆之后与件对不上 ⇒ 长途网(FAN_EN / 两路 TACH / FAN_PWM_L)必然布不通。
# 这里让走廊跟着 `_base_window.json` 走,免得每试一组就改 `pcb_base.py`。
# ⚠ 判据(§6.4):逐 y 的水平断面上,Σ 走廊 x 跨度 ≤ 27.30 − 8.00 = 19.30 mm。
# 本机 `l1_section.py` 核;走廊 x 一律 ≤ 27.5,槽指头在 x ≥ 27.5,天然不碰。
_extra = CFG.get('pd_l1_extra')
if _extra is not None:
_keep = [(n, r) for n, r in RB.B.PD_L1 if not n.startswith('FAN')]
RB.B.PD_L1 = _keep + [(n, tuple(r)) for n, r in _extra]
print(u'L1 走廊:保留 %d 条非风扇 + 本轮 %d 条 %s'
% (len(_keep), len(_extra), ' '.join(sorted(set(n for n, _ in _extra)))))
pads = RB.load_pads()
def in_win(x, y, m=0.0):
return WIN[0] - m <= x <= WIN[2] + m and WIN[1] - m <= y <= WIN[3] + m
# ── 窗口外加过路费:包住整块板,挖掉窗口 ──────────────────────────────────
_orig_pen = RT.pen_pads
def _toll_pads():
u"""窗口内**不属待布网**的焊盘 —— 必须一起收过路费,否则新线会贴着它们走。
2026-09-12 实测:不收的话,`+3V3` 贴 `Q8.3` 只剩 **0.003**、`FAN_OUT_TACH_RAW` 贴
`Q10.2` 只剩 **0.027**(判据 0.15),`fan_check` 一跑就拦下来、整版不能下发。
`route_base._pen` 本来就是靠 `GND_PEN_PADS` / `LATER_PEN_PADS` 收这笔费的,
我覆盖 `RT.pen_pads` 时漏了这一步。
⛔ 判据是「**不是当前正在布的那个网**」,不是「不在待布网集合里」。第一版写成后者,
于是 `Q8.3`(网 `$6N48`,本身也在待布名单里)被漏掉 —— 实测它周围过路费始终是 0,
而 `+3V3` 的新线就贴着它走到 **0.003**。同属待布名单的别的网,对当前这个网来说一样是障碍。
"""
out = []
for p in pads:
if p[6] not in RT.LAY or str(p[1]).endswith(RB.TH_SUFFIX):
continue
if in_win(p[2], p[3]):
out.append(p)
return out
def _pen(g, ps, halfw):
u"""窗口外收过路费。⛔ **不能各向同性**(2026-09-12 实测)。
朝插槽那一侧(x > 窗口东界)必须近乎封死:`OUT_PEN` 从 40 降到 12 的那一轮,
`$6N64` / `FAN_EN` 立刻跑到 x 28.75~32.50 —— **正是槽体 x 28~70 的插槽区**,
183 段里 50 段跑出窗口(之前每轮都是 0)。而降它本是为了让跨窗口的网够得着 U20 那边的主干。
⇒ 分开定价:东侧(插槽方向)用 `SLOT_PEN`,其余三面用 `OUT_PEN`。
"""
_orig_pen(g, list(ps) + _toll_pads(), halfw)
# ⛔ 光收过路费**挡不住**(2026-09-12 第二次印证,同「插槽方向」那一条):
# 实测 `Q8.3` 周围过路费确实是 10,A* 照样贴着它走到 **0.003**;`Q10.2` 同理 0.027。
# `penalize` 只是定价,`block_rect` 才是墙 ⇒ 窗口内**别的网**的焊盘按 `PAD_KEEP` 外扩封死。
# 代价实测很小:外扩 0.10 时窗口内可走格点 7297 → 6802(72% → 68%,少 495 个)。
# ⛔ 外扩量必须按**当前这一档的线宽**算,不能写死。2026-09-12 实测:按 0.15 细线定的
# `PAD_KEEP=0.10` 挡不住 0.40 的 `+3V3`(它半宽就有 0.20)—— 照样贴 `Q8.3` 到 **0.003**。
# `route_groups` 是按线宽分档建图的,每档传进来的 `halfw` 正是「半宽 + 净距」,直接用它。
# (一刀切取最粗的 0.56 会把细线通道也削掉 1/3:窗口内可走格点 68% → 52%。)
r = max(PAD_KEEP, halfw)
for p in _toll_pads():
g.block_rect(p[6], p[2] - p[4] / 2 - r, p[3] - p[5] / 2 - r,
p[2] + p[4] / 2 + r, p[3] + p[5] / 2 + r, net=p[7])
for l in RT.LAY:
for r in RB.B.rect_minus((0.0, 0.0, RB.B.BW, RB.B.BH), [WIN]):
if r[0] >= WIN[2] - 1e-6:
continue # 东侧(插槽方向)下面单独封死,不是加价
g.penalize(l, r[0], r[1], r[2], r[3], OUT_PEN)
# ⛔ 插槽方向必须**封死**,不能只加价:本机实测过路费确实加到了 200
# (窗口内 0 / 插槽区 200),A* 照样钻 —— `$6N64` 宁可付 200×若干格也要从
# x 25.75 抄到 27.75,因为窗口内绕不过去。`penalize` 只是定价,`block_rect` 才是墙。
# 槽体 x 28~70 是 1 mm 间距的手焊针排,走线进去是致命的。
g.block_rect(l, WIN[2], 0.0, RB.B.BW, RB.B.BH, net='#slotwall')
RT.pen_pads = _pen
segs = [tuple(s) for s in cp['segs']]
vias = [tuple(v) for v in cp['vias']]
board_vias = [tuple(v) for v in cp['board_vias']]
# ⛔ 只装**窗口内**的脚;窗口外的同网铜留在 segs 里当「已有铜」。
bypad = {}
outside = {}
for p in pads:
if not p[7] or p[7] not in NETS or str(p[1]).endswith(RB.TH_SUFFIX):
continue
if in_win(p[2], p[3]):
bypad.setdefault(p[7], []).append(p)
else:
outside[p[7]] = outside.get(p[7], 0) + 1
# ⛔⛔ 2026-09-12 实测(第一版**漏了这一步,是设计缺陷**):
# 「窗口外的同网铜留在占用图里,A* 碰到就算到达」——**不成立**。
# `route_groups` 布的是 `bypad[net]` 里的脚**两两相连**,窗外主干根本不是它的终点判据。
# 于是「窗外还有脚」的网全军覆没:`FAN_PWM` / `FAN_EN` 窗内只剩 1 只脚 ⇒ 跨度 0、无事可做;
# `FAN_TACH_IN` / `FAN_TACH_OUT` / `+3V3` 窗内那两只脚彼此并不需要连(各自要去的是窗外主干)
# ⇒ 实测这 5 个网**一段都没产出**,而窗外脚数为 0 的 8 个网全部正常产出。
# ⇒ 把主干在**窗口边界处的端点**做成虚拟焊盘塞进 bypad,A* 才有终点可去
# (同 `route_base` 用 `finger_escape()` 的 launch 点喂金手指的办法)。
def _clip(v, lo, hi):
return max(lo, min(hi, v))
edge_pts = {}
for s in cp['segs']:
lay, x1, y1, x2, y2, w, net = s[0], s[1], s[2], s[3], s[4], s[5], s[6]
if net not in NETS or lay not in RT.LAY:
continue
a, b = in_win(x1, y1), in_win(x2, y2)
if a == b:
continue # 整段在窗内或整段在窗外,都不是接口
if lay != RB.B.BOTTOM:
# ⛔ 落点不能取在 **L1**:那一层在这一带整片是 `+12V` 大铜,格点是 255(多网叠加,
# 谁都不能走)。实测 `FAN_EN` 的落点取到 L1 之后「起步格点 0 个」,直接报「出不来」。
continue
ix, iy = (x1, y1) if a else (x2, y2) # 取落在窗内的那一头
# ⛔ 端点正好压在主干线上(`FAN_PWM` 实测净距 −0.127)⇒ 起步格点也是 0。
# 沿这一段往**窗内**退 RETREAT,让出一个能起步的格点。
ox, oy = (x2, y2) if a else (x1, y1) # 段的另一头(窗外那一侧)
seg_len = math.hypot(ix - ox, iy - oy)
if seg_len > 1e-6:
ux, uy = (ix - ox) / seg_len, (iy - oy) / seg_len
ix, iy = ix + ux * min(RETREAT, seg_len * 0.4), iy + uy * min(RETREAT, seg_len * 0.4)
edge_pts.setdefault(net, []).append((lay, round(ix, 4), round(iy, 4), w))
# 主干整条都在窗外的网:取离窗口最近的那个端点,夹到窗边上当落点
for net in NETS:
if net in edge_pts or not outside.get(net):
continue
best = None
for s in cp['segs']:
# ⛔ 同样只在 L4 找:窗缘落点分支第一版漏了这条,`FAN_EN` 的落点取到 **L1**,
# 而那一层这一带整片是 `+12V` 大铜、格点是 255(多网叠加,谁都不能走),
# 于是「起步格点 0 个 ⇒ 出不来」,本机单独验那个坐标却有 16 个可起步 —— 差别就在层。
if s[6] != net or s[0] != RB.B.BOTTOM:
continue
for (px, py) in ((s[1], s[2]), (s[3], s[4])):
cx = _clip(px, WIN[0], WIN[2])
cy = _clip(py, WIN[1], WIN[3])
d = ((px - cx) ** 2 + (py - cy) ** 2) ** 0.5
if best is None or d < best[0]:
best = (d, s[0], px, py, s[5])
if best and best[0] <= 6.0: # 主干就在窗边上:直接拿它当落点
edge_pts.setdefault(net, []).append((best[1], round(best[2], 4), round(best[3], 4), best[4]))
elif best:
# ⛔ 主干远在窗外(`FAN_EN` / `FAN_TACH_OUT` 的最近端点在 x≈42~43,**在槽区里面**,
# 离窗 14.5~15.6 mm)。**不能**放宽上限去接它 —— 那等于让 A* 从窗口里拉一条 15 mm 的线
# 横穿插槽区,正是设窗口要避免的事(上一版 `+3V3` 穿针排就是这么来的)。
# ⇒ 在**窗口边缘**给它一个落点:窗内这一段布到边上为止,窗外那一小段单独接。
ex = _clip(best[2], WIN[0] + 0.3, WIN[2] - 0.3)
ey = _clip(best[3], WIN[1] + 0.3, WIN[3] - 0.3)
edge_pts.setdefault(net, []).append((best[1], round(ex, 4), round(ey, 4), best[4]))
print(u' ⚠ %s 的主干离窗 %.1f mm(在槽区里),只布到窗缘 (%.2f, %.2f),窗外那段要另接'
% (net, best[0], ex, ey))
n_virt = 0
for net, pts in edge_pts.items():
pts.sort(key=lambda q: -q[3]) # 宽的(主干)优先
for (lay, px, py, w) in pts[:2]: # 每个网最多给两个落点,够 A* 挑
bypad.setdefault(net, []).append(
['#trunk', 'edge%d' % n_virt, px, py, 0.3, 0.3, lay, net, 'RECT'])
n_virt += 1
print(u'给跨窗口的网补了 %d 个「主干接口」虚拟落点:%s'
% (n_virt, ' '.join(sorted(edge_pts)) or u'无'))
print(u'窗口 x %.2f~%.2f y %.2f~%.2f;窗口外过路费 %d' % (WIN[0], WIN[2], WIN[1], WIN[3], OUT_PEN))
for n in NETS:
print(u' %-18s 窗口内 %d 脚 / 窗口外 %d 脚(外面的不参与,只当已有铜的接口)'
% (n, len(bypad.get(n, [])), outside.get(n, 0)))
drop = [n for n in NETS if not bypad.get(n)]
if drop:
print(u' (这些网窗口内一只脚都没有,跳过:%s)' % ' '.join(drop))
print(u'现有铜 %d 段 / %d 孔' % (len(segs), len(vias)))
n0, v0 = len(segs), len(vias)
RT.AUTO_CACHE.clear()
# ⛔ 诊断:`$6N49` / `$6N74` / `$6N45` 这几条链一路报「一根铜都没有」,而窗口空着 85%、
# 焊盘尺寸与 width_of 都正常 —— 看着不像拥塞,像**布线器压根没处理它们**。
# `route_groups` 按线宽分档、逐档调 A*,这里把「每一档收了哪些网」和每档前后的段数打出来,
# 一眼就能看出某个网是不是从头到尾没进过任何一档。
_orig_rg = RT.route_groups
def _rg(bp, pd, sg, vi, bv, auto, priority=(), only=None):
want = set(only) if only is not None else set(bp)
seen = {}
for net, ps in bp.items():
if only is not None and net not in only:
continue
seen[net] = round(RT.width_of(net, auto, ps), 3)
buckets = {}
for net, w in seen.items():
buckets.setdefault(w, []).append(net)
print(u' 分档(线宽 → 网):')
for w in sorted(buckets, reverse=True):
print(u' %.3f mm: %s' % (w, ' '.join(sorted(buckets[w]))))
miss = sorted(want - set(seen))
if miss:
print(u' ⚠ 这些网在 only 里却没进任何一档:%s' % ' '.join(miss))
before = len(sg)
out = _orig_rg(bp, pd, sg, vi, bv, auto, priority=priority, only=only)
print(u' route_groups 共产出 %d 段' % (len(sg) - before))
by = {}
for s in sg[before:]:
by[s[6]] = by.get(s[6], 0) + 1
for net in sorted(seen):
print(u' %-18s %s' % (net, (u'%d 段' % by[net]) if net in by else u'⛔ 0 段'))
return out
RT.route_groups = _rg
# ⛔ 顺序决定成败:`route_groups` 按「出口数少 → 跨度大」排,`$6N49` 排在 `FAN_12V_SW`(37 段) 之后,
# 走廊被先布的线占掉 ⇒ 它四只脚**两两直连全被挡**(本机实测 −0.499 ~ +0.139,最好的一对差 0.011),
# 报「一根铜都没有」,看着像板子挤,其实是顺序。router 自己的注释里记过同一个坑
# ("同样的摆位,先布它就通")。⇒ 把这几条挤在一起的本地链**顶到 priority 最前面**。
PRI = tuple(CFG.get('priority') or ()) + tuple(n for n in NETS if n not in (CFG.get('priority') or ()))
print(u'布线优先级:%s' % ' '.join(PRI[:6]))
logs = RT.route_groups(bypad, pads, segs, vias, board_vias, {},
priority=PRI, only=set(bypad))
if logs and '--repair' in sys.argv:
print(u' 还剩 %d 条没布通,拆线重布(只拆本次新布的)' % len(logs))
segs, vias, logs = RT.repair(bypad, pads, segs, vias, logs, board_vias, {}, n0, v0)
new_s = [list(s) for s in segs[n0:]]
new_v = [list(v) for v in vias[v0:]]
print(u'新增 %d 段 / %d 孔' % (len(new_s), len(new_v)))
for m in logs:
print(u' [!] ' + m)
# 连通性只看窗口内的脚(窗口外的主干本来就通)
chk = [q for q in RB.check_pads(pads) if q[7] in set(bypad) and in_win(q[2], q[3])]
left = RT.check_connected(chk, segs, vias)
print(u'连通性(窗口内口径):%s' % (u'全通' if not left else left))
# 跑出窗口的新铜要点名 —— 这是这套办法最该盯的失败模式
stray = [s for s in new_s if not (in_win(s[1], s[2], 0.5) and in_win(s[3], s[4], 0.5))]
print(u'跑出窗口的新线段:%d%s' % (len(stray), u' ⚠' if stray else u' ✓'))
for s in stray[:8]:
print(u' %s L%s (%.2f,%.2f)->(%.2f,%.2f)' % (s[6], s[0], s[1], s[2], s[3], s[4]))
json.dump({'window': list(WIN), 'nets': NETS, 'segs': new_s, 'vias': new_v,
'logs': list(logs), 'unconnected': list(left), 'stray': len(stray)},
io.open(os.path.join(HERE, '_base_window_repair.json'), 'w', encoding='utf-8'),
ensure_ascii=False)
print(u'已写 _base_window_repair.json')
+190
查看文件
@@ -0,0 +1,190 @@
# -*- coding: utf-8 -*-
u"""底板**位号丝印**的重排与下发。
/c/Python314/python .claude/r2/silk_base.py # 只算 + 体检(改前 / 改后对照)
/c/Python314/python .claude/r2/silk_base.py --push # 下发(pcb_PrimitiveAttribute.modify)+ 读回核对
## 判据
位号文字框(字高 `fs`、字宽 ≈ 0.62×字高×字符数,`alignMode=3` 居中)不许:
① 压同面焊盘(顶层丝印 layer 3 对 L1 + 通孔盘;底层 layer 4 对 L4 + 通孔盘)
② 压过孔 ③ 越板框(留 `EDGE` 0.5) ④ 与别的位号重叠
⚠ **大件(本体 ≥ 5 mm)的位号保留原位**:五个插槽的位号故意放在槽体西侧 x=28(离本体 21 mm),
那是设计,不是问题;U20 / 电解 / 座子同理。它们先占位,小件绕开它们。
⚠ 位号必须**紧挨器件**才有意义 ⇒ 离本体外框有硬上限 `MAXD`。够不着就降级:
理想位 → 允许压过孔 → 允许压焊盘 → 保留原位。单纯"找个空地"会把位号甩到十几毫米外。
⚠ `rotation` 字段单位是**度且不归一化**(900 = 180°),读回要 `%% 360`。
"""
import io
import json
import math
import os
import sys
sys.stdout.reconfigure(encoding='utf-8')
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(HERE))
sys.path.insert(0, HERE)
from eda import run # noqa: E402
import pcb_base as B # noqa: E402
import place_base as PL # noqa: E402
MIL = 0.0254
SNAP = os.path.join(HERE, '_base_silk.json')
PLAN = os.path.join(HERE, '_base_silk_plan.json')
CHAR_W, EDGE, GAP, MAXD = 0.62, 0.5, 0.15, 2.2
# ⛔ 保留原位的**显式名单**(位号故意远离本体,不是问题):
# 五个插槽的位号放在槽体西侧 x=28(离本体 21 mm)、U20 让开天线净空、
# 顶面件与板边 USB-C 的位号只能朝板内放。
# ⚠ 别用「本体 ≥ 5 mm」当判据:那会把 R9A(7.4×3.5) / D3(6.8×2.2) / U13(2.3×5.8)
# 这些**该重排**的中等件也豁免掉 —— 实测残留 25 个里有 18 个是这么来的。
KEEP = {'J34', 'J35', 'J36', 'J37', 'J38', 'U20',
'J26', 'J27', 'J33', 'C2', 'C3', 'J1', 'J23'}
READ = u"""
await eda.dmt_EditorControl.openDocument('%s');
await new Promise(r=>setTimeout(r,2500));
const M = 0.0254, nrm = v => ((v %% 360) + 360) %% 360;
const A = (await eda.pcb_PrimitiveAttribute.getAll()||[]).filter(a=>a.key==='Designator')
.map(a=>({ref:a.value, lay:a.layer, x:a.x*M, y:a.y*M, rot:nrm(a.rotation),
fs:a.fontSize*M, vis:a.valueVisible?1:0, id:a.primitiveId}));
const C = (await eda.pcb_PrimitiveComponent.getAll()||[])
.map(c=>({ref:c.designator, x:c.x*M, y:c.y*M, rot:nrm(c.rotation||0), lay:c.layer}));
const V = (await eda.pcb_PrimitiveVia.getAll()||[]).map(v=>[v.x*M, v.y*M, String(v.net), v.diameter*M]);
return {attrs:A, comps:C, vias:V};
""" % B.PCB
def snap():
r = run(READ, timeout=600)
d = r.get('result')
assert d, json.dumps(r, ensure_ascii=False)[:400]
io.open(SNAP, 'w', encoding='utf-8').write(json.dumps(d, ensure_ascii=False))
return d
def tbox(ref, x, y, rot, fs):
h = fs
w = CHAR_W * h * len(ref)
if rot in (90, 270):
w, h = h, w
return (x - w / 2., y - h / 2., x + w / 2., y + h / 2.)
def ov(p, q):
return not (p[2] <= q[0] or q[2] <= p[0] or p[3] <= q[1] or q[3] <= p[1])
def plan(d, pads, sz):
comp = {c['ref']: c for c in d['comps']}
V = d['vias']
padsL = {3: [p for p in pads if p[6] in (1, 12)], 4: [p for p in pads if p[6] in (2, 12)]}
def hit_pad(bb, lay):
return any(ov(bb, (p[2] - p[4] / 2., p[3] - p[5] / 2., p[2] + p[4] / 2., p[3] + p[5] / 2.))
for p in padsL[lay])
def hit_via(bb):
return any(ov(bb, (v[0] - v[3] / 2., v[1] - v[3] / 2., v[0] + v[3] / 2., v[1] + v[3] / 2.))
for v in V)
def outside(bb):
return bb[0] < EDGE or bb[1] < EDGE or bb[2] > B.BW - EDGE or bb[3] > B.BH - EDGE
placed, out, stat = [], {}, {}
for a in d['attrs']:
if a['vis'] and a['ref'] in KEEP:
placed.append((a['lay'], tbox(a['ref'], a['x'], a['y'], a['rot'], a['fs'])))
stat[u'白名单保留'] = stat.get(u'白名单保留', 0) + 1
small = [a for a in d['attrs'] if a['vis'] and a['ref'] not in KEEP]
small.sort(key=lambda a: -(sz.get(a['ref'], (1, 1))[0] * sz.get(a['ref'], (1, 1))[1]))
for a in small:
ref, c, lay, fs = a['ref'], comp.get(a['ref']), a['lay'], a['fs']
if not c:
continue
w, h = sz.get(ref, (1.0, 1.0))
if int(round(c['rot'])) % 180:
w, h = h, w
best, level = None, None
for lv in (0, 1, 2):
r = 0.0
while r <= MAXD and best is None:
for ang in (90, 270, 0, 180, 45, 135, 225, 315):
for rot in ((0, 90) if ang in (0, 180) else (0,)):
tw, th = CHAR_W * fs * len(ref), fs
if rot == 90:
tw, th = th, tw
ca, sa = math.cos(math.radians(ang)), math.sin(math.radians(ang))
if abs(ca) <= 0.7 and abs(sa) <= 0.7:
off = math.hypot(w / 2. + tw / 2., h / 2. + th / 2.) + GAP + r
elif abs(ca) > 0.7:
off = w / 2. + tw / 2. + GAP + r
else:
off = h / 2. + th / 2. + GAP + r
x, y = c['x'] + off * ca, c['y'] + off * sa
bb = tbox(ref, x, y, rot, fs)
if outside(bb) or any(b[0] == lay and ov(bb, b[1]) for b in placed):
continue
if lv < 2 and hit_pad(bb, lay):
continue
if lv < 1 and hit_via(bb):
continue
best, level = (round(x, 4), round(y, 4), rot, bb), lv
break
if best:
break
r += 0.25
if best:
break
if best:
placed.append((lay, best[3]))
out[ref] = {'x': best[0], 'y': best[1], 'rot': best[2], 'id': a['id'], 'lay': lay}
k = [u'理想位', u'允许压过孔', u'允许压焊盘'][level]
stat[k] = stat.get(k, 0) + 1
else:
stat[u'无解(保留原位)'] = stat.get(u'无解(保留原位)', 0) + 1
return out, stat
def main():
d = snap()
pads = json.load(io.open(os.path.join(HERE, '_base_pads.json'), encoding='utf-8'))
sz = PL.sizes()
p, stat = plan(d, pads, sz)
io.open(PLAN, 'w', encoding='utf-8').write(json.dumps(p, ensure_ascii=False, indent=0))
print(u'位号 %d(可见 %d);重排 %d 个' % (len(d['attrs']), sum(a['vis'] for a in d['attrs']), len(p)))
for k, v in sorted(stat.items(), key=lambda t: -t[1]):
print(u' %-14s %d' % (k, v))
if '--push' not in sys.argv:
print(u'(只算;加 --push 下发)')
return
rows = [[v['id'], v['x'] / MIL, v['y'] / MIL, v['rot']] for v in p.values()]
n = 0
for k in range(0, len(rows), 60):
r = run(u"""
await eda.dmt_EditorControl.openDocument('%s');
await new Promise(r=>setTimeout(r,1500));
const R = %s;
let n = 0;
for (const [id, x, y, rot] of R) {
try { await eda.pcb_PrimitiveAttribute.modify(id, {x: x, y: y, rotation: rot}); n++; } catch (e) {}
}
return n;
""" % (B.PCB, json.dumps(rows[k:k + 60])), timeout=600)
n += r.get('result') or 0
run(u"await eda.pcb_Document.save(); return 1;", timeout=300)
print(u' modify 成功 %d / %d' % (n, len(rows)))
# 读回核对
d2 = snap()
now = {a['ref']: a for a in d2['attrs']}
bad = [k for k, v in p.items()
if k not in now or abs(now[k]['x'] - v['x']) > 0.02 or abs(now[k]['y'] - v['y']) > 0.02
or int(now[k]['rot']) % 360 != v['rot'] % 360]
print(u'读回核对:%d 个对上,%d 个没对上 %s' % (len(p) - len(bad), len(bad), bad[:10]))
if __name__ == '__main__':
main()
+1087 -1
查看文件
文件差异内容过多而无法显示 加载差异
+650 -8
查看文件
@@ -29,9 +29,10 @@
| D11 | **DAC 沿用 DAC63204** | 固件 A4 驱动、NVM 安全默认值、R1 §4.5 三条外围硬要求零改动 | | D11 | **DAC 沿用 DAC63204** | 固件 A4 驱动、NVM 安全默认值、R1 §4.5 三条外围硬要求零改动 |
| **D11b** | ⚠️ **2026-09-08 改判:DAC 不放刀片,放底板 —— 2 颗喂 5 路**(就是 R1 的 U6/U7 那套原样搬)| 5 颗 → 2 颗;刀片再省 5 个无源件与一个 I²C 设备。**敢跨连接器的依据是设计文档 §4.4 的失效表**:`VDAC` 断线 = Rinj 悬空 = 3.890 V(正好是目标值)、对地短路 = 4.010 V,注入增益只有 0.2007,噪声与两板地差到输出衰减 5 倍。见 §15 | | **D11b** | ⚠️ **2026-09-08 改判:DAC 不放刀片,放底板 —— 2 颗喂 5 路**(就是 R1 的 U6/U7 那套原样搬)| 5 颗 → 2 颗;刀片再省 5 个无源件与一个 I²C 设备。**敢跨连接器的依据是设计文档 §4.4 的失效表**:`VDAC` 断线 = Rinj 悬空 = 3.890 V(正好是目标值)、对地短路 = 4.010 V,注入增益只有 0.2007,噪声与两板地差到输出衰减 5 倍。见 §15 |
| D12 | **D± 留一条退路**:刀片与底板各引 4 pin 1.25 mm 座 | 金手指那条路万一不稳,割断改飞 5 根短线;代价 ¥2 | | D12 | **D± 留一条退路**:刀片与底板各引 4 pin 1.25 mm 座 | 金手指那条路万一不稳,割断改飞 5 根短线;代价 ¥2 |
| **D14** | **刀片板框硬上限 98 × 98 mm**(不是 100 × 100)| 用户要求刀片控制在 10×10 cm 内。留 2 mm 余量是因为 100.0 正好压在嘉立创特价档的边界上,尺寸判定没有余地。**当前实际 98 × 76,两个方向都有富余。** | | **D14** | **刀片板框硬上限 98 × 98 mm**(不是 100 × 100)| 用户要求刀片控制在 10×10 cm 内。留 2 mm 余量是因为 100.0 正好压在嘉立创特价档的边界上,尺寸判定没有余地。~~当前实际 98 × 76~~ ⇒ **2026-09-11 用户拍板压到 90 × 68**(§34);同日晚 **主板转 180°、两条排线槽到顶部、U4 到右侧**(§35,已下发)。 |
| **D16** | **底板电源入口改 USB-C PD 取电**(2026-09-11 用户拍板「走方案 B」,取代决策 #20 的 DC-005):C 座 + CH224A 只请求 20V → LM61495 降压到 12V,12V 母线及以下不改 | 兼容各家手机氮化镓充电器的只有 PD(私有 100~120W 协议要手机鉴权);单口 20V×5A = 100W ⇒ 12V 侧约 95W,常态 10~25W 够、最坏 108W 靠固件限功率。见 §36 |
| **D13** | **ESP32-S3 的原生 USB 接到 Hub 的第 6 个下行口**,删掉 J22 调试 USB-C,改留一个 5 针 UART 应急口 | **全机只剩一根线到电脑**(5 台手机 + 控制器共用上游)。详见 §8.2 | | **D13** | **ESP32-S3 的原生 USB 接到 Hub 的第 6 个下行口**,删掉 J22 调试 USB-C,改留一个 5 针 UART 应急口 | **全机只剩一根线到电脑**(5 台手机 + 控制器共用上游)。详见 §8.2 |
| **D15** | **底板改 4 层、板框硬上限 98 × 98,插槽面只放 5 个插槽,其余器件全部放背面**(2026-09-10 用户拍板,取代 D5)| 2 层版布到 63 条卡死:D± 墙(19 个网 0 通过)+ 西侧 12V 干道(18 个网只能走底层跳)。4 层后 L2/L3 两块完整地给 D± 和所有信号当参考,墙不存在;背面没有刀片投影,高件不限位置;U32 可直接坐在中间槽正下方。**叠层:L1 插槽面 = 槽焊盘 + 12V 大铺铜 + 少量线 / ~~L2 GND~~ ⇒ **L2 整层 +3V3**(2026-09-10 用户拍板,见下)/ L3 GND / L4 器件面 = 全部器件 + 信号 + D±(参考 L3)+ 5V 局部铜**。 | **D15** | **底板改 4 层、板框硬上限 98 × 98,插槽面只放 5 个插槽,其余器件全部放背面**(2026-09-10 用户拍板,取代 D5)| 2 层版布到 63 条卡死:D± 墙(19 个网 0 通过)+ 西侧 12V 干道(18 个网只能走底层跳)。4 层后 L2/L3 两块完整地给 D± 和所有信号当参考,墙不存在;背面没有刀片投影,高件不限位置;U32 可直接坐在中间槽正下方。**叠层:L1 插槽面 = 槽焊盘 + 12V 大铺铜 + 少量线 / ~~L2 GND~~ ⇒ ~~L2 整层 +3V3~~(2026-09-10 晚)⇒ **L2 整层 GND**(2026-09-10 深夜用户拍板:双地,L1 信号参考 L2 地、L4 参考 L3 地,换层回流靠 GND 孔闭合;依据 Ott 叠层 / 乐鑫 ESP32-S3 指南 / TI SPRAAR7;+3V3 约 0.5 A 改回布线)/ L3 GND / L4 器件面 = 全部器件 + 信号 + D±(参考 L3)+ 5V 局部铜**。
**L2 改 +3V3 的理由**:4 层布线跑了十轮,信号网卡在 37~38 个,大头是 +3V3 这棵 47 只脚、跨全板的电源树。L3 保持整层地(L4 上全部器件与 D± 的参考不变,M5「D± 参考层 L3 无开口」原样成立);L2 让出来做一整块 +3V3,+3V3 每脚打孔下去、不再布线。L1 上仅有的几段 D±(USB6 / 上行、USB4 的一跳)参考的是一整块 +3V3,同样不跨分割。曾考虑在 L3 分区,被否:L3 在 D± 下必须留地条,会把 +3V3 切成几块,+5V 平面也排不开。98 的理由同 D14。**插槽手焊**,工厂只贴背面。代价:2 层版布局布线作废(存档 `.claude/r2/archive_base2L/`);背面最高 12 mm(C2/C3 直立电解),底板下要 ≥ 15 mm 铜柱,外壳跟着改 | **L2 改 +3V3 的理由**:4 层布线跑了十轮,信号网卡在 37~38 个,大头是 +3V3 这棵 47 只脚、跨全板的电源树。L3 保持整层地(L4 上全部器件与 D± 的参考不变,M5「D± 参考层 L3 无开口」原样成立);L2 让出来做一整块 +3V3,+3V3 每脚打孔下去、不再布线。L1 上仅有的几段 D±(USB6 / 上行、USB4 的一跳)参考的是一整块 +3V3,同样不跨分割。曾考虑在 L3 分区,被否:L3 在 D± 下必须留地条,会把 +3V3 切成几块,+5V 平面也排不开。98 的理由同 D14。**插槽手焊**,工厂只贴背面。代价:2 层版布局布线作废(存档 `.claude/r2/archive_base2L/`);背面最高 12 mm(C2/C3 直立电解),底板下要 ≥ 15 mm 铜柱,外壳跟着改 |
### 0.2 本文档按这些默认值展开(要改说一声) ### 0.2 本文档按这些默认值展开(要改说一声)
@@ -123,7 +124,7 @@
| | 值 | | | 值 |
|---|---| |---|---|
| 刀片板框 | **98 × 76 mm**(硬上限 98×98,见 D14),1.6 mm 厚(金手指要求),四角 R2 | | 刀片板框 | ~~98 × 76~~ ⇒ **90 × 68 mm**(2026-09-11,§34;硬上限 98×98,见 D14),1.6 mm 厚(金手指要求),四角 R2。下面分区图是 98×76 版,新数见 §34.1 |
| 底板板框 | ~~110 × 125 mm~~ ⇒ **≤ 98 × 98 mm、4 层**(D15,2026-09-10),1.6 mm 厚(插槽保持力),四角 R5 | | 底板板框 | ~~110 × 125 mm~~ ⇒ **≤ 98 × 98 mm、4 层**(D15,2026-09-10),1.6 mm 厚(插槽保持力),四角 R5 |
| 槽距 | **20.32 mm**(A4),5 槽跨度 81.28 mm | | 槽距 | **20.32 mm**(A4),5 槽跨度 81.28 mm |
| 插槽 | PCI-E x4 直插,槽体 39 mm(A1) | | 插槽 | PCI-E x4 直插,槽体 39 mm(A1) |
@@ -226,7 +227,7 @@ R1 §22.3e 里有一处**自相矛盾**——「侧键面(贴合面)最高
| 底板 | **J33 调试应急口 5P 1.25 mm**(GND / TXD0 / RXD0 / EN / IO0) | 1 | **R2 新增(D13 的退路)** | | 底板 | **J33 调试应急口 5P 1.25 mm**(GND / TXD0 / RXD0 / EN / IO0) | 1 | **R2 新增(D13 的退路)** |
| 底板 | **U35 TCA9548A** I²C 八路多路器 | 1 | **R2 新增(D8)** | | 底板 | **U35 TCA9548A** I²C 八路多路器 | 1 | **R2 新增(D8)** |
| 底板 | U13 INA226(输入侧)+ Rs6 5 mΩ | 1 | R1 §19 | | 底板 | U13 INA226(输入侧)+ Rs6 5 mΩ | 1 | R1 §19 |
| 底板 | U32 USB2517I + Y1 24 MHz + R708 12k + U31 AMS1117-3.3 + J23 上游 USB-C | 1 套 | R1 §21,**`CFG_SEL[2:0]=110` 不变**(R1 §21.6b) | | 底板 | U32 USB2517I + Y1 24 MHz + R708 12k + U31 AMS1117-3.3 + J23 上游 USB-C | 1 套 | R1 §21,~~`CFG_SEL[2:0]=110` 不变~~ → **2026-09-10 改 `000`**:`PRT_SWP1~3` 上拉(口 1~3 片内对调,北去三对不再 U 形掉头)、`PRT_SWP4~6` 下拉,+ **`R723` 1 MΩ 跨晶振**(检查清单 required,R1 起漏了)。接法见 `.claude/r2/a4_hub.py` 文件头 |
| 底板 | 风扇:Q7/Q9/Q10 + J26~J30 + RT601/602 | 1 套 | R1 §22.6 | | 底板 | 风扇:Q7/Q9/Q10 + J26~J30 + RT601/602 | 1 套 | R1 §22.6 |
| 底板 | **5 × PCI-E x4 插槽** | 5 | R2 新增 | | 底板 | **5 × PCI-E x4 插槽** | 5 | R2 新增 |
| 底板 | D± 退路座 4pin 1.25 mm | 5 | **R2 新增(D12)** | | 底板 | D± 退路座 4pin 1.25 mm | 5 | **R2 新增(D12)** |
@@ -345,6 +346,10 @@ A16 让给 `VDAC`(上下两针都是地,是全连接器夹得最干净的位
> 理由:U32 翻到背面后只能 rot 270(下行口朝北、USB1 在西),南去的几对要绕 U32 东侧; > 理由:U32 翻到背面后只能 rot 270(下行口朝北、USB1 在西),南去的几对要绕 U32 东侧;
> Hub 在走廊 3 要绕三对、D± 最远 ≈ 80 mm,走廊 2 只绕两对、最远 ≈ 58 mm(估算 45/23/3/40/58)。 > Hub 在走廊 3 要绕三对、D± 最远 ≈ 80 mm,走廊 2 只绕两对、最远 ≈ 58 mm(估算 45/23/3/40/58)。
> M5 验收的「D± ≤ 45 mm」只有 U32 进中间槽正下方的过孔带才做得到,没选;两层版是 ~70 mm。 > M5 验收的「D± ≤ 45 mm」只有 U32 进中间槽正下方的过孔带才做得到,没选;两层版是 ~70 mm。
> **2026-09-11 用户拍板:「插槽过孔带不放件」在 U32 正南 / 正北各开一条窄带的口子**(`place_base.HUB_STRIPS`:
> J37 北排投影 x 44~58.2 / y 30.2~34、J36 南排投影 x 44~58 / y 43.4~46.4)。走廊 2 净高 11.12 而 U32 占 9.75,
> 按检查清单「VDDA33 / VDD33 每脚 0.1 µF 紧贴」,南排 46 脚和北排 5 / 10 脚的去耦只有放进这两条才能 ≈1.3 mm
> (不放开是 7~8 mm / >15 mm)。代价:这两段槽针的过孔改打到两排之间;5 / 10 脚去耦的地孔拟打在正上方 J36 GND 针的盘中孔。
### 6.1 叠层 ### 6.1 叠层
@@ -381,9 +386,13 @@ R2 里 Hub 坐在槽阵正中、D± 又排在近端针位:
| 最近一对(槽 3) | ≈ 8 mm | | 最近一对(槽 3) | ≈ 8 mm |
| 第 6 对(→ ESP32,D13) | ESP32 在 y 84~114,穿过槽阵,≈ **55 mm** | | 第 6 对(→ ESP32,D13) | ESP32 在 y 84~114,穿过槽阵,≈ **55 mm** |
USB 2.0 不要求 pair-to-pair 等长,只管 **intra-pair skew(目标 ≤ 1 mm)**, USB 2.0 不要求 pair-to-pair 等长,只管 **intra-pair skew(≤ 150 mil = 3.81 mm)**,
所以 8 mm 与 41 mm 并存没问题。 所以 8 mm 与 41 mm 并存没问题。
> 2026-09-10 用户定:底板 skew 由 1 mm 放宽到 **150 mil**。底板 D± 是按构造画的(北去 U 形掉头、南去东绕),
> 实测六对长度差 56~136 mil,全部超 1 mm;USB 2.0 HS 480 Mbps 下 150 mil ≈ 25 ps,是常用预算,不必加蛇形。
> 规则在 `rules_base.DIFF['differentailPairLenTolerMax']`。
**阻抗估算**(1.6 mm 双面板 1 oz 微带,εr ≈ 4.3): **阻抗估算**(1.6 mm 双面板 1 oz 微带,εr ≈ 4.3):
| 线宽 / 间距 | 估算 Zdiff | | 线宽 / 间距 | 估算 Zdiff |
@@ -552,7 +561,7 @@ R1 §10.2 的四条关键回路**一条不变**,而且因为刀片小、只有
| 任务包 | 影响 | | 任务包 | 影响 |
|---|---| |---|---|
| A2 HAL | 无 | | A2 HAL | 无 |
| A3 INA226 驱动 | 无(地址表从「六片不同地址」变成「五片同地址 + 一片输入侧」) | | A3 INA226 驱动 | 地址表从「六片不同地址」变成「五片同地址 + 一片输入侧」。⚠ **底板输入侧 U13(0x43)的 IN+ / IN− 对调接**(2026-09-11,A1-INPUT:IN+ 接 Rs6 下游 +12V、IN− 接上游 VIN_SW —— 为了让两条 Kelvin 在 PCB 上不交叉;原来 IN− 绕 37 mm 把 U13 的 I²C 圈死)⇒ 芯片读数为负。驱动已加 `ina226_set_reversed()`(默认全部正接 = R1 行为),**R2 固件启动时必须调 `ina226_set_reversed(INA226_ADDR_INPUT, true)`**,否则 SW-8 反灌判据会把正常供电当成反灌 |
| A4 DAC63204 驱动 | **无**(D11 的全部意义)。**D11b 之后连地址表都不用改**:仍是 R1 的 `U6` 0x4A / `U7` 0x4B 两颗、通道映射表原样,而且 DAC 挂主干总线,**写 DAC 不用进 mux 事务** | | A4 DAC63204 驱动 | **无**(D11 的全部意义)。**D11b 之后连地址表都不用改**:仍是 R1 的 `U6` 0x4A / `U7` 0x4B 两颗、通道映射表原样,而且 DAC 挂主干总线,**写 DAC 不用进 mux 事务** |
| **A5 74HC595 驱动** | ⛔ **作废**(D9 改用 PCA9555)。代码保留但不再挂进链路 | | **A5 74HC595 驱动** | ⛔ **作废**(D9 改用 PCA9555)。代码保留但不再挂进链路 |
| A6 PCA9555 驱动 | **复用**,从 1 个实例变 5 个实例;「接地脚永远是输入」那条硬约束照旧 | | A6 PCA9555 驱动 | **复用**,从 1 个实例变 5 个实例;「接地脚永远是输入」那条硬约束照旧 |
@@ -1203,10 +1212,12 @@ T7 那颗 2A 慢断保险丝**当场就用同样的办法找到了**,见 §20.
R1 的 `new` 表不能直接用:5 个 `VBUS_EN*` + 4 个 `KEY_*` 随 D9/D13 空了出来, R1 的 `new` 表不能直接用:5 个 `VBUS_EN*` + 4 个 `KEY_*` 随 D9/D13 空了出来,
新增 `PRSNT#×5` 与 `MUX_RST#`。共 26 脚(23 信号 + `USB6_D±` + `IO0`/`TXD0`/`RXD0`/`EN`)。 新增 `PRSNT#×5` 与 `MUX_RST#`。共 26 脚(23 信号 + `USB6_D±` + `IO0`/`TXD0`/`RXD0`/`EN`)。
- `NTC1 = IO1` / `NTC2 = IO2` —— **必须 ADC1**(ADC2 与 WiFi 冲突),脚本里有断言。 - `NTC1` / `NTC2` —— **必须 ADC1**(ADC2 与 WiFi 冲突),脚本里有断言。
~~`NTC1 = IO1` / `NTC2 = IO2`~~ ⇒ **2026-09-11 方案 B 改为 `NTC1 = IO4` / `NTC2 = IO5`**(仍是 ADC1,见 §29.6)。
- `USB6_DM = IO19` / `USB6_DP = IO20` —— 原生 USB 硬定死,脚名脚号从 `_p3_live.json` 查的。 - `USB6_DM = IO19` / `USB6_DP = IO20` —— 原生 USB 硬定死,脚名脚号从 `_p3_live.json` 查的。
- ⚠️ **模组是 R8(八线 PSRAM)还是 N8 没定**:本页按可能上 R8 保守处理,`IO35/36/37` 全部不接。 - ⚠️ **模组是 R8(八线 PSRAM)还是 N8 没定**:本页按可能上 R8 保守处理,`IO35/36/37` 全部不接。
若确定 N8,这三脚可放出来当 GPIO —— 现在全板只剩 `IO47/IO48` 两个备用脚。 若确定 N8,这三脚可放出来当 GPIO。~~现在全板只剩 `IO47/IO48` 两个备用脚~~ ⇒ 方案 B 之后
`IO47/IO48` 给了风扇,备用脚变成 **`IO1/IO2`**(模块脚 39/38,都是 ADC1)。
### 20.7 ✅ T7 也一起结了:`F1` = `F1206SB2000V063TM`(2026-09-08) ### 20.7 ✅ T7 也一起结了:`F1` = `F1206SB2000V063TM`(2026-09-08)
@@ -2041,6 +2052,7 @@ M4b 画的 37 段板框、M6 之后的状态,全都不在了(何时被删无
⚠ 它以前不报,是因为**板框那时候是「导线」**(§27.3),DRC 根本没把它当板框。 ⚠ 它以前不报,是因为**板框那时候是「导线」**(§27.3),DRC 根本没把它当板框。
⇒ 要么下单时和嘉立创确认「金手指缺口处铜到板边 0.2 mm 可做」,要么把缺口收窄到 ⇒ 要么下单时和嘉立创确认「金手指缺口处铜到板边 0.2 mm 可做」,要么把缺口收窄到
CEM 的 1.6 mm(clearance 变 0.35,但防呆筋的配合余量少 0.15)。**这一条需要用户拍板。** CEM 的 1.6 mm(clearance 变 0.35,但防呆筋的配合余量少 0.15)。**这一条需要用户拍板。**
⇒ ✅ **2026-09-11 晚 用户拍板 1.6**,已落地(§35.7),刀片 DRC 0 条。
### 27.9 新增/改动的脚本 ### 27.9 新增/改动的脚本
@@ -2476,6 +2488,34 @@ R1 §交接二 踩过一次(配错一格,把一个本来正确的风扇块
验收:原理图端口读回 8 个逐个对上;`xpage` 每个 `EFUSE_EN` 2 个端口 / 每个 `PRSNT` 3 个, 验收:原理图端口读回 8 个逐个对上;`xpage` 每个 `EFUSE_EN` 2 个端口 / 每个 `PRSNT` 3 个,
**真正漏接 0**;PCB 焊盘读回 9 只脚逐个对上。 **真正漏接 0**;PCB 焊盘读回 9 只脚逐个对上。
### 29.6 ⛔ ESP32 的 NTC / 风扇 / UP_VBUS_DET / PRSNT5# 引脚**重新分配**(方案 B,2026-09-11 用户拍板)
底板 4 层布线做到第 26 轮,U20 周围约 10 个网(INT#、NTC1/2、EFUSE_EN3~5、PRSNT…)断在同一道横贯全板、两层都封死的墙上。
查墙上各网的两端(PROGRESS「U20 一簇诊断」):**NTC1/NTC2 在北排东头(IO1/IO2)却要去南边的热敏电阻**(RT601 (85,26) / RT602 (19,8)),
**风扇那组在南排(IO4~IO7)却要去西北角风扇电路** —— 两组方向全反,风扇线横穿全板就是那道墙。布线器层面四轮 A/B 都是此消彼长。
服务器上先只改布线器读到的焊盘网名做试算(`route_base --u20-remap`),六组重分配里方案 B 最好:离线断开 **29 → 27**。
| 模块脚 | IO | 换前 | **换后** |
|---|---|---|---|
| 4 / 5 | IO4 / IO5(ADC1_CH3/CH4)| `FAN_TACH_OUT` / `FAN_TACH_IN` | **`NTC1` / `NTC2`** |
| 6 / 7 | IO6 / IO7 | `FAN_PWM` / `FAN_EN` | **`UP_VBUS_DET` / `PRSNT5#`** |
| 22 / 23 | IO14 / IO21 | `PRSNT5#` / `UP_VBUS_DET` | **`FAN_TACH_OUT` / `FAN_TACH_IN`** |
| 24 / 25 | IO47 / IO48 | 备用 | **`FAN_PWM` / `FAN_EN`** |
| 38 / 39 | IO2 / IO1 | `NTC2` / `NTC1` | **备用**(仍是 ADC1)|
电气约束:全部走 GPIO matrix,不碰 USB(IO19/20)/ UART0(IO43/44)/ strapping(IO0/3/45/46)/ R8 PSRAM(IO35~37);
NTC 仍在 ADC1。`s3_pin_audit.audit()` 按 R8 八线严格口径:ERROR 0,WARN 与换前相同(IO0 strapping、UART0 两脚,均有意为之)。
风扇测速线原理图上有 10k/3.3k 分压(最高 2.98 V),接任意 GPIO 都安全;`FAN_EN` 复位高阻由 A6 的 100k 下拉保证关断,IO48 非 strapping。
**怎么改的**:原理图 `u20_remap_b.py`(删端口重建;24/25 新接线、38/39 删端口删线;**J33 从 (430,455) 挪到 (450,420)** 给 24/25 两行让位,
地线改接 U20 地干线节点 (320,440));PCB `pcb_pinswap.py`(`WANT` 加这 10 只,38/39 置空);源头表 `a3_ctrl.py` 的 `PINMAP` / `U20_NC` /
J33 画法、`_esp32_pinmap.json` 的 `r2_base` 一起改(顺带把 §29.5 那 9 只一直没同步进 `a3_ctrl.py` 的网名也更正了)。
验收:原理图读回改后状态逐项对上;`verify_sch` 单脚网络 0、无名端口 0(悬空只剩 20 只有意空脚,数目与换前相同);
`xpage` 真正漏接 0;`port_audit --r2 BASE` 354 个可疑 0;PCB 焊盘读回 19 只逐个对上;**「从原理图导入变更」0 条变更**。
⚠ **固件**:R2 底板固件尚未开写;写的时候按本节 + §29.5 的表(`_esp32_pinmap.json` 的 `r2_base` 是机器可读版)。
代价:第 31 轮试算里 RXD0(应急 UART)与 PRSNT2# 在所有重分配变体都断 —— 布线器问题(RXD0 的扇出孔落在北排焊盘南侧),另补。
--- ---
## 30. `+3V3` 铜墙(2026-09-10 用户拍板) ## 30. `+3V3` 铜墙(2026-09-10 用户拍板)
@@ -2683,3 +2723,605 @@ M10 旋转插槽后,旧路由器不是单纯左右镜像:D± 从插槽北排
恰好堵住 U32→U20 的 USB6 通道,已规划移到 `(78,109.5)` / `(78,111)` 并通过布局自检; 恰好堵住 U32→U20 的 USB6 通道,已规划移到 `(78,109.5)` / `(78,111)` 并通过布局自检;
但 EasyEDA 窗口断开导致定点下发 HTTP 500,板端尚未移动。重连后要先下发这两件、 但 EasyEDA 窗口断开导致定点下发 HTTP 500,板端尚未移动。重连后要先下发这两件、
重抓 `_base_pads.json`,再重跑完整路由 / 连通性 / DRC。 重抓 `_base_pads.json`,再重跑完整路由 / 连通性 / DRC。
---
## 34. 刀片压到 90 × 68(2026-09-11 用户拍板)
**用户要求**:刀片 98×76 → **90×68**。先做可行性核账(几何两个方向都放得下,
真正要取舍的只有 C60 和功率列宽度),两处交用户拍板:
**手机上移、C60 留在两槽之间**;**删掉 J2**。
### 34.1 几何(事实来源 `pcb_blade.py`,与手机绑定的一律从 `PHONE_ORG` 推)
| 项 | 旧(M7) | 新 |
|---|---|---|
| 板框 | 98 × 76 | **90 × 68**,四角 R2,一条闭合折线(读回 54 点、外接 0~90 × 0~68)|
| 手机原点 | (3, 29) | **(1.5, 22.34)** |
| 左边距 | H2 离板边 3.7 | **2.2**:M1.6 尼龙螺钉头 Ø3.2 还剩 **0.6** |
| 服务带(金手指区上沿 → 接口边)| 18 | **11.34** = C60 本体 11 + 0.34 |
| 手机顶 → 板边 | 2.34 | **1.0** ⇒ ⚠ **顶边导轨只能咬 ≤ 1 mm**,否则压到主板 |
| 功率列 | x 72~98(26)| **x 70.59~90(19.4)**:整块西移 8,§7.2 四条回路的相对关系一点没动 |
| 排线槽 y | 24.8~27.8 | **18.14~21.14**(接口边外 1.2~4.2,跟手机走,不再写死)|
| 金手指 | x=49 | **x=45**(仍居中;底板插槽不用改,只是悬臂两侧各短 4 mm)|
| 七个孔 | — | H1 (2.90,32.24) H2 (2.20,49.64) H3 (3.20,61.04) H4 (43.40,24.24) H5 (58.90,63.54) H6 (66.10,50.44) H7 (66.30,54.74) |
**C60** 定在 **(38.175, 16.5)**:x 取两槽正中,y **贴着金手指区上沿**(本体 11 的下沿 = 11.0)。
居中(上下各余 0.17)时 H4 的螺钉头圈只剩 2.07 < `HOLE_KEEP` 2.2;贴下沿之后上沿离接口边 0.34、离 H4 2.24。
⚠ 它的焊盘外沿 12.9 宽,离两侧铣槽各只有 **0.45**(本体按 11 算是 1.4;项目 `MILL_KEEP` 0.5 是按本体判的,
嘉立创铣槽到铜下限约 0.3)—— 投板前再确认一次。
### 34.2 原理图两处(B4-EDGE / B3B-USB 整页重画,截图已自查)
1. **删 `J2`**(D± 退路座):底板 `J39~J43` 在 §28.11 已删,刀片这头没了对端,D12 实质作废;
旧版 D± 往东绕到 x≈60 就是为了它。
2. **`U7` 两侧对调**:1/3 脚 = 金手指侧 `USB_DM/DP`,6/4 脚 = 手机侧 `USB_DMJ/DPJ`(R1 相反)。
**手性推导**:`U7` 在 B 面,信号从 4/6 那排流向 1/3 那排时 DP 永远在前进方向右手边(旋转不改手性,只有镜像改);
而金手指(DP 在西、向北出线)和 J4(DPJ 在东、从北边进)两头都要 DP 在**左**手边 ⇒
旧接法前后各要交叉一次(旧版 DP/DM 各 6~7 个孔)。1↔6、3↔4 内部直通,电气等价。
3. `J1` 改为**不进 BOM**(§17 遗留)。
4. 验证:两页 `verify_sch --nets` PASS、`xpage` 漏接 0、`port_audit --r2 BLADE` 142 个可疑 0。
⚠ 整页重画让 B3B 那 9 件的 uniqueId 整体差 1(skill §9o-2),导入对话框一度提出「J2 换成 BM24 封装、J4 换成 SOT-23-6」。
`uid_sync_base.py --blade`(新加)按位号两阶段写回;**「PCB 有、原理图没有」的件(J2)占着的号正是错位那一串要用的** ⇒
同一次写入把它挪到 `zzdel_J2`。对齐后对话框只剩真实差异(删 J2、U7 四只脚改网、R62/R64/R65/C67/C68 两脚互换 = 无极性件重画后脚号翻转),
应用后 82 件逐件对上。
### 34.3 摆位(`pcb_place.py`)
- 所有 M7 手排坐标改成「M7 值 + 平移」:跟手机的加 `DX/DY = (−1.5, −6.66)`,功率区加 `PX = −8`。
侧键 MOS 只移 y(Q4 本来就贴板边)。
- `J3`(EDL 座)挪到**服务带里、尾插槽东头**,rot 270,读回信号盘在西 x 67.18、正对槽口。
- `U7` **手排在 J4 那两只 D± 脚正北** (51.40, 30.55),下排直直往南 ~2 mm 接 J4;它北边 B 面留一块换层孔的空地。
- `DIS`(泄放 R19 2512 + Q2)**第三版挪到电池车道正北、手机投影下**(R19 (22.07, 31.0) / Q2 (29.50, 31.0),B 面、都 < 1 mm 高):
前两版放在电池槽正下方,而服务带压到 11.34 之后,**槽下那条 6.4 mm 高的带是 U4(左下角)往东去的唯一走廊**
(~8 个网:SDA/SCL/BUCK_PG/EDL_EN/VBUS_EN/EFUSE_FLT/INA_ALERT/EFUSE_PG),R19+Q2 把 B 面那一层从 x 18 到 32 整段占满。
第三轮服务器试「三个长网先布」:它们通了,SDA 整网断成 4 块 —— 典型的此消彼长,是走廊容量问题不是顺序问题。
新位置紧挨电池座 J5(R19 本来就接 VO_P = 电池正),正下方是 VO-L3 北叶,就地扎孔照旧。
- VO-L3 岛西沿 17.5、北叶跟手机走,V12-L3 岛跟板边走。
- 分区第二版(第一轮服务器布线之后改的,见 §34.5):`EFUSE_AUX` 回到 M7 的 11.2 宽、整块放到 J3 以北
(x 64.8~76 × y 20.7~40);`SVC_E` 在 J3 正北一小块、`GROUPS` 里排在 EFUSE_AUX **前面**(两区重叠,谁先摆谁先挑);
`SVC_R` 只留 U7 换层孔空地以东。
- 读回 82 件位置 / 层全对;自检(重叠 / 越框 / 金手指区 / 槽与车道 / 螺钉头 / 分段限高)全过。
- `place_audit` 报 11 处「摆错边」/ 8 处「去耦 > 3 mm」:U4 上拉、U1 FB、C6/C7 与 M7 相对位置完全相同;
仍偏远的是 U5 那一簇(`C68` CT 9 mm、`C67` VBUS 7.2 mm、`R64` 绕 U5)—— 都是低速 / 直流网,暂接受。
### 34.4 D± 构造(`router.dp_blade()`,不交给 A*)
金手指 B14/B15 顶层直上 → 并拢成 **W 0.28 / S 0.18** → 尾插槽上沿以北 45° 东让 H4(线边离孔边 0.53)→ 北上 →
东去到 U7 正上方 → 南下、在 U7 上排北边**各一个孔**下 B 面;U7 下排 → J4 在 B 面 **0.18** 宽(BM24 0.35 节距)。
中心线折线 + 左右各偏 0.23、斜接 ⇒ 45° 拐角处线距不变。
| | 旧(M7,A* 分开布)| 新 |
|---|---|---|
| USB_DP / USB_DM | 59.6 / 63.4 mm,**0.531** / 0.15 宽,7 / 6 孔 | **42.05 / 40.52 mm**,0.28 / 0.28,1 / 1 孔 |
| USB_DPJ / USB_DMJ | 11.0 / 25.8 mm(差 14.8)| **2.42 / 2.34 mm**(差 0.08)|
本机静态校核(纯几何):净距 < 0.15 的 0 处,最紧 0.17(进 J4 那 0.8 mm,节距定死的)。
对内长度差 1.52 mm 来自两个右拐角内侧线短(规则容差 5.0;底板 UP 那对是 1.9)。
`pcb_rules.DIFF` 的差分线宽下限 0.20 → **0.15**(否则整对 USBPHONE 在 DRC 里刷线宽)。
顺带:旧版 `USB_DP` 被撑到 0.531 = U7 焊盘宽 —— 线宽被焊盘封顶逻辑反向拉宽了;构造之后这两对不再经过那段逻辑。
### 34.5 布线上服务器
CLAUDE.md 的「布线运算只在 10.5.5.13」原来只落实在底板(`remote_route.ps1`)。刀片补上:
- `router.py`:`--replan` / `--pf` 在本机**硬阻断**(没有 `DCDC_ROUTE_SERVER` 直接退出);
离线时板上已有过孔改读 `_blade_board_vias.json`(`--snapshot-vias` 写)。
- `remote_blade.sh ['变体' ...]`:快照 → 打包(不带 `_base_*` / png)→ 服务器每个变体一份副本、PyPy 并行 →
拉回 `_blade_routes.var<k>.json/.log`,**不自动覆盖缓存**。`MODE=stitch` 同一套流程算 GND 缝合孔落点
(`pcb_route.py --stitch-plan`,本机 `--stitch` 只下发)。
- ⚠ 第一版脚本把远程命令写成 `cat > run.sh && setsid … &`:结尾的 `&` 把整条 && 链(连 cat)放后台,
ssh 一返回 stdin 就关 ⇒ run.sh 0 字节、一个变体都没起、`done` 永远不出现,本机空转。
已改成「先 scp 上传 run.sh、另起一次 ssh 启动」,并在 5 秒内查不到 `v*.log` 就退出。
**第一轮(v0 默认 19 条没搞定 / v1 `--pf` 21 条,M7 是全通)**,按根因分三类:
1. **`+12V` 整条没布**(五根指全失败、F1.1 一根铜都没有):不是 D± 挡的 —— M7 那条 0.8 的 12V 是 B 面 x≈36
从阻尼电容两盘之间北上、y≈60 顶层东去 F1,全程在 D± 顶端(y 35.7)以北。这回五根指(x 28~31)出口被封:
北边电池槽(y 18.14),B 面正上方 R19/Q2,而 `+3V3`(在 `FIRST_NETS` 里)先布、一根 y 15.5 的顶层长线把槽下走廊封死。
服务带 18 mm 时这事吸收得了,11.34 就不行 ⇒ **`+12V` 排到 `FIRST_NETS` 最前**。
2. **我自己引入的摆位退化**:EFUSE_AUX 西边少 4.3 mm(`$1N165: R16.1`)、U6 被挤到离 J3 20 mm(`$3N45`)、
C68 离 U5 7.9 mm(`$5N71`)⇒ 分区第二版(§34.3)。
3. U1/U2 一带 `+3V3 / EFUSE_EN / EFUSE_PG / $1N136 / $1N173` 各断成两块 —— 同一片拥塞,看第二轮。
**第二 ~ 四轮**:
- 第二轮(+12V 排最前 + 分区第二版):v0 自报 11 条,但**按铜判只断 3 个网**(`$1N173` / `BUCK_PG` / `EDL_EN`,都是从 U4 / Q2 所在的西南角出发的长网);
布线器自报的 `+3V3 / VBUS / SCL / SDA / $1N136`「铜分成 2 块」全是误报 ⇒ 新写 `blade_conn.py`(离线按铜重叠并查集),**比较变体只看它**。
- 第三轮(`router.py --first` 把三个长网提前,4 组变体):三个长网通了,**SDA 整网断成 4 块** —— 此消彼长。
量化:U4 往东约 8 个网,唯一走廊是电池槽下那条 6.4 mm 高的带,DIS(R19+Q2)把 B 面那一层整段占满 ⇒ DIS 第三版挪走(§34.3)。
- 第四轮(DIS 挪走后 6 组变体):最好的 v3(三长网先布 + `--pf`)按铜只断 2 个网(`+12V_F: R4.1`、`+5V: U5.1`,都是局部被围)。
但 `router_check` 精确净距查出**六组全部**在 U2 角上有两处孔压线(−0.095)+ 两处线距 0.054,另有若干 0.125(< 0.15)。
**第五 / 六轮(修 FIRST_NETS 循环之后)**:
- 第五轮(只修过孔图口径):六组逐段不变 ⇒ 否掉那个假设。
- 第六轮(补上「已放的过孔进占用图」):**非 `--pf` 的三组精确净距 0 条**(U2 角孔压线、0.054 线距全消失);
代价是按铜真断开升到 5~7 个网 —— 以前「少失败」有一部分是靠线压着别人的孔换来的,去掉之后露出真实的容量瓶颈。
`--pf` 三组仍有一批 0.125(它自己那条路的口径另有问题),不采用。
- 选 **v2**(三长网先布、不带 `--pf`)做底:净距 0,断 5 个网 ——
U2 角 `$1N147`(U2.15) / `EFUSE_PG`(U2.2)(M6/M7 就是最难的那两只)、U4 角 `INA_ALERT`(U4.15) / `SDA`(U4.23·R40.1·U3.4 都断开)、
`$5N71`(U5.4,C68 摆得远)。缓存 `r6-v2`;下一步只对这 5 个网在服务器上补线(`MODE=repair`)。
**这一轮顺带修掉的四个工具毛病**:
| 毛病 | 后果 | 改法 |
|---|---|---|
| `router_check.py` 现算 `pcb_route.stitch_points()` | 本机扫孔位(违反 CLAUDE.md);板上还没铜时算出不避让导线的 5 mm 格点 ⇒ 一排「+12V 线 vs GND 过孔 −0.895」、三处「孔压孔 −0.041」全是假问题 | 只读服务器算好的 `_blade_stitch_pts.json`,没有就不算 |
| `plan_all()` 的 FIRST_NETS 循环往过孔图打线时少了 `VIA_D/2 + CLR − halfw` | 表里先布的网的孔可以落在更早那根线旁 0.2 mm 内。底板在 `route_groups` / `grid_now` 早修过(VIA_MAP_FIX),刀片这条路没跟上 | 同 `_block_stub` 的口径 + `exact_v`。⚠ **第五轮 A/B:六组逐段不变 —— 这不是 U2 角那两处的原因**(方向猜反了)|
| ⛔ **同一个循环从来没把本轮已放的过孔打进占用图**(`build_grid` 只含板上旧孔 / D± / VO_P 孔)| 金手指 launch 孔、表里先布的网的孔只靠挂在上面的短线间接挡着(0.15 线只护住孔心 ±0.31),**后布的线压进 Ø0.6 的孔**:`$1N164` 的孔压 `EFUSE_PG`、`EFUSE_EN` 的孔压 `$1N147`(−0.095)—— 和 `route_groups` 注释里 M7 记过的是同一对,那边早修了、这边漏了 | `g` / `gt` 两张图都按 `route_groups` 的口径把 `vias` 打进去 |
| `board_check.py` 在刀片上把 `+3V3` 当铜面网 | 只查「脚挨不挨铜」,刀片 +3V3 的真实连通一直没验过(条件是给底板槽墙写的,写反了)| 两块板都按布线网查 |
| `repair_route.py` 只能读 M6 的板上读回 | 没法在服务器上对缓存补线 | `--cache` 读 `_blade_routes.json`、写 `_blade_routes.repaired.json`;本机硬阻断;`MODE=repair` |
### 34.6 布线 / 铺铜 / DRC 结果(2026-09-11 晚,板上现状)
板上 = 服务器第六轮 `r6-v2`(缓存 `_blade_routes.r6-v2.json`,`router_check` 精确净距 0)+ 斜切 + 缝合孔 + 铺铜:
| 项 | 数 |
|---|---|
| 铜 | L1 728 + L4 479 = **1207 段**(斜切 3 轮 241 个直角)/ 过孔 **359**(布线 281,其中 GND 23 = 金手指地指母线;缝合 78)|
| D± | USB_DP / DM **42.0 / 40.5 mm、每根 1 孔**;DPJ / DMJ 2.4 mm |
| +12V | 144 mm,**1.19 宽**(FIRST_NETS 那条路按「本网最小焊盘边」取宽,F1 1206 盘 ⇒ 1.19;比规则 0.8 宽,电气无害,DRC 不报)|
| 铺铜 | **25 / 25 块全填** |
| `board_check` | **51 / 57 网全通**;没通 = 下面 5 个网 + GND(它读不了铺铜)|
| DRC 间距 | **4 条 = PCIe 缺口到 A11/A12/B11/B12 指 7.9 mil**(§27.8 老问题,几何定死,待用户拍板)|
| DRC 连接性 | **39 条** = 5 个信号网 12 条 + **GND 27 条(铺铜够不着的细间距焊盘)** |
**还没接上的 5 个信号网**(作业单 `.claude/r2/HANDROUTE-90x68.md`):`$1N147`(U2.15)、`EFUSE_PG`(U2.2)、
`INA_ALERT`(U4.15↔U3.3)、`SDA`(U4.23 / R40.1 / U3.4)、`$5N71`(U5.4↔C68.2)。
结构原因:+12V 在 L4 上 x≈42.5 从 y 13 竖到 y 61,和 L1 上的 D± 墙(x 44~45.4,y 4~35.7)并排只剩 ~1 mm,
⇒ **y 13~35.7 东西向两层都过不去**,左下角 U4 去东边的网只能爬到手机底下横过去。服务器定点补线(`MODE=repair`)也接不上。
**铺铜够不着的 27 个 GND 焊盘**(DRC 连接性原文):J4(BM24 0.35 节距)1/2/8/12/16/19/23/24/29/37/41/47 + 大地盘;
U2 1/16/21(EP);U3 1/2/7;U1 2/3;U4 20/21;Q4.2、K4.1、C13.2;一个 GND 孔。
刀片布线器一直 `SKIP = ('GND',)` 靠铺铜,而 0.35 / 0.5 节距的盘之间铺铜进不去 —— 要给这些盘各打**引出线 + 过孔下 L2**
(U2 的 EP 用盘中孔),流程里还没有这一步(底板 route_base 有「GND 打孔」)。M7 大概率同样存在,只是当时没数连接性。
**收尾时又抓到两个工具毛病**(都已修):
- `pcb_route.stitch()` 下发前「删掉板上所有 GND 孔」—— 会连布线器的 23 个地指母线孔一起删(地指下到 L2 的唯一通路)。改成只删不在布线缓存里的。
另:它第一次重写时字符串拼接和 `%` 格式化的优先级写错,下发直接抛异常 —— 而收尾链用 `| tail` 接管道,**退出码被 tail 吃掉,链子没停**,
一路跑到了斜切 / 重载。以后链子一律 `set -o pipefail`。
- `miter.py` 的「只去铜不加铜」只对拐角外侧成立,斜边凹侧会加铜(见其模块头更正):1.19 宽的 +12V 拐角斜切后压上凹侧一个 `$3N30` 孔
(DRC 导线 / 钻孔到过孔 0 mil)。板上那个角已恢复成直角(DRC 间距 7 → 4);脚本加了新斜边的精确净距校核。
## 35. 主板转 180°、两条排线槽到顶部、U4 到右侧(2026-09-11 晚 用户拍板)
**用户**:「重新优化刀片板布局」→「将排线插槽和排线移到顶部,主板不旋转」。核账:**电池排线 38 mm 够不到顶边**
(J600 在 v 4.6,光跨主板就要 40.05 —— R1 §25.16 v3b 早核过同一件事),尾插 55 mm 余约 4 mm。
摆给用户三条路之后,用户改为 **「都挪,主板跟着旋转」**,随后又补一条 **「U4 移到右侧」**。板框仍是 90 × 68。
### 35.1 几何(事实来源 `pcb_blade.py`:`PHONE_ROT = 180` + `phone_xy(u, v)` 唯一实现旋转)
| 项 | §34(90×68 第一版)| 新 |
|---|---|---|
| 主板 | 接口边朝下,原点 (1.5, 22.34) | **转 180°,接口边朝上**;投影 x 1.5~68.59 / **y 11.0~55.66**(远端边贴金手指区上沿)|
| 服务带 | 底部 11.34(金手指区之上)| **顶部 12.34**(接口边 55.66 → 板顶 68)|
| 两条槽 | y 18.14~21.14 | **y 56.86~59.86**;尾插槽 x 5.01~25.01(左)、电池槽 x 38.82~50.82(中)|
| 排线在 B 面 | 穿槽后往 +y 到 J5/J4 | 穿槽后往 **−y**;J5 (44.82, 52.66) / J4 (15.01, 51.66) **底层 rot 180** |
| C60 | (38.175, 16.5),贴金手指区 | **(31.915, 61.5)**,两槽之间;H4 螺钉头圈 2.2 定下沿 56.0,上沿离板边 1.0 |
| J3 | 服务带尾插槽东头 rot 270 | **尾插槽正北** (20.3, 63.3) rot 0,信号盘朝南对槽口 |
| 七个孔 | — | H1 (67.19,45.76) H2 (67.89,28.36) H3 (66.89,16.96) H4 (26.69,53.76) H5 (11.19,14.46) H6 (3.99,27.56) H7 (3.79,23.26) |
| 侧键 K1~K4 | 左下 | **右上** (64~67, 49~52),紧挨功率列 |
⚠ 机械上要确认:SIM 卡托(u 大的那一端)转过来在**左边**,装着卡托会伸出板左沿 ~1.8 mm;C60 上沿离板顶 1.0(顶边导轨只能咬 ≤ 1 mm,同原来手机顶那条)。
### 35.2 为什么功率链也整块转 180°
12V 从金手指(底部 x 28.5~30.5)进,VO_P 要去 J5 / C60(现在都在顶部)⇒ 功率链改成 **F1 在底、U3 在顶**。
做法:`STACK_UP/DN` / `HAND` 仍按「F1 在顶」的旧锚 (81, 50.9) 算,再 `flip_pwr()` 刚体转到新锚 **(80, 27.5)**,
§7.2 四条回路一个相对数都不动(F1 例外保留 rot 90,让 +12V 脚朝南)。顺带的好处:
- U2 刚体转成 rot 0 之后,**全部信号脚(p2/p3/p14/p15/p17~p20)朝西**(朝手机、朝 U4),外围 EFUSE_E / EFUSE_AUX 和脚在同一侧
—— 原来 EFUSE_AUX 摆在西边、p2/p15 却在东半边,`U2.2 / U2.15` 从 M6 起就出不来;
- U1 的 FB/COMP 转到西边(贴手机那一侧),正好是 §3.1「内条放最怕吵的」;L1 离手机右沿 ~8 mm(原 8.6)。
### 35.3 L3 两个岛上下对调,+12V 不再走线
- `VO_P` 岛(`pcb_pour.VO_RECTS`):上半 x 26~89.5 / y 38~56.3,加两个上叶夹着电池槽(C60 / Rs1·U3),U 形单环。
- `+12V` 岛:**x 27~84 / y 9~20**,金手指 12V 三指 → F1。+12V 从此是「每个焊盘打孔进岛」(`router.PLANE_VIA_NETS`),
**L1 / L4 上不再有 12V 走线** —— §34 那根 L4 x≈42.5 的竖线就是把板子东西切两半、5 个网布不通的主因。§7.1 本来就写 L3 = 12V + VO 灌铜。
金手指这头:B1(对排 A1 = PRSNT#,不能打孔)在 L1 上横接到 B2 的孔;B2/B3 各两个孔(y 11.75 / 10.5)。
- 大电流焊盘多打孔(`VIAS_PER_PAD`):J5 / Rs1 各 3、C60 / F1 各 2。⚠ 顺带查出的老问题:§34 那版 **Rs1.1 的 4 A 只靠 Kelvin 线上那一个 Ø0.3 孔进岛**
(Kelvin 焊盘被整个排除在平面孔之外)。现在只排除 U3 那头的取样脚(`KELVIN_SENSE`)。
### 35.4 D±(`router.dp_blade()`)
U7 跟 J4 一起转 180° ⇒ 在 J4 **正南**,下排 3(DP 西)/1(DM 东) 直接朝着金手指。走法:金手指直上 → 并拢 → y 13 起 45° 往西北 →
直上进 U7 下排,每根 1 个孔。**USB_DP / USB_DM 53.07 / 53.07 mm**(§34 是 42.0 / 40.5,差 1.5);DPJ/DMJ 2.48 / 2.54。
手性零交叉(先左转再右转,DP 一直在左手边;两个转角内外侧长度抵掉)。静态最紧 0.17(进 J4 那段,节距定死)。
### 35.5 离线试布线:先合成焊盘、服务器比变体,再下发
板上还是 §34 的旧摆位 + 旧铜,所以试布线不碰板:`synth_pads.py` 把改摆位前抓的焊盘表(`r2/archive_blade_prerot_0911/`)
按新摆位刚体变换(底层 90° 按 §25.3 实测的「先镜像再逆时针」);`remote_blade.sh` 加 `NOSNAP=1` 跳过抓板,过孔快照写 `[]`。
**下发后必须 `pcb_route.py --dump` 重抓、`pads_diff.py` 和合成表逐脚对账,不过不许下铜。**
服务器几轮(每轮 4 组,PyPy 各 ~30 s)按铜判断(`blade_conn.py`):
| 轮 | 改了什么 | 断开的信号网 |
|---|---|---|
| 1 | 新布局第一版 | 4:U2.2 / U2.15 / U2.20 出不来 + SDA(U4.23) |
| 2 | 调 FIRST_NETS 顺序 | 4~5:**换顺序 = 换一批 U2 的脚断**(p2/p15/p20 ↔ p3/p14/p18)|
| 3 | 细间距芯片「禁换层圈」`VIA_KEEP_M` 0.9 / 1.2;`--gndvia` 先打孔 | 4;`--gndvia` 放在信号前面 ⇒ 9~10(GND 孔抢出口)|
| 4 | U2 对格(U1_Y 27.5 → 27.6);`--gndvia` 挪到信号之后 | 4(U2.2 / U2.15 仍出不来);`--gndvia` 不再伤信号 |
| 5 | 排布器按「芯片脚→外围件连线不交叉」换位;上拉不按 +3V3 找去向 | 5~6:U2 西北 / 西南角的对端顺序顺了,**U2.2 / U2.15 仍出不来** |
| 6 | U2 角上四只脚**预扇出**(`router.QFN_FAN`,`--nofan` 对照)| 带禁换层圈 6(扇出端点被逼在 L4 上挤不出去);**关禁换层圈 2**(U2.17 + U4.7)⇒ `VIA_KEEP_M` 默认改 0 |
| 7 | 同上,四组都关禁换层圈 | 2;`--gndvia`(放在最后)也是 2 ⇒ GND 打孔不再伤信号 |
| 8 | U2 西列 p17~p20 也预扇出 | 2:EDL_EN(U4.7) + EFUSE_FLT(U2.20) |
| 9 | U4 北排 p7/p8 扇出后**就地落孔换 L1**(拓扑交叉:它们往西、西边的 KEY 往东北)| 1:$1N156(U2.17);定点补线补上后 `router_check` 查出 8 条:GND 引出脚压 USB_THERM(`--gndvia` 只核落点没核引出脚)、$1N158 从扇出端点斜着压 C14.2(虚拟焊盘名 `fan:` 被当成金手指落点、不查邻盘)⇒ 两处都修 |
| 10 | 同上修完 | 1:$1N158(U2.18),补线补不上 —— C14 的 GND 盘朝着 U2、信号盘在远端 |
| 11 | 排布器 `flip_2pin()`:两脚件在 rot 0/180 之间挑「接芯片的盘朝芯片」 | **0**(v0 `--gndvia --first EFUSE_FLT,EDL_EN` 与 v2 `--first EFUSE_FLT` 都是 0);`router_check` 精确净距 **0 条** |
**结果**(第十一轮 v0,缓存 `_blade_routes.r11new-v0.json`):678 段 / 275 孔(GND 51,其中 `--gndvia` 打的细间距地脚孔)/ 总长 2012 mm;
信号网按铜全通;D± 53.07 / 53.07。没打上孔的 GND 脚 16 只(多是 QFN 散热盘分瓣、SOT-23 地脚),等铺铜后按 DRC 连接性核。
§34 那版:断 5 个网 + 27 只 GND 脚铺铜够不着。
### 35.6 下发与板上结果(2026-09-11 晚,用户确认)
⚠ 下发前抓板发现:板上**铜层 0 段 / 0 孔**(§34 那版的铜已经不在了)、**15 件被用户在客户端里挪过**(U4 放右侧那一类尝试)——
与交接记录不符,问过用户后覆盖;下发前整板快照 `_blade_board_prerot_0911.json`。
| 步骤 | 结果 |
|---|---|
| 槽 / 孔重画 | 两条槽 y 56.86~59.86、七个孔,`pcb_blade --verify` 过 |
| 摆位 | 82 件读回全对;`pads_diff.py` **359 块焊盘和合成表逐块一致**(J4/J5 底层 rot 180、C40/R40/R41 转 90° 都对)|
| 铜 | 678 段 / 275 孔 + 缝合 72(服务器算;`NOSNAP=1` + 由缓存生成的导线快照,**布线孔当零长度导线**一起避让 —— `stitch_points()` 本身只避焊盘和导线)|
| 斜切 / 铺铜 | 4 轮到 0 直角;重载逐项相等;铺铜 **25/25** |
| `board_check` | 信号 **56/56 全通** |
| DRC | 第一遍 10 = 缺口 4 + GND 6(U1.2/3 离散热盘 0.3 没接、U3.2、J4.12 的「斜线→桥线→孔」DRC 不认、J4.16 头顶被 USB_THERM 孔堵);补 6 段 + 1 孔(先进缓存、`router_check` 0 条再下板)⇒ **4 条,全是 PCIe 缺口 0.2 mm(§27.8)** |
最终快照 `_blade_board_rot180_final_0911.json`。还欠用户:SIM 卡托 / 顶边导轨 / 排线实物走位这三条机械确认。
### 35.7 PCIe 缺口 1.9 → 1.6(用户拍板,§27.8 结案)
`fp_edge.NOTCH_W = 1.6`(CEM 值;1.9 是 KiCad 封装值)⇒ 口部断点 38.70 / 41.30,缺口边到 11/12 号指铜边 **0.35**(原 0.2)。
**PCB 上元件的封装是工程库副本,不是个人库原件**:J1 `getState_Footprint()` = `a1ad3deadadbf758 @ 05a65c22…`。
API 没有「更新封装」(`IPCB_PrimitiveComponent` 只有位号 / 位置 / uniqueId 这些 setter),但**直接改工程库那份副本 + 重载 PCB,实例就跟着变**:
`fp_edge.py --notch` 只删 / 重画两份封装的板框层 16 段(焊盘不碰)→ 重载后 J1 内部缺口读回 38.70~41.30、64 只焊盘网名全不变 →
`pcb_blade --outline`(板框折线里的缺口同源)→ 铺铜 25/25 → **DRC 0 条**(间距 0 / 连接性 0)。
⚠ `lib_Footprint.search` 在这台客户端上抛 `o[r].match is not a function`,所以 `--notch` 按 `FP_COPIES` 里的 uuid 直接开编辑器。
第五轮放大看才看清 U2.2 的真实卡法:对端顺序已经顺了,但 A* 一次只看一根 ——
$1N164(p3) 出脚后斜到 x 79.0 往北走,压住 U2.2 头顶那一列;反过来让 EFUSE_PG 先布,它又在 U2.2 正北落孔,孔挡住 p3 那一列。
两只 0.5 节距的脚同时往西北出**是走得通的**(西边那只先拐、东边那只多直走半格再拐,两条 45° 平行线相距 0.707),
贪心 A* 找不到这种配合 ⇒ 和 D± 一样改成**构造**:p2/p3、p15/p14 各画「直走 1.0 / 1.5 + 45° 0.75」,端点在格点上、
落在禁换层圈外,A* 从端点起步。静态校核离相邻异网焊盘最近 0.275。
这几轮顺带抓出来、已经修掉的:
- `ctrl_hand` 的 I²C 上拉:**位号写反**(R41→SDA、R40→SCL,实际 R40 是 SDA 上拉)+ 排到 U4 南边时 +3V3 那只盘朝着芯片 ⇒ 改成按网名反查、pad1 朝 U4(南边 rot 270)。
- **U2 不在 0.25 格点上**:刚体转之后 U2 中心 = U1_Y + 21.9,取 27.5 时西列 p17~p20 落在 x.15 / x.65,出线挤到 0.25 以下。
U1 两短边的 EN/PG/FB/COMP 和 U2 没法同时对格(相对距 21.9 不是 0.25 的倍数),旧版也一样,保 U2。
- **U2 角上出不来的真因是摆位交叉**:只按「离对端最近」排,EFUSE_PG(p2) 的上拉 R15 被 +3V3 的最近焊盘(U3.6)拽到东北,
而 $1N164(p3) 的分压 R16/R17 在西边 ⇒ 两根一出脚就交叉,先布的那根横在另一只脚头上。
⇒ `target_point()` 对「还挂着别的信号网」的件不再按 +3V3 找去向;`reassign()` 的代价加「芯片脚 → 外围件连线」交叉数 × 6 mm。
---
## 36. D16:底板电源入口改 USB-C PD 取电(2026-09-11 用户拍板「走方案 B」)
### 36.1 决定与理由
用户要求改成「用 Type-C 从 120W 氮化镓充电器取电,并兼容各手机厂商的充电器」。可行性结论(会话里给过用户):
- **各家 120W 标称都是私有协议**(小米 / vivo 20V 6A、华为超级快充、OPPO SUPERVOOC),要手机端鉴权,取电芯片拿不到。
**唯一的公约数是 USB PD**:小米 120W / 华为 100W 的 C 口 PD 只到 **65W**(20V 3.25A),vivo 120W / Anker 这类 100W。
UFCS 互通只有 40~44W,而且 CH224A 不支持 UFCS。
- **单口上限 20V × 5A = 100W**(5A 要 e-marker 线;普通 3A 线 60W)⇒ 降压后 12V 侧 **约 95W / 7.9A**,
低于 R1 §22.6 的最坏 9.99A(≈108W)。整板常态 10~25W,任何一档都够;**最坏工况靠固件限功率**(R1 §8.3 早就规划了「读输入侧 INA226 按优先级降载」)。
- **20V 不能直接进 12V 母线**:U29 泄放 13.2V 导通(10Ω 上压 40W)、两块板的 SMBJ13A 14.4V 击穿、刀片 TPS54622 VIN 上限 17V。
⇒ 板上加一级 **20V → 12V 降压**,12V 母线及以下(A1 的储能 / 检流 / 泄放、五片刀片)**一个字不改**。
- 顺带消掉决策 #20 的已知风险:DC-005 家族额定 2~5A 却要走 10A;USB-C 在 20V 下只走 5A。
### 36.2 电路(新页 `A1B-PD`,脚本 `r2/a1b_pd.py`)
`J1` USB-C(TYPE-C-31-M-12,C165948,**与 J23 同料**,额定 5A/20V)→ `F1` 12A → `VIN_FUSED`(20V,≤5A)
→ `D3` SMBJ22A + `C21~C24` 10µ/50V + `C19/C20` 100n/50V → `U37` **LM61495QRPHRQ1**(C3188410,10A,3~36V)→ `L4` → **`VIN_SW`**(12V)→ A1-INPUT 的 Rs6。
| 件 | 取值 | 依据 |
|---|---|---|
| `U36` CH224A(C42459160)| 单电阻 `R7` 120k ⇒ **只请求 20V** | 各家 PD 都有 20V;12V 不是每家都有(小米没有);28V EPR 手机充电器不给 |
| `D3` | SMBJ22A(C151920)| VRWM 22V > PD 20V 档上限 21V。⚠ **固件不许让 CH224 请求 28V**(寄存器 0x0A=5) |
| `R8` RT | 22.1k ⇒ **720 kHz** | `RT = 16.4/fsw − 0.633`;电感下限 `L > Vout/(fsw·0.6·10A)` = 2.8µH。400 kHz 要 ≥5µH,SRP1265A-6R8M 立创查不到料号,故提频率 |
| `L4` | SRP1265A-4R7M(C780205)| Isat 28A / Irms 13.5A / 8.4mΩ;纹波 1.42A(14%)|
| FB | `R10` 110k / `R11` 10k | VFB 1.0V ⇒ **12.0V** |
| EN | `R12` 100k / `R13` 10k | 1.263V × 11 = **13.9V** 才启动(12.8~15.0)⇒ PD 协商到 20V 之前(5V / 9V)不启动,母线大电容不会在 5V 下拽充电器;也挡住插错口(PC 只给 5V)|
| SPSP / SYNC / BIAS | VCC / GND / GND | 扩频开;轻载自动 PFM;BIAS 按手册「输出不低于 12V 就接地」(BIAS 绝对最大 16V)|
| `C14~C17` | 22µ/25V | 输出侧另有 A1 的 C2/C3 470µF + C4/C5 10µF |
| I²C | CFG2/CFG3 → TCA9548A **通道 5**(`SDA6`/`SCL6`)| 寄存器 **0x50 = 当前档位最大电流 ×50mA** ⇒ 固件按它定整板功率上限(60 / 65 / 100W)。CH224A 自带 10k 上拉 |
删掉的(原 A1-INPUT):`J1` DC-005、`Q1` AO4407 反接保护、`DZ1`/`R1`/`C1` 栅极缓启动(USB-C 不会插反;缓启动由降压软启动 + EN 门限代替)。
`F1` 位号保留、搬到 A1B-PD。DP/DM、PG、RESET# 都不用;SBU / D± 不接(只取电)。
### 36.3 原理图改动
| 页 | 改动 |
|---|---|
| **A1B-PD**(新)| 25 件 / 90 段 / 21 旗 / 21 端口。U37 两侧 10 间距的脚挂不下旗,功能块间一律用端口(`SW12` `BST12` `VCC12` `RT12` `EN12` `FB12`)|
| **A1-INPUT** | `a1_input.py` 加 `D16_DROP` / `D16_SEGS`:旧入口 6 件和挂在它们上的导线组整组删,`VIN_SW` 组只摘掉 Q1 漏极那五段;删完断言没有导线端点压在被删件的引脚上 |
| **A3-CTRL** | TCA9548A 脚 15/16(通道 5)引出 `SDA6`/`SCL6`(x 545,和 SDA5/SCL5 同列;名字只能 4 个字,右边 x≈620 起是 DAC 那一列的标签);A2 的地旗从 (585,270) 挪到 (530,290),让开新标签 |
改前两页的现状快照:`r2/_a1_a3_pre_d16.json`(A1 页上当时有 104 条文字,脚本只生成 16 条 —— 历次重画残留的)。
### 36.4 还没做的
- ⬜ 用户截图确认三页方向与排版(CLAUDE.md 规矩)。
- 🔶 PCB:导入 / 摆位 / 脚本已完成,布线进行中 —— 见 §36.5。
- ⬜ 固件:CH224A 驱动(TCA9548A 选通道 5,读 0x09 / 0x50)+ 整板功率上限按 0x50 取;禁止写 0x0A=5。
- ⬜ 丝印:J1 旁标「PWR 20V」,J23 标「USB」。两颗同料的 C 座插错不会坏(见 A1B-PD 页【C】),但要标。
### 36.5 PCB(2026-09-11 深夜 ~ 09-12)
**导入**
- 上次推三页时存了盘,但 A1-INPUT / A3-CTRL / A1B-PD **74 件的原理图 uniqueId 全是空的**(存盘、重开工程都不给分配;A4-HUB 那次是给了的)。
空着导入 ⇒ 板上已摆好布好的 A1 / A3 49 件会被当成「移除 + 增加」。⇒ **原理图侧 `setState_UniqueId()` 可用**:
保留件写 PCB 现有的 ggeNNN,23 颗新件写 `d16_<位号>`;存盘、重开工程读回 74/74。之后 `uid_sync_base` 0 / 141 不一致。
- 对话框是虚拟表格,按行读会超 30 s;改从表格的 React props 里直接取 `rows`(185 行 / 107 条明细):
移除 4(C1 DZ1 Q1 R1)/ 增加 23 / J1 换封装 / 改网络 29(U35.15/16 → SDA6/SCL6 + 11 组自动网名整组换号)/ 通道ID 50。
应用时取消「包含设计规则」「同时更新导线的网络」—— **换号那 11 组板上 51 段 / 10 孔照样被改了名**(两个勾都没勾)。
- 验收:164 件位号 + 封装逐件对上;**导入前后全板焊盘网名逐脚比对,差异 = 对话框清单,一条不多**;铜一处未动(仅两段共线 VIN_SW 被并)。
快照:`_base_board_pre_d16_0911.json` / `_base_padnets_pre_d16.json`(导入前)、`_base_board_post_import_d16.json`。
**摆位(`place_base.PD_HAND`,27 件逐件手排,`--only-pd` 只下发这一窝)** —— 图 `M18`(会话里给过用户 v4)
- J1 C 座底层 rot 180 口朝南 (12.5, 5.25);F1 在 J1 正北(两组 VBUS 在 F1 南合流);U36 CH224A 在 J1 东(CC 列朝西、I²C 列朝东进槽下走廊)。
- **U37 底层 rot 270:SW 边朝西接横放的 L4(rot 180,SW 端朝东),信号边(FB/RT/VCC/EN)朝东**。
这样转的理由:U37 两侧 VIN 要连通(见下),信号边朝东时 FB 采样线能沿东侧空带北上回 VIN_SW,不用跨 VIN;
L4 的输出端正好落在西北角挨着 C2 / C5;L1 小岛变成南北向一长条,与 12V 电流方向平行。
- 两颗 100n(C19/C20)贴 U37 南北两侧 VIN/PGND(中心距 1.5);南侧大电容 C21/C22,北侧 C23/C24(经 L1 小岛);
BOOT C18 在西北角落 L4.1 上沿;东侧一列 R8 / R11 / R10 / C13;EN 分压东南;输出电容 C14~C17 在 L4 北沿一行;
**RT602 改贴 U37 东侧测降压**(用户选;原来测 DC-005 接触热)。
- 留空区 `PD_FREE`:J1 两组 VBUS 之间(CC 换层 + VIN_RAW 合流)/ BST 出路 / EN·SYNC·SPSP 出口 / SW 连接段。
- 读回:位姿逐件对上、朝向断言(J1/J23 口朝南、U37 SW 在西、L4 SW 端朝东)过。
⚠ `verify()` 原来按焊盘块中心比,U37 这类多边形焊盘抓回来按板上水平条带拆,转 90° 后拆法不同 ⇒ 报了假的 0.375 mm;
改成**逐脚比所有块合起来的外框**(27 件最大差 0.002)。泄放链 Q3 漏极网名原来写死 `$1N114`(导入后是 `$1N213`)⇒ 改按 5 脚现取。
**两处单层走不通 → 用户选的换层方案(`pcb_base.PD_L1`)**
1. **CC 夹在两组 VBUS 之间**(USB-C 脚序决定:GND VBUS CC2 … CC1 VBUS GND),VIN_RAW 一合流 CC 就被围住 ⇒
CC 在两组 VBUS 之间打孔上 **L1**,在 12V 大铜里各开一条窄走廊东去 U36 再下来(CC2 南、CC1 北,不交叉)。
⛔ 没选「壳体底下走 L4」:20V / CC 压在接地金属壳下只隔一层阻焊。
2. **LM61495 两侧 VIN 连通**(VIN1/VIN2 分两侧、中间夹 SW,TI 做法是 VIN 走另一层)⇒ U37 正上方 L1 挖一块 **VIN_FUSED 小岛**
(18.9~22.9, 13.4~29.2);L1 12V 主块在这一段还剩 18 mm。
- 实现:`route_base._build` 先把窗口从 12V 挡线图里扣掉、再按各自的网封(⚠ router `_mark` 在别的网已占的格子上写会变 255,不能叠加);
`pour_base` 12V 大铜绕开小岛(外扩 0.5),小岛自己铺 VIN_FUSED。
**脚本改动**
- `pour_base`:入口铜块不再按「本网焊盘外框」自动撑(新件东一颗西一颗,一撑就把 U37 包进去)⇒ 手画矩形 `PD_POUR`
(VIN_RAW 1 / VIN_FUSED L4 3 + L1 小岛 / SW12 / VIN_SW 3);Q1 栅极缺口删除。离线断面:J1→F1 6.60(要 3,不强制)/
L4→储能→Rs6 7.95(要 6.5)/ L1 12V 主块 13.20(要 8)。
- `rules_base`:加 SW12(PWR9A、铺铜全连接);`_base_npth.json` 补 J1 两颗定位孔(按 J23 同料平移)。
- 新脚本:`d16_route.py`(服务器:固定构造 → 早段地孔 → 逐网补线 → 晚段地孔)、`remote_d16.sh`(上传 / 拉回,`MODE=reach` 跑
`d16_reach.py` 泛洪诊断)、`d16_check.py`(本机四层精确净距 + L1 窗口 + 连通)、`d16_push.py`(下发 + 读回按几何逐条核)。
**板上删旧铜**:VIN_RAW / VIN_FUSED / GATE 整网 + 角内 VIN_SW / NTC2 + 压新件焊盘的 GND ⇒ 线 36 / 孔 2,按几何匹配删。
⚠ **删完线数只少 34**:EasyEDA 删除过程中把残段合成了**两条新线**(新 ID、新几何,导入后的快照里没有),按几何再删一次才干净
(`_base_board_post_del_d16.json`,1787 段 / 580 孔)。⇒ 删铜后一律「期望 = 旧快照 − 删除清单」与读回逐条比。
**服务器补线(`d16_route.py`,每轮 ~15~25 s)**
- 第一轮:SPSP(南侧)→ VCC(东北角)在 L4 上只能绕东侧,一根线把 FB / RT / EN 的出路全封了;0805 / 1206 的 GND 盘被当成散热盘打了盘中孔;
VIN_RAW 0.5 落不进 C 座 VBUS 0.3 宽的焊盘条;VIN_SW 报 `C2.1#t` 出不来是通孔的 L1 障碍副本被当成了焊盘(假的)。
- ⇒ 固定构造(本机 `d16_check` 四层精确净距 0 条后才上服务器):
SW12 U37.16↔L4.1(1.0 宽,夹在两只 PGND 角脚之间)/ **SPSP→VCC 走 U37 肚子底下**(0.15 宽,从 SW 焊盘和东侧那排脚之间穿过,
离 FB 0.186、BIAS 0.165)/ U37 PGND×2、BIAS、SYNC、AGND 的「短线 + 孔」(AGND 从 R8 与 R11 中间往东引出)/ J1 两只贴片 GND 脚接旁边外壳通孔脚 /
C18.SW → L4.1 / VCC 6 脚 → C13 / VIN_RAW 两组 VBUS 各一根竖线进 F1.1。盘中打孔阵只给 U36 散热盘。
- 第二轮:GND 地孔全打上;FB / RT / EN / VIN_FUSED / VIN_SW / NTC2 通;剩 BST12(被 SW12 的绕线封了)、VCC 6 脚、VIN_RAW ⇒ 上面最后三条固定线。
- 第四轮:除 SDA6/SCL6 外全通(197 段 / 49 孔,本机 0 条)⇒ **已下发存盘**(`_base_board_post_push_d16.json`)。
读回时两段同网 VIN_FUSED 被 EasyEDA 拼成一根(0.4 的短线挪了 0.05 mm),按实际坐标核过:铜覆盖原两段、离 C18 0.276。
**I²C:CH224A 改挂主总线(用户 2026-09-12)**
- SDA6 / SCL6(→ U35 通道 5)连着两轮「没布通」。`d16_reach.py` 在布线网格上泛洪:U35.15/16 能到的范围被整个圈在北半边
(y ≥ 46.9、x ≤ 72.6),南边 U36 能走遍全板但碰不上 —— 圈住它的是 J36 槽一带的原有布线(GND / +3V3 / L1 +12V、+5V / 风扇测速,
内层残留线挡孔),不是新入口造成的。A* 没有搜索上限,「没布通」= 真没有路。
- 用户选**改挂主 I²C**:0x22/0x23 与主总线上 U13 0x43 / U6 0x4A / U7 0x4B / U35 0x70 不冲突。
`a1b_pd.py` U36.2/3 → SCL/SDA;`a3_ctrl.py` U35.15/16 回 NC。推送、verify_sch 两页 PASS、xpage 0;
uniqueId 同样被整页重画清空 → 再写回;导入 20 条改网(含 7 组自动网名换号:DAC 6 组 + CFG1 `$9N85 → $9N145`),焊盘网名比对 0 意外。
⚠ 导入后要 `place_base --dump` 重抓焊盘表,否则 route_nets 拿旧网名判「已连通」。
- SCL 能接(落在 U13.5 往东那根上),**SDA 接不进**:南边唯一的 SDA 是 U13.4,东边 INA_ALERT / SDA / SCL 0.5 mm 并排出束、
西边本体底下有一段 SCL 把 2、5 脚连起来 ⇒ 四面被围,这束一直夹着进北边,中间放不下一个孔(`d16_tap` 30 个落点全不通)。
- 用户选**拆 U13 这一束重布**(`d16_rip.py`,只改数据不动板;拆除区 (15,38)~(40,58) 内三网外层铜,U13 本体底下的 2—5 脚 SCL 连线留着):
- 12 组(先后 × 线宽 0.2/0.15)全都至少断 1 块 —— 南北之间能穿墙的路只有两条(那条走廊 + 经 L1 绕行的一条),却有三根线;
- 实验:候选落点 30 → 100,INA → SDA → SCL 三网都能回北边,但 U13 的 SCL 新路从南边兜过 U36,把 U36.3 和 SDA 隔开;
INA_ALERT 不接回的话,U13 的 SCL 反而回不去(这条路不成立);
- ⇒ 逐步排序 `--steps`:**INA:1 → SDA:1 → SDA:2 → SCL:1 → SCL:2**(线宽 0.15、候选 100)**0 块断开**,本机净距 0 条。
代价:新 132 段 / 21 孔、~303 mm(U13 的 SCL 从北侧 2 脚绕了 44 段 / 13 孔,INA_ALERT 36 段 / 5 孔)—— I²C 100~400 kHz 下长线电容仍在允许范围内。
- 同批下发:C18 → L4.1 那段终点伸进焊盘(`board_check` 把 3.5×5 的 L4.1 读成 5×3.5,报 SW12 两块)。
---
## 37. 底板顶面件:C2 / C3 / J26 / J27 / J33(2026-09-12 用户要求)
用户:「将风扇座,C2,C11,J33 移到顶面」——追问时澄清 **C11 是指 C3**(`C11` 是 0402 的 3V3 去耦,
翻面等于把铜从 L4 挪到 L1、还要在 12V 大铜里挖窗,去耦电容又跑到芯片另一面,已排除)。
### 37.1 为什么这五件翻面是「零代价」,而别的件不是
名单在 **`pcb_base.TOP_EXTRA`**,`side_of()` 按它判层。判据只有一条:
**焊盘全在封装 y = 0 那一排**。`place_base.pad_off()` 翻面只把原生偏移的 **y 取反**,
所以 y=0 的脚翻面之后孔位、针序、铜**一根都不变** —— 纯粹是装配面 + 丝印换面。
⛔ 焊盘不在一条线上的件(贴片、双排针)不成立:那种翻面会把铜整个挪到另一层,要重新布线。
### 37.2 顶面的限高不归 `H_BOTTOM` 管
插槽把刀片底边抬到板上约 **11 mm**(§6.6),刀片自己 y 0~11 又是金手指禁布区(两面无件)
⇒ 顶面件(12 mm 的电解、11.5 mm 的排针)**高度不受限**,只有**横向**约束:
| 约束 | 判据 |
|---|---|
| 刀片板面 | 竖在 `y = 槽心 ±0.8`(板厚 1.6),顶面件本体离它 ≥ `CARD_CLR` 0.5 |
| 槽体 | `slot_body(i)`,x 28~70 |
| 背面 | 通孔件在背面**只占焊盘**(±4.61 × ±0.8),不是 5 mm 深的本体 —— 所以能塞进背面件的缝里 |
`place_base.selfcheck` 已加这一支。**J27 原位 (13.5, 88.0) 的本体 y 85.5~90.5 正好穿过 J34 那片刀片**
(89.64)⇒ 必须挪;而 x=13.5 往南焊盘先压 C738 再压 R754 ⇒ 只能往西,定在 **(9.0, 85.5, rot180)**。
rot180 是扫出来的:针序反转把 12V 脚换到靠 C738 那侧,未完成从 6 条降到 2 条。
### 37.3 ⛔ A6 西北角是「单层布线区」—— 这一轮最贵的一课
`l1_12v_rects()` 的 12V 主块是 **x 0.5~27.8 / y 0.5~97.5 的一整块**,把整个 A6 区的 L1 占死
⇒ 过孔上得去但走不动,那一带等于**单层**,而 L4 早被风扇驱动那一窝件占满。
于是「几何上放得下」≠「线接得上」:按用户先选的「两只座都南移进走廊」,
**扫了 800 个候选落位**(`fan_scan.py`,PyPy 上单轮补线 ~1.2 s)、压线宽、拆线重布、
修了通孔盘放大量,**始终有 3~4 条布不通**。
**解法**:给 `FAN_PWM_L` 在 `pcb_base.PD_L1` 里开一条 **L1 走廊 `(4.0, 86.0, 21.0, 89.0)`**。
它只挡**导线图**(`route_base._build` 先从 12V 里扣掉窗、再按窗的网名封),
**铺铜那边不用开窗** —— L1 的 12V 铺铜本来就会绕开异网导线,D16 的 CC 两根走的就是这个路子。
- 断面账:主块宽 27.30,被一根 0.15 的线占掉约 0.5 ⇒ 仍 **≈26.8 mm ≫ §6.4 要的 8 mm**。
- y 86~89 **不碰任何一槽的 12V 指头**(槽 5 南指 y≈83.04 / 北帽 y≈96.24)。
- 结果:四个风扇网**全通**,补 60 段 / 3 孔 / 87.5 mm,其中 PWM 借 L1 走 13.9 mm。
### 37.4 `TH_GROW`:通孔盘被统一放大 0.6,把排针的中间几只脚封死了
`route_base.TH_GROW = 0.6` 本是为 **J1 / J23 的长槽孔**加的(槽比焊盘大),却对所有通孔盘生效:
2.54 间距的风扇座(圆盘 1.6)在占用图里变成 **2.2 × 2.2**,相邻两脚之间只剩 0.34,
而出口要「半盘 1.10 + 线半宽 + 净距 = **1.33**」才落得下第一格 ⇒ **中间那几只脚一根铜都出不来**。
症状是布线器报「`J26.3` 没布通 / 一根铜都没有」,看着像没地方走,其实是被自己虚胖的邻盘封死;
压线宽、拆线重布、换落位全都救不回来。已改成按件区分(`th_grow()`,只有 `TH_GROW_REFS` 放大)。
### 37.5 两条方法论
1. **能不动的件就别动**。J26 原地翻面 = 零布线代价;它一旦挪窝,那三条线就得在单层区重布,
而**布线器布不出来的路,不等于板上原来没有** —— 拆之前先备份几何,走不通就原样画回去
(`ripped_backup.json` → `restore_ripped.py`,这一轮真用上了)。
2. **搜索的判据缺一条,就会被精准地钻这个空子**。`fan_scan` 起初没查「两只座本体不许重叠」,
扫出来的「最优」是两只座**摞在一起**(J26=(13.5,94.5) / J27=(12.0,94.5))—— 布线代价当然最低。
- ⬜ NTC2 绕了 108 mm / 12 孔(RT602 新位置 → 东边的老线),通,不好看。
**收尾(修正 3)与最终状态**
- DRC 第一遍:间距 3(全是「钻孔到钻孔」:服务器扇出的 VIN_FUSED / GND 孔紧挨着固定孔,0 / 0 / 8.5 mil)+ 物理 4(R10 采样线 0.2 < PWR9A 下限 0.3)
+ 连接性 4(J36.1 / J36.4 / Q6.2 GND —— 新线切了铺铜 / 铺铜分块变了)。
⇒ 删 3 个服务器孔(留固定孔,C19/C20 的地仍经各自引出线接到固定孔)、采样线加宽 0.3、J36.4 / Q6.2 各补 GND 孔(`d16_gndfix.py`,
J36.1 找不到落孔点,靠与 J36.4 相连的那根 L1 GND 线)。`d16_check` 补了**同网钻孔间距**(原来只查异网)。
- **最终**:快照 `_base_board_final_d16.json`(2104 段 / 648 孔),图 `M18-d16-routed.png`;重铺 147 块填 135(功率铜面全填);
**DRC 间距 0 / 连接性 0**,只剩插槽封装自带 5 条(§27.8 起的老问题);`board_check` 118 / 121(GND 6 只通孔 / 铺铜脚、XTAL1/2 圆弧 —— 老误报);
断面 L4→储能→Rs6 7.95 mm、L1 12V 主块 13.20 mm。
- ⚠ `reload_base` 重载前会 save():一次 `d16_push … | tail && reload_base` 把读回差 1 段(被同网宽线整段覆盖的碎段)的版本存了进去,
无害;链条一律 `set -o pipefail`。
- ⚠ **2026-09-12 复核板面**(用户问「板上是不是最后那版」):与 `_base_board_final_d16.json` 逐项比,**少了 16 块铺铜、全是内层地**
(GND-L2-0~7 / GND-L3-0~7,L2/L3 各 13 → 5;L1 55 / L4 66 一块没少),线 / 孔 / 弧 / 填充 / 164 件位姿全同 ——
也就是说**内层地平面被删掉一多半,而 DRC / board_check 都不会直接指出来**(一个不读铺铜,一个把它算成「靠铜面成网」)。
同时 USB2 差分对有 5 段直角改成了 45° 斜切(三个角),四层精确净距重算 0 条 ⇒ 改好不是改坏,**保留**。
⇒ 只 `pour_base --push` 重建 147 块 / 填 135(L2、L3 各回 13),重铺前后线 0 变、孔 0 变、元件位姿 0 变;
恢复前存档 `_base_board_before_repour_0912.json`,此后基准 `_base_board_d16_restored_0912.json`。
**教训:交接 / 收尾前按快照逐项比,铺铜要按「层 + 网 + 名字」逐块比,光看总块数看不出内层被删。**
## 38. 底板风扇窗口按用户新布局重布 + L1 走廊重开(2026-09-12 用户要求)
用户:「将底板按照现在我的布局重新布线」→ 选「**风扇窗口整体重布**(拆掉自己手布的 43 段重来)」+「GND 回流孔一起补」。
### 38.1 ⛔ 走廊是跟着**摆位**走的,摆位一变走廊就作废
§37.3 给 `FAN_PWM_L` 开 L1 走廊解决了「A6 是单层布线区」,后来又按当时的摆位加了三条
(`FAN_TACH_OUT` 贴东缘竖带 / `FAN_EN` / `FAN_PWM` 短横带)。这一轮发现:
**用户重摆之后,那四条走廊注释里写的坐标全是旧的** —— `R748.1` 从 (18.2,91.0) 跑到 **(5.13,73.91)**、
`R757.2` 从 (27.2,93.0) 跑到 **(18.11,83.57)**。走廊还在原地,等于没开
⇒ 四条长途网(`FAN_EN` / 两路 `TACH` / `FAN_PWM_L`)必然布不通,**与布线器参数无关**。
⇒ 走廊必须**和摆位一起维护**。为此把走廊做成可配置(`_base_window.json` 的 `pd_l1_extra`,
`repair_window.py` 运行时替换 `B.PD_L1` 里的 `FAN_*`),服务器单轮 1.5 s,试一组不用改源码;
定稿后再固化回 `pcb_base.PD_L1`。
### 38.2 走廊必须**盖住焊盘正上方**,不能只盖沿途
只盖沿途的话,那只脚得先在 L4 挪到走廊下方才打得了孔 —— 实测 `J27.4` / `Q10.3` 就是这么卡住的:
| 版本 | 走廊 | 未完成 |
|---|---|---|
| 无走廊(旧的四条按旧摆位) | — | 4 |
| v1:只盖沿途 | 4 条 | 3(`FAN_EN` 0 段 → 11 段并布通) |
| **v2:盖住焊盘(定稿)** | **6 条** | **2** |
| v3:把 `FAN_TACH_IN` 竖带再放大 | 6 条 | 2(**换了人**:TACH_IN 通了、`FAN_EN` 掉了) |
| v4 = v3 + 把 `FAN_EN` 顶到 priority 最前 | 6 条 | 2(还是换人,总数不变) |
⇒ 再次印证「priority 是按下葫芦浮起瓢」(§36 同结论)。**别再调**。
### 38.3 定稿的六条走廊与断面账
```
FAN_EN (12.00, 76.10, 27.50, 77.10) R748.1 西段走 L4、东段借 L1 到窗缘
FAN_TACH_IN (16.00, 75.15, 27.50, 76.05) 主干端点 (26.00,75.62) 在窗缘
FAN_TACH_IN (13.70, 76.05, 16.20, 90.40) 竖带盖住 Q9.3 / R753.2
FAN_TACH_OUT (17.40, 80.70, 27.50, 81.70) 盖住 Q10.3
FAN_TACH_OUT (17.40, 81.70, 18.80, 84.10) 竖段接 R757.2
FAN_PWM_L (10.30, 83.90, 22.50, 87.30) 盖住 J27.4 与 Q6.3
```
- 判据(§6.4 ≥ 8.0 mm):主块宽 27.30,**逐 y 水平断面**上扣掉走廊的 x 跨度 ⇒ 新工具 `l1_section.py`。
纯几何算 **11.80 mm**;铺铜之后 `pour_base` 的实测断面(还要扣掉线和孔)**9.82 mm**,仍达标。
- **横带很贵、竖带几乎免费**:横带按它的 x 跨度整段扣,竖带只扣 1~3 mm。
- 走廊 x 一律 ≤ 27.5,而槽指头在 x ≥ 27.5 ⇒ **不碰任何一槽的供电路径**。
- 只挡**导线图**,铺铜不用开窗(同 §37.3)。
### 38.4 「布不通」这次不是拥塞
- L4 沿四条路径**逐 x 扫过纵向空隙:没有一条彻底堵死**(最窄 0.9 mm)。
- L1 上按 0.25 网格泛洪:`J27.4` → 已布 L1 段 **686 个格点可达**;`R753.2` → 窗缘 **757 个可达**。
- 换层孔在 `J27.4` 旁 **0.25 mm** 处就落得下。
⇒ 三项都排除之后,`FAN_PWM_L: J27.4` 仍 0 段,**剩下的怀疑对象是 A\* 自身**(同 §36 的 `$6N64`)。
### 38.5 板上结果
拆 43 段 → 补 **139 段 / 7 孔** → GND 回流孔 **10 个** → 缝合孔 **25 个**;
板上 外层 1960 / 内层 116(用户手布,一段没动)/ 过孔 669;铺铜 147 块建齐、填出 132(空的 15 块全是 GND 小瓦片)。
`fan_check` 对**板上真实快照**逐项净距达标(含「新孔 vs 内层线」);669 个孔 vs 新线异网净距 0 处不足。
**遗留**:① `FAN_PWM_L: J27.4` 没接上;② `FAN_EN` / `FAN_TACH_IN` / `FAN_TACH_OUT` 到 U20 的**窗外段**
(缺口 15.6 / 17.7 / 16.6 mm,中间是槽体 x 28~70 的手焊 1 mm 针排,按设计不许穿 —— **拆线前也没通**);
③ `DZ3.1`(+12V) 被挪走后离开了原来的换层孔,最近同网铜 9.1 mm(L4 上那一带没有 12V 铜,要靠孔上 L1)
—— **拆线前就是这样**,已在本轮顺手补一个 +12V 过孔(落点就在焊盘中心)。
⚠ 与拆线前逐网对比(同一套并查集判据):`GND` 未接 58 → **48**、`+3V3` 未接 2 → **0**、
两路 `TACH` 未接 2/1 → **0**、`+12V` **一字不差**、`XTAL1/2` 不变(老误报)
⇒ 本轮**没有引入任何新的连通性问题**。
### 38.6 两条操作上的坑
- **EasyEDA 会合并线段**:拆线点名 43 段、外层实际只少 42(`FAN_PWM` 主干被延长);下发 139 段、
板上净增 142,其中 8 段"按坐标找不到" —— 逐段核过**全部被同网同层铜覆盖,真缺失 0**。
⇒ 拆 / 发之后要按「几何是否被覆盖」判,不能只按坐标比对。
- **EasyEDA 的 PCB API 回的是 mil**。抓板脚本漏了 ×0.0254 时,连通性判据会把**每一只脚**都报成"未接",
而数值上看着完全正常。
## 39. 底板位号丝印重排(2026-09-12 用户要求「按当前 EDA 中底板状态定版,优化丝印放置」)
板上布线以**用户自己布好的那一版为准**(定版,不再动铜);这一节只改丝印。
### 39.1 现状体检
板上 **164 个位号**(`pcb_PrimitiveAttribute`,`key=='Designator'`),字高统一 1.143 mm、
居中对齐、不镜像;顶层丝印(layer 3)10 个、底层(layer 4)153 个可见,`C743` 不可见。
**板上没有任何元件外框丝印**(丝印层 0 线 0 弧)—— 装配时只有位号可依。
| 判据 | 改前 | 改后 |
|---|---|---|
| 压同面焊盘 | **128** | **4** |
| 压过孔 | 48 | 8 |
| 越板框(留 0.5) | 8 | 3 |
| 位号互相重叠 | 34 对 | **0** |
重排 150 个(理想位 143 / 允许压过孔 4 / 允许压焊盘 3),白名单保留 13 个;
位号离本体中心平均 2.19 mm、最远 6.97 mm(`L4`,本体 15×12.5,位号贴在本体外 0.5 mm)。
### 39.2 ⛔ 三条判据上的坑
1. **`rotation` 字段是「度」且不归一化**:`900` 表示 180°、`720` 表示 0°。
按 ×10 解读会得到「9° / 18° / 27°…」这种**看着像真的却完全错误**的朝向分布。读回一律 `% 360`。
2. **不能用「本体 ≥ 5 mm」当保留白名单**。第一版这么写,把 `R9A`(7.4×3.5) / `D3`(6.8×2.2) /
`U13`(2.3×5.8) 这些**该重排**的中等件一起豁免了 —— 改后残留 25 个里 **18 个**是这么来的。
⇒ 改成**显式名单** `silk_base.KEEP`:五个插槽(位号故意放槽体西侧 x=28,离本体 21 mm)、
`U20`(让开天线净空)、顶面件 `J26/J27/J33/C2/C3`、板边 USB-C `J1/J23`。收紧后残留 25 → 12。
3. **必须给「离本体」设硬上限**(`MAXD = 2.2` mm,量到本体外框)。
不设的话贪心会把位号甩到十几毫米外的空地上 —— 实测平均 3.28 / 最远 22.2 mm,
那样的位号对装配没有指向性。够不着就**降级**:理想位 → 允许压过孔 → 允许压焊盘 → 保留原位。
### 39.3 可行性先算过再动手
按现字高 1.143 mm 扫一遍:163 个可见位号里 **161 个有解**(无解只有 `R13` / `C745`);
缩到 0.9 mm 则全部有解。⇒ **不用缩字号**,保持 1.143 mm 可读性。
### 39.4 残留(都已定位,不是漏查)
- 白名单 5 个:`J23` 压过孔、`J26`/`J33`/`U20` 越框、`J1` 压 `U36.11` —— 都是原本就如此的设计位。
- 重排后 7 个降级:`C731`/`C744`/`C749`/`R603` 压过孔,`C745`/`R12`/`R13` 压焊盘 + 过孔
(都在最密的 0402 群里,周围 2.2 mm 内没有干净落点)。
工具:**`silk_base.py`**(`--push` 走 `pcb_PrimitiveAttribute.modify(id, {x, y, rotation})`,
下发后读回逐个核对),快照 `_base_silk.json`、方案 `_base_silk_plan.json`。