第十八章 Chisel基础——模块与硬件类型

Chisel在构建硬件的思路上类似Verilog。在Verilog中,是以“模块(module)”为基本单位组成一个完整的独立功能实体,所以Chisel也是按模块划分的,只不过不是用关键字“module”开头来定义模块,而是用一个继承自Module类的自定义class。

在Verilog里,模块内部主要有“线网(wire)”和“四态变量(reg)”两种硬件类型,它们用于描述数字电路的组合逻辑和时序逻辑。在Chisel里,也按这个思路定义了一些硬件类型,包括基本的线网和寄存器,以及一些常用的其它类型。前一章介绍了Chisel的数据类型,这还不够,因为这些数据类型是无法独立工作的。实际的电路应该是由硬件类型的对象构成的,不管是信号的声明,还是用赋值进行信号传递,都是由硬件类型的对象来完成的。数据类型和硬件类型融合在一起,才能构成完整、可运行的组件。比如要声明一个线网,这部分工作由硬件类型来完成;这个线网的位宽是多少、按无符号数还是有符号数解释、是不是向量等等,这些则是由作为参数的数据类型对象来定义的。

本章将介绍Chisel里的常用硬件类型以及如何编写一个基本的模块,对于高级类型,读者可以自行研究。这些类型的语法很简单,都是由定义在单例对象里的apply工厂方法来完成。字面的名字已经把硬件含义表明得很清楚,至于它们的具体实现是什么,读者可以不用关心。

一、Chisel是如何赋值的

有了硬件类型后,就可以用赋值操作来进行信号的传递或者电路的连接。只有硬件赋值才有意义,单纯的数据对象进行赋值并不会被编译器转换成实际的电路,因为在Verilog里也是对wire、reg类型的硬件进行赋值。那么,赋值操作需要什么样的操作符来完成呢?

在Chisel里,所有对象都应该由val类型的变量来引用,因为硬件电路的不可变性。因此,一个变量一旦初始化时绑定了一个对象,就不能再发生更改。但是,引用的对象很可能需要被重新赋值。例如,输出端口在定义时使用了“=”与端口变量名进行了绑定,那等到驱动该端口时,就需要通过变量名来进行赋值操作,更新数据。很显然,此时“=”已经不可用了,因为变量在声明的时候不是var类型。即使是var类型,这也只是让变量引用新的对象,而不是直接更新原来的可变对象。

为了解决这个问题,几乎所有的Chisel类都定义了方法“:=”,作为等号赋值的代替。所以首次创建变量时用等号初始化,如果变量引用的对象不能立即确定状态或本身就是可变对象,则在后续更新状态时应该用“:=”。从前面讲的操作符优先级来判断,该操作符以等号结尾,而且不是四种逻辑比较符号之一,所以优先级与等号一致,是最低的。例如:

val x = Wire(UInt(4.W))

val y = Wire(UInt(4.W))

x := "b1010".U  // 向4bit的线网x赋予了无符号数10

y := ~x  // 把x按位取反,传递给y

二、端口

   Ⅰ、定义端口列表

定义一个模块前一定要先定义好端口。整个端口列表是由方法“IO[T <: Data](iodef: T)”来定义的,通常其参数是一个Bundle类型的对象,而且引用的字段名称必须是“io”。因为端口存在方向,所以还需要方法“Input[T <: Data](source: T)”和“Output[T <: Data](source: T)”来为每个端口表明具体的方向。注意,“Input[T <: Data](source: T)”和“Output[T <: Data](source: T)”仅仅是复制它们的参数,所以不能是已经被硬件类型包裹的数据类型。目前Chisel还不支持双向端口inout,只能通过黑盒里的Analog端口来模拟外部Verilog的双向端口。

一旦端口列表定义完成,就可以通过“io.xxx”来使用。输入可以驱动内部其它信号,输出可以被其他信号驱动。可以直接进行赋值操作,布尔类型的端口还能直接作为使能信号。端口不需要再使用其它硬件类型来定义,不过要注意从性质上来说它仍然属于组合逻辑的线网。例如:

class MyIO extends Bundle {
   val in = Input(Vec(5, UInt(32.W)))
   val out = Output(UInt(32.W))
}

......
   val io = IO(new MyIO)  // 模块的端口列表
......

   Ⅱ、翻转端口列表的方向

对于两个相连的模块,可能存在大量同名但方向相反的端口。仅仅为了翻转方向而不得不重写一遍端口显得费时费力,所以Chisel提供了“Flipped[T <: Data](source: T)”方法,可以把参数里所有的输入转输出,输出转输入。如果是黑盒里的Analog端口,则仍是双向的。例如:

 class MyIO extends Bundle {
   val in = Input(Vec(5, UInt(32.W)))
   val out = Output(UInt(32.W))
}

......
   val io = IO(new MyIO)  // in是输入,out是输出
......
   val io = IO(Flipped(new MyIO))  // out是输入,in是输出

   Ⅲ、整体连接

翻转方向的端口列表通常配合整体连接符号“<>”使用。该操作符会把左右两边的端口列表里所有同名的端口进行连接,而且同一级的端口方向必须是输入连输出、输出连输入,父级和子级的端口方向则是输入连输入、输出连输出。注意,方向必须按这个规则匹配,而且不能存在端口名字、数量、类型不同的情况。这样就省去了大量连线的代码。例如:

class MyIO extends Bundle {
   val in = Input(Vec(5, UInt(32.W)))
   val out = Output(UInt(32.W))
}

......
   val io = IO(new Bundle {
       val x = new MyIO       
       val y = Flipped(new MyIO)
   })

   io.x <> io.y  // 相当于 io.y.in := io.x.in; io.x.out := io.y.out 
......

三、模块

   Ⅰ、定义模块

在Chisel里面是用一个自定义的类来定义模块的,这个类有以下三个特点:①继承自Module类。②有一个抽象字段“io”需要实现,该字段必须引用前面所说的端口对象。③在类的主构造器里进行内部电路连线。因为非字段、非方法的内容都属于主构造方法,所以用操作符“:=”进行的赋值、用“<>”进行的连线或一些控制结构等等,都属于主构造方法。从Scala的层面来讲,这些代码在实例化时表示如何构造一个对象;从Chisel的层面来讲,它们就是在声明如何进行模块内部子电路的连接、信号的传递,类似于Verilog的assign和always语句。实际上这些用赋值表示的电路连接在转换成Verilog时,组合逻辑就是大量的assign语句,时序逻辑就是always语句。

还有一点需要注意,这样定义的模块会继承一个字段“clock”,类型是Clock,它表示全局时钟,在整个模块内都可见。对于组合逻辑,是用不上它的,而时序逻辑虽然需要这个时钟,但也不用显式声明。还有一个继承的字段“reset”,类型是Reset,表示全局复位信号,在整个模块内可见。对于需要复位的时序元件,也可以不用显式使用该字段。如果确实需要用到全局时钟和复位,则可以通过它们的字段名称来使用,但要注意类型是否匹配,经常需要“reset.toBool”这样的语句把Reset类型转换成Bool类型用于控制。隐式的全局时钟和复位端口只有在生成Verilog代码时才能看到。

  • 16
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值