原理图/PCB 已在 EasyEDA 里做到 12 页跨页 0 漏接,固件刚完成骨架包 A1。 这次是把已有的一切一次性纳入版本控制,没有改动任何设计内容。 纳入: - 设计与需求文档、PROGRESS.md、小米8 参考原理图 PDF 与追脚脚本 - .claude/ 下的 EasyEDA 桥接工具链与几何/网表缓存(eda-parts.json、 pcb-parts.json、pcb-pads.json、p2-ch1.json 等,是重画各页的唯一事实来源) - 117 张验证截图(本项目所有方向问题都是靠人眼看截图发现的,属于证据不是垃圾) - firmware/ 的 A1 骨架与 site/ 文档站源码 .gitignore 排除:构建产物(firmware/build、host/build、sdkconfig)、 site/node_modules、__pycache__、桥接日志 _bridge.log、脚本自标的 *.stale。 .gitattributes 默认 `* -text`:现有 .py/.json 大多是 CRLF,原样存, 不让 git 重写行尾。唯一例外是 *.sh 强制 LF —— 带 \r 的 shell 脚本 bash 跑不了。 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QYuExfgyYa6DzRRcUso8M8
226 行
11 KiB
Python
226 行
11 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""载板 C5-AUX:五路 USB 的 ESD + CC 上拉 Rp + 口温电阻(方案 D,§13.5)。
|
||
|
||
原厂尾插小板上本来就有的三样东西,方案 D 不装那块小板,所以在载板上补回来:
|
||
|
||
U51~U55 USBLC6-2SC6 D± 的 ESD(顺带钳 VBUS)
|
||
R761~765 56 kΩ CC1 -> VBUS 的 Rp(Type-C Default USB 档,§23.3 有分压核算)
|
||
R771~775 100 kΩ 口温 pin38 -> GND
|
||
|
||
⚠ 100k 这个值是**从主板图纸反推**的(2026-09-04,§13.5):
|
||
主板上 USB_CONN_THERM 是 SMB1355 的 BIAS(1) --[R640 20k 1%]-- TSKIN(7) 这个分压网络,
|
||
下臂就是小板上的 NTC。分压灵敏度在 R_ntc = 20k 时最高:
|
||
10k NTC(B=3435) 在 8°C 才到 20k -> 不可能(过温传感器不会把灵敏区放在 8 度)
|
||
100k NTC(B=4250) 在 63°C 到 20k -> 正好是接口过温的跳变区 ✔
|
||
而且 100k/B=4250(104EF/104FB)本来就是高通的标准热敏件。
|
||
我们放**固定**电阻:100k -> 手机恒读约 25°C;放 10k 会读成 60°C 以上,可能触发热限制。
|
||
|
||
为什么单独一页:C5-J1401A/B 上五颗 52 脚座子已经把版面占满,
|
||
C3-USB 的五列只有 215 单位宽、下方又是标题栏,都塞不下。
|
||
|
||
⚠ U51~U55 的 1/6 是同一个 I/O1、3/4 是同一个 I/O2(芯片内部短接),
|
||
而 EasyEDA 会在引脚处截断导线,所以 D± 在网表里天然是两段:
|
||
USBn_DP / USBn_DM = Hub 侧(C2-HUB 过来)
|
||
USBn_DPJ / USBn_DMJ = 座子侧(C5-J1401 过去)
|
||
PCB 上必须让走线从一个焊盘进、另一个焊盘出,不能只搭一个脚。
|
||
|
||
用法: /c/Python314/python .claude/c5_aux.py [--push]
|
||
"""
|
||
import json
|
||
import os
|
||
import sys
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
sys.path.insert(0, HERE)
|
||
from eda import run
|
||
|
||
PAGE = 'b1ffbaef65ef82c9' # C5-AUX
|
||
LIB = '0819f05c4eef4c71ace90d822a990e87'
|
||
P = {
|
||
'ESD': '1e5e214711544da8ba71fb2137ae703f', # C2687116 USBLC6-2SC6
|
||
'R56K': '0d85e42011ec4910b1749e936b037dd7', # C25796 56k 0402
|
||
'R100K': 'acc294b6661948399f609b41c77ed19f', # C25741 100k 0402
|
||
}
|
||
|
||
# 五个位排成 2+2+1 三排。一开始排成上三下二(间距 350),结果**相邻两位的端口标签叠在一起**
|
||
# —— 端口图元的文字比想象中宽('USB1_DMJ' 这种 8 字名字实际占 90 单位左右,不是 60)。
|
||
# 拉到 500 的横向间距就干净了。最下面一排的 y 不能再低 —— 图纸右下角是标题栏(实测占 x>=464 / y<=185,
|
||
# 不是原先估的 161;截图上文字压到标题栏就是这么发现的)。
|
||
SITES = [(1, 300, 720), (2, 800, 720), (3, 300, 520), (4, 800, 520), (5, 300, 320)]
|
||
|
||
|
||
def call(js, timeout=180):
|
||
r = run(js, timeout=timeout)
|
||
if not r.get('success'):
|
||
raise RuntimeError(json.dumps(r, ensure_ascii=False)[:900])
|
||
return r['result']
|
||
|
||
|
||
def desig(n):
|
||
return {'u': 'U5%d' % n, 'rcc': 'R76%d' % n, 'rth': 'R77%d' % n}
|
||
|
||
|
||
def placement():
|
||
out = []
|
||
for n, bx, by in SITES:
|
||
d = desig(n)
|
||
out.append([d['u'], P['ESD'], bx, by, 0])
|
||
out.append([d['rcc'], P['R56K'], bx, by - 80, 0])
|
||
out.append([d['rth'], P['R100K'], bx, by - 130, 0])
|
||
return out
|
||
|
||
|
||
def wiring(pins):
|
||
W, F, PT, TX = [], [], [], []
|
||
|
||
def w(*p):
|
||
for i in range(0, len(p) - 2, 2):
|
||
W.append([p[i], p[i + 1], p[i + 2], p[i + 3]])
|
||
|
||
for n, bx, by in SITES:
|
||
d = desig(n)
|
||
u = pins[d['u']]
|
||
io1a, io1b = u['1'], u['6'] # I/O1 -> DM
|
||
io2a, io2b = u['3'], u['4'] # I/O2 -> DP
|
||
ug, uv = u['2'], u['5']
|
||
assert io1a[1] == io1b[1] == by + 10, u'CH%d 的 I/O1 不在 y=%d' % (n, by + 10)
|
||
assert io2a[1] == io2b[1] == by - 10, u'CH%d 的 I/O2 不在 y=%d' % (n, by - 10)
|
||
|
||
# ---- D± 穿过 ESD:左边是座子侧(_J),右边是 Hub 侧 ----
|
||
w(bx - 105, by + 10, io1a[0], by + 10)
|
||
PT.append(['BI', 'USB%d_DMJ' % n, bx - 105, by + 10, 180])
|
||
w(bx - 105, by - 10, io2a[0], by - 10)
|
||
PT.append(['BI', 'USB%d_DPJ' % n, bx - 105, by - 10, 180])
|
||
w(io1b[0], by + 10, bx + 105, by + 10)
|
||
PT.append(['BI', 'USB%d_DM' % n, bx + 105, by + 10, 0])
|
||
w(io2b[0], by - 10, bx + 105, by - 10)
|
||
PT.append(['BI', 'USB%d_DP' % n, bx + 105, by - 10, 0])
|
||
|
||
# GND(2) 和 VBUS(5) 都夹在两个 I/O 中间,往哪边走都要跨过一条 D 线;
|
||
# 全页就这五处交叉,都是纯交叉(无结点)= 不相连,原理图的常规画法。
|
||
w(ug[0], ug[1], bx - 75, ug[1], bx - 75, by - 45)
|
||
F.append(['Ground', 'GND', bx - 75, by - 45, 0])
|
||
# VBUS(5) 不单独出端口 —— 往下搭到 56k 那一端的 VBUSn 节点上,
|
||
# 否则它的端口图元会和右边 D± 的两个端口挤成一团。
|
||
w(uv[0], uv[1], bx + 75, uv[1], bx + 75, uv[1] - 80)
|
||
|
||
# ---- 56k:CC1 -> VBUS ----
|
||
r = pins[d['rcc']]
|
||
ra = min(r.values(), key=lambda v: v[0])
|
||
rb = max(r.values(), key=lambda v: v[0])
|
||
w(bx - 80, ra[1], ra[0], ra[1])
|
||
PT.append(['BI', 'CC1_%d' % n, bx - 80, ra[1], 180])
|
||
w(rb[0], rb[1], bx + 80, rb[1])
|
||
PT.append(['BI', 'VBUS%d' % n, bx + 80, rb[1], 0])
|
||
|
||
# ---- 10k:口温 -> GND ----
|
||
r2 = pins[d['rth']]
|
||
r2a = min(r2.values(), key=lambda v: v[0])
|
||
r2b = max(r2.values(), key=lambda v: v[0])
|
||
w(bx - 80, r2a[1], r2a[0], r2a[1])
|
||
PT.append(['BI', 'THERM_%d' % n, bx - 80, r2a[1], 180])
|
||
w(r2b[0], r2b[1], bx + 60, r2b[1])
|
||
F.append(['Ground', 'GND', bx + 60, r2b[1], 0])
|
||
|
||
TX.append((bx - 140, by + 45, u'CH%d' % n, 12))
|
||
|
||
TX += [
|
||
(45, 806, u'【载板 C5-AUX】五路 USB 的 ESD + CC 上拉 + 口温电阻 —— 原厂尾插小板上本来就有的三样,方案 D 补回来', 14),
|
||
(45, 786, u'D± 从 C2-HUB 过来(USBn_DP/DM)→ 穿过 U5n → 去 C5-J1401 的座子(USBn_DPJ/DMJ)。VBUSn 来自 C3-USB 的负载开关。', 10),
|
||
|
||
(640, 340, u'R761~765 56k:CC1 → VBUS 的 Rp。整条链路里已经没有 USB-C 连接器了,', 10),
|
||
(640, 324, u'没人提供 Rp,手机看不到 Rp 就只充电不枚举。56k = Default USB 档;', 10),
|
||
(640, 308, u'配手机侧 Rd 5.1k 分压 0.32~0.52V,落在 UFP 的 vRd-USB 窗口', 10),
|
||
(640, 292, u'0.25~0.61V 内,两端各有 ≥70mV 余量(§23.3)。', 10),
|
||
(640, 268, u'⚠ R771~775 100k:从主板图纸反推(BIAS-[R640 20k]-TSKIN 分压,下臂是 NTC)。', 10),
|
||
(640, 244, u'⚠ U5n 的 1/6 是同一个 I/O1、3/4 是同一个 I/O2(芯片内部短接),而 EasyEDA', 10),
|
||
(640, 228, u' 会在引脚处截断导线,所以 D± 在网表里天然是两段(_P Hub 侧 / _J 座子侧)。', 10),
|
||
(640, 212, u' PCB 上走线必须从一个焊盘进、另一个焊盘出(串在线中),不能只搭一个脚;', 10),
|
||
(640, 196, u' 这颗不允许不贴,不贴就断线。', 10),
|
||
]
|
||
|
||
# 下发前的几何断言(skill §6c / §8a-2b)
|
||
for a, b, c, e in W:
|
||
assert (a == c) != (b == e), u'有一段导线不是横平竖直或零长度: %s' % ((a, b, c, e),)
|
||
for v in (a, b, c, e):
|
||
assert v % 5 == 0, u'导线坐标 %d 不是 5 的倍数: %s' % (v, (a, b, c, e))
|
||
for lst in (F, PT):
|
||
for it in lst:
|
||
assert it[2] % 5 == 0 and it[3] % 5 == 0, \
|
||
u'%s 坐标 (%d,%d) 不是 5 的倍数' % (it[1], it[2], it[3])
|
||
return W, F, PT, TX
|
||
|
||
|
||
def main():
|
||
push = '--push' in sys.argv
|
||
place = placement()
|
||
print(u'C5-AUX: %d 个位,元件 %d 个' % (len(SITES), len(place)))
|
||
if not push:
|
||
print(u'(未推送,加 --push)')
|
||
return
|
||
|
||
call("""
|
||
await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r=>setTimeout(r,1200));
|
||
const n=(await eda.dmt_Schematic.getCurrentSchematicPageInfo()).name;
|
||
if (n!=='C5-AUX') throw new Error('page='+n);
|
||
for (const c of await eda.sch_PrimitiveComponent.getAll()){
|
||
if (c.getState_ComponentType()==='sheet') continue;
|
||
await eda.sch_PrimitiveComponent.delete(c.getState_PrimitiveId());
|
||
}
|
||
for (const w of await eda.sch_PrimitiveWire.getAll()) await eda.sch_PrimitiveWire.delete(w.getState_PrimitiveId());
|
||
for (const t of await eda.sch_PrimitiveText.getAll()) await eda.sch_PrimitiveText.delete(t.getState_PrimitiveId());
|
||
return 1;
|
||
""" % PAGE)
|
||
|
||
for i in range(0, len(place), 5):
|
||
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(place[i:i + 5]), LIB))
|
||
|
||
raw = call("""
|
||
const out={};
|
||
for (const c of await eda.sch_PrimitiveComponent.getAll()){
|
||
if (c.getState_ComponentType()!=='part') continue;
|
||
out[c.getState_Designator()] = (await c.getAllPins()).map(
|
||
p=>[p.getState_PinNumber(), (p.getState_PinName?p.getState_PinName():''),
|
||
Math.round(p.getState_X()), Math.round(p.getState_Y())]);
|
||
}
|
||
return out;
|
||
""")
|
||
pins = {k: {p: (x, y) for p, _n, x, y in v} for k, v in raw.items()}
|
||
pnames = {k: {p: _n for p, _n, x, y in v} for k, v in raw.items()}
|
||
want = {'1': 'I/O1', '2': 'GND', '3': 'I/O2', '4': 'I/O2', '5': 'VBUS', '6': 'I/O1'}
|
||
bad = []
|
||
for n, _bx, _by in SITES:
|
||
got = pnames[desig(n)['u']]
|
||
for k, v in want.items():
|
||
if got.get(k, '').upper() != v.upper():
|
||
bad.append(u'%s.%s 期望 %s 实测 %s' % (desig(n)['u'], k, v, got.get(k)))
|
||
if bad:
|
||
raise SystemExit(u'引脚核对失败:\n ' + '\n '.join(bad))
|
||
|
||
W, F, PT, TX = wiring(pins)
|
||
|
||
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(W, 40, "for (const s of %s) await eda.sch_PrimitiveWire.create(s); return 1;")
|
||
batch(PT, 25, "for (const p of %s) await eda.sch_PrimitiveComponent.createNetPort(p[0],p[1],p[2],p[3],p[4],false); return 1;")
|
||
batch(F, 25, "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], 12,
|
||
"for (const t of %s) await eda.sch_PrimitiveText.create(t[0],t[1],t[2],0,null,null,t[3],false,false,false,0); return 1;")
|
||
print(u'C5-AUX: 导线 %d 旗 %d 端口 %d 文字 %d' % (len(W), len(F), len(PT), len(TX)))
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|