Документация Engee
Notebook

Возможности генерации кода Verilog

Введение

В современном мире цифровых технологий производительность и энергоэффективность вычислений зачастую определяются аппаратной платформой. Центральные процессоры (CPU) универсальны, но не всегда оптимальны для специализированных задач, таких как обработка сигналов в реальном времени, компьютерное зрение, протоколы связи или нейронные сети. Здесь на первый план выходят ПЛИС (Программируемые Логические Интегральные Схемы).

ПЛИС — это уникальный класс микросхем, внутренняя структура которых (соединения логических элементов, блоков памяти, специализированных узлов) может быть многократно перепрограммирована самим разработчиком после изготовления. Это позволяет создавать не программное, а физическое «железо», идеально заточенное под конкретный алгоритм. В результате достигаются беспрецедентные показатели: параллелизм, предсказуемое время отклика и минимальное энергопотребление.

Однако программирование ПЛИС кардинально отличается от написания кода для процессора. Проектирование на Verilog (RTL-уровень) требует специфичного, 'аппаратного' мышления, глубокого понимания временных диаграмм, тактирования и оптимизации ресурсов. Это создаёт высокий порог входа и увеличивает время разработки сложных алгоритмических модулей. Вместо последовательных инструкций здесь нужно описать аппаратуру — цифровую схему, состоящую из регистров, сумматоров, мультиплексоров, конечных автоматов, работающих параллельно. Для этого существуют Языки Описания Аппаратуры (HDL — Hardware Description Language). Двумя основными представителями являются VHDL и Verilog.

Verilog — это язык, на котором разработчик описывает, как должны быть соединены элементы и как они должны себя вести во времени. Компилятор (синтезатор) преобразует это описание в конфигурацию (bitstream) для ПЛИС, фактически «прошивая» в неё созданную вами схему. Таким образом, вы не пишете программу, а проектируете цифровое устройство.

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

Ключевые структурные элементы ПЛИС: DSP, FF, LUT и BRAM

Основу архитектуры ПЛИС составляют конфигурируемые логические блоки (CLB, или LAB в терминологии Altera/Intel). Они являются базовыми строительными элементами и состоят из более мелких модулей — срезов. Каждый срез включает в себя программируемую таблицу истинности (LUT) для реализации комбинационной логики и триггер (FF) для хранения данных. Именно эти элементы выполняют основные логические и регистровые функции, формируя ядро любой цифровой схемы.

Для высокопроизводительных вычислений в ПЛИС интегрированы специализированные DSP-блоки. Их ключевая особенность — оптимизация для операций умножения-накопления (MAC). Такие блоки способны не только быстро перемножать числа, но и прибавлять к результату третье значение, а затем накапливать сумму в аккумуляторе. Часто они также включают предварительный сумматор, позволяющий выполнять сложение перед умножением, например, по формуле (A+D)×B+C. Это делает их незаменимыми для цифровой обработки сигналов, реализации фильтров и нейронных сетей.

Встроенная блочная память (BRAM) предоставляет в ПЛИС ресурсы для хранения данных. Это синхронная двухпортовая память, позволяющая независимо читать и писать данные по двум адресам одновременно. BRAM может конфигурироваться в различных форматах и часто поддерживает встроенные функции, такие как FIFO-контроллеры для организации очередей данных, а также механизмы контроля ошибок (ECC) для повышения надёжности хранения информации.

SVG1.png

1. LUT (Look-Up Table) — Таблица настройки

  • Программируемая таблица истинности
  • Основной элемент для реализации комбинационной логики

Функции:

  • Любая булева функция
  • Маленькая распределённая память
  • Может работать как сдвиговый регистр (SRL)

2. FF (Flip-Flop) — Триггер

  • D-триггер для хранения 1 бита
  • Синхронизируется тактовым сигналом

Виды:

  • FDCE: С асинхронным сбросом (Clear)
  • FDPE: С асинхронной установкой (Preset)
  • FDRE: С синхронным сбросом

Схема:

2.png

3. DSP (Digital Signal Processor) — Блок ЦОС

  • Специализированный блок для математических операций
  • Оптимизирован для умножения-накопления

Структура типичного DSP блока:

3.png

На схеме показана типичная архитектура DSP-блока. Он не только выполняет быстрое умножение (A×B), но и имеет предсумматор (Pre-adder) для вычисления (A+D) перед умножением, а также аккумулятор для операций умножения-накопления (MAC), что критически важно для цифровых фильтров и свёрток.

Основные операции:

  • P = A × B + C — MAC операция
  • P = (A + D) × B + C — с предсумматором
  • Pattern detection — сравнение с образцом

4. BRAM (Block RAM) — Блочная память

  • Встроенная оперативная память
  • Двухпортовая синхронная память

Особенности:

  • True dual-port — оба порта независимы
  • Синхронная работа — регистры на входах/выходах
  • Встроенный FIFO контроллер — в некоторых моделях
  • ECC поддержка — контроль ошибок

Ключевые выводы:

  1. LUT — основа цифровой логики, заменяет тысячи отдельных логических вентилей
  2. FF — обеспечивают синхронность и хранение состояния
  3. DSP — ускоряют математические вычисления в 10-100 раз
  4. BRAM — предоставляют быструю локальную память без внешних чипов

Именно комбинация этих четырёх элементов позволяет ПЛИС эффективно реализовывать сложные цифровые системы, от простых контроллеров до процессов обработки сигналов.

Принцип работы Verilog на примере XOR

1. Исходный код на Verilog

module half_adder(
    input a, b,
    output y
);
    assign y = a ^ b; // XOR операция
endmodule
  • Простейший полусумматор
  • Использует оператор XOR (^)
  • Вычисляет сумму без учёта переноса

2. Синтез и оптимизация

  • Компилятор Verilog анализирует код
  • Создаёт промежуточное представление схемы
  • Оптимизирует логику для минимизации задержек

Схема реализации XOR через LUT в ПЛИС

SVG2.png

Суть: Вся логика XOR упакована в 4 конфигурационных бита LUT, которые загружаются в ПЛИС и работают как lookup table — входы становятся адресом, выход — значением из соответствующей ячейки памяти.

Обзор статьи

Данная статья — это практическое руководство, которое проведёт вас по полному циклу создания верифицированного аппаратного модуля на Verilog из алгоритмической модели. Мы не только покажем «как», но и подробно объясним «почему»:

  • Зачем нужна фиксированная точка вместо плавающей? — Ответ кроется в эффективности использования ресурсов ПЛИС.
  • Как векторный код превращается в конвейерную схему? — Это основа высокой производительности.
  • Как автоматически генерировать читаемый и оптимизированный Verilog-код? — Используя шаблоны и системные настройки.
  • Как быть уверенным, что созданная схема работает правильно? — Применяя двухэтапную верификацию с помощью Verilator и Icarus Verilog.

В качестве простого, но показательного примера мы возьмём калькулятор параметров трапеции. Этот учебный пример идеально подходит для демонстрации всего цикла модельно-ориентированного проектирования: мы проследим, как идея превращается в алгоритм, алгоритм — в конвейерную модель с фиксированной точкой, а модель — в готовую для синтеза аппаратную реализацию, прошедшую строгую верификацию. На его примере мы наглядно покажем типовые операции (сложение, умножение), которые составляют основу реальных DSP-алгоритмов, раскрыв ключевые нюансы подхода.

Вспомогательные функции

Начнём с описания функций вспомогательной библиотеки:

  • run_model(name) — загружает и выполняет модель Engee.

  • read_v(filename) — читает и выводит содержимое указанного файла, удобно для отладки.

  • extract_verilog_parameters(output) — анализирует вывод симуляции Verilog, извлекая числовые значения параметров.

  • Код в конце библиотеки проверяет наличие файла tb.v и генерирует его, если файл отсутствует.

In [ ]:
include("helper_lib.jl")

Работа с числами с фиксированной точкой в Engee

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

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

В Engee для работы с фиксированной точкой используется функция fi(), которая имеет следующий синтаксис:

x = fi(Value, Sign, Total_bits, Fractional_bits)

Параметры функции:

Параметр Описание Пример
Value Исходное десятичное число 128.9
Sign Тип знака: 1 - знаковый, 0 - беззнаковый 1
Total_bits Общая разрядность числа (биты) 16
Fractional_bits Количество бит дробной части 7

Практический пример:

In [ ]:
Value = 128.9
Sign = 1;
Total_bits = 16;
Fractional_bits = 7;

x = fi(Value, Sign, Total_bits, Fractional_bits)
println("fi: $x")
fi: 128.8984375

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

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

  2. Процесс квантования — преобразование исходного числа с учётом шага квантования

  3. Битовое представление — формирование конечного двоичного слова

SVG3.png

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

In [ ]:
display(x + fi(2.8,0,8,6))
display(3x)
fi(131.6953125, 1, 17, 7)
fi(386.6953125, 1, 80, 7)

Как мы видим, при сложении чисел с разными форматами система автоматически увеличивает разрядность на 1 бит для предотвращения переполнения. Более существенное расширение происходит при умножении на целые числа типа Int64 — в этом случае к целой части добавляется 64 бита, что приводит к значительному увеличению разрядности результата (с 16 до 80 бит).

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

Системная модель алгоритма расчёта параметров трапеции

Для начальной проверки алгоритма мы реализовали системную модель на языке Julia, которая вычисляет основные геометрические параметры трапеции: периметр (P), среднюю линию (m) и площадь (S) для разных значений высоты.

In [ ]:
a, b, c, d = 5, 7, 3, 4
h = [4,5]

P = a + b + c + d
m = (a + b) / 2
S = m .* h

println("Периметр P = $P")
println("Средняя линия m = $m")
println("Площадь S = $S")
Периметр P = 19
Средняя линия m = 6.0
Площадь S = [24.0, 30.0]

Данная реализация представляет собой векторизованную модель, где операция вычисления площади выполняется для массива значений высоты h = [4, 5] с использованием оператора .*. Модель работает с типами данных с плавающей запятой (Float64), что обеспечивает высокую точность и удобство отладки.

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

Модель под генерацию Verilog кода

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

В отличие от системной модели с плавающей запятой, для генерации Verilog-кода мы задаём входные параметры с единым типом данных fixdt(1, 16, 8) — знаковый 16-битный формат с 8 битами дробной части, одинаковый тип данных используется исключительно для упрощения, вы можете подобрать оптимальные типы при необходимости.

6.png

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

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

7.png

Теперь посмотрим на весь верхний уровень модели, он сохраняет ту же логику вычислений, что и исходный скрипт, но с ключевым отличием в типах данных. Входные параметры a, b, c, d соответствуют значениям из первоначального теста (5, 7, 3, 4), однако теперь они представлены в формате фиксированной точки fixdt(1, 16, 8).

8.png

Так же для отладки модели были включены следующие настройки.

9.png

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

10.png

Основные особенности модели:

  • Все вычисления выполняются с фиксированной точкой
  • Сохранены оригинальные математические формулы
  • Добавлена конвейерная обработка для векторных операций
  • Выходные сигналы готовы для генерации Verilog-кода

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

In [ ]:
run_model("model")
println("")

P_sim = (collect(simout["model/P"])).value[end]
m_sim = (collect(simout["model/m"])).value[end]
S_sim = (collect(simout["model/S"])).value[end]
S_sim_2 = (collect(simout["model/S"])).value[end-1]

println("Периметр P = $P_sim")
println("Средняя линия m = $m_sim")
println("Площадь S = $([S_sim, S_sim_2])")
Building...
Progress 0%
Progress 100%
Progress 100%

Периметр P = 19.0
Средняя линия m = 6.0
Площадь S = Fixed{1, 16, 8}[24.0, 30.0]

Полученные значения полностью совпадают с результатами системной модели. Успешное совпадение результатов подтверждает, что модель с фиксированной точкой корректно реализует алгоритм расчёта параметров трапеции и готова к генерации аппаратного кода.

Генерация кода и пользовательские шаблоны

Пользовательские шаблоны

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

Для таких случаев Engee предоставляет механизм пользовательских шаблонов генерации с расширением .cgt. Эти шаблоны позволяют определить, как конкретный тип блока должен быть преобразован в целевой язык описания аппаратуры.

Ключевые особенности подхода:

  1. Макроподстановки — $(...) заменяются на реальные значения из модели
  2. Условная генерация — логика зависит от свойств сигналов (разрядность, тип)
  3. Отдельные функции для повторяющихся операций
  4. Явное управление типами данных

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

In [ ]:
read_v("$(@__DIR__)/product.cgt")
Содержимое файла /user/my_projects/Demo/Verilog_s/product.cgt:
==================================================
//! BlockType = :Product
//! TargetLang = :Chisel

//! @Definitions
val $(output(1)) = Wire($(show_chisel_type(output(1))))
/*! @Step
function cast(sig)
    ".asTypeOf(FixedPoint($(sig.ty.bits).W,$(sig.ty.fractional_bits).BP))"
end
function maybe_cast_res_begin()
    output(1).ty.fractional_bits == 0 ? "(" : ""
end
function maybe_cast_res_end()
    output(1).ty.fractional_bits == 0 ? ").asSInt" : ""
end
*/
$(output(1)) := \
  $(maybe_cast_res_begin())\
    $(input(1))$(cast(input(1)))*$(input(2))$(cast(input(2)))\
  $(maybe_cast_res_end())

Заголовок

//! BlockType = :Product
//! TargetLang = :Chisel
  • BlockType = :Product — указывает, что шаблон применяется к блокам типа "Product" (умножение)
  • TargetLang = :Chisel — генерирует код на языке Chisel (Scala-based HDL), он является промежуточным звеном при генерации Verilog из моделей Engee.

Секция определений (@Definitions)

//! @Definitions
val $(output(1)) = Wire($(show_chisel_type(output(1))))
  • Создаёт выходной провод (Wire) с типом, соответствующим выходному сигналу
  • $(output(1)) — макроподстановка для имени первого выходного порта
  • $(show_chisel_type(...)) — функция, возвращающая тип данных в синтаксисе Chisel

Секция шага генерации (@Step)

/*! @Step
function cast(sig)
    ".asTypeOf(FixedPoint($(sig.ty.bits).W,$(sig.ty.fractional_bits).BP))"
end
  • Определяет вспомогательную функцию cast()
  • Добавляет приведение типа для сигналов фиксированной точки в Chisel
  • $(sig.ty.bits) \text{и} $(sig.ty.fractional_bits) — извлекают разрядность из метаданных сигнала

Условное приведение типов результата

function maybe_cast_res_begin()
    output(1).ty.fractional_bits == 0 ? "(" : ""
end
function maybe_cast_res_end()
    output(1).ty.fractional_bits == 0 ? ").asSInt" : ""
end
  • Условные функции для обработки целочисленных результатов
  • Если дробная часть равна 0, результат преобразуется в знаковое целое (.asSInt)
  • Оборачивает выражение в скобки при необходимости приведения типа

Основное выражение генерации

$(output(1)) := \
  $(maybe_cast_res_begin())\
    $(input(1))$(cast(input(1)))*$(input(2))$(cast(input(2)))\
  $(maybe_cast_res_end())
  • Генерирует операцию умножения: выход := вход1 * вход2
  • При необходимости добавляет приведения типов для обоих входов
  • Обрабатывает условное приведение типа результата

Генерация кода

Для генерации кода из модели необходима выбрать целевой язык генератора, в нашем случае это Verilog.


11.png

Следующий этап - это генерация аппаратного кода на языке Verilog. Для этого используется функция engee.generate_code(), со следующими параметрами:

Параметр Назначение Пример
Первый Путь к файлу модели "$(@__DIR__)/model.engee"
Второй Выходная директория для кода "$(@__DIR__)/V_Code"
Третий Имя генерируемой подсистемы subsystem_name="trapeze_calculator"

Так же код можно сгенерировать нажав на блок подсистемы правой кнопкой мыши.
12.png

In [ ]:
engee.generate_code(
    "$(@__DIR__)/model.engee",
    "$(@__DIR__)/V_Code",
    subsystem_name="trapeze_calculator"
)

read_v("$(@__DIR__)/V_Code/model_trapeze_calculator.v")
Содержимое файла /user/my_projects/Demo/Verilog_s/V_Code/model_trapeze_calculator.v:
==================================================
module model_trapeze_calculator(
  input         clock,
                reset,
  input  [15:0] io_a,
                io_b,
                io_h,
                io_c,
                io_d,
  output [15:0] io_m,
                io_P,
                io_S
);

  wire [15:0] __1Accum_T = io_a + io_b;
  wire [23:0] _Product_new_T_1 =
    {{9{__1Accum_T[15]}}, __1Accum_T[15:1]} * {{8{io_h[15]}}, io_h};
  assign io_m = {__1Accum_T[15], __1Accum_T[15:1]};
  assign io_P = __1Accum_T + io_c + io_d;
  assign io_S = _Product_new_T_1[23:8];
endmodule


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

Модуль принимает пять 16-битных входов (основания, высота и боковые стороны) и формирует три 16-битных выхода (средняя линия, периметр, площадь). Все вычисления оптимизированы: сумма оснований считается один раз с повторным использованием, деление на 2 выполняется сдвигом, а умножение для площади реализовано с расширением разрядности до 24 бит для предотвращения переполнения.

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

Атомарные подсистемы

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

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

13.png

Алгоритм разделён на два специализированных модуля:

  • Area — вычисляет сумму оснований, среднюю линию и площадь
  • model_atomic_trapeze_calculator — верхний уровень, добавляющий расчёт периметра
In [ ]:
engee.generate_code(
    "$(@__DIR__)/model_atomic.engee",
    "$(@__DIR__)/V_Code_atomic",
    subsystem_name="trapeze_calculator"
)

read_v("$(@__DIR__)/V_Code_atomic/model_atomic_trapeze_calculator.v")
read_v("$(@__DIR__)/V_Code_atomic/Area.v")
Содержимое файла /user/my_projects/Demo/Verilog_s/V_Code_atomic/model_atomic_trapeze_calculator.v:
==================================================
module model_atomic_trapeze_calculator(
  input         clock,
                reset,
  input  [15:0] io_a,
                io_b,
                io_h,
                io_c,
                io_d,
  output [15:0] io_m,
                io_S,
                io_P
);

  wire [15:0] _Area_io_A_add_B;
  Area Area (
    .io_h       (io_h),
    .io_b       (io_b),
    .io_a       (io_a),
    .io_A_add_B (_Area_io_A_add_B),
    .io_m       (io_m),
    .io_S       (io_S)
  );
  assign io_P = _Area_io_A_add_B + io_c + io_d;
endmodule


Содержимое файла /user/my_projects/Demo/Verilog_s/V_Code_atomic/Area.v:
==================================================
module Area(
  input  [15:0] io_h,
                io_b,
                io_a,
  output [15:0] io_A_add_B,
                io_m,
                io_S
);

  wire [15:0] __1Accum_T = io_a + io_b;
  wire [23:0] _Product_new_T_1 =
    {{9{__1Accum_T[15]}}, __1Accum_T[15:1]} * {{8{io_h[15]}}, io_h};
  assign io_A_add_B = __1Accum_T;
  assign io_m = {__1Accum_T[15], __1Accum_T[15:1]};
  assign io_S = _Product_new_T_1[23:8];
endmodule


Несмотря на структурные различия, обе версии генерируют аппаратно идентичные результаты. Разделение на модули является чисто организационным улучшением без влияния на производительность.
14.png

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

Тестирование сгенерированного кода: инструменты верификации

После генерации аппаратного кода необходима его тщательная проверка на корректность работы. В Engee доступны два основных инструмента верификации. Оба инструмента дополняют друг друга: Verilator идеален для быстрой проверки функциональности на ранних этапах, тогда как Icarus Verilog незаменим для детальной временной верификации и подготовки финальных тестов перед синтезом. Использование обоих методов гарантирует максимальную надёжность сгенерированного кода.

Verilator

Verilator — это высокопроизводительный симулятор, который преобразует Verilog-код в оптимизированные C++/C-модели. Его ключевые особенности:

  • Генерация C-кода: Verilog транслируется в эквивалентную программу на C
  • Интеграция с моделью: тестирование выполняется непосредственно внутри среды Engee
  • Автоматизация: не требует ручного написания тестового окружения
  • Высокая скорость: симуляция через скомпилированный C-код значительно быстрее интерпретируемых решений

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

Для автоматической генерации C-функции нам необходимо включить эту настройку в параметрах генерации кода.

15.png

Далее при генерации кода автоматически будет создан скрипт для генерации модели с блоком C-функции, в нашем случае файл называется model_trapeze_calculator_verification.jl, выполним его запуск и посмотрим на результаты.

In [ ]:
include("$(@__DIR__)/V_Code/model_trapeze_calculator_verification.jl")
16.png

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

17.png
In [ ]:
run_model("model_verification")
println("")

P_sim_C = (collect(simout["model_verification/test.P"])).value[end]
m_sim_C = (collect(simout["model_verification/test.m"])).value[end]
S_sim_C = (collect(simout["model_verification/test.S"])).value[end]
S_sim_2_C = (collect(simout["model_verification/test.S"])).value[end-2]

println("Периметр P = $P_sim_C")
println("Средняя линия m = $m_sim_C")
println("Площадь S = $([S_sim_C, S_sim_2_C])")
Building...
Progress 0%
Progress 100%
Progress 100%

Периметр P = 19.0
Средняя линия m = 6.0
Площадь S = Fixed{1, 16, 8}[24.0, 30.0]

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

Icarus Verilog

Icarus Verilog — это интерпретирующий симулятор, выполняющий Verilog-код непосредственно. Его отличительные черты:

  • Прямая интерпретация: код выполняется без промежуточного преобразования
  • Требует тестбенч: необходимо создание полного тестового окружения на Verilog
  • Детализированная отладка: позволяет глубоко анализировать временные диаграммы
  • Стандартная совместимость: строгое соответствие стандартам Verilog

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

In [ ]:
read_v("$(@__DIR__)/tb.v")
Содержимое файла /user/my_projects/Demo/Verilog_s/tb.v:
==================================================
module tb_trapeze;
reg clk=0,rst=1;
reg[15:0] a=5<<8,b=7<<8,h=4<<8,c=3<<8,d=4<<8;
wire[15:0] m,P,S;
model_trapeze_calculator dut(clk,rst,a,b,h,c,d,m,P,S);
always #5 clk=~clk;
initial begin
    #15 rst=0;
    #10; // h=4
    $display("h=4: m=%0.2f P=%0.2f S=%0.2f", 
            $itor($signed(m))/256.0,
            $itor($signed(P))/256.0,
            $itor($signed(S))/256.0);
    h=5<<8;
    #10; // h=5
    $display("h=5: m=%0.2f P=%0.2f S=%0.2f", 
            $itor($signed(m))/256.0,
            $itor($signed(P))/256.0,
            $itor($signed(S))/256.0);
    #10 $finish;
end
endmodule

Данный тестбенч выполняет полную проверку сгенерированного модуля model_trapeze_calculator с использованием симулятора Icarus Verilog.\

Тестбенч инициализирует систему с активным сбросом и входными данными в формате фиксированной точки (сдвиг на 8 бит для дробной части). После снятия сброса через 15 единиц времени выполняется последовательное тестирование для двух значений высоты трапеции: сначала для h=4, затем для h=5.

Для работы с числами фиксированной точки используется сдвиг <<8, а преобразование результатов обратно в десятичный формат выполняется через $itor($signed(...))/256.0. Тактовый сигнал генерируется с периодом 10 единиц времени, а временные задержки управляют последовательностью тестовых воздействий.

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

Процесс тестирования включает компиляцию и выполнение Verilog-кода с последующей обработкой выходных данных. Функция extract_verilog_parameters анализирует вывод симуляции и извлекает вычисленные значения параметров трапеции для обеих высот.

In [ ]:
run(`iverilog -o sim tb.v model_trapeze_calculator.v`)
output = read(`vvp sim`, String)
m_verilog, P_verilog, S4_verilog, S5_verilog = extract_verilog_parameters(output);
println(output)
h=4: m=6.00 P=19.00 S=24.00
h=5: m=6.00 P=19.00 S=30.00

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

Вывод

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

In [ ]:
using DataFrames
df = DataFrame(
    Тест = ["Скрипт Julia", "Модель Engee", "C-код Verilator", "Тестбенч Verilog"],
    P = [P, P_sim, P_sim_C, P_verilog],
    m = [m, m_sim, m_sim_C, m_verilog],
    S_h4 = [S[1], S_sim, S_sim_C, S4_verilog],
    S_h5 = [S[2], S_sim_2, S_sim_2_C, S5_verilog]
)
Out[0]:
4×5 DataFrame
RowТестPmS_h4S_h5
StringFloat64Float64Float64Float64
1Скрипт Julia19.06.024.030.0
2Модель Engee19.06.024.030.0
3C-код Verilator19.06.024.030.0
4Тестбенч Verilog19.06.024.030.0

Исходя из этой таблицы мы можем сделать следующие выводы:

  1. Переход от чисел с плавающей точкой в скрипте Julia к фиксированной точке в аппаратной реализации выполнен без потери точности для данного алгоритма.

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

  3. Успешное прохождение тестов как через промежуточное C-представление (Verilator), так и через нативную симуляцию Verilog (Icarus) исключает случайные совпадения.

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

Использование Engee и подобных инструментов существенно снижает порог входа и ускоряет разработку для ПЛИС. Алгоритмисты и инженеры-системщики получают возможность напрямую участвовать в создании аппаратных ускорителей, фокусируясь на логике и оптимизации алгоритма, а не на рутинном и подверженном ошибкам переводе алгоритмов на HDL. Показанный подход — это мост между высокоуровневым моделированием и эффективным 'железом', обеспечивающий надёжность за счёт автоматической генерации и строгой верификации.

Примечание

В данном примере намеренно были опущены такие этапы как синтез и имплементация так-как они на прямую зависят от аппаратной платформы.

Синтез (Synthesis) — это процесс преобразования описания цифровой схемы на языке HDL (Verilog/VHDL) в технологический нетлист — список конкретных логических элементов (LUT, триггеров, блоков памяти) и соединений между ними. Синтезатор анализирует код, оптимизирует логические выражения и отображает их на примитивы целевой архитектуры ПЛИС. На этом этапе определяется, какие ресурсы кристалла будут использованы и как они связаны логически.

Имплементация (Implementation) — следующий этап, когда синтезированный нетлист физически размещается на конкретном кристалле ПЛИС. Процесс включает размещение (placement) — распределение логических элементов по конкретным физическим ячейкам чипа, и трассировку (routing) — установление реальных соединений через программируемые переключатели. Имплементация напрямую зависит от конкретной модели ПЛИС и генерирует итоговый битовый поток (bitstream) для программирования устройства.

Производители ПЛИС и среды разработки

Производитель Основная среда разработки Ключевые серии ПЛИС
AMD/Xilinx Vivado Design Suite (для 7-series и новее), ISE (для старых серий) Spartan, Artix, Kintex, Virtex, Zynq, Versal
Intel Quartus Prime Design Suite MAX, Cyclone, Arria, Stratix, Agilex
Lattice Semiconductor Lattice Diamond, Lattice Radiant iCE40, MachXO, ECP5, CrossLink
Microchip Libero SoC Design Suite IGLOO, ProASIC3, PolarFire, RTG4
Open Source Yosys + nextpnr (независимые инструменты) Поддержка Lattice iCE40/ECP5, Xilinx 7-series

Vivado и Quartus Prime являются наиболее распространёнными профессиональными средами, предоставляющими полный цикл разработки — от ввода кода до генерации битстрима. Lattice предлагает бесплатные инструменты для своих ПЛИС, а открытый стек Yosys/nextpnr позволяет работать с некоторыми архитектурами без проприетарного ПО. Выбор среды определяется конкретной ПЛИС.