这一节将会介绍如何使用Chisel组件来实现组合逻辑。
本节将会演示三种基本的Chisel类型(UInt,无符号整数;SInt,有符号整数;Bool,布尔值)可以如何连接和操作。
需要注意的是所有的Chisel变量都声明为Scala的val
,绝对不要用Scala中的var
来实现硬件构造,因为构造本身在定义后就不会变化了,只有它的值可以在硬件上运行时变化。连线可以用于参数化的类型。
首先构造一个Module
,这个就不详细介绍了:
import chisel3._
class MyModule extends Module {
val io = IO(new Bundle {
val in = Input(UInt(4.W))
val out = Output(UInt(4.W))
})
}
基于这个,我们可以在数据上使用各种运算符:
import chisel3._
class MyModule extends Module {
val io = IO(new Bundle {
val in = Input(UInt(4.W))
val out = Output(UInt(4.W))
})
io.out := io.in
val two = 1 + 1
println(two)
val utwo = 1.U + 1.U
println(utwo)
}
object MyModule extends App {
println(getVerilogString(new MyModule()))
}
输出如下:
可以看到第一个加法val two = 1 + 1
打印的结果是2,而第二个加法val utwo = 1.U + 1.U
打印的结果是MyModule.utwo: OpResult[UInt<1>]
。原因在于第一个是将两个Scala整数相加,而后者是将两个Chisel的UInt
相加,所以打印时视为一个硬件节点,输出指针和类型名称。注意,1.U
是将Scala的Int
1转换成Chisel的UInt
字面值。
还有虽然测试的内容和输入输出无关,但也要把输出给连起来,不然会报错:
如果运算符两边的数据类型不匹配的话也会报错,比如:
class MyModule extends Module {
val io = IO(new Bundle {
val in = Input(UInt(4.W))
val out = Output(UInt(4.W))
})
val two = 1.U + 1
println(two)
io.out := io.in
}
会导致类型不匹配的错误:
所以执行操作的时候要清除不同类型的区别,Scala是一个强类型的语言,因此所有的转换都必须是显式的。
再看看其他操作符:
import chisel3._
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
}
object MyOperators extends App {
println(getVerilogString(new MyOperators()))
}
输出结果为:
module MyOperators(
input clock,
input reset,
input [3:0] io_in,
output [3:0] io_out_add,
output [3:0] io_out_sub,
output [3:0] io_out_mul
);
wire [1:0] _io_out_sub_T_1 = 2'h2 - 2'h1; // @[MyModule.scala 12:21]
wire [4:0] _io_out_mul_T = 3'h4 * 2'h2; // @[MyModule.scala 13:21]
assign io_out_add = 4'h5; // @[MyModule.scala 11:14]
assign io_out_sub = {{2'd0}, _io_out_sub_T_1}; // @[MyModule.scala 12:14]
assign io_out_mul = _io_out_mul_T[3:0]; // @[MyModule.scala 13:14]
endmodule
MyModuleTest.scala
内容如下:
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 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!!")
}
}
测试结果通过。
Chisel内置了多路选择运算符Mux
和拼接运算符Cat
。
其中,Mux
类似于传统的三元运算符,参数依次为(条件, 为真时的值, 为假时的值)
,建议用true.B
和false.B
来创建Chisel中的布尔值。
Cat
的两个参数依次为高位(MSB)和低位(LSB),但只能接受两个参数,如果要拼接多个值那就需要嵌套多个Cat
或使用更高级的特性。
用法示例如下:
import chisel3._
import chisel3.util._
class MyOperators 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)
}
object MyOperators extends App {
println(getVerilogString(new MyOperators()))
}
输出如下:
module MyOperators(
input clock,
input reset,
input [3:0] io_in,
output [3:0] io_out_mux,
output [3:0] io_out_cat
);
assign io_out_mux = 4'h3; // @[MyModule.scala 12:14]
assign io_out_cat = 4'h5; // @[MyModule.scala 13:14]
endmodule
注意到生成的Verilog代码压根儿就没有mux或者concat的组合逻辑实现,而是两个常数赋值。
这是因为FIRRTL转换过程中简化了电路,消除了一些显然的逻辑。
测试:
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 MyOperators) {c =>
c.io.out_mux.expect(3.U)
c.io.out_cat.expect(5.U)
}
println("SUCCESS!!")
}
}
测试通过。
关于Chisel操作符的列表可以参照Chisel cheatsheet,完整的列表和实现细节可以参照Chisel API。
实现乘加操作,输入是4bit无符号整数A,B和C,输出为8bit无符号整数(A * B) + C
,并通过测试:
test(new MAC) { c =>
val cycles = 100
import scala.util.Random
for (i <- 0 until cycles) {
val in_a = Random.nextInt(16)
val in_b = Random.nextInt(16)
val in_c = Random.nextInt(16)
c.io.in_a.poke(in_a.U)
c.io.in_b.poke(in_b.U)
c.io.in_c.poke(in_c.U)
c.io.out.expect((in_a * in_b + in_c).U)
}
}
println("SUCCESS!!")
直接算就完事了,解答如下:
import chisel3._
import chisel3.util._
class MAC extends Module {
val io = IO(new Bundle {
val in_a = Input(UInt(4.W))
val in_b = Input(UInt(4.W))
val in_c = Input(UInt(4.W))
val out = Output(UInt(8.W))
})
io.out := (io.in_a * io.in_b) + io.in_c
}
object MyOperators extends App {
println(getVerilogString(new MAC()))
}
输出如下:
module MAC(
input clock,
input reset,
input [3:0] io_in_a,
input [3:0] io_in_b,
input [3:0] io_in_c,
output [7:0] io_out
);
wire [7:0] _io_out_T = io_in_a * io_in_b; // @[MyModule.scala 12:22]
wire [7:0] _GEN_0 = {{4'd0}, io_in_c}; // @[MyModule.scala 12:33]
assign io_out = _io_out_T + _GEN_0; // @[MyModule.scala 12:33]
endmodule
测试通过。
上面的仲裁器用于将FIFO中的数据仲裁到两个并行处理单元中,规则如下:
模板和测试如下:
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))
})
/*
在这里填上相应的代码
*/
}
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!!")
观察一下:
那么我们的思路如下:
pe0_ready
和pe1_ready
确定fifo_ready
(是否有空闲的PE);解答如下:
import chisel3._
import chisel3.util._
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
}
object Arbiter extends App {
println(getVerilogString(new Arbiter()))
}
输出如下:
module Arbiter(
input clock,
input reset,
input io_fifo_valid,
output io_fifo_ready,
input [15:0] io_fifo_data,
output io_pe0_valid,
input io_pe0_ready,
output [15:0] io_pe0_data,
output io_pe1_valid,
input io_pe1_ready,
output [15:0] io_pe1_data
);
assign io_fifo_ready = io_pe0_ready | io_pe1_ready; // @[MyModule.scala 22:33]
assign io_pe0_valid = io_fifo_valid & io_pe0_ready; // @[MyModule.scala 23:33]
assign io_pe0_data = io_fifo_data; // @[MyModule.scala 25:15]
assign io_pe1_valid = io_fifo_valid & io_pe1_ready & ~io_pe0_ready; // @[MyModule.scala 24:48]
assign io_pe1_data = io_fifo_data; // @[MyModule.scala 26:15]
endmodule
测试通过。
这一部分的练习会体现出Chisel的强大特性——参数化的能力。
这里要求构造一个参数化的加法器,能够在发生溢出时饱和,也能够得到阶段结果。比如对于4bit的整数加法,15+15既可以得到15,也可以得到14,就看给的参数是啥。
模板如下:
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))
})
/*
在这里填上相应的代码
*/
}
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)
}
}
// ensure we test saturation vs. truncation
c.io.in_a.poke(15.U)
c.io.in_b.poke(15.U)
if (saturate) {
c.io.out.expect(15.U)
} else {
c.io.out.expect(14.U)
}
}
}
println("SUCCESS!!")
观察上面的模板,传递进来的参数叫做saturate
,类型为Scala中的布尔型,而不是Chisel中的布尔型。所以这里要创建的不是一个又能饱和又能截断的硬件,而是一个生成器,要么生成一个饱和加法器,要么生成一个截断加法器,这是在编译的时候就已经确定了的。
然后需要注意的是输入输出都是4bit的UInt
,Chisel有内置的宽度推理,根据cheatsheet里面说的,常规加法结果的位宽等于两个输入最宽的那个,也就是说项目的计算只能得到一个4bit的连线:
val sum = io.in_a + io.in_b
为了检查结果是否需要饱和,需要把加法结果放入一个5bit的连线中。
根据cheatsheet的描述,可以使用+&
操作符:
val sum = io.in_a +& io.in_b
最后,如果把一个4bit的UInt
连线到一个5bit的UInt
,那就会自动截断最高有效位,利用这个特性就可以很容易地为非饱和加法器得到截断结果。
解答如下:
import chisel3._
import chisel3.util._
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
}
}
object ParameterizedAdder extends App {
println(getVerilogString(new ParameterizedAdder(true)))
println(getVerilogString(new ParameterizedAdder(false)))
}
生成的Verilog如下:
// saturation
module ParameterizedAdder(
input clock,
input reset,
input [3:0] io_in_a,
input [3:0] io_in_b,
output [3:0] io_out
);
assign io_out = 4'hf; // @[MyModule.scala 13:12]
endmodule
// truncation
module ParameterizedAdder(
input clock,
input reset,
input [3:0] io_in_a,
input [3:0] io_in_b,
output [3:0] io_out
);
wire [4:0] sum = io_in_a + io_in_b; // @[MyModule.scala 11:21]
assign io_out = sum[3:0]; // @[MyModule.scala 15:12]
endmodule
测试通过。
最后附上Chisel3的Cheat Sheet: