Files
STM32F4-Base/tools/netlist_semantics/netlist_sem.py
T

422 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""netlist_sem.py - KiCad Pcbnew 导出网表 (.tel) 语义化工具
将精简导出网表($PACKAGES/$NETS,无料号、无引脚功能名)结合引脚知识库
(pinout_kb.json) 与规则引擎,生成紧凑的结构化语义图(Markdown),
供 LLM / 工程师快速理解板级连接。
用法(仓库根目录,统一 venv 解释器):
& '.venv\Scripts\python.exe' tools/netlist_semantics/netlist_sem.py dump-pins docs/硬件平台/V0.0.tel
& '.venv\Scripts\python.exe' tools/netlist_semantics/netlist_sem.py semantic docs/硬件平台/V0.0.tel -o docs/硬件平台/V0.0_网表语义图.md
纯标准库实现,无第三方依赖。
"""
import argparse
import json
import os
import re
import sys
from collections import OrderedDict, defaultdict
# ---------------------------------------------------------------- 解析
REF_RE = re.compile(r'^[A-Z]{1,4}\d+$')
def _group_entries(lines, is_start=None):
"""按条目分组。is_start(ln) 判断是否新条目起始行;缺省:非空白起始行=新条目。"""
if is_start is None:
def is_start(ln):
return ln[0] not in ' \t'
entries = []
cur = []
for ln in lines:
if ln.strip() == '':
continue
if is_start(ln):
if cur:
entries.append('\n'.join(cur))
cur = [ln]
else:
if not cur:
cur = [ln.strip()]
else:
cur.append(ln.strip())
if cur:
entries.append('\n'.join(cur))
return entries
def _tokens_after(text, sep=';'):
head, _, tail = text.partition(sep)
tail = tail.replace(',', ' ')
return head.strip(), [t for t in tail.split() if t]
def _parse_package(entry):
"""解析单个 PACKAGE 条目,返回 (fp, value, [refs])。
兼容两种行格式:
单行: FP ! FP ! '{Value}' ; REF1 REF2 ...
多行: FP ! FP ! '{Value}' , 然后续行列 REF
字段(封装/值)取首行按 ! 分割;refs 取首行 ';' 之后 + 所有续行。
"""
lines = entry.split('\n')
first = lines[0].strip()
head = first.split(';', 1)[0] if ';' in first else first
fields = [x.strip() for x in head.split('!')]
fp = fields[0] if fields else ''
value = fields[2].strip().rstrip(',') if len(fields) > 2 else ''
refpart = first.split(';', 1)[1] if ';' in first else ''
if len(lines) > 1:
refpart = (refpart + '\n' if refpart else '') + '\n'.join(lines[1:])
toks = re.split(r'[\s,]+', refpart.replace(',', ' '))
refs = [t for t in toks if t and REF_RE.match(t)]
return fp, value, refs
def parse_netlist(path):
"""解析 .tel。返回 (packages, nets)
packages: {ref: {'fp': 封装, 'value': 值}}
nets: {net: [(ref, pin), ...]}
"""
with open(path, encoding='utf-8', errors='replace') as f:
text = f.read()
sections = {}
name, buf = None, []
for ln in text.splitlines():
if ln.startswith('$'):
if name:
sections[name] = buf
name = ln.strip().lstrip('$')
buf = []
elif name:
buf.append(ln)
if name:
sections[name] = buf
packages = {}
for entry in _group_entries(sections.get('PACKAGES', []),
lambda ln: ' ! ' in ln):
fp, value, refs = _parse_package(entry)
if not fp:
continue
for r in refs:
packages[r] = {'fp': fp, 'value': value}
nets = {}
for entry in _group_entries(sections.get('NETS', [])):
head, toks = _tokens_after(entry)
name = head.strip().strip("'\"")
if not name:
continue
pins = []
for t in toks:
ref, _, pin = t.rpartition('.')
if REF_RE.match(ref) and pin:
pins.append((ref, pin))
if pins:
nets[name] = pins
return packages, nets
def build_pin_index(packages, nets):
"""pin_index[ref][pin] = [net, ...];unused_refs = 有封装无网络的位号"""
pin_index = defaultdict(lambda: defaultdict(list))
seen_refs = set()
for net, pins in nets.items():
for ref, pin in pins:
pin_index[ref][pin].append(net)
seen_refs.add(ref)
for ref in list(pin_index):
if ref not in packages:
pass # 网络中出现但 PACKAGES 缺失(如被 DNP 隐藏),仍保留
unused = [r for r in packages if r not in seen_refs]
return pin_index, sorted(unused)
# ---------------------------------------------------------------- 知识库
def load_kb(path):
"""返回 (instances, nets)
instances: {ref: {mpn, pkg, desc}}
nets: {net: {func, gpio, note}}
说明:本工程 KiCad 符号引脚号为任意编号(非数据手册脚号,如 U17.133=PB3),
故语义标注落在【网络名】与【位号】两级,不做引脚级断言。
"""
with open(path, encoding='utf-8') as f:
kb = json.load(f)
return kb.get('instances', {}), kb.get('nets', {})
# ---------------------------------------------------------------- 分类规则
GROUND_NETS = {'GND', 'AGND', 'PGND', 'CGND'}
RAIL_NETS = {'3.3V', '5V', 'C3.3V', 'G3.3V', 'AVCC', 'AVDDL', 'STM_VDDA', 'VDD50'}
CLOCK_NETS = {'XI', 'XO', 'RXI', 'RXO', 'CLOCK'}
def classify_net(name, pins=None, pin_index=None):
"""返回 (类别, 说明)。类别: GND / POWER / CLOCK / SIGNAL
串行时钟 (SCK/SCL/SCLK) 归 SIGNAL(接口信号),仅系统/晶振时钟归 CLOCK。"""
if name in GROUND_NETS or name.endswith('_GND'):
return 'GND', '地'
if name in RAIL_NETS or re.fullmatch(r'\d+\.\d+V', name) or name.startswith('VDD'):
return 'POWER', '电源轨'
if name in CLOCK_NETS:
return 'CLOCK', '时钟'
return 'SIGNAL', '信号'
# 接口分组规则:按网名匹配(顺序敏感,先匹配先归组)
INTERFACE_RULES = [
('以太网 MDI', 'U2 RTL8305 双口 MDI → T1 网络变压器 + J1/J2 RJ45(端口A/B);U1 CH395F 侧为无名网',
lambda n: n in ('RX_P', 'RX_N', 'TX_P', 'TX_N') or
re.fullmatch(r'RX[AB]_[PN]|TX[AB]_[PN]', n) is not None),
('SPI-NAND (SPI1)', 'U17 SPI1 → U18 74LVC16245 → U19 GD5F2GQ5UE NAND(双向缓冲)',
lambda n: n.startswith('GD_') or n in ('SPI1_SOMI', 'SPI1_SIMO')),
('SPI-ETH (SPI2)', 'U17 SPI2 → U1 CH395F(SPI 以太网)',
lambda n: n.startswith('CH395F_')),
('I2C-时钟', 'U17 I2C → U16 SD2506 时钟/RTC 芯片',
lambda n: n in ('SD_SDA', 'SD_SCL')),
('UART-ST', 'U17 USART → U9/U10/U11 隔离器 → RS232/RS485 通道',
lambda n: re.fullmatch(r'ST_(TX|RX)\d', n) is not None or n == 'ST_DIR4'),
('RS-485', 'U11 信号隔离 + U8 光耦(DIR) → U15 TP8485E → U12 ESD → CN1 485 总线',
lambda n: n in ('RS485_A', 'RS485_B', 'TX4', 'RX4', 'DIR4')),
('RS-232', 'U9/U10 隔离 → U13 双通道 232 收发器 → RS232 总线(CN1 / U14)',
lambda n: re.fullmatch(r'RS232_(TX|RX)\d', n) is not None or
re.fullmatch(r'[TR]X\d', n) is not None),
('LCD 8080 并口', 'U17 FSMC → H1 16P 排针 → 板外 LCD(16bit 8080 并口)',
lambda n: n.startswith('LCD_')),
('ADC 并口', 'U17 FSMC 读 ↔ U3 TPAFE5160(16bit + 控制线)',
lambda n: n.startswith('TP_') or re.fullmatch(r'DB\d+', n) is not None),
('ADC 输入', 'U3 TPAFE5160 8 通道输入(经 R 分压/保护至 AIN)',
lambda n: n.startswith('AIN') or n.startswith('ADC')),
('按键', 'U17 GPIO 按键(上拉 3.3V,低有效)',
lambda n: re.fullmatch(r'KEY\d+', n) is not None),
('子板接口 (U18)', 'U17 ↔ U18 74LVC16245 ↔ U5 WINGTAT 子板(继电器/告警/状态/外键)',
lambda n: n.startswith('EXTK') or n.startswith('HT_') or n.startswith('ST_IN_') or
n.endswith('_LED') or n in ('RELAY_STO', 'H16', 'V15', 'J3', 'F3', 'K3', 'RAS',
'DQM0', 'DQM1', 'WAIT1', 'SDCKE', 'CF_P', 'CF_Q',
'V_ZL1', 'V_ZL2', 'R16')),
('SWD/JTAG 调试', 'U17 调试口 → H2 3P 排针(SWCLK/SWDIO/GND)',
lambda n: n in ('SWCLK', 'SWDIO', 'NRST')),
('状态指示', '板级 LED / PHY 中断 / 显示控制',
lambda n: re.fullmatch(r'LED\d+', n) is not None or
n in ('PBLED', 'DIS_EEE', 'INT')),
]
def group_interfaces(signals):
groups = OrderedDict()
ungrouped = []
for net in signals:
placed = False
for gname, gdesc, pat in INTERFACE_RULES:
if pat(net):
groups.setdefault(gname, {'desc': gdesc, 'nets': []})['nets'].append(net)
placed = True
break
if not placed:
ungrouped.append(net)
return groups, ungrouped
# ---------------------------------------------------------------- 输出
def render_semantic(packages, nets, pin_index, instances, nets_kb):
L = []
ap = L.append
def net_ann(net):
"""网络语义注释(来自知识库 nets 表);无名网($..)返回空。"""
if net.startswith('$'):
return ''
ann = nets_kb.get(net)
if not ann:
return ''
bits = []
if ann.get('func'):
bits.append(ann['func'])
if ann.get('gpio'):
bits.append(ann['gpio'])
if ann.get('note'):
bits.append(ann['note'])
return ' '.join(bits)
ics = sorted((r for r in pin_index if r.startswith('U')),
key=lambda r: int(re.sub(r'\D', '', r) or 0))
oths = sorted((r for r in pin_index if not r.startswith('U')))
ap('# V0.0 网表语义图(自动生成)')
ap('')
ap('> 由 `tools/netlist_semantics/netlist_sem.py` 从 `V0.0.tel` + `pinout_kb.json` 生成。')
ap('> 网表仅含位号/引脚号/网名;MPN 与信号语义由知识库补全。')
ap('> 注意:KiCad 符号引脚号为任意编号(非数据手册脚号,如 U17.133=PB3),引脚功能以【网名+知识库】为准。')
ap('')
ap('## 1. 概览')
ap('')
ap(f'- 元件位号: {len(packages)} 组 / 网络: {len(nets)} 条')
n_gnd = sum(1 for n in nets if n in GROUND_NETS or n.endswith('_GND'))
n_rail = sum(1 for n in nets if n in RAIL_NETS)
n_unnamed = sum(1 for n in nets if n.startswith('$'))
ap(f'- 地网络: {n_gnd} / 电源轨: {n_rail} / 未命名(内部)网: {n_unnamed}')
ap('')
# --- 电源域
ap('## 2. 电源与地')
ap('')
ap('| 网络 | 类别 | 连接引脚数 | 主要去向 |')
ap('|---|---|---|---|')
for net in sorted(nets, key=lambda n: (n not in RAIL_NETS, n not in GROUND_NETS, n)):
cat, _ = classify_net(net, nets[net], pin_index)
if cat not in ('GND', 'POWER'):
continue
pins = nets[net]
ics_hit = sorted(set(r for r, _ in pins if r.startswith('U')))
others = sorted(set(r for r, _ in pins if not r.startswith('U')))
dest = ', '.join(ics_hit[:4] + (['…'] if len(ics_hit) > 4 else []))
if others:
dest += f" / {len(others)}×被动件"
ap(f'| {net} | {cat} | {len(pins)} | {dest} |')
ap('')
# --- 接口
groups, ungrouped = group_interfaces(
[n for n in nets if not n.startswith('$') and
classify_net(n, nets[n], pin_index)[0] == 'SIGNAL'])
ap('## 3. 接口与总线')
ap('')
for gname, g in groups.items():
ap(f'### {gname}')
ap('')
ap(f'{g["desc"]}')
ap('')
ap('| 网络 | 语义(知识库) | 连接 |')
ap('|---|---|---|')
for net in g['nets']:
pins = nets[net]
desc = ', '.join(f'{r}.{p}' for r, p in pins[:8]) + (' …' if len(pins) > 8 else '')
ap(f'| {net} | {net_ann(net) or "—"} | {desc} |')
ap('')
if ungrouped:
ap('### 未归类信号')
ap('')
ap(', '.join(ungrouped))
ap('')
# --- 芯片引脚
ap('## 4. 芯片引脚')
ap('')
for ref in ics:
info = instances.get(ref, {})
mpn = info.get('mpn', '?')
desc = info.get('desc', '')
fp = packages.get(ref, {}).get('fp', '?')
pins = pin_index[ref]
ap(f'### {ref} — {mpn}')
ap('')
if desc:
ap(f'{desc}')
ap('')
ap(f'封装: `{fp}`({len(pins)} 脚入网)')
ap('')
ap('| Pin | 网络 | 语义(知识库) |')
ap('|---|---|---|')
def _key(kv):
p = kv[0]
m = re.fullmatch(r'(\d+)', p)
return (0, int(m.group(1))) if m else (1, p)
for pin, nets_h in sorted(pins.items(), key=_key):
uniq = sorted(set(nets_h))
ann = ';'.join(a for a in (net_ann(n) for n in uniq) if a) or '—'
ap(f'| {pin} | {", ".join(uniq)} | {ann} |')
ap('')
# --- 其余器件
ap('## 5. 连接件/晶振/变压器/无源/分立')
ap('')
ap('| 位号 | 器件(知识库 MPN) | 引脚→网络 |')
ap('|---|---|---|')
for ref in oths:
info = instances.get(ref, {})
mpn = info.get('mpn', '')
fp = packages.get(ref, {}).get('fp', '?')
what = f'{mpn}({fp})' if mpn else fp
pins = pin_index[ref]
desc = '; '.join(f'{p}→{", ".join(sorted(set(ns)))}'
for p, ns in sorted(pins.items(), key=lambda kv: kv[0]))
ap(f'| {ref} | {what} | {desc} |')
ap('')
# 文件结尾必须恰好一个换行:pre-commit end-of-file-fixer 同时禁止
# “无结尾换行”和“结尾空行”(双换行)。rstrip 归一化末尾多余换行
# (含上方收尾的 ap('')),今后增删末尾行也不会破坏提交门禁。
return '\n'.join(L).rstrip('\n') + '\n'
def dump_pins(packages, nets):
pin_index, unused = build_pin_index(packages, nets)
print(f'== PACKAGES: {len(packages)} refs; NETS: {len(nets)} ==')
print(f'== refs in packages but on no net: {unused} ==')
ics = sorted((r for r in pin_index if r.startswith('U')),
key=lambda r: int(re.sub(r'\D', '', r) or 0))
oths = sorted(r for r in pin_index if not r.startswith('U'))
for ref in ics + oths:
fp = packages.get(ref, {}).get('fp', '?')
pins = pin_index[ref]
print(f'\n### {ref} [{fp}] ({len(pins)} pins)')
def _key(kv):
p = kv[0]
m = re.fullmatch(r'(\d+)', p)
return (0, int(m.group(1))) if m else (1, p)
for pin, nets_h in sorted(pins.items(), key=_key):
print(f' {pin:>4} {", ".join(sorted(set(nets_h)))}')
# ---------------------------------------------------------------- main
def main():
here = os.path.dirname(os.path.abspath(__file__))
ap = argparse.ArgumentParser(
description='KiCad Pcbnew .tel 网表语义化(解析 + 引脚知识库 + 规则引擎)')
sub = ap.add_subparsers(dest='cmd', required=True)
p1 = sub.add_parser('dump-pins', help='按位号列出 引脚→网络(用于编制知识库)')
p1.add_argument('netlist')
p2 = sub.add_parser('semantic', help='生成语义图 Markdown')
p2.add_argument('netlist')
p2.add_argument('-o', '--out', default=None,
help='输出文件(缺省打印到 stdout)')
p2.add_argument('--kb', default=os.path.join(here, 'pinout_kb.json'))
a = ap.parse_args()
packages, nets = parse_netlist(a.netlist)
if a.cmd == 'dump-pins':
dump_pins(packages, nets)
else:
pin_index, unused = build_pin_index(packages, nets)
instances, nets_kb = load_kb(a.kb)
doc = render_semantic(packages, nets, pin_index, instances, nets_kb)
if a.out:
with open(a.out, 'w', encoding='utf-8', newline='\n') as f:
f.write(doc)
print(f'written: {a.out} ({len(doc)} chars)')
else:
sys.stdout.write(doc)
print(f'[stats] packages={len(packages)} nets={len(nets)} '
f'kb_instances={len(instances)} kb_nets={len(nets_kb)} unused={unused}',
file=sys.stderr)
if __name__ == '__main__':
main()