Detailed explanation of the use of port (IO) and Module in chisel

Posted by PoohStix on Sun, 23 Jan 2022 11:47:56 +0100

The main content is extracted from: https://blog.csdn.net/qq_34291505/article/details/87880730

1, Port (similar to input and output in verilog)

1. Define port list

Before defining a module, you must first define the port. The whole port list is defined by the method "io [T <: data] (iodef: T)". Generally, its parameter is an object of type Bundle, and the referenced field name must be "io". Because the port has a direction, the methods "input [T <: data] (source: T)" and "output [T <: data] (source: T)" are also required to indicate the specific direction for each port. Note that "input [T <: data] (source: T)" and "output [T <: data] (source: T)" only copy their parameters, so they cannot be data types wrapped by hardware types. At present, Chisel does not support two-way port inout, and can only simulate the two-way port of external Verilog through the Analog port in the black box.

Once the port list is defined, it can be used through "io.xxx". The input can drive other internal signals, and the output can be driven by other signals. The assignment operation can be carried out directly, and the Boolean port can also be directly used as an enable signal. The port does not need to be defined by other hardware types, but it should be noted that it still belongs to the wire network of combinational logic in nature. For example:

class MyIO extends Bundle {
   val in = Input(Vec(5, UInt(32.W)))
   val out = Output(UInt(32.W))
}

......
   val io = IO(new MyIO)  // Port list of modules
......
2. Flip the direction of the port list

For two connected modules, there may be a large number of ports with the same name but in the opposite direction. It is time-consuming and laborious to rewrite the port just to flip the direction. Therefore, Chisel provides the "flipped [T <: data] (source: T)" method, which can convert all inputs in the parameters to output and output to input. If it is the Analog port in the black box, it is still bidirectional. For example:

 class MyIO extends Bundle {
   val in = Input(Vec(5, UInt(32.W)))
   val out = Output(UInt(32.W))
}

......
   val io = IO(new MyIO)  // in is the input and out is the output
......
   val io = IO(Flipped(new MyIO))  // out is the input and in is the output
3. Integral connection

The port list in the flip direction is usually used with the overall connection symbol "< >". This operator will connect all ports with the same name in the port list on the left and right sides, and the port direction of the same level must be input connected to output and output connected to input, and the port direction of parent and child level must be input connected to input and output connected to output. Note that the directions must match according to this rule, and there cannot be different port names, quantities and types. This saves a lot of connection code. For example:

class MyIO extends Bundle {
   val in = Input(Vec(5, UInt(32.W)))
   val out = Output(UInt(32.W))
}

......
   val io = IO(new Bundle {
       val x = new MyIO       
       val y = Flipped(new MyIO)
   })

   io.x <> io.y  // Equivalent to io y.in := io. x.in;  io. x.out := io. y.out 
......
4. Dynamically modify ports

Chisel can create optional ports by introducing Scala's Boolean parameters, optional values and if statements. When instantiating the module, different ports can be generated by controlling the Boolean input parameters. For example:

class ModuleWithOptionalIOs(flag: Boolean) extends Module {
   val io = IO(new Bundle {
       val in = Input(UInt(12.W))
       val out = Output(UInt(12.W))
       val out2 = if (flag) Some(Output(UInt(12.W))) else None
  })
  
   io.out := io.in
   if(flag) {
     io.out2.get := io.in
   }
} 

Note that the port should be packaged as an optional value, so that the object None can be used instead when the port is not needed, and the compiled Verilog will not generate this port. When assigning a value to an optional port, you should first liberate the port with the get method of the optional value. The convenience of optional value syntax is also reflected here.

Note:

If the port is defined as a multi-dimensional vec, it will be expanded into ports with the same bit width and different names in the generated verilog code.

chisel Code:

package simple

import chisel3._
import chisel3.util._
import chisel3.Driver

class REG extends Module {
  val io = IO(new Bundle {
    val a = Input(Vec(3, UInt(8.W)))
    val en = Input(Bool())
    val c = Output(Vec(3, UInt(8.W)))
  })

  //val reg3 = Reg(Vec(3,Vec(480,Vec(480, UInt(8.W)))))
  val reg = Reg(Vec(3, UInt(8.W)))
  reg :=  io.a
  io.c := reg
} 

object REG extends App {
  (new chisel3.stage.ChiselStage).emitVerilog(new REG(), Array("--target-dir", "generated"))
}

Generated verilog code:

module REG(
  input        clock,
  input        reset,
  input  [7:0] io_a_0,
  input  [7:0] io_a_1,
  input  [7:0] io_a_2,
  input        io_en,
  output [7:0] io_c_0,
  output [7:0] io_c_1,
  output [7:0] io_c_2
);
`ifdef RANDOMIZE_REG_INIT
  reg [31:0] _RAND_0;
  reg [31:0] _RAND_1;
  reg [31:0] _RAND_2;
`endif // RANDOMIZE_REG_INIT
  reg [7:0] reg_0; // @[reg_test.scala 17:16]
  reg [7:0] reg_1; // @[reg_test.scala 17:16]
  reg [7:0] reg_2; // @[reg_test.scala 17:16]
  assign io_c_0 = reg_0; // @[reg_test.scala 19:8]
  assign io_c_1 = reg_1; // @[reg_test.scala 19:8]
  assign io_c_2 = reg_2; // @[reg_test.scala 19:8]
  always @(posedge clock) begin
    reg_0 <= io_a_0; // @[reg_test.scala 18:7]
    reg_1 <= io_a_1; // @[reg_test.scala 18:7]
    reg_2 <= io_a_2; // @[reg_test.scala 18:7]
  end
// Register and memory initialization
`ifdef RANDOMIZE_GARBAGE_ASSIGN
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_INVALID_ASSIGN
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_REG_INIT
`define RANDOMIZE
`endif
`ifdef RANDOMIZE_MEM_INIT
`define RANDOMIZE
`endif
`ifndef RANDOM
`define RANDOM $random
`endif
`ifdef RANDOMIZE_MEM_INIT
  integer initvar;
`endif
`ifndef SYNTHESIS
`ifdef FIRRTL_BEFORE_INITIAL
`FIRRTL_BEFORE_INITIAL
`endif
initial begin
  `ifdef RANDOMIZE
    `ifdef INIT_RANDOM
      `INIT_RANDOM
    `endif
    `ifndef VERILATOR
      `ifdef RANDOMIZE_DELAY
        #`RANDOMIZE_DELAY begin end
      `else
        #0.002 begin end
      `endif
    `endif
`ifdef RANDOMIZE_REG_INIT
  _RAND_0 = {1{`RANDOM}};
  reg_0 = _RAND_0[7:0];
  _RAND_1 = {1{`RANDOM}};
  reg_1 = _RAND_1[7:0];
  _RAND_2 = {1{`RANDOM}};
  reg_2 = _RAND_2[7:0];
`endif // RANDOMIZE_REG_INIT
  `endif // RANDOMIZE
end // initial
`ifdef FIRRTL_AFTER_INITIAL
`FIRRTL_AFTER_INITIAL
`endif
`endif // SYNTHESIS
endmodule

Not only that, you can also see that the chisel compiler will expand Vec[T] into one-dimensional data instead of converting it into multi-dimensional reg in verilog. This should be noted!!!

Two, module (similar to module, endmodule in verilog)

1. Definition module

In Chisel, a user-defined class is used to define modules. This class has the following three characteristics:

  • Inherited from Module class.
  • There is an abstract field "io" to be implemented, which must refer to the port object mentioned above.
  • Make internal circuit wiring in the main constructor of the class.

Because the contents of non fields and non methods belong to the main construction method, the assignment with the operator ": =, the connection with" < > "or some control structures belong to the main construction method. From the perspective of Scala, these codes represent how to construct an object when instantiating; From the Chisel level, they are declaring how to connect sub circuits and transmit signals within the module, similar to Verilog's assign and always statements. In fact, when these circuit connections represented by assignment are converted into Verilog, combinatorial logic is a large number of assign statements, and temporal logic is always statements.

It should also be noted that the module defined in this way will inherit a field "clock", the type is clock, which represents the global clock and is visible in the whole module. For combinational logic, it is not used, and although temporal logic needs this clock, it does not need to be explicitly declared.

There is also an inherited field "reset", the type is reset, which represents the global reset signal, which is visible in the whole module. For timing elements that need to be reset, this field can also be used without explicit.

If you really need to use the global clock and Reset, you can use them through their field names, but pay attention to whether the types match. You often need statements such as "reset.toBool" to convert the Reset type to Bool type for control. The implicit global clock and Reset port can only be seen when generating Verilog code.

To write a dual input multiplexer, the code is as follows:

// mux2.scala
package test
 
import chisel3._
 
class Mux2 extends Module {
  val io = IO(new Bundle{
    val sel = Input(UInt(1.W))
    val in0 = Input(UInt(1.W))
    val in1 = Input(UInt(1.W))
    val out = Output(UInt(1.W))
  })
 
  io.out := (io.sel & io.in1) | (~io.sel & io.in0)
}

Here, "new Bundle {...}" is written to declare an anonymous class to inherit from the Bundle, and then instantiate the anonymous class. For short and simple port list, you can use this simple writing method. For a large public interface, it should be written as a named Bundle subclass separately to facilitate modification. "io.out: =..." is actually a part of the main construction method, which realizes the logical behavior of the output port through the built-in operator and three input ports.

2. Instantiation module

To instantiate a Module, you do not need to directly generate an instance object with new. You also need to pass the instance object to the apply method of the singleton object Module. This awkward syntax is caused by the syntax limitations of Scala, just as the port needs to be written as "IO(new Bundle {...})", the unsigned number needs to be written as "UInt(n.W)", and so on. For example, the following code constructs a four input multiplexer by instantiating the two input multiplexer just now:

// mux4.scala
package test
 
import chisel3._
class Mux4 extends Module {
  val io = IO(new Bundle {
    val in0 = Input(UInt(1.W))
    val in1 = Input(UInt(1.W))
    val in2 = Input(UInt(1.W))
    val in3 = Input(UInt(1.W))
    val sel = Input(UInt(2.W))
    val out = Output(UInt(1.W))
  })
  val m0 = Module(new Mux2)
  m0.io.sel := io.sel(0)
  m0.io.in0 := io.in0
  m0.io.in1 := io.in1
  val m1 = Module(new Mux2)
  m1.io.sel := io.sel(0)
  m1.io.in0 := io.in2
  m1.io.in1 := io.in3
  val m2 = Module(new Mux2)
  m2.io.sel := io.sel(1)
  m2.io.in0 := m0.io.out
  m2.io.in1 := m1.io.out
  io.out := m2.io.out
}
3. Instantiate multiple modules

As in the previous example, module Mux2 is instantiated three times. In fact, only three modules need to be instantiated at one time. For repeated modules to be instantiated multiple times, the vector factory method vecinit [T <: Data] can be used. Because the parameter type received by this method is a subclass of Data, and the field io of the module is just the Bundle type, and the actual circuit connection only needs to be for the port of the module, the IO field of the module to be instantiated can be formed into a sequence or passed as a parameter in the form of repeated parameters. Sequences are usually used as parameters, which saves code. One way to generate a sequence is to call the method fill in the singleton object Seq. An overloaded version of the method has two single parameter lists. The first receives an Int type object, indicating the number of elements in the sequence, and the second is a named parameter, receiving elements in the sequence.

Because Vec is an indexable sequence, multiple modules instantiated in this way are similar to "module array", indexing the nth module with a subscript. In addition, because the element of Vec is already the port field io of the module, when you want to reference a specific port of the instantiated module, there is no need to appear "IO" in the path. For example:

// mux4_2.scala
package test
 
import chisel3._
class Mux4_2 extends Module {
  val io = IO(new Bundle {
    val in0 = Input(UInt(1.W))
    val in1 = Input(UInt(1.W))
    val in2 = Input(UInt(1.W))
    val in3 = Input(UInt(1.W))
    val sel = Input(UInt(2.W))
    val out = Output(UInt(1.W))
  })
  // Three Mux2 are instantiated, and the parameter is the port field io
  val m = VecInit(Seq.fill(3)(Module(new Mux2).io))  
  m(0).sel := io.sel(0)  // The port of the module is indexed by subscript, and there is no "io" in the path
  m(0).in0 := io.in0
  m(0).in1 := io.in1
  m(1).sel := io.sel(0)
  m(1).in0 := io.in2
  m(1).in1 := io.in3
  m(2).sel := io.sel(1)
  m(2).in0 := m(0).out
  m(2).in1 := m(1).out
  io.out := m(2).out
}
4. Dynamic naming module

Chisel can dynamically define the module name, that is, the module name when converted to Verilog does not use the defined class name, but uses the return string of the overridden desiredName method. Both modules and black boxes apply. For example:

class Coffee extends BlackBox {
   val io = IO(new Bundle {
       val I = Input(UInt(32.W))
       val O = Output(UInt(32.W))
   })
   override def desiredName = "Tea"
}

class Salt extends Module {
   val io = IO(new Bundle {})
   val drink = Module(new Coffee)
   override def desiredName = "SodiumMonochloride"
}

The corresponding Verilog is:

module SodiumMonochloride(
     input   clock,
     input   reset
);
     wire [31:0] drink_O;
     wire [31:0] drink_I;
     Tea drink (
         .O(drink_O),
         .I(drink_I)
     );
     assign drink_I = 32'h0;
endmodule 

Topics: chisel