An open API service indexing awesome lists of open source software.

https://github.com/WangXuan95/BSV_Tutorial_cn

一篇全面的 Bluespec SystemVerilog (BSV) 中文教程,介绍了BSV的调度、FIFO数据流、多态等高级特性,展示了BSV相比于传统Verilog开发的优势。
https://github.com/WangXuan95/BSV_Tutorial_cn

bluespec bluespec-systemverilog bsv fpga hardware-description-language hdl verilog

Last synced: about 1 year ago
JSON representation

一篇全面的 Bluespec SystemVerilog (BSV) 中文教程,介绍了BSV的调度、FIFO数据流、多态等高级特性,展示了BSV相比于传统Verilog开发的优势。

Awesome Lists containing this project

README

          

BSV 中文教程
===========================
欢迎 :hand: 这里(可能)是首个中文 Bluespec SystemVerilog (BSV) 教程。

![图1](./readme_image/0.bluespec.png)

当前版本 2023/3/28 。同步更新至:

- GitHub : https://github.com/WangXuan95/BSV_Tutorial_cn
- Gitee : https://gitee.com/wangxuan95/BSV_Tutorial_cn

 

# 1 前言

## 为什么要 BSV?Verilog 不好用?
BSV 是一门高级硬件描述语言(**H**igh-**L**evel **H**ardware **D**escription **L**anguage, **HL-HDL**),与 Verilog 一样,被用于 FPGA 或 ASIC 的设计和验证。BSV 于 2003 年被 Bluespec 公司开发,期间是商业收费工具,到 2020 年它的编译器才开源,这才给了我们接触它的机会。

Verilog 的语法简单、特性少,却能全面且精准地描述数字电路,是“小而美”的语言。学习 Verilog 只需要掌握3种常见写法:`assign`, `always @ (*)` 和 `always @ (posedge clk)` ,剩下的就依赖于你对电路设计的理解了。当然,这才是最难的,包括各种繁杂的硬件设计思维——状态机、并行展开、流水线化、握手信号、总线协议等。

读到这里你有没有意识到问题——用如此简单的抽象级别来描述如此复杂的数字电路系统,会不会很吃力?相信每个接触过复杂的 Verilog 系统的读者,都体会过被 always 块下的几十个状态所支配的恐惧,也清晰地记得模块实例化时那几十行吓人的端口连接。因此,我们需要一种抽象层次更高的 HDL 来提高开发效率,BSV 就能胜任这一工作。

> :pushpin: 在笔者看来 BSV 并不能替代 Verilog ——至少现在不能。这就像各种高级语言也没有替代 C 语言一样。一些说法把 Verilog/VHDL 在数字逻辑设计中的地位,比作汇编语言在程序设计中的地位,笔者认为该说法具有误导性。Verilog/VHDL 所在的抽象层次恰到好处,且是通用(跨平台)的——支持各厂家的FPGA和ASIC,因此成为了数字逻辑设计的主流语言,这与 C 语言的理念类似,应当比作 C 语言。
>

## BSV 效果如何?

BSV的优势包括但不限于:

- 输入输出信号封装为method方法、自动生成握手信号。
- 可用复合数据类型来组织数据,提高代码可读性和可维护性。
- 提供各种小型FIFO模块,在构成复杂的弹性流水线电路时,比Verilog更高效。
- 可用顺序结构、瞬时结构、并行结构构成状态机,相比Verilog手动维护状态转移更加方便。
- 支持多态,获得尽可能多的代码复用。
- 在多态的加持下,BSV的模块库会比Verilog模块库的通用性更强,因此BSV具有大量的官方库或第三方库,来支持各种常见功能,例如定点数、浮点数、LSFR、CRC、AXI总线等

BSV生成的Verilog和手写Verilog相比,资源量和时钟频率不差多少。但 BSV 的代码量往往很低,并获得更高的可读性、可维护性。这里给出一些直观样例:

- BSV 编写 RISC-V RV32I 流水线 CPU 只有200行(手写Verilog可能要600行),在 EP4CE115F29C8 FPGA 占 5kLE,时钟频率达到 77MHz 。
- BSV 编写 JPEG 图像压缩器只有300行(手写Verilog可能要1000行),在 EP4CE115F29C8 FPGA 占 12kLE,时钟频率达到 43MHz,性能达到 344 M像素/秒 。

个人认为 BSV 非常适合编写模块 (IP核)。方法是:用BSV编写模块和testbench,在BSV阶段就做好验证,然后生成Verilog模块。另外你还能用BSV testbench来生成Verilog testbench,进行Verilog仿真。后续使用时,把 Verilog 模块嵌入到 FPGA 项目中即可。

## 关于本教程

在开始前,读者要有如下基础:

- 熟悉 Verilog (如果你只会 VHDL,可以先花数小时了解一下 Verilog 的语法);熟悉数字电路设计,比如数据的位表示、状态机、流水线、握手信号、串并转换、单双口RAM等知识。
- 起码要知道软件编程语言中的基本概念:数据类型、分支、循环、数组、函数。

另外,如果有以下知识,学起来会更轻松:

* 了解一点面向对象的概念,例如 C++、JAVA、Python 中的类、实例、成员变量、方法函数等概念。
* 会打开命令行无脑输命令,因为 BSV 编译器要在 Linux 命令行(或 Windows WSL)中使用。

本教程参考了 《BSV by example》 [1] 这篇很棒的官方教程。但也有很大不同:

* 本教程基于开源的 BSV 编译器 bsc,会带读者在命令行中使用编译器。
* 本教程不是直译自 [1],而是根据其大纲而重构的,符合中文语法习惯,直译原文则会很生硬。
* 本教程根据自己的理解,对 [1] 的内容进行删减和重排序。
* 本教程会讲述一些 [1] 中没有讲,但也很实用的内容,它们来自于内容更全的 BSV 参考指南 [2] 。
* 本教程在讲解中配套了很多有实际意义的代码,还展示了3个大型项目:SPIFlash读写器、RISC-V流水线CPU、JPEG图像压缩器。
* 作为实用教程,笔者将讲述 BSV 生成的 Verilog 模块的特性,指导读者把它嵌入 Verilog 项目中。

笔者也是 BSV 初学者,完全凭借对数字逻辑设计的热爱~~用爱发电~~编写了本教程。出现不准确的表述也在所难免,可以提 issue 让笔者改进。

> :point_right: 在此感谢部分网友帮我提出了的部分笔误。

## 参考资料

[1] R. Nikhil and K. Czeck. BSV by Example: The next-generation language for electronic system design. Bluespec, Inc. 2010. http://csg.csail.mit.edu/6.S078/6_S078_2012_www/resources/bsv_by_example.pdf.

[2] Bluespec SystemVerilog Reference Guide. Bluespec, Inc. 2017. https://web.ece.ucsb.edu/its/bluespec/doc/BSV/reference-guide.pdf. (是内容最全 BSV 的官方资料,可作为查阅文档)

 

## 目录 :page_with_curl:

- [2 BSV概览](#head6)
- [2.1 BSV vs. Verilog](#head7)
- [2.2 BSV vs. Chisel/SpinalHDL](#head8)
- [2.3 BSV vs. HLS](#head9)
- [2.4 总结](#head10)
- [3 准备工作](#head11)
- [3.1 安装 bsc 编译器](#head12)
- [3.2 安装 iverilog 和 Tcl](#head13)
- [3.3 安装 gtkwave](#head14)
- [3.4 部署 bsvbuild.sh 脚本](#head17)
- [3.5 找一款顺手的代码编辑器](#head18)
- [4 项目组织与构建](#head19)
- [4.1 bsvbuild.sh 脚本](#head20s)
- [4.2 单模块项目](#head20)
- [4.3 单包多模块项目](#head21)
- [4.4 多包项目](#head22)
- [4.5 生成与查看波形](#head23)
- [5 类型与变量](#head24)
- [5.1 类型类](#head25)
- [5.2 基本数据类型](#head26)
- [5.3 Integer 与 String 类型](#head31)
- [5.4 使用 $display 打印](#head34)
- [5.5 变量定义与赋值](#head35)
- [5.6 组合逻辑电路](#head39)
- [5.7 元组 Tuple](#head45)
- [5.8 Maybe 类型](#head46)
- [6 时序逻辑电路](#head47)
- [6.1 寄存器 Reg](#head48)
- [6.2 读写顺序与调度注解](#head52)
- [6.3 线网 Wire](#head53)
- [6.4 规则 rule](#head59)
- [6.5 调度属性](#head64)
- [6.6 并发寄存器 mkCReg](#head75)
- [7 模块与接口](#head76)
- [7.1 模块层次结构](#head77)
- [7.2 值方法与动作方法](#head80)
- [e.g. 比特编码器 v1](#head83)
- [7.3 方法的隐式条件](#head84)
- [e.g. 比特编码器 v2](#head86)
- [e.g. 比特编码器 v3](#head88)
- [7.4 动作值方法](#head89)
- [e.g. 比特编码器 v4](#head90)
- [7.5 方法实现的简写](#head91)
- [e.g. 自增寄存器 v1](#head92)
- [7.6 使用现成的接口](#head93)
- [e.g. 自增寄存器 v2](#head94)
- [7.7 接口嵌套](#head95)
- [e.g. 可配置自增寄存器 v1](#head96)
- [7.8 用元组返回多个接口](#head97)
- [e.g. 可配置自增寄存器 v2](#head98)
- [7.9 动作函数](#head99)
- [8 存储与队列](#head100)
- [8.1 BRAMCore](#head101)
- [8.2 BRAM](#head106)
- [8.3 队列 FIFO 概览](#head114)
- [8.4 常规 FIFO](#head115)
- [e.g. 开平方计算流水线 v2](#head118)
- [8.5 不保护 FIFO](#head119)
- [8.6 LFIFO](#head120)
- [e.g. 累加式写入的存储器](#head121)
- [8.7 DFIFOF](#head122)
- [e.g. 比特编码器 v5](#head123)
- [8.8 BypassFIFO](#head124)
- [8.9 大容量的 BRAMFIFO](#head125)
- [9 高级数据类型](#head126)
- [9.1 数组 array](#head127)
- [9.2 向量 Vector](#head129)
- [9.3 typedef 关键字](#head134)
- [9.4 枚举 enum](#head135)
- [9.5 结构体 struct](#head136)
- [9.6 标签联合体 union tagged](#head137)
- [9.7 case 语句与表达式](#head138)
- [10 多态](#head142)
- [10.1 多态中的基本概念](#head143)
- [10.2 多态函数举例](#head148)
- [10.3 多态模块举例](#head149)
- [e.g. 自定义更多的寄存器](#head150)
- [e.g. 自定义双缓冲模块](#head151)
- [11 状态机 StmtFSM](#head152)
- [11.1 基本的构建和使用方法](#head153)
- [11.2 执行结构](#head156)
- [11.3 分支和循环](#head164)
- [11.4 区别局部变量和寄存器](#head168)
- [11.5 举例](#head169)
- [e.g. SPI 发送器](#head170)
- [12 BSV for Verilog](#head171)
- [12.1 输入输出信号](#head172)
- [12.2 删除不必要的信号](#head177)
- [12.3 引入缺少的 Verilog 库文件](#head180)
- [13 样例研究](#head183)
- [13.1 SPIFlash 读写器](#head184)
- [13.2 RISC-V 流水线 CPU](#head187)
- [13.3 JPEG 图像压缩器](#head190)

 

 

# 2 BSV概览

## 2.1 BSV vs. Verilog

为了提升读者的学习热情:raising_hand:,我先展示一个实用样例:编写一个 **SPI 总线的发送控制器** 。虽然使用 Verilog 写起来有一点难度,但用 BSV 写起来**非常简单**!

SPI 发送控制器的时序波形如**图1**,驱动时钟是 `clk`。当 `en=1` 时,说明外界发起了一个发送请求,于是从 `data` 信号上读入 8bit 数据,随后的若干周期,在 `mosi` 信号上按从高到低的顺序逐个输出它的比特位(并转串),同时还要产生 `sck` 和 `ss` 的波形。在发送的过程中,`rdy=0`指示模块正忙。忙完后 `rdy` 恢复 1,此时才能响应下一个 `en=1` 的请求。

| ![图1](./readme_image/1.SPI.png) |
| :---: |
| **图1**:SPI 发送控制器的波形 |

> :pushpin: `en` 和 `rdy` 构成了一对握手信号,在 `en` 与 `rdy` 同时 `=1` 的周期完成一次握手,这是一种常用的硬件设计方法,很多总线协议都会用到,只是名称可能不一样。比如,AXI 总线的握手信号叫 valid 和 ready 。

下面用 Verilog 实现它。整个发送的过程共经历了 21 个 `clk` 周期,我们用一个 `0~20` 的计数变量来表示当前状态。`cnt=0` 表示空闲状态,遇到 `en=1` 时就令 `cnt<=1`,并在之后的每个时钟周期都令 `cnt+1`,并根据 `cnt` 的值输出 `ss`, `sck` 和 `mosi`,直到 `cnt=20` 时归零。另外注意到 `2≤cnt≤17` 时,输出的信号是 8 个有规律的循环,它们的输出描述可以合并,没必要繁琐地逐一描述。这样,该模块的核心部分实现大概如下 :point_down: (不用仔细看)

```verilog
// Verilog SPI 发送(可综合)
reg [4:0] cnt = 0;
reg [7:0] rdata = 0;
assign rdy = (cnt==0) ? 1 : 0;

always @ (posedge clk or negedge rstn)
if(!rstn) begin
{ss, sck, mosi} <= 3’b111;
end else begin
if(cnt==0) begin
if(en) begin
rdata <= data;
cnt <= 1;
end
end else if(cnt==1) begin
ss <= 1’b0; // ss 拉低
cnt <= cnt + 1;
end else if(cnt<=17) begin
sck <= cnt[0]; // cnt 为偶数时,令 sck=0,cnt 为奇数时,令 sck=1。
mosi <= rdata[8-(cnt/2)]; // 在 mosi 上产生串行输出
cnt <= cnt + 1;
end else if(cnt==18) begin
mosi <= 1’b1;
cnt <= cnt + 1;
end else if(cnt==19) begin
ss <= 1’b1; // ss 拉高
cnt <= cnt + 1;
end else begin
cnt <= 0;
end
end
```

以上 Verilog 代码已经是一个很简短的实现了,但可读性很差,难于修改,如果我们想在 `ss` 拉低之前再插入一个时钟周期干其它的事情,则后面的所有状态转移以及 `2≤cnt≤17` 时的奇偶判断都得改,容易改出 bug。

各位读者在用 Verilog 编写 testbench 时可能想过:“如果电路设计中也能使用 testbench 中的顺序执行语法,那该多方便啊”,比如以上 SPI 发送过程可以写作如下 :point_down: ,用 `@(posedge clk) ...` 来表示“等到下一个时钟上升沿干某某某事情”,这种顺序执行模式很符合人类的思维,修改起来也方便,但不可综合(无法生成电路)。

```verilog
// Verilog SPI 发送(testbench 写法,不可综合!!)
reg signed [31:0] cnt = 7; // cnt 初始值为 7
initial begin
{ss, sck, mosi} <= 3’b111;
@(posedge clk) ss <= 1’b0; // 等到下一个时钟上升沿,ss 拉低
while(cnt>=0) begin // while 循环,cnt 从 7 递减到 0,共8次
@(posedge clk) begin // 等到下一个时钟上升沿
sck <= 1’b0; // sck 拉低
mosi <= wdata[cnt]; // mosi 依次产生串行 bit
end
@(posedge clk) begin // 等到下一个时钟上升沿
sck <= 1’b1; // sck 拉高
cnt = cnt - 1; // cnt 每次循环都递减
end
end
@(posedge clk) mosi <= 1’b1; // mosi 拉高
@(posedge clk) ss <= 1’b1; // ss 拉高,发送结束
end
```

下面展示 SPI 控制器的 BSV 实现。8~24 行描述的是一个类似于 Verilog testbench 的流程,详见代码注释。

```bsv
module mkSPIWriter (SPIWriter); // BSV SPI 发送(可综合!!), 模块名称为 mkSPIWriter
Reg#(bit) ss <- mkReg(1'b1);
Reg#(bit) sck <- mkReg(1'b1);
Reg#(bit) mosi <- mkReg(1'b1);
Reg#(Bit#(8)) wdata <- mkReg(8'h0);
Reg#(int) cnt <- mkReg(7); // cnt 的复位值为 7

FSM spiFsm <- mkFSM ( // mkFSM 是一个状态机自动生成器,能根据顺序模型生成状态机 spiFsm
seq // seq...endseq 描述一个顺序模型,其中的每个语句占用1个时钟周期
ss <= 1’b0; // ss 拉低
while (cnt>=0) seq // while 循环,cnt 从 7 递减到 0,共8次
action // action...endaction 内的语句在同一周期内执行,即原子操作。
sck <= 1’b0; // sck 拉低
mosi <= wdata[cnt]; // mosi 依次产生串行 bit
endaction
action // action...endaction 内的语句在同一周期内执行,即原子操作。
sck <= 1’b1; // sck 拉高
cnt <= cnt - 1; // cnt 每次循环都递减
endaction
endseq
mosi <= 1’b1; // mosi 拉高
ss <= 1’b1; // ss 拉高,发送结束
cnt <= 7; // cnt 置为 7,保证下次 while 循环仍然正常循环 8 次
endseq ); // 顺序模型结束

method Action write(Bit#(8) data); // 当外部需要发送 SPI 时,调用此 method。参数 data 是待发送的字节
wdata <= data;
spiFsm.start(); // 试图启动状态机 spiFsm
endmethod

method Bit#(3) spi = {ss,sck,mosi}; // 该 method 用于将 SPI 信号引出到模块外部
endmodule
```

首先,以上 BSV 代码看似与 Verilog testbench 写法一样,都是顺序执行。但 Verilog testbench 不可综合,BSV 却可综合。这是 BSV 的 `StmtFSM` 包提供的自动化生成状态机的功能(详见注释)。相比于 Verilog 手动编写的状态机,省略了状态转移行为的手动管理,使我们可以专注地描述每个状态下的行为。

其次,你会发现 BSV 代码中完全没出现时钟 (`clk`) 和复位 (`rstn`) 信号。实际上 BSV 在默认情况下使用同步时序逻辑,所有的动作都同步于一个默认时钟的上升沿,所有的寄存器都受控于一个默认的复位信号。BSV 转换成 Verilog 后,时钟和复位信号会显现出来。

> 在数字逻辑设计中,应该养成能用同步时序,就不用异步时序的好习惯,除非涉及到跨时钟域的需要。Verilog 自由度很高,导致 Verilog 初学者爱在 `always @ (…)` 敏感列表中加入奇奇怪怪的门控时钟或不同边沿的触发,导致各种冒险,也使时序约束变得困难。初学 BSV 时就不容易犯这种问题。当然,BSV 也提供了异步(多时钟)设计方法和异步组件(例如异步FIFO),详见 [2]。本教程**只涉及同步时序设计**,因为这已经能涵盖大部分数字逻辑设计的需要。

最后,你还会注意到这个 BSV 代码中看不出模块输入输出信号的定义。因为 BSV 将它们封装成了**方法** (method)。以上述代码中的方法`method Action write` 为例,当外部需要启动 SPI 发送时就调用此方法。参数 `Bit#(8) data` 是待发送的字节,会映射为 Verilog 中的一个 8 位的输入信号,而握手信号 en 和 rdy 会自动生成(如下 :point_down: )。当 `spiFsm` 状态机正忙(即正在发送SPI时)时,对 `method Action write` 的调用将无法生效;用 Verilog 的思维来讲:此时 `rdy=0`,即使 `en=1` 也无法发送数据。

```bsv
// 用 BSV 编译器把 BSV 模块转化为 Verilog 后的接口定义
module mkSPIWriter( // 这些注释是笔者加上的
input CLK, // 自动生成的时钟
input RST_N, // 自动生成的复位
// 由 method Action write(Bit#(8) data) 生成的信号
input [7:0] write_data, // 对应波形图1中的 data 信号
input EN_write, // 对应波形图1中的 en 信号(是自动生成的握手信号)
output RDY_write, // 对应波形图1中的 rdy 信号(是自动生成的握手信号)
// 由 method Bit#(3) spi 生成的信号
output [2:0] spi, // 3bit 分别对应 ss,sck,mosi 信号
);
```

总结起来,在本例中,BSV 相比 Verilog 具有更高层次的抽象:

* **隐式时钟/复位**:只需给出寄存器的复位状态,和每个时钟周期的行为,时钟和复位信号会自动生成。
* **方法抽象**:把接口动作抽象为方法调用。
* **自动生成握手信号**:BSV 会根据方法能否能生效,来自动生成握手信号。
* **自动生成状态机**:提供了根据顺序模型自动生成状态机的库,使我们能专注于行为描述,不需要手动维护状态判断和跳转。

有些读者可能担心 BSV 抽象层次过高,会不会失去对电路细节的控制能力?实际上,BSV 并不屏蔽我们对每一个时钟周期下的行为的描述能力,相反是在提升这个描述能力,让我们能专注于描述行为,而不必重复一些繁琐的低层次工作。而且之后你会发现,只要你愿意,也可以把 BSV 写成 Verilog 那样的寄存器传输(RTL)级的抽象层次,但那样还不如直接写 Verilog 了。

## 2.2 BSV vs. Chisel/SpinalHDL

Chisel 和 SpinalHDL 也可以归类为高级硬件描述语言(HL-HDL)。因为笔者并没有学过它们 ,本节留待日后补充。这里摆出 BSV 官方对 Chisel 和 BSV 的比较:

> :point_right: BSV 的抽象层次高于 Chisel,因为 Chisel 仍与 Verilog一样处于经典时钟逻辑层次的抽象(尽管Chisel提供了面向对象等高级特性),而 BSV 具有原子事务(Atomic Transactions Level)的抽象级别。
>
> :point_right: BSV 的语法是专门针对硬件设计而设计的,不依附于任何现有语言。不像 Chisel 和 SpinalHDL 是基于现有语言 Scala 的,可能存在一些只适合软件设计思路的干扰性语法。

 

## 2.3 BSV vs. HLS

**BSV** 与高层次综合 (**H**igh **L**evel **S**ynthesis, **HLS**) 的理念有本质上的不同。BSV 的目标是提高电路的时钟周期级行为的描述能力,而 HLS 则试图屏蔽时序的概念,以无时序描述能力的高级语言(C/C++)为起点,靠**自动调度**来确定执行时序。虽然这让软件设计人员能够快速上手,但也让 HLS 的应用场合受限。例如,HLS 可以设计高性能的神经网络加速器,但设计 CPU 就捉襟见肘。

以下文字摘自 [1],讲述了 BSV 相比于 HLS 的优势:

> :point_right: 基于 C/SystemC 的 HLS 被许多业内人视为发展方向。不幸的是,这存在一定的问题。众所周知,高性能软件的关键是好的算法,好的算法是由算法工程师设计的,而不是由自动工具设计的(编译器只会优化我们编写的算法,而不会提升算法本身)。同样,面积小、时序好的硬件也是由硬件工程师设计的。因此,至关重要的是要赋予硬件工程师最大的表达架构的能力。HLS 却恰恰相反——它掩盖了架构,而是尝试用启发式方法选择架构。设计人员对此活动只有间接的控制能力,例如约束硬件资源限制和指定优化方法(unrolling, peeling, fusion 等),很难(常常涉及猜测)将产生一个好的架构。

 

## 2.4 总结

**图2**比较了 Verilog, VHDL, Chisel, HLS, BSV 的抽象层次(Level of abstraction)和架构透明度(Architectural Transparent) 。架构透明度越高,语言对电路细节(微架构)的控制能力越强,生成的电路的行为也越容易被开发者掌控(可预测性高)。

| ![图2](./readme_image/2.compare.png) |
| :---: |
| **图2**:比较 Verilog, VHDL, Chisel, HLS 与 BSV |

 

 

# 3 准备工作

本章教大家搭建 BSV 开发环境。BSV 编译器运行在 Linux 系统上,可以使用以下平台之一:

* Linux 实体机 :computer:
* Linux 虚拟机
* Windows10 bash (**W**indows **S**ubsystem of **L**inux, **WSL**),强烈推荐 :raised_hands: !!WSL 的开启方法可以参考:
* Install WSL:https://docs.microsoft.com/en-us/windows/wsl/install
* win10开启wsl,让我们愉快的使用Linux:https://zhuanlan.zhihu.com/p/384026893

> :point_right: 提示:开启 WSL 后,在 Windows 的某个目录(文件夹)下打开 WSL 命令行的方式是:在”文件资源管理器“空白处摁住shift+右键 → ”在此处打开 PowerShell 窗口“ → 在 PowerShell 中输入 wsl + 回车 → 即可进入 Linux 环境。

 

## 3.1 安装 bsc 编译器

BSV 的编译器 bsc 是以源码的形式发布在[Bluespec官方bsc仓库](https://github.com/B-Lang-org/bsc)的,并未提供编译好的可执行文件。不过,笔者帮你们编译好了,它在本仓库的 **bsc-build.tar.gz** 压缩包中。(笔者是在 **Ubuntu 16.04 (x86_64)** 中编译它的,复制到 WSL 上发现也能工作)

首先进入 **bsc-build.tar.gz** 所在的目录的命令行,运行以下命令把 **bsc-build.tar.gz** 复制到你想安装的位置并解压,例如你想把它安装在 `/opt` 目录下:

```bash
$ tar -xzvf bsc-build.tar.gz -C /opt
# 注意 tar 前面的 $ 仅仅是提示你输入命令的起始符,不要把 $ 也输进去了
# 如果显示 Permission Denied,请在命令前面加上 sudo(下同)。
```

以上命令在 `/opt` 目录中解压出一个叫 `bsc` 的目录,其中 `bsc/bin` 目录下有可执行文件 bsc。运行一下它试试:

```bash
$ /opt/bsc/bin/bsc # 如果打印如下,说明 bsc 正常工作
Usage:
bsc -help to get help
bsc [flags] file.bsv to partially compile a Bluespec file
bsc [flags] -verilog -g mod file.bsv to compile a module to Verilog
bsc [flags] -verilog -g mod -u file.bsv to recursively compile modules to Verilog
bsc [flags] -verilog -e topmodule to link Verilog into a simulation model
bsc [flags] -sim -g mod file.bsv to compile to a Bluesim object
bsc [flags] -sim -g mod -u file.bsv to recursively compile to Bluesim objects
bsc [flags] -sim -e topmodule to link objects into a Bluesim binary
bsc [flags] -systemc -e topmodule to link objects into a SystemC model
```

然后用 vi 或 nano(或任何你习惯的文本编辑器)编辑当前用户的 `.bashrc` 文件:

```bash
$ vi ~/.bashrc
```

将以下两行追加到 `.bashrc` 文件的末尾(目的是把 bsc 和相关 lib 添加到永久环境变量):

```bash
export PATH=/opt/bsc/bin:$PATH
export LIBRARY_PATH=/opt/bsc/lib:$LIBRARY_PATH
```

然后启动一个新的命令行,运行一下 bsc 试试,能正常工作即可:

```bash
$ bsc
```

如果笔者提供的 bsc 在你的 Linux 上不能工作,请前往[Bluespec官方bsc仓库](https://github.com/B-Lang-org/bsc),自行按照 README 的指示编译 bsc 编译器。注:WSL 下编译 bsc 可能面临各种依赖问题,因此建议用 Linux 实体机或虚拟机编译 bsc 。

 

## 3.2 安装 iverilog 和 Tcl

Icarus Verilog (iverilog) 用于进行 BSV 和 Verilog 的联合仿真;tcl-dev 是为了使用 Bluespec Tcl Shell (bluetcl)。运行以下命令安装:

```bash
$ apt-get install iverilog tcl-dev
```

安装好后,运行一下 iverilog 试试:

```bash
$ iverilog # 如果打印如下,说明 iverilog 正常工作
iverilog: no source files.
Usage: iverilog [-ESvV] [-B base] [-c cmdfile|-f cmdfile]
[-g1995|-g2001|-g2005|-g2005-sv|-g2009|-g2012] [-g]
[-D macro[=defn]] [-I includedir]
[-M [mode=]depfile] [-m module]
[-N file] [-o filename] [-p flag=value]
[-s topmodule] [-t target] [-T min|typ|max]
[-W class] [-y dir] [-Y suf] source_file(s)
```

 

## 3.3 安装 gtkwave

为了查看仿真产生的波形,需要安装 gtkwave。

### 在 Linux 上安装 gtkwave

如果使用的是 Linux 实体机或虚拟机,可以直接安装:

```bash
$ apt-get install gtkwave
```

今后在使用时,用以下命令查看仿真产生的 .vcd 波形文件 :

```bash
$ gtkwave wave.vcd #今后用该命令查看波形文件 wave.vcd
```

### 在 Windows 上安装 gtkwave

你不能在 WSL 中安装 gtkwave,因为 gtkwave 是一个图形界面 (GUI),而 WSL 是没有 GUI 的。替代办法就是直接在 Windows 上安装 gtkwave。请前往 [**gtkwave官网** ](http://gtkwave.sourceforge.net/)下载 ZIP 压缩包,把它解压到你想安装的目录下,找到其中的 `gtkwave/bin` 目录里面的 **gtkwave.exe** ,运行它,如果打开了一个窗口,则安装成功。

 

## 3.4 部署 bsvbuild.sh 脚本

为了方便调用 bsc 和 iverilog 等工具进行编译、仿真、生成波形、生成 Verilog 的流程,我编写了自动脚本 **bsvbuild.sh** 。请运行以下命令把它复制到 `/opt/bsc/bin` 目录下(也就是3.1节中bsc的安装目录),并提供运行权限:

```bash
# 请在 bsvbuild.sh 所在的目录运行以下命令:
$ cp bsvbuild.sh /opt/bsc/bin
$ chmod +x /opt/bsc/bin/bsvbuild.sh
```

然后运行 **bsvbuild.sh** ,会打印该脚本的使用方法:

```bash
$ bsvbuild.sh # 如果打印如下,说明 bsvbuild.sh 正常工作

usage: run following command under the directory which contains .bsv source file(s):
/opt/bsc/bin/bsvbuild.sh - [] [] [] []
省略更多打印 ...
```

第4章会通过例子展示 **bsvbuild.sh** 的使用方法。

 

## 3.5 找一款顺手的代码编辑器

BSV 的代码文件名后缀为 .bsv ,尽管用记事本都能编写,但没有高亮和补全写起来确实很难受。这里我推荐用 vscode ,并给他安装 BSV 的高亮插件。

首先安装 vscode (过程略)。然后打开 vscode ,如**图3**操作,点击”扩展“ → 输入”bluespec“ → 找到”Bluespec System Verilog“ → 点击右侧”安装“ → 安装完成后,点击”启用“。

| ![图3](./readme_image/3.vscode_bsv.png) |
| :-----------------------------------: |
| **图3**:在vscode中安装BSV高亮扩展 |

启用该插件后,重启 vscode ,再打开的 .bsv 文件就有 BSV 的高亮。

 

# 4 项目组织与构建

本章讲述 BSV 的项目组织结构;以及用命令行进行编译、仿真、生成波形、生成 Verilog 的方式。

## 4.1 bsvbuild.sh 脚本

**bsvbuild.sh** 的命令格式如下:

```bash
bsvbuild.sh - [] [] [] []
```

其中:

- `` 是仿真顶层模块名,如果省略,默认为 `mkTb`
- `` 是仿真顶层模块所在的文件名,如果省略,默认为 `Tb.bsv`
- `` 是仿真打印(如果有仿真打印的话)的输出文件名,如果省略,则默认打印到 stdout(屏幕)
- `` 是 BSV 仿真的限制时间(单位:时钟周期),必须是一个正整数。如果省略,则为无穷(只在遇到 `$finish;` 时结束仿真)

而 `` 是一个重要的编译参数,其取值和含义如**表1**。

​ **表1**:**bsvbuild.sh** 的编译参数 `` 的取值及其含义。

| \ | 生成Verilog? | 仿真方式 | 仿真打印? | 生成仿真波形(.vcd)? |
| :-------: | :----------------: | :------: | :----------------: | :------------------: |
| -bs | | BSV | :heavy_check_mark: | |
| -bw | | BSV | | :heavy_check_mark: |
| -bsw | | BSV | :heavy_check_mark: | :heavy_check_mark: |
| -v | :heavy_check_mark: | - | | |
| -vs | :heavy_check_mark: | Verilog | :heavy_check_mark: | |
| -vw | :heavy_check_mark: | Verilog | | :heavy_check_mark: |
| -vsw | :heavy_check_mark: | Verilog | :heavy_check_mark: | :heavy_check_mark: |

可以看到, BSV 代码可以进行两种仿真方式:

* 直接用 BSV 仿真
* 生成 Verilog 后再仿真

这两种仿真方式的结果在正常情况下应该相同,这说明了 BSV 生成的 Verilog 正确性。据 BSV 官方说:BSV正确性是100%保证的,不会像 HLS 那样偶尔会出现 C 仿真与 C-Verilog co-simulation 结果不一致的情况。

另外,据我测试,Verilog 仿真的编译速度略微快于 BSV ,但 BSV 仿真的运行速度远远快于 Verilog。

 

## 4.2 单模块项目

BSV 项目是由**包** (package) 和**模块** (module) 来组织的。我们首先看看单包、单模块项目。打开 `src/1.Hello/Hello.bsv` 可以看到如下代码,它打印 `Hello World!` 后直接退出:

```bsv
// 代码路径:src/1.Hello/Hello.bsv
package Hello; // 包名: Hello。每个.bsv文件内只能有1个与文件名相同的包

module mkTb (); // 模块名: mkTb,该模块没有接口
rule hello; // 规则名: hello
$display("Hello World!"); // 就像 Verilog 的 $display 那样,
// 该语句不参与综合, 只是在仿真时打印
$finish; // 仿真程序退出
endrule
endmodule

endpackage
```

在代码所在的目录中打开命令行,运行以下命令:

```bash
# 在 src/1.Hello/ 目录下运行以下命令
$ bsvbuild.sh -bs mkTb Hello.bsv
```

该命令含义是:以 `mkTb` 为顶层模块,以 `Hello.bsv` 为顶层文件进行仿真,`-bs` 参数代表进行 BSV 仿真,只打印,不生成仿真波形文件。

该命令会产生如下输出。可以看到 **bsvbuild.sh** 调用了一些编译链接命令,然后进行仿真并打印出了 `Hello World!` ,最后因为遇到 `$finish;` 而结束。

```bash
top module: mkTb
top file : Hello.bsv
print simulation log to: /dev/stdout

maximum simulation time argument:

bsc -sim -g mkTb -u Hello.bsv
checking package dependencies
compiling Hello.bsv
code generation for mkTb starts
Elaborated module file created: mkTb.ba
All packages are up to date.
bsc -sim -e mkTb -o sim.out
Bluesim object created: mkTb.{h,o}
Bluesim object created: model_mkTb.{h,o}
Simulation shared library created: sim.out.so
Simulation executable created: sim.out

./sim.out > /dev/stdout
Hello World!
```

因为顶层模块名为默认名称 `mkTb` ,上述命令可以简化为:

```bash
# 在 src/1.Hello/ 目录下运行以下命令
$ bsvbuild.sh -bs Hello.bsv
```

你可以把以上命令的 `-bs` 参数改成**表1**中的其它参数,看看效果如何。

### 仿真打印到文件

上述仿真打印是显示在屏幕上的(也就是 `/dev/stdout` ),你也可以添加一个文件名作为参数,比如:

```bash
# 在 src/1.Hello/ 目录下运行以下命令
$ bsvbuild.sh -bs mkTb Hello.bsv display.txt
```

这样就会把 `Hello World!` 打印在一个新文件 `display.txt` 中(如果文件已存在,则覆盖),该文件名的后缀必须是 `.txt` 或 `.log` 。

当你需要大量仿真打印时,可以像这样指定一个仿真打印文件,而不是打印在屏幕上。

 

## 4.3 单包多模块项目

我们再看看如何组织单包、多模块项目。打开 `src/2.DecCounter/DecCounter.bsv` 。它的结构如下:

```bsv
// 代码路径:src/2.DecCounter/DecCounter.bsv (部分)
interface DecCounter; // 接口名 DecCounter,用于连接调用者和被调用者
method UInt#(4) count; // 方法1:可被被调用者调用
method Bool overflow; // 方法2:可被被调用者调用
endinterface

(* synthesize *)
module mkDecCounter (DecCounter); // 模块名 mkDecCounter,被调用者,接口是DecCounter
//...
method UInt#(4) count ... // 实现方法1
method Bool overflow ... // 实现方法2
endmodule

module mkTb (); // 模块名 mkTb ,调用者
DecCounter counter <- mkDecCounter; // 例化一个 mkDecCounter,并拿到它的接口,叫做 counter

// counter.count ... // 通过接口名 counter 来调用子模块,比如调用 count 方法
endmodule
```

上述代码首先定义了一个**接口** (interface),接口类型名为 `DecCounter`,其中包含两个**方法** (method)。然后规定模块 `mkDecCounter` 的接口为 `DecCounter` ,这样,它就必须实现接口 `DecCounter` 下的所有方法。然后 `mkTb` 模块中例化了一个 `mkDecCounter` 作为子模块,并拿到了它的接口,显然,该接口的类型为 `DecCounter` ,并被命名为 `counter` 。最后,`mkTb` 中可以调用 `counter` 的方法。

运行以下命令进行仿真:

```bash
# 在 src/2.DecCounter/ 目录下运行以下命令
$ bsvbuild.sh -bs mkTb DecCounter.bsv
```

该命令中只需指定顶层模块 `mkTb` ,无需指定子模块 `mkDecCounter` ,因为 BSV 编译器会自动找到 `mkDecCounter` 。

该命令打印如下:

```
省略前面的编译信息 ...
./sim.out > /dev/stdout
count= 0
count= 1
count= 2
count= 3
count= 4
count= 5
count= 6
count= 7
count= 8
count= 9
```

### 限制仿真时间

以上仿真是因为遇到了代码中的 `$finish;` 而停止的。你也可以在命令中添加一个正整数作为参数,来限制仿真时间,比如:

```bash
# 在 src/2.DecCounter/ 目录下运行以下命令
$ bsvbuild.sh -bs mkTb DecCounter.bsv 4
```

这样,该仿真最多只会运行4个时钟周期,在第4周期时即使没遇到 `$finish;` 也会停止。因此我们会看到该命令只会打印前三行:

```
省略前面的编译信息 ...
./sim.out -m 4 > /dev/stdout
count= 0
count= 1
count= 2
```

另外注意,用参数限制仿真时间只对 BSV 仿真有用,对 Verilog 仿真则没用。比如如果你运行如下命令,仿真不会在第4周期时停止。

```bash
# 在 src/2.DecCounter/ 目录下运行以下命令
$ bsvbuild.sh -vs mkTb DecCounter.bsv 4
```

> :point_right: 任何 BSV 仿真顶层代码中最好都要有 `$finish;` 来在适当的时候结束仿真,否则可能陷入死循环(按 Ctrl+C 可强制退出)。

### 生成 Verilog 代码

我们来 BSV 生成的 Verilog 是什么样。注意到 `mkDecCounter` 的定义上有一个 `(* synthesis *)` 属性,它告诉编译器,该 BSV 模块需要可综合,且单独生成一个 Verilog 模块。除了顶层模块 `mkTb` 必然要生成一个 Verilog 模块外,每个添加了 `(* synthesis *)` 的 BSV 模块都会生成 1 个 Verilog 模块,而不添加 `(* synthesis *)`的 BSV 模块会嵌入它的上级(调用者)的 Verilog 代码体内。

运行 Verilog 仿真命令:

``` bash
# 在 src/2.DecCounter/ 目录下运行以下命令
$ bsvbuild.sh -vs mkTb DecCounter.bsv
```

除了打印仿真信息外,以上命令还产生了两个 Verilog 文件。

- `mkDecCounter.v` : 包含 Verilog 模块 `mkDecCounter` 。
- `mkTb.v` : 包含 Verilog 模块 `mkTb` 。是仿真的顶层,上述仿真结果就是运行该模块所产生的。

如果删除 `mkDecCounter` 上方的 `(* synthesis *)` 属性,则上述命令只会产生1个 Verilog 模块 `mkTb` ,而 `mkDecCounter` 则被嵌入 `mkTb` 中。这能帮助我们缩减 Verilog 模块的数量——当一个 BSV 模块过于复杂时,为了提升可读性,把它拆分成多个 BSV 模块来实现;当生成 Verilog 后,这些 BSV 模块只产生一个 Verilog 模块,作为黑箱使用或发布。

 

## 4.4 多包项目

我们再看看如何组织多包、多模块项目。打开目录 `src/3.SPIWriter/` ,目录下有两个 `.bsv` 文件,每个文件内都有一个包 (package),其中 `SPIWriter.bsv` 就包含 2.1 节中所述的 SPI 发送控制器,而 `TbSPIWriter.bsv` 中的 `mkTb` 调用了 `mkSPIWriter` 进行仿真。与单包多模块项目不同的是,调用者 `mkTb` 与被调用者 `mkSPIWriter` 不在同一个包中,因此 `TbSPIWriter.bsv` 中用如下语句引入了被调用包:

```bsv
import SPIWriter::*; // 引入用户编写的包 SPIWriter (对应文件SPIWriter.bsv)
```

编译命令如下。只需给出顶层文件和顶层模块名,无需指定它调用的其它文件名或包名,编译器会自动寻找。

```bash
# 在 src/3.SPIWriter/ 目录下运行以下命令
$ bsvbuild.sh -bs mkTb TbSPIWriter.bsv
```

> 注:规范的 Verilog 项目中,每个 .v 文件只能包含一个模块。而规范的 BSV 项目中,每个 .bsv 文件只能包含一个包,但每个包可以包含多个模块,这些模块往往共同实现某个功能。

 

## 4.5 生成与查看波形

在目录 `src/3.SPIWriter/` 下,运行以下命令生成 Verilog 仿真波形。

```bash
# 在 src/3.SPIWriter/ 目录下运行以下命令
$ bsvbuild.sh -vw mkTb TbSPIWriter.bsv
```

运行后,发现生成了两个 Verilog 模块:`mkTb.v` 和 `mkSPIWriter.v` ,以及一个仿真波形文件 `mkTb_vw.vcd `,该波形文件就是以 `mkTb.v` 为顶层文件仿真而生成的(仿真引擎是 iverilog)。

因此,通过观察波形,我们可以理解 `mkTb.v` 如何通过各个输入输出信号与 `mkSPIWriter.v` 交互,进而理解 `mkSPIWriter.v` 的输入输出行为。将来我们要在 Verilog 项目中用到 SPI 发送器时,可以调用 `mkSPIWriter.v` 。

### 用 gtkwave 打开波形

为了查看波形,用 gtkwave 打开生成的波形文件 `mkTb_vw.vcd` 。如果你用是 Linux 实体机/虚拟机,运行命令:

```bash
# 在 src/3.SPIWriter/ 目录下运行以下命令
$ gtkwave mkTb_vw.vcd # 只有有 GUI 的 Linux 实体机/虚拟机 能运行该命令
```

如果你用的是 WSL ,请在 Windows 中把 3.3 节安装的 **gtkwave.exe** 设置为 `.vcd` 文件的打开方式,操作如**图4**。这样,今后只要双击 `.vcd` 文件就能查看波形。

| ![图4](./readme_image/4.set_vcd_as_gtkwave.png) |
| :----------------------------------------------------------: |
| **图4**:在 Windows 中,把 **gtkwave.exe** 设为 `.vcd` 文件的打开方式 |

### gtkwave 的基本使用

打开 **gtkwave** 后,按**图5**操作:

1. 在左上方展开模块层次。这里的 `top` 是 `mkTb` 模块的实例化;`spi_writer` 是子模块 `mkSPIWriter` 的实例化。我们选中 `top` 。
2. 在左下方寻找我们关注的信号,比如这里我们找到被调用者 `spi_writer` 的 4 个输入输出接口,选中它们。
3. 点击左下角的 Append ,把选中的信号加入右侧查看窗口。
4. `spi_writer$spi[2:0]` 信号包含了 `ss`, `sck` 和 `mosi`,为方便查看,双击它展开每个位 。
5. 用左上角的放大镜 :mag: 按钮调整波形缩放。

| ![图5](./readme_image/5.gtkwave_usage.png) |
| :--------------------------------------: |
| **图5**:**gtkwave** 基本用法 |

可以看出,**图5**与我们预想 SPI 的波形(**图1**)相同,说明 `mkSPIWriter` 的设计是成功的。

提示:每次编译后,新的 `.vcd` 文件都会覆盖旧的。此时没必要每次都重新打开 **gtkwave** ,那样就要重新添加信号,太麻烦了。我们只需在 **gtkwave** 左上角点击 File → Reload Waveform 即可。

> :point_right: 调试 BSV 代码时,建议结合仿真打印(来自$display()等)和查看波形这两种方式。很多硬件工程师习惯只看波形,虽然波形涵盖海量的细节信息,但令人眼花缭乱。而 $display() 能帮你快速打印你想要的信息。该经验也适用于 Verilog 调试。

 

 

# 5 类型与变量

类型 (type) 是编程语言对数据或资源的一个抽象,是为了提高代码可读性,并让编译器在编译期排查一些低级错误(例如计算一个IP地址的平方,虽然也可以算,但很可能是程序员粗心写出来的,因此报错)。要记住:对于 Verilog 和 BSV 这些 HDL,对任何类型的运算操作,最终都会编译成对若干比特位的数字逻辑运算。

与 Verilog 不同,BSV 是强类型语言,会进行严格的类型检查。每个变量都有一个类型,变量只能取与之兼容的值。

BSV 对大小写有严格的要求,类型名和常量的首字母总是大写,比如 `UInt`, `Bit`, `Bool`, `True`, `False` 。而变量名的首字母总是小写。例如对于以下的布尔变量(布尔变量 `Bool`,要么取`True`,要么取`False`),类型名 `Bool` 首字母大写,变量名 `oflow` 首字母小写:

```bsv
Bool oflow = cnt >= 9;
```

甚至对于接口变量也这样,比如以下代码实例化了模块 `mkSPIWriter` 并获得其接口,接口类型名 `SPIWriter` 首字母大写,接口变量名 `spi_writer` 首字母小写:

```bsv
SPIWriter spi_writer <- mkSPIWriter;
```

后续我们会理解为什么接口也是类型。在 BSV 中**万物皆变量或类型** 。

 

## 5.1 类型类

BSV 中的类型必须派生 (deriving) 自零个、一个或多个**类型类** (type class)。用户可以自定义类型类,但多数情况下使用预定义的类型类就够,如**表2**。

​ **表2**:BSV 中的类型类一览。

| 类型类 | 说明 |
| -------------- | ------------------------------------------------------------ |
| `Bits` | 派生出的类型的变量可以用 `pack()` 函数转换为位向量(`Bit#(n)`类型);反之,位向量也可以用 `unpack()` 函数转换为该类型的变量。 |
| `Eq` | 派生出的类型的变量之间可以判断是否相等。 |
| `Ord` | 派生出的类型的变量之间可以比较大小。 |
| `Arith` | 派生出的类型的变量之间可以进行算术运算(加减乘除)。 |
| `Literal` | 派生出的类型的变量可以创建自从整数字面量(例如123, 4567 这样)。 |
| `RealLiteral` | 派生出的类型的变量可以创建自从实数字面量(例如12.3, 45.67 这样)。 |
| `Bounded` | 派生出的类型的变量具有有限范围。 |
| `Bitwise` | 派生出的类型的变量之间可以进行按位运算(与、或、非等)。 |
| `BitReduction` | 派生出的类型的变量可以进行逐位合并运算来产生1位的结果(类比Verilog中的 \|a 写法)。 |
| `BitExtend` | 派生出的类型的变量可以进行位扩展操作。 |

> :pushpin: BSV 的类型类就像 C++ 中的虚类 (virtual class)。类可以派生自多个虚类,这在C++中叫做“多继承/多派生”。

例如,以下代码自定义了一个**结构体** (stuct) 类型,用来表示以太帧报头,类型名为 `EthHeader`,它派生自 `Bits` 和 `Eq` 类型类。

```bsv
typedef struct {
UInt#(48) dst_mac; // 成员变量1:目的地址
UInt#(48) src_mac; // 成员变量2:源地址
UInt#(16) pkt_type; // 成员变量3:帧类型
} EthHeader deriving(Bits, Eq); // 派生自的类型类是 Bits 和 Eq
```

对于 `EthHeader` 类型的两个变量:

```bsv
EthHeader hdr1 = EthHeader{dst_mac: 'h0123456789AB, src_mac: 'h456789ABCDEF, pkt_type: 'h0800};
EthHeader hdr2 = EthHeader{dst_mac: 'h0123456789AB, src_mac: 'h456789ABCDEF, pkt_type: 'h0860};
```

因为派生自 `Eq` 类型类,可以用 `==` 判断它们是否相等:

```bsv
hdr1 == hdr2 // 若相等,该语句返回 True,否则返回 False
// 只有当3个成员变量都相等时,才返回 True
```

又因为派生自 `Bits` 类型类,可以用 `pack()` 函数来把它转换为 `Bit#(112)` 类型的变量,也即把三个成员变量拼接成一个 112 位的向量:

```bsv
Bit#(112) bits = pack(hdr1); //结构体的成员变量共占 48+48+16=112 位
```

> :pushpin: `Bits` 是最重要的类型类,只有派生自 Bits 的类型的变量作为寄存器、FIFO、或存储器内的值时,才是**可综合**的。因为硬件中本质上都是位向量的逻辑运算。

BSV 中常用的类型转换函数如**表3**。注意 :如果代码中包含过多类型转换,表明类型设计或选择不佳,我们应该精心设计数据类型(例如数据向量类型、CAN总线帧类型等),让代码变得可读、可维护。

​ **表3**:BSV 中的类型转换函数一览。

| 函数名 | 类型类 | 说明 |
| ------------ | ----------- | ------------------------------------------------------------ |
| `pack` | `Bits` | 把派生自 `Bits` 类型类的类型的变量转化为位向量,也即`Bit#(n)`类型。 |
| `unpack` | `Bits` | 把位向量转化为派生自 `Bits` 类型类的类型,具体是什么类型,取决于 `=` 左值的类型。 |
| `truncate` | `BitExtend` | 高位截断,比如把 Int#(32) 截断为 Int#(16) 。具体截断为多少位,取决于 `=` 左值的类型。 |
| `zeroExtend` | `BitExtend` | 高位补零扩展,比如把 UInt#(16) 扩展为 UInt#(32) 。具体扩展为多少位,取决于 `=` 左值的类型。 |
| `signExtend` | `BitExtend` | 高位符号扩展,比如把 Int#(16) 扩展为 Int#(32) 。具体扩展为多少位,取决于 `=` 左值的类型。 |
| `extend` | `BitExtend` | 高位扩展,根据类型自动选择采用 `zeroExtend` 还是 `signExtend` |

 

## 5.2 基本数据类型

本节介绍 BSV 预定义的几种类型。它们都派生自 `Bits` 类型类,因此可以作为寄存器、FIFO、或存储器内的值,我们称之为**可综合数据类型**。

### Bit#(n) 类型

`Bit#(n)` 是 n 位向量,下面语句定义了一个 8 位向量,并给他赋值为 `'h12` (即16进制的`0x12`):

```bsv
Bit#(8) a = 'h12; //和 Verilog 中的 wire [7:0] a = 8'h12 效果类似
```

因为 1 位向量很常用,所以 BSV 还规定 `bit` 是 `Bit#(1)` 的别名:

```bsv
bit a = 'b1; // 等效于 Bit#(1) a = 'b1
```

`Bit#(8)` 类似 Verilog 中的 `wire [7:0]` ,不同之处在于:Verilog 的位宽检查很宽松,允许隐式的高位截断和扩展(虽然会报编译时 Warning),而 Verilog 的显式的截断和扩展写起来很不优雅:

```bsv
// 这是 Verilog !! 不是 BSV !!
wire [ 6:0] t1 = 7'h12;
wire [11:0] t2 = t1; // 隐式零扩展,会报编译时 Warning
wire [11:0] t3 = {5'h0, t1}; // 显式零扩展,需要手动给出位宽,有点难看
wire [ 3:0] t4 = t1; // 隐式截断,会报编译时 Warning
wire [ 3:0] t5 = t1[3:0]; // 显式截断,需要手动给出位宽,有点难看
```

BSV 严格进行位宽检查,只支持显式的高位截断和扩展,但写起来很优雅:

```bsv
Bit#(7) t1 = 'h12;
//Bit#(12) t2 = t1; // 隐式零扩展,错误!!!
Bit#(12) t3 = extend(t1); // 显式零扩展,自动根据左值(12位)判断出来要补 12-7=5位
//Bit#(4) t4 = t1; // 隐式截断,错误!!!
Bit#(4) t5 = truncate(t1); // 显式截断,自动根据左值(4位)判断出来要保留 4位
```

> :pushpin: 后续我们会学到 Bit#(n) 是一个多态类型(泛型),而 Bit#(7) 和 Bit#(4) 完全不是一种数据类型,这也解释了为什么 BSV 必须进行显式截断和扩展。

用常数对 `Bit#(n)` 类型的变量进行赋值时,和 Verilog 类似,可以用二进制、十进制或十六进制表示常数,举例如下 :point_down:

```bsv
Bit#(7) t1 = 7'b0010111; // 二进制,位宽匹配
Bit#(7) t2 = 'b0010111; // 二进制,位宽自动匹配
Bit#(7) t3 = 'b10111; // 二进制,位宽自动匹配(高位补零)
//Bit#(7) t4 = 5'b10111; // 二进制,位宽不匹配,错误!!
//Bit#(7) t5 = 'b10010111; // 二进制,位宽自动匹配失败!超出 7 位表示范围,错误!!
Bit#(7) t6 = 7'd123; // 十进制,位宽匹配
Bit#(7) t7 = 'd123; // 十进制,位宽自动匹配
Bit#(7) t8 = 123; // 十进制(省略'd),位宽自动匹配
//Bit#(7) t9 = 132; // 十进制(省略'd),位宽自动匹配失败!超出表示范围 0~127 ,错误!!
Bit#(7) t10= 7'h34; // 十六进制,位宽匹配
Bit#(7) t11= 'h56; // 十六进制,位宽自动匹配
Bit#(80) t12= 12; // 十进制,位宽自动匹配
```

注意 BSV 和 Verilog 定义常数时的不同点在于:

- Verilog 把省略位宽的常数(比如 `'b0010111` 、 `'d123` 、 `123` 和 `'h56` )都当作 32 位的,导致 `wire [6:0] t8 = 123` 会报编译时 Warning,因为进行了隐式截断。但 BSV 不对省略位宽的常数做假设,而是根据左值的位宽进行自动位宽匹配,因此 `Bit#(7) t8 = 123` 不报编译时 Warning 。
- 对于不省略位宽的常数,则必须保证左值和右值位宽相等,否则会报错。例如 `Bit#(7) t4 = 5'b10111` 会报错。

BSV 中还有一种 Verilog 没有的常数定义方式: `'0` 和 `'1` 。`'0` 代表所有位都为 0 ;`'1` 代表所有位都为 1 ,它们也会自动匹配左值的位宽。比如:

```bsv
Bit#(45) t1 = '1; // t1 的所有位置 1
Bit#(89) t2 = '0; // t2 的所有位置 0
```

与 Verilog 类似,`Bit#(n)` 类型支持位下标选择、位拼接、逐位合并运算、按位逻辑运算、算术运算(等效于无符号数算术运算)、大小比较(等效于无符号数比较)。记住:在进行这些运算时, BSV 依然会进行严格的位宽检查。

```bsv
Bit#(8) t1 = 'hFF;
Bit#(16) t2 = 'h10;
Bit#(16) t3 = 'h3456;

// 位下标选择、位拼接
Bit#(5) t4 = t3[12:8]; // 得到 5'h14
bit t5 = t3[7]; // 得到 1'b0
Bit#(13) t6 = { t3[2:0], t3[1:0], t1 }; // 得到 13'h1AFF

// 算术运算、按位逻辑运算
Bit#(16) t8 = t3 - t2; // 减法,得到 16'h3446
Bit#(16) t9 = t3 * t2; // 乘法,得到 16'h4560
//Bit#(16) t10 = t2 * t1; // t2 与 t1 位宽不同,报错!!
Bit#(16) t11= t2 * extend(t1); // t1 先拓展为 16'h00FF ,乘法得到 16'h0FF0
Bit#(16) t12= t2 | t3; // 按位或,得到 16'h3456
Bit#(16) t13= t2 | extend(t1); // 按位或,得到 16'h00FF

// 逐位合并运算
bit t14= &t2; // t2 的所有位求与,得到 1'b0
Bool t15= unpack(&t2); // t2 的所有位求与,要把 bit 类型转化为 Bool 类型,必须用 unpack

// 大小比较
Bool t16= t3 > t2; // t3 > t2 ? ,得到 True
bit t17= pack(t3 > t2); // t3 > t2 ? ,要把 Bool 类型转化为 bit 类型,必须用 pack
Bool t18= t2 > extend(t1); // t1 先拓展为 16'h00FF,再比较大小,得到 False
```

### UInt#(n) 类型

`UInt#(n)` 是 n 位的无符号数,取值范围为 `0~2^n-1` 。例如,考虑到 `2^11=2048`,所以 `UInt#(11)` 的取值范围为 `0~2047` 。`UInt#(n)` 用途很广,比如计数器变量。

与 `Bit#(n)` 不同点在于: `UInt#(n)` 不能进行位下标选择和位拼接:

```bsv
UInt#(4) t1 = 13;
//UInt#(2) t2 = t1[2:1]; // 错误!!
//UInt#(8) t3 = {t1, t1}; // 错误!!
```

除此之外,`UInt#(n)` 的特性与 `Bit#(n)` 相同,包括能进行常数赋值、 `extend()`(零扩展,等效于 `zeroExtend()` )、`truncate()` 、逐位合并运算、按位逻辑运算、算术运算、大小比较。但是注意:`UInt#(n)` 与 `Bit#(n)` 在运算中不能混用,如果要混用,就要用 `pack()` 和 `unpack()` 函数转换,比如:

```bsv
UInt#(16) t1 = 12;
Bit#(16) t2 = 34;
//UInt#(16) t3 = t1 + t2; // 错误!!因为 t1, t2 类型不同,不能混用
UInt#(16) t4 = t1 + unpack(t2); // 正确
Bit#(16) t5 = pack(t1) + t2; // 正确
```

再比如:

```bsv
UInt#(16) t1 = 12;
//bit t2 = &t1; // 错误的逐位合并,应该用 UInt#(1) 承接结果
UInt#(1) t3 = &t1; // 正确的逐位合并
```

### Int#(n) 类型

`Int#(n)` 是 n 位有符号数,取值范围为 `-2^(n-1) ~ 2^(n-1)` 。例如,考虑到 `2^10=1024`,所以 `Int#(11)` 的取值范围为 `-1024~1023` 。

`int` 是 `Int#(32)` 的别名, 取值范围为 `-2147483648 ~ 2147483648` ,是 C 语言的 `int` 的同义词。

> :pushpin: 之前讲过,所有的类型名的首字母都是大写,但 int 和 bit 是唯二的特例。

`Int#(n)` 不支持位下标选择和位拼接。除此之外,`Int#(n)` 支持其它操作:包括常数赋值、 `extend()`(这里是符号扩展,等效于 `signExtend()` )、`truncate()` 、逐位合并运算、按位逻辑运算、算术运算、大小比较。

特别注意: `Int#(n)` 之间比较大小用的是有符号比较,在 `extend()` 时也进行符号扩展,即把原来的最高位扩展到高位。看下面的例子:

```bsv
module mkTb();
rule extend_example;
UInt#(8) u1 = 'hFA; // 相当于十进制的 250
Int#(8) i1 = unpack(pack(u1)); //把u1转换成有符号数i1,会得到十进制的 -5
$display("%d", u1 > 2); //无符号比较,打印 1,显然 250 > 2 成立
$display("%d", i1 > 2); //有符号比较,打印 0,显然 -5 > 2 不成立

UInt#(16) u2 = extend(u1); //u1零扩展
Int#(16) i2 = extend(i1); //i1符号扩展
$display("u2=%x", u2); //打印 u2=00fa, 因为 fa 零扩展得到 00fa
$display("i2=%x", i2); //打印 i2=fffa, 因为 fa 符号扩展得到 fffa
$finish;
endrule
endmodule
```

以上代码中,有符号数和无符号数之间的互相转化方法是 `i1 = unpack(pack(u1))` ,这是一种常见的写法。

### Bool 类型

`Bool` 类型只有两种取值:`True` 和 `False` 。

虽然 `Bool` 底层是用 1 比特实现的,但 `Bool` 类型与 `bit` 类型不能混淆,它们之间可以用 `pack` 和 `unpack` 互相转化。

```bsv
Bool b1 = True;
bit b2 = pack(b1); // 得到 b2 = 1'b1;
Bool b3 = unpack(b2); // 得到 b3 = True
```

`Bool` 可以进行非(`!`)、且(`&&`)、或(`||`)运算。注意要区别于 `Bit#(n)` 的按位非(`~`)、按位与(`&`)、按位或(`|`):

```bsv
Bool b1 = True; // Bool :
Bool b2 = !b1; // 非
Bool b3 = b1 && b2; // 且
bit b4 = 1; // Bit#(n) :
bit b5 = ~b4; // 按位非
bit b6 = b4 & b5; // 按位与
```

所有类型的大小比较都会得到 `Bool` 类型,`if(cond)` 、 `while(cond)` 、`for(... ;cond ;...)` 中的条件表达式 `cond` 必须是 `Bool` 类型。

 

## 5.3 Integer 与 String 类型

本节介绍两种不派生自 `Bits` 类型类的类型,它们不能作为寄存器、FIFO 或存储器中的取值。

### Integer 类型

`Integer` 类型派生自 `Arith` 类型类,是数学上的整数,是无界的,对他进行算术运算永远不会溢出,不像 `UInt#(n)` 和 `Int#(n)` 是有界的。`Integer` 可以用于仿真,也可在可综合电路中作为循环下标,比如:

```bsv
int arr[16]; // 数组
for (int i=0; i<16; i=i+1) // 正确
arr[i] = i;
for (Integer i=0; i<16; i=i+1) // 也正确
arr[i] = fromInteger(i);
```

但 `Integer` 不能作为寄存器、FIFO或存储器中的取值:

```bsv
Reg#(int) <- mkReg(0); // 寄存器里存放 int 类型,正确
//Reg#(Integer) <- mkReg(0); // 寄存器里存放 Integer 类型,错误!!
```

### String 类型

`String` 类型表示一个字符串,一般用作仿真打印、指定仿真文件名等作用。 `String` 具有不定的长度,可以使用 `+` 拼接,比如:

```bsv
rule test;
String s = "Hello";
String t = "BSV Strings";
String r = s + t;
$display(r); // 会打印 HelloBSV Strings
endrule
```

要在 String 中指定一些特殊字符,需要用转义字符 `\` ,如**表4**。

​ **表4**:String 中的特殊字符。

| 写法 | 含义 |
| ------ | -------------------------- |
| `\r` | 回车符,ASCII 码是 0x0D |
| `\n` | 换行符,ASCII 码是 0x0A |
| `\t` | 制表符,ASCII 码是 0x09 |
| `\\` | 反斜杠,ASCII 码是 0x5C |
| `\"` | 双引号,ASCII 码是 0x22 |
| `\xHH` | ASCII 码是 0xHH 的任意字符 |

 

## 5.4 使用 $display 打印

`$display` 和 `$write` 是用来进行仿真打印的系统任务(system task),它们的使用方法类似 Verilog 的 `$display` 和 `$write` 。其中 `$display` 会在结尾自动打印换行 `'\n'` ,而 `$write` 不会。

调用格式是:

```bsv
$display(格式字符串, 变量1, 变量2, 变量3, ...);
```

比如:

```bsv
$display("a=%d b=%3d c=%08x d=%x e=%b", a, b, c, d, e );
```

`%d` , `%3d` , `%08x` 这些代表的是以什么格式打印一个变量,具体如**表5** 。

​ **表5**:$display 和 $write 中的格式打印。

| 格式串 | 含义 | 举例 |
| ------ | -------------------------------------------------------- | ------- |
| `%d` | 以十进制打印,根据变量位宽决定打印占用多少个字符 | `%d` |
| `%nd` | 以十进制打印,占用 n 个字符,不够则用空格补齐 | `%3d` |
| `%0nd` | 以十进制打印,占用 n 个字符,不够则用 '0' 补齐 | `%05d` |
| `%x` | 以十六进制打印,变量若为 n 位,则打印占用 (n+3)/4 个字符 | `%x` |
| `%nx` | 以十六进制打印,占用 n 个字符,不够则用空格补齐 | `%4x` |
| `%0nx` | 以十六进制打印,占用 n 个字符,不够则用 '0' 补齐 | `%016x` |
| `%b` | 以二进制打印,变量若为 n 位,则打印占用 n 个字符 | `%b` |
| `%nb` | 以二进制打印,占用 n 个字符,不够则用空格补齐 | `%8b` |
| `%0nb` | 以二进制打印,占用 n 个字符,不够则用 '0' 补齐 | `%032b` |

 

## 5.5 变量定义与赋值

### 变量定义

变量定义的格式是:

```
类型名 变量名;
```

比如:

```bsv
UInt#(6) value;
```

可以在变量定义时为它赋值,称作“初始化”,比如:

```bsv
UInt#(6) value = 42;
```

之后也可以继续赋值,比如:

```bsv
UInt#(6) value = 42;
value = 53;
```

### 值赋值 = 与副作用赋值 <-

BSV 有两种赋值符号:

- 值赋值 (`=`) :左边的变量(左值)被绑定到右边的值(右值),成为右值的一个副本。

- 副作用赋值 (`<-`) :右值会引起副作用,包括实例化了一个硬件实体、或引起了硬件状态(寄存器、触发器)的变化。例如:
- 实例化了一个模块并用 `<-` 获得其接口;

- 调用一个动作值方法 (ActionValue method) 并用 `<-` 获得其返回值(后续7.4节会细讲)。

在 5.2 节的各种例子里,我们一直用的是值赋值 (`=`) ,因为它仅仅是把表达式的值赋给了左值,没有实例化硬件实体,也没有引起硬件状态变化。

下面是一个区分 `=` 和 `<-` 的例子:

```bsv
module mkTb();
Reg#(int) ra <- mkReg(0); // 1. 定义一个 Reg#(int) 类型的接口变量 ra 。
// mkReg(0) 实例化一个初值=0的寄存器,ra 拿到了该寄存器的接口

Reg#(int) rb = ra; // 2. 定义一个 Reg#(int) 类型的接口变量 rb 。
// 并没有实例化寄存器,而是让 rb 成为 ra 的别名

Reg#(int) rc <- mkReg(0); // 3. 定义一个 Reg#(int) 类型的接口变量 rc 。
// mkReg(0) 实例化一个初值=0的寄存器,rc 拿到了该寄存器的接口

rule increase;
ra <= ra + 1; // 每个时钟周期都让 ra + 1
rc <= rc + 2; // 每个时钟周期都让 rc + 2
$display("ra = %1d, rb = %1d, rc = %1d", ra, rb, rc); // 会发现 ra 与 rb 值永远相等
// rc 则与 ra 不同
endrule

rule exit;
if(ra > 5) $finish;
endrule
endmodule
```

后续我们会学到,`Reg#(int)` 是“寄存器的接口”类型,且该寄存器存放 `int` 类型。而 `Reg#(int) rb = ra;` 语句并没有实例化寄存器,而是让 `rb` 成为 `ra` 的别名,所以要用值赋值 (`=`)。而 `Reg#(int) rc <- mkReg(0);` 语句实例化了一个寄存器,让 `rc` 拿到了该寄存器的接口,所以要用副作用赋值 (`<-`) 。

仿真打印如下。可以看出,因为 `rb` 是 `ra` 的别名,所以他俩永远都一样。而 `rc` 独立于 `ra` ,与 `ra` 的值无关。

```
ra = 0, rb = 0, rc = 0
ra = 1, rb = 1, rc = 2
ra = 2, rb = 2, rc = 4
ra = 3, rb = 3, rc = 6
ra = 4, rb = 4, rc = 8
ra = 5, rb = 5, rc = 10
```

> :pushpin: 注意:寄存器更新语句 `ra <= ra + 1` 中用到了写入符号 (`<=`),该符号不是赋值符号,是寄存器写入方法 `ra._write(ra + 1)` 的简写,要与 `=` 和 `<-` 区分开 (后续6.1节会细讲)。

### let 语句

在定义变量并赋初始值时,如果右值的类型可以被编译器推断出来,则左值的类型名可以省略,用 `let` 关键字代替。举例:

```bsv
UInt#(8) a = 1;
UInt#(8) b = 2;
UInt#(8) c = a + b; //定义变量 c,不省略类型声明
let d = a + b; //定义变量 d,
//可以推断出 d 的类型也是 UInt#(8) ,因此省略类型声明
```

当类型名比较复杂时,可以用 `let` 来简化代码。

注意: `let` 只用在变量定义并赋初始值时。对已经定义过的变量,直接赋值即可,不要用 `let` ,举例:

```bsv
UInt#(8) a = 1;
UInt#(8) b = 2;
// let b = a + b; // 错误!! 不要用 let ,因为 b 已经定义过了
b = a + b; // 正确,直接赋值即可
```

注意:不要在无法推断右值的类型时使用 `let` 。比如以下例子中,可以用 `let sub <- mkSub;` 语句来定义一个 `Sub` 类型的接口,因为用户自定义的接口 `Sub` 不是多态接口类型,而是一个确定的接口类型,且代码明确指出 `mkSub` 的接口是 `Sub`, 编译器能推断出 `mkSub` 一定会获得 `Sub` 类型的接口。但是,不要用 `let ra <- mkReg(0);` 语句来定义一个寄存器接口,因为 `Reg#()` 是一个多态类型接口,仅靠右值 `mkReg(0)` 无法推断出左值应该是 `Reg#(int)` 还是 `Reg#(bit)` 抑或是 `Reg#(UInt#(8))` 之类的类型。

```bsv
interface Sub; // 自定义的接口,不是多态类型,是确定的数据类型
method int read;
endinterface

module mkSub (Sub); // mkSub 模块的接口是 Sub
// ...
endmodule

module mkTb ();
let sub <- mkSub; // 可以用 let
// mkSub 一定会获得 Sub 类的接口,无歧义
// 等效于 Sub sub <- mkSub;

// let ra <- mkReg(0); // 不能用 let !!!
// 不知道 mkReg 会获得 Reg#(int) 还是 Reg#(bit) 还是 Reg#(UInt#(8)) 之类的接口
// 这里要用完整类型名,比如 Reg#(bit) <- mkReg(0);
endmodule
```

 

## 5.6 组合逻辑电路

有了上述变量定义、赋值、运算的知识,你已经能写出组合逻辑电路了!众所周知,在 C/C++、Java、Python 等计算机编程语言中,定义的变量会对应一片内存空间(比如C语言的全局变量放在 BSS 段、局部变量放在栈空间)。不同于它们,BSV 中定义的变量永远不会占用存储空间(比如寄存器、存储器),只代表一个编译时的符号;或者代表电线中的一个节点。因此,用变量和表达式可以组成组合逻辑电路。

下面展示组合逻辑的样例——把二进制编码的数字转化为格雷码、再从格雷码转化回二进制编码。

> :pushpin: 扩展阅读:格雷码在+1或-1时,只会导致1个比特位变化,可以把跨时钟域采样导致的冒险的误差降低到最小。典型的应用是作为异步 FIFO 中存储器指针。详见: https://en.wikipedia.org/wiki/Gray_code

二进制码转化为格雷码的方式是 “右移1位后与自身进行异或”。例如 6 位二进制码转化为格雷码,用 Verilog 编写如下:

```verilog
// 这是 Verilog !
// 二进制编码的 wire [5:0] bin 转化为格雷码 gray
wire [5:0] gray = (bin >> 1) ^ bin;
```

格雷码转化为二进制码则略为麻烦:

- 二进制码最高位 = 格雷码最高位
- 二进制码第2高位 = 格雷码第2高位 异或 二进制码最高位
- 二进制码第3高位 = 格雷码第3高位 异或 二进制码第2高位
- ……

用 Verilog 编写如下,该组合逻辑比较复杂,所以用 `always @ (*)` 而不是 `assign` 来写

```verilog
// 这是 Verilog !
// 格雷码的 wire [5:0] gray 转化为二进制码 bin
reg [5:0] bin;
always @ (*) begin
bin[5] = gray[5];
bin[4] = gray[4] ^ bin[5];
bin[3] = gray[3] ^ bin[4];
bin[2] = gray[2] ^ bin[3];
bin[1] = gray[1] ^ bin[2];
bin[0] = gray[0] ^ bin[1];
end
```

我们用 BSV 实现二进制码与格雷码的互相转化。

目录 `src/4.GrayCode/` 里有五个 .bsv 文件,分别展示了 BSV 中五种实现组合逻辑电路的方法。

### 写法1:写在规则内(没用循环)

首先打开`GrayCode_v1.bsv` ,如下,它把组合逻辑表达式写在**规则**(rule)内,这样,获得的结果(`cnt_gray` 和 `cnt_bin` 变量)的作用域就仅限于该规则,不像上述 Verilog 代码那样,`reg [5:0] bin` 的作用域是整个模块。限制变量作用域有利于提高可读性!

```bsv
// 代码路径:src/4.GrayCode/GrayCode_v1.bsv (部分)
module mkTb ();

Reg#(Bit#(6)) cnt <- mkReg(0);

rule up_counter; // 每周期都执行
cnt <= cnt + 1; // cnt 从0自增到63
if(cnt >= 63) $finish; // 自增到 63 时,仿真结束
endrule

rule convert; // 每周期都执行
// 把 cnt (二进制编码)转化为 cnt_gray (格雷码)
Bit#(6) cnt_gray = (cnt >> 1) ^ cnt;

// 把 cnt_gray (格雷码) 转化回 cnt_bin (二进制编码)
Bit#(6) cnt_bin = cnt_gray;
cnt_bin[4] = cnt_gray[4] ^ cnt_bin[5];
cnt_bin[3] = cnt_gray[3] ^ cnt_bin[4];
cnt_bin[2] = cnt_gray[2] ^ cnt_bin[3];
cnt_bin[1] = cnt_gray[1] ^ cnt_bin[2];
cnt_bin[0] = cnt_gray[0] ^ cnt_bin[1];

$display("cnt=%b cnt_gray=%b cnt_bin=%b", cnt, cnt_gray, cnt_bin );
endrule

endmodule
```

以上代码打印如下(只展示前8行):

```
cnt=000000 cnt_gray=000000 cnt_bin=000000
cnt=000001 cnt_gray=000001 cnt_bin=000001
cnt=000010 cnt_gray=000011 cnt_bin=000010
cnt=000011 cnt_gray=000010 cnt_bin=000011
cnt=000100 cnt_gray=000110 cnt_bin=000100
cnt=000101 cnt_gray=000111 cnt_bin=000101
cnt=000110 cnt_gray=000101 cnt_bin=000110
cnt=000111 cnt_gray=000100 cnt_bin=000111
```

### 写法2:写在规则内

打开 `GrayCode_v2.bsv` ,看到它与写法1的不同是:把格雷码转二进制码中的重复性高的五行写成了 `for` 循环。这个 `for` 循环可综合,且不代表任何时序行为,编译器会把它**全展开**(unroll)成纯组合逻辑电路。这样的好处是提高了可读性。(实际上 Verilog 的 `always` 块里也能写 `for` 循环):

```bsv
// 代码路径:src/4.GrayCode/GrayCode_v2.bsv (部分)
Bit#(6) cnt_bin = cnt_gray;
for(int i=4; i>=0; i=i-1)
cnt_bin[i] = cnt_gray[i] ^ cnt_bin[i+1];
// for 循环完全展开(unroll)后,等效于:
//cnt_bin[4] = cnt_gray[4] ^ cnt_bin[5];
//cnt_bin[3] = cnt_gray[3] ^ cnt_bin[4];
//cnt_bin[2] = cnt_gray[2] ^ cnt_bin[3];
//cnt_bin[1] = cnt_gray[1] ^ cnt_bin[2];
//cnt_bin[0] = cnt_gray[0] ^ cnt_bin[1];
```

### 写法3:写在模块内

打开 `GrayCode_v3.bsv` ,看到它与写法2的不同是:把相关的变量定义和计算都移动到了规则外,但仍然在模块内,形如:

```bsv
// 代码路径:src/4.GrayCode/GrayCode_v3.bsv (部分)
module mkTb ();

Reg#(Bit#(6)) cnt <- mkReg(0);

// 把 cnt (二进制编码)转化为 cnt_gray (格雷码)
Bit#(6) cnt_gray = (cnt >> 1) ^ cnt; // cnt_gray 会根据 cnt 的变化而变化

// 把 cnt_gray (格雷码) 转化回 cnt_bin (二进制编码)
Bit#(6) cnt_bin = cnt_gray;
// 该循环不表示任何时序行为,编译器会把它完全展开(unroll)为组合逻辑
for(int i=4; i>=0; i=i-1)
cnt_bin[i] = cnt_gray[i] ^ cnt_bin[i+1]; // cnt_bin 会根据 cnt_gray 的变化而变化

//...
```

用写法3,`cnt_gray` 和 `cnt_bin` 变量的作用域就是整个模块,本模块的任何规则都能访问组合逻辑电路的结果。这种作用域与 Verilog 类似——所有 `wire` 都是全局的,模块内的任何 `always` 块都能访问。

### 写法4:写成函数,写在模块内

打开 `GrayCode_v4.bsv` ,看到它把二进制码转格雷码、格雷码转二进制码写成了**函数** (function) :

```bsv
// 代码路径:src/4.GrayCode/GrayCode_v4.bsv (部分)
module mkTb ();

// 函数:把二进制编码转化为格雷码
function Bit#(6) binary2gray(Bit#(6) value); // 输入参数:Bit#(6) ,返回 Bit#(6)
return (value >> 1) ^ value;
endfunction

// 函数:把格雷码转化为二进制编码
function Bit#(6) gray2binary(Bit#(6) value); // 输入参数:Bit#(6) ,返回 Bit#(6)
for(int i=4; i>=0; i=i-1)
value[i] = value[i] ^ value[i+1];
return value;
endfunction

//...
```

本模块内可以调用这两个函数,每处调用都会实例化一个组合逻辑电路:

```bsv
// 代码路径:src/4.GrayCode/GrayCode_v4.bsv (部分)
rule convert;
Bit#(6) cnt_gray = binary2gray(cnt); // 调用函数 binary2gray
Bit#(6) cnt_bin = gray2binary(cnt_gray); // 调用函数 gray2binary
$display("cnt=%b cnt_gray=%b cnt_bin=%b", cnt, cnt_gray, cnt_bin );
endrule
```

函数可以单次定义,多次调用。对于常用、普适的组合逻辑电路,推荐用函数!

### 写法5:写成函数,写在模块外

打开 `GrayCode_v5.bsv` ,与写法4唯一的不同是,它把两个函数定义在了模块外。这样,它们就不再属于某个模块,而是属于整个包,能被包内的所有模块调用。另外,其它包(文件)也可以用引入语句:

```bsv
import GrayCode_v5::*;
```

然后就可以调用 `GrayCode_v5` 里的这两个函数。对于工具性更强组合逻辑电路,可以用这种方式,做到单次定义,多包调用。

如果需要函数具有“多个返回值”的效果,可以使用 Tuple 数据类型,将在 8.3 节讲到。

 

## 5.7 元组 Tuple

元组相关的代码见 `src/5.TupleTest/TupleTest.bsv`

元组是把多个类型的变量放在一起的复合数据类型, BSV 预定义了二元组、三元组、四元组、……、八元组。

以下语句定义了一个二元组并赋值:

```bsv
Tuple2#(Bool, Int#(9)) t2 = tuple2(True, -25);
```

类型名为 `Tuple2#(Bool, Int#(9))` ,说明该二元组由一个 `Bool` 类型与一个 `Int#(9)` 类型组成。该二元组的变量名为 `t2` 。`tuple2()` 是一个函数,用于构建二元组。

同理,以下语句定义一个八元组并赋值。

```bsv
Tuple8#(int, Bool, Bool, int, UInt#(3), int, bit, Int#(6)) t8
= tuple8(-3, False, False, 19, 1, 7, 'b1, 45);
```

用函数 `tpl_1()` 、 `tpl_2()` 、 `tpl_3()` 、... 可以获得元组的第 1, 2, 3, … 个元素:

```bsv
// 代码路径:src/5.TupleTest/TupleTest.bsv (部分)
Bool v1 = tpl_1(t2); // 获取 t2 的第一个元素,得到 True
int v2 = tpl_2(t2); // 获取 t2 的第二个元素,得到 -25
//Bool v3=tpl_3(t2); // 不能获取 t2 的第三个元素,因为 t2 是 Tuple2
Bool v3 = tpl_3(t8); // 获取 t8 的第三个元素,得到 False
```

元组的方便之处在于,可以用 `match` 语句来承接元组中的元素,比如如下例子:(它会自动定义两个变量 v1 和 v2)。

```bsv
match {.va, .vb} = t2; // 隐式定义了2个变量来承接 t2 的值
// va 是 Bool 类型的 True
// vb 是 Int#(9) 类型的 -25
```

下面看看元组的用途,如果函数 (function) 和方法 (method) 想要达到“多个返回值”的效果,就可以用元组。我们用 BSV 的预定义函数 `split()` 举例,它可以把一个 `Bit#()` 类型拆分成两个 `Bit#()` 。它的函数原型如下:

```bsv
function Tuple2#(Bit#(m), Bit#(n)) split (Bit#(mn) xy) // split函数返回二元组,两个元素是拆分后的 Bit#()
provisos (Add#(m,n,mn)); // provisos 关键字是函数声明中的补充要求,
// 这里要求 m + n == mn
// 因为拆分后的位宽加起来要等于拆分前的位宽
```

比如我们要把一个 `Bit#(13)` 变量拆成 `Bit#(8)` (高位)和一个 `Bit#(5)` (低位),可以用:

```bsv
Bit#(13) b13 = 'b1011100101100;
Tuple2#(Bit#(8), Bit#(5)) tsplit = split(b13);
match {.b8, .b5} = tsplit; // 得到 b8='b10111001 b5=01100
```

 

## 5.8 Maybe 类型

`Maybe#(td)` 是 BSV 预定义的一种多态类型,他能给任意类型(设类型名为 `td`)的数据附加上“是否有效”的信息。

以下代码中,我们定义两个 Maybe 类型的变量,它们中的数据类型都是 `Int#(9)` ,一个无效,一个有效:

```bsv
Maybe#(Int#(9)) value1 = tagged Invalid; // 无效
Maybe#(Int#(9)) value2 = tagged Valid 42; // 有效,取值为 42
```

BSV 针对 `Maybe#(td)` 类型提供了两个函数:

- `isValid(x)` : 接受 `Maybe#(td)` 类型的变量 `x` 作为参数:
- `x` 无效则返回 False
- `x` 有效则返回 True

- `fromMaybe(dv, x)` : 接受 `td` 类型的变量 `dv` 和 `Maybe#(td)` 类型的变量 `x` 作为参数:
- `x` 无效则返回 `dv`
- `x` 有效则返回 `x` 中的取值。

使用例:

```bsv
let v1 = isValid(value1); // 得到 v1 是 Bool 类型的 False
let d1 = fromMaybe(-99, value1); // 得到 d1 是 Int#(9) 类型的 -99
let v2 = isValid(value2); // 得到 v2 是 Bool 类型的 True
let d2 = fromMaybe(-99, value2); // 得到 d2 是 Int#(9) 类型的 42
```

 

 

# 6 时序逻辑电路

本章我们会学习时序逻辑电路的描述方法,包括两类重要的模块:寄存器 `Reg` 、线网 `Wire` ;以及 BSV 的重要概念—— 规则 (rule) 及其调度机制和属性。

## 6.1 寄存器 Reg

寄存器是一类用于保存数据(或者叫保存电路状态)的模块。本节涉及:

- 接口`Reg#()` 以及其配套的模块 `mkReg` 、 `mkRegU` 、 `mkDReg`

### mkReg 和 mkRegU

`mkReg` 和 `mkRegU` 都是模块名,用来实例化寄存器,唯一的区别是 `mkRegU` 的初始值未知(dont-care,可能是0或1),转化成 Verilog 后,你会发现 `mkReg` 定义的寄存器会在同步复位信号 `RST_N` 的控制下恢复默认值,而 `mkRegU` 不会。

以下例子定义并实例化了两个可以存储 `int` 值的寄存器 `x` 和 `y` :

```bsv
Reg#(int) x <- mkReg(23); //初值=23
Reg#(int) y <- mkRegU; //初值未知
```

根据左值的类型 `Reg#(int)` ,编译器得知该寄存器中存储的数据的类型是 `int` ,那么被实例化的寄存器位宽显然就是 32 位。

`Reg#(int)` 是一个**接口** (interface) 的类型名,`int` 可以换成其它任何类型,因此 `Reg#()` 是个多态接口,其定义为:

```bsv
interface Reg#(type td); // 寄存器中存储的数据的类型名为 td ,可能是任何类型
method Action _write (td x); // 该方法用于把 td 类型的变量 x 写入寄存器
method td _read; // 该方法用于读出寄存器的值,得到 td 类型的返回值
endinterface
```

而 `mkReg` 的模块定义为:

```bsv
module mkReg#(td v) (Reg#(td)) // 第一个括号里是模块参数,是一个类型为 td 的变量 v ,这里是作为寄存器初始值。
// 第二个括号里,表示 mkReg 具有 Reg#(td) 类型的接口
provisos (Bits#(td, sz)); // 要求 td 派生自 Bits 类型类,即寄存器的值必须有特定的位宽(保证寄存器可综合)
```

以上 `interface Reg#(type td)` 的定义中有两个方法: `_write` 和 `_read`,其中 `_write` 方法用于写入寄存器;`_read` 方法用于读寄存器 。比如对于我们熟悉的计数器,完整的写法是:

```bsv
module mkTb ();
Reg#(int) x <- mkReg(23);

rule up_counter; // rule 每时钟周期都会执行一次
x._write( x._read + 1 ); // 寄存器的x的值先读出来,+1后再写回去
$display ("x=%d", x._read );
endrule

rule done (x >= 26); // 只有满足条件 x >= 26 的时钟周期才会执行退出
$finish;
endrule
endmodule
```

因为寄存器的读写非常常用,所以 BSV 规定可以用寄存器名本身代替 `_read` ,用写入符号 `<=` 代替 `_write` 。

```bsv
x <= x + 1; // 简化写法,等效于 x._write( x._read + 1 );
$display ("x=%d", x ); // 简化写法,等效于 $display ("x=%d", x._read );
```

以上代码的仿真打印如下:

```
x= 23
x= 24
x= 25
```

第一行打印的是 `x=23` ,考虑到寄存器的初始值是 `23` ,说明打印的是当前时钟周期的旧值,而不是执行 `x<=x+1` 后的新值。这也符合同步时序逻辑的行为——时钟上升沿时,只能采样到寄存器的旧值;对于当前上升沿写入寄存器的值,等到下个时钟上升沿才能采到。

### mkDReg

BSV 还提供了一种实用的寄存器模块 `mkDReg` ,它和 `mkReg` / `mkRegU` 具有相同的接口 `Reg#(type td)` 。区别是 `mkDReg` 只在写入后的下一个周期读出写入的值,其余周期都会读出默认值。也就是说: `mkDReg` 只能保留一周期的写入结果。

使用 `mkDReg` 前,需要先引入包:

```bsv
import DReg::*;
```

`mkDReg` 举例如下:

```bsv
// 代码路径:src/6.RegTest/RegTest.bsv (部分)
module mkTb ();
Reg#(int) cnt <- mkReg(0);

rule up_counter; // rule 每时钟周期都会执行一次
cnt <= cnt + 1;
if(cnt > 9) $finish;
endrule

Reg#(int) reg1 <- mkReg(99); // reg1 初值 = 99
Reg#(int) reg2 <- mkDReg(99); // reg2 默认值 = 99

rule test (cnt%3 == 0); // rule条件:只在能整除3的周期执行,相当于每3周期执行一次
reg1 <= -cnt;
reg2 <= -cnt;
endrule

rule show;
$display("cnt=%2d reg1=%2d reg2=%2d", cnt, reg1, reg2);
endrule
endmodule
```

本例中,我们在 `cnt=0, 3, 6, 9` 时写入了 `reg1` 和 `reg2 `,考虑到 `reg1` 来自 `mkReg` 模块,它的初始值 `99` 只在最开始有,而之后每次写入都更新为新值,且在下次写入前不会改变。而 `reg2` 来自 `mkDReg` 模块,在每次写入的下一个周期会读出写入值,其余周期都读出默认值 `99` 。仿真打印的结果支持了该结论:

```
cnt= 0 reg1=99 reg2=99
cnt= 1 reg1= 0 reg2= 0
cnt= 2 reg1= 0 reg2=99
cnt= 3 reg1= 0 reg2=99
cnt= 4 reg1=-3 reg2=-3
cnt= 5 reg1=-3 reg2=99
cnt= 6 reg1=-3 reg2=99
cnt= 7 reg1=-6 reg2=-6
cnt= 8 reg1=-6 reg2=99
cnt= 9 reg1=-6 reg2=99
cnt=10 reg1=-9 reg2=-9
```

`mkDReg` 在刚性流水线向后传递时非常有用。你当然可以用 `mkReg` 配合一些规则来实现 `mkDReg` 相同的效果,但在合适的地方用 `mkDReg` 可以降低代码量。后续我们会看到,BSV 提供了大量类似于此的常用模块库。

### e.g. 开平方计算流水线 v1

本例子用 `mkDReg` 构成一个 16 级**刚性流水线**电路,用来计算 `UInt#(32)` 类型的开平方 (sqrt)。

开平方算法使用逐次逼近迭代法(可以避免乘法运算,降低资源开销),Python 代码如下,它是一个 16 次迭代的过程(不懂 Python 就把它当做伪代码看):

```python
# Python 实现整数开方
# 效果: y = sqrt(x)
x = 114514 # 输入数据
y = 0 # 待计算的开方结果
for n in range(15, -1, -1): # 迭代 16 次,迭代变量 n=15,14,13,...,2,1,0
t = (y<<1<= t: # 迭代体
x -= t # 迭代体
y += (1<= t) begin
x = x - t;
y = y + (1<=0; n=n-1)
dregs[n] <- mkDReg( tuple2(0, 0) );
```

然后我们编写计算行为。每级流水线的行为都是调用迭代函数进行迭代计算,是高度重复的,所以用 for 循环批量生成 16 个规则 (rule) ,每个规则都从上一级段寄存器中取出数据,经过 `sqrtIteration` 函数完成迭代计算,然后写入下一级寄存器:

```bsv
for(int n=15; n>=0; n=n-1) // 该 for 循环用来批量部署 rule
rule pipe_stages;
dregs[n] <= sqrtIteration( dregs[n+1] , n );
endrule
```

最后编写测试代码,每周期向最前级寄存器 `dregs[16]` 写入想要开方的数据,从最末级寄存器 `dreg[0]` 拿出开方结果:

```bsv
// 代码路径:src/15.Sqrt/Sqrt_v1.bsv (部分)
Reg#(UInt#(32)) cnt <- mkReg(1);
rule sqrter_input;
UInt#(32) x = cnt * 10000000; // x 是待开方的数据
dregs[16] <= tuple2(x, 0); // 把 x=x, y=0 写入最前级流水段寄存器
$display("input:%d output:%d", x, tpl_2(dregs[0])); // 从流水线最末级寄存器拿出数据
cnt <= cnt + 1;
if(cnt > 40) $finish;
endrule
```

打印结果不在这里展示,读者可自行运行 `src/15.Sqrt/Sqrt_v1.bsv` 来验证。因为从输入到输出之间有 17 级流水线,所以每个打印的输出数据对应的是 17 行之前的输入数据。也就是说:该流水线的延迟是17周期。但不影响吞吐率高达1数据/周期

> :triangular_flag_on_post: 目前我们没学模块定义和调用,所以该代码的实现和测试是放在同一个模块中的,没什么实际使用价值。8.2 节中我们将把它实现为模块,并使用 FIFO 给他加入反压 (back-pressure) 功能。

 

## 6.2 读写顺序与调度注解

熟悉 Verilog 的读者应该知道,寄存器每周期只能写入一个值,但能在任何地方被读取,且读到的值永远是上一周期写入的旧值。为了保证能读到旧值,我们说,在同一个时钟周期内,**在逻辑上**,寄存器的 `_read` 方法必须先于 `_write` 方法执行。

为了约定同一个周期内的方法之间的**顺序约束**,BSV 规定了如**表6** 的六种**调度注解** (Scheduling Annotation)。

​ **表6**:BSV 规定的六种**调度注解**。其中 mA 和 mB 是同一个模块的两个方法。

| 调度注解 | 顺序要求 | 规则放置要求 | 备注 |
| -------- | ----------------------------- | --------------------------------------- | --------------- |
| **CF** | mA 和 mB 可以任意颠倒顺序 | mA 和 mB 可以写在同一个规则或不同的规则 | |
| **SB** | mA 必须在 mB 之前执行 | mA 和 mB 可以写在同一个规则或不同的规则 | 和 **SA** 互逆 |
| **SA** | mA 必须在 mB 之后执行 | mA 和 mB 可以写在同一个规则或不同的规则 | 和 **SB** 互逆 |
| **SBR** | mA 必须在 mB 之前执行 | mA 和 mB 只能写在不同的规则 | 和 **SAR** 互逆 |
| **SAR** | mA 必须在 mB 之后执行 | mA 和 mB 只能写在不同的规则 | 和 **SBR** 互逆 |
| **C** | mA 和 mB 无法在同一个周期执行 | mA 和 mB 只能写在不同的规则 | |

寄存器(包括 `mkReg`, `mkRegU`, `mkDReg`)具有如**表7**的调度注解。

​ **表7**:寄存器的调度注解。

| mkReg、mkRegU、mkDReg | _read | _write |
| --------------------- | ------ | ------- |
| **\_read** | **CF** | **SB** |
| **\_write** | **SA** | **SBR** |

对**表7**解读如下:

- _read **CF** _read:两次 `_read` 之间不存在冲突,可以以任意的顺序排列。
- _read **SB** _write:代表 `_read` 必须排到 `_write` 之前,保证读到旧值。且 `_read` 和 `_write` 可以放在同一规则内。
- _write **SA** _read:代表 `_write` 必须排到 `_read` 之后,也就是上一条调度注解反过来。
- _write **SBR** _write: 代表两次 `_write` 不能放在同一个规则内。但可以放在不同规则内(后一次 `_write` 会覆盖前一次 `_write` 的数据 ,成为下一周期的寄存器值)

BSV 的每一个硬件模块的方法之间都有调度注解,用来指示该模块的方法在同一周期内的**逻辑执行顺序**。在 6.3 节中,我们会看到规则之间也有逻辑执行顺序,编译器会根据它们调用的方法的调度注解来排列多个规则在同一个时钟周期内的逻辑执行顺序。

为了理解寄存器的 _write **SBR** _write ,试试编译如下代码:

```bsv
module mkTb ();
Reg#(int) cnt <- mkReg(0);
rule up_counter;
cnt <= cnt + 1;
if(cnt > 1) $finish;
endrule

Reg#(int) x <- mkReg(0);

rule test1;
x <= cnt + 1; // x._write
x <= cnt + 99; // x._write 再次
endrule

rule show;
$display("cnt=%3d x=%3d", cnt, x);
endrule
endmodule
```

编译时会报错如下。因为 **SBR** 不允许两次 `_write` 放在同一个规则里。

```
Error: "test.bsv", line 15, column 9: (G0004)
Rule `RL_test1' uses methods that conflict in parallel:
x.write(...)
and
x.write(...)
For the complete expressions use the flag `-show-range-conflict'.
```

但我们可以写成如下这样,因为两个分支只能执行一个,所以不报任何 Warning 。

```bsv
rule test1;
if(cnt%2 == 0) // 判断cnt能否整除2
x <= cnt + 1;
else
x <= cnt + 99;
endrule
```

我们还可以把两条 `x._write` 放在不同的规则里:

```bsv
Reg#(int) x <- mkReg(0);

rule test1;
$display("test1");
x <= cnt + 1;
endrule

rule test2;
$display("test2");
x <= cnt + 99;
endrule

rule show;
$display("cnt=%3d x=%3d", cnt, x);
endrule
```

编译时,会产生如下编译时 Warning :point_down: ,表明检测到两个规则都想在每周期执行 `x._write` ,因为 **SBR** 要求必须确定两次 `_write` 的顺序,编译器自作主张地让规则 `test1` 先于规则 `test2` 执行。

```
Warning: "test.bsv", line 3, column 8: (G0036)
Rule "test1" will appear to fire before "test2" when both fire in the same
clock cycle, affecting:
calls to x.write vs. x.write
Warning: "test.bsv", line 3, column 8: (G0117)
Rule `test2' shadows the effects of `test1' when they execute in the same
clock cycle. Affected method calls:
x.write
```

仿真打印如下 :point_down: 。可以看出,虽然 `test1` 和 `test2` 都是每周期都会执行,但因为 `test2` 在逻辑上后执行,所以最终是 `test2` 中的 `x<=cnt+99` 覆盖掉了 `test1` 中的 `x<=cnt+1` 的执行结果。

```
cnt= 0 x= 0
test1
test2
cnt= 1 x= 99
test1
test2
cnt= 2 x=100
test1
test2
```

 

## 6.3 线网 Wire

线网 Wire 是一类用于在规则 (rule) 之间瞬时传递数据的模块,这里的瞬时是指在当前周期内。

Wire 包括以下几种接口和模块:

- 接口 `Wire#(type td)` 以及其配套的模块 `mkDWire` 、 `mkWire` 、 `mkBypassWire`
- 接口 `RWire#(type td)` 以及其配套的模块 `mkRWire`
- 接口 `PulseWire` 以及其配套的模块 `mkPulseWire`

> :pushpin: 在 Verilog 里,wire 用来放置组合逻辑电路的结果。但 BSV 里有更简单的描述组合逻辑电路的方法,即 5.6节讲过的定义变量并赋值,或者用函数。而 Wire 虽然也可以用来构建组合逻辑,但不是必要的。

### mkDWire

`Wire#(type td)` 的接口定义如下,具有一个写方法 `_write` 和一个读方法 `_read` 。

```bsv
interface Wire#(type td); // Wire中的数据的类型名为 td ,可能是任何类型
method Action _write (td x); // 该方法用于把 td 类型的变量 x 写入
method td _read; // 该方法用于读出,得到 td 类型的返回值
endinterface
```

与寄存器相同, `_write` 方法可以简写为 `<=` ,`_read` 方法可以简写为 Wire 的名称本身。

以下语句实例化一个 `mkDWire` ,其中的数据类型是 `int`,指定它的默认值是 `42` :

```
Wire#(int) <- mkDWire(42);
```

`mkDWire` 的行为是:当某个周期使用 `_write` 方法写入,在同周期就可以用 `_read` 方法读到该写入值。如果没有写入,则 `_read` 会读到它的默认值。注意 Wire 不保存数据,当前周期写入的数据不会传递到后面的任何周期。

以下展示一个例子,比较了 `mkDWire` 和 `mkReg` ,在 `cnt` 整除 2 的周期写入 `mkDWire` 和 `mkReg` 。

```bsv
// 代码路径:src/7.WireTest/TestDWire.bsv (部分)
module mkTb ();
Reg#(int) cnt <- mkReg(0);
rule up_counter;
cnt <= cnt + 1;
if(cnt > 3) $finish;
endrule

Wire#(int) w1 <- mkDWire(99); // w1 默认值 = 99
Reg#(int) r1 <- mkReg(99); // r1 初始值 = 99

rule test1 (cnt%2 == 0); // rule条件:只在能整除2的周期激活
w1 <= cnt;
endrule

rule test2 (cnt%2 == 0); // rule条件:只在能整除2的周期激活
r1 <= cnt;
endrule

rule show;
$display("cnt=%2d w1=%2d r1=%2d", cnt, w1, r1);
endrule
endmodule
```

打印结果如下。可以看到,在不写入的周期, `mkDWire` 读到的是默认值 99, 在写入的周期,`mkDWire` 能读到写入的值(即读到新值);而 `mkReg` 上写入的值只能在下一周期读到(即读到旧值)。

```
cnt= 0 w1= 0 r1=99
cnt= 1 w1=99 r1= 0
cnt= 2 w1= 2 r1= 0
cnt= 3 w1=99 r1= 2
cnt= 4 w1= 4 r1= 2
```

`mkDWire` 以及后面要讲的 `mkWire` 、`mkBypassWire` 具有如**表8**的调度注解。

​ **表8**:`mkDWire` 、 `mkWire` 、 `mkBypassWire` 的调度注解。

| mkDWire、mkWire | _read | _write |
| --------------- | ------- | ------- |
| **\_read** | **CF** | **SAR** |
| **\_write** | **SBR** | **C** |

**表8**解读如下:

- _read **CF** _read:两次 `_read` 之间不存在冲突,可以以任意的顺序排列。
- _read **SAR** _write:代表 `_read` 必须排到 `_write` 之后,保证 `_read` 到新值(与 `Reg` 的顺序正好相反)。且 `_read` 和 `_write` 不能放在同一个规则内。
- _write **SBR** _read:也就是上一条调度注解反过来。
- _write **C** _write: 代表两次 `_write` 不能在同一周期执行,且不能放在同一规则内 。

### mkWire

学习 `mkWire` 时,我们将第一次接触方法的**隐式条件**的概念。

`mkWire` 的 `_read` 方法被添加了**隐式条件**:

- 当本周期进行了 `_write` 时,`_read` 的**隐式条件**满足,才能执行 `_read` 方法读出该值。
- 当本周期没有进行 `_write` 时,`_read` 的**隐式条件**不满足,会阻止 `_read` 所在的规则的激活。

因此 `mkWire` 不需要默认值。

举例如下:

```bsv
// 代码路径:src/7.WireTest/TestWire.bsv (部分)
module mkTb ();
Reg#(int) cnt <- mkReg(1);
rule up_counter;
cnt <= cnt + 1;
if(cnt > 7) $finish;
endrule

Wire#(int) w1 <- mkWire;
Wire#(int) w2 <- mkWire;

rule test1 (cnt%2 == 0); // rule条件:只在能整除2的周期激活
$display("cnt=%1d test1", cnt);
w1 <= cnt;
endrule

rule test2 (cnt%3 == 0); // rule条件:只在能整除3的周期激活
$display("cnt=%1d test2", cnt);
w2 <= cnt;
endrule

rule show; // 只能在 w1._read 和 w2._read 的周期激活
$display("cnt=%1d w1=%2d w2=%2d", cnt, w1, w2);
endrule
endmodule
```

仿真打印如下:

```
cnt=2 test1
cnt=3 test2
cnt=4 test1
cnt=6 test1
cnt=6 test2
cnt=6 w1= 6 w2= 6
cnt=8 test1
```

可以看出,只有在 `test1` �