上一篇文章中,我们利用了Scala中的IO操作、循环以及序列上的map
函数轻松实现了Chisel逻辑表的生成,效率远高于用Verilog或VHDL和其他脚本语言配合使用。不过Scala作为一种高级编程语言,其面向对象编程和函数式编程特性会为开发带来更多便利。Chisel自然也继承了Scala的语言特性,接下来的两篇文章将分别讲解如何利用Chisel的面向对象编程特性和函数式编程特性,这一篇文章从面向对象特性开始。
Chisel是从Scala拓展得到的,因此也是一个面向对象的语言。作为硬件组件,Chisel的Module
也是Scala的一个类。因此,我们可以用继承的思想来把一个共同的行为归为一个父类,下面我们就试试如何利用继承。
前面的例子中,我们学习了几种不同类型的计数器,它们可以用于低频tick的生成。我们假设想要尝试不同的版本,比如比较它们对资源的需求。我们从一个抽象类开始定义接口:
abstract class Ticker(n: Int) extends Module {
val io = IO(new Bundle{
val tick = Output(Bool())
})
}
这个抽象类就是所有类型的Ticker
的共同特点的提炼,因为他们的io
接口都是一样的。下面我们就可以用这个抽象类来实现一个带一个计数器、向上计数、用于tick生成的Ticker
了:
class UpTicker(n: Int) extends Ticker(n) {
val N = (n-1).U
val cntReg = RegInit(0.U(8.W))
cntReg := cntReg + 1.U
when(cntReg === N) {
cntReg := 0.U
}
io.tick := cntReg === N
}
接下来,对于所有版本的Ticker
逻辑我们都可以用同一个testbench进行测试。我们只需要定义接受Ticker
的子类型的testbench就行了。这个TickerTester
应该有这么几个参数:
[T <: Ticker]
,用于接受一个Ticker
类或从Ticker
继承的类;T
或其子类型;测试器会等待第一个tick的发生(开始的时间可能和其他实现有所不同),然后检查tick是否每n
个时钟周期发生一次。测试器的实现代码如下:
import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec
trait TickerTestFunc {
def testFn[T <: Ticker](dut: T, n: Int) = {
// -1表示还没得到任何tick
var count = -1
for (_ <- 0 to n * 3) {
// 检查输出是否正确
if (count > 0)
dut.io.tick.expect(false.B)
else if (count == 0)
dut.io.tick.expect(true.B)
// 在每个tick上重置计数器
if (dut.io.tick.peek.litToBoolean)
count = n-1
else
count -= 1
dut.clock.step()
}
}
}
这里我们把测试器实现为了一个特质trait
,我们可以在testbench上with
这个特质就能用它了。
在实现测试器的过程中,首先,我们可以测试测试器本身,比如使用一些println
作为调试手段。如果我们对这个Ticker
比较有信心了,并且测试器也没问题,那我们就可以继续实现其他Ticker
了。下面是向下计数的Ticker
的Chisel实现:
class DownTicker(n: Int) extends Ticker(n) {
val N = (n-1).U
val cntReg = RegInit(N)
cntReg := cntReg - 1.U
when(cntReg === 0.U) {
cntReg := N
}
io.tick := cntReg === N
}
然后是向下计数到-1
并通过避免使用比较器因此消耗硬件更少的优化版本:
class NerdTicker(n: Int) extends Ticker(n) {
val N = n
val MAX = (N-2).S(8.W)
val cntReg = RegInit(MAX)
io.tick := false.B
cntReg := cntReg - 1.S
when(cntReg(7)) {
cntReg := MAX
io.tick := true.B
}
}
有了这三种实现,我们就可以用ScalaTest
规范来测试了,分别为不同版本的实现创建一个实例,然后把它们传递给测试函数,测试部分实现如下:
class TickerTest extends AnyFlatSpec with ChiselScalatestTester with TickerTestFunc {
"UpTicker 5" should "pass" in {
test(new UpTicker(5)) { dut => testFn(dut, 5) }
}
"DownTicker 7" should "pass" in {
test(new DownTicker(7)) { dut => testFn(dut, 7) }
}
"NerdTicker 11" should "pass" in {
test(new NerdTicker(11)) { dut => testFn(dut, 11) }
}
}
最后,我们直接运行测试就行了:
sbt "testOnly TickerTest"
测试结果如下:
全部通过测试。
这一篇文章我们提取了之前实现的几种tick生成模块的共同特征,实现了一个抽象类Ticker
。对于这个Ticker
,我们通过继承的方法实现了几种不同的版本,还可以用统一的测试接口对这些版本进行测试,整个代码清晰了很多,对于实现和测试成本也小了很多。此外,我们针对不同版本的实现也构建了统一的测试器函数,以trait的形式with
到testbench中就可以调用,非常方便。Scala作为支持函数式编程的语言,Chisel自然也可以利用函数式编程的特性,下一篇文章我们就共同学习利用函数式编程的硬件生成。