吃透Chisel语言.32.Chisel进阶之硬件生成器(一)——Chisel中的参数化

Chisel进阶之硬件生成器(一)——Chisel中的参数化

Chisel区别于其他硬件描述语言的最强大的地方在于,我们可以用Chisel写硬件生成器。对于老一点的硬件描述语言,比如VHDL和Verilog,我们通常使用其他的编程语言(比如Java或Python)来生成硬件。但是在Chisel中,构造硬件时可以利用Scala和Java库的强大能力。因此,我们既可以在Chisel中直接描述硬件,也可以用Chisel写硬件生成器,这一部分我们就仔细学习一下Chisel中如何写硬件生成器,第一篇文章从Chisel中的参数化开始。

一点Scala预备知识

这一小节简单介绍一下Scala,写Chisel硬件生成器的时候用这些知识已经足够了。

Scala中有两种变量类型:valvarval会给定一个表达式的命名,且不能被重新赋值。下面的代码片段定义了一个叫zero的变量,如果我们尝试对zero重新赋值的话,编译器就会报错:

val zero = 0
zero = 1

console中执行结果如下:

在这里插入图片描述

在Chisel中,我们只将val用于命名硬件组件,而我们用:=操作符是个Chisel操作符,并非Scala的赋值操作符=

而Scala中的var是更经典的变量类型,定义为var的变量可以重新赋值:

var x = 2
x = 3

此时编译器不会报错。

写硬件生成器的时候,我们不仅需要val,更需要var,前者用于命名硬件组件,后者用于命名可配置的参数。

我们可能想知道这些变量都是什么类型。由于上面的例子中,我们赋了一个整数常量给变量,所以变量类型是推导出来的,是Scala的Int类型。大多数情况下,Scala编译器都能够推导变量类型。如果我们想要显式指定,那也是可以的:

val number: Int = 42

Scala中的简单循环我们前面已经用过了,是这么写的:

for (i <- 0 until 10) {
    println(i)
}

循环变量i是不需要声明的。

我们可以在硬件生成器中使用循环,下面的循环就连接了移位寄存器的每一位:

val shiftReg = RegInit(0.U(8.W))

shiftReg(0) := inVal

for (i <- 1 until 8) {
    shiftReg(i) := shiftReg(i-1)
}

很明显,until的左边的下界是包含在内的,右边的上界是不包含在内的。

Scala中的条件语句用的是ifelse,需要注意的是,硬件生成中,Scala条件的值是在Scala运行时计算的。所以Scala条件语句不会生成Mux,而是允许我们写可配置的硬件生成器。Scala中条件语句的语法如下:

for (i <- 0 until 10) {
    if (i%2 == 0) {
        println(i + " is even")
    } else {
        println(i + " is odd")
    }
}

Chisel组件和函数都可以用参数进行配置,参数可以是一个简单的整数常量,也可以是一个Chisel硬件类型,下面我们就介绍Chisel中四种参数化的方法。

简单的参数化

电路最基本的参数化方法就是把位宽定义为参数。参数可以传递给Chisel模块的构造器,下面的例子就实现了一个位宽可配置的加法器,其中位宽n是参数,它的类型为Scala的Int,它会被在被实例化的时候传递给构造器,然后用于IO Bundle:

class ParamAdder(n: Int) extends Module {
	val io = IO(new Bundle {
        val a = Input(UInt(n.W))
        val b = Input(UInt(n.W))
        val c = Output(UInt(n.W))
    })
    
    io.c := io.a + io.b
}

这个参数化版本的加法器可以这么用:

val add8 = Module(new ParamAdder(8))
val add16 = Module(new ParamAdder(16))

带类型参数的函数

位宽作为可配置参数只是硬件生成器的起点,更高级的配置是类型作为参数。这个特性允许在Chisel中创建可以接受任意数据类型的Mux。为了展示类型怎么作为参数,我们给出接受任意类型的Mux的函数的例子:

def myMux[T <: Data](sel: Bool, tPath: T, fPath: T): T = {
    val ret = WireDefault(fPath)
    when (sel) {
        ret := tPath
    }
    ret
}

Chisel允许用类型参数化函数,上面的例子中参数就是Chisel类型。中括号中的表达式[T <: Data]定义了类型参数T的集合是DataData的子集。Data是Chisel类型系统的根类型。

myMux函数有三个参数,一个布尔值的条件,一个用于true路径的值,一个用于false路径的值。两个路径的值的类型都是TT会在调用函数的时候给定。函数内容本身是很直接的,定义一个线网默认值为fPath,如果条件为真的话就把值改为tPath。这种情况是经典的Mux函数,函数的结尾我们返回了Mux的硬件。

下面的代码就构造了一个UInt类型的Mux:

val resA = myMux(selA, 5.U, 10.U)

Mux的两个路径的值应该是同一种类型,下面的用法就会导致运行时错误:

val resErr = myMux(selA, 5.U, 10.S)

我们可以定义一个有两个字段的Bundle类型:

class ComplexIO extends Bundle {
    val d = UInt(10.W)
    val b = Bool()
}

要想构造上面这个Bundle的常量,我们首先需要创建一个Wire,然后分别给它的子字段赋值,然后就可以在上面的Mux中使用了:

val tVal = Wire(new ComplexIO)
tVal.b := true.B
tVal.d := 42.U
val fVal = Wire(new ComplexIO)
fVal.b := false.B
fVal.d := 13.U

// 在Mux中使用Bundle类型
val resB = myMux(selB, tVal, fVal)

在我们函数的初始设计中,我们还可以使用WireDefault来创建一个类型为T的线网作为默认值。如果我们需要创建一个没有默认值的Chisel类型的线网,我们可以使用fPath.cloneType来获取相应的Chisel类型。下面的代码就展示了另一种实现上面的Mux的方法:

def myMuxAlt[T <: Data](sel: Bool, tPath: T, fPath: T): T = {
    val ret = Wire(fPath.cloneType)
    ret := fPath
    when (sel) {
        ret := tPath
    }
    ret
}

带类型参数的Chisel模块

我们也可以用Chisel类型参数化Chisel模块。假设我们想要设计一个片上网络来在不同的处理器核之间移动数据,然而我们不想在路由接口硬编码数据格式,而是希望可以参数化数据格式。和带类型参数的函数类似,我们可以给Module构造器一个类型参数T。此外,我们需要用一个构造器参数来给定类型。这个例子里面,我们路由端口的数量也是可配置的:

class NocRouter[T <: Data](dt: T, n: Int) extends Module {
    val io = IO(new Bundle {
        val inPort = Input(Vec(n, dt))
        val address = Input(Vec(n, UInt(8.W)))
        val outPort = Output(Vec(n, dt))
    })
    
    // 根据地址路由负载
    // ...
}

要使用这个路由的时候,我们首先需要定义想要路由的数据类型,比如一个Chisel的Bundle

class Payload extends Bundle {
    val data = UInt(16.W)
    val flag = Bool()
}

现在我们就可以通过传递一个自定义的Bundle的实例和端口的数量给构造器就可以创建一个路由了:

val router = Module(new NocRouter(new Payload, 2))

参数化的Bundle

在路由的例子里面,我们用了两个不同的向量字段来表示路由的输入,一个用于输入地址,一个用于输入数据,都是参数化的。更优雅的解决方案是创建一个本身就是参数化的Bundle,比如:

class Port[T <: Data](dt: T) extends Bundle {
    val address = UInt(8.W)
    val data = dt.cloneType
}

这个Bundle有一个类型参数T,是Chisel的Data类型的子类型。在Bundle内,我们通过在参数上调用cloneType定义了一个字段data。然而,我们需要使用构造器参数的时候,它的参数就变成了该类的公共字段。Chisel需要复制Bundle的类型的时候,比如用于一个Vec,这个公共字段就不好使了。解决办法就是把这个参数字段变成私有的:

class Port[T <: Data](private val dt: T) extends Bundle {
    val address = UInt(8.W)
    val data = dt.cloneType
}

有了这个新的Bundle,我们就可以定义新的路由了:

class NocRouter2[T <: Data](dt: T, n: Int) extends Module {
    val io = IO(new Bundle {
        val inPort = Input(Vec(n, dt))
        val outPort = Output(Vec(n, dt))
    })
    
    // 根据地址路由负载
    // ...
}

现在就可以用以Payload为参数的Port来实例化一个路由了:

val router = Module(NocRouter2(new Port(new Payload), 2))

结语

这一篇文章从Scala的valvar开始,学习了两种类型的变量在Chisel之中的使用,然后分别介绍了Chisel中四种参数化的方法,对于我们构建可复用的模块有重大意义。这一部分的关键是将Chisel作为写硬件生成器的语言,而不仅仅是作为描述硬件的语言,这个参数化就是作为硬件生成器的开始。下一篇文章将以真值表的例子介绍Chisel中组合逻辑电路的生成,作为用Chisel写硬件生成器的例子。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值