开源硬件描述语言Chisel的组合电路设计

2017-03-31 01:26雷思磊
单片机与嵌入式系统应用 2017年3期
关键词:数据类型开源端口

雷思磊

(酒泉卫星发射中心,酒泉 735000)

雷思磊

(酒泉卫星发射中心,酒泉 735000)

Chisel是加州大学伯克利分校研究人员设计并发布的一种开源的硬件设计语言,已成功用于实现多种处理器,在介绍Chisel基本概念的基础上,通过一个组合电路设计示例和一个时序电路设计示例,说明应用Chisel进行数字电路设计的主要流程。

Chisel; Scala;开源硬件

引 言

Chisel(Constructing Hardware In a Scala Embedded Language)是由加州大学伯克利分校设计并发布的一种开源硬件描述语言,建立在Scala语言之上,是Scala特定领域语言的一个应用,具有高度参数化的生成器(highly parameterized generators),可以支持高级硬件设计。其产生源于加州大学伯克利分校的研究人员认为现有的硬件描述语言(如VHDL、Verilog HDL等)最初设计的目的是用来仿真的,所以有很多不可综合的语法,此外,VHDL、Verilog HDL缺少目前高级语言具备的一些特性,比如对象、继承等[1],所以设计了Chisel,并将其开源,而因为Chisel就是使用Scala编写的一些类,所以在使用Chisel设计电路时,实际就是在写Scala程序,从而可以使用到Scala的许多高级特性,比如面向对象、函数式编程、类型参数化、类型推断等。

采用Chisel设计的电路,经过编译,可以得到针对FPGA、ASIC的Verilog HDL代码,还可以得到对应的时钟精确C++模拟器,如图1所示。

图1 使用Chisel可以得到多种目标

目前,有多个开源项目使用Chisel作为开发语言,包括采用RISC-V架构的开源标量处理器Rocket[2]、开源乱序执行处理器BOOM(Berkeley Out-of-Order Machine)[3]等。本文首先介绍Chisel的基本概念,然后使用Chisel实现一个简单的组合逻辑电路、一个简单的时序逻辑电路,以此说明应用Chisel进行数字电路设计的主要流程。

1 Chisel的基本概念[4]

1.1 数据类型

Chisel的数据类型用来指明在线上流动的信号(flowing on wire)、存储在状态元素(State Element)中的值的类型,对应Verilog HDL线网型、寄存器型等。虽然数字电路最终都是对二进制数字矢量进行操作,但是定义一些抽象的数据类型,有助于更加清晰地表达,同时也有利于产生更加优化的电路。具体类型如下:

① Bits:Bit的集合。

② SInt:有符号整数。

③ UInt:无符号整数。

④ Bool:布尔类型。

⑤ Bundle:一些元素的集合,每个元素都有一个变量名,类似于C语言中的结构体。

⑥ Vec:一些元素组成的向量,每个元素都有一个索引序号,类似于C语言中的数组。

上面的数据类型也对应一些类,比如SInt类型就对应SInt类,当声明一个SInt变量时,就需要调用SInt类的构造函数,比如:

SInt(5)

在上面的定义中并没有指明数据的宽度,但Chisel编译器会推测数据宽度,自动选择最小的宽度,当然也可以明确指明数据宽度,比如:

SInt(5,7) //此处明确指出宽度是7bit

Bundle是一些元素的集合,每个元素都有一个变量名,类似于C语言中的结构体。用户可以通过定义Bundle的子类来定义一个Bundle类型的变量,如下:

class MyFloat extends Bundles{

val sign = Bool()

val exponent = UInt(width=8)

val significant = UInt(width=23)

}

val x = new MyFloat()

val xs = x.sign

Vec是一些元素组成的向量,每个元素都有一个索引序号,类似于C语言中的数组。下面是一个Vec类型的变量的定义:

//定义了一个由5个23位宽度的SInt组成的Vec

val myVec = Vec.fill(5){SInt(width = 23)}

val reg3 = myVec(3) //取出其中的一个元素

1.2 组合电路

在Chisel中,每个电路都是一些节点的集合,每个节点是一个硬件操作单元,具有0个、1个或者多个输入,依据一个输入驱动一个输出。不同的节点可以通过操作符连接在一起,例如可以通过如下表达式表示一个简单的组合逻辑电路:

val out = (a & b) | (~c & d)

在上述表达式中,“&”表示bit与,“|”表示bit或,“~”表示bit反,a、b、c、d表示一些变量。任何一个简单的表达式都可以被转化为一个电路树,等式右边的变量作为叶子节点,操作符组成中间节点,电路的最终输出是电路树根节点的输出。

Chisel支持的操作符略——编者注。

1.3 函 数

Chisel支持将一些重复的逻辑定义为函数,然后再多处使用,例如下面定义了一个简单的函数:def clb(a:UInt, b:UInt, c:UInt, d:UInt) = (a & b) | (~c&d)

函数clb有4个参数a、b、c、d,此处的def是Scala中定义的关键字,用来定义函数,每个参数后面跟一个冒号,然后是数据类型。在参数之后定义返回类型,也可以不定义,Chisel会自动推测,上例中就没有定义返回类型。等号之后的就是函数体。函数定义之后,其使用方法如下:

val out = clb(a, b, c, d)

1.4 端 口

端口就是硬件单元对外的接口,需要指明方向(输入还是输出)。一个端口声明的例子如下:

class Decoupled extends Bundle{

val ready = Bool(OUTPUT)

val data = UInt(INPUT, 32)

val valid = Bool(INPUT)

}

其中INPUT、OUTPUT指定方向,后面指出宽度,对于Bool类型,其宽度就是1,所以不需要明确指出。

1.5 模 块

Chisel中的模块与Verilog HDL中模块的概念十分相似,都是用层次结构描述电路。Chisel中的模块定义为一个类,其定义遵循以下几点:继承自Module类、有一个命名为io的端口、在其构造函数中连接子电路。如下是一个2选1选择器的模块定义,其中io端口是Bundle类型。

class Mux2 extends Module{

val io = new Bundle{

val sel = UInt(INPUT, 1)

val in0 = UInt(INPUT, 1)

val in1 = UInt(INPUT, 1)

val out = UInt(OUTPUT, 1)

}

io.out := (io.sel & io.in1) | (~io.sel & io.in1)

}

1.6 状态单元

如果要实现时序电路,还需要状态单元,Chisel支持的最简单的状态单元就是上升沿触发的寄存器,可以使用如下方式例化:

val reg = Reg(next=in)

上述代码形成的电路就是:将输入赋值给输出,但是输出比输入延迟一个时钟周期。此处没有声明变量reg的数据类型,Chisel会自动从输入变量in推测reg的类型。另外,也没有声明clock、reset信号,因为在Chisel中,clock、reset都是全局信号,不需要显示声明。

使用寄存器可以组成许多有用的电路,如下是一个上升沿检测电路的代码,输入是一个Boolean变量,如果检测到上升沿,那么输出true:

def risingedge(x: Bool) = x && !Reg(next=x)

如下是一个计数器的代码,计数到最大值,然后从0开始重新计数:

def counter(max: UInt) = {

val x = Reg(init = UInt(0, max.getWidth))

x := Mux(x === max, UInt(0), x + UInt(1))

x

}

2 基于Chisel的组合电路设计

2.1 Chisel开发环境搭建

实验环境是Ubuntu14.04(64位),下载sbt0.13.8,解压到一个路径下,比如/home/riscv/riscv/sbt,将其中的bin路径添加到环境变量PATH中,也就是打开~/.profile文件,在最后添加如下语句:

export PATH=/home/riscv/riscv/sbt/bin/:$PATH

2.2 比较器电路

设计一个比较器电路以说明Chisel进行数字电路设计的基本流程。新建一个文件夹chisel_max,在该文件夹下新建文件max2.scala、build.sbt,其中build.sbt的内容如下(注意两行之间空一行),表示需要最新的Chisel库,以及一些依赖库的地址:

resolvers ++= Seq("scct-github-repository" at

"http://mtkopone.github.com/scct/maven-repo")

libraryDependencies += "edu.berkeley.cs" %% "chisel" % "latest.release"

max2.scala的内容如下,这是一个比较器,从两个8位的输入中,选择一个较大的数,作为输出:

import Chisel._

class Max2 extends Module {

val io = new Bundle {

val in0 = UInt(INPUT,8)

val in1 = UInt(INPUT,8)

val out = UInt(OUTPUT,8)

}

io.out := Mux(io.in0 > io.in1, io.in0, io.in1)

}

object Hello {

def main(args: Array[String]) : Unit={

val margs=Array("--backend","v","--compile")

chiselMain(margs, () => Module(new Max2()))

}

}

上述代码首先引入Chisel依赖,然后定义了一个Max2类,这个电路有两个8位输入,一个8位输出,输出值是两个输入中较大的那一个值。在最后定义了一个方法main,其中使用了对象chiselMain,通过给chiselMain传递不同的参数,可以得到C++模拟器、Verilog HDL代码等不同的结果。chiselMain支持的参数如表1所列。

表1 chiselMain支持的参数

由表2可知,因为backend的值是v,所以上述代码会得到Verilog代码。在终端中进入上面的chisel_max目录,输入sbt,会下载相关的依赖包,最后会给出>符号,此时输入run,即可得到对应的Verilog代码文件。得到的Verilog代码位于文件Max2.v中,实现的正是比较器的功能,内容如下:

module Max2(

input [7:0] io_in0,

input [7:0] io_in1,

output[7:0] io_out

);

wire[7:0] T0;

wire T1;

assign io_out = T0;

assign T0 = T1 ? io_in0 : io_in1;

assign T1 = io_in1 < io_in0;

endmodule

2.3 C++模拟器的使用

使用C++模拟器的主要目的是用来做测试,所以先介绍一下相关的类,Chisel采用测试向量的机制来测试电路,测试向量所在的类是Tester的子类。测试类继承自Tester,然后连接到指定的模块,就可以向该模块输入测试向量了,具体来说,Tester提供了如下方法:

① poke用来设置端口的值;

② step用来表示执行一个时钟;

③ peek用来读取端口的值;

④ expect用来判断读取的端口值是否是预期的值。

如下是添加了测试类的比较器:

import Chisel._

class Max2 extends Module {

val io = new Bundle {

val in0 = UInt(INPUT,8)

val in1 = UInt(INPUT,8)

val out = UInt(OUTPUT,8)

}

io.out := Mux(io.in0 > io.in1, io.in0, io.in1)

}

class Max2Tests(c: Max2) extends Tester(c) {

for (i <- 0 until 10) {

// FILL THIS IN HERE

val in0 = rnd.nextInt(1 << 8)

val in1 = rnd.nextInt(1 << 8)

poke(c.io.in0, in0)

poke(c.io.in1, in1)

step(1)

expect(c.io.out, if(in0 > in1) in0 else in1)

}

}

object max2 {

def main(args: Array[String]) : Unit={

val margs=Array("--backend","c","--genHarness","--compile","--test")

chiselMainTest(margs, () => Module(new Max2())){c => new Max2Tests(c)}

}

}

与第2.2节比较,有如下几点不同:

① 增加了一个Max2Tests类,继承自Tester,其中使用poke方法给Max2电路的输入赋值,此处随机产生了10组数据,使用expect判断Max2电路的输出是否正确,是否符合预期。

图2 计数器对应的电路

② 在main函数中将chiselMain替换为chiselMainTest,目的是进行测试。

③ chiselMianTest的参数中backend的值为c,表示得到C++代码,此外,还添加了一个test参数。

打开终端,进入chisel_max的目录,输入sbt,再输入run,可以得到下面的结果,限于篇幅,本文只给出第一组数据的测试结果,输入的测试值为0x6b、0x99,输出的值为0x99,是输入值中较大的,符合预期。

……

STARTING ./Max2

RESET 5

POKE Max2.io_in0 <-0x6b

POKE Max2.io_in1 <-0x99

STEP 1 -> 1

PEEK Max2.io_out -> 0x99

EXPECT Max2.io_out <- 153 == 153 PASS

……

3 基于Chisel的时序电路设计

本节通过一个同步计数器电路设计,说明Chisel时序电路的设计要点,这个电路有一个使能输入,当使能输入为1的时候,计数器才计数,并且是在时钟上升沿计数。电路设计如下:

import Chisel._

class Counter extends Module {

val io = new Bundle {

val enb = Bool(INPUT)

val out = UInt(OUTPUT, 4)

}

val counter1 = Reg(UInt(width = 4), UInt(0))

when ( io.enb ) { counter1 := counter1 + UInt(1)}

io.out := counter1

}

object counter_obj {

def main(args: Array[String]) : Unit={

……

}

Counter类中定义的是同步计数器,具有一个使能输入enb、一个计数结果输出out。在Counter的构造函数中,定义了一个寄存器counter1,其中有隐含的时钟输入,接着是一个条件判断,enb为真的时候,counter1的值才加1,最后将counter1的值作为输出。编译上述代码得到对应的Verilog HDL代码,然后在QuartusII中编译,可以得到如图2所示电路。从电路结构分析,实现的正是计数器。

结 语

Chisel基于Scala,充分利用了Scala的一些高级特性,在本文示例电路的实现中也体现了这一点,Chisel仍然在持续改进中,目前已成功使用Chisel编写实现了数种开源处理器。此外,Chisel的开源特性,也有助于用户了解一种硬件设计语言的内部实现机理,并在此基础上进行特定的优化、改进。

Lei Silei

(Jiuquan Satellite Launch Center,Jiuquan 735000,China)

Chisel is an open source hardware design language,which is designed and published by researchers of university of California at Berkeley.It has been successfully used to achieve a variety of processors.In the paper,the basic concept of Chisel is introduced.The main processes of Chisel digital circuit design is illustrated through a combination of circuit design example and a sequential circuit design example.

Chisel; Scala;open source

TP368.1

A

猜你喜欢
数据类型开源端口
详谈Java中的基本数据类型与引用数据类型
一种端口故障的解决方案
如何理解数据结构中的抽象数据类型
五毛钱能买多少头牛
端口阻塞与优先级
基于SeisBase模型的地震勘探成果数据管理系统设计
大家说:开源、人工智能及创新
开源中国开源世界高峰论坛圆桌会议纵论开源与互联网+创新2.0
开源计算机辅助翻译工具研究
8端口IO-Link参考设计套件加快开发速度