R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
+324
@@ -0,0 +1,324 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""C2-HUB:给 U32(USB2517I) 的 **PRT_SWP1~5** 五个 strap 脚各加一颗 100k 下拉(R900~R904)。
|
||||
|
||||
/c/Python314/python .claude/c2_straps.py # 干跑 + 全页碰撞检查
|
||||
/c/Python314/python .claude/c2_straps.py --push # 删旧 + 放件 + 连线 + 重写页面文字
|
||||
|
||||
## 依据(不是「悬空取 0」)
|
||||
|
||||
设计文档 §21.6 原来写「这些脚都是 Strap,悬空取默认 '0'」—— **手册里没有这句话**。
|
||||
`DS00001598C`(LCSC 给 C478081 链的那一份,59 页):
|
||||
|
||||
- Table 5-3 缓冲类型:`IPD` = 带内部弱下拉、`IPU` = 带内部弱上拉,
|
||||
而这十个脚全是 **`I/O12` = 纯输入输出缓冲**,一个字都没提内部下拉。
|
||||
(同节里 `OCS[1..7]_N` 确实是 `IPU`、`TEST` 确实是 `IPD` —— §21.6 那两条是对的。)
|
||||
- Table 5-2:`CFG_SEL[2:0]=000` → 「Internal Default Configuration • **Strap Options are Enabled**」,
|
||||
所以这些脚**会**在 RESET_N negation 时被采样。
|
||||
- §7.5 + Figure 7-3「LED STRAPPING OPTION」(第 42 页)给的推荐无源方案就是 **100 kΩ**。
|
||||
|
||||
## 为什么只做五颗(2026-09-07 用户拍板)
|
||||
|
||||
Table 7-1 的内部默认值本来就是 `FAh Port Swap = 00h`、`F6h/F8h Boost = 00h` ——
|
||||
电阻并不改变设定,只是把**被采样到的值**钉成本来就该有的那个。所以按「采到 1 会怎样」筛:
|
||||
|
||||
| strap | 采到 '1' 的后果 | 要不要电阻 |
|
||||
|---|---|---|
|
||||
| `PRT_SWP1~5`(U32 51/49/47/33/31)| 对应端口 `D+`/`D−` **内部对调** → 那一路手机枚举不出来 | ✅ **R900~R904** |
|
||||
| `PRT_SWP6/7`(17/15)| 口 6/7 已被 R714/R715 上拉禁用 | ❌ 无后果 |
|
||||
| `BOOST0/1`(50/48)| PHY 驱动加强,只影响 SI/EMI | ❌ 无功能后果 |
|
||||
| `GANG_EN`(34)| 联动通断/过流 —— 本设计 `PRTPWR`/`OCS` 一根都没接 | ❌ 无后果 |
|
||||
|
||||
失效长相值得记一笔:**不是板子挂掉,是某一路手机认不出来**,而且悬空脚采到什么
|
||||
取决于漏电/耦合/温度,可能每次上电、每块板都不一样 —— 五机位农场上最难查的那类现象。
|
||||
|
||||
## 这一页的坑
|
||||
|
||||
页面很挤,而且**文字有三重叠影**(标题 ×3、6 条说明各 ×3,其中一份还压在 J23 上)。
|
||||
本脚本顺手把文字删干净重写成 1 份,腾出 U32 下方 x 460~880 那条带子。
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
from eda import run
|
||||
|
||||
PAGE = '472c9bdbd7180ac4' # C2-HUB
|
||||
LIB = '0819f05c4eef4c71ace90d822a990e87'
|
||||
R100K = 'acc294b6661948399f609b41c77ed19f' # C25741 0402WGF1003TCE
|
||||
GEO = os.path.join(HERE, '_c2hub_geo.json')
|
||||
|
||||
FRAME = (20, 10, 1145, 812)
|
||||
TITLE = (455, 10, 1145, 192)
|
||||
|
||||
# (位号, U32 脚号, 脚坐标, 引出方向, 电阻 x) —— 电阻中心 y=290,pin (x,310)/(x,270)
|
||||
# ⚠ 右侧不能走 x=760:那一列全是端口引脚(y 405~465),竖线穿过去就是短路。
|
||||
# ⚠ 右侧 y=475/495 的横线不能超过 x≈810:PRTDIS7 端口(900,490)的标签占 813~900;
|
||||
# y=515 在标签带(485~495)之上,可以往右走远一点。
|
||||
STRAP = [
|
||||
('R904', '31', (320, 360), 'L', 270), # PRT_SWP5
|
||||
('R903', '33', (320, 340), 'L', 310), # PRT_SWP4
|
||||
('R901', '49', (680, 495), 'R', 700), # PRT_SWP2
|
||||
('R902', '47', (680, 475), 'R', 780), # PRT_SWP3
|
||||
('R900', '51', (680, 515), 'R', 860), # PRT_SWP1
|
||||
]
|
||||
# 2026-09-07 用户拍板去掉的五颗(BOOST / GANG_EN / 已禁用口的 SWP6-7),
|
||||
# 连同它们在上一版画的线一起删。旧版的完整表留在这里,仅用于「删干净」。
|
||||
DROP = ['R905', 'R906', 'R907', 'R908', 'R909']
|
||||
OLD_STRAP = [
|
||||
('R906', '15', (320, 520), 'L', 190), ('R905', '17', (320, 500), 'L', 230),
|
||||
('R904', '31', (320, 360), 'L', 270), ('R903', '33', (320, 340), 'L', 310),
|
||||
('R901', '49', (680, 495), 'R', 700), ('R908', '48', (680, 485), 'R', 740),
|
||||
('R902', '47', (680, 475), 'R', 780), ('R907', '50', (680, 505), 'R', 820),
|
||||
('R900', '51', (680, 515), 'R', 860), ('R909', '34', (680, 345), 'R', 940),
|
||||
]
|
||||
OLD_BUS = [(190, 310, 230), (700, 940, 820)]
|
||||
|
||||
RY, RTOP, RBOT, BUS = 290, 310, 270, 250 # 电阻中心 / 上脚 / 下脚 / 地母线
|
||||
# ⚠️ 地旗的 x **必须落在某颗电阻的分支点上**(skill §8a-3):
|
||||
# EasyEDA 会把共线的两段导线合并成一条,所以「我分两段建、中间那点是端点」不成立。
|
||||
# 上一版左母线的旗放在 x=250(几颗电阻之间的空档)→ 落在合并后的中段上,
|
||||
# PCB 网表里那四颗的地端是 `$16N236` 而不是 GND。verify_sch 四道检查全过,只有网表抓得到。
|
||||
LBUS = (270, 310, 270) # 左母线 x0,x1,地旗 x(270 = R904 的分支点)
|
||||
RBUS = (700, 860, 780) # 右母线 x0,x1,地旗 x(780 = R902 的分支点)
|
||||
|
||||
TITLE_TXT = u'【P4B-HUB】板载 USB 2.0 七口 Hub —— 对外只有一根 USB 线(§15.1 / §21)'
|
||||
NOTES = [
|
||||
u'U32 USB2517I CFG_SEL[2:0]=000 → 内部默认配置 + strap 使能;寄存器 06h 默认 9Bh、bit7=1 = 自供电',
|
||||
u'(声明成总线供电,主机会按每口 100mA 做预算,而手机描述符要 500mA → 拒绝配置,枚举不上)',
|
||||
u'口 1~5 → CH1~CH5;口 6/7 由 R714/R715 各 10k 上拉禁用(拆掉电阻即可放出来)',
|
||||
u'RBIAS 必须 12.0k±1%;晶振 24MHz±350ppm;VDD18/VDD18PLL 各 ≥1µF 且 ESR<0.1Ω',
|
||||
u'VBUS_DET 接真·上游 VBUS(可插拔 Hub 必须如此),100k/100k 分压进 3.3V 逻辑',
|
||||
u'PRTPWR/OCS 全不接:端口通断由 ESP32 经 TPS22918 控制(§15.2 对策 2)',
|
||||
]
|
||||
STRAP_TXT = u'R900~R904:PRT_SWP1~5 各 100k 下拉(I/O12 无内部下拉,采到 1 会把该口 D+/D− 对调)'
|
||||
STRAP_TXT2 = u'BOOST/GANG_EN/SWP6-7 不装电阻:采到 1 也无功能后果,理由见 §21.6a'
|
||||
|
||||
|
||||
def call(js, timeout=240):
|
||||
r = run(js, timeout=timeout)
|
||||
if not r.get('success'):
|
||||
raise RuntimeError(json.dumps(r, ensure_ascii=False)[:900])
|
||||
return r['result']
|
||||
|
||||
|
||||
def open_page():
|
||||
call("""
|
||||
await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r=>setTimeout(r,1800));
|
||||
const n=(await eda.dmt_Schematic.getCurrentSchematicPageInfo()).name;
|
||||
if (n!=='C2-HUB') throw new Error('page='+n);
|
||||
return 1;""" % PAGE)
|
||||
|
||||
|
||||
def _wires(strap, buses):
|
||||
W = []
|
||||
|
||||
def w(net, *pts):
|
||||
for i in range(len(pts) - 1):
|
||||
a, b = pts[i], pts[i + 1]
|
||||
assert (a[0] == b[0]) != (a[1] == b[1]), u'%s: %s->%s 非正交或零长' % (net, a, b)
|
||||
for v in (a[0], a[1], b[0], b[1]):
|
||||
assert v % 5 == 0, u'%s 坐标不是 5 的倍数' % net
|
||||
W.append((a, b, net))
|
||||
|
||||
for des, pin, (px, py), side, rx in strap:
|
||||
w('STRAP_' + pin, (px, py), (rx, py), (rx, RTOP))
|
||||
w('GND', (rx, RBOT), (rx, BUS))
|
||||
for x0, x1, fx in buses:
|
||||
if x0 != fx:
|
||||
w('GND', (x0, BUS), (fx, BUS))
|
||||
if fx != x1:
|
||||
w('GND', (fx, BUS), (x1, BUS))
|
||||
return W
|
||||
|
||||
|
||||
def build():
|
||||
W = _wires(STRAP, [LBUS, RBUS])
|
||||
F = [('Ground', 'GND', LBUS[2], BUS, 0), ('Ground', 'GND', RBUS[2], BUS, 0)]
|
||||
PL = [(des, rx, RY, 270) for des, pin, xy, side, rx in STRAP]
|
||||
TX = [(TITLE_TXT, 45, 800, 15)]
|
||||
for i, t in enumerate(NOTES):
|
||||
TX.append((t, 340, 780 - 18 * i, 11))
|
||||
TX.append((STRAP_TXT, 45, 205, 10))
|
||||
TX.append((STRAP_TXT2, 45, 188, 10))
|
||||
return W, F, TX, PL
|
||||
|
||||
|
||||
# ---------------- 检查 ----------------
|
||||
|
||||
def _on(p, a, b, tol=0.6):
|
||||
(px, py), (ax, ay), (bx, by) = p, a, b
|
||||
if abs(ax - bx) < tol:
|
||||
return abs(px - ax) < tol and min(ay, by) - tol <= py <= max(ay, by) + tol
|
||||
if abs(ay - by) < tol:
|
||||
return abs(py - ay) < tol and min(ax, bx) - tol <= px <= max(ax, bx) + tol
|
||||
return False
|
||||
|
||||
|
||||
def _tw(s, size):
|
||||
n = sum(1 for ch in s if ord(ch) > 0x2000)
|
||||
return size * (n + 0.5 * (len(s) - n))
|
||||
|
||||
|
||||
def check(W, F, TX, PL, geo):
|
||||
errs = []
|
||||
mine = {(px, py) for _, _, (px, py), _, _ in STRAP}
|
||||
old = {(px, py) for _, _, (px, py), _, _ in OLD_STRAP}
|
||||
ends = set()
|
||||
for a, b, _ in W:
|
||||
ends.add(a); ends.add(b)
|
||||
for name, x, y in geo['pins']:
|
||||
if (x, y) in old: # 上一版画到的十个脚:旧线会在下发时删掉
|
||||
continue
|
||||
for a, b, net in W:
|
||||
if _on((x, y), a, b):
|
||||
errs.append(u'新线 %s %s-%s 压住已有引脚 %s @(%d,%d)' % (net, a, b, name, x, y))
|
||||
break
|
||||
for des, x, y, rot in PL:
|
||||
for name, ex, ey in geo['pins']:
|
||||
if name.split('.')[0] in ([d for d, _, _, _, _ in OLD_STRAP] + DROP):
|
||||
continue
|
||||
if abs(ex - x) < 22 and abs(ey - y) < 30:
|
||||
errs.append(u'新元件 %s@(%d,%d) 压住 %s' % (des, x, y, name))
|
||||
n = len(W)
|
||||
par = list(range(n))
|
||||
|
||||
def find(i):
|
||||
while par[i] != i:
|
||||
par[i] = par[par[i]]; i = par[i]
|
||||
return i
|
||||
for i in range(n):
|
||||
for j in range(i + 1, n):
|
||||
a1, a2, _ = W[i]; b1, b2, _ = W[j]
|
||||
if _on(a1, b1, b2) or _on(a2, b1, b2) or _on(b1, a1, a2) or _on(b2, a1, a2):
|
||||
ri, rj = find(i), find(j)
|
||||
if ri != rj:
|
||||
par[ri] = rj
|
||||
grp = {}
|
||||
for i in range(n):
|
||||
grp.setdefault(find(i), []).append(i)
|
||||
cnt = {}
|
||||
for idxs in grp.values():
|
||||
names = sorted({W[i][2] for i in idxs})
|
||||
if len(names) > 1:
|
||||
errs.append(u'短路: ' + u' + '.join(names))
|
||||
cnt[names[0]] = cnt.get(names[0], 0) + 1
|
||||
for k, c in cnt.items():
|
||||
if c > 1 and k != 'GND':
|
||||
errs.append(u'网络 %s 被拆成 %d 块' % (k, c))
|
||||
for kind, net, x, y, rot in F:
|
||||
if (x, y) not in ends:
|
||||
errs.append(u'旗 %s @(%d,%d) 不在导线端点(skill §8a-3)' % (net, x, y))
|
||||
for s, x, y, size in TX:
|
||||
x2 = x + _tw(s, size)
|
||||
if x < FRAME[0] or x2 > FRAME[2] or y < FRAME[1] or y > FRAME[3]:
|
||||
errs.append(u'文字越框: %s… 右到 %.0f' % (s[:14], x2))
|
||||
if x2 > TITLE[0] and y < TITLE[3]:
|
||||
errs.append(u'文字压标题栏: %s… 右到 %.0f @y=%d' % (s[:14], x2, y))
|
||||
return errs
|
||||
|
||||
|
||||
def main():
|
||||
push = '--push' in sys.argv
|
||||
geo = json.load(io.open(GEO, encoding='utf-8'))
|
||||
W, F, TX, PL = build()
|
||||
errs = check(W, F, TX, PL, geo)
|
||||
print(u'保留 %d 颗(R900~R904 = PRT_SWP1~5)/ 删 %s' % (len(PL), ','.join(DROP)))
|
||||
print(u'导线 %d 段 / 地旗 %d / 文字 %d' % (len(W), len(F), len(TX)))
|
||||
if errs:
|
||||
print(u'\n检查 %d 条:' % len(errs))
|
||||
for e in errs:
|
||||
print(u' ' + e)
|
||||
raise SystemExit(u'未通过,不下发')
|
||||
print(u'全页碰撞检查通过')
|
||||
if not push:
|
||||
print(u'加 --push 执行')
|
||||
return
|
||||
|
||||
open_page()
|
||||
# 1) 删掉上一版画的所有 strap 线(判据:整条图元的每一段都被旧声明覆盖)
|
||||
oldW = _wires(OLD_STRAP, OLD_BUS)
|
||||
dead = [[a[0], a[1], b[0], b[1]] for a, b, _ in oldW]
|
||||
r = call("""
|
||||
const OLD = %s, tol = 0.6;
|
||||
const MYFLAG = [230, 250, 270, 780, 820]; // 本脚本历次版本放过地旗的 x
|
||||
const on = (p, a, b) => {
|
||||
if (Math.abs(a[0]-b[0]) < tol) return Math.abs(p[0]-a[0]) < tol && p[1] >= Math.min(a[1],b[1])-tol && p[1] <= Math.max(a[1],b[1])+tol;
|
||||
if (Math.abs(a[1]-b[1]) < tol) return Math.abs(p[1]-a[1]) < tol && p[0] >= Math.min(a[0],b[0])-tol && p[0] <= Math.max(a[0],b[0])+tol;
|
||||
return false; };
|
||||
let del = 0;
|
||||
for (const w of await eda.sch_PrimitiveWire.getAll()) {
|
||||
const p = w.getState_Line(); let all = p.length >= 4;
|
||||
for (let i = 0; i + 3 < p.length; i += 4) {
|
||||
const a = [p[i], p[i+1]], b = [p[i+2], p[i+3]];
|
||||
if (!OLD.some(o => on(a, [o[0],o[1]], [o[2],o[3]]) && on(b, [o[0],o[1]], [o[2],o[3]]))) { all = false; break; }
|
||||
}
|
||||
if (all) { await eda.sch_PrimitiveWire.delete(w.getState_PrimitiveId()); del++; }
|
||||
}
|
||||
for (const c of await eda.sch_PrimitiveComponent.getAll()) {
|
||||
const t = c.getState_ComponentType();
|
||||
// ⚠️ 只能删**我自己画的那两个地旗**:第一版写成「y==250 的旗全删」,
|
||||
// 把页面上原有的 `+3V3_HUB`(1130,250)(RESET_N 上拉 R716 的电源端)一起删了,
|
||||
// 导入变更时才以 `R716:2 +3V3_HUB -> $16N210` 暴露出来。
|
||||
if (t === 'netflag' && c.getState_Net() === 'GND' && Math.round(c.getState_Y()) === %d
|
||||
&& MYFLAG.includes(Math.round(c.getState_X()))) { await eda.sch_PrimitiveComponent.delete(c.getState_PrimitiveId()); del++; continue; }
|
||||
if (t === 'part' && %s.includes(c.getState_Designator())) { await eda.sch_PrimitiveComponent.delete(c.getState_PrimitiveId()); del++; }
|
||||
}
|
||||
for (const t of await eda.sch_PrimitiveText.getAll()) await eda.sch_PrimitiveText.delete(t.getState_PrimitiveId());
|
||||
return del;""" % (json.dumps(dead), BUS, json.dumps(DROP)))
|
||||
print(u'已删旧线/旗/元件 %d 项,并清空全部文字' % r)
|
||||
|
||||
# 2) 补齐还缺的电阻(R900~R904 上一版已经放过,这里只补缺)
|
||||
have = call("""
|
||||
const out = [];
|
||||
for (const c of await eda.sch_PrimitiveComponent.getAll())
|
||||
if (c.getState_ComponentType() === 'part') out.push(c.getState_Designator());
|
||||
return out;""")
|
||||
todo = [[d, R100K, x, y, r] for d, x, y, r in PL if d not in have]
|
||||
if todo:
|
||||
call("""
|
||||
for (const [d,uuid,x,y,rot] of %s){
|
||||
await eda.sch_PrimitiveComponent.create({libraryUuid:'%s', uuid}, x, y, undefined, rot, false, true, true);
|
||||
for (const c of await eda.sch_PrimitiveComponent.getAll()){
|
||||
if (c.getState_ComponentType()!=='part') continue;
|
||||
if (Math.round(c.getState_X())!==x || Math.round(c.getState_Y())!==y) continue;
|
||||
await c.toAsync().setState_Designator(d).done();
|
||||
}
|
||||
}
|
||||
return 1;""" % (json.dumps(todo), LIB))
|
||||
print(u'补放 %d 颗(其余 %d 颗上一版已在)' % (len(todo), len(PL) - len(todo)))
|
||||
|
||||
got = call("""
|
||||
const out={};
|
||||
for (const c of await eda.sch_PrimitiveComponent.getAll()){
|
||||
if (c.getState_ComponentType()!=='part') continue;
|
||||
const d=c.getState_Designator();
|
||||
if (!/^R9[0-9][0-9]$/.test(d)) continue;
|
||||
out[d]=(await c.getAllPins()).map(p=>[Math.round(p.getState_X()),Math.round(p.getState_Y())]).sort();
|
||||
}
|
||||
return out;""")
|
||||
if sorted(got) != sorted(d for d, _, _, _ in PL):
|
||||
raise SystemExit(u'板上 R9xx 应为 %s,实测 %s' % (sorted(d for d, _, _, _ in PL), sorted(got)))
|
||||
bad = [u'%s 引脚 %s' % (d, got.get(d)) for d, x, y, r in PL
|
||||
if sorted([[x, RTOP], [x, RBOT]]) != got.get(d)]
|
||||
if bad:
|
||||
raise SystemExit(u'引脚坐标对不上:\n ' + u'\n '.join(bad))
|
||||
print(u'五颗电阻引脚坐标逐个核对通过')
|
||||
|
||||
def batch(items, size, js):
|
||||
for i in range(0, len(items), size):
|
||||
call(js % json.dumps(items[i:i + size], ensure_ascii=False))
|
||||
|
||||
batch([[a[0], a[1], b[0], b[1]] for a, b, _ in W], 40,
|
||||
"for (const s of %s) await eda.sch_PrimitiveWire.create(s); return 1;")
|
||||
batch([list(f) for f in F], 20,
|
||||
"for (const f of %s) await eda.sch_PrimitiveComponent.createNetFlag(f[0],f[1],f[2],f[3],f[4],false); return 1;")
|
||||
batch([list(t) for t in TX], 10,
|
||||
"for (const t of %s) await eda.sch_PrimitiveText.create(t[1],t[2],t[0],0,null,null,t[3],false,false,false,0); return 1;")
|
||||
call("return await eda.sch_Document.save();")
|
||||
print(u'推送完成并存盘')
|
||||
|
||||
|
||||
main()
|
||||
在新工单中引用
屏蔽一个用户