# -*- coding: utf-8 -*- u"""C2-HUB:把 U32(USB2517I) 从 `CFG_SEL=000`(strap 使能)改成 **`CFG_SEL=110`(strap 禁用)**。 /c/Python314/python .claude/c2_cfgsel.py # 干跑 + 全页碰撞检查 /c/Python314/python .claude/c2_cfgsel.py --push # 删 + 改 + 重写页面文字 ## 为什么(2026-09-07 用户拍板,取代 c2_straps.py 那一轮) `c2_straps.py` 给 `PRT_SWP1~5` 加的五颗 100k 下拉在**载板上摆不下**: `pcb2_strap_solve.py` 对每颗枚举「位置 × 4 朝向 × 地过孔 × 走线」, R903/R904 有解,而 **R900/R901/R902(= CH1/CH2/CH3 三路)走线 0 解** —— U32 是 QFN-64 9×9 / 0.5 mm 间距,焊盘间只有约 0.2 mm 缝,穿不过 0.20 mm 的线, 唯一出路是垂直出边;而东边紧贴 R710/C750/R709 那一列(x≈106.3), 从 `U32.47` 往东只能走 0.7 mm。解不出来的正好是最关键的三路。 ⇒ 改走 Table 5-2 的另一档:`110` = Internal Default Configuration • **Strap Options are Disabled**。 strap 根本不采样,一颗电阻都不用,U32 拥挤的东/南两侧一点不动。 ## datasheet 依据(DS00001598C,逐条核过原文) - **Table 5-2**(第 15~16 页):`110` 那一行**只有**「Internal Default Configuration • Strap Options are Disabled」—— 没有 `111` 的 Ganged Power/OC Switching, 没有 `100/101` 的 Dynamic Power Switching,没有 `010` 的 Bus Power Operation。 **它是唯一干净的「关 strap」档位。** - **Table 7-1 内部默认值**逐条核过,全是我们要的:`FAh Port Swap=00h`(极性正常)、 `F6h/F7h/F8h Boost=00h`、`09h Non-Removable=00h`、`07h CFG2=20h` (`PORT_PWR=0`/`CURRENT_SNS=00`,而我们 PRTPWR/OCS 一根没接)、 `06h` bit7=1(自供电,正是 §15.1 边界 2 要的)。 - **Table 5-1 + 5-3 的缓冲类型**决定了三个脚各自怎么接: | 脚 | 缓冲类型 | 复用 | 怎么接 | |---|---|---|---| | `CFG_SEL2`(13) | **`I`(纯输入)** | 无(原文还说「某些封装没有这个脚,那时内部按 '0' 处理」)| **直接接 +3V3_HUB,不要电阻** | | `CFG_SEL1`(42) | `I/O12` | `HS_IND`(**输出**)| 必须经 `R712` 10k 上拉,硬接会和 HS_IND 打架 | | `CFG_SEL0`(41) | `I/OSD12` 开漏 | `SCL/SMBCLK` | `R711` 10k 下拉,**不动** | ⇒ 交接里原计划的 `R713`(CFG_SEL2 的 10k 上拉)**不需要**:pin 13 现在就是硬接 GND 的, 改成硬接 +3V3_HUB 完全对称,而且省掉一次「在 U32 西侧找位置 + 布线」。 ## ⚠️ 必须连带去掉 R714/R715 口 6/7 现在是靠 strap 关的(§21.5:两颗 10k 上拉 `USBDN6_DP`/`USBDN7_DP`)。 strap 一停这两个口就开着,而那两颗上拉对 Hub 内部 15k 下拉分压得 `3.3 × 15/25 = 1.98 V`,**正好压在全速设备「已插入」的 2.0 V 判据上** → 两个幽灵口反复枚举失败。去掉之后 D+/D− 悬空、被 Hub 自己的 15k 下拉拉低 = 空闲,无害。 ## BOM 净 −7 颗 `R900~R904`(strap 下拉)+ `R714`/`R715`(口 6/7 禁用上拉)。 `R712` 保留,只是远端从 GND 改 +3V3_HUB。 ## 页面上怎么改 - 右侧那排下拉的地母线原来是 `(920,360)—(1070,360)` 一条,`R712.2` 在 x=970 挂在中间。 拆成两半:`R711` 单独一个地旗 (920,330),`R719/R720` 沿用原来的 (995,330) 那个, `R712.2` 往下引 30 个单位挂 `+3V3_HUB` 电源旗(Power rot=180 = 符号在下、引脚在上)。 - `CFG_SEL2` 那根横线不动,只把左端 (170,540) 的 `GND` 端口删掉重建成 `+3V3_HUB` (§3b:端口只能删了重建,不能改网络名)。 """ 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 FRAME = (20, 10, 1145, 812) TITLE = (455, 10, 1145, 192) # ---- 要删的整条导线图元:按「它的每一段都在这张表里」匹配(EasyEDA 会合并共线段, # 所以只能整条删再重画,不能做外科手术,见 skill §11) DEAD = [ # R904 / R903 的 strap 线 + 左地母线 (270, 360, 320, 360), (270, 310, 270, 360), (270, 250, 270, 270), (310, 250, 270, 250), (310, 250, 310, 270), (310, 340, 320, 340), (310, 310, 310, 340), # R901 / R902 / R900 的 strap 线 + 右地母线 (700, 495, 680, 495), (700, 310, 700, 495), (700, 250, 700, 270), (780, 250, 700, 250), (780, 250, 780, 270), (860, 250, 780, 250), (860, 250, 860, 270), (780, 475, 680, 475), (780, 310, 780, 475), (860, 515, 680, 515), (860, 310, 860, 515), # R714 / R715 及其 +3V3_HUB 母线 (935, 490, 900, 490), (935, 450, 900, 450), (1060, 490, 975, 490), (1060, 490, 1060, 450), (1060, 450, 975, 450), (1060, 490, 1060, 515), # U32 的 USBDN6_DP / USBDN7_DP 到端口那两根 (760, 565, 680, 565), (760, 545, 680, 545), # 右侧下拉的地母线(要拆成两半,整条删掉重画) (1070, 360, 1020, 360), (1020, 360, 995, 360), (995, 330, 995, 360), (970, 360, 995, 360), (920, 360, 970, 360), ] NEW_W = [ (1070, 360, 1020, 360), (1020, 360, 995, 360), (995, 360, 995, 330), # R719/R720 的地 (920, 360, 920, 330), # R711 的地 # ⚠ 旗落在 305 而不是 330:放 330 时 `+3V3_HUB` 标签(宽约 43 单位)跟右边 25 单位外 # (995,330) 的地旗标签恰好相接,而且一个电源旗挂在两个地旗中间看着就像地。 # 截图才看得出来,几何验证四道全过(skill §10 那句「证明不了看着对」)。 (970, 360, 970, 305), # R712 -> +3V3_HUB ] DEL_PARTS = ['R900', 'R901', 'R902', 'R903', 'R904', 'R714', 'R715'] DEL_FLAGS = [('GND', 270, 250), ('GND', 780, 250), ('GND', 995, 330), ('+3V3_HUB', 1060, 515)] DEL_PORTS = [('PRTDIS6', 760, 545), ('PRTDIS7', 760, 565), ('PRTDIS6', 900, 450), ('PRTDIS7', 900, 490), ('GND', 170, 540)] NEW_F = [('Ground', 'GND', 995, 330, 0), ('Ground', 'GND', 920, 330, 0), ('Power', '+3V3_HUB', 970, 305, 180)] NEW_P = [('BI', '+3V3_HUB', 170, 540, 180)] TITLE_TXT = u'【P4B-HUB】板载 USB 2.0 七口 Hub —— 对外只有一根 USB 线(§15.1 / §21)' NOTES = [ u'U32 USB2517I CFG_SEL[2:0]=110 → 内部默认配置 + strap 禁用;寄存器 06h 默认 9Bh、bit7=1 = 自供电', u'(声明成总线供电,主机会按每口 100mA 做预算,而手机描述符要 500mA → 拒绝配置,枚举不上)', u'口 1~5 → CH1~CH5;口 6/7 悬空(strap 停采样后原 R714/R715 上拉分压到 1.98V 变幽灵口,已删)', 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)', ] BOT = [ u'CFG_SEL2(13) 是纯输入(Table 5-3 = I),直接接 +3V3_HUB;CFG_SEL1 复用 HS_IND 输出,必须经 R712 上拉', u'strap 停采样 ⇒ PRT_SWP / BOOST / GANG_EN 全不采样,Table 7-1 默认值即所需(§21.6b)', ] 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 build_text(): TX = [(TITLE_TXT, 45, 800, 15)] for i, t in enumerate(NOTES): TX.append((t, 340, 780 - 18 * i, 11)) TX.append((BOT[0], 45, 222, 10)) TX.append((BOT[1], 45, 205, 10)) return TX def _tw(s, size): n = sum(1 for ch in s if ord(ch) > 0x2000) return size * (n + 0.5 * (len(s) - n)) 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 check(geo): u"""下发前的本地自检。geo = 当前页 dump(parts/flags/ports/wires)。""" errs = [] # 1) 正交、非零长、坐标 5 的倍数(skill §6d / §8a-2b) for a, b, c, e in NEW_W: if (a == c) == (b == e): errs.append(u'新线 %s 不是横平竖直或零长' % ((a, b, c, e),)) for v in (a, b, c, e): if v % 5: errs.append(u'新线坐标 %d 不是 5 的倍数' % v) for it in NEW_F + NEW_P: if it[2] % 5 or it[3] % 5: errs.append(u'旗/端口 %s 坐标不是 5 的倍数' % (it,)) # 2) 旗/端口必须落在导线**端点**上(skill §8a-2 / §8a-3) ends = set() for a, b, c, e in NEW_W: ends.add((a, b)); ends.add((c, e)) dead = set(DEAD) | {(c, e, a, b) for a, b, c, e in DEAD} for w in geo['wires']: segs = [(w[i], w[i + 1], w[i + 2], w[i + 3]) for i in range(0, len(w) - 3, 4)] if all(s in dead for s in segs): continue # 整条会被删掉 for a, b, c, e in segs: ends.add((a, b)); ends.add((c, e)) for kind, net, x, y, rot in NEW_F: if (x, y) not in ends: errs.append(u'旗 %s@(%d,%d) 不在任何导线端点(skill §8a-3)' % (net, x, y)) for d, net, x, y, rot in NEW_P: if (x, y) not in ends: errs.append(u'端口 %s@(%d,%d) 不在任何导线端点' % (net, x, y)) # 3) 新线不能从任何**留下来的**元件引脚**中间穿过去**(被删元件的脚不算)。 # 终止在引脚上是正常的(那正是连接),压在线段**中段**才是错 —— 那会把不相干的 # 引脚串进这个网络,而且 verify_sch 的口径是「落在某条线段上」,它抓不到。 gone = set(DEL_PARTS) for des, x, y, rot, pins in geo['parts']: if des in gone: continue for nm, px, py in pins: for a, b, c, e in NEW_W: if (px, py) in ((a, b), (c, e)): continue # 端点 = 正常连接 if _on((px, py), (a, b), (c, e)): errs.append(u'新线 %s 从 %s.%s@(%d,%d) 中间穿过' % ((a, b, c, e), des, nm, px, py)) # 4) 新旗/端口的净空:符号连文字按 30 单位算(skill §12a) occupied = [] for des, x, y, rot, pins in geo['parts']: if des not in gone: occupied.append((des, x, y)) for net, x, y, rot in geo['flags']: if (net, x, y) not in DEL_FLAGS: occupied.append((u'旗' + net, x, y)) for it in NEW_F + NEW_P: x, y = it[2], it[3] for nm, ox, oy in occupied: if abs(ox - x) < 25 and abs(oy - y) < 30: errs.append(u'新旗/端口 @(%d,%d) 离 %s@(%d,%d) 太近' % (x, y, nm, ox, oy)) # 5) 文字越框 / 压标题栏 for s, x, y, size in build_text(): 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[:16], x2)) if x2 > TITLE[0] and y < TITLE[3]: errs.append(u'文字压标题栏: %s… 右到 %.0f @y=%d' % (s[:16], x2, y)) return errs DUMP_JS = """ const parts=[],flags=[],ports=[],wires=[]; for(const c of await eda.sch_PrimitiveComponent.getAll()){ const t=c.getState_ComponentType(); const x=Math.round(c.getState_X()), y=Math.round(c.getState_Y()); if(t==='part'){ const pins=(await c.getAllPins()).map(p=>[p.getState_PinName?p.getState_PinName():'', Math.round(p.getState_X()),Math.round(p.getState_Y())]); parts.push([c.getState_Designator(),x,y,Math.round(c.getState_Rotation()),pins]); } else if(t==='netflag') flags.push([c.getState_Net(),x,y,Math.round(c.getState_Rotation())]); else if(t==='netport') ports.push([c.getState_Net(),x,y,Math.round(c.getState_Rotation()), c.getState_Component().name]); } for(const w of await eda.sch_PrimitiveWire.getAll()) wires.push(w.getState_Line().map(v=>Math.round(v))); return {parts,flags,ports,wires};""" def dump(): open_page() return call(DUMP_JS) def main(): push = '--push' in sys.argv geo = dump() print(u'当前页:元件 %d / 旗 %d / 端口 %d / 导线图元 %d' % (len(geo['parts']), len(geo['flags']), len(geo['ports']), len(geo['wires']))) errs = check(geo) print(u'\n计划:删元件 %d / 删旗 %d / 删端口 %d / 删线段 %d;' u'新线 %d / 新旗 %d / 新端口 %d / 文字 %d' % (len(DEL_PARTS), len(DEL_FLAGS), len(DEL_PORTS), len(DEAD), len(NEW_W), len(NEW_F), len(NEW_P), len(build_text()))) 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'\n加 --push 执行') return r = call(""" const DEAD=%s, tol=0.6; const on=(p,a,b)=>{ if(Math.abs(a[0]-b[0])=Math.min(a[1],b[1])-tol && p[1]<=Math.max(a[1],b[1])+tol; if(Math.abs(a[1]-b[1])=Math.min(a[0],b[0])-tol && p[0]<=Math.max(a[0],b[0])+tol; return false;}; let dw=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+3on(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()); dw++;} } const PARTS=%s, FLAGS=%s, PORTS=%s; let dp=0,df=0,dq=0; for(const c of await eda.sch_PrimitiveComponent.getAll()){ const t=c.getState_ComponentType(); const x=Math.round(c.getState_X()), y=Math.round(c.getState_Y()); if(t==='part'){ if(PARTS.includes(c.getState_Designator())){ await eda.sch_PrimitiveComponent.delete(c.getState_PrimitiveId()); dp++;} continue;} const n=c.getState_Net(); if(t==='netflag' && FLAGS.some(f=>f[0]===n&&f[1]===x&&f[2]===y)){ await eda.sch_PrimitiveComponent.delete(c.getState_PrimitiveId()); df++; continue;} if(t==='netport' && PORTS.some(f=>f[0]===n&&f[1]===x&&f[2]===y)){ await eda.sch_PrimitiveComponent.delete(c.getState_PrimitiveId()); dq++;} } for(const t of await eda.sch_PrimitiveText.getAll()) await eda.sch_PrimitiveText.delete(t.getState_PrimitiveId()); return {dw,dp,df,dq};""" % (json.dumps([list(x) for x in DEAD]), json.dumps(DEL_PARTS), json.dumps([list(x) for x in DEL_FLAGS]), json.dumps([list(x) for x in DEL_PORTS]))) print(u'\n已删:导线图元 %d / 元件 %d / 旗 %d / 端口 %d(文字全清)' % (r['dw'], r['dp'], r['df'], r['dq'])) if r['dp'] != len(DEL_PARTS): raise SystemExit(u'元件删除数对不上:期望 %d 实删 %d' % (len(DEL_PARTS), r['dp'])) if r['df'] != len(DEL_FLAGS) or r['dq'] != len(DEL_PORTS): raise SystemExit(u'旗/端口删除数对不上:旗 %d/%d 端口 %d/%d' % (r['df'], len(DEL_FLAGS), r['dq'], len(DEL_PORTS))) 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([list(w) for w in NEW_W], 40, "for (const s of %s) await eda.sch_PrimitiveWire.create(s); return 1;") batch([list(f) for f in NEW_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(p) for p in NEW_P], 20, "for (const p of %s) await eda.sch_PrimitiveComponent.createNetPort(p[0],p[1],p[2],p[3],p[4],false); return 1;") batch([list(t) for t in build_text()], 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();") # 回查(skill §9ad:save() 的返回值不算数,要重新数图元) g2 = call(DUMP_JS) print(u'落盘后回查:元件 %d / 旗 %d / 端口 %d / 导线图元 %d' % (len(g2['parts']), len(g2['flags']), len(g2['ports']), len(g2['wires']))) left = [p[0] for p in g2['parts'] if p[0] in DEL_PARTS] if left: raise SystemExit(u'这些元件没删掉:%s' % left) got = {(f[0], f[1], f[2]) for f in g2['flags']} for k, n, x, y, r_ in NEW_F: if (n, x, y) not in got: raise SystemExit(u'新旗 %s@(%d,%d) 没建出来' % (n, x, y)) gotp = {(p[0], p[1], p[2]) for p in g2['ports']} for d, n, x, y, r_ in NEW_P: if (n, x, y) not in gotp: raise SystemExit(u'新端口 %s@(%d,%d) 没建出来' % (n, x, y)) print(u'推送完成并存盘,回查通过') main()