Эта статья посвящена разработке под ПЛИС в Engee, она является пошаговой инструкцией под разработку Verilog. Предположим, мы построили в Engee модель регулятора или фильтра, подали тестовые сигналы и убедились, что алгоритм работает. Теперь нужно перенести эти вычисления в программируемую логическую интегральную схему — ПЛИС (FPGA). Это необходимо, когда требуется параллельно обрабатывать поток данных и выдавать результаты в заданном темпе. Однако работающая модель еще не определяет всех деталей будущей аппаратной реализации. Нужно выбрать, как представлять числа, какие операции выполнять одновременно и где хранить результаты предыдущих шагов. В статье описывается процесс генерации Verilog-кода из моделей Engee, выбор форматов с фиксированной точкой, анализ полученного RTL и способы его верификации. Мы сосредоточимся на пути от графической модели Engee до оценки ресурсов и временных ограничений перед синтезом. Формирование битстрима зависит от целевой САПР, поэтому этот финальный этап мы рассмотрим отдельно в разделе интеграции.

Сначала разберемся, что именно мы переносим в ПЛИС. В отличие от программы для процессора, RTL-описание задает структуру цифровой схемы: вычислительные узлы, связи и регистры. Независимые узлы могут работать параллельно. Для описания схем используют языки Verilog и VHDL; полученный RTL сначала проверяют в симуляции, затем синтезируют. В статье будем использовать Verilog и разбирать его конструкции по мере появления в примерах.
Чтобы перейти от вычислений к схеме, нужно явно определить, где хранятся данные и когда они изменяются. Такое описание называют уровнем регистровых передач (Register Transfer Level, RTL). Например, сумму входных сигналов может вычислять комбинационная логика: ее результат зависит от текущих входов. Накопленную ошибку регулятора, напротив, нужно сохранять в регистре и обновлять по фронту тактового сигнала. Инструмент синтеза преобразует RTL в нетлист — список логических элементов и связей между ними.
После синтеза средства проектирования размещают элементы на кристалле, трассируют соединения и формируют битстрим для загрузки в ПЛИС. В статье рассматривается именно этот путь — от RTL-описания до конфигурации устройства.
В Engee графическую модель можно преобразовать в RTL-описание на Verilog. При этом автоматическая генерация не отменяет необходимости настройки типов данных, разрядностей и параметров хранения состояния. Эти параметры влияют на структуру полученного кода и его поведение. Первое из них — представление чисел. От него зависят диапазон значений, точность вычислений и затраты аппаратных ресурсов. Поэтому начнем с форматов данных, а затем посмотрим, как они отражаются в коде.
Типы данных с фиксированной точкой
При моделировании удобно использовать плавающую точку: она позволяет работать с числами разного масштаба без заранее заданного положения двоичной точки. В ПЛИС такую арифметику тоже можно реализовать — с помощью IP-блоков, а в некоторых семействах есть специализированная поддержка. Однако обработка мантиссы, порядка и особых значений требует дополнительной логики. Если диапазон сигнала известен заранее, часто можно выбрать более простое представление: отвести постоянное число битов под целую и дробную части. Это формат с фиксированной точкой. Выбирая разрядность и положение точки, мы согласуем нужные точность и диапазон с затратами ресурсов. В Engee формат задается функцией fi(Value, Sign, Total_bits, Fractional_bits). Процесс перевода в фиксированную точку включает три этапа: вычисление диапазона представимых значений, квантование исходного числа с учетом шага и формирование итогового битового представления. Выбрать форматы входов недостаточно: нужно проследить, что происходит с числами внутри модели. Полная сумма двух знаковых W-битных чисел с одинаковым масштабом требует W+1 бит; при разных форматах сначала согласуют дробные части. Для полного произведения складывают разрядности операндов, а число дробных битов результата равно сумме дробных разрядностей операндов. При приведении результата к более узкому формату можно потерять точность или получить переполнение. Поэтому настройки типов данных и преобразований в блоках Engee должны учитывать промежуточные значения.

Округление и переполнение
При преобразовании числа к более узкому формату важно заранее определить два правила: как округлять результат и что делать при выходе за допустимый диапазон.
Округление. Усечение отбрасывает младшие биты — как в примере с трапецией ниже, где выбираются биты Productnew_T_1[23:8]. Такой способ прост в аппаратной реализации, но создает систематическую погрешность: в дополнительном коде отбрасывание младших битов равносильно округлению к минус бесконечности, ошибка лежит в пределах от −1 LSB до 0, а в среднем составляет около −½ LSB выходного формата. Альтернативы — округление к ближайшему значению (перед отбрасыванием прибавляется ½ LSB, это дополнительный сумматор) или к ближайшему четному. Выбранный режим следует задать в формате Engee и проверить по сгенерированному RTL.
Переполнение. При переносе (wrap-around) старшие биты отбрасываются, поэтому значение может резко изменить знак. При насыщении (saturation) результат ограничивается максимальным или минимальным представимым значением. Насыщение требует дополнительной логики, но обычно обеспечивает более предсказуемое поведение. Пример такого переполнения разобран ниже, в калькуляторе трапеции. После настройки генератора выбирают подсистему и запускают преобразование. Входы и выходы выбранной подсистемы формируют интерфейс Verilog-модуля. Особенности генерации покажем на регуляторе, арифметической схеме и КИХ-фильтре.
Архитектура генератора Verilog-кода
Выбранные форматы задают параметры, которые учитываются при построении RTL. Генератор анализирует блоки модели, их параметры и соединения, а затем строит аппаратное описание соответствующих вычислений. Например, блок задержки задает хранение состояния, а арифметический блок — операцию над сигналами. Для каждого блока должен быть определен способ преобразования в RTL. Поэтому наличие блока в среде моделирования еще не означает, что генератор поддерживает его трансляцию. Процесс проходит в несколько этапов:
● Анализ графической модели: определение модульной структуры, выделение виртуальных и атомарных подсистем.
● Трансляция вычислительных блоков в аппаратные примитивы на языке Chisel.
● Преобразование Chisel в Verilog с учетом типов данных, разрядностей и тактирования.
Chisel служит промежуточным языком внутри генератора. Для обычной генерации пользователю не нужно писать на нем код: достаточно выбрать подсистему, которую требуется реализовать в ПЛИС, и указать каталог для результата. Например, генерацию можно запустить из скрипта следующим образом:
engee.generate_code("path/to/model.engee","path/to/output_dir",subsystem_name="SubSystem",target="verilog")
Параметры:
1. Путь к файлу модели;
2. Выходная директория;
3. Subsystem_name — имя подсистемы для генерации;
4. Target — целевой язык.
Тот же запуск доступен из графической модели: сначала настраиваем генератор кода, как показано ниже.

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

Примеры сгенерированного кода
ПИ-регулятор

Регулятор не только выполняет вычисления, но и хранит результат предыдущего шага. На входы setpoint и feedback поступают требуемое и измеренное значения, а выход command задает управляющее воздействие. Модуль называется pid_fixed_SubSystem, но в приведенном фрагменте реализованы только пропорциональная ветвь и накопление ошибки (это ПИ-регулятор).
Основные конструкции RTL в этом фрагменте:
● reg: описывает регистры состояния (обновляются по фронту clock);
● wire: описывает комбинационную логику;
● Явное знаковое расширение: старший бит повторяется (например, {13{_AddAccum_T[15]}}), чтобы при умножении отрицательных чисел результат был корректным.
module pi_fixed_SubSystem(
input clock, reset,
input [15:0] io_setpoint, io_feedback,
output [15:0] io_command
);
reg [15:0] UnitDelay_state;
wire [15:0] AddAccumT = io_setpoint - io_feedback;
wire [28:0] Gain2_new_T_1 = {{13{_AddAccum_T[15]}}, AddAccumT} * 29'hA4;
wire [29:0] Gainnew_T_1 = {{14{_AddAccum_T[15]}}, AddAccumT} * 30'h6000;
Сначала из заданного значения вычитается измеренное — так вычисляется ошибка регулирования. Разность считается в 16 битах без расширения, поэтому при больших значениях противоположных знаков она переполнится: для точной разности нужно W+1 бит. Затем ошибка умножается на коэффициенты двух ветвей.
Константы 0xA4 и 0x6000 — это целочисленные коды коэффициентов. Положение двоичной точки задается форматами сигналов. Если у коэффициентов 14 дробных битов, то срез [28:14] возвращает произведению ту же дробность, что и у входа. Коды соответствуют примерно 0,01 (0xA4 / 2¹⁴) и 1,5 (0x6000 / 2¹⁴). Отбрасывание 14 младших битов реализует усечение. В интегральной ветви добавленный младший ноль (1’h0) эквивалентен сдвигу влево на один разряд; этот сдвиг компенсируется отбрасыванием младшего бита на выходе, сохраняя баланс дробной части.
Для пропорциональной ветви достаточно текущей ошибки. Для интегральной ветви нужно учитывать и предыдущие значения: к сохраненному состоянию добавляется очередной вклад ошибки. В модели эту память задает блок Unit Delay, а в RTL — регистр UnitDelay_state. Процесс always @(posedge clock) обновляет регистр по положительному фронту clock. Если в этот момент активен reset, состояние обнуляется.
Здесь важно соотнести шаг модели с тактом ПЛИС. В данном фрагменте UnitDelay_state обновляется на каждом положительном фронте clock. Однако шаг дискретизации модели может составлять, например, 1 мс, тогда как ПЛИС работает на частоте 100 МГц. Если регистр обновлять на каждом фронте такого clock, накопитель будет изменяться каждые 10 нс, а не каждую миллисекунду: регулятор будет интегрировать примерно в 100 000 раз быстрее, что нарушит устойчивость системы.
Чтобы сохранить частоту модели, используется сигнал разрешения (clock enable). Ручные правки в сгенерированном RTL будут потеряны при повторной генерации, поэтому логику согласования частот лучше задавать в самой модели Engee. Пример реализации делителя для clock enable:
reg [16:0] clk_divider = 0;
wire ce_1ms = (clk_divider == 17'd99999);
always @(posedge clock) begin
if (reset) clk_divider <= 0;
else if (clk_divider == 17'd99999) clk_divider <= 0;
else clk_divider <= clk_divider + 1;
end
always @(posedge clock) begin
if (reset) UnitDelay_state <= 16'h0;
else if (ce_1ms) UnitDelay_state <= Add1Accum_T; // Обновление только по CE
end
Когда обе составляющие вычислены, их нужно сложить и получить управляющее воздействие. В коде перед сложением выбираются нужные биты промежуточных результатов:
assign io_command = Gainnew_T_1[29:14] + {_Add_1Accum_T[15], Add1Accum_T[15:1]};
Во втором слагаемом знак сохраняется, а младший разряд отбрасывается: это эквивалентно арифметическому сдвигу вправо на один бит. В следующем примере памяти нет: арифметические формулы сразу преобразуются в комбинационную логику Verilog.
Калькулятор параметров трапеции
Возьмем три знакомые формулы: периметр P, среднюю линию m и площадь S трапеции. В Engee их можно записать напрямую, а для двух значений высоты сразу вычислить вектор площадей. Эти числа служат исключительно тестовыми данными для проверки формул; геометрически они не описывают реальную трапецию (высота не может превышать боковую сторону c = 3, а здесь взяты 4 и 5).
a, b, c, d = 5, 7, 3, 4
h = [4, 5]
P = a + b + c + d
m = (a + b) / 2
S = m .* h

Те же формулы соберем из блоков Engee и зададим сигналам формат fixdt(1, 16, 8). В тестовом окружении блок Signal From Workspace последовательно подает значения высоты. Для каждого набора входных данных модель вычисляет m, P и S. Таким образом, вместо обработки целого вектора выполняется серия расчетов для отдельных значений. Это еще не аппаратный конвейер: между операциями нет регистров, а выходы зависят от текущих входов. Такая схема называется комбинационной. Порты clock и reset присутствуют в интерфейсе, но в данном фрагменте кода не используются. Для создания конвейера потребовалось бы добавить регистры между вычислительными ступенями.
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] Productnew_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 = Productnew_T_1[23:8];
endmodule
Этот модуль является чисто комбинационным: выходные сигналы зависят только от текущих входных значений, регистры состояния и интегральные составляющие в нем отсутствуют.
КИХ-фильтр

Калькулятор обрабатывает текущие входы и не хранит историю. КИХ-фильтру, напротив, нужны текущий отсчет и несколько предыдущих. В следующей модели QPSK-модулятор соединен с КИХ-фильтром на 11 коэффициентов. Каждый выход фильтра представляет собой сумму отсчетов, умноженных на соответствующие коэффициенты. Для прямой формы нужны текущий отсчет и еще 10 задержанных: блоки задержки хранят историю, умножители задают веса, а сумматоры объединяют результаты. Генератор переносит эту структуру в RTL. Количество физических умножителей и DSP-блоков зависит от архитектуры и оптимизаций синтезатора.
Пользовательские шаблоны генерации
Если генератор не умеет преобразовывать нужный блок в аппаратное описание, можно задать способ преобразования самостоятельно с помощью пользовательского шаблона — файла с расширением .cgt. В шаблоне указывают, как использовать входы, выходы и типы блока в конструкциях промежуточного языка Chisel. Так можно расширить набор поддерживаемых операций. Ниже приведен шаблон .cgt, задающий преобразование блока умножения в Chisel.
Пример шаблона для блока умножения (промежуточный язык — Chisel):
//! 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())
Графическую модель также можно собрать из скрипта, используя средства управления Engee и поддерживаемые блоки. Создание модели и генерация RTL — два отдельных шага: сначала формируют модель, затем запускают генератор. Такой подход показан в примерах:
1. RRC-filter
2. CIC-filter
Атомарные подсистемы
Шаблоны определяют, как преобразовывать отдельные блоки. По мере роста модели возникает другой вопрос: как организовать сгенерированный код? В крупной модели регулятор, модулятор и фильтр удобно выделять в отдельные модули. В Engee для этого используют атомарные подсистемы, которые при генерации RTL сохраняют собственные входы и выходы. Обычная, виртуальная подсистема помогает организовать графическую модель, но ее содержимое включается в родительский модуль. Атомарная подсистема сохраняет это разделение и в сгенерированном RTL.

Пример: монолитный и атомарный
В модели данные последовательно проходят через генератор, QPSK-модулятор и фильтр. В монолитном варианте регистры и вычислительная логика всех этих частей находятся в одном модуле. Если выделить атомарные подсистемы, Gen_data, QPSK_modulator и fir станут отдельными модулями с собственными интерфейсами. Так границы основных частей модели сохраняются и в коде.

Выбор структуры иерархии
• Монолитный вариант удобен для небольшого примера и проверки функции целиком.
• Атомарные подсистемы удобны, когда нужны отдельные интерфейсы, независимые тесты и повторное использование частей проекта. Сама по себе иерархия не задает задержки и не гарантирует определенный результат синтеза: он зависит от оптимизаций, ограничений и настроек сохранения границ модулей.
Верификация сгенерированного кода
После генерации RTL нужно убедиться, что при преобразовании сохранилось поведение исходной модели. Для этого подают одинаковые входные данные на модель Engee и на сгенерированное описание, а затем сравнивают выходы. Если схема содержит регистры, сравнение должно учитывать начальное состояние и число тактов до появления результата. В Engee для такой проверки используется Verilator: он преобразует Verilog в C++-модель, на основе которой собирается библиотека симуляции. Если включить создание верификационного скрипта, формируется проверочная модель с блоком C Function. Этот блок использует библиотеку и позволяет сравнить поведение RTL с исходной моделью.

RTL можно проверить и независимо от Engee моделей — например, с помощью Icarus Verilog. Для этого нужен тестбенч: небольшое окружение, которое подает входные данные и наблюдает выходы схемы. Icarus компилирует RTL вместе с тестбенчем, а программа vvp выполняет событийную симуляцию. Так удобно проверять сброс, последовательности отсчетов и граничные значения. В приведенном тестбенче калькулятора сначала задается высота 4, затем она меняется на 5, после чего выводятся результаты. Задержки в тестбенче определяют моменты действий в симуляции, но не показывают, за какое время вычисление будет выполнено в реальной ПЛИС.
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;
$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;
$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
На основе этих входных данных мы убедились, что данные совпали как в Julia-скрипте, так в модели Engee и в блоке C-функции с нашим тесбенчем, генерация прошла без ошибок.

Интеграция с инструментами синтеза
Симуляция позволяет проверить, правильно ли схема вычисляет результат на выбранных тестах. Но для реализации в ПЛИС нужно ответить еще на два вопроса: достаточно ли ресурсов выбранного устройства и сможет ли схема работать на требуемой частоте. Для этого RTL включают в проект САПР целевой ПЛИС, задают ограничения для тактового сигнала и интерфейсов, выполняют синтез, размещение и трассировку. В отчетах указываются затраты логики, регистров и DSP-блоков. Временной анализ показывает, выполняются ли заданные ограничения. Ниже представлена таблица основных поставщиков САПР.
|
Производитель |
Среда разработки |
Серии ПЛИС |
|
AMD/Xilinx |
Vivado Design Suite |
Spartan, Artix, Kintex, Virtex, Zynq, Versal |
|
Intel |
Quartus Prime |
MAX, Cyclone, Arria, Stratix, Agilex |
|
Lattice |
Diamond, Radiant |
iCE40, MachXO, ECP5, CrossLink |
|
Open Source |
Yosys + nextpnr |
iCE40, ECP5, Xilinx 7-series |
Ограничения генератора
Ниже перечислены основные особенности генератора кода верилог в Engee, о которых стоит помнить.
В сгенерированном коде часть блоков модели может отсутствовать: если выражение зависит только от констант, генератор или синтезатор вычисляет его заранее и заменяет готовым значением. Ненужная логика при этом удаляется. Если значение должно меняться во время работы устройства, его нужно передавать через входной порт подсистемы.
Для схемы с памятью важно определить, когда обновляются данные. В рассмотренных примерах используется единый clock и синхронный сброс. Сгенерированный RTL не содержит сведений о шаге дискретизации модели: предполагается, что частота clock уже соответствует темпу обновления данных либо между тактовым сигналом и регистрами предусмотрен сигнал разрешения clock enable. Если в модели разные блоки имеют разные шаги дискретизации, это само по себе не создает разные тактовые домены в RTL. При необходимости такие домены проектируют и проверяют отдельно, включая передачу данных между ними. Ручные изменения RTL необходимо согласовывать с моделью и учитывать при повторной генерации.
Разрядности промежуточных сигналов, режимы округления и переполнения задаются в модели Engee и учитываются при генерации RTL. Подробный порядок выбора форматов приведен в подразделе «Выбор разрядностей и контроль точности».
Заключение
В итоге генератор Engee преобразует выбранную подсистему модели в RTL-описание на Verilog. Перед синтезом необходимо проверить соответствие RTL исходной модели, включая разрядности сигналов, режимы округления, переполнение и тактирование. После этого синтез и временной анализ позволяют оценить затраты ресурсов и возможность работы схемы на требуемой частоте.
ссылка на оригинал статьи https://habr.com/ru/articles/1103786/