寄存器(Registers)

一、简介(Introduction)

在SpinalHDL中创建寄存器与VHDL和Verilog中区别很大。

在SpinalHDL中, 没有process/always块, 寄存器在声明的时候就清晰地定义出来。这种和传统事件驱动HDL的不同带来了巨大的影响:

  • 你可以在同一区域对寄存器和wire赋值, 意味着代码不需要再分成process/always块

  • 这让很多事情变得灵活多变, 参考Functions

时钟和复位是分别处理的, 参考时钟域章节。

二、例化(Instantiation)

有四种方式例化寄存器:

语句 描述
Reg(type: Data) 给定类型的寄存器
RegInit(resetValue: Data) 当复位信号有效, 复位值是resetValue的寄存器
RegNext(nextValue: Data) 每周期采样给定nextValue的寄存器
RegNextWhen(nextValue: Data, cond: Bool) 当条件触发时采样给定nextValue的寄存器

以下是一些寄存器声明的例子:

//4 bits UInt寄存器
val reg1 = Reg(UInt(4 bits))

//每周期采样reg1的寄存器
val reg2 = RegNext(reg1 + 1)

//当复位信号有效初始化为0的4 bits寄存器
val reg3 = RegInit(U"0000")
reg3 := reg2
when(reg2 === 5) {
    reg3 := 0xF
}

//当条件真时采样reg3的寄存器
val reg4 = RegNextWhen(reg3, cond)

以上代码会产生如下电路图:

register

备注:上述的reg3例子展示了如何给RegInit赋值。同样的方法也适用于给其他寄存器类型赋值(Reg, RegNext, RegNextWhen)。正如在组合逻辑赋值中, 最后被赋值的语句有效, 但是如果没有赋值语句, 寄存器会保留原值。

同样的, RegNext是在Reg语句上建立的抽象层次, 以下两种时序代码是完全等价的:

//标准方式
val something = Bool()
val value = Reg(Bool())
value := something

//简短方式
val something = Bool()
val value = RegNext(something)

三、复位值(Reset value)

除了RegInit(value: Data)语句可以直接创建带有复位值的寄存器, 也可以通过在寄存器上调用init(value: Data)函数设置复位值。

//当reset发生被初始化为0的UInt 4bits寄存器
val reg1 = Reg(UInt(4 bits)) init(0)

如果你有一个包含包(Bundle)的寄存器, 你可以对包的每一个元素用init函数。

case class ValidRGB() extends Bundle {
    val valid   = Bool()
    val r, g, b = UInt(8 bits)
}

val reg = Reg(ValidRGB())
reg.valid init(False)       //只有寄存器包有效, 才会有复位值

四、仿真下的例化(Initialization value for simulation)

对于在RTL中不需要复位值但是在仿真时需要初始值的寄存器(避免传递x值), 你可以通过调用randBoot()函数产生随机初始值。

//随机初始化的UInt寄存器
val reg1 = Reg(UInt(4 bits)) randBoot()

五、寄存器向量(Register vectors)

对于wire, 可以用Vec定义寄存器向量。

val vecReg1 = Vec(Reg(UInt(8 bits)), 4)
val vecReg2 = Vec.fill(8)(Reg(Bool()))

像往常一样可以用init方法初始化, 该方法可以和foreach迭代结合作用在寄存器上。

val vecReg1 = Vec(Reg(UInt(8 bits)) init(0), 4)
val vecReg2 = Vec.fill(8)(Reg(Bool()))
vecReg2.foreach(_ init(False))

对于当init值未知时初始化必须被推迟的情况, 可以用以下函数:

case class ShiftRegister[T <: Data](dataType: hardType[T], depth: Int, initFunc: T => Unit) extends Component{
    val io = new Bundle {
        val input   = in(dataType())
        val output  = out(dataType())
    }

    val regs = Vec.fill(depth)(Reg(dataType()))
    regs.foreach(initFunc)

    for(i <- 1 to (depth-1)) {
        regs(i) := regs(i-1)
    }

    regs(0) := io.input
    io.ouutput := regs(depth-1)
}

object SRConsumer {
    def initIdleFlow[T <: Data](flow: Flow[T]): Unit = {
        flow.valid init(False)
    }
}

class SRConsumer() extends Component {
    //...
    val sr = ShiftRegister(Flow(UInt(8 bits)), 4, SRConsumer.initIdleFlow[UInt])
}

六、把线类型转化为寄存器(Transforming a wire into a register)

有时把wire转换为register很有用。例如当你用包(Bundle), 如果你想让产生一些寄存器类型输出, 你可能更倾向于写成io.myBundle.PORT := newValue而不是用val PORT = Reg(...)来声明寄存器, 并把他们的输出与io.myBundle.PORT := PORT端口连接起来。为了实现这个, 你只需要在你想要转换为寄存器的端口使用.setAsReg()

val io = new Bundle {
    val apb = master(Apb3(apb3Config))
}

io.apb.PADDR.setAsReg()
io.apb.PWRITE.setAsReg() init (False)

when(someCondition) {
    io.apb.PWRITE := True
}

在上述代码你需要注意的是, 你也可以给定一个初始值

备注:寄存器的时钟域用的是wire的时钟域, 并不取决于.setAsReg()在哪里调用。 在上述例子中, wire定义在io包中, 和模块有着相同的时钟域。即便io.apb.PADDR.setAsReg()在有着不同时钟域的ClockingArea中书写, 寄存器还是会采用模块的时钟而非ClockingArea的时钟。