原理图/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
707 行
33 KiB
Python
707 行
33 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""下板 PCB1 五路功率级的**手工布线** —— §10.2 那四条关键回路的事实来源。
|
||
|
||
/c/Python314/python .claude/pcb1_route_pwr.py # 计划 + 全部校核
|
||
/c/Python314/python .claude/pcb1_route_pwr.py --push # 校核跑绿才下发,完事自动回读
|
||
/c/Python314/python .claude/pcb1_route_pwr.py --verify # 只按板上实测铜再核一遍
|
||
/c/Python314/python .claude/pcb1_route_pwr.py --clean --push # 先删掉上一版再重画
|
||
/c/Python314/python .claude/pcb1_route_pwr.py --ch 1 # 只看某一路(调试用)
|
||
|
||
## 为什么这几条不交给栅格布线器
|
||
|
||
§10.2 的四条:热回路面积、SW 铜箔面积、FB 短且远离 SW、分流电阻的 Kelvin 四线 ——
|
||
**每一条的判据都不是「连通」**,栅格布线器优化的是长度和过孔数,
|
||
它给出的解可以完全连通而四条全不满足。所以这四条按母版手写坐标,
|
||
剩下的细信号(EN 分压、补偿、PG、按键驱动、eFuse 编程电阻)留给后面的信号布线。
|
||
|
||
## 坐标系
|
||
|
||
路径全部写在 **CH1 母版的局部坐标**里(和 `pcb1_place.CH1` 同一套),
|
||
再按 `pcb1_place.CH_PLACE` 仿射到五路:rot 0 是平移,rot 180 是 `(bx-lx, by-ly)`。
|
||
⇒ **改一处等于五路一起改**,这也是 §26.3「不做 y 镜像,只转 180°」的用处:
|
||
母版里手调出来的关键回路原样保留。
|
||
|
||
## 网络名一律反查,不写死
|
||
|
||
四条主干里有三条挂在自动命名网上(SW=`$2N665`、eFuse 输出=`$2N699`、BOOT=`$2N667`),
|
||
而且**每一路的编号都不一样**(CH1 是 `$2N…`、CH2 是 `$8N…`、CH4 是 `$12N…`)。
|
||
写死等于静默失效(`pcb1_rules.py` 文件头第 4 条踩过)。
|
||
这里按 `(位号模板, 引脚号)` 到 `pcb1-parts.json` 里现查。
|
||
|
||
## 校核(下发前后走同一段代码)
|
||
|
||
1. 每条路径至少落在一个**本网焊盘**里,并列出它实际盖住了哪些焊盘
|
||
2. 逐段「胶囊 vs 轴对齐框」净距 ≥ 0.2 mm:异网焊盘 / 异网过孔 / 同层异网走线 / 本轮其它异网段
|
||
⚠️ **线的端点不是铜的端点**:圆头端帽往外再探半个线宽(§26.11 那一条),
|
||
所以这里一律按胶囊算,不按线段算。
|
||
3. 拐角角度:< 90° 的尖角报出来(EasyEDA 的 45° 化只处理直角)
|
||
4. 载流:按 IPC-2221 外层 1 oz 反算每一段的 ΔT,超过 30 °C 的报出来
|
||
"""
|
||
import io
|
||
import json
|
||
import math
|
||
import os
|
||
import sys
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
sys.path.insert(0, HERE)
|
||
|
||
import pcb1_place as PL # noqa: E402
|
||
from eda import run # noqa: E402
|
||
|
||
PCB = '7e9415a16a909c51'
|
||
MIL = PL.MIL
|
||
L_TOP, L_BOT = 1, 2
|
||
CLR = 0.2 # pcb1_rules.MIN_CLEARANCE
|
||
VIA_R = 0.305 # 过孔铜箔半径(外径 0.61)
|
||
EPS = 1e-6
|
||
SNAP = os.path.join(HERE, '_pcb1_place_now.json')
|
||
|
||
# ══════════════════ 母版路径(CH1 局部坐标,mm)══════════════════
|
||
# net: ('pad', 位号模板, 引脚号) —— 到 pcb1-parts.json 里按这一路的位号反查网名
|
||
# pts: [(x, y), ...];w 是整条路径的线宽(要变宽就拆成两条)
|
||
R = []
|
||
|
||
|
||
def add(name, net, w, pts, i=0.0, layer=L_TOP, why=u''):
|
||
u"""i = 这一段实际要过多少电流(A),0 = 细信号不算温升。"""
|
||
R.append(dict(name=name, net=net, w=w, pts=pts, i=i, layer=layer, why=why))
|
||
|
||
|
||
# ── ① 热回路:+12V 走顶层,地在正下方的 Inner1 整平面上回流 ────────────
|
||
# §10.2-1「2 颗 10 µF MLCC 必须同层紧贴 IC 的 VIN/GND 引脚」。
|
||
# ⚠️ **地不另画顶层线**:Inner1 就在顶层下面 0.2104 mm,回流电流是镜像电流,
|
||
# 它贴着顶层这条 +12V 走,回路面积 = 走线长 x 0.21 mm。
|
||
# 要是让 +12V 下到 Inner2(L3)再回来,回路高度变成 L2-L3 的 1.13 mm,**差 5 倍**。
|
||
_12V = ('pad', 'C%01', '1')
|
||
add('12V-C101->U1', _12V, 1.0, [(4.001, 10.299), (6.600, 11.000)], i=1.6,
|
||
why=u'C%01 的 +12V 脚直接上 U%1 的 PVIN(p6),全程顶层')
|
||
add('12V-U1-tie', _12V, 0.4, [(6.600, 11.000), (6.600, 11.950)], i=1.1,
|
||
why=u'把 PVIN 三个脚 p6/p5/p4 串起来;北端停在 11.95,'
|
||
u'离 p3(GND, 底 12.361) 还有 0.211')
|
||
# ⚠️ C%02 / C%03 到 U%1 的顶层直路是**堵死的**:C%01 的 GND 扇出孔 (5.475,12.300)
|
||
# 和 U%1.2 的 (5.922,13.354) 把那条 1.7 mm 宽的走廊切成 0.148 mm。
|
||
# ⇒ 从西边绕一条母线,把三颗罐的 +12V 脚在顶层并成一个节点再一起进 U%1。
|
||
add('12V-bus', _12V, 0.8, [(2.550, 8.701), (2.550, 13.949)], i=1.1,
|
||
why=u'西侧 +12V 母线:C%03 / C%01 / C%02 三颗的 +12V 脚并到一起')
|
||
add('12V-bus-C103', _12V, 0.8, [(2.550, 8.701), (4.001, 8.701)], i=0.6, why=u'母线 -> C%03.2')
|
||
add('12V-bus-C101', _12V, 0.8, [(2.550, 10.299), (4.001, 10.299)], i=1.1, why=u'母线 -> C%01.1')
|
||
add('12V-bus-C102', _12V, 0.8, [(2.550, 13.949), (4.001, 13.949)], i=0.6, why=u'母线 -> C%02.1')
|
||
|
||
# ── ② SW 节点:面积压到刚好够载流(§10.2-2)────────────────────────────
|
||
# ⚠️ U%1 的 SW 脚(p11/p12)夹在 p10(EN) 与 p13(BOOT) 中间,0.5 mm 间距,
|
||
# 只能先用窄线往东出 1 mm,出了焊盘区再放宽。
|
||
# ⚠️ 窄到 **0.30 就到底了**,不是 0.28:`pcb1_rules.TRACK_RULES` 里
|
||
# SWNODE / PWR4A 的 `strokeWidthMin` 都是 0.30(= 11.8 mil)。
|
||
# 先按 0.28 下发过一次,DRC 立刻报 50 条**物理错误**「线宽 11mil,应该 ≥ 11.8mil」——
|
||
# 间距错误一条没有、连接性也降了,光看那两支会以为全过。
|
||
# 0.30 的线从 0.28 高的焊盘出来,上下各留 0.21,正好还够。
|
||
_SW = ('pad', 'L%01', '1')
|
||
add('SW-p11', _SW, 0.30, [(10.224, 12.001), (11.200, 12.001)], i=2.0, why=u'U%1.11 出线(短桩)')
|
||
# ⚠️ p12 的短桩往东多走 0.3(11.200 -> 11.500):`SW-run` 那条 1.2 宽的线,
|
||
# 西端圆头端帽半径 0.6,起点放 11.200 时端帽离 **U%1.13(BOOT) 焊盘**只有 0.080。
|
||
# 起点东移到 11.500 之后是 0.347。短桩本身 0.30 宽、离 BOOT 焊盘 0.21,没变。
|
||
add('SW-p12', _SW, 0.30, [(10.224, 12.501), (11.500, 12.501)], i=2.0, why=u'U%1.12 出线(短桩)')
|
||
add('SW-tie', _SW, 0.30, [(11.200, 12.001), (11.200, 12.501)], i=2.0, why=u'两个 SW 脚并起来(短桩)')
|
||
# ⚠️⚠️ **SW 不再往南潜了(2026-09-06 第二十一轮)。** 老版本是
|
||
# `SW-down (11.200,12.250)->(11.750,11.600)` + `SW-run` 东行再 45° 抬回来,
|
||
# 目的是从 C%05.1(BOOT) 焊盘**下面**绕过去(那时它的焊盘底在 12.732,缝只有 0.581)。
|
||
# 代价是那一潜正好压在 **p9(COMP) / p10(EN) 唯一的出线走廊**上 ——
|
||
# 1.5 mm 宽的 `SW-run` 西端圆头端帽在 y = 11.5 那一行往西探到 x 11.007,
|
||
# 而两个脚要并排出来至少需要到 x 11.62。⇒ 两个脚只能活一个,`EN` 五路全布不通
|
||
# (`pcb1_escape_audit.py` 实测:两层加起来只能走 0.28 mm²)。
|
||
# ⇒ **把 C%05 北移 0.6 mm**(见 `pcb1_place.CH1` 里那一段),BOOT 焊盘底变 13.332,
|
||
# SW 就能平着走 y 12.500(1.2 宽,顶 13.10,离 BOOT 焊盘 0.232)。
|
||
# y < 11.9 整片空出来:COMP 走 y 11.001、EN 走 y 11.501,各走自己那一行。
|
||
# 顺带:SW 铜面积 10.9 -> 9.5 mm²、节点长度 9.6 -> 8.9 mm,**§10.2-2 要的就是小**。
|
||
# ⚠️ 立管 x 13.500 宽 1.50 ⇒ 铜跨 12.75~14.25,**顺路盖住 C%05.2(SW) 的焊盘**
|
||
# (12.570~13.070),所以不用再单画一段去接自举电容。
|
||
add('SW-run', _SW, 1.20, [(11.500, 12.500), (13.500, 12.500)], i=4.0,
|
||
why=u'东行段:平着走 p12 那一行,从 C%05.1(BOOT,底 13.332) 下面过,净距 0.232')
|
||
add('SW-riser', _SW, 1.50, [(13.500, 12.500), (13.500, 16.600)], i=4.0,
|
||
why=u'立管直上进 L%01 的 SW 脚(焊盘 12.501~14.501 / 16.301~19.701),'
|
||
u'顺路盖住 C%05 的 SW 脚 (12.820,13.001)')
|
||
|
||
# ── BOOT:自举电容,贴着 p13 平着往东 1.8 mm ───────────────────────────
|
||
_BOOT = ('pad', 'C%05', '1')
|
||
# 警告:BOOT 不能和 SW 走同一条水平缝。原来两条都在 y≈12.5~13.0 那一带,
|
||
# SW-down 放宽到 1.0 之后圆头端帽顶到 12.75,离 BOOT 只剩 0.101。
|
||
# 改成 BOOT 先往**北**抬到 13.75(U%1 北边是空的,p14 在 x<=9.391),
|
||
# 平着过去再往南扎进 C%05.1,两条从此不共用任何一条缝。
|
||
# C%05 北移 0.6 之后 C%05.1 的焊盘是 y 13.332~13.872,(11.979,13.750) 直接落在里面,
|
||
# 不用再往南扎一段。
|
||
add('BOOT', _BOOT, 0.30, [(10.224, 13.001), (10.900, 13.750), (11.979, 13.750)],
|
||
why=u'U%1.13 -> C%05.1,从 U%1 北边绕过去;终点落在焊盘 (13.332~13.872) 正中')
|
||
|
||
# ── ③ VOn:电感 -> 三颗输出电容 -> eFuse 输入 ─────────────────────────
|
||
_VO = ('pad', 'L%01', '2')
|
||
add('VO-up', _VO, 1.50, [(7.900, 18.400), (7.900, 19.400)], i=4.0, why=u'L%01.2 焊盘内往北')
|
||
add('VO-diag', _VO, 1.50, [(7.900, 19.400), (6.100, 21.200), (5.200, 22.100)], i=4.0,
|
||
why=u'两段 45° 转到 x=5.200 这一列')
|
||
add('VO-col', _VO, 1.35, [(5.200, 22.100), (5.200, 28.400)], i=4.0,
|
||
why=u'VO 立列:正好穿过 C%07.1 / C%06.1 的焊盘,北端进 U%14 的 p11/p12/p13')
|
||
add('VO-C108', _VO, 0.50, [(5.200, 22.700), (7.979, 22.501)], i=0.5,
|
||
why=u'支到 C%08.1(高频去耦,只过纹波)')
|
||
add('VO-U14w', _VO, 0.30, [(5.200, 28.400), (4.300, 28.400), (4.001, 28.700),
|
||
(4.001, 30.000)], i=1.0,
|
||
why=u'补 U%14 西列的两个 VO 脚 p10/p9;绕开散热盘西缘 4.700')
|
||
|
||
# ── ④ eFuse 输出 $2N699 -> 分流电阻上游 ───────────────────────────────
|
||
_F = ('pad', 'Rs%01', '2')
|
||
add('F-row', _F, 1.00, [(5.399, 31.840), (3.650, 31.840)], i=2.5,
|
||
why=u'U%14 北排三个输出脚;东端停在 5.550,离 p3($2N698, 西 6.099) 0.249')
|
||
add('F-p78', _F, 0.60, [(4.001, 30.750), (4.001, 31.840)], i=1.5,
|
||
why=u'补西列的 p8/p7;南端停在 30.650,离 p9(VO, 顶 30.200) 0.25')
|
||
add('F-corner', _F, 1.20, [(3.650, 31.840), (3.050, 32.440)], i=4.0, why=u'45° 转北')
|
||
# ⚠️ 北端 39.300 -> 40.100:Rs%01 北移 0.8(见 pcb1_place 里那一段)。
|
||
add('F-trunk', _F, 1.20, [(3.050, 32.440), (3.050, 40.100)], i=4.0,
|
||
why=u'主干北上,走 U%8 西边;离 U%8.1 的 GND 扇出孔 (4.279,34.696) 0.324')
|
||
|
||
# ── ⑤ Kelvin 四线(§10.2-4)──────────────────────────────────────────
|
||
# ⚠️ 两条取样线**并排往北走 1.7 mm 再分开**,这是这一轮把 Rs%01 转 180° 换来的:
|
||
# 转之前「西 = VO_P / 东 = $2N699」而 INA 的 IN+ 在西、IN− 在东,两条必然交叉。
|
||
# ⚠️ IN− 那条必须从**电阻本体下面**(两个焊盘之间)横过去,而且要绕到
|
||
# U%8.7 的 GND 扇出孔 (6.500,39.732) **北边** —— 它南边那条缝只有 0.208 mm。
|
||
# ⚠️ 这两条整体 +0.8(U%8 和 Rs%01 一起北移,相对关系一格没变)。
|
||
add('K-IN+', _F, 0.30, [(4.960, 38.409), (4.960, 39.900), (4.760, 40.100),
|
||
(3.300, 40.100)],
|
||
why=u'U%8.10(IN+) -> Rs%01.2 的焊盘。取样点在焊盘上,不在主干上')
|
||
_VOP = ('pad', 'Rs%01', '1')
|
||
add('K-IN-', _VOP, 0.30, [(5.500, 38.409), (5.500, 40.300), (5.700, 40.500),
|
||
(8.600, 40.500)],
|
||
why=u'U%8.9(IN−) -> Rs%01.1 的焊盘。**U%8.9 没有下到 VO_P 小岛的过孔**'
|
||
u'(pcb1_pour.ROUTED_PADS 里挡掉的),不然取样点会落到岛上')
|
||
|
||
# ── ⑥ FB:短、且远离 SW(§10.2-3)────────────────────────────────────
|
||
_FB = ('pad', 'R%01', '2')
|
||
# ⚠️ 不能从 x≈6.07 直下:那条线正压在 R%08.1 ($2N655) 的焊盘上;
|
||
# 往西绕也不行 —— C%03 的东缘 4.676 到它自己那个 GND 扇出孔的西缘 5.170
|
||
# 只剩 0.494 mm 的缝,0.25 的线要 0.65。⇒ 走 R%08 **东边**,到 y≈4.5 再往西折。
|
||
add('FB-U1', _FB, 0.25, [(7.750, 10.276), (7.750, 4.500), (7.150, 3.900),
|
||
(6.066, 3.900), (6.066, 3.001)],
|
||
why=u'U%1.7(FB) 往南出,贴着散热盘南爪西边 0.224 下来,全程在 SW 的**反面**')
|
||
add('FB-R101', _FB, 0.25, [(6.066, 3.001), (3.432, 3.001)], why=u'R%07.1 -> R%01.2')
|
||
add('FB-R102', _FB, 0.20, [(3.432, 3.001), (3.432, 4.000), (2.566, 4.866),
|
||
(2.566, 5.201)],
|
||
why=u'R%01.2 -> R%02.1,绕开 R%01.1(VO) 和 R%02.2(GND)')
|
||
|
||
|
||
# ══════════════════ 几何 ══════════════════
|
||
def parts():
|
||
return {c['d']: c for c in json.load(
|
||
io.open(os.path.join(HERE, 'pcb1-parts.json'), encoding='utf-8'))}
|
||
|
||
|
||
PARTS = parts()
|
||
|
||
|
||
def net_of(spec, n):
|
||
u"""('pad', 位号模板, 引脚号) -> 这一路上那个焊盘的网名(现查,不写死)。"""
|
||
_, tok, pin = spec
|
||
des = PL.ch_designator(tok, n)
|
||
p = PARTS.get(des)
|
||
if not p:
|
||
sys.exit(u'反查网名失败:没有位号 %s' % des)
|
||
for num, _lx, _ly, _w, _h, net in p['pads']:
|
||
if str(num) == pin:
|
||
if not net:
|
||
sys.exit(u'反查网名失败:%s.%s 没有网络' % (des, pin))
|
||
return net, '%s.%s' % (des, pin)
|
||
sys.exit(u'反查网名失败:%s 没有引脚 %s' % (des, pin))
|
||
|
||
|
||
def xf(n, x, y):
|
||
u"""母版局部坐标 -> 板坐标。rot 180 的两路是纯旋转,不是镜像(§26.3)。"""
|
||
rot, bx, by = PL.CH_PLACE[n]
|
||
return (bx + x, by + y) if rot == 0 else (bx - x, by - y)
|
||
|
||
|
||
def board_paths(only=None):
|
||
u"""展开成板上的实际路径 [(名字, 网名, 层, 线宽, [(x,y)...], 通道, 为什么)]。"""
|
||
out = []
|
||
for n in sorted(PL.CH_PLACE):
|
||
if only and n != only:
|
||
continue
|
||
for r in R:
|
||
net, anchor = net_of(r['net'], n)
|
||
pts = [xf(n, x, y) for x, y in r['pts']]
|
||
out.append(dict(name='%s@CH%d' % (r['name'], n), net=net, layer=r['layer'],
|
||
w=r['w'], pts=pts, ch=n, anchor=anchor, i=r['i'],
|
||
why=r['why'].replace('%', str(n))))
|
||
return out
|
||
|
||
|
||
COPPER = os.path.join(HERE, 'pcb-copper.json')
|
||
|
||
|
||
def poly_pads():
|
||
u"""板上 POLYGON 焊盘的**真实轮廓**(板坐标 mm),键是 `位号.引脚`。
|
||
|
||
⚠️ 非要真轮廓不可:U%1 的散热盘是个**十字** —— 中间 2.03 x 2.03 的方块,
|
||
上下各两根 0.24 mm 宽的指头伸到 ±2.16。按包围盒算的话,
|
||
FB 那条从 U%1.7 往南出的线(x 7.75)会被判成压在散热盘上,
|
||
而实际上它离最近的那根指头还有 0.224 mm。反过来也一样危险:
|
||
包围盒宽出来的那一圈会**掩盖**真正贴到指头上的走线。
|
||
"""
|
||
d = json.load(io.open(COPPER, encoding='utf-8'))
|
||
out = {}
|
||
for q in d['pads']:
|
||
sh = q.get('shape')
|
||
if not sh or sh[0] != 'POLYGON' or not q.get('des'):
|
||
continue
|
||
pts = [v for v in sh[1] if isinstance(v, (int, float))]
|
||
out['%s.%s' % (q['des'], q['num'])] = list(zip(
|
||
[v / MIL for v in pts[0::2]], [v / MIL for v in pts[1::2]]))
|
||
return out
|
||
|
||
|
||
POLY = poly_pads()
|
||
|
||
|
||
def all_pads():
|
||
u"""板上全部焊盘 [(x0,y0,x1,y1,net,tag,poly_or_None)]。"""
|
||
out = []
|
||
for d, (x, y, rot) in PL.POS.items():
|
||
a = math.radians(rot)
|
||
ct, st = math.cos(a), math.sin(a)
|
||
p = PARTS.get(d)
|
||
if not p:
|
||
continue
|
||
for num, lx, ly, w, h, net in p['pads']:
|
||
lx, ly, w, h = lx / MIL, ly / MIL, w / MIL, h / MIL
|
||
if rot in (90, 270):
|
||
w, h = h, w
|
||
px, py = x + lx * ct - ly * st, y + lx * st + ly * ct
|
||
tag = '%s.%s' % (d, num)
|
||
out.append((px - w / 2.0, py - h / 2.0, px + w / 2.0, py + h / 2.0,
|
||
net or '', tag, POLY.get(tag)))
|
||
return out
|
||
|
||
|
||
def seg_poly_dist(ax, ay, bx, by, poly):
|
||
u"""线段到多边形的最短距离(端点在里面 -> 0)。"""
|
||
def inside(px, py):
|
||
c = False
|
||
n = len(poly)
|
||
for i in range(n):
|
||
x1, y1 = poly[i]
|
||
x2, y2 = poly[(i + 1) % n]
|
||
if (y1 > py) != (y2 > py) and px < (x2 - x1) * (py - y1) / (y2 - y1) + x1:
|
||
c = not c
|
||
return c
|
||
if inside(ax, ay) or inside(bx, by):
|
||
return 0.0
|
||
d = 1e9
|
||
n = len(poly)
|
||
for i in range(n):
|
||
x1, y1 = poly[i]
|
||
x2, y2 = poly[(i + 1) % n]
|
||
d = min(d, seg_seg_dist(ax, ay, bx, by, x1, y1, x2, y2))
|
||
return d
|
||
|
||
|
||
def seg_pad_dist(ax, ay, bx, by, pad):
|
||
poly = pad[6]
|
||
if poly:
|
||
return seg_poly_dist(ax, ay, bx, by, poly)
|
||
return seg_box_dist(ax, ay, bx, by, pad[:4])
|
||
|
||
|
||
def snapshot(refresh=False):
|
||
if refresh or not os.path.exists(SNAP):
|
||
import pcb1_pour as P
|
||
return P.snapshot(refresh=True)
|
||
return json.load(io.open(SNAP, encoding='utf-8'))
|
||
|
||
|
||
def seg_box_dist(ax, ay, bx, by, b):
|
||
u"""线段到轴对齐框的最短距离(0 = 相交)。"""
|
||
x0, y0, x1, y1 = b
|
||
|
||
def pt(px, py):
|
||
dx = max(x0 - px, 0.0, px - x1)
|
||
dy = max(y0 - py, 0.0, py - y1)
|
||
return math.hypot(dx, dy)
|
||
|
||
# 端点到框 + 框的四条边到线段
|
||
d = min(pt(ax, ay), pt(bx, by))
|
||
for cx, cy, dx, dy in ((x0, y0, x1, y0), (x1, y0, x1, y1),
|
||
(x1, y1, x0, y1), (x0, y1, x0, y0)):
|
||
d = min(d, seg_seg_dist(ax, ay, bx, by, cx, cy, dx, dy))
|
||
return d
|
||
|
||
|
||
def seg_seg_dist(ax, ay, bx, by, cx, cy, dx, dy):
|
||
def d_pt_seg(px, py, x1, y1, x2, y2):
|
||
vx, vy = x2 - x1, y2 - y1
|
||
L = vx * vx + vy * vy
|
||
t = 0.0 if L == 0 else max(0.0, min(1.0, ((px - x1) * vx + (py - y1) * vy) / L))
|
||
return math.hypot(px - (x1 + t * vx), py - (y1 + t * vy))
|
||
|
||
def cr(ox, oy, px, py, qx, qy):
|
||
return (px - ox) * (qy - oy) - (py - oy) * (qx - ox)
|
||
|
||
d1 = cr(ax, ay, bx, by, cx, cy)
|
||
d2 = cr(ax, ay, bx, by, dx, dy)
|
||
d3 = cr(cx, cy, dx, dy, ax, ay)
|
||
d4 = cr(cx, cy, dx, dy, bx, by)
|
||
if d1 * d2 < 0 and d3 * d4 < 0:
|
||
return 0.0
|
||
return min(d_pt_seg(ax, ay, cx, cy, dx, dy), d_pt_seg(bx, by, cx, cy, dx, dy),
|
||
d_pt_seg(cx, cy, ax, ay, bx, by), d_pt_seg(dx, dy, ax, ay, bx, by))
|
||
|
||
|
||
def amps(w_mm, dT=20.0):
|
||
u"""外层 1 oz 的载流(IPC-2221,常数 0.048 / 铜厚 1.37 mil)。"""
|
||
A = w_mm / 0.0254 * 1.37
|
||
return 0.048 * (dT ** 0.44) * (A ** 0.725)
|
||
|
||
|
||
def dT_for(w_mm, i_amp):
|
||
a20 = amps(w_mm, 20.0)
|
||
return 20.0 * (i_amp / a20) ** (1.0 / 0.44) if a20 > 0 else 999.0
|
||
|
||
|
||
# 短于这个长度的段不查温升。IPC-2221 那条式子假定的是**长而均匀**的导体,
|
||
# 而 QFN 0.5 mm 间距的脚只能用 0.28 的窄线出 1 mm —— 两端都焊在大铜上,
|
||
# 拿它去套公式会算出 265 °C 这种没有物理意义的数。真实做法就是这样出线。
|
||
STUB_LEN = 2.0
|
||
DT_LIMIT = 30.0
|
||
|
||
|
||
def check(paths, refresh=False, use_board=False):
|
||
u"""下发前 / 下发后共用的一整套校核。返回 (错误列表, 统计)。"""
|
||
errs, notes = [], []
|
||
pads = all_pads()
|
||
snap = snapshot(refresh)
|
||
vias = [(v[0] or '', v[1], v[2], (v[3] or 0.61) / 2.0) for v in snap.get('via', ())]
|
||
lines = [(l[0] or '', l[1], l[2], l[3], l[4], l[5], l[6] or 0.0)
|
||
for l in snap.get('line', ())]
|
||
|
||
# 1) 落脚 + 组内连通。
|
||
# 警告:判据是**按 (通道, 网络) 分组**算的,不是每条路径各自算。
|
||
# 像 SW-tie / 12V-bus 这种中间段本来就不碰焊盘,逐条查会刷一屏假错误;
|
||
# 真正要保证的是「这一组铜连成一片,而且落在本网的焊盘上」。
|
||
for p in paths:
|
||
hit = []
|
||
for pad in pads:
|
||
if pad[4] != p['net']:
|
||
continue
|
||
for i in range(len(p['pts']) - 1):
|
||
ax, ay = p['pts'][i]
|
||
bx, by = p['pts'][i + 1]
|
||
if seg_pad_dist(ax, ay, bx, by, pad) < p['w'] / 2.0 - EPS:
|
||
hit.append(pad[5])
|
||
break
|
||
p['hit'] = sorted(set(hit))
|
||
|
||
groups = {}
|
||
for p in paths:
|
||
groups.setdefault((p['ch'], p['net']), []).append(p)
|
||
for (ch, net), g in sorted(groups.items()):
|
||
if not any(p['hit'] for p in g):
|
||
errs.append(u'CH%d %-9s 整组一个本网焊盘都没盖到' % (ch, net))
|
||
par = list(range(len(g)))
|
||
|
||
def find(i):
|
||
while par[i] != i:
|
||
par[i] = par[par[i]]
|
||
i = par[i]
|
||
return i
|
||
|
||
for i in range(len(g)):
|
||
for j in range(i + 1, len(g)):
|
||
touch = False
|
||
for a in range(len(g[i]['pts']) - 1):
|
||
for b in range(len(g[j]['pts']) - 1):
|
||
d = seg_seg_dist(g[i]['pts'][a][0], g[i]['pts'][a][1],
|
||
g[i]['pts'][a + 1][0], g[i]['pts'][a + 1][1],
|
||
g[j]['pts'][b][0], g[j]['pts'][b][1],
|
||
g[j]['pts'][b + 1][0], g[j]['pts'][b + 1][1])
|
||
if d < (g[i]['w'] + g[j]['w']) / 2.0 - EPS:
|
||
touch = True
|
||
break
|
||
if touch:
|
||
break
|
||
if touch:
|
||
par[find(i)] = find(j)
|
||
for i in range(len(g)):
|
||
for j in range(i + 1, len(g)):
|
||
if set(g[i]['hit']) & set(g[j]['hit']):
|
||
par[find(i)] = find(j)
|
||
ncomp = len(set(find(i) for i in range(len(g))))
|
||
if ncomp > 1:
|
||
errs.append(u'CH%d %-9s 这一组铜断成 %d 块(%s)'
|
||
% (ch, net, ncomp,
|
||
' '.join(p['name'].split('@')[0] for p in g)))
|
||
|
||
# 2) 净距
|
||
def seg_clear(p, ax, ay, bx, by):
|
||
m = p['w'] / 2.0 + CLR
|
||
bad = []
|
||
for pad in pads:
|
||
if pad[4] == p['net']:
|
||
continue
|
||
d = seg_pad_dist(ax, ay, bx, by, pad) - p['w'] / 2.0
|
||
if d < CLR - 1e-4:
|
||
bad.append((d, u'焊盘 %s(%s)' % (pad[5], pad[4] or '-')))
|
||
for net, vx, vy, vr in vias:
|
||
if net == p['net']:
|
||
continue
|
||
d = seg_box_dist(ax, ay, bx, by, (vx - vr, vy - vr, vx + vr, vy + vr)) \
|
||
- p['w'] / 2.0
|
||
if d < CLR - 1e-4:
|
||
bad.append((d, u'过孔 %s @(%.2f,%.2f)' % (net or '-', vx, vy)))
|
||
for net, lay, lx1, ly1, lx2, ly2, lw in lines:
|
||
if net == p['net'] or lay != p['layer']:
|
||
continue
|
||
d = seg_seg_dist(ax, ay, bx, by, lx1, ly1, lx2, ly2) \
|
||
- p['w'] / 2.0 - lw / 2.0
|
||
if d < CLR - 1e-4:
|
||
bad.append((d, u'走线 %s @(%.2f,%.2f)' % (net or '-', lx1, ly1)))
|
||
for q in paths:
|
||
if q is p or q['net'] == p['net'] or q['layer'] != p['layer']:
|
||
continue
|
||
for j in range(len(q['pts']) - 1):
|
||
cx, cy = q['pts'][j]
|
||
dx, dy = q['pts'][j + 1]
|
||
d = seg_seg_dist(ax, ay, bx, by, cx, cy, dx, dy) \
|
||
- p['w'] / 2.0 - q['w'] / 2.0
|
||
if d < CLR - 1e-4:
|
||
bad.append((d, u'本轮 %s(%s)' % (q['name'], q['net'])))
|
||
return bad
|
||
|
||
for p in paths:
|
||
for i in range(len(p['pts']) - 1):
|
||
ax, ay = p['pts'][i]
|
||
bx, by = p['pts'][i + 1]
|
||
for d, who in sorted(seg_clear(p, ax, ay, bx, by))[:3]:
|
||
errs.append(u'%-22s 第%d段 净距 %.3f mm(要 %.2f)-> %s'
|
||
% (p['name'], i + 1, d, CLR, who))
|
||
|
||
# 3) 尖角
|
||
for p in paths:
|
||
for i in range(1, len(p['pts']) - 1):
|
||
x0, y0 = p['pts'][i - 1]
|
||
x1, y1 = p['pts'][i]
|
||
x2, y2 = p['pts'][i + 1]
|
||
a1 = math.atan2(y0 - y1, x0 - x1)
|
||
a2 = math.atan2(y2 - y1, x2 - x1)
|
||
ang = abs(math.degrees(a1 - a2)) % 360
|
||
ang = min(ang, 360 - ang)
|
||
if ang < 89.0:
|
||
errs.append(u'%-22s 第%d个拐点是 %.0f° 的尖角' % (p['name'], i, ang))
|
||
|
||
# 4) 载流
|
||
for p in paths:
|
||
if p['i'] <= 0:
|
||
continue
|
||
p['dT'] = dT_for(p['w'], p['i'])
|
||
if p['dT'] > DT_LIMIT and length(p) > STUB_LEN:
|
||
errs.append(u'%-22s 线宽 %.2f mm 带 %.1f A 温升 %.0f °C(上限 %.0f)'
|
||
% (p['name'], p['w'], p['i'], p['dT'], DT_LIMIT))
|
||
elif p['dT'] > DT_LIMIT:
|
||
notes.append(u'%-22s %.2f mm / %.1f A / 长 %.2f mm —— 短桩,不套 IPC 公式'
|
||
% (p['name'], p['w'], p['i'], length(p)))
|
||
return errs, notes
|
||
|
||
|
||
def length(p):
|
||
return sum(math.hypot(p['pts'][i + 1][0] - p['pts'][i][0],
|
||
p['pts'][i + 1][1] - p['pts'][i][1])
|
||
for i in range(len(p['pts']) - 1))
|
||
|
||
|
||
def report(paths):
|
||
print(u'下板 PCB1 五路功率级手工布线(§10.2 的四条关键回路)')
|
||
print(u' 母版 %d 条路径 x 5 路 = %d 条;全部顶层' % (len(R), len(paths)))
|
||
ch1 = [p for p in paths if p['ch'] == 1]
|
||
print(u'')
|
||
print(u' %-16s %-9s %5s %5s %6s %7s %s'
|
||
% (u'路径(CH1)', u'网络', u'线宽', u'电流', u'长度', u'ΔT', u'盖到的焊盘'))
|
||
for p in ch1:
|
||
dt = (u'%5.0f°C' % p['dT']) if p['i'] > 0 and 'dT' in p else u' -'
|
||
print(u' %-16s %-9s %5.2f %5.1f %6.2f %7s %s'
|
||
% (p['name'].split('@')[0], p['net'], p['w'], p['i'], length(p), dt,
|
||
' '.join(p['hit'])[:44]))
|
||
tot = sum(length(p) for p in ch1)
|
||
sw = sum(length(p) for p in ch1 if p['name'].startswith('SW'))
|
||
sw_a = sum(length(p) * p['w'] for p in ch1 if p['name'].startswith('SW'))
|
||
print(u'')
|
||
print(u' 一路合计 %.1f mm;其中 **SW 节点 %.1f mm / 铜面积 %.1f mm²**(§10.2-2)'
|
||
% (tot, sw, sw_a))
|
||
|
||
|
||
# ══════════════════ 下发 ══════════════════
|
||
def push(paths):
|
||
seg = []
|
||
for p in paths:
|
||
for i in range(len(p['pts']) - 1):
|
||
(ax, ay), (bx, by) = p['pts'][i], p['pts'][i + 1]
|
||
seg.append({'net': p['net'], 'lay': p['layer'],
|
||
'x1': ax * MIL, 'y1': ay * MIL,
|
||
'x2': bx * MIL, 'y2': by * MIL, 'w': p['w'] * MIL})
|
||
JS = u"""
|
||
await eda.dmt_EditorControl.openDocument('%s');
|
||
await new Promise(r => setTimeout(r, 1500));
|
||
const S = %s;
|
||
let n = 0;
|
||
for (const s of S) {
|
||
const ln = await eda.pcb_PrimitiveLine.create(s.net, s.lay, s.x1, s.y1, s.x2, s.y2, s.w);
|
||
if (ln) n++;
|
||
}
|
||
await eda.pcb_Document.save();
|
||
return {n, totalLine: (await eda.pcb_PrimitiveLine.getAll()).length};
|
||
""" % (PCB, json.dumps(seg))
|
||
r = run(JS, timeout=900)
|
||
if not r.get('success'):
|
||
print(u'失败:', json.dumps(r, ensure_ascii=False)[:500])
|
||
sys.exit(1)
|
||
print(u' 下发:新建走线 %d 段,板上走线总数 %d'
|
||
% (r['result']['n'], r['result']['totalLine']))
|
||
return len(seg)
|
||
|
||
|
||
def verify(n_expect=None):
|
||
u"""按板上**实测**的线再核一遍。
|
||
|
||
警告:判据是**覆盖**,不是「端点对得上」。
|
||
EasyEDA 会在 T 形接点上把已有的线**劈成两条** —— 西侧那条 +12V 母线
|
||
(2.550, 8.701)->(2.550, 13.949) 下发之后,板上是
|
||
(2.550,8.701)-(2.550,10.299) 和 (2.550,10.299)-(2.550,13.949) 两条,
|
||
因为 C%01 那条支线正好接在中间。按端点比会报「40 段对不上」,
|
||
而板上的铜一点没少。⇒ 沿着计划的每一段每 0.2 mm 取一个点,
|
||
要求都被某条同网同层同线宽的实测线盖住。
|
||
"""
|
||
snap = snapshot(refresh=True)
|
||
on = [(l[0] or '', l[1], l[2], l[3], l[4], l[5], round(l[6] or 0, 3))
|
||
for l in snap.get('line', ())]
|
||
paths = board_paths()
|
||
miss, nseg = [], 0
|
||
for p in paths:
|
||
# ⚠️ **不能只认「同宽」的线。** 一段窄线整个落在一条宽线的圆头端帽里时,
|
||
# EasyEDA 会把它**直接吞掉**(实测:SW-p12 东端那 0.3 mm 落在 SW-run
|
||
# 1.2 mm 的西端帽里,板上就没有那一小段了)—— 铜一点没少,按同宽比却报 5 条。
|
||
# 正确判据是「计划的那段铜**被实测铜包住**」:实测线不窄于计划线,
|
||
# 且中心距 ≤ (o_w − p_w) / 2。
|
||
cand = [o for o in on
|
||
if o[0] == p['net'] and o[1] == p['layer'] and o[6] >= p['w'] - 0.011]
|
||
for i in range(len(p['pts']) - 1):
|
||
nseg += 1
|
||
(ax, ay), (bx, by) = p['pts'][i], p['pts'][i + 1]
|
||
L = math.hypot(bx - ax, by - ay)
|
||
n = max(2, int(L / 0.2) + 1)
|
||
bad = 0
|
||
for k in range(n + 1):
|
||
t = k / float(n)
|
||
px, py = ax + (bx - ax) * t, ay + (by - ay) * t
|
||
if not any(_d_pt_seg(px, py, o[2], o[3], o[4], o[5])
|
||
<= (o[6] - p['w']) / 2.0 + 0.02 for o in cand):
|
||
bad += 1
|
||
if bad:
|
||
miss.append(u'%s 第%d段(%d/%d 个采样点没被盖住)'
|
||
% (p['name'], i + 1, bad, n + 1))
|
||
print(u' 回读:板上 %d 条走线;本轮 %d 段里没被实测铜盖住的 %d 条'
|
||
% (len(on), nseg, len(miss)))
|
||
for m in miss[:10]:
|
||
print(u' [!!] %s' % m)
|
||
return miss
|
||
|
||
|
||
def _d_pt_seg(px, py, x1, y1, x2, y2):
|
||
vx, vy = x2 - x1, y2 - y1
|
||
L = vx * vx + vy * vy
|
||
t = 0.0 if L == 0 else max(0.0, min(1.0, ((px - x1) * vx + (py - y1) * vy) / L))
|
||
return math.hypot(px - (x1 + t * vx), py - (y1 + t * vy))
|
||
|
||
|
||
def clean(paths):
|
||
u"""只删**本轮画的**那些线。
|
||
|
||
警告:不能按线宽删。板上同一个网络上还有扇出的短线(`pcb_fanout` 的
|
||
TRK_W_TIGHT 也用 0.20 / 0.30),按宽度一刀切会把它们一起端掉。
|
||
判据是**两个端点都落在计划路径的走廊里**(0.02 mm)——
|
||
EasyEDA 会在 T 形接点上劈线,所以劈出来的两段也照样被认出来。
|
||
"""
|
||
snap = snapshot(refresh=True)
|
||
kill = []
|
||
for idx, (net, lay, x1, y1, x2, y2, w) in enumerate(
|
||
[(l[0] or '', l[1], l[2], l[3], l[4], l[5], round(l[6] or 0, 3))
|
||
for l in snap.get('line', ())]):
|
||
for p in paths:
|
||
if p['net'] != net or p['layer'] != lay or abs(p['w'] - w) > 0.01:
|
||
continue
|
||
for i in range(len(p['pts']) - 1):
|
||
(ax, ay), (bx, by) = p['pts'][i], p['pts'][i + 1]
|
||
if (_d_pt_seg(x1, y1, ax, ay, bx, by) < 0.02
|
||
and _d_pt_seg(x2, y2, ax, ay, bx, by) < 0.02):
|
||
kill.append((net, lay, x1, y1, x2, y2, w))
|
||
break
|
||
else:
|
||
continue
|
||
break
|
||
if not kill:
|
||
print(u' 清理:板上没有本轮画过的线')
|
||
return 0
|
||
JS = u"""
|
||
await eda.dmt_EditorControl.openDocument('%s');
|
||
await new Promise(r => setTimeout(r, 1500));
|
||
const K = %s, M = 39.3701, q = v => Math.round(v / M * 1e4) / 1e4;
|
||
const del = [];
|
||
for (const l of await eda.pcb_PrimitiveLine.getAll()) {
|
||
for (const k of K) {
|
||
if ((l.net || '') === k[0] && l.layer === k[1]
|
||
&& Math.abs(q(l.startX) - k[2]) < 0.005 && Math.abs(q(l.startY) - k[3]) < 0.005
|
||
&& Math.abs(q(l.endX) - k[4]) < 0.005 && Math.abs(q(l.endY) - k[5]) < 0.005) {
|
||
del.push(l.primitiveId); break;
|
||
}
|
||
}
|
||
}
|
||
if (del.length) await eda.pcb_PrimitiveLine.delete(del);
|
||
await eda.pcb_Document.save();
|
||
return {n: del.length, left: (await eda.pcb_PrimitiveLine.getAll()).length};
|
||
""" % (PCB, json.dumps(kill))
|
||
r = run(JS, timeout=900)
|
||
if not r.get('success'):
|
||
print(u'失败:', json.dumps(r, ensure_ascii=False)[:400])
|
||
sys.exit(1)
|
||
print(u' 清理:删掉本轮的 %d 条(认出 %d 条),板上还剩 %d 条走线'
|
||
% (r['result']['n'], len(kill), r['result']['left']))
|
||
return r['result']['n']
|
||
|
||
|
||
def main():
|
||
only = None
|
||
if '--ch' in sys.argv:
|
||
only = int(sys.argv[sys.argv.index('--ch') + 1])
|
||
paths = board_paths(only)
|
||
errs, notes = check(paths, refresh='--push' in sys.argv or '--verify' in sys.argv)
|
||
report(paths)
|
||
print(u'')
|
||
if errs:
|
||
print(u'!! %d 个问题:' % len(errs))
|
||
for e in errs[:40]:
|
||
print(u' ' + e)
|
||
if len(errs) > 40:
|
||
print(u' ... 还有 %d 条' % (len(errs) - 40))
|
||
sys.exit(1)
|
||
if notes:
|
||
print(u' 提示 %d 条:' % len(notes))
|
||
for t in notes:
|
||
print(u' · ' + t)
|
||
print(u'✅ 全部校核通过(本网落脚 / 组内连通 / 净距 / 拐角 / 载流)')
|
||
if '--verify' in sys.argv:
|
||
verify()
|
||
return
|
||
if '--push' in sys.argv:
|
||
if '--clean' in sys.argv:
|
||
clean(paths)
|
||
push(paths)
|
||
verify()
|
||
else:
|
||
print(u'(未加 --push,没有下发)')
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|