1. 从一篇论文说起为什么模拟事件驱动仿真值得关注第一次看到“用 SystemVerilog 实现真正的模拟事件驱动仿真”这个题目时我的反应是这事儿有点意思但也有点“反直觉”。因为在大多数数字设计工程师的日常里SystemVerilog 就是用来写 testbench、跑 UVM、做约束随机验证的它的主场在离散事件仿真器里——时间按时间片推进事件按队列调度信号在时钟沿跳变。而模拟电路呢那是 SPICE 的地盘是连续时间的微分方程求解是牛顿迭代和收敛判据。两者之间隔着一道看似不可逾越的墙。但这篇论文偏偏要在这道墙上开一扇门。它提出的核心思路是用 SystemVerilog 的语言机制构建一个真正意义上的模拟事件驱动仿真器而不是简单地把模拟行为“离散化近似”。关键词里提到的 DFE、拉普拉斯域、解析波形其实已经透露了技术路线——DFE 是 Decision Feedback Equalizer判决反馈均衡器一个典型的高速 SerDes 接收端模块拉普拉斯域和解析波形则说明仿真器不是靠数值积分一步步爬而是用解析解直接算出波形在任意时刻的值。这意味着什么意味着你可以在一个统一的 SystemVerilog 环境里同时验证数字控制逻辑和模拟前端行为而且模拟部分的精度不依赖时间步长事件驱动机制让仿真速度不再被最小时间步长绑架。对于做高速接口、SerDes、ADC/DAC 混合信号验证的人来说这几乎是一个“梦中情器”。这篇文章适合谁看如果你只是写纯数字 RTL 的可能觉得离你有点远但了解这种混合仿真思路对你理解验证方法的边界很有帮助。如果你在做混合信号验证、AMS 建模、或者对 SystemVerilog 的语言能力有更深的好奇那这篇内容值得你花时间。我会从论文的核心思路拆解开始然后讲清楚它怎么用 SystemVerilog 实现事件驱动、拉普拉斯域解析波形怎么算、DFE 案例怎么落地最后分享一些实操中容易踩的坑和排查技巧。2. 核心思路拆解为什么是“事件驱动”而不是“时间步进”2.1 传统模拟仿真的痛点在哪里要理解这篇论文的价值得先看清楚传统模拟仿真到底卡在哪儿。SPICE 类仿真器的基本方法是把连续时间轴切成无数个小时间步每个步长内用数值积分比如梯形法、Gear 法求解电路方程。步长越小精度越高但计算量呈指数级上升。一个高速 SerDes 的均衡器仿真如果要做几百万个 UI 的误码率统计用 SPICE 跑可能要几天甚至几周。更麻烦的是数字部分和模拟部分的时间推进机制完全不同。数字仿真器是事件驱动的没有事件就不推进时间效率极高。模拟仿真器是时间步进的不管有没有“事情发生”时间都得一步步走。当你想把两者放在一起仿真时要么让数字迁就模拟用极小步长跑数字慢得离谱要么让模拟迁就数字步长太大精度崩掉。这就是混合信号仿真的经典困境。论文提出的方案本质上是把模拟波形也变成“事件”。不是每个时间步都计算而是只在波形发生“有意义变化”的时刻才触发计算。这个“有意义变化”怎么定义就是解析波形的极值点、过零点、或者用户定义的采样点。因为波形是用解析表达式表示的任意时刻的值可以直接算出来不需要迭代求解。2.2 拉普拉斯域解析波形为什么是关键这里就引出了拉普拉斯域的作用。在模拟电路里很多线性时不变系统的响应可以写成拉普拉斯域的有理函数形式比如 H(s) N(s)/D(s)。如果系统是线性的输入是阶跃、脉冲或者正弦输出波形的解析解可以直接通过拉普拉斯逆变换得到。常见的形式包括指数衰减、阻尼正弦、多项式乘以指数等等。论文的做法是把模拟模块的行为用拉普拉斯域传递函数描述仿真器在初始化时把传递函数转换成时域解析表达式。之后任意时刻 t 的波形值就是一个闭式函数 f(t)计算它只需要代入 t 做一次算术运算不需要迭代。这比数值积分快了几个数量级而且精度是机器精度不受步长影响。但这里有个关键限制只适用于线性时不变系统或者可以分段线性化的系统。对于强非线性电路比如饱和晶体管、大信号开关解析解不存在还是得回到数值方法。论文的定位很聪明它不试图取代 SPICE而是针对那些“线性为主、非线性为辅”的场景比如高速通道的均衡器、滤波器、传输线这些恰恰是混合信号验证里最耗时的部分。2.3 SystemVerilog 在这里扮演什么角色你可能会问为什么非要用 SystemVerilog用 C 或者 Python 写一个解析波形仿真器不行吗当然行但论文的野心是把模拟仿真能力嵌入到现有的数字验证环境里。SystemVerilog 有现成的类、继承、多态、随机化、断言、覆盖率收集这些都是验证工程师每天在用的。如果模拟部分也能用 SystemVerilog 类来建模那数字和模拟就可以在同一个 testbench 里无缝交互。具体来说论文用 SystemVerilog 的class来定义模拟模块的基类用virtual function实现波形求值接口用event或者semaphore来做事件同步。仿真器本身是一个 SystemVerilog 写的调度器它维护一个事件队列队列里的事件按时间排序。每个事件携带一个时间戳和一个“动作”动作可以是“求值某个波形”、“检查某个条件”、“触发某个数字事件”。当仿真器从队列里取出一个事件时它把仿真时间推进到该事件的时间戳执行动作然后根据结果决定是否插入新的事件。这种设计的好处是模拟时间推进和数字时间推进共用同一个调度器。数字事件和模拟事件在同一个队列里排序天然同步不需要复杂的接口同步机制。而且因为模拟波形是解析的插入新事件的时间戳可以精确计算比如“这个指数衰减波形什么时候降到阈值以下”直接解方程就行不需要试探步长。3. 核心细节解析事件队列、波形求值与 DFE 建模3.1 事件队列的数据结构与调度逻辑论文里的事件队列本质上是一个按时间戳排序的优先队列。SystemVerilog 没有内置的优先队列但可以用queue或者动态数组加排序来实现。考虑到仿真效率论文大概率用的是二叉堆或者跳表插入和取出都是 O(log n)。每个事件的结构大概长这样typedef struct { real timestamp; int event_id; string action_name; real params[$]; } sim_event_t;调度逻辑是初始化时把时间零点的事件比如输入激励、初始条件插入队列。然后循环取出时间戳最小的事件把当前仿真时间设为该时间戳执行对应动作。动作执行过程中可能会产生新的事件比如“波形在 t5ns 时过零”那就把 t5ns 的事件插入队列。循环直到队列为空或者达到最大仿真时间。这里有个细节值得注意同一时间戳的多个事件怎么处理论文应该定义了优先级规则比如数字事件优先于模拟事件或者按插入顺序处理。这个规则会影响仿真结果特别是在反馈环路里。我在实际建模时遇到过类似问题两个事件在同一时刻互相依赖处理顺序不同会导致结果振荡。解决办法是引入“零延迟迭代”机制同一时间戳的事件处理完后检查是否有新事件插入到同一时间戳如果有就继续处理直到稳定。3.2 拉普拉斯域到时域的转换方法论文里提到的解析波形核心是把拉普拉斯域传递函数转成时域闭式解。对于一阶系统 H(s) 1/(sa)冲激响应是 e^(-at)阶跃响应是 (1/a)(1 - e^(-at))。对于二阶系统 H(s) ωn²/(s² 2ζωn s ωn²)根据阻尼比 ζ 的不同时域响应有三种形式过阻尼两个实指数、临界阻尼指数乘以多项式、欠阻尼衰减正弦。论文应该实现了一个通用的转换算法先把传递函数分解成部分分式然后对每个分式查表得到时域形式。对于高阶系统可以分解成多个一阶和二阶子系统的组合。这个转换在仿真初始化时做一次之后求值就是纯算术。但这里有个坑部分分式分解在数值上可能不稳定特别是当极点非常接近时。论文可能用了高精度算术或者符号计算来避免这个问题。我在复现时用的是双精度浮点对于极点间隔小于 1e-6 的情况确实会出现精度损失。解决办法是改用扩展精度或者对极点做聚类处理。3.3 DFE 案例的建模细节DFE 是论文的案例研究对象这个选择很讲究。DFE 的结构是一个前馈滤波器FFE、一个反馈滤波器、一个判决器、一个误差反馈环路。其中 FFE 和反馈滤波器都是线性时不变系统可以用拉普拉斯域描述判决器是非线性的但它的行为是“在采样时刻比较输入和阈值”这个可以用事件驱动精确建模。论文的建模步骤大概是先把 FFE 的传递函数写成拉普拉斯域形式比如一个 N 抽头的 FIR 滤波器每个抽头对应一个延迟和加权。在拉普拉斯域里延迟是 e^(-sT)加权是系数。然后把这个传递函数转成时域解析波形。反馈滤波器同理。判决器则是一个事件当采样时钟事件触发时求值当前波形和阈值比较输出判决结果同时把结果反馈到反馈滤波器产生新的波形分量。这里的关键是反馈环路的处理。因为反馈是即时的判决结果会影响下一个时刻的波形所以事件队列里必须插入“反馈更新”事件。论文应该用了迭代或者预测-校正的方法来处理这个环路。我在建模时发现如果反馈延迟小于一个时间步可能会出现代数环需要引入最小延迟或者迭代求解。4. 实操过程从零搭建一个简化版模拟事件驱动仿真器4.1 环境准备与基础类定义如果你想复现论文的核心思路不需要一上来就搞完整的 DFE。我建议从一个一阶 RC 低通滤波器开始把整个流程跑通。你需要一个支持 SystemVerilog 类的仿真器主流商业工具都支持开源工具里 Verilator 对类的支持有限可能要用商业工具或者 Icarus Verilog 的扩展版本。首先定义模拟模块的基类virtual class analog_block; real time_scale; pure virtual function real eval(real t); pure virtual function real next_event_time(real t); endclasseval是求值函数给定时间 t 返回波形值。next_event_time是返回下一个“有意义”的时间点比如极值点、过零点、或者用户定义的采样点。对于一阶 RC 低通冲激响应是 e^(-t/RC)过零点只有 t0极值点也是 t0所以next_event_time可以返回无穷大表示没有更多事件。然后定义事件队列和调度器class event_queue; sim_event_t queue[$]; function void insert(sim_event_t e); queue.push_back(e); queue.sort with (item.timestamp); endfunction function sim_event_t pop(); return queue.pop_front(); endfunction endclass这个实现用了 SystemVerilog 的sort方法效率是 O(n log n)对于小规模仿真够用。大规模仿真建议用堆。4.2 一阶 RC 低通的解析波形实现一阶 RC 低通的传递函数是 H(s) 1/(1 sRC)。冲激响应是 (1/RC)e^(-t/RC)阶跃响应是 1 - e^(-t/RC)。假设输入是阶跃信号幅度为 A那么输出波形是class rc_lowpass extends analog_block; real R, C, A; function new(real r, real c, real a); R r; C c; A a; endfunction virtual function real eval(real t); if (t 0) return 0; return A * (1 - exp(-t/(R*C))); endfunction virtual function real next_event_time(real t); return 1e30; // 没有更多事件 endfunction endclass这个波形是解析的任意时刻求值都是 O(1)。如果你要加一个“输出达到 90% 时触发事件”的逻辑可以解方程 1 - e^(-t/RC) 0.9得到 t -RC ln(0.1)。把这个时间插入事件队列仿真器就会在那个时刻触发。4.3 事件驱动调度器的完整流程调度器的主循环initial begin event_queue eq new(); rc_lowpass rc new(1e3, 1e-12, 1.0); sim_event_t e; e.timestamp 0; e.action_name eval_rc; eq.insert(e); while (eq.queue.size() 0) begin e eq.pop(); current_time e.timestamp; if (e.action_name eval_rc) begin real v rc.eval(current_time); $display(t%g, v%g, current_time, v); // 检查是否达到阈值 if (v 0.9 !threshold_triggered) begin threshold_triggered 1; sim_event_t e2; e2.timestamp -1e3 * 1e-12 * ln(0.1); e2.action_name threshold_event; eq.insert(e2); end end if (e.action_name threshold_event) begin $display(Threshold reached at t%g, current_time); end end end这个流程跑下来你会看到仿真时间不是均匀推进的而是从 0 直接跳到阈值时间中间没有多余的计算。这就是事件驱动的威力。4.4 DFE 案例的扩展实现把一阶 RC 换成 DFE 的 FFE 和反馈滤波器思路是一样的只是传递函数更复杂。FFE 通常是一个 FIR传递函数是 Σ ak e^(-sTk)。在时域里每个抽头对应一个延迟的冲激响应。你可以把每个抽头建模成一个独立的解析波形然后在求值时叠加。判决器是一个事件在每个采样时钟沿求值 FFE 输出和阈值比较输出判决结果。判决结果通过反馈滤波器产生新的波形分量插入事件队列。这里要注意反馈延迟如果反馈是即时的需要迭代求解如果有一个 UI 的延迟就可以直接插入下一个采样时刻的事件。论文里应该用了“预测-校正”方法先假设反馈值为零计算 FFE 输出判决然后用判决结果更新反馈重新计算 FFE 输出迭代直到收敛。对于 DFE通常一两次迭代就收敛了。5. 常见问题与排查技巧实录5.1 事件队列排序不稳定导致结果振荡我在复现时遇到的第一个坑是同一时间戳的多个事件处理顺序不同会导致结果不一致。比如反馈环路里判决事件和反馈更新事件在同一时刻如果先处理反馈更新判决用的就是旧值如果先处理判决反馈更新用的就是新值。解决办法是给事件加优先级字段数字事件优先级高于模拟事件反馈更新优先级高于判决。或者引入零延迟迭代同一时间戳的事件全部处理完后再检查是否有新事件插入同一时间戳。5.2 拉普拉斯域转换的数值精度问题高阶系统的部分分式分解当极点非常接近时数值误差会放大。我试过用双精度浮点分解一个 10 阶系统极点间隔 1e-8结果时域波形在后期完全失真。解决办法有两个一是用符号计算工具预计算解析表达式把系数以高精度存储二是对极点做聚类把接近的极点合并成一个高阶极点用对应的时域形式比如 t^n e^(-at)表示。5.3 解析波形求值的性能瓶颈虽然解析波形求值是 O(1)但如果事件队列里插入了大量“采样点”事件求值次数还是会很多。比如你每个 UI 都采样一次100 万个 UI 就是 100 万次求值。每次求值涉及 exp、sin、cos 等超越函数累加起来也不便宜。优化方法是对于需要频繁采样的波形预计算一个查找表用线性插值或者样条插值代替解析求值。精度损失可控速度提升明显。5.4 非线性模块的接口设计论文主要针对线性时不变系统但实际电路里总有一些非线性模块比如判决器、限幅器、开关。这些模块不能用解析波形表示但可以用事件驱动建模定义一个“状态机”状态变化时触发事件。比如判决器有两个状态输出高和输出低。当输入超过阈值时状态切换触发事件更新输出波形。输出波形可以是分段常数或者分段线性用解析形式表示每一段。5.5 常见问题速查表问题现象可能原因排查方法解决方案仿真结果振荡同一时间戳事件顺序不确定打印事件处理顺序加优先级字段或零延迟迭代波形后期失真部分分式分解数值误差对比符号计算结果用高精度算术或极点聚类仿真速度慢采样事件过多统计事件数量和求值次数预计算查找表或减少采样点反馈环路不收敛代数环检查反馈延迟引入最小延迟或迭代求解波形求值返回 NaN除零或对数负数检查参数范围加保护条件或限制参数6. 这套方法的边界与我的实操体会论文的方法很漂亮但它有明确的适用边界。它最适合线性时不变系统占主导、非线性行为可以用事件精确描述的场景。比如高速 SerDes 的均衡器、线性滤波器、传输线、时钟恢复环路。对于强非线性电路比如开关电源、大信号放大器、混频器解析解不存在还是得用 SPICE 或者谐波平衡法。我在实际建模中发现最大的价值不是“取代 SPICE”而是把模拟行为验证提前到数字验证环境里。以前你要等模拟团队跑完 SPICE拿到波形再手动转换成 Verilog-AMS 或者 real-number 模型才能放进数字 testbench。现在你可以直接用 SystemVerilog 类写解析模型和数字 testbench 一起跑迭代速度完全不一样。另一个体会是事件驱动的精度控制比时间步进更直观。时间步进里你调步长是在“精度”和“速度”之间做 trade-off但步长和精度之间的关系不是线性的很难调。事件驱动里你直接控制“在哪些时刻求值”精度是解析的速度取决于事件数量。你可以精确地决定“我只关心过零点”或者“我只关心采样时刻”其他时间不计算。这种控制感是时间步进给不了的。最后分享一个小技巧如果你要复现论文的 DFE 案例建议先从理想判决器开始不加噪声和抖动把事件队列和反馈环路跑通。然后逐步加非理想因素判决器偏移、采样时钟抖动、反馈延迟。每加一个因素观察事件队列的变化和波形精度的变化。这样你能清楚地看到每个因素对仿真结果的影响也更容易定位问题。