QPSK+FIR Verilog
В данном примере представлена модель QPSK-модулятора в сочетании с КИХ-фильтром. Цель примера — продемонстрировать возможности генератора кода, а также выполнить верификацию этого кода с помощью симулятора Verilog, встроенного в среду Engee.
Модель имеет модульную структуру и состоит из трёх базовых блоков:

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

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

Теперь перейдём к запуску модели и кодогенерации.
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") # Запуск модели.
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")
collect(simout["QPSK+FIR_verilog/FIR.re"]).value
Графики нам пригодится для сравнения с работой кода Verilog.
Теперь выполним генерацию кода из блоков модели и опишем тестовый модуль аналогично входам в модели.
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 для анализа временных диаграмм.
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
Теперь запустим симулцию.
# Компиляция
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.
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")
Как мы видим это возможно, но не очень удобно и грамоско, плюсом мы не вытащили названия полей.
Поэтому давайте воспользуемся вторым вариантом и проанализируем TXT.
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")
Результаты верификации подтверждают, что поведение тестового примера совпадает с ожидаемой моделью. Незначительные расхождения объясняются лишь различными типами данных. Работа модулей корректна: QPSK формирует символы, а FIR-фильтр их обрабатывает.
Вывод
В данном примере мы разобрали как можно визуализировать тесты симуляции Verilog кода, а также сгенерировали простую модель передающего тракта системы связи.