数字集成电路十七:chisel学习小记

以下内容均摘自:https://blog.csdn.net/qq_34291505

一、Chisel的常见问题

在学习Chisel前,应该熟悉一些常见问题,这些问题在编写Chisel的任何时候都应该牢记。

①Chisel是寄宿在Scala里的语言,所以它本质还是Scala。为了从Chisel转变成Verilog,语言开发人员开发了一个中间的标准交换格式——Firrtl,它跟Vrilog是同一级别的,两者都比Chisel低一级。编写的Chisel代码首先会经过Firrtl编译器,生成Firrtl代码,也就是一个后缀格式为“.fir”的文件,然后由这个Firrtl文件再去生成对应的Verilog代码。如果读者有兴趣看一看Firrtl的格式,其实与Verilog很接近,只不过是由机器生成的、很死板的代码。Firrtl编译器也并不是只针对Chisel,有兴趣和能力的读者也可以开发针对Java、Python、C++等语言的Firrtl编译器。因为Firrtl只是一种标准的中间媒介,如何从一端到另一端,完全是自定义的。另外,Firrtl也并不仅仅是生成Verilog,同样可以开发工具生成VHDL、SystemVerilog等语言。

②Scala里的语法,在Chisel里也基本能用,比如Scala的基本值类、内建控制结构、函数抽象、柯里化、模式匹配、隐式参数等等。但是读者要记住这些代码不仅要通过Scala编译器的检查,还需要通过Firrtl编译器的检查。

③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.”等等。

⑦应该用一个名字有意义的包来打包实现某个功能的文件集。例如,要实现一个自定义的微处理器,则可以把顶层包命名为“mycpu”,进而再划分成“myio”、“mymem”、“mybus”、“myalu”等子包,每个子包里包含相关的源文件。

⑧Chisel现在仍在更新中,很可能会添加新功能或删去老功能。因此,本教程介绍的内容在将来并不一定就正确,读者应该持续关注Chisel3的GitHub的发展动向。

二、向量与混合向量

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

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]类似,只不过包含的元素可以不全都一样。它的工厂方法是通过重复参数或者序列作为参数来构造的,并且也有一个叫MixedVecInit[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))
   })

Bundle可以和UInt进行相互转换。Bundle类有一个方法asUInt,可以把所含的字段拼接成一个UInt数据,并且前面的字段在高位。例如:

class MyBundle extends Bundle {
   val foo = UInt(4.W)  // 高位
   val bar = UInt(4.W)  // 低位
}

val bundle = Wire(new MyBundle)
bundle.foo := 0xc.U
bundle.bar := 0x3.U
val uint = bundle.asUInt  // 12*16 + 3 = 195

有一个隐式类fromBitsable,可以把Data类型的对象转化成该类型,然后通过方法fromBits来接收一个Bits类型的参数来给该对象赋值。不过,该方法在Chisel3中已经被标注为过时,不推荐使用。例如:

class MyBundle extends Bundle {
   val foo = UInt(4.W)  // 高位
   val bar = UInt(4.W)  // 低位
}

val uint = 0xb4.U
val bundle = Wire(new MyBundle).fromBits(uint)  // foo = 11, bar = 4

四、Chisel的内建操作符

数字集成电路十七:chisel学习小记
数字集成电路十七:chisel学习小记
数字集成电路十七:chisel学习小记
数字集成电路十七:chisel学习小记
这里要注意的一点是相等性比较的两个符号是“ === ” 和 “ =/= ”。因为“ == ”和“ != ”已经被Scala占用,所以Chisel另设了这两个新的操作符。按照优先级的判断准则,“===”和“=/=”的优先级以首个字符为“=”来判断,也就是在逻辑操作中,相等性比较的优先级要比与、或、异或都高。

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

对应的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值。

六、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。在Verilog代码上,这两种RAM都是由reg类型的变量来表示的,区别在于第二种RAM的读地址会被地址寄存器寄存一次。例如:

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

写RAM的语法是:

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

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

读RAM的语法是:

out := mem.read(address, rd_en)

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

要综合出实际的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。

七、状态机

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

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

它会根据参数n返回对应元素数的List[UInt],每个元素都是不同的,所以可以作为枚举值来使用。最好把枚举状态的变量名也组成一个列表,然后用列表的模式匹配来进行赋值。有了枚举值后,可以通过“switch…is…is”语句来使用。其中,switch里是相应的状态寄存器,而每个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的编译器才能识别成变量模式匹配。它生成的Verilog为:

// DetectTwoOnes.v
module DetectTwoOnes(
  input   clock,
  input   reset,
  input   io_in,
  output  io_out
);
  reg [1:0] state; 
  wire  _T_1; 
  wire  _T_2; 
  wire  _T_3; 
  wire  _T_4; 
  assign _T_1 = 2'h0 == state; 
  assign _T_2 = 2'h1 == state; 
  assign _T_3 = 2'h2 == state; 
  assign _T_4 = io_in == 1'h0; 
  assign io_out = state == 2'h2;
 
  always @(posedge clock) begin
    if (reset) begin
      state <= 2'h0;
    end else begin
      if (_T_1) begin
        if (io_in) begin
          state <= 2'h1;
        end
      end else begin
        if (_T_2) begin
          if (io_in) begin
            state <= 2'h2;
          end else begin
            state <= 2'h0;
          end
        end else begin
          if (_T_3) begin
            if (_T_4) begin
              state <= 2'h0;
            end
          end
        end
      end
    end
  end
endmodule

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

前一章介绍了Chisel的数据类型,其中常用的就五种:UInt、SInt、Bool、Bundle和Vec[T]。本章介绍了硬件类型,最基本的是IO、Wire和Reg三种,还有指明端口方向的Input、Output和Flipped。Module是沿袭了Verilog用模块构建电路的规则,不仅让熟悉Verilog/VHDL的工程师方便理解,也便于从Chisel转化成Verilog代码。

数据类型必须配合硬件类型才能使用,它不能独立存在,因为编译器只会把硬件类型生成对应的Verilog代码。从语法规则上来讲,这两种类型也有很大的区别,编译器会对数据类型和硬件类型加以区分。尽管从Scala的角度来看,硬件类型对应的工厂方法仅仅是“封装”了一遍作为入参的数据类型,其返回结果没变,比如Wire的工厂方法定义为:

def apply[T <: Data](t: T)(implicit sourceInfo: SourceInfo, 
						            compileOptions: CompileOptions): T

可以看到,入参t的类型与返回结果的类型是一样的,但是还有配置编译器的隐式参数,很可能区别就源自这里。

但是从Chisel编译器的角度来看,这两者就是不一样。换句话说,硬件类型就好像在数据类型上“包裹了一层外衣(英文原文用单词binding来形容)”。比如,线网“Wire(UInt(8.W))”就像给数据类型“UInt(8.W)”包上了一个“Wire( )”。所以,在编写Chisel时,要注意哪些地方是数据类型,哪些地方又是硬件类型。这时,静态语言的优势便体现出来了,因为编译器会帮助程序员检查类型是否匹配。如果在需要数据类型的地方出现了硬件类型、在需要硬件类型的地方出现了数据类型,那么就会引发错误。程序员只需要按照错误信息去修改相应的代码,而不需要人工逐个检查。

例如,在前面介绍寄存器组的时候,示例代码里的一句是这样的:

val reg0 = RegNext(VecInit(io.a, io.a)) 

读者可能会好奇为什么不写成如下形式:

val reg0 = RegNext(Vec(2, io.a)) 

如果改成这样,那么编译器就会发出如下错误:

[error] chisel3.core.Binding$ExpectedChiselTypeException: vec type 
'chisel3.core.UInt@6147b2fd' must be a Chisel type, not hardware 

这是因为方法Vec期望第二个参数是数据类型,这样它才能推断出返回的Vec[T]是数据类型。但实际的“io.a”是经过Input封装过的硬件类型,导致Vec[T]变成了硬件类型,所以发生了类型匹配错误。错误信息里也明确指示了,“Chisel type”指的就是数据类型,“hardware”指的就是硬件类型,而vec的类型应该是“Chisel type”,不应该变成硬件。

Chisel提供了一个用户API——chiselTypeOf[T <: Data](target: T): T,其作用就是把硬件类型的“封皮”去掉,变成纯粹的数据类型。因此,读者可能会期望如下代码成功:

val reg0 = RegNext(Vec(2, chiselTypeOf(io.a)))  

但是编译器仍然发出了错误信息:

[error] chisel3.core.Binding$ExpectedHardwareException: reg next 
'Vec(chisel3.core.UInt@65b0972a, chisel3.core.UInt@25155aa4)' must be 
hardware, not a bare Chisel type. Perhaps you forgot to wrap it in Wire(_) or IO(_)? 

只不过,这次是RegNext出错了。chiselTypeOf确实把硬件类型变成了数据类型,所以Vec[T]的检查通过了。但RegNext是实打实的硬件——寄存器,它也需要根据入参来推断返回结果的类型,所以传入一个数据类型Vec[T]就引发了错误。错误信息还额外提示程序员,是否忘记了用Wire()或IO()来包裹裸露的数据类型。甚至是带有字面量的数据类型,比如“0.U(8.W)”这样的对象,也被当作是硬件类型。

综合考虑这两种错误,只有写成“val reg0 = RegNext(VecInit(io.a, io.a))”合适,因为VecInit专门接收硬件类型的参数来构造硬件向量,给VecInit传入数据类型反而会报错,尽管它的返回类型也是Vec[T]。另外,Reg(_)的参数是数据类型,不是硬件类型,所以示例代码中它的参数是Vec,而别的参数都是VecInit。

有了基本的数据类型和硬件类型后,就已经可以编写绝大多数组合逻辑与时序逻辑电路。下一章将介绍Chisel库里定义的常用原语,有了这些原语就能更快速地构建电路,而不需要只用这些基本类型来搭积木。

九、黑盒

因为Chisel的功能相对Verilog来说还不完善,所以设计人员在当前版本下无法实现的功能,就需要用Verilog来实现。在这种情况下,可以使用Chisel的BlackBox功能,它的作用就是向Chisel代码提供了用Verilog设计的电路的接口,使得Chisel层面的代码可以通过模块的端口来进行交互。

如果读者尝试在Chisel的模块里例化另一个模块,然后生成Verilog代码,就会发现端口名字里多了“io_”这样的字眼。很显然,这是因为Chisel要求模块的端口都是由字段“io”来引用的,语法分析脚本在生成Verilog代码时会保留这个端口名前缀。

假设有一个外部的Verilog模块,它的端口列表声明如下:

module Dut ( input [31: 0] a, input clk, input reset, output [3: 0] b );

按照Verilog的语法,它的例化代码应该是这样的:

Dut u0 ( .a(u0_a), .clk(u0_clk), .reset(u0_reset), .b(u0_b) ); 

其中,例化时的名字和连接的线网名是可以任意的,但是模块名“Dut”和端口名“.a”、“.clk”、“.reset”、 “.b”是固定的。

倘若把这个Verilog模块声明成普通的Chisel模块,然后直接例化使用,那么例化的Verilog代码就会变成:

Dut u0 ( .io_a(io_u0_a), .io_clk(io_u0_clk), .io_reset(io_u0_reset), .io_b(io_u0_b) );

也就是说,本来应该是“.a”,变成了“.io_a”。当然,这样做首先在Chisel层面上就不会成功,因为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)
}

它对应生成的Verilog代码为:

// UseDut.v
module UseDut(
  input         clock,
  input         reset,
  input  [31:0] io_toDut_a,
  output [3:0]  io_toDut_b
);
  wire [31:0] u0_a; // @[blackbox.scala 20:18]
  wire  u0_clk; // @[blackbox.scala 20:18]
  wire  u0_reset; // @[blackbox.scala 20:18]
  wire [3:0] u0_b; // @[blackbox.scala 20:18]
  Dut u0 ( // @[blackbox.scala 20:18]
    .a(u0_a),
    .clk(u0_clk),
    .reset(u0_reset),
    .b(u0_b)
  );
  assign io_toDut_b = u0_b; // @[blackbox.scala 25:14]
  assign u0_a = io_toDut_a; // @[blackbox.scala 22:11]
  assign u0_clk = clock; // @[blackbox.scala 23:13]
  assign u0_reset = reset; // @[blackbox.scala 24:15]
endmodule

可以看到,例化黑盒生成的Verilog代码,完全符合Verilog例化模块的语法规则。通过黑盒导入Verilog模块的端口列表给Chisel模块使用,然后把Chisel代码转换成Verilog,把它与导入的Verilog一同传递给EDA工具使用。

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就变成了:

...
  Dut #(.DATA_WIDTH(32), .MODE("Sequential"), .RESET("Asynchronous")) u0 ( 
  // @[blackbox.scala 23:18]
    .a(u0_a),
    .clk(u0_clk),
    .reset(u0_reset),
    .b(u0_b)
  );
...

通过这种方式,借助Verilog把Chisel的功能暂时补齐了。比如UCB发布的Rocket-Chip,就是用黑盒导入异步寄存器,供内部代码使用。

十、自定义时钟域和复位域

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

这结合了Scala的柯里化、传名参数和单参数列表的语法特性,让DSL语言的自定义方法看上去就跟内建的while、for、if等结构一样自然,所以Scala很适合构建DSL语言。

读者再仔细看一看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  
} 

现在,被例化的模块不是作为返回结果,而是变成了变量m的引用对象,故而传名参数是只有定义、没有有用的返回值的空函数。如果编译这个模块,就会得到“没有相关成员”的错误信息:

[error] /home/esperanto/chisel-template/src/main/scala/module.scala:42:16: 
value m is not a member of Unit
[error]   clockB_child.m.io.in := io.stuff
[error]                ^ 

如果独立时钟域有多个变量要与外部交互,则应该在模块内部的顶层定义全局的线网,让所有时钟域都能访问。

除了单例对象withClockAndReset,还有单例对象withClock和withReset,分别用于构建只有独立时钟和只有独立复位信号的作用域,三者的语法是一样的。

上一篇:php编译扩展库时报错:Cannot find autoconf. Please check your autoconf installation and the $PHP_AUTOCONF envi


下一篇:2020.11.30 字符串