Generating Verilog from Engee fixed-point models
Consider Verilog generation from four Engee fixed-point models. When calculating the area and perimeter, we will show the effect of bit depth, overflow, and register addition. Then we will check the generated modules using test benches.
Fixed-point arithmetic
Type sets the sign, bit depth, and number of fractional bits. The value is stored as . For a signed type, the range is
Let's compare the fixdt(1,16,8) and fixdt(1,6,2) formats. The models use Floor rounding, saturation is disabled: overflow leads to a cyclic transition of the value.
Description of models
The first three models calculate the midline, perimeter, and area of the trapezoid:
Set , , , , ; when checking the case, we also use .
- model_old — calculations in fixdt format(1,16,8).
- model_new is the same calculation in fixdt format(1,6,2).
- model_delay is the fixdt(1,16,8) and Unit Delay format at the square output.
- model_triangle — separate calculation by , .
The models use the Add, Gain, and Product libraries. TargetHardware = Verilog is selected for generation.
Preparation
The models and test benches are located in the sample folder. The generated code is saved to a temporary folder inside /user. We will prepare the launch and verification functions.
using Main: engee, plot, plot!, histogram, hline!, savefig, Animation, frame, gif
root = @__DIR__
generated = mktempdir("/user"; prefix=".verilog_demo_")
println("Generated files: ", generated)
function run_demo_model(root::String, name::String)
path = joinpath(root, name * ".engee")
@assert isfile(path) "Нет модели: $path"
model = engee.load(path; force=true)
try
return engee.run(model; verbose=false)
finally
engee.close(model; force=true)
end
end
function logged_values(result, signal)
return Float64.((collect(result[signal])).value)
end
for file in ("model_old.engee", "model_new.engee", "model_delay.engee",
"model_triangle.engee", "tb_trapezoid_delay.v", "tb_triangle.v")
@assert isfile(joinpath(root, file)) "Нет исходного файла: $file"
end
println("Initial models and testbench are found")
Let's check the availability of files for four models and two test benches.
Step 1. Bit depth and overflow
Let's run two trapezoid models. With sufficient bit depth, we expect a perimeter of 19 and an area of 24.
old_sim = run_demo_model(root, "model_old")
new_sim = run_demo_model(root, "model_new")
old_P = logged_values(old_sim, "P")
new_P = logged_values(new_sim, "P")
old_S = logged_values(old_sim, "S")
new_S = logged_values(new_sim, "S")
@assert !isempty(old_P) && !isempty(new_P)
@assert isapprox(old_P[end], 19.0; atol=1/256)
println("P: 16 бит = $(old_P[end]), 6 бит = $(new_P[end])")
println("S: 16 бит = $(old_S[end]), 6 бит = $(new_S[end])")
In the fixdt(1,16,8) format, we get P = 19 and S = 24. In the fixdt(1,6,2) format, the sum of bases 12 goes beyond the range and turns into -4. Therefore, m = -2, P = 3 and S = -8.
Let's generate a Verilog and check the port bit depth: 16 and 6 bits, respectively.
old_dir = joinpath(generated, "01_trapezoid_16bit")
new_dir = joinpath(generated, "02_trapezoid_6bit")
mkpath(old_dir)
mkpath(new_dir)
engee.generate_code(joinpath(root, "model_old.engee"), old_dir,
subsystem_name="trapeze_calculator")
engee.generate_code(joinpath(root, "model_new.engee"), new_dir,
subsystem_name="trapeze_calculator")
old_v = joinpath(old_dir, "model_old_trapeze_calculator.v")
new_v = joinpath(new_dir, "model_new_trapeze_calculator.v")
@assert isfile(old_v) && isfile(new_v)
@assert occursin(r"input\s+\[15:0\]", read(old_v, String))
@assert occursin(r"input\s+\[5:0\]", read(new_v, String))
println("RTL: 16-bit and 6-bit inputs are confirmed")
In the generated code, Gain is implemented by shifting, and Product is implemented by multiplying with the selection of the desired result digits. The sizes of the signals reflect the fixed point formats in the model.
Step 2. Register and clock check
Let's start the model with Unit Delay. In the first step, the area should be zero, then equal to the result of the previous step.
delay_sim = run_demo_model(root, "model_delay")
delay_P = logged_values(delay_sim, "P")
@assert length(delay_P) >= 5 && length(old_P) >= 5
@assert isapprox(delay_P[1], 0.0; atol=1/256)
@assert all(isapprox.(delay_P[2:5], old_P[1:4]; atol=1/256))
println("P without delay: ", old_P[1:5])
println("P with a delay: ", delay_P[1:5])
The simulation result confirms the area delay by one step.
We will generate a module with a register and perform a clock check in Icarus Verilog. The test bench checks the reset and the results for the two height values. iverilog and vvp are needed to run.
delay_dir = joinpath(generated, "03_trapezoid_delay")
mkpath(delay_dir)
engee.generate_code(joinpath(root, "model_delay.engee"), delay_dir,
subsystem_name="trapeze_calculator")
delay_v = joinpath(delay_dir, "model_delay_trapeze_calculator.v")
@assert isfile(delay_v)
delay_rtl = read(delay_v, String)
@assert occursin(r"\breg\b", delay_rtl)
@assert occursin("posedge clock", delay_rtl)
delay_tb = joinpath(root, "tb_trapezoid_delay.v")
delay_exe = joinpath(delay_dir, "sim_delay")
run(`iverilog -o $delay_exe $delay_tb $delay_v`)
delay_output = read(`vvp $delay_exe`, String)
@assert occursin("h=4: m=6.00 P=19.00 S=24.00", delay_output)
@assert occursin("h=5: m=6.00 P=19.00 S=30.00", delay_output)
println(delay_output)
The test bench confirms m = 6, P = 19, and squares 24 and 30. Unit Delay is converted to a register that is updated at the edge of the clock signal.
Step 3. Separate computing module
In a separate model, we calculate the area of the triangle at a = 8 and h = 5. The expected result is 20.
triangle_sim = run_demo_model(root, "model_triangle")
triangle_S = logged_values(triangle_sim, "S")
@assert !isempty(triangle_S)
println("The S values of the Engee model: ", triangle_S)
The model outputs S = 20.
Let's generate the S_triangle module and check three sets of input data. The expected areas are 10, 10.5 and 30.
triangle_dir = joinpath(generated, "04_triangle")
mkpath(triangle_dir)
engee.generate_code(joinpath(root, "model_triangle.engee"), triangle_dir,
subsystem_name="S_triangle")
triangle_v = joinpath(triangle_dir, "model_triangle_S_triangle.v")
@assert isfile(triangle_v)
triangle_rtl = read(triangle_v, String)
@assert occursin("module model_triangle_S_triangle", triangle_rtl)
triangle_tb = joinpath(root, "tb_triangle.v")
triangle_exe = joinpath(triangle_dir, "sim_triangle")
run(`iverilog -o $triangle_exe $triangle_tb $triangle_v`)
triangle_output = read(`vvp $triangle_exe`, String)
for expected in ("a=5, h=4: S=10.00", "a=7, h=3: S=10.50",
"a=10, h=6: S=30.00")
@assert occursin(expected, triangle_output) "Не совпал RTL-результат: $expected"
end
println(triangle_output)
All three checks match the expected values. The choice of product digits takes into account the fractional part of the format and the division by two.
We will close the models if they are still open.
for name in ("model_old", "model_new", "model_delay", "model_triangle")
if name in [m.name for m in engee.get_all_models()]
engee.close(name; force = true)
end
end
"Verilog demo passed; four models and two RTL testbenches verified"
Conclusion
The generated Verilog preserves the features of the model's calculations: bit depth, overflow, and one-clock delay. Checks in Icarus Verilog confirmed the expected results for the considered computing modules.