Chisel教程——03.Chisel中的组合逻辑(结尾附上Chisel3 Cheat Sheet)

Chisel组合逻辑

这一节将会介绍如何使用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的Int1转换成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!!")
  }
}

测试结果通过。

多路选择器(Mux)和拼接(Concatenation)

Chisel内置了多路选择运算符Mux和拼接运算符Cat

其中,Mux类似于传统的三元运算符,参数依次为(条件, 为真时的值, 为假时的值),建议用true.Bfalse.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

练习

实现乘加操作(MAC)

实现乘加操作,输入是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

测试通过。

仲裁器(Arbiter)

在这里插入图片描述

上面的仲裁器用于将FIFO中的数据仲裁到两个并行处理单元中,规则如下:

  1. 如果两个处理单元都空着,那就优先发送到PE0;
  2. 如果至少有一个可用,那仲裁器应该告诉FIFO已经准备好接受数据;
  3. 在断言数据有效之前,等待PE断言它已经准备好;
  4. 提示:可能需要二元运算符来实现;

模板和测试如下:

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!!")

观察一下:

  1. 有来自FIFO的两个输入,分别是FIFO有效和FIFO的数据,一个到FIFO的输出告知已经准备好;
  2. 到PE分别有两个输出,是数据和告知PE数据是否有效,一个PE的输入告知是否准备好;

那么我们的思路如下:

  1. 通过信号pe0_readype1_ready确定fifo_ready(是否有空闲的PE);
  2. 如果FIFO数据有效,PE0的如果准备好了就让PE0上(设置pe0_valid);
  3. 如果FIFO数据有效,且PE0没有准备好,PE1准备好了,那就让PE1上(设置pe1_valid);
  4. 同时提供数据,但只有设置了pe_valid的才会有用;

解答如下:

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

最后附上Chisel3的Cheat Sheet:

在这里插入图片描述
在这里插入图片描述

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值