R2 底板改 4 层(D15):规则 / 几何 / 背面摆位 / 4 层布线与铺铜,已下发
- 规则:铜层 2→4,差分 0.28/0.18,间距 0.15,过孔 0.3/0.2(rules_base) - 几何:98×98 板框 + 4×M3,槽阵居中,L1 12V 大铜 + 指头(pcb_base) - 摆位:五插槽留 L1,其余 133 件翻到 L4;背面镜像规则实测;Hub 放走廊 2、 东移 2.5 mm;扇出圈 / 小件间距 1.5(去耦例外)/ 9 A 过孔阵禁区(place_base) - 叠层按用户决定改为 L1 槽面 / L2 整层 +3V3 / L3 整层 GND / L4 器件面 - 布线:D± 七对按构造布通(USB3 掉头、USB1/2 北去掉头、USB4/5 东绕、 J23 桥),+3V3 / GND / +12V 平面网每脚一孔,0.125 网格 + PathFinder 开关(route_base、remote_route.ps1);剩 37 个网待手工补 - 铺铜:4 层重写,入口链自南向北切段 + 断面校核(pour_base); 已下发布线 / 缝合孔 / 铜面轮廓,填铜未成功(待查) - 工具:plot_base / plot_routes / shot_base,board_check 通孔两层都算 Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_0168Bu6SEx3cXXCJLkdnE6zB
这个提交包含在:
+573
@@ -0,0 +1,573 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""R2 底板 **A4-HUB** 页:USB2517I 七口 Hub + Hub 专用 3.3V LDO。
|
||||
|
||||
/c/Python314/python .claude/r2/a4_hub.py # 只算 + 本地自检
|
||||
/c/Python314/python .claude/r2/a4_hub.py --push # 清页 + 重画(幂等,只有主代理跑)
|
||||
|
||||
M3 的第四页。**主体是 R1 载板 `C2-HUB` 整页原样搬过来**(`OFF = (0,0)`,块内坐标一个字
|
||||
不动,连 U32 / J23 / Y1 / 去耦阵列的绝对引脚坐标都照抄 `_c2hub_live.json`),
|
||||
在此之上做三处改动:
|
||||
|
||||
| # | 改动 | 依据 |
|
||||
|---|---|---|
|
||||
| 1 | **新增 `U31` AMS1117-3.3 + `C752`/`C753`/`C737`** —— R1 里这颗不在 C2-HUB 页上 | 契约 §1.1 / §2 |
|
||||
| 2 | **放出第 6 个下行口**:`USBDN6_DM/DP`(53/54) → `USB6_DM/DP` 双向端口去 A3 | 方案 §8.2(D13)|
|
||||
| 3 | `HUB_RST` 的保命上拉保留并写进页面文字;`UP_VBUS_DET` 端口 `BI`→`OUT`、`HUB_RST` 的第二个端口 `BI`→`IN` | 契约 §3 第 1 条 / §1.2 |
|
||||
|
||||
---
|
||||
|
||||
## ⭐ T6:`CFG_SEL[2:0]=110` 下七个下行口是不是默认全开?—— **是,全开**
|
||||
|
||||
逐条对 `kicad/datasheets/ic/USB2517.pdf`(Microchip **DS00001598C**,USB2517/USB2517I,
|
||||
59 页,sha256 记在同名 `.json` 里;和 `eda-parts.json` 指名要用的 SMSC 2517 Rev 2.8
|
||||
是同一份内容的 Microchip 重排版,**不是** USB251x**B** 的 00001692C):
|
||||
|
||||
| 手册位置 | 原文 | 结论 |
|
||||
|---|---|---|
|
||||
| **Table 5-2**(p.15~16)`CFG_SEL[2:0]=110` | 「Internal Default Configuration • Strap Options are Disabled」 | strap **一个都不采样**,`PRT_DIS_*` 失效 |
|
||||
| **Table 7-1**(p.20)`0Ah PDS` Port Disable (Self) | 默认 **00h** | ⇒ 七个口**全部可用** |
|
||||
| **Table 7-1**(p.20)`0Bh PDB` Port Disable (Bus) | 默认 **00h** | 同上(本板自供电,看 0Ah)|
|
||||
| **7.2.1.11**(p.26)`PORT_DIS_SP` 位定义 | 「'0' = port is available, '1' = port is disabled」 | 00h = 一个都没禁 |
|
||||
| **Table 7-1**(p.20)`09h NRD` Non-Removable | 默认 **00h** | 全部可移除;`NON_REM[1:0]` 是 strap,同样不采样 |
|
||||
| **Table 7-1**(p.20)`06h CFG1` | 默认 **9Bh**,bit7 `SELF_BUS_PWR`=1 | 自供电(只有 `010` 那一档强制总线供电,`110` 不改)|
|
||||
|
||||
⇒ **第 6 个口不需要改任何 strap,直接把 `USBDN6_DP/DM` 引出来接线即可**。
|
||||
方案 §8.2 表第 6 条写的「若需要禁用多余口,改的是 strap 而不是电路」在这里反过来了:
|
||||
本来就全开,要做的是**连线**,不是改 strap。
|
||||
|
||||
⚠️ 沿用 R1 第三十三轮的定案(设计文档 §21.6b):`CFG_SEL[2:0]=110`(strap 禁用)
|
||||
取代旧的「`PRT_SWP1~5` 各 100k 下拉」,本页三个 CFG_SEL 脚照 R1 接:
|
||||
|
||||
CFG_SEL2(13) —— Table 5-3 缓冲类型 `I`(纯输入)→ 直接接 +3V3_HUB,不加电阻
|
||||
CFG_SEL1(42) —— 复用 HS_IND 输出,必须经 R712 10k **上拉**(硬接会和输出打架)
|
||||
CFG_SEL0(41) —— R711 10k 下拉 ⇒ 1/1/0 = 110
|
||||
|
||||
⚠️ **口 7 是全开且悬空的**(`USBDN7_DP/DM` 55/56 不接)。这是**故意**的,理由见 §21.6b:
|
||||
strap 停采样以后原来那两颗 10k 上拉会把 D+ 分压到 `3.3×15/(10+15)=1.98V`,
|
||||
正好压在全速「已插入」的 2.0V 判据上 → 反复枚举失败的幽灵口,比 `lsusb` 里多一个
|
||||
空口难看得多。悬空时被 Hub 内部 15k 下拉拉低 = 空闲,无害。R1 已把 `R714/R715` 删掉,
|
||||
本页同样不装。
|
||||
|
||||
## 第 6 个口要不要串阻 / ESD?——**不要**,和 R1 的五个口一样直连
|
||||
|
||||
R1 的口 1~5(`_c2hub_live.json` 实测)是 `U32` 引脚 → 一段短线 → `USBn_DP/DM` 端口,
|
||||
**中间一颗元件都没有**;设计文档也写着「不要在 D± 上串电阻或加 ESD 之外的任何东西」,
|
||||
而 ESD 在手机原厂尾插小板上。第 6 个口的对端是**同一块板上的 ESP32-S3 原生 USB**
|
||||
(IO19=D−/IO20=D+),不过连接器、不出板、走线 ≈25mm(方案 §6.3 第 6 对),
|
||||
比五个手机口的处境好得多 ⇒ 照做,直连。
|
||||
|
||||
## U31:5V → 3.3V,只喂 U32
|
||||
|
||||
Vin = +5V(A2 的 U30,2A 轨) Vout = +3V3_HUB(**只**给 U32)
|
||||
Hub 满配电流 ≈ 0.42 A(设计文档 §9.4)
|
||||
耗散 P = 0.42 × (5.0 − 3.3) = **0.714 W**,SOT-223 封装
|
||||
→ PCB 上必须给 SOT-223 的散热片(2 脚 tab)铺一块铜:
|
||||
θJA 靠铜面从 ~150 °C/W 压到 ~60 °C/W ⇒ ΔT ≈ 43 K,环境 40 °C 下结温 ~83 °C,
|
||||
对 125 °C 的上限有余量。**这条是 PCB 的约束,原理图上只能记着。**
|
||||
压差裕量:AMS1117 手册 p.1「dropout guaranteed maximum 1.3 V」(@0.8 A),
|
||||
这里只有 0.42 A、压差 1.7 V ⇒ 够。
|
||||
|
||||
输出电容 **必须是固态钽** `C737` T491A226K010AT 22µF/10V(`C116575`)——
|
||||
AMS1117 手册 §Stability 原文:「The addition of **22µF solid tantalum** on the output
|
||||
will ensure stability for all operating conditions.」**陶瓷不算数**(ESR 太低会让环路失稳)。
|
||||
⚠️ 这颗**1 脚是正极**(钽的条纹侧),本脚本用显式引脚号断言 `1` 在上、`2` 在下,
|
||||
放件时若方向反了 `Sheet.place()` 会自己翻 180 重试(skill §6b/§6c)。
|
||||
|
||||
输入侧 `C752` 10µF/50V 0805(`C440198`)+ `C753` 100nF 0402(`C1525`),都在 `+5V` 干线上。
|
||||
|
||||
⚠️ **契约 §3 第 3 条:`+3V3_HUB` 与 `+3V3` 是两条轨。** 本页从头到尾没有 `+3V3`,
|
||||
也没有 `+12V`;两条轨只在 GND 上相遇(`build()` 里有断言)。
|
||||
|
||||
## HUB_RST 的保命处理(契约 §3 第 1 条,硬约束)
|
||||
|
||||
`R716` 10k 把 `RESET_N`(43) 上拉到 `+3V3_HUB`(R1 已有,本页原样保留、不许删,
|
||||
`main()` 里逐脚断言它还在原位)。ESP32 复位 / 高阻 / 未烧固件时 `HUB_RST` 被这颗电阻
|
||||
拉高 = **Hub 保持工作**。理由写进页面文字:D13 之后 ESP32 的控制台与烧录口就挂在这颗
|
||||
Hub 后面,一旦「不驱动 = 复位」,误复位就等于自断控制台,而且下一次烧录也进不来
|
||||
(退路是 A3 的 `J33` 5P UART 应急口,但那是退路,不是设计依据)。
|
||||
|
||||
## 几何约定(skill)
|
||||
|
||||
Y 轴向上(§1)、坐标全部 5 的倍数(§3/§8a-2b)、旗端口只落在导线端点(§8a/§8a-2)、
|
||||
端口方向决定标签在左还是在右(§8b:`IN` 与 `BI`/`OUT` 相反)、
|
||||
netport 符号只有 ~10 高,10 单位间距排一列不打架(§8b-2)、
|
||||
标题栏 `x>455 且 y<192`(§8c)、文字宽度 ≈ 字号×(CJK + 0.5×ASCII)(§12)。
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
from schcheck import Sheet, _on, call, two_pin, PARTS # noqa: E402
|
||||
|
||||
PAGE, NAME = '09d583ff95cd35d9', 'A4-HUB'
|
||||
R2_PROJECT = '05a65c22f14846b8b95360ef8d3c1e2e'
|
||||
|
||||
SRC = os.path.join(HERE, '_c2hub_live.json') # R1 载板 C2-HUB 实测 dump
|
||||
OFF = (0, 0) # 整页原样搬,一个字不动
|
||||
|
||||
# ── ⚠ 唯一一处必须挪的 R1 图元:VBUS_UP 分压那一小块 ────────────────────
|
||||
# R1 载板 C2-HUB 上它坐在 y 45~135 / x 780~1060,搬到本页正好**落进 A4 标题栏**
|
||||
# (skill §8c:标题栏是 x>455 且 y<192,比想象的大)—— 图上 R721 / R722 /
|
||||
# UP_VBUS_DET 端口直接压在「PhoneRail-R2」和日期格上。
|
||||
# 六项几何验证不看这个(它只管连接性),`text_clash` 也只查文字,
|
||||
# **是靠人眼截图发现的**。整块平移 +380,落到 x 730~1070 / y 425~515 那条空带
|
||||
# (上边是 U32 左列端口的 y≤465 @ x=760、下边是 +3V3_HUB 端口 y=525,都不撞)。
|
||||
SHIFT_RECT = (770, 40, 1080, 140) # 源页坐标里的这一块
|
||||
SHIFT_D = (0, 380)
|
||||
|
||||
|
||||
def _in_rect(x, y, r=SHIFT_RECT):
|
||||
return r[0] <= x <= r[2] and r[1] <= y <= r[3]
|
||||
|
||||
|
||||
def shift_block(W, F, PT, place, exp):
|
||||
u"""把 SHIFT_RECT 里的图元整块平移 SHIFT_D。跨界的导线一律当错误。"""
|
||||
dx, dy = SHIFT_D
|
||||
W2 = []
|
||||
n = 0
|
||||
for a, b, net in W:
|
||||
ia, ib = _in_rect(*a), _in_rect(*b)
|
||||
assert ia == ib, u'导线 %s %s-%s 跨过 SHIFT_RECT 边界,不能整块平移' % (net, a, b)
|
||||
if ia:
|
||||
W2.append(((a[0] + dx, a[1] + dy), (b[0] + dx, b[1] + dy), net))
|
||||
n += 1
|
||||
else:
|
||||
W2.append((a, b, net))
|
||||
for lst in (F, PT):
|
||||
for it in lst:
|
||||
if _in_rect(it[2], it[3]):
|
||||
it[2] += dx
|
||||
it[3] += dy
|
||||
n += 1
|
||||
moved = []
|
||||
for it in place:
|
||||
if _in_rect(it[2], it[3]):
|
||||
it[2] += dx
|
||||
it[3] += dy
|
||||
moved.append(it[0])
|
||||
n += 1
|
||||
for d in moved:
|
||||
exp[d] = dict((k, (v[0] + dx, v[1] + dy)) for k, v in exp[d].items())
|
||||
assert sorted(moved) == ['R721', 'R722'], u'平移到的元件不是 R721/R722 而是 %s' % (moved,)
|
||||
assert n, u'SHIFT_RECT 一个图元都没框到 —— 源页坐标变了?'
|
||||
return W2
|
||||
|
||||
# ── 改动 3:端口方向按契约 §1.2 校正(键 = (网名, x, y) → 新 dir)───────────
|
||||
# R1 这两个是随手写的 BI;契约里 `UP_VBUS_DET` 是 A4 发给 A3、`HUB_RST` 是 A3 发给 A4,
|
||||
# 主代理要拿端口清单对账,方向必须对得上。**只改 dir,坐标一个字不动**。
|
||||
# ⚠ 顺带:`IN` 与 `BI`/`OUT` 的标签方向正好相反(skill §8b),所以改 dir 会让标签换边 ——
|
||||
# (1130,340) 是 rot=180,`BI` 时标签在左、`IN` 时在右,右边到 1145 只剩 15,
|
||||
# 于是这一个连 rot 一起翻成 0(`PORT_ROT`),标签仍旧落在左边。
|
||||
PORT_DIR = {
|
||||
('UP_VBUS_DET', 930, 60): 'OUT', # A4 → A3(电脑在不在)
|
||||
('HUB_RST', 1130, 340): 'IN', # A3 → A4;(760,435) 那个 R1 本来就是 IN
|
||||
}
|
||||
PORT_ROT = {('HUB_RST', 1130, 340): 0}
|
||||
|
||||
# ── 改动 2:第 6 个下行口(D13)──────────────────────────────────────────
|
||||
# U32 右列 53=USBDN6_DM(680,535) / 54=USBDN6_DP(680,545)。
|
||||
# 右侧端口列在 x=760,R1 已占 405/415/425/455/465/525/575/585/595/605/615/625/635/645/655,
|
||||
# **535 / 545 正好是空的**,插进去即可,间距仍是 10(skill §8b-2)。
|
||||
USB6 = [('USB6_DM', 535), ('USB6_DP', 545)]
|
||||
|
||||
# ── 改动 1:U31 AMS1117-3.3 块(新画)───────────────────────────────────
|
||||
# 落在 R1 版图留下的空带 x 300~900 / y 190~330(`assert_clear()` 每次重算校验)。
|
||||
# 引脚(契约 §4 实测,rot 0,相对中心):1 GND(-45,+10) 2 VOUT(-45,0) 3 VIN(-45,-10) 4 VOUT(+45,0)
|
||||
# ⚠ GND 夹在 VIN 与 VOUT 中间、且在 VIN **上方**,所以「+5V 从左边平进、GND 往左再往下」
|
||||
# 是唯一不产生交叉的走法:GND 的下行点必须**在 +5V 端口符号左边**
|
||||
# (+5V 引脚 x=440,IN+rot0 的符号往左伸到 380;GND 下行点 x=350),差 30 净空。
|
||||
U31_C = (700, 265)
|
||||
U31_PINS = {'1': (655, 275), '2': (655, 265), '3': (655, 255), '4': (745, 265)}
|
||||
C752_C, C753_C = (480, 235), (520, 235) # 输入 10µF/50V 0805 + 100nF 0402,跨距 ±20
|
||||
# ⚠ 2026-09-08 主代理实测更正(原来写 (810,250) / rot270 / ±15,推送时 place() 四轮翻转全对不上):
|
||||
# T491A226K010AT(C116575)**空页四个角度逐一放件读回**的结果是
|
||||
# rot 0 → pin1 (0,+20) rot 90 → pin1 (-20,0) rot180 → pin1 (0,-20) rot270 → pin1 (+20,0)
|
||||
# ⇒ 正极(1 脚)朝上要的是 **rot 0**,跨距是 **±20 不是 ±15**。
|
||||
# `p1b-pins.json` 里那个 ±15 是别的封装的电容,skill §8d:引脚跨距因符号而异,必须读回。
|
||||
C737_C = (810, 245) # 输出 22µF/10V 固态钽:rot0、±20 ⇒ 1脚(正)在 265、2脚在 225
|
||||
NEW_PARTS = [
|
||||
['U31', 'C6186', U31_C[0], U31_C[1], 0],
|
||||
['C752', 'C440198', C752_C[0], C752_C[1], 270],
|
||||
['C753', 'C1525', C753_C[0], C753_C[1], 270],
|
||||
['C737', 'C116575', C737_C[0], C737_C[1], 0],
|
||||
]
|
||||
BLOCK = (300, 190, 900, 330) # 新块要占的净空矩形(含端口标签)
|
||||
|
||||
# ── U32 上故意不接的脚(设计文档 §21.6 / §21.6b)+ J23 的 SBU ──────────────
|
||||
# 53/54 已被改动 2 接走,所以**不在**这张表里。
|
||||
NC = {
|
||||
'U32': ['14', '15', '16', '17', '18', '20', '21', '22', '23', '26', '27', '28',
|
||||
'29', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39',
|
||||
'47', '48', '49', '50', '51', '55', '56'],
|
||||
'J23': ['A8', 'B8'], # SBU1/SBU2,USB2.0 用不上
|
||||
}
|
||||
|
||||
# ── 靠同名旗/端口成网、导线上天然分块的网络(契约 §7 第 10 条)──────────────
|
||||
# R1 这一页的画法就是「每个芯片脚一段短线 + 一个同名 netport」,所以页内网络天然多块。
|
||||
# 五个下行口 / USB6 / UP_VBUS_DET / +5V **不在**表里 —— 它们只许有一块。
|
||||
ALLOW_SPLIT = set([
|
||||
'GND', '+3V3_HUB',
|
||||
'VDD18', 'VDD18PLL', 'XTAL1', 'XTAL2', 'RBIAS',
|
||||
'CFGSEL0', 'CFGSEL1', 'NONREM0', 'NONREM1',
|
||||
'UP_DP', 'UP_DM', 'VBUS_UP', 'VBUS_DET', 'HUB_RST',
|
||||
])
|
||||
|
||||
# ⚠ 可用的文字区只有三块(R1 版图 + 标题栏 x>455 且 y<192 挤剩下的,`text_clash`
|
||||
# / `label_clash` 每次重算):
|
||||
# A 标题 (45, 800)
|
||||
# B 说明块 x 345~800 / y 690~780 —— R1 原来放说明的位置
|
||||
# C U31 说明块 x 45~355 / y 195~260 —— x=60 的地线在 y 270~300,别越过 260
|
||||
# D HUB_RST x 900~1145 / y 204~240 —— 就在 R716 底下;x=1130 的线从 y=250 起
|
||||
# 左下 y<190 那一片**不能用**:J23 的本体和 12 根引脚就在 x 155~360 / y 45~180。
|
||||
# y<665 而 x 345~680 那一片也不能用:U32 的符号本体在那里(引脚只到 y=660,
|
||||
# 本体还要往上一点),所以 B 块最低只能到 y=690(R1 原来也是到这儿为止)。
|
||||
TEXTS = [
|
||||
(45, 800, u'【A4-HUB】板载 USB 2.0 七口 Hub + Hub 专用 3.3V —— 对电脑只有一根线(D13)', 14),
|
||||
|
||||
# B:Hub 配置 / T6 / 第 6 口 / HUB_RST
|
||||
(345, 780, u'U32 USB2517I:CFG_SEL[2:0]=110 = 内部默认配置 + strap 禁用(§21.6b)', 11),
|
||||
(345, 762, u'CFG_SEL2(13) 纯输入接 +3V3_HUB;CFG_SEL1 经 R712 上拉、CFG_SEL0 经 R711 下拉', 11),
|
||||
(345, 744, u'T6 已核 DS00001598C p.20 Table 7-1:0Ah/0Bh 端口禁用默认 00h ⇒ 七口全开', 11),
|
||||
(345, 726, u'⇒ 口 6 无需改 strap 直接连线;口 1~5 → A5 五槽,口 6 → A3 的 ESP32 原生 USB', 11),
|
||||
(345, 708, u'口 7 全开但悬空(R714/R715 已删:10k 上拉把 D+ 分到 1.98V 会变幽灵口)', 11),
|
||||
(345, 690, u'HUB_RST 固件只在启动自检脉冲一次;误复位 = 自断控制台 + 下次烧不进去', 11),
|
||||
|
||||
# C:U31 说明(就在 U31 块左边)
|
||||
# ⚠ 2026-09-08 主代理改:原来这四行在 x=45,**被 R1 带来的地旗 (60,270) 压穿**
|
||||
# —— `Ground rot=0` 的符号带 GND 标签往锚点**下方**伸到 y≈240,正好落在第 1/2 行上
|
||||
# (渲染成「U32 满配」上盖一个 GND)。同 A2-AUX 的两处叠字,六项几何验证与
|
||||
# `text_clash`(只查文字之间)都够不着,靠人眼截图发现。⇒ 整块挪到 x=100 / y 246~300、字号 10→9。
|
||||
# x=110 那一版会被 x=350 的地竖线(y 195~275)划穿 —— 脚本自带的 `text_clash`
|
||||
# 当场报了出来;x=200 那一版又压上 U32 左排引脚(y 330~660 一直伸到 x=320)。
|
||||
# ⚠ 注意 `flag_clash` **不建模旗的标签占地**:`Ground rot=0` 的符号带 GND 字
|
||||
# 从锚点往下伸约 38 个单位(本页实测 (60,270) 的旗压到 y≈240)。把这个盒子
|
||||
# 连同 U32 的引脚包围盒 (310,330)-(690,670)、其余文字块一起当障碍全页扫一遍,
|
||||
# **字号 10 的四行一个位置都放不下**,字号 9 才有 —— 这就是它比别的块小一号的原因。
|
||||
(100, 300, u'【U31】+5V → +3V3_HUB 专用 LDO(AMS1117-3.3)', 9),
|
||||
(100, 282, u'U32 满配约 0.42A,挂不进 U19 那条 0.5A 的 +3V3 轨', 9),
|
||||
(100, 264, u'耗散 0.42A×1.7V=0.71W @SOT-223,tab 必须铺铜', 9),
|
||||
(100, 246, u'C737 必须 22µF 固态钽(手册 Stability),陶瓷不算数', 9),
|
||||
|
||||
# D:HUB_RST 的保命上拉(就在 R716 底下)
|
||||
(900, 240, u'⚠ R716 10k 是 HUB_RST 的保命上拉:', 10),
|
||||
(900, 222, u'ESP32 复位 / 高阻时 Hub 保持工作 ——', 10),
|
||||
(900, 204, u'控制台与烧录口就在它后面(D13)', 10),
|
||||
]
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
def load(path, off):
|
||||
u"""整页读回并平移。返回 (导线[(a,b,net)], 旗, 端口, place, expect, 悬空引脚数)。
|
||||
|
||||
与 `a2_aux.load()` 的两点不同:
|
||||
1. 本页**整页搬**,没有 keep 过滤(U32 一颗芯片就把全页导线连成一片,切不开);
|
||||
2. **允许悬空引脚** —— U32 有 30 个脚是故意不接的(见 NC),
|
||||
`a2_aux` 那句 `assert hit` 在这里会当场炸掉。改成计数,由 `build()` 和 NC 对账。
|
||||
"""
|
||||
src = json.load(io.open(path, encoding='utf-8'))
|
||||
dx, dy = off
|
||||
pts = src['wires']
|
||||
segs = [((int(round(pts[i][0])) + dx, int(round(pts[i][1])) + dy),
|
||||
(int(round(pts[i + 1][0])) + dx, int(round(pts[i + 1][1])) + dy))
|
||||
for i in range(0, len(pts), 2)]
|
||||
for a, b in segs:
|
||||
assert a != b, u'%s 有零长度导线 %s(skill §8a)' % (path, (a,))
|
||||
assert (a[0] == b[0]) != (a[1] == b[1]), u'%s 有斜线 %s-%s' % (path, a, b)
|
||||
|
||||
n = len(segs)
|
||||
par = list(range(n))
|
||||
|
||||
def find(i):
|
||||
while par[i] != i:
|
||||
par[i] = par[par[i]]
|
||||
i = par[i]
|
||||
return i
|
||||
|
||||
for i in range(n):
|
||||
for j in range(i + 1, n):
|
||||
a1, a2 = segs[i]
|
||||
b1, b2 = segs[j]
|
||||
if (_on(a1, b1, b2) or _on(a2, b1, b2)
|
||||
or _on(b1, a1, a2) or _on(b2, a1, a2)):
|
||||
ri, rj = find(i), find(j)
|
||||
if ri != rj:
|
||||
par[ri] = rj
|
||||
|
||||
gname, gpins = defaultdict(set), defaultdict(set)
|
||||
for lst in (src['flags'], src['ports']):
|
||||
for f in lst:
|
||||
xy = (int(round(f[2])) + dx, int(round(f[3])) + dy)
|
||||
hit = [i for i in range(n) if _on(xy, segs[i][0], segs[i][1])]
|
||||
assert hit, u'%s 的旗/端口 %s 不在任何导线上' % (path, f[1])
|
||||
gname[find(hit[0])].add(f[1])
|
||||
nfloat = 0
|
||||
for p in src['parts']:
|
||||
for q in p[9]:
|
||||
xy = (int(round(q[2])) + dx, int(round(q[3])) + dy)
|
||||
hit = [i for i in range(n) if _on(xy, segs[i][0], segs[i][1])]
|
||||
if not hit: # 故意不接的脚,由 NC 兜底核对
|
||||
nfloat += 1
|
||||
continue
|
||||
gpins[find(hit[0])].add('%s.%s' % (p[0], q[0]))
|
||||
|
||||
W, F, PT = [], [], []
|
||||
for r_ in sorted(set(find(i) for i in range(n))):
|
||||
names = gname.get(r_, set())
|
||||
assert len(names) <= 1, u'组 %s 名字不唯一:%s' % (sorted(gpins[r_])[:3], sorted(names))
|
||||
# 无名的短节点(J23 的两个 CC)沿用 R1 不起名,`#` 开头只给本地自检用、不下发
|
||||
net = sorted(names)[0] if names else '#' + '+'.join(sorted(gpins[r_])[:2])
|
||||
for i in range(n):
|
||||
if find(i) == r_:
|
||||
W.append((segs[i][0], segs[i][1], net))
|
||||
|
||||
for f in src['flags']:
|
||||
xy = (int(round(f[2])) + dx, int(round(f[3])) + dy)
|
||||
F.append([f[0].split('-')[0], f[1], xy[0], xy[1], f[4]])
|
||||
hit_dir = 0
|
||||
for p in src['ports']:
|
||||
xy = (int(round(p[2])) + dx, int(round(p[3])) + dy)
|
||||
d, rot = p[0].replace('Netport-', ''), p[4]
|
||||
key = (p[1], xy[0], xy[1])
|
||||
if key in PORT_DIR:
|
||||
d = PORT_DIR[key]
|
||||
rot = PORT_ROT.get(key, rot)
|
||||
hit_dir += 1
|
||||
PT.append([d, p[1], xy[0], xy[1], rot])
|
||||
assert hit_dir == len(PORT_DIR), \
|
||||
u'端口方向修正表只命中 %d/%d(坐标写错了)' % (hit_dir, len(PORT_DIR))
|
||||
|
||||
byname = {}
|
||||
for k, v in PARTS.items():
|
||||
byname.setdefault(v['name'], k)
|
||||
place, exp = [], {}
|
||||
for p in src['parts']:
|
||||
d, nm = p[0], p[1]
|
||||
base = nm.split('_C')[0] if '_C' in nm else nm
|
||||
k = byname.get(nm) or byname.get(base)
|
||||
assert k, u'eda-parts.json 里找不到器件 %r(%s)' % (nm, d)
|
||||
assert not p[6], u'%s 是镜像件,Sheet.place 处理不了' % d
|
||||
place.append([d, k, int(round(p[3])) + dx, int(round(p[4])) + dy, p[5]])
|
||||
exp[d] = dict((str(q[0]), (int(round(q[2])) + dx, int(round(q[3])) + dy))
|
||||
for q in p[9])
|
||||
return W, F, PT, place, exp, nfloat
|
||||
|
||||
|
||||
def assert_clear(W, F, PT, exp, rect):
|
||||
u"""新块要占的矩形里不许有 R1 的任何图元 —— 坐标改错了当场终止,别等截图。"""
|
||||
x0, y0, x1, y1 = rect
|
||||
bad = []
|
||||
for a, b, net in W:
|
||||
for p in (a, b):
|
||||
if x0 <= p[0] <= x1 and y0 <= p[1] <= y1:
|
||||
bad.append(u'导线 %s 端点 %s' % (net, (p,)))
|
||||
if a[0] == b[0] and x0 <= a[0] <= x1 \
|
||||
and min(a[1], b[1]) < y1 and max(a[1], b[1]) > y0:
|
||||
bad.append(u'竖线 %s x=%d 穿过' % (net, a[0]))
|
||||
if a[1] == b[1] and y0 <= a[1] <= y1 \
|
||||
and min(a[0], b[0]) < x1 and max(a[0], b[0]) > x0:
|
||||
bad.append(u'横线 %s y=%d 穿过' % (net, a[1]))
|
||||
for it in F + PT:
|
||||
if x0 <= it[2] <= x1 and y0 <= it[3] <= y1:
|
||||
bad.append(u'旗/端口 %s @(%d,%d)' % (it[1], it[2], it[3]))
|
||||
for d, ps in exp.items():
|
||||
for num, xy in ps.items():
|
||||
if x0 <= xy[0] <= x1 and y0 <= xy[1] <= y1:
|
||||
bad.append(u'引脚 %s.%s @%s' % (d, num, (xy,)))
|
||||
assert not bad, u'新块矩形 %s 里有 R1 的东西:\n %s' % (rect, '\n '.join(sorted(set(bad))))
|
||||
|
||||
|
||||
def add_u31(s):
|
||||
u"""U31 AMS1117-3.3:+5V(IN 端口) → U31 → +3V3_HUB(Power 旗)。
|
||||
|
||||
走法(Y 轴向上):
|
||||
|
||||
+5V ─●──●──────── 3 VIN ← y=255,从左平进,全程无竖线
|
||||
C752 C753 2 VOUT ──< +3V3_HUB ← 635,BI+rot180,符号往左
|
||||
│ │ 1 GND ──┐
|
||||
⏚─────┴────┴──────────┬────┘ ← GND 先往左到 x=350 再下行
|
||||
350,195 y=215 地干线 ──● C737 下脚
|
||||
|
||||
2 脚和 4 脚都是 VOUT(4 脚是散热片),**两个都接**:4 脚往右接 C737 与电源旗,
|
||||
2 脚被 1/3 脚夹在中间(同一个 x、上下各差 10)只能往左,走 y=255 与 y=275 之间那条
|
||||
20 单位的走廊,末端放一个同名 `+3V3_HUB` netport ——
|
||||
netport 符号只有 ~10 高,R1 这一页两列端口就是 10 单位间距排下来的(skill §8b-2)。
|
||||
"""
|
||||
p = U31_PINS
|
||||
# 输入:+5V 干线(C752 / C753 的上脚落在这条线上,EasyEDA 会在引脚处截断,skill §8b)
|
||||
# ⚠ 两颗电容必须待在 x<535 —— 535~635 是下面那个 `+3V3_HUB` 端口标签占的横向范围。
|
||||
s.w('+5V', (440, 255), (C752_C[0], 255), (C753_C[0], 255), p['3'])
|
||||
s.port('IN', '+5V', 440, 255, 0) # IN + rot0 ⇒ 符号在**左**(§8b),占 380~440
|
||||
# 地:U31 GND 左出 → x=350 下行 → 地旗;y=215 是全块的地干线,T 在竖线中段
|
||||
s.w('GND', p['1'], (350, 275), (350, 195))
|
||||
s.gnd(350, 195, 0) # Ground rot0:符号在下、引脚在上,导线从上方来
|
||||
s.w('GND', (350, 215), (C752_C[0], 215), (C753_C[0], 215),
|
||||
(C737_C[0], 215), (C737_C[0], 225))
|
||||
# 输出:4 脚(散热片)→ C737 → 电源旗;2 脚往左出一个同名端口
|
||||
s.w('+3V3_HUB', (635, 265), p['2'])
|
||||
s.port('BI', '+3V3_HUB', 635, 265, 180) # BI + rot180 ⇒ 符号在**左**,占 535~635
|
||||
s.w('+3V3_HUB', p['4'], (C737_C[0], 265), (880, 265), (880, 320))
|
||||
s.pwr('+3V3_HUB', 880, 320, 0) # Power rot0:符号在上、引脚在下,导线从下方来
|
||||
|
||||
|
||||
def add_usb6(s):
|
||||
u"""第 6 个下行口引到右侧端口列(D13)。串阻 / ESD 一律不加,同 R1 的口 1~5。"""
|
||||
for net, y in USB6:
|
||||
s.w(net, (680, y), (760, y))
|
||||
s.port('BI', net, 760, y, 0) # BI + rot0 ⇒ 符号在右,占 760~854
|
||||
|
||||
|
||||
def _text_boxes(s):
|
||||
out = []
|
||||
for x, y, txt, size in s.TX:
|
||||
cjk = sum(1 for ch in txt if ord(ch) > 0x2E7F)
|
||||
w = size * (cjk + 0.5 * (len(txt) - cjk))
|
||||
out.append((x, y - size * 0.8, x + w, y + size * 0.8, txt))
|
||||
return out
|
||||
|
||||
|
||||
def text_clash(s, exp):
|
||||
u"""文字压在导线 / 引脚 / 旗端口上(同 a1_input.py / a2_aux.py)。"""
|
||||
errs = []
|
||||
pts = []
|
||||
for d, ps in exp.items():
|
||||
if '?' in ps:
|
||||
for xy in ps['?']:
|
||||
pts.append((xy[0], xy[1], u'引脚 %s' % d))
|
||||
else:
|
||||
for num, xy in ps.items():
|
||||
pts.append((xy[0], xy[1], u'引脚 %s.%s' % (d, num)))
|
||||
for it in s.F + s.PT:
|
||||
pts.append((it[2], it[3], u'旗/端口 %s' % it[1]))
|
||||
for x0, y0, x1, y1, txt in _text_boxes(s):
|
||||
for px, py, what in pts:
|
||||
if x0 <= px <= x1 and y0 <= py <= y1:
|
||||
errs.append(u'文字压住 %s @(%d,%d): %s' % (what, px, py, txt[:26]))
|
||||
for a, b, net in s.W:
|
||||
if a[1] == b[1]:
|
||||
if y0 <= a[1] <= y1 and min(a[0], b[0]) <= x1 and max(a[0], b[0]) >= x0:
|
||||
errs.append(u'文字压住导线 %s y=%d: %s' % (net, a[1], txt[:26]))
|
||||
else:
|
||||
if x0 <= a[0] <= x1 and min(a[1], b[1]) <= y1 and max(a[1], b[1]) >= y0:
|
||||
errs.append(u'文字压住导线 %s x=%d: %s' % (net, a[0], txt[:26]))
|
||||
return sorted(set(errs))
|
||||
|
||||
|
||||
def label_clash(s):
|
||||
u"""端口的符号+标签往锚点某一侧伸出去 ~40+6.7×字符(skill §8b),会压住页面文字。
|
||||
|
||||
契约 §7 第 8 条点名说这是本项目的常见事故,而 `flag_clash` 的「横向 <20」判据
|
||||
根本够不着。这里把标签算成一个盒子,和**页面文字**逐个比,顺带查越框。
|
||||
"""
|
||||
errs = []
|
||||
tb = _text_boxes(s)
|
||||
for d, net, x, y, rot in s.PT:
|
||||
left = (d == 'IN') == (int(rot) % 360 == 0) # §8b:IN 与 BI/OUT 正好相反
|
||||
# 净空用 6.7/字符(§8b 说是偏保守的安全值);**越框**只能用实测的 5/字符
|
||||
# (`PORTTEST` 8 字符实测 80 单位 = 40 + 5×8),否则 R1 那个贴着右边框的
|
||||
# `NONREM0`(1070 + 40 + 5×7 = 1145,正好压线)会被误判成越框。
|
||||
for k, tag in ((6.7, None), (5.0, 'frame')):
|
||||
w = 40 + k * len(net)
|
||||
bx0, bx1 = (x - w, x) if left else (x, x + w)
|
||||
if tag == 'frame':
|
||||
if bx0 < 20 or bx1 > 1145:
|
||||
errs.append(u'端口 %s @(%d,%d,%s,rot%d) 的标签越框 %.0f~%.0f'
|
||||
% (net, x, y, d, rot, bx0, bx1))
|
||||
continue
|
||||
for tx0, ty0, tx1, ty1, txt in tb:
|
||||
if bx0 < tx1 and tx0 < bx1 and ty0 < y + 6 and y - 6 < ty1:
|
||||
errs.append(u'端口 %s @(%d,%d) 的标签压住文字: %s' % (net, x, y, txt[:26]))
|
||||
return sorted(set(errs))
|
||||
|
||||
|
||||
def flag_clash(s, gx=20, gy=10):
|
||||
u"""两面旗/端口的符号叠在一起(同 a1_input.py / a2_aux.py)。"""
|
||||
errs, items = [], [(it[1], it[2], it[3]) for it in s.F + s.PT]
|
||||
for i in range(len(items)):
|
||||
for j in range(i + 1, len(items)):
|
||||
(n1, x1, y1), (n2, x2, y2) = items[i], items[j]
|
||||
if abs(x1 - x2) < gx and abs(y1 - y2) < gy:
|
||||
errs.append(u'旗/端口 %s @(%d,%d) 与 %s @(%d,%d) 挨太近'
|
||||
% (n1, x1, y1, n2, x2, y2))
|
||||
return sorted(set(errs))
|
||||
|
||||
|
||||
def build():
|
||||
W, F, PT, place, exp, nfloat = load(SRC, OFF)
|
||||
W = shift_block(W, F, PT, place, exp)
|
||||
want_nc = sum(len(v) for v in NC.values()) + len(USB6) # 53/54 在源页上还是悬空的
|
||||
assert nfloat == want_nc, \
|
||||
u'源页悬空引脚 %d 个,NC 表 + 第 6 口 = %d 个(对不上就是漏接或者表过期)' \
|
||||
% (nfloat, want_nc)
|
||||
assert_clear(W, F, PT, exp, BLOCK)
|
||||
|
||||
s = Sheet(PAGE, NAME)
|
||||
s.W += W
|
||||
s.F += F
|
||||
s.PT += PT
|
||||
add_u31(s)
|
||||
add_usb6(s)
|
||||
for t in TEXTS:
|
||||
s.txt(t[0], t[1], t[2], t[3])
|
||||
|
||||
place = place + [list(x) for x in NEW_PARTS]
|
||||
exp = dict(exp)
|
||||
exp['U31'] = dict(U31_PINS) # 4 脚件,逐脚断言
|
||||
exp['C752'] = two_pin(C752_C[0], C752_C[1]) # 无极性,只比坐标集合
|
||||
exp['C753'] = two_pin(C753_C[0], C753_C[1])
|
||||
exp['C737'] = {'1': (C737_C[0], C737_C[1] + 20), # ⚠ 钽电容:1 脚是正极,必须在上
|
||||
'2': (C737_C[0], C737_C[1] - 20)} # 跨距 ±20、rot0(本页实测,见 C737_C 处注释)
|
||||
assert len(set(d for d, _k, _x, _y, _r in place)) == len(place), u'位号撞车'
|
||||
# 契约 §2:A4 只许用 U31 / U32 / Y1 / J23 + R708~R730 + C737 / C740~C760
|
||||
for d, _k, _x, _y, _r in place:
|
||||
ok = d in ('U31', 'U32', 'Y1', 'J23') \
|
||||
or (d[0] == 'R' and d[1:].isdigit() and 708 <= int(d[1:]) <= 730) \
|
||||
or (d == 'C737') \
|
||||
or (d[0] == 'C' and d[1:].isdigit() and 740 <= int(d[1:]) <= 760)
|
||||
assert ok, u'位号 %s 不在契约 §2 给 A4 的区段里' % d
|
||||
# 契约 §3 第 3 条 + 本页不许出现 +12V
|
||||
for it in s.F + s.PT:
|
||||
assert it[1] not in ('+3V3', '+12V'), u'本页不该出现 %s' % it[1]
|
||||
return s, place, exp
|
||||
|
||||
|
||||
def main():
|
||||
push = '--push' in sys.argv
|
||||
s, place, exp = build()
|
||||
s.assert_grid()
|
||||
pins = {}
|
||||
for d, want in exp.items():
|
||||
if '?' in want:
|
||||
a, b = sorted(want['?'])
|
||||
pins[d] = {'1': a, '2': b}
|
||||
else:
|
||||
pins[d] = dict(want)
|
||||
errs = s.netcheck(pins, NC, ALLOW_SPLIT)
|
||||
errs += text_clash(s, exp) + label_clash(s) + flag_clash(s)
|
||||
# 契约 §3 第 1 条的硬约束复核:HUB_RST 必须有一颗 10k 上拉到 +3V3_HUB
|
||||
assert PARTS['C25744']['name'] == '0402WGF1002TCE', u'C25744 不再是 10k 电阻'
|
||||
r716 = [it for it in place if it[0] == 'R716']
|
||||
assert len(r716) == 1 and r716[0][1] == 'C25744', u'R716 不是 10k(HUB_RST 上拉没了)'
|
||||
assert pins['R716']['1'] == (1130, 320) and pins['R716']['2'] == (1130, 280), \
|
||||
u'R716(HUB_RST 上拉)位置变了,重新核对 §3 第 1 条'
|
||||
# 第 6 个下行口确实接出去了
|
||||
for net, y in USB6:
|
||||
assert any(it[1] == net and it[0] == 'BI' for it in s.PT), u'%s 端口没画' % net
|
||||
print(u'R1 C2-HUB 原样搬 %d 件 + 新增 %d 件(U31 块)= %d 件'
|
||||
% (len(place) - len(NEW_PARTS), len(NEW_PARTS), len(place)))
|
||||
s.report(len(place), errs)
|
||||
if not push:
|
||||
return
|
||||
call("await eda.dmt_Project.openProject('%s'); await new Promise(r=>setTimeout(r,2500)); return 1;"
|
||||
% R2_PROJECT)
|
||||
s.clear()
|
||||
s.place(place, exp)
|
||||
s.push_geometry()
|
||||
print(u'接着跑 verify_sch.py --nets')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
在新工单中引用
屏蔽一个用户