时序电路是输出取决于输入和先前值的电路。由于我们对同步设计(时钟设计)感兴趣,因此当我们谈论时序电路时,我们指的是同步时序电路。为了构建时序电路,我们需要可以存储状态的元件:所谓的寄存器。
寄存器
构建时序电路的基本元件是寄存器。寄存器是D触发器的集合。D触发器在时钟的上升沿捕获其输入的值,并将其存储在其输出。或者,换句话说:寄存器在时钟的上升沿用输入的值更新其输出。
图6.1显示了寄存器的示意符号。时钟信号和重置信号一般在Chisel不声明。
在Chisel中,具有输入d和输出q的寄存器定义为:
val q = RegNext(d)
寄存器的输入和输出可以是由向量和束的组合构成的任意复杂类型。
也可以通过两个步骤定义和使用寄存器:
val delayReg = Reg(UInt(4.W))
delayReg := delayIn
首先,我们定义寄存器并给予它一个名字。其次,我们将信号delayIn连接到寄存器的输入端。还要注意,寄存器的名称包含字符串Reg。为了容易区分组合电路和时序电路,通常的做法是将标记Reg作为名称的一部分。另外,请注意,Scala中的名称(因此也包括Chisel中的名称)通常使用CamelCase。变量名以小写字母开头,类以大写字母开头。
寄存器可以在复位时初始化。复位信号作为时钟信号隐含在Chisel中。我们将重置值(例如零)作为参数提供给寄存器构造函数RegInit。寄存器的输入与Chisel赋值语句连接。
val valReg = RegInit(0.U(4.W))
valReg := inVal
Chisel中复位的默认实现是同步复位。对于同步复位,D触发器不需要任何更改,只需在输入端添加一个多路复用器,在复位时的初始值和数据值之间进行选择。图6.2所示为具有同步复位的寄存器原理图,其中复位驱动多路复用器。然而,由于经常使用同步复位,所以现代FPGA触发器包含到触发器的同步复位(和设置)输入,以不浪费用于多路复用器的LUT资源。
计数器
最基本的时序电路之一是计数器。在其最简单的形式中,计数器是寄存器,其中输出连接到加法器,并且加法器的输出连接到寄存器的输入。图6.6显示了这样一个自由运行的计数器。
具有4位寄存器的自由运行计数器从0计数到15,然后再次返回到0。计数器也应重置为已知值。
val cntReg = RegInit(0.U(4.W))
cntReg := cntReg + 1.U
当我们想对事件计数时,我们使用一个条件来递增计数器,如图6.7和下面的代码所示。
val cntEventsReg = RegInit(0.U(4.W))
when(event){
cntEventsReg := cntEventsReg +1.U
}
Counting Up and Down(上下倒数)
为了计数到一个值,然后从0重新开始,我们需要将计数器值与最大常量进行比较,例如,使用when条件语句。
//- start when_counter
val cntReg = RegInit(0.U(8.W))
cntReg := cntReg + 1.U
when(cntReg === N) {
cntReg := 0.U
}
我们也可以使用多路复用器作为计数器:
//- start mux_counter
val cntReg = RegInit(0.U(8.W))
cntReg := Mux(cntReg === N, 0.U, cntReg + 1.U)
//- end
如果我们处于倒计时的状态,我们首先用最大值重置计数器寄存器,并在达到0时将计数器重置为该值。
//- start down_counter
val cntReg = RegInit(N)
cntReg := cntReg - 1.U
when(cntReg === 0.U) {
cntReg := N
}
//- end
当我们编码和使用更多的计数器时,我们可以定义一个带有参数的函数来为我们生成一个计数器。
def genCounter(n: Int) = {
val cntReg = RegInit(0.U(8.W))
cntReg := Mux(cntReg === n.U, 0.U, cntReg + 1.U)
cntReg
}
函数genCounter的最后一个语句是函数的返回值,在本例中是计数寄存器cntReg。
Generating Timing with Counters
除了计数事件,计数器通常用于产生时间的概念,即分频。
移位寄存器
移位寄存器是按顺序连接的触发器的集合。寄存器(触发器)的每个输出端连接到下一个寄存器的输入端。图6.12显示了一个4级移位寄存器。该电路在每个时钟节拍上将数据从左向右移位。在这种简单的形式中,电路实现了从din到dout的4抽头延迟。
这个简单移位寄存器的Chisel代码:(1)创建4位寄存器shiftReg,(2)将移位寄存器的低3位与输入din连接起来,用于寄存器的下一个输入,以及(3)使用寄存器的最高有效位(MSB)作为输出dout。
移位寄存器通常用于将串行数据转换为并行数据或将并行数据转换为串行数据。第11.2节显示了使用移位寄存器实现接收和发送功能的串行端口。
val shiftReg = Reg(UInt(4.W))
shiftReg := shiftReg(2, 0) ## din //##按位相连
val dout = shiftReg(3)
具有并行输出的移位寄存器
移位寄存器的串行输入并行输出配置将串行输入流转换为并行字。这可以在串行端口(UART)中用于接收功能。图6.13显示了一个4位移位寄存器,其中每个触发器输出连接到一个输出位。在4个时钟周期后,该电路将4位串行数据字转换为q中可用的4位并行数据字。在本例中,我们假设第0位(最低有效位)首先发送,因此当我们想要读取整个字时,它将到达最后一个阶段。
图6.13显示了一个具有并行输出功能的4位移位寄存器。
并行加载移位寄存器
图6.14显示了一个具有并行加载功能的4位移位寄存器。
内存
存储器可以由寄存器的集合构建,在Chisel中为Vec的Reg。然而,这在硬件上是昂贵的,并且更大的存储器结构被构建为SRAM。对于ASIC,存储器编译器构造存储器。FPGA包含片上存储器块,也称为块RAM。这些片上存储器块可以组合成更大的存储器。FPGA中的存储器通常具有一个读取端口和一个写入端口,或者可以在运行时在读取和写入之间切换的两个端口。
FPGA(以及ASIC)通常支持同步存储器。同步存储器在其输入上具有寄存器(读和写地址、写数据和写使能)。这意味着在设置地址后的一个时钟周期内读取数据可用。
图6.15显示了这种同步存储器的原理图。存储器是双端口的,具有一个读端口和一个写端口。读端口有一个输入,即读地址(rdAddr),和一个输出,即读数据(rdData)。写端口有三个输入:地址(wrAddr)、要写入的数据(wrData)和写入使能(wrEna)。注意,对于所有输入,存储器中有一个寄存器显示同步行为。
为了支持片上存储器,Chisel提供了存储器构造函数SyncReadMem。清单6.2显示了一个Memory组件,它实现了1KiB的内存,具有字节范围的输入和输出数据以及写使能。
class Memory() extends Module {
val io = IO(new Bundle {
val rdAddr = Input(UInt(10.W))
val rdData = Output(UInt(8.W))
val wrAddr = Input(UInt(10.W))
val wrData = Input(UInt(8.W))
val wrEna = Input(Bool())
})
val mem = SyncReadMem(1024, UInt(8.W))
io.rdData := mem.read(io.rdAddr)
when(io.wrEna) {
mem.write(io.wrAddr, io.wrData)
}
}
一个有趣的问题是,当在同一个时钟周期中,当同时读写一个地址的数据时,读出的数据会发生什么。有三种可能性:新写入的值、旧值或undefined(可能是旧值中的一些位和一些新写入的数据的混合)。FPGA中的哪种可能性取决于FPGA类型,有时可以指定。Chisel文档读取的数据是未定义的。
如果我们想读出新写入的值,我们可以构建一个转发电路,它检测到地址相等并转发写入数据。图6.16显示了带有转发电路的存储器。读取和写入地址与写入使能进行比较和门控,以在写入数据或存储器读取数据的转发路径之间进行选择。写入数据通过寄存器延迟一个时钟周期。