Всем привет! Я Константин Павлов, старший инженер по разработке систем на кристалле. Работаю в группе прототипирования в компании YADRO, занимаюсь отладкой на ПЛИС исходного кода, который затем будет работать в ASIC.

В статье я расскажу об итеративной (многократной) сборке проектов ПЛИС. Зачем она нужна и какими способами — вендорскими и самописными — ее возможно реализовать. А еще на примерах из практики покажу, каких впечатляющих результатов можно добиться, используя итеративную сборку.

Что такое итеративная сборка

Разработчики, использующие языки программирования общего назначения, например C или Python, привыкли, что, если код написан синтаксически верно, компилятор предсказуемо качественно его соберет. Различия в производительности и эффективности работы программ, которую обеспечивают конкурирующие компиляторы, несущественны для большинства применений.

При использовании языков описания аппаратуры (HDL) дело обстоит совершенно иначе. Каждый, кто сколько-нибудь серьезно занимался разработкой на HDL, не раз упирался в ограничения выбранной аппаратуры. Или оказывался в ситуации, когда IDE вообще не может собрать проект без нарушения временных ограничений (в просторечии — «времянки не сходятся»). То есть проект собрался, но практического смысла в полученных артефактах нет: они неработоспособны.

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

  • Архитектура выбранного семейства ПЛИС.

Серии ПЛИС отличаются по производительности, емкости, доступным вариантам корпуса. Даже в рамках одной серии от одного производителя существуют чипы с различным спидгрейдом и различными достижимыми максимальными частотами. Количество тактовых ресурсов, линий IO, специализированных аппаратных блоков всегда конечно и может вдруг оказаться недостаточным для выполнения поставленной задачи.

  • Среда разработки (IDE).

Выбрав модель ПЛИС, разработчик зачастую вынужден использовать определенную безальтернативную среду разработки, предоставленную вендором. IDE разных производителей по-разному реализуют процесс сборки, имеют различный функционал и дают разные возможности для анализа дизайна. Необходимость уметь пользоваться всевозможными наборами инструментов требует от разработчика дополнительных усилий и кругозора.

  • Параметры модулей проекта.

HDL позволяют описывать модули параметрически. Например, условный двоичный счетчик может иметь параметр N, определяющий его ширину. Такой счетчик без проблем имплементируется в проекте ПЛИС, если параметр N равен 8. Но, если решаемая задача требует реализовать счетчик шириной N равной 1024, этот абсолютно корректный код можно будет воплотить в жизнь только при условии снижения тактовых частот. А это часто недопустимо исходя из требований решаемой задачи.

  • Констрейны.

Файлы ограничений в формате SDC/XDC — стандарт в индустрии. Их необходимо причислять к исходным файлам проекта ПЛИС наряду с HDL-кодом, поскольку они существенно влияют на то, как IDE будет интерпретировать HDL, на каких низкоуровневых примитивах и в каком месте на кристалле реализует логические функции. В крупных проектах ПЛИС работа по наполнению и оптимизации файлов констрейнов является наиболее трудным и творческим этапом во всем маршруте разработки.

Это только основные факторы, влияющие на успешность сборки проекта ПЛИС. Мы видим, что «голые» HDL-исходники — это далеко не все, что необходимо для успешной сборки. FPGA-разработчик сталкивается с огромной неопределенностью, вызванной десятками факторов, на всех этапах развития проекта.

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

Задачи, которые помогает решить И. С.:

  • Выбор электронной компонентной базы.

Такая задача возникает на раннем этапе разработки проекта, когда мы только прорабатываем его архитектуру и состав аппаратных средств. И. С. может помочь с выбором оптимальных по характеристикам моделей ПЛИС для использования в целевом проекте. Запустив сборку заготовки будущего проекта под несколько моделей ПЛИС, можно проанализировать, какая из них будет наилучшим образом удовлетворять требованиям.

  • Проверка синтезируемости кода во всем диапазоне перестройки параметров.

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

  • Контроль отсутствия временных нарушений.

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

  • Выбор наилучшей стратегии синтеза и имплементации.

Такая задача обычно возникает на завершающем этапе разработки проекта, когда все необходимые функции реализованы и отлажены по отдельности. Разработчику необходимо собрать проект целиком, поэтому заполненность ПЛИС близка к 100%. Возникает потребность не просто «как-то» собирать проект с дефолтными настройками, а делать это эффективно, за минимальное время, при этом без ухудшения качества. И. С. на данном этапе может помочь проварьировать стратегии имплементации, доступные в IDE, и выбрать оптимальную для конкретного проекта.

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

Способы организации итеративной сборки

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

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

Второй способ, который можно предложить, — это использование подхода Design runs. Он позволяет скомпилировать проект с разными стратегиями сборки или под разные модели ПЛИС, не выходя за пределы единственного открытого дизайна в окне IDE. Недостаток этого подхода в том, что он доступен только в среде AMD/Xilinx Vivado. Design runs — встроенная стандартная функция Vivado, но способ по-прежнему полуручной, поскольку детальный анализ и сравнение результатов сборки не автоматизированы.

В среде Intel/Altera Quartus есть похожая, но не полностью аналогичная функция, которая называется SEED. SEED — это числовой параметр проекта, при изменении которого меняется и расстановка примитивов проекта на кристалле. Меняя SEED, можно собрать «другой вариант» проекта с другими максимальными рабочими частотами (Fmax). Собрав несколько аналогичных проектов, можно выбрать тот, который обеспечивает наилучшие характеристики. Аналогично Design runs, это полуручной способ. Детальный анализ и сравнение результатов сборки тоже не автоматизированы. 

Следующий способ — это инкрементальная компиляция. С ней мы можем переиспользовать для сборки результаты предыдущих запусков. Если от запуска к запуску изменения проекта незначительны, IDE будет собирать заново только изменившиеся части дизайна. Так мы можем на основе одной «удачной» сборки получать ее доработанные варианты с улучшенными характеристиками. Инкрементальная компиляция доступна во всех известных IDE для разработки ПЛИС, однако реализована везде по-разному. Способ нельзя назвать удобным для реализации И. С. Да и детальный анализ и сравнение результатов сборки здесь тоже никак не автоматизированы.

Утилита Quartus Design Space Explorer

От простых и неэффективных методов многократной сборки FPGA перейдем к тем, которые заслуживают большего внимания. В частности — к утилите dse.exe из пакета Intel/Altera Quartus. Ее окно показано на рисунке.

Скриншот из Quartus Prime Design Space Explorer II. Источник.

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

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

Пример использования dse.exe

Однажды я разрабатывал среднего размера FPGA-проект ( ~10 килоLUT ), в котором была критически важна производительность преобразования потока данных. Я считал, что алгоритм обработки сделан уже достаточно хорошо, поэтому увеличивать производительность планировал за счет повышения тактовой частоты. Я задался вопросом, а можно ли получить бо́льшие частоты — Fmax в терминах IDE Quartus — просто изменяя внешние условия, например, операционную систему и версию среды. Здесь-то мне и пригодилась утилита dse.exe.

Я сделал несколько виртуальных машин с Windows 7 и Linux Manjaro, установил на них различные версии Quartus Lite Edition и воспользовался Design Space Explorer, чтобы перебирать значения SEED. Это позволило получить статистику по сборке одного и того же FPGA-проекта для каждой из комбинаций «операционная система/версия IDE». Чтобы среда собирала проект с максимальными возможными частотами Fmax, в констрейнах была назначена заведомо недостижимая тактовая частота дизайна в 1 ГГц.

Полученные значения Fmax приведены в таблице и на графиках ниже.

Таблица значений Fmax для каждого запуска сборки.
График значений Fmax для каждого запуска сборки.

Это совсем не очевидно, но, оказывается, достижимая рабочая частота собранного проекта ПЛИС существенно отличается в зависимости от внешних, совершенно искусственных факторов.

Худший результат сборки — проект работает максимум на 207 МГц. Лучший из полученных результатов —  проект работает на частоте 374 МГц. Из раза в раз на Quartus версии 13.1 на Windows получаются плохие результаты. А если взять Linux и собирать проект в Quartus 20.1 с агрессивными настройками, тот же самый код компилируется с Fmax практически в два раза выше!

Это реальный пример, какую пользу можно получить от итеративной сборки. Сделав несколько запусков компиляции в различных условиях, мы выявили закономерности, позволяющие оптимизировать параметры дизайна. В данном случае мы получили почти двукратный прирост в производительности прошивки ПЛИС, просто поменяв машину, на которой собираем релиз!

Мой подход к итеративной сборке

Design Space Explorer — отличная утилита, но она работает только с проектами для ПЛИС Intel/Altera. Прямых аналогов от других производителей я не знаю. 

Мне захотелось создать инструмент, который бы работал похожим образом, но был более универсален. Он должен предоставлять возможность делать копии базового проекта ПЛИС, менять параметры запуска компиляции и итеративно собирать проекты. А также иметь средства для автоматизированного анализа результатов.

Идею я реализовал в виде шаблона для итеративной сборки FPGA-проектов. Он состоит из нескольких HDL-исходников и вспомогательных скриптов. Любой более сложный проект можно будет сделать на основе шаблона и поэкспериментировать с И. С. Я разработал это решение с расчетом, чтобы его можно было легко адаптировать под любую IDE для разработки ПЛИС и получить максимальную гибкость в применении. 

Рассмотрим вариант шаблонного проекта, предназначенный для итеративной сборки ПЛИС AMD/Xilinx. Структура файлов проекта следующая:

├── Makefile
└── base
    ├── Makefile
    ├── scripts
    │   ├── compile_vivado.tcl
    │   ├── get_fmax_vivado_special.tcl
    │   └── set_prj_vivado.tcl
    └── src
        ├── define.vh
        ├── fast_counter.sv
        ├── main.sv
        └── timing.xdc

Проект состоит из головного, или «внешнего», makefile и директории базового проекта. Базовый проект base содержит директорию src/ с исходными файлами HDL, директорию scripts/ с вспомогательными TCL-скриптами, и «внутренний» makefile.

«Внешний» makefile

«Внешний» makefile нужен, чтобы скопировать основной проект ПЛИС необходимое количество раз. В нем также определяется переменная VAR, которая будет определять различия дочерних проектов друг от друга. В шаблоне реализован последовательный перебор натуральных чисел от VAR_MIN до VAR_MAX, однако ничто не мешает реализовать любой, сколь угодно сложный закон перестройки параметров. Переменная внедряется в дочерние проекты через автогенерируемый файл define.vh. Для каждого значения переменной VAR будет создан и независимо собран дочерний проект.

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

Листинг «внешнего» makefile
VAR_START = 5
VAR_STOP = 64
VAR = $(shell seq $(VAR_START) ${VAR_STOP})

JOBS = $(addprefix job,${VAR})


.PHONY: all report clean


all: report
	echo '$@ success'

${JOBS}: job%:
	mkdir -p ./$*; \
	cp -r ./base/* ./$*; \
	echo "// Do not edit. This file is auto-generated" > ./$*/src/define.vh; \
	echo "\`define WIDTH $*" >> ./$*/src/define.vh; \
	$(MAKE) -C ./$* all

fmax.csv: ${JOBS}
	echo '# FMAX summary report for iterative compilation' > fmax.csv; \
	echo 'var, clk1, clk2' >> fmax.csv; \
	export BC_LINE_LENGTH=0; \
	v=$(VAR_START);	while [ "$$v" -le $(VAR_STOP) ]; do \
		echo $$v | xargs echo -n >> fmax.csv; \
		echo -n ', ' >> fmax.csv; \
		(cat ./$$v/test.runs/impl_1/main_timing_summary_routed.rpt | \
			grep -A6 '| Intra Clock Table' | tail -n1 | gawk {'print $$2'} | \
			xargs echo -n '1000/(1-'; echo ')') | bc | xargs echo -n >> fmax.csv; \
		echo -n ', ' >> fmax.csv; \
		(cat ./$$v/test.runs/impl_1/main_timing_summary_routed.rpt | \
			grep -A7 '| Intra Clock Table' | tail -n1 | gawk {'print $$2'} | \
			xargs echo -n '1000/(1-'; echo ')') | bc | xargs echo -n >> fmax.csv; \
		echo >> fmax.csv; \
		v=$$((v+1)); \
	done

report: fmax.csv
	cat fmax.csv

clean:
	v=$(VAR_START);	while [ "$$v" -le $(VAR_STOP) ]; do \
		rm -rfv $$v; \
		rm -rfv fmax.csv; \
		v=$$((v+1)); \
	done

«Внутренний» makefile

«Внутренний» makefile предназначен для сборки конкретного дочернего проекта. В нем указывается имя проекта, модель ПЛИС и состав исходных файлов. Этот файл необходимо заполнить один раз при подготовке базового проекта.

Листинг «внутреннего» makefile
PROJ = test

# selecting largest pin count part in the family
PART = xc7a200tffv1156-1

# # selecting target part
# PART = xc7k325tffg900-2

SRCS = src/main.sv \
       src/define.vh \
       src/fast_counter.sv

XDCS = src/timing.xdc

SCRIPTS = scripts/allow_undefined_ports.tcl

#------------------------------------------------------------------------------

.PHONY: all info setup compile clean

all: setup compile

info:
	echo -e \\n '    Project name: ' $(PROJ) \
	        \\n '    Part:         ' $(PART) \
		 \\n '    Sources:      ' $(SRCS) \
		 \\n '    Constraints:  ' $(XDCS) \
		 \\n '    Scripts:      ' $(SCRIPTS)


setup: .setup.done

.setup.done: $(SRCS) $(SCRIPTS)
	# passing args as files
	echo $(PROJ) > .proj
	echo $(PART) > .part
	echo $(SRCS) | tr -s " " "\012" > .srcs
	echo $(XDCS) | tr -s " " "\012" > .xdcs
	echo $(SCRIPTS) | tr -s " " "\012" > .scripts
	# processing
	vivado -mode batch -source scripts/set_prj_vivado.tcl
	# cleaning
	rm -f .proj
	rm -f .part
	rm -f .srcs
	rm -f .xdcs
	rm -f .scripts


compile: .compile.done

.compile.done: .setup.done
	vivado -mode batch -source scripts/compile_vivado.tcl


clean:
	$(shell ./clean_vivado.sh )
	rm -f $(PROJ).xpr
	rm -f .part
	rm -f .srcs
	rm -f .xdcs
	rm -f .scripts

Вспомогательные TCL-скрипты 

В среду Vivado встроен скриптовый язык TCL. Поэтому для сборки проектов потребовалось подготовить несколько вспомогательных TCL-скриптов.

В скрипте set_prj_vivado.tcl мы считываем параметры проекта, прокинутые из «внутреннего» makefile, создаем проект Vivado под выбранную модель ПЛИС, добавляем исходники в проект.

Код скрипта set_prj_vivado.tcl
# quickly read args, and don't even close file handles :)
set proj [split [read [open ".proj" r]] "\n"]
set part [split [read [open ".part" r]] "\n"]
set srcs [split [read [open ".srcs" r]] "\n"]
set xdcs [split [read [open ".xdcs" r]] "\n"]
set scripts [split [read [open ".scripts" r]] "\n"]

# remove last empty elements
set idx [lsearch ${proj} ""]
set proj [lreplace ${proj} ${idx} ${idx}]
set idx [lsearch ${part} ""]
set part [lreplace ${part} ${idx} ${idx}]
set idx [lsearch ${srcs} ""]
set srcs [lreplace ${srcs} ${idx} ${idx}]
set idx [lsearch ${xdcs} ""]
set xdcs [lreplace ${xdcs} ${idx} ${idx}]
set idx [lsearch ${scripts} ""]
set scripts [lreplace ${scripts} ${idx} ${idx}]

#puts "proj = [list ${proj}]"
#puts "part = [list ${part}]"
#puts "srcs = [list ${srcs}]"
#puts "xdcs = [list ${xdcs}]"
#puts "scripts = [list ${scripts}]"

# for example, xc7k325tffg900-2
create_project -force ${proj} . -part ${part}

if {${srcs} ne ""} {
  add_files -fileset sources_1 ${srcs}
  update_compile_order -fileset sources_1
}

if {${xdcs} ne ""} {
  add_files -fileset constrs_1 ${xdcs}
}

if {${scripts} ne ""} {
  add_files -fileset utils_1 ${scripts}
}

set aup_script "scripts/allow_undefined_ports.tcl"
if {${aup_script} in ${scripts}} {
  set_property STEPS.WRITE_BITSTREAM.TCL.PRE [get_files ${aup_script} -of [get_fileset utils_1]] [get_runs impl_1]
}

exec touch .setup.done

Скрипт сompile_vivado.tcl нужен, чтобы запустить синтез и имплементацию. Затем нам нужно просто дождаться завершения процесса.

Код скрипта сompile_vivado.tcl
open_project test.xpr

reset_runs impl_1
launch_runs impl_1
wait_on_run impl_1

#open_run impl_1
#write_bitstream -force test.bit

# TODO create marker file only when Vivado is successful
exec touch .compile.done

Запуск итеративной сборки нужно производить из корня проекта командой make -j. Компиляция дочерних проектов происходит параллельно. Утилита make контролирует, сколько независимых процессов компиляции будут выполняться одновременно. Как только один из процессов сборки завершится, make запустит на сборку очередной дочерний проект.

Преимущества предложенного решения

  • Компактный код, который легко адаптировать под задачу. При необходимости шаблон легко корректировать и поддерживать. Например, сделать итерации не по одному, а по двум параметрам в дочерних проектах.

  • Простота. Мы не применяем никаких изощренных техник, нам не нужно устанавливать или изучать дополнительные технологии. Утилита make широко известна и активно используется в индустрии, работает везде.

  • Предложенный подход актуален при работе с любой средой проектирования, с ПЛИС любого производителя. В статье описан вариант шаблона для Vivado, но можно поменять запуск Vivado на запуск Quartus, Gowin EDA, IceCube и любого другого САПР или тулчейна. Мы получили универсальный инструмент для И.С., не заточенный на определенного вендора или конкретную серию ПЛИC.

  • Встроенные возможности по анализу отчетов. Команды, которые мы можем использовать в make-файле, универсальны. В нашем распоряжении весь арсенал текстовых утилит типа sed, awk и прочих. У нас нет так называемого vendor lock: мы используем технологии, которые одинаково работают с любым проектом и на любой аппаратной платформе.

Шаблон для итеративной сборки FPGA-проектов, который мы рассмотрели, полностью доступен в моем GitHub-репозитории. Вариант того же шаблона, но адаптированного под среду Quartus, вы найдете по этой ссылке.

Всем удачи! Исследуйте свои проекты ПЛИС с помощью итеративной сборки.