Генерация кода из модели с вложенными подсистемами.
Данный пример логически продолжает предыдущую демонстрацию. Его цель — показать возможности генератора кода Verilog, а именно различные подходы к структурированию итогового проекта за счёт использования атомарных подсистем.
На рисунке ниже представлена реализованная модель. В ней конфигурация "Treat as atomic unit"
применена только к одному из двух блоков, что позволяет наглядно сравнить результаты генерации.

Теперь перейдём к генерации кода и сравнению результатов с исходной моделью. Для начала объявим вспомогательную функцию, которая будет запускать нашу модель.
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
Теперь выполним настройку модели для генерации кода на Verilog.

Генерацию кода мы выполним напрямую из интерфейса модели (по нажатию кнопки), чтобы не перегружать и без того объёмный скрипт.

Далее объявляем вспомогательную функцию для чтения сгенерированных файлов Verilog. Принцип работы функции read_v
заключается в следующем. Сначала она проверяет существование указанного файла в файловой системе. Если файл не обнаружен, выводится соответствующее сообщение. При успешном обнаружении функция считывает всё содержимое файла в виде строки и наглядно выводит его в консоль, обрамляя заголовком и разделителями для удобства восприятия.
function read_v(filename)
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
end
Теперь перейдём к сравнению реализации подсистем. Ключевое различие заключается в организации выходного кода.

В случае реализации без использования атомарных подсистем головной файл проекта включает в себя всю логику системы, представленную в виде единого монолитного модуля. Этот модуль содержит многочисленные регистры и сложные комбинационные цепи, объединённые в одном пространстве имён.
read_v("$(@__DIR__)/model_RX.v")

Подход с применением атомарных подсистем демонстрирует модульный принцип построения проекта. Головной файл выступает в роли обёртки верхнего уровня, которая инстанцирует и соединяет между собой отдельные функциональные блоки: генератор данных, модулятор и фильтр. Каждый из этих блоков является самостоятельным модулем (Gen_data, QPSK_modulator, fir) со своими чёткими интерфейсами ввода-вывода. Такая структура не только отражает логическое разделение системы на компоненты, но и значительно улучшает читаемость, сопровождаемость и возможности повторного использования кода.
read_v("$(@__DIR__)/model_RX_atomic_code/model_RX_atomic.v")
Из проведённого сравнения также следует, что написание тестового окружения (testbench) является более простой задачей именно для версии проекта без атомарных подсистем. Поскольку сгенерированный код в этом случае представляет собой единый модуль, его подключение и наблюдение за выходами не требует описания сложной иерархии. В данном примере вся логика системы содержится внутри одного блока, что позволяет напрямую отслеживать выходные сигналы.
Этим преимуществом мы и воспользуемся для проведения верификации. Так как функциональное поведение обеих версий проекта идентично, проверку корректности работы мы будем осуществлять именно на монолитной реализации.
Данный testbench выполняет две основные функции:
- Генерация тактового сигнала и сброса: создаётся периодический тактовый сигнал (clock) и управляющий сигнал сброса (reset) для инициализации устройства.
- Логирование выходных сигналов: все выходные данные устройства (io_Out3, io_Out1, io_Out2) записываются в текстовый файл "output.txt" на каждом положительном фронте тактового сигнала после снятия сигнала сброса.
read_v("$(@__DIR__)/tb.v")
Теперь выполним запуск исходной модели и проведём тестирование сгенерированного кода. Это позволит нам верифицировать корректность работы генератора, сравнив поведение исходной системы в среде моделирования Engee с результатами, полученными при выполнении сгенерированного Verilog-кода в симуляторе.
run_model("model") # Запуск модели.
run(`iverilog -o sim tb.v model_RX.v`)# Компиляция
run(`vvp sim`)# Запуск симуляции
Для последующего анализа необходима функция парсинга текстового файла, сгенерированного тестбенчем. Реализованная функция parse_simulation_data
преобразует сырые логированные данные в формат, пригодный для анализа, сама функция возвращает три массива, готовых для анализа и построения графиков.
Алгоритм работы:
- Чтение данных: Функция считывает файл, разделяя строки по пробелам (временная метка, сигнал valid, шестнадцатеричные значения квадратурных компонентов)
- Преобразование форматов: Строки hex преобразуются в числа UInt8 через парсинг из шестнадцатеричного представления
- Нормализация: Ключевой этап - преобразование чисел через дополнительный код (twos complement) с последующей нормализацией на диапазон [-1.0, ~0.992] путем деления на 128
using DelimitedFiles
function parse_simulation_data(filename)
data = readdlm(filename, ' ', skipstart=0)
valid = Int.(data[:, 2])
re_hex = string.(data[:, 3])
im_hex = string.(data[:, 4])
Re_u8 = [parse(UInt8, h, base=16) for h in re_hex]
Im_u8 = [parse(UInt8, h, base=16) for h in im_hex]
function twos_complement_to_float(x::UInt8)
x_signed = reinterpret(Int8, x)
return Float64(x_signed) / 128.0
end
Re = twos_complement_to_float.(Re_u8)
Im = twos_complement_to_float.(Im_u8)
return valid, Im, Re
end
Val, Im, Rm = parse_simulation_data("output.txt")
Re_sim = collect(simout["model/RX.Out1"]).value
Im_sim = collect(simout["model/RX.Out2"]).value
Val_sim = collect(simout["model/RX.Out3"]).value;
Теперь перейдём к сравнительному анализу. На представленных ниже графиках отображены: диаграмма созвездия, графики реальной и мнимой частей сигнала и диаграмма сигнала валидности.
function plot_iq_constellation(Re, Im; title="", color=:blue)
scatter(Re, Im,
aspect_ratio=:equal,
markersize=2,
markerstrokewidth=0,
alpha=0.6,
title=title,
xlabel="In-phase Component (I)",
ylabel="Quadrature Component (Q)",
legend=false,
grid=true,
color=color)
end
valid_indices_sim = findall(Val_sim .== 1) # Индексы где Val_sim == 1
valid_indices = findall(Val .== 1) # Индексы где Val == 1
p1 = plot_iq_constellation(Re_sim[valid_indices_sim], Im_sim[valid_indices_sim],
title="Созвездие модели", color=:blue)
p2 = plot_iq_constellation(Rm[valid_indices], Im[valid_indices],
title="Созвездие Verilog", color=:red)
plot(p1, p2, layout=(1,2), size=(800,400))
plot(Re_sim, label="Re_sim", seriestype=:steppost)
plot!(Rm, label="Rm", seriestype=:steppost)
plot(Im_sim, label="Im_sim", seriestype=:steppost)
plot!(Im, label="Im", seriestype=:steppost)
plot(Val_sim, label="Valid_sim", seriestype=:steppost)
plot!(Val, label="Valid", seriestype=:steppost)
Визуальный анализ этих графиков позволяет убедиться в полном соответствии работы сгенерированного кода поведению исходной модели. Форма сигналов, характер созвездия и временные параметры полностью идентичны, что подтверждает корректность функционирования автоматически сгенерированного Verilog-кода и его точную эквивалентность исходной математической модели.
Вывод
Проведённая работа наглядно демонстрирует эффективность использования автоматической генерации Verilog-кода из моделей Engee. Экспериментально подтверждена полная функциональная эквивалентность между исходной математической моделью системы и её аппаратной реализацией, полученной через кодогенерацию.
Сравнительный анализ двух подходов к структурированию проекта - монолитного и модульного с использованием атомарных подсистем - показал их принципиальные различия в организации кода при сохранении идентичного поведения. Модульный подход обеспечивает лучшую читаемость, сопровождаемость и возможности повторного использования кода, в то время как монолитная реализация упрощает создание тестового окружения.
Верификация результатов через сравнение временных диаграмм и диаграмм созвездия подтвердила точное соответствие всех выходных сигналов, что свидетельствует о корректной работе инструмента кодогенерации и возможности его практического применения для проектирования цифровых систем.