# -*- 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()