当前位置:网站首页>Chisel tutorial - 03 Combinatorial logic in chisel (chisel3 cheat sheet is attached at the end)

Chisel tutorial - 03 Combinatorial logic in chisel (chisel3 cheat sheet is attached at the end)

2022-07-07 23:41:00 github-3rr0r

Chisel Combinatorial logic

This section will introduce how to use Chisel Component to implement combinatorial logic .

This section will demonstrate three basic Chisel type (UInt, Unsigned integer ;SInt, Signed integers ;Bool, Boolean value ) How to connect and operate .

It's important to note that all Chisel Variables are declared as Scala Of val, Never use Scala Medium var To realize hardware construction , Because the structure itself will not change after definition , Only its value can be changed when running on hardware . Wires can be used for parameterized types .

Common operators

Add implementation

First construct a Module, I won't introduce this in detail :

import chisel3._

class MyModule extends Module {
    
  val io = IO(new Bundle {
    
    val in  = Input(UInt(4.W))
    val out = Output(UInt(4.W))
  })
}

Based on this , We can use various operators on data :

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()))
}

Output is as follows :

 Insert picture description here

You can see the first addition val two = 1 + 1 The result of printing is 2, And the second addition val utwo = 1.U + 1.U The result of printing is MyModule.utwo: OpResult[UInt<1>]. The reason is that the first is to combine two Scala Add whole numbers , The latter is to combine two Chisel Of UInt Add up , So it is regarded as a hardware node when printing , Output pointer and type name . Be careful ,1.U Yes, it will Scala Of Int1 convert to Chisel Of UInt Face value .

And although the content of the test has nothing to do with input and output , But also connect the output to , Otherwise, it will report a mistake :

 Insert picture description here

If the data types on both sides of the operator do not match, an error will also be reported , such as :

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
}

Will cause type mismatch errors :

 Insert picture description here

So clear the differences between different types when performing operations ,Scala It's a strongly typed language , Therefore, all transformations must be explicit .

reduce 、 ride 、 Division implementation

Look at other operators :

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()))
}

The output is :

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 The contents are as follows :

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

Test result passed .

Multiplexer (Mux) And splicing (Concatenation)

Chisel Built in multiple selection operator Mux And splicing operators Cat.

among ,Mux Similar to the traditional ternary operator , The parameters are ( Conditions , The value when it is true , The value when it is false ), Suggest using true.B and false.B To create Chisel Boolean values in .

Cat The two parameters of are high order (MSB) And low order (LSB), But only two parameters can be accepted , If you want to splice multiple values, you need to nest multiple Cat Or use more advanced features .

Usage examples are as follows :

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()))
}

Output is as follows :

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

Notice the generated Verilog There is no code at all mux perhaps concat Combinatorial logic implementation of , Instead, two constants are assigned .

This is because FIRRTL The circuit is simplified in the conversion process , Eliminated some obvious logic .

test :

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

The test passed .

About Chisel The list of operators can refer to Chisel cheatsheet, The complete list and implementation details can be referred to Chisel API.

practice

Realize multiplication and addition (MAC)

Realize multiplication and addition , Input is 4bit Unsigned integer A,B and C, Output is 8bit Unsigned integer (A * B) + C, And pass the test :

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

It's done directly , The answer is as follows :

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()))
}

Output is as follows :

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

The test passed .

Arbiter (Arbiter)

 Insert picture description here

The arbiter above is used to put FIFO The data in is arbitrated into two parallel processing units , The following rules :

  1. If both processing units are empty , Then send it to PE0;
  2. If at least one is available , The arbiter should tell FIFO Ready to accept data ;
  3. Before asserting that the data is valid , wait for PE Assert that it is ready ;
  4. Tips : You may need binary operators to implement ;

Templates and tests are as follows :

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))
  })

  /*  Fill in the corresponding code here  */
}

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

Observe :

  1. From FIFO Two inputs of , Namely FIFO Effective and FIFO The data of , One to FIFO The output of informs that it is ready ;
  2. To PE There are two outputs , It's data and information PE Is the data valid , One PE The input of tells whether it is ready ;

Then our ideas are as follows :

  1. Passing signal pe0_ready and pe1_ready determine fifo_ready( Are there any free PE);
  2. If FIFO The data is valid ,PE0 If you are ready, let PE0 On ( Set up pe0_valid);
  3. If FIFO The data is valid , And PE0 Not ready ,PE1 Be on it , Then let's PE1 On ( Set up pe1_valid);
  4. At the same time, provide data , But only set pe_valid Will be useful ;

The answer is as follows :

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()))
}

Output is as follows :

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

The test passed .

Parametric adder

This part of the exercise will reflect Chisel Powerful features —— Parameterization capability .

Here, it is required to construct a parameterized adder , It can be saturated when overflow occurs , You can also get stage results . For example, for 4bit The integer addition of ,15+15 You can get 15, You can also get 14, It depends on the parameters given .

The template is as follows :

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))
  })

  /*  Fill in the corresponding code here  */
}

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

Observe the template above , The parameter passed in is called saturate, The type is Scala Boolean in , instead of Chisel Boolean in . So what we want to create here is not a saturated and truncated hardware , It's a generator , Or generate a saturation adder , Or generate a truncated adder , This is determined at the time of compilation .

Then it should be noted that the input and output are 4bit Of UInt,Chisel There is built-in width reasoning , according to cheatsheet It says , The bit width of the conventional addition result is equal to the widest of the two inputs , In other words, the calculation of the project can only get one 4bit Connection of :

val sum = io.in_a + io.in_b

In order to check whether the results need to be saturated , You need to put the addition result into a 5bit In the connection of .

according to cheatsheet Description of , have access to +& The operator :

val sum = io.in_a +& io.in_b

Last , If you put one 4bit Of UInt Connect to a 5bit Of UInt, That will automatically truncate the most significant bit , Using this feature, we can easily get the truncation result for the unsaturated adder .

The answer is as follows :

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)))
}

Generated Verilog as follows :

// 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

The test passed .

Chisel3 Cheat Sheet

The last attached Chisel3 Of Cheat Sheet:

 Insert picture description here
 Insert picture description here

原网站

版权声明
本文为[github-3rr0r]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/02/202202130555354260.html