跳转到内容
新建笔记

SKiDL:符号库、引脚与可验证网表

SKiDL 用 Python 创建元件、引脚和网络,再导出网表。它适合把重复连接、参数和层级组织写成可检查的程序;生成网表并不表示已经完成 PCB 布局、电气设计或实物验证。

本文示例固定为 SKiDL 2.3.0 与 KiCad 官方符号库 9.0.0。Python 包、符号库和封装库是三份不同的依赖:安装 SKiDL 不等于已经安装 KiCad 的全部库。示例实际使用其中14个符号库文件;封装名称写入网表,但本文没有加载封装实体或生成 PCB。

在已启用的 Python 虚拟环境中安装固定版本:

终端窗口
python -m pip install "skidl==2.3.0"

准备一个符号库目录,例如当前目录下的 symbols/,其中直接包含 Device.kicad_sym、Connector_Generic.kicad_sym 等文件。可以使用相同版本的本地 KiCad 安装,或取得KiCad 官方 9.0.0 符号库。复制下面四个完整程序后,以这个目录作为第一个参数运行。

各程序明确指定 tool=KICAD9 和 lib_search_paths[KICAD9]。这避免把 KICAD 默认别名误当作固定版本:本次安装的 SKiDL 2.3.0 默认工具已经指向 KiCad 10。若希望通过环境设置默认搜索路径,应按所选版本使用 KICAD9_SYMBOL_DIR;原稿中的 KICAD_SYMBOL_DIRS 不是此版本导出的接口。

导入时可能提示其他 KiCad 版本的路径未设置;本例依靠随后明确配置的 KiCad 9 搜索路径工作。fp-lib-table 缺失则表示没有可查询的封装库,不能据网表里的一个字符串宣布封装已经验证。命令行输出文件也不要命名成与脚本同名的 .log,以免在 Windows 上与 SKiDL 自建日志争用文件。

元件、引脚、网络与模板

跳转到“元件、引脚、网络与模板”
对象或操作含义容易混淆的地方
Part(library, name, ...)从指定库创建元件实例库名、符号名都必须真实存在;器件商品料号未必等于符号名
part[1]、part["8"]按引脚号选取端子引脚号来自符号定义及封装映射,不能按绘图顺序推算
part["V+"]按引脚名匹配同名引脚可能有多个;多单元器件尤其需要检查
Net("GND")创建一个网络对象名字叫 GND 不代表已经接上所有器件的地引脚
net += pin1, pin2把多个端子连接到网络Python 变量名、网络名和器件位号是不同概念
pin1 += pin2直接把两只引脚连到同一网络没有显式命名时,导出的网络可能使用自动名称
dest=TEMPLATE创建供复制或查询的模板模板不是电路里的实物实例,不在模板上设置位号
template * 8从模板创建八个实例实例可分别设置阻值、位号和稳定的标签
Circuit()明确组织一份电路用上下文管理器把该段创建的网络与实例放入对应电路

调试时可以查看 part.get_pins()、引脚的 num/name 和网络所含端子,再检查导出的网表;不要仅凭变量名判断连接正确。ERC() 检查部分引脚类型、驱动和连接问题,仍需要阅读其告警与错误。

保存为 inspect_symbols.py。程序逐一加载原笔记涉及的32个官方符号,打印引脚数量,展示 LM358 的实际引脚名称,并按名称查询 ATmega328P 的 PB0;最后通过符号库文件路径创建一个电阻实例。

inspect_symbols.py
import argparse
from pathlib import Path
from skidl import Circuit, KICAD9, Part, SchLib, TEMPLATE, lib_search_paths, search, show
CATALOG = (
("Device", "R"), ("Device", "C"), ("Device", "L"),
("Device", "D"), ("Device", "D_Schottky"), ("Device", "LED"),
("Device", "C_Polarized"), ("Device", "Crystal_GND24"),
("Switch", "SW_Push"),
("Transistor_BJT", "Q_NPN_BCE"), ("Transistor_BJT", "2N3904"),
("Transistor_FET", "IRF540N"),
("Diode", "1N4148"), ("Diode", "1N4007"),
("Connector_Generic", "Conn_01x02"),
("Connector_Generic", "Conn_01x04"),
("Connector_Generic", "Conn_02x05_Odd_Even"),
("Connector_Generic", "Conn_01x40"),
("Connector", "USB_A"), ("Connector", "USB_B_Micro"),
("Connector", "Barrel_Jack"),
("Amplifier_Operational", "LM358"),
("Amplifier_Operational", "TL072"),
("Amplifier_Operational", "LM741"),
("74xx", "74HC00"), ("74xx", "74HC04"),
("Timer", "NE555D"),
("MCU_Microchip_ATmega", "ATmega328P-P"),
("MCU_ST_STM32F1", "STM32F103C8Tx"),
("Regulator_Linear", "AMS1117-3.3"),
("Regulator_Linear", "AMS1117-5.0"),
("Regulator_Switching", "LM2596S-5"),
)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("symbols", type=Path)
parser.add_argument("--search", action="store_true")
args = parser.parse_args()
symbols = args.symbols.resolve()
if not symbols.is_dir():
parser.error("symbols must be an existing symbol-library directory")
lib_search_paths[KICAD9] = [str(symbols)]
print("libraries:", ", ".join(sorted(p.stem for p in symbols.glob("*.kicad_sym"))))
for library, name in CATALOG:
part = Part(library, name, tool=KICAD9, dest=TEMPLATE)
print(f"{library}:{name}: {len(part.pins)} pins")
if args.search:
search("LM358", tool=KICAD9) # Prints matches; this is not a returned list.
amplifier = show("Amplifier_Operational", "LM358", tool=KICAD9)
if amplifier is None:
raise RuntimeError("LM358 symbol could not be inspected")
print("LM358:", [(pin.num, pin.name) for pin in amplifier.get_pins()])
mcu = Part("MCU_Microchip_ATmega", "ATmega328P-P", tool=KICAD9, dest=TEMPLATE)
pin = mcu["PB0"]
print("ATmega328P PB0:", pin.num, pin.name)
direct_library = SchLib(str(symbols / "Device.kicad_sym"), tool=KICAD9)
with Circuit():
resistor = Part(direct_library, "R", tool=KICAD9, value="10k", ref="R100")
resistor.value = "4.7k"
print("direct file:", resistor.ref, resistor.value)
if __name__ == "__main__":
main()
终端窗口
python inspect_symbols.py ./symbols --search

SKiDL 2.3.0 的 search / search_parts 输出匹配结果,不能把它们当成返回元件列表的函数。show / show_part 返回用于查看引脚的模板;失败时应检查是否返回 None。原稿中的 lib_search()、part_search() 在此版本并不存在。查看库文件可以像程序中那样列出 *.kicad_sym;查询语义与接口见 SKiDL 文档。

下面列出原示例中需要修正的符号路径,其余无源器件、二极管、运放、连接器、MCU和电源器件已保留在程序的 CATALOG 中。

原记录写法本文验证的 KiCad 9.0.0 写法
Device:Q_NPN_BCETransistor_BJT:Q_NPN_BCE
Device:SW_PushSwitch:SW_Push
Device:CPDevice:C_Polarized
Connector:Conn_01x02、Conn_01x04、Conn_02x05_Odd_Even对应符号在 Connector_Generic
Logic_74xx:74HC00、74HC04对应符号在 74xx
MCU_Microchip_ATmega:ATmega328P-PU库中符号为 ATmega328P-P;实际采购型号和封装还需按器件资料核对
Regulator_Switching:LM2596S-5.0库中符号为 LM2596S-5

第三方库则需要另外取得与核对。原稿中的 SparkFun-Sensors:MPU-9250 和 Adafruit:Feather_ESP32 保留为其原有使用意图,不保证它们存在于当前第三方库,也没有把这两项算作已加载成功。可把可信的库目录加入相应 lib_search_paths,或像程序中的 SchLib(...) 一样明确读取已知文件;旧 .lib 与新 .kicad_sym 应选择对应格式的工具接口。

保存为 rc_filter.py。三针接口依次为输入、输出、地,串联电阻后接对地电容。low_pass 显式接收三个网络,消除了原稿中未定义的 gnd、input1 和 output1。

rc_filter.py
import argparse
import math
from pathlib import Path
from skidl import Circuit, KICAD9, Net, Part, erc_logger, lib_search_paths
def low_pass(input_net, output_net, ground_net, cutoff_hz, resistance_ohms):
if not all(math.isfinite(v) and v > 0 for v in (cutoff_hz, resistance_ohms)):
raise ValueError("frequency and resistance must be finite and positive")
capacitance = 1 / (2 * math.pi * cutoff_hz * resistance_ohms)
resistor = Part("Device", "R", tool=KICAD9,
value=f"{resistance_ohms:g}",
footprint="Resistor_SMD:R_0805_2012Metric", tag="filter-r")
capacitor = Part("Device", "C", tool=KICAD9,
value=f"{capacitance * 1e9:.6g}nF",
footprint="Capacitor_SMD:C_0805_2012Metric", tag="filter-c")
input_net += resistor[1]
output_net += resistor[2], capacitor[1]
ground_net += capacitor[2]
return resistor, capacitor, capacitance
def build_filter(symbols, cutoff_hz=1000.0, resistance_ohms=1000.0):
symbols = Path(symbols).resolve()
if not (symbols / "Device.kicad_sym").is_file():
raise FileNotFoundError("symbols directory must contain Device.kicad_sym")
lib_search_paths[KICAD9] = [str(symbols)]
circuit = Circuit()
with circuit:
input_net, output_net, ground_net = Net("INPUT"), Net("OUTPUT"), Net("GND")
connector = Part("Connector_Generic", "Conn_01x03", tool=KICAD9,
footprint="Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical",
tag="signals")
input_net += connector[1]
output_net += connector[2]
ground_net += connector[3]
resistor, capacitor, capacitance = low_pass(
input_net, output_net, ground_net, cutoff_hz, resistance_ohms)
return circuit, resistor, capacitor, capacitance
def main():
parser = argparse.ArgumentParser()
parser.add_argument("symbols", type=Path)
parser.add_argument("--cutoff", type=float, default=1000.0)
parser.add_argument("--resistance", type=float, default=1000.0)
parser.add_argument("--output", type=Path, default=Path("rc_filter.net"))
args = parser.parse_args()
circuit, resistor, capacitor, capacitance = build_filter(
args.symbols, args.cutoff, args.resistance)
circuit.ERC()
if erc_logger.error.count:
raise RuntimeError("Resolve ERC errors before exporting")
circuit.generate_netlist(file=str(args.output), tool=KICAD9, do_backup=False)
print(f"{resistor.ref}={resistor.value}; {capacitor.ref}={capacitor.value}")
print(f"ideal capacitance={capacitance:.9g} F; output={args.output}")
if __name__ == "__main__":
main()
终端窗口
python rc_filter.py ./symbols --cutoff 1000 --output rc_1k.net
python rc_filter.py ./symbols --cutoff 10000 --output rc_10k.net

理想无负载、源阻抗可忽略时,fc=1/(2πRC)f_c=1/(2\pi RC)。取 R=1 kΩR=1\,\mathrm{k\Omega},1 kHz 对应约159.155 nF,10 kHz 对应约15.9155 nF。程序输出的是按公式计算的数值;采购时需要选择实际标准值,并重新考虑容差、信号源阻抗和负载。网表不会自动完成这些分析。

程序为元件填写了具体封装字符串和 tag。封装字符串需要与实际安装的库、采购器件及焊盘编号核对;tag 用于保持元件关联,不能代替位号或封装检查。根节点或子电路缺少层级标签时,本次网表生成器仍会给出相应提示,示例没有关闭这些提示。

模板、八位总线与分压子电路

跳转到“模板、八位总线与分压子电路”

保存为 bus_and_divider.py。前半部分把两组八针接口逐位通过电阻相连,每个位使用独立网络;它替换原稿中没有定义的 some_ic。后半部分示范三针接口和一个20 kΩ/10 kΩ分压器。

bus_and_divider.py
import argparse
from pathlib import Path
from skidl import (Bus, Circuit, KICAD9, Net, Part, TEMPLATE, erc_logger,
lib_search_paths, subcircuit)
@subcircuit
def voltage_divider(input_net, output_net, ground_net,
upper_value="20k", lower_value="10k"):
upper = Part("Device", "R", value=upper_value, tool=KICAD9,
footprint="Resistor_SMD:R_0805_2012Metric", tag="upper")
lower = Part("Device", "R", value=lower_value, tool=KICAD9,
footprint="Resistor_SMD:R_0805_2012Metric", tag="lower")
input_net += upper[1]
output_net += upper[2], lower[1]
ground_net += lower[2]
def build_demo(symbols):
symbols = Path(symbols).resolve()
if not (symbols / "Device.kicad_sym").is_file():
raise FileNotFoundError("symbols directory must contain Device.kicad_sym")
lib_search_paths[KICAD9] = [str(symbols)]
circuit = Circuit()
with circuit:
template = Part("Device", "R", value="1k", tool=KICAD9, dest=TEMPLATE,
footprint="Resistor_SMD:R_0805_2012Metric")
resistors = template * 8
data = Bus("DATA", 8)
output = Bus("OUTPUT", 8)
connector_footprint = "Connector_PinHeader_2.54mm:PinHeader_1x08_P2.54mm_Vertical"
input_connector = Part("Connector_Generic", "Conn_01x08", tool=KICAD9,
footprint=connector_footprint, tag="bus-in")
output_connector = Part("Connector_Generic", "Conn_01x08", tool=KICAD9,
footprint=connector_footprint, tag="bus-out")
for index, resistor in enumerate(resistors):
resistor.value = f"{(index + 1) * 1000}"
resistor.tag = f"bus-r-{index}"
data[index] += input_connector[index + 1], resistor[1]
output[index] += resistor[2], output_connector[index + 1]
supply, divided, ground = Net("VCC"), Net("DIVIDED"), Net("GND")
header = Part("Connector_Generic", "Conn_01x03", tool=KICAD9,
footprint="Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical",
tag="divider-header")
supply += header[1]
divided += header[2]
ground += header[3]
voltage_divider(supply, divided, ground)
return circuit
def main():
parser = argparse.ArgumentParser()
parser.add_argument("symbols", type=Path)
parser.add_argument("--output", type=Path, default=Path("bus_and_divider.net"))
args = parser.parse_args()
circuit = build_demo(args.symbols)
circuit.ERC()
if erc_logger.error.count:
raise RuntimeError("Resolve ERC errors before exporting")
circuit.generate_netlist(file=str(args.output), tool=KICAD9, do_backup=False)
print(f"parts={len(circuit.parts)}; output={args.output}")
if __name__ == "__main__":
main()
终端窗口
python bus_and_divider.py ./symbols

Bus 是一组可索引的网络,不会自动把八根线短接,也不是具有通信协议的硬件总线控制器。循环中的 index+1 是本例连接器1~8的引脚号;更换器件后不能继续假设信号号与引脚号存在这种简单对应关系。

分压器把输入、输出、地都作为参数传入,因此不依赖外部全局变量。理想空载条件下,输出为输入的 10/(20+10)=1/310/(20+10)=1/3;有负载时应把负载与下臂电阻的并联作用计算进去。复用子电路时还应为每个实例设置稳定的层级标识并检查生成网表的命名。

LM358 的多单元与供电引脚

跳转到“LM358 的多单元与供电引脚”

KiCad 9.0.0 的 Amplifier_Operational:LM358 包含两路运放及供电引脚:

功能引脚号符号中的引脚名
第一组输出、反相、同相输入1、2、3~、-、+
第二组同相、反相、输出5、6、7+、-、~
正、负电源8、4V+、V-

原示例直接索引 IN+、OUT、VCC、GND,与该符号实际名字不符。两组输入名称又重复,所以本例按引脚号连接。保存为 lm358_buffer.py:

lm358_buffer.py
import argparse
from pathlib import Path
from skidl import Circuit, KICAD9, Net, Part, POWER, erc_logger, lib_search_paths
def build_buffer(symbols):
symbols = Path(symbols).resolve()
if not (symbols / "Amplifier_Operational.kicad_sym").is_file():
raise FileNotFoundError("symbols directory lacks Amplifier_Operational.kicad_sym")
lib_search_paths[KICAD9] = [str(symbols)]
circuit = Circuit()
with circuit:
input_net, output_net = Net("INPUT"), Net("OUTPUT")
supply, ground = Net("VCC"), Net("GND")
# The header represents an external, already supplied power source.
supply.drive = POWER
ground.drive = POWER
opamp = Part("Amplifier_Operational", "LM358", tool=KICAD9,
footprint="Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", tag="opamp")
resistor = Part("Device", "R", value="1k", tool=KICAD9,
footprint="Resistor_SMD:R_0805_2012Metric", tag="input-r")
capacitor = Part("Device", "C", value="100nF", tool=KICAD9,
footprint="Capacitor_SMD:C_0805_2012Metric", tag="decoupling")
header = Part("Connector_Generic", "Conn_01x04", tool=KICAD9,
footprint="Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical",
tag="signals-power")
input_net += header[1], resistor[1]
resistor[2] += opamp[3] # First amplifier's non-inverting input.
output_net += header[2], opamp[1], opamp[2] # Voltage-follower feedback.
supply += header[3], opamp[8], capacitor[1]
ground += header[4], opamp[4], capacitor[2], opamp[5]
opamp[7] += opamp[6] # Second amplifier: unity feedback, input at GND.
return circuit
def main():
parser = argparse.ArgumentParser()
parser.add_argument("symbols", type=Path)
parser.add_argument("--output", type=Path, default=Path("lm358_buffer.net"))
args = parser.parse_args()
circuit = build_buffer(args.symbols)
circuit.ERC()
if erc_logger.error.count:
raise RuntimeError("Resolve ERC errors before exporting")
circuit.generate_netlist(file=str(args.output), tool=KICAD9, do_backup=False)
print(f"parts={len(circuit.parts)}; output={args.output}")
if __name__ == "__main__":
main()
终端窗口
python lm358_buffer.py ./symbols

第一路组成电压跟随连接;第二路的同相端接地、输出回接反相端,避免把另一半运放完全遗漏。VCC/GND 声明为外部电源驱动,只是 ERC 所用的来源描述,不能让物理电路自行供电。这里检查的是网表连接,没有模拟放大器的输入共模范围、输出摆幅、稳定性或上电过程;LM358也不是理想轨到轨器件,实际供电与信号范围要核对选用型号的资料。TI LM358 数据手册的引脚表与电气特性

原稿的 Arduino 和 STM32 片段只创建了若干元件,没有连接成可运行开发板。下面保留这些设计意图,并列出继续设计所需的信息。

原场景与元件原有参数/连接意图必须另行完成的部分
ATmega328P、Crystal_GND24、两只22 pF电容晶振回路按晶体负载电容和杂散电容计算;核对晶体四焊盘中哪些属于外壳接地,不能全部当振荡端
SW_Push、10 kΩ电阻复位按钮与上拉确认复位引脚、有效极性、去抖/滤波和下载方式
Barrel_Jack、AMS1117-5.0、100 µF极性电容输入供电与稳压输入范围、压差、功耗、稳压器所需电容及极性,不能只连接一个稳压器符号
LED、1 kΩ电阻、USB_B_Micro状态指示与USB接口LED电流和极性;ATmega328P不因添加USB插座就获得原生USB功能,需明确转换接口或实际用途
STM32F103C8Tx、四只100 nF电容、10 µF极性电容去耦与储能逐一连接所有电源/地、模拟供电和备份电源,按器件资料布置去耦
四针SWD接口、状态LED与用户按钮调试和人机输入明确SWDIO、SWCLK、参考电压、地及复位需求;四针是否足够取决于调试器与设计约定

本次实际加载了两个MCU符号并核对引脚数量(28与48),没有声称这两份清单已经构成可用的Arduino或STM32板卡。作为接口架构参考,Arduino UNO R3 官方手册列出了独立的 ATmega16U2 USB 桥接处理器;USB 插座与 USB 控制功能必须分别考虑。

原稿还建议把常用电阻、电容创建过程封装为便捷函数。可以沿用这一做法,显式传入 value、ref、封装和电路上下文;本文的 low_pass、voltage_divider 进一步把相互连接的一组元件封装起来。库加载失败时应保留具体异常信息,区分目录不存在、符号名错误和文件解析失败,避免用一个不带异常类型的 except 把所有问题都归结为“库未安装”。

本页四个Python程序已在 Python 3.12、SKiDL 2.3.0 和固定版本符号库上实际执行。三个电路均导出网表,连接拓扑、元件值、总线各位隔离、LM358两组放大器和供电脚被逐项核对;独立检查包含参数边界及故意制造的输出冲突。ERC 的通过只覆盖其规则范围,封装库缺失与层级标签提示保留在验证记录中。

没有生成原理图绘图、PCB文件、Gerber或实物测试结论。继续设计时,从原理图阅读核对网络与器件资料,再按PCB串扰与布线约束检查物理实现。

来源:CSDN 原文,首次发布于2025-07-17;API和库名按上述固定版本重新核对。