尧图精选

PyCircuit 6:用Python自动化电路仿真与PCB设计的开源框架

🕒 发布时间:2026/9/28 1:09:17 📁 来源:尧图网络
为了那瓶醋把一整套硬件开发的锅碗瓢盆都重新掂量了一遍——听起来像冲动消费实际上是我这半年最上头的技术折腾。PyCircuit 6这个名字表面上是一个基于Python的电路描述、仿真与PCB生成框架本质上却是一套从原理图想法到可打样板卡的全流程自动化方案。这套东西不是给画板工具有什么意见而是给像我这种习惯在代码里思考电路的人准备的。如果你也经历过在图形化EDA里拉线拉到眼瞎、想用脚本批量生成板卡却找不到趁手接口、或者仅仅想在Jupyter里快速验证一个电路拓扑却被工具链折腾到怀疑人生——那这篇东西你应该能看出点门道。1. 这盘饺子是怎么开始包的1.1 那瓶醋一次被工具链搞崩溃的“小实验”事情的起因特别小。我手头有个NTC温度监测的小设计说白了就是一颗负温度系数热敏电阻配合一个运放跟随器再接到单片机的ADC引脚上。放在正常流程里这活儿交给KiCad或者立创EDA从画原理图到出PCB熟练工一小时能搞定。但我当时的状态是手头有一堆仿真要在不同温度点下验证阻值分压的线性度还要对比几种运放的实际失调电压影响。在图形化EDA里干这事就变得格外痛苦——每换一个电阻值就要手动改一次原理图跑仿真又要切到另外一个软件数据还得自己导出来分析。我当时的念头特别朴素我能不能用Python写几行描述性的代码把这个电路定义出来然后自动跑仿真、自动出网表、甚至直接把PCB底稿给生成了为了这一瓶醋我起手写了一百多行临时脚本。那会儿我真没打算做一个框架我只是想赶紧把手头这个小实验跑完。结果呢可想而知——脚本越写越厚越写越像样回头看的时候PyCircuit的雏形已经躺在那儿了。1.2 现成的工具链为什么让人想自己动手很多人会问LTSpice不香吗KiCad加Python脚本库不香吗怎么就非要自己包一盘饺子。先说仿真。LTSpice的电路描述确实严谨但那套SpiceNetlist的写法符号化的元件命名方式跟现代程序员惯用的一切皆对象、一切皆表达式的思维模式差着辈分。你需要把电阻写成R1 N001 N002 10k这种平面文本模型在稍微复杂点的电路里这种纯文本网表简直像天书。PySpice等其他Python封装库确实帮了大忙但它依旧没脱离文本网表的面向过程封装电路结构仍然是扁平的节点列表没法自然地表达层次化、参数化的设计意图。再说PCB设计。业界确实有SKiDL这样的Python库可以通过代码生成网表和PCB但它偏底层缺少现代Python项目该有的类型提示、数据验证和友好的异常处理。我自己尝试过的典型路径是这样的用Python脚本生成网表文件再喂给KiCad的PCB编辑器做布局布线。听起来很美好实际上网表格式的兼容性坑多到爆坐标原点的约定、封装名的匹配、甚至元件编号的排序规则稍微不一致就能让整个流程报废。而市面上更流行的做法是继续在图形化EDA里手工操作配合厂商提供的脚本插件做一些批量修改。你要是只改三五个电阻值这方案没毛病要是想在一个晚上扫完上百个温度采样点的器件选型组合手动画图就成了返工大赛。我希望的形态是电路本身就是代码仿真和板卡设计都是这门代码的编译产物——不是用脚本去操作EDA而是用声明式编程直接表达硬件。这就是PyCircuit 6最终的核心理念。1.3 从临时脚本到第六个大版本的进化路线版本越迭代越能看出最初设计的幼稚。v0.x时代PyCircuit跟个玩具一样只能描述电阻、电容这些无源元件仿真功能靠调用外部LTSpice来跑本质是一个网表生成器加仿真结果解析器。v1到v3开始加入基础的元件库管理、常数表达式和参数扫描功能勉强能用于简单的分压和RC滤波计算。v4是一个分水岭我把仿真引擎从外包给外部工具换成了内置的动态编译方案——通过解析电路描述动态构造出点数方程组再用改进的牛顿-拉夫逊迭代求解。这一版的代价是迭代收敛算法写得比较糙稍微遇到非线性强的器件就崩。v5光荣地进入了能被真实项目使用但只敢私下推荐的阶段加入了SPICE兼容子电路、节点电压追踪这些实用功能。而v6就是我眼前这版算是一次彻底的内部重构——把原来纠缠在一起的电路定义、仿真求解和PCB导出拆成三个独立模块同时引入了异步仿真和组件级热分析接口。写到这里我自己都有点感慨一开始真的只是想要一瓶醋啊。2. PyCircuit 6的核心设计与模块拆解2.1 电路描述拿来写电路的Python长什么样PyCircuit 6最核心的设计决策是让用代码描述电路这件事变得像写普通Python类一样自然。它没有自定义一套奇怪的DSL领域特定语言而是完全基于Python的对象体系来做声明式建模。一个典型的NTC分压电路在PyCircuit 6里的描述是这样的from pycircuit import Circuit, Resistor, NTC from pycircuit.components.opamp import OpAmp from pycircuit.analysis import DCSweep class TemperatureSensor(Circuit): def configure(self): # 上拉电阻10kΩ self.r1 Resistor(value10_000, tolerance0.01) # NTC热敏电阻25℃时10kΩB值3950 self.ntc NTC(r2510_000, b_value3950, temp_c25.0) # 运放跟随器把高阻抗输出变成低阻抗输出 self.op OpAmp(modelLM358, supply_voltage(3.3, 0)) # 连接关系 self.r1.connect(self.node_vcc, self.node_ntc_top) self.ntc.connect(self.node_ntc_top, self.node_gnd) self.op.input_positive(self.node_ntc_top) self.op.input_negative(self.node_ntc_top) # 单位增益负反馈 self.op.output(self.node_adc)这段代码不需要任何注释稍微懂点电路的人就能直接读懂。这是PyCircuit 6和旧版网表工具的本质区别元件是对象连接点是节点对象元件之间的连接就是方法调用。其实这个设计最开始灵感来自Python里ORM对象关系映射的思路——像SQLAlchemy那样把数据库表结构用类来定义PyCircuit就是把电路拓扑用类来定义了。节点系统在实现上有个非常关键的细节所有的connect方法返回的node对象内部会维护一个连接集执行拓扑检查时会自动判断短路、开路和悬空节点。这个机制很便宜但对新手友好度极高——你会在拿到仿真结果之前就发现这个点没接上或这两个网络短在一起了。2.2 仿真内核不妥协的求解器设计电路描述再漂亮仿真不准也是白搭。PyCircuit 6的仿真引擎是在v4时代积累下的经验上彻底重写的。它采用改进节点分析法MNAModified Nodal Analysis来构造电路方程这一点与SPICE的主流程一致。差别在于求解层——我没有去逐行翻译SPICE的C代码而是用NumPy的稀疏矩阵工具直接从零实现了一套求解器。所有元件模型的雅可比矩阵偏导都是逐项解析计算得到的在迭代收敛时能做到稳定、快速。直接上关键参数这里有一个我踩过好几次坑的经验求解器配置项: max_iterations: 25 # 牛顿迭代最大次数 convergence_abs: 1e-9 # 绝对收敛阈值电压 convergence_rel: 1e-6 # 相对收敛阈值 dc_ramp_steps: 5 # 直流工作点逐步提升次数 temp: 27.0 # 仿真温度摄氏度这套配置解决了我过去遇到的直流工作点不收敛老大难问题。原理很简单直接在室温下求解一个带二极管的电路时初始猜测值容易让牛顿迭代震荡Derivative的值在指数区附近会爆炸。通过把电源电压从0开始逐步ramp到目标值5步每一步都以上一步的解作为初值迭代稳定性的收益立竿见影。所以如果你遇到仿真结果满地打滚第一个检查项就是dc_ramp_steps——很多商业EDA把这个开关藏得很深我直接把它提到了配置面板的显眼位置。v6的另外一个升级是异步仿真。过去跑一个蒙特卡洛参数扫描几百个仿真案例会阻塞住整个Python进程。现在PyCircuit底层用asyncio并行调度加上numba对器件模型的热点做JIT加速一个200个样本的容差分析从过去的三分钟压到了二十秒左右。2.3 PCB导出从网表到板卡的最后一公里如果说电路描述和仿真是饺子的馅那PCB导出就是最后那口锅不给力整个饺子就白包了。PyCircuit 6的PCB接口采用分层设计最底层是统一的元件-封装-引脚模型中间层是网表生成器支持KiCad 6/7的s-expression格式最上层是板卡布局驱动。我在这层做了一个跟自己较劲的设计决定不搞自动布线。市面上的开源自动布线器比如KiCad的Freerouting插件体验大家心里有数与其在自动布线里做二次封装不如把布局这个能自动化做好而布线交给工程师自己用KiCad处理。所以PCB导出的产物是一个扔进KiCad就能继续编辑的工程文件包含完整的元件坐标、封装分配、覆铜约束和网表。实际生成效果类似这样from pycircuit.pcb import PcbProject, Placement project PcbProject(board_nametemp_sensor_v1, width_mm20, height_mm15) place Placement() place.put(r1, position_mm(2.0, 3.0), rotation0) place.put(ntc, position_mm(2.0, 7.0), rotation0) place.put(op, position_mm(9.0, 5.0), rotation90) project.add_placement(place) project.export_kicad(output/temp_sensor)导出后每个元件的坐标会精确到0.01mm封装名跟当前KiCad的标准库保持同步。更重要的是所有网络会带上从仿真阶段继承过来的电气属性——比如node_adc会被标记为模拟信号网络自动添加覆铜间距规则。这个细节很多人看不起但它直接决定了你从PyCircuit导出到PCB工程的间隙需不需要手工补规则。2.4 版本6的架构升级解耦、插件与被逼出来的类型标注PyCircuit 6从设计上一开始就强制三个层次解耦Circuit电路定义、Solver数字求解、PCB制造输出。不夸张地说v5时代这三块是缠在一起的要单独换某个存储格式或者替换求解器得动“手术”。v6用了严格的分层接口每个模块面对的都是明确定义的数据模型而不是直接调用对方内部函数。额外多嘴一句这次升级我还做了一个在开源项目里不算特别常见但很有用的决定——整个代码库加了完整的dataclass数据模型和类型标注并公开了API离线文档。你可能会觉得类型标注有什么稀奇实际上对于硬件描述类的库来说类型系统太重要了。你定义Resistor(value...)的时候如果传进去一个字符串pyright/pylance在代码编辑器里就能立刻给你划红线而不是拖到仿真的时候才发现参数错误。我在README里写了一句:这可能是你用过最先报错的硬件设计库因为它压根不给你把小错误带到仿真阶段的机会。3. 实操过程用PyCircuit 6完整搭建一个温度监测电路3.1 环境准备与安装清单开始之前先把环境搭好。PyCircuit 6要求Python 3.10及以上核心依赖是numpy、scipy和numba。安装命令很简单pip install pycircuit6.0.0如果你想在Jupyter里交互式跑仿真并把波形图画出来我推荐顺手装一个ipywidgets和plotly。另外在实际的PCB导出环节你本机最好装一个KiCad 7至少6.0以上因为PyCircuit生成的工程文件并不自带板框和覆铜的图形界面编辑能力向导式流程依赖KiCad来兜底。平台方面Windows/Linux/macOS我都实测过。一个小心得在Linux上安装numba时可能会遇到LLVM编译警告属于正常现象不影响运行。真正要注意的是Python版本3.12以下最稳3.12对numba的版本有额外要求建议装之前看一眼官方支持矩阵。3.2 五步搭一个NTC温度监测电路我已经把全部代码写在下面了这一段可以直接“抄作业”。第一步是定义元件库参数。NTC热敏电阻的温阻曲线我用的是一个简化β模型其实真实器件更精确的是Steinhart-Hart方程。为了方便对比我在代码里同时写了两种模型调用方式import numpy as np from pycircuit import Circuit, Resistor, NTC, Subcircuit from pycircuit.components.opamp import OpAmp class TempSensor(Circuit): def configure(self): self.vcc self.create_voltage_source(vcc, voltage3.3) # 模型一简化B值模型 self.ntc_b_model NTC(r2510_000, b_value3950, modelbeta) # 模型二精确Steinhart-Hart方程 self.ntc_sh_model NTC(r2510_000, a1.129e-3, b2.341e-4, c0.8774e-7, modelsteinhart_hart) self.r_pull Resistor(value10_000, tolerance0.01) self.op_follower OpAmp(modelLM358, supply_voltage(3.3, 0)) self.node_vcc.connect(self.r_pull.p) self.r_pull.n.connect(self.ntc_b_model.p) self.ntc_b_model.n.connect(self.node_gnd) self.ntc_node self.r_pull.n self.op_follower.input_positive(self.ntc_node) self.op_follower.input_negative(self.ntc_node) self.op_follower.output(self.node_adc_out)第二步进行直流温度扫描仿真。温度从-20℃扫到60℃步进5℃观察ADC节点电压from pycircuit.analysis import DCSweep sensor TempSensor() sweep_result sensor.dc_sweep( sweep_vartemp_c, # 温度作为扫描变量 start-20, end60, step5 ) # 数据可以直接变成DataFrame df sweep_result.to_dataframe() print(df.head())这段跑完你能看到一条漂亮的阻值分压曲线。在-20℃时NTC阻值接近几十千欧ADC分压接近0在60℃时阻值降到几千欧ADC电压接近满量程。这里有个细节必须表扬一下因为源码里温阻曲线用的是解析表达式所以扫描时不会出现数值抖动你看到的曲线是完全光滑的而不是离散采样阶梯。第三步验证跟随器的作用。去掉运放直接测量分压点的等效输出阻抗在ADC输入端加载1μA电流扰动对比两种情况下的电压变化。这个步骤对理解“为什么需要高阻抗缓冲”特别直观——PyCircuit里可以直接注入电流源from pycircuit import CurrentSource sensor_with_buffer TempSensor() sensor_with_buffer.add_component(CurrentSource(probe, value1e-6, # 1uA扰动 intoadc_out)) v_buffered sensor_with_buffer.simulate_op_point() sensor_unbuffered TempSensor() sensor_unbuffered.add_component(CurrentSource(probe, value1e-6, intontc_node)) v_unbuffered sensor_unbuffered.simulate_op_point() print(f带缓冲输出压降: {3.3 - v_buffered[adc_out]:.6f} V) print(f无缓冲输出压降: {3.3 - v_unbuffered[ntc_node]:.6f} V)阻抗匹配这种概念光看书本上一堆公式很抽象但你亲手在代码里注入电流源看到两种接法的压降差了三个数量级就再也忘不掉了。第四步导出网表和PCB工程from pycircuit.pcb import PcbProject, Placement project PcbProject(board_namentc_monitor, width_mm30, height_mm20) project.attach_circuit(TempSensor()) placement Placement() placement.put(r_pull, position_mm(5, 5), rotation0) placement.put(ntc_b_model, position_mm(5, 12), rotation0) placement.put(op_follower, position_mm(15, 7), rotation90) project.add_placement(placement) project.export_kicad(output/ntc_monitor)导出后你会得到一个output/ntc_monitor.kicad_pcb文件用KiCad打开元件已经在指定的坐标上网络也全部连通。板框默认是一个30×20mm的矩形你可以直接在KiCad里调整布线、覆铜和丝印。第五步把电路打包复用。如果你这设计以后要用在其他项目里只需要继承基类然后复用class MyProjectCircuit(Circuit): def configure(self): self.add_subcircuit(TempSensor(), instance_nametemp_sensor_1) # 在这里继续添加你的主控电路这套模块化继承的设计让电路本身成为可组合的软件组件极度适合做设计方案库的沉淀。3.3 实操中必须注意的三个细节这个流程跑下来我建议你额外盯住三个点。第一dc_sweep的扫描变量名不是拍脑袋定的。PyCircuit对内置参数有命名约定温度是temp_c、电阻阻值是value、电压源输出是voltage。你要是把温度变量起名叫temperature代码不会报错但扫出来全是恒定的27℃。这种静默错误的杀伤力极大我一度以为是自己NTC模型写错了排查了半天。第二KiCad 6和7的kicad_pcb格式有细微差异PyCircuit 6默认输出KiCad 7格式如果你用的是KiCad 6需要在export_kicad时显式传参数kicad_version6不然可能会遇到无法识别的文件头部报错。这个兼容问题在v6.0.2版本之后已经自动检测了老版本要手动指定。第三运放的供电引脚不要悬空。在代码里我写了supply_voltage(3.3, 0)这个参数会在仿真时自动给OPAMP模型接好正负电源轨。你要是漏配了这个参数仿真照跑但运放的工作点会完全错误输出级饱和成电源轨。我见过不止一个人被这个坑绊倒因为电路图里看起来运放只是接了三根线电源被PyCircuit藏在了模型内部。4. 常见问题与排查技巧实录4.1 仿真不收敛先检查这五件事仿真不收敛是模拟电路设计里最折磨人的问题PyCircuit 6虽然加了各种优化但牛顿迭代终归不是万灵药。我自己遇到过的典型不收敛场景按出现频率排序如下现象首要检查项解决方案直流工作点振荡初始值距离解太远增大dc_ramp_steps到8~10电压出现NaN模型表达式除零或对数负值检查二极管模型里Isat是否过小加温度钳制高增益运放不收敛反馈回路初始偏置不当给反馈支路并联一个大电阻如1GΩ做初次参考开关电源拓扑崩溃电感电流低步长突变打开dynamic_timestep选项报错Matrix singular存在短路/悬空节点代码中调用circuit.check_topology()做静态检查这里最有用的排查手段其实是PyCircuit自带的拓扑检查器。在仿真之前先跑一遍check_topology()它会列出每一个只连接了一个引脚的外部节点。很多“不收敛”其实是线路没接全并不是算法不行。4.2 网表导出后KiCad打不开怎么办这个问题我太有发言权了。PyCircuit早期版本导出的板卡文件扔给KiCad经常报“未知的net class”。后来才定位清楚KiCad的net_class定义必须出现在所有元件和网络之前而早期脚本把网络定义直接写进了板卡中部。v6在导出时做了两件事一是把所有net_class汇总到文件头部二是自动检查元件引脚命名跟KiCad库里的pad编号是否一致。如果你自己改了封装库最常遇到的是“封装引脚数为4但元件定义里只写了2个pad”。这种错PyCircuit在导出前不会察觉因为它只关心逻辑引脚不关心封装几何。我的建议是自定义封装时务必通过pcb.verify_footprint(DIP-8, circuit.op_follower)这类API让PyCircuit核对封装名与引脚数量把问题拦在生成之前而不是事后炸给KiCad。4.3 大型电路仿真越跑越慢的优化经验当电路规模超过几百个节点时Python对象创建的开销和矩阵求解的时间都会开始明显增加。我的实测数据一个包含120个元件、450个节点的音频均衡器电路直接跑瞬态仿真需要40多秒。这个规模其实并不算很大但优化空间却很可观。第一个优化点是把常参数从Python对象提升到numpy数组预处理。PyCircuit内部会用numba.jit对元件残差函数做编译如果你的元件参数是Python原生float每次调用模型都会有一次数据类型转换开销。把电阻、电容这些直接定义成numpy.float64能减少约15%的求解时间。第二个优化点是利用v6的异步任务通道。把同一电路的不同初始条件和不同扫描参数拆成多个协程并行执行在8核机器上实测有4.6倍的加速比。注意如果你把NTC的B值参数和温度扫描同时放入异步池会导致底层稀疏矩阵的求解器线程竞争这种情况下反而建议退回到同步模式。4.4 封装库不够全自己写封装模型也很快其实封装库缺乏是我最初做PCB导出时最大的痛点。PyCircuit内置的封装库覆盖了常用电阻电容、DIP运放、SOT-23三极管这些基础款但一旦用到自家的传感器模块或者商用量产的连接器就得自己写。不过说实话写好一个封装描述远比想象中简单。一个SOT-23-3封装定义是这样的from pycircuit.pcb import Footprint, Pad, Layer class SOT23_3(Footprint): name SOT-23-3 description Small Outline Transistor, 3 pins pads [ Pad(1, position_mm(-0.95, 1.0), size_mm(0.6, 0.8)), Pad(2, position_mm(-0.95, -1.0), size_mm(0.6, 0.8)), Pad(3, position_mm(0.95, 0.0), size_mm(0.6, 0.8)), ]写完保存成Python模块在电路描述里用footprintSOT23_3一绑定导出网表的时候封装名就像模像样地出现在KiCad里了。整个过程不需要打开KiCad的封装编辑器一个纯文本文件就能搞定。这让我批量导入几百个元件的封装规格成了可能配套一个厂商的规格书Excel解析脚本大批量的器件库导入也就一两个小时的事。4.5 一个印象深刻的bug仿真结果完全准确但PCB错误最后分享一个真正让我抓狂的debug经历。某个电源电路仿真结果完美——纹波、效率、稳定性全都在设计目标内。信心满满地导出PCB发给板厂打样。板子到手一上电DCDC输出直接没电压。排查到最后发现PyCircuit网表导出时同一个网络vcc_5v在电路描述里被创建了两个不同名称的节点对象它们是逻辑上等效的因为通过代码拼写一致但内部节点ID不同导致网表里出现了两个物理不连接的同名网络。这个bug的根因在于我只能保证字符串层面的名称一致但没有在导出时对节点做统一编号。v6发布后我立刻在节点合并逻辑上打了补丁规定所有节点在电路构建阶段就要通过circuit.merge_net(node_a, node_b)做显式合并不能再靠名称大小写撞运气。从那以后我再也没有因为这个原因烧过板子。这件事给我的教训是在硬件设计工具里隐式约定永远不如显式校验可靠。能静态检查的错误就别拖到仿真能仿真发现的错误就别拖到打板。最后再分享一点个人体会。做PyCircuit这半年多最大的收获倒不是工具本身而是深刻意识到“为了那瓶醋去包饺子”这件事在工程领域真不一定是坏事。那瓶醋代表着一个足够痛、足够具体的需求它逼着你把问题想透而当需求足够尖锐时你很难走两步就半途而废因为手头的麻烦源源不断地推着你往前走。PyCircuit 6当然不是万能工具它替代不了老工程师在布线阶段的直觉和经验但它把硬件开发里最枯燥、最机械的那部分工作变成了可复用、可批量执行的代码。我已经把项目里所有新设计的电路都迁到了PyCircuit上后续打算把元件库对接在线商城提取真实库存和价格数据让BOM成本和电路设计在同一个流程里完成——那估计又是一盘新饺子不过配了醋怎么着都值得再包一包。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →