Engee documentation
Notebook

Counter

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

Let's start by analyzing the implemented model. This is a simple model made using basic blocks. It supports code generation, including a code generation template for a block of comparison operators. The generation template itself is presented below.

This code is a Chisel code generator designed to generate a block of a logical operator (for example, ==, !=, <, >). It uses built-in helper functions and labels (for example, //!, /*!) to generate the Verilog code.

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

/*! #----------------------------------------------------# */
/*! @Definitions
function show_chisel_type(x::typ) :: String
  if x.bits == 1 && 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 && !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 it is a 1-bit boolean value.
  • UInt(...) or SInt(...) if it is an integer type (without a fractional part).
  • FixedPoint(...) if there is a fractional part (fixed point).

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


2. Announcement of the output signal

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

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


3. Generating the block body (block @Step)

patch_op(op)
Converts operators to Chisel syntax:

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

get_idx(len, blen)
It is used to access indexes in vector (massive) logic.:

  • returns "" if is a 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 one — signed It may be required zext (zero expansion to a compatible format).

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

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

with processing zext, indexes and translation of operators.


This code implements a universal Chisel block generator for logical operations with support for vector signals, broadcasting, and type conversion. It is compiled into Chisel code for hardware description of blocks, for example, for FPGA or ASIC.

Next, let's look at 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. the limit of the maximum allowable value of the counter;
  3. Reset the counter.

Now that we've figured out the model, we'll generate its code and verify the resulting code based on the generated C function and based on the handwritten Verilog test bench, after adding a folder with compare.cgt en route Engee.

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 working with the code and the source model are identical, which indicates that the code generator is working correctly.
Now let's move on to writing a test binding 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

Module testbench It is intended for checking the operation of the counter. Below we list the actions that he performs.

  1. Initializes 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 signal after 10 clock cycles reset.
  4. Outputs values io_Step, io_Limit_max and io_Cnt to monitor the behavior of the counter.
  5. Completes the simulation after 200 clock cycles.

Now let's run our test using the JDoodle Verilog Online website. It is a convenient online platform for writing, compiling and running Verilog code directly in the 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 indicate that the code is working correctly.
And it is similar to the results obtained in the model. This means that the generated counter can be used in FPGA operation.