10
0
0

ORTiS 船舶系统仿真记录:50 ns 发散排查、两腿模型和 PI 闭环

2026-10-24
2026-10-26
文章摘要
|

前情提要

最近在折腾 ORTiS SolverCodegen,想把论文里的船舶双母线电网跑起来。

这个项目做的事情挺直接:写一份网表,交给命令行工具,它就能生成一个针对这张电路的 C++ 求解器。生成的头文件可以放进自己的程序里,每调用一次,电路就向前推进一个时间步。

我先从手册里的 RLC 电路入手,确认了网表、代码生成和仿真入口的用法,接着换成仓库提供的 shipboard_system.netlist。论文用的是 50 ns,示例网表也写着 50 ns,我就先按这个步长跑。

结果编译过了,仿真却很快出了问题。DC/DC 刚启用,某个节点的电压就冲到几百万伏,接着正负交替、越算越大。继续做诊断,最后真的算出了 Inf。

这篇记录一下我是怎么从这个现象查到两个“闲置桥臂”,再把生成器改成正式两腿模型,最后给一台逆变器加上 PI 闭环的。

文中 50 ns 指电路模型的时间步长。这里做的是 CPU 双精度离线仿真,后面提到的运行结果也都来自这套离线程序。

先弄清楚:网表、HPP 和仿真程序各管什么

ORTiS 的基本流程可以写成:

网表 .netlist
    ↓ SolverCodegen
求解器 .hpp
    ↓ 自己编写的 C++ 驱动
门极信号 → 求解器逐步推进 → CSV → 波形图

网表负责元件参数和接线;生成器负责把电路翻译成求解代码;外围驱动负责时间循环、PWM、控制器以及数据保存。

比如网表里一条桥变换器定义,并不会自动带上“60 Hz 三相 SPWM”或者“把输出稳在 6 kV”的控制逻辑。这些都得在驱动程序里写清楚。

求解器使用 LB-LMC 方法。可以先把它理解成两个步骤:

1. 根据过去的状态,更新各元件对 b 的贡献
2. 求解 Gx = b,得到这一时刻的节点电压

G 是固定的系统矩阵,b 里面带着元件的历史状态和开关作用。这个区别后来很关键:每一步矩阵能解出来,不代表连续推进很多步以后数值一定稳定。

第一次跑:到底是振铃,还是数值已经飞了

最开始看到波形震荡,我还不能直接认定它发散了。论文里的降压过程也有振铃,需要先看它的包络是变小还是变大。

这里有两个不同的运行记录,得分开说。

第一组试跑使用原样生成的 HPP,外围驱动设置了异常电压停止条件。在约 20.10 µs,节点 55 的电压已经达到约 −2.49×10⁹ V,程序就停了。这份记录还没有 Inf 或 NaN,它只是提前被阈值拦住了。

后来我恢复了矩阵乘法中被阈值省略的小系数项,并取消提前电压停止条件,单独检查它最后会走到哪里。节点 55 的一段记录是这样的:

仿真时间节点 55 电压
20.05 µs+5.00×10⁶ V
20.10 µs−2.49×10⁹ V
20.15 µs+1.24×10¹² V
25.60 µs−1.54×10³⁰⁶ V
25.65 µs+Inf,停止

这就不是正常的降压振铃了。它在每个很短的离散步里反号放大,幅值没有收敛的趋势。

未修正的 50 ns 积分持续增长

图 1:红线是保留完整矩阵、但没有修正闲置腿积分的诊断结果;蓝线是中途试验过的局部积分修正版。蓝线还不是后面正式的两腿模型。纵轴为对数刻度。

还有个小细节:功率列在 25.20 µs 就先出现了 Inf,因为计算 v*v/R 时,平方比电压本身更早溢出。到 25.65 µs,节点电压也出现了 Inf。保存的这批数据里没有 NaN,程序遇到非有限电压后就退出了。

我也试过减小 dt。把原模型缩到 0.1 ns、0.05 ns 后,已经能完成 40 ms 的对照运行。但这个结果反而留下了一个问题:论文能用 50 ns,为什么我这里需要小到这个程度?

另外,论文 Fig.16 的横轴左端已经接近 0.47 s,振铃发生在约 0.5 s 的降压指令之后。那是一段被截出来的暂态窗口,不能把图左端当作从零状态启动,更不能据此认为数值发散等久一点就会自己恢复。

在源码里找到两个“闲置桥臂”

我回头检查 DC/DC 的网表,发现它复用了三腿桥元件:

#const DCDC_LFILT 0.0001
#const DCDC_RFILT 0.0001
#const DCDC_HIGHZ 1.0e6

BridgeConverter3LegIdealSwitches dcdc1(DT, DCDC_CIN, DCDC_LFILT, DCDC_RFILT) {1,0,2,3,19,55}
Resistor dcdc1_highz(DCDC_HIGHZ) {55,0}

这个元件有 A、B、C 三个桥臂。一个桥臂就是由上、下开关构成的一路输出,后面还串着电感和电阻。

在这份 DC/DC 接法里,A、B 用来建立正、负输出;C 没有承担需要的输出功能,只接了一个 1 MΩ 电阻到地。所以我把 C 称作“闲置桥臂”。第二台 DC/DC 也是同样的接法,只是多余节点变成了 56。

问题是,代码并没有把这个“闲置”当成不存在。

在 BridgeConverter3LegIdealSwitches.cpp 中,第三腿门极为 true 时接正母线,为 false 时接负母线。把门极设成 0,只是选择下管,并不能单独关掉这一腿。 原接口只有整台变换器的使能信号,没有单腿停用选项。

三腿复用与正式两腿模型的区别

图 2:这里只画一台 DC/DC 的功能结构。原接法多出的 C 支路仍参与开关和积分;正式两腿模型则从生成阶段取消整条支路。

我又对照了原始建模论文。2017 年论文第 739 页 Fig.3 给出的桥臂更新也是前向 Euler,与现在这段实现基本一致。因此,不能把差异简单归结为“论文用了隐式积分,代码却写成显式积分”。

还有一个结构上的线索:2017 年论文写的是 54 个节点,当前示例却有 56 个非地节点,多出的两个恰好就是 55、56。

这说明两者不能默认视为完全相同的数值模型。不过论文没有公开 DC/DC 的内部闲置腿处理,仅凭节点数也不能断言作者当年一定使用了我后来写的两腿实现。

1 MΩ 让矩阵可逆,却给 Euler 留了个难题

第三腿的电感是 100 µH。把这个接线代进去,节点方程给出:

v55 = Rhigh * i
L * di/dt = u - (Rhigh + Rf) * i

对应的时间常数是:

tau = L / (Rhigh + Rf)
    ≈ 100 µH / 1 MΩ
    ≈ 0.1 ns

连续时间下,这个支路应该非常快地衰减到一个很小的电流。10 kV 输入、1 MΩ 负载,稳态电流大约只有 10 mA。

但前向 Euler 要用过去的电流和端电压去推下一步。省略实例名、并令中性点接地后,核心式子可以写成:

i_next = i + dt / L * (u - v_out - Rf * i);

再把 v_out = Rhigh*i 代回去,齐次部分的放大因子就是:

a = 1 - dt * (Rhigh + Rf) / L

取 dt = 50 ns:

a ≈ -499

也就是说,这个模式的误差会每步反号,并放大约 499 倍。这个局部模式要求:

|a| < 1
dt < 2L / (Rhigh + Rf) ≈ 0.2 ns

这就解释了前面看到的现象,也解释了为什么缩到 0.1 ns 后不再出现同样的爆炸。

高阻让稳态电流更小,也让这条串联 RL 支路的衰减更快。对固定步长的显式积分来说,用一个很大的电阻近似开路,并不总是数值上无害。

那把 1 MΩ 直接删了行不行?也不行。这个桥的输出电感是显式电流源,不给 G 提供非零电导。保留输出节点、却删掉它唯一的接地电阻,会让对应节点悬空,矩阵又变成奇异的。

手册第 7.2 节恰好把这两件事分开讲了:高阻通路可以帮助解决显式解耦带来的奇异问题;元件时间常数与步长不匹配,仍然可能导致数值不稳定。

把 DC/DC 换成真正的两腿元件

排查时,我试过保留原支路,只把两个闲置腿改成局部解析积分,并按步内平均电流计算母线电容的电荷。这个中间版本在 50 ns 下完成了长时运行,帮助确认问题确实集中在这两条快速动态上。

但目标 DC/DC 本来就只需要 A、B 两腿,所以最后我选择改生成器。

我新开了 codex/two-leg-dcdc-50ns 分支,增加了正式元件:

BridgeConverter2LegIdealSwitches label(DT, C, L, R) {P,G,N,A,B}

对应第一台 DC/DC 的网表差异是:

-BridgeConverter3LegIdealSwitches dcdc1(DT, DCDC_CIN, DCDC_LFILT, DCDC_RFILT) {1,0,2,3,19,55}
-Resistor dcdc1_highz(DCDC_HIGHZ) {55,0}
+BridgeConverter2LegIdealSwitches dcdc1(DT, DCDC_CIN, DCDC_LFILT, DCDC_RFILT) {1,0,2,3,19}

第二台同样去掉 C 端、节点 56 和对应高阻。六台三相逆变器继续使用原来的三腿元件。

这次不是在生成后的 HPP 里把 il3 强行清零,而是在生成阶段就只建立两腿的状态和源贡献:

  • 五个端子:P/G/N/A/B。
  • 两个门极控制位,以及整桥使能信号。
  • 四个状态输出:上下直流电容电压、A/B 两腿电流。
  • 不再生成 C 腿的电流状态、控制位、源贡献和多余节点。

两条有效桥臂仍然用原来的前向 Euler,外部线性电感、电容保留原有的梯形法。最终模型回到 54 个非地节点,HPP 直接使用 CLI 的输出,没有再打局部积分或矩阵乘法补丁。

前面几种版本可以这样区分:

版本关键变化50 ns 结果
原样生成的三腿复用版本原接线、原积分出现巨大电压,触发保护
只恢复完整矩阵乘法补回被省略的小系数,积分不变仍然发散,最终出现 Inf
中间局部积分版本保留闲置腿,只改变其积分与电荷计算能完成所测长时案例
正式两腿版本从生成器中取消闲置腿,其余有效支路仍用原积分能完成所测长时案例

所以,恢复完整矩阵既没有造成这次发散,也没有单独解决它。“完整矩阵”和“修改积分”是两件不同的事。

回到 50 ns,把仿真跑久一点

改完以后,我重新生成 HPP,并从零初态开始跑了两类事件:

供电接线仍沿用原示例:左侧有外部输入源,远端节点 35/36 没有独立外部电源。下面的结果也包含这个条件。

案例仿真时长50 ns 下的步数事件
母线降压0.6 s1200 万步0.5 s 降低 DC/DC 占空比
交流幅值降低10.2 s2.04 亿步10.133 s 降低逆变器调制度

两份运行的保存数据都没有再出现前面的非有限数问题。

正式两腿模型的长时开环结果

图 3:正式两腿模型,50 ns,桥变换器仍采用前向 Euler,外部线性 L/C 保留原积分方法。上排是母线降压,下排是六台逆变器同时开环降低调制度的案例。

母线案例中,占空比在 0.5 s 从 0.8 改成 0.6,母线由约 ±6 kV 过渡到约 ±2 kV。降压后有振铃,但包络在衰减。

我又生成了一份 25 ns 的母线案例,两者使用同一个 50 ns 门极控制网格。在事件窗口严格对齐的 2 µs 采样点上,三相电压相对 RMS 差均小于 0.022%。这是新模型对步长的敏感性检查,不是它与论文原始数据的误差。

论文中的两幅图也要分清:Fig.16 是开环母线降至原来的三分之一;Fig.19 则是另一组外部微控制器闭环控制,把某一台三相逆变器的输出幅值降低。上面的开环幅值阶跃,只能作为相似事件的对照。

再给一台逆变器加上 PI

接下来我给逆变器 1 加了一个输出电压幅值 PI,其余五台启动后保持 m=1。

这里 PI 改的是 SPWM 调制度。控制链是:

前一个 PWM 周期的三相电压均值
    ↓ Clarke 变换,去除零序
电压幅值
    ↓ 一阶低通
与幅值参考比较
    ↓ PI + 抗积分饱和
调制度 m
    ↓ SPWM
逆变器门极

我没有每隔 10 µs 只取一个固定载波相位的瞬时电压,而是先对整个载波周期求平均,再计算幅值,避免把开关纹波直接混进反馈。

这次采用的参数如下,都是本次自行设计和试验的值:

参数数值
电路步长50 ns
PWM 频率100 kHz
PI 更新周期10 µs
测幅低通时间常数0.2 ms
电压归一化基值6000 V
Kp0.2
Ki600 /s
调制度范围0~1

误差先按 6000 V 归一化,控制部分的核心代码是:

const double e = (reference_V - filtered_V) / 6000.0;
const double proposed_I = I + Ki * Tc * e;
const double proposed_output = Kp * e + proposed_I;

const bool blocked = (proposed_output > 1.0 && e > 0.0)
                  || (proposed_output < 0.0 && e < 0.0);
if (!blocked)
    I = proposed_I;

m = std::max(0.0, std::min(1.0, Kp * e + I));

如果误差还要求输出继续往饱和方向走,就冻结这一次积分;误差反向时,则允许它退出来。

在 10.133 s,我把参考值从 6000 V 改为 3000 V。闭环程序没有直接把 m 乘以 0.5,也没有使用母线电压前馈,调制度完全由反馈误差和积分项决定。

单逆变器 PI 长时仿真

图 4:闭环和匹配开环都运行到 10.2 s。这里的开环对照也只改变逆变器 1,其他五台保持不变,与图 3 的六台同时降压案例分开统计。

除了 10.2 s 的参考阶跃,我还做了另一组 0.25 s 的母线扰动试验。两组结果列在下面,这里的“幅值”指滤后的 Clarke 反馈幅值,不是相电压 RMS。

工况及统计窗口PI 闭环幅值均值匹配开环幅值均值
3 kV 参考,10.17~10.20 s3000.042 V2988.851 V
另一组母线扰动试验,0.22~0.25 s2999.866 V2490.652 V

参考降低之后,滤后反馈幅值约 6.97 ms 进入并保持在 3000 V ±2% 范围内。末段均值虽然接近 3000 V,但仍有波动,该窗口最大绝对偏差约为 12.405 V。

这组短时扰动具体是这样做的:先于 0.133 s 把参考降为 3 kV,再于 0.163 s 把 DC/DC 占空比从 0.8 改成 0.75。这样本地母线会从约 ±6 kV 降到约 ±5 kV。

PI 与开环的母线扰动对照

图 5:参考保持 3 kV 时,母线下降使开环输出停在约 2491 V;PI 则把调制度提高到约 0.602,输出重新回到约 3000 V。扰动瞬间仍有振铃,随后衰减。

这比只看一次参考阶跃更能说明反馈起了作用:同样的电源变化,开环保持 m=0.5,闭环会根据实际输出继续调整。

不过,6 kV 参考还有一个不能略过的问题。前段调制度已经到 m=1,实际滤后幅值均值约为 5977.540 V,仍有 22.460 V 的残差。这属于当前线性调制上限的限制,PI 无法凭空增加电源可提供的电压。抗饱和的作用,是避免积分继续堆积。我没有把上限放大,也没有偷偷把参考改成 5978 V 来消掉这个误差。

最后,把这次单台逆变器的闭环降压和论文 Fig.19 并排看一下:

论文 Fig.19 与本次 PI 结果并列

图 6:左侧原图来自 Milton、Benigni 的 2019 ESTS 论文第 375 页 Fig.19;右侧是本次单逆变器电压幅值 PI。比较的是降幅和暂态特征,并未逐点拟合原图。

文件和复算入口

这次改动保存在本地分支 codex/two-leg-dcdc-50ns,没有修改 master。两腿模型和 PI 分别对应本地提交 72c794d、cdbab52。下面的命令针对包含这些改动的工作副本,上游原始版本没有本文新增的两腿元件和 PI 脚本。

主要文件是:

include/codegen/components/BridgeConverter2LegIdealSwitches.hpp
src/codegen/components/BridgeConverter2LegIdealSwitches.cpp
src/codegen/netlist/producers/BridgeConverter2LegIdealSwitchesProducer.cpp

examples/shipboard_system_two_leg/
├── shipboard_system.netlist
├── simulate.cpp
├── run.py
├── pi_voltage_controller.hpp
├── simulate_pi.cpp
├── run_pi.py
└── analyze_pi.py

本机环境是 Windows、MinGW 15.2、CMake 和 Python,绘图需要 NumPy、Matplotlib。下面假定 cmake、g++、python 已加入 PATH,并在仓库根目录执行:

cmake -S . -B build/two_leg_codegen -G "MinGW Makefiles" `
  -DCMAKE_BUILD_TYPE=Release `
  "-DCMAKE_CXX_FLAGS=-Wno-template-body" `
  "-DCMAKE_EXE_LINKER_FLAGS=-static" `
  -DBUILD_TESTING=OFF `
  -DSOLVERCODEGEN_BUILD_TESTS=ON

cmake --build build/two_leg_codegen --target SolverCodegen test_pi_voltage_controller --parallel 4
ctest --test-dir build/two_leg_codegen --output-on-failure

-Wno-template-body 是为了兼容本机新编译器和仓库里的旧版 Eigen;-static 是为了避开本机 Anaconda 与 MinGW 的运行库 DLL 冲突。它们不改变电路模型或积分方法。

代码生成器的端到端回归,以及 PI 的测幅、限幅、阶跃和抗扰检查,也都放进了测试,方便后续修改时一起验证。

开环长时运行和 PI 运行分别用:

python examples/shipboard_system_two_leg/run.py --codegen build/two_leg_codegen/SolverCodegen.exe

python examples/shipboard_system_two_leg/run_pi.py --codegen build/two_leg_codegen/SolverCodegen.exe

输出会分别放到:

build/shipboard_two_leg_50ns_20261024/
build/shipboard_two_leg_pi_20261024/

目录里有生成的 HPP、运行日志、CSV、哈希记录、分析 JSON 和波形图。我也检查了 HPP 在编译和运行前后的哈希,确认没有事后修改生成文件。PI 脚本可以加 --probe-only 只跑短时调参,或加 --analyze-only 只重新统计绘图。

最后再交代一下与论文还没有对齐的部分:本文的 60 Hz 参考来自读图后的假设,PWM、启动过程和 PI 参数由本次自行设定;原网表远端节点 35/36 没有独立外部输入源,这个条件也保留了。论文的外部微控制器、72 位定点 FPGA 和真实 HIL 链路,都没有在这次 CPU 离线试验中重建。

所以我把结果称为模型验证和论文特征对照。原作者当年的完整 DC/DC 实现和 PI 参数,现有公开资料还不足以恢复。

参考资料

  1. ORTiS SolverCodegen 上游仓库。
  2. ORTiS v0.9 CLI 手册,主要参考三腿元件说明和第 7.2 节奇异性、稳定性排查。
  3. Milton、Benigni、Bakos,System-Level, FPGA-Based, Real-Time Simulation of Ship Power Systems,2017。桥臂更新见 Fig.3,双母线系统说明见第 743~744 页。
  4. Milton、Benigni,Software and Synthesis Development Libraries for Power Electronic System Real-Time Simulation,2019。母线降压见 Fig.16,外部 PI 闭环试验见 Fig.17、Fig.19。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

ORTiS 船舶系统仿真记录:50 ns 发散排查、两腿模型和 PI 闭环
/archives/c%2B%2B-curcuit-codegen-solver
作者
Administrator
发布于
2026-10-24
许可协议
CC BY-NC-SA 4.0