# -*- 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())