Сообщество Engee

QPSK+FIR Verilog

Автор
avatar-yurevyurev
Notebook

QPSK+FIR Verilog

В данном примере мы рассмотрим модель QPSK модулятора в связке с КИХ-фильтром, цель данного примера продемонстрировать возможности генератора когда, а так же верифицировать этот код при помощи симулятора Verilog встроенного в Engee, сама модель достаточно простая и состоит из 3 блоков.

image.png

  1. Gen_data - просто генерирует комбинации бит от [0,0] до [1,1]

  2. QPSK_modulator - преобразует последовательность битов в комплексные символы (сдвиги фазы несущей в этой реализации не учитывается).

image.png

  1. FIR - это цифровой фильтр с конечной импульсной характеристикой, который вычисляет каждое выходное значение как взвешенную сумму последних 11 входных отсчётов.

image.png

Теперь перейдём к запуску модели и кодогенерации.

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)

Графики нам пригодится для сравнения с работой кода Verilog.
Теперь выполним генерацию кода из блоков модели и опишем тестовый модуль аналогично входам в модели.

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"
)

Модуль tb — это тестовый стенд (testbench), который проверяет работу модулей QPSK-модулятора и FIR-фильтра, подавая на вход тестовые данные и записывая результаты.

Тестовый стенд подает на вход циклическую последовательность битов [11, 00, 10, 01], записывает выходные значения модулятора (QPSK_Re, QPSK_Im) и фильтра (FIR_Re, FIR_Im) в файл output_data.txt и визуализирует их в консоли, а также генерирует файл waveforms test_codgen_ic.vcd для анализа временных диаграмм.

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
==================================================
Конец файла

Теперь запустим симулцию.

In [ ]:
# Компиляция
run(`iverilog -o sim tb.v QPSKFIR_verilog_FIR.v QPSKFIR_verilog_QPSK_modulator.v`)
# Запуск симуляции
run(`vvp sim`)

Как мы видим по результатам выполнения симуляции были сформированы 2 файла txt и vcd.

TXT — текстовый файл с таблицей данных (временные метки, значения сигналов).

VCD (Value Change Dump) — бинарный файл временных диаграмм, используемый для визуальной отладки сигналов в GTKWave и других анализаторах.

Давайте попробуем выполнить парсинг VCD.

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

Как мы видим это возможно, но не очень удобно и грамоско, плюсом мы не вытащили названия полей.

Поэтому давайте воспользуемся вторым вариантом и проанализируем 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]:

Как мы можем заметить поведения теста идентично модели, не считая различий в типах данных, мы видим, что QPSK формирует символа, а FIR выполняет фильтрацию этих данных.

Вывод

В данном примере мы разобрали как можно визуализировать тесты симуляции Verilog кода, а так же сгенерировали простую модель передающего тракта системы связи.