控制流
动机
本系列到目前为止,Chisel中的软硬件之间都有很强的对应关系。但引入控制流之后就不一样了,对软硬件的看法就应该有很大的分歧了。
本节会在生成器软件和硬件中都引入控制流。如果重新连接到一个Chisel连线会怎么样呢?如何让一个多路选择器有两个以上的输入呢?本节会给出这两个问题的答案。
最后连接语义
前面提到,Chisel通过运算符:=
来连接组件,由于各种原因,允许发射多条连接语句到相同的组件。
对于赋值之后重新赋值的多条语句,最后一条连接语句会生效:
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 LastConnect
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!
when
,elsewhen
和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
}
他们仨必须按照上面的顺序出现,后面的可以省略,可以出现任意多的elsewhen
子句。
任何为真的子句条件都会终止执行,子句执行的动作可以是复杂的语句块或者嵌套的when
。
与Scala中的if
不同的是,when
中的子句不会返回值。所以不能这么用:
val result = when(squareIt) { x * x}.otherwise {x}
解决方案会在后面的连线(Wire)小节提到。
下面是一个用when
构造Chisel条件语句的例子:
import chisel3._
import chisel3.util._
class Max3 extends Module {
val io = IO(new Bundle {
val in_a = Input(UInt(16.W))
val in_b = Input(UInt(16.W))
val in_c = Input(UInt(16.W))
val out = Output(UInt(16.W))
})
when(io.in_a >= io.in_b && io.in_a >= io.in_c) {
io.out := io.in_a
}.elsewhen(io.in_b >= io.in_c) {
io.out := io.in_b
}.otherwise {
io.out := io.in_c
}
}
object Max3 extends App {
println(getVerilogString(new Max3()))
}
输出为:
module Max3(
input clock,
input reset,
input [15:0] io_in_a,
input [15:0] io_in_b,
input [15:0] io_in_c,
output [15:0] io_out
);
wire [15:0] _GEN_0 = io_in_b >= io_in_c ? io_in_b : io_in_c; // @[MyModule.scala 14:34 15:12 17:12]
assign io_out = io_in_a >= io_in_b & io_in_a >= io_in_c ? io_in_a : _GEN_0; // @[MyModule.scala 12:50 13:12]
endmodule
测试部分:
import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec
class MyModuleTest extends AnyFlatSpec with ChiselScalatestTester {
behavior of "MyOperators"
it should "get right results" in {
test(new Max3) { c =>
// verify that the max of the three inputs is correct
c.io.in_a.poke(6.U)
c.io.in_b.poke(4.U)
c.io.in_c.poke(2.U)
c.io.out.expect(6.U) // input 1 should be biggest
c.io.in_b.poke(7.U)
c.io.out.expect(7.U) // now input 2 is
c.io.in_c.poke(11.U)
c.io.out.expect(11.U) // and now input 3
c.io.in_c.poke(3.U)
c.io.out.expect(7.U) // show that decreasing an input works as well
c.io.in_a.poke(9.U)
c.io.in_b.poke(9.U)
c.io.in_c.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!
}
}
测试通过。
Wire
结构
前面提到when
不能返回值,Chisel中的Wire
结构就是解决这个问题的方法之一。Wire
定义了一个电路组件,他可以出现在:=
操作符的左边或右边。
下面以一个四输入的使用Wire
的排序电路为例:
为了演示,首先设计一个小的组合排序器,接受四个数值输入得到四个数值输出,考虑下面的图:
其中红色的线表示左边的值比右边的小,直接复制值就行,黑色的线表示左边的值比右边的值大时交换两个的值。
图中展示了一系列以row
开头的格子,可以使用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
row11 := io.in1
}.otherwise {
row10 := io.in1
row11 := io.in0
}
when(io.in2 < io.in3 ) {
row12 := io.in2
row13 := io.in3
}.otherwise {
row12 := io.in3
row13 := io.in2
}
val row20 = Wire(UInt(16.W))
val row21 = Wire(UInt(16.W))
val row22 = Wire(UInt(16.W))
val row23 = Wire(UInt(16.W))
when(row10 < row13) {
row20 := row10
row23 := row13
}.otherwise {
row20 := row13
row23 := row10
}
when(row11 < row12) {
row21 := row11
row22 := row12
}.otherwise {
row21 := row12
row22 := row11
}
when(row20 < row21) {
io.out0 := row20
io.out1 := row21
}.otherwise {
io.out0 := row21
io.out1 := row20
}
when(row22 < row23) {
io.out2 := row22
io.out3 := row23
}.otherwise {
io.out2 := row23
io.out3 := row22
}
}
object Sort4 extends App {
println(getVerilogString(new Sort4()))
}
输出的Verilog代码如下:
module Sort4(
input clock,
input reset,
input [15:0] io_in0,
input [15:0] io_in1,
input [15:0] io_in2,
input [15:0] io_in3,
output [15:0] io_out0,
output [15:0] io_out1,
output [15:0] io_out2,
output [15:0] io_out3
);
wire [15:0] row10 = io_in0 < io_in1 ? io_in0 : io_in1; // @[MyModule.scala 21:25 22:11 25:11]
wire [15:0] row11 = io_in0 < io_in1 ? io_in1 : io_in0; // @[MyModule.scala 21:25 23:11 26:11]
wire [15:0] row12 = io_in2 < io_in3 ? io_in2 : io_in3; // @[MyModule.scala 29:26 30:11 33:11]
wire [15:0] row13 = io_in2 < io_in3 ? io_in3 : io_in2; // @[MyModule.scala 29:26 31:11 34:11]
wire [15:0] row20 = row10 < row13 ? row10 : row13; // @[MyModule.scala 42:23 43:11 46:11]
wire [15:0] row23 = row10 < row13 ? row13 : row10; // @[MyModule.scala 42:23 44:11 47:11]
wire [15:0] row21 = row11 < row12 ? row11 : row12; // @[MyModule.scala 50:23 51:11 54:11]
wire [15:0] row22 = row11 < row12 ? row12 : row11; // @[MyModule.scala 50:23 52:11 55:11]
assign io_out0 = row20 < row21 ? row20 : row21; // @[MyModule.scala 58:23 59:13 62:13]
assign io_out1 = row20 < row21 ? row21 : row20; // @[MyModule.scala 58:23 60:13 63:13]
assign io_out2 = row22 < row23 ? row22 : row23; // @[MyModule.scala 66:23 67:13 70:13]
assign io_out3 = row22 < row23 ? row23 : row22; // @[MyModule.scala 66:23 68:13 71:13]
endmodule
测试代码:
import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec
class MyModuleTest extends AnyFlatSpec with ChiselScalatestTester {
behavior of "MyOperators"
it should "get right results" 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中List
的特性(Scala Standard Library 2.12.13 - scala.collection.immutable.List (scala-lang.org)),构造各种输入的排列组合:
test(new Sort4) { c =>
List(1, 2, 3, 4).permutations.foreach {
case i0 :: i1 :: i2 :: i3 :: Nil => {
println(s"Sorting $i0 $i1 $i2 $i3")
c.io.in0.poke(i0.U)
c.io.in1.poke(i1.U)
c.io.in2.poke(i2.U)
c.io.in3.poke(i3.U)
c.io.out0.expect(1.U)
c.io.out1.expect(2.U)
c.io.out2.expect(3.U)
c.io.out3.expect(4.U)
}
case _ => println(s"Matching Error!")
}
}
println("SUCCESS!!")
这里的case
是Scala的经典用法。前面List(1, 2, 3, 4).permutations.foreach
得到的是各种排列的List
的迭代器,每个List
作为变量输入到后面的偏函数中。后面的case
用于匹配模式,i0 :: i1 :: i2 :: i3
是构造了一个模式,用于匹配列表中的四个值,功能相当于将列表中的值解包给四个变量。
感谢@浑水摸鱼大师 指出这里代码中的问题(见评论),目前已经修改:
博主,我是从头跟着你的帖子学习下来的,我在复现Sort4代码时,发现你简化版的Test里case i0 :: i1 :: i2 :: i3这行代码一直报错,应该修改为case i0 :: i1 :: i2 :: i3 :: Nil,意思是先创建一个空列表Nil,再逐步向列表头部添加元素i3、i2、i1、i0。
练习
多项式电路
x 2 − 2 x + 1 2 x 2 + 6 x + 3 4 x 2 − 10 x − 5 x^2 - 2x + 1\\ 2x^2 + 6x + 3\\ 4x^2 - 10x -5 x2−2x+12x2+6x+34x2−10x−5
构造一个电路用于计算上面的多项式,一个选择器输入用于决定计算哪个多项式。
提示:使用Wire
来存放
x
2
x^2
x2,这样就只需要计算一次了。
首先用测试驱动开发的思想用Scala写一个模型:
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 main(args: Array[String]): Unit = {
assert(poly0(0) == 1)
assert(poly1(0) == 3)
assert(poly2(0) == -5)
assert(poly0(1) == 0)
assert(poly1(1) == 11)
assert(poly2(1) == -11)
}
测试通过,现在把这三个包装为一个参数化的函数,像硬件一样。使用Scala中的if
语句,基于输入select
选择对应的多项式:
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)
}
}
def main(args: Array[String]): Unit = {
assert(poly(1, 0) == 3)
assert(poly(1, 1) == 11)
assert(poly(2, 1) == -11)
}
测试通过。
那我们现在按照原来的思路实现就行了:
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 out = 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.out := result
}
object Polynomial extends App {
println(getVerilogString(new Polynomial))
}
输出如下:
module Polynomial(
input clock,
input reset,
input [1:0] io_select,
input [31:0] io_x,
output [31:0] io_out
);
wire [63:0] _square_T = $signed(io_x) * $signed(io_x); // @[MyModule.scala 14:18]
wire [34:0] _result_T = 3'sh2 * $signed(io_x); // @[MyModule.scala 16:28]
wire [31:0] square = _square_T[31:0]; // @[MyModule.scala 12:20 14:10]
wire [34:0] _GEN_3 = {{3{square[31]}},square}; // @[MyModule.scala 16:22]
wire [34:0] _result_T_3 = $signed(_GEN_3) - $signed(_result_T); // @[MyModule.scala 16:22]
wire [34:0] _result_T_6 = $signed(_result_T_3) + 35'sh1; // @[MyModule.scala 16:35]
wire [34:0] _result_T_7 = 3'sh2 * $signed(square); // @[MyModule.scala 18:19]
wire [35:0] _result_T_8 = 4'sh6 * $signed(io_x); // @[MyModule.scala 18:34]
wire [35:0] _GEN_4 = {{1{_result_T_7[34]}},_result_T_7}; // @[MyModule.scala 18:28]
wire [35:0] _result_T_11 = $signed(_GEN_4) + $signed(_result_T_8); // @[MyModule.scala 18:28]
wire [35:0] _result_T_14 = $signed(_result_T_11) + 36'sh3; // @[MyModule.scala 18:41]
wire [35:0] _result_T_15 = 4'sh4 * $signed(square); // @[MyModule.scala 20:19]
wire [36:0] _result_T_16 = 5'sha * $signed(io_x); // @[MyModule.scala 20:35]
wire [36:0] _GEN_5 = {{1{_result_T_15[35]}},_result_T_15}; // @[MyModule.scala 20:28]
wire [36:0] _result_T_19 = $signed(_GEN_5) - $signed(_result_T_16); // @[MyModule.scala 20:28]
wire [36:0] _result_T_22 = $signed(_result_T_19) - 37'sh5; // @[MyModule.scala 20:42]
wire [36:0] _GEN_0 = io_select == 2'h1 ? $signed({{1{_result_T_14[35]}},_result_T_14}) : $signed(_result_T_22); // @[MyModule.scala 17:33 18:12 20:12]
wire [36:0] _GEN_1 = io_select == 2'h0 ? $signed({{2{_result_T_6[34]}},_result_T_6}) : $signed(_GEN_0); // @[MyModule.scala 15:27 16:12]
assign io_out = _GEN_1[31:0]; // @[MyModule.scala 11:20]
endmodule
测试代码如下:
import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec
class MyModuleTest extends AnyFlatSpec with ChiselScalatestTester {
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)
}
}
behavior of "MyOperators"
it should "get right results" in {
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.out.expect(poly(select, x).S)
}
}
}
println("SUCCESS!!")
}
}
测试通过。
有限状态机(FSM)
通过卡诺图来优化状态机逻辑太蛋疼了,可以用综合工具来优化,还会产生不直观、不可读的代码。而用Chisel的控制流和最后连接语义来编写是明智的选择。
一个硕士在学习生涯会经历四个状态:空闲(idel),写代码(coding),写论文(writing),毕业(graduate)。这几种状态的转换基于三种条件:咖啡,灵光乍现的idea,来自导师的pressure。一旦毕业了,就会回到空闲状态。
下面的FSM图展示了这些状态和它们之间的转换:
图中所有没标签的转换(即没有输入)都会返回到空闲状态而不是留在当前状态。输入的优先级是coffee > idea > pressure,所以当处于空闲状态的时候如果同时收到了咖啡和压力,就会转移到写代码状态。
下面是Scala的测试代码:
import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec
class MyModuleTest extends AnyFlatSpec with ChiselScalatestTester {
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
}
(0 until states.size).foreach{ state => assert(gradLife(state, false, false, false) == states("idle")) }
assert(gradLife(states("writing"), true, false, true) == states("writing"))
assert(gradLife(states("idle"), true, true, true) == states("coding"))
assert(gradLife(states("idle"), false, true, true) == states("idle"))
assert(gradLife(states("grad"), false, false, false) == states("idle"))
}
目前还没学习到时序逻辑,所以这里的当前状态就用一个到模块的输入来表示,下一个状态就用一个输出来表示,跟上面的gradLife
功能一致。现在用Chisel来实现。
Chisel提供了一个方便的状态机映射函数,叫做Enum
,为了使用这些状态,将他们当作UInt
字面值来使用。
注意,硬件上判断相等与否用的是三个等号===
!
实现如下:
import chisel3._
import chisel3.util._
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
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
}
}
}
object GradLife extends App {
println(getVerilogString(new GradLife))
}
输出的Verilog代码如下:
module GradLife(
input clock,
input reset,
input [1:0] io_state,
input io_coffee,
input io_idea,
input io_pressure,
output [1:0] io_nextState
);
wire [1:0] _GEN_0 = io_pressure ? 2'h2 : 2'h0; // @[MyModule.scala 15:16 21:29 22:20]
wire [1:0] _GEN_1 = io_idea ? 2'h0 : _GEN_0; // @[MyModule.scala 19:25 20:20]
wire [1:0] _GEN_2 = io_coffee ? 2'h1 : _GEN_1; // @[MyModule.scala 17:21 18:20]
wire [1:0] _GEN_3 = io_idea | io_pressure ? 2'h2 : 2'h0; // @[MyModule.scala 15:16 27:40 28:20]
wire [1:0] _GEN_4 = io_coffee ? 2'h1 : _GEN_3; // @[MyModule.scala 25:21 26:20]
wire [1:0] _GEN_5 = io_pressure ? 2'h3 : 2'h0; // @[MyModule.scala 15:16 33:29 34:20]
wire [1:0] _GEN_6 = io_coffee | io_idea ? 2'h2 : _GEN_5; // @[MyModule.scala 31:32 32:20]
wire [1:0] _GEN_7 = io_state == 2'h2 ? _GEN_6 : 2'h0; // @[MyModule.scala 15:16 30:36]
wire [1:0] _GEN_8 = io_state == 2'h1 ? _GEN_4 : _GEN_7; // @[MyModule.scala 24:35]
assign io_nextState = io_state == 2'h0 ? _GEN_2 : _GEN_8; // @[MyModule.scala 16:27]
endmodule
测试代码如下:
import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec
class MyModuleTest extends AnyFlatSpec with ChiselScalatestTester {
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
}
behavior of "MyOperators"
it should "get right results" in {
test(new GradLife) { c =>
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!!")
}
}
测试通过。