Сообщество Engee

QPSK приёмник и передатчик на ПЛИС

Автор
avatar-yurevyurev
Notebook

OPSK приёмник и передатчик на ПЛИС

Данный пример демонстрирует тестовую модель системы связи, разработанную для генерации кода. В модель сознательно внесён ряд упрощений, чтобы наглядно показать процесс перехода от простой концепции к готовому коду для ПЛИС. Полное описание проекта и детальный разбор всех этапов работы представлены в этой статье.
(https://habr.com/ru/companies/etmc_exponenta/articles/969564/)

Первая модель, которую мы рассмотрим, сравнивает блоки, разработанные для когенерации кода, с блоками из стандартной библиотеки Engee. Это позволяет верифицировать созданные нами алгоритмы.

1.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("test_sim") # Запуск модели.
Building...
Progress 0%
Progress 41%
Progress 100%
Progress 100%
Out[0]:
SimulationResult(
    run_id => 3,
    "QPSK_demodulator-1.Out_Valid" => WorkspaceArray{Bool}("test_sim/QPSK_demodulator-1.Out_Valid")
,
    "out_v" => WorkspaceArray{UInt32}("test_sim/out_v")
,
    "Complex to Real-Imag-1.r" => WorkspaceArray{Float64}("test_sim/Complex to Real-Imag-1.r")
,
    "Nyquist_filter_TX.re" => WorkspaceArray{Float64}("test_sim/Nyquist_filter_TX.re")
,
    "out_ref" => WorkspaceArray{Int64}("test_sim/out_ref")
,
    "Complex to Real-Imag-2.r" => WorkspaceArray{Vector{Float64}}("test_sim/Complex to Real-Imag-2.r")
,
    "Complex to Real-Imag.r" => WorkspaceArray{Vector{Float64}}("test_sim/Complex to Real-Imag.r")
,
    "Nyquist_filter_RX.re" => WorkspaceArray{Float64}("test_sim/Nyquist_filter_RX.re")
,
    "Gain.1" => WorkspaceArray{Fixed{1, 16, 10, Int16}}("test_sim/Gain.1")

)
In [ ]:
out_v_data = collect(simout["test_sim/out_v"]).value
out_ref_data = collect(simout["test_sim/out_ref"]).value

p_ref = plot(out_ref_data, 
           seriestype = :steppost,  # Тип графика - ступеньки
           label="Референсный сигнал", 
           linewidth=2, 
           color=:orange,
           ylabel="Значение", 
           title="Референсный сигнал (out_ref)",
           xlims=(0, 16),  # Диапазон от 0 до 16
           legend=:topright)

p_v = plot(out_v_data, 
           seriestype = :steppost,  # Тип графика - ступеньки
           label="Сигнал модели под генерацию кода", 
           linewidth=2, 
           color=:blue,
           ylabel="Значение", 
           title="Сигнал модели под генерацию кода (out_v)",
           xlims=(0, 150),  # Диапазон от 0 до 150
           legend=:topright)
plot(p_ref, p_v, 
     layout=(@layout [a; b]),  # Два графика вертикально
     size=(800, 600))
Out[0]:

Построив графики выходов QPSK-демодуляторов для обеих моделей, мы видим их идентичную работу. Единственное различие в поведении заключается в том, что благодаря наличию сигнала валидности в модели под генерацию кода, с её выхода исключаются все выборки до момента накопления полного кадра.
На основании проведённого теста можно сделать вывод, что разработанные нами фильтры и модуляторы работают корректно.

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

image.png
In [ ]:
run_model("model") # Запуск модели.
Building...
Progress 0%
Progress 10%
Progress 51%
Progress 99%
Progress 100%
Progress 100%
Out[0]:
SimulationResult(
    run_id => 4,
    "Test.err_val" => WorkspaceArray{Bool}("model/System/Test.err_val")
,
    "RX.valid" => WorkspaceArray{Bool}("model/System/RX.valid")
,
    "Gen_data.Valid" => WorkspaceArray{Bool}("model/System/Gen_data.Valid")
,
    "Data Type Conversion-1.1" => WorkspaceArray{Fixed{0, 1, 0, UInt8}}("model/System/Gen_data/Data_rend/Data Type Conversion-1.1")
,
    "QPSK_modulator.Re" => WorkspaceArray{Fixed{1, 8, 7, Int8}}("model/System/TX/QPSK_modulator.Re")
,
    "Nyquist_filter_RX.FIR_out" => WorkspaceArray{Fixed{1, 16, 14, Int16}}("model/System/RX/Nyquist_filter_RX.FIR_out")
,
    "err_bit_1" => WorkspaceArray{Bool}("model/System/err_bit_1")
,
    "Nyquist_filter_TX.re" => WorkspaceArray{Fixed{1, 16, 14, Int16}}("model/System/TX/Nyquist_filter_TX.re")
,
    "Delay_103.Out1" => WorkspaceArray{Bool}("model/System/Test/Delay_103.Out1")
,
    "Nyquist_filter_RX.im" => WorkspaceArray{Fixed{1, 16, 14, Int16}}("model/System/RX/Nyquist_filter_RX.im")
,
    "Nyquist_filter_RX.re" => WorkspaceArray{Fixed{1, 16, 14, Int16}}("model/System/RX/Nyquist_filter_RX.re")
,
    "Switch.1" => WorkspaceArray{Fixed{1, 16, 14, Int16}}("model/System/TX/Switch.1")
,
    "System.Re_filt_RX" => WorkspaceArray{Fixed{1, 16, 14, Int16}}("model/System.Re_filt_RX")
,
    "QPSK_demodulator.valid" => WorkspaceArray{Bool}("model/System/RX/QPSK_demodulator.valid")
,
    "Data Type Conversion.1" => WorkspaceArray{Fixed{0, 1, 0, UInt8}}("model/System/Gen_data/Data_rend/Data Type Conversion.1")
,
    "Valid_out.1" => WorkspaceArray{Bool}("model/System/Test/Valid_out.1")
,
    "err_bit_2" => WorkspaceArray{Bool}("model/System/err_bit_2")

)
In [ ]:
err_bit_1_data = collect(simout["model/System/err_bit_1"]).value
err_bit_2_data = collect(simout["model/System/err_bit_2"]).value
err_val_data = collect(simout["model/System/Test.err_val"]).value
sum_err_bit_1 = sum(err_bit_1_data)    
sum_err_bit_2 = sum(err_bit_2_data)   
sum_err_val = sum(err_val_data)       
println("Сумма ошибок по первому биту: ", sum_err_bit_1)
println("Сумма ошибок по второму биту: ", sum_err_bit_2)
println("Сумма ошибок по валидам:    ", sum_err_val)
Сумма ошибок по первому биту: 0
Сумма ошибок по второму биту: 0
Сумма ошибок по валидам:    0

Как мы видим, модель не регистрирует ошибок. Это означает, что по крайней мере без учёта канала связи система работает корректно.

Следующим шагом является генерация кода на Verilog и проверка его работоспособности. Помимо самого кода, нами также была создана модель на языке С с использованием верилятора для функциональной верификации.Без имени.png

In [ ]:
run_model("verification") # Запуск модели.
Building...
Progress 0%
Progress 100%
Progress 100%
Out[0]:
SimulationResult(
    run_id => 7,
    "C Function.3" => WorkspaceArray{Bool}("verification/C Function.3")
,
    "C Function.1" => WorkspaceArray{Bool}("verification/C Function.1")
,
    "C Function.2" => WorkspaceArray{Bool}("verification/C Function.2")

)

Для проверки работоспособности сгенерированного кода, по аналогии с предыдущей моделью, выполним подсчёт ошибок для битовых потоков и сигналов валидности.

In [ ]:
err_bit_1_data = collect(simout["verification/C Function.1"]).value
err_bit_2_data = collect(simout["verification/C Function.2"]).value
err_val_data = collect(simout["verification/C Function.3"]).value
sum_err_bit_1 = sum(err_bit_1_data)    
sum_err_bit_2 = sum(err_bit_2_data)   
sum_err_val = sum(err_val_data)       
println("Сумма ошибок по первому биту: ", sum_err_bit_1)
println("Сумма ошибок по второму биту: ", sum_err_bit_2)
println("Сумма ошибок по валидам:    ", sum_err_val)
Сумма ошибок по первому биту: 0
Сумма ошибок по второму биту: 0
Сумма ошибок по валидам:    0

Как мы видим, сгенерированный код работает идентично исходной модели. Это даёт уверенность в том, что, при отсутствии проблем с ресурсами или временными характеристиками (таймингами), он будет корректно функционировать на ПЛИС, сама файловая структура сгенерированного проекта представлена ниже.

In [ ]:
current_dir = @__DIR__
println("Структура файлов в директории: $current_dir")
println()
for (root, dirs, files) in walkdir(current_dir)
    for dir in dirs
        if startswith(dir, "model_System_code")
            println("$dir/")
            dir_path = joinpath(root, dir)
            for file in readdir(dir_path)
                println("  └── $file")
            end
            println()
        end
    end
end
Структура файлов в директории: /user/my_projects/Demo/OPSK_verilog

model_System_code/
  └── Delay_103.v
  └── Delay_20.v
  └── Gen_data.v
  └── Nyquist_filter_RX.v
  └── Nyquist_filter_TX.v
  └── QPSK_demodulator.v
  └── QPSK_modulator.v
  └── RX.v
  └── TX.v
  └── Test.v
  └── bit_and_val_2.v
  └── delay_8.v
  └── delay_8_1_1.v
  └── fir_41.v
  └── gen_sin_cos_demod.v
  └── gen_sin_cos_mod.v
  └── model_System.v
  └── obj_dir
  └── quadrature_demodulator.v
  └── quadrature_modulator.v

Вывод

Результаты всех этапов тестирования (визуальное сравнение графиков, подсчёт битовых ошибок, верификация C-моделью) однозначно свидетельствуют о работоспособности проекта. Разработанные алгоритмы верифицированы, а процесс автоматической генерации кода из модели отработан и дал корректный результат. Таким образом, представленный пример подтверждает жизнеспособность методологии модельно-ориентированного проектирования (Model-Based Design) для создания цифровых систем связи.

Показан полный путь от функциональной модели в среде Engee до готового для синтеза кода на языке описания аппаратуры, что существенно ускоряет процесс разработки под ПЛИС.