原理图/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
267 行
14 KiB
Python
267 行
14 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""载板 C1-CONN:板对板排母 J31/J32 + I2C 温度芯片 U34。
|
||
|
||
用法: /c/Python314/python .claude/c1_conn.py [--push]
|
||
|
||
## 编号(2026-09-05 换件后统一,见设计文档 §22.3f)
|
||
|
||
下板公头 `C55020601` 与载板母座 `C5307344` **是同一套编号**:
|
||
第 r 行 = 引脚 2r−1(左列)/ 2r(右列),第 1 行在元件中心上方 95 个单位。
|
||
(换件前公头是 DIP 式 `r / 41−r`,两套编号对不上,那个陷阱已经消失。)
|
||
|
||
本脚本直接复用 p5_btb.py 里那两张按行组织的表,逐行生成,不做任何手工转写。
|
||
|
||
⚠️ 但 PCB 上那条还在:符号编号一致 ≠ 第 r 行真对上第 r 行 ——
|
||
公头在下板顶层、母座在载板底层,底层件在顶视图里是镜像的。
|
||
以 `pcb2-header-pads.json` 导出的 80 个坐标为准。
|
||
"""
|
||
import json
|
||
import os
|
||
import sys
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
sys.path.insert(0, HERE)
|
||
from eda import run
|
||
from p5_btb import J24, J25 # 逐行的 (左列网络, 右列网络),唯一事实来源
|
||
|
||
PAGE = '1cc8af8cba7ec7b2' # C1-CONN
|
||
LIB = '0819f05c4eef4c71ace90d822a990e87'
|
||
|
||
P = {
|
||
'HDRF': '786346fbe2e94eaf9cd217eca540dc4b', # C5307344 PC104-2*20(塑高 10.5,直插)
|
||
'LM75': '16898f8cfee9460099b731d841af8f30', # C34565 LM75BD,118
|
||
'C100N': '87bb635d0b2f489a9f60e7cd225beb3c', # C1525 100nF 0402
|
||
'LDO': '9f9c6cb41c7449fd8acf96aceed2661a', # C6186 AMS1117-3.3 (U31)
|
||
'C10U': '6e5726223dd84f70bc3b626fc7d1f72c', # C440198 10uF/50V 0805
|
||
'C22U': '0de9d28bbcd34ca3bf2a4d78faaa50bb', # C12891 22uF/25V 1206
|
||
}
|
||
|
||
# 载板侧的两条排母,和下板 J24 / J25 一一对插
|
||
CONNS = [('J31', 'J24', 300, 500, J24), ('J32', 'J25', 800, 500, J25)]
|
||
|
||
STUB = 40
|
||
ROW0 = 95 # 第 1 行相对元件中心的 y 偏移(实测;换件后公母两颗都是 95)
|
||
DY = 10
|
||
|
||
U34 = (600, 300) # LM75BD 中心
|
||
# U31 从下板 P1B-5V 搬过来:载板上唯一的 3.3V 来源(§9.5)。
|
||
# 放在 C1-CONN 而不是 C2-HUB,是因为 +5V 从排针进来就在这一页;
|
||
# §9.5 要求的「就近 Hub 供电」是 PCB 摆位约束,和原理图分页无关。
|
||
U31 = (250, 330)
|
||
C736, C737 = (165, 300), (400, 315)
|
||
C760 = (760, 295) # 0402 引脚跨距是 ±20(实测),放 295 才让上脚正好落在 315
|
||
|
||
NC = {'U34': ['3']} # OS 过温输出不用
|
||
|
||
|
||
def call(js, timeout=180):
|
||
r = run(js, timeout=timeout)
|
||
if not r.get('success'):
|
||
raise RuntimeError(json.dumps(r, ensure_ascii=False)[:800])
|
||
return r['result']
|
||
|
||
|
||
def plan():
|
||
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]])
|
||
|
||
# ⚠️ 2026-09-05 起 GND 也走 netport,不再画地母线 —— 逐针表按几何重排之后
|
||
# GND 散在两列的若干段,每段配母线 + 地旗的话旗会掉进相邻行的端口里
|
||
# (行距 10,旗高 15~20,skill §7)。成因与新分配见 p5_btb.py 的表头 / §25.10。
|
||
for desig, mate, cx, cy, rows in CONNS:
|
||
assert len(rows) == 20, u'%s 必须正好 20 行' % desig
|
||
lx, rx = cx - 30, cx + 30
|
||
lp, rp = lx - STUB, rx + STUB
|
||
for i, (nnet, snet) in enumerate(rows):
|
||
y = cy + ROW0 - DY * i
|
||
w(lx, y, lp, y)
|
||
PT.append(['BI', nnet, lp, y, 180])
|
||
w(rx, y, rp, y)
|
||
PT.append(['BI', snet, rp, y, 0])
|
||
TX.append((cx - 60, cy + ROW0 + 45,
|
||
u'%s(对插下板 %s)北排(奇)↑ / 南排(偶)↓' % (desig, mate), 11))
|
||
|
||
# ---- U34 LM75BD:1=SDA 2=SCL 3=OS 4=GND 5=A2 6=A1 7=A0 8=VCC ----
|
||
ux, uy = U34
|
||
w(ux - 40, uy + 15, ux - 80, uy + 15)
|
||
PT.append(['BI', 'SDA', ux - 80, uy + 15, 180])
|
||
w(ux - 40, uy + 5, ux - 80, uy + 5)
|
||
PT.append(['BI', 'SCL', ux - 80, uy + 5, 180])
|
||
w(ux - 40, uy - 15, ux - 80, uy - 15, ux - 80, uy - 45) # 4 GND
|
||
F.append(['Ground', 'GND', ux - 80, uy - 45, 0])
|
||
# A0/A1/A2 全部接地 -> 0x48
|
||
for dy in (5, -5, -15):
|
||
w(ux + 40, uy + dy, ux + 80, uy + dy)
|
||
w(ux + 80, uy + 5, ux + 80, uy - 45)
|
||
F.append(['Ground', 'GND', ux + 80, uy - 45, 0])
|
||
# 8 VCC -> +3V3_HUB,顺路接 C760
|
||
w(ux + 40, uy + 15, C760[0], uy + 15)
|
||
w(ux + 100, uy + 15, ux + 100, uy + 45)
|
||
F.append(['Power', '+3V3_HUB', ux + 100, uy + 45, 0])
|
||
w(C760[0], C760[1] - 20, C760[0], uy - 45)
|
||
F.append(['Ground', 'GND', C760[0], uy - 45, 0])
|
||
|
||
# ---- U31 AMS1117-3.3:1=GND 2=VOUT 3=VIN 4=VOUT(散热片) ----
|
||
w(205, 320, 120, 320) # 3 VIN
|
||
PT.append(['BI', '+5V', 120, 320, 180])
|
||
w(165, 280, 165, 255) # C736 下极 -> GND(上极搭在 VIN 那条线上)
|
||
F.append(['Ground', 'GND', 165, 255, 0])
|
||
# ⚠ 这里原来写的是 362:旗的位置被吸附到 360,导线端点却留在 362,
|
||
# 旗的引脚就落在导线**中段** —— 渲染正常、verify_sch 也过,但 J31 pin1 的地
|
||
# 实际是断的(port_audit 抓到的)。坐标必须落在 5 的倍数上,见下面的断言。
|
||
w(205, 340, 175, 340, 175, 360) # 1 GND(旗压得低一点,别顶到 J31 的 HUB_RST 标签)
|
||
F.append(['Ground', 'GND', 175, 360, 180])
|
||
w(205, 330, 145, 330, 145, 375) # 2 VOUT
|
||
PT.append(['BI', '+3V3_HUB', 145, 375, 180])
|
||
w(295, 330, 430, 330, 430, 355, 460, 355) # 4 VOUT(散热片),C737 搭在中途
|
||
PT.append(['BI', '+3V3_HUB', 460, 355, 0])
|
||
w(400, 300, 400, 275) # C737 下极 -> GND
|
||
F.append(['Ground', 'GND', 400, 275, 0])
|
||
|
||
TX += [
|
||
(45, 806, u'【载板 C1-CONN】板对板排母 + 5V→3.3V + I2C 温度芯片', 14),
|
||
(45, 784, u'J31 / J32 是 2×20 排母,分别对插下板的 J24 / J25。逐针定义见设计文档 §22.2。', 10),
|
||
(45, 764, u'编号:第 r 行 = 引脚 2r−1(北排,左)/ 2r(南排,右)—— 公头 C55020601 与母座 C5307344 同一套(§22.3f 实测)。', 10),
|
||
(45, 748, u'分配按载板几何排(§25.10):北排喂北边的站、南排喂南边的站;J31 正对 site1/site4,J32 正对 site3/site5,site2 取 J31 北排最右三针。', 10),
|
||
(45, 732, u'母座 C5307344(PC104-2*20):塑高 10.5、直插、行距 2.54、2.5A 每针;', 10),
|
||
(45, 716, u'配下板公头(塑高 2.5 / 配合针长 6)对插 = 13.0mm 板间距,由塑料对塑料定位。', 10),
|
||
(45, 694, u'⚠ PCB 上仍要实打实核一次「第 r 行对第 r 行」:母座在载板底层,顶视图里是镜像的。', 10),
|
||
(45, 678, u'⚠ 到货核两件:触头镀层(商城只写「黄铜」)、对插后板间距是否真的 13.0mm。', 10),
|
||
(45, 205, u'U34 LM75BD:I2C 温度传感器,A0/A1/A2 全接地 → 地址 0x48(§8.2 里 0x48 是空的)。', 10),
|
||
(45, 189, u'它测的是载板温度(手机底下),下板那两路 NTC 测的是功率级 —— 风扇反馈要用这一颗。', 10),
|
||
(45, 173, u'供电取 +3V3_HUB(U31 就在本页右下)。排针上没有 3.3V,载板的 3.3V 只有这一个来源。', 10),
|
||
(45, 157, u'OS(过温输出,pin3)不接。', 10),
|
||
(800, 350, u'U31 AMS1117-3.3(从下板 P1B-5V 搬过来):', 10),
|
||
(800, 334, u'载板上唯一的 3.3V 来源 —— U32 Hub、U34、', 10),
|
||
(800, 318, u'五路 EDL 模拟开关都吃它。Hub 满配约 420mA,', 10),
|
||
(800, 302, u'耗散 0.42A×1.7V≈0.71W:SOT-223 要铺铜散热,', 10),
|
||
(800, 286, u'且必须摆在 U32 旁边(§9.5 / §10 就近供电)。', 10),
|
||
]
|
||
# ---- 下发前的几何断言(skill §6c:几何量必须读回,且坐标吸附到 5 的倍数)----
|
||
# 旗/端口的坐标会被吸附到 5 的倍数,导线端点不会。两边对不齐 2 个单位,
|
||
# 旗就落在导线中段 = 真断线,而 verify_sch 和 DRC 都看不出来。
|
||
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 kind, lst in (('flag', F), ('port', PT)):
|
||
for it in lst:
|
||
x, y = it[2], it[3]
|
||
assert x % 5 == 0 and y % 5 == 0, u'%s %s 的坐标 (%d,%d) 不是 5 的倍数' % (kind, it[1], x, y)
|
||
return W, F, PT, TX
|
||
|
||
|
||
def main():
|
||
push = '--push' in sys.argv
|
||
W, F, PT, TX = plan()
|
||
print(u'导线 %d 旗 %d 端口 %d 文字 %d' % (len(W), len(F), len(PT), len(TX)))
|
||
if not push:
|
||
return
|
||
|
||
call("""
|
||
await eda.dmt_EditorControl.openDocument('%s'); await new Promise(r=>setTimeout(r,900));
|
||
const n=(await eda.dmt_Schematic.getCurrentSchematicPageInfo()).name;
|
||
if (n!=='C1-CONN') 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)
|
||
print(u'C1-CONN 已清空')
|
||
|
||
place = [[d, P['HDRF'], cx, cy, 0] for d, _, cx, cy, _ in CONNS]
|
||
place.append(['U34', P['LM75'], U34[0], U34[1], 0])
|
||
place.append(['C760', P['C100N'], C760[0], C760[1], 270])
|
||
place.append(['U31', P['LDO'], U31[0], U31[1], 0])
|
||
place.append(['C736', P['C10U'], C736[0], C736[1], 270])
|
||
place.append(['C737', P['C22U'], C737[0], C737[1], 270])
|
||
for i in range(0, len(place), 4):
|
||
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 + 4]), LIB))
|
||
print(u'已放置 %d 个元件' % len(place))
|
||
|
||
pins = 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;
|
||
""")
|
||
bad = []
|
||
for desig, _, cx, cy, _ in CONNS:
|
||
pm = {p: (x, y) for p, _n, x, y in pins[desig]}
|
||
for r in range(20):
|
||
y = cy + ROW0 - DY * r
|
||
if pm.get(str(2 * r + 1)) != (cx - 30, y):
|
||
bad.append(u'%s 第%d行左 = pin%d,期望 %s 实测 %s'
|
||
% (desig, r + 1, 2 * r + 1, (cx - 30, y), pm.get(str(2 * r + 1))))
|
||
if pm.get(str(2 * r + 2)) != (cx + 30, y):
|
||
bad.append(u'%s 第%d行右 = pin%d,期望 %s 实测 %s'
|
||
% (desig, r + 1, 2 * r + 2, (cx + 30, y), pm.get(str(2 * r + 2))))
|
||
want34 = {'1': (U34[0] - 40, U34[1] + 15), '2': (U34[0] - 40, U34[1] + 5),
|
||
'4': (U34[0] - 40, U34[1] - 15), '8': (U34[0] + 40, U34[1] + 15),
|
||
'7': (U34[0] + 40, U34[1] + 5), '6': (U34[0] + 40, U34[1] - 5),
|
||
'5': (U34[0] + 40, U34[1] - 15)}
|
||
pm = {p: (x, y) for p, _n, x, y in pins['U34']}
|
||
nm = {p: _n for p, _n, x, y in pins['U34']}
|
||
for p, xy in want34.items():
|
||
if pm.get(p) != xy:
|
||
bad.append(u'U34.%s 期望 %s 实测 %s' % (p, xy, pm.get(p)))
|
||
print(u'U34 引脚名: %s' % ' '.join('%s=%s' % (k, nm[k]) for k in sorted(nm, key=int)))
|
||
pmc = {p: (x, y) for p, _n, x, y in pins['C760']}
|
||
want_c = sorted([(C760[0], C760[1] - 20), (C760[0], C760[1] + 20)])
|
||
if sorted(pmc.values()) != want_c:
|
||
bad.append(u'C760 引脚期望 %s 实测 %s' % (want_c, sorted(pmc.values())))
|
||
if (C760[0], U34[1] + 15) not in pmc.values():
|
||
bad.append(u'C760 没有引脚落在 +3V3 那条横线 y=%d 上' % (U34[1] + 15))
|
||
want31 = {'1': (205, 340), '2': (205, 330), '3': (205, 320), '4': (295, 330)}
|
||
pm31 = {p: (x, y) for p, _n, x, y in pins['U31']}
|
||
for p, xy in want31.items():
|
||
if pm31.get(p) != xy:
|
||
bad.append(u'U31.%s 期望 %s 实测 %s' % (p, xy, pm31.get(p)))
|
||
# C736 的上极必须正好落在 VIN 那条横线(y=320)上,下极是接地那头
|
||
v736 = sorted(v[1] for v in {p: (x, y) for p, _n, x, y in pins['C736']}.values())
|
||
if v736 != [280, 320]:
|
||
bad.append(u'C736 引脚 y 期望 [280, 320] 实测 %s' % v736)
|
||
v737 = sorted(v[1] for v in {p: (x, y) for p, _n, x, y in pins['C737']}.values())
|
||
if v737 != [300, 330]:
|
||
bad.append(u'C737 引脚 y 期望 [300, 330] 实测 %s' % v737)
|
||
if bad:
|
||
raise SystemExit(u'引脚坐标对不上:\n ' + '\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(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], 15,
|
||
"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;")
|
||
json.dump(NC, open(os.path.join(HERE, 'c1-nets-nc.json'), 'w', encoding='utf-8'),
|
||
ensure_ascii=False, indent=1)
|
||
print(u'推送完成')
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|