# -*- coding: utf-8 -*- u"""R2 底板 **A6-FAN** 页:风墙驱动(高边总开关 + 反相 PWM + 两路测速)+ 两路 NTC 测温。 /c/Python314/python .claude/r2/a6_fan.py # 只算 + 本地自检 /c/Python314/python .claude/r2/a6_fan.py --push # 清页 + 重画(幂等) ## 内容从哪来 | 块 | 来源 | 处理 | |---|---|---| | 风扇全部(`Q6~Q10` `D2` `DZ3` `J26/J27` `C738` `R733/R734/R746~R757`)| R1 `P6-FAN-EDL`(`_p6_live.json`)| **拓扑一字不改**,版图重排 | | NTC(`RT601` `RT602` + 分压电阻 + 滤波电容)| R1 `P3-CONTROL`(`_p3_live.json`)| 拓扑照搬,**位号改号** | ⛔ **EDL 部分不在本页**(已随方案挪到刀片 `B3-PHONE`)。 实测 `_p6_live.json` 里 **一件 EDL 器件都没有** —— 24 个元件全是风扇块 (R1 早在 2026-09-06 就把第三片 74HC595 / `U33` 整条链挪到载板 `C6-KEYS` 了, 页面 `texts` 里还留着「已搬走」那两行说明)。所以没有「逐件挑掉」这一步, `REF_KEEP` 就是 dump 里的全部 24 件,脚本用它做逐脚拓扑对账(见下)。 ## 位号改号(NTC 那一块) R1 P3 的 NTC 上臂用的是 `R601~R650` 那一档(`R618` / `R619`,10k `C25744`), 而契约 §2 把 `R601~R650` 整段划给了 **A3-CTRL**。⇒ 本页改用 A6 的新号段 `R660~R699`: | R1 P3 | 本页 | 是什么 | |---|---|---| | `R618` | **`R660`** | NTC1 分压上臂 10k → `+3V3` | | `R619` | **`R661`** | NTC2 分压上臂 10k → `+3V3` | | `C613` | **`C736`** | NTC1 节点滤波 100nF | | `C614` | **`C739`** | NTC2 节点滤波 100nF | | `RT601` / `RT602` | 原名 | 契约 §2 明确点名给 A6 | ⚠ 契约 §2 只给了 A6 电阻新号段,**没给电容号段**(`C738` 是单点分配)。 `C736` / `C739` 是全表唯一两个没人认领的编号(A2 到 `C735`、A4 从 `C737`/`C740` 起、 A5 `C760~C790`),选它们既不撞车也不用改契约。**已在汇报里单列。** ## 版图为什么不是 1:1 平移 `schcheck.Sheet.place()` 的 `create(..., mirror=false)` 写死不支持镜像, 而 R1 P6 上 **`Q7` / `Q9` / `Q10` 三颗 MOS 是 `mirror=True`** 放的(栅极在右)。 ⇒ 这三颗改用 **`rot=180` 不镜像**,栅极同样在右,但漏源上下互换: AO3400A/AO3401A rot 0 : G(cx-20,cy) D(cx+10,cy+20) S(cx+10,cy-20) rot180: G(cx+20,cy) D(cx-10,cy-20) S(cx-10,cy+20) - `Q7`(P-MOS 高边)rot180 后 **S 在上 / D 在下**,正好还是「源极接 +12V 母线、 漏极向下出 `FAN_12V_SW`」,只是栅极网络要挪到它右边 ⇒ `DZ3` / `R746` / `R747` 从 x=150/240 挪到 x=380/470,`Q7` 留在 x=300。 - `Q9` / `Q10`(TACH 接收 N-MOS)rot180 后 **D 在下 / S 在上**,所以这两颗的 地旗在上(`Ground rot=180`)、`+3V3` 上拉旗在下(`Power rot=180`)。 看着别扭但电气一致;换成 rot0 会把栅极甩到左边,而原始 TACH 是从右边的风扇座 过来的,绕线要横穿 MOS 本体,更糟。**这是本页唯一一处违反「电源在上、地在下」的地方。** 改完靠 `topo_check()` 兜底:把 R1 dump 和本页各自**按几何反推一遍网表**, 再逐个 `位号.脚号` 比对分组,**完全不看坐标、不看网络名**。 只要拓扑动了一根线就会当场终止。 ## 三条硬约束怎么落实的 1. **`FAN_EN` 复位态关电**(契约 §3 第 2 条):`FAN_EN` → `R748` 1k → `Q8` 栅极, `R749` 100k 下拉;`Q8` 关 ⇒ `R746` 100k 把 `Q7` 栅极拉回源极 ⇒ P-MOS 截止。 ESP32 高阻 / 复位期间风扇不转。**拓扑照抄 R1,一点没简化。** 2. **`DZ3` 钳位**:`AO3401A` 的 `Vgs` 绝对最大 ±12V(`eda-parts` `C15127` 的 role), 13.2V 母线直接把栅极拉到地就超了。`DZ3`(BZT52C8V2) **K 接源极 / A 接栅极**。 3. **`D2`(SS34)**:**K 接 `FAN_12V_SW` / A 接 `GND`**,做负向钳位 (高边拓扑下它不再是低边续流二极管,见 `FAN_CIRCUIT.md`)。 ⚠ 2 和 3 都是极性件,本项目已经在极性件上栽过三次(C60 电解、U5 镜像、C801)。 所以 `EXPECT` 里 **`D2` / `DZ3` 写死脚号→绝对坐标**(`'1': K 在上, '2': A 在下`), 不是 `two_pin()` 那种只比坐标集合的宽松判据 —— 翻 180° 会当场被 `place()` 判死。 `eda-parts` 的 role 说 `DZ3` 要 `create(rot=90)`、`D2` 要 `setState_Rotation(270)`, 两句话本身就打架;**本脚本一律传 270 并按脚号断言**,`place()` 的翻转重试循环 会把真实方向试出来,对不上就终止。方向表一张都别信(skill §6b/§6c)。 """ 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, PARTS # noqa: E402 PAGE, NAME = '68c9ffaa578572d3', 'A6-FAN' R2_PROJECT = '05a65c22f14846b8b95360ef8d3c1e2e' REF_P6 = os.path.join(HERE, '_p6_live.json') # R1 P6-FAN-EDL(实测只剩风扇) REF_P3 = os.path.join(HERE, '_p3_live.json') # R1 P3-CONTROL(只取 NTC 六件) # 器件键(`.claude/eda-parts.json`) K_NMOS = 'C20917' # AO3400A K_PMOS = 'C15127' # AO3401A(Vgs ±12V ⇒ 必配 DZ3) K_SS34 = 'C8678' # SS34 肖特基 K_ZEN = 'C2891776' # BZT52C8V2 8.2V K_FAN4 = 'C2847671' # 2510-4A 四线风扇座 K_22U = 'C12891' # 22uF/25V 1206(引脚 ±15,不是 ±20) K_100N = 'C1525' # 100nF 0402 K_NTC = 'C13564' # NCP18XH103F03RB 10k B=3380K K_1K = 'C11702' K_10K = 'C25744' K_15K = 'C25756' # TACH 栅极串阻(10k 会让高电平不够) K_100K = 'C25741' DY = -240 # 排风通道 = 进风通道整体下移 NTC_DX = 260 # NTC 第二路 = 第一路整体右移 # ── R1 → 本页的位号改号(NTC 那一块,见文档串)──────────────────────── NTC_REMAP = {'R618': 'R660', 'R619': 'R661', 'C613': 'C736', 'C614': 'C739'} NTC_KEEP = set(['RT601', 'RT602', 'R660', 'R661', 'C736', 'C739']) # ── 拓扑对账时要把 R1 侧的 1/2 脚对调的非极性件 ────────────────────── # R1 上 R753/R757 的上拉在漏极**上方**(p2 接 +3V3);本页 Q9/Q10 转了 180°, # 漏极在下,上拉挪到漏极**下方**(p1 接 +3V3)。电阻无极性,电气完全一致, # 但按「位号.脚号」比对会不一致 ⇒ 在参照侧对调后再比。 REF_SWAP = set(['R753', 'R757']) # ══════════════════════════════════════════════════════════════════════ # 引脚期望(skill §6b/§6c:方向表不可信,一律按脚号断言绝对坐标) # ══════════════════════════════════════════════════════════════════════ def rc(x, y, rot, half=20): u"""两脚无极性件(R / C / NTC)。实测:rot0 → 1 在左;rot90 → 1 在下;rot270 → 1 在上。""" if rot == 0: return {'1': (x - half, y), '2': (x + half, y)} if rot == 90: return {'1': (x, y - half), '2': (x, y + half)} if rot == 270: return {'1': (x, y + half), '2': (x, y - half)} raise AssertionError(u'rc 只支持 0/90/270') def mos(x, y, rot): u"""SOT-23 MOS:脚号 1=G 2=S 3=D(AO3400A 与 AO3401A 同一套偏移,实测)。""" if rot == 0: return {'1': (x - 20, y), '3': (x + 10, y + 20), '2': (x + 10, y - 20)} if rot == 180: return {'1': (x + 20, y), '3': (x - 10, y - 20), '2': (x - 10, y + 20)} raise AssertionError(u'mos 只支持 0/180') def diode_k_up(x, y): u"""极性件:pin1 = K 在上,pin2 = A 在下。**写死脚号,不用坐标集合。**""" return {'1': (x, y + 20), '2': (x, y - 20)} def fan4(x, y): u"""2510-4A:全部在左侧,1=GND 2=FAN_12V_SW 3=TACH 4=PWM(eda-parts role)。""" return {'1': (x - 20, y + 15), '2': (x - 20, y + 5), '3': (x - 20, y - 5), '4': (x - 20, y - 15)} # ══════════════════════════════════════════════════════════════════════ # 放置表 # ══════════════════════════════════════════════════════════════════════ def placement(): p = [ # ── A 高边总开关:FAN_EN → Q8 → Q7 → FAN_12V_SW ──────────────── ['R748', K_1K, 200, 560, 0], # FAN_EN 栅极串阻 ['R749', K_100K, 230, 510, 90], # Q8 栅极下拉(复位默认关电) ['Q8', K_NMOS, 280, 560, 0], ['R747', K_1K, 380, 660, 90], # Q8 漏极 → Q7 栅极 ['DZ3', K_ZEN, 380, 750, 270], # K 接源极 / A 接栅极 ['R746', K_100K, 470, 750, 90], # Q7 栅极拉回源极 ['Q7', K_PMOS, 300, 730, 180], # rot180:G 在右 / S 在上 / D 在下 # ── C FAN_12V_SW 上的钳位与储能 ──────────────────────────────── ['C738', K_22U, 190, 650, 270], ['D2', K_SS34, 250, 650, 270], # K 接 FAN_12V_SW / A 接 GND # ── B PWM 反相器:FAN_PWM → Q6 → FAN_PWM_L(开漏)────────────── ['R733', K_1K, 200, 370, 0], ['R734', K_100K, 250, 310, 270], ['Q6', K_NMOS, 320, 370, 0], # ── D 进风测速 + J26 ─────────────────────────────────────────── ['Q9', K_NMOS, 750, 720, 180], ['R750', K_10K, 960, 750, 90], # TACH 上拉到 FAN_12V_SW ['R751', K_15K, 810, 720, 0], # 栅极串阻(10k 高电平不够) ['R752', K_10K, 790, 680, 90], # 栅极对地 ['R753', K_10K, 740, 650, 90], # 漏极上拉 +3V3 ['J26', K_FAN4, 1085, 725, 0], # ── NTC1 / NTC2(从 R1 P3-CONTROL 搬来)──────────────────────── ['RT601', K_NTC, 600, 270, 270], ['R660', K_10K, 600, 330, 270], # ⚠ 从 x=660 挪到 700:RT601 的型号文字「NCP18XH103F03RB」画在符号右边、约 65 单位宽, # 原来 60 的间距下正好压住 C736 的位号/值(渲染成 …F03RB0nF),和 A2-AUX 的 # C733「100uF0」是同一类事故 —— 六项几何验证与 flag_clash 都够不着,只有放大截图看得出来。 ['C736', K_100N, 700, 270, 270], ] # 排风通道 = 进风通道整体下移 DY,位号 +4 for d, k, x, y, r in [it for it in p if it[0] in ('Q9', 'R750', 'R751', 'R752', 'R753', 'J26')]: out = {'Q9': 'Q10', 'R750': 'R754', 'R751': 'R755', 'R752': 'R756', 'R753': 'R757', 'J26': 'J27'}[d] p.append([out, k, x, y + DY, r]) # NTC 第二路 = 第一路整体右移 for d, k, x, y, r in [it for it in p if it[0] in ('RT601', 'R660', 'C736')]: out = {'RT601': 'RT602', 'R660': 'R661', 'C736': 'C739'}[d] p.append([out, k, x + NTC_DX, y, r]) return p def expect(): e = {} for d, _k, x, y, r in placement(): if d in ('Q6', 'Q7', 'Q8', 'Q9', 'Q10'): e[d] = mos(x, y, r) elif d in ('D2', 'DZ3'): e[d] = diode_k_up(x, y) # 极性件:写死脚号 elif d in ('J26', 'J27'): e[d] = fan4(x, y) elif d == 'C738': e[d] = rc(x, y, r, 15) # 1206 是 ±15(skill §8d) else: e[d] = rc(x, y, r) return e # ══════════════════════════════════════════════════════════════════════ # 页面文字 # ══════════════════════════════════════════════════════════════════════ TEXTS = [ (45, 245, u'【A6-FAN】风墙:高边总开关 + 反相 PWM + 两路独立测速 + 两路 NTC', 11), (45, 225, u'FAN_EN 复位/高阻 → Q8 截止 → R746(100k) 把 Q7 栅极拉回源极 → 风扇断电', 10), (45, 207, u'(契约 §3 第 2 条:复位态必须关电)R749 100k 保证 Q8 栅极不浮空', 10), (45, 189, u'DZ3 BZT52C8V2:K 接 Q7 源极、A 接栅极,把 Vgs 钳在 -8.2V', 10), (45, 171, u'AO3401A 的 Vgs 绝对最大 ±12V,13.2V 母线直接拉地就超了', 10), (45, 153, u'Q6 开漏反相器:栅极高 = 风扇 PWM 线被拉低,固件 D_gpio = 1 - D_fan', 10), (45, 135, u'0% PWM 风扇仍转约 3300RPM,要停转只能撤 FAN_EN,不能靠占空比', 10), (45, 117, u'D2 SS34:K 接 FAN_12V_SW、A 接 GND,负向钳位(不是低边续流)', 10), (45, 99, u'测速:10k 上拉到 FAN_12V_SW,15k/10k 分压 → 栅极 3.43V@12V;', 10), (45, 81, u'漏极 10k 上拉 +3V3,反相输出。ESP32 只看到 0/3.3V,不接触 12V', 10), (45, 63, u'首板只装一只风扇(方案 §0.2 A3):空着那一路 TACH 悬空,被 10k', 10), (45, 45, u'拉到 12V → 接收 MOS 常通 → GPIO 恒低无跳变;关电时则恒高。', 10), (45, 27, u'两种都是 0 脉冲,固件必须按配置屏蔽未装的那一路,否则误报停转', 10), (410, 665, u'NTC:RT601/RT602 10k B=3380K,上臂 R660/R661 10k 接 +3V3', 10), (410, 647, u'25℃ 分压 1.65V;NTC1/NTC2 必须进 ESP32 的 ADC1(IO1~IO10)', 10), (410, 629, u'摆位:J26/J27 在底板 y>114.5,刀片投影外,可放高件(§6.6)', 10), (410, 611, u'⚠ 四针针序与旧的 3 针风扇座不同,装配前先对丝印', 10), (835, 615, u'J26 = 进风,J27 = 排风(2510-4A 四线直针)', 10), (835, 595, u'1=GND(黑) 2=FAN_12V_SW(红) 3=TACH(黄) 4=PWM(棕)', 10), ] # 每一列文字的右边界(`assert_grid` 只管标题栏那条,这里再管一次不许压到电路) TEXT_CAP = [(200, 540), (800, 790), (10000, 1145)] # ══════════════════════════════════════════════════════════════════════ # 连线 # ══════════════════════════════════════════════════════════════════════ def build(): s = Sheet(PAGE, NAME) w, g, p, pt, tx = s.w, s.gnd, s.pwr, s.port, s.txt # ── A 高边总开关 ────────────────────────────────────────────────── w('FAN_EN', (140, 560), (180, 560)) pt('IN', 'FAN_EN', 140, 560, 0) # IN + rot0 ⇒ 标签朝左 w('#Q8_G', (220, 560), (260, 560)) # R748.2 → Q8.G w('#Q8_G', (230, 530), (230, 560)) # R749.2 w('GND', (230, 460), (230, 490)) g(230, 460, 0) w('GND', (290, 500), (290, 540)) # Q8.S g(290, 500, 0) w('#Q8_D', (290, 580), (380, 580)) w('#Q8_D', (380, 580), (380, 640)) # → R747.1 w('#Q7_G', (380, 680), (380, 730)) # R747.2 → 栅极母线 w('#Q7_G', (320, 730), (470, 730)) # Q7.G / DZ3.A / R746.1 w('+12V', (290, 750), (290, 790)) # Q7.S w('+12V', (290, 790), (545, 790)) w('+12V', (380, 770), (380, 790)) # DZ3.K w('+12V', (470, 770), (470, 790)) # R746.2 p('+12V', 545, 790, 0) # ── C FAN_12V_SW 母线 + 钳位 / 储能 ─────────────────────────────── w('FAN_12V_SW', (290, 690), (290, 710)) # Q7.D w('FAN_12V_SW', (140, 690), (290, 690)) p('FAN_12V_SW', 140, 690, 0) w('FAN_12V_SW', (190, 665), (190, 690)) # C738.1 w('GND', (190, 600), (190, 635)) g(190, 600, 0) w('FAN_12V_SW', (250, 670), (250, 690)) # D2.1 = K w('GND', (250, 600), (250, 630)) # D2.2 = A g(250, 600, 0) # ── B PWM 反相器 ────────────────────────────────────────────────── w('FAN_PWM', (140, 370), (180, 370)) pt('IN', 'FAN_PWM', 140, 370, 0) w('#Q6_G', (220, 370), (300, 370)) # R733.2 → Q6.G w('#Q6_G', (250, 330), (250, 370)) # R734.1 w('GND', (250, 280), (250, 290)) g(250, 280, 0) w('GND', (330, 350), (370, 350)) # Q6.S w('GND', (370, 320), (370, 350)) g(370, 320, 0) w('FAN_PWM_L', (330, 390), (330, 430)) # Q6.D(开漏,靠风扇内部上拉) w('FAN_PWM_L', (330, 430), (400, 430)) pt('BI', 'FAN_PWM_L', 400, 430, 0) # ── D/E 两路测速接收 + 风扇座 ───────────────────────────────────── for dy, raw, out in ((0, 'FAN_IN_TACH_RAW', 'FAN_TACH_IN'), (DY, 'FAN_OUT_TACH_RAW', 'FAN_TACH_OUT')): gate = '#Q9_G' if dy == 0 else '#Q10_G' w('GND', (1050, 740 + dy), (1065, 740 + dy)) # J.1 w('GND', (1050, 740 + dy), (1050, 770 + dy)) g(1050, 770 + dy, 180) w('FAN_12V_SW', (1030, 730 + dy), (1065, 730 + dy)) # J.2 w('FAN_12V_SW', (1030, 730 + dy), (1030, 790 + dy)) p('FAN_12V_SW', 1030, 790 + dy, 0) w('FAN_PWM_L', (1020, 710 + dy), (1065, 710 + dy)) # J.4 w('FAN_PWM_L', (1020, 670 + dy), (1020, 710 + dy)) pt('BI', 'FAN_PWM_L', 1020, 670 + dy, 180) w(raw, (830, 720 + dy), (1065, 720 + dy)) # J.3 → R751.2 w(raw, (960, 720 + dy), (960, 730 + dy)) # R750.1 w(raw, (990, 650 + dy), (990, 720 + dy)) pt('BI', raw, 990, 650 + dy, 180) w('FAN_12V_SW', (960, 770 + dy), (960, 790 + dy)) # R750.2 p('FAN_12V_SW', 960, 790 + dy, 0) w(gate, (770, 720 + dy), (790, 720 + dy)) # Q9.G ← R751.1 w(gate, (790, 700 + dy), (790, 720 + dy)) # R752.2 w('GND', (790, 630 + dy), (790, 660 + dy)) # R752.1 g(790, 630 + dy, 0) w('GND', (740, 740 + dy), (740, 770 + dy)) # Q9.S(rot180 ⇒ 在上) g(740, 770 + dy, 180) w(out, (740, 670 + dy), (740, 700 + dy)) # Q9.D ← → R753.2 w(out, (680, 690 + dy), (740, 690 + dy)) pt('OUT', out, 680, 690 + dy, 180) w('+3V3', (740, 610 + dy), (740, 630 + dy)) # R753.1 p('+3V3', 740, 610 + dy, 180) # ── NTC 两路(R1 P3-CONTROL 搬来,上臂改号 R660/R661)───────────── for dx, net in ((0, 'NTC1'), (NTC_DX, 'NTC2')): w('+3V3', (600 + dx, 350), (600 + dx, 360)) # R660.1 p('+3V3', 600 + dx, 360, 0) w(net, (600 + dx, 290), (600 + dx, 310)) # RT601.1 ← → R660.2 w(net, (600 + dx, 300), (760 + dx, 300)) w(net, (700 + dx, 290), (700 + dx, 300)) # C736.1 pt('OUT', net, 760 + dx, 300, 0) w('GND', (600 + dx, 220), (600 + dx, 250)) # RT601.2 g(600 + dx, 220, 0) w('GND', (700 + dx, 220), (700 + dx, 250)) # C736.2 g(700 + dx, 220, 0) for t in TEXTS: tx(t[0], t[1], t[2], t[3]) return s # ══════════════════════════════════════════════════════════════════════ # 本地自检:几何 + 与 R1 的逐脚拓扑对账 # ══════════════════════════════════════════════════════════════════════ def _uf(segs): 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 return find def partition(segs, named, pins, where, strict=True): u"""纯几何反推网表:返回「同一网络的 `位号.脚号` 集合」的集合。 同名旗/端口所在的组按名字合并(`GND` / `+12V` 这类天然分块的网络); 无名组各自成网。**不看坐标、不看我自己声明的网络名** —— 这样才能用来 和 R1 的 dump 对账。 """ find = _uf(segs) gname, gpins = defaultdict(set), defaultdict(set) for name, xy in named: hit = [i for i in range(len(segs)) if _on(xy, segs[i][0], segs[i][1])] if not hit: assert not strict, u'%s:旗/端口 %s @%s 不在任何导线上' % (where, name, xy) continue gname[find(hit[0])].add(name) for d, pd in pins.items(): for num, xy in pd.items(): hit = [i for i in range(len(segs)) if _on(tuple(xy), segs[i][0], segs[i][1])] if not hit: assert not strict, u'%s:悬空引脚 %s.%s @%s' % (where, d, num, xy) continue gpins[find(hit[0])].add('%s.%s' % (d, num)) merged = defaultdict(set) for r, ps in gpins.items(): names = sorted(gname.get(r, set())) assert len(names) <= 1, u'%s:一个组挂了多个名字 %s' % (where, names) merged[names[0] if names else '#%d' % r] |= ps return frozenset(frozenset(v) for v in merged.values()) def restrict(part, keep, remap=None, swap=()): u"""只留下 keep 里的位号;顺便做位号改名与「非极性件 1/2 脚对调」。""" flip = {'1': '2', '2': '1'} out = set() for grp in part: cur = set() for it in grp: d, num = it.split('.') if d in swap: num = flip[num] d = (remap or {}).get(d, d) if d in keep: cur.add('%s.%s' % (d, num)) if cur: out.add(frozenset(cur)) return frozenset(out) def load_ref(path): src = json.load(io.open(path, encoding='utf-8')) r = lambda v: int(round(v)) # noqa: E731 pts = src['wires'] segs = [((r(pts[i][0]), r(pts[i][1])), (r(pts[i + 1][0]), r(pts[i + 1][1]))) for i in range(0, len(pts), 2)] segs = [t for t in segs if t[0] != t[1]] named = ([(f[1], (r(f[2]), r(f[3]))) for f in src['flags']] + [(q[1], (r(q[2]), r(q[3]))) for q in src['ports']]) pins = dict((q[0], dict((str(z[0]), (r(z[2]), r(z[3]))) for z in q[9])) for q in src['parts']) return segs, named, pins, set(pins) def topo_check(s, exp): u"""把本页与 R1 两页的网表分组逐个比对。差一根线就报出来。""" errs = [] mine = partition([(a, b) for a, b, _n in s.W], [(it[1], (it[2], it[3])) for it in s.F + s.PT], exp, u'本页') segs, named, pins, allref = load_ref(REF_P6) ref6 = partition(segs, named, pins, u'R1 P6', strict=False) edl = sorted(d for d in allref if d.startswith('U4') or d.startswith('J6')) assert not edl, u'_p6_live.json 里居然有 EDL 器件 %s,要逐件挑掉' % (edl,) a = restrict(ref6, allref, swap=REF_SWAP) b = restrict(mine, allref) for grp in sorted(a - b, key=lambda z: sorted(z)): errs.append(u'R1 P6 有、本页没有的网络分组: %s' % (sorted(grp),)) for grp in sorted(b - a, key=lambda z: sorted(z)): errs.append(u'本页多出来的网络分组: %s' % (sorted(grp),)) segs, named, pins, _all3 = load_ref(REF_P3) ref3 = partition(segs, named, pins, u'R1 P3', strict=False) a = restrict(ref3, NTC_KEEP, remap=NTC_REMAP) b = restrict(mine, NTC_KEEP) for grp in sorted(a - b, key=lambda z: sorted(z)): errs.append(u'R1 P3 的 NTC 块有、本页没有的分组: %s' % (sorted(grp),)) for grp in sorted(b - a, key=lambda z: sorted(z)): errs.append(u'本页 NTC 块多出来的分组: %s' % (sorted(grp),)) return errs def geom_check(s, exp): u"""几件 `netcheck` 管不到、但本项目真踩过的事故。""" errs = [] # skill §8a:旗/端口的引脚不许和元件引脚同坐标(零长度导线,重画就断) pinxy = {} for d, pd in exp.items(): for num, xy in pd.items(): key = tuple(xy) assert key not in pinxy, u'引脚重叠: %s.%s 与 %s @%s' % (d, num, pinxy[key], key) pinxy[key] = '%s.%s' % (d, num) for it in s.F + s.PT: if (it[2], it[3]) in pinxy: errs.append(u'旗/端口 %s 压在元件引脚 %s 上(skill §8a)' % (it[1], pinxy[(it[2], it[3])])) # 文字右边界(分列各自的上限) for x, y, txt, size in s.TX: cjk = sum(1 for ch in txt if ord(ch) > 0x2E7F) right = x + size * (cjk + 0.5 * (len(txt) - cjk)) cap = next(c for lo, c in TEXT_CAP if x < lo) if right > cap: errs.append(u'文字右边界 %.0f 超过本列上限 %d: %s' % (right, cap, txt[:22])) return errs def text_clash(s, exp): u"""文字压在导线 / 引脚 / 旗端口上(同 a1_input.py / a2_aux.py)。""" errs, boxes, pts = [], [], [] for x, y, txt, size in s.TX: cjk = sum(1 for ch in txt if ord(ch) > 0x2E7F) boxes.append((x, y - size * 0.8, x + size * (cjk + 0.5 * (len(txt) - cjk)), y + size * 0.8, txt)) for d, ps in exp.items(): 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 boxes: 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[:22])) 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[:22])) elif 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[:22])) return sorted(set(errs)) def flag_clash(s, gx=20, gy=10): 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)) # `ALLOW_SPLIT` 只放行「靠同名旗/端口成网」的电源与三处开漏节点(契约 §7 第 10 条): # GND / +3V3 / FAN_12V_SW —— 多面旗,导线上天然分块 # FAN_PWM_L —— Q6 漏极一处 + 两个风扇座各一处,共 3 个同名端口 ALLOW_SPLIT = set(['GND', '+3V3', 'FAN_12V_SW', 'FAN_PWM_L']) NC = {} # 本页没有悬空不接的引脚 def main(): push = '--push' in sys.argv s = build() exp = expect() s.assert_grid() for d, _k, _x, _y, _r in placement(): assert _k in PARTS, u'eda-parts.json 里没有器件键 %s(%s)' % (_k, d) errs = s.netcheck(exp, NC, ALLOW_SPLIT) errs += geom_check(s, exp) + topo_check(s, exp) errs += text_clash(s, exp) + flag_clash(s) print(u'风扇块 %d 件(R1 P6 原样)+ NTC 块 6 件(R1 P3 搬来,上臂改号 R660/R661)' % (len(placement()) - 6)) s.report(len(placement()), 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(placement(), exp) s.push_geometry() print(u'接着跑 verify_sch.py --nets 和 .claude/xpage.py') if __name__ == '__main__': main()