Icarus Verilog VVP 仿真引擎深入解析:从 VVP 汇编语法到事件驱动的运行时实现

原创2026-10-03 18:15:101,722 阅读
文章标签:编译器编程语言硬件开发

Icarus Verilog VVP 仿真引擎深入解析:从 VVP 汇编语法到事件驱动的运行时实现

导读

VVP(Verilog Virtual Processor)是 Icarus Verilog 编译器的运行时仿真引擎,其输入不是 Verilog 源文件,而是由编译器(iverilog)生成的一种类汇编语言描述文件。本文以仓库中 Documentation/developer/guide/vvp/vvp.rst 为骨架,结合 vvp/ 目录下的真实源码,系统讲解 VVP 汇编的语句格式、各类结构语句(functor、DFF、UDP、变量、网线、事件、算术等)、可执行指令、真值表编码,以及"functor 结构网 + 线程 + 事件调度"三者的协作关系。读完本文,你将能够读懂 iverilog 生成的 .vvp 文件,理解 VVP 的编译与仿真生命周期,并能利用这些知识进行调试与二次开发。

1. VVP 是什么:面向机器的仿真汇编语言

VVP 仿真引擎的输入是类似于传统处理器汇编语言的源代码。它被设计为供其他工具(包括 Icarus Verilog 编译器)机器生成的代码,因此语法虽然可读,但并不追求对人类友好。

VVP 源码文件由一系列**语句(statements)**组成,每条语句可以包含:

  • 标签(label):符号,可选,部分操作码要求必须有;
  • 操作码(opcode):关键字(.functor、.scope 等结构关键字,或 %set、%assign 等指令);
  • 操作数(operands):取决于操作码的具体参数。

1.1 语句终止与注释

每条语句以分号 ; 结束。分号同时也是注释行的开始——分号之后的文本全部视为注释:

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

分号是必须的,无论其后是否有注释。

1.2 多行语句

语句可以跨越多行,只要续行的第一列没有文本(标签的首字符除外)。

1.3 词法层验证

从源码 vvp/lexor.lex 可以看到词法规则与文档的对应关系:

  • 左对齐的 <a href="https://link.gitcode.com/i/9f1502a5fe18cc8118260753a1557680" target="_blank">.$_a-zA-Z\\][.$_a-zA-Z\\0-9<>/]* 被识别为 T_LABEL([vvp/lexor.lex#L86-L89);
  • ".functor"、".scope"、".dff/p" 等以 . 开头的关键字被逐个识别为独立 token(vvp/lexor.lex#L130-L257);
  • 以 % 开头的 token 被识别为 T_INSTR(vvp/lexor.lex#L278-L281);
  • 分号规则 ";".* { return ';'; } 印证了"分号即注释起点"的语义(vvp/lexor.lex#L345)。

2. 头语句(Header Statements)

在正式代码之前,源码可以包含若干头语句,用于把编译器的参数或全局信息传递给 vvp 运行时。所有头语句以左对齐(顶格)的关键字开头。

2.1 加载 VPI 模块::vpi_module

:vpi_module "name" ;

该语句指定一个 vvp 在编译程序其余部分之前需要加载的 VPI 模块。编译器会在标准的 VPI_MODULE_PATH 中查找名为 name.vpi 的文件并尝试动态加载。从 vvp/main.cc#L80-L85 可以看到,运行时还支持通过命令行 -M path 追加模块搜索路径、-M - 清空路径、-m module 预加载模块。

2.2 时间精度::vpi_time_precision

:vpi_time_precision [+|-]<value> ;

该语句指定仿真时钟单次 tick 的时间精度,主要用于显示(display)和 VPI 目的——引擎本身并不关心单位,编译器会预先换算时间值。

<value> 以 10 的幂次表示单个仿真 tick 的秒数:

  • +0 表示 1 秒;
  • -9 表示 1 纳秒;
  • 如果省略该记录,精度默认为 +0。

2.3 其他头语句

词法器(vvp/lexor.lex#L78-L82)还识别其他头语句关键字::ivl_version、:ivl_delay_selection、:file_names。其中 :file_names 在 vvp/examples/hello.vvp 中真实出现,用于登记源文件名字符串表(供 %vpi_call 的 <file-index> 引用):

:file_names 2;
    "N/A";
    "<interactive>";

3. 标签与符号(Labels and Symbols)

标签与符号由以下字符构成:

a-z
A-Z
0-9
.$_<>

标签和符号不能以数字或 . 开头,以便与关键字和数字区分。

  • 标签(Label):语句开头的符号,且必须从第一文本列开始(左对齐)。词法器正是通过"左对齐"这一位置特征来区分标签与符号的;
  • 符号(Symbol):语句操作数中出现的名字,是对标签的引用。符号引用的标签不必先声明后使用,但最终必须被声明;
  • 符号引用 functor 时,代表该 functor 输出的 vvp_ipoint_t 指针(输入不能被符号化引用);
  • 若 functor 属于向量的一部分,则符号代表第一个 functor 的 vvp_ipoint_t,可用 [] 运算符引用向量中除第一个之外的 functor;
  • 特殊符号 C<0>、C<1>、C<x>、C<z> 作为 functor 输入时表示给定值的常量驱动器。

4. 数字(Numbers)

十进制数字 token 限制为 64 位且无符号。某些上下文可能会进一步约束数字大小。词法器同时接受 0x 十六进制数字(vvp/lexor.lex#L287-L289),并支持 1'b/'sb 形式的四值二进制向量字面量(vvp/lexor.lex#L102-L127)。

5. 作用域语句(Scope Statements)

作用域语句用于建立仿真的层次化命名空间。语法如下:

<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> 实例的基础名。对 module 是实例名;对 task/function 是任务或函数名
<type-name> 类型名。大多数作用域与 <name> 相同;对 module 和 class 作用域,它是定义的名字而非实例名
<file>、<lineno> 该作用域实例化的位置(对 module 即实例位置)
<def-file>、<def-lineno> 作用域定义处的源文件和行号(module 的定义位置而非实例位置)
<is-cell> 仅对 module 实例有意义:非零表示 celltype 而非普通 module
<parent> 父作用域引用(仅长格式)

短格式仅用于根作用域(root scopes)。在 vvp/examples/hello.vvp 中可以看到真实例子:

main	.scope module, "main" "main" 0 0;

6. 参数语句(Parameter Statements)

参数是作用域内的具名常量,具有类型、值以及标签,以便作为 VPI 对象被引用。语法:

<label> .param/str <name> <local-flag> <file-idx> <lineno>, <value>;
<label> .param/l <name> <local-flag> <file-idx> <lineno>, <value>;
<label> .param/r <name> <local-flag> <file-idx> <lineno>, <value>;

<name> 以字符串命名参数,被放入当前作用域作为 vpiParameter 对象。.param 后缀指定参数类型:

  • .param/str —— 参数具有字符串值;
  • .param/l —— 参数具有逻辑向量值;
  • .param/r —— 参数具有 real(实数)值。

例如:

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

布尔/逻辑值还可以带符号:若为有符号,值以 + 字符开头(注意:值是 2 的补码,+ 只表示"有符号",不代表"正数")。

7. Functor 语句(Functor Statements)

functor 是仿真的基本结构单元:它包含一个类型(以真值表形式给出)和至多四个输入。functor 语句必须有标签。

7.1 语法

<label> .functor <type>, symbol_list ;
<label> .functor <type> [<drive0> <drive1>], symbol_list ;

symbol_list 是 4 个其他 functor 标签的名字,将本语句 functor 的输入连接到其他 functor 的输出。未连接的输入用 C<?> 符号表示。<type> 选择用于实现该 functor 的真值查找表——大多数核心门类型都内置了真值表。

7.2 运行时语义

  • 所有输入和输出的初始值都是 x,其他值作为运行时行为传递;
  • 若输入带有 C<?> 符号,输入被初始化为指定 bit 值;如果这导致输出不再是 x,则创建一个传播事件,在运行时开始阶段执行;
  • functor 忽略输入的强度(strength),输出具有固定的 drive0 与 drive1 强度——强度信息经过 functor 时通常会丢失;
  • 仿真中几乎所有的结构特征都可以用 functor 表示:它执行"把至多 4 个输入合并为 1 个输出"这一最基础的任务。

7.3 MUXZ 真值表示例

 Q | A  B  S  n/a
 --+-------------
 A | *  *  0
 B | *  *  1

7.4 布尔门如何编译成 functor

Documentation/developer/guide/vvp/vvp.rst 给出了 Verilog 门到 VVP 汇编的映射示例。至多 4 输入的逻辑门可以用单个 functor 实现:

and gate (out, i1, i2, i3);

编译为:

gate	.functor and, i1, i2, i3;

超过 4 个输入的门需要级联 functor:

and gate (out, i1, i2, i3, i4, i5, i6, i7, i8);

编译为:

gate.0	.functor and, i1, i2, i3, i4;
gate.1	.functor and, i5, i6, i7, i8;
gate	.functor and, gate.0, gate.1;

8. DFF 与 Latch 语句

Verilog 语言本身没有 DFF 原语,但综合工具会生成 DFF 器件,最好用通用器件来仿真,因此 VVP 提供 DFF 语句:

<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/n/aclr <width> <d>, <clk>, <ce>, <async-input>;
<label> .dff/p/aset <width> <d>, <clk>, <ce>, <async-input>[, <set-value>];
<label> .dff/n/aset <width> <d>, <clk>, <ce>, <async-input>[, <set-value>];
  • /p 变体仿真上升沿触发触发器,/n 变体仿真下降沿触发触发器;
  • 生成的 functor 通常在 <clk> 指定沿上同步工作,<ce> 使能高有效;
  • <clk> 与 <ce> 是 port 1、port 2 上的单 bit 向量(或标量);port-0 可以是任意类型的数据;
  • 器件在时钟装载时把输入转移到输出;
  • <async-input> 是特殊异步输入:上升沿导致器件 clear/set、强制输出传播、并在异步输入撤销前禁用时钟——即实现带异步 clr/set 的 DFF。

同样地,综合会生成 D 型锁存器,因此有 LATCH 语句:

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

<en> 是 port-1 上的单 bit 向量(或标量),port-0 可以是任意类型数据。当 <en> 为逻辑 1 时,器件把输入转移到输出。

在 vvp/dff.cc、vvp/latch.cc 中可以找到这两种器件的实际实现,词法器也在 vvp/lexor.lex#L180-L188 中为 .dff/*、.latch 关键字建立了对应 token。

9. UDP 语句(User Defined Primitives)

UDP 语句要么定义一个用户自定义原语,要么通过创建 UDP functor 实例化一个先前定义的 UDP。UDP functor 的输入个数与 UDP 定义所需一致。

9.1 组合型与时序型 UDP

  • 组合型(combinatorial)UDP:输出仅是其当前输入的函数;
  • 时序型(sequential)UDP:携带输出状态,可以对输入边沿做出响应。

UDP 的功能由表格定义:每行是描述输入状态或边沿以及新输出状态的字符串。

  • 组合型 UDP 每个输入需要一个字符,行尾再有一个字符表示输出状态;
  • 时序型 UDP 还需要一个额外字符表示当前状态,位于行首。

任何输入跳变或新状态至多匹配一行(或所有匹配必须给出相同输出状态)。若无行匹配,输出变为 1'bx。

输出状态可指定为 "0"、"1" 或 "x";时序型 UDP 还可使用 "-" 表示不变。

9.2 输入/当前状态字符集

"1": 1
"0": 0
"x": x
"b": 1, 0
"h": 1, x
"l": 0, x
"?": 1, 0, x

9.3 边沿规范

时序型 UDP 中至多一个输入状态规范可替换为边沿规范。有效边沿:

"*": (??)	"_": (?0)	"+": (?1)	"%": (?x)
"P": (0?)			"r": (01)	"Q": (0x)
"N": (1?)	"f": (10)			"M": (1x)
"B": (x?)	"F": (x0)	"R": (x1)

"n": (1?) | (?0)
"p": (0?) | (?1)

9.4 定义与实例化

组合型 UDP 定义:

<type> .udp/comb "<name>", <number>, "<row0>", "<row1>", ... ;

<type> 是标识该 UDP 的标签,<number> 是输入个数,"<name>" 用于公开标识。

时序型 UDP 需要额外的初始化值:

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

<init> 是所有实例的初始值(不支持为单个实例提供初始值),必须是 0、1 或 2(2 表示 1'bx)。

创建 UDP functor 实例:

<label> .udp <type>, <symbol_list> ;

<label> 标识 functor,<type> 是先前定义的 UDP 标签,<symbol_list> 是每个 UDP 输入对应的符号列表。UDP 的运行时实现位于 vvp/udp.cc。

10. 变量语句(Variable Statements)

变量(variable)是可以被行为代码写入(因此没有结构输入)的 bit 向量,其输出传播给 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 代码使用,变量被放入当前作用域;
  • 宽度由最高位与最低位索引定义;若两索引相等(通常为 0),向量宽度为 1;若宽度大于 1,则创建连续的 functor 数组,标签值是最低有效位的地址;
  • 变量不接收输入(其值由赋值事件行为化地设置),但有输出,并以常规方式传播到 functor 网络。

10.1 变量的赋值通道

变量通过过程代码的 %set 与 %assign 赋值:这些指令把值写到 port-0 输入,然后值被保持。

  • %cassign/v 语句像 %set/v 一样工作,但写入变量的 port-1:对 port-1 的写激活连续赋值模式,此时 port-0 的值被忽略;该模式持续到 long(1) 被写入 port-3(命令端口);
  • %force/v 语句写入 port-2 以进入 force 模式:它覆盖连续赋值模式,直到 long(2) 写入 port-3 禁用 force 模式。

10.2 reg 变量编译示例

Verilog 的 reg a; 编译为:

a	.var "a", 0, 0;

11. 网线语句(Net Statements)

net 与 variable 类似,区别在于线程不能写入 net(除非使用 force),并且 net 被赋予不同的 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>;
  • 与 .var 类似,.net 创建具有给定基础名和维度的 VPI 对象;<symbol> 是馈入 net 向量的 functor,vpiHandle 持有对该 functor 的引用;
  • .net 的输入被复制到输出,从这个意义上它像一个"二极管",目的是承载各种 VPI 与事件挂件;
  • .net 与 .net8 都是向量类型,都可能表示 wire,但 .net8 保留经过它的强度值,而 .net 把强度值降为 4 值逻辑;.net8 只应在强度信息确实可能存在时使用;
  • <label> 是必需的,用于定位其代表的 net 对象;该标签不映射到 functor,因此只有知道要访问 .net 的引用(特别是行为代码的 %load 与 %wait 指令)才能定位该符号。对 net 与 reg 对象的引用通过 .net 标签进行,而非一般的 functor 符号(指令内部仍存储 functor 指针)。

另外,.alias 语句不创建新节点,而是为已有节点创建 net 别名,处理一个 net 在不同作用域有不同名字的特殊情况。

12. 类型转换语句(Cast Statements)

有时需要把实数网线转换为 bit 网线,或反之。语法:

<label> .cast/int <width>, <symbol>;
<label> .cast/2 <width>, <symbol>;
<label> .cast/real <symbol>;
<label> .cast/real.s <symbol>;
  • .cast/int:输出 <label> 是 <width> bit 宽的 bit 网线,输入 <symbol> 应把 real 值送入该 functor;
  • .cast/real:输出是 real 值网线,输入应提供 bit 值;.cast/real.s 中这些 bit 被解释为有符号值。

13. 延时语句(Delay Statements)

延时节点是结构性的 net 延时节点,承载并管理传播延时。延时节点可以固定延时或可变延时:

  • 固定延时节点只有被延时的输入,延时量直接写在节点行上;
  • 可变延时节点有三个额外输入,接收用于延时的 rise、fall、decay 时间。
.delay <width> ( <rise>, <fall>, <decay> ) <input> ;
.delay <width> <input>, <rise>, <fall>, <decay> ;
  • 第一种形式:三个常量(64 位)数字作为初始延时,接受单个输入;
  • 第二种形式:4 个 net 输入,第一个是被延时值,其余是延时值;
  • <width> 指定输入 net 的 bit 宽度,宽度 0 表示 real 值 net。

14. 模块路径延时语句(Module Path Delay Statements)

模块路径延时从输入取得数据,再经过一组模块路径延时。每个可能的延时集的 <src> 是激活该延时的触发器:

.modpath <width> <input> , [ <src> (<delays> [? <condition>]) ] ;

<width> 指定输入 net 的 bit 宽度。

15. 数组索引语句(Array Index Statements)

变量可以汇集为数组。数组的各个 word 分别声明,再由该语句汇集:

<label> .array "name", <last> <first> ;

此语句之后的、具有相同 "name" 的 .var 或 .net 语句按顺序被收集为数组的 word。

另一种语法为已有数组创建别名,维度和存储取自 <src> 处的 .array:

<label> .array "name", <src> ;

16. 事件语句(Event Statements)

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

<label> .event <type>, <symbols_list>;
<label> .event "name";
  • 该语句声明一个 %wait 指令可以作为操作数的对象;线程执行 %wait 时,把自己放入事件的通知列表并挂起;<symbols_list> 是可能触发该事件的一组输入;
  • <type> 描述触发事件的条件:posedge、negedge、edge 或 anyedge;
  • 若类型是 "name" 字符串,则是命名事件:通过 %set 指令接收事件,而非来自 functor 的输出;
  • 事件有输入(除非是命名事件)时,至多 4 个符号指向 functor;事件检测任一输入上的相应边沿并在事件为真时发出信号。通常(Verilog 中)posedge/negedge 事件只监视单个 bit,因此生成代码只包含该 bit 的单个符号;但如果事件 OR 表达式中有多个同类型边沿事件,编译器可能把至多 4 个合并为一个事件。

需要合并更多事件(例如 Verilog 中的事件 or 表达式)时使用:

<label> .event/or <symbols_list>;

此时符号列表列出所有要合并触发本事件的事件,只要其中一个输入事件触发,本事件就触发。

16.1 命名事件的编译方式

命名事件实现为 functor:它没有输入、只有事件输出。生成代码方式如下:

a  .event "name";

这会创建一个 functor 并使其成为 mode-2 functor。触发器 -> a 会产生一条 %set a, 0; 语句,足以触发事件。事件实现见 vvp/event.cc。

17. 解析器语句(Resolver Statements)

解析器(resolver)是强度感知的 functor,有 4 个输入,其任务通常是利用强度解析计算解析后的输出。functor 的类型用于选择特定的解析函数:

<label> .resolv tri,  <symbols_list>;
<label> .resolv tri0, <symbols_list>;
<label> .resolv tri1, <symbols_list>;

解析器的输出是 vvp_vector8_t 值,即包含强度的向量。实现见 vvp/resolv.cc。

18. 位选择语句(Part Select Statements)

位选择语句是带三个输入的 functor:port-0 接收向量,输出该向量的一个(通常更小的)选中部分;其他输入以规范 bit 号与宽度指定所选部分,通常这些 bit 是常量:

<label> .part <symbol>, <base>, <wid>;
<label> .part/pv <symbol>, <base>, <wid>, <vector_wid>;
<label> .part/v <symbol>, <symbol>, <wid>;
<label> .part/v.s <symbol>, <symbol>, <wid>;
  • 输入通常是 .reg 或 .net,但可以是网线中的任意向量节点;
  • .part/pv 是 .part 的反向变体:输出实际写入输出的一部分;节点使用特殊的 part-select-write 函数传播网络的一部分;<vector_wid> 是目标 net(写入该 part)的总宽度,目标节点用它检查后续输出宽度;
  • .part/v 在 port-1 接收向量(或 long)输入作为位选择的 base,使位选择可以移动;.part/v.s 把该向量视为有符号值。

19. 位拼接语句(Part Concatenation Statements)

与位选择相反,.concat 语句是接收输入向量值并产生单一向量输出的 functor 节点,该输出是所有输入的拼接:

<label> .concat [W X Y Z], <symbols_list> ;
  • [ 与 ] 包围的 4 个数字是各输入的期望宽度,用于确定输入向量在输出中的位置,按 LSB 到 MSB 列出;
  • 输入同样按 LSB 到 MSB 列出,LSB 向量输入通过真实 functor 的 port-0 进入;
  • 初始输出值是 (W+X+Y+Z) 个 bit 的 bx;输入值传播时,bit 被放入输出向量的正确位置并传播新的输出值。

20. 重复向量语句(Repeat Vector Statements)

与拼接类似,但输入被重复固定次数:

<label> .repeat <wid>, <rept count>, <symbol> ;
  • <wid> 是输出向量的宽度(十进制数);
  • <rept count> 是输入向量值重复次数以构成输出宽度;
  • 输入宽度由这两个数字隐含;<symbol> 是输入源。

21. 替换语句(Substitution Statements)

替换语句在 Verilog 中没有直接对应物,只出现在综合中,是如下形式的简写:

foo = <a>;
foo[n] = <s>;

格式:

<label> .substitute <wid>, <soff> <swid>, <symbol>, <symbol> ;

第一个 <symbol> 必须具有 <wid> 宽度并被直通,除了 [<soff> +: <swid>] 范围内的 bit;第二个 <symbol> 收集进入该部分的向量。

22. 归约逻辑(Reduction Logic)

归约逻辑语句接收单一向量并传播单个 bit:

<label> .reduce/and  <symbol> ;
<label> .reduce/or   <symbol> ;
<label> .reduce/xor  <symbol> ;
<label> .reduce/nand <symbol> ;
<label> .reduce/nor  <symbol> ;
<label> .reduce/xnor <symbol> ;

器件有单一输入(任意宽度的向量),对向量所有 bit 执行逻辑运算(类似 Verilog),产生并传播单 bit 宽度的向量。

23. 扩展逻辑(Expansion Logic)

符号扩展节点与归约相反:接收窄向量或单 bit,补齐为更宽的向量:

<label> .expand/s <wid>, <symbol> ;

.expand/s 节点把输入符号符号扩展到给定宽度。

24. Force 语句

24.1 旧方法(functor 方式)

force 语句创建表示 Verilog force 语句的 functor:

<label> .force <signal>, <symbol_list>;
  • <signal> 代表被 force 的信号;
  • <symbol_list> 指定要 force 到 <signal> 上的表达式 bit;
  • <label> 标识 force functor;force functor 的数量与 <symbol_list> 中符号数相同。

激活/停用单 bit 上的 force:

%force	<label>, <width>;
%release <signal>;

<label>/<width> 是 force functor 向量的标签/宽度,<signal> 是驱动被 force 信号的 functor 标签。

24.2 新方法(链接方式)

如 .var 一节所述,%force 指令把常量值 force 到 .var 或 .net 上,匹配的 %release 释放该值。但有时需要把某个 functor(即另一个 .net)的值 force 到 .var/.net 上,此时使用 %force/link:

%force/link <dst>, <src> ;
%release/link <dst> ;

这会把节点 <src> 的输出链接到 <dst> 的 .var/.net 节点的 force 输入。链接后,输出 functor 自动驱动值到目标节点的 force 端口。匹配的 %release/link 指令移除链接(仍需 %release 才能真正释放)。%release/link 释放最后一次 %force/link,无论链接来自何处;新的 %force/link 会移除先前的链接。

同理,连续赋值端口有:

%cassign/link <dst>, <src> ;
%deassign/link <dst> ;

25. 结构性算术语句(Structural Arithmetic)

Verilog 各种算术运算符(+-*/%)在结构上下文中可用作接收向量的双输入 functor。所有运算符接收两个输入并产生固定宽度输出;输入向量在需要时被填充以获得期望的输出宽度:

<label> .arith/sub  <wid>, <A>, <B>;
<label> .arith/sum  <wid>, <A>, <B>;
<label> .arith/mult <wid>, <A>, <B>;
<label> .arith/div  <wid>, <A>, <B>;
<label> .arith/mod  <wid>, <A>, <B>;
  • 所有情况下都没有宽度限制,只要宽度固定;
  • 注意:.arith/mult 的输入宽度不一定等于输出宽度(文档注明此点尚未定论);
  • 这些器件支持 .s 与 .r 后缀:.s 表示有符号向量器件,.r 表示 real 值器件。

词法器还额外支持 .arith/pow 及其 .s/.r 变体(vvp/lexor.lex#L139-L142),表明幂运算也已加入结构算术家族。

26. 结构性比较语句(Structural Compare)

算术语句处理输出较宽的运算符,而比较器输出是单 bit,实现方式略有不同,但语法很相似:

<label> .cmp/eeq <wid>, <A>, <B>;
<label> .cmp/nee <wid>, <A>, <B>;
<label> .cmp/eq  <wid>, <A>, <B>;
<label> .cmp/ne  <wid>, <A>, <B>;
<label> .cmp/ge  <wid>, <A>, <B>;
<label> .cmp/gt  <wid>, <A>, <B>;
<label> .cmp/ge.s <wid>, <A>, <B>;
<label> .cmp/gt.s <wid>, <A>, <B>;
<label> .cmp/weq <wid>, <A>, <B>;
<label> .cmp/wne <wid>, <A>, <B>;

算术语句生成 <wid> 宽的输出,而比较器产生单 bit 向量结果。普通版本做无符号比较,.s 版本做有符号比较(相等性不需要关心符号)。词法器还额外识别 .cmp/eqx、.cmp/eqz 以及 .r 变体(vvp/lexor.lex#L161-L176)。

27. 结构性移位语句(Structural Shifter)

结构上下文中的可变移位用 .shift 语句实现:

<label> .shift/l <wid>, <data symbol>, <shift symbol>;
<label> .shift/r <wid>, <data symbol>, <shift symbol>;
  • 移位器有一个定义输出向量宽度的宽度、一个作为待移位输入数据的 <data symbol>、以及一个作为移位量的 <shift symbol>;
  • port 0 来的向量是待移位数据,必须恰好等于输出宽度;
  • port 1 的输入是移位量。

28. 结构性函数调用(Structural Function Calls)

.ufunc 语句定义对用户自定义函数的调用:

<label> .ufunc/real <flabel>, <wid>,
        [<isymbols> ( <psymbols> )] <ssymbol>;

<label> .ufunc/vec4 <flabel>, <wid>,
        [<isymbols> ( <psymbols> )] <ssymbol>;

<label> .ufunc/e <flabel>, <wid>, <trigger>,
        <isymbols> ( <psymbols> ) <ssymbol>;
  • 第一个变体用于只需在输入值变化时调用的函数;
  • 第二个变体用于还需要在触发事件发生时调用的函数;
  • <flabel> 是函数实现第一条指令的代码标签——仿真器将分支跳转到这段代码;
  • <wid> 是输出向量以 bit 计的宽度;
  • <trigger> 是触发事件的标签;
  • <isymbols> 是函数每个输入的 net 符号列表——这些是网络中的点,ufunc 器件监视这些 net 的输入变化;
  • <psymbols> 列表与 <isymbols> 大小完全一致,是表示函数输入端口的变量;ufunc 在调用函数前对它们执行赋值;
  • <ssymbol> 是函数作用域名。

29. 线程语句(Thread Statements)

线程语句创建仿真的初始线程,代表 initial 与 always 块,也可能代表其他在启动时创建线程的原因:

.thread <symbol> [, <flag>]

该语句创建起始地址位于 <symbol> 指定指令处的线程。仿真开始时为 .thread 语句创建一个线程,并从 <symbol> 寻址的指令开始执行。

<flag> 修改线程的创建/执行行为,支持的标志:

$push -- 将线程推入调度器。这只影响启动(时间 0):被推入的线程
        会在非推入线程之前启动。这有助于解决 time-0 竞争。

29.1 线程的一般特性

线程语句创建设计中的初始线程,包括原 Verilog 的 initial 与 always 语句,以及为各种目的生成的一些合成线程。行为代码也可以创建瞬时线程(transient threads),用于支持 fork/join、命名块和任务激活等结构。

  • 瞬时线程通过 %fork 指令创建:操作数给出起始地址,新线程被称为 fork 线程的子线程;子线程被压入子线程栈,一个线程只能有一个直接子线程;
  • 瞬时线程通过 %join 指令收割:%join 等待子线程栈顶线程完成,然后继续;没有子线程时 %join 是错误;
  • VVP 中的瞬时线程是传统线程与函数调用的混合体。事实上 vvp 中没有 %call 指令,函数调用由调用方的 %fork/%join 与被调用方的 %end 配合完成;%fork 是函数调用的推广——调用方不一定等待被调用方;
  • 为保证线程与父系关系的正确行为,所有 %fork 必须在父线程中有对应的 %join,子线程中有 %end。缺少正确匹配会导致层次关系混乱,错误代码的行为是未定义的。

29.2 线程上下文(Thread Context)

线程的上下文是该线程独有的全部本地数据,分为两个地址空间:bit 内存与 word 内存。

  • bit 内存:4 值 bit(0,1,x,z)区域,可以任意长度的 strip 寻址。例如 8 位值可以位于地址 8 到 15。地址 0、1、2、3 的 bit 是特殊常量值:从这些位置读取会得到 0、1、x、z 值的向量,因此可用于构造其他复杂值;
  • word 内存:带标签的 word 区域。每个 word 的值可以是 64 位无符号整数(uint64_t)、64 位有符号整数(int64_t)或 64 位浮点数(double)。这些 word 与 bit 有独立的地址空间。

29.3 线程与作用域

Verilog 的 disable 语句值得特别说明,因为它与线程的交互方式特殊:设计中各处的线程可以使用 disable 语句影响(结束)设计中的其他线程。

  • Verilog 中 disable 的操作数是作用域名,其行为是让在该作用域中执行的所有线程结束;线程终止包括该线程的所有子线程;
  • vvp 中所有线程都在某个作用域内,这正是 disable 访问目标线程的方式;
  • initial/always 线程如何加入作用域:只要在 .scope 声明之后声明即可(见上文作用域语句)。.thread 语句位于 .scope 语句之后,就使该线程加入命名作用域;
  • 瞬时线程加入的作用域是 %fork 指令的操作数:作用域按名字引用,fork 创建的线程原子性地加入该作用域。一旦加入,线程就留在那里直到结束。线程从不改变作用域,瞬时线程也不例外。

30. VPI 任务/函数调用(Vpi Task/Function Calls)

线程用 %vpi_call 或 %vpi_func 指令调用 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" 等,会被查找并与注册的系统任务/函数比较;
  • <args>... 是逗号分隔的参数列表,作为 vpi 句柄提供给 VPI 代码;
  • 普通 %vpi_call 在系统函数被当作任务调用时会报错;/w 后缀版本会显示警告并忽略函数试图返回的任何值;/i 后缀版本静默忽略被当作任务调用的系统函数返回的值。

一个真实的最小示例 vvp/examples/hello.vvp(等价于 Verilog 的 initial $display("Hello, World.");):

main	.scope module, "main" "main" 0 0;

code	%vpi_call 0 0 "$display", "Hello, World." {0 0 0};
	%end;
	.thread	code;

30.1 &A<> 参数:变量数组 word 引用

&A '<' <symbol> , <number> '>'
&A '<' <symbol> , <base_symbol> '>'
&A '<' <symbol> , <base> <width> <"s" or "u"> '>'
  • <symbol> 是变量数组的标签,<number> 是无符号整数的规范 word 索引;
  • 第二种形式从给定信号或 &A<>/&PV<> 选择中获取 <base>;
  • 第三种形式从线程空间获取索引(<width> bit 从 <base> 开始)。base 值可以有符号或无符号。

30.2 &PV<> 参数:信号部分引用

&PV '<' <symbol> , <base> , <width> '>'
&PV '<' <symbol> , <base_symbol> , <width> '>'
&PV '<' <symbol> , <tbase> <twid> <"s" or "u"> , <width> '>'
  • <symbol> 是信号标签,<base> 是位选择的规范起始 bit,<width> 是选择中的 bit 数;
  • 第二种形式从给定信号或 &A<>/&PV<> 选择中获取 <base>;
  • 第三种形式使用 <twid> bit(从 <tbase> 开始)从线程空间获取 <base>;base 值可以有符号或无符号。

31. 真值表(Truth Tables)

functor 所代表的逻辑用真值表表达。functor 有 4 个输入与 1 个输出,每个输入/输出有 4 种可能值(0、1、x、z),因此每个需要 2 bit。functor 输入共 8 bit,输出 2 bit。根据 8-bit 输入生成 2-bit 输出的完整查找表是 512 bit,可打包进 64 字节——小到"表比实现逻辑的代码还省空间"。

31.1 4 值编码

实现真值表需要为 4 值信号指定 2-bit 编码,文档作者"伪随机"选择了如下编码:

1'b0  : 00
1'b1  : 01
1'bx  : 10
1'bz  : 11

31.2 表布局

表是 64 字节的数组,每个字节保存 4 个 2-bit 输出。用输入 1、2、3 构造 6-bit 字节地址:

 332211

输入 0 的 2 bit 用于选择 8-bit 字节中 4 个 2-bit 对中的哪一个作为输出:

MSB -> zzxx1100 <- LSB

因此,完整真值表描述为 64 个 8-bit 字节。

vvp 引擎为基本门类型内置了真值表(见 vvp/draw_tt.c 与 vvp/logic.cc),程序员无需给出,只需命名类型即可获得对应真值表。

32. 可执行指令(Executable Instructions)

线程运行可执行代码,类似于处理器运行机器码。VVP 有大量可执行指令操作码,全部以 % 开头并进入单一地址空间。附加到可执行指令的标签会被赋予指令地址,可以作为 %jmp 指令的目标和线程的起始点。

Documentation/developer/guide/vvp/opcodes.rst 更详细地描述了各种指令,包括:

  • 算术指令:%add、%add/wr、%sub、%abs/wr 等(文档还讨论了"基于栈的算术"规划——字符串算术采用类似 FORTH/HP 计算器 RPN 的抽象栈方式,从栈中取操作数并把结果压回栈);
  • 赋值指令:%assign/v0(非阻塞赋值的向量版本,<delay> 是未来调度时刻,<bit> 是目标向量基址,宽度在索引寄存器 0)、%assign/ar(给实数数组 word 赋值)、%set(阻塞赋值)等;
  • 流程控制与线程:%jmp、%fork、%join、%end、%alloc(为自动分配作用域分配新实例存储)、%free;
  • 事件与等待:%wait、%waitfor、%evctl(设置线程事件控制信息);
  • 函数调用:%vpi_call 系列与 %vpi_func 系列;
  • 变量访问:%load(读取 functor 输出)、%set、%cassign/v、%force/v 等。

每个线程还有至多 16 bit 可用作标志位,作为返回布尔值的运算(如比较)的目标,也作为测试与分支指令的输入。

33. Functor、线程与事件三者的关系

综合上述主要组件,需要说明它们之间的关系:

  • Functor 提供设计的结构描述(在可用结构描述的范围内),独立于线程运行。具体而言:functor 的输入被设置时,它计算新的输出值;若输出与现有输出不同,则创建传播事件。functor 输出通过真值表查找计算,不借助线程;
  • **传播事件(propagation events)**是 vvp 中三种事件之一。它们被调度在某个时间执行,简单指向需要传播输出的 functor。事件到期时,被引用 functor 的输出被传播到其连接的所有输入,这些 functor 再按需创建新事件;
  • 赋值事件(assignment events)(三种事件中的第二种)由行为代码中的非阻塞赋值创建。执行 <=(vvp 中即 %assign)时创建赋值事件,包含接收值的 functor 输入的 vvp_ipoint_t 指针以及值本身。它们与传播事件不同,因为:
    • a) 没有哪个 functor 的输出是要赋的值(这正是值首次进入 functor 网络的方式);
    • b) 这允许行为代码创建任意长度的波形馈入变量。Verilog 允许非阻塞赋值这样做,但门输出不行;
  • 最后一种事件是线程调度事件(thread schedule event):只指向要执行的线程。线程由带程序计数器和私有存储的虚拟处理器构成。线程可以执行 %assign 创建赋值事件,执行 %set 做阻塞赋值,还可以用 %load 读取 functor 的输出;
  • 核心事件调度器接收这三种事件并调用正确的代码使设计发生变化:若事件是传播或赋值事件,则拨动 functor 网络;若事件是线程调度,则运行线程。事件队列的实现不重要,但目前实现为 skip list(跳表):一个带有跳过 delta-time 事件的跳过指针的有序单向链表;
  • functor 网络与线程是分离的。它们通过线程指令 %set、%assign、%waitfor 和 %load 通信。对线程而言,functor 网络是一大块结构体,线程通过某些 functor 访问指令对其戳弄。

34. VVP 的编译与执行流程

vvp 程序分几步运行:

  1. 初始化(Initialization):数据结构清空,表准备好编译;
  2. 编译(Compilation):读取并编译输入文件,按需建立符号表,分配并链接对象;
  3. 清理(Cleanup):释放仅用于编译的符号表与其他资源,减少内存占用;
  4. 仿真(Simulation):运行事件仿真。

从源码看,这一流程与 vvp/lib_main.cc 的 vvp_run() 一一对应:

  • 初始化由 vvp/compile.cc 的 compile_init() 完成,它分配空符号表并初始化各地址空间(vvp/compile.cc#L930-L938),同时调用各个 *_init() 子函数;
  • 编译由语法分析器(vvp/parse.y)控制,在规则中调用各种 compile_* 函数(都位于 compile.cc),并按需调用其他代码段;
  • 解析完成后调用 compile_cleanup():完成未解析引用(通过 resolv_list 待决链表反复解析,见 vvp/compile.cc#L840-L872),然后释放所有符号表与编译期资源;compile_cleanup() 返回后 parser 与 compile.cc 中的函数不再使用;
  • 清理之后启动仿真:执行 schedule_simulate()(vvp/schedule.cc#L1129),做最终设置并启动仿真与事件队列。

此外,vvp/lib_main.cc#L371-L394 显示在 -v 详细模式下,vvp 会统计并输出 functor、logic、bufif、resolv、信号、opcode、net、array、scope 等对象数量,以及 vvp/lib_main.cc#L409-L427 中的各类事件计数(时间步、线程调度事件、赋值事件等),这对性能分析与调试很有价值。

34.1 命令行选项

vvp/main.cc#L56-L110 定义了 vvp 的命令行参数:

选项 含义
-h 打印帮助信息
-i 交互模式(无缓冲 stdio)
-l file 日志文件,- 表示 stderr
-M path 追加 VPI 模块目录;-M - 清空 VPI 模块路径
-m module 加载 VPI 模块
-n 非交互模式($stop 视为 $finish)
-N 同 -n,但退出码为 1 而非 0
-q 安静模式(抑制 MCD bit 0 上的输出)
-s 立即执行 $stop
-v 详细进度消息
-V 打印版本信息

使用方式:vvp [options] input-file [+plusargs...]。

35. 从 Verilog 到 VVP:编译器如何生成这些语句

vvp 仿真引擎设计为接收 Verilog 的编译形式作为输入,这意味着存在一个把 Verilog 编译成 vvp 引擎可读形式的编译器(即 iverilog 前端的 code generator,相关代码在仓库根目录的 emit.cc 等文件中)。

35.1 布尔逻辑门

AND、OR、NAND 等门简单而明显地用 functor 语句实现。任何至多 4 输入的逻辑都可用单个 functor 实现(示例见上文 7.4 节)。超过 4 个输入时级联 functor。

35.2 reg 与其他变量

Reg 与 integer 属于 Verilog 所称的"变量(variables)",即行为代码可以赋值的东西;它们不同于"网线(nets)"(包括 wire 等)。变量的每个 bit 由一条 .var 语句创建:

reg a;

编译为:

a	.var "a", 0, 0;

35.3 命名事件

事件一般实现为 functor,但命名事件没有输入、只有事件输出(示例见上文 16.1 节)。

36. 自动分配作用域(Automatically Allocated Scopes)

若 .scope 语句的 <type> 为 autofunction 或 autotask,该作用域被标记为自动分配作用域。该作用域中声明的每个变量或事件的 functor 被加入一个列表,需要为作用域的每个动态实例自动分配。

  • 把自动函数/任务的输入参数复制到作用域变量之前,需要分配新的作用域实例。过程代码中的函数/任务调用由 %alloc 指令处理;结构性函数调用由 .ufunc 语句生成的 phantom 代码处理。两种情况下 VVP 都会优先复用先前释放的作用域实例,只有在没有可用实例时才创建新实例;
  • 复制完自动函数的结果或自动任务的输出参数后,作用域实例可被释放。过程代码调用由 %free 指令处理;结构性调用由 .ufunc 生成的 phantom 代码处理。VVP 把实例加入该作用域的释放实例列表,下次需要新实例时可复用存储;
  • 对每个自动分配作用域实例,VVP 创建一组条目,称为作用域上下文(scope context)。该数组中每个条目是指向保存一个作用域变量或事件状态存储的指针。任何变量/事件在该数组中的索引(上下文索引)存储在其关联的 functor 中;
  • 每个 VVP 线程跟踪其当前写上下文与当前读上下文。在静态作用域中执行的线程,两者都初始化为空值;在自动分配作用域中执行的线程,两者都初始化为指向分配给该作用域的上下文;
  • 开始复制自动函数/任务的输入参数之前,调用方线程的当前写上下文被设置为该函数/任务调用的上下文;线程执行完函数/任务体被重新 join 之后、开始复制结果或输出参数之前,当前写上下文被重置为先前值,当前读上下文被设置为函数/任务调用的上下文;完成复制结果或输出参数后,当前读上下文被重置为先前值;
  • 读写自动分配变量/事件的状态时,关联 functor 通过当前运行线程的读/写上下文间接寻址,使用其存储的上下文索引。

37. 进一步学习

登录后查看全文
iverilog