大数跨境

Verilator 的 eval 实现:调度区域与实例对照

Verilator 的 eval 实现:调度区域与实例对照 ai算法芯片与系统
2026-10-07
3
导读:Verilator按IEEE 1800分层事件队列生成eval函数,核心是ico、act、nba三个区域,五层循环保证收敛。边沿检测在act,时序逻辑在nba,组合逻辑在ico。调度框架VlEvalL

 

目录

一、引言
二、eval 函数全表
三、完整调度顺序
四、五层循环的意义
五、调度框架的复用
六、边沿检测:eval_act 的核心
七、always 块在哪个 eval
八、阻塞赋值 vs 非阻塞赋值
九、各 eval 区域的实例对照
十、两套调度模型
十一、性能特征
十二、总结


一、引言 📖

Verilator 把 Verilog 编译成 C++ 时,不是把所有逻辑塞进一个大函数,而是按照 IEEE 1800 SystemVerilog 标准的分层事件队列,把仿真调度拆成多个区域。每个区域对应一个 eval_* 函数。

这套机制的存在,是为了精确模拟 SystemVerilog 的调度语义:组合逻辑要迭代到稳定,非阻塞赋值要延迟提交,断言要在特定区域采样。没有这些区域,仿真结果就会和标准仿真器不一致。


二、eval 函数全表 📋

Verilator 生成的模块类里,每个 eval_* 函数对应一个调度区域:

函数
缩写
对应区域
用途
evalBegin
—
—
每次 eval 入口
evalStatic
Static
静态初始化
一次性初始化
evalInitial
Initial
初始块
处理 initial
evalStl
Settle
Settle
迭代到稳定
evalSample
Sample
Preponed
采样供断言使用
evalIco I
nactive Combinational
Pre-Active
顶层输入驱动的组合逻辑
evalAct Act
ive
Active
阻塞赋值、边沿检测
evalInact Inact
ive
Inactive
#0
 延迟
evalNba N
on-Blocking Assignment
NBA
提交非阻塞赋值
evalObs Obs
erved
Observed
断言检查
evalReact React
ive
Reactive
program
 块
evalPostponed
Postponed
Postponed
$strobe
 / $monitor
evalEnd
—
—
每次 eval 出口
evalFinal
Final
Final
仿真结束

纯 RTL 设计通常只用到 evalStatic、evalIco、evalAct、evalNba 四个,其余是给断言和 program 块预留的空壳。

三、完整调度顺序 🗓️

Verilator 5.x 的调度逻辑封装在 VlEvalLoop::evalImpl() 里,所有模块共用这一份通用代码:


   
   
   
   
    
   
   
   
   void VerilatedEvalLoop::evalImpl() {
    m_model.evalBegin();

    // 一次性初始化

    if
 (!m_didInit) {
        m_model.evalStatic();
        m_model.evalInitial();
        do
 { checkConvergence(...); }
        while
 (m_model.evalStl(firstIteration));
        m_didInit = true;
    }

    // 采样(每次 eval)

    m_model.evalSample();

    // 输入组合逻辑循环

    do
 { checkConvergence(...); }
    while
 (m_model.evalIco(firstIteration));

    // 五层嵌套事件循环

    do
 {                                    // React
        do
 {                                // Observed
            do
 {                            // NBA
                do
 {                        // Inactive
                    do
 {                    // Active
                        checkConvergence
(...);
                    } while (m_model.evalAct());
                } while (m_model.evalInact());
            } while (m_model.evalNba());
        } while (m_model.evalObs());
    } while (m_model.evalReact());

    // 时间步末尾

    m_model.evalPostponed();
    m_model.evalEnd();
}

三层结构


四、五层循环的意义 🔄

五层循环是 IEEE 1800 分层事件队列的直接实现:

嵌套而不是顺序,是因为每个区域的副作用会触发更内层的区域:

  • • Active 触发 NBA:阻塞赋值可能改变寄存器输入
  • • NBA 触发 Active:寄存器更新后组合逻辑要重算
  • • NBA 触发 Observed:寄存器变化触发断言
  • • Observed 触发 Reactive:断言触发 program 块
  • • Reactive 触发 Active:program 块的赋值又回到 Active

所以外层每迭代一次,都要重跑所有内层,直到全部收敛。

收敛条件

每层循环的退出条件是 eval_X()返回 false:

  • • evalAct 返回 true → 还有 Active 工作,继续循环
  • • evalAct 返回 false → Active 稳定,跳出
  • • 五层全部返回 false → 整个 evalImpl结束

checkConvergence 设置上限(默认 10000 次),防止真正的死循环。


五、调度框架的复用 🏗️

每个模块的 eval_step() 只有一行:


   
   
   
   
    
   
   
   
   void Vfp_adder::eval_step() {
    VL_DEBUG_IF
(...);
    m_evalLoop.eval();     // ← 所有调度在这
}

调度顺序是通用的,Verilator 5.x 把它封装在 VlEvalLoop 里。每个模块类通过虚函数实现 evalAct / evalNba / evalIco 等钩子。

VerilatedModel 里定义纯虚函数,每个模块类重写它们。这就是 evalBegin / evalNba 声明里全是 override final 的原因。

框架复用,逻辑各异——这是 Verilator 5.x 相比 4.x 的重要改进。


六、边沿检测:eval_act 的核心 🔍

posedge clk / negedge rst_n 的检测在 eval_act 里完成:


   
   
   
   
    
   
   
   
   bool Vfp_adder___024root___eval_act(Vfp_adder___024root* vlSelf) {
    vlSelfRef.__VactTriggered[0] =
        (((~rst_n) & __Vtrigprevexpr_rst_n) << 1)   // negedge rst_n
      | ((clk & ~__Vtrigprevexpr_clk) << 0);        // posedge clk

    vlSelfRef.__Vtrigprevexpr_clk   = clk;
    vlSelfRef.__Vtrigprevexpr_rst_n = rst_n;

    trigger_orInto__act_vec_vec
(__VnbaTriggered, __VactTriggered);
    return
 0;
}

两个触发变量

变量
含义
生命周期
__VactTriggered 本次
 Active 检测到的边沿
每次 eval_act 重算
__VnbaTriggered 累积
待执行的触发
跨多次 eval 保留,直到 eval_nba 消费

firstIteration 参数

eval_ico(firstIteration) 和 eval_stl(firstIteration) 带一个布尔参数:

  • • 第一次迭代:true —— 强制所有触发位置位,让所有逻辑至少跑一遍
  • • 后续迭代:false —— 只处理新增触发

这是为了区分"初始化"和"增量更新"。


七、always 块在哪个 eval 📦

always 块的逻辑分散在多个 eval 函数里,取决于敏感列表:

Verilog 写法
边沿检测
块内逻辑
always @(posedge clk) eval_act eval_nba
always @(posedge clk or posedge rst) eval_act eval_nba
always @(*)
 / always_comb
eval_ico eval_ico
always @(a or b) eval_ico eval_ico
assign
—
eval_static

时序逻辑拆两半


   
   
   
   
    
   
   
   
   always @(posedge clk or posedge rst) begin
    if
 (rst) i <= 0;
    else
     i <= i + 1;
end

边沿检测在 eval_act:


   
   
   
   
    
   
   
   
   __VactTriggered[0] = (clk & ~clk_prev) | ((~rst) & rst_prev) << 1;

块内逻辑在 eval_nba → nba_sequent__TOP__0:


   
   
   
   
    
   
   
   
   if (rst) __Vdly__i = 0;
else
     __Vdly__i = i + 1;
i = __Vdly__i;

拆两半的意义:在没有边沿的周期里,eval_nba 直接返回 false,不进入时序逻辑。只有边沿来了才跑。


八、阻塞赋值 vs 非阻塞赋值 🔀

两种赋值在 Verilator 生成代码里的处理不同:

赋值类型
语义
位置
实现
<= 非阻塞
用旧值算,末尾统一更新
eval_nba __Vdly__
 临时变量
= 阻塞(组合)
立即生效
eval_ico
直接赋值
= 阻塞(时序)
立即生效
eval_nba
直接赋值

混合场景


   
   
   
   
    
   
   
   
   always @(posedge clk) begin
    a = b;      // 阻塞:立即写
    c = a;      // 看到新 a
    d <= e;     // 非阻塞:写到临时变量
    f <= d;     // 看到旧 d
end

生成代码:


   
   
   
   
    
   
   
   
   vlSelfRef.a = vlSelfRef.b;        // ① 阻塞:直接写
vlSelfRef.c = vlSelfRef.a;        // ② 读到新 a

__Vdly__d = vlSelfRef.e;          // ③ 非阻塞:临时变量
__Vdly__f = vlSelfRef.d;          // ④ 读到旧 d

vlSelfRef.d = __Vdly__d;          // 末尾统一提交
vlSelfRef.f = __Vdly__f;

同一个 always 块里两种赋值共存,Verilator 用两套机制处理。


九、各 eval 区域的实例对照 🧪

9.1 eval_static


   
   
   
   
    
   
   
   
   localparam LUT [0:3] = '{1, 2, 4, 8};
assign
 y = LUT[idx];

   
   
   
   
    
   
   
   
   void Vmod___024root___eval_static(Vmod___024root* vlSelf) {
    vlSelf->__Vtable_LUT[0] = 1;
    vlSelf->__Vtable_LUT[1] = 2;
    vlSelf->__Vtable_LUT[2] = 4;
    vlSelf->__Vtable_LUT[3] = 8;
}
  • • 只在首次 eval() 之前执行一次
  • • 处理 localparam、静态查找表、常量数组
  • • 后续所有 eval() 都不再调用
  • • 生成代码位于 *__Slow.cpp 文件

9.2 eval_initial


   
   
   
   
    
   
   
   
   initial begin
    rst = 1;
    #10 rst = 0;
end

   
   
   
   
    
   
   
   
   void Vmod___024root___eval_initial(Vmod___024root* vlSelf) {
    vlSelf->rst = 1;
    vlSelf->__Vtiming_initial_coroutine =
        Vmod___024root___eval_initial__TOP__Vtiming__0
(vlSelf);
}
  • • 时间零点执行一次
  • • 处理 initial 块里的顺序语句
  • • 带 #delay 时启动协程(需要 --timing)
  • • 不带 --timing 时无 #delay,直接执行赋值

9.3 eval_stl


   
   
   
   
    
   
   
   
   wire a, b, c;
assign
 a = ~b;
assign
 b = ~c;
assign
 c = ~a;

   
   
   
   
    
   
   
   
   bool Vmod___024root___eval_stl(Vmod___024root* vlSelf, bool firstIteration) {
    if
 (firstIteration) {
        vlSelf->a = 0; vlSelf->b = 0; vlSelf->c = 0;
    }
    vlSelf->a = ~vlSelf->b;
    vlSelf->b = ~vlSelf->c;
    vlSelf->c = ~vlSelf->a;
    return
 (vlSelf->a != __Vprevexpr_a) | (vlSelf->b != __Vprevexpr_b);
}
  • • 只在首次 eval() 时进入
  • • 迭代求解组合逻辑环路,直到信号稳定
  • • 超出 --converge-limit 时报错
  • • 普通设计里通常一轮收敛,几乎不占时间

9.4 eval_sample


   
   
   
   
    
   
   
   
   assert property (@(posedge clk) req |-> ##[1:3] ack);

   
   
   
   
    
   
   
   
   void Vmod___024root___eval_sample(Vmod___024root* vlSelf) {
    vlSelf->__Vsampled_req = vlSelf->req;
    vlSelf->__Vsampled_ack = vlSelf->ack;
    vlSelf->__Vsampled_clk = vlSelf->clk;
}
  • • 每次 eval() 都执行
  • • 在 Observed 区域之前采样,保证断言看到的是时间步开始的值
  • • 没有 SVA 断言时为空函数
  • • 采样值存在 __Vsampled_* 前缀的变量里

9.5 eval_ico


   
   
   
   
    
   
   
   
   input wire a, b;
wire
 sum = a ^ b;
wire
 cout = a & b;

   
   
   
   
    
   
   
   
   bool Vmod___024root___eval_ico(Vmod___024root* vlSelf, bool firstIteration) {
    vlSelf->__VicoTriggered[0] =
        ((vlSelf->b != vlSelf->__Vtrigprevexpr_b) << 1)
      | ((vlSelf->a != vlSelf->__Vtrigprevexpr_a) << 0);
    vlSelf->__Vtrigprevexpr_a = vlSelf->a;
    vlSelf->__Vtrigprevexpr_b = vlSelf->b;

    if
 (vlSelf->__VicoTriggered[0] & 0b11) {
        vlSelf->sum  = vlSelf->a ^ vlSelf->b;
        vlSelf->cout = vlSelf->a & vlSelf->b;
    }
    return
 0;
}
  • • 处理顶层输入驱动的组合逻辑
  • • 用 __VicoTriggered 位图记录哪些输入变了
  • • 只重算依赖变化输入的逻辑,其余保持
  • • 在五层嵌套循环之前独立执行

9.6 eval_act


   
   
   
   
    
   
   
   
   always @(posedge clk or negedge rst_n) begin
    q <= d;
end

   
   
   
   
    
   
   
   
   bool Vmod___024root___eval_act(Vmod___024root* vlSelf) {
    vlSelf->__VactTriggered[0] =
        (((~vlSelf->rst_n) & vlSelf->__Vtrigprevexpr_rst_n) << 1)
      | ((vlSelf->clk & ~vlSelf->__Vtrigprevexpr_clk) << 0);
    vlSelf->__Vtrigprevexpr_clk   = vlSelf->clk;
    vlSelf->__Vtrigprevexpr_rst_n = vlSelf->rst_n;

    Vmod___024root___trigger_orInto__act_vec_vec
(
        vlSelf->__VnbaTriggered, vlSelf->__VactTriggered);
    return
 0;
}
  • • 只做边沿检测,不执行逻辑
  • • 检测 posedge clk 和 negedge rst_n
  • • 结果累积到 __VnbaTriggered
  • • 函数本身恒返回 0

9.7 eval_inact


   
   
   
   
    
   
   
   
   always @(posedge clk) begin
    a <= b;
    #0 a = a | c;
end

   
   
   
   
    
   
   
   
   bool Vmod___024root___eval_inact(Vmod___024root* vlSelf) {
    if
 (vlSelf->__VinactTriggered & INACT_BIT) {
        vlSelf->a = vlSelf->a | vlSelf->c;
        vlSelf->__VinactTriggered &= ~INACT_BIT;
    }
    return
 0;
}
  • • 处理 #0 延迟的语句
  • • 在 Active 之后、NBA 之前执行
  • • 没有 #0 时为空函数
  • • 大部分设计用不到

9.8 eval_nba


   
   
   
   
    
   
   
   
   always @(posedge clk or negedge rst_n) begin
    if
 (!rst_n) q <= 0;
    else
        q <= d;
end

   
   
   
   
    
   
   
   
   bool Vmod___024root___eval_nba(Vmod___024root* vlSelf) {
    if
 (!Vmod___024root___trigger_anySet__act(vlSelf->__VnbaTriggered))
        return
 false;

    CData __Vdly__q = vlSelf->q;
    if
 (!vlSelf->rst_n) {
        __Vdly__q = 0;
    } else {
        __Vdly__q = vlSelf->d;
    }
    vlSelf->q = __Vdly__q;

    Vmod___024root___trigger_clear__act
(vlSelf->__VnbaTriggered);
    return
 true;
}
  • • 执行非阻塞赋值的时序逻辑
  • • 用 __Vdly__ 临时变量模拟 <= 语义
  • • 函数末尾统一提交到实际寄存器
  • • 返回 true 表示有工作,触发外层循环

9.9 eval_obs


   
   
   
   
    
   
   
   
   assert property (@(posedge clk) req |-> ##1 ack)
    else
 $error("ack missing");

   
   
   
   
    
   
   
   
   bool Vmod___024root___eval_obs(Vmod___024root* vlSelf) {
    if
 (vlSelf->__Vsampled_req && !vlSelf->__Vassert_pending) {
        vlSelf->__Vassert_pending = 1;
    }
    if
 (vlSelf->__Vassert_pending && vlSelf->__Vsampled_ack) {
        vlSelf->__Vassert_pending = 0;
    } else if (vlSelf->__Vassert_pending && vlSelf->__Vassert_timeout) {
        VL_WRITEF
("assertion failed: ack missing\n");
        vlSelf->__Vassert_pending = 0;
    }
    return
 0;
}
  • • 执行 SVA 断言检查
  • • 在 NBA 之后、Reactive 之前
  • • 没有断言时为空函数
  • • 断言失败时打印错误信息

9.10 eval_react


   
   
   
   
    
   
   
   
   program test_prog(input clk);
    initial
 begin
        @(posedge clk);
        $display
("program executed");
    end

endprogram

   
   
   
   
    
   
   
   
   bool Vmod___024root___eval_react(Vmod___024root* vlSelf) {
    if
 (vlSelf->__VreactTriggered & PROG_BIT) {
        VL_WRITEF
("program executed\n");
        vlSelf->__VreactTriggered &= ~PROG_BIT;
    }
    return
 0;
}
  • • 执行 program 块和 clocking 块
  • • 最外层循环
  • • 没有 program 时为空函数
  • • 普通 RTL 设计不会用到

9.11 eval_postponed


   
   
   
   
    
   
   
   
   initial begin
    $monitor
("time=%0t q=%b", $time, q);
    $strobe
("q=%b", q);
end

   
   
   
   
    
   
   
   
   void Vmod___024root___eval_postponed(Vmod___024root* vlSelf) {
    if
 (vlSelf->__Vmonitor_enabled) {
        VL_WRITEF
("time=%0t q=%b\n", vlSelf->__Vtime, vlSelf->q);
    }
}
  • • 时间步末尾只执行一次
  • • 处理 $monitor、$strobe 等输出
  • • 此时所有信号已稳定,打印的是最终值
  • • 没有相关系统任务时为空函数

9.12 eval_final


   
   
   
   
    
   
   
   
   final begin
    $display
("simulation ended, total=%0d", counter);
end

   
   
   
   
    
   
   
   
   void Vmod___024root___eval_final(Vmod___024root* vlSelf) {
    VL_WRITEF
("simulation ended, total=%0d\n", vlSelf->counter);
}
  • • 仿真结束时执行
  • • 处理 final 块
  • • 只跑一次,不参与 eval() 循环
  • • 没有 final 时为空函数

十、两套调度模型 🆚

Verilator 的调度实现经历了一次重要重构。

4.x:每个模块生成完整调度


   
   
   
   
    
   
   
   
   void Vfp_adder::eval() {
    eval_static
();
    eval_ico
();
    do
 {
        eval_act
();
        eval_nba
();
    } while (还有触发);
    eval_end
();
}

每个模块都有一份完整的调度代码——冗余严重,大设计里生成代码体积庞大。

5.x:统一调度框架


   
   
   
   
    
   
   
   
   void Vfp_adder::eval_step() {
    m_evalLoop.eval();     // 通用
}

所有模块共用 VlEvalLoop——通过虚函数回调到具体区域实现。

改进效果

项
4.x
5.x
调度代码量
每模块一份
全设计一份
虚函数开销
无
每区域一次
生成代码体积
大
小
可维护性
差
好

代价:虚函数调用开销。但因为每区域调用次数少,实际影响可忽略。


十一、性能特征 ⚖️

eval 调用的开销来源

操作
周期
虚函数分发
~5
触发位检查
~3
实际逻辑
不定
清空触发位
~2
固定开销 ~10 周期

纯 RTL 设计的实际路径

对于没有断言、没有 #delay 的纯 RTL,每次 eval() 实际只走三个区域:

  • • evalIco:输入组合逻辑
  • • evalAct:边沿检测
  • • evalNba:时序更新

其余区域是空函数——调用它们只花虚函数分发的开销。

多区域切换的代价

当设计里混用 always @(*) 和 always @(posedge clk) 时,evalIco 和 evalNba 会分别被触发:

多一次外层迭代——因为 NBA 更新可能触发新的 Active 逻辑。


十二、总结 🎯

Verilator 的 eval 实现是一套精密的调度系统,核心要点:

  • • 分层事件队列:7 个区域对应 IEEE 1800 标准,五层嵌套循环保证收敛
  • • 调度框架复用:VlEvalLoop 是一份通用代码,各模块通过虚函数实现具体区域
  • • 边沿检测在 eval_act:clk & ~clk_prev 检测上升沿,__VactTriggered 累积到 __VnbaTriggered
  • • 时序逻辑在 eval_nba:nba_sequent 真正更新寄存器,用 __Vdly__ 模拟非阻塞赋值
  • • 组合逻辑在 eval_ico:always @(*) 的边沿检测和逻辑体都在这里
  • • 两套调度模型:4.x 每模块生成完整调度,5.x 用 VlEvalLoop 统一

理解这套机制,能帮助阅读 Verilator 生成的代码、分析仿真性能瓶颈,以及判断哪些区域被实际使用——对纯 RTL 设计,绝大多数区域是空壳,真正跑的是 eval_ico、eval_act、eval_nba 三个。

 


【声明】内容源于网络
0
0
ai算法芯片与系统
长期关注ai领域,算法,芯片,软件(系统,框架,编译器,算子库)等联合设计
内容 228
粉丝 0
ai算法芯片与系统 长期关注ai领域,算法,芯片,软件(系统,框架,编译器,算子库)等联合设计
总阅读6.0k
粉丝0
内容228