chisel多时钟域设计(注释)

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

  • 在Verilog里,多时钟域的设计很简单,只需声明多个时钟端口,然后不同的always语句块根据需要选择不同的时钟作为敏感变量即可。
  • 在Chisel里,则相对复杂一些,因为这与Scala的变量作用域相关,而且时序元件在编译时都是自动地隐式跟随当前时钟域

本章将介绍多时钟域设计的语法,这其实很简单。

一、没有隐式端口的模块

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

这样的模块一般用于纯组合逻辑。在类内顶层不能出现使用时钟的相关操作(这里之所以说顶层,是因为还会存在自定义时钟域和复位域),比如定义寄存器(因为寄存器是时序元件,需要用到时钟和复位信号),否则会报错没有隐式端口。例如:

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

它生成的Verilog代码为:

// MyModule.v
module MyModule(
  input  [3:0] io_a,
  input  [3:0] io_b,
  output [3:0] io_c
);
  assign io_c = io_a & io_b; // @[module.scala 13:8]
endmodule

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

二、定义一个时钟域和复位域

chisel3包里有一个单例对象withClockAndReset(这点和原文不同,因为该单例对象已经移到了chisel3包里面),其apply方法定义如下:

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

该方法的作用就是创建一个新的时钟和复位域,作用范围仅限于它的传名参数的内部

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

注意不要忘记在IO中定义自定义的时钟域和复位域要用的时钟和复位信号的端口;使用隐式时钟和复位域时不用定义,是因为隐式的时钟和复位信号的端口会被自动添加。

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

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]                ^ 

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

注1:

注意,是定义在模块内部的顶层,不能定义在某一个时钟域内,否则外顶层和其他时钟域都无法访问

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


下面再举一个例子来引出一些细节

package grammer

import chisel3._

class ChildModule extends Module {
  val io = IO(new Bundle{
    val in = Input(Bool())
    val clockChild = Input(Clock())
    val out = Output(Bool())
  })
  withClock(io.clockChild){
  	//该寄存器跟随时钟io.clockChild,隐式复位信号reset
    val regclock = RegNext(io.in,0.U)
    io.out := regclock
  }
}
class MultiClockTester extends Module {
   val io = IO(new Bundle {
   	   //注意不要忘记定义自定义的时钟和复位信号端口,
   	   //以前不定义,是因为隐式的时钟和复位信号会自动添加这两个端口
       val clockA = Input(Clock())
       val resetA = Input(Bool())
       val clockChild = Input(Clock())
       val resetB = Input(Bool())
       val stuff_in = Input(Bool())
       val stuff_out = Output(Bool())
       val outregClock = Output(Bool())
       val outregClockA = Output(Bool())
       val outregClockB = Output(Bool())
   })
   // 这个寄存器跟随当前模块的隐式全局时钟clock
   val regClock = RegNext(io.stuff_in,0.U)

   val clockA_child = withClockAndReset(io.clockA,io.resetA.asAsyncReset()) {
       // 在该花括号内,所有时序元件都跟随时钟io.clockA
       // 所有寄存器的复位信号都是io.resetA

       // 这个寄存器跟随io.clockA
       val regClockA = RegNext(io.stuff_in,0.U)

       regClock := regClockA
       io.outregClockA := regClockA

       Module(new ChildModule)
    }
    clockA_child.io.clockChild := io.clockChild
    clockA_child.io.in := io.stuff_in
    io.stuff_out := clockA_child.io.out

   withReset(io.resetB) {
       // 在该花括号内,所有时序元件都跟随时钟隐式时钟clock
       // 所有寄存器的复位信号都是io.resetB

       // 这个寄存器跟随clock
       val regClockB = RegNext(io.stuff_in,0.U)
       io.outregClock := regClock
       io.outregClockB := regClockB
    }
}

object MultiClockTester extends App {
  (new chisel3.stage.ChiselStage).emitVerilog(new MultiClockTester(), Array("--target-dir", "generated"))
}

生成的部分verilog代码如下:

module ChildModule(
  input   reset,
  input   io_in,
  input   io_clockChild,
  output  io_out
);
`ifdef RANDOMIZE_REG_INIT
  reg [31:0] _RAND_0;
`endif // RANDOMIZE_REG_INIT
  reg  REG; // @[MultiClockTester.scala 12:27]
  assign io_out = REG; // @[MultiClockTester.scala 13:12]
  always @(posedge io_clockChild or posedge reset) begin
    if (reset) begin
      REG <= 1'h0;
    end else begin
      REG <= io_in;
    end
  end
endmodule
module MultiClockTester(
  input   clock,
  input   reset,
  input   io_clockA,
  input   io_resetA,
  input   io_clockChild,
  input   io_resetB,
  input   io_stuff_in,
  output  io_stuff_out,
  output  io_outregClock,
  output  io_outregClockA,
  output  io_outregClockB
);
`ifdef RANDOMIZE_REG_INIT
  reg [31:0] _RAND_0;
  reg [31:0] _RAND_1;
  reg [31:0] _RAND_2;
`endif // RANDOMIZE_REG_INIT
  wire  clockA_child_reset; // @[MultiClockTester.scala 43:14]
  wire  clockA_child_io_in; // @[MultiClockTester.scala 43:14]
  wire  clockA_child_io_clockChild; // @[MultiClockTester.scala 43:14]
  wire  clockA_child_io_out; // @[MultiClockTester.scala 43:14]
  reg  regClock; // @[MultiClockTester.scala 31:26]
  reg  REG; // @[MultiClockTester.scala 38:31]
  reg  REG_1; // @[MultiClockTester.scala 54:31]
  ChildModule clockA_child ( // @[MultiClockTester.scala 43:14]
    .reset(clockA_child_reset),
    .io_in(clockA_child_io_in),
    .io_clockChild(clockA_child_io_clockChild),
    .io_out(clockA_child_io_out)
  );
  assign io_stuff_out = clockA_child_io_out; // @[MultiClockTester.scala 47:18]
  assign io_outregClock = regClock; // @[MultiClockTester.scala 55:23]
  assign io_outregClockA = REG; // @[MultiClockTester.scala 41:24]
  assign io_outregClockB = REG_1; // @[MultiClockTester.scala 56:24]
  assign clockA_child_reset = io_resetA; // @[MultiClockTester.scala 33:72]
  assign clockA_child_io_in = io_stuff_in; // @[MultiClockTester.scala 46:24]
  assign clockA_child_io_clockChild = io_clockChild; // @[MultiClockTester.scala 45:32]
  always @(posedge clock) begin
    if (reset) begin // @[MultiClockTester.scala 31:26]
      regClock <= 1'h0; // @[MultiClockTester.scala 31:26]
    end else begin
      regClock <= REG; // @[MultiClockTester.scala 40:17]
    end
    if (io_resetB) begin // @[MultiClockTester.scala 54:31]
      REG_1 <= 1'h0; // @[MultiClockTester.scala 54:31]
    end else begin
      REG_1 <= io_stuff_in; // @[MultiClockTester.scala 54:31]
    end
  end
  always @(posedge io_clockA or posedge io_resetA) begin
    if (io_resetA) begin
      REG <= 1'h0;
    end else begin
      REG <= io_stuff_in;
    end
  end
endmodule

注2:

从上面的例子中可以看出:

  • 一个寄存器只和它定义时所处的时钟域有关,即使在其他时钟域被赋值,那么它还是跟随自己的时钟。从regClock在顶层定义,在时钟域A中被赋值,然后verilog代码中regClock还是跟随隐式的clock时钟可以看出来。
  • 不同的时钟域,会生成不同的always块,它们的敏感变量不一样,都是自己时钟域的时钟信号。但是不同的复位域不会产生不同的always块,因为复位信号属于哪一个always块取决于它跟随哪一个时钟信号。如上例中的io_resetB,它就在always @(posedge clock)块中。
  • always块中使用的时钟和复位信号默认都是上升沿。
  • 时钟域是可以嵌套的,当前的时钟域会覆盖掉上一层的时钟域。隐式的时钟域属于最顶层的时钟域,当我们再自定义时钟域时,自定义的时钟域就会覆盖掉顶层隐式的时钟域。同理,我们还可以在自定义时钟域中再定义子时钟域。
  • 当使用withClock时,时序元件只跟随显式提供的时钟信号,复位信号仍然使用默认的;同理使用withReset时,时序元件只跟随显式提供的复位信号,时钟信号仍然使用默认的。这里的默认指的不一定是顶层的隐式时钟和复位信号,更准确的说是上一层时钟域的时钟或者复位信号。
  • 我们还可以在时钟域中例化其他模块,如ChildModule,该模块默认使用的是自己顶层的隐式时钟。但它也可以像顶层模块MultiClockTester一样,定义自己的时钟域,具体使用哪个时钟和复位信号就可以由顶层模块显式提供。

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

默认情况下,声明的时序元件都是以时钟的正沿和高有效的复位信号作为敏感变量,但是在多时钟域的语法里,可以改变其行为。

  • 复位信号比较简单,只需要加上取反符号或逻辑非符号。
  • 时钟信号稍微麻烦一些,需要先用asUInt方法把Clock类型转换成UInt类型,再用asBool转换成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.asBool).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)
}

它生成的Verilog主要是:

// NegativeClkRst.v
module NegativeClkRst(
  input  [3:0] io_in,
  input        io_myClk,
  input        io_myRst,
  output [3:0] io_out
);
  wire  _T; // @[negclkrst.scala 14:32]
  wire  _T_2; // @[negclkrst.scala 14:22]
  wire  _T_3; // @[negclkrst.scala 14:47]
  wire  _T_4; // @[negclkrst.scala 14:56]
  reg [3:0] _T_5; // @[negclkrst.scala 15:23]
  assign _T = $unsigned(io_myClk); // @[negclkrst.scala 14:32]
  assign _T_2 = ~ _T; // @[negclkrst.scala 14:22]
  assign _T_3 = _T_2; // @[negclkrst.scala 14:47]
  assign _T_4 = ~ io_myRst; // @[negclkrst.scala 14:56]
  assign io_out = _T_5; // @[negclkrst.scala 17:12]
 
  always @(posedge _T_3) begin
    if (_T_4) begin
      _T_5 <= 4'h0;
    end else begin
      _T_5 <= io_in;
    end
  end
endmodule

四、示例:异步FIFO

在跨时钟域设计中,经常需要使用异步FIFO来同步不同时钟域的数据传输。下面是笔者自己编写的一个异步FIFO例子,数据位宽和深度都是参数化的,读、写地址指针的交互采用格雷码和两级寄存器采样,以便改善亚稳态。通过在Vivado 2018.3里综合后,可以得到以BRAM为存储器的FIFO。

// FIFO.scala
package fifo
 
import chisel3._
import chisel3.util._
import chisel3.experimental._
class FIFO(width: 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设计多时钟域电路,重点是学会apply方法的使用,以及对第二个参数列表的理解。要注意独立时钟域里只有最后的表达式能被作为返回值给变量引用,并被外部访问,其它的定义都是对外不可见的。

你可能感兴趣的:(#,chisel学习笔记,chisel,多时钟域,数字电路)