Engee documentation
Notebook

QPSK+FIR Verilog

This example shows a model of a QPSK modulator combined with an FIR filter. The purpose of the example is to demonstrate the capabilities of the code generator, as well as to verify this code using the Verilog simulator embedded in the Engee environment.

The model has a modular structure and consists of three basic blocks:

image.png
  1. Gen_data - simply generates combinations of bits from [0,0] to [1,1]

  2. QPSK_modulator - converts a sequence of bits into complex symbols (carrier phase shifts are not taken into account in this implementation).

image.png
  1. FIR is a digital filter with finite impulse response that calculates each output value as a weighted sum of the last 11 input samples.
image.png

Now let's move on to launching the model and code generation.

In [ ]:
function run_model( name_model)
    Path = (@__DIR__) * "/" * name_model * ".engee"
    if name_model in [m.name for m in engee.get_all_models()] # Checking the condition for loading a model into the kernel
        model = engee.open( name_model ) # Open the model
        model_output = engee.run( model, verbose=true ); # Launch the model
    else
        model = engee.load( Path, force=true ) # Upload a model
        model_output = engee.run( model, verbose=true ); # Launch the model
        engee.close( name_model, force=true ); # Close the model
    end
    sleep(0.1)
    return model_output
end
run_model("QPSK+FIR_verilog") # Launching the model.
Building...
Progress 0%
Progress 80%
Progress 100%
Progress 100%
Out[0]:
SimulationResult(
    "FIR.im" => WorkspaceArray{Fixed{1, 8, 7, Int8}}("QPSK+FIR_verilog/FIR.im")
,
    "FIR.re" => WorkspaceArray{Fixed{1, 8, 7, Int8}}("QPSK+FIR_verilog/FIR.re")
,
    "FIR.valid" => WorkspaceArray{Bool}("QPSK+FIR_verilog/FIR.valid")

)
In [ ]:
Re = collect(simout["QPSK+FIR_verilog/FIR.re"]).value
Im = collect(simout["QPSK+FIR_verilog/FIR.im"]).value
plot(Re, label="Re")
plot!(Im, label="Im")
Out[0]:
In [ ]:
collect(simout["QPSK+FIR_verilog/FIR.re"]).value
Out[0]:
301-element Vector{Fixed{1, 8, 7, Int8}}:
 fi(-0.0078125, 1, 8, 7)
 fi(-0.0078125, 1, 8, 7)
 fi(-0.0234375, 1, 8, 7)
 fi(-0.015625, 1, 8, 7)
 fi(-0.03125, 1, 8, 7)
 fi(0.25, 1, 8, 7)
 fi(0.234375, 1, 8, 7)
 fi(0.2421875, 1, 8, 7)
 fi(0.2265625, 1, 8, 7)
 fi(0.2265625, 1, 8, 7)
 fi(0.21875, 1, 8, 7)
 fi(0.21875, 1, 8, 7)
 fi(0.21875, 1, 8, 7)
 ⋮
 fi(0.2265625, 1, 8, 7)
 fi(0.21875, 1, 8, 7)
 fi(0.21875, 1, 8, 7)
 fi(0.21875, 1, 8, 7)
 fi(0.21875, 1, 8, 7)
 fi(0.21875, 1, 8, 7)
 fi(0.21875, 1, 8, 7)
 fi(0.21875, 1, 8, 7)
 fi(0.21875, 1, 8, 7)
 fi(0.21875, 1, 8, 7)
 fi(0.21875, 1, 8, 7)
 fi(0.2265625, 1, 8, 7)

The graphs will be useful for us to compare with the work of the Verilog code.
Now we will generate the code from the model blocks and describe the test module in the same way as the inputs to the model.

In [ ]:
engee.generate_code(
"$(@__DIR__)/test_codgen_ic.engee",
"$(@__DIR__)/prj",
subsystem_name="QPSK_modulator"
)
engee.generate_code(
"$(@__DIR__)/test_codgen_ic.engee",
"$(@__DIR__)/prj",
subsystem_name="fir-1"
)

The tb module is a testbench that verifies the operation of the QPSK modulator and FIR filter modules by feeding test data and recording the results.

The test bench supplies a cyclic sequence of bits to the input [11, 00, 10, 01], writes the output values of the modulator (QPSK_Re, QPSK_Im) and filter (FIR_Re, FIR_Im) to a file output_data.txt and it visualizes them in the console, and also generates the waveforms test file_codgen_ic.vcd for time chart analysis.

In [ ]:
filename = "$(@__DIR__)/prj/tb.v"
try
    if !isfile(filename)
        println("The $filename file was not found!")
        return
    end
    println("Contents of the $filename file:")
    println("="^50)
    content = read(filename, String)
    println(content)
    println("="^50)
    println("End of file")
catch e
    println("Error reading the file: ", e)
end
Содержимое файла /user/my_projects/Demo/QPSK+FIR_verilog/prj/tb.v:
==================================================
module tb;
  reg clock;
  reg reset;
  reg io_bit_1;
  reg io_bit_2;
  reg io_Valid;
  wire io_Out_Valid;
  wire [3:0] io_Out_Re;
  wire [3:0] io_Out_Im;
  wire fir_valid;
  wire [7:0] fir_re;
  wire [7:0] fir_im;
  
  reg [1:0] test_pattern [0:3];
  integer pattern_index;
  integer cycle_count;
  
  QPSKFIR_verilog_QPSK_modulator qpsk_mod (
    .clock(clock),
    .reset(reset),
    .io_bit_1(io_bit_1),
    .io_bit_2(io_bit_2),
    .io_Valid(io_Valid),
    .io_Out_Valid(io_Out_Valid),
    .io_Out_Re(io_Out_Re),
    .io_Out_Im(io_Out_Im)
  );
  
  QPSKFIR_verilog_FIR fir_filter (
    .clock(clock),
    .reset(reset),
    .io_Re(io_Out_Re),
    .io_Im(io_Out_Im),
    .io_Valid(io_Out_Valid),
    .io_valid(fir_valid),
    .io_re(fir_re),
    .io_im(fir_im)
  );
  
  always #5 clock = ~clock;
  
  initial begin
    $dumpfile("test_codgen_ic.vcd");
    $dumpvars(0, tb);
  end
  
  // Функции для преобразования в дробный формат
  function real qpsk_to_real;
    input [3:0] value;
    begin
      qpsk_to_real = $signed(value) / 8.0; // Q3.4: 3 бита целая часть, 4 бита дробная
    end
  endfunction
  
  function real fir_to_real;
    input [7:0] value;
    begin
      fir_to_real = $signed(value) / 128.0; // Q1.7: 1 бит знак, 7 бит дробная часть
    end
  endfunction
  
  integer data_file;
  initial begin
    data_file = $fopen("output_data.txt", "w");
    $fwrite(data_file, "Cycle\tTime\tQPSK_Valid\tQPSK_Re\tQPSK_Im\tFIR_Valid\tFIR_Re\tFIR_Im\tQPSK_Re_Real\tQPSK_Im_Real\tFIR_Re_Real\tFIR_Im_Real\n");
  end
  
  initial begin
    test_pattern[0] = 2'b11;
    test_pattern[1] = 2'b00;
    test_pattern[2] = 2'b10;
    test_pattern[3] = 2'b01;
    pattern_index = 0;
    cycle_count = 0;
  end
  
  initial begin
    clock = 0;
    reset = 1;
    io_bit_1 = 0;
    io_bit_2 = 0;
    io_Valid = 0;
    #20 reset = 0;
    #10;
    for (integer i = 0; i < 50; i = i + 1) begin
      io_bit_1 = test_pattern[pattern_index][1];
      io_bit_2 = test_pattern[pattern_index][0];
      io_Valid = 1;
      pattern_index = (pattern_index + 1) % 4;
      #10;
    end
    #100;
    $fclose(data_file);
    $finish;
  end
  
  always @(posedge clock) begin
    if (!reset) begin
      real qpsk_re_real, qpsk_im_real, fir_re_real, fir_im_real;
      
      // Преобразуем в дробные числа
      qpsk_re_real = qpsk_to_real(io_Out_Re);
      qpsk_im_real = qpsk_to_real(io_Out_Im);
      fir_re_real = fir_to_real(fir_re);
      fir_im_real = fir_to_real(fir_im);
      
      $fwrite(data_file, "%d\t%0t\t%b\t%d\t%d\t%b\t%d\t%d\t%f\t%f\t%f\t%f\n", 
              cycle_count, $time, 
              io_Out_Valid, $signed(io_Out_Re), $signed(io_Out_Im),
              fir_valid, $signed(fir_re), $signed(fir_im),
              qpsk_re_real, qpsk_im_real, fir_re_real, fir_im_real);
              
      $display("Cycle: %d | QPSK: Valid=%b, Re=%d(%f), Im=%d(%f) | FIR: Valid=%b, Re=%d(%f), Im=%d(%f)",
               cycle_count,
               io_Out_Valid, $signed(io_Out_Re), qpsk_re_real, $signed(io_Out_Im), qpsk_im_real,
               fir_valid, $signed(fir_re), fir_re_real, $signed(fir_im), fir_im_real);
               
      cycle_count = cycle_count + 1;
    end
  end
endmodule
==================================================
Конец файла

Now let's run the simulation.

In [ ]:
# Compilation
run(`iverilog -o sim tb.v QPSKFIR_verilog_FIR.v QPSKFIR_verilog_QPSK_modulator.v`)
# Running the simulation
run(`vvp sim`)

As we can see, based on the results of the simulation, 2 txt and vcd files were generated.

TXT is a text file with a data table (timestamps, signal values).

VCD (Value Change Dump) is a binary file of time diagrams used for visual debugging of signals in GTKWave and other analyzers.

Let's try to perform VCD parsing.

In [ ]:
function vcd_to_txt(vcd_filename; output_txt="simple_output.txt")
    println("Simplified VCD to TXT conversion: ", vcd_filename)
    lines = readlines(vcd_filename)
    signals = Dict{String, Vector{Tuple{Float64, Any}}}()
    current_time = 0.0
    for line in lines
        line = strip(line)
        isempty(line) && continue
        if startswith(line, "\$var") 
            parts = split(line)
            if length(parts) >= 4
                signal_id = parts[3]
                signal_name = parts[4]
                signals[signal_name] = []
            end
        elseif startswith(line, "#")
            current_time = parse(Float64, line[2:end])
        elseif length(line) >= 2
            value_char = line[1:1]
            signal_id = line[2:end]
            value = if value_char == "0"
                0
            elseif value_char == "1"
                1
            else
                0
            end
            for (name, values) in signals
                if occursin(signal_id, name) || signal_id == string(hash(name))[1:min(3, end)]
                    push!(values, (current_time, value))
                    break
                end
            end
        end
    end
    open(output_txt, "w") do io
        header = "Time\t" * join(keys(signals), "\t")
        println(io, header)
        all_times = Set{Float64}()
        for values in values(signals)
            for (time, _) in values
                push!(all_times, time)
            end
        end
        sorted_times = sort(collect(all_times))
        for time in sorted_times
            row = string(time)
            for signal_name in keys(signals)
                value = 0
                for (t, v) in signals[signal_name]
                    if t == time
                        value = v
                        break
                    elseif t < time
                        value = v
                    end
                end
                row *= "\t" * string(value)
            end
            println(io, row)
        end
    end
    println("Simplified conversion completed: ", output_txt)
end
vcd_to_txt("test_codgen_ic.vcd", output_txt="simple_result.txt")
Упрощенная конвертация VCD в TXT: test_codgen_ic.vcd
Упрощенная конвертация завершена: simple_result.txt

As we can see, this is possible, but not very convenient and fast, plus we did not pull out the names of the fields.

So let's use the second option and analyze the TXT.

In [ ]:
using DataFrames
using DelimitedFiles
using Plots

data = readdlm("output_data.txt", '\t', skipstart=1)  # Skipping the title
cycle = data[:, 1]
qpsk_re = data[:, 4]  # QPSK_Re in the 4th column
qpsk_im = data[:, 5]  # QPSK_Im in the 5th column
fir_re = data[:, 7]   # FIR_Re in the 7th column
fir_im = data[:, 8]   # FIR_Im in the 8th column

p = plot(layout=(2, 1), size=(800, 600))
plot!(p[1], cycle, qpsk_re, label="QPSK Real", linewidth=2, color=:blue)
plot!(p[1], cycle, qpsk_im, label="QPSK Imag", linewidth=2, color=:red)
title!(p[1], "QPSK Signal")
plot!(p[2], cycle, fir_re, label="FIR Real", linewidth=2, color=:green)
plot!(p[2], cycle, fir_im, label="FIR Imag", linewidth=2, color=:orange)
title!(p[2], "FIR Filter Output")
Out[0]:

The verification results confirm that the behavior of the test case matches the expected model. Minor discrepancies are explained only by different types of data. The modules are working correctly: QPSK generates characters, and the FIR filter processes them.

Conclusion

In this example, we have figured out how to visualize Verilog code simulation tests, and also generated a simple model of the transmission path of the communication system.