GeckoCIRCUITS 二次开发实战:工具组扩展
一、开篇引言
在储能 PCS 的控制器开发中,我经常需要在仿真阶段验证控制算法——比如 SOGI-PLL 的锁相精度、PR 控制器的谐波抑制效果。MATLAB/Simulink 当然可以做,但它的仿真速度和电路级建模能力有限。GeckoCIRCUITS 是一个开源的电力电子仿真软件,电路仿真能力很强,同时开源,可以按自己的需要和使用习惯定制化自己的仿真工具。
但它的控制库只提供了基础的 PI、s 域传递函数,没有 z 域传递函数、矩阵运算,也没有重复控制、LQR等这些现代控制算法模块。于是我决定给它写一套。这篇文章记录我在这个过程中真正踩过的坑、做过的设计决策,以及最后验证的结果。
1.1 开发目标
- 矩阵工具组:提供完整的矩阵运算链路,支持现代控制理论(LQR、状态空间、观测器等)的图形化建模
- DSP 工具组:补齐数字信号处理常用算法模块,实现从仿真到嵌入式代码的无缝衔接
1.2 本文导航
本文将从架构剖析入手,详细讲述两大工具组的设计思路、实现细节及验证案例。
二、GeckoCIRCUITS 架构剖析(二次开发前置知识)
2.1 整体架构分层
┌─────────────────────────────────────────────────────┐
│ GUI 层(SchematischeEingabe2 / ToolBar 等) │
├─────────────────────────────────────────────────────┤
│ 组件注册层(ControlTyp / CircuitTyp 枚举) │
├─────────────────────────────────────────────────────┤
│ 控制模块层(RegelBlock 子类 + Calculator 计算器) │
├─────────────────────────────────────────────────────┤
│ 仿真内核层(SimulationsKern / LKMatrices) │
├─────────────────────────────────────────────────────┤
│ 数学基础库(Matrix / LUDecomposition / Polynom) │
└─────────────────────────────────────────────────────┘
2.2 控制模块(Control Block)的核心设计模式
- 抽象基类体系:
RegelBlock.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/RegelBlock.java) →AbstractReglerSingleInputSingleOutput/AbstractReglerVariableInputs - Calculator 分离模式:UI 参数配置(ReglerXxx.java)与仿真计算(XxxCalculator.java)解耦
- 关键接口:
AbstractControlCalculatable.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/calculators/AbstractControlCalculatable.java) 的berechneYOUT(double deltaT)方法
- 关键接口:
- 终端注册机制:
TerminalControlInput/TerminalControlOutput的动态数量管理 - 组件注册入口:
ControlTyp.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ControlTyp.java#L23-L137) 枚举(新增模块必须在此注册 ID)
2.3 矩阵数据传输协议
关键洞察:GeckoCIRCUITS 原有信号以标量为主,矩阵数据需要自定义序列化协议
矩阵输出信号(double[] 数组编码):
[0] = 行数 m
[1] = 列数 n
[2] = a₁₁, [3] = a₁₂, ..., [2+m*n] = aₘₙ
参考实现:ReglerMatrix.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerMatrix.java#L50-L92) 中的 MatrixOutputCalculator
三、矩阵工具组扩展设计与实现

3.1 工具组功能全景
| 模块编号 | 类名 | 功能 | 输入 | 输出 |
|---|---|---|---|---|
| 96 | ReglerMatrix | 矩阵常量输入 | 0 | 矩阵 M |
| 97 | ReglerMatrixAdd | 矩阵加法 A+B | 矩阵×2 | 矩阵 |
| 98 | ReglerMatrixGain | 矩阵数乘 k·A | 矩阵×1, 标量×1 | 矩阵 |
| 99 | ReglerMatrixMult | 矩阵乘法 A·B | 矩阵×2 | 矩阵 |
| 100 | ReglerMatrixView | 矩阵可视化查看 | 矩阵×1 | 0 |
| 101 | ReglerMatrixInverse | 矩阵求逆 A⁻¹ | 矩阵×1 | 矩阵 |
| 102 | ReglerMatrixTranspose | 矩阵转置 Aᵀ | 矩阵×1 | 矩阵 |
| 103 | ReglerMatrixDeterminant | 行列式 det(A) | 矩阵×1 | 标量 |
| 104 | ReglerMatrixRank | 矩阵秩 rank(A) | 矩阵×1 | 标量 |
| 105 | ReglerMatrixEigenvalues | 特征值分解 | 矩阵×1 | 特征值向量 |
| 106 | ReglerVectorInput | 向量输入 | 0 | 列向量 |
3.2 核心功能以及坑点详解
3.2.1 组件注册与元数据定义(扩展机制)
亮点:
- 枚举 + 静态
tinfo声明式注册,新增模块只需追加一行枚举值 - 模块 ID 96~106 与官方 0~95 物理隔离,避免版本升级冲突
ControlTypeInfo捆绑类名 / 显示符号 / 国际化键,元信息单一来源
3.2.1.1 枚举批量注册(11 个矩阵模块一次性接入)
ControlTyp.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ControlTyp.java#L127-L137)
// —— 矩阵工具组模块 ID 规划:96~106 区间预留,与官方 0~95 解耦 ——
C_MATRIX(96, ReglerMatrix.tinfo), // 矩阵常量输入
C_MATRIX_ADD(97, ReglerMatrixAdd.tinfo), // 矩阵加法 A+B
C_MATRIX_GAIN(98, ReglerMatrixGain.tinfo), // 矩阵数乘 k·A
C_MATRIX_MULT(99, ReglerMatrixMult.tinfo), // 矩阵乘法 A·B
C_MATRIX_VIEW(100, ReglerMatrixView.tinfo), // 矩阵可视化
C_MATRIX_INV(101, ReglerMatrixInverse.tinfo), // 矩阵求逆 A⁻¹
C_MATRIX_TRANS(102, ReglerMatrixTranspose.tinfo), // 矩阵转置 Aᵀ
C_MATRIX_DET(103, ReglerMatrixDeterminant.tinfo),// 行列式 det(A)
C_MATRIX_RANK(104, ReglerMatrixRank.tinfo), // 秩 rank(A)
C_MATRIX_EIG(105, ReglerMatrixEigenvalues.tinfo),// 特征值分解
C_VECTOR_INPUT(106, ReglerVectorInput.tinfo); // 列向量输入
设计意图:矩阵模块 ID 从 96 起步,与原作者的 0~95 物理隔离,避免版本升级时的 ID 冲突,同时支持旧
.ipes文件的 TokenMap 反向映射(getFromIntNumber())。
3.2.1.2 单个模块的元信息声明模式
ReglerMatrixMult.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerMatrixMult.java#L10-L19)
public final class ReglerMatrixMult extends RegelBlock {
public static final ControlTypeInfo tinfo = new ControlTypeInfo(
ReglerMatrixMult.class, "M*", I18nKeys.MATRIX_MULTIPLICATION
);
public ReglerMatrixMult() {
super(0, 0);
XIN.add(new TerminalControlInputWithLabel(this, -2, -XIN.size(), "M1"));
XIN.add(new TerminalControlInputWithLabel(this, -2, -XIN.size(), "M2"));
YOUT.add(new TerminalControlOutputWithLabel(this, 3, -YOUT.size(), "M"));
}
}
设计模式:
RegelBlock(UI 层)+ 内部Calculator(仿真层)的解耦结构,ControlTypeInfo绑定类名、显示符号、国际化键。
3.2.2 矩阵信号编码协议(自定义序列化)
亮点:GeckoCIRCUITS 原生只传标量 double[1],矩阵需要在 double[] 上自定义协议,这是整个矩阵工具组能跑通的"地基"。
优点:
- 不改动仿真内核,仅在原生
double[]通道上扩展出矩阵传输能力 - 统一
[rows, cols, data…]协议,模块间可直接级联,零类型转换 - 定长缓冲 + 尾部置零,内存布局确定,适合实时仿真步进
3.2.2.1 协议定义 + 编码输出(以矩阵常量源为例)
ReglerMatrix.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerMatrix.java#L74-L91)
private void updateBufferContent() {
int rows = mat.getRowDimension();
int cols = mat.getColumnDimension();
int dataSize = rows * cols;
int totalSize = 2 + dataSize;
// ———— 协议头部:[0]=行数, [1]=列数 ————
_outputBuffer[0] = rows;
_outputBuffer[1] = cols;
// ———— 协议载荷:[2..2+m*n-1] = 行优先展开的矩阵元素 ————
double[][] data = mat.getArray();
int idx = 2;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
_outputBuffer[idx++] = data[i][j];
}
}
// 尾部置零,保证与固定缓冲区 MAX_BUFFER_SIZE 对齐
for (int i = totalSize; i < _outputBuffer.length; i++) {
_outputBuffer[i] = 0;
}
}
3.2.2.2 协议解码 + 维度校验 + 执行运算(以矩阵乘法为例)
核心设计思想:矩阵运算模块全部遵守同一套 [rows, cols, a₁₁, a₁₂, …, aₘₙ] 协议,模块间可直接级联搭建 LQR / 状态空间模型,无需任何类型转换。
ReglerMatrixMult.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerMatrixMult.java#L44-L127)
@Override
public void berechneYOUT(final double deltaT) {
double[] input1 = _inputSignal[0];
double[] input2 = _inputSignal[1];
// ========== Step 1:输入有效性防御 ==========
if (input1 == null || input2 == null || input1.length < 2 || input2.length < 2) {
setFallbackOutput(_outputBuffer); return;
}
int rows1 = (int) input1[0]; int cols1 = (int) input1[1];
int rows2 = (int) input2[0]; int cols2 = (int) input2[1];
// ========== Step 2:维度兼容性断言(A.cols == B.rows) ==========
if (cols1 != rows2 || rows1 <= 0 || cols1 <= 0 || cols2 <= 0) {
setFallbackOutput(_outputBuffer); return;
}
if (rows1 > 10 || cols1 > 10 || rows2 > 10 || cols2 > 10) {
setFallbackOutput(_outputBuffer); return;
}
// ========== Step 3:从 double[] 解码为 double[][] ==========
double[][] data1 = new double[rows1][cols1];
double[][] data2 = new double[rows2][cols2];
int idx = 2;
for (int i = 0; i < rows1; i++)
for (int j = 0; j < cols1; j++) data1[i][j] = input1[idx++];
idx = 2;
for (int i = 0; i < rows2; i++)
for (int j = 0; j < cols2; j++) data2[i][j] = input2[idx++];
// ========== Step 4:调用数学库执行乘法 ==========
Matrix mat1 = new Matrix(data1);
Matrix mat2 = new Matrix(data2);
Matrix result = mat1.times(mat2);
// ========== Step 5:结果重新编码为 double[] ==========
int resultRows = result.getRowDimension();
int resultCols = result.getColumnDimension();
_outputBuffer[0] = resultRows;
_outputBuffer[1] = resultCols;
double[][] resultData = result.getArray();
idx = 2;
for (int i = 0; i < resultRows; i++)
for (int j = 0; j < resultCols; j++) _outputBuffer[idx++] = resultData[i][j];
}
3.2.3 参数持久化与工程坑点解决(ASCII 导入/导出 + 复制)
踩坑记录:这是二次开发最容易踩坑的部分——矩阵文本带换行符会被 ASCII 解析器截断,copyAdditionalParameters 忘记重写会导致复制后数据丢失。
应对策略:
- 换行符转义保证多行矩阵文本在
.ipes文件中读写往返不丢失 copyAdditionalParameters深拷贝裸成员变量,复制模块不共享引用- 导入解析失败自动回退单位矩阵,容错性好
3.2.3.1 换行符转义编码(.ipes 文件读写)
ReglerMatrix.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerMatrix.java#L187-L213)
// ===== 导出:将矩阵文本中的 \n 编码为 \\n,防止 TokenMap 跨行截断 =====
@Override
protected void exportAsciiIndividual(final StringBuffer ascii) {
super.exportAsciiIndividual(ascii);
String encodedMatrixText = matrixText;
if (encodedMatrixText.contains("\n")) {
encodedMatrixText = encodedMatrixText.replaceAll("\n", "\\\\n");
}
DatenSpeicher.appendAsString(ascii.append("\nmatrixText"), encodedMatrixText);
}
// ===== 导入:反向解码 \\n → \n,还原用户编辑的多行格式 =====
@Override
protected void importIndividual(final TokenMap tokenMap) {
super.importIndividual(tokenMap);
if (tokenMap.containsToken("matrixText")) {
String loadedMatrixText = tokenMap.readDataLine("matrixText", matrixText);
String decodedMatrixText = loadedMatrixText;
if (loadedMatrixText.contains("\\n") && !loadedMatrixText.contains("\n")) {
decodedMatrixText = loadedMatrixText.replaceAll("\\\\n", "\n");
}
matrixText = decodedMatrixText;
try { mat = parseMatrix(matrixText); }
catch (Exception e) { mat = Matrix.identity(2, 2); }
}
}
3.2.3.2 模块复制时的深拷贝(Ctrl-C 拖拽复制的数据同步)
踩坑记录:基类
copyFabric()→copyLKBlockPars()默认只复制注册的UserParameter,matrixText和mat作为裸成员变量必须在copyAdditionalParameters中手动克隆,否则新粘贴的模块会回退到"1,0;0,1"单位矩阵。
ReglerMatrix.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerMatrix.java#L215-L229)
@Override
public void copyAdditionalParameters(final AbstractBlockInterface originalBlock) {
super.copyAdditionalParameters(originalBlock);
if (originalBlock instanceof ReglerMatrix) {
ReglerMatrix orig = (ReglerMatrix) originalBlock;
// 1. 复制原始文本(用户编辑内容)
this.matrixText = orig.matrixText;
// 2. 重新解析生成 Matrix 对象(避免引用共享)
try { this.mat = orig.parseMatrix(this.matrixText); }
catch (Exception e) { this.mat = Matrix.identity(2, 2); }
// 3. 同步显示参数(行数列数)
_displayRows.setUserValue(this.mat.getRowDimension());
_displayCols.setUserValue(this.mat.getColumnDimension());
}
}
3.2.4 高性能策略(输入变化检测 + 缓存复用)
踩坑记录:矩阵求逆 / 特征值是 O(n³) 运算,在仿真步长 1µs 级下每步重算会严重拖慢。采用"输入未变直接跳过"的增量计算策略。
应对策略:
- 输入指纹逐元素比对,未变化则整帧跳过 O(n³) 运算
- 时不变矩阵场景下长仿真只计算一次,加速效果显著
- 同一模式在求逆 / 特征值模块间直接复用
3.2.4.1 输入指纹比较 + 缓存命中跳过(矩阵求逆)
ReglerMatrixInverse.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerMatrixInverse.java#L48-L79)
// 逐元素比较上一帧输入,指纹未变则整帧跳过
private boolean inputChanged(double[] newInput, double[] oldInput) {
if (newInput == null || newInput.length != oldInput.length) return true;
for (int i = 0; i < newInput.length; i++) {
if (newInput[i] != oldInput[i]) return true;
}
return false;
}
@Override
public void berechneYOUT(final double deltaT) {
// ... 输入拷贝到 _inputMatrix ...
if (inputChanged(_inputMatrix, _lastInput)) {
_needsRecalculation = true;
System.arraycopy(_inputMatrix, 0, _lastInput, 0, _inputMatrix.length);
}
if (!_needsRecalculation) return; // 缓存命中,直接 exit
// ... 执行 inverse() ...
_needsRecalculation = false; // 标记缓存有效
}
性能收益:在 LQR 控制中状态矩阵 A 通常时不变,10 秒仿真(步长 1µs)下,逆运算从 10⁷ 次缩减为 1 次,对 O(n³) 复杂度收益尤为显著。同样模式也在
ReglerMatrixEigenvalues.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerMatrixEigenvalues.java#L48-L79) 中复用。
3.2.5 动态端子 + 可配置向量(VariableTerminalNumber 接口)
亮点:向量输入模块支持用户动态增减输入维度(1~100),体现"可配置拓扑"的设计能力。
优点:
- 端子数量随用户参数动态增删,模块拓扑可配置
- 参数列表与物理端子成对扩缩容,状态始终一致
- 复制 / 导入时先扩容再写数据,避免索引越界
ReglerVectorInput.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerVectorInput.java#L61-L77)
// 根据用户设置的 count,动态增删"默认值参数 + 是否连输入信号标志 + 物理端子"
private void updateInputTerminals(int count) {
// —— 扩容:补齐参数列表与 XIN 端子 ——
while (_defaultValues.size() < count) {
int idx = _defaultValues.size();
_defaultValues.add(UserParameter.Builder.<Double>start("default" + idx, 0.0).shortName("d" + idx).build());
_useInputSignals.add(UserParameter.Builder.<Boolean>start("useInput" + idx, false).shortName("u" + idx).build());
XIN.add(new TerminalControlInputWithLabelAndVisibility(this, -3, -XIN.size(), "x" + idx));
}
// —— 缩容:从尾部移除 ——
while (_defaultValues.size() > count) {
int idx = _defaultValues.size() - 1;
_defaultValues.remove(idx);
_useInputSignals.remove(idx);
XIN.pop();
}
updateInputTerminalVisibility();
}
配套的复制/导入安全:在
copyAdditionalParameters和importIndividual中必须先调用updateInputTerminals(origCount)扩容列表,再按索引写入数据,否则会触发IndexOutOfBoundsException(实际踩坑点)。
3.2.6 矩阵对话框(Dialog)的交互设计
设计思想:
- 文本解析(逗号+分号格式)
- 实时预览 + 语法校验
- 撤销/重做支持(AbstractUndoGenericModel)
ReglerMatrixDialog.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerMatrixDialog.java)
private void updatePreview() {
String text = matrixTextArea.getText().trim();
Matrix mat;
try {
mat = element.parseMatrix(text);
} catch (Exception e) {
matrixPreviewArea.setText("Invalid format!\nUse: row1;row2\nExample: 1,0;0,1");
return;
}
// 格式化输出矩阵
StringBuilder sb = new StringBuilder();
sb.append("Size: ").append(mat.getRowDimension())
.append(" x ").append(mat.getColumnDimension()).append("\n\n");
// 逐行输出矩阵元素
double[][] data = mat.getArray();
for (int i = 0; i < mat.getRowDimension(); i++) {
sb.append("[ ");
for (int j = 0; j < mat.getColumnDimension(); j++) {
sb.append(df.format(data[i][j]));
if (j < mat.getColumnDimension() - 1) {
sb.append(", ");
}
}
sb.append(" ]\n");
}
matrixPreviewArea.setText(sb.toString());
}
四、DSP 工具组扩展设计与实现

4.1 工具组功能全景
| 模块编号 | 类名 | 算法类型 | 典型应用 |
|---|---|---|---|
| 92 | ReglerDPRCONTROL | 数字比例谐振控制 | 并网逆变器电流控制 |
| 93 | ReglerDiscreteTransferFunction | 离散传递函数 G(z⁻¹) | 数字滤波器设计 |
| 94 | Regler1P1ZCompensator | 一极一零补偿器 | 电压/电流环补偿 |
| 95 | ReglerRepetitiveController | 重复控制器 | 周期性误差抑制 |
| 107 | ReglerSOGI | 二阶广义积分器 | 电网同步(SOGI-PLL) |
| 108 | ReglerPark | Park/反Park 变换 | 电机 dq 解耦控制 |
| 109 | ReglerFrequencyDiscriminator | 鉴频器 | 频率测量 |
| 110 | ReglerZeroOrderHold | 零阶保持 | 离散化建模 |
| 112 | ReglerQuantizer | 量化器 | ADC/DAC 建模 |
| 113 | ReglerPWM | PWM 调制器 | 脉冲宽度调制 |
4.2 数字控制器设计(以 DPR 为例)
- 准比例谐振(QPR)传递函数离散化(双线性变换 / 零极点匹配)
DPRControlCalculator.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/calculators/DPRControlCalculator.java) 差分方程实现- 参数整定界面:谐振频率、比例增益、谐振增益、截止带宽
4.3 DSP 工具组核心功能以及坑点详解
4.3.1 参数暴露方式的三种范式:对话框 / 引脚 / 混合
设计思想:DSP 工具组内部同时存在三种参数暴露范式,由仿真需求动态选择。
- 静态结构参数(采样率、阻尼系数)走
UserParameter+ 对话框,改参数不必重新连线 - 运行时变化的量(谐振频率、控制器增益)走引脚,支持在线整定与外部闭环
- 混合范式(SOGI)兼顾两者,是推荐默认
4.3.1.1 混合范式(以 SOGI 为例:会被闭环修改的参数是信号)
设计意图:
f0会跟随 PLL 输出实时波动(频率自适应 SOGI-PLL 的前提),必须作为信号参与闭环;而采样率、阻尼系数 K(默认 √2)在一次运行内不变,走UserParameter+ReglerSOGIDialog。
ReglerSOGI.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerSOGI.java#L39-L47)
public ReglerSOGI() {
super();
XIN.add(new TerminalControlInputWithLabel(this, -3, -XIN.size(), "input"));
XIN.add(new TerminalControlInputWithLabel(this, -3, -XIN.size(), "f0")); // 谐振频率走引脚
YOUT.add(new TerminalControlOutputWithLabel(this, 3, -YOUT.size(), "vd")); // 正交双输出
YOUT.add(new TerminalControlOutputWithLabel(this, 3, -YOUT.size(), "vq"));
}
4.3.1.2 全引脚范式的极端案例(重复控制器:10 输入 3 输出)
踩坑记录 1(无对话框的代价):
gain / vrms / work freq / ...九个参数全部引脚化,虽然支持运行时在线整定(自适应控制的刚需),但每次修改参数都要重新连线,模块在原理图上占半个屏幕。Regler1P1ZCompensator的 7 引脚是折中,但同样没有参数对话框(DialogWindowWithoutInput)。
踩坑记录 2(模块与其他模块节奏失调):YOUT 有 3 个端子(output/x0/y0),其中 x0/y0 用于外接数字滤波器,滤波输出又灌回模块的 x1/y1 ,模块间的仿真步频容易失调,并且仿真机制造成一拍仿真延迟。后来滤波器改为内置,由用户输入差分方程,避免了以上问题。
ReglerRepetitiveController.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerRepetitiveController.java#L20-L37)
XIN.add(new TerminalControlInputWithLabel(this, -3, -XIN.size(), "input"));
XIN.add(new TerminalControlInputWithLabel(this, -3, -XIN.size(), "gain"));
XIN.add(new TerminalControlInputWithLabel(this, -3, -XIN.size(), "vrms"));
XIN.add(new TerminalControlInputWithLabel(this, -3, -XIN.size(), "work freq"));
XIN.add(new TerminalControlInputWithLabel(this, -3, -XIN.size(), "sample freq"));
// ... lead num / x1 / y1 / max / min 共 10 个输入端子
4.3.2 PWM 动态输出端子:预创建 + 折叠栈 + 参数监听兜底
亮点:PWM 模块的输出端子数随模式在 1/2/4 间切换(单路 / 互补 / 移相互补),但没有沿用矩阵组"动态增删端子"的路线,而是构造时一次建满 4 个,再折叠收纳,从根本上避开了导入/复制时的索引越界。
- 端子对象永不销毁,折叠栈(
Stack)只挪引用,展开后连线关系可恢复 - 折叠 / 展开 / 重排三个入口(
setFolded/setExpanded/updateTerminalCount)收敛到同一个getTargetTerminalCount()计算 - 用参数监听器兜住"从
.ipes文件恢复"这条隐秘路径
4.3.2.1 构造时建满端子 + 参数监听器
ReglerPWM.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerPWM.java#L114-L133)
public ReglerPWM() {
super();
XIN.add(new TerminalControlInputWithLabel(this, -3, -XIN.size(), "duty"));
YOUT.add(new TerminalControlOutputWithLabel(this, 3, -YOUT.size(), "pwm1"));
YOUT.add(new TerminalControlOutputWithLabel(this, 3, -YOUT.size(), "pwm2"));
YOUT.add(new TerminalControlOutputWithLabel(this, 3, -YOUT.size(), "pwm3"));
YOUT.add(new TerminalControlOutputWithLabel(this, 3, -YOUT.size(), "pwm4"));
// start in SINGLE mode: fold extra terminals
setFolded();
// when the mode parameter changes (user picks another mode in the dialog,
// or the value is restored from a saved file) re-apply the terminal count.
// Without this, reopening a file restores the mode value but leaves the
// terminals folded to the default (SINGLE) count.
_mode.addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
updateTerminalCount();
}
});
}
4.3.2.2 折叠 / 展开的对称实现
ReglerPWM.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerPWM.java#L200-L233)
@Override
public void setFolded() {
final int targetCount = getTargetTerminalCount();
while (YOUT.size() > targetCount) {
_terminalStack.push((TerminalControlOutput) YOUT.pop());
}
}
@Override
public void setExpanded() {
final int targetCount = getTargetTerminalCount();
while (YOUT.size() < targetCount && !_terminalStack.isEmpty()) {
YOUT.add(_terminalStack.pop());
}
}
// updateTerminalCount():先按目标数折叠、再按目标数展开,两种方向一段代码全覆盖
踩坑记录(文件恢复端子不同步):注释原文就是事故现场——
.ipes重新打开时 mode 值能正确恢复,但端子仍停留在默认 SINGLE 的折叠数,pwm2~pwm4 的连线全部丢失。修复方式是给UserParameter _mode挂ActionListener,让"参数值变化"(无论来自对话框还是文件导入)统一触发updateTerminalCount()。凡是端子拓扑依赖参数的模块,参数监听器是必选项而非可选项。
对话框侧的同步:ReglerPWMDialog.processInputs()除了写回 mode/carrier 两个参数,还必须显式调用element.updateTerminalCount(),保证对话框"确认"后端子数立即跟上。
4.3.3 离散传递函数持久化:token 拼写即契约 + 定长数组防御
亮点:离散传递函数(DTF)的 ASCII 导出代码里藏着一个"将错就错"的兼容性智慧——token 命名是错的,但发布之后就是持久化契约,谁都不能改。
优点:
- 读写 token 字符串严格一致,旧
.ipes文件永远可读 - 定长系数数组(
MAX_ARRAY_SIZE = 20)让导出格式恒定、仿真期零 GC - 所有写入路径统一"清零 + 截断",杜绝旧系数残留
4.3.3.1 错位命名的 token(发布即冻结)
ReglerDiscreteTransferFunction.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerDiscreteTransferFunction.java#L121-L146)
@Override
protected void exportAsciiIndividual(final StringBuffer ascii) {
ascii.append("\nnominatorPoles "); // ← 拼写错误:应为 numerator
for (int i = 0; i < _poles.length; i++) { // ← 且存的确实是"极点" _poles
ascii.append(_poles[i]);
ascii.append(' ');
}
ascii.append("\ndenominatorZeros "); // ← 零点 _zeros 挂在 denominator 前缀下
for (int i = 0; i < _zeros.length; i++) {
ascii.append(_zeros[i]);
ascii.append(' ');
}
// nominatorPolynom / denominatorPolynom 同理
}
踩坑记录(token 是持久化契约):
nominator应为numerator,且nominatorPoles里存的是极点、denominatorZeros里存的是零点——双重错位。但importIndividual()用完全相同的字符串读取(tokenMap.readDataLine("nominatorPoles", _poles)),读写自洽,历史文件全部可用。二次开发时若"顺手修正"拼写,所有旧.ipes会静默读回默认值。对照矩阵组 3.2.3 的经验:持久化字符串发布即冻结,错了也只能将错就错,最多新增一个别名 token 做迁移。
4.3.3.2 定长数组的写入防御(清零 + 截断)
ReglerDiscreteTransferFunction.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerDiscreteTransferFunction.java#L168-L186)
public void setNumeratorPolynom(final List<Double> values) {
for (int i = 0; i < _numeratorPolynom.length; i++) {
_numeratorPolynom[i] = 0; // 1. 先整段清零(清掉上一次的系数)
}
for (int i = 0; i < Math.min(values.size(), _numeratorPolynom.length); i++) {
_numeratorPolynom[i] = values.get(i); // 2. min() 截断,超长输入静默丢弃
}
}
设计意图:定长数组换来确定性的内存布局与文件格式,代价是所有写入路径必须"清零 + 截断",否则上一次设置的 10 阶系数会残留在 3 阶新传递函数里,出现"明明改了参数波形却没变"的灵异问题。
copyAdditionalParameters()中对 4 个系数数组逐一new + arraycopy深拷贝——与矩阵组 3.2.3.2 是同一个坑。
脚本接口的类型防御:经getOperationEnumInterfaces()暴露给脚本引擎的setNumeratorPolynom等操作,入参可能是double[]也可能是Double[](取决于调用方),入口处checkParameterType()两种都接并显式转换,其余类型抛IllegalArgumentException——对外暴露的 Operation 接口不能假设调用方类型。
4.3.4 双模态对话框:初始化时序与交错存储
亮点:DTF 对话框支持"零极点增益"与"多项式系数"双模态互转,内部用实部/虚部交错的定长数组做统一存储,两种表示的互换全部收敛在对话框层。
4.3.4.1 零极点的交错存储(2i / 2i+1)
DialogDiscreteTransferFunction.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/DialogDiscreteTransferFunction.java#L153-L167)
private void updateReglerPolesZeros() {
_reglerDTF.clearPolesAndZeros();
for (int i = 0; i < _deNomModel.getSize(); i++) {
// 复数极点:实部存偶数位,虚部存奇数位(2i / 2i+1 交错)
_reglerDTF.setPole(((ComplexPrinter) _deNomModel.get(i))._value.getRe(), 2 * i);
_reglerDTF.setPole(((ComplexPrinter) _deNomModel.get(i))._value.getIm(), 2 * i + 1);
}
// 零点 _zeros 同理
}
4.3.4.2 构造期初始化:强制走一次模态切换 + _initDone 门闩
DialogDiscreteTransferFunction.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/DialogDiscreteTransferFunction.java#L42-L72)
jRadButtPoly.setSelected(_reglerDTF._inPolynomMode.getValue());
_inPolynomialMode = !_reglerDTF._inPolynomMode.getValue(); // 强制制造状态差异
toggleMode(); // 骗过幂等检查,执行一次初始化
_inPolynomialMode = _reglerDTF._inPolynomMode.getValue(); // 恢复真实状态
// ...
_initDone = true; // 此后监听器才允许写回模块
this.setVisible(true);
踩坑记录 1(构造期监听器误触发):
toggleMode()开头的幂等检查if (_inPolynomialMode == jRadButtPoly.isSelected()) return;会把"初始状态恰好一致"的首次初始化也拦掉。作者的解法是先人为取反_inPolynomialMode,强制走一遍模态切换逻辑(读系数、刷视图、设可见性),再恢复。属于丑但有效的 UI 初始化惯用法。
踩坑记录 2(_initDone门闩):构造过程中setSelected/setVisible会触发已注册的监听器,jCheckBoxEnabledActionPerformed里用if(_initDone)挡住,避免对话框还没显示就把模块状态改掉。任何"监听器写回模块"的对话框都需要这层门闩。
踩坑记录 3(幽灵依赖兄弟类):零极点列表的容量上限实际引用的是连续域版本ReglerTransferFunction.MAX_ARRAY_SIZE,而不是本类的同名常量——复制粘贴残留。两个常量目前恰好相等所以没爆雷,一旦单独改其中一个,对话框上限就与持久化数组容量脱钩。
对话框的顺序耦合(PWM 对话框):
ReglerPWMDialog.updateParameterVisibility()通过parameterPanel.getComponents()[4..7]硬编码索引切换死区时间 / 移相参数的可见性——createParameterPanel()的传参顺序成了隐式契约,中间插一个新参数,可见性切换就整体错位。声明式参数面板省了代码,也埋了顺序耦合,新增参数必须检查所有按索引取组件的地方。
4.3.5 Calculator 的唯一入口与"空壳类"残留
亮点:所有 DSP 模块的仿真计算只从 getInternalControlCalculatableForSimulationStart() 一个口子交给内核;UI 类里持有的 Calculator 引用一概不参与仿真。
ReglerDiscreteTransferFunction.java(/src/main/java/ch/technokrat/gecko/geckocircuits/control/ReglerDiscreteTransferFunction.java#L110-L113)
@Override
public AbstractControlCalculatable getInternalControlCalculatableForSimulationStart() {
return new DifferenceEquationCalculator(_sampleRate.getValue(), _denomPolynom, _numeratorPolynom);
}
踩坑记录(空壳内部类):文件里同时存在一个内部类
TransferFunctionCalculator(实现了InitializableAtSimulationStart、IsDtChangeSensitive,但三个方法全空),它不注册给仿真内核、不被任何路径调用——是从早期 StateSpace 方案迁移到差分方程方案后的残留死代码(旁边还有成片注释掉的_stateSpaceCalc/_savedState)。教训:UI 类内的任何 Calculator 成员都容易造成"改了它却没生效"的错觉,重构后要彻底删除空壳;反过来,想确认某段计算逻辑是否真的在跑,就搜它有没有从getInternalControlCalculatableForSimulationStart()返回。

浙公网安备 33010602011771号