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()] # Проверка условия загрузки модели в ядро
        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
    sleep(0.1)
    return model_output
end
run_model("QPSK+FIR_verilog") # Запуск модели.
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("Файл $filename не найден!")
        return
    end
    println("Содержимое файла $filename:")
    println("="^50)
    content = read(filename, String)
    println(content)
    println("="^50)
    println("Конец файла")
catch e
    println("Ошибка при чтении файла: ", 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 [ ]:
# Компиляция
run(`iverilog -o sim tb.v QPSKFIR_verilog_FIR.v QPSKFIR_verilog_QPSK_modulator.v`)
# Запуск симуляции
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("Упрощенная конвертация VCD в TXT: ", 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("Упрощенная конвертация завершена: ", 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)  # Пропускаем заголовок
cycle = data[:, 1]
qpsk_re = data[:, 4]  # QPSK_Re в 4-й колонке
qpsk_im = data[:, 5]  # QPSK_Im в 5-й колонке
fir_re = data[:, 7]   # FIR_Re в 7-й колонке
fir_im = data[:, 8]   # FIR_Im в 8-й колонке

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.