Сообщество Engee

Генерация кода из модели с вложенными подсистемами.

Автор
avatar-yurevyurev
Notebook

Генерация кода из модели с вложенными подсистемами.

Данный пример логически продолжает предыдущую демонстрацию. Его цель — показать возможности генератора кода Verilog, а именно различные подходы к структурированию итогового проекта за счёт использования атомарных подсистем.

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

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
Out[0]:
run_model (generic function with 1 method)

Теперь выполним настройку модели для генерации кода на Verilog.

image.png

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

image.png

Далее объявляем вспомогательную функцию для чтения сгенерированных файлов Verilog. Принцип работы функции read_v заключается в следующем. Сначала она проверяет существование указанного файла в файловой системе. Если файл не обнаружен, выводится соответствующее сообщение. При успешном обнаружении функция считывает всё содержимое файла в виде строки и наглядно выводит его в консоль, обрамляя заголовком и разделителями для удобства восприятия.

In [ ]:
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
Out[0]:
read_v (generic function with 1 method)

Теперь перейдём к сравнению реализации подсистем. Ключевое различие заключается в организации выходного кода.

bez_imeni.png

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

In [ ]:
read_v("$(@__DIR__)/model_RX.v")
Содержимое файла /user/start/examples/codegen/qpsk_and_fir_verilog_v2/model_RX.v:
==================================================
/* Code generated by Engee
 * Model name: model.engee
 * Code generator: release-1.1.22
 * Date: Wed Sep 10 06:27:06 2025 GMT
 */

module model_RX(
  input        clock,
               reset,
  output       io_Out3,
  output [7:0] io_Out1,
               io_Out2
);

  reg         UnitDelay_1_state;
  reg         UnitDelay_13_state;
  reg         UnitDelay_state;
  reg         UnitDelay_1_1_state;
  reg         UnitDelay_2_state;
  reg         UnitDelay_3_state;
  reg         UnitDelay_4_state;
  reg         UnitDelay_13_1_state;
  reg         UnitDelay_14_state;
  reg         UnitDelay_5_state;
  reg         UnitDelay_6_state;
  reg         UnitDelay_14_1_state;
  reg         UnitDelay_7_state;
  reg         UnitDelay_8_state;
  reg         UnitDelay_9_state;
  reg         UnitDelay_10_state;
  reg         UnitDelay_11_state;
  reg  [3:0]  UnitDelay_12_state;
  reg  [3:0]  UnitDelay_13_2_state;
  reg  [3:0]  UnitDelay_14_2_state;
  reg  [3:0]  UnitDelay_15_state;
  reg  [3:0]  UnitDelay_16_state;
  reg  [3:0]  UnitDelay_17_state;
  reg  [3:0]  UnitDelay_18_state;
  reg  [3:0]  UnitDelay_19_state;
  reg  [3:0]  UnitDelay_20_state;
  reg  [3:0]  UnitDelay_21_state;
  reg  [3:0]  UnitDelay_22_state;
  reg  [3:0]  UnitDelay_23_state;
  reg  [3:0]  UnitDelay_24_state;
  reg  [3:0]  UnitDelay_25_state;
  reg  [3:0]  UnitDelay_26_state;
  reg  [3:0]  UnitDelay_27_state;
  reg  [3:0]  UnitDelay_28_state;
  reg  [3:0]  UnitDelay_29_state;
  reg  [3:0]  UnitDelay_30_state;
  reg  [3:0]  UnitDelay_31_state;
  wire        LogicalOperator = UnitDelay_13_state ^ UnitDelay_14_state;
  wire        LogicalOperator_1 = UnitDelay_1_state ^ UnitDelay_14_1_state;
  wire [2:0]  _IdxAccum_T_2 =
    {1'h0, LogicalOperator, 1'h0} + {2'h0, LogicalOperator_1} + 3'h1;
  wire        _tmp6_T = _IdxAccum_T_2 == 3'h2;
  wire        _constellation_selector_im_T = _IdxAccum_T_2 == 3'h1;
  wire [3:0]  _constellation_selector_re_new_T_1 =
    _constellation_selector_im_T | ~(_tmp6_T | _IdxAccum_T_2 == 3'h3) ? 4'h6 : 4'hA;
  wire [3:0]  _constellation_selector_im_new_T_1 =
    _constellation_selector_im_T | _tmp6_T ? 4'h6 : 4'hA;
  wire [10:0] _Gain_1_new_T_1 =
    {{7{_constellation_selector_re_new_T_1[3]}}, _constellation_selector_re_new_T_1}
    * 11'h7FF;
  wire [10:0] _Gain_1_1_new_T_1 =
    {{7{_constellation_selector_im_new_T_1[3]}}, _constellation_selector_im_new_T_1}
    * 11'h7FF;
  wire [10:0] _Gain_3_new_T_1 =
    {{7{UnitDelay_14_2_state[3]}}, UnitDelay_14_2_state} * 11'h7FE;
  wire [10:0] _Gain_3_1_new_T_1 =
    {{7{UnitDelay_15_state[3]}}, UnitDelay_15_state} * 11'h7FE;
  wire [10:0] _Gain_5_new_T_1 =
    {{7{UnitDelay_19_state[3]}}, UnitDelay_19_state} * 11'h7FE;
  wire [10:0] _Gain_5_1_new_T_1 =
    {{7{UnitDelay_18_state[3]}}, UnitDelay_18_state} * 11'h7FE;
  wire [10:0] _Gain_6_new_T_1 = {{7{UnitDelay_21_state[3]}}, UnitDelay_21_state} * 11'h30;
  wire [10:0] _Gain_6_1_new_T_1 =
    {{7{UnitDelay_20_state[3]}}, UnitDelay_20_state} * 11'h30;
  wire [10:0] _Gain_7_new_T_1 =
    {{7{UnitDelay_22_state[3]}}, UnitDelay_22_state} * 11'h7FE;
  wire [10:0] _Gain_7_1_new_T_1 =
    {{7{UnitDelay_23_state[3]}}, UnitDelay_23_state} * 11'h7FE;
  wire [10:0] _Gain_9_new_T_1 =
    {{7{UnitDelay_27_state[3]}}, UnitDelay_27_state} * 11'h7FE;
  wire [10:0] _Gain_9_1_new_T_1 =
    {{7{UnitDelay_26_state[3]}}, UnitDelay_26_state} * 11'h7FE;
  wire [10:0] _Gain_11_new_T_1 =
    {{7{UnitDelay_30_state[3]}}, UnitDelay_30_state} * 11'h7FF;
  wire [10:0] _Gain_11_1_new_T_1 =
    {{7{UnitDelay_31_state[3]}}, UnitDelay_31_state} * 11'h7FF;
  always @(posedge clock) begin
    if (reset) begin
      UnitDelay_1_state <= 1'h0;
      UnitDelay_13_state <= 1'h0;
      UnitDelay_state <= 1'h0;
      UnitDelay_1_1_state <= 1'h0;
      UnitDelay_2_state <= 1'h0;
      UnitDelay_3_state <= 1'h0;
      UnitDelay_4_state <= 1'h0;
      UnitDelay_13_1_state <= 1'h1;
      UnitDelay_14_state <= 1'h0;
      UnitDelay_5_state <= 1'h0;
      UnitDelay_6_state <= 1'h0;
      UnitDelay_14_1_state <= 1'h1;
      UnitDelay_7_state <= 1'h0;
      UnitDelay_8_state <= 1'h0;
      UnitDelay_9_state <= 1'h0;
      UnitDelay_10_state <= 1'h1;
      UnitDelay_11_state <= 1'h0;
      UnitDelay_12_state <= 4'h0;
      UnitDelay_13_2_state <= 4'h0;
      UnitDelay_14_2_state <= 4'h0;
      UnitDelay_15_state <= 4'h0;
      UnitDelay_16_state <= 4'h0;
      UnitDelay_17_state <= 4'h0;
      UnitDelay_18_state <= 4'h0;
      UnitDelay_19_state <= 4'h0;
      UnitDelay_20_state <= 4'h0;
      UnitDelay_21_state <= 4'h0;
      UnitDelay_22_state <= 4'h0;
      UnitDelay_23_state <= 4'h0;
      UnitDelay_24_state <= 4'h0;
      UnitDelay_25_state <= 4'h0;
      UnitDelay_26_state <= 4'h0;
      UnitDelay_27_state <= 4'h0;
      UnitDelay_28_state <= 4'h0;
      UnitDelay_29_state <= 4'h0;
      UnitDelay_30_state <= 4'h0;
      UnitDelay_31_state <= 4'h0;
    end
    else begin
      UnitDelay_1_state <= UnitDelay_13_1_state;
      UnitDelay_13_state <= UnitDelay_10_state;
      UnitDelay_state <= UnitDelay_1_1_state;
      UnitDelay_1_1_state <= UnitDelay_9_state;
      UnitDelay_2_state <= UnitDelay_11_state;
      UnitDelay_3_state <= UnitDelay_5_state;
      UnitDelay_4_state <= UnitDelay_2_state;
      UnitDelay_13_1_state <= UnitDelay_8_state;
      UnitDelay_14_state <= UnitDelay_13_state;
      UnitDelay_5_state <= UnitDelay_4_state;
      UnitDelay_6_state <= UnitDelay_3_state;
      UnitDelay_14_1_state <= UnitDelay_1_state;
      UnitDelay_7_state <= UnitDelay_6_state;
      UnitDelay_8_state <= LogicalOperator_1;
      UnitDelay_9_state <= UnitDelay_7_state;
      UnitDelay_10_state <= LogicalOperator;
      UnitDelay_11_state <= 1'h1;
      UnitDelay_12_state <= _constellation_selector_re_new_T_1;
      UnitDelay_13_2_state <= _constellation_selector_im_new_T_1;
      UnitDelay_14_2_state <= UnitDelay_12_state;
      UnitDelay_15_state <= UnitDelay_13_2_state;
      UnitDelay_16_state <= UnitDelay_15_state;
      UnitDelay_17_state <= UnitDelay_14_2_state;
      UnitDelay_18_state <= UnitDelay_16_state;
      UnitDelay_19_state <= UnitDelay_17_state;
      UnitDelay_20_state <= UnitDelay_18_state;
      UnitDelay_21_state <= UnitDelay_19_state;
      UnitDelay_22_state <= UnitDelay_21_state;
      UnitDelay_23_state <= UnitDelay_20_state;
      UnitDelay_24_state <= UnitDelay_23_state;
      UnitDelay_25_state <= UnitDelay_22_state;
      UnitDelay_26_state <= UnitDelay_24_state;
      UnitDelay_27_state <= UnitDelay_25_state;
      UnitDelay_28_state <= UnitDelay_26_state;
      UnitDelay_29_state <= UnitDelay_27_state;
      UnitDelay_30_state <= UnitDelay_29_state;
      UnitDelay_31_state <= UnitDelay_28_state;
    end
  end // always @(posedge)
  assign io_Out3 = UnitDelay_state;
  assign io_Out1 =
    _Gain_1_new_T_1[10:3] + {8{UnitDelay_12_state[3]}} + _Gain_3_new_T_1[10:3]
    + {{6{UnitDelay_17_state[3]}}, UnitDelay_17_state[3:2]} + _Gain_5_new_T_1[10:3]
    + _Gain_6_new_T_1[10:3] + _Gain_7_new_T_1[10:3]
    + {{6{UnitDelay_25_state[3]}}, UnitDelay_25_state[3:2]} + _Gain_9_new_T_1[10:3]
    + {8{UnitDelay_29_state[3]}} + _Gain_11_new_T_1[10:3];
  assign io_Out2 =
    _Gain_1_1_new_T_1[10:3] + {8{UnitDelay_13_2_state[3]}} + _Gain_3_1_new_T_1[10:3]
    + {{6{UnitDelay_16_state[3]}}, UnitDelay_16_state[3:2]} + _Gain_5_1_new_T_1[10:3]
    + _Gain_6_1_new_T_1[10:3] + _Gain_7_1_new_T_1[10:3]
    + {{6{UnitDelay_24_state[3]}}, UnitDelay_24_state[3:2]} + _Gain_9_1_new_T_1[10:3]
    + {8{UnitDelay_28_state[3]}} + _Gain_11_1_new_T_1[10:3];
endmodule


==================================================
Конец файла

image.png

Подход с применением атомарных подсистем демонстрирует модульный принцип построения проекта. Головной файл выступает в роли обёртки верхнего уровня, которая инстанцирует и соединяет между собой отдельные функциональные блоки: генератор данных, модулятор и фильтр. Каждый из этих блоков является самостоятельным модулем (Gen_data, QPSK_modulator, fir) со своими чёткими интерфейсами ввода-вывода. Такая структура не только отражает логическое разделение системы на компоненты, но и значительно улучшает читаемость, сопровождаемость и возможности повторного использования кода.

In [ ]:
read_v("$(@__DIR__)/model_RX_atomic_code/model_RX_atomic.v")
Содержимое файла /user/start/examples/codegen/qpsk_and_fir_verilog_v2/model_RX_atomic_code/model_RX_atomic.v:
==================================================
/* Code generated by Engee
 * Model name: model.engee
 * Code generator: release-1.1.22
 * Date: Wed Sep 10 06:28:25 2025 GMT
 */

module model_RX_atomic(
  input        clock,
               reset,
  output [7:0] io_Out1,
               io_Out2,
  output       io_Out3
);

  wire [3:0] _QPSK_modulator_io_Out_Re;
  wire [3:0] _QPSK_modulator_io_Out_Im;
  wire       _Gen_data_io_Out1;
  wire       _Gen_data_io_Out2;
  Gen_data Gen_data (
    .clock   (clock),
    .reset   (reset),
    .io_Out1 (_Gen_data_io_Out1),
    .io_Out2 (_Gen_data_io_Out2)
  );
  QPSK_modulator QPSK_modulator (
    .io_bit_1  (_Gen_data_io_Out1),
    .io_bit_2  (_Gen_data_io_Out2),
    .io_Out_Re (_QPSK_modulator_io_Out_Re),
    .io_Out_Im (_QPSK_modulator_io_Out_Im)
  );
  fir fir (
    .clock    (clock),
    .reset    (reset),
    .io_Re    (_QPSK_modulator_io_Out_Re),
    .io_Im    (_QPSK_modulator_io_Out_Im),
    .io_valid (io_Out3),
    .io_re    (io_Out1),
    .io_im    (io_Out2)
  );
endmodule


==================================================
Конец файла

Из проведённого сравнения также следует, что написание тестового окружения (testbench) является более простой задачей именно для версии проекта без атомарных подсистем. Поскольку сгенерированный код в этом случае представляет собой единый модуль, его подключение и наблюдение за выходами не требует описания сложной иерархии. В данном примере вся логика системы содержится внутри одного блока, что позволяет напрямую отслеживать выходные сигналы.

Этим преимуществом мы и воспользуемся для проведения верификации. Так как функциональное поведение обеих версий проекта идентично, проверку корректности работы мы будем осуществлять именно на монолитной реализации.

Данный testbench выполняет две основные функции:

  1. Генерация тактового сигнала и сброса: создаётся периодический тактовый сигнал (clock) и управляющий сигнал сброса (reset) для инициализации устройства.
  2. Логирование выходных сигналов: все выходные данные устройства (io_Out3, io_Out1, io_Out2) записываются в текстовый файл "output.txt" на каждом положительном фронте тактового сигнала после снятия сигнала сброса.
In [ ]:
read_v("$(@__DIR__)/tb.v")
Содержимое файла /user/start/examples/codegen/qpsk_and_fir_verilog_v2/tb.v:
==================================================
`timescale 1ns/1ps

module model_RX_tb;
  reg clock;
  reg reset;
  wire io_Out3;
  wire [7:0] io_Out1;
  wire [7:0] io_Out2;

  model_RX dut (
    .clock(clock),
    .reset(reset),
    .io_Out3(io_Out3),
    .io_Out1(io_Out1),
    .io_Out2(io_Out2)
  );
  integer file;
  
  always #5 clock = ~clock;
  
  initial begin
    clock = 0;
    reset = 1;
    file = $fopen("output.txt", "w");
    #10 reset = 0;
    #1000;
    $fclose(file);
    $finish;
  end
  
  always @(posedge clock) begin
    if (!reset) begin
      $fdisplay(file, "%0t %b %h %h", $time, io_Out3, io_Out1, io_Out2);
    end
  end
endmodule
==================================================
Конец файла

Теперь выполним запуск исходной модели и проведём тестирование сгенерированного кода. Это позволит нам верифицировать корректность работы генератора, сравнив поведение исходной системы в среде моделирования Engee с результатами, полученными при выполнении сгенерированного Verilog-кода в симуляторе.

In [ ]:
run_model("model") # Запуск модели.
run(`iverilog -o sim tb.v model_RX.v`)# Компиляция
run(`vvp sim`)# Запуск симуляции
Building...
Progress 0%
Progress 100%
Progress 100%
Out[0]:
Process(`vvp sim`, ProcessExited(0))

Для последующего анализа необходима функция парсинга текстового файла, сгенерированного тестбенчем. Реализованная функция parse_simulation_data преобразует сырые логированные данные в формат, пригодный для анализа, сама функция возвращает три массива, готовых для анализа и построения графиков.

Алгоритм работы:

  1. Чтение данных: Функция считывает файл, разделяя строки по пробелам (временная метка, сигнал valid, шестнадцатеричные значения квадратурных компонентов)
  2. Преобразование форматов: Строки hex преобразуются в числа UInt8 через парсинг из шестнадцатеричного представления
  3. Нормализация: Ключевой этап - преобразование чисел через дополнительный код (twos complement) с последующей нормализацией на диапазон [-1.0, ~0.992] путем деления на 128
In [ ]:
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;

Теперь перейдём к сравнительному анализу. На представленных ниже графиках отображены: диаграмма созвездия, графики реальной и мнимой частей сигнала и диаграмма сигнала валидности.

In [ ]:
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))
Out[0]:
In [ ]:
plot(Re_sim, label="Re_sim", seriestype=:steppost)
plot!(Rm, label="Rm", seriestype=:steppost)
Out[0]:
In [ ]:
plot(Im_sim, label="Im_sim", seriestype=:steppost)
plot!(Im, label="Im", seriestype=:steppost)
Out[0]:
In [ ]:
plot(Val_sim, label="Valid_sim", seriestype=:steppost)
plot!(Val, label="Valid", seriestype=:steppost)
Out[0]:

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

Вывод

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

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

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