Chisel Language I
本文归纳总结于Chisel的官方tutorial:https://mybinder.org/v2/gh/freechipsproject/chisel-bootcamp/master
1. Basic knowledege of Scala
我使用Jetbrain的IntelliJ作为IDE。
1.1 变量
Chisel是基于Scala的,所以有必要先将Scala的基础语法学一下:
首先是 var 和 val:var和val都是定义变量,但是有一个区别,var的变量的值是可以被操作的,而val的变量的值是不可被操作的。即:
var a = 1
a+=1
这两句是可以被执行的,如果将var换成val,那么便不可执行第二句。
输出用println,var和val可以自由定义各种变量包括字符串。
1.2 条件
条件语句与其他编程语言几乎一致,if else, else if都可以被使用。如:
if (a>b)
println(a)
else if (a==b)
println(c)
else
println(b)
无需添加分号。
但是,在scala中,if语句会有一个返回值:
val a_or_b = if (a== b)
a
else
b
在这个语句中,a_or_b的值由if else的结果来决定,条件判定成功则为a,否则是b。
1.3 函数
1.3.1 函数定义
def times2(x: Int): Int = 2 * x
def distance (x: Int, y: Int, returnPositive: Boolean): Int = {
val xy = x * y
if (returnPositive)
xy.abs
else
-xy.abs
}
函数定义如上所描述, 要用冒号来提示变量和函数的类型。上文中的.abs表示绝对值。先进行绝对值操作,再进行符号操作。
1.3.2 递归和嵌套
def asciiTriangle(rows: Int) {
def printRow(columns: Int): Unit = println("X" * columns)
if(rows > 0) {
printRow(rows)
asciiTriangle(rows - 1)
}
}
上述函数使用了递归与嵌套功能, asciiTriangle函数被递归使用来重复输出“X”,而printRow是被嵌套在 asciiTriangle函数中,所以如果在 asciiTriangle函数范围外调用的话是无效的。
如若使用 asciiTriangle(6),效果如下图所示:
1.4 列表
val x = 7
val y = 14
val list1 = List(1, 2, 3) // 1,2,3
val list2 = x :: y :: y :: Nil // 7,14,14
val list3 = list1 ++ list2 // 1,2,3,7,7,14
val m = list2.length // 3
val s = list2.size // 3
val headOfList = list1.head // 1
val restOfList = list1.tail // 2,3
val third = list1(2) // 3
上方代码描述了如何定义列表。 :: 等价于Tcl中的lappend,即像列表尾部增添新元素。
++表示将两个列表连接。 length和size等价,都是描述列表长度。 .head描述首元素,tail表示去除首元素后的列表。其他方面与其他编程语言并无区别。
1.5 For 循环
for (i <- 0 to 7)
{ print(i + " ") } //0 1 2 3 4 5 6 7
以上为for循环的0到7(包括边缘)的迭代。
for (i <- 0 until 7)
{ print(i + " ") } 。// 0 1 2 3 4 5 6
用until来替代to可以不包括右边界。
val intList = List(1, 0,-1, 2)
var listSum = 0
for (value <- intList ) {
listSum += value
}
println("sum is " + listSum)
和python类似,scala的for循环也可以对列表进行遍历。
val intList = List(1, 0, -1, 2)
var listSum = 0
var i =0
for (value <- intList) {
if (i!=3)
listSum += value
i=i+1
}
println("sum is " + listSum)
使用标志符i可以实现不包括指定元素进行遍历。
1.6 Packages and Imports
package mytools
class Tool1 { ... }
当写一个包供以后使用时,应使用以上格式的代码进行打包。
import mytools.Tool1
当引用外部的包时,如使用mytools中的Tool1,应使用以上的import。
import chisel3._
import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester}
当使用Chisel时应至少采用以上的package。
1.7 类的例子
class WrapCounter(counterBits: Int) {
val max: Long = (1 << counterBits) - 1
var counter = 0L
def inc(): Long = {
counter = counter + 1
if (counter > max) {
counter = 0
}
counter
}
println(s"counter created with max value $max")
}
上方代码描述了一个根据bit来定的counter的class。inc是class内部的函数,用来count,每调用一次inc,就会多记一次数,直到inc的返回值counter大于max。
counter的定义中的0L代表着这是一个long类型的数据。
val x = new WrapCounter(8) // 最大值255的计数器
x.inc() // 1
var a = x.inc() // 2
if(x.counter == x.max) {
println("counter is about to wrap")
}
上方代码为实际调用class及其内部函数/变量的例子。
1.8 代码块
def add1(c: Int): Int = c + 1 // +1
class RepeatString(s: String) {
val repeatedString = s + s //重复字符串
}
上方两个代码块中c和s分别为各自的参数。
val stringList = intList.map { i =>i.toString }
上方代码提供了另一种可调参的代码块,stringList利用map的功能,将intList里的每一个元素转变为字符形式,i负责表示List的元素。
def myMethod(count: Int, wrap: Boolean, wrapValue: Int = 24): Unit = { ... }
上方代码所定义的函数中, Unit表示返回值无实际意义,即与C语言中void类似,也可省略不写。在实际调用中,可以采取以下方法调用:
myMethod(count = 10, wrap = false, wrapValue = 23)
调用时如若使用了参数名称,那么调用时可不按顺序。
myMethod(wrap = false, count = 10)
由于wrapValue的值默认是24,调用时也可不重新赋值,可以省略。
2. Chisel
环境配置用了很多时间,最主要是不熟悉sbt导致的,熟悉sbt后再去配置chisel会事半功倍。最终我采用了wsl+vs code的方式配置了chisel开发环境。
2.1 First Module of Chisel
class Passthrough extends Module {
val io = IO(new Bundle {
val in = Input(UInt(4.W))
val out = Output(UInt(4.W))
})
io.out := io.in
}
上方代码定义了一个4 bit 无符号output = 4 bit无符号 input的IO。
class Passthrough extends Module { :所有Chisel class必须extends成module。
val io = IO(...) : 定义了变量为io型变量,io必须被命名为io,为了和IO对应。
new Bundle { val in = Input(...) val out = Output(...)} :定义了新的硬件结构,in和out并分别赋予他们对应的input和output值。
UInt(4.W) : Unsigned int型变量的width为4。
io.out := io.in : io.in drives io.out, := 代表着右方变量驱动左方变量。
println(getVerilog(new Passthrough)) :使用此语句可将以上chisel翻译成verilog。
每一个Chisel module都需要一个对应的tester来检验基本功能,因为硬件编程我们无法从结果得出程序是否正确,所以tester的功能是十分重要的。
test(new Passthrough()) { c =>
c.io.in.poke(0.U) // Set our input to value 0
c.io.out.expect(0.U) // Assert that the output correctly has 0
c.io.in.poke(1.U) // Set our input to value 1
c.io.out.expect(1.U) // Assert that the output correctly has 1
c.io.in.poke(2.U) // Set our input to value 2
c.io.out.expect(2.U) // Assert that the output correctly has 2
}
println("SUCCESS!!") // Scala Code: if we get here, our tests passed!
以上代码是module passthrough的tester,poke和expect是成组使用的函数,poke用来set value,而expect用来检验value。上述代码的思想便是:如若将input设置为a,检验output是否也为a。如若相同,则pass,否则程序有问题。
2.2 组合逻辑
2.2.1 常用操作符
首先为上文的passthrough的代码增加一些操作:
class MyModule extends Module {
val io = IO(new Bundle {
val in = Input(UInt(4.W))
val out = Output(UInt(4.W))
})
val two = 1 + 1
println(two)
val utwo = 1.U + 1.U
println(utwo)
io.out := io.in
}
编译结果如下:
two变量被视作是数字1相加,所以会被println正常打印,而1.U是一个从Scala Int (1)到Chisel UInt的类型转换,utwo被视作将两个Chisel硬件node相加,所以也被视作一个硬件,println会打印出名字和指针。
class MyOperators extends Module {
val io = IO(new Bundle {
val in = Input(UInt(4.W))
val out_add = Output(UInt(4.W))
val out_sub = Output(UInt(4.W))
val out_mul = Output(UInt(4.W))
})
io.out_add := 1.U + 4.U
io.out_sub := 2.U - 1.U
io.out_mul := 4.U * 2.U
}
以上代码提供了更多操作符:加,减,乘。作为此代码的tester,只需检验结果是否为无符号5,1,8即可。
test(new MyOperators) {c =>
c.io.out_add.expect(5.U)
c.io.out_sub.expect(1.U)
c.io.out_mul.expect(8.U)
}
println("SUCCESS!!")
2.2.2 Mux and Concatenation
class MyOperatorsTwo extends Module {
val io = IO(new Bundle {
val in = Input(UInt(4.W))
val out_mux = Output(UInt(4.W))
val out_cat = Output(UInt(4.W))
})
val s = true.B
io.out_mux := Mux(s, 3.U, 0.U)
io.out_cat := Cat(2.U, 1.U)
}
以上代码展示了chisel中的mux和concatenation,即多路复用和串接。
Mux的功能为:Y=SA+S'B, S为bool值的true或者false,当S为true时,output只和A相关,当S为false时输出只和B相关。
Cat的功能时将两个数字串接,前后串接,如上例中2进制的10与2进制的1串接在一起就是101。
相应的,这两个功能的tester就是检验值是否为3和5,如下所示:
test(new MyOperatorsTwo) { c =>
c.io.out_mux.expect(3.U)
c.io.out_cat.expect(5.U)
}
println("SUCCESS!!")
2.2.3 练习
class Arbiter extends Module {
val io = IO(new Bundle {
// FIFO
val fifo_valid = Input(Bool())
val fifo_ready = Output(Bool())
val fifo_data = Input(UInt(16.W))
// PE0
val pe0_valid = Output(Bool())
val pe0_ready = Input(Bool())
val pe0_data = Output(UInt(16.W))
// PE1
val pe1_valid = Output(Bool())
val pe1_ready = Input(Bool())
val pe1_data = Output(UInt(16.W))
})
io.fifo_ready := io.pe0_ready || io.pe1_ready
io.pe0_valid := io.fifo_valid && io.pe0_ready
io.pe1_valid := io.fifo_valid && io.pe1_ready && !io.pe0_ready
io.pe0_data := io.fifo_data
io.pe1_data := io.fifo_data
}
以上代码为上图的结构,FIFO output数据给Arbiter,由Arbiter来决定在适合的时候将数据传递给PE0和PE1。 适合的时间为: PE0 PE1 至少有一个ready。 特殊原则: PE0和PE1同时ready时,由PE0先接受数据。并且需要先检验PE是否ready再检验data是否valid。
tester如以下代码所示,由位操作来不断让valid,ready的值变化,从而达到测试不同情况的结果。
test(new Arbiter) { c =>
import scala.util.Random
val data = Random.nextInt(65536)
c.io.fifo_data.poke(data.U)
for (i <- 0 until 8) {
c.io.fifo_valid.poke((((i >> 0) % 2) != 0).B)
c.io.pe0_ready.poke((((i >> 1) % 2) != 0).B)
c.io.pe1_ready.poke((((i >> 2) % 2) != 0).B)
c.io.fifo_ready.expect((i > 1).B)
c.io.pe0_valid.expect((i == 3 || i == 7).B)
c.io.pe1_valid.expect((i == 5).B)
if (i == 3 || i ==7) {
c.io.pe0_data.expect((data).U)
} else if (i == 5) {
c.io.pe1_data.expect((data).U)
}
}
}
println("SUCCESS!!")
2.2.4 参数化硬件编程
Parameterization capability是chisel最强大的功能之一,以下代码会对此做出解释:
class ParameterizedAdder(saturate: Boolean) extends Module {
val io = IO(new Bundle {
val in_a = Input(UInt(4.W))
val in_b = Input(UInt(4.W))
val out = Output(UInt(4.W))
})
val sum = io.in_a +& io.in_b
if (saturate) {
io.out := Mux(sum > 15.U, 15.U, sum)
} else {
io.out := sum
}
}
以上代码描述了一个加法器,此加法器可选择最大进位截断或者输出当前计算器可显示的最大值。
其中+&是用来连接a bit的wire和a+1bit的wire。
saturate并非是hardware结构的bool,所以这是在软件上由人为决定最终生成的硬件是截断最高位还是输出饱和值。
利用Mux来确定是否需要显示饱和值15,即2bit的1111,如果sum的值并没有大于15,那么正常输出结果即可。利用saturate作为flag来确定是否需要启动saturate的功能。
此代码的tester如下所示:
for (saturate <- Seq(true, false)) {
test(new ParameterizedAdder(saturate)) { c =>
// 100 random tests
val cycles = 100
import scala.util.Random
import scala.math.min
for (i <- 0 until cycles) {
val in_a = Random.nextInt(16)
val in_b = Random.nextInt(16)
c.io.in_a.poke(in_a.U)
c.io.in_b.poke(in_b.U)
if (saturate) {
c.io.out.expect(min(in_a + in_b, 15).U) // 两个值间较小值会作为输出
} else {
c.io.out.expect(((in_a + in_b) % 16).U) //截断
}
}
c.io.in_a.poke(15.U) // ensure we test saturation vs. truncation
c.io.in_b.poke(15.U) // ensure we test saturation vs. truncation
if (saturate) {
c.io.out.expect(15.U)
} else {
c.io.out.expect(14.U)
}
}
}
println("SUCCESS!!")
2.3 控制流
2.3.1 Last Connect Semantics
class LastConnect extends Module {
val io = IO(new Bundle {
val in = Input(UInt(4.W))
val out = Output(UInt(4.W))
})
io.out := 1.U
io.out := 2.U
io.out := 3.U
io.out := 4.U
}
test(new LastConnect) { c =>
c.io.out.expect(4.U) } // Assert that the output correctly has 4
println("SUCCESS!!") // Scala Code: if we get here, our tests passed!
以上的code和tester解释了什么是Last Connect Semantics,即,当多个wire同时驱动一个hardware时,只有最后一条语句是有效的。
2.3.2 when, elsewhen, and otherwise
Chisel的条件逻辑语句实现主要依靠when,elsewhen 和 otherwise,形式如下:
when(someBooleanCondition) {
// things to do when true
}.elsewhen(someOtherBooleanCondition) {
// things to do on this condition
}.otherwise {
// things to do if none of th boolean conditions are true
}
硬件实现上要严格遵守以上结构。并且,Scala中,if语句存在返回值,可以直接在赋值语句中使用的这种行为在when语句中并不适用。
import chisel3._
import chisel3.util._
class Max3 extends Module {
val io = IO(new Bundle {
val in1 = Input(UInt(16.W))
val in2 = Input(UInt(16.W))
val in3 = Input(UInt(16.W))
val out = Output(UInt(16.W))
})
when(io.in1 >= io.in2 && io.in1 >= io.in3) {
io.out := io.in1
}.elsewhen(io.in2 >= io.in3) {
io.out := io.in2
}.otherwise {
io.out := io.in3
}
}
object Max3 extends App {
(new chisel3.stage.ChiselStage).emitVerilog(new Max3())
println(getVerilogString (new Max3))
}
以上代码描述了when语句的使用,以下代码为其tester:
import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec
class max3test extends AnyFlatSpec with ChiselScalatestTester {
behavior of "max3"
it should "pass" in {
test(new Max3) { c =>
// verify that the max of the three inputs is correct
c.io.in1.poke(6.U)
c.io.in2.poke(4.U)
c.io.in3.poke(2.U)
c.io.out.expect(6.U) // input 1 should be biggest
c.io.in2.poke(7.U)
c.io.out.expect(7.U) // now input 2 is
c.io.in3.poke(11.U)
c.io.out.expect(11.U) // and now input 3
c.io.in3.poke(3.U)
c.io.out.expect(7.U) // show that decreasing an input works as well
c.io.in1.poke(9.U)
c.io.in2.poke(9.U)
c.io.in3.poke(6.U)
c.io.out.expect(9.U) // still get max with tie
}
}
println("SUCCESS!!") // Scala Code: if we get here, our tests passed!
}
2.3.3 The wire construct
Wire可以出现在操作符:=的左边或右边。为了详细阐述wire,以上图作为例子,当数据的左值小于右值时,数据依据红线进行流动,反之则是黑线。
符合此逻辑的代码如下:
import chisel3._
import chisel3.util._
class Sort4 extends Module {
val io = IO(new Bundle {
val in0 = Input(UInt(16.W))
val in1 = Input(UInt(16.W))
val in2 = Input(UInt(16.W))
val in3 = Input(UInt(16.W))
val out0 = Output(UInt(16.W))
val out1 = Output(UInt(16.W))
val out2 = Output(UInt(16.W))
val out3 = Output(UInt(16.W))
})
val row10 = Wire(UInt(16.W))
val row11 = Wire(UInt(16.W))
val row12 = Wire(UInt(16.W))
val row13 = Wire(UInt(16.W))
when(io.in0 < io.in1) {
row10 := io.in0 // preserve first two elements
row11 := io.in1
}.otherwise {
row10 := io.in1 // swap first two elements
row11 := io.in0
}
when(io.in2 < io.in3) {
row12 := io.in2 // preserve last two elements
row13 := io.in3
}.otherwise {
row12 := io.in3 // swap last two elements
row13 := io.in2
}
val row21 = Wire(UInt(16.W))
val row22 = Wire(UInt(16.W))
when(row11 < row12) {
row21 := row11 // preserve middle 2 elements
row22 := row12
}.otherwise {
row21 := row12 // swap middle two elements
row22 := row11
}
val row20 = Wire(UInt(16.W))
val row23 = Wire(UInt(16.W))
when(row10 < row13) {
row20 := row10 // preserve middle 2 elements
row23 := row13
}.otherwise {
row20 := row13 // swap middle two elements
row23 := row10
}
when(row20 < row21) {
io.out0 := row20 // preserve first two elements
io.out1 := row21
}.otherwise {
io.out0 := row21 // swap first two elements
io.out1 := row20
}
when(row22 < row23) {
io.out2 := row22 // preserve first two elements
io.out3 := row23
}.otherwise {
io.out2 := row23 // swap first two elements
io.out3 := row22
}
}
object Sort4 extends App {
(new chisel3.stage.ChiselStage).emitVerilog(new Sort4())
println(getVerilogString (new Sort4))
}
从以上代码可以看出,wire是在一个IO系统中,负责传递数据的工作,不作为最终输出结果,仅仅是充当导线的作用,传递input的值到output,传递过程中可以存在众多逻辑操作,从而达到想要的结果。
tester如下代码所示:
import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec
class Sort4test extends AnyFlatSpec with ChiselScalatestTester {
behavior of "sort4"
it should "pass " in {
test(new Sort4) { c =>
// verify the inputs are sorted
c.io.in0.poke(3.U)
c.io.in1.poke(6.U)
c.io.in2.poke(9.U)
c.io.in3.poke(12.U)
c.io.out0.expect(3.U)
c.io.out1.expect(6.U)
c.io.out2.expect(9.U)
c.io.out3.expect(12.U)
c.io.in0.poke(13.U)
c.io.in1.poke(4.U)
c.io.in2.poke(6.U)
c.io.in3.poke(1.U)
c.io.out0.expect(1.U)
c.io.out1.expect(4.U)
c.io.out2.expect(6.U)
c.io.out3.expect(13.U)
c.io.in0.poke(13.U)
c.io.in1.poke(6.U)
c.io.in2.poke(4.U)
c.io.in3.poke(1.U)
c.io.out0.expect(1.U)
c.io.out1.expect(4.U)
c.io.out2.expect(6.U)
c.io.out3.expect(13.U)
}
println("SUCCESS!!") // Scala Code: if we get here, our tests passed!
}}
tester的检验部分也是针对IO的最终结果,而不是对wire进行检验。
2.3.4 练习
2.3.4.1 多项式
import chisel3._
import chisel3.util._
class Polynomial extends Module {
val io = IO(new Bundle {
val select = Input(UInt(2.W))
val x = Input(SInt(32.W))
val fOfX = Output(SInt(32.W))
})
val result = Wire(SInt(32.W))
val square = Wire(SInt(32.W))
square := io.x * io.x
when(io.select === 0.U) {
result := (square - (2.S * io.x)) + 1.S
}.elsewhen(io.select === 1.U) {
result := (2.S * square) + (6.S * io.x) + 3.S
}.otherwise {
result := (4.S * square) - (10.S * io.x) - 5.S
}
io.fOfX := result
}
object Polynomial extends App {
(new chisel3.stage.ChiselStage).emitVerilog(new Polynomial())
println(getVerilogString (new Polynomial))
}
从以上练习与example中,可以总结出,在用chisel一个IO结构时,先要给出在新定义一个这个IO的新bundle,bundle中需要包含input和output。定义完IO之后,再对IO内部需要用到的逻辑过程进行定义和描述,如以上代码中的wire square 和wire result和when语句。最终再将结果传递给IO中的output。
以上代码中,SInt为有符号整数类型。
以上代码的tester如下:
import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec
class Polynomialtest extends AnyFlatSpec with ChiselScalatestTester {
behavior of "Polynomial"
it should "pass" in {
def poly0(x: Int): Int = x*x - 2*x + 1
def poly1(x: Int): Int = 2*x*x + 6*x + 3
def poly2(x: Int): Int = 4*x*x - 10*x - 5
def poly(select: Int, x: Int): Int = {
if(select == 0) {
poly0(x)
}
else if(select == 1) {
poly1(x)
}
else {
poly2(x)
}
}
test(new Polynomial) { c =>
for(x <- 0 to 20) {
for(select <- 0 to 2) {
c.io.select.poke(select.U)
c.io.x.poke(x.S)
c.io.fOfX.expect(poly(select, x).S)
}
}
}
println("SUCCESS!!") // Scala Code: if we get here, our tests passed!
}
}
2.3.4.2 有限状态机(Finite State Machine)
有限状态机是常用的hardware结构,以下是本次exercise中的例子:
研究生总是会有以下几个阶段:idle,coding,writing和grad。各个state根据不同的激励信号会导致不同的next state,若多种激励信号同时到达,以以下为主:coffee > idea > pressure。
Chisel代码如下所示:
class GradLife extends Module {
val io = IO(new Bundle {
val state = Input(UInt(2.W))
val coffee = Input(Bool())
val idea = Input(Bool())
val pressure = Input(Bool())
val nextState = Output(UInt(2.W))
})
val idle :: coding :: writing :: grad :: Nil = Enum(4)
io.nextState := idle //如果本身就是grad阶段,那么下一阶段自然就是idle
when (io.state === idle) {
when (io.coffee)
{ io.nextState := coding }
.elsewhen (io.idea)
{ io.nextState := idle }
.elsewhen (io.pressure)
{ io.nextState := writing }
} .elsewhen (io.state === coding) {
when (io.coffee)
{ io.nextState := coding }
.elsewhen (io.idea || io.pressure)
{ io.nextState := writing }
} .elsewhen (io.state === writing) {
when (io.coffee || io.idea)
{ io.nextState := writing }
.elsewhen (io.pressure)
{ io.nextState := grad }
}
}
因为hardware的结构本身就是input直接连接到output,所以无需多余的wire进行逻辑操作,而逻辑判断的语句使用when就可以完成。
tester如下所示:
import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec
class gradtest extends AnyFlatSpec with ChiselScalatestTester {
behavior of "gradlife"
it should "pass" in {
def states = Map("idle" -> 0, "coding" -> 1, "writing" -> 2, "grad" -> 3)
def gradLife (state: Int, coffee: Boolean, idea: Boolean, pressure: Boolean): Int = {
var nextState = states("idle")
if (state == states("idle")) {
if (coffee) { nextState = states("coding") }
else if (idea) { nextState = states("idle") }
else if (pressure) { nextState = states("writing") }
} else if (state == states("coding")) {
if (coffee) { nextState = states("coding") }
else if (idea || pressure) { nextState = states("writing") }
} else if (state == states("writing")) {
if (coffee || idea) { nextState = states("writing") }
else if (pressure) { nextState = states("grad") }
}
nextState
}
test(new GradLife) { c =>
// verify that the hardware matches the golden model
for (state <- 0 to 3) {
for (coffee <- List(true, false)) {
for (idea <- List(true, false)) {
for (pressure <- List(true, false)) {
c.io.state.poke(state.U)
c.io.coffee.poke(coffee.B)
c.io.idea.poke(idea.B)
c.io.pressure.poke(pressure.B)
c.io.nextState.expect(gradLife(state, coffee, idea, pressure).U)
}
}
}
}
}
println("SUCCESS!!") // Scala Code: if we get here, our tests passed!
}
}
2.4 时序逻辑电路
2.4.1 寄存器
import chisel3._
import chisel3.util._
class RegisterModule extends Module {
val io = IO(new Bundle {
val in = Input(UInt(12.W))
val out = Output(UInt(12.W))
})
val register = Reg(UInt(12.W))
register := io.in + 1.U
io.out := register
}
object RegisterModule extends App {
(new chisel3.stage.ChiselStage).emitVerilog(new RegisterModule())
println(getVerilogString (new RegisterModule))
}
以上代码描述了寄存器的使用,REG型变量register连接io.in,并对此值加1然后作为io的output。
寄存器的定义通过关键字:Reg,而UInt标志了我们想要的是一个12位宽的无符号型寄存器。
此代码的tester如下:
import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec
class regtest extends AnyFlatSpec with ChiselScalatestTester {
behavior of "reg"
it should "pass" in {
test(new RegisterModule) { c =>
for (i <- 0 until 100) {
c.io.in.poke(i.U)
c.clock.step(1)
c.io.out.expect((i + 1).U)
}
}
println("SUCCESS!!")
}
}
此tester检验了从0到99的所有数字。step函数的作用为使test harness(测试工具)让clock运转一次,然后将会使得register将input传递到output。Step函数的参数代表clock的运行次数。
此前的tester中并没有应用step函数是因为,此前的电路只是组合逻辑电路,input会被立刻通过组合逻辑电路传递到output,而以上的电路为时序逻辑电路,时序逻辑电路需要时钟配合来运行。Step也只被应用在时序逻辑电路中。
Chisel中,如上文提到的,形如2.U的变量,是一种hardware硬件结构,所以:
val myReg = Reg(UInt(2.W)) //OK, reg需要一个model作为type
val myReg = Reg(2.U) // illegal,因为2.U已经是一个hardware node, 不能再被当作model来定义REG
Chisel还提供了另一种方法来使用Reg,RegNext。RegNext的意义为延迟一周期输出,使用方法如下:
class RegNextModule extends Module {
val io = IO(new Bundle {
val in = Input(UInt(12.W))
val out = Output(UInt(12.W))
})
// register bitwidth is inferred from io.out
io.out := RegNext(io.in + 1.U)
}
RegNext表明:一个寄存器将会得到io的input并加一然后传递到output,但需要延迟一个周期,生成的verilog会和之前的代码一样。
上述的代码中,使用register时是将register初始化为一个随机data,以下代码可以为register赋予初始值:
val myReg = RegInit(UInt(12.W), 0.U)
val myReg = RegInit(0.U(12.W))
关键字RegInit能够初始化寄存器,以两种方式:
第一种方式使用了两个argument,第一个argument负责定义node的datatype和width,第二个argument负责赋予这个node的值。
第二种方式只使用了一个argument,直接对node进行初始化赋值。
import chisel3._
import chisel3.util._
class RegInitModule extends Module {
val io = IO(new Bundle {
val in = Input(UInt(12.W))
val out = Output(UInt(12.W))
})
val register = RegInit(0.U(12.W))
register := io.in + 1.U
io.out := register
}
object RegInitModule extends App {
(new chisel3.stage.ChiselStage).emitVerilog(new RegInitModule ())
println(getVerilogString (new RegInitModule ))
}
以上代码描述了如何使用RegInit来初始化register并使用。
2.4.1.1 Control Flow
import chisel3._
import chisel3.util._
class FindMax extends Module {
val io = IO(new Bundle {
val in = Input(UInt(10.W))
val max = Output(UInt(10.W))
})
val max = RegInit(0.U(10.W))
when (io.in > max) {
max := io.in
}
io.max := max
}
object FindMax extends App {
(new chisel3.stage.ChiselStage).emitVerilog(new FindMax ())
println(getVerilogString (new FindMax ))
}
以上代码描述了如何使用初始化register并且将输入数据中的最大值赋予register并由寄存器出传递给IO的output。
以下代码为其tester:
import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec
class regcftest extends AnyFlatSpec with ChiselScalatestTester {
behavior of "regcf"
it should "pass" in {
test(new FindMax) { c =>
c.io.max.expect(0.U)
c.io.in.poke(1.U)
c.clock.step(1)
c.io.max.expect(1.U)
c.io.in.poke(3.U)
c.clock.step(1)
c.io.max.expect(3.U)
c.io.in.poke(2.U)
c.clock.step(1)
c.io.max.expect(3.U)
c.io.in.poke(24.U)
c.clock.step(1)
c.io.max.expect(24.U)
}
println("SUCCESS!!")
}
}
从Tester的代码中可以看出来每次向register传递数值时都必须调用一次step。
2.4.1.2 Other Register Examples
在Chisel中,对寄存器的调用是在寄存器的输出中被执行的,可操作的种类取决于寄存器的type。可以写形如以下的寄存器:
val reg: UInt = Reg(UInt(4.W))
此代码意味着reg的类型为UInt,所以reg可以执行UInt可执行的所有操作。
Reg的类型包括但不仅限于UInt,如以下代码中使用了SInt类型。
import chisel3._
import chisel3.util._
class Comb extends Module {
val io = IO(new Bundle {
val in = Input(SInt(12.W))
val out = Output(SInt(12.W))
})
val delay: SInt = Reg(SInt(12.W))
delay := io.in
io.out := io.in - delay
}
object Comb extends App {
(new chisel3.stage.ChiselStage).emitVerilog(new Comb ())
println(getVerilogString (new Comb ))
}
2.4.2 Exercises
import chisel3._
import chisel3.util._
class MyShiftRegister(val init: Int = 1) extends Module {
val io = IO(new Bundle {
val in = Input(Bool())
val out = Output(UInt(4.W))
})
val state = RegInit(UInt(4.W), init.U)
val nextState = (state << 1) | io.in
state := nextState
io.out := state
}
object MyShiftRegister extends App {
(new chisel3.stage.ChiselStage).emitVerilog(new MyShiftRegister ())
println(getVerilogString (new MyShiftRegister ))
}
以上代码实现了4bit移位寄存器的效果,其中RegInit语句初始化了一个4位寄存器,通过:
val nextState = (state << 1) | io.in
来为实现移位的效果。因为移位寄存器上,实际上当有新的值来到时,那么之前的值就会被迫移位,通过io.in的true或者false来决定新输入值是1还是0,通过“|”这个按位或符号,来将值传递给末尾。然后将nextState的值更新到State上,最后将数据输出到io.out。
此代码的tester如下:
import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec
class shiftertest extends AnyFlatSpec with ChiselScalatestTester {
behavior of "shifter"
it should "pass" in {
test(new MyShiftRegister()) { c =>
var state = c.init
for (i <- 0 until 10) {
// poke in LSB of i (i % 2)
c.io.in.poke(((i % 2) != 0).B)
// update expected state
state = ((state * 2) + (i % 2)) & 0xf
c.clock.step(1)
c.io.out.expect(state.U)
}
}
println("SUCCESS!!")
}
}
此tester用奇偶数来决定输入值是1还是0, state = ((state * 2) + (i % 2)) & 0xf ,此句用来推算移位后的值,0xf等价于二进制的1111,即移位寄存器的最大值,通过此方法来将最大值锁定在1111,因为移位寄存器的最大值就是1111。
2.4.2.2 参数化移位寄存器
import chisel3._
import chisel3.util._
class MyOptionalShiftRegister(val n: Int, val init: BigInt = 1) extends Module {
val io = IO(new Bundle {
val en = Input(Bool())
val in = Input(Bool())
val out = Output(UInt(n.W))
})
val state = RegInit(init.U(n.W))
val nextState = (state << 1) | io.in
when (io.en) {
state := nextState
}
io.out := state
}
object MyOptionalShiftRegister extends App {
(new chisel3.stage.ChiselStage).emitVerilog(new MyOptionalShiftRegister (8,1))
println(getVerilogString (new MyOptionalShiftRegister(8,1) ))
}
参数化功能是chisel的强势功能,上方代码展示了如何使用参数化功能,最后的代码将chisel翻译成verilog,采用了8位寄存器并且初始化值为1。由于是串行移位寄存器,所以寄存器位宽会直接影响delay。io.en是enable功能,只有当io.en为true时才会启动移位功能。
其tester如下所示:
import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec
class parshiftertest extends AnyFlatSpec with ChiselScalatestTester {
behavior of "parshifter"
it should "pass" in {
for (i <- Seq(3, 4, 8, 24, 65)) {
println(s"Testing n=$i")
test(new MyOptionalShiftRegister(n = i)) { c =>
val inSeq = Seq(0, 1, 1, 1, 0, 1, 1, 0, 0, 1)
var state = c.init
var i = 0
c.io.en.poke(true.B)
while (i < 10 * c.n) {
// poke in repeated inSeq
val toPoke = inSeq(i % inSeq.length)
c.io.in.poke((toPoke != 0).B)
// update expected state
state = ((state * 2) + toPoke) & BigInt("1"*c.n, 2)
c.clock.step(1)
c.io.out.expect(state.U)
i += 1
}
}
}
println("SUCCESS!!")
}
}
Comments
Post a Comment