文件
phonerail/.claude/pcb1_ctrl_place.py
zhou yang和Claude Opus 5 2bfe308b30 初始导入: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
2026-09-06 18:18:14 +08:00

217 行
9.0 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# -*- coding: utf-8 -*-
u"""控制块(U6 / U7 / U21 / U22 / U23)**按连接几何**求位置,替掉 `affine()` 的原理图仿射。
/c/Python314/python .claude/pcb1_ctrl_place.py # 评估现状 + 搜最优,只打印
/c/Python314/python .claude/pcb1_ctrl_place.py --iters 40000
## 为什么要换掉 affine
`pcb1_place._P3_SET = affine(['P3-CONTROL','P3B-EXPAND'], 73, 34, 42, 22)` 是把这两页的
**原理图坐标**线性映射到板上的一块 42 × 22 —— 保的是「原理图上挨着的板上也挨着」,
和「谁连谁」没有半点关系。实测后果(2026-09-06):
* 焊盘框总面积只有 253 mm²,却全挤在 924 mm² 的框里(填充率 27%),
而 S-M 块可用的是 50 × 58 —— `y 0.5~32` 大片空着,§26.14 那 91 条布不通
正好集中在 U21~U23 周边。
* **U22 和 U23 的位置正好放反了**:U22 只服务 CH3/CH4/CH5(理想 x ≈ 105)却待在 79.6,
U23 服务五路全部(理想 x ≈ 78)却待在 103.7。
* U6 管 CH1/CH2/CH3,而 CH1 在 x 17、CH3 在 x 139 —— `VDAC3` 因此拉了 93.8 mm。
## 目标函数:HPWL,但**只算控制块动得了的那部分**
线长用半周长(HPWL = 包围盒宽 + 高,布线器的标准代理指标)。
⚠️ 五路自己的焊盘、排针、U13 都是**钉死的**,它们贡献的那部分包围盒不管怎么摆都在 ——
所以只把**控制块这 5 颗芯片参与的网络**算进去,且芯片用**中心点**近似(引脚级差别 < 5 mm,
而这里比的是几十毫米的差距)。
⚠️ 电源/地(`GND` / `+3V3` / `+5V` / `+12V`)**不进目标函数**:它们走平面和铺铜,
不占布线走廊。把它们算进去会把所有芯片都吸到板子正中,反而更堵。
## 约束
* 待在 S-M 块内(`AREA`),并且避开 U20(ESP32 模组,天线必须朝下板边,不能动)、
两颗测温电阻、以及南北两排五路模块和排针的地盘。
* 每颗芯片按**预留框**占地(比焊盘框大一圈),给它自己的去耦和上下拉电阻留出位置 ——
这些附属件之后交给 `pcb1_place.relax()` 和 `DECAP` 表摆。
* 预留框两两不重叠(含 `CLR` 间隙)。
"""
import io
import json
import math
import os
import random
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import pcb1_netlist as NL # noqa: E402
# ── 要摆的 5 颗,以及给它「本尊 + 附属件」留的框(宽, 高,mm)──────────────
# SOIC-16 焊盘框 9.49 x 7.44,另有一颗 0805 去耦 + 两三颗 0402
# TSSOP-24 焊盘框 7.55 x 7.70,WQFN-16 只有 3.80 x 3.80
# ⚠️⚠️ 预留框**故意开得比「本尊 + 附属件」还大**:多出来的那一圈就是**走线通道**。
# 只按占地摆的话,退火会把 5 颗芯片贴在一起去换那几毫米线长 ——
# 而 §26.14 那 91 条布不通全都是「拥挤」型的(单独布每一条都通)。
# 实测 affine 摆出来的 U21(79.61,56.00) 和 U22(79.61,47.96) 中心只隔 8.04 mm,
# 两个 SOIC-16 的焊盘框(7.44 高)之间只剩 0.6 mm —— 一根线都插不进去。
CHIPS = {
'U21': (15.0, 12.0),
'U22': (15.0, 12.0),
'U23': (13.0, 12.0),
'U6': (10.0, 10.0),
'U7': (10.0, 10.0),
}
AREA = (70.0, 0.5, 118.0, 59.0) # S-M 块
CLR = 0.6 # 预留框之间的最小缝
# 块内动不了的东西(绝对框,摆位时要躲开)
BLOCK = [
(78.0, 0.9, 98.0, 28.4, u'U20 ESP32 模组(天线朝下板边,不能动)'),
(69.9, 24.3, 71.7, 37.7, u'RT601 / RT602 测温'),
]
# 电源地不进目标函数(见文件头)
POWER = {'GND', '+3V3', '+5V', '+12V', '+3V3_HUB'}
def load():
pads, _v, _l = NL.geom(False)
nets = {}
for p in pads:
if p['net'] and p['net'] not in POWER:
nets.setdefault(p['net'], []).append(p)
# 每个网络拆成「钉死的焊盘」和「控制块的哪几颗芯片」
jobs = []
for net, ps in nets.items():
chips = sorted(set(p['ref'] for p in ps if p['ref'] in CHIPS))
if not chips:
continue
fixed = [(p['x'], p['y']) for p in ps if p['ref'] not in CHIPS]
jobs.append((net, chips, fixed))
return jobs
def hpwl(jobs, pos):
tot = 0.0
for _net, chips, fixed in jobs:
xs = [x for x, _y in fixed] + [pos[c][0] for c in chips]
ys = [y for _x, y in fixed] + [pos[c][1] for c in chips]
if len(xs) < 2:
continue
tot += (max(xs) - min(xs)) + (max(ys) - min(ys))
return tot
def boxes(pos):
out = {}
for c, (w, h) in CHIPS.items():
x, y = pos[c]
out[c] = (x - w / 2, y - h / 2, x + w / 2, y + h / 2)
return out
def legal(pos):
bs = boxes(pos)
for c, b in bs.items():
if b[0] < AREA[0] or b[1] < AREA[1] or b[2] > AREA[2] or b[3] > AREA[3]:
return False
for bx0, by0, bx1, by1, _w in BLOCK:
if b[0] < bx1 + CLR and b[2] > bx0 - CLR and b[1] < by1 + CLR and b[3] > by0 - CLR:
return False
ks = sorted(bs)
for i in range(len(ks)):
for j in range(i + 1, len(ks)):
a, b = bs[ks[i]], bs[ks[j]]
if a[0] < b[2] + CLR and a[2] > b[0] - CLR and a[1] < b[3] + CLR and a[3] > b[1] - CLR:
return False
return True
def anneal(jobs, start, iters=30000, seed=7):
rnd = random.Random(seed)
cur = dict(start)
if not legal(cur): # 起点不合法就先随机找一个合法起点
for _ in range(200000):
cur = {c: (rnd.uniform(AREA[0] + 7, AREA[2] - 7),
rnd.uniform(AREA[1] + 6, AREA[3] - 6)) for c in CHIPS}
if legal(cur):
break
else:
sys.exit(u'⛔ 连一个合法起点都找不到 —— AREA / BLOCK / 预留框 对不上')
best, bcost = dict(cur), hpwl(jobs, cur)
cost = bcost
for k in range(iters):
T = 6.0 * (1.0 - k / float(iters)) + 0.02
c = rnd.choice(list(CHIPS))
step = 12.0 * T / 6.0 + 0.4
cand = dict(cur)
if rnd.random() < 0.15: # 偶尔整颗换位置(跳出局部最优)
c2 = rnd.choice([x for x in CHIPS if x != c])
cand[c], cand[c2] = cur[c2], cur[c]
else:
cand[c] = (cur[c][0] + rnd.gauss(0, step), cur[c][1] + rnd.gauss(0, step))
if not legal(cand):
continue
nc = hpwl(jobs, cand)
if nc < cost or rnd.random() < math.exp((cost - nc) / max(T, 1e-6)):
cur, cost = cand, nc
if nc < bcost:
best, bcost = dict(cand), nc
return best, bcost
def report(jobs, pos, title):
print(u'\n== %s ==' % title)
for c in sorted(CHIPS):
print(u' %-4s (%6.2f, %6.2f)' % (c, pos[c][0], pos[c][1]))
print(u' HPWL 合计 %.0f mm(%d 个网络)' % (hpwl(jobs, pos), len(jobs)))
def per_net(jobs, a, b, n=12):
rows = []
for net, chips, fixed in jobs:
def one(pos):
xs = [x for x, _y in fixed] + [pos[c][0] for c in chips]
ys = [y for _x, y in fixed] + [pos[c][1] for c in chips]
return (max(xs) - min(xs)) + (max(ys) - min(ys)) if len(xs) > 1 else 0.0
rows.append((one(a) - one(b), net, one(a), one(b)))
rows.sort(reverse=True)
print(u'\n改善最大的 %d 个网络:' % n)
for d, net, x, y in rows[:n]:
print(u' %-12s %6.1f -> %6.1f 省 %5.1f mm' % (net, x, y, d))
worse = [r for r in rows if r[0] < -0.5]
if worse:
print(u' 变差的 %d 个,最差 %s %.1f -> %.1f'
% (len(worse), worse[-1][1], worse[-1][2], worse[-1][3]))
def main():
argv = sys.argv[1:]
iters = int(argv[argv.index('--iters') + 1]) if '--iters' in argv else 30000
import pcb1_place as P
now = {c: (P.POS[c][0], P.POS[c][1]) for c in CHIPS}
jobs = load()
report(jobs, now, u'现状(affine 摆的)')
if not legal(now):
print(u' ⚠️ 现状本身不满足这里的预留框约束(预留框比实际占地大,正常)')
best, bcost = None, 1e18
for seed in range(6):
p, c = anneal(jobs, now, iters, seed)
if c < bcost:
best, bcost = p, c
report(jobs, best, u'退火搜出来的')
a = hpwl(jobs, now)
print(u'\n总线长 %.0f -> %.0f mm,**省 %.0f mm(%.0f%%)**' % (a, bcost, a - bcost,
(a - bcost) / a * 100))
per_net(jobs, now, best)
out = {c: [round(best[c][0], 2), round(best[c][1], 2)] for c in sorted(CHIPS)}
io.open(os.path.join(HERE, '_pcb1_ctrl_pos.json'), 'w', encoding='utf-8').write(
json.dumps(out, ensure_ascii=False, indent=1))
print(u'\n已写 _pcb1_ctrl_pos.json')
return 0
if __name__ == '__main__':
sys.exit(main())