Engee documentation
Notebook

Counter

In this example, we will develop and test a counter model that supports Verilog code generation and test its performance using a cloud compiler.

Let's start with analysing the implemented model. It is a simple model made with basic blocks. It supports code generation, including the code generation pattern for the comparison operators block. The generation template itself is shown below.

This code is a Chisel code generator designed to generate a logical operator block (e.g. ==, !=, <, >). It uses built-in auxiliary functions and labels (e.g. //!, /*!) to generate Verilog code.

//! BlockType = :RelationalOperator
//! TargetLang = :Chisel

/*! #----------------------------------------------------# */
/*! @Definitions
function show_chisel_type(x::typ) :: String
	if x.bits == 1 &amp;&amp; x.fractional_bits == 0
		out = "Bool()"
	elseif x.fractional_bits == 0
		out = (x.is_unsigned ? "U" : "S") * "Int($(x.bits).W)"
	else
		out = "FixedPoint($(x.bits).W,$(x.fractional_bits).BP)"
	end
	out
end
function show_chisel_type(x::signal) :: String
	base_type = show_chisel_type(x.ty)
	len = x.rows * x.cols
	len == 1 ? base_type : "Vec($(len), $(base_type))"
end
*/
val $(output(1)) = Wire($(show_chisel_type(output(1))))
/*! #----------------------------------------------------# */
/*! @Step
function patch_op(op)
   op == "==" ? "===" : op == "~=" ? "=/=" : op
end
function get_idx(len, blen)
   if len == 1
       return ""
   elseif len == blen
       return "(i)"
   else
       return "(i % $(len)))"
   end
end
function maybe_zext(sig, sig2)
   if sig.ty.is_unsigned &amp;&amp; !sig2.ty.is_unsigned
       return ".zext"
   end
   ""
end
function impl(sig1, sig2, op, out)
   dim0 = dim(out)
   dim1 = dim(sig1)
   dim2 = dim(sig2)
   blen = lcm(dim1, dim2) # broadcasted length
   if blen == 1
       loop_decl = ""
   else
       loop_decl = "for (i <- 0 until $(blen)) "
   end
   "$(loop_decl)$(output(1))$(get_idx(dim0, blen)) := \
      $(sig1)$(get_idx(dim1, blen))$(maybe_zext(sig1, sig2)) \
        $(patch_op(op)) \
      $(sig2)$(get_idx(dim2, blen))$(maybe_zext(sig2, sig1))"
end
*/
$(impl(input(1), input(2), param.Operator, output(1))))
/*! #----------------------------------------------------# */

The code is a step-by-step description of the block.


1. Definition of functions (block @Definitions)

show_chisel_type(x::typ)

Defines how to represent the type x in Chisel syntax:

  • Bool(), if a 1-bit logical value.
  • UInt(...) or SInt(...), if integer type (no fractional part).
  • FixedPoint(...), if fractional part (fixed point).

show_chisel_type(x::signal) If the signal is an array (Vec), it returns Vec(n, base_type), otherwise just base_type.


2. Output signal declaration

chisel
val $(output(1)) = Wire($(show_chisel_type(output(1))))

Creates an output signal as Wire(...), whose type is determined by the functions described above.


3. Generation of block body (block @Step)

patch_op(op) Converts statements to Chisel syntax:

  • "==""==="
  • "~=""=/=" (inequality)

get_idx(len, blen) Used to refer to indices in vector (array) logic:

  • returns "", if scalar.
  • (i), if the lengths match.
  • (i % len), if broadcasting is required.

maybe_zext(sig, sig2) If one of the signals is unsigned, and the other is signed, zext (extending with zeros to a compatible format) may be required.

impl(sig1, sig2, op, out) The main function of string generation:

  • determines the size of the signals and their broadcast length;
  • if necessary, wraps in for-cycle.
  • forms an assignment of the form
chisel
output(i) := input1(i) <op> input2(i)

with processing of zext, indices and operator translation.


This code implements a generic Chisel-block generator for logic operations with support for vector signals, broadcasting and type conversion. It is compiled into Chisel code for hardware block description, e.g. for FPGA or ASIC.

Next let's consider the implemented model itself and its generation settings. image.png

As we can see, the model is quite simple and has three input ports:

  1. the step with which the counter increment is performed;
  2. limit of the maximum permissible counter value;
  3. counter reset.

Now that we understand the model, let's generate its code and verify the resulting code based on the generated C function and on the handwritten testbench Verilog, having previously added the folder with compare.cgt to the Engee path.

image_3.png

In [ ]:
function run_model(name_model)
    Path = string(@__DIR__) * "/" * name_model * ".engee"
    
    if name_model in [m.name for m in engee.get_all_models()] # Проверка условия загрузки модели в ядро
        model = engee.open( name_model ) # Открыть модель
        model_output = engee.run( model, verbose=true ); # Запустить модель
    else
        model = engee.load( Path, force=true ) # Загрузить модель
        model_output = engee.run( model, verbose=true ); # Запустить модель
        engee.close( name_model, force=true ); # Закрыть модель
    end
    return model_output
end

run_model("Counter_C_test")
Cnt_ref = simout["Counter_C_test/Cnt_ref"].value;
Cnt_C = simout["Counter_C_test/Cnt_C"].value;
plot(Cnt_ref)
plot!(Cnt_C)
Building...
Progress 0%
Progress 100%
Progress 100%
Out[0]:

As we can see, the results of C code and the initial model are identical, which indicates that the code generator works correctly. Now let's move on to writing the test pipelining of our generated code.

module count_Count(
 input clock,
               reset,
 input [31:0] io_Step,
               io_Limit_max,
 input io_Reset,
 output [31:0] io_Cnt
);

 reg [31:0] UnitDelay_state;
 wire [31:0] Switch =
  $signed(UnitDelay_state) > $signed(io_Limit_max) | io_Reset ? 32'h0 : UnitDelay_state;
 always @(posedge clock) begin
   if (reset)
     UnitDelay_state <= 32'h0;
   else
     UnitDelay_state <= io_Step + Switch;
 end // always @(posedge)
 assign io_Cnt = Switch;
endmodule

module testbench;
 reg clock;
 reg reset;
 reg [31:0] io_Step;
 reg [31:0] io_Limit_max;
 reg io_Reset;
 wire [31:0] io_Cnt;
 
 // Instantiate the module under test
 count_Count dut (
   .clock(clock),
   .reset(reset),
   .io_Step(io_Step),
   .io_Limit_max(io_Limit_max),
   .io_Reset(io_Reset),
   .io_Cnt(io_Cnt)
 );
 
 // Clock generation
 always 5 clock = ~clock;
 
 // Test sequence
 initial begin
   // Initialize signals
   clock = 0;
   reset = 1;
   io_Step = 3;
   io_Limit_max = 27;
   io_Reset = 0;
   
   // Display header
  $display("tStep\tLimit\tCnt");
   $monitor(io_Step, io_Limit_max, io_Cnt);
   
   // Apply reset
   10 reset = 0;
   
   // Run simulation until counter wraps around
   200;
   $finish;
 end
endmodule

The testbench module is designed to test the operation of the counter. Below we list the actions it performs.

  1. initialises the signals:
    • clock = 0;
    • reset = 1;
    • io_Step = 3;
    • io_Limit_max = 27;
    • io_Reset = 0.
  2. Generates a clock signal with a period of 10 clock cycles.
  3. Deactivates the reset signal after 10 clock cycles.
  4. Outputs the values io_Step, io_Limit_max and io_Cnt to observe the counter behaviour.
  5. Ends the simulation after 200 clock cycles.

Now let's run our test using the JDoodle Verilog Online website. This is a convenient online platform for writing, compiling and running Verilog code right in your browser.

In [ ]:
# display(MIME("text/html"), 
#                 """<iframe 
#                 src="https://www.jdoodle.com/execute-verilog-online" 
#                 width="750" height="500" 
#                 style="border: none;">
#                 </iframe>""")

image.png

Conclusion

The simulation results show that the code works correctly. And it is similar to the results obtained in the simulation. This means that the generated counter can be used in FPGA operation.