zoukankan      html  css  js  c++  java
  • ChiSel 学习笔记

    原文链接

    https://blog.csdn.net/qq_34291505/article/details/87365907

    第十七章 Chisel基础——数据类型

    为了从Chisel转变成Verilog,语言开发人员开发了一个中间的标准交换格式——Firrtl,它跟Vrilog是同一级别的,两者都比Chisel低一级。编写的Chisel代码首先会经过Firrtl编译器,生成Firrtl代码,也就是一个后缀格式为“.fir”的文件,然后由这个Firrtl文件再去生成对应的Verilog代码。如果读者有兴趣看一看Firrtl的格式,其实与Verilog很接近,只不过是由机器生成的、很死板的代码。Firrtl编译器也并不是只针对Chisel
    Verilog的最初目的是用于电路验证,所以它有很多不可综合的语法。Firrtl在转变成Verilog时,只会采用可综合的语法,因此读者完全不用担心用Chisel写出来的电路不可综合。只要能正确生成Verilog,那就能被综合器生成电路

    Chisel目前不支持四态逻辑里的x和z,只支持0和1。由于只有芯片对外的IO处才能出现三态门,所以内部设计几乎用不到x和z。而且x和z在设计中会带来危害,忽略掉它们也不影响大多数设计,还简化了模型。当然,如果确实需要,可以通过黑盒语法与外部的Verilog代码互动,也可以在下游工具链里添加四态逻辑
    Chisel会对未被驱动的输出型端口和线网进行检测,如果存在,会进行报错

    Chisel的代码包并不会像Scala的标准库那样被编译器隐式导入,所以每个Chisel文件都应该在开头至少写一句“import chisel3._”。这个包包含了基本的语法,对于某些高级语法,则可能需要“import chisel3.util._”、“import chisel3.experimental._”、 “import chisel3.testers._”等等

    Chisel的数据类型

    hisel定义了自己的一套数据类型,读者应该跟Scala的九种基本值类区分开来。而且Chisel也能使用Scala的数据类型,但是Scala的数据类型都是用于参数和内建控制结构,构建硬件电路还是得用Chisel自己的数据类型,在使用时千万不要混

     在实际硬件构成里,并不会用到Data,读者也不用关心它的具体实现细节,应该关注Data类的两大子类:聚合类Aggregate和元素类Element

    聚合类Aggregate的常用子类是向量类Vec[T]和包裹类Bundle

    Vec[T]类用于包含相同的元素,元素类型T可以是任意的Data子类。

    Bundle类用于被自定义的类继承,这样自定义的类就能包含任意Data的子类对象,常用于协助构造模块的端口,故而衍生出了一些预定义的端口子类

    Element类衍生出了Analog、Bits和Clock三个子类,单例对象DontCare和特质Reset

    Analog用于在黑盒中模拟inout端口,目前在实际Chisel里并无其他用途。Bits类的两个子类SInt和UInt是最常用的两个数据类型,它们是用补码表示的有符号整数和无符号整数。不仅用来协助定义端口位宽,还用来进行赋值

    FixedPoint类提供的API带有试验性质,而且将来可能会发生改变,所以不常用。Bool类是Chisel自己的布尔类型,区别于Scala的Boolean。Bool类是UInt类的子类

    Clock类表示时钟,Chisel里的时钟是专门的一个类型,并不像Verilog里那样是1bit的线网。复位类型Reset也是如此。单例对象DontCare用于赋值给未驱动的端口或线网,防止编译器报错

    数据字面量

    能够表示具体值的数据类型为UInt、SInt和Bool。实际可综合的电路都是若干个bit,所以只能表示整数,这与Verilog是一致的。要表示浮点数,本质还是用多个bit来构建,而且要遵循IEEE的浮点标准。对于UInt,可以构成任意位宽的线网或寄存器。对于SInt,在Chisel里会按补码解读,转换成Verilog后会使用系统函数$signed,这是可综合的。对于Bool,转换成Verilog后就是1bit的线网或寄存器。

    Chisel定义了一系列隐式类:fromBigIntToLiteral、fromtIntToLiteral、fromtLongToLiteral、fromStringToLiteral、fromBooleanToLiteral。回顾前面讲述的隐式类的内容,也就是会有相应的隐式转换。以隐式类fromtIntToLiteral为例,存在一个同名的隐式转换,把相应的Scala的Int对象转换成一个fromtIntToLiteral的对象。而fromtIntToLiteral类有两个方法U和S,分别构造一个等值的UInt对象和SInt对象。再加上Scala的基本值类都是用字面量构造对象,所以要表示一个UInt对象,可以写成“1.U”的格式,这样编译器会插入隐式转换,变成“fromtIntToLiteral(1).U”,进而构造出字面值为“1”的UInt对象。同理,也可以构造SInt。还有相同行为的方法asUInt和asSInt。

    1.U                // 字面值为“1”的UInt对象

    -8.S               // 字面值为“-8”的SInt对象

    "b0101".U     // 字面值为“5”的UInt对象

    true.B            // 字面值为“true”的Bool对象 

    数据宽度

    Chisel3专门设计了宽度类Width。还有一个隐式类fromIntToWidth,就是把Int对象转换成fromIntToWidth类型的对象,然后通过方法W返回一个Width对象

    1.U              // 字面值为“1”、宽度为1bit的UInt对象
    
    1.U(32.W)   // 字面值为“1”、宽度为32bit的UInt对象

    UInt、SInt和Bool都不是抽象类,除了可以通过字面量构造对象以外,也可以直接通过apply工厂方法构造没有字面量的对象

    有字面量的数据类型用于赋值、初始化寄存器等操作,而无字面量的数据类型则用于声明端口、构造向量等

    类型转换

    UInt、SInt和Bool三个类都包含四个方法:asUInt、asSInt、toBool和toBools

    toBool会把1bit的“1”转换成Bool类型的true,“0”转换成false。如果位宽超过1bit,则用toBools转换成Bool类型的序列Seq[Bool]

    Bool类还有一个方法asClock,把true转换成电压常高的时钟,false转换成电压常低的时钟。Clock类只有一个方法asUInt,转换成对应的0或1

    向量

    如果需要一个集合类型的数据,除了可以使用Scala内建的数组、列表、集等数据结构外,还可以使用Chisel专属的Vec[T]。T必须是Data的子类,而且每个元素的类型、位宽必须一样。Vec[T]的伴生对象里有一个apply工厂方法,接收两个参数,第一个是Int类型,表示元素的个数,第二个是元素

    val myVec = Wire(Vec(3, UInt(32.W)))
    
    val myReg = myVec(0)

    还有一个工厂方法VecInit[T],通过接收一个Seq[T]作为参数来构造向量,或者是多个重复参数。不过,这个工厂方法常把有字面值的数据作为参数,用于初始化寄存器组、ROM、RAM等,或者用来构造多个模块。

    因为Vec[T]也是一种序列,所以它也定义了诸如map、flatMap、zip、foreach、filter、exists、contains等方法。尽管这些方法应该出现在软件里,但是它们也可以简化硬件逻辑的编写,减少手工代码量。

    混合向量

    混合向量MixedVec[T]与普通的向量Vec[T]类似,只不过包含的元素可以不全都一样

    对于构造Vec[T]和MixedVec[T]的序列,并不一定要逐个手写,可以通过Scala的函数,比如fill、map、flatMap、to、until等来生成

    val mixVec = Wire(MixedVec((1 to 10) map { i => UInt(i.W) }))

    包裹

    抽象类Bundle很像C语言的结构体(struct),用户可以编写一个自定义类来继承自它,然后在自定义的类里包含其它各种Data类型的字段。它可以协助构建线网或寄存器,但是最常见的用途是用于构建一个模块的端口列表,或者一部分端口

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

    Chisel的内建操作符

     

     

    位宽推断

    第十八章 Chisel基础——模块与硬件类型

    实际的电路应该是由硬件类型的对象构成的,不管是信号的声明,还是用赋值进行信号传递,都是由硬件类型的对象来完成的。数据类型和硬件类型融合在一起,才能构成完整、可运行的组件。

    Chisel是如何赋值的

    在Chisel里,所有对象都应该由val类型的变量来引用,因为硬件电路的不可变性。

    引用的对象很可能需要被重新赋值。例如,输出端口在定义时使用了“=”与端口变量名进行了绑定,那等到驱动该端口时,就需要通过变量名来进行赋值操作,更新数据

    Chisel类都定义了方法“:=”,作为等号赋值的代替。所以首次创建变量时用等号初始化,如果变量引用的对象不能立即确定状态或本身就是可变对象,则在后续更新状态时应该用“:=”

    val x = Wire(UInt(4.W))
    
    val y = Wire(UInt(4.W))
    
    x := "b1010".U  // 向4bit的线网x赋予了无符号数10
    
    y := ~x  // 把x按位取反,传递给y

    端口

    定义端口列表

    整个端口列表是由方法“IO[T <: Data](iodef: T)”来定义的,通常其参数是一个Bundle类型的对象,而且引用的字段名称必须是“io”。因为端口存在方向,所以还需要方法“Input[T <: Data](source: T)”和“Output[T <: Data](source: T)”来为每个端口表明具体的方向。

    目前Chisel还不支持双向端口inout,只能通过黑盒里的Analog端口来模拟外部Verilog的双向端口

    class MyIO extends Bundle {
       val in = Input(Vec(5, UInt(32.W)))
       val out = Output(UInt(32.W))
    }
    
    ......
       val io = IO(new MyIO)  // 模块的端口列表
    ......

    翻转端口列表的方向

    对于两个相连的模块,可能存在大量同名但方向相反的端口。仅仅为了翻转方向而不得不重写一遍端口显得费时费力,所以Chisel提供了“Flipped[T <: Data](source: T)”方法,可以把参数里所有的输入转输出,输出转输入。如果是黑盒里的Analog端口,则仍是双向的。

    class MyIO extends Bundle {
       val in = Input(Vec(5, UInt(32.W)))
       val out = Output(UInt(32.W))
    }
    
    ......
       val io = IO(new MyIO)  // in是输入,out是输出
    ......
       val io = IO(Flipped(new MyIO))  // out是输入,in是输出

    整体连接

    翻转方向的端口列表通常配合整体连接符号“<>”使用。该操作符会把左右两边的端口列表里所有同名的端口进行连接,而且同一级的端口方向必须是输入连输出、输出连输入,父级和子级的端口方向则是输入连输入、输出连输出。

    方向必须按这个规则匹配,而且不能存在端口名字、数量、类型不同的情况。这样就省去了大量连线的代码

    模块

    定义模块

    Chisel里面是用一个自定义的类来定义模块的,这个类有以下三个特点:

    ①继承自Module类。

    ②有一个抽象字段“io”需要实现,该字段必须引用前面所说的端口对象。

    ③在类的主构造器里进行内部电路连线。

    这样定义的模块会继承一个字段“clock”,类型是Clock,它表示全局时钟,在整个模块内都可见。对于组合逻辑,是用不上它的,而时序逻辑虽然需要这个时钟,但也不用显式声明。还有一个继承的字段“reset”,类型是Reset,表示全局复位信号,在整个模块内可见。对于需要复位的时序元件,也可以不用显式使用该字段。

    // mux2.scala
    package test
     
    import chisel3._
     
    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)
    }

    “new Bundle { ... }”的写法是声明一个匿名类继承自Bundle,然后实例化匿名类。对于短小、简单的端口列表,可以使用这种简便写法。对于大的公用接口,应该单独写成具名的Bundle子类,方便修改。“io.out := ...”其实就是主构造方法的一部分

    例化模块

    并不是直接用new生成一个实例对象就完成了,还需要再把实例的对象传递给单例对象Module的apply方法。


      val m0 = Module(new Mux2)

    例化多个模块

    对于要多次例化的重复模块,可以利用向量的工厂方法VecInit[T <: Data]。

    所以可以把待例化模块的io字段组成一个序列

    生成序列的一种方法是调用单例对象Seq里的方法fill,该方法的一个重载版本有两个单参数列表,第一个接收Int类型的对象,表示序列的元素个数,第二个是传名参数,接收序列的元素。

    // mux4_2.scala
    package test
     
    import chisel3._
     
    class Mux4_2 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 m = VecInit(Seq.fill(3)(Module(new Mux2).io))  // 例化了三个Mux2,并且参数是端口字段io
      m(0).sel := io.sel(0)  // 模块的端口通过下标索引,并且路径里没有“io”
      m(0).in0 := io.in0
      m(0).in1 := io.in1
     
      m(1).sel := io.sel(0)
      m(1).in0 := io.in2
      m(1).in1 := io.in3
     
      m(2).sel := io.sel(1)
      m(2).in0 := m(0).out
      m(2).in1 := m(1).out
     
      io.out := m(2).out
    }

    线网

    Chisel把线网作为电路的节点,通过工厂方法“Wire[T <: Data](t: T)”来定义。可以对线网进行赋值,也可以连接到其他电路节点,这是组成组合逻辑的基本硬件类型。

    val myNode = Wire(UInt(8.W))

    myNode := 0.U 

    因为Scala作为软件语言是顺序执行的,定义具有覆盖性,所以如果对同一个线网多次赋值,则只有最后一次有效

    寄存器

    如果模块里没有多时钟域的语句块,那么寄存器都是由隐式的全局时钟来控制。对于有复位信号的寄存器,如果不在多时钟域语句块里,则由隐式的全局复位来控制,并且高有效

    目前Chisel所有的复位都是同步复位,异步复位功能还在开发中。如果需要异步复位寄存器,则需要通过黑盒引入

    有五种内建的寄存器,

    第一种是跟随寄存器“RegNext[T <: Data](next: T)”,在每个时钟上升沿,它都会采样一次传入的参数,并且没有复位信号。它的另一个版本的apply工厂方法是“RegNext[T <: Data](next: T, init: T)”,也就是由复位信号控制,当复位信号有效时,复位到指定值,否则就跟随。
    第二种是复位到指定值的寄存器“RegInit[T <: Data](init: T)”,参数需要声明位宽,否则就是默认位宽。可以用内建的when语句进行条件赋值。

    第三种是普通的寄存器“Reg[T <: Data](t: T)”,它可以在when语句里用全局reset信号进行同步复位(reset信号是Reset类型,要用toBool进行类型转换),也可以进行条件赋值或无条件跟随。参数同样要指定位宽。

    第四种是util包里的带一个使能端的寄存器“RegEnable[T <: Data](next: T, init: T, enable: Bool)”,如果不需要复位信号,则第二个参数可以省略给出。

    第五种是util包里的移位寄存器“ShiftRegister[T <: Data](in: T, n: Int, resetData: T, en: Bool)”,其中第一个参数in是带移位的数据,第二个参数n是需要延迟的周期数,第三个参数resetData是指定的复位值,可以省略,第四个参数en是使能移位的信号,默认为true.B。

    // reg.scala
    package test
     
    import chisel3._
    import chisel3.util._
     
    class REG extends Module {
      val io = IO(new Bundle {
        val a = Input(UInt(8.W))
        val en = Input(Bool())
        val c = Output(UInt(1.W))
      })
     
      val reg0 = RegNext(io.a)
      val reg1 = RegNext(io.a, 0.U)
      val reg2 = RegInit(0.U(8.W))
      val reg3 = Reg(UInt(8.W))
      val reg4 = Reg(UInt(8.W))
      val reg5 = RegEnable(io.a + 1.U, 0.U, io.en)
      val reg6 = RegEnable(io.a - 1.U, io.en)
      val reg7 = ShiftRegister(io.a, 3, 0.U, io.en)
      val reg8 = ShiftRegister(io.a, 3, io.en)
      
      reg2 := io.a.andR
      reg3 := io.a.orR
     
      when(reset.toBool) {
        reg4 := 0.U
      } .otherwise {
        reg4 := 1.U
      }
     
      io.c := reg0(0) & reg1(0) & reg2(0) & reg3(0) & reg4(0) & reg5(0) & reg6(0) & reg7(0) & reg8(0)
    }

    寄存器组

    如果把子类型Vec[T]作为参数传递进去,就会生成多个位宽相同、行为相同、名字前缀相同的寄存器。同样,寄存器组在Chisel代码里可以通过下标索引。

    // reg2.scala
    package test
     
    import chisel3._
    import chisel3.util._
     
    class REG2 extends Module {
      val io = IO(new Bundle {
        val a = Input(UInt(8.W))
        val en = Input(Bool())
        val c = Output(UInt(1.W))
      })
     
      val reg0 = RegNext(VecInit(io.a, io.a))
      val reg1 = RegNext(VecInit(io.a, io.a), VecInit(0.U, 0.U))
      val reg2 = RegInit(VecInit(0.U(8.W), 0.U(8.W)))
      val reg3 = Reg(Vec(2, UInt(8.W)))
      val reg4 = Reg(Vec(2, UInt(8.W)))
      val reg5 = RegEnable(VecInit(io.a + 1.U, io.a + 1.U), VecInit(0.U(8.W), 0.U(8.W)), io.en)
      val reg6 = RegEnable(VecInit(io.a - 1.U, io.a - 1.U), io.en)
      val reg7 = ShiftRegister(VecInit(io.a, io.a), 3, VecInit(0.U(8.W), 0.U(8.W)), io.en)
      val reg8 = ShiftRegister(VecInit(io.a, io.a), 3, io.en)
      
      reg2(0) := io.a.andR
      reg2(1) := io.a.andR
      reg3(0) := io.a.orR
      reg3(1) := io.a.orR
     
      when(reset.toBool) {
        reg4(0) := 0.U
        reg4(1) := 0.U
      } .otherwise {
        reg4(0) := 1.U
        reg4(1) := 1.U
      }
     
      io.c := reg0(0)(0) & reg1(0)(0) & reg2(0)(0) & reg3(0)(0) & reg4(0)(0) & reg5(0)(0) & reg6(0)(0) & reg7(0)(0) & reg8(0)(0) &
              reg0(1)(0) & reg1(1)(0) & reg2(1)(0) & reg3(1)(0) & reg4(1)(0) & reg5(1)(0) & reg6(1)(0) & reg7(1)(0) & reg8(1)(0)
    }

    用when给电路赋值

    由于Scala已经占用了“if…else if…else”语法,所以相应的Chisel控制结构改成了when语句,其语法如下:

    when用于给带使能信号的寄存器更新数据,组合逻辑不常用。对于有复位信号的寄存器,推荐使用RegInit来声明,这样生成的Verilog会自动根据当前的时钟域来同步复位,尽量不要在when语句里用“reset.toBool”作为复位条件

    除了when结构,util包里还有一个与之对偶的结构“unless”,如果unless的判定条件为false.B则一直执行,否则不执行

    数据类型与硬件类型的区别

    hisel的数据类型,其中常用的就五种:UInt、SInt、Bool、Bundle和Vec[T]。本章介绍了硬件类型,最基本的是IO、Wire和Reg三种,还有指明端口方向的Input、Output和Flipped

    在编写Chisel时,要注意哪些地方是数据类型,哪些地方又是硬件类型。这时,静态语言的优势便体现出来了,因为编译器会帮助程序员检查类型是否匹配。如果在需要数据类型的地方出现了硬件类型、在需要硬件类型的地方出现了数据类型

    为VecInit专门接收硬件类型的参数来构造硬件向量,给VecInit传入数据类型反而会报错

    第十九章 Chisel基础——常用的硬件原语

    Chisel在语言库里定义了很多常用的硬件原语,读者可以直接导入相应的包来使用

    多路选择器

    第一种形式是二输入多路选择器“Mux(sel, in1, in2)”。sel是Bool类型,in1和in2的类型相同,都是Data的任意子类型。当sel为true.B时,返回in1,否则返回in2

    所以Mux可以内嵌Mux,Mux(c1, a, Mux(c2, b, Mux(..., default)))

    第二种就是针对上述n输入多路选择器的简便写法,形式为“MuxCase(default, Array(c1 -> a, c2 -> b, ...))”,它的展开与嵌套的Mux是一样的。第一个参数是默认情况下返回的结果,第二个参数是一个数组,数组的元素是对偶“(成立条件,被选择的输入)”。MuxCase在chisel3.util包里

    第三种是MuxCase的变体,它相当于把MuxCase的成立条件依次换成从0开始的索引值,就好像一个查找表,其形式为“MuxLookup(idx, default, Array(0.U -> a, 1.U -> b, ...))”。它的展开相当于“MuxCase(default, Array((idx === 0.U) -> a, (idx === 1.U) -> b, ...))”。MuxLookup也在chisel3.util包里。

    第四种是chisel3.util包里的独热码多路选择器,它的选择信号是一个独热码。如果零个或多个选择信号有效,则行为未定义

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

    ROM

    可以通过工厂方法“VecInit[T <: Data](elt0: T, elts: T*)”或“VecInit[T <: Data](elts: Seq[T])”来创建一个只读存储器,参数就是ROM里的常量数值,对应的Verilog代码就是给读取ROM的线网或寄存器赋予常量值

    // rom.scala
    package test
     
    import chisel3._
     
    class ROM extends Module {
      val io = IO(new Bundle {
        val sel = Input(UInt(2.W))
        val out = Output(UInt(8.W))  
      })
     
      val rom = VecInit(1.U, 2.U, 3.U, 4.U)
     
      io.out := rom(io.sel)
    }

    RAM

    Chisel支持两种类型的RAM。第一种RAM是同步(时序)写,异步(组合逻辑)读,通过工厂方法“Mem[T <: Data](size: Int, t: T)”来构建

    val asyncMem = Mem(16, UInt(32.W)) 

    由于现代的FPGA和ASIC技术已经不再支持异步读RAM,所以这种RAM会被综合成寄存器阵列。第二种RAM则是同步(时序)读、写,通过工厂方法“SyncReadMem[T <: Data](size: Int, t: T)”来构建,这种RAM会被综合成实际的SRAM

    val syncMem = SyncReadMem(16, UInt(32.W))

    写RAM的语法是

    when(wr_en) {
         mem.write(address, dataIn) 
         out := DontCare
    }

    读RAM的语法是

    out := mem.read(address, rd_en)

    带写掩模的RAM

    RAM通常都具备按字节写入的功能,比如数据写入端口的位宽是32bit,那么就应该有4bit的写掩模信号,只有当写掩模比特有效时,对应的字节才会写入。Chisel也具备构建带写掩模的RAM的功能。

    而write方法有一个重载版本,就是第三个参数是接收写掩模信号的。当下标为0的写掩模比特是true.B时,最低的那个字节会被写入,依次类推。下面是一个带写掩模的单端口RAM

    // maskram.scala
    package test
     
    import chisel3._
    import chisel3.util._
     
    class MaskRAM extends Module {
      val io = IO(new Bundle {
        val addr = Input(UInt(10.W))
        val dataIn = Input(UInt(32.W))
        val en = Input(Bool())
        val we = Input(UInt(4.W))
        val dataOut = Output(UInt(32.W))  
      })
     
      val dataIn_temp = Wire(Vec(4, UInt(8.W)))
      val dataOut_temp = Wire(Vec(4, UInt(8.W)))
      val mask = Wire(Vec(4, Bool()))
     
      val syncRAM = SyncReadMem(1024, Vec(4, UInt(8.W)))
     
      when(io.en) {
        syncRAM.write(io.addr, dataIn_temp, mask)
        dataOut_temp := syncRAM.read(io.addr)
      } .otherwise {
        dataOut_temp := DontCare
      } 
     
      for(i <- 0 until 4) {
        dataIn_temp(i) := io.dataIn(8*i+7, 8*i)
        mask(i) := io.we(i).toBool
        io.dataOut := Cat(dataOut_temp(3), dataOut_temp(2), dataOut_temp(1), dataOut_temp(0))
      }
    }

    从文件读取数据到RAM

    在experimental包里有一个单例对象loadMemoryFromFile,它的apply方法可以在Chisel层面上从txt文件读取数据到RAM里、、

    MemBase[T]类型的,也就是Mem[T]和SyncReadMem[T]的超类,该参数接收一个自定义的RAM对象。第二个参数是文件的名字及路径,用字符串表示。第三个参数表示读取的方式为十六进制或二进制,默认是MemoryLoadFileType.Hex,也可以改成MemoryLoadFileType.Binary。注意,没有十进制和八进制

    计数器

    Chisel在util包里定义了一个自增计数器原语Counter,它的工厂方法接收两个参数:第一个参数是Bool类型的使能信号,为true.B时计数器从0开始每个时钟上升沿加1自增,为false.B时则计数器保持不变;第二个参数需要一个Int类型的具体正数,当计数到该值时归零。该方法返回一个二元组,其第一个元素是计数器的计数值,第二个元素是判断计数值是否等于期望值的结果。

    // counter.scala

    package test

    import chisel3._

    import chisel3.util._

    class MyCounter extends Module {

      val io = IO(new Bundle {

        val en = Input(Bool())

        val out = Output(UInt(8.W))

        val valid = Output(Bool())  

      })

      val (a, b) = Counter(io.en, 233)

      io.out := a

      io.valid := b

    }

    16位线性反馈移位寄存器

    如果要产生伪随机数,可以使用util包里的16位线性反馈移位寄存器原语LFSR16,它接收一个Bool类型的使能信号,用于控制寄存器是否移位,缺省值为true.B。它返回一个UInt(16.W)类型的结果。

    // lfsr.scala
    package test
     
    import chisel3._
    import chisel3.util._
     
    class LFSR extends Module {
      val io = IO(new Bundle {
        val en = Input(Bool())
        val out = Output(UInt(16.W))  
      })
     
      io.out := LFSR16(io.en)
    }

    状态机

    Chisel没有直接构建状态机的原语。不过,util包里定义了一个Enum特质及其伴生对象。伴生对象里的apply方法定义如下

    def apply(n: Int): List[UInt]

    参数n返回对应元素数的List[UInt],每个元素都是不同的,所以可以作为枚举值来使用。最好把枚举状态的变量名也组成一个列表,然后用列表的模式匹配来进行赋值。有了枚举值后,可以通过“switch…is…is”语句来使用

    // fsm.scala
    package test
     
    import chisel3._
    import chisel3.util._
     
    class DetectTwoOnes extends Module {
      val io = IO(new Bundle {
        val in = Input(Bool())
        val out = Output(Bool())
      })
     
      val sNone :: sOne1 :: sTwo1s :: Nil = Enum(3)
      val state = RegInit(sNone)
     
      io.out := (state === sTwo1s)
     
      switch (state) {
        is (sNone) {
          when (io.in) {
            state := sOne1
          }
        }
        is (sOne1) {
          when (io.in) {
            state := sTwo1s
          } .otherwise {
            state := sNone
          }
        }
        is (sTwo1s) {
          when (!io.in) {
            state := sNone
          }
        }
      }
    }

    枚举状态名的首字母要小写,这样Scala的编译器才能识别成变量模式匹配。

    Chisel基础——生成Verilog与基本测试

    把一个Chisel模块编译成Verilog代码,并进一步使用Verilator做一些简单的测试

    生成Verilog

    生成Verilog的程序自然是在主函数里例化待编译的模块,然后运行这个主函数。例化待编译模块需要特殊的方法调用。chisel3包里有一个单例对象Driver,它包含一个方法execute,该方法接收两个参数,第一个参数是命令行传入的实参即字符串数组args,第二个是返回待编译模块的对象的无参函数。运行这个execute方法,就能得到Verilog代码。

    接着,读者需要在src/test/scala文件夹下编写对应的主函数文件

    // fullAdderGen.scala
    package test
     
    object FullAdderGen extends App {
      chisel3.Driver.execute(args, () => new FullAdder)
    }

    在这个主函数里,只有一个execute函数的调用,第一个参数固定是“args”,第二个参数则是无参的函数字面量“() => new FullAdder”。因为Chisel的模块本质上还是Scala的class,所以只需用new构造一个对象作为返回结果即可。主函数里可以包括多个execute函数,也可以包含其它代码。还有一点要注意的是,建议把设计文件和主函数放在一个包里,比如这里的“package test”,这样省去了编写路径的麻烦。

    要运行这个主函数,需要在build.sbt文件所在的路径下打开终端,然后执行命令

    :~/chisel-template$ sbt 'test:runMain test.FullAdderGen'

    sbt后面有空格,再后面的内容都是被单引号对或双引号对包起来。其中,test:runMain是让sbt执行主函数的命令,而test.FullAdderGen就是要执行的那个主函数

    终端的路径下就会生成三个文件:FullAdder.anno.json、FullAdder.fir和FullAdder.v。

    第二个后缀为“.fir”的文件就是对应的Firrtl代码,第三个自然是对应的Verilog文件。

    在命令里增加参数

    给Firrtl传递参数

    命令后面继续增加可选的参数。例如,增加参数“--help”查看帮助菜单

    最常用的是参数“-td”,可以在后面指定一个文件夹,这样之前生成的三个文件就在该文件夹里,而不是在当前路径下

    给主函数传递参数

    Scala的类可以接收参数,自然Chisel的模块也可以接收参数。假设要构建一个n位的加法器,具体位宽不确定,根据需要而定。那么,就可以把端口位宽参数化,例化时传入想要的参数即可。

    package test
     
    import chisel3._
     
    class Adder(n: Int) extends Module {
      val io = IO(new Bundle {
        val a = Input(UInt(n.W))
        val b = Input(UInt(n.W))
        val s = Output(UInt(n.W))
        val cout = Output(UInt(1.W))  
      })
     
      io.s := (io.a +& io.b)(n-1, 0)
      io.cout := (io.a +& io.b)(n)
    }
     
    // adderGen.scala
    package test
     
    object AdderGen extends App {
      chisel3.Driver.execute(args, () => new Adder(args(0).toInt))
    }

    比如例子中的主函数期望第一个参数即args(0)是一个数字字符串,这样就能通过方法toInt转换成Adder所需的参数。

    ~/chisel-template$  sbt 'test:runMain test.AdderGen 8 -td ./generated/adder'

    编写简单的测试

    Chisel的测试有两种,第一种是利用Scala的测试来验证Chisel级别的代码逻辑有没有错误。因为这部分内容比较复杂,而且笔者目前也没有深入学习有关Scala测试的内容,所以这部分内容可有读者自行选择研究。第二种是利用Chisel库里的peek和poke函数,给模块的端口加激励、查看信号值,并交由下游的Verilator来仿真、产生波形。这种方式比较简单,类似于Verilog的testbench,适合小型电路的验证。对于超大型的系统级电路,最好还是生成Verilog,交由成熟的EDA工具,用UVM进行验证。

    要编写一个简单的testbench,首先也是定义一个类,这个类的主构造方法接收一个参数,参数类型就是待测模块的类名。其次,这个类继承自PeekPokeTester类,并且把接收的待测模块也传递给此超类。最后,测试类内部有四种方法可用:①“poke(端口,激励值)”方法给相应的端口添加想要的激励值,激励值是Int类型的;②“peek(端口)”方法返回相应的端口的当前值;③“expect(端口,期望值)”方法会对第一个参数(端口)使用peek方法,然后与Int类型的期望值进行对比,如果两者不相等则出错;④“step(n)”方法则让仿真前进n个时钟周期。

     

    package test
     
    import scala.util._
    import chisel3.iotesters._
     
    class AdderTest(c: Adder) extends PeekPokeTester(c) {
      val randNum = new Random
      for(i <- 0 until 10) {
        val a = randNum.nextInt(256)
        val b = randNum.nextInt(256)
        poke(c.io.a, a)
        poke(c.io.b, b)
        step(1)
        expect(c.io.s, (a + b) & 0xff)
        expect(c.io.cout, ((a + b) & 0x100) >> 8)
      }
    }

     

    第一个包scala.util里包含了Scala生成伪随机数的类Random,第二个包chisel3.iotesters包含了测试类PeekPokeTester

    运行测试

     自然也是通过主函数,但是这次是使用iotesters包里的execute方法。该方法与前面生成Verilog的方法类似,仅仅是多了一个参数列表,多出的第二个参数列表接收一个返回测试类的对象的函数:

    // addertest.scala
    object AdderTestGen extends App {
      chisel3.iotesters.Driver.execute(args, () => new Adder(8))(c => new AdderTest(c))
    }

    ~/chisel-template$  sbt 'test:runMain test.AdderTestGen -td ./generated/addertest --backend-name verilator' 

    执行成功后,就能在相应文件夹里看到一个新生成的文件夹,里面是仿真生成的文件。其中,“Adder.vcd”文件就是波形文件,使用GTKWave软件打开就能查看,将相应的端口拖拽到右侧就能显示波形。

    第二十一章 Chisel基础——黑盒

    例化黑盒

    如果定义Dut类时,不是继承自Module,而是继承自BlackBox,则允许只有端口定义,也只需要端口定义。此外,在别的模块里例化黑盒时,编译器不会给黑盒的端口名加上“io_”

    // blackbox.scala
    package test
     
    import chisel3._
     
    class Dut 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 UseDut extends Module {
      val io = IO(new Bundle {
        val toDut_a = Input(UInt(32.W))
        val toDut_b = Output(UInt(4.W))  
      })
     
      val u0 = Module(new Dut)
     
      u0.io.a := io.toDut_a
      u0.io.clk := clock
      u0.io.reset := reset
      io.toDut_b := u0.io.b
    }
     
    object UseDutTest extends App {
      chisel3.Driver.execute(args, () => new UseDut)
    }

    BlackBox的构造方法可以接收一个Map[String, Param]类型的参数,这会使得例化外部的Verilog模块时具有配置模块的“#(参数配置)”。映射的键固定是字符串类型,它对应Verilog里声明的参数名;映射的值对应传入的配置参数,可以是字符串,也可以是整数和浮点数。虽然值的类型是Param,这是一个Chisel的印章类,但是单例对象chisel3.experimental里定义了相应的隐式转换,可以把BigInt、Int、Long、Double和String转换成对应的Param类型

    ...
    import chisel3.experimental._
     
    class Dut extends BlackBox(Map("DATA_WIDTH" -> 32,
                                   "MODE" -> "Sequential",
                                   "RESET" -> "Asynchronous")) {
      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))  
      })
    }
    ..

    复制Verilog文件

    chisel3.util包里有一个特质HasBlackBoxResource,如果在黑盒类里混入这个特质,并且在src/main/resources文件夹里有对应的Verilog源文件,那么在Chisel转换成Verilog时,就会把Verilog文件一起复制到目标文件夹。


    ...
    import chisel3.util._

    class Dut extends BlackBox with HasBlackBoxResource {
    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))
    })

    setResource("/dut.v")
    }

    注意,相比一般的黑盒,除了端口列表的声明,还多了一个特质里的setResource方法的调用。方法的入参是Verilog文件的相对地址,即相对src/main/resources的地址

    内联Verilog文件

    hisel3.util包里还有有一个特质HasBlackBoxInline,混入该特质的黑盒类可以把Verilog代码直接内嵌进去。内嵌的方式是调用特质里的方法“setInline(blackBoxName: String, blackBoxInline: String)”,类似于setResource的用法。这样,目标文件夹里就会生成一个单独的Verilog文件,复制内嵌的代码。该方法适合小型Verilog设计。

    inout端口

    Chisel目前只支持在黑盒中引入Verilog的inout端口。Bundle中使用 “Analog(位宽)”声明Analog类型的端口,经过编译后变成Verilog的inout端口

    模块里的端口可以声明成Analog类型,但只能用于与黑盒连接,不能在Chisel代码中进行读写。

    使用前,要先用“chisel3.experimental._”进行导入。

    第二十二章 Chisel基础——多时钟域设计


    // inout.scala
    package test

    import chisel3._
    import chisel3.util._
    import chisel3.experimental._

    class InoutIO extends Bundle {
    val a = Analog(16.W)
    val b = Input(UInt(16.W))
    val sel = Input(Bool())
    val c = Output(UInt(16.W))
    }

    class InoutPort extends BlackBox with HasBlackBoxInline {
    val io = IO(new InoutIO)

    setInline("InoutPort.v",
    """
    |module InoutPort( inout [15:0] a,
    | input [15:0] b,
    | input sel,
    | output [15:0] c);
    | assign a = sel ? 'bz : b;
    | assign c = sel ? a : 'bz;
    |endmodule
    """.stripMargin)
    }

    class MakeInout extends Module {
    val io = IO(new InoutIO)

    val m = Module(new InoutPort)

    m.io <> io
    }

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

    第二十二章 Chisel基础——多时钟域设计

    数字电路中免不了用到多时钟域设计,尤其是设计异步FIFO这样的同步元件

    在Chisel里,则相对复杂一些,因为这与Scala的变量作用域相关,而且时序元件在编译时都是自动地隐式跟随当前时钟域。

    没有隐式端口的模块

    继承自Module的模块类会获得隐式的全局时钟与同步复位信号,即使在设计中用不上它们也没关系。如果读者确实不喜欢这两个隐式端口,则可以选择继承自RawModule,这样在转换成Verilog时就没有隐式端口。

    // module.scala
    package test
     
    import chisel3._
    import chisel3.experimental._
     
    class MyModule extends RawModule {
      val io = IO(new Bundle {
        val a = Input(UInt(4.W))
        val b = Input(UInt(4.W))
        val c = Output(UInt(4.W))
      })
     
      io.c := io.a & io.b
    }
     
    object ModuleGen extends App {
      chisel3.Driver.execute(args, () => new MyModule)
    }

    RawModule也可以包含时序逻辑,但要使用多时钟域语法。

    定义一个时钟域和复位域

    chisel3.core包里有一个单例对象withClockAndReset,其apply方法定义如下:

    def apply[T](clock: Clock, reset: Reset)(block: ⇒ T): T

    在编写代码时不能写成“import chisel3.core._”,这会扰乱“import chisel3._”的导入内容。正确做法是用“import chisel3.experimental._”导入experimental对象,它里面用同名字段引用了单例对象chisel3.core.withClockAndReset,这样就不需要再导入core包。

    class MultiClockModule extends Module {
       val io = IO(new Bundle {
           val clockB = Input(Clock())
           val resetB = Input(Bool())
           val stuff = Input(Bool())
       })
       // 这个寄存器跟随当前模块的隐式全局时钟clock
       val regClock1 = RegNext(io.stuff)
    
       withClockAndReset(io.clockB, io.resetB) {
           // 在该花括号内,所有时序元件都跟随时钟io.clockB
           // 所有寄存器的复位信号都是io.resetB
    
           // 这个寄存器跟随io.clockB
           val regClockB = RegNext(io.stuff)
           // 还可以例化其它模块
           val m = Module(new ChildModule)
        }
    
       // 这个寄存器跟随当前模块的隐式全局时钟clock
       val regClock2 = RegNext(io.stuff)
    }
    ————————————————

    因为第二个参数列表只有一个传名参数,所以可以把圆括号写成花括号,这样还有自动的分号推断。再加上传名参数的特性,尽管需要一个无参函数,但是可以省略书写“() =>”

    withClockAndReset(io.clockB, io.resetB) {
        sentence1
        sentence2
        ...
        sentenceN
    }

    实际上相当于:

    withClockAndReset(io.clockB, io.resetB)( () => (sentence1; sentence2; ...; sentenceN) )

    读者再仔细看一看apply方法的定义,它的第二个参数是一个函数,同时该函数的返回结果也是整个apply方法的返回结果

    class MultiClockModule extends Module {
       val io = IO(new Bundle {
           val clockB = Input(Clock())
           val resetB = Input(Bool())
           val stuff = Input(Bool())
       })
       
       val clockB_child = withClockAndReset(io.clockB, io.resetB) {
           Module(new ChildModule)
        }
    
       clockB_child.io.in := io.stuff  
    } 

    如果传名参数全都是定义,最后没有表达式用于返回,那么apply的返回结果类型自然就是Unit。

    class MultiClockModule extends Module {
       val io = IO(new Bundle {
           val clockB = Input(Clock())
           val resetB = Input(Bool())
           val stuff = Input(Bool())
       })
       
       val clockB_child = withClockAndReset(io.clockB, io.resetB) {
           val m = Module(new ChildModule)
        }
    
       clockB_child.m.io.in := io.stuff  
    } 

    除了单例对象withClockAndReset,还有单例对象withClock和withReset

    使用时钟负沿和低有效的复位信号

    可以改变其行为。复位信号比较简单,只需要加上取反符号或逻辑非符号。时钟信号稍微麻烦一些,需要先用asUInt方法把Clock类型转换成UInt类型,再用toBool转换成Bool类型,此时可以加上取反符号或逻辑非符号,最后再用asClock变回Clock类型

    // negclkrst.scala
    package test
     
    import chisel3._
    import chisel3.experimental._
     
    class NegativeClkRst extends RawModule {
      val io = IO(new Bundle {
        val in = Input(UInt(4.W))
        val myClk = Input(Clock())
        val myRst = Input(Bool())
        val out = Output(UInt(4.W))
      })
      
      withClockAndReset((~io.myClk.asUInt.toBool).asClock, ~io.myRst) {
        val temp = RegInit(0.U(4.W))
        temp := io.in
        io.out := temp
      }
    }
     
    object NegClkRstGen extends App {
      chisel3.Driver.execute(args, () => new NegativeClkRst)
    }

    示例:异步FIFO

    // FIFO.scala
    package fifo
     
    import chisel3._
    import chisel3.util._
    import chisel3.experimental._
     
    class FIFO( Int, depth: Int) extends RawModule {
      val io = IO(new Bundle {
        // write-domain
        val dataIn = Input(UInt(width.W))
        val writeEn = Input(Bool())
        val writeClk = Input(Clock())
        val full = Output(Bool())
        // read-domain
        val dataOut = Output(UInt(width.W))
        val readEn = Input(Bool())
        val readClk = Input(Clock())
        val empty = Output(Bool())
        // reset
        val systemRst = Input(Bool())
      })
     
      val ram = SyncReadMem(1 << depth, UInt(width.W))   // 2^depth
      val writeToReadPtr = Wire(UInt((depth + 1).W))  // to read clock domain
      val readToWritePtr = Wire(UInt((depth + 1).W))  // to write clock domain
     
      // write clock domain
      withClockAndReset(io.writeClk, io.systemRst) {
        val binaryWritePtr = RegInit(0.U((depth + 1).W))
        val binaryWritePtrNext = Wire(UInt((depth + 1).W))
        val grayWritePtr = RegInit(0.U((depth + 1).W))
        val grayWritePtrNext = Wire(UInt((depth + 1).W))
        val isFull = RegInit(false.B)
        val fullValue = Wire(Bool())
        val grayReadPtrDelay0 = RegNext(readToWritePtr)
        val grayReadPtrDelay1 = RegNext(grayReadPtrDelay0)
     
        binaryWritePtrNext := binaryWritePtr + (io.writeEn && !isFull).asUInt
        binaryWritePtr := binaryWritePtrNext
        grayWritePtrNext := (binaryWritePtrNext >> 1) ^ binaryWritePtrNext
        grayWritePtr := grayWritePtrNext
        writeToReadPtr := grayWritePtr
        fullValue := (grayWritePtrNext === Cat(~grayReadPtrDelay1(depth, depth - 1), grayReadPtrDelay1(depth - 2, 0)))
        isFull := fullValue
     
        when(io.writeEn && !isFull) {
          ram.write(binaryWritePtr(depth - 1, 0), io.dataIn)
        }
     
        io.full := isFull    
      }
      // read clock domain
      withClockAndReset(io.readClk, io.systemRst) {
        val binaryReadPtr = RegInit(0.U((depth + 1).W))
        val binaryReadPtrNext = Wire(UInt((depth + 1).W))
        val grayReadPtr = RegInit(0.U((depth + 1).W))
        val grayReadPtrNext = Wire(UInt((depth + 1).W))
        val isEmpty = RegInit(true.B)
        val emptyValue = Wire(Bool())
        val grayWritePtrDelay0 = RegNext(writeToReadPtr)
        val grayWritePtrDelay1 = RegNext(grayWritePtrDelay0)
     
        binaryReadPtrNext := binaryReadPtr + (io.readEn && !isEmpty).asUInt
        binaryReadPtr := binaryReadPtrNext
        grayReadPtrNext := (binaryReadPtrNext >> 1) ^ binaryReadPtrNext
        grayReadPtr := grayReadPtrNext
        readToWritePtr := grayReadPtr
        emptyValue := (grayReadPtrNext === grayWritePtrDelay1)
        isEmpty := emptyValue
     
        io.dataOut := ram.read(binaryReadPtr(depth - 1, 0), io.readEn && !isEmpty)
        io.empty := isEmpty
      }  
    }
     
    object FIFOGen extends App {
      chisel3.Driver.execute(args, () => new FIFO(args(0).toInt, args(1).toInt))
    }

    第二十三章 Chisel基础——函数的应用

    对于Chisel这样的高级语言,函数的使用更加方便,还能节省不少代码量。不管是用户自己写的函数、Chisel语言库里的函数还是Scala标准库里的函数,都能帮助用户节省构建电路的时间

    用函数抽象组合逻辑

    与Verilog一样,对于频繁使用的组合逻辑电路,可以定义成Scala的函数形式,然后通过函数调用的方式来使用它。这些函数既可以定义在某个单例对象里,供多个模块重复使用,也可以直接定义在电路模块里。

    // function.scala
    import chisel3._
     
    class UseFunc extends Module {
      val io = IO(new Bundle {
        val in = Input(UInt(4.W))
        val out1 = Output(Bool())
        val out2 = Output(Bool())
      })
     
      def clb(a: UInt, b: UInt, c: UInt, d: UInt): UInt =
        (a & b) | (~c & d)
     
      io.out1 := clb(io.in(0), io.in(1), io.in(2), io.in(3))
      io.out2 := clb(io.in(0), io.in(2), io.in(3), io.in(1))
    }

    用工厂方法简化模块的例化

    在Scala里,往往在类的伴生对象里定义一个工厂方法,来简化类的实例化。同样,Chisel的模块也是Scala的类,也可以在其伴生对象里定义工厂方法来简化例化、连线模块

    // mux4.scala
    import chisel3._
     
    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)
    }
     
    object Mux2 {
      def apply(sel: UInt, in0: UInt, in1: UInt) = {
        val m = Module(new Mux2)
        m.io.in0 := in0
        m.io.in1 := in1
        m.io.sel := sel
        m.io.out
      }
    }
     
    class Mux4 extends Module {
      val io = IO(new Bundle {
        val sel = Input(UInt(2.W))
        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 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))
    }

    用Scala的函数简化代码

    比如在生成长的序列上,利用Scala的函数就能减少大量的代码。

    利用Scala的for、yield组合可以产生相应的判断条件与输出结果的序列,再用zip函数将两个序列组成一个对偶序列,再把对偶序列作为MuxCase的参数,就能用几行代码构造出任意位数的译码器。


    // decoder.scala
    package decoder

    import chisel3._
    import chisel3.util._
    import chisel3.experimental._

    class Decoder(n: Int) extends RawModule {
    val io = IO(new Bundle {
    val sel = Input(UInt(n.W))
    val out = Output(UInt((1 << n).W))
    })

    val x = for(i <- 0 until (1 << n)) yield io.sel === i.U
    val y = for(i <- 0 until (1 << n)) yield 1.U << i
    io.out := MuxCase(0.U, x zip y)
    }

    object DecoderGen extends App {
    chisel3.Driver.execute(args, () => new Decoder(args(0).toInt))
    }

    Chisel的打印函数

    printf函数只能在Chisel的模块里使用,并且会转换成Verilog的系统函数“$fwrite”,包含在宏定义块“ `ifndef SYNTHESIS......`endif ”里。通过Verilog的宏定义,可以取消这部分不可综合的代码。因为后导入的chisel3包覆盖了Scala的标准包,所以Scala里的printf函数要写成“Predef.printf”的完整路径形式。
    ————————————————

    Scala风格

    Chisel自定义了一个p插值器,该插值器可以对字符串内的一些自定义表达式进行求值、Chiel类型转化成字符串类型等

    val myUInt = 33.U
    // 显示Chisel自定义的类型的数据
    printf(p"myUInt = $myUInt") // myUInt = 33
    // 显示成十六进制
    printf(p"myUInt = 0x${Hexadecimal(myUInt)}") // myUInt = 0x21
    // 显示成二进制
    printf(p"myUInt = ${Binary(myUInt)}") // myUInt = 100001
    // 显示成字符(ASCⅡ码)
    printf(p"myUInt = ${Character(myUInt)}") // myUInt = !

     对于自定义的Bundle类型,可以重写toPrintable方法来定制打印内容。当自定义的Bundle配合其他硬件类型例如Wire构成具体的硬件

    class Message extends Bundle {
      val valid = Bool()
      val addr = UInt(32.W)
      val length = UInt(4.W)
      val data = UInt(64.W)
      override def toPrintable: Printable = {
          val char = Mux(valid, 'v'.U, '-'.U)
          p"Message:
    " +
          p"  valid  : ${Character(char)}
    " +
          p"  addr   : 0x${Hexadecimal(addr)}
    " +
          p"  length : $length
    " +
          p"  data   : 0x${Hexadecimal(data)}
    "
      }
    }
    
    val myMessage = Wire(new Message)
    myMessage.valid := true.B
    myMessage.addr := "h1234".U
    myMessage.length := 10.U
    myMessage.data := "hdeadbeef".U
    
    printf(p"$myMessage")

    C风格

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

    Chisel的对数函数

    chisel3.util包里有一个单例对象Log2,它的一个apply方法接收一个Bits类型的参数,计算并返回该参数值以2为底的幂次。返回类型是UInt类型,并且是向下截断的

    chisel3.util包里还有四个单例对象:log2Ceil、log2Floor、log2Up和log2Down,它们的apply方法的参数都是Int和BigInt类型,返回结果都是Int类型。log2Ceil是把结果向上舍入,log2Floor则向下舍入。log2Up和log2Down不仅分别把结果向上、向下舍入,而且结果最小为1。

    单例对象isPow2的apply方法接收Int和BigInt类型的参数,判断该整数是不是2的n次幂,返回Boolean类型的结果

    与硬件相关的函数

    Reverse("b1101".U)  // 等于"b1011".U
    
    Reverse("b1101".U(8.W))  // 等于"b10110000".U
    
    Reverse(myUIntWire)  // 动态旋转

    单例对象Cat有两个apply方法,分别接收一个Bits类型的序列和Bits类型的重复参数,将它们拼接成一个UInt数。

    Cat("b101".U, "b11".U)  // 等于"b10111".U
    
    Cat(myUIntWire0, myUIntWire1)  // 动态拼接
    
    Cat(Seq("b101".U, "b11".U))  // 等于"b10111".U
    
    Cat(mySeqOfBits)  // 动态拼接 

    1计数器

    分别接收一个Bits类型的参数和Bool类型的序列,计算参数里“1”或“true.B”的个数,返回对应的UInt值

    PopCount(Seq(true.B, false.B, true.B, true.B))  // 等于3.U

    PopCount(Seq(false.B, false.B, true.B, false.B))  // 等于1.U

    PopCount("b1011".U)  // 等于3.U

    PopCount("b0010".U)  // 等于1.U

    PopCount(myUIntWire)  // 动态计数
    ————————————————

    独热码转换器

    OHToUInt("b1000".U)  // 等于3.U

    OHToUInt("b1000_0000".U)  // 等于7.U 

    无关位

    Verilog里可以用问号表示无关位,那么用case语句进行比较时就不会关心这些位。Chisel里有对应的BitPat类,可以指定无关位。

    "b10101".U === BitPat("b101??") // 等于true.B

    "b10111".U === BitPat("b101??") // 等于true.B

    "b10001".U === BitPat("b101??") // 等于false.B 

    dontCare方法接收一个Int类型的参数,构造等值位宽的全部无关位。例如:

    val myDontCare = BitPat.dontCare(4)  // 等于BitPat("b????") 

    一种是单例对象Lookup,其apply方法定义

    def apply[T <: Bits](addr: UInt, default: T, mapping: Seq[(BitPat, T)]): T 

    第二种是单例对象ListLookup,它的apply方法与上面的类似,区别在于返回结果是一个T类型的列表

    defapply[T <: Data](addr: UInt, default: List[T], mapping: Array[(BitPat, List[T])]): List[T] 

    第二十四章 Chisel基础——其它议题

    动态命名模块

    转成Verilog时的模块名不使用定义的类名,而是使用重写的desiredName方法的返回字符串。模块和黑盒都适用

    class Coffee extends BlackBox {
       val io = IO(new Bundle {
           val I = Input(UInt(32.W))
           val O = Output(UInt(32.W))
       })
       override def desiredName = "Tea"
    }
    
    class Salt extends Module {
       val io = IO(new Bundle {})
       val drink = Module(new Coffee)
       override def desiredName = "SodiumMonochloride"
    }

    动态修改端口

    选值以及if语句可以创建出可选的端口,在例化该模块时可以通过控制Boolean入参来生成不同的端口

    class ModuleWithOptionalIOs(flag: Boolean) extends Module {
       val io = IO(new Bundle {
           val in = Input(UInt(12.W))
           val out = Output(UInt(12.W))
           val out2 = if (flag) Some(Output(UInt(12.W))) else None
      })
      
       io.out := io.in
       if(flag) {
         io.out2.get := io.in
       }
    } 

    生成正确的块内信号名

    在when、withClockAndReset等语句块里定义的信号(线网和寄存器),转换成Verilog时不会生成正确的变量名

    package test
     
    import chisel3._
     
    class TestMod extends Module {
      val io = IO(new Bundle {
        val a = Input(Bool())
        val b = Output(UInt(4.W))
      })
      when (io.a) {
        val innerReg = RegInit(5.U(4.W))
        innerReg := innerReg + 1.U
        io.b := innerReg
      } .otherwise {
        io.b := 10.U
      }
    }
     
    object NameGen extends App {
      chisel3.Driver.execute(args, () => new TestMod)

    如果想让名字正确,则需要在build.sbt文件里加上:

    addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full) 

    拆包一个值(给拼接变量赋值)

    class MyBundle extends Bundle {
      val a = UInt(2.W)
      val b = UInt(4.W)
      val c = UInt(3.W)
    }
    
    val z = Wire(UInt(9.W))
    z := ...
    val unpacked = z.asTypeOf(new MyBundle)
    unpacked.a
    unpacked.b
    unpacked.c

    子字赋值

     在Verilog中,可以直接给向量的某几位赋值。同样,Chisel受限于Scala,不支持直接给Bits类型的某几位赋值

    办法是先调用Bits类型的toBools方法。该方法根据调用对象的0、1排列返回一个相应的Seq[Bool]类型的结果,并且低位在序列里的下标更小,比如第0位的下标就是0、第n位的下标就是n。然后用这个Seq[Bool]对象配合VecInit构成一个向量,此时就可以给单个比特赋值。注意,必须都是Bool类型,要注意赋值前是否需要类型转换。子字赋值完成后,Bool向量再调用asUInt、asSInt方法转换回来

    class TestModule extends Module {
       val io = IO(new Bundle {
           val in = Input(UInt(10.W))
           val bit = Input(Bool())
           val out = Output(UInt(10.W))
       })
       val bools = VecInit(io.in.toBools)
       bools(0) := io.bit
       io.out := bools.asUInt
    }

    参数化的Bundle

    Chisel提供了一个内部的API函数cloneType,任何继承自Data的Chisel对象,要复制自身时,都是由cloneType负责返回该对象的复制对象当自定义的Bundle的主构造方法没有参数时,Chisel会自动推断出如何构造Bundle对象的复制,原因很简单,因为构造一个新的复制对象不需要任何参数,仅仅使用关键字new就行了。但是,如果自定义的Bundle带有参数列表,那么Chisel就无法推断了,因为传递进去的参数可以是任意的,并不一定就是完全地复制。此时需要用户自己重写Bundle类的cloneType方法override def cloneType = (new CustomBundle(arguments)).asInstanceOf[this.type]

    class ExampleBundle(a: Int, b: Int) extends Bundle {

       val foo = UInt(a.W)

       val bar = UInt(b.W)

       override def cloneType = (new ExampleBundle(a, b)).asInstanceOf[this.type]

    }

     

    class ExampleBundleModule(btype: ExampleBundle) extends Module {

       val io = IO(new Bundle {

           val out = Output(UInt(32.W))

           val b = Input(chiselTypeOf(btype))

       })

       io.out := io.b.foo + io.b.bar

    }

     

    class Top extends Module {

       val io = IO(new Bundle {

           val out = Output(UInt(32.W))

           val in = Input(UInt(17.W))

       })

       val x = Wire(new ExampleBundle(31, 17))

       x := DontCare

       val m = Module(new ExampleBundleModule(x))

       m.io.b.foo := io.in

       m.io.b.bar := io.in

       io.out := m.io.out

    }

    Chisel泛型

    无论是Chisel的函数还是模块,都可以用类型参数和上、下界来泛化。在例化模块时,传入不同类型的参数,就可能会产生不同的电路,而无需编写额外的代码,当然前提是逻辑、类型必须正确。

    未驱动的线网

    Chisel的Invalidate API支持检测未驱动的输出型IO以及定义不完整的Wire定义,在编译成firrtl时会产生“not fully initialized”错误。换句话说,就是组合逻辑的真值表不完整,不能综合出完整的电路。如果确实需要不被驱动的线网,则可以赋给一个DontCare对象,这会告诉Firrtl编译器,该线网故意不被驱动。转换成的Verilog会赋予该信号全0值

    val io = IO(new Bundle {
        val outs = Output(Vec(10, Bool()))
    })
    io.outs <> DontCare 

    第二十五章 Chisel进阶——隐式参数的应用

    用Chisel编写的CPU,比如Rocket-Chip、RISCV-Mini等,都有一个特点,就是可以用一个配置文件来裁剪电路。这利用了Scala的模式匹配、样例类、偏函数、可选值、隐式定义等语法

    相关定义

    Chisel提供了一个内部的API函数cloneType,任何继承自Data的Chisel对象,要复制自身时,都是由cloneType负责返回该对象的复制对象

  • 相关阅读:
    nginx配置ssl并结局TP3.2路由pathinfo
    TP3.2公共模板
    linux 上mysql慢日志查询
    RBAC流程
    Linux下安装Lnmp环境
    php操作redis命令大全
    Opencv无法调用cvCaptureFromCAM无法打开电脑自带摄像头
    c++考研复习之非递归前序中序后序遍历二叉树
    学习《Numpy基础知识》
    学习《Numpy快速教程
  • 原文地址:https://www.cnblogs.com/flymood/p/12322260.html
Copyright © 2011-2022 走看看