R2:底板 / 刀片布局布线脚本、快照、截图与规划文档更新(含外壳、打印件、布线归档)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -0,0 +1,197 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
u"""ESP32-S3 引脚占用审计 —— 直接读 `五路直供电源板设计.md` §8.1 的 GPIO 表。
|
||||
|
||||
/c/Python314/python .claude/s3_pin_audit.py
|
||||
/c/Python314/python .claude/s3_pin_audit.py --octal # 按八线 PSRAM(R8) 严格判
|
||||
/c/Python314/python .claude/s3_pin_audit.py --free # 顺便列出还空着的脚
|
||||
|
||||
## 为什么自己写而不是用现成的
|
||||
|
||||
社区那个 `agodianel/esp32-claude-workbench` 的 `esp32_pin_audit`(78★)
|
||||
**判据全是 ESP32-classic 的,全仓库没有芯片型号分支**,放到 S3 上双向都错:
|
||||
|
||||
FORBIDDEN_PINS = {6,7,8,9,10,11} # classic 的内部 flash;S3 上是普通 IO → 假警报
|
||||
STRAPPING_PINS = {0,2,12,15} # S3 是 {0,3,45,46}
|
||||
INPUT_ONLY_PINS = {34,...,39} # S3 **没有**仅输入脚 → 假警报
|
||||
(S3 真正的 flash/PSRAM 脚 26~32 它一个都不查 → **静默漏报,这条最要命**)
|
||||
|
||||
一个自信地给错答案的检查器比没有检查器更糟,所以这里按 Espressif 官方文档重写。
|
||||
|
||||
## 判据出处
|
||||
|
||||
`docs.espressif.com/.../esp32s3/api-reference/peripherals/gpio.html`(2026-09-07 核对):
|
||||
|
||||
· 物理存在 GPIO0~21 与 GPIO26~48,**22~25 不存在**
|
||||
· GPIO26~32 供 SPI flash / PSRAM,不建议他用
|
||||
· 用八线(Octal) flash 或 PSRAM 时 **GPIO33~37 也被占**(接 SPIIO4~7 / SPIDQS)
|
||||
· strapping 脚是 **GPIO0 / 3 / 45 / 46**
|
||||
· GPIO19 / 20 默认给 USB-Serial-JTAG,改作普通 IO 则 USB-JTAG 失效
|
||||
· ADC1 = GPIO1~10,ADC2 = GPIO11~20(**ADC2 在 Wi-Fi 工作时不可用**)
|
||||
· **没有任何仅输入脚**(这一条与 ESP32-classic 不同)
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
DOC = os.path.join(HERE, '..', u'五路直供电源板设计.md')
|
||||
|
||||
# --- ESP32-S3 事实表(改这里要附官方出处)----------------------------------
|
||||
EXISTS = set(range(0, 22)) | set(range(26, 49)) # 22~25 不存在
|
||||
FLASH_PSRAM = set(range(26, 33)) # 26~32 恒占
|
||||
OCTAL_ONLY = set(range(33, 38)) # 33~37 仅八线版占用
|
||||
STRAPPING = {0: u'boot 模式', 3: u'JTAG 源选择',
|
||||
45: u'VDD_SPI 电压选择', 46: u'boot / ROM 日志'}
|
||||
USB_JTAG = {19: u'USB D−', 20: u'USB D+'}
|
||||
ADC1 = set(range(1, 11))
|
||||
ADC2 = set(range(11, 21))
|
||||
UART0 = {43: u'U0TXD', 44: u'U0RXD'}
|
||||
|
||||
|
||||
def parse_doc_pins(path):
|
||||
u"""从设计文档的 §8.1 表里抓 `| ... | IOxx | ... | `信号` | ...` 行。
|
||||
|
||||
表里有历史版本的重复表格,同一个 IO 可能出现多次 —— 以**最后一次**为准
|
||||
(文档是顺序追加的,后面的是改版后的定稿)。
|
||||
"""
|
||||
if not os.path.exists(path):
|
||||
return None, u'找不到设计文档 %s' % path
|
||||
txt = io.open(path, encoding='utf-8', errors='replace').read()
|
||||
pins = {}
|
||||
# 表列:| 模块脚 | GPIO | 方向/边 | 信号 | 去哪儿 |
|
||||
# ⚠️ 不能用「一行一个 IO」的正则:`| 13 / 14 | IO19 / IO20 | 东 |
|
||||
# `USB_DM` / `USB_DP` |` 这种**一行两脚**会整行匹配失败 ——
|
||||
# 结果 IO19/20 既不在已分配里、又被算进「空闲脚」,正是静默漏报。
|
||||
# 所以按列切开,把该列里的 IO 号和信号名成对取出。
|
||||
for line in txt.splitlines():
|
||||
if not line.startswith(u'|'):
|
||||
continue
|
||||
cells = line.split(u'|')
|
||||
if len(cells) < 6:
|
||||
continue
|
||||
nums = [int(x) for x in re.findall(r'IO(\d{1,2})\b', cells[2])]
|
||||
if not nums:
|
||||
continue
|
||||
sigs = re.findall(r'`([^`]+)`', cells[4])
|
||||
if not sigs: # 信号列没打反引号的,退回整格文本
|
||||
s = re.sub(r'[*\s]', u'', cells[4])
|
||||
sigs = [s] if s else []
|
||||
for i, n in enumerate(nums):
|
||||
sig = sigs[i] if i < len(sigs) else (sigs[-1] if sigs else u'')
|
||||
sig = sig.strip().strip(u'*')
|
||||
if sig and sig not in (u'同', u'—'):
|
||||
pins[n] = sig
|
||||
return pins, None
|
||||
|
||||
|
||||
def audit(pins, octal):
|
||||
findings = [] # (等级, GPIO, 信号, 说明)
|
||||
reserved = set(FLASH_PSRAM)
|
||||
if octal:
|
||||
reserved |= OCTAL_ONLY
|
||||
|
||||
for n in sorted(pins):
|
||||
sig = pins[n]
|
||||
if n not in EXISTS:
|
||||
findings.append((u'ERROR', n, sig,
|
||||
u'ESP32-S3 上**没有** GPIO%d(22~25 是空缺)' % n))
|
||||
continue
|
||||
if n in FLASH_PSRAM:
|
||||
findings.append((u'ERROR', n, sig,
|
||||
u'SPI flash / PSRAM 恒占(26~32),拿来做 IO 会起不来'))
|
||||
elif n in OCTAL_ONLY:
|
||||
lvl = u'ERROR' if octal else u'WARN'
|
||||
findings.append((lvl, n, sig,
|
||||
u'八线(R8/Octal) flash/PSRAM 版占用 33~37;'
|
||||
u'换 R8 料就冲突%s' % (u'' if octal else u'(当前按非八线判)')))
|
||||
if n in STRAPPING:
|
||||
findings.append((u'WARN', n, sig,
|
||||
u'strapping 脚(%s)—— 外部电路不能在上电瞬间把它拉错'
|
||||
% STRAPPING[n]))
|
||||
if n in USB_JTAG:
|
||||
findings.append((u'INFO', n, sig,
|
||||
u'USB-Serial-JTAG 的 %s,占了就没有原生 USB 调试口'
|
||||
% USB_JTAG[n]))
|
||||
if n in UART0:
|
||||
findings.append((u'WARN', n, sig,
|
||||
u'ROM 默认 UART0 %s,占了会跟启动日志抢线' % UART0[n]))
|
||||
# ADC2 只在真的当模拟输入用时才有 Wi-Fi 冲突
|
||||
if n in ADC2 and re.search(r'NTC|ADC|AIN|VSENSE|TEMP', sig, re.I):
|
||||
findings.append((u'ERROR', n, sig,
|
||||
u'落在 ADC2(GPIO11~20),**Wi-Fi 一开 ADC2 就读不了**;'
|
||||
u'模拟量要挪到 ADC1 = GPIO1~10'))
|
||||
return findings, reserved
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--octal', action='store_true',
|
||||
help=u'按八线 PSRAM(WROOM-1 R8)严格判,33~37 视为占用')
|
||||
ap.add_argument('--free', action='store_true', help=u'列出还空着的脚')
|
||||
ap.add_argument('--doc', default=DOC)
|
||||
a = ap.parse_args()
|
||||
|
||||
pins, err = parse_doc_pins(a.doc)
|
||||
if err:
|
||||
print(err)
|
||||
return 2
|
||||
if not pins:
|
||||
print(u'在文档里没抓到 IOxx 行 —— §8.1 的表格式可能改了,先看一眼再说。')
|
||||
return 2
|
||||
|
||||
print(u'从 %s 抓到 %d 个已分配 GPIO:' % (os.path.basename(a.doc), len(pins)))
|
||||
print(u' ' + u', '.join(u'IO%d=%s' % (n, pins[n]) for n in sorted(pins)))
|
||||
print()
|
||||
|
||||
findings, reserved = audit(pins, a.octal)
|
||||
order = {u'ERROR': 0, u'WARN': 1, u'INFO': 2}
|
||||
findings.sort(key=lambda f: (order[f[0]], f[1]))
|
||||
|
||||
if not findings:
|
||||
print(u'✓ 没有发现问题。')
|
||||
else:
|
||||
icon = {u'ERROR': u'❌', u'WARN': u'⚠️ ', u'INFO': u'ℹ️ '}
|
||||
for lvl, n, sig, msg in findings:
|
||||
print(u'%s [%-5s] IO%-2d %-16s %s' % (icon[lvl], lvl, n, sig, msg))
|
||||
|
||||
ne = sum(1 for f in findings if f[0] == u'ERROR')
|
||||
nw = sum(1 for f in findings if f[0] == u'WARN')
|
||||
print(u'\n合计:ERROR %d,WARN %d,INFO %d'
|
||||
% (ne, nw, len(findings) - ne - nw))
|
||||
|
||||
# 冲突:同一个脚被分给两个信号(parse 已经按最后一次覆盖,这里查文档正文里
|
||||
# 是否还有别的表把同一信号挂到两个脚上)
|
||||
rev = {}
|
||||
for n, s in pins.items():
|
||||
rev.setdefault(s, []).append(n)
|
||||
dup = {s: v for s, v in rev.items() if len(v) > 1}
|
||||
if dup:
|
||||
print(u'\n⚠️ 同一信号出现在多个脚上(多半是文档里的历史表没清):')
|
||||
for s, v in sorted(dup.items()):
|
||||
print(u' %-16s → %s' % (s, ', '.join(u'IO%d' % x for x in sorted(v))))
|
||||
|
||||
if a.free:
|
||||
used = set(pins)
|
||||
free = sorted(EXISTS - used - reserved - set(STRAPPING) - set(UART0))
|
||||
print(u'\n还空着的脚(已排除 flash/PSRAM %s、strapping、UART0):'
|
||||
% (u'26~37' if a.octal else u'26~32'))
|
||||
print(u' ' + u', '.join(u'IO%d' % n for n in free))
|
||||
# ⚠️ ADC1 里也要把 strapping 剔掉:IO3 是 ADC1_CH2,但它是 JTAG 源选择
|
||||
# strapping 脚,挂个 NTC 分压上去会在上电瞬间把它拉到中间电平。
|
||||
adc1_free = sorted(ADC1 - used - set(STRAPPING) - reserved)
|
||||
adc1_strap = sorted((ADC1 - used) & set(STRAPPING))
|
||||
print(u' 其中 ADC1(能在 Wi-Fi 下用的模拟脚)还剩:%s'
|
||||
% (u', '.join(u'IO%d' % n for n in adc1_free) if adc1_free else u'无'))
|
||||
if adc1_strap:
|
||||
print(u' (%s 也是 ADC1,但同时是 strapping 脚,接模拟量要先确认上电电平)'
|
||||
% u', '.join(u'IO%d' % n for n in adc1_strap))
|
||||
|
||||
return 1 if ne else 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
在新工单中引用
屏蔽一个用户