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