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
这个提交包含在:
zhou yang
2026-09-10 21:02:10 +08:00
共同撰写人 Claude Opus 5
父节点 18d3d69856
当前提交 efcba30a0c
共修改 280 个文件,包含 92007 行新增和 15053 行删除
+740
查看文件
@@ -0,0 +1,740 @@
# -*- coding: utf-8 -*-
u"""R2 底板 **A3-CTRL** 页:ESP32-S3 主控 + TCA9548A I²C 多路 + DAC63204 ×2 + 应急 UART。
/c/Python314/python .claude/r2/a3_ctrl.py # 只算 + 本地自检
/c/Python314/python .claude/r2/a3_ctrl.py --push # 清页 + 重画(幂等)
四页并行设计里的第三页。跨页网络名 / 位号区段 / 端口方向以 `.claude/r2/A-CONTRACT.md` 为准。
## 页面分区
| 区 | 位置 | 内容 |
|---|---|---|
| A | 左上 x37..600 y370..812 | `U20` ESP32-S3-WROOM-1 + 3V3 去耦(C601/C602)+ EN 上电复位(R601/C603)+ 26 根信号引出 |
| B | 左下 x35..245 y85..380 | 上拉电阻组 R602~R611(I²C 主干 ×2、INT#、INA_ALERT、PRSNT1#~5#、MUX_RST#)|
| C | 中部 x303..570 y395..480 | `J33` 应急 UART 排针(GND/TXD0/RXD0/EN/IO0),塞在 U20 与 U35 之间那条空带 |
| D | 中下 x246..612 y185..440 | `U35` TCA9548A(0x70)+ VCC 去耦 C604 |
| E | 右下 x613..1133 y200..440 | `U6` DAC63204(0x4A)整块从 R1 P3B 搬来 |
| F | 右上 x613..1133 y470..710 | `U7` DAC63204(0x4B)整块从 R1 P3B 搬来 |
## 从 R1 搬了什么、丢了什么
- **ESP32 那一块没有整块 `load()` 搬**,只照抄了 U20 / C601 / C602 / R601 / C603
五件的**电气拓扑**(3V3 去耦 + EN 的 10k/100nF 上电复位),导线重画。
理由:R2 的信号集合和 R1 完全不是一回事(去掉 J22、RT601/602、KEY_*、VBUS_EN*、
BUCK_PG,加上 PRSNT#×5、MUX_RST#、HUB_RST),26 根引出线要重新指派引脚,
把 R1 的线搬过来再逐条改名比重画更容易出错。位置也重排过:R1 的 R601/C603 在 x=120,
搬到 x=130 之后位号文字(画在符号右侧)会顶到 U20 的符号体(x≥205),所以整列重摆。
- **两颗 DAC 是真·整块搬**(`load()`,见下),U23 PCA9555 及其外围一件不要。
## DAC 两块的平移量与「为什么不能并排放在底部」
R1 `P3B-EXPAND` 的 DAC 区是一条 **x33..1123 × y220..460 的横带**(含旗/端口符号),
整条搬过来会横穿 U20 的地线(U20 的 GND/EPAD 引出到 x=320、y380..440),
而且源坐标 `(310,430)` 那条竖线的**端点**正好落在 U20 地线的 y=430 横段上 ——
那不是"交叉",是 **T 型相连 = 把 GND 短到 DAC 的 EP 网络上**。所以必须拆成两块竖着摞:
| 块 | 平移 | 落位 |
|---|---|---|
| U6(源 x33..553 / y220..460)| dx=+580 dy=−20 | x613..1133 / y200..440 |
| U7(源 x563..1123 / y220..460)| dx=+50 dy=+250 | x613..1133 / y470..710 |
dy 的下界是**标题栏**:U6 的 `VDAC4` 端口在源坐标 (480,260),dy 再小 8 个单位
它就掉进 `x>455 且 y<192` 的标题栏里(skill §8c)。`build()` 里逐个断言了这一条。
## ⭐ GPIO 重新指派(本页最要紧的一件事)
R1 的 `.claude/_esp32_pinmap.json:new` **不能直接用**:那张表里 5 个 `VBUS_EN*`
和 4 个 `KEY_*` 的脚在 R2 全部空出来了(D9 作废 74HC595、VBUS_EN 挪到刀片),
而 R2 新增了 `PRSNT1#~5#`、`MUX_RST#`。逐条复核后重排,结果写进 `PINMAP` 表,
并同步写回 `_esp32_pinmap.json` 的 `"r2_base"` 键(不动 `new`/`old`/`pin2io`)。
硬约束(逐条对着 ESP32-S3 数据手册核过):
1. **`NTC1`/`NTC2` 必须落在 ADC1**(IO1~IO10;ADC2 与 WiFi 共用,开 WiFi 后读不到)。
⇒ `NTC1=IO1`(模组脚 39)、`NTC2=IO2`(脚 38),和 R1 一致。
2. **flash/PSRAM 占用的脚**:ESP32-S3-WROOM-1 模组把 SPI flash 的 IO26~IO32 根本没引出
——符号 41 个脚里查不到这几个(实测 `_p3_live.json`),所以不用躲。
**IO35/IO36/IO37 引出来了,但 R8(八线 PSRAM)版本用它们做 PSRAM 的 D4~D7。**
⚠ **本页按「可能上 R8 版」保守处理:IO35/36/37 一律不接**(进 NC 表)。
⇒ **待确认项**:若最终确定选 N8(四线 PSRAM 或无 PSRAM)版,这三个脚可放出来当 GPIO。
不接的代价只是少三个备用脚;接了再换 R8 版就是死机 —— 所以往保守一侧倒。
3. **strapping 脚**:IO0 / IO3 / IO45 / IO46。
- `IO0` 给 J33 第 5 脚(进下载模式用)—— 模组内部有上拉,排针悬空 = 高 = 正常启动 ✓
- `IO3`(JTAG_SEL) / `IO45`(VDD_SPI 电压) / `IO46`(ROM 打印) **一律不接**,
悬空时靠内部弱下拉取默认值,是出厂推荐状态。
4. **只输入的脚**:IO46 在 S3 上是 input-only,且它是 strapping —— 已归入不接。
5. **原生 USB 硬连线**:`IO19 = USB_D−`、`IO20 = USB_D+`(模组脚 13 / 14,脚名与脚号
实测自 `_p3_live.json`,没有猜)。这两根不走 GPIO matrix,改不了。
⇒ `USB6_DM=IO19`、`USB6_DP=IO20`(契约 §1.2 同样这么写)。
6. 其余信号全部走 GPIO matrix,落哪个脚都行 ⇒ 按「出线方向 = 目的地」排:
低位脚(IO38~IO42,符号下缘)给往页面下方去的 I²C 与 MUX / 中断簇,
高位脚(IO4~IO18,符号上缘)给 EFUSE_EN / PRSNT / FAN 这些纯端口信号。
**引脚够不够**:模组引出 34 个 IO,扣掉 6 个 strapping/PSRAM 保留 + 2 个 USB,
剩 26 个,减去 23 根信号 ⇒ 只剩 **IO47 / IO48 两个备用脚**。一个不多。
## 契约里少写了一根:`HUB_RST`
契约 §1.2 明写 `HUB_RST` 由 **A3 的 ESP32** 发出去 A4,但任务书列 GPIO 时漏了它。
本页按契约办:`HUB_RST = IO8`(模组脚 12),不是 strapping 脚。
硬约束 §3.1 的"不驱动 = 不复位"由 A4 侧 10k 上拉到 `+3V3_HUB` 保证,本页只出一个端口。
## 外部上拉 vs ESP32 内部上拉(PRSNT1#~5#)
`PRSNT#` 完全可以用 ESP32 的内部上拉(~45kΩ)省掉 5 颗电阻。**本项目仍然用外部 10k**:
1. ESP32 复位期间(EN 拉低到固件跑起来之间)内部上拉是关的,PRSNT# 会浮着 ——
而这几根线跨了金手指、外面挂着五片可插拔的刀片,浮空输入在这种长走线上会乱跳。
2. 45kΩ 对一条穿过金手指、带插拔弹跳的线来说太软;10k 的噪声容限和
`INT#` / `INA_ALERT` 那两条开漏线一致,调试时可以用同一套判据。
3. 以后若要把 PRSNT# 改成开漏 wire-OR 或接到别的芯片上,外部上拉不用改板。
同理 `INT#`(五槽 wire-OR,契约 §1.2 钉死上拉在本页)和 `INA_ALERT`(A1 的 U13 开漏)
也各一颗 10k。刀片上 B2-CTRL 已有本槽 I²C 分支的 4.7k,**底板只管主干这一对**(R602/R603)。
## 踩过 / 绕开的坑
- **TCA9548A 的 `SDn` 脚号在 `SCn` 前面**(4=SD0 5=SC0 6=SD1 7=SC1 …)。按 SCL/SDA 的
书写习惯接过去就是整排反的,而且几何验证一路 PASS。本页 `U35_CH` 把 (SDn, SCn) 脚号
写成显式二元组,不靠顺序推。
- **端口标签往锚点某一侧伸出去约 40+6.7×字符数**(skill §7 第 8 条 / §8b)。本页三处
险些相撞:上拉组标签右边界 245 vs U35 的 `MUX_RST#` 标签左边界 256(留 11);
U35 右侧标签右边界 612 vs DAC 块左边界 613(留 1,就是靠 dx=+580/+50 顶出来的);
ESP32 第二列标签右边界 590 vs 同上。改任何一个 x 都要重算这三处 —— `label_clash()`
会兜住同一 y 上的相叠,跨块的间距靠 `build()` 里那条 `bx[0] >= 613` 断言。
- **`dir` 决定标签朝哪边**(skill §8b):`IN` 的标签在引脚**左**侧,`BI`/`OUT` 在右侧。
所以本页用 `port_rot(dir, side)`,`side` 说的是"标签往哪边伸",rot 由 dir 反推。
一律写 rot=0 会让所有 IN 口的标签整排倒过去压到 U20 身上。
- **TXD0 / RXD0 两根从 U20 左侧并排引出的线怎么不打架**:判据是"**里侧那根拐得更低**"。
TXD0(脚 37,y=585)走里侧竖线 x=185 且拐在 y=410,RXD0(脚 36,y=595)走外侧
x=175 拐在 y=430。反过来排就会在 (185,585) 出一个交叉点 —— 第一版就是这么写的,
`crossings()` 当场报出来了。
"""
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, pins_of, PARTS # noqa: E402
PAGE, NAME = '38327f806bc13133', 'A3-CTRL'
R2_PROJECT = '05a65c22f14846b8b95360ef8d3c1e2e'
SRC_DAC = os.path.join(HERE, '_p3b_live2.json') # R1 P3B-EXPAND
KEEP_U6 = set(['U6', 'C607', 'C608', 'C609', 'R614', 'R615'])
KEEP_U7 = set(['U7', 'C610', 'C611', 'C612', 'R616', 'R617'])
OFF_U6 = (580, -20)
OFF_U7 = (50, 250)
# R1 上这两颗 DAC 都有空着的通道:U6 只用 3 路(VDAC1/2/4,通道 3 空),
# U7 只用 2 路(VDAC3/5,通道 2/3 空)。源页上这些 OUTx/FBx 本来就没接线。
# ⚠ datasheet 要求"FBx 接回 OUTx"只对**启用的**通道成立;空通道上电默认关闭、
# 输出高阻,R1 就是这么留的,本页照搬不动。写成显式表,免得被当成漏连。
DAC_NC = {'U6': ['1', '2'], 'U7': ['1', '2', '3', '4']} # FB3/OUT3 (+U7 的 OUT2/FB2)
# ── 器件键(`.claude/eda-parts.json`)───────────────────────────────────
K_ESP = 'C9900163599' # ESP32-S3-WROOM-1
K_MUX = 'C130026' # TCA9548APWR
K_HDR = 'C50951' # 2.54mm 1*5P 直排针
K_10K = 'C25744' # 0402 10kΩ (0402WGF1002TCE)
K_4K7 = 'C25900' # 0402 4.7kΩ (0402WGF4701TCE = 470×10^1)
K_100N = 'C1525' # 0402 100nF
K_22U = 'C12891' # 1206 22µF(引脚 ±15,不是 ±20)
U20X, U20Y = 250, 600 # ESP32 中心
U35X, U35Y = 450, 280 # TCA9548A 中心
J33X, J33Y = 430, 455 # 应急 UART 排针中心(挪到 U20 与 U35 之间那条空带)
COL1, COL2 = 340, 470 # ESP32 信号端口的两列锚点 x
# ── U20 引脚相对坐标(实测自 `_p3_live.json`,rot 0)────────────────────
U20_REL = {
'1': (45, -160), '2': (-45, 15), '3': (-45, 5), '4': (45, 160),
'5': (45, 150), '6': (45, 140), '7': (45, 130), '8': (45, 50),
'9': (45, 40), '10': (45, 30), '11': (45, 20), '12': (45, 120),
'13': (45, 10), '14': (45, 0), '15': (45, 170), '16': (45, -110),
'17': (45, 110), '18': (45, 100), '19': (45, 90), '20': (45, 80),
'21': (45, 70), '22': (45, 60), '23': (45, -10), '24': (45, -120),
'25': (45, -130), '26': (45, -100), '27': (45, 200), '28': (45, -20),
'29': (45, -30), '30': (45, -40), '31': (45, -50), '32': (45, -60),
'33': (45, -70), '34': (45, -80), '35': (45, -90), '36': (-45, -5),
'37': (-45, -15), '38': (45, 180), '39': (45, 190), '40': (45, -170),
'41': (45, -200),
}
# 模组脚号 -> IOxx(与 `_esp32_pinmap.json:pin2io` 一致,缺的几条是本页按符号脚名补的)
PIN2IO = {
'4': 'IO4', '5': 'IO5', '6': 'IO6', '7': 'IO7', '8': 'IO15', '9': 'IO16',
'10': 'IO17', '11': 'IO18', '12': 'IO8', '13': 'IO19', '14': 'IO20',
'15': 'IO3', '16': 'IO46', '17': 'IO9', '18': 'IO10', '19': 'IO11',
'20': 'IO12', '21': 'IO13', '22': 'IO14', '23': 'IO21', '24': 'IO47',
'25': 'IO48', '26': 'IO45', '27': 'IO0', '28': 'IO35', '29': 'IO36',
'30': 'IO37', '31': 'IO38', '32': 'IO39', '33': 'IO40', '34': 'IO41',
'35': 'IO42', '36': 'IO44', '37': 'IO43', '38': 'IO2', '39': 'IO1',
}
ADC1 = set('IO%d' % i for i in range(1, 11))
# ── ⭐ R2 底板 GPIO 分配表 ──────────────────────────────────────────────
# (模组脚号, 网络名, dir, 端口列, 理由)
# dir 是**本页这一侧**的方向:ESP32 输出 = OUT,ESP32 采样 = IN,双向 = BI。
PINMAP = [
('27', 'IO0', 'BI', COL1, u'strapping/下载;接 J33 第 5 脚,悬空=正常启动'),
('39', 'NTC1', 'IN', COL2, u'IO1 = ADC1_CH0 —— 必须 ADC1,ADC2 与 WiFi 冲突'),
('38', 'NTC2', 'IN', COL1, u'IO2 = ADC1_CH1 —— 同上'),
('4', 'FAN_TACH_OUT', 'IN', COL2, u'IO4 测速脉冲输入,任意 GPIO'),
('5', 'FAN_TACH_IN', 'IN', COL1, u'IO5 测速脉冲输入,任意 GPIO'),
('6', 'FAN_PWM', 'OUT', COL2, u'IO6 LEDC PWM,任意 GPIO'),
('7', 'FAN_EN', 'OUT', COL1, u'IO7 复位高阻=关电,由 A6 的 100k 下拉保证'),
('12', 'HUB_RST', 'OUT', COL2, u'IO8 非 strapping;不复位由 A4 的 10k 上拉保证'),
('17', 'EFUSE_EN5', 'OUT', COL1, u'IO9 直驱刀片 eFuse EN'),
('18', 'EFUSE_EN4', 'OUT', COL2, u'IO10 直驱刀片 eFuse EN'),
('19', 'EFUSE_EN3', 'OUT', COL1, u'IO11 直驱刀片 eFuse EN'),
('20', 'EFUSE_EN2', 'OUT', COL2, u'IO12 直驱刀片 eFuse EN'),
('21', 'EFUSE_EN1', 'OUT', COL1, u'IO13 直驱刀片 eFuse EN'),
('22', 'PRSNT5#', 'IN', COL2, u'IO14 在位检测,本页外部 10k 上拉'),
('8', 'PRSNT4#', 'IN', COL1, u'IO15 在位检测,本页外部 10k 上拉'),
('9', 'PRSNT3#', 'IN', COL2, u'IO16 在位检测,本页外部 10k 上拉'),
('10', 'PRSNT2#', 'IN', COL1, u'IO17 在位检测,本页外部 10k 上拉'),
('11', 'PRSNT1#', 'IN', COL2, u'IO18 在位检测,本页外部 10k 上拉'),
('13', 'USB6_DM', 'BI', COL1, u'IO19 ⚠ 原生 USB PHY 硬连线 D−,不可改'),
('14', 'USB6_DP', 'BI', COL2, u'IO20 ⚠ 原生 USB PHY 硬连线 D+,不可改'),
('23', 'UP_VBUS_DET', 'IN', COL1, u'IO21 电脑在不在,A4 分压后送来'),
('31', 'INA_ALERT', 'IN', COL2, u'IO38 A1 的 U13 开漏,本页 10k 上拉'),
('32', 'INT#', 'IN', COL1, u'IO39 五槽 wire-OR 开漏,本页唯一一颗 10k 上拉'),
('33', 'MUX_RST#', 'BI', COL2, u'IO40 U35 复位;本页 10k 上拉 ⇒ 高阻=不复位'),
('34', 'SDA', 'BI', COL1, u'IO41 I²C 主干(不经 mux)'),
('35', 'SCL', 'BI', COL2, u'IO42 I²C 主干(不经 mux)'),
]
# 有意不接(进 NC 表,不算悬空)
U20_NC = ['15', '16', '24', '25', '26', '28', '29', '30']
U20_NC_WHY = {
'15': u'IO3 strapping(JTAG_SEL)', '16': u'IO46 strapping + input-only',
'24': u'IO47 备用', '25': u'IO48 备用', '26': u'IO45 strapping(VDD_SPI)',
'28': u'IO35 留给 R8 版八线 PSRAM', '29': u'IO36 同上', '30': u'IO37 同上',
}
# ── TCA9548A 引脚相对坐标(契约 §4,实测)。⚠ SDn 脚号在 SCn 前面 ───────
U35_REL = {
'1': (-50, 55), '2': (-50, 45), '3': (-50, 35), '4': (-50, 25),
'5': (-50, 15), '6': (-50, 5), '7': (-50, -5), '8': (-50, -15),
'9': (-50, -25), '10': (-50, -35), '11': (-50, -45), '12': (-50, -55),
'13': (50, -55), '14': (50, -45), '15': (50, -35), '16': (50, -25),
'17': (50, -15), '18': (50, -5), '19': (50, 5), '20': (50, 15),
'21': (50, 25), '22': (50, 35), '23': (50, 45), '24': (50, 55),
}
# (SDn 脚号, SCn 脚号, 槽号)。通道 0~3 在左列,通道 4 在右列(脚 13=SD4 / 14=SC4)
U35_CH_L = [('4', '5', 1), ('6', '7', 2), ('8', '9', 3), ('10', '11', 4)]
U35_NC = ['15', '16', '17', '18', '19', '20'] # 通道 5~7 悬空
# ── 上拉电阻组(左下):(y, 位号, 器件键, 网名, dir, 说明) ───────────────
BANK_X_BUS, BANK_X_R, BANK_X_PT = 45, 85, 145
# ⚠ 行距 30 而不是 20:0402 的**位号 + 值是两行文字,画在符号右侧**,占竖直方向
# 约 20 个单位。20 行距时上下两颗的文字会贴在一起(几何验证抓不到,只有渲染出来才看得见)。
BANK_PITCH, BANK_TOP = 30, 380
BANK = [
('R602', K_4K7, 'SDA', 'BI', u'I²C 主干上拉 4.7k'),
('R603', K_4K7, 'SCL', 'BI', u'I²C 主干上拉 4.7k'),
('R604', K_10K, 'INT#', 'IN', u'五槽 wire-OR 的唯一一颗上拉'),
('R605', K_10K, 'INA_ALERT', 'IN', u'A1 的 U13 开漏'),
('R606', K_10K, 'PRSNT1#', 'IN', u'刀片在位时把它拉到地'),
('R607', K_10K, 'PRSNT2#', 'IN', u'刀片在位时把它拉到地'),
('R608', K_10K, 'PRSNT3#', 'IN', u'刀片在位时把它拉到地'),
('R609', K_10K, 'PRSNT4#', 'IN', u'刀片在位时把它拉到地'),
('R610', K_10K, 'PRSNT5#', 'IN', u'刀片在位时把它拉到地'),
('R611', K_10K, 'MUX_RST#', 'BI', u'高阻 = 不复位'),
]
BANK_Y = dict((b[0], BANK_TOP - BANK_PITCH * i) for i, b in enumerate(BANK))
# ── J33 应急 UART:脚号 -> 网名(脚 1 是 GND,直接挂地旗)───────────────
J33_MAP = [('2', 'TXD0'), ('3', 'RXD0'), ('4', 'EN'), ('5', 'IO0')]
J33_REL = {'1': (-20, 20), '2': (-20, 10), '3': (-20, 0),
'4': (-20, -10), '5': (-20, -20)}
TEXTS = [
(620, 805, u'【A3-CTRL】ESP32-S3 主控 + TCA9548A I²C 多路 + DAC63204 ×2 + 应急 UART', 12),
(620, 782, u'【E/F】DAC63204 ×2(U6=0x4A / A0→SDA,U7=0x4B / A0→SCL),挂 I²C 主干、不经 mux。', 10),
(620, 766, u'三处 datasheet 强制要求(图上看不出来、接错也不报错):FBx 必须接回 OUTx;', 10),
(620, 750, u'CAP 接 2.2µF 到 AGND;VREF 空置时 100nF 到地 + 10k 上拉 VDD。', 10),
(620, 734, u'内部基准 1.212V ×2 ⇒ 满量程 2.424V(不是 2.5V)。U6 出 VDAC1/2/4,U7 出 VDAC3/5。', 10),
(620, 718, u'⚠ U23 PCA9555 留在刀片上(B2-CTRL),本页一件不搬。', 10),
# ⚠ 2026-09-08 主代理改:第一行原来在 y=96,**压住 R611(85,110) 的「10kΩ」值标签**
# (值画在电阻符号下方,占到 y≈90)。`text_clash` 只查文字 vs 导线/引脚、
# `label_clash` 只查端口标签 vs 旗,两者都够不着「文字 vs 元件的位号/值」这一类。
# 把六个区(A ESP32 / B 上拉组 / C J33 / D U35 / E·F 两颗 DAC)和标题栏
# (x>455 且 y<192)当障碍全页扫过:本页能放下这一块的只有左下角 y 10~80 那条,
# ⇒ 五行压进 74/58/42/26/10,字号 10→9(字号 10 的五行放不进 70 的高度)。
(90, 74, u'【B】上拉组:SDA/SCL 主干 4.7k(分支的那对在刀片 B2-CTRL),', 9),
(90, 58, u'INT# 是五槽 wire-OR 唯一的一颗 10k,INA_ALERT 对 A1 的 U13 开漏。', 9),
(90, 42, u'PRSNT1#~5# 用外部 10k 而非 ESP32 内部上拉:复位期间内部上拉是关的,', 9),
(90, 26, u'跨金手指的长线浮空会乱跳,45k 对插拔弹跳也太软。', 9),
(90, 10, u'【C】J33 = D13 的保命口:控制台挂在 Hub 后面,Hub 一复位就自断后路。', 9),
]
# ══════════════════════════════════════════════════════════════════════
# 从 R1 页面整块搬运(照抄 a2_aux.py 的 load(),本页不需要改名表)
# ══════════════════════════════════════════════════════════════════════
def load(path, keep, off, src_nc=None):
u"""读回一页,按 keep 过滤,整块平移。返回 (导线[(a,b,net)], 旗, 端口, place, expect)。
⚠ 过滤的判据是「导线组里的引脚属于谁」,不是坐标框 —— 坐标框会把别人的线一起切走。
`src_nc = {位号: [脚号...]}` 是**源页上本来就没接**的脚(R1 两颗 DAC 都有空着的通道),
不列进来就会被当成"源页悬空引脚"而终止。
"""
src_nc = src_nc or {}
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, gowner, gpins = defaultdict(set), 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])
for p in src['parts']:
for q in p[9]:
if str(q[0]) in src_nc.get(p[0], []):
continue
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])]
assert hit, u'%s 源页悬空引脚 %s.%s' % (path, p[0], q[0])
gowner[find(hit[0])].add(p[0])
gpins[find(hit[0])].add('%s.%s' % (p[0], q[0]))
keepgrp = {}
for r_, owners in gowner.items():
inside, outside = owners & keep, owners - keep
assert not (inside and outside), \
u'%s 的导线组同时挂着 %s 和 %s,切不开' % (path, sorted(inside), sorted(outside))
keepgrp[r_] = bool(inside)
W, F, PT = [], [], []
for r_, ok in keepgrp.items():
if not ok:
continue
names = gname.get(r_, set())
assert len(names) <= 1, u'组 %s 名字不唯一:%s' % (sorted(gpins[r_])[:3], sorted(names))
# 无名的短节点沿用 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))
def kept_at(xy):
hit = [i for i in range(n) if _on(xy, segs[i][0], segs[i][1])]
return keepgrp[find(hit[0])] if hit else False
for f in src['flags']:
xy = (int(round(f[2])) + dx, int(round(f[3])) + dy)
if kept_at(xy):
F.append([f[0].split('-')[0], f[1], xy[0], xy[1], f[4]])
for p in src['ports']:
xy = (int(round(p[2])) + dx, int(round(p[3])) + dy)
if kept_at(xy):
PT.append([p[0].replace('Netport-', ''), p[1], xy[0], xy[1], p[4]])
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]
if d not in keep:
continue
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
# ══════════════════════════════════════════════════════════════════════
# 本页手画的部分
# ══════════════════════════════════════════════════════════════════════
def rel(cx, cy, table):
return dict((k, (cx + dx, cy + dy)) for k, (dx, dy) in table.items())
def hz(cx, cy, half=20):
u"""横放两脚件的期望(非极性件只比坐标集合,翻转只换脚号,无害)。"""
return {'?': set([(cx - half, cy), (cx + half, cy)])}
def vt(cx, cy, half=20):
u"""竖放两脚件的期望。"""
return {'?': set([(cx, cy - half), (cx, cy + half)])}
def port_rot(d, side):
u"""side='R' 标签往右伸、'L' 往左伸。skill §8b:IN 的方向和 BI/OUT 相反。"""
if side == 'R':
return 180 if d == 'IN' else 0
return 0 if d == 'IN' else 180
def build_local(s):
u"""A/B/C/D 四区(ESP32、上拉组、J33、TCA9548A)。返回 (place, expect)。"""
place, exp = [], {}
U = rel(U20X, U20Y, U20_REL) # U20 绝对引脚坐标
M = rel(U35X, U35Y, U35_REL) # U35 绝对引脚坐标
# ── A 区:ESP32 ────────────────────────────────────────────────────
place.append(['U20', K_ESP, U20X, U20Y, 0])
exp['U20'] = dict(U)
# 3V3 干线 x=160(U20 脚2 -> 上到旗),C601/C602 去耦挂在上面
place.append(['C601', K_22U, 100, 760, 0]) # 1206,引脚 ±15(skill §8d 实测)
place.append(['C602', K_100N, 100, 710, 0]) # 0402,引脚 ±20
exp['C601'] = hz(100, 760, 15)
exp['C602'] = hz(100, 710, 20)
s.w('+3V3', (160, 615), (160, 790))
s.pwr('+3V3', 160, 790, 0) # 符号在上、引脚在下,导线从下方 ✓
s.w('+3V3', (115, 760), (160, 760))
s.w('+3V3', (120, 710), (160, 710))
s.w('+3V3', (160, 615), (205, 615)) # -> U20 脚2 (3V3)
# EN 上电复位:R601 10k 上拉 + C603 100nF 到地
place.append(['R601', K_10K, 130, 660, 270]) # rot270 -> 引脚 (x, y±20)
# ⚠ C603 摆在 x=100 而不是和 R601 同一条 x=130:0402 的**型号文字画在符号右侧**
# (skill §8b-3),x=130 时它横着占到 x≈200,正好压在 RXD0 那根竖线(x=175)上。
place.append(['C603', K_100N, 100, 480, 270])
exp['R601'] = vt(130, 660)
exp['C603'] = vt(100, 480)
s.w('+3V3', (130, 680), (160, 680)) # R601 上脚 -> 3V3 干线(T)
s.w('EN', (130, 500), (130, 640)) # R601 下脚往下的 EN 竖线
s.w('EN', (100, 500), (130, 500)) # -> C603 上脚
s.w('EN', (130, 605), (205, 605)) # T 到 U20 脚3 (EN)
s.w('EN', (90, 520), (130, 520)) # T 出一小段给端口(避开 GND 干线)
s.port('BI', 'EN', 90, 520, port_rot('BI', 'L'))
# 地:C601/C602 走 x=60 干线;C603 单独一条,让开 EN 端口的标签
s.w('GND', (60, 690), (60, 760))
s.w('GND', (60, 760), (85, 760))
s.w('GND', (60, 710), (80, 710))
s.gnd(60, 690, 0) # 符号在下、引脚在上,导线从上方 ✓
s.w('GND', (60, 460), (100, 460))
s.w('GND', (60, 440), (60, 460))
s.gnd(60, 440, 0)
# TXD0 / RXD0:外圈先拐的嵌套走法,避免两条互相交叉(见文档串)
# 判据:**里侧那根(x 大的)必须拐得更低**,它的横段才不会切到外侧那根的竖段。
# 反过来排(TXD0 走 x=175 在 y=430 拐)会在 (185,585) 造出一个交叉点。
s.w('TXD0', (185, 585), (205, 585)) # 脚 37,里侧竖线 x=185,拐在 y=410
s.w('TXD0', (185, 410), (185, 585))
s.w('TXD0', (160, 410), (185, 410))
s.port('BI', 'TXD0', 160, 410, port_rot('BI', 'L'))
s.w('RXD0', (175, 595), (205, 595)) # 脚 36,外侧竖线 x=175,拐在 y=430
s.w('RXD0', (175, 430), (175, 595))
s.w('RXD0', (160, 430), (175, 430))
s.port('BI', 'RXD0', 160, 430, port_rot('BI', 'L'))
# U20 的地(脚 1 / 40 + EPAD 41)
for pin in ('1', '40', '41'):
s.w('GND', U[pin], (320, U[pin][1]))
s.w('GND', (320, 400), (320, 440))
s.w('GND', (320, 370), (320, 400))
s.gnd(320, 370, 0)
# 26 根信号引出线 + 端口
for pin, net, d, col, _why in PINMAP:
y = U[pin][1]
s.w(net, (295, y), (col, y))
s.port(d, net, col, y, port_rot(d, 'R'))
# ── B 区:上拉电阻组 ───────────────────────────────────────────────
bot = BANK_Y[BANK[-1][0]] - 5
s.w('+3V3', (BANK_X_BUS, bot), (BANK_X_BUS, BANK_TOP))
s.pwr('+3V3', BANK_X_BUS, bot, 180) # 符号在下、引脚在上,导线从上方 ✓
for ref, key, net, d, _why in BANK:
y = BANK_Y[ref]
place.append([ref, key, BANK_X_R, y, 0])
exp[ref] = hz(BANK_X_R, y, 20)
s.w('+3V3', (BANK_X_BUS, y), (BANK_X_R - 20, y))
s.w(net, (BANK_X_R + 20, y), (BANK_X_PT, y))
s.port(d, net, BANK_X_PT, y, port_rot(d, 'R'))
# ── C 区:J33 应急 UART ────────────────────────────────────────────
place.append(['J33', K_HDR, J33X, J33Y, 0])
J = rel(J33X, J33Y, J33_REL)
exp['J33'] = dict(J)
# 脚1 的地往左走到 x=115 再下去 —— 往右走会贴着连接器符号的上沿画过去,
# 往下走会撞上第 5 脚那一排端口标签(x73..140)。
# 脚1 的地从**第 2~5 脚那排引出线的上方**往左走再下去 —— 从下面走会切到那四根线。
s.w('GND', J['1'], (340, J['1'][1]))
# 地旗的引脚落在 y=415 而不是 440:地符号往下伸约 20,440 时它会压住
# 第 5 脚(IO0)那个端口的标签(x310..370 @ y435)。
s.w('GND', (340, 415), (340, J['1'][1]))
s.gnd(340, 415, 0) # 符号在下、引脚在上,导线从上方 ✓
for pin, net in J33_MAP:
s.w(net, (370, J[pin][1]), J[pin])
s.port('BI', net, 370, J[pin][1], port_rot('BI', 'L'))
# ── D 区:TCA9548A ─────────────────────────────────────────────────
place.append(['U35', K_MUX, U35X, U35Y, 0])
exp['U35'] = dict(M)
# 通道 0~3 -> SDA1/SCL1 ~ SDA4/SCL4(左列)
for sd, sc, ch in U35_CH_L:
for pin, net in ((sd, 'SDA%d' % ch), (sc, 'SCL%d' % ch)):
s.w(net, (350, M[pin][1]), M[pin])
s.port('BI', net, 350, M[pin][1], port_rot('BI', 'L'))
# 通道 4 -> SDA5/SCL5(右列:脚 13 = SD4、脚 14 = SC4)
for pin, net in (('13', 'SDA5'), ('14', 'SCL5')):
s.w(net, M[pin], (545, M[pin][1]))
s.port('BI', net, 545, M[pin][1], port_rot('BI', 'R'))
# RESET# -> MUX_RST#(10k 上拉在 B 区)
s.w('MUX_RST#', (350, M['3'][1]), M['3'])
s.port('BI', 'MUX_RST#', 350, M['3'][1], port_rot('BI', 'L'))
# A0 / A1 / A2 全接地 -> 地址 0x70
s.w('GND', (370, M['1'][1]), M['1'])
s.w('GND', (370, M['2'][1]), M['2'])
s.w('GND', (370, 325), (370, 355))
s.gnd(370, 355, 180) # 符号在上、引脚在下,导线从下方 ✓
s.w('GND', M['21'], (585, M['21'][1])) # A2 从右侧绕出去,避开 SDA5/SCL5
s.w('GND', (585, 270), (585, 305))
s.gnd(585, 270, 0)
# GND 脚
s.w('GND', (370, M['12'][1]), M['12'])
s.w('GND', (370, 205), (370, 225))
s.gnd(370, 205, 0)
# 上游 I²C 主干
for pin, net in (('22', 'SCL'), ('23', 'SDA')):
s.w(net, M[pin], (545, M[pin][1]))
s.port('BI', net, 545, M[pin][1], port_rot('BI', 'R'))
# VCC + 去耦 C604
place.append(['C604', K_100N, 570, 385, 0])
exp['C604'] = hz(570, 385, 20)
s.w('+3V3', M['24'], (530, M['24'][1]))
s.w('+3V3', (530, 335), (530, 405))
s.w('+3V3', (530, 385), (550, 385))
s.pwr('+3V3', 530, 405, 0)
s.w('GND', (590, 385), (590, 415))
s.gnd(590, 415, 180)
return place, exp
# ══════════════════════════════════════════════════════════════════════
# 排版自检(照抄 a1_input.py / a2_aux.py,加了一条端口标签相叠)
# ══════════════════════════════════════════════════════════════════════
def text_clash(s, exp):
errs, boxes, pts = [], [], []
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))
boxes.append((x, y - size * 0.8, x + w, 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[: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 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))
def label_clash(s):
u"""**不同列**的端口标签横向对撞。skill §8b:符号+标签往某一侧伸 40+6.7×字符。
`flag_clash` 只看锚点距离(横向 <20),够不着这一类 —— P3B 上就出过
「左边器件的 rot=0 标签和右边器件的 rot=180 标签在中间撞上」,两列看着离得很远。
⚠ **同一列**(锚点 x 相同)按 10 的间距排下来是正常的(skill §8b-2,
R1 的 P3 一列排了 21 个),所以这里只比不同 x 的。
"""
errs, boxes = [], []
for d, net, x, y, rot in s.PT:
w = 40 + 6.7 * len(net)
left = (rot == 0) if d == 'IN' else (rot == 180)
lo, hi = (x - w, x) if left else (x, x + w)
boxes.append((lo, hi, y - 8, y + 8, net, x, True))
# 旗的符号:横向约 ±10,纵向从引脚往符号那一侧伸约 20(Ground rot0 / Power rot180
# 是往下,另两种往上)。**端口标签压住地旗**在本页真的发生过一次(J33 的 IO0)。
for kind, net, x, y, rot in s.F:
down = (kind == 'Ground') == (rot == 0)
boxes.append((x - 10, x + 10, y - 20 if down else y, y if down else y + 20,
net, None, False))
for i in range(len(boxes)):
for j in range(i + 1, len(boxes)):
a, b = boxes[i], boxes[j]
if a[6] and b[6] and a[5] == b[5]: # 同一列的端口,10 间距是正常的
continue
if a[0] < b[1] and b[0] < a[1] and a[2] < b[3] and b[2] < a[3]:
errs.append(u'符号/标签相叠: %s 与 %s(x %.0f..%.0f × %.0f..%.0f)'
% (a[4], b[4], max(a[0], b[0]), min(a[1], b[1]),
max(a[2], b[2]), min(a[3], b[3])))
return sorted(set(errs))
def bbox(W, F, PT, exp):
xs, ys = [], []
for a, b, _ in W:
xs += [a[0], b[0]]
ys += [a[1], b[1]]
for it in F + PT:
xs.append(it[2])
ys.append(it[3])
for ps in exp.values():
for xy in ps.values():
xs.append(xy[0])
ys.append(xy[1])
return min(xs), max(xs), min(ys), max(ys)
def assert_no_overlap(n1, b1, n2, b2, gap=30):
ax0, ax1, ay0, ay1 = b1
bx0, bx1, by0, by1 = b2
sep_x = ax1 + gap <= bx0 or bx1 + gap <= ax0
sep_y = ay1 + gap <= by0 or by1 + gap <= ay0
assert sep_x or sep_y, u'%s 与 %s 重叠:x%d..%d y%d..%d vs x%d..%d y%d..%d' % (
n1, n2, ax0, ax1, ay0, ay1, bx0, bx1, by0, by1)
def build():
s = Sheet(PAGE, NAME)
place, exp = build_local(s)
a = load(SRC_DAC, KEEP_U6, OFF_U6, DAC_NC)
b = load(SRC_DAC, KEEP_U7, OFF_U7, DAC_NC)
assert_no_overlap(u'U6 块', bbox(*a[:3], exp=a[4]), u'U7 块', bbox(*b[:3], exp=b[4]))
for tag, blk in ((u'U6 块', a), (u'U7 块', b)):
bx = bbox(*blk[:3], exp=blk[4])
assert bx[0] >= 613, u'%s 左边界 %d 顶到 ESP32/U35 的端口标签' % (tag, bx[0])
assert bx[3] <= 700, u'%s 上边界 %d 顶到说明文字' % (tag, bx[3])
for it in blk[1] + blk[2]: # 旗/端口不许掉进标题栏
assert not (it[2] > 455 and it[3] < 192), \
u'%s 的 %s @(%d,%d) 掉进标题栏' % (tag, it[1], it[2], it[3])
# 端口标签算进来的真实左右边界(skill §7 第 8 条:符号+标签约 40+6.7×字符)
for d, net, x, _y, rot in blk[2]:
w = 40 + 6.7 * len(net)
left = (rot == 0) if d == 'IN' else (rot == 180)
lo, hi = (x - w, x) if left else (x, x + w)
assert lo >= 600, u'%s 的 %s 标签左边界 %.0f 顶到左边的块' % (tag, net, lo)
assert hi <= 1145, u'%s 的 %s 标签右边界 %.0f 出框' % (tag, net, hi)
for W, F, PT, pl, ex in (a, b):
s.W += list(W)
s.F += F
s.PT += PT
dup = set(ex) & set(exp)
assert not dup, u'位号撞车:%s' % sorted(dup)
place += pl
exp.update(ex)
for t in TEXTS:
s.txt(t[0], t[1], t[2], t[3])
return s, place, exp
# 靠同名旗/端口成网、导线上天然分块的网络(契约 §7 第 10 条)
ALLOW_SPLIT = set(['GND', '+3V3', 'SDA', 'SCL', 'EN', 'TXD0', 'RXD0', 'IO0',
'MUX_RST#', 'INT#', 'INA_ALERT',
'PRSNT1#', 'PRSNT2#', 'PRSNT3#', 'PRSNT4#', 'PRSNT5#'])
def check_pinmap():
u"""GPIO 分配表本身的自检:不撞 NC、不重号、NTC 在 ADC1、USB 是硬连线那两个。"""
used = [p for p, _n, _d, _c, _w in PINMAP]
assert len(set(used)) == len(used), u'PINMAP 有重复脚号'
assert not (set(used) & set(U20_NC)), u'PINMAP 用到了 NC 表里的脚'
# 电源 / 地 / EN / UART 这几个脚不走 PINMAP,单独列出来兜底
HARD = set(['1', '2', '3', '36', '37', '40', '41']) # GND 3V3 EN RXD0 TXD0 GND EPAD
miss = set(U20_REL) - set(used) - set(U20_NC) - HARD
assert not miss, u'有脚既不在 PINMAP 也不在 NC 表里:%s' % sorted(miss)
by = dict((n, p) for p, n, _d, _c, _w in PINMAP)
for net in ('NTC1', 'NTC2'):
assert PIN2IO[by[net]] in ADC1, \
u'%s 落在 %s,不是 ADC1(IO1~IO10)' % (net, PIN2IO[by[net]])
assert PIN2IO[by['USB6_DM']] == 'IO19', u'USB6_DM 必须是 IO19(原生 USB D−)'
assert PIN2IO[by['USB6_DP']] == 'IO20', u'USB6_DP 必须是 IO20(原生 USB D+)'
# 契约 §1.4:R1 有、R2 没有的网络,写了就是错
dead = set(['BUCK_PG', 'IOEXP_INT#', 'USB_DP', 'USB_DM'])
for _p, n, _d, _c, _w in PINMAP:
assert n not in dead and not n.startswith('KEY_') and not n.startswith('VBUS_EN'), \
u'%s 是契约 §1.4 明确不存在的网络' % n
def dump_pinmap():
u"""把本页的分配写回 `.claude/_esp32_pinmap.json` 的 "r2_base" 键(不动 new/old/pin2io)。"""
path = os.path.join(os.path.dirname(HERE), '_esp32_pinmap.json')
d = json.load(io.open(path, encoding='utf-8'))
d['r2_base'] = dict((net, int(pin)) for pin, net, _d, _c, _w in PINMAP)
d['r2_base_nc'] = dict((p, U20_NC_WHY[p]) for p in U20_NC)
with io.open(path, 'w', encoding='utf-8') as f:
f.write(json.dumps(d, ensure_ascii=False, indent=1, sort_keys=True))
print(u'已更新 %s 的 r2_base(%d 根)' % (os.path.basename(path), len(PINMAP)))
def main():
push = '--push' in sys.argv
check_pinmap()
s, place, exp = build()
s.assert_grid()
nc = {'U20': U20_NC, 'U35': U35_NC}
nc.update(DAC_NC)
pins = pins_of(exp) # 把 {'?': {两个坐标}} 摊平成 {脚号: (x,y)}
errs = s.netcheck(pins, nc, ALLOW_SPLIT)
errs += text_clash(s, pins) + flag_clash(s) + label_clash(s)
print(u'ESP32:用掉 %d 脚,有意不接 %d 脚(其中 IO47/IO48 是仅剩的备用)'
% (len(PINMAP), len(U20_NC)))
s.report(len(place), errs)
dump_pinmap()
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()