Icarus Verilog 的 vvp 代码生成器(-tvvp):从 Verilog 编译到虚拟处理器指令并驱动仿真运行

原创2026-10-03 12:16:171,904 阅读
文章标签:编译器编程语言硬件开发

Icarus Verilog 的 vvp 代码生成器(-tvvp):从 Verilog 编译到虚拟处理器指令并驱动仿真运行

vvp 是 Icarus Verilog 默认且最常用的代码生成目标(target),负责把经编译器 elaborat 之后的网表与行为代码翻译成一种面向"Verilog 虚拟处理器"的类汇编中间格式,再由同名的 vvp 运行时解释执行,从而完成仿真。本文以 Documentation/targets/tgt-vvp.rst 为主线,结合 tgt-vvp/vvp.c 等源码,讲解如何用 -tvvp 生成可执行 .vvp 文件、逐行读懂其内容、用 vvp 运行仿真,并深入理解该中间格式的组件模型、语句体系、执行指令与事件调度原理。

vvp target:Icarus Verilog 的主仿真引擎

Icarus Verilog 的编译流程分为两阶段:前端把 Verilog 源文件解析并 elaborat 成中间设计(ivl_design_t),随后把该设计交给某个代码生成器(target)处理。target 由 -t 命令行选项选择,仓库内置的 target 列表见 Documentation/targets/index.rst。其中 vvp target 生成的是面向 vvp 运行时的代码,由于它是主仿真引擎,因此也是日常使用最频繁的目标。

-tvvp 甚至可以省略:当你只给出 -o 输出文件名而没有显式指定 -t 时,Icarus Verilog 默认就使用 vvp target。这意味着绝大多数 iverilog -o xxx.vvp ... 的用法,本质上都在走这条代码生成路径。

生成命令与最小示例

生成 vvp 可执行文件的典型命令(两条等价):

%  iverilog -o top.vvp -s top hello_world.v
%  iverilog -o top.vvp -tvvp -s top hello_world.v

其中:

  • -o top.vvp 指定输出文件路径(对应 tgt-vvp/vvp.c 中 target_design() 通过 ivl_design_flag(des, "-o") 读取的路径);
  • -s top 指定顶层模块(root scope),top.vvp 中所有 .scope 结构均以该顶层为根展开;
  • -tvvp 显式选择 vvp 代码生成器,省略时同样生效。

示例源文件 hello_world.v:

module top;
    initial $display("Hello World!");
endmodule

逐行剖析生成的 top.vvp

对上面的示例,top.vvp 内容大致如下(具体地址、路径会随构建环境变化):

#! /usr/local/bin/vvp
:ivl_version "13.0 (devel)" "(s20221226-119-g8cb2e1a05-dirty)";
:ivl_delay_selection "TYPICAL";
:vpi_time_precision + 0;
:vpi_module "/usr/local/lib/ivl/system.vpi";
:vpi_module "/usr/local/lib/ivl/vhdl_sys.vpi";
:vpi_module "/usr/local/lib/ivl/vhdl_textio.vpi";
:vpi_module "/usr/local/lib/ivl/v2005_math.vpi";
:vpi_module "/usr/local/lib/ivl/va_math.vpi";
S_0x563c3c5d1540 .scope module, "top" "top" 2 1;
 .timescale 0 0;
    .scope S_0x563c3c5d1540;
T_0 ;
    %vpi_call 2 2 "$display", "Hello World!" {0 0 0};
    %end;
    .thread T_0;
# The file index is used to find the file name in the following table.
:file_names 3;
    "N/A";
    "<interactive>";
    "hello_world.v";

逐部分解读:

  • Shebang 行:第一行 #! /usr/local/bin/vvp 是 shebang。文件被直接执行时,内核会用 vvp 来解释该文件。tgt-vvp/vvp.c 的 draw_execute_header() 负责写出这一行:它读取设计标志 VVP_EXECUTABLE 作为解释器路径,可拼接 VVP_EXTRA_ARGS 附加参数,并在非 MinGW 平台对该输出文件执行 fchmod(..., 0755) 赋予可执行权限。

  • # 注释:所有以 # 开头的内容是注释,因此 shebang 行本身也天然符合这一规则。

  • :ivl_version:记录生成该文件所用的 Icarus Verilog 版本(主版本号 + 开发标签),由 draw_execute_header() 用 VERSION 与 VERSION_TAG 宏写出,便于排查版本不匹配问题。

  • :ivl_delay_selection:延迟选择策略,取值为 min/typical/max 之一(此处为 TYPICAL),由 ivl_design_delay_sel() 得到。它决定模块路径延迟、门延迟等取哪种工艺角。

  • :vpi_time_precision:仿真时间精度。此处 + 0 表示每个仿真 tick 为 1 秒量级;由于源文件未显式指定精度,编译器使用了默认值。在 tgt-vvp/vvp.c 的 target_design() 中,该值由 ivl_design_time_precision() 取得,负数时会写成 - 号(例如 - 9 即 1 纳秒)。

  • 多个 :vpi_module:声明 vvp 启动时需要动态加载的 VPI 模块及其加载顺序(system.vpi、vhdl_sys.vpi、vhdl_textio.vpi、v2005_math.vpi、va_math.vpi 等)。它们来自设计标志 VPI_MODULE_LIST,在 draw_module_declarations() 中按逗号分隔逐一展开成 :vpi_module "..." 行。

  • .scope 与 .timescale:创建名为 top 的模块级作用域,并设置该作用域的 timescale。注意第一行 .scope module, "top" "top" 2 1 中的 2 1 对应源文件索引与行号;S_0x... 是该作用域在文件内的符号标签。

  • 线程 T_0 与两条指令:

    • %vpi_call 2 2 "$display", "Hello World!" {0 0 0}; —— 调用 VPI 系统任务 $display,前两个数字是源文件索引与行号,花括号中是调用所需的各类型栈(vec4/real/str)使用量;
    • %end; —— 终止当前线程,仿真随之结束。
  • :file_names 文件表:文件末尾维护一张字符串表,供前面的文件索引引用("N/A"、"<interactive>"、"hello_world.v")。target_design() 通过 ivl_file_table_size() 与 ivl_file_table_item() 写出该表。

运行 .vvp 文件

生成之后有两种运行方式,效果相同:

%  ./top.vvp
%  vvp top.vvp

第一种依赖 shebang(前提是文件保留可执行权限),第二种直接调用解释器。vvp 本身是位于 vvp 目录的独立可执行程序,其命令行选项在 vvp/main.cc 中定义:

  • -h:打印帮助;
  • -i:交互模式(stdout 无缓冲);
  • -l file:日志文件,- 表示 stderr;
  • -M path:追加 VPI 模块搜索路径,-M - 清空该路径;
  • -m module:启动时额外加载指定 VPI 模块;
  • -n:非交互模式,$stop 视为 $finish;-N 同 -n 但退出码为 1;
  • -q:安静模式,抑制 MCD 位 0 上的输出;
  • -s:立即执行一次 $stop;
  • -v:冗长进度信息;
  • -V:打印版本信息。

vvp 中间格式:类汇编的"虚拟处理器"语言

vvp 引擎的输入是"不像汇编语言但可读"的中间代码,设计目标就是由编译器机器化生成,因此语法对人类并不特别友好,但足够结构化。完整语法约定见 Documentation/developer/guide/vvp/vvp.rst。

通用格式

源文件是一系列语句,每条语句由可选的标签、操作码和随操作码而定的操作数组成,一律以分号 ; 结束。分号同时也是注释起始符:

Label .functor and, 0x5a, x, y  ; This is a comment.

语句可跨多行,只要续行的第一列不出现文本(标签必须从第一列开始)。

Header 语句

在所有非注释代码之前,允许若干左对齐关键字开头的 header 语句,用于把编译器计算的全局参数传给运行时:

  • :module "name" ; —— 声明 vvp 在编译程序前应加载的 VPI 模块:编译器在标准 VPI_MODULE_PATH 中查找名为 name.vpi 的文件并动态加载;
  • :vpi_time_precision [+|-]<value>; —— 指定单个仿真 tick 的时间精度,值以 10 的幂表示(+0 为 1 秒,-9 为 1 纳秒),主要用于显示与 VPI 场景,引擎本身不关心单位;缺省视为 +0。

标签、符号与数字

  • 标签与符号由 a-z A-Z 0-9 . $ _ < > 组成,不能以数字或 . 开头(以便与关键字、数字区分);
  • 标签必须从第一列开始,这是词法分析器区分标签与符号的依据;操作码必须是关键字;
  • 符号是对标签的引用,允许先使用后声明,但最终必须声明;符号引用 functor 时指向其输出的 vvp_ipoint_t;向量 functor 的符号指向第一个 functor,可用 [] 访问向量中其他成员;
  • 特殊符号 C<0>、C<1>、C<x>、C<z> 在 functor 输入位置表示给定值的常量驱动;
  • 十进制数字标记为 64 位无符号,某些上下文会进一步限制位数。

组件模型:nexus、functor 网与符号命名

vvp 的 net 结构(门、信号、解析器组成的网络)围绕 nexus(ivl_nexus_t)组织,每个 nexus 代表一个已解析的值。设计命名规范见 Documentation/developer/guide/tgt-vvp/tgt-vvp.rst:

  • wires 与 regs:网络和变量命名为 V_<full-name>,其中 <full-name> 是信号的完整层次名;
  • 逻辑器件:and、or、buf、bufz 等逻辑器件命名为 L_<full-name>,符号挂在器件输出所连接的 functor 上。

每个 nexus 的输入由一个驱动源供给;若设计中该 nexus 有多个驱动源,这些驱动先汇入一个 resolver(或 resolver 树)形成单一输出,再馈送给其他门的输入或 .net 对象。这种"单一驱动 → 多消费"的结构正是门级仿真的数据流骨架。

语句类型:从结构网表到线程

vvp 源文件由若干类语句构成,每一类对应 Verilog 设计中的一个侧面。以下按类别列出核心语法(完整细节见 Documentation/developer/guide/vvp/vvp.rst)。

作用域与参数(.scope / .param)

<label> .scope <type>, <name> <type-name> <file> <lineno> ;
<label> .scope <type>, <name> <type-name> <file> <lineno>, <def-file> <def-lineno> <is-cell>, <parent> ;

<type> 取值包括 module、autofunction、function、autotask、task、begin、fork、autobegin、autofork、generate。<name> 是实例名,<type-name> 是类型名(模块/类作用域下是定义名而非实例名),<file>/<lineno> 是实例化位置,<def-file>/<def-lineno> 是定义位置,<is-cell> 仅对模块实例有意义(celltype 时为真)。短形式仅用于根作用域。

参数用 .param/str(字符串)、.param/l(逻辑向量,可为有符号,有符号值以 + 前缀标注,注意是 2 的补码表示而非"正数"含义)、.param/r(实数)声明,同时带标签以便作为 VPI 对象引用:

P_123 .param/str "hello", "Hello, World.";

functor:结构仿真的基本单元(.functor)

functor 是仿真的基本结构单元,包含一个类型(即真值表)和最多 4 个输入,必须带标签:

<label> .functor <type>, symbol_list ;
<label> .functor <type> [<drive0> <drive1>], symbol_list ;
  • symbol_list 是最多 4 个其他 functor 输出的标签,未连接的输入用 C<?> 常量符号;
  • 所有输入与输出的初始值均为 x;若输入用 C<?> 常量且使输出非 x,会在运行时开始时创建传播事件;
  • functor 忽略输入强度,输出带固定的 drive0/drive1 强度,因此强度信息通常在经过 functor 时丢失;
  • 大多数核心门类型(and/or/buf 等)内建真值表,直接写类型名即可;
  • 超过 4 输入的门(如 8 输入 and)级联展开为多个 functor,例如 and gate(out, i1..i8) 拆成 gate.0 .functor and, i1,i2,i3,i4;、gate.1 .functor and, i5,i6,i7,i8;、gate .functor and, gate.0, gate.1;。

真值表本身用 2 位编码表示 4 值逻辑:1'b0→00、1'b1→01、1'bx→10、1'bz→11,一张完整 4 输入真值表为 64 个 8 位字节(每个字节容纳 4 个 2 位输出)。因为表比实现逻辑的代码还小,查表是 functor 求值的首选方式。

DFF、Latch 与 UDP

综合后常见的触发器与锁存器有专门语句:

<label> .dff/p <width> <d>, <clk>, <ce>;
<label> .dff/n <width> <d>, <clk>, <ce>;
<label> .dff/p/aclr <width> <d>, <clk>, <ce>, <async-input>;
<label> .dff/p/aset <width> <d>, <clk>, <ce>, <async-input>[, <set-value>];

/p 为正沿触发、/n 为负沿触发;<clk> 与 <ce> 是端口 1/2 上的单比特使能;端口 0 可以是任意数据类型;aclr/aset 实现带异步清零/置位,异步输入上升沿时强制输出传播并暂时禁用时钟。

锁存器:

<label> .latch <width> <d>, <en>;

<en> 为逻辑 1 时把端口 0 的数据传到输出。

UDP 支持组合与时序两种形态:组合 UDP 的每一行是输入字符序列加输出状态字符;时序 UDP 额外在行首带当前状态字符,且最多允许一个输入位置使用边沿描述(如 r=0→1、f=1→0、*=任意变化等)。定义与实例化语法:

<type> .udp/comb "<name>", <number>, "<row0>", "<row1>", ... ;
<type> .udp/sequ "<name>", <number>, <init>, "<row0>", "<row1>", ... ;
<label> .udp  <type>, <symbol_list> ;

<init> 必须为 0、1 或 2(2 表示 1'bx)。若没有任何行匹配,输出变为 1'bx。

变量、网络与数组(.var / .net / .array)

变量是行为代码可写的位向量,无结构输入,输出按常规方式传播进 functor 网:

<label> .var   "name", <msb> <lsb>;   Unsigned logic variable
<label> .var/s "name", <msb> <lsb>;   Signed logic variable
<label> .var/2u "name", <msb> <lsb>;  Unsigned bool/bit variable
<label> .var/2s "name", <msb> <lsb>;  Signed bool/bit variable
<label> .var/real "name", <msb>, <lsb>; real variable
<label> .var/i "name", <msb>, <lsb>;  vpiIntegerVar variable
<label> .var/str "name";              vpiStringVar variable

"name" 是声明时的基名,供 VPI 代码访问。宽度由 msb/lsb 索引决定(相等即 1 位;大于 1 位时创建连续 functor 数组,标签地址指向最低位)。变量由 %set/%assign 指令经端口 0 赋值;%cassign 写端口 1 进入连续赋值模式(端口 0 写入被忽略),直到端口 3 收到 long(1) 命令解除;%force 写端口 2 进入 force 模式,直到端口 3 收到 long(2)。

网络与变量类似但线程不能直接写(force 除外),VPI 类型码也不同:

<label> .net      "name", <msb>, <lsb>, <symbol>;
<label> .net/s    "name", <msb>, <lsb>, <symbol>;
<label> .net8     "name", <msb>, <lsb>, <symbol>;
<label> .net8/s   "name", <msb>, <lsb>, <symbol>;
<label> .net/real "name", <msb>, <lsb>, <symbol>;

.net 输入复制到输出(类似二极管),作用在于挂接 VPI 与事件追踪设施;.net8 保留经过的强度值,.net 则把强度归约为 4 值逻辑,.net8 仅在确实可能出现强度信息时使用。行为代码通过 .net 标签而非通用 functor 符号访问网络对象。

数组把同名 .var/.net 收集为字:

<label> .array "name", <last> <first> ;
<label> .array "name", <src> ;   // 别名形式,尺寸与存储取自 <src>

事件、解析器与结构运算

线程需要与 functor 网同步或异步交互,.event 对象负责唤醒等待中的线程:

<label> .event <type>, <symbols_list>;
<label> .event "name";            // 命名事件,由 %set 触发
<label> .event/or <symbols_list>; // 事件或:任一输入触发即可

<type> 可取 posedge、negedge、edge 或 anyedge;%wait 把线程挂到事件的敏感列表上。

其余结构语句包括:

  • Resolver:<label> .resolv tri|tri0|tri1, <symbols_list>;,强度感知的 4 输入 functor,输出为带强度的 vvp_vector8_t;
  • Part select:.part、.part/pv(向输出的一部分写)、.part/v(基址可动态变化,.part/v.s 按有符号处理);
  • Concatenation:.concat [W X Y Z], <symbols_list>;,宽度按 LSB→MSB 排列,输出初始为全 bx;
  • Repeat:.repeat <wid>, <rept count>, <symbol>;,把输入重复固定次数拼成输出;
  • Substitute:.substitute <wid>, <soff> <swid>, <symbol>, <symbol>;,综合产物,等价于 foo=<a>; foo[n]=<s>; 的合写;
  • Reduction / Expansion:.reduce/and|or|xor|nand|nor|xnor 把任意宽向量归约成单比特;.expand/s <wid>, <symbol> 做符号扩展;
  • 结构算术:.arith/sub|sum|mult|div|mod <wid>, <A>, <B>;,支持 .s(有符号)与 .r(实数)后缀;
  • 结构比较:.cmp/eeq|nee|eq|ne|ge|gt <wid>, <A>, <B>; 及 .ge.s/.gt.s 有符号变体,输出单比特;.cmp/weq|wne 为通配比较;
  • 结构移位:.shift/l|r <wid>, <data symbol>, <shift symbol>;;
  • 结构函数调用:.ufunc/real|vec4|e <flabel>, <wid>, [<isymbols> ( <psymbols> )] <ssymbol>;,输入网变化(或触发事件)时调用用户函数,先向端口变量赋值再跳转到函数代码;
  • 模块路径延迟:.modpath <width> <input> , [ <src> (<delays> [? <condition>]) ] ;;
  • 延迟节点:.delay <width> ( <rise>, <fall>, <decay> ) <input> ;(固定延迟)或 .delay <width> <input>, <rise>, <fall>, <decay> ;(可变延迟),<width> 为 0 表示实数网络。

线程(.thread)与 VPI 调用

线程语句创建仿真的初始线程,对应源 Verilog 的 initial 和 always 块:

.thread <symbol> [, <flag>]

<flag> 目前支持 $push:让线程在启动时被推入调度器,使被推线程先于非推线程启动,用于解决 time-0 竞争。

  • 瞬时线程由 %fork 创建(作为 forking 线程的子线程压入子线程栈,一个线程只能有一个直接子线程),由 %join 回收;%join/detach <n> 用于实现 join_none/join_any。vvp 没有 %call 指令,函数调用就是 %fork/%join 加 %end 的组合;
  • 每个线程有两块本地存储:位内存(4 值位,可按任意长度条带寻址,地址 0–3 是固定的 0/1/x/z 常量值)与字内存(带标签的 64 位无符号/有符号整数或双精度浮点字);
  • disable 通过作用域定位线程:所有线程都属于某个作用域(静态线程在 .scope 之后用 .thread 加入;瞬时线程由 %fork 的操作数指定作用域并原子加入),%disable <scope-label> 终止该作用域内所有线程及其子线程,%disable/flow 只禁用线程层级中最近的单个线程(用于 SystemVerilog return/continue/break),%disable/fork 终止当前线程所有分离子线程。

VPI 任务/函数调用指令:

%vpi_call <file-index> <lineno> <name>, <args>... ;
%vpi_call/w <file-index> <lineno> <name>, <args>... ;
%vpi_call/i <file-index> <lineno> <name>, <args>... ;
%vpi_func <file-index> <lineno> <name>, <args>... ;
%vpi_func/r <file-index> <lineno> <name>, <args>... ;
%vpi_func/s <file-index> <lineno> <name>, <args>... ;

<file-index> 是文件名字符串表的索引,<lineno> 是源码行号;<name> 是系统任务/函数名(如 $display、$strobe),运行时与注册的系统任务/函数匹配;参数以逗号分隔,VPI 代码以 vpiHandle 方式获取。/w 在把系统函数当任务调用时给出警告并忽略返回值,/i 则静默忽略。另外还有 &A<>(变量数组字引用)与 &PV<>(信号部分引用)两种特殊参数形式,支持从信号或线程空间动态取基址。

执行指令(Opcodes):虚拟处理器的指令集

所有可执行指令以 % 开头,统一编入单个地址空间,操作数不超过 3 个;指令标签即指令地址,可作为 %jmp 的目标或线程起点。完整指令清单见 Documentation/developer/guide/vvp/opcodes.rst。

每个线程有最多 16 个标志位(flags),作为布尔运算(如比较)的结果目标,以及测试/分支指令的输入。

关键指令族:

  • 算术:%add/%addi/%sub/%mul/%div/%mod/%pow 及 i(立即数)、/s(有符号)、/wr(实数)变体;二进制算术一般取三个参数(左操作数、右操作数、基址),左操作数被结果替换,结果宽度与操作数相同。实数运算使用栈式(RPN)模型;
  • 比较与分支:%cmp/u、%cmp/s、%cmp/e、%cmp/ne、%cmpi/*、%cmp/we、%cmp/wne(通配比较,用于 ==?/!=?)、%cmp/z/%cmp/x(casez/casex,z/x 视为 don't care)、%cmp/str(字符串比较)。结果写入标志位:位 4=eq、位 5=lt、位 6=eeq(===),组合这三个位可实现全部 Verilog 比较运算符;%jmp <code-label> 无条件跳转,%jmp/[01xz] <code-label>, <flag> 按标志值条件跳转;
  • 赋值:%assign/v0、%assign/vec4、%assign/wr(非阻塞赋值,/d 变体从索引寄存器取延迟,/e 变体用线程事件控制寄存器)、%set/v(阻塞赋值,值立即生效)、%cassign/*(连续赋值,写变量端口 1)、%force/*(force 赋值,写端口 2)、%release/*(解除 force,/net 与 /reg 语义不同:net 释放后未强制值立即传播,reg 保持强制值直到有新值传播);
  • 栈操作:%pushi/vec4 <vala>, <valb>, <wid>(立即向量,a/b 位编码 0/1/x/z)、%pushi/real、%pushi/str、%pop/*、%dup/*、%load/*、%store/*、%ret/*/%retload/*(函数参数与返回值);%store/vec4a、%load/vec4a 处理数组,且会在标志位 4 置位时抑制实际读写(用于无效地址检测);
  • 位与向量操作:%and/%or/%xor/%xnor/%nand/%nor 及 /r 归约形式、%inv、%blend(实现 'bx ? <a> : <b>)、%concat/%concati、%part/s|u、%pad/s|u(符号/零扩展)、%replicate、%split/vec4、%shiftl/%shiftr[/s];
  • 类型转换:%cast2、%cast/vec4/str、%cast/vec2/dar、%cast/vec4/dar(动态数组↔向量,宽度不匹配时报错停机)、%cvt/rv、%cvt/vr、%cvt/sr、%cvt/ur(实数↔整数/向量互转,可能损失精度);
  • 线程与流程:%fork/%join/%join/detach、%disable*、%delay <low>, <high>(两个 32 位分量构成 64 位延迟,%delay 0,0 让出处理器但在当前时间片恢复)、%delayx <idx>(延迟存于索引寄存器,支持运行时计算延迟)、%wait <functor-label>(挂起线程到 functor 敏感列表)、%wait/fork、%breakpoint(无条件进入交互调试器;运行时调试被编译掉时为 no-op)、%alloc/%free(自动作用域实例分配/释放)、%event/%event/nb(向事件对象发脉冲);
  • 事件控制:%evctl 族把事件与重复次数写入线程事件控制寄存器,供 %assign/*/e 使用;%evctl/c 清除事件控制信息,管理不当会触发断言;
  • 对象与 SystemVerilog 扩展:%new/cobj、%new/darray、%null、%delete/obj、%load/obj、%store/obj、%prop/*(类属性读写)、%store/prop/*、%test_nul(实现 <var>==null)、%set/dar/obj/*、%store/dar/*、%store/qf|qb/*(队列 push_front/push_back)、%qpop/*(队列弹出)、%substr/%substr/vec4(SystemVerilog 子串)、%putc/str/v;
  • 调试与杂项:%debug/thr(引擎调试辅助,不应由代码生成器产出)、%file_line <file> <line> <description>(向 stderr 输出 <file>:<line>: <description>,便于过程性追踪)、%flag_inv/%flag_mov/%flag_or/%flag_set/imm/%flag_get/vec4/%flag_set/vec4、索引寄存器操作 %ix/load、%ix/add、%ix/sub、%ix/mul、%ix/mov、%ix/vec4[/s]、%ix/getv[/s]。

事件调度模型:三类事件驱动仿真

functor 网与线程相互独立,通过三类事件在 vvp/schedule.cc 实现的调度器中运转(见 Documentation/developer/guide/vvp/vvp.rst):

  1. 传播事件(propagation event):functor 输出变化时由真值表查表产生新输出,若与旧值不同则创建传播事件;事件到期时把输出传播到所有相连输入,进而可能级联产生新事件。传播求值不借助线程。
  2. 赋值事件(assignment event):行为代码执行非阻塞赋值(<=,即 %assign)时创建,携带目标 functor 输入指针与值。它与传播事件不同的原因有二:不存在以该值为输出的 functor(这正是值进入 functor 网的途径);且允许行为代码生成任意长度的波形(Verilog 只允许非阻塞赋值这样做)。
  3. 线程调度事件(thread schedule event):指向某线程让其执行。线程由"程序计数器 + 私有存储"的虚拟处理器构成,可执行 %assign/%set/%load/%wait 与 functor 网交互。

事件队列当前实现为 skip list——带跳指针的排序单向链表,可跳过 delta-time 事件。线程与 functor 网通过 %set、%assign、%wait、%load 等指令通信:对线程而言,functor 网是一块可以通过特定访问指令戳弄的结构。

vvp 引擎的编译与执行流程

vvp 程序按四步运行(Documentation/developer/guide/vvp/vvp.rst):

  1. 初始化:清空数据结构、准备编译所需的表,由 vvp/compile.cc 的 compile_init() 完成,它依次调用各子模块的 <foo>_init();
  2. 编译:读取并编译输入文件,由 vvp/parse.y 驱动的解析器按规则调用各类 compile_*() 函数,建立符号表、分配并链接对象;
  3. 清理:compile_cleanup() 完成未决引用解析,释放符号表等编译期资源,压缩内存占用;
  4. 仿真:调用 schedule_simulate() 完成最终设置并启动事件队列,进入事件仿真循环。

这套"编译期 + 运行期"分离的设计意味着:iverilog 把 Verilog 翻译成 .vvp 中间代码,vvp 再把它编译为内部对象并仿真——与"源代码 → 汇编 → 处理器"的层次完全对应。

进阶:影响 vvp 代码生成的编译选项

除了 -tvvp、-o、-s,tgt-vvp/vvp.c 的 target_design() 还响应两个设计标志:

  • -pfileline=N:fileline 标志决定是否为过程性语句附带文件与行号信息(默认关闭)。非法(非数字、含多余字符、负值)会报 vvp.tgt error 并返回失败;
  • -pdebug=draw:debug_flags 中的 draw 关键字打开结构绘制调试(debug_draw),由 process_debug_string() 解析逗号分隔的调试标志。

另外,VVP_EXECUTABLE(shebang 解释器路径)与 VPI_MODULE_LIST(逗号分隔的 VPI 模块列表)这类标志通常由 iverilog 主程序依据配置自动填充,是理解 top.vvp 头部各 :vpi_module 行的实现入口。

总结

vvp target 是 Icarus Verilog 仿真的核心路径:iverilog -o top.vvp -s top hello_world.v 生成带 shebang、版本/精度/延迟策略头、VPI 模块表、作用域与线程指令的 .vvp 中间代码;./top.vvp 或 vvp top.vvp 加载并执行。中间格式以"functor 结构网 + 线程虚拟处理器 + 三类事件调度"为骨架,配以 .scope/.var/.net/.functor/.event 等结构语句与 % 前缀执行指令,完整覆盖 Verilog/SystemVerilog 的行为、结构、时序、VPI 与面向对象的仿真需求。想深入了解指令语义,可通读 Documentation/developer/guide/vvp/opcodes.rst;想研究生成器实现,则从 tgt-vvp/vvp.c 与 vvp/compile.cc 入手。

登录后查看全文
iverilog