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 && 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 a 1-bit logical value.UInt(...)
orSInt(...)
, 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.
As we can see, the model is quite simple and has three input ports:
- the step with which the counter increment is performed;
- limit of the maximum permissible counter value;
- 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.
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)
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.
- initialises the signals:
clock
= 0;reset
= 1;io_Step
= 3;io_Limit_max
= 27;io_Reset
= 0.
- Generates a clock signal with a period of 10 clock cycles.
- Deactivates the
reset
signal after 10 clock cycles. - Outputs the values
io_Step
,io_Limit_max
andio_Cnt
to observe the counter behaviour. - 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.
# display(MIME("text/html"),
# """<iframe
# src="https://www.jdoodle.com/execute-verilog-online"
# width="750" height="500"
# style="border: none;">
# </iframe>""")
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.