Chisel简明教程


1. 简介

Chisel(Scala嵌入式硬件构造语言)是一种嵌入在高级编程语言Scala中的硬件构造语言。Chisel是一个特殊类定义、预定义对象和Scala内部使用约定的库,因此当你编写Chisel代码时,实际上是在编写一个构建硬件图的Scala程序。随着经验的积累并希望使代码更简洁或更可重用,利用Scala语言的潜在强大功能变得很重要。它是由加州大学伯克利分校的 ADEPT 实验室开发的。

Chisel 的主要特点如下:

  1. Scala 嵌入式语言: Chisel 是 Scala 编程语言的一个嵌入式领域特定语言(EDSL)。这意味着 Chisel 程序可以利用 Scala 的全部功能,如面向对象编程、函数式编程、类型系统等。
  2. 硬件抽象: Chisel 提供了一套抽象的硬件构建块,如寄存器、存储器、多路复用器等,使开发人员可以更高效地描述硬件电路。
  3. 硬件生成: Chisel 编译器可以将 Chisel 代码转换为 Verilog 或 FIRRTL(Flexible Intermediate Representation for RTL)格式,从而可以与其他硬件设计工具集成。
  4. 可扩展性: Chisel 的模块化设计允许开发人员定义和扩展自己的硬件构建块,并将它们集成到更大的系统中。
  5. 性能: Chisel 的设计目标之一是生成高效的硬件电路。通过利用 Scala 的优化和特性,Chisel 可以产生与手写 Verilog 代码性能相当的电路。
  6. 可测试性: Chisel 提供了良好的测试支持,开发人员可以编写单元测试和属性测试来验证电路的正确性。

Chisel 的主要应用场景包括:

  • 处理器设计: Chisel 被用于设计 RISC-V 等处理器架构。
  • 加速器设计: Chisel 可以用于设计高性能的专用加速器,如机器学习加速器。
  • 自定义硬件加速: Chisel 可用于在 FPGA 或 ASIC 上实现定制的硬件加速器。
  • 教育和研究: Chisel 被广泛用于大学和研究机构的硬件课程和项目中。

2. 安装

Chisel是基于Scala实现的,Scala又是基于Java实现的,所以需要安装Java。需要严格按照官方要求安装temurin-17-jdk,如果安装其他版本的java,部分情况下可能无法编译。Linux使用22.04版本。

2.1. 安装Scala

sudo apt update
sudo apt install -y curl

curl -fL https://github.com/Virtuslab/scala-cli/releases/latest/download/scala-cli-x86_64-pc-linux.gz | gzip -d > scala-cli
chmod +x scala-cli
sudo mv scala-cli /usr/local/bin/scala-cli

scala-cli version
scala-cli install completions --shell bash

2.2. 安装Chisel

Chisel在执行的过程中,会下载相关库,所以初次编译Chisel工程时,需要联网。

curl -O -L https://github.com/chipsalliance/chisel/releases/latest/download/chisel-example.scala
scala-cli chisel-example.scala

如果输出如下信息,表明Chisel安装成功。

2.3. 安装Java

用sudo权限执行下面命令,因为网络服务的问题,temurin-17-jdk的安装可能需要几小时。

# Ensure the necessary packages are present:
apt install -y wget gpg apt-transport-https

# Download the Eclipse Adoptium GPG key:
wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg --dearmor | tee /etc/apt/trusted.gpg.d/adoptium.gpg > /dev/null

# Configure the Eclipse Adoptium apt repository
echo "deb https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | tee /etc/apt/sources.list.d/adoptium.list

# Update the apt packages
apt update

# Install
apt install -y temurin-17-jdk

2.4. 安装sbt

sbt( Scala build tool)是Scala的编译工具,可以简化编译与测试。

 -s -L https://github.com/sbt/sbt/releases/download/v1.9.7/sbt-1.9.7.tgz | tar xvz
sudo mv sbt/bin/sbt /usr/local/bin/

3. 示例

一个标准的Chisel工程,由Chisel电路源码,测试代码,sbt配置文件,make文件构成。推荐使用官方的工程模板,在此基础上进行修改添加。

官方工程模板路径:https://github.com/chipsalliance/chisel-template.git

3.1. 文件结构

  • main目录下存放电路Chisel代码。
  • test目录下存放电路试Chisel代码。
  • build.sc是存放mill编译信息,暂时不用。
  • build.sbt是存放sbt编译信息,包括各种库的版本号等。

3.2. 运行

sbt run

此命令是用来将Chisel电路代码转换为Verilog/SystemVerilog电路代码的。初始编译时,会下载相关库。再次编译则不再下载,直接编译。

编译成功,并且默认在当前目录生成GCD.v的Verilog代码,其内容为:

3.3. 测试

Chisel框架可以直接对Chisel电路代码进行测试,这种测试方式效率更高,并且测试的过程中同样可以生成vcd波形。因为Chisel生成的Verilog/SystemVerilog代码可读性不好,不好调试。所以尽量在Chisel代码阶段就进行全面的验证,尽量让Chisel生成的Verilog之后进行的验证不出现Bug。

4. Chisel语法

4.1. 基本信号类型

Chisel有三种数据类型,分别为Bits、Uint和Sint。其中Uint和Sint是从Bits扩展而来,目前主要使用Uint和Sint。

Bits(8.W)	// 位宽8Bit类型
UInt(8.W)	// 位宽8Bit无符号类型
SInt(10.W)	// 位宽8Bit有符号类型

4.2. 常量

常量就是字面量,常量的位宽描述可以省略,编译器会自动根据常量大小推断位宽大小。

4.2.1. 数字常量

Chisel的数据进制有16进制h, 8进制o, 2进制b。如下常量255,位宽默认推断为8。

"hff".U
"o377".U
"b1111_1111".U

数字字面量可以用下划线分隔,提升可读性。

4.2.2. 字符常量

val c = 'A'.U

4.2.3. 数据类型

U表示UInt,S表示Sing。

0.U		   // 定义了一个UInt类型的常量0
-3.S	   // 定义了一个SInt类型的常量-3
3.U(4.w) // 定义了一个位宽为4的UInt类型常量3

4.3. 逻辑类型

Chisel引入了Bool类型,其有true和false两个值。Bool常量:

true.B
false.B

4.4. 组合电路

组合电路就运用各种操作符的组合来描述的电路,组合电路没有时钟和寄存器。下面代码描述一个与门连接信号a和b,然后把输出信号与信号c通过或门连接在一起:

val logic = (a & b) | c

对对应的电路示意图如下:

4.5. 操作符

下表为Chisel内置的操作符,并按优先级排列。

Operator

Description

Data types

/ %

multiplication, division, modulus

UInt, SInt

+ -

addition, subtraction

UInt, SInt

=== =/=

equal, not equal

UInt, SInt, returns Bool

> >= < <=

comparison

UInt, SInt, returns Bool

<< >>

shift left, shift right (sign extend on SInt)

UInt, SInt

˜

NOT

UInt, SInt, Bool

& | ˆ

AND, OR, XOR

UInt, SInt, Bool

!

logical NOT

Bool

&& ||

logical AND, OR

Bool

4.6. 函数操作

下表为Chisel内置函数操作符。

Function

Description

Data types

v.andR v.orR v.xorR

AND, OR, XOR reduction

UInt, SInt, returns Bool

v(n)

extraction of a single bit

UInt, SInt

v(end, start)

bitfield extraction

UInt, SInt

Fill(n, v)

bitstring replication, n times

UInt, SInt

a ## b

bitfield concatenation

UInt, SInt

Cat(a, b, ...)

bitfield concatenation

UInt, SInt

// 对输入向量 v 进行逻辑与操作,并返回结果。
val andResult = v.asUInt().andR()

// 对输入向量 v 进行逻辑或操作,并返回结果。
val orResult = v.asUInt().orR()

// 对输入向量 v 进行逻辑异或操作,并返回结果
val xorResult = v.asUInt().xorR()

// 从向量 v 中提取索引为 n 的单个位。
val singleBit = v(n)

// 从向量 v 中提取从索引 start 到索引 end 的位字段
val bitfield = v(end, start)

// 将位串 v 复制 n 次以生成较长的位串。
val replicatedBits = Fill(n, v)

// 将位字段 a 和 b 进行连接形成更大的位字段。
val concatenated = a ## b

// 将多个位字段 a、b、... 进行连接形成更大的位字段。
// ##适用于连接两个字段,Cat适用于连接多个字段
val concatenatedFields = Cat(a, b, ...)

4.7. 寄存器

Chisel中提供Register组件,对应Verilog中的Register,其本质是一个D-触发器(D flip-flop)的集合。Chisel中的寄存器默认连接到全局时钟,并在每个时钟的上升沿更新寄存器的值。在声明寄存器并初始化时,寄存器会默认连接到全局同步复位信号。

val reg = RegInit(0.U(8.W))		// 声明寄存器并初始化
reg := d											// 将信号d连接到寄存器reg
val q = reg										// 将寄存器的输出赋值给变量q

Chisel中有一个隐式的全局时钟信号clock,一个隐式的全局同步复位信号reset。当使用RegInit创建一个寄存器时,就默认将clock和reset接入了创建的寄存器reg。上述代码的等效电路图如下:

上述代码也可简化为下面的形式:

val reg = RegNext(d, 0.U(8.W))
val q = reg

带使能的寄存器,使能时才赋值值inVal,否则使用默认值0.U(4.W):

val resetEnableReg2 = RegEnable(inVal, 0.U(4.W), enable) 

利用寄存器检测上升沿,当din在上一个时钟为低,在当前时钟为高时,表示din是上升沿:

val risingEdge = din & !RegNext(din)

4.8. Bundle

如果有一组信号会用在多个模块中,为了复用这一组信号,就需要用到Bundle。

class Channel() extends Bundle {
    val data = UInt(32.W)
    val valid = Bool()
}

这样就可以在多个模块中实例化Channel模块来复用信号,提升代码编写效率。

val ch = Wire(new Channel())
ch.data := 123.U
ch.valid := true.B

val b = ch.valid

Chisel不允许给提取范围赋值,高低Byte示例:

val assignWord = Wire(UInt(16.W))
// assignWord(7, 0) := lowByte // Error

class Split extends Bundle {
    val high = UInt(8.W)
    val low = UInt(8.W)
}

val split = Wire(new Split())
split.low := lowByte
split.high := highByte

assignWord := split.asUInt // 类型转换

4.9. Vec

Chisel中使用Vec来表示一组相同类型信号的集合,也即一个向量(Vector,和C中的数组一个意思)。Vec可以通过索引来访问相应的元素。

val v = Wire(Vec(3, UInt(4.W)))   // 构建3个UInt(4.W)类型的线网
v(0) := 1.U
v(1) := 3.U
v(2) := 5.U

val idx = 1.U(2.W)
val a = v(idx)

val registerFile = Reg(Vec(32, UInt(32.W))) // 构建32个UInt(32.W)的寄存器
registerFile(idx) := dIn
val dOur = registerFile(idx)

4.10. 线网

线网(Wire),在数据电路设计中,Wire相当连线,起辅助作用。Wire定义的对象,也可以理解为引用,是别名,并不会形成实际电路器件。

val number = Wire(UInt())
number := 10.U
val reg = Reg(SInt())  // =是构建对象
reg := number - 3.U    // :=是连线,可以理解为将number-3.U接到寄存器的输入信号上

Wire声明初始化的简化形式:

val number = WireDefault(10.U(4.W))

4.11. IO

IO是Chisel中用于声明模块接口信号,即Verilog中的Port。Input和Output默认都是Wire类型。

class MyModule extends Module {
  val io = IO(new Bundle {
    val in = Input(UInt(8.W))      // 输入信号
    val out = Output(UInt(8.W))    // 输出信号
  })

  io.out := io.in + 1.U
}

Chisel中没有inout类型,可以自定义一个类似的类型:

import chisel3._

class MyModule extends Module {
  val io = IO(new Bundle {
    val inout = new BidirectionalIO(UInt(8.W))
    val out = Output(UInt(8.W))
  })

  io.out := io.inout.data + 1.U
}

class BidirectionalIO[T <: Data](gen: T) extends Bundle {
  val data = Input(gen)
  val enable = Input(Bool())
  val output = Output(gen)
  val direction = Output(Bool())
}

object Main extends App {
  chisel3.Driver.execute(args, () => new MyModule)
}

4.12. 包

包(Package)是Chisel中管理代码组织结构的。

4.12.1. 创建包

直接在文件中首行指定包名即创建了一个包,多个文件可以共用一个包名。下例为一个mypack包下一个Abc类。

package mypack
import chisel3._

class Abc extends Module {
    val io = IO(new Bundle{})
}

4.12.2. 导入包

导入整个包:

import mypack._    // 利用通配符—导入整个包的内容

class AbcUser extends Module {
    val io = IO(new Bundle{})
    val abc = new Abc()
}

导入指定包的指定模块:

import mypack.Abc

class AbcUser extends Module {
    val io = IO(new Bundle{})
    val abc = new Abc()
}

4.13. 模块

Chisel使用模块来描述硬件电路,等价于Verilog中的模块(Module)。示例:

class MyModule extends Module {
  val io = IO(new Bundle {     
    val in = Input(UInt(8.W))
    val out = Output(UInt(8.W))
  })

  // 硬件逻辑
  io.out := io.in + 1.U
}

Verilog示例:

module MyModule(
  input wire [7:0]  in,    // Port,输入信号
  output wire [7:0]  out   // Port,输出信号
);

  assign out = in + 1;

endmodule

4.14. switch/is

switch/is可以转换为一个复合的多路径选择器电路。示例:

class ALU extends Module {
  val io = IO(new Bundle {
    val a = Input(UInt(16.W))
    val b = Input(UInt(16.W))
    val fn = Input(UInt(2.W))
    val y = Output(UInt(16.W))
  })

  io.y := 0.U

  switch(io.fn) {
    is(0.U) {io.y := io.a + io.b}
    is(1.U) {io.y := io.a - io.b}
    is(2.U) {io.y := io.a | io.b}
    is(3.U) {io.y := io.a & io.b}
  }
}

Verilog:

module ALU(
  input         clock,
  input         reset,
  input  [15:0] io_a,
  input  [15:0] io_b,
  input  [1:0]  io_fn,
  output [15:0] io_y
);
  wire [15:0] _io_y_T_1 = io_a + io_b; 
  wire [15:0] _io_y_T_3 = io_a - io_b; 
  wire [15:0] _io_y_T_4 = io_a | io_b; 
  wire [15:0] _io_y_T_5 = io_a & io_b; 
  wire [15:0] _GEN_0 = 2'h3 == io_fn ? _io_y_T_5 : 16'h0; 
  wire [15:0] _GEN_1 = 2'h2 == io_fn ? _io_y_T_4 : _GEN_0; 
  wire [15:0] _GEN_2 = 2'h1 == io_fn ? _io_y_T_3 : _GEN_1; 
  assign io_y = 2'h0 == io_fn ? _io_y_T_1 : _GEN_2; 
endmodule

4.15. Bulk Connection

Verilog中的Port经常遇到非常多的信号,两个模块间的信号连接需要编写大量类似的代码,而且容易出错。Chisel提供一种块连接(Buld Connection)操作符<>来快速连接模块中的端口信号,提升效率,减少出错。一个简单流水线示例,两个块连接的Bundle必须输入输出相匹配,这里可以用内置函数Flipped来将IO类型反转。示例代码:

class CommIO extends Bundle {
    val instr = Output(UInt(32.W))
    val pc = Output(UInt(32.W))
}

class Fetch extends Module {
    val io = IO(new Bundle {
        val a = Input(UInt(32.W))
        val b = Input(UInt(32.W))
        val commIO = new CommIO()
    })

    io.commIO.instr := io.a
    io.commIO.pc := io.b
}

class Decode extends Module {
    val io = IO(new Bundle {
        val commIO = new CommIO()
        val inIO = Flipped(new CommIO())
    })

    io.commIO.instr := io.inIO.instr + 1.U
    io.commIO.pc  :=  io.inIO.pc+ 1.U
}

class Top extends Module {
    val io = IO(new Bundle {
        val result = Input((UInt(32.W)))
        val out = Output(Bool())
    })

    val fetch = Module(new Fetch())
    val decode = Module(new Decode())

    fetch.io.a := io.result + 2.U
    fetch.io.b := io.result + 3.U
    decode.io.inIO <> fetch.io.commIO

    io.out := decode.io.commIO.pc.orR | decode.io.commIO.instr.orR
}

4.16. 函数

模块内的可以使用函数来复用代码,如:

class testModule extends Module {
    def adder (x: UInt, y: UInt) = { // 定义一个加法函数
        x + y  											 // 最后一个值作为返回值
    }

    val io = IO(new Bundle {
        val a = Input(UInt(32.W))
        val b = Input(UInt(32.W))
        val out = Output(UInt(32.W))
    })

    val out = adder(io.a, io.b)
    io.out := out
}

4.17. Chisel中使用Verilog

有一些最佳实践的Verilog代码,Chisel中希望拿过来直接使用。有2种方法使用Verilog,分别为:

4.17.1. 内联Verilog代码

Chisel代码:


class BlockBoxAdderIO extends Bundle {
    val a = Input(UInt(32.W))
    val b = Input(UInt(32.W))
    val cin = Input(Bool())
    val c = Output(UInt(32.W))
    val cout = Output(Bool())
}

class InlineBlackBoxAdder extends HasBlackBoxInline {
    val io = IO(new BlockBoxAdderIO)
    setInline("InlineBlackBoxAdder.v",
             s"""
             |module InlineBlackBoxAdder(a, b, cin, c, cout);
             |input [31:0] a, b;
             |input cin;
             |output [31:0] c;
             |output cout;
             |wire [32:0] sum;
             | 
             |assign sum = a + b + cin;
             |assign c = sum[31:0];
             |assign cout = sum[32];
             |
             |endmodule
             """.stripMargin)
}

class Top extends Module {
    val io = IO(new BlockBoxAdderIO)
    val adder = Module(new InlineBlackBoxAdder)

    adder.io.a := 0.U
    adder.io.b := 0.U
    adder.io.cin := false.B
    io.c := adder.io.c
    io.cout := adder.io.cout
}

Verilog代码,两个文件:


module InlineBlackBoxAdder(a, b, cin, c, cout);
  input [31:0] a, b;
  input cin;
  output [31:0] c;
  output cout;
  wire [32:0] sum;

  assign sum = a + b + cin;
  assign c = sum[31:0];
  assign cout = sum[32];

endmodule
module Top(
  input         clock,
  input         reset,
  input  [31:0] io_a,
  input  [31:0] io_b,
  input         io_cin,
  output [31:0] io_c,
  output        io_cout
);
  wire [31:0] adder_a; 
  wire [31:0] adder_b; 
  wire  adder_cin; 
  wire [31:0] adder_c; 
  wire  adder_cout; 
  InlineBlackBoxAdder adder ( 
    .a(adder_a),
    .b(adder_b),
    .cin(adder_cin),
    .c(adder_c),
    .cout(adder_cout)
  );
  assign io_c = adder_c; 
  assign io_cout = adder_cout; 
  assign adder_a = 32'h0; 
  assign adder_b = 32'h0; 
  assign adder_cin = 1'h0; 
endmodule

4.17.2. 加载Verilog文件

InlineBlackBoxAdder.v文件存放在src/main/resources/ResourceBlackBoxAdder.v。

class BlockBoxAdderIO extends Bundle {
  val a = Input(UInt(32.W))
  val b = Input(UInt(32.W))
  val cin = Input(Bool())
  val c = Output(UInt(32.W))
  val cout = Output(Bool())
}

class ResourceBlackBoxAdder extends HasBlackBoxResource {
  val io = IO(new BlockBoxAdderIO)
  addResource("/ResourceBlackBoxAdder.v")
}

class Top extends Module {
  val io = IO(new BlockBoxAdderIO)
  val adder = Module(new ResourceBlackBoxAdder)

  adder.io.a := 0.U
  adder.io.b := 0.U
  adder.io.cin := false.B
  io.c := adder.io.c
  io.cout := adder.io.cout
}

Chisel会将src/main/resources/ResourceBlackBoxAdder.v考虑到生成目录中。

Verilog:

module Top(
  input         clock,
  input         reset,
  input  [31:0] io_a, 
  input  [31:0] io_b, 
  input         io_cin, 
  output [31:0] io_c, 
  output        io_cout 
);
  wire [31:0] adder_a; 
  wire [31:0] adder_b; 
  wire  adder_cin; 
  wire [31:0] adder_c; 
  wire  adder_cout; 
  ResourceBlackBoxAdder adder ( 
    .a(adder_a),
    .b(adder_b),
    .cin(adder_cin),
    .c(adder_c),
    .cout(adder_cout)
  );
  assign io_c = adder_c; 
  assign io_cout = adder_cout; 
  assign adder_a = 32'h0; 
  assign adder_b = 32'h0; 
  assign adder_cin = 1'h0; 
endmodule

4.18. when

除了swich这种条件选择方式来描述多路选择电路之处,还可以when来描述更灵活的多路选择逻辑电路。

val w = Wire(UInt())

when (cond) {
    w := 1.U
} .otherwise {
    w := 2.U
}

多层条件逻辑:

val w = Wire(UInt())

when (cond) {
    w := 1.U
} .elsewhen (cond2) {
    w := 2.U
} .otherwise {
    w := 3.U
}

其对应的电路示意图:

4.19. 时序电路

时序电路需要时钟来驱动,并且一般需要使用上一个时钟的状态。时序电路一般用来设计计数器、状态机等电路。在Chisel中谈论的时序电路默认指同时时序电路。

如:

val regVal = RegInit(0.U(4.W))
regVal := inVal

电路图:

波形:

4.20. 参数化

Chisel的参数化是继承自Scala的,不仅支持值参数化,还是类型泛型参数化。

4.20.1. 值参数化

直接通过构造函数传递值参数,这种方式可以实现可变位宽。

class MyModule(width: Int) extends Module {
  val io = IO(new Bundle {
    val in = Input(UInt(width.W))
    val out = Output(UInt(width.W))
  })

  val shifted = io.in << 2 // 将输入数据左移两位,偏移量为2
  io.out := shifted
}

在实例化模块时,你可以直接传递参数值。

val myModule1 = Module(new MyModule(8)) // 创建一个8位宽的模块
val myModule2 = Module(new MyModule(16)) // 创建一个16位宽的模块

4.20.2. 类型参数化

上面的值参数化指定了类型,如果参数类型可变,其泛化能力更强,可以进一步提升代码的复用性。

class DecoupledIO[T <: Data](gen: T) extends Bundle {
val ready = Input(Bool())
val valid = Output(Bool())
val bits = Output(gen)
}

在实例化时:

val m1 = DecoupledIO(Uint(4.W))
val m2 = DecoupledIO(Sint(8.W))

4.20.3. 混合参数化

值参数化和类型参数化混合使用。

class DataBundle extends Bundle {
  val a = UInt(32.W)
  val b = UInt(32.W)
}

class Fifo[T <: Data](gen: T, n: Int) extends Module {
  val io = IO(new Bundle {
    val enqVal = Input(Bool())
    val enqRdy = Output(Bool())
    val deqVal = Output(Bool())
    val deqRdy = Input(Bool())
    val enqDat = Input(gen)
    val deqDat = Output(gen)
  })
  val enqPtr     = RegInit(0.U((log2Up(n)).W))
  val deqPtr     = RegInit(0.U((log2Up(n)).W))
  val isFull     = RegInit(false.B)
  val doEnq      = io.enqRdy && io.enqVal
  val doDeq      = io.deqRdy && io.deqVal
  val isEmpty    = !isFull && (enqPtr === deqPtr)
  val deqPtrInc  = deqPtr + 1.U
  val enqPtrInc  = enqPtr + 1.U
  val isFullNext = Mux(doEnq && ~doDeq && (enqPtrInc === deqPtr),
                         true.B, Mux(doDeq && isFull, false.B,
                         isFull))
  enqPtr := Mux(doEnq, enqPtrInc, enqPtr)
  deqPtr := Mux(doDeq, deqPtrInc, deqPtr)
  isFull := isFullNext
  val ram = Mem(n, gen)
  when (doEnq) {
    ram(enqPtr) := io.enqDat
  }
  io.enqRdy := !isFull
  io.deqVal := !isEmpty
  ram(deqPtr) <> io.deqDat
}

实例化:

val fifo = Module(new Fifo(new DataBundle, 8))

4.21. 枚举类型

枚举类型可以提升代码的可读性,适用于match/case或switch/is。

object StoreFunct3 extends ChiselEnum {
    val sb, sh, sw = Value
    val ukn = Value(7.U)
}

4.22. BlackBox

像一些IP,无法直接引入Chisel。如果自行编写一个空的Module,将端口信号留空来模拟IP时,Chisel会报错。因为Chisel要求端口信号必须有连接。为了解决这个问题Chisel提供了BlackBox模块,可以实际模拟IP进行实例化。

4.22.1. 普通

class FakeMod extends BlackBox {
  val io = IO(new Bundle {
    val a = Input(UInt(32.W))
    val clk = Input(Clock())
    val reset = Input(Bool())
    val b = Output(UInt(4.W))  
  })
}
 
class MyMod extends Module {
  val io = IO(new Bundle {
    val inA = Input(UInt(32.W))
    val outB = Output(UInt(4.W))  
  })
 
  val dut = Module(new FakeMod)
 
  dut.io.a := io.inA
  dut.io.clk := clock
  dut.io.reset := reset
  io.outB := dut.io.b
}

object Main extends App {
  emitVerilog(
    new MyMod,
    Array(
      "--emission-options=disableMemRandomization,disableRegisterRandomization",
      "--info-mode=use",
      "--target-dir=hdl",
      "--full-stacktrace"
    )
  )
}

生成的Verilog:

module MyMod(
  input         clock,
  input         reset,
  input  [31:0] io_inA, 
  output [3:0]  io_outB 
);
  wire [31:0] dut_a; 
  wire  dut_clk; 
  wire  dut_reset; 
  wire [3:0] dut_b; 
  FakeMod dut ( 
    .a(dut_a),
    .clk(dut_clk),
    .reset(dut_reset),
    .b(dut_b)
  );
  assign io_outB = dut_b; 
  assign dut_a = io_inA; 
  assign dut_clk = clock; 
  assign dut_reset = reset; 
endmodule

4.22.2. 固定参数化

Verilog中的Module实例化时,很多是带有参数的。Chisel参数版本虚拟Module.

class IBUFDS extends BlackBox(Map("DIFF_TERM" -> "TRUE",
                                  "DELAY" -> 5)) {
  val io = IO(new Bundle {
    val O = Output(Clock())
    val I = Input(Clock())
  })
}

class Top extends Module {
  val io = IO(new Bundle {
    val a = Output(UInt(4.W))
  })
  val ibufds = Module(new IBUFDS)

  ibufds.io.I := clock
  io.a := 4.U
}

生成的Verilog:

module Top(
  input        clock,
  input        reset,
  output [3:0] io_a 
);
  wire  ibufds_O; 
  wire  ibufds_I; 
  IBUFDS #(.DELAY(5), .DIFF_TERM("TRUE")) ibufds ( 
    .O(ibufds_O),
    .I(ibufds_I)
  );
  assign io_a = 4'h4; 
  assign ibufds_I = clock; 
endmodule

4.22.3. 动态参数化

在实例化的时候指定参数大小。

class IBUFDS(val delay: Int) extends BlackBox(Map("DIFF_TERM" -> "TRUE",
                                                  "DELAY" -> delay)) {
  val io = IO(new Bundle {
    val O = Output(Clock())
    val I = Input(Clock())
  })
}

class Top extends Module {
  val io = IO(new Bundle {
    val a = Output(UInt(4.W))
  })
  val ibufds = Module(new IBUFDS(10))

  ibufds.io.I := clock
  io.a := 4.U
}

生成的Verilog:

module Top(
  input        clock,
  input        reset,
  output [3:0] io_a 
);
  wire  ibufds_O; 
  wire  ibufds_I; 
  IBUFDS #(.DELAY(10), .DIFF_TERM("TRUE")) ibufds ( 
    .O(ibufds_O),
    .I(ibufds_I)
  );
  assign io_a = 4'h4; 
  assign ibufds_I = clock; 
endmodule

5. 内置方法

Chisel中的内置方法是以Scala 中的单例对象来实现的。

5.1. Reg

  • RegEnable,创建可使能的寄存器。
  • ShiftRegister,创建移位寄存器。
  • ShiftRegister,创建Vec的移位寄存器。

5.2. OneHot

OHToUInt("b0100".U) // results in 2.U
UIntToOH(2.U) // results in "b0100".U

5.3. Mux

MuxCase(default, Array(c1 -> a, c2 -> b))

MuxLookup(idx, default)(Seq(0.U -> a, 1.U -> b))
MuxLookup(myEnum, default)(Seq(MyEnum.a -> 1.U, MyEnum.b -> 2.U, MyEnum.c -> 3.U))

val hotValue = PriorityMux(Seq(
  io.selector(0) -> 2.U,
  io.selector(1) -> 4.U,
  io.selector(2) -> 8.U,
  io.selector(4) -> 11.U,
))

val hotValue = Mux1H(Seq(
  io.selector(0) -> 2.U,
  io.selector(1) -> 4.U,
  io.selector(2) -> 8.U,
  io.selector(4) -> 11.U,
))

5.4. Lookup

ListLookupLookup 都是 Chisel 中用于根据地址选择不同值的工具。它们的功能类似于 Mux,但可以根据更复杂的方式进行选择。

ListLookup(2.U,  // address for comparison
                 List(10.U, 11.U, 12.U),   // default "row" if none of the following cases match
     Array(BitPat(2.U) -> List(20.U, 21.U, 22.U),  // this "row" hardware-selected based off address 2.U
           BitPat(3.U) -> List(30.U, 31.U, 32.U))
) // hardware-evaluates to List(20.U, 21.U, 22.U)

Lookup(2.U, 0.U, Seq(
    BitPat(2.U) -> 20.U,
    BitPat(3.U) -> 30.U
)) // hardware-evaluates to 20.U

5.5. GradCode

2进制和格雷码互相转换,BinaryToGray 和 GrayToBinary 。

5.6. Counter

快速创建计数器。

class MyModule extends Module {
  val io = IO(new Bundle {
    val enable = Input(Bool())
    val count = Output(UInt(8.W))
  })

  val counter = Counter(10)  // 创建一个计数范围为 0 到 9 的计数器

  when(io.enable) {
    counter.inc()  // 在每个时钟周期内增加计数器的值
  }

  io.count := counter.value
}

5.7. Bitwise

  1. FillInterleaved,逐步填充。
  * FillInterleaved(2, "b1 0 0 0".U)  // equivalent to "b11 00 00 00".U
  * FillInterleaved(2, "b1 0 0 1".U)  // equivalent to "b11 00 00 11".U
  * FillInterleaved(2, myUIntWire)  // dynamic interleaved fill
  *
  * FillInterleaved(2, Seq(false.B, false.B, false.B, true.B))  // equivalent to "b11 00 00 00".U
  * FillInterleaved(2, Seq(true.B, false.B, false.B, true.B))  // equivalent to "b11 00 00 11".U
  1. PopCount,统计1或true个数
 PopCount(Seq(true.B, false.B, true.B, true.B))  // evaluates to 3.U
 PopCount(Seq(false.B, false.B, true.B, false.B))  // evaluates to 1.U

 PopCount("b1011".U)  // evaluates to 3.U
 PopCount("b0010".U)  // evaluates to 1.U
 PopCount(myUIntWire)  // dynamic count
  1. Fill,填充
 Fill(2, "b1000".U)  // equivalent to "b1000 1000".U
 Fill(2, "b1001".U)  // equivalent to "b1001 1001".U
 Fill(2, myUIntWire)  // dynamic fill
  1. Reverse,取反
Reverse("b1101".U)  // equivalent to "b1011".U
Reverse("b1101".U(8.W))  // equivalent to "b10110000".U
Reverse(myUIntWire)  // dynamic reverse

5.8. Math

Log2(8.U)  // evaluates to 3.U
Log2(13.U)  // evaluates to 3.U (truncation)
Log2(myUIntWire)

count := log2Up(4).asUint // 这里直接在编译时进行计算出结果

5.9. rand

  // 创建一个 32 位的伪随机数发生器
  val prng = random.LFSR(4)
  io.count := prng

6. Chisel常见电路模块

6.1. 多路选择器

val y = Mux(sel, a, b)

当sel是个Chisel中的Bool类型值,为true的时候选择输出a,否则选择输出b。这里a和b可以是任意的Chisel基本类型或聚合类(比如bundle或vector),只要它俩的类型是一样的就行。

其电路示意图如下:

Chisel支持多种多路选择器,分别为:

MuxCase(default, Array(c1 -> a, c2 -> b, ...))
MuxLookup(idx, default)(Seq(0.U -> a, 1.U -> b, ...))
  val hotValue = chisel3.util.Mux1H(Seq(
    io.selector(0) -> 2.U,
    io.selector(1) -> 4.U,
    io.selector(2) -> 8.U,
    io.selector(4) -> 11.U,
  ))

6.2. printf

Scala风格:

val myUInt = 33.U
printf(cf"myUInt = $myUInt") // myUInt = 33
// Hexadecimal
printf(cf"myUInt = 0x$myUInt%x") // myUInt = 0x21 // myUInt = 0x21
// Binary
printf(cf"myUInt = $myUInt%b") // myUInt = 100001 // myUInt = 100001
// Character
printf(cf"myUInt = $myUInt%c") // myUInt = !

C风格:

val myUInt = 32.U
printf("myUInt = %d", myUInt) // myUInt = 32

6.3. 计数器

  1. 可置位计数器:

实现简单,只支持正向计数,可能溢出。

class UpCounter(counterWidth: Int) extends Module {
  val io = IO(new Bundle {
    val count = Output(UInt(counterWidth.W))
  })

  val count = RegInit(0.U(counterWidth.W))
  count := count + 1.U
  io.count := count
}
  1. 加减计数器:

支持正反向计数,可能溢出。

class UpDownCounter(counterWidth: Int) extends Module {
  val io = IO(new Bundle {
    val count = Output(UInt(counterWidth.W))
    val inc = Input(Bool())
    val dec = Input(Bool())
  })

  val count = RegInit(0.U(counterWidth.W))
  count :=  Mux(io.inc, count + 1.U, Mux(io.dec, count - 1.U, count)) 
  io.count := count
}
  1. 环形计数器:

使用独热编码实现。

class RingCounter(counterWidth: Int) extends Module {
  val io = IO(new Bundle {
    val count = Output(UInt(counterWidth.W))
    val reset = Input(Bool())
  })

  val count = RegInit(1.U(counterWidth.W))
  when(io.reset) { count := 1.U }
  .otherwise { count := Cat(count(counterWidth-2,0), count(counterWidth-1)) }
  io.count := count
}
  1. 扭环形计数器:

每次也只变动1位,搞信号干扰好。

class TwistedRingCounter(counterWidth: Int) extends Module {
  val io = IO(new Bundle {
    val count = Output(UInt(counterWidth.W))
    val reset = Input(Bool())
  })

  val count = RegInit(1.U(counterWidth.W))
  when(io.reset) { count := 1.U }
  .otherwise { count := Cat(count(0), count(counterWidth-1,1)) }
  io.count := count
}
  1. 格雷码计数器:

一次只有一位变化,可以提升信号的准确性。但是消耗更多资源。

class GrayCounter(counterWidth: Int) extends Module {
  val io = IO(new Bundle {
    val count = Output(UInt(counterWidth.W))
  })

  val count = RegInit(0.U(5.W))
  count := count + 1.U

  io.count := (count >> 1.U) ^ count
}

6.4. 解码器

Decoder可以翻译为解码器或译码器,如2-4独热解码器可以将2位2进制编码转换为4位独热编码。

电路图:

真值表:

分析真值表,发现这种逻辑既可以用when表示,如:

result := 0.U

when (sel === 0.U) {
    result := 1.U
}.elsewhen (sel === 1.U) {
    result := 2.U
}.elsewhen (sel === 2.U) {
    result := 4.U
}.otherwise {
    result := 8.U
}

也可以用swich表示,如:

result := 0.U

switch (sel) {
    is (0.U) { result := 1.U}
    is (1.U) { result := 2.U}
    is (2.U) { result := 4.U}
    is (3.U) { result := 8.U}
}

6.5. 脉冲宽度调制

脉冲宽度调制(PWM,Pulse-Width Modulation)是一个信号处理的术语,用于将信号调制为常量周期且占空比在一定范围内的信号。

class Pwm extends Module {
  val io = IO(new Bundle {
    var inHigh = Input(UInt(4.W))
    var inCycle = Input(UInt(4.W))
    var dout = Output(Bool())
  })

  val cntReg = RegInit(0.U(io.inCycle.getWidth.W))
  cntReg := Mux(cntReg === (io.inCycle-1.U), 0.U, cntReg + 1.U)
  io.dout := io.inHigh > cntReg
}

class PwmTest extends AnyFlatSpec with ChiselScalatestTester {
  "PWM" should "pass" in {
    test(new Pwm).withAnnotations(Seq(WriteVcdAnnotation)) { dut =>
      dut.io.inHigh.poke(4.U)
      dut.io.inCycle.poke(10.U)
      dut.clock.setTimeout(50001)
      // Just let it run to generate the waveform
      dut.clock.step(50000)
    }
  }
}

生成的波形如下,占空比40%。

6.6. 移位寄存器

利用寄存器加拼接功能,可以快速实现移位加拼接,适用于UART的读写操作。

class ShiftRegister extends Module {
  val io = IO(new Bundle {
    val din = Input(UInt(1.W))
    val dout = Output(UInt(1.W))
  })

  val shiftReg = Reg(UInt(4.W))
  shiftReg := shiftReg(2, 0) ## io.din
  io.dout := shiftReg(3)
}

class ShiftRegisterTest extends AnyFlatSpec with ChiselScalatestTester {
  "ShiftRegister" should "pass" in {
    test(new ShiftRegister).withAnnotations(Seq(WriteVcdAnnotation)) { dut =>
      dut.io.din.poke(1.U)
      dut.clock.step()
      dut.io.din.poke(0.U)
      dut.clock.step(3)
      dut.io.dout.expect(1.U)
      dut.clock.step()
      dut.io.dout.expect(0.U)
      dut.clock.step()
      dut.io.dout.expect(0.U)
    }
  }
}

6.7. 内存模块

6.7.1. 同步SRAM

Chisel有提供SyncReadMem构造SRAM。下面是一个8位通道,1024Bit的SRAM。SyncReadMem的实现是以Register来模拟的。实际使用FPGA可以通过Vivado中的IP核生成,芯片生产应用中常通过Memory Compiler生成来替换此模块。


class Memory() extends Module {
  val io = IO(new Bundle {
    val rdAddr = Input(UInt(10.W))
    val rdData = Output(UInt(8.W))
    val wrAddr = Input(UInt(10.W))
    val wrData = Input(UInt(8.W))
    val wrEna = Input(Bool())
  })

  val mem = SyncReadMem(1024, UInt(8.W))

  io.rdData := mem.read(io.rdAddr)

  when(io.wrEna) {
    mem.write(io.wrAddr, io.wrData)
  }
}

class MemoryTest extends AnyFlatSpec with ChiselScalatestTester {
  "Memory" should "pass" in {
    test(new Memory).withAnnotations(Seq(WriteVcdAnnotation)) { dut =>
      dut.io.wrEna.poke(true.B)
      for (i <- 0 to 10) {
        dut.io.wrAddr.poke(i.U)
        dut.io.wrData.poke((i*10).U)
        dut.clock.step()
      }
      dut.io.wrEna.poke(false.B)

      dut.io.rdAddr.poke(10.U)
      dut.clock.step()
      dut.io.rdData.expect(100.U)
      dut.io.rdAddr.poke(5.U)
      dut.io.rdData.expect(100.U)
      dut.clock.step()
      dut.io.rdData.expect(50.U)

      // Same address read and write
      dut.io.wrAddr.poke(20.U)
      dut.io.wrData.poke(123.U)
      dut.io.wrEna.poke(true.B)
      dut.io.rdAddr.poke(20.U)
      dut.clock.step()
      println(s"Memory data: ${dut.io.rdData.peekInt()}")
    }
  }
}

6.7.2. 大容量

下面的示例是一个32位带宽,参数化容量的SRAM。

class DataMem(memAddrWidth: Int) extends Module {
  val io = IO(new Bundle {
    val rdAddr = Input(UInt(memAddrWidth.W))
    val rdData = Output(UInt(32.W))
    val wrAddr = Input(UInt(memAddrWidth.W))
    val wrData = Input(UInt(32.W))
    val wr = Input(Bool())
    val wrMask = Input(UInt(4.W))
  })

  val mem = SyncReadMem(1 << memAddrWidth, Vec(4, UInt(8.W)))

  val rdVec = mem.read(io.rdAddr)
  io.rdData := rdVec(3) ## rdVec(2) ## rdVec(1) ## rdVec(0)
  val wrVec = Wire(Vec(4, UInt(8.W)))
  val wrMask = Wire(Vec(4, Bool()))
  for (i <- 0 until 4) {
    wrVec(i) := io.wrData(i * 8 + 7, i * 8)
    wrMask(i) := io.wrMask(i)
  }
  when (io.wr) {
    mem.write(io.wrAddr, wrVec, wrMask)
  }
}

6.8. 去抖动电路

在电路启动时,信号可能不稳定,有一些抖动信号,需要去抖动(Debouncing)电路来过滤抖动信号。去抖动,主要通过多次采样,如果采样结构符合预期,即认为是消除抖动了。如下示例,指定采样时间,采样3个信号,有2个符合要求,即认为是成功,并且连续两次操作。


class Debounce(fac: Int = 100000000/100) extends Module {
  val io = IO(new Bundle {
    val btnU = Input(Bool())
    val led = Output(UInt(8.W))
  })

  val btn = io.btnU

  //- start input_sync
  val btnSync = RegNext(RegNext(btn))

  //- start input_debounce
  val btnDebReg = Reg(Bool())

  val cntReg = RegInit(0.U(32.W))
  val tick = cntReg === (fac-1).U

  cntReg := cntReg + 1.U
  when (tick) {
    cntReg := 0.U
    btnDebReg := btnSync
  }
  //- end

  //- start input_majority
  val shiftReg = RegInit(0.U(3.W))
  when (tick) {
    // shift left and input in LSB
    shiftReg := shiftReg(1, 0) ## btnDebReg
  }
  // Majority voting
  val btnClean = (shiftReg(2) & shiftReg(1)) | (shiftReg(2) & shiftReg(0)) | (shiftReg(1) & shiftReg(0))
  //- end

  //- start input_usage
  val risingEdge = btnClean & !RegNext(btnClean)

  // Use the rising edge of the debounced and
  // filtered button to count up
  val reg = RegInit(0.U(8.W))
  when (risingEdge) {
    reg := reg + 1.U
  }
  //- end

  io.led := reg
}

6.9. Finite-State Machine

有限状态机(FSM,Finite-State Machine),描述一组状态(states)和状态之间的条件状态转移(state transitions)。有限,表示状态的数量确定的,不是无限的。状态机的代码结构更清晰,更容易与时序结合,适合表示数据电路设计中的逻辑。照输出我们可以将将FSM分为moore型和mealy型两类。

状态机的状态码编码方式有,二进制码、独热码和格雷码,对信号要求高的采用独热码或格雷码。

6.9.1. Moore

Moore状态机的输出只依赖于当前状态,与输入无关。状态机会根据当前状态和输入信号来确定下一个状态,并且状态转移和输出信号变化都是在时钟上升沿同步发生。块图如下:

示例状态转换图如下:

对应的状态转换表:

输出Ring Bell只与当前状态是否为Red有关,所以这是Moore状态机。

Chisel代码:


class SimpleFsm extends Module {
  val io = IO(new Bundle{
    val badEvent = Input(Bool())
    val clear = Input(Bool())
    val ringBell = Output(Bool())
  })

  // The three states
  object State extends ChiselEnum {
    val green, orange, red = Value
  }

  // The state register 
  import State._
  val stateReg = RegInit(green)

  // Next state logic
  switch (stateReg) {
    is (green) {
      when(io.badEvent) {
        stateReg := orange
      }
    }
    is (orange) {
      when(io.badEvent) {
        stateReg := red
      } .elsewhen(io.clear) {
        stateReg := green
      }
    }
    is (red) {
      when (io.clear) {
        stateReg := green
      }
    }
  }
  // Output logic
  io.ringBell := stateReg === red
}

波形,当状态变为01(orange)时,且外部输出事件badEvent使能,此时状态转换为red,输出结果为响铃。

6.9.2. Meely

Meely型状态机的输出不仅与当前状态有关,同时与输入有关。结构图如下:

下图是一个上升沿检测的状态转换图,只有从状态zero到one,并且外部输入为1的时候,输出结果才算检测到。

对应的Chisel代码:

class RisingFsm extends Module {
  val io = IO(new Bundle{
    val din = Input(Bool())
    val risingEdge = Output(Bool())
  })

  // The two states
  object State extends ChiselEnum {
    val zero, one = Value
  }

  // The state register
  import State._
  val stateReg = RegInit(zero)

  // default value for output
  io.risingEdge := false.B

  // Next state and output logic
  switch (stateReg) {
    is(zero) {
      when(io.din) {
        stateReg := one
        io.risingEdge := true.B
      }
    }
    is(one) {
      when(!io.din) {
        stateReg := zero
      }
    }
  }
}

6.10. Arbiter

当同时有多个输入时,如何决定输出呢?此时就需要用仲裁器(Arbiter)。Chisel内置Arbiter模块,可以简化操作。

class ArbBundle extends Bundle{
  val number = UInt(3.W)
}
class ArbExample extends Module{
  val io = IO(new Bundle{
    val requests = Flipped(Vec(4, Decoupled(new ArbBundle)))
    val out = Decoupled(new ArbBundle)
  })
  val arbiter = Module(new Arbiter(new ArbBundle, 4))
  arbiter.io.in <> io.requests
  io.out <> arbiter.io.out
}

上面使用的是Arbiter,是一个组合电路,当有多个输入时,每次都是从最小开始判断仲裁结果。

RRArbiter是轮询的,是一个时序电路,从前往后,每次判断一个,保证每个得到相同的仲裁概率。

class ArbBundle extends Bundle{
  val number = UInt(3.W)
}
class ArbExample extends Module{
  val io = IO(new Bundle{
    val requests = Flipped(Vec(4, Decoupled(new ArbBundle)))
    val out = Decoupled(new ArbBundle)
  })
  val arbiter = Module(new RRArbiter(new ArbBundle, 4))
  arbiter.io.in <> io.requests
  io.out <> arbiter.io.out
}

6.11. FIFO

FIFO就是队列(Queue)数据结构,在数据电路设计中,多称为FIFO。FIFO及其变种形式是数字电路设计中用得非常多的一种数据结构。示例为一个标准的FIFO,其有两个参数,分别为Size表示数据的宽度,depth表示数据的深度(其实现是以多个FifoRegister来存储数据)。

class WriterIO(size: Int) extends Bundle {
  val write = Input(Bool())
  val full = Output(Bool())
  val din = Input(UInt(size.W))
}

class ReaderIO(size: Int) extends Bundle {
  val read = Input(Bool())
  val empty = Output(Bool())
  val dout = Output(UInt(size.W))
}

class FifoRegister(size: Int) extends Module {
  val io = IO(new Bundle {
    val enq = new WriterIO(size)
    val deq = new ReaderIO(size)
  })

  object State extends ChiselEnum {
    val empty, full = Value
  }
  import State._

  val stateReg = RegInit(empty)
  val dataReg = RegInit(0.U(size.W))

  when(stateReg === empty) {
    when(io.enq.write) {
      stateReg := full
      dataReg := io.enq.din
    }
  }.elsewhen(stateReg === full) {
    when(io.deq.read) {
      stateReg := empty
      dataReg := 0.U // just to better see empty slots in the waveform
    }
  }.otherwise {
    // There should not be an otherwise state
  }

  io.enq.full := (stateReg === full)
  io.deq.empty := (stateReg === empty)
  io.deq.dout := dataReg
}

class BubbleFifo(size: Int, depth: Int) extends Module {
  val io = IO(new Bundle {
    val enq = new WriterIO(size)
    val deq = new ReaderIO(size)
  })

  val buffers = Array.fill(depth) { Module(new FifoRegister(size)) }
  for (i <- 0 until depth - 1) {
    buffers(i + 1).io.enq.din := buffers(i).io.deq.dout
    buffers(i + 1).io.enq.write := ~buffers(i).io.deq.empty
    buffers(i).io.deq.read := ~buffers(i + 1).io.enq.full
  }
  io.enq <> buffers(0).io.enq
  io.deq <> buffers(depth - 1).io.deq
}

object Main extends App {
    ChiselStage.emitSystemVerilogFile(
    new BubbleFifo(8, 16),
    firtoolOpts = Array("-disable-all-randomization", "-strip-debug-info", "--split-verilog", "-o", "hdl/")
  )
}

Testbench代码:

class BubbleFifoTest extends AnyFlatSpec with ChiselScalatestTester {
  behavior of "Bubble FIFO"

  it should "pass" in {
    test(new BubbleFifo(8, 4))
    .withAnnotations(Seq(WriteVcdAnnotation)) { dut =>
      // Some default signal values.
      dut.io.enq.din.poke("hab".U)
      dut.io.enq.write.poke(false.B)
      dut.io.deq.read.poke(false.B)
      dut.clock.step()
      var full = dut.io.enq.full.peekBoolean()
      var empty = dut.io.deq.empty.peekBoolean()

      // Write into the buffer
      dut.io.enq.din.poke("h12".U)
      dut.io.enq.write.poke(true.B)
      dut.clock.step()
      full = dut.io.enq.full.peekBoolean()

      dut.io.enq.din.poke("hff".U)
      dut.io.enq.write.poke(false.B)
      dut.clock.step()
      full = dut.io.enq.full.peekBoolean()

      dut.clock.step() // see the bubbling of the first element

      // Fill the whole buffer with a check for full condition
      // Only every second cycle a write can happen
      for (i <- 0 until 7) {
        full = dut.io.enq.full.peekBoolean()
        dut.io.enq.din.poke((0x80 + i).U)
        dut.io.enq.write.poke((!full).B)
        dut.clock.step()
      }

      // Now we know it is full, so do a single read and watch
      // how this empty slot bubbles up to the FIFO input.
      dut.io.deq.read.poke(true.B)
      dut.io.deq.dout.expect("h12".U)
      dut.clock.step()
      dut.io.deq.read.poke(false.B)
      dut.clock.step(6)

      // Now read out the whole buffer.
      // Also watch that maximum read out is every second clock cycle.
      for (i <- 0 until 7) {
        empty = dut.io.deq.empty.peekBoolean()
        dut.io.deq.read.poke((!empty).B)
        dut.clock.step()
      }

      // Now write and read at maximum speed for some time.
      for (i <- 1 until 16) {
        full = dut.io.enq.full.peekBoolean()
        dut.io.enq.din.poke(i.U)
        dut.io.enq.write.poke((!full).B)

        empty = dut.io.deq.empty.peekBoolean()
        dut.io.deq.read.poke((!empty).B)
        dut.clock.step()
      }
    }
  }
}

波形中可以看出随着时钟的推进,每个存储的数据往后移动。

7. 生成Verilog/SystemVerilog

Chisel的版本升级比较快,版本之间的兼容性不太好。Chisel架构有两个,一个是Berkeley大学开的,主要用来生成Verilog,一个是chipsalliance开发的,主要用来生成SystemVerilog。

7.1. 生成Verilog

scalaVersion := "2.13.10"

scalacOptions ++= Seq(
  "-deprecation",
  "-feature",
  "-unchecked",
  "-Xfatal-warnings",
  "-language:reflectiveCalls",
)

val chiselVersion = "3.6.0"
addCompilerPlugin("edu.berkeley.cs" %% "chisel3-plugin" % chiselVersion cross CrossVersion.full)
libraryDependencies += "edu.berkeley.cs" %% "chisel3" % chiselVersion
libraryDependencies += "edu.berkeley.cs" %% "chiseltest" % "0.6.2"

上面的配置只能兼容Verilator v5.002及之前的版本。后续Verilator版本与Chisel3不兼容。

Chisel代码:

class GrayCounter(counterWidth: Int) extends Module {
  val io = IO(new Bundle {
    val count = Output(UInt(counterWidth.W))
  })

  val count = RegInit(0.U(5.W))
  count := count + 1.U

  io.count := (count >> 1.U) ^ count
}

object Main extends App {
  emitVerilog(
    new GrayCounter(4),
    Array(
      "--emission-options=disableMemRandomization,disableRegisterRandomization",
      "--info-mode=use",
      "--target-dir=hdl",
      "--full-stacktrace"
    )
  )
}

7.2. 生成SystemVerilog

chipsalliance架构,生成的SystemVerilog代码更简洁。

ThisBuild / scalaVersion     := "2.13.12"
ThisBuild / version          := "2.5.0"
ThisBuild / organization     := "edu.berkeley.cs"

val chiselVersion = "6.2.0"
val chiseltestVersion = "6.0.0"

lazy val root = (project in file("."))
  .settings(
    name := "%NAME%",
    libraryDependencies ++= Seq(
      "org.chipsalliance" %% "chisel" % chiselVersion,
      "org.scalatest" %% "scalatest" % "3.2.16" % "test",
      "edu.berkeley.cs" %% "chiseltest" % chiseltestVersion % "test",
    ),
    scalacOptions ++= Seq(
      "-language:reflectiveCalls",
      "-deprecation",
      "-feature",
      "-Xcheckinit",
      "-Ymacro-annotations",
    ),
    addCompilerPlugin("org.chipsalliance" % "chisel-plugin" % chiselVersion cross CrossVersion.full),
  )

Chisel代码:

class FakeMod extends BlackBox {
  val io = IO(new Bundle {
    val a = Input(UInt(32.W))
    val clk = Input(Clock())
    val reset = Input(Bool())
    val b = Output(UInt(4.W))  
  })
}
 
class MyMod extends Module {
  val io = IO(new Bundle {
    val inA = Input(UInt(32.W))
    val outB = Output(UInt(4.W))  
  })
 
  val dut = Module(new FakeMod)
 
  dut.io.a := io.inA
  dut.io.clk := clock
  dut.io.reset := reset
  io.outB := dut.io.b
}
 
object Main extends App {
    ChiselStage.emitSystemVerilogFile(
    new MyMod,
    firtoolOpts = Array("-disable-all-randomization", "-strip-debug-info", "--split-verilog", "-o", "hdl/")
  )
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值