原理图/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
217 行
9.0 KiB
Python
217 行
9.0 KiB
Python
# -*- 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())
|