上一篇见此:
chisel快速入门(一)_沧海一升的博客-CSDN博客简单介绍了chisel,使硬件开发者能快速上手chisel。https://blog.csdn.net/qq_21842097/article/details/121415341
十、运行和测试
现在我们已经定义了模块,我们将讨论如何实际运行并测试电路。Chisel代码可以转换为C++或Verilog。 为了编译电路,我们需要调用chiselMain:
object tutorial {def main(args: Array[String]) = {chiselMain(args, () => Module(new Mux2())) }
}
测试是电路设计的关键部分,因此在 Chisel 中,我们通过使用 Tester 类的子类在 Scala 中提供测试向量来提供一种测试电路的机制:
class Tester[T <: Module] (val c: T, val isTrace: Boolean = true) {var t: Intval rnd: Randomdef int(x: Boolean): BigInt def int(x: Int): BigIntdef int(x: Bits): BigInt def reset(n: Int = 1)def step(n: Int): Intdef pokeAt(data: Mem[T], index: Int, x: BigInt) def poke(data: Bits, x: BigInt)def poke(data: Aggregate, x: Array[BigInt])def peekAt(data: Mem[T], index: Int)def peek(data: Bits): BigIntdef peek(data: Aggregate): Array[BigInt]def expect (good: Boolean, msg: String): Boolean def expect (data: Bits, target: BigInt): Boolean
}
它将tester绑定到模块,并允许用户使用给定的调试协议编写测试。用户会用到一下这些:
- poke: 设置输入端口以及状态值
- step: 以一个时间单元执行电路
- peek: 读取端口和状态值
- expect: 比较peek获得的值和期望的值
用户使用如下的方式连接tester和模块:
object chiselMainTest { def apply[T <: Module](args: Array[String], comp: () => T)( tester: T => Tester[T]): T
}
当- -test作为参数传递给chiselMainTest时,tester实例在独立的进程中运行被测器件(DUT),并连接stdin和stdout,这样调试命令可以发送到DUT,响应也可以从DUT接收,如图所示。
举例说明:
class Mux2Tests(c: Mux2) extends Tester(c) { val n = pow(2, 3).toIntfor (s <- 0 until 2) {for (i0 <- 0 until 2) { for (i1 <- 0 until 2) {poke(c.io.sel, s)poke(c.io.in1, i1)poke(c.io.in0, i0)step(1)expect(c.io.out, (if (s == 1) i1 else i0))}}}
}
使用poke将Mux2的每个输入的分别设置为合适的值。对于这个例子,我们通过硬编码输入到一些已知的值并检查输出是否对应于已知的值来测试Mux2。为此,在每次迭代中,我们生成模块输入,让模拟将这些值分配给我们正在测试的器件c的输入,单步运行电路并对比期望值。最后,简单说明一下如何调用测试器:
chiselMainTest(args + "--test", () => Module(new Mux2())){ c => new Mux2Tests(c)
}
还有其他的一些命令参数:
- –targetDir 目标路径名前缀
- –genHarness 生成C++文件
- –backend v 生成verilog
- –backend c 生成C++(默认)
- –vcd 开启vcd打印
- –debug 把所有的wire放入class文件
十一、状态元素
Chisel支持的状态元素的最简单形式是上升沿触发寄存器,可以实例化为:
val reg = Reg(next = in)
该电路具有输出,该输出是前一个时钟周期的输入信号产生的值。注意,我们不必指定Reg的类型,因为它会在实例化时从输入开始自动推断。在当前版本的Chisel中,时钟和复位是全局信号,在需要时可以隐式包含。
使用寄存器,我们可以快速定义一些有用的电路结构。 例如,当当前值为true且之前的值为false时,上升沿检测器能够获取到布尔信号并输出true,如下所示:
def risingedge(x: Bool) = x && !Reg(next = x)
计数器是一个重要的时序电路。 如果想构建一个向上计数器,计数到最大值max后回到零:
def counter(max: UInt) = {val x = Reg(init = UInt(0, max.getWidth))x := Mux(x === max, UInt(0), x + UInt(1))x
}
计数器复位值为0(宽度大到足以容纳max),当电路的全局复位置位时,寄存器将初始化为该值。
计数器可用于构建很多有用的时序电路。例如,我们可以通过在计数器达到零时输出true来构建脉冲发生器:
def pulse(n: UInt) = counter(n - UInt(1)) === UInt(0)
然后可以通过切换方波发生器脉冲序列,在每个脉冲上的true和false之间切换:
// Flip internal state when input true.
def toggle(p: Bool) = {val x = Reg(init = Bool(false)) x := Mux(p, !x, x)x
}
// Square wave of a given period.
def squareWave(period: UInt) = toggle(pulse(period/2))
1、转发声明
纯组合电路在节点之间不存在周期,如果检测到这样的周期,则Chisel将报告错误。因为它们不具有周期,所以可以总是以前馈方式构建组合电路,通过添加一些输入从已经定义的节点导出的新节点。
时序电路在节点之间具有反馈,因此有时需要在生成节点被定义之前输出。因为Scala顺序执行程序语句,所以我们允许数据节点作为wire来提供节点声明,这样可以立即被使用,但其输入将稍后设置。
如下例所示,在简单的CPU中,我们需要定义pcPlus4和brTarget的线,以便在定义之前引用它们:
val pcPlus4 = UInt()
val brTarget = UInt()
val pcNext = Mux(io.ctrl.pcSel, brTarget, pcPlus4)
val pcReg = Reg(next = pcNext, init = UInt(0, 32))
pcPlus4 := pcReg + UInt(4)
...
brTarget := addOut
接线操作符:=用于在pcReg和addOut定义后连接。
2、条件更新
在前面使用到寄存器的示例中,我们简单地将组合逻辑块连接到寄存器的输入。当描述状态元素的操作时,指定何时将发生寄存器更新并且用几个单独的语句指明这些更新。
Chisel以when的形式提供条件更新规则,以支持这种顺序逻辑描述的风格。例如,
val r = Reg(init = UInt(0, 16))
when (cond) {r := r + UInt(1)
}
其中只有在cond为真时,才在当前时钟周期的结尾更新寄存器r。when的参数是返回Bool值。后面的更新块只能包含使用赋值运算符:=,简单表达式和用val定义的命名引线的更新语句。
在条件更新序列中,条件为真的最近条件更新优先。 例如:
when (c1) { r := UInt(1) }
when (c2) { r := UInt(2) }
上述表达式会根据以下真值表更新r:
条件更新结构可以嵌套,任何给定块在所有外嵌套条件的联合下才能执行。
条件可以使用when,.elsewhen,.otherwise来链式表达,对应于Scala中的if, else if, else。例如:
when (c1) { u1 }
.elsewhen (c2) { u2 }
.otherwise { ud }
// the same as
when (c1) { u1 }
when (!c1 && c2) { u2 }
when (!(c1 || c2)) { ud }
Chisel还允许Wire,即一些组合逻辑的输出,成为条件性更新语句的目标,以允许逐步构建复杂的组合逻辑表达式。Chisel不允许不指定组合输出,并且如果组合输出未遇到无条件更新,则报告错误。
3、有限状态机
在数字设计中有限状态机(FSM)是时序电路常用的类型。简单FSM的例子就是奇偶校验生成器:
class Parity extends Module { val io = new Bundle {val in = Bool(dir = INPUT)val out = Bool(dir = OUTPUT) }val s_even :: s_odd :: Nil = Enum(UInt(), 2) val state = Reg(init = s_even)when (io.in) {when (state === s_even) { state := s_odd }when (state === s_odd) { state := s_even } }io.out := (state === s_odd)
}
其中Enum(Uint(), 2)生成两个UInt数。当io.in为true时更新状态。需要注意的是,FSM的所有机制都建立在寄存器,线和条件更新的基础上。
下面是一个复杂的FSM例子,这是一个自动售货机接收货币的电路:
class VendingMachine extends Module {val io = new Bundle {val nickel = Bool(dir = INPUT)val dime = Bool(dir = INPUT)val valid = Bool(dir = OUTPUT)}val s_idle :: s_5 :: s_10 :: s_15 :: s_ok :: Nil = Enum(UInt(), 5)val state = Reg(init = s_idle) when (state === s_idle) {when (io.nickel) { state := s_5 }when (io.dime) { state := s_10 } }when (state === s_5) {when (io.nickel) { state := s_10 } when (io.dime) { state := s_15 }}when (state === s_10) {when (io.nickel) { state := s_15 }when (io.dime) { state := s_ok } }when (state === s_15) {when (io.nickel) { state := s_ok } when (io.dime) { state := s_ok }}when (state === s_ok) {state := s_idle}io.valid := (state === s_ok)
}
采用switch风格代码如下:
class VendingMachine extends Module {val io = new Bundle {val nickle = Bool(dir = INPUT)val dime = Bool(dir = INPUT)val valid = Bool(dir = OUTPUT)}val s_idle :: s_5 :: s_10 :: s_15 :: s_ok :: Nil = Enum(UInt(), 5)val state = Reg(init = s_idle) switch (state) { is (s_idle) {when (io.nickel) { state := s_5 }when (io.dime) { state := s_10 } } is (s_5) {when (io.nickel) { state := s_10 } when (io.dime) { state := s_15 }}is (s_10) {when (io.nickel) { state := s_15 }when (io.dime) { state := s_ok } }is (s_ok) {state := s_idle}}io.valid := (state === s_ok)
}
十二、内存
Chisel提供了创建只读和读/写存储器的功能。
1、ROM
用户可以使用Vec定义ROM:
Vec(inits: Seq[T])
Vec(elt0: T, elts: T*)
其中inits是初始化ROM的初始Data序列。例如,用户可以创建一个初始化为1,2,4,8的小型ROM,并使用计数器作为地址生成器循环访问所有值,如下所示:
val m = Vec(Array(UInt(1), UInt(2), UInt(4), UInt(8)))
val r = m(counter(UInt(m.length)))
我们可以使用如下初始化的ROM创建n值正弦查找表:
def sinTable (amp: Double, n: Int) = { val times = Range(0, n, 1).map(i => (i*2*Pi)/(n.toDouble-1) - Pi) val inits = times.map(t => SInt(round(amp * sin(t)), width = 32)) Vec(inits)
}
def sinWave (amp: Double, n: Int) =sinTable(amp, n)(counter(UInt(n))
其中amp用于缩放存储在ROM中的固定点值。
2、Mem
存储器在Chisel中被给予特殊处理,因为存储器的硬件实现具有许多变化,例如,FPGA存储器与ASIC存储实例化的结果完全不同。Chisel定义了一个内存抽象,可以映射到简单的Verilog行为描述,也可以映射到从代工厂或IP厂商提供的外部内存生成器获得的内存模块实例。
Chisel通过Mem结构可以支持随机存取存储器。写入Mems是正边沿触发,读取是组合或正边沿触发。
object Mem {def apply[T <: Data](type: T, depth: Int,seqRead: Boolean = false): Mem
}
class Mem[T <: Data](type: T, depth: Int, seqRead: Boolean = false)extends Updateable { def apply(idx: UInt): T}
通过使用UInt索引创建到Mems的端口。具有一个写入端口和两个组合读取端口的32-entry的寄存器堆可以如下表示:
val rf = Mem(UInt(width = 64), 32)
when (wen) { rf(waddr) := wdata }
val dout1 = rf(waddr1)
val dout2 = rf(waddr2)
如果设置了可选参数seqRead,当读地址为Reg时,Chisel将尝试推断顺序读端口。
单读端口,单写端口SRAM可以描述如下:
val ram1r1w = Mem(UInt(width = 32), 1024, seqRead = true)
val reg_raddr = Reg(UInt())
when (wen) { ram1r1w(waddr) := wdata }
when (ren) { reg_raddr := raddr }
val rdata = ram1r1w(reg_raddr)
单端口SRAM可以在读和写条件在链中相同时相互排斥时推断:
val ram1p = Mem(UInt(width = 32), 1024, seqRead = true)
val reg_raddr = Reg(UInt())
when (wen) { ram1p(waddr) := wdata }
.elsewhen (ren) { reg_raddr := raddr }
val rdata = ram1p(reg_raddr)
如果相同的Mem地址在相同的时钟沿上被写入和顺序读取,或者如果顺序读取使能被清除,则读取数据为未定义。
十三、接口和批量连接
对于更复杂的模块,在定义模块的 IO 时定义和实例化接口类通常很有用。
首先,接口类促进重用,允许用户以有用的形式一次性捕获所有通用接口。 其次,接口允许用户通过模块之间的批量连接来显着减少布线。 最后,用户可以在一个地方对大型接口进行更改,从而减少添加或删除接口部分时所需的更新次数。
1、端口类、子类和嵌套
正如我们之前看到的,用户可以通过定义一个继承 Bundle 的类来定义他们自己的接口。
例如,可以为握手数据定义一个简单的链接,如下所示:
class SimpleLink extends Bundle {
val data = UInt(16, OUTPUT)
val valid = Bool(OUTPUT)
}
然后我们可以通过使用包继承添加奇偶校验位来扩展 SimpleLink:
class PLink extends SimpleLink {
val parity = UInt(5, OUTPUT)
}
通常,用户可以使用继承将他们的接口组织成层次结构。 从那里我们可以通过将两个 PLink 嵌套到一个新的 FilterIO 包中来定义过滤器接口:
class FilterIO extends Bundle {
val x = new PLink().flip
val y = new PLink()
}
其中flip递归地改变Bundle的“相性”,将输入改变为输出和将输出改变为输入。
我们现在可以通过定义一个过滤器类继承模块来定义一个过滤器:
class Filter extends Module {
val io = new FilterIO()
...
}
其中io包含了FilterIO。
2、Bundle Vector
除了单个元素之外,元素Vector可以组成更丰富的分层接口。 例如,为了创建一个带有输入向量的交叉开关,产生一个输出向量,并由 UInt 输入选择,我们使用 Vec 构造函数。
class CrossbarIo(n: Int) extends Bundle {val in = Vec.fill(n){ new PLink().flip() } val sel = UInt(INPUT, sizeof(n))val out = Vec.fill(n){ new PLink() }
}
其中Vec用第一个参获取大小,区块返回一个端口作为第二个参数。
3、批量连接
我们现在可以将两个过滤器组成一个过滤器块,如下所示:
class Block extends Module { val io = new FilterIO()val f1 = Module(new Filter()) val f2 = Module(new Filter())f1.io.x <> io.x f1.io.y <> f2.io.x f2.io.y <> io.y
}
其中<>批量连接同级模块之间的相反接口或父/子模块之间的相同接口。批量连接将相同名称的端口彼此连接。在所有连接完成后,Chisel警告用户端口是否只有一个到它们的连接。
4、接口视图
考虑一个由控制路径和数据路径子模块以及主机和内存接口组成的简单 CPU,如图。
在这个 CPU 中,我们可以看到控制路径和数据路径各自只连接到一部分指令和数据内存接口。Chisel 允许用户通过部分实现接口来做到这一点。 用户首先定义完整的 ROM 和 Mem 接口如下:
class RomIo extends Bundle { val isVal = Bool(INPUT)val raddr = UInt(INPUT, 32) val rdata = UInt(OUTPUT, 32)
}
class RamIo extends RomIo { val isWr = Bool(INPUT)val wdata = UInt(INPUT, 32)
}
现在控制逻辑可以根据这些接口构建接口:
class CpathIo extends Bundle { val imem = RomIo().flip() val dmem = RamIo().flip()
}
而且控制和数据通路模块可以通过部分地分配来给这个接口来构建,如下所示:
class Cpath extends Module { val io = new CpathIo();...io.imem.isVal := ...;io.dmem.isVal := ...; io.dmem.isWr := ...; ...
}
class Dpath extends Module { val io = new DpathIo(); ...io.imem.raddr := ...; io.dmem.raddr := ...; io.dmem.wdata := ...;...
}
我们现在可以使用批量连接来连接CPU,就像使用其他bundle一样:
class Cpu extends Module {val io = new CpuIo()val c = Module(new CtlPath()) val d = Module(new DatPath()) c.io.ctl <> d.io.ctlc.io.dat <> d.io.dat c.io.imem <> io.imemd.io.imem <> io.imemc.io.dmem <> io.dmemd.io.dmem <> io.dmemd.io.host <> io.host
}