Understand chisel language. 33.Chisel advanced hardware generator - chisel combinational logic circuit generation: Taking BCD coding table as an example

Chisel advanced hardware generator (2) -- chisel combinational logic circuit generation: Taking BCD coding table as an example

In the last article, we learned the use of two types of variables in Chisel, and then introduced four parameterization methods in Chisel, which is of great significance for us to build reusable modules. The key to this part is to use Chisel as a language to write hardware generators, not just as a language to describe hardware. This parameterization is the beginning of hardware generators. This article will introduce the generation of combinational logic circuit in Chisel with the example of logic table, as an example of writing hardware generator with Chisel.

Generating combinatorial logic with logic table

In Chisel, we can create a logic table to generate combinational logic circuits, using the Vec of Chisel converted from Scala's Array. We may have a file with a logic table in it, and then we can read the logic table file in the hardware generation. The following example demonstrates how to use the Source class in scala standard io library to read data.txt. The data.txt file contains integer constants represented by text:

import chisel3._
import scala.io.Source

class FileReader extends Module {
    val io = IO(new Bundle {
        val address = Input(UInt(8.W))
        val data = Output(UInt(8.W))
    })
    
    val array = new Array[Int](256)
    var idx = 0
    
    // Read file to Scala array
    val source = Source.fromfile("data.txt")
    for (line <- source.getLines()) {
        array(idx) = line.toInt
        idx += 1
    }
    
    // Convert Scala integer array to Vec of Chisel UInt
    val table = VecInit(array.map(_.U(8.W)))
    
    // Use truth table
    io.data := table(io.address)
}

The above code looks very easy to understand as a whole, but there is a really weird sentence:

val table = VecInit(array.map(_.U(8.W)))

Array in scala can be implicitly converted into a sequence (Seq), which supports the map function. The map function will call a function on each element of the sequence, and then return the sequence composed of the return values of these function calls. Then look, our function is _ U(8.W), at first glance, can this also be a function? This function means:_ Represent each Int value from the scala array, and then based on this_ Call the conversion function from Scala integer to UInt literal of Chisel, with a width of 8 bits. Finally, based on the generated sequence, use the Chisel object VecInit to create a Vec in Chisel.

We can use the initialization of Vec with Chisel from Scala sequence to represent a message, and then send it to the serial port. The following code puts a standard "Hello World!" From Scala's String to Chisel's Vec:

val msg = "Hello World!"
val text = VecInit(msg.map(_.U))
val len = msg.length.U

String in scala can also be used as a sequence, so the map function can execute Chisel on each Scala character Char U conversion. This string of code is extracted from the example of serial port used to send welcome information. A special article will talk about the implementation of this serial port later.

We can use all the capabilities of scala to generate combinatorial logic (or logic tables), such as generating a table of trigonometric functions represented by fixed-point numbers and constants for calculating the constants of digital filters, or writing a small Scala assembler to generate the code of microprocessors written with Chisel. All these functions are in the same code base (the same language) and can be executed during hardware generation.

Example of BCD logic table

A classic example of combinatorial logic is the conversion of binary numbers to BCD (binary coded decimal). BCD is used to represent each bit of a decimal number with a 4-bit binary number. For example, decimal number 13 is represented as 1101 in binary, while 1 and 3 are encoded respectively in BCD, that is, 00010011. BCD encoding allows numbers to be represented in decimal, which is more user-friendly than hexadecimal.

If it is a traditional hardware description language, such as Verilog and VHDL, we need to use another scripting language or programming language to generate similar logic tables. For example, we can use a java program to calculate the logic table from binary to BCD, and then the Java program prints out the Verilog code that can be used for the project. In this way, it takes about 100 lines of Java code, most of which are used to generate Verilog strings, and the key part is only two lines.

But with Chisel, the situation is completely different. We can directly take the generation of logic tables as a part of hardware generation. The following code uses Chisel to generate the logic table of binary (0 to 99) to BCD conversion:

import chisel3._

class BcdTable extends Module {
    val io = IO(new Bundle {
        val address = Input(UInt(8.W))
        val data = Output(UInt(8.W))
    })
    
    val table = Wire(Vec(100, UInt(8.W)))
    
    // Binary to BCD
    for (i <- 0 until 100) {
        table(i) := (((i/10)<<4) + i%10).U
    }
    
    io.data := table(io.address)
}

The generated Verilog code is as follows:

module BcdTable(
  input        clock,
  input        reset,
  input  [7:0] io_address,
  output [7:0] io_data
);
  wire [7:0] _GEN_1 = 7'h1 == io_address[6:0] ? 8'h1 : 8'h0; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_2 = 7'h2 == io_address[6:0] ? 8'h2 : _GEN_1; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_3 = 7'h3 == io_address[6:0] ? 8'h3 : _GEN_2; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_4 = 7'h4 == io_address[6:0] ? 8'h4 : _GEN_3; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_5 = 7'h5 == io_address[6:0] ? 8'h5 : _GEN_4; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_6 = 7'h6 == io_address[6:0] ? 8'h6 : _GEN_5; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_7 = 7'h7 == io_address[6:0] ? 8'h7 : _GEN_6; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_8 = 7'h8 == io_address[6:0] ? 8'h8 : _GEN_7; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_9 = 7'h9 == io_address[6:0] ? 8'h9 : _GEN_8; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_10 = 7'ha == io_address[6:0] ? 8'h10 : _GEN_9; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_11 = 7'hb == io_address[6:0] ? 8'h11 : _GEN_10; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_12 = 7'hc == io_address[6:0] ? 8'h12 : _GEN_11; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_13 = 7'hd == io_address[6:0] ? 8'h13 : _GEN_12; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_14 = 7'he == io_address[6:0] ? 8'h14 : _GEN_13; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_15 = 7'hf == io_address[6:0] ? 8'h15 : _GEN_14; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_16 = 7'h10 == io_address[6:0] ? 8'h16 : _GEN_15; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_17 = 7'h11 == io_address[6:0] ? 8'h17 : _GEN_16; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_18 = 7'h12 == io_address[6:0] ? 8'h18 : _GEN_17; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_19 = 7'h13 == io_address[6:0] ? 8'h19 : _GEN_18; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_20 = 7'h14 == io_address[6:0] ? 8'h20 : _GEN_19; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_21 = 7'h15 == io_address[6:0] ? 8'h21 : _GEN_20; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_22 = 7'h16 == io_address[6:0] ? 8'h22 : _GEN_21; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_23 = 7'h17 == io_address[6:0] ? 8'h23 : _GEN_22; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_24 = 7'h18 == io_address[6:0] ? 8'h24 : _GEN_23; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_25 = 7'h19 == io_address[6:0] ? 8'h25 : _GEN_24; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_26 = 7'h1a == io_address[6:0] ? 8'h26 : _GEN_25; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_27 = 7'h1b == io_address[6:0] ? 8'h27 : _GEN_26; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_28 = 7'h1c == io_address[6:0] ? 8'h28 : _GEN_27; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_29 = 7'h1d == io_address[6:0] ? 8'h29 : _GEN_28; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_30 = 7'h1e == io_address[6:0] ? 8'h30 : _GEN_29; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_31 = 7'h1f == io_address[6:0] ? 8'h31 : _GEN_30; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_32 = 7'h20 == io_address[6:0] ? 8'h32 : _GEN_31; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_33 = 7'h21 == io_address[6:0] ? 8'h33 : _GEN_32; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_34 = 7'h22 == io_address[6:0] ? 8'h34 : _GEN_33; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_35 = 7'h23 == io_address[6:0] ? 8'h35 : _GEN_34; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_36 = 7'h24 == io_address[6:0] ? 8'h36 : _GEN_35; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_37 = 7'h25 == io_address[6:0] ? 8'h37 : _GEN_36; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_38 = 7'h26 == io_address[6:0] ? 8'h38 : _GEN_37; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_39 = 7'h27 == io_address[6:0] ? 8'h39 : _GEN_38; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_40 = 7'h28 == io_address[6:0] ? 8'h40 : _GEN_39; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_41 = 7'h29 == io_address[6:0] ? 8'h41 : _GEN_40; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_42 = 7'h2a == io_address[6:0] ? 8'h42 : _GEN_41; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_43 = 7'h2b == io_address[6:0] ? 8'h43 : _GEN_42; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_44 = 7'h2c == io_address[6:0] ? 8'h44 : _GEN_43; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_45 = 7'h2d == io_address[6:0] ? 8'h45 : _GEN_44; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_46 = 7'h2e == io_address[6:0] ? 8'h46 : _GEN_45; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_47 = 7'h2f == io_address[6:0] ? 8'h47 : _GEN_46; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_48 = 7'h30 == io_address[6:0] ? 8'h48 : _GEN_47; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_49 = 7'h31 == io_address[6:0] ? 8'h49 : _GEN_48; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_50 = 7'h32 == io_address[6:0] ? 8'h50 : _GEN_49; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_51 = 7'h33 == io_address[6:0] ? 8'h51 : _GEN_50; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_52 = 7'h34 == io_address[6:0] ? 8'h52 : _GEN_51; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_53 = 7'h35 == io_address[6:0] ? 8'h53 : _GEN_52; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_54 = 7'h36 == io_address[6:0] ? 8'h54 : _GEN_53; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_55 = 7'h37 == io_address[6:0] ? 8'h55 : _GEN_54; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_56 = 7'h38 == io_address[6:0] ? 8'h56 : _GEN_55; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_57 = 7'h39 == io_address[6:0] ? 8'h57 : _GEN_56; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_58 = 7'h3a == io_address[6:0] ? 8'h58 : _GEN_57; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_59 = 7'h3b == io_address[6:0] ? 8'h59 : _GEN_58; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_60 = 7'h3c == io_address[6:0] ? 8'h60 : _GEN_59; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_61 = 7'h3d == io_address[6:0] ? 8'h61 : _GEN_60; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_62 = 7'h3e == io_address[6:0] ? 8'h62 : _GEN_61; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_63 = 7'h3f == io_address[6:0] ? 8'h63 : _GEN_62; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_64 = 7'h40 == io_address[6:0] ? 8'h64 : _GEN_63; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_65 = 7'h41 == io_address[6:0] ? 8'h65 : _GEN_64; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_66 = 7'h42 == io_address[6:0] ? 8'h66 : _GEN_65; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_67 = 7'h43 == io_address[6:0] ? 8'h67 : _GEN_66; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_68 = 7'h44 == io_address[6:0] ? 8'h68 : _GEN_67; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_69 = 7'h45 == io_address[6:0] ? 8'h69 : _GEN_68; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_70 = 7'h46 == io_address[6:0] ? 8'h70 : _GEN_69; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_71 = 7'h47 == io_address[6:0] ? 8'h71 : _GEN_70; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_72 = 7'h48 == io_address[6:0] ? 8'h72 : _GEN_71; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_73 = 7'h49 == io_address[6:0] ? 8'h73 : _GEN_72; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_74 = 7'h4a == io_address[6:0] ? 8'h74 : _GEN_73; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_75 = 7'h4b == io_address[6:0] ? 8'h75 : _GEN_74; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_76 = 7'h4c == io_address[6:0] ? 8'h76 : _GEN_75; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_77 = 7'h4d == io_address[6:0] ? 8'h77 : _GEN_76; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_78 = 7'h4e == io_address[6:0] ? 8'h78 : _GEN_77; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_79 = 7'h4f == io_address[6:0] ? 8'h79 : _GEN_78; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_80 = 7'h50 == io_address[6:0] ? 8'h80 : _GEN_79; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_81 = 7'h51 == io_address[6:0] ? 8'h81 : _GEN_80; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_82 = 7'h52 == io_address[6:0] ? 8'h82 : _GEN_81; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_83 = 7'h53 == io_address[6:0] ? 8'h83 : _GEN_82; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_84 = 7'h54 == io_address[6:0] ? 8'h84 : _GEN_83; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_85 = 7'h55 == io_address[6:0] ? 8'h85 : _GEN_84; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_86 = 7'h56 == io_address[6:0] ? 8'h86 : _GEN_85; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_87 = 7'h57 == io_address[6:0] ? 8'h87 : _GEN_86; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_88 = 7'h58 == io_address[6:0] ? 8'h88 : _GEN_87; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_89 = 7'h59 == io_address[6:0] ? 8'h89 : _GEN_88; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_90 = 7'h5a == io_address[6:0] ? 8'h90 : _GEN_89; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_91 = 7'h5b == io_address[6:0] ? 8'h91 : _GEN_90; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_92 = 7'h5c == io_address[6:0] ? 8'h92 : _GEN_91; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_93 = 7'h5d == io_address[6:0] ? 8'h93 : _GEN_92; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_94 = 7'h5e == io_address[6:0] ? 8'h94 : _GEN_93; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_95 = 7'h5f == io_address[6:0] ? 8'h95 : _GEN_94; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_96 = 7'h60 == io_address[6:0] ? 8'h96 : _GEN_95; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_97 = 7'h61 == io_address[6:0] ? 8'h97 : _GEN_96; // @[hello.scala 16:{13,13}]
  wire [7:0] _GEN_98 = 7'h62 == io_address[6:0] ? 8'h98 : _GEN_97; // @[hello.scala 16:{13,13}]
  assign io_data = 7'h63 == io_address[6:0] ? 8'h99 : _GEN_98; // @[hello.scala 16:{13,13}]
endmodule

Do you think it's easy?

epilogue

This article takes BCD as an example to explain how to use Scala's features to generate logic tables and use them in Chisel hardware generation. We use file reading operations and for loops in Scala, as well as map functions commonly used in scala sequences. We can generate Chisel code based on logical tables in a programming environment, as can complex logical tables. This article only uses the basic functions of scala. In the next two articles, we will use the features of scala object-oriented programming and functional programming to further understand how to use powerful Scala to generate Chisel hardware.

Posted by xpherism on Sun, 07 Aug 2022 02:31:27 +0930