
做电力电子仿真的同行应该都有这种体会PSCAD 里库元件不够用是常态MMC 换流器、自定义保护逻辑、新型拓扑验证最后几乎都要落到“自定义元件”这一步。而新建 Custom Component 之后脚本编辑器里最显眼的就是两个子程序dsdyn 和 dsout。这两个名字在官方文档里出现得最多但真正说清楚“为什么需要两个子程序”“各自管什么”“代码应该怎么写”的资料极少。官方手册写得全而不细加上通篇英文和 Fortran 代码很多人在第一步就卡住了。我当年啃 MMC 示例模型文档时光分清 dsdyn 和 dsout 就花了好几天最后还是靠把文档丢给 DeepSeek 当翻译加助教才把整套逻辑真正吃透。这篇文章把两件事讲透第一dsdyn 和 dsout 到底是什么、怎么用、有什么坑第二怎么用 DeepSeek 高效翻译和理解 PSCAD 英文文档让官方说明书不再劝退。不管是刚接触 PSCAD 的研究生还是想自己写 MMC 控制模型的工程师都能直接用得上。1. PSCAD 自定义元件的整体框架与 dsdyn/dsout 的定位1.1 为什么需要自定义元件PSCAD 自带元件库再丰富也覆盖不了所有需求。拿 MMC 来说你要模拟一个模块化多电平换流器库里没有现成的“MMC 阀”块。你能找到的只是一个通用开关模型或者子模块库但真正的“电容电压动态”“桥臂电流分配”“调制与均压”全都要自己写。这时候只有两条路一条是把几十个模块手工拼成一个大电路另一条就是写一个自定义元件把整套逻辑封装成一个可拖拽使用的模块。自定义元件User Defined ModelUDM采取“图形界面 Fortran 脚本”的双层结构。图形界面负责定义输入输出端口、参数对话框、元件图标脚本则用 Fortran 描述元件的电气行为和控制逻辑。PSCAD 会自动为你的元件生成一个 Fortran 骨架文件这个文件里有几个预留的子程序其中最核心的就是 dsdyn 和 dsout。理解自定义元件关键在于理解一个事实PSCAD 的仿真内核是按固定步长推进的而自定义元件在每个步长内都必须完成“基于当前状态和输入计算新的状态和输出”这个过程。dsdyn 和 dsout就是把这个过程拆成了两步。1.2 dsdyn 与 dsout 在仿真主循环中的位置PSCAD 每个仿真步长内做的事情可以简化为四条从电气网络和其他元件读取当前的输入信号调用所有元件包括自定义元件的 dsdyn 子程序完成动态状态更新调用所有元件的 dsout 子程序计算输出信号调用网络求解器解电气方程更新波形记录也就是说在任何一个步长里自定义元件的两个子程序都会被依次调用一遍先 dsdyn后 dsout。这个次序是固定的而且不是巧合——PSCAD 希望你先更新所有元件的“状态记忆”再用最新状态去算输出避免同一步长内出现状态和输出互相不一致的情况。我见过不少新手把输出计算直接写在 dsdyn 里看起来也能跑但一旦系统里有多个自定义元件耦合就容易出现一个步长内“状态已经更新、输出还没算完”的时序混乱结果是波形出现奇怪毛刺排查起来非常痛苦。所以从一开始就要养成习惯动态更新放 dsdyn输出映射放 dsout。1.3 官方文档到底难在哪既然 dsdyn 和 dsout 这么重要直接看官方手册总行了吧问题在于官方手册的“自定义模型”章节默认读者已经具备两套知识一是 Fortran 语法至少能看懂 IF/THEN/ELSE 和变量声明二是仿真原理知道什么是积分、什么是状态变量。在这个前提下文档把大部分篇幅都用来罗列参数和接口变量比如 STORE、NSTORF、ICALL每个都给一两句话定义但几乎不给完整示例。更麻烦的是术语密集。State Variable、Internal Variable、Storage Quantity 这几个词在文档里频繁出现含义相近但完全不相等中文资料又少硬生生啃下来非常吃力。这也是为什么我会想到让 DeepSeek 先做术语提取再逐段翻译最后结合代码反推逻辑。这个方法我会在第四章详细展开。2. dsdyn 子程序动态计算的核心逻辑拆解2.1 dsdyn 到底负责什么dsdyn 的全称是 Dynamic Simulation Subroutine官方定义是“处理元件动态行为的子程序”但更直白地说它就是元件“记忆和进化”的地方。具体来说dsdyn 要做三件事初始化在仿真开始前给所有需要跨步长保存的量赋初值状态更新每个步长根据输入、参数和上一步状态计算新的状态值内部标志管理比如检测到过流、过压后把某个内部标志置位供同一元件的 dsout 读取注意第三件事。自定义元件内部经常需要一个“标志位”——比如电流过流后我要封锁触发脉冲这个“是否封锁”的信息需要在仿真过程中持续保存而且要让输出阶段知道。这类逻辑放在 dsdyn 里再合适不过。为什么状态更新必须放在 dsdyn 而不是 dsout因为时序。PSCAD 在同一步长内先 dsdyn 后 dsout你只有在 dsdyn 里更新了状态dsout 才能拿到本步的最新值。反过来如果你在 dsout 里改状态那这个修改要等下一步的 dsdyn 才能被看到整个系统就慢了一步遇到非线性环节甚至可能振荡。2.2 关键变量与代码骨架在实际写代码前有几个变量必须弄清楚它们是 PSCAD 自动填充的全局量DT仿真步长单位秒。做积分时直接用比如 STORE STORE 输入 * DT。ICALL调用标识PSCAD 在不同仿真阶段用不同数值调用 dsdyn你需要用 IF 判断写分支。最典型的是初始化分支和正常运行分支。STORE 和 NSTORFSTORE 是可以跨步长保存数据的浮点存储数组NSTORF 是当前可用位置的索引。元件里每声明一个“存储槽”编译时 PSCAD 就会分配一个索引你在代码里按索引存取。一个典型的 dsdyn 骨架大概是这样的! DSDYN - Dynamic part IF (icall .EQ. 0) THEN ! 初始化阶段 STORE(NSTORF) 0.0 STORE(NSTORF 1) 0.0 ELSE ! 动态阶段更新状态 STORE(NSTORF) STORE(NSTORF) DT * 输入1 STORE(NSTORF1) STORE(NSTORF1) DT * 输入2 ENDIF这里我要特别提醒一件事不同版本的 PSCAD 对 ICALL 的枚举值定义不一样。老版本和 X4 之后的版本在细节上有差异网上很多帖子抄来抄去很容易误导。正确做法是打开你自己版本的手册查清楚 ICALL 每个数值对应的阶段再写初始化分支。2.3 dsdyn 常见的三个坑我先说我在写 MMC 电容电压动态时踩过的一个坑。MMC 每个子模块电容都在充放电按原理电容电压是电流积分的结果所以我顺理成章地把所有子模块电容电压的积分更新写进 dsdyn。一开始只写了 ICALL0 的初始化和 ELSE 的动态更新没考虑到 PSCAD 在正式仿真前还有一次额外的初始化调用。结果就是仿真刚一开始积分器就基于一个未初始化完全的状态跑了一两个步长波形开头出现明显跳变。类似的问题总结下来有三类第一初始化分支写不完整。记住所有存储变量、状态变量必须在初始化分支里显式赋初值哪怕你确定默认就是 0。这个习惯能帮你省掉大量“仿真开头波形异常”的排查时间。第二把不该在这个阶段的逻辑塞进来。比如大规模数组操作或排序算法某些版本下在初始化阶段执行可能并不合适而且每次步长调用会拖慢仿真速度。放在合理阶段运行时会有差别。第三存储索引数错位。如果你在接口定义里声明了三个存储变量实际代码里却用了 NSTORF、NSTORF1、NSTORF3那第三个数据就会覆盖别的变量结果不可预期。建议在代码头部用一个常量或者注释注明每个索引的用途比如! NSTORF0 : 电容电压状态 ! NSTORF1 : 上次循环电流 ! NSTORF2 : 过流标志位3. dsout 子程序输出计算与 dsdyn 的分工3.1 dsout 的职责边界dsout 的全称是 Output Subroutine它的职责很简单根据当前输入、参数和 dsdyn 更新好的状态计算出元件所有输出端口的数值。dsout 本质上是一段“纯映射”代码——给一组输入算一组输出不改变任何状态记忆。典型场景包括PI/PID 控制器输出计算MMC 的调制波生成比如最近电平逼近、载波移相保护逻辑判断后的跳闸/封锁信号输出查表、限幅、符号判断等代数运算在 PSCAD 里输入输出端口在接口定义时会自动生成带 $ 前缀的引用名。比如你给输入端口起名 Err那么在代码里就用 $Err 读取输出端口起名 Ctl就用 $Ctl 赋值。这个约定习惯之后非常方便因为 PSCAD 编译时会自动把这些名称替换成真正的内部变量。3.2 典型代码结构dsout 的代码通常很短核心就是赋值。比如 PI 控制器! DSOUT - Output part $Ctl $Kp * $Err $Ki * STORE(NSTORF) IF ($Ctl .GT. $LimitMax) $Ctl $LimitMax IF ($Ctl .LT. $LimitMin) $Ctl $LimitMin这里 STORE(NSTORF) 是 dsdyn 里算好的积分项直接拿来做输出计算即可。如果你有多个输出就依次赋值如果你需要中间变量比如先把某几个量相加再输出可以直接在 dsout 里用局部变量。有一个容易被忽略的点dsout 虽然名叫输出子程序但它也可以不直接对应物理端口。比如接口定义里有一个“辅助输出”变量用于向其他元件传递数据或者一个供监测用的内部跟踪变量都可以在 dsout 里赋值。3.3 两者分工一个类比彻底说清很多人记不住 dsdyn 和 dsout 谁管什么我给一个特别通俗的类比把元件看成一个人跑步。dsdyn 是“训练记录本”每次跑完把今天的新状态里程、心率、疲劳程度写进记录本。dsout 是“体检报告”基于记录本上的数据算出一组数字给别人看配速、卡路里、预计完赛时间。体检报告不会改变训练记录本但它依赖记录本上的数据训练记录本每天更新但不会被体检报告写回。这就是两者的关系。在实际项目中这个类比也是我判断代码写没写对的标尺如果一个变量是“过程积累量”它应该在 dsdyn 里更新如果一个变量是“给别人看的计算结果”它应该进 dsout。比如 MMC 子模块的电容电压是积累量在 dsdyn 里积分更新而桥臂参考电压是计算结果在 dsout 里根据调制算法算出。3.4 dsout 里的几个实用技巧先说限幅。输出限幅放在 dsout 里做用 Fortran 的 IF 判断或者 MIN/MAX 函数都行。但要注意如果你做的是控制器输出限幅之后最好把“限幅实际输出值”存回一个存储变量供 dsdyn 里的积分逻辑使用实现所谓的抗饱和anti-windup。否则积分一直累加限幅器后信号却封顶了恢复时会有一段很长的迟滞——这是 PI 控制最经典的问题。再说查表。如果你的 dsout 里需要查一个二维表可以把表数据定义成局部数组并在初始化阶段dsdyn 的 ICALL0 分支填充然后在 dsout 里直接用索引查。别小看这个技巧我在处理自定义保护曲线时就是这么做的比每次步长重复填充数组高效得多。4. 用 DeepSeek 翻译和理解 PSCAD 英文文档的实操方法4.1 先别急着全文翻译先让 AI 做目录地图拿到一份 PSCAD 英文文档比如 Users Guide 里的自定义模型章节我的经验是不要上来就把全文扔给 AI 让它翻。正确做法是分三步第一步让 AI 翻译目录和章节标题针对每个标题用一句话概括“这一章大概讲什么”。这一步能帮你建立整份文档的结构感知道哪些章节是核心、哪些可以跳过。第二步让 AI 提取术语表。提取出来的术语可能多达几十个我会让它做成中英对照表并标注在 PSCAD 上下文中的含义。这个表就是后续所有翻译和代码阅读的基准。第三步才开始逐段翻译正文但要求 AI 在翻译时保留英文术语并在括号里标注。我常用的 prompt 格式是这样的你是一名精通 PSCAD 的技术文档翻译专家。请把下面的英文文档目录翻译成中文 并对每个章节用一句话说明它大概讲什么。术语请保留英文并在括号里标注中文。 [粘贴目录文本]注意这个 prompt 里我刻意强调了“用一句话说明”目的是让 AI 输出更结构化而不是只给一个词组翻译。4.2 术语表怎么建才能不白建术语表不是把单词翻成中文就完事关键是“在 PSCAD 里的含义”这一列。比如 State Variable 直译是“状态变量”但你需要知道在自定义元件上下文里它指的是“需要在时间步之间记忆的值”。再比如 STORE直译是“存储”但在代码里它指的是那个浮点存储数组。我让 DeepSeek 生成的术语表长这样英文术语中文翻译在 PSCAD 自定义元件中的含义DSDYN动态仿真子程序每个步长更新元件状态记忆DSOUT输出子程序每个步长计算元件输出ICALL调用标识标识当前调用阶段用于分支STORE存储数组保存跨步长数据的浮点数组NSTORF存储索引当前可用存储位置的下标DT仿真步长两个仿真点的时间间隔TRACE跟踪输出调试时输出中间变量到窗口有了这张表再去看文档正文很多句子就能直接读懂了。我建议把这张表单独存成一个 Markdown 文件作为整个项目期的速查手册。4.3 让 AI 解释“为什么”而不是只让 AI 翻译翻译只是表层理解“为什么这么设计”才是吃透文档的关键。同样是 PSCAD 文档里的一句话“The values stored in the storage array are preserved between calls to the subroutine.” 你让 AI 翻译它只会说“存储数组中的值在子程序调用之间被保留”。但你让 AI 深入解释它会告诉你因为 PSCAD 调用的子程序是局部作用域每次进入时所有局部变量都会被重新初始化为了跨步长保持状态就必须使用 STORE 数组这种“保活”机制所以所有需要记忆的状态量都必须写到 STORE 里而不是普通局部变量。这种“翻译 解释”结合的方式才是用 AI 读技术文档的正确姿势。我的提问模板是下面这段话是 PSCAD 官方文档里对 STORE 数组的说明。请先翻译成中文 然后解释1) 为什么需要这个机制2) 如果不用它仿真会发生什么问题 3) 在实际代码中怎么用。最后给一段最小示例代码。 [粘贴原文]注意这里我要求 AI“给一段最小示例代码”因为示例比任何描述都直观。4.4 让 AI 生成代码和改代码除了翻译文档DeepSeek 还有一个很实用的场景根据文档描述直接生成 dsdyn/dsout 代码或者帮你改写已有代码。比如我在摸 PSCAD 自定义元件时直接这样问我要在 PSCAD 自定义元件里实现一个带限幅的 PI 控制器。 输入端口叫 Err输出端口叫 Ctl参数有 Kp、Ki、LimitMax、LimitMin。 积分状态用 STORE(NSTORF) 保存在 dsdyn 里更新在 dsout 里算输出并限幅。 请帮我写出三段内容dsdyn 的代码段、dsout 的代码段、以及 PSCAD 接口定义里的配置说明。AI 给出的代码虽然不是 100% 直接能编译但骨架和逻辑基本都在我只需要微调变量名、补初始化分支就能放进 PSCAD 验证。相比从零开始查手册写代码效率至少提升两倍。这里有一个必须强调的注意事项AI 生成的代码一定要逐行检查不能无脑使用。尤其是 STORE 索引、状态初始化、积分方向这些细节AI 很容易一本正经地写错。我在工作中总结的口诀是“AI 给思路人来定细节AI 给框架人来填坑”。4.5 报错信息也能交给 AI 反向学习PSCAD 编译自定义元件时的报错信息往往指向 Fortran 编译器输出比如 Undefined variable、Type mismatch、Array index out of bounds 等等。对新手来说光是定位报错在哪一行就要花半天。把报错粘贴给 DeepSeek再附上你的相关代码通常它能直接指出三到五种可能原因和对应排查步骤。比如我在 PSCAD X4 里编译自定义元件报错说 storage variable index out of bounds。 我的 dsdyn 代码里用了 STORE(NSTORF) STORE(NSTORF1) STORE(NSTORF2) 但是接口定义里只声明了 2 个存储变量。请帮我分析问题并给出修改方案。这种用法实际上是“逆向学习”报错逼着你去理解 STORE 和 NSTORF 的分配机制AI 帮你把机制讲清楚一次报错就能彻底搞懂一类问题比自己瞎试高效得多。5. 常见问题、编译报错与调试技巧实录5.1 术语理解偏差是最大的暗坑AI 翻译再强也架不住专业术语在不同上下文里的含义差异。我实际遇到过一个案例文档里提到 internal variableDeepSeek 直译成“内部变量”这没问题但和 Fortran 里“局部变量”混在一起理解就不对了。在 PSCAD 文档中internal variable 特指在元件内部声明、不对外输出、但在 dsdyn/dsout 之间共享的变量。这跟“子程序内部的局部变量”是两个层级的概念。类似的还有 state variable 和 storage quantity 的关系。简单说state variable 是“语义上的状态”storage quantity 是“物理上的存储槽位”两者通过 STORE 数组建立联系。如果 AI 翻译把这一层绕开了你看到的译文虽然通顺但理解还是偏的。我的解决办法很简单每翻译一段都回到代码里找对应物。文档说“state variable 必须在 dsdyn 中更新”我就翻编译后的模板源码看哪个变量对应这个 state variable、它和 STORE 怎么映射。代码不会骗人翻译可能存在偏差但编译器的行为和 Fortran 源码是硬事实拿硬事实校准软翻译理解就不会跑偏。5.2 编译报错速查表实际调试中我总结了一些高频报错的套路报错现象最常见原因排查步骤Storage array index out of boundsSTORE 索引超过声明数量核对接口定义里的存储变量数量逐个数 NSTORF 偏移Undefined variable XXX变量名拼错或未声明检查接口定义中的 $ 变量名检查是否忘记在 Declarations 里声明局部变量Type mismatch in assignment整数与实数混用检查赋值两侧类型注意 Fortran 里整数除以整数仍是整数Division by zero某处分母可能为 0在除法前加防护判断特别是初始化阶段Syntax error in DSDYN/DSOUTIF/ENDIF 不匹配或括号缺失用编译器定位行号检查子程序结尾 END 是否配对这张表不是标准答案每个版本、每个项目都会有自己的变体。但排查思路是通用的先看行号再对接口定义最后搜变量名。5.3 调试用 TRACE 把内部状态看穿dsdyn 和 dsout 的代码在不加输出时是“黑盒”循环几十步后波形对不对你只能猜。最直接的调试手段就是用 TRACE 把关键中间变量实时输出到 PSCAD 的信息窗口或者输出成记录曲线。举个例子我在调 MMC 的均压排序逻辑时怀疑某个桥臂的选择排序结果不对就在 dsdyn 里把每次排序后选中的子模块编号 TRACE 出来CALL TRACE(SEL_MOD, REAL(SELECTED_MODULE))然后跑仿真观察 TRACE 输出是否和理论判断一致。这个定位过程比对着波形猜逻辑快了不是一点半点。TRACE 的具体语法在 PSCAD 不同版本里略有差别以你本地手册的调试章节为准。另外建议把 dsdyn 和 dsout 分段加 TRACE 时先在头部打印 TIME方便对齐时间轴。调试完再删掉所有 TRACE避免影响后续仿真速度。5.4 关于版本差异的补充最后补一个容易被忽视的点PSCAD 不同大版本4.x 和 X4/5.x在自定义元件的脚本格式上有些差异网上很多经验帖是基于特定版本的。如果你用 X4 搜到一篇基于 4.6 的教程代码里使用的某些函数或调用方式可能已经变了要让 DeepSeek 帮你翻译文档时明确标注版本号并让它提示“这段内容在不同版本中的差异”。养成“对版本”的习惯能避开很多无意义的时间浪费。6. 完整案例用 dsdyndsout 手写一个 PI 控制器6.1 建模目标与接口设计作为完整走一个流程的示例我们建一个自定义 PI 控制器。建模目标输入误差信号 $Err输出控制信号 $Ctl参数Kp比例系数、Ki积分系数、LimitMax上限、LimitMin下限状态积分累积量用 STORE(NSTORF) 保存在 PSCAD 里新建 Custom Component 后需要做三步界面配置在接口定义里添加输入端口名字写 Err添加输出端口名字写 Ctl在参数对话框里添加四个实数参数Kp、Ki、LimitMax、LimitMin并给默认值比如 2.0、10.0、100.0、-100.0。这些名字会直接影响代码里的引用方式起名要简短、有意义、不含空格。6.2 dsdyn 代码初始化与积分更新在 PSCAD 自动生成的模板里找到 DSDYN 段按下面的逻辑写! DSDYN - Dynamic part IF (icall .EQ. 0) THEN ! 初始化积分状态 STORE(NSTORF) 0.0 ELSE ! 每个仿真步长积分累加Ki 并入积分项 STORE(NSTORF) STORE(NSTORF) $Ki * $Err * DT ENDIF解释一下每一步ICALL0 分支对应仿真开始前的初始化调用给积分状态赋 0ELSE 分支是正常运行阶段每一小步把误差按时间积分累加用当前仿真步长 DT 做乘法积分项里直接乘上 Ki这样 dsout 里的表达式可以更紧凑。如果你要做的是积分分离 PI可以在这里加一个判断当误差大于某个阈值时不执行积分更新避免积分饱和。这个逻辑改动只在 dsdyn 里做就行dsout 不用动。6.3 dsout 代码输出计算与限幅在 DSOUT 段写! DSOUT - Output part $Ctl $Kp * $Err STORE(NSTORF) ! 输出限幅 IF ($Ctl .GT. $LimitMax) THEN $Ctl $LimitMax ELSEIF ($Ctl .LT. $LimitMin) THEN $Ctl $LimitMin ENDIF注意这里我用的是 Fortran 的 ELSEIF 写法。在 dsout 里STORE(NSTORF) 读到的就是 dsdyn 刚更新好的积分值所以输出计算用的状态是最新的。如果你要加入抗积分饱和anti-windup一个简单做法是在 dsout 算完限幅后把“真实输出”写回一个存储变量让 dsdyn 在下一步积分时参考。更复杂的做法是限制积分累加范围例如! 在 dsdyn 的积分更新里加上下限 STORE(NSTORF) STORE(NSTORF) $Ki * $Err * DT IF (STORE(NSTORF) .GT. $LimitMax) STORE(NSTORF) $LimitMax IF (STORE(NSTORF) .LT. $LimitMin) STORE(NSTORF) $LimitMin这样积分项永远不会跑到离谱的数值输出限幅也更容易一步到位。6.4 在 PSCAD 中验证元件封装好后搭一个最小的闭环测试电路阶跃信号作为参考输入被控对象用一阶惯性环节PSCAD 库里 Transfer Function 块回路反馈构成闭环自定义 PI 的输出进被控对象被控对象输出再反馈回来。运行仿真后看两点第一稳态误差。如果被控对象是纯增益或一阶惯性加入 Ki 后稳态误差应该趋近于 0。如果稳态误差不等于 0检查积分项是否一直在累加、限幅是不是提前触发了。第二响应形态。把自定义 PI 的输出波形和库里自带 PID 模块的输出波形放在同一个示波器里对比两者应该基本重合。如果偏差明显先检查参数是否一致再看 dsdyn 里的积分方向。这套流程同样可以直接迁移到 MMC 控制器的搭建上把 PI 的输出从直流电压控制换成桥臂电流内环把限幅改成电容电压均衡约束本质都是 dsdyn 管理状态、dsout 计算输出这套逻辑。说实话我做自定义元件的前两年都是在“边猜边试”直到把 PSCAD 英文文档交给 DeepSeek 精读一遍才真正建立起对 dsdyn/dsout 的完整认知。这套“翻译 术语表 代码反推 报错反向学习”的流程后来也用在读 MMC 示例模型、看改造项目里的旧代码上效果都很稳定。最后分享一个我现在的习惯每次新建自定义元件我都会在 dsdyn 和 dsout 开头留一段注释写明每个存储索引的用途、每个端口对应的物理意义。看起来是小事但几个月后回看代码时能省掉大量回忆时间。再加上一份 DeepSeek 生成的术语表整个项目期的查文档时间至少砍半。