ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用Python打造硬件自动化测试:PyCircuit 6实现电路仿真与批量验证

用Python打造硬件自动化测试:PyCircuit 6实现电路仿真与批量验证 从一次批量刷机需求聊起。我做硬件开发这些年最烦的不是画板子、写驱动而是那些“重复但不完全一样”的收尾工作几十块板子焊完一块块上电、量电压、烧固件、测电平错了还得拿万用表满板子找短路。为了把这摊活自动化我写了 PyCircuit 6——一套用 Python 描述电路、生成网表、跑仿真、还能直接驱动仪器做自动化测试的开源工具链。说白了就是“为了那瓶醋重新包了一盘硬件开发的饺子”醋是想让硬件测试变成一行命令饺子是电路描述、仿真引擎、仪器驱动全做了。这套东西不是给画板子的大厂EDA软件抢饭碗的它面向的是嵌入式工程师、硬件开发者和喜欢折腾的DIY玩家。如果你也受够了手工操作仪器、靠肉眼盯波形、用Excel记录测试数据那 PyCircuit 6 的思路应该能给你一些参考。1. 那瓶醋到底是什么从一次批量烧录需求说起1.1 重复劳动让我决定动手去年年中接了个小批量的项目50块控制板的研发验证。每块板子的流程都一样接上USB转串口用烧录工具把固件刷进去然后量几个关键节点电压再检查IO电平翻转是否正常。听起来很简单但50块板子就是50遍重复。更痛苦的是调试验证阶段的回归测试。改一版固件就要把之前测过的功能再手工过一遍漏掉一个IO口没测往往要到客户现场才暴露。我当时的想法很朴素能不能写一个脚本把“上电、烧录、测量、断言、生成报告”这一整套动作固化下来。PySerial我能轻松写pyvisa控制万用表也不算难真正卡住我的是测试用例本身——我想在脚本里描述“板子上R3和C2之间应该有个3.3V电压”但板子的连接关系、元件参数、网络名称全部躺在原理图里脚本根本没法引用。这个痛点就是那瓶醋让硬件测试用例能和电路定义对上话。为了喝到这口醋我不得不开始“包饺子”——先写一个能描述电路的Python库再基于它做网表提取、仿真验证、测试执行。1.2 PyCircuit 的定位不做 EDA做胶水层市面上能画原理图的工具很多KiCad免费开源、Altium功能全但价格贵、立创EDA上手快但它们有一个共性——交互式、图形化、数据模型封闭。电路信息被锁在工程文件里想用脚本批量改、批量检查、接测试框架非常别扭。PyCircuit 的定位一开始就明确了我不做又一个EDA而是做一个胶水层。用一套文本化的电路描述作为数据中枢向前对接仿真引擎向后对接自动化测试仪器。用户可以像写代码一样描述电路再通过同一份描述生成网表、跑出仿真曲线、驱动硬件测试。这样“设计数据”和“验证数据”不再各说各话。和 KiCad 的关系也说得清楚PyCircuit 不替代原理图绘制也不会去画PCB。它输出的是标准化网表和仿真数据这些数据可以被 KiCad 的 netlist 工具链读取也可以喂给 ngspice 做基准校验。反过来从 KiCad 导出的网表也能导入 PyCircuit 做测试用例的“预期值”定义。这个“双向往返”是后面所有自动化流程的地基。很多工具只能单向导出设计完就甩给制造而 PyCircuit 强调的是把设计数据重新拉回验证环路形成闭环。2. 用Python描述电路三层模型与网表生成实操2.1 定义元件库参数化封装比原理图符号更关键电路描述的第一步不是画符号而是定义元件模型。PyCircuit 里元件不是一张图而是一个参数化对象——它知道自己是电阻、电容还是芯片知道引脚定义还知道自己的电气特性参数。举个例子定义一个电阻器from pycircuit import Element, Pin Element.register class Resistor(Element): kind R pins [Pin(p1), Pin(p2)] params {resistance: 1e3} def netlist_entry(self, ref, node1, node2): return {ref: ref, device: R, value: self.resistance, nodes: [node1, node2]}这比在原理图里拖一个电阻符号多了一层含义参数在这里是真正的数值将来仿真会用到它测试断言也会用到它。我建议做元件库时把耐压、功率、温度系数都写进去哪怕第一版用不到。因为测试断言很可能关心“3.3V网络上的电阻是否超功率”没有这个参数后面没法做自动化检查。元件的引脚命名也要规范。我用的是“功能名_方向”的方式比如in_p、in_n、out、gnd而不是搞一堆pin1、pin2。原因很简单网表级的芯片引脚排布和原理图符号引脚排布不是一回事按功能命名代码里读到引脚名就能知道它是输入还是输出做连接检查时能立刻发现“把电源接到输出了”这类低级错误。2.2 连接关系与网表生成电路就是一张有向图电路本质上是图元件是节点导线是边。很多人觉得画原理图才是电路设计但我包了饺子之后发现用图结构描述电路反而更严谨。PyCircuit 内部用 networkx 管理连接关系。每个连接点是一个“网络”net元件引脚挂在网络上。定义电路时不需要关心坐标和线怎么走只需要声明“谁和谁相连”from pycircuit import Circuit, R, C, LED, Battery circuit Circuit(blink) vcc circuit.add_net(VCC) gnd circuit.add_net(GND) batt circuit.add_component(Battery(BAT1, voltage5.0), {: vcc, -: gnd}) led circuit.add_component(LED(LED1, vf2.0, if_max0.02), {anode: vcc, cathode: N_LED}) r1 circuit.add_component(R(R1, resistance150), {p1: N_LED, p2: gnd})这里add_component返回的其实是同一条网络名比如N_LED会被自动创建并复用。做完这步电路内部已经是一张完整的图了。接下来做网表生成就顺手多了netlist circuit.write_netlist() print(netlist.to_text())生成的内容和标准 SPICE 网表兼容可以直接交给 ngspice 仿真也能导出给 KiCad 导入。但 PyCircuit 做了件更实用的事拓扑检查。它会检查网络是否有悬空引脚、是否有重复连接、是否存在两个电源输出直接并联的情况。这些检查在手工画图时靠人眼盯在 PyCircuit 里是构造电路时的自动校验。注意网表规格和引脚方向不能想当然。SPICE 网表里元件是有方向性的二极管的正负极接反了仿真结果会错得离谱但表面上看起来“正常”。所以我在write_netlist里强制要求每个元件都声明“节点映射”不允许用默认顺序。这个设计一开始加的时候觉得啰嗦后来救了我好几次。2.3 一个LED闪烁电路的定义与网表输出示例完整跑通一个例子比看十段文档都管用。我用一个经典的“5V电源 150Ω电阻 LED”电路演示import pycircuit as pc c pc.Circuit(led_smoke_test) c.add_component(pc.Battery(BAT1, voltage5.2), {: VIN, -: GND}) c.add_component(pc.Resistor(R1, resistance150, tolerance0.05), {p1: VIN, p2: LED_A}) c.add_component(pc.LED(LED1, vf2.0, if_max0.02), {anode: LED_A, cathode: GND}) c.add_component(pc.Probe(P1, labelLED电流), {pin: LED_A}) report c.write_netlist() print(report.text()) print(report.topo_check())这个例子里我故意加了Probe元件。它不参与电气计算只负责在后续仿真和硬件测试时把“LED_A”这个节点的电流/电压关联到测试项里。这就把“设计”和“验证”的对应关系直接固化在网表里了。拓扑检查输出会提示网络VIN有1个电源输出、1个电阻输入能源来源合法网络LED_A有电阻输出和LED阳极输入路径完整网络GND全局接地正常。这些信息在手工读原理图时需要花十几分钟确认PyCircuit 在构造电路时就给出来了。如果你是从零开始学建议不要直接上复杂芯片先拿这种两三个元件的电路把“定义元件—连接网络—生成网表—拓扑检查”走一圈。我踩过的第一个大坑就是在没有网络名自动复用机制前手误把LED_A和LED_A 写成了两个网络导致拓扑检查一直报悬空。后来所有网络名都做了 strip 和大小写归一化这类问题才算根治。3. 仿真验证怎么做PyCircuit 的MNA引擎与实测对账3.1 改进节点分析法的落地细节有了网表下一步就是仿真。PyCircuit 6 内置的仿真引擎基于改进节点分析法MNA这是 SPICE 类软件通用的方法思路不复杂先把电路抽象成节点电压变量和支路电流变量列出一组线性方程再求解大型稀疏矩阵。以纯电阻网络为例每个节点满足基尔霍夫电流定律KCL流入节点的电流之和等于0。电阻的伏安关系给出电流表达式代入 KCL 就得到关于节点电压的方程。把所有方程组合得到一个形如Ax b的线性方程组其中x是节点电压向量。PyCircuit 里我用 scipy.sparse 存储矩阵用spsolve直接求解。第一次实验时我天真地用 dense 矩阵解3个节点没问题解到50个节点时速度和内存都不对劲了。换成稀疏矩阵后1000节点的电路也在一眨眼内完成。这一步是仿真的性能分水岭。对于含电容、电感的动态电路MNA出来的方程会变成微分代数方程。PyCircuit 采用瞬态分析的经典做法用梯形法和向后欧拉法做时间步进。时间步长可以用固定步长也可以用自适应步长。我默认开了自适应步长遇到信号变化剧烈时自动细化平坦时加大步长效率高很多。3.2 瞬态仿真、非线性迭代与数值稳定性二极管、晶体管这些非线性元件才是仿真真正麻烦的地方。以二极管为例它的电流和电压是指数关系I Is * (exp(V/(n*Vt)) - 1)MNA 方程里突然出现指数项没法直接线性求解必须做牛顿-拉夫逊迭代。每轮迭代都重新计算工作点直到电压变化小于容差。这里有个新手很容易忽略的细节牛顿迭代的初值不能乱给。我试过给二极管初始电压设成0V结果在低电流区迭代半天不收敛后来改成根据电源电压估算工作点收敛速度立刻快起来。PyCircuit 里对每个非线性元件都配了一个guess_operating_point()方法仿真器启动时会先做一次直流工作点计算给后续瞬态分析提供合理初值。数值稳定性也是个大坑。曾经写过一个振荡电路仿真曲线到后段直接发散到无穷大查了一晚上发现是时间步长过大梯形法在状态变化快的区域产生了数值振荡。解决办法是把最大步长限制到信号最快变化周期的十分之一并且开启步长收缩机制如果本步迭代不收敛自动把步长减半重试最多重试8次。这个机制现在成了默认策略实测下来极少出现发散情况。为了验证仿真器的正确性我拿 PyCircuit 的仿真结果和 ngspice 跑同一份网表做对账。第一个版本误差很大主要原因是二极管模型参数不一致。后来 PyCircuit 把 ngspice 的1N4148、1N4007等常用器件模型参数直接内置误差才控制在3%以内。对大多数设计验证场景这个精度完全够用。3.3 用仿真结果反查原理图错误仿真最大的价值不是“算出正确结果”而是“如果结果错了能帮你定位错在哪”。我有一次给一块电源板写测试仿真时发现某个节点电压比预期低了0.8V顺着网表一查发现是原理图里一个反馈电阻和分压电阻的位置接反了。如果靠硬件实测去发现这个问题至少要焊完板子、上电、再拿示波器量耗时一个下午用仿真5分钟就定位了。具体做法是“逐步缩小故障域”先仿真整个电路报错或异常就锁定异常节点把异常节点附近的子电路单独仿真观察是否符合子电路预期再不行就逐个断开可疑元件跑“开环/短路”对比仿真。这套排查手法在 PyCircuit 里被我封装成了Diagnose.candidate_failures()它会根据仿真偏差量列出三个最可疑的元件实测命中率还挺高。仿真也不可能覆盖所有问题。比如接触电阻、电缆压降、PCB走线阻抗这些在原理图仿真里没有的物理量仍然需要硬件测试来兜底。这也就是我坚持“仿真实测对账”的原因仿真负责把“设计逻辑错误”提前揪出来实测负责把“物理实现偏差”兜住两者缺一不可。4. 硬件自动化测试实践从电源上电到CI/CD回归4.1 用pySerial与pyvisa驱动真实硬件饺子包到这一步真正让我觉得“醋真好喝”的是把 PyCircuit 和真实硬件接通的那一刻。硬件自动化测试的底线是能用脚本控制电源上电、能用万用表测电压、能通过串口操作设备。PyCircuit 6 定义了三个抽象层PowerSupply、DMM、SerialPort分别对应程控电源、数字万用表和串口设备。底层驱动支持 pySerial 和 pyvisa这样无论是USB串口模块还是GPIB/SCPI仪器都能用统一的接口操作。from pycircuit.instrument import PowerSupply, DMM from pycircuit.runtime import TestRunner ps PowerSupply(USB0::0x1AB1::0x0E11::DS1Z000001::INSTR) dmm DMM(USB0::0x2E8A::0x1042::DMM12345::INSTR) runner TestRunner() runner.add_step(上电, lambda: ps.set_voltage(5.0, current_limit0.5)) runner.add_step(读电压, lambda: dmm.measure_voltage(VIN)) runner.add_step(下电, lambda: ps.set_voltage(0.0)) runner.run()我第一版写测试代码时直接把仪器指令堆在测试用例里结果换一台电源就要改十几个测试文件。后来把所有仪器操作收敛到InstrumentDriver类测试用例只依赖抽象接口设备层通过配置文件切换。这个抽象很值得做哪怕你现在只有一台仪器——等第二台不同品牌的仪器进来你会感谢当初的设计。4.2 测试项定义与阈值判断自动化的关键不是“能执行”而是“知道什么算合格”。PyCircuit 对测试项的定义做了约束每个测试项必须有名称、测量函数、上下限、容差模式和失败级别。阈值不应该是拍脑袋写死的。我强烈建议阈值尽量从元件参数和仿真结果衍生。比如一个3.3V输出的LDO手册给精度±2%测试阈值就应该是3.234V到3.366V如果仿真显示满载时纹波是30mV阈值可以加一个纹波上限。这样测试用例不是“代码里写死的断言”而是“设计数据的自然延伸”。from pycircuit.test import TestItem, Tolerance items [ TestItem( nameVIN5V时LDO输出3.3V, measurelambda ctx: dmm.measure_voltage(VOUT), limitTolerance.percent(base3.3, percent2.0), ), ]这里measure函数接收一个ctx上下文里面有当前电路的网表、仿真结果、仪器句柄。这样同一份测试项既能在实机上跑也能在仿真模式下预热验证。4.3 把pytest变成硬件测试入口测硬件和测软件的思维方式很不一样但工具可以统一。PyCircuit 6 做了一个pytest插件把每个测试项直接变成一个 pytest 用例这样硬件测试也能享受测试报告的生态。pytest --pycircuit-configboards/ctrl_board.yaml -m hw实际跑一轮批量测试时pytest 的断言失败信息会告诉我们“VOUT实测3.41V超出3.366V上限”同时旁边的--pycircuit-report参数会生成一份包含测试项、实测值、阈值、仿真预测值的JSON报告。这份报告既可以在本机看也能传到服务端做归档。硬件测试的CI/CD是个常常被忽视但收益极高的方向。我在本地搭了一套极简流程代码推送触发固件编译编译通过后通过工装连接真实板卡运行 PyCircuit 测试测试报告推回仓库。第一次完整跑通的瞬间我觉得“那瓶醋”值了——从改一行固件到拿到完整的硬件回归报告只需要10分钟而以前是至少一小时的手工活。注意CI里跑硬件测试首先要确保串口/仪器不被锁死。我的教训是给仪器访问加了超时和重试机制并且每轮测试结束后强制释放资源。否则测试进程异常退出后仪器一直被占用下一轮任务只能干等。5. 踩坑总结与常见问题速查5.1 六个最影响仿真/实测一致性的坑做了一整轮仿真和实测对账后我总结出下面这六个最容易让“仿真预测”和“实际读数”对不上的原因坑表现排查方法接触电阻未建模实测值比仿真值低几十毫伏测量点导线改用四线开尔文接法元件公差未纳入仿真电压偏差在±5%左右打开 Monte Carlo 仿真设置元件容差电源内阻过大大电流时电压跌落严重在仿真里给电源串联一个几十毫欧内阻表笔/线缆压降万用表读数和示波器不一致校准时用闭环补偿先量基准电压仿真步长过大高频纹波仿真不出来检查瞬态仿真的最大步长设置仪器采样时序不对上电瞬间读数毛刺明显测量前加固定延时或者做多次采样取中位数我自己最常掉进去的是第一项板卡上那些排针座插拔次数多了接触电阻能到几十毫欧在小电流场景看不出来一旦测大电流电源压降就很明显。后来我在测试治具里改用镀金探针并增加“先空载校准再带载测试”的流程实测和仿真误差从10%以上降到了3%以内。5.2 数据管理、日志与版本兼容硬件项目的可复现性比软件开发更苛刻。你没法像重跑测试用例那样把“物理板卡”复制一份所以数据和日志必须留得足够完整。我给 PyCircuit 定的规矩是每次测试跑出的JSON报告里必须包含电路网表哈希值、元件版本、仪器驱动版本、固件版本和测试脚本版本。这样任何一份异常报告都能追溯到当时的完整环境。一开始我觉得这个功能冗余直到有次客户反馈某块板测试不过我翻报告发现网表哈希和当前主分支不一致——是研发改了原理图但没同步更新测试基线。没有这个哈希光靠聊天记录根本查不出来。日志这块推荐结构化和非结构化混着记。结构化的测试项数据写JSON方便统计分析测量过程中的示波器截图/波形数据单独存文件命名带上测试项和时间戳。还有一个小技巧日志不打到标准输出因为pytest的彩色输出和仪器数据混在一起基本没法看。PyCircuit 提供的--log-file参数会把日志写到独立文件命令行只保留进度条和失败摘要。版本兼容是重灾区。PyCircuit 从 v5 升到 v6 时我重构了元件参数的数据结构老版本文档写的电路脚本一律加载失败。为了不让早期用户造反我写了个legacy_adapter模块自动识别旧格式并做字段映射。经验就是凡是涉及外部可写格式的改动必须提供迁移脚本还要在发布前拿旧版本案例做兼容性回归。5.3 后续扩展方向饺子包完了醋也喝上了但锅里还能继续下东西。我在 PyCircuit 6 里预留了两个扩展点一个是插件系统允许别人自定义元件模型和测试步骤另一个是JSON格式的网表接口方便和外部工具链对接。插件系统的设计很直接写一个 Python 包里面注册自定义 Element 或 TestStep放到pycircuit_plugins目录下PyCircuit 启动时会自动发现。实测发现这个机制非常适合团队内部共享元件库——不同项目组把自己特有的传感器、电机驱动芯片做成插件整个公司都能复用。JSON网表的想法来自一次“被迫集成”客户想把 PyCircuit 的测试数据导入他们自研的MES系统但MES只认JSON。后来我直接让write_netlist()支持formatjson输出里包含了网络、元件、引脚、参数和测试项关联对接成本一下子降下来了。如果你也有对外数据交换的需求JSON 格式一定值得提前设计别等客户来提了再加班。最后说说 Web 界面。PyCircuit 6 提供了一个非常简易的的本地 Web 报告视图用 FastAPI 把测试报告和仿真曲线渲染成页面。它不算漂亮但在团队协作时很有用测试人员不用装 Python 环境打开浏览器就能看到报告和曲线还能留言标记异常。这个方向我后续打算继续完善让它支持多人同时查看和审计批注。回顾整个“包饺子”过程我最想分享的体会是不要怕为了一个很小的需求去做一个看起来过大的工具。我做 PyCircuit 的初衷只是“懒得手工测板子”但埋头把这个系统做完之后我对电路仿真、仪器控制、测试工程化的理解比之前十年加起来都深。而且工具本身带来的复利很惊人以前新项目启动要重新搭测试环境现在新板卡接入 PyCircuit 基本就是改一个 yaml 配置加几十行网表定义半小时就能跑起来。如果你也想在硬件开发里引入自动化我的建议是先别追求大而全。选一块你手头最熟悉的板子用 PyCircuit 把上电、测电压、读串口这三件事跑通让“脚本能自动操作硬件”这个感觉建立起来。很多时候推动工程化改进的不是宏大的规划而是“这活真不想再干第二遍”的那口气。
返回列表