Chisel入门------Chisel的基本语法4

概述

    本节将具体的介绍集中常见的硬件电路,并探索如何使用Chisel语言进行描述。

10 示例设计

10.1 FIFO Bufer

    通过在写入端和读取端插入缓冲器可以解耦。常见的FIFO是先进先出buffer,其中empty信号和full信号可以用作握手信号。

 一个FIFO具有多个不同的实现方式:例如,片上存储器的读写指针,或者简单使用状态机的寄存器链。对于一些小的FIFO(最多几十个元素),使用寄存器链来实现是更优的选择,其对资源的需求很低。FIFO的源代码可以在schoeberl/chisel-examples: Chisel examples and code snippets (github.com)找到。

    首先,为读写端口定义IO信号,要求数据大小可配:

class WriterIO(size: Int) extends Bundle {
val write = Input(Bool())
val full = Output(Bool())
val din = Input(UInt(size.W))
}
class ReaderIO(size: Int) extends Bundle {
val read = Input(Bool())
val empty = Output(Bool())
val dout = Output(UInt(size.W))
}

下边定义了一个深度为1的buffer:

class FifoRegister(size: Int) extends Module {
val io = IO(new Bundle {
val enq = new WriterIO(size)
val deq = new ReaderIO(size)
})
object State extends ChiselEnum {
val empty , full = Value
}
import State._
val stateReg = RegInit(empty)
val dataReg = RegInit(0.U(size.W))
when(stateReg === empty) {
when(io.enq.write) {
stateReg := full
dataReg := io.enq.din
}
}.elsewhen(stateReg === full) {
when(io.deq.read) {
stateReg := empty
dataReg := 0.U // just to better see empty slots in
the waveform
}
}.otherwise {
// There should not be an otherwise state
}
io.enq.full := (stateReg === full)
io.deq.empty := (stateReg === empty)
io.deq.dout := dataReg
}

下边定义一个位宽和深度可调的FIFO:

class BubbleFifo(size: Int, depth: Int) extends Module {
val io = IO(new Bundle {
val enq = new WriterIO(size)
val deq = new ReaderIO(size)
})
val buffers = Array.fill(depth) { Module(new
FifoRegister(size)) }
for (i <- 0 until depth - 1) {
buffers(i + 1).io.enq.din := buffers(i).io.deq.dout
buffers(i + 1).io.enq.write := ˜buffers(i).io.deq.empty
buffers(i).io.deq.read := ˜buffers(i + 1).io.enq.full
}
io.enq <> buffers(0).io.enq
io.deq <> buffers(depth - 1).io.deq
}

上边提出的连接各个单独buffer来完成的DIDO被称为气泡FIFO,因为数据气泡通过序列。当数据速率比时钟速率慢得多时,这是一个好的解决方案,比如串行端口的解耦缓冲区。但是,当数据速率接近于时钟频率时,气泡FIFO有两个限制:1)因为每个缓冲区的状态必须在空和满之间切换,所以FIFO的最大吞吐量是每两个时钟周期输出一个bit。2)数据需要通过完整的FIFO冒泡,所以输入到输出的延迟至少是缓冲区的数量。后续也将介绍其他的实现方法。

10.2 串行端口

    串行端口(也称UART或RS232)是最简单的通信方式之一,其传输方式如下,一个起始位0,8bit数据(先输出低有效位),最后是一个或两个停止位1。当没有数据传输时,保持为1

 下边以模块化的方式设计UART,每个模块的功能都最小。设计一个发射器TX,一个接收器RX,一个buffer,然后使用这些基本组件。

    首先需要一个端口定义:

class UartIO extends DecoupledIO(UInt(8.W)) {
}

下边是一个串行发射Tx的代码。IO端口是txd,用于串行发送数据,channel则是用于接收需要串行发射的数据:

class Tx(frequency: Int, baudRate: Int) extends Module {
val io = IO(new Bundle {
val txd = Output(UInt(1.W))
val channel = Flipped(new UartIO())
})
val BIT_CNT = ((frequency + baudRate / 2) / baudRate -
1).asUInt
val shiftReg = RegInit(0x7ff.U)
val cntReg = RegInit(0.U(20.W))
val bitsReg = RegInit(0.U(4.W))
io.channel.ready := (cntReg === 0.U) && (bitsReg === 0.U)
io.txd := shiftReg(0)
when(cntReg === 0.U) {
cntReg := BIT_CNT
when(bitsReg =/= 0.U) {
val shift = shiftReg >> 1
shiftReg := 1.U ## shift(9, 0)
bitsReg := bitsReg - 1.U
} .otherwise {
when(io.channel.valid) {
// two stop bits , data , one start bit
shiftReg := 3.U ## io.channel.bits ## 0.U
bitsReg := 11.U
} .otherwise {
shiftReg := 0x7ff.U
}
}
} .otherwise {
cntReg := cntReg - 1.U
}
}

下边是一个比特buffer,和上边介绍的气泡FIFO类似:

class Buffer extends Module {
val io = IO(new Bundle {
val in = Flipped(new UartIO())
val out = new UartIO()
})
object State extends ChiselEnum {
val empty , full = Value
}
import State._
val stateReg = RegInit(empty)
val dataReg = RegInit(0.U(8.W))
io.in.ready := stateReg === empty
io.out.valid := stateReg === full
when(stateReg === empty) {
when(io.in.valid) {
dataReg := io.in.bits
stateReg := full
}
} .otherwise { // full
when(io.out.ready) {
stateReg := empty
}
}
io.out.bits := dataReg
}

将单独的发射器和单个缓冲区结合:

class BufferedTx(frequency: Int, baudRate: Int) extends
Module {
val io = IO(new Bundle {
val txd = Output(UInt(1.W))
val channel = Flipped(new UartIO())
})
val tx = Module(new Tx(frequency , baudRate))
val buf = Module(new Buffer())
buf.io.in <> io.channel
tx.io.channel <> buf.io.out
io.txd <> tx.io.txd
}

接收器RX代码如下:

class Rx(frequency: Int, baudRate: Int) extends Module {
val io = IO(new Bundle {
val rxd = Input(UInt(1.W))
val channel = new UartIO()
})
val BIT_CNT = ((frequency + baudRate / 2) / baudRate - 1).U
val START_CNT = ((3 * frequency / 2 + baudRate / 2) /
baudRate - 1).U
// Sync in the asynchronous RX data , reset to 1 to not
start reading after a reset
val rxReg = RegNext(RegNext(io.rxd, 1.U), 1.U)
val shiftReg = RegInit(0.U(8.W))
val cntReg = RegInit(0.U(20.W))
val bitsReg = RegInit(0.U(4.W))
val validReg = RegInit(false.B)
when(cntReg =/= 0.U) {
cntReg := cntReg - 1.U
} .elsewhen(bitsReg =/= 0.U) {
cntReg := BIT_CNT
shiftReg := rxReg ## (shiftReg >> 1)
bitsReg := bitsReg - 1.U
// the last bit shifted in
when(bitsReg === 1.U) {
validReg := true.B
}
} .elsewhen(rxReg === 0.U) {
// wait 1.5 bits after falling edge of start
cntReg := START_CNT
bitsReg := 8.U
}
when(validReg && io.channel.ready) {
validReg := false.B
}
io.channel.bits := shiftReg
io.channel.valid := validReg
}

串行端口发送“hello world”:

class Sender(frequency: Int, baudRate: Int) extends Module {
val io = IO(new Bundle {
val txd = Output(UInt(1.W))
})
val tx = Module(new BufferedTx(frequency , baudRate))
io.txd := tx.io.txd
val msg = "Hello World!"
val text = VecInit(msg.map(_.U))
val len = msg.length.U
val cntReg = RegInit(0.U(8.W))
tx.io.channel.bits := text(cntReg)
tx.io.channel.valid := cntReg =/= len
when(tx.io.channel.ready && cntReg =/= len) {
cntReg := cntReg + 1.U
}
}

将接收方和发送方结合到一起:

class Echo(frequency: Int, baudRate: Int) extends Module {
val io = IO(new Bundle {
val txd = Output(UInt(1.W))
val rxd = Input(UInt(1.W))
})
val tx = Module(new BufferedTx(frequency , baudRate))
val rx = Module(new Rx(frequency , baudRate))
io.txd := tx.io.txd
rx.io.rxd := io.rxd
tx.io.channel <> rx.io.channel
}

10.3 不同的FIFO设计

10.3.1 参数化FIFO

    将抽象FIFO类定义为以Chisel类型T为参数的泛型类,以便其能够缓存任意Chisel类型,抽象类中还判断了denpth是否是一个有用值。

abstract class Fifo[T <: Data](gen: T, val depth: Int)
extends Module {
val io = IO(new FifoIO(gen))
assert(depth > 0, "Number of buffer elements needs to be larger than 0")
}

将上边提到的空满信号可以看作是ready/valid握手:

class DecoupledIO[T <: Data](gen: T) extends Bundle {
val ready = Input(Bool())
val valid = Output(Bool())
val bits = Output(gen)
}

使用DecoupledIO接口,我们定义了自己的FIFO接口,FifoIO具有写入序列enq和读出序列deq两个端口,包含ready/valid接口。

class FifoIO[T <: Data](private val gen: T) extends Bundle {
val enq = Flipped(new DecoupledIO(gen))
val deq = new DecoupledIO(gen)
}

有了抽象基类和接口,我们就可以专门针对不同的参数(速度,面积功率或简单性)设计不同的FIFO实现。

10.3.2 重新设计FIFO束

    使用标准的ready/valid接口重新定义10.1中的气泡FIFO并使用Chisel数据类型进行参数化:

class BubbleFifo[T <: Data](gen: T, depth: Int) extends
Fifo(gen: T, depth: Int) {
private class Buffer() extends Module {
val io = IO(new FifoIO(gen))
val fullReg = RegInit(false.B)
val dataReg = Reg(gen)
when(fullReg) {
when(io.deq.ready) {
fullReg := false.B
}
}.otherwise {
when(io.enq.valid) {
fullReg := true.B
dataReg := io.enq.bits
}
}
io.enq.ready := !fullReg
io.deq.valid := fullReg
io.deq.bits := dataReg
}
private val buffers = Array.fill(depth) { Module(new
Buffer()) }
for (i <- 0 until depth - 1) {
buffers(i + 1).io.enq <> buffers(i).io.deq
}
io.enq <> buffers(0).io.enq
io.deq <> buffers(depth - 1).io.deq
}

上边将Buffer组件放在BubbleFifo内部并作为私有类。这使得这个组件仅在这个类中需要,以避免污染命名空间。buffer类也进行了优化。只是用了移位fullReg来表示空和满。

    气泡FIFO简单移动,且所需资源较少。然而,每个buffer阶段需要在空和满之间翻转,这也限制了其带宽。

10.3.3 双缓冲FIFO

    一种解决方案是,即使缓冲区寄存器已满,也要保持准备状态。在读取端还没准备好时,为了能够接受来自生产者的数据,我们需要第二个缓冲区,称之为影子寄存器。当buffer满时,新数据将被存储在影子寄存器中,且ready被撤销。当读取端ready就绪时,数据将从数据寄存器到读取端,并从影子寄存器传输到数据寄存器。

class DoubleBufferFifo[T <: Data](gen: T, depth: Int)
extends Fifo(gen: T, depth: Int) {
private class DoubleBuffer[T <: Data](gen: T) extends
Module {
val io = IO(new FifoIO(gen))
object State extends ChiselEnum {
val empty , one, two = Value
}
import State._
val stateReg = RegInit(empty)
val dataReg = Reg(gen)
val shadowReg = Reg(gen)
switch(stateReg) {
is(empty) {
when(io.enq.valid) {
stateReg := one
dataReg := io.enq.bits
}
}
is(one) {
when(io.deq.ready && !io.enq.valid) {
stateReg := empty
}
when(io.deq.ready && io.enq.valid) {
stateReg := one
dataReg := io.enq.bits
}
when(!io.deq.ready && io.enq.valid) {
stateReg := two
shadowReg := io.enq.bits
}
}
is(two) {
when(io.deq.ready) {
dataReg := shadowReg
stateReg := one
}
}
}
io.enq.ready := (stateReg === empty || stateReg === one)
io.deq.valid := (stateReg === one || stateReg === two)
io.deq.bits := dataReg
}
private val buffers = Array.fill((depth + 1) / 2) {
Module(new DoubleBuffer(gen)) }
for (i <- 0 until (depth + 1) / 2 - 1) {
buffers(i + 1).io.enq <> buffers(i).io.deq
}
io.enq <> buffers(0).io.enq
io.deq <> buffers((depth + 1) / 2 - 1).io.deq
}

如上所示的双缓冲器FIFO,因为每个缓冲元素可以存储两个元素,所以我们只需要一般的缓冲元素(depth/2)。具有三种状态:empty,one和two,其表示缓冲区的填充水平。

    当全速运行FIFO时,接收端总是ready,那么状态总是one,只有当接收端ready失效,才会进入two状态。然而,相比于单一的气泡FIFO,相同的缓冲区容量条件下,重启队列只需要一般的时钟周期。

10.3.4 带寄存器内存的FIFO

    可以在硬件上实现基于内存的FIFO,对于比较小的队列,可以使用寄存器文件(Reg(Vec()))。

class RegFifo[T <: Data](gen: T, depth: Int) extends
Fifo(gen: T, depth: Int) {
def counter(depth: Int, incr: Bool): (UInt , UInt) = {
val cntReg = RegInit(0.U(log2Ceil(depth).W))
val nextVal = Mux(cntReg === (depth - 1).U, 0.U, cntReg
+ 1.U)
when(incr) {
cntReg := nextVal
}
(cntReg , nextVal)
}
// the register based memory
val memReg = Reg(Vec(depth , gen))
val incrRead = WireDefault(false.B)
val incrWrite = WireDefault(false.B)
val (readPtr , nextRead) = counter(depth , incrRead)
val (writePtr , nextWrite) = counter(depth , incrWrite)
val emptyReg = RegInit(true.B)
val fullReg = RegInit(false.B)
val op = io.enq.valid ## io.deq.ready
val doWrite = WireDefault(false.B)
switch(op) {
is("b00".U) {}
is("b01".U) { // read
when(!emptyReg) {
fullReg := false.B
emptyReg := nextRead === writePtr
incrRead := true.B
}
}
is("b10".U) { // write
when(!fullReg) {
doWrite := true.B
emptyReg := false.B
fullReg := nextWrite === readPtr
incrWrite := true.B
}
}
is("b11".U) { // write and read
when(!fullReg) {
doWrite := true.B
emptyReg := false.B
when(emptyReg) {
fullReg := false.B
}.otherwise {
fullReg := nextWrite === nextRead
}
incrWrite := true.B
}
when(!emptyReg) {
fullReg := false.B
when(fullReg) {
emptyReg := false.B
}.otherwise {
emptyReg := nextRead === nextWrite
}
incrRead := true.B
}
}
}
when(doWrite) {
memReg(writePtr) := io.enq.bits
}
io.deq.bits := memReg(readPtr)
io.enq.ready := !fullReg
io.deq.valid := !emptyReg
}

当输入端valid有效且FIFO不满时:1)写buffer;2)确保emptyReg无效;3)如果写指针将在下一个时钟周期赶上读指针,则标记缓冲区已满;4)写指针增加。

当接收端ready有效且FIFO不空时:1)确保fullReg无效;2)如果读指针将在下一个时钟周期赶上写指针,则buffer标记为空;3)读指针增加。

11.3.4 带片上存储的FIFO

    对于较大型的FIFO,使用片上存储是更好的选择:

class MemFifo[T <: Data](gen: T, depth: Int) extends
Fifo(gen: T, depth: Int) {
def counter(depth: Int, incr: Bool): (UInt , UInt) = {
val cntReg = RegInit(0.U(log2Ceil(depth).W))
val nextVal = Mux(cntReg === (depth - 1).U, 0.U, cntReg
+ 1.U)
when(incr) {
cntReg := nextVal
}
(cntReg , nextVal)
}
val mem = SyncReadMem(depth , gen, SyncReadMem.WriteFirst)
val incrRead = WireInit(false.B)
val incrWrite = WireInit(false.B)
val (readPtr , nextRead) = counter(depth , incrRead)
val (writePtr , nextWrite) = counter(depth , incrWrite)
val emptyReg = RegInit(true.B)
val fullReg = RegInit(false.B)
val outputReg = Reg(gen)
val outputValidReg = RegInit(false.B)
val read = WireDefault(false.B)
io.deq.valid := outputValidReg
io.enq.ready := !fullReg
val doWrite = WireDefault(false.B)
val data = Wire(gen)
data := mem.read(readPtr)
io.deq.bits := data
when(doWrite) {
mem.write(writePtr , io.enq.bits)
}
val readCond = !outputValidReg && ((readPtr =/= writePtr) || fullReg)
// should add optimization when downstream is ready for pipielining
when(readCond) {
read := true.B
incrRead := true.B
outputReg := data
outputValidReg := true.B
emptyReg := nextRead === writePtr
fullReg := false.B // no concurrent read when full (at
the moment)
}
when(io.deq.fire) {
outputValidReg := false.B
}
io.deq.bits := outputReg
when(io.enq.fire) {
emptyReg := false.B
fullReg := (nextWrite === readPtr) & !read
incrWrite := true.B
doWrite := true.B
}
}

片上存储器在读的时候会在下一个时钟周期吐数出来,但是寄存器文件的话会在当前时钟周期读出,所以还需要增加一个额外的寄存器来处理这个延迟:

class CombFifo[T <: Data](gen: T, depth: Int) extends
Fifo(gen: T, depth: Int) {
val memFifo = Module(new MemFifo(gen, depth))
val bufferFIFO = Module(new DoubleBufferFifo(gen, 2))
io.enq <> memFifo.io.enq
memFifo.io.deq <> bufferFIFO.io.enq
bufferFIFO.io.deq <> io.deq
}

10.4 多时钟存储器

    多时钟存储器也可以用来做一些跨时钟域的处理。Chisel中使用withClock和withClockAndReset结构可以支持多时钟设计。使用withClock(clk)可以定义所有的存储元素都有clk定时。对于多时钟存储器,内存模块应该由外部的withClock块定义,而每个端口应该有自己的withClock块。

class MemoryIO(val n: Int, val w: Int) extends Bundle {
val clk = Input(Bool())
val addr = Input(UInt(log2Up(n).W))
val datai = Input(UInt(w.W))
val datao = Output(UInt(w.W))
val en = Input(Bool())
val we = Input(Bool())
}
class MultiClockMemory(ports: Int, n: Int = 1024, w: Int =
32) extends Module {
val io = IO(new Bundle {
val ps = Vec(ports , new MemoryIO(n, w))
})
val ram = SyncReadMem(n, UInt(w.W))
for (i <- 0 until ports) {
val p = io.ps(i)
withClock(p.clk.asClock) {
val datao = WireDefault(0.U(w.W))
when(p.en) {
datao := ram(p.addr)
when(p.we) {
ram(p.addr) := p.datai
}
}
p.datao := datao
}
}
}

11 互联

11.1 经典的微处理器总线

    下边是一个简单的经典计算机原理图。CPU通过系统总线连接到外部存储器和输入输出设备。这种类型的总线互联在早期的微处理器(如Z80或6502)中很常见。

 11.2 片上总线

     对于芯片来说,使用三态门驱动来实现共享总线是不实际的。此外芯片上的线互联相比于PCB要更加偏移。所以,可以将数据总线分为写数据总线和读数据总线。

 11.2.1 组合逻辑握手

    下边是一确认读请求示意图。处理器驱动地址中线和读信号rd,ack信号等待数据准备好后用于确认请求,这种方式的确定是会传输路径中引入组合逻辑,进而影响最大时钟频率。

 11.2.2 流水线握手

    使用一个简单的流水线握手总线协议,避免单周期组合循环,可以更好地适应现代SoC设计。对于周期时钟,读或写命令由rd或wr断言发出信号。执行命令时,地址和写入数据必须有效。命令只对单周期有效,每个命令都需要一个ack确认。

 11.2.3 IO设备

    下边是一个实现了流水线互联的IO设备,包含四个可加载计数器。

class CounterDevice extends Module {
val io = IO(new Bundle() {
val addr = Input(UInt(2.W))
val wr = Input(Bool())
val rd = Input(Bool())
val wrData = Input(UInt(32.W))
val rdData = Output(UInt(32.W))
val ack = Output(Bool())
})
val ackReg = RegInit(false.B)
val addrReg = RegInit(0.U(2.W))
val cntRegs = RegInit(VecInit(Seq.fill(4)(0.U(32.W))))
ackReg := io.rd || io.wr
when(io.rd) {
addrReg := io.addr
}
io.rdData := cntRegs(addrReg)
for (i <- 0 until 4) {
cntRegs(i) := cntRegs(i) + 1.U
}
when (io.wr) {
cntRegs(io.addr) := io.wrData
}
io.ack := ackReg
}

11.2.4 内存映射设备

    为了选择单个设备,可以使用较高位来进行地址译码,这被称之为内存映射设备。

 对于如上述UART这类的握手设备,可以通过地址映射的方式实现读写:

11.3 总线和接口标准

11.3.1 Wishone

    Wishone是一个点到点的通信定义,而不是传统意义上的总线。其是几个开源IP核所公用的标准。Whishone也是微处理器和背板总线的传统总线。然而,对于一个Soc互联,通常点到点不是一个最好的方法。主机在整个读或写周期内都保持数据和地址有效。这使得只有一个数据有效的主机互联变得复杂。

11.3.2 AXI

    其是AMBA总线的一种,还包括APB、AHB和ASB(在新的设计中不推荐使用)。

11.3.3 Open Core Protocol

    Sonics公司将OCP定义为一个开方,免费的标准。

11.3.4 进一步的总线规格

    Avalon(阿瓦隆)接口规范是由英特尔提供的片上系统互联规范。

    On-Chip Peripheral Bus(OPB)是IBM提供的开放标准。

12 调试,测试和验证

12.1 调试

    可以通过使用调试器或者简单的将关心的值发印出来进行调试,一般通过波形图的方式进行。

12.2 Chisel测试

    ChiselTest基于ScalaTest。Chisel测试中使用peek,poke,expect和step方法来操作DUT的IO端口。可以操作Chisel中的UInt,SInt和Bool类型,然而,当进行查看时,通常更希望为Scala类型,所以可以使用peekInt()返回Scala Int类型或使用peekBoolean()返回Scala中Boolean类型。

可以在test()函数中定义一个测试,该函数将要测试的模块作为参数:

class BcdTableTest extends AnyFlatSpec with
ChiselScalatestTester {
"BCD table" should "output BCD encoded numbers" in {
test(new BcdTable) { dut =>
dut.io.address.poke(0.U)
dut.io.data.expect("h00".U)
dut.io.address.poke(1.U)
dut.io.data.expect("h01".U)
dut.io.address.poke(13.U)
dut.io.data.expect("h13".U)
dut.io.address.poke(99.U)
dut.io.data.expect("h99".U)
}
}
}

或者,可以使用“模块名”的语法来引用模块。当需要对单个模块进行多个测试时非常有用:

class BcdTableTest extends FlatSpec with
ChiselScalatestTester {
behavior of "BCD table"
it should "output BCD encoded numbers" in {
test(new BcdTable) { dut =>
...
}
}
}

    简单的测试可以使用poke将测试向量写入DUT,推进时钟后使用expect来测试输出。为了调试,也可以使用peek来查看端口值并打印出来进行手动检查。下边的测试代码用来测试上一张中介绍的计数器设备:

"CounterDevice" should "work" in {
test(new CounterDevice()) { dut =>
dut.io.ack.expect(false.B)
dut.clock.step()
dut.io.addr.poke(0.U)
dut.io.rd.poke(true.B)
dut.io.ack.expect(false.B)
dut.clock.step()
dut.io.rd.poke(false.B)
dut.io.ack.expect(true.B)
dut.clock.step(100)
dut.io.rd.poke(true.B)
dut.io.addr.poke(1.U)
dut.clock.step()
assert(dut.io.rdData.peekInt() > 100)
dut.io.wr.poke(true.B)
dut.io.wrData.poke(0.U)
dut.clock.step()
dut.io.wr.poke(false.B)
dut.io.rd.poke(true.B)
dut.clock.step()
dut.io.rdData.expect(1.U)
dut.io.addr.poke(0.U)
dut.clock.step()
assert(dut.io.rdData.peekInt() > 100)
}
}

可以看到,虽然这个测试只涵盖了比较少的情况,但是读起来已经很长了。这些poke和expect是比较复杂的。首先,将引入代表读和写请求的函数。这些代码抽象了测试代码中对接口的“敲打”。下边显示了使用这些函数的测试:

"CounterDevice" should "work with functions" in {
test(new CounterDevice()) { dut =>
def step(n: Int = 1) = {
dut.clock.step(n)
}
def read(addr: Int) = {
dut.io.addr.poke(addr.U)
dut.io.rd.poke(true.B)
step()
dut.io.rd.poke(false.B)
while (!dut.io.ack.peekBoolean()) {
step()
}
dut.io.rdData.peekInt()
}
def write(addr: Int, data: Int) = {
dut.io.addr.poke(addr.U)
dut.io.wrData.poke(data.U)
dut.io.wr.poke(true.B)
step()
dut.io.wr.poke(false.B)
while (!dut.io.ack.peekBoolean()) {
step()
}
}
for (i <- 0 until 4) {
assert(read(i) < 10, s"Counter $i should have just started")
}
step(100)
for (i <- 0 until 4) {
assert(read(i) > 100, s"Counter $i should advance")
}
write(2, 0)
write(3, 1000)
assert(read(2) < 5, "Counter should reset")
assert(read(3) > 1000, "Counter should load")
}
}

其中,定义了step()函数来推进时钟,read()函数以地址作为参数并返回读取值。write()函数接收Int类型的地址和数据参数。是有这三个可用的函数,便可以用更少的代码来编写更容易读的测试。

    当你有一个大型的测试,你可能希望只运行测试的一个子集,实现这一点最简单的方式是运行SBT命令的标记测试:

object Unnecessary extends Tag("Unnecessary")
class TagTest extends AnyFlatSpec with Matchers {
"Integers" should "add" taggedAs(Unnecessary) in {
17 + 25 should be (42)
}
}

默认情况下,运行所有测试可以使用sbt test或sbt testOnly *。要删除不必要的测试,可以通过:

$ sbt "testOnly * -- -l Unnecessary"

当运行该命令,中断将会显示这个测试被忽略:

[info] TagTest:
[info] Integers
...
[info] No tests were executed.

12.3 多线程测试

    ChiselTest通过使用fork join调用可以使用多线程测试。fork创建一个新的测试线程,测试代码块作为其参数,而join可以在测试变量上调用,去等待加入主线程。

    运行多个线程确实给peek和poke带来了一些限制,因为没有两个线程可以同时窥探同一个信号。为了报财政操作正确,线程在调用step时同步,下边的代码片段是对FIFO的一个小测试,在一个线程中对其进行入队,在主线程中进行出队:

it should "work with multiple threads" in {
test(new BubbleFifo(8, 4)) { dut =>
val enq = fork {
while (dut.io.enq.full.peekBoolean())
dut.clock.step()
dut.io.enq.din.poke(42.U)
dut.io.enq.write.poke(true.B)
dut.clock.step()
dut.io.enq.write.poke(false.B)
}
while (dut.io.deq.empty.peekBoolean())
dut.clock.step()
dut.io.deq.dout.expect(42.U)
dut.io.deq.read.poke(true.B)
dut.clock.step()
dut.io.deq.empty.expect(true.B)
enq.join()
}
}

12.4 模拟后端

    默认情况下,用ChiselTest编写的测试有Treadle模拟后端运行。Treale的优点在于启动时间段,而且不需要安装任何额外的工具。然而,更大的系统测试可能需要另外一个后端的支持,比如,latches,或者其他可以在仿真速度上受益的工具。为了实现这一点,ChiselTest还支持另外两种后端:Verilator和Synopsys VCS。因为Verilator是开源的,所以,本节主要讨论其使用,另外,VCS也快可以等效替代。

    切换到不同的后端知识像withAnnotations中添加另一个注释,要使用Verilator:

test(new Dut()).withAnnotations(Seq(VerilatorBackendAnnotation))
{ }

额外的灵活性来自于为启动后端的模拟器命令提供自己开关的能力。使用verilatorFlags来增加开关到Verilator的仿真命令行,或者verilatorCFlags来增加开关到GCC。它们应该和后端注释一起出现在注释列表中。可以参考工具的手册来查找命令参数的详细列表。

    注意,ChiselTest0.3.4及后续版本在模拟中直接支持代码覆盖率吧,要支持此功能,请安装Verilator4.028以上版本。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值