356 行
18 KiB
Python
356 行
18 KiB
Python
# -*- 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])<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 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+3<p.length;i+=4){
|
||
const a=[p[i],p[i+1]], b=[p[i+2],p[i+3]];
|
||
if(!DEAD.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()); 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()
|