HDL ---Chisel入门笔记

Chisel的基本概念

  1. Chisel硬件表达
    Chisel只支持二进制逻辑,不支持三态信号。

  2. Chisel数据类型和数据字面量

    • 数据类型用于指定状态元素中保存的值或wire上传输的值。 Chisel 所有的数据类型都是 Data 类的子类,所有最终继承自 Data 类的对象都可以在实际硬件中表示成一个 bit 向量。

    • 常用的数据类型有: Bits,表示一行 bit 的集合; UInt,表示无符号整数; SInt,用补码表示有符号整数,它和 UInt 都是 FixedPoint 的子类; Bool,表示一个布尔值;还有其他 Data 的子类是这些常用类的超类,但是不用于构建电路,而是为了定义这些类.
      HDL ---Chisel入门笔记_第1张图片

    • Bundle 和 Vec 用于表示上述类型的集合,其中 Bundle 常用于构建模块的 I/O,而 Vec 常用于构建重复单元如多根线网、多个例化的模块、寄存器组等。

    • Bundle和Vec是可以允许用户使用其他数据类型来扩展Chisel数据类型集合的类。绑裹类型把若干命名的可以是不同类型的的域集合在一起变成一个连贯清晰的单元,这非常像 C 语言中的 struct。用户通过从 Bundle 衍生一个子类就可以定义自己的绑裹类型Bundle用class来定义,用户可以通过将一个类定义为Bundle的子类来定义自己的bundle。

    • Chisel 内建基础类和聚合类不需要使用 new 关键字,但是用户自定义的数据类型就必须使用。使用 Scala apply 构造器,用户自定义的数据类型也可以省略 new 关键字.

    • 绑裹类型和向量类型可以任意相互嵌套构造复杂的数据结构。

  3. Chisel数据字面量

    • Chisel的数据字面量可以通过 Scala 的 Int 和 String 类型隐式转换得到, Scala 的 Int 默认是十进制,以 0x 开头是十六进制,这会相应地转化成 Chisel的十进制和十六进制的数值字面量。字符串以“b”开头会被转化成 Chisel 的二进制,相应的,以“o”开头对应八进制,以“h”开头对应十六进制。常量和字面数值表示成 Scala 整数或者附有对应类型构造器的字符串:

      1.U // decimal 1-bit lit from Scala Int.
      “ha”.U // hexadecimal 4-bit lit from string.
      “o12”.U // octal 4-bit lit from string.
      “b1010”.U // binary 4-bit lit from string.
      5.S // signed decimal 4-bit lit from Scala Int.
      -8.S // negative decimal 4-bit lit from Scala Int.
      5.U // unsigned decimal 3-bit lit from Scala Int.
      true.B // Bool lits from Scala lits.
      false.B

    • Chisel编译器默认使用最小的位宽来保存常量,有符号类型会包括一个符号比特。字面数字也可以显式地指明位宽,如下所示:

      “ha”.U(8.W) // hexadecimal 8-bit lit of type UInt
      “o12”.U(6.W) // octal 6-bit lit of type UInt
      “b1010”.U(12.W) // binary 12-bit lit of type UInt
      5.S(7.W) // signed decimal 7-bit lit of type SInt
      5.U(8.W) // unsigned decimal 8-bit lit of type UInt

    • UInt 类型的字面常量的数值用零扩展到目标位宽。SInt 类型的字面常量的数值用符号为扩展到目标位宽。如果给出的位宽太小不能保存参量数值,Chisel会报错

  4. 组合电路

    • 在Chisel中,电路会被表示为一张节点图。每个节点是具有零个或多个输入并驱动一个输出的硬件运算符。
    • Uint是一种退化类型的节点,它没有输入,并且在其输出上驱动一个恒定的值。创建和连接节点的一种方法是使用字面表达式。
      eg. (a&b)|(c&d)
    • 任何简单的表达式都可以直接转换成电路树,在叶子处使用命名的导线和操作符形成内部节点。表达式的电路输出取自树根处的运算符。
    • 在Chisel中给一个wire命名就是声明一个变量,使用Scala中的关键词val。如下:
      val sel = a | b
      val out = (sel & in1) | (~sel & in2)
  5. 内建操作符
    针对不同数据类型,Chisel定义了不同的硬件操作符,如下表所示:
    HDL ---Chisel入门笔记_第2张图片
    Chisel有一个很大的特点就是变量宽度推测,wire的宽度是可以推测出来的,除了右移操作外,宽度推测机制得到的输出wire的宽度始终大于或者等于输入wire的宽度。按照如下规则推测:
    HDL ---Chisel入门笔记_第3张图片

  6. 函数
    我们可以定义函数来分解一个重复的逻辑,这样可以在后续设计中重复使用。
    eg. def clb(a: UInt, b: UInt, c: UInt, d: UInt): UInt = (a & b) | (~c & d)
    其中clb是表示以a,b,c,d为参数的函数,并返回一个布尔电路的输出。def关键字是Scala的一部分,表示引入了一个函数定义,每个语句后面跟一个冒号,然后是它的类型,函数返回类型在参数列表之后的冒号之后。(=)符号将函数参数列表与函数定义分隔开。
    然后我们就可以在其他的电路中使用了:val out = clb(a,b,c,d)

  7. 端口
    端口用作硬件组件的接口。一个端口可以是任意的Data对象,但它是具有方向的。 Chisel提供端口构造函数,以允许在构建时给对象添加(输入或输出)。原始的端口构造函数需要将方向作为第一个参数(方向为INPUT或OUTPUT),将位数作为第二个参数(除了始终为1位的布尔值)。

    eg. 端口声明如下所示
    class Decoupled extends Bundle {
    val ready = Bool(OUTPUT)
    val data = UInt(INPUT, 32)
    val valid = Bool(INPUT)
    }

  8. 模块

  • Module用class来定义,继承Module。继承自 Module。 包含一个用函数 IO() 包裹的接口,并且保存在一个名为 io 的端口域内。在它的构造器内连接它所有的子电路。

    例如,考虑把你自己的双输入多路选择器定义成一个模块:
    class Mux2 extends Module {
    val io = IO(new Bundle{
    val sel = Input(UInt(1.W))
    val in0 = Input(UInt(1.W))
    val in1 = Input(UInt(1.W))
    val out = Output(UInt(1.W))
    })
    io.out := (io.sel & io.in1) | (~io.sel & io.in0)
    }

    模块的连线接口是一组端口绑裹在一起形成的Bundle。接入模块的接口被命名成 io 的域。对这个Mux2 模块而言,io 是包含四个域的一个绑裹,每一个域都是这个多路选择器的一个端口。
    这里用在定义主体里的赋值操作符 := 是 Chisel 定义的特殊的操作符,连接右手边电路的输出到左手边电路的输入。

  • 模块的工厂方法
    与 Scala 一样, Chisel 也可以在模块(类)的伴生对象里定义 apply 工厂方法,该方法实例化一个模块,并连接到传入的参数。这样,其他模块在实例化该模块时,只需写成“模块名 (连接端口)”的形式来调用 apply 方法即可。如下所示,重写 Mux4 模块:
    HDL ---Chisel入门笔记_第4张图片

  1. 状态元素

    • Reg
      Chisel支持的状态元素的最简单形式是上升沿触发寄存器,可以实例化为:
      val reg = Reg(next = in)
      这个电路的输出是输入信号 in 被延时一个时钟周期的拷贝。请注意,我们并不需要指定 Reg 的数据类型,因为当用这种方式例化寄存器的时候,它的类型可以从输入那里自动推断出来。在当前版本的Chisel中,时钟和复位是全局信号,在需要时可以隐式包含。
    • RegNext
      这个寄存器的功能正如他的名字一样好理解,把输入延迟一个周期也就是在Next周期输出。具体的用法有两种:
      RegNext(next):
      这里的next就是你想延迟的输入信号,例如 B := RegNext(A),这里把A的信号延迟了一个周期送给了B。这种形式的用法没有显式的指出初始值。
      RegNext(next,init= initial_value):
      这种形式显式指出了初始值,也就是复位后的值initial_value, B := RegNext(A,init=C),这个也好理解,如果复位的时候,延迟一个时钟周期将C输出给B,否则,将A延迟一周期输出给B。
    • RegInit
      这个只有一种用法,就是指明他的初始化或者是复位时候的值。
      val r = RegInit(resetData)
    • RegEnable
      这就是带有使能端的Reg,有两种用法:
      RegNext(updateData,enable):
      这种形式有两个参量,一个是需要控制的数据,一个控制信号,当enable=1的时候,将updateData在下个时钟上升沿输出。

      val r = Reg(clonedUpdateData)
      when (enable)
      { r := updateData }
      RegNext(updateData,resetData,enable):
      这个相比于上边的也就多了一个复位时候的值。
      val r = RegInit(resetData)
      when (enable) { r := updateData }

    • ShiftRegister
      ShiftRegister(in , n , en )
      in:需要移位的数据
      n:需要移位的时钟周期数

      en:使能信号,为true的时候移位
      if (n != 0) {
      RegEnable(apply(in, n-1, en), en)
      } else {
      in
      }
      ShiftRegister(in ,n ,resetData,en)
      resetData :复位的时候为每个寄存器赋初值
      if (n != 0) {
      RegEnable(apply(in, n-1, resetData, en), resetData, en)
      } else {
      in
      }

  2. 条件更新

    • 在前面使用到寄存器的示例中,我们简单地将组合逻辑块连接到寄存器的输入。当描述状态元素的操作时,指定何时将发生寄存器更新并且用几个单独的语句指明这些更新。Chisel以when的形式提供条件更新规则,以支持这种顺序逻辑描述的风格。
      val r = Reg(init = UInt(0, 16))
      when (cond) {
      r := r + UInt(1)
      }
      其中只有在cond为真时,才在当前时钟周期的结尾更新寄存器r。when的参数是返回Bool值。后面的更新块只能包含使用赋值运算符:=,简单表达式和用val定义的命名引线的更新语句。
    • 在条件更新序列中,条件为真的最近条件更新优先。
      when (c1) { r := 1.U }
      when (c2) { r := 2.U }
    • Chisel 为条件更新的一些普遍的形式提供了一些语法糖。unless 语句与 when 语句类似,但是在它们生效的条件相反。换言之,
      unless © { body }
      等效于
      when (!c) { body }
    • 条件更新构造表达式块可以嵌套,某一表达式块在其所有嵌套在外边的条件都满足的时候才会被执行。
      例如
      when (a) { when (b) { body } }
      等效于
      when (a && b) { body }
    • 多个条件可以用 when, .elsewhen, .otherwise 串在一起,就像 Scala 语言中的 if, else if 和 else。
      when (c1) { u1 }
      .elsewhen (c2) { u2 }
      .otherwise { ud }
      等效于
      when (c1) { u1 }
      when (!c1 && c2) { u2 }
      when (!(c1 || c2)) { ud }
    • 当处理针对一个共同的键值一系列比较的时候,我们还为条件更新引入了 switch 语法。例如,
      switch(idx) {
      is(v1) { u1 }
      is(v2) { u2 }
      }
      等效于:
      when (idx === v1) { u1 }
      .elsewhen (idx === v2) { u2 }
  3. 有限状态机
    下面给出了一个稍微复杂点儿的 FSM 例子,这是为自动售货机如何收钱而设计的一个电路:

    class VendingMachine extends Module {
    val io = IO(new Bundle {
    val nickel = Input(Bool())
    val dime = Input(Bool())
    val valid = Output(Bool()) })
    val s_idle :: s_5 :: s_10 :: s_15 :: s_ok :: Nil =
    Enum(5)
    val state = Reg(init = s_idle)
    when (state === s_idle) {
    when (io.nickel) { state := s_5 }
    when (io.dime) { state := s_10 }
    }
    when (state === s_5) {
    when (io.nickel) { state := s_10 }
    when (io.dime) { state := s_15 }
    }
    when (state === s_10) {
    when (io.nickel) { state := s_15 }
    when (io.dime) { state := s_ok }
    }
    when (state === s_15) {
    when (io.nickel) { state := s_ok }
    when (io.dime) { state := s_ok }
    }
    when (state === s_ok) {
    state := s_idle
    }
    io.valid := (state === s_ok)
    }

下面再给出用 switch 语句设计的自动售货机的

FSM:
class VendingMachine extends Module {
val io = IO(new Bundle {
val nickel = Input(Bool())
val dime = Input(Bool())
val valid = Output(Bool())
})
val s_idle :: s_5 :: s_10 :: s_15 :: s_ok :: Nil =
Enum(5)
val state = Reg(init = s_idle)
switch (state) {
is (s_idle) {
when (io.nickel) { state := s_5 }
when (io.dime) { state := s_10 }
}
is (s_5) {
when (io.nickel) { state := s_10 }
when (io.dime) { state := s_15 }
}
is (s_10) {
when (io.nickel) { state := s_15 }
when (io.dime) { state := s_ok }
}
is (s_15) {
when (io.nickel) { state := s_ok }
when (io.dime) { state := s_ok }
}
is (s_ok) {
state := s_idle
}
}
io.valid := (state === s_ok)
}

  1. 存储器
    Chisel 提供了创建只读存储器和可读写存储器的机制。

    • 只读存储器 (ROM)
      可以使用 Vec 类型来定义只读存储器,如示例:
      Vec(inits: Seq[T])
      Vec(elt0: T, elts: T*)
      或者,可以通过一个被初始化的向量来创建一个只读存储器,形式为:
      在这里插入图片描述
      要么把一个元素为 Data 类型的序列作为参数,要么直接用若干个 Data 类型的元素作为参数。如:
      在这里插入图片描述
      用户可以创建一个小 ROM,初始化成1, 2, 4, 8,并且用一个计数器作为地址生成器来遍历它所有的值,如下例所示:

      val m = Vec(Array(1.U, 2.U, 4.U, 8.U))
      val r = m(counter(UInt(m.length.W)))
      我们可以 ROM 创建一个有 n 个入口的 sine 函数的查找表,如下例所示:
      def sinTable (amp: Double, n: Int) = {
      val times =Range(0, n, 1).map(i => (i2Pi)/(n.toDouble-1) - Pi)
      val inits =times.map(t => SInt(round(amp * sin(t)), width = 32))
      Vec(inits)
      }
      def sinWave (amp: Double, n: Int) =sinTable(amp, n)(counter(UInt(n.W))
      这里的 amp 用来缩放 ROM 中保存的定点数值。

    • 可读写存储器 (Mem)
      Chisel 支持两种类型的 RAM。

      • 第一种 RAM 是同步(时序)写,异步(组合逻辑)读,用语法“Mem(数量, UInt(位宽))”来构建。如:
        在这里插入图片描述
        由于现代的 FPGA 和 ASIC 技术已经不再支持异步读 RAM,所以这种 RAM 会被综合成寄存器阵列。

      • 第二种 RAM 则是同步(时序)读、写,用语法“SyncReadMem(数量, UInt(位宽))”来构建,这种 RAM 会被综合成实际的 SRAM。如:
        在这里插入图片描述
        写 RAM 的语法是“when(wr_en) { mem.write(address, dataIn); out := DontCare }”,其中DontCare 告诉 Chisel 的未连接线网检测机制,写入 RAM 时读端口的行为无需关心。读 RAM的语法是“out := mem.read(address, rd_en)”。读、写使能信号都可以省略,而且多条读、写语句会生成多个读写端口。如下面的双端口读写 RAM
        HDL ---Chisel入门笔记_第5张图片
        单个读端口、单个写端口的 SRAM 可以如下设计:

        val ram1r1w =Mem(1024, UInt(32.W))
        val reg_raddr = Reg(UInt())
        when (wen) { ram1r1w(waddr) := wdata }
        when (ren) { reg_raddr := raddr }
        val rdata = ram1r1w(reg_raddr)

        如果读取和写入被放在在同一个 when 语句中,并且它们的条件是互斥的,那么就可以推断这是一个单端口 SRAM

        val ram1p = Mem(1024, UInt(32.W))
        val reg_raddr = Reg(UInt())
        when (wen) { ram1p(waddr) := wdata }
        .elsewhen (ren) { reg_raddr := raddr }
        val rdata = ram1p(reg_raddr)

  2. 接口和成批连接
    对于复杂的电路模块来说,定义和例化接口类是非常有效的设计模块 IO 的方法。首先最重要的,接口类允许用户用有效的形式化的方法抓住那些能够设计一次复用多次的接口,鼓励他们复用设计。再者,接口支持在生产者和消费者模块之间使用成批连接,允许用户大幅节省连线语句。最后,用户可以在管理大接口变动的时候只在一处做更改,在增加减少接口子域的时候减少了更改的位置数量。

    • 端口:子类和嵌套
      如我们之前讨论过的,用户可以从 Bundle 继承子类来定义自己的接口。例如,用户可以像这样定一个简单的链路来传输握手数据:
      class SimpleLink extends Bundle {
      val data = Output(UInt(16.W))
      val valid = Output(Bool())
      }
      我们下面可以用继承的方法增加奇偶位来扩展
      SimpleLink:
      class PLink extends SimpleLink {
      val parity = Output(UInt(5.W))
      }
      一般来说,用户可以用继承的方法组织出层级化的接口。
      现在,只要把两个 PLink 套叠在一起形成新的绑裹FilterIO,我们就可以定义过滤器接口了:
      class FilterIO extends Bundle {
      val x = new PLink().flip
      val y = new PLink()
      }
      这里 flip 逐级递归地反转一个绑裹内部成员的“性别”,把输入变成输出,输出变成输入。
      现在我们可以从模块继承一个过滤器类来定义过滤器了:
      class Filter extends Module {
      val io = IO(new FilterIO())

      }
    • 向量绑裹
      元素的向量超过单个元素,可以形成更加丰富的接口层级。例如,根据一个 UInt 输入从一组输入生成一组输入的交换器可以通过 Vec 构造器实现:
      class CrossbarIo(n: Int) extends Bundle {
      val in = Vec(n, new PLink().flip())
      val sel = Input(UInt(sizeof(n).W))
      val out = Vec(n, new PLink())
      }
      这里 Vec 的第一个参数规定了数量,第二个参数是能产生端口的语句块。
    • 成批连接
      现在我们来用两个过滤来组成一个过滤器丛,如下例所示:
      class Block extends Module {
      val io = IO(new FilterIO())
      val f1 = Module(new Filter())
      val f2 = Module(new Filter())
      f1.io.x <> io.x
      f1.io.y <> f2.io.x
      f2.io.y <> io.y
      }
      这里 <> 就是成批连接的操作符,它可以把两个子模块的相对方向的端口连接起来,也可以把父模块和子模块的相同方向的端口连接起来。成批连接能够把相同名字的子端口相互连接起来。当所有的连接都成功之后,电路就开始详细展开了。如果有端口被连接了多个端口,Chisel 就会报告警告给用户。
  3. 函数式的模块创建
    为模块构造设计一个函数式的接口也是非常有用的。例如,我们可以设计一个构造器,它以多路选择器模块作为输入,以更复杂的多路选择器模块作为输出:

    object Mux2 {
    def apply (sel: UInt, in0: UInt, in1: UInt) = {
    val m = new Mux2()
    m.io.in0 := in0
    m.io.in1 := in1
    m.io.sel := sel
    m.io.out
    }
    }

    这里 object Mux2 是在模块类 Mux2 的基础上创建的一个 Scala 单例对象,apply 定义了一个方法可以创建一个 Mux2 的实例。有了这个 Mux2 创建函数,Mux4 的设计规范就显著的简单了。

    class Mux4 extends Module {
    val io = IO(new Bundle {
    val in0 = Input(UInt(1.W))
    val in1 = Input(UInt(1.W))
    val in2 = Input(UInt(1.W))
    val in3 = Input(UInt(1.W))
    val sel = Input(UInt(2.W))
    val out = Output(UInt(1.W))
    })
    io.out := Mux2(io.sel(1),
    Mux2(io.sel(0), io.in0, io.in1),
    Mux2(io.sel(0), io.in2, io.in3))
    }

对比:

class Mux2 extends Module {
val io = IO(new Bundle{
val sel = Input(UInt(1.W))
val in0 = Input(UInt(1.W))
val in1 = Input(UInt(1.W))
val out = Output(UInt(1.W))
})
io.out := (io.sel & io.in1) | (~io.sel & io.in0)
}

Mux4:

class Mux4 extends Module {
val io = IO(new Bundle {
val in0 = Input(UInt(1.W))
val in1 = Input(UInt(1.W))
val in2 = Input(UInt(1.W))
val in3 = Input(UInt(1.W))
val sel = Input(UInt(2.W))
val out = Output(UInt(1.W))
})
val m0 = Module(new Mux2())
m0.io.sel := io.sel(0)
m0.io.in0 := io.in0; m0.io.in1 := io.in1
val m1 = Module(new Mux2())
m1.io.sel := io.sel(0)
m1.io.in0 := io.in2; m1.io.in1 := io.in3
val m3 = Module(new Mux2())
m3.io.sel := io.sel(1)
m3.io.in0 := m0.io.out; m3.io.in1 := m1.io.out
io.out := m3.io.out
}

  1. 内建多路选择
    如上,Chisel 内建的版本允许 in0 和 in1 是任何数据类型,只要它们都属于继承于 Data 的相同子类。
    高阶版本:
    MuxCase(default, Array(c1 -> a, c2 -> b, …))
    等价于:
    Mux(c1, a, Mux(c2, b, Mux(…, default)))
    Chisel 还提供了 MuxLookup,这是一个 n 路的多路选择器:
    MuxLookup(idx, default,Array(0.U -> a, 1.U -> b, …))
    用 MuxCase:
    MuxCase(default, Array((idx === 0.U) -> a, (idx === 1.U) -> b, …))
    请注意,这些条件(如c1, c2)必须用小括号括起来。
  2. 多时钟域设计
  • Chisel3 支持多时钟域设计,除了当前模块隐式的全局时钟和复位信号,还可以在 IO 里面定义新的时钟与复位信号,然后传递给多时钟域构造方法。当然,为了安全地跨越时钟域,应该正确使用同步逻辑例如异步 FIFO。 时钟域内的定义只对包含它的模块可见。
  • 如果想构造多个时钟与复位信号,则可以用 withClockAndReset 方法,需要“importchisel3.experimental._”导入。该方法接收两个参数,一个时钟信号一个复位信号,花括号内定义使用传入的时钟与复位信号的电路。如:如果只想要多个时钟域,复位信号使用当前模块统一的隐式 reset,则可以用 withClock方法,它只接受一个输入时钟作为参数。如:
    HDL ---Chisel入门笔记_第6张图片
    HDL ---Chisel入门笔记_第7张图片
  • 如果只想要多个时钟域,复位信号使用当前模块统一的隐式 reset,则可以用 withClock方法,它只接受一个输入时钟作为参数。如:
    HDL ---Chisel入门笔记_第8张图片
  1. 编写测试文件
  • Chisel 可以方便地对小型电路编写测试,但对于大型电路,由于 Chisel 本身还是受限于Scala,所以测试性能不佳,而且目前没有 UVM 的支持。对于大型电路,建议转成Verilog文件用 EDA 工具做 UVM 或者用 Verilator 把 Verilog 转成 C++来仿真。
  • 要编写仿真文件,首先要用“import chisel3.iotesters._”导入相应文件。然后,因为测试模块也是一个模块,所以测试文件应该也定义成一个 class,但是这个 class 不像普通模块一样继承自 Module,而是继承自 PeekPokeTester。另外,测试类接收一个参数,该参数类型就是待测模块的 class 名称(类型),而且这个参数还要传递给 PeekPokeTester 的构造器。如:
    在这里插入图片描述
    这表明测试模块叫 Tester,继承自 PeekPokeTester。参数是待测模块 Test,并且 PeekPokeTester的构造器也需要这个参数。
  • 有四个方法可供测试使用:

    ① “poke(端口,激励值)”方法给相应的端口添加想要的激励值;
    ② “peek(端口)”方法返回相应的端口的当前值;
    ③ “expect(端口,期望值)”方法会对第一个参数(端口)使用 peek 方法,然后与期望值进行对比,如果两者不相等则出错;
    ④ “step(n)”方法则让仿真前进 n 个时钟周期。

  • 因为测试文件只用于仿真, 无需转成 Verilog, 所以类似 for、 do…while、 to、 until、 map等 Scala 高级语法都可以尽情使用,帮助测试代码更加简洁有效。如下所示是一个对 Test 模块进行简单测试的代码:
    HDL ---Chisel入门笔记_第9张图片
  1. 生成 Verilog 文件和波形文件
    • 生成Verilog文件
      Scala 的程序也是从主函数开始运行的,所以 Chisel 把相应电路模块的 class 生成 Verilog文件的命令也是放在主函数里。如果没有测试文件,不需要测试结果,只要电路的 Verilog文件,则主函数编写如下:

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

      其中, execute 函数的第一个参数接收命令行给出的参数,第二个参数是一个无参的函
      数字面量,该函数字面量返回一个想要生成 Verilog 的电路模块(class)的实例,所以可以写成“new ClassName”。
      运行时,在终端输入:

        sbt”test:runMain add.AddMain”
      

      终端当前的路径应该和提供的 build.sbt 文件一致,那么该命令运行成功后,会在当前路
      径生成想要的 firrtl 文件和 Verilog 文件。
      如果想指定文件存放路径,则可以给主函数传入参数,这些参数由 args 接收:
      sbt”test:runMain add.AddMain” –target-dir ./result/add

    • 如果有测试文件,要生成波形文件,则主函数这样编写:

      object AddMain extends App {
      iotesters.Driver.execute(args, () => new Add) (c => new AddTester©)
      }

      与之前不一样的是,首先要导入“import chisel3.iotesters._”。其次使用的 execute 函数不一样(在 iotesters 包里),该函数有第二个参数列表,需要传入一个函数字面量,这个函数字面量返回测试模块(class)的实例。

      运行这个主函数可以执行命令:

      sbt”test:runMain add.AddMain -–target-dir ./result/add –backend-name verilator”

      该命令需要安装 Verilator,运行成功后会有一个“.vcd”文件,用 GTKWave 打开查看
      波形。如果只想在终端查看仿真运行的信息,则执行:
      sbt”test:runMain add.AddMain –is-verbose”

你可能感兴趣的:(HDL硬件描述语言)