天天看点

chisel常用的硬件原语(更新)

主体内容摘自:https://blog.csdn.net/qq_34291505/article/details/87862433

前两章介绍了基本的数据类型和硬件类型,已经足够编写基本的小规模电路。至于要如何生成Verilog,会在后续章节讲解。如果要编写大型电路,当然也可以一砖一瓦地搭建,但是费时费力,完全体现不出软件语言的优势。Chisel在语言库里定义了很多常用的硬件原语,读者可以直接导入相应的包来使用。让编译器多干活,让程序员少费力。

一、多路选择器

因为多路选择器是一个很常用的电路模块,所以Chisel内建了几种多路选择器。

  • 第一种形式是二输入多路选择器“

    Mux(sel, in1, in2)

    ”。sel是Bool类型,in1和in2的类型相同,都是Data的任意子类型。当sel为true.B时,返回in1,否则返回in2。Mux在chisel3包里。
  • 因为Mux仅仅是把一个输入返回,所以Mux可以内嵌Mux,构成n输入多路选择器,类似于嵌套的三元操作符。其形式为“

    Mux(c1, a, Mux(c2, b, Mux(..., default)))

    ”。第二种就是针对上述n输入多路选择器的简便写法,形式为“

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

    ”,它的展开与嵌套的Mux是一样的。第一个参数是默认情况下返回的结果,第二个参数是一个数组,数组的元素是对偶“(成立条件(

    Bool类型

    ),被选择的输入)”。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包里。
  • 第四种多路选择器是Mux1H ,是chisel3.util包里的独热码多路选择器,它的选择信号是一个独热码。如果零个或多个选择信号有效,则行为未定义。它有以下几种常用的定义形式:
val hotValue = Mux1H(io.selector,Seq(2.U,4.U,8.U,11.U))

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

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

以上三种形式是等价的,io.selector是一个UInt类型的数据,并且位宽不能小于待选择数据的个数。在第一种形式中,Mux1H会从低到高依次将io.selector的每一位作为一个选择信号,并和提供的被选择数据一一对应。

  • 第五种多路选择器是chisel3.util包里的优先级选择器PriorityMux,当多个选择信号有效时,按照定义时的顺序,返回更靠前的被选数据。有以下三种定义形式:
val priorityValue = PriorityMux(io.selector,Seq(2.U,4.U,8.U,11.U))

val priorityValue = PriorityMux(Seq(io.selector(0),io.selector(1),
									io.selector(2),io.selector(3)),Seq(2.U,4.U,8.U,11.U))

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

以上三种形式是等价的,io.selector是一个Bits类型的数据,位宽不小于待选择数据的个数。

内建的多路选择器会转换成Verilog的三元操作符“? :”,这对于构建组合逻辑而言是完全足够的,而且更推荐这种做法,所以when语句常用于给寄存器赋值,而很少用来给线网赋值。

读者可能习惯用always语句块来编写电路,但这存在一些问题:

  • 首先,always既可以综合出时序逻辑又能综合出组合逻辑,导致reg变量存在二义性,常常使得新手误解reg就是寄存器;
  • 其次,if…else 不能传播控制变量的未知态x(某些EDA工具可以),使得仿真阶段无法发现一些错误,但是assign语句会在控制变量为x时也输出x。

注1:

对这句话的理解是,verilog中的if是不能写成

if(a==x) b=0

这种形式的,即使你在testbench里面将a赋值为x,这条分支也不成立,总之这样写是不对的。正是因为这样,在仿真的时候,由于分支

if(a==x)

不成立,那么b就不会为0,那么你就错误的认为a不等于x,所以a的未知态就没有传播出去。

工业级的Verilog,都是用assign语句来构建电路。时序逻辑也是通过例化触发器模块来完成的,相应的端口都是由assign来驱动,而且触发器会使用SystemVerilog的断言来寻找always语句里的x和z。整个设计应该尽量避免使用always语句。

二、优先编码器

Chisel内建了两种优先编码器,它的作用是对多个输入信号中优先级最高的一个信号进行编码。

  • 第一种优先编码器是

    PriorityEncoder

    ,它有两种定义形式:
PriorityEncoder("b1010".U)

PriorityEncoder(Seq(true.B, false.B, true.B, false.B))
           

以上两种形式是等价的,返回值类型都是UInt,值为1.U。

  • 第二种优先编码器是

    PriorityEncoderOH

    ,它也有两种定义形式:
PriorityEncoderOH("b1010".U)
PriorityEncoderOH(Seq(false.B, true.B, true.B, false.B))
           

它和第一种编码器的区别在于该编码器会把编码结果转换成独热码。第一种形式返回一个UInt的数据2.U,第二种形式返回一个Seq:Seq(false.B, true.B, false.B, false.B)。

三、ROM

可以通过

apply

方法“

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)
}
           

对应的Verilog为:

// ROM.v
module ROM(
  input        clock,
  input        reset,
  input  [1:0] io_sel,
  output [7:0] io_out
);
  wire [2:0] _GEN_1; 
  wire [2:0] _GEN_2; 
  wire [2:0] _GEN_3; 
  assign _GEN_1 = 2'h1 == io_sel ? 3'h2 : 3'h1; 
  assign _GEN_2 = 2'h2 == io_sel ? 3'h3 : _GEN_1; 
  assign _GEN_3 = 2'h3 == io_sel ? 3'h4 : _GEN_2; 
  assign io_out = {{5'd0}, _GEN_3};
endmodule
           

在这个例子里需要提的一点是,Vec[T]类的apply方法不仅可以接收Int类型的索引值,另一个重载版本还能接收UInt类型的索引值。所以对于承担地址、计数器等功能的部件,可以直接作为由Vec[T]构造的元素的索引参数,比如这个例子中根据sel端口的值来选择相应地址的ROM值。

注2:

个人认为,并没有真正的ROM概念,只要能够存储数据并且可以将数据取出就行,所以不一定使用

vecinit,像vec、mixedvec、mixedvecinit以及reg

等都是可以的。如下面的例子:
//或者val rom = Reg(Vec(4, UInt(8.W)))
 val rom = Wire(Vec(4, UInt(8.W)))
  
 rom(0) := 1.U(8.W)
 rom(1) := 2.U(8.W)
 rom(2) := 3.U(8.W)
 rom(3) := 4.U(8.W)
 io.out := rom(io.sel)
           

生成的verilog代码是:

module ROM(
  input        clock,
  input        reset,
  input  [1:0] io_sel,
  output [7:0] io_out
);
  wire [7:0] _GEN_1 = 2'h1 == io_sel ? 8'h2 : 8'h1; // @[rom.scala 18:10 rom.scala 18:10]
  wire [7:0] _GEN_2 = 2'h2 == io_sel ? 8'h3 : _GEN_1; // @[rom.scala 18:10 rom.scala 18:10]
  assign io_out = 2'h3 == io_sel ? 8'h4 : _GEN_2; // @[rom.scala 18:10 rom.scala 18:10]
endmodule
           

四、RAM

Chisel支持两种类型的RAM,都在chisel3包里。

  • 第一种RAM是同步(时序)写,异步(组合逻辑)读,通过

    apply

    方法“

    Mem[T <: Data](size: Int, t: T)

    ”来构建。例如:

由于现代的FPGA和ASIC技术已经不再支持异步读RAM,所以这种RAM会被综合成寄存器阵列。

  • 第二种RAM则是同步(时序)读、写,通过

    apply

    方法“

    SyncReadMem[T <: Data](size: Int, t: T)

    ”来构建,这种RAM会被综合成实际的SRAM。在Verilog代码上,这两种RAM都是由reg类型的变量来表示的,区别在于第二种RAM的读地址会被地址寄存器寄存一次。例如:

写RAM的语法是:

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

其中DontCare告诉Chisel的未连接线网检测机制,写入RAM时读端口的行为无需关心。

读RAM的语法是:

读、写使能信号都可以省略。

不使用write和read方法,而是使用赋值语句,也可以实现读写,写法如下例所示:

import chisel3._
class RWSmem extends Module {
  val width: Int = 32
  val io = IO(new Bundle {
    val enable = Input(Bool())
    val write = Input(Bool())
    val addr = Input(UInt(10.W))
    val dataIn = Input(UInt(width.W))
    val dataOut = Output(UInt(width.W))
  })

  val mem = SyncReadMem(1024, UInt(width.W))
  io.dataOut := DontCare
  when(io.enable) {
    val rdwrPort = mem(io.addr)
    when (io.write) { rdwrPort := io.dataIn }
      .otherwise    { io.dataOut := rdwrPort }
  }
}
           

注3: 关于单双端口RAM

  • 当读写操作有效的条件,分别是同一条件的两种互斥的情况时,会被推断为单端口RAM。如:
val mem = SyncReadMem(1024, UInt(width.W))
  io.dataOut := DontCare
  when(io.enable) {
    when (io.write) { mem.write(io.addr, io.dataIn) }
      .otherwise    { io.dataOut := mem.read(io.addr) }
  }
           
  • 反之,如果读写操作有效的条件不互斥,那么会被推断为双口RAM。如下面这种写法:
val mem = SyncReadMem(1024, UInt(width.W))
  // Create one write port and one read port
  mem.write(io.addr, io.dataIn)
  io.dataOut := mem.read(io.addr, io.enable)
           

要综合出实际的SRAM,读者最好了解自己的综合器是如何推断的,按照综合器的推断规则来编写模块的端口定义、时钟域划分、读写使能的行为等等,否则就可能综合出寄存器阵列而不是SRAM。以Vivado 2018.3为例,下面的单端口SRAM代码经过综合后会映射到FPGA上实际的BRAM资源,而不是寄存器:

// ram.scala
package test
 
import chisel3._
class SinglePortRAM 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(Bool())
    val dataOut = Output(UInt(32.W))  
  })
  val syncRAM = SyncReadMem(1024, UInt(32.W))
  when(io.en) {
    when(io.we) {
      syncRAM.write(io.addr, io.dataIn)
      io.dataOut := DontCare
    } .otherwise {
      io.dataOut := syncRAM.read(io.addr)
    }
  } .otherwise {
    io.dataOut := DontCare
  }
}
           
chisel常用的硬件原语(更新)

Vivado的BRAM最多支持真·双端口,按照对应的Verilog模板逆向编写Chisel,然后用编译器把Chisel转换成Verilog。但此时编译器生成的Verilog代码并不能被Vivado的综合器识别出来。原因在于SyncReadMem生成的Verilog代码是用一级寄存器保存输入的读地址,然后用读地址寄存器去异步读取RAM的数据,而Vivado的综合器识别不出这种模式的RAM。读者必须手动修改成用一级寄存器保存异步读取的数据而不是读地址,然后把读数据寄存器的内容用assign语句赋值给读数据端口,这样才能被识别成真·双端口BRAM。

尚不清楚其它综合器是否有这个问题。经过咨询SiFive的工作人员,对方答复因为当前转换的代码把延迟放在地址一侧,所以流水线的节拍设计也是根据这个来的。考虑到贸然修改SyncReadMem的行为,可能会潜在地影响其它用户对流水线的设计,故而没有修改计划。如果确实需要自定义的、对综合器友好的Verilog代码,可以使用黑盒功能替代,或者给Firrtl编译器传入参数,改用自定义脚本来编译Chisel。

五、带写掩模的RAM(更新)

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

当构建RAM的数据类型为Vec[T]时,就会推断出该RAM具有写掩模。此时,需要定义一个Seq[Bool]类型的写掩模信号,序列的元素个数为数据写入端口的位宽除以字节宽度。而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(Bool())
    val mask = Input(Vec(4, Bool()))
    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 syncRAM = SyncReadMem(1024, Vec(4, UInt(8.W)))
  dataOut_temp := DontCare
  when(io.en) {
    when(io.we){
      syncRAM.write(io.addr, dataIn_temp, io.mask)
    }.otherwise{
      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)
    io.dataOut := Cat(dataOut_temp(3), dataOut_temp(2), dataOut_temp(1), dataOut_temp(0))
  }
}
           

读、写端口和写掩模可以不用定义成一个UInt,也可以是Vec[UInt],这样定义只是为了让模块对外只有一个读端口、一个写端口和一个写掩模端口。

注意,编译器会把Vec[T]的元素逐个展开,而不是合并成压缩数组的形式。也正是如此,上述代码对应的Verilog中,把RAM主体定义成了

“reg [7:0] syncRAM_0 [0:1023]”、“reg [7:0] syncRAM_1 [0:1023]”、“reg [7:0] syncRAM_2 [0:1023]”和“reg [7:0] syncRAM_3 [0:1023]”,而不是一个“reg [31:0] syncRAM [0:1023]”

。这样,Vivado综合出来的电路是四小块BRAM,而不是一大块BRAM。

读写的时候也可以不使用write和read方法,而是采用赋值的形式,如下例这种写法,同样可以实现带掩膜的写操作:

import chisel3._

class MaskedRWSmem extends Module {
  val width: Int = 32
  val io = IO(new Bundle {
    val enable = Input(Bool())
    val write = Input(Bool())
    val mask = Input(Vec(2, Bool()))
    val addr = Input(UInt(10.W))
    val dataIn = Input(Vec(2, UInt(width.W)))
    val dataOut = Output(Vec(2, UInt(width.W)))
  })

  val mem = SyncReadMem(1024, Vec(2, UInt(width.W)))
  io.dataOut := DontCare
  when(io.enable) {
    val rdwrPort = mem(io.addr)
    when (io.write) {
      when(io.mask(0)) {
        rdwrPort(0) := io.dataIn(0)
      }
      when(io.mask(1)) {
        rdwrPort(1) := io.dataIn(1)
      }
    }.otherwise { io.dataOut := rdwrPort }
  }
}
           

注4:

val syncRAM = SyncReadMem(1024, Vec(4, UInt(8.W)))

这种写法只是为了让编译器推断出该RAM具有写掩模,而和第三部分不带掩膜的RAM的定义方式:

val syncRAM = SyncReadMem(1024, UInt(32.W))

相比,这两种写法没有必然的联系。
我一开始以为难道因为4*8=32,这两种写法就等价吗。其实并不是,由上面的分析已经可知,RAM从1块变成了4块。而且,既然定义成了这样,那么读写数据也要类型一致,如

val dataIn_temp = Wire(Vec(4, UInt(8.W))),val dataOut_temp = Wire(Vec(4, UInt(8.W)))

六、从文件读取数据到RAM

chisel3.util.experimental包里有一个单例对象loadMemoryFromFile

,它的apply方法可以在Chisel层面上从txt文件读取数据到RAM里。其定义如下所示:

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

该方法其实就是调用Verilog的系统函数

“$readmemh”和“$readmemb”

,所以要注意文件路径的书写和数据的格式都要按照Verilog的要求书写,比如下面这种格式:

7

d

15

举例:

// loadmem.scala
package test
 
import chisel3._
import chisel3.util.experimental.{loadMemoryFromFile,loadMemoryFromFileInline}
 
class LoadMem extends Module {
  val io = IO(new Bundle {
    val address = Input(UInt(3.W))
    val value   = Output(UInt(8.W))
  })
  val memory = Mem(8, UInt(8.W))
  io.value := memory.read(io.address)
  loadMemoryFromFile(memory, "/home/alex/Desktop/chisel-examples/mem.txt")
}
           

那么就会得到两个Verilog文件:

// LoadMem.v
module LoadMem(
  input        clock,
  input        reset,
  input  [2:0] io_address,
  output [7:0] io_value
);
`ifdef RANDOMIZE_MEM_INIT
  reg [31:0] _RAND_0;
`endif // RANDOMIZE_MEM_INIT
  reg [7:0] memory [0:7]; // @[LoadMemTester.scala 12:19]
  wire [7:0] memory_MPORT_data; // @[LoadMemTester.scala 12:19]
  wire [2:0] memory_MPORT_addr; // @[LoadMemTester.scala 12:19]
  assign memory_MPORT_addr = io_address;
  assign memory_MPORT_data = memory[memory_MPORT_addr]; // @[LoadMemTester.scala 12:19]
  assign io_value = memory_MPORT_data; // @[LoadMemTester.scala 13:12]
endmodule
 
// LoadMem.LoadMem.memory.v
module BindsTo_0_LoadMem(
  input        clock,
  input        reset,
  input  [2:0] io_address,
  output [7:0] io_value
);

initial begin
  $readmemh("/home/alex/Desktop/chisel-examples/mem.txt", LoadMem.memory);
end
endmodule

bind LoadMem BindsTo_0_LoadMem BindsTo_0_LoadMem_Inst(.*);
           

在用Verilator仿真时,它会识别这个Chisel代码,从文件读取数据。

注5:

在chisel3.4.3版本(目前最新),还引入了一个单例对象

loadMemoryFromFileInline

,它的使用方法和

loadMemoryFromFile

一模一样,只不过不再单独生成一个用来读取文件数据到mem的BindsTo_0_LoadMem模块,而是将读取文件数据的代码直接嵌入到LoadMem模块中,如下所示:
module LoadMem(
  input        clock,
  input        reset,
  input  [2:0] io_address,
  output [7:0] io_value
);
  reg [7:0] memory [0:7]; // @[LoadMemTester.scala 12:19]
  wire [7:0] memory_MPORT_data; // @[LoadMemTester.scala 12:19]
  wire [2:0] memory_MPORT_addr; // @[LoadMemTester.scala 12:19]
  assign memory_MPORT_addr = io_address;
  assign memory_MPORT_data = memory[memory_MPORT_addr]; // @[LoadMemTester.scala 12:19]
  assign io_value = memory_MPORT_data; // @[LoadMemTester.scala 13:12]
// Register and memory initialization
`ifdef RANDOMIZE_GARBAGE_ASSIGN
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_INVALID_ASSIGN
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_REG_INIT
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_MEM_INIT
`define RANDOMIZE
`endif
`ifndef RANDOM
`define RANDOM $random
`endif
  integer initvar;
`ifndef SYNTHESIS
`ifdef FIRRTL_BEFORE_INITIAL
`FIRRTL_BEFORE_INITIAL
`endif
initial begin
  `ifdef RANDOMIZE
    `ifdef INIT_RANDOM
      `INIT_RANDOM
    `endif
    `ifndef VERILATOR
      `ifdef RANDOMIZE_DELAY
        #`RANDOMIZE_DELAY begin end
      `else
        #0.002 begin end
      `endif
    `endif
  `endif // RANDOMIZE
  $readmemh("/home/alex/Desktop/chisel-examples/mem.txt", memory);
end // initial
`ifdef FIRRTL_AFTER_INITIAL
`FIRRTL_AFTER_INITIAL
`endif
`endif // SYNTHESIS
endmodule

           

七、计数器(更新)

计数器也是一个常用的硬件电路。在Chisel.util包里定义了一个自增计数器原语Counter,它的

apply

方法是:

apply(cond: Bool, n: Int): (UInt, Bool)

,接收两个参数:

  • 第一个参数是Bool类型的使能信号,为true.B时计数器从0开始每个时钟上升沿加1自增,为false.B时则计数器保持不变;
  • 第二个参数需要一个Int类型的具体正数,当计数到n时归零。

该方法返回一个二元组:

  • 其第一个元素是计数器的计数值,
  • 第二个元素是判断计数值是否等于n的结果。

apply

方法还有两个重载版本:

  • def apply(r: Range, enable: Bool = true.B, reset: Bool = false.B): (UInt, Bool)

    ,该版本的返回值和上述一样,只不过入参有所不同:
  • r:是一个

    scala.collection.immutable.Range

    类型的参数,用来提供一个计数的范围。
  • enable:有效时表示该clk使能计数器计数。
  • reset:有效时表示该clk使计数器复位到初始值,这里的初始值是提供的计数范围的起始值。如果不提供,那么默认跟随隐式复位信号,如果提供,那么就会有多个复位信号。
  • apply(n: Int): Counter

    ,和上述两个方法不同,该方法返回的是一个Counter对象,有如下的属性和方法可以使用:

def inc(): Bool

        随时钟使计数器+1,返回值表示计数器是否在下一个时钟周期结束

def n: Int

        无参函数,返回计数器的计数最大值n

def range: Range

        无参函数,返回计数器的计数范围,类型为Range

def reset(): Unit

        使计数器复位到初始值0

val value: UInt

        表示计数器此时的计数值,因为是val类型,所以只能读取

返回一个对象之后,就需要根据所需逻辑,手动的使能、复位计数,或者获取此时的计数器状态。

假设我们想从0计数到233,那么分别使用上述三种版本的apply方法的写法如下:

  • apply(cond: Bool, n: Int): (UInt, Bool)

// 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
}
           

生成的verilog代码如下:

module MyCounter(
  input        clock,
  input        reset,
  input        io_en,
  output [7:0] io_out,
  output       io_valid
);
`ifdef RANDOMIZE_REG_INIT
  reg [31:0] _RAND_0;
`endif // RANDOMIZE_REG_INIT
  reg [7:0] a; // @[Counter.scala 60:40]
  wire  wrap_wrap = a == 8'he8; // @[Counter.scala 72:24]
  wire [7:0] _wrap_value_T_1 = a + 8'h1; // @[Counter.scala 76:24]
  assign io_out = a; // @[MyCounter.scala 15:10]
  assign io_valid = io_en & wrap_wrap; // @[Counter.scala 118:17 Counter.scala 118:24]
  always @(posedge clock) begin
    if (reset) begin // @[Counter.scala 60:40]
      a <= 8'h0; // @[Counter.scala 60:40]
    end else if (io_en) begin // @[Counter.scala 118:17]
      if (wrap_wrap) begin // @[Counter.scala 86:20]
        a <= 8'h0; // @[Counter.scala 86:28]
      end else begin
        a <= _wrap_value_T_1; // @[Counter.scala 76:15]
      end
    end
  end
endmodule
           
  • def apply(r: Range, enable: Bool = true.B, reset: Bool = false.B): (UInt, Bool)

import chisel3._
import chisel3.util._
import scala.collection.immutable.Range

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(Range(0,233),io.en)
  io.out := a
  io.valid := b
}
           

生成的verilog代码如下:

module MyCounter(
  input        clock,
  input        reset,
  input        io_en,
  output [7:0] io_out,
  output       io_valid
);
`ifdef RANDOMIZE_REG_INIT
  reg [31:0] _RAND_0;
`endif // RANDOMIZE_REG_INIT
  reg [7:0] a; // @[Counter.scala 60:40]
  wire  wrap_wrap = a == 8'he8; // @[Counter.scala 72:24]
  wire [7:0] _wrap_value_T_1 = a + 8'h1; // @[Counter.scala 76:24]
  assign io_out = a; // @[MyCounter.scala 17:10]
  assign io_valid = io_en & wrap_wrap; // @[Counter.scala 137:24 Counter.scala 138:12]
  always @(posedge clock) begin
    if (reset) begin // @[Counter.scala 60:40]
      a <= 8'h0; // @[Counter.scala 60:40]
    end else if (io_en) begin // @[Counter.scala 137:24]
      if (wrap_wrap) begin // @[Counter.scala 86:20]
        a <= 8'h0; // @[Counter.scala 86:28]
      end else begin
        a <= _wrap_value_T_1; // @[Counter.scala 76:15]
      end
    end
  end
endmodule
           
  • apply(n: Int): Counter

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 cnt = Counter(233)

  when(io.en){
    cnt.inc()
  }

  val a = cnt.value
  val b = cnt.value === cnt.n.U

  io.out := a
  io.valid := b
}
           
module MyCounter(
  input        clock,
  input        reset,
  input        io_en,
  output [7:0] io_out,
  output       io_valid
);
`ifdef RANDOMIZE_REG_INIT
  reg [31:0] _RAND_0;
`endif // RANDOMIZE_REG_INIT
  reg [7:0] value; // @[Counter.scala 60:40]
  wire  wrap = value == 8'he8; // @[Counter.scala 72:24]
  wire [7:0] _value_T_1 = value + 8'h1; // @[Counter.scala 76:24]
  assign io_out = value; // @[MyCounter.scala 22:10]
  assign io_valid = value == 8'he9; // @[MyCounter.scala 20:21]
  always @(posedge clock) begin
    if (reset) begin // @[Counter.scala 60:40]
      value <= 8'h0; // @[Counter.scala 60:40]
    end else if (io_en) begin // @[MyCounter.scala 15:14]
      if (wrap) begin // @[Counter.scala 86:20]
        value <= 8'h0; // @[Counter.scala 86:28]
      end else begin
        value <= _value_T_1; // @[Counter.scala 76:15]
      end
    end
  end
endmodule
           

八、线性反馈移位寄存器(更新)

如果要产生

伪随机数

,可以使用chisel3.util.random包里的线性反馈移位寄存器原语

LFSR

def apply(width: Int, increment: Bool = true.B, seed: Option[BigInt] = Some(1)): UInt
           
  • 第一个参数是寄存器的位宽。
  • 第二个参数是一个

    Bool类型

    的使能信号,用于控制寄存器是否移位,缺省值为true.B。
  • 第三个参数是一个随机种子,是可选值类型。

它返回一个UInt(width.W)类型的结果。例如:

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

  io.out := LFSR(16,io.en,Some(1))
}
           

它生成的主要Verilog代码为:

// LFSR.v
module LFSR16(
  input   clock,
  input   reset,
  input   io_increment,
  output  io_out_0,
  output  io_out_1,
  output  io_out_2,
  output  io_out_3,
  output  io_out_4,
  output  io_out_5,
  output  io_out_6,
  output  io_out_7,
  output  io_out_8,
  output  io_out_9,
  output  io_out_10,
  output  io_out_11,
  output  io_out_12,
  output  io_out_13,
  output  io_out_14,
  output  io_out_15
);
`ifdef RANDOMIZE_REG_INIT
  reg [31:0] _RAND_0;
  reg [31:0] _RAND_1;
  reg [31:0] _RAND_2;
  reg [31:0] _RAND_3;
  reg [31:0] _RAND_4;
  reg [31:0] _RAND_5;
  reg [31:0] _RAND_6;
  reg [31:0] _RAND_7;
  reg [31:0] _RAND_8;
  reg [31:0] _RAND_9;
  reg [31:0] _RAND_10;
  reg [31:0] _RAND_11;
  reg [31:0] _RAND_12;
  reg [31:0] _RAND_13;
  reg [31:0] _RAND_14;
  reg [31:0] _RAND_15;
`endif // RANDOMIZE_REG_INIT
  reg  state_0; // @[PRNG.scala 47:50]
  reg  state_1; // @[PRNG.scala 47:50]
  reg  state_2; // @[PRNG.scala 47:50]
  reg  state_3; // @[PRNG.scala 47:50]
  reg  state_4; // @[PRNG.scala 47:50]
  reg  state_5; // @[PRNG.scala 47:50]
  reg  state_6; // @[PRNG.scala 47:50]
  reg  state_7; // @[PRNG.scala 47:50]
  reg  state_8; // @[PRNG.scala 47:50]
  reg  state_9; // @[PRNG.scala 47:50]
  reg  state_10; // @[PRNG.scala 47:50]
  reg  state_11; // @[PRNG.scala 47:50]
  reg  state_12; // @[PRNG.scala 47:50]
  reg  state_13; // @[PRNG.scala 47:50]
  reg  state_14; // @[PRNG.scala 47:50]
  reg  state_15; // @[PRNG.scala 47:50]
  wire  _T_2 = state_15 ^ state_13 ^ state_12 ^ state_10; // @[LFSR.scala 15:41]
  wire  _GEN_0 = io_increment ? _T_2 : state_0; // @[PRNG.scala 61:23 PRNG.scala 62:11 PRNG.scala 47:50]
  assign io_out_0 = state_0; // @[PRNG.scala 69:10]
  assign io_out_1 = state_1; // @[PRNG.scala 69:10]
  assign io_out_2 = state_2; // @[PRNG.scala 69:10]
  assign io_out_3 = state_3; // @[PRNG.scala 69:10]
  assign io_out_4 = state_4; // @[PRNG.scala 69:10]
  assign io_out_5 = state_5; // @[PRNG.scala 69:10]
  assign io_out_6 = state_6; // @[PRNG.scala 69:10]
  assign io_out_7 = state_7; // @[PRNG.scala 69:10]
  assign io_out_8 = state_8; // @[PRNG.scala 69:10]
  assign io_out_9 = state_9; // @[PRNG.scala 69:10]
  assign io_out_10 = state_10; // @[PRNG.scala 69:10]
  assign io_out_11 = state_11; // @[PRNG.scala 69:10]
  assign io_out_12 = state_12; // @[PRNG.scala 69:10]
  assign io_out_13 = state_13; // @[PRNG.scala 69:10]
  assign io_out_14 = state_14; // @[PRNG.scala 69:10]
  assign io_out_15 = state_15; // @[PRNG.scala 69:10]
  always @(posedge clock) begin
    state_0 <= reset | _GEN_0; // @[PRNG.scala 47:50 PRNG.scala 47:50]
    if (reset) begin // @[PRNG.scala 47:50]
      state_1 <= 1'h0; // @[PRNG.scala 47:50]
    end else if (io_increment) begin // @[PRNG.scala 61:23]
      state_1 <= state_0; // @[PRNG.scala 62:11]
    end
    if (reset) begin // @[PRNG.scala 47:50]
      state_2 <= 1'h0; // @[PRNG.scala 47:50]
    end else if (io_increment) begin // @[PRNG.scala 61:23]
      state_2 <= state_1; // @[PRNG.scala 62:11]
    end
    if (reset) begin // @[PRNG.scala 47:50]
      state_3 <= 1'h0; // @[PRNG.scala 47:50]
    end else if (io_increment) begin // @[PRNG.scala 61:23]
      state_3 <= state_2; // @[PRNG.scala 62:11]
    end
    if (reset) begin // @[PRNG.scala 47:50]
      state_4 <= 1'h0; // @[PRNG.scala 47:50]
    end else if (io_increment) begin // @[PRNG.scala 61:23]
      state_4 <= state_3; // @[PRNG.scala 62:11]
    end
    if (reset) begin // @[PRNG.scala 47:50]
      state_5 <= 1'h0; // @[PRNG.scala 47:50]
    end else if (io_increment) begin // @[PRNG.scala 61:23]
      state_5 <= state_4; // @[PRNG.scala 62:11]
    end
    if (reset) begin // @[PRNG.scala 47:50]
      state_6 <= 1'h0; // @[PRNG.scala 47:50]
    end else if (io_increment) begin // @[PRNG.scala 61:23]
      state_6 <= state_5; // @[PRNG.scala 62:11]
    end
    if (reset) begin // @[PRNG.scala 47:50]
      state_7 <= 1'h0; // @[PRNG.scala 47:50]
    end else if (io_increment) begin // @[PRNG.scala 61:23]
      state_7 <= state_6; // @[PRNG.scala 62:11]
    end
    if (reset) begin // @[PRNG.scala 47:50]
      state_8 <= 1'h0; // @[PRNG.scala 47:50]
    end else if (io_increment) begin // @[PRNG.scala 61:23]
      state_8 <= state_7; // @[PRNG.scala 62:11]
    end
    if (reset) begin // @[PRNG.scala 47:50]
      state_9 <= 1'h0; // @[PRNG.scala 47:50]
    end else if (io_increment) begin // @[PRNG.scala 61:23]
      state_9 <= state_8; // @[PRNG.scala 62:11]
    end
    if (reset) begin // @[PRNG.scala 47:50]
      state_10 <= 1'h0; // @[PRNG.scala 47:50]
    end else if (io_increment) begin // @[PRNG.scala 61:23]
      state_10 <= state_9; // @[PRNG.scala 62:11]
    end
    if (reset) begin // @[PRNG.scala 47:50]
      state_11 <= 1'h0; // @[PRNG.scala 47:50]
    end else if (io_increment) begin // @[PRNG.scala 61:23]
      state_11 <= state_10; // @[PRNG.scala 62:11]
    end
    if (reset) begin // @[PRNG.scala 47:50]
      state_12 <= 1'h0; // @[PRNG.scala 47:50]
    end else if (io_increment) begin // @[PRNG.scala 61:23]
      state_12 <= state_11; // @[PRNG.scala 62:11]
    end
    if (reset) begin // @[PRNG.scala 47:50]
      state_13 <= 1'h0; // @[PRNG.scala 47:50]
    end else if (io_increment) begin // @[PRNG.scala 61:23]
      state_13 <= state_12; // @[PRNG.scala 62:11]
    end
    if (reset) begin // @[PRNG.scala 47:50]
      state_14 <= 1'h0; // @[PRNG.scala 47:50]
    end else if (io_increment) begin // @[PRNG.scala 61:23]
      state_14 <= state_13; // @[PRNG.scala 62:11]
    end
    if (reset) begin // @[PRNG.scala 47:50]
      state_15 <= 1'h0; // @[PRNG.scala 47:50]
    end else if (io_increment) begin // @[PRNG.scala 61:23]
      state_15 <= state_14; // @[PRNG.scala 62:11]
    end
  end
endmodule
           

九、XOR和XNOR

chisel3.util.random

下面还有两个原语,

异或XOR和同或XNOR

,使用非常简单,入参是两个Bool类型的数据,返回值也是Bool类型的数据。

chisel常用的硬件原语(更新)
chisel常用的硬件原语(更新)

十、总结

本章介绍了Chisel内建的常用原语,使用原语可以帮助我们极大的简化代码设计,避免重复造轮子!!!

继续阅读