Icarus Verilog 的 vvp 代码生成器(-tvvp):从 Verilog 编译到虚拟处理器指令并驱动仿真运行
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只禁用线程层级中最近的单个线程(用于 SystemVerilogreturn/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):
- 传播事件(propagation event):functor 输出变化时由真值表查表产生新输出,若与旧值不同则创建传播事件;事件到期时把输出传播到所有相连输入,进而可能级联产生新事件。传播求值不借助线程。
- 赋值事件(assignment event):行为代码执行非阻塞赋值(
<=,即%assign)时创建,携带目标 functor 输入指针与值。它与传播事件不同的原因有二:不存在以该值为输出的 functor(这正是值进入 functor 网的途径);且允许行为代码生成任意长度的波形(Verilog 只允许非阻塞赋值这样做)。 - 线程调度事件(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):
- 初始化:清空数据结构、准备编译所需的表,由 vvp/compile.cc 的
compile_init()完成,它依次调用各子模块的<foo>_init(); - 编译:读取并编译输入文件,由 vvp/parse.y 驱动的解析器按规则调用各类
compile_*()函数,建立符号表、分配并链接对象; - 清理:
compile_cleanup()完成未决引用解析,释放符号表等编译期资源,压缩内存占用; - 仿真:调用
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 入手。