初始导入:PhoneRail 五路手机直供电源板

原理图/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
这个提交包含在:
zhou yang
2026-09-06 18:18:14 +08:00
共同撰写人 Claude Opus 5
当前提交 2bfe308b30
共修改 526 个文件,包含 80633 行新增和 0 行删除
+266
查看文件
@@ -0,0 +1,266 @@
# -*- 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()