Добрый день!
В прошлой статье мы рассматривали настройку Buildroot для кастомной платы на базе Zynq-7000. В результате мы получили минимальную Linux-систему, настроили аппаратную платформу в Vivado и успешно загрузили собранный образ на целевое устройство.
До этого момента PL-часть почти не трогали. На первых этапах bring-up это нормально: bitstream обычно шьют через JTAG или кладут в boot раздел, чтобы PL конфигурировалась ещё до старта Linux.
Такой подход удобен для первоначальной отладки, но не всегда подходит для реальных проектов, если планируется в процессе работы менять bitstream. Для этого в Linux есть подсистема FPGA Manager.
На практике часто возникает необходимость конфигурировать PL уже после запуска Linux.
Например:
-
обновлять FPGA-логику без перепрошивки всей системы;
-
динамически подключать различные аппаратные модули;
-
использовать несколько вариантов bitstream для разных режимов работы устройства;
-
выполнять частичную или полную реконфигурацию FPGA во время работы системы.
Поддержка partial reconfiguration зависит от конкретного драйвера, версии ядра и vendor flow. В этой статье рассматривается обычная full reconfiguration на Zynq-7000.
В этой статье мы разберём:
-
зачем может понадобиться Device Tree Overlay;
-
какие kernel options нужны для FPGA Manager и overlay-сценариев;
-
как загрузить bitstream после старта Linux;
-
как завернуть загрузку bitstream в C++-утилиту.
В качестве примеров будем использовать платформу Zynq-7000 и Buildroot, настроенный в предыдущей статье.
В этой статье фокус будет на Zynq-7000. Для ZynqMP общий подход похож, но детали загрузки и требования к firmware-стеку отличаются, поэтому их лучше разобрать отдельно.
В этой статье я не буду разбирать полноценный overlay. Здесь покажу только, как fpga_loader оборачивает configfs-интерфейс. Формирование DTS overlay, нюансы использования стоит рассмотреть отдельно, т.к. это довольно большое количество материала.
Перед полной реконфигурацией PL нужно убедиться, что PS/Linux не обращается к устройствам в PL: остановить userspace, отвязать/выгрузить драйверы, отключить DMA, quiesce AXI-транзакции, убрать overlay или хотя бы гарантировать отсутствие активных обращений. Иначе можно получить bus hang, зависание ядра или device timeout.
Полезные материалы по теме:
FPGA Manager
Перед дальнейшими действиями полезно понять, как Linux вообще программирует FPGA.
Исторически загрузка bitstream выполнялась загрузчиком или внешним программатором. Однако по мере распространения SoC FPGA, таких как Zynq и Zynq UltraScale+, в ядре Linux появился универсальный фреймворк FPGA Manager.
FPGA Manager — это общий слой в ядре Linux для загрузки FPGA image. Он прячет платформенные детали за единым интерфейсом: userspace пишет имя firmware, а дальше уже конкретный драйвер решает, как именно заливать image в FPGA.
С точки зрения пользователя всё выглядит достаточно просто:
Bitstream ↓FPGA Manager ↓Драйвер FPGA ↓ PL
Пользователь помещает bitstream в файловую систему и инициирует загрузку через sysfs-интерфейс FPGA Manager. Дальнейшая работа выполняется драйвером, специфичным для конкретной платформы.
В случае Zynq-7000 драйвер взаимодействует с блоком PCAP (Processor Configuration Access Port), через который процессорная система может программировать PL напрямую без участия JTAG.
Пишем минимальную PL-прошивку для проверки
Чтобы попробовать загрузку bitstream через FPGA Manager, сначала сделаем минимальную прошивку для PL.
Я не RTL-разработчик, поэтому пример намеренно минимальный: задача здесь не показать идеальный Verilog/SystemVerilog, а получить простой bitstream для проверки FPGA Manager.
Первым делом давайте включим тактирование PL от PS, это можно сделать тут
Zynq Processing System -> Clock Configuration -> FCLK_CLK0
IO PLL, requested freq. 50 MHz

Напишем небольшой модуль, который будет просто мигать диодом. Никакой полезности он не представляет, просто нужен для того, чтобы показать нам, что хоть что-то работает.
module blink #(parameter int unsigned CLK_HZ = 50_000_000,parameter int unsigned BLINK_HZ = 1,parameter bit LED_ACTIVE_HIGH = 1'b1)(input logic clk_i,input logic rst_n,output logic led_o);initial beginif (BLINK_HZ < 1) $fatal(1, "BLINK_HZ must be >= 1");if (CLK_HZ < 1) $fatal(1, "CLK_HZ must be >= 1");if (CLK_HZ < (BLINK_HZ * 2)) $fatal(1, "CLK_HZ too low for requested BLINK_HZ");endlocalparam int unsigned COUNT_MAX = CLK_HZ / (BLINK_HZ * 2);localparam int unsigned DIV_MAX = COUNT_MAX - 1;localparam int unsigned CNT_W = (COUNT_MAX <= 1) ? 1 : $clog2(COUNT_MAX);logic [CNT_W-1:0] cnt;logic led_raw;always_ff @(posedge clk_i or negedge rst_n) beginif (!rst_n) begincnt <= '0;led_raw <= 1'b0;end else if (cnt == DIV_MAX[CNT_W-1:0]) begincnt <= '0;led_raw <= ~led_raw;end else begincnt <= cnt + 1'b1;endendalways_comb beginled_o = (LED_ACTIVE_HIGH) ? led_raw : ~led_raw;endendmodule
Далее попробуем собрать модуль, как видим синтез проходит.
Теперь нам нужно будет добавить этот модуль в BlockDesign, это можно сделать кликнув ПКМ -> Add module.
Однако, у меня не получилось добавить SystemVerilog модуль, и поэтому пришлось написать небольшой wrapper на Verilog, после чего все успешно добавилось.
module blink_bd ( input wire clk_i, input wire rst_n, output wire [1:0] led_o);wire led_blink;blink #( .CLK_HZ(50000000), .BLINK_HZ(1), .LED_ACTIVE_HIGH(1'b1)) u_blink ( .clk_i(clk_i), .rst_n(rst_n), .led_o(led_blink));assign led_o[0] = led_blink;assign led_o[1] = ~led_blink;endmodule
Добавляем модуль (ПКМ -> Add module)

Соединяем клоки, reset, дальше нам нужно вывести наружу led_o
Тыкаем ПКМ led_o, make external.
После чего BlockDesign будет выглядеть следующим образом.

В констрейнах я укажу используемые выводы для светодиодов (мы используем две штуки):
## LEDs (PL pins)set_property IOSTANDARD LVCMOS33 [get_ports {led_o_0[0]}]set_property IOSTANDARD LVCMOS33 [get_ports {led_o_0[1]}] set_property PACKAGE_PIN V15 [get_ports {led_o_0[0]}]set_property PACKAGE_PIN V13 [get_ports {led_o_0[1]}]
После чего можно собирать проект. Важно не забыть, что нам нужно обновить наш xsa если вы ещё этого не сделали, поскольку в предыдущей статье мы отключали тактирование, и если мы оставим наши ps_init, то тактовый сигнал на описанный выше модуль приходить не будет.
Синтезируем, имплементируем, вроде бы что-то собралось.

Генерируем битстрим.
Дальше нам нужно экспортировать xsa файл вместе с bitstream. Сделать это можно нажав вот сюда:
Export hardware, Include bitstream

Дальше нам нужно подкинуть наш xsa в подготовленный ранее buildroot. А именно — ps_init_gpl (c + h) из xsa файла, и сгенерировать новый DTS.
Обратите внимание: для Zynq-7000 в Xilinx/AMD FPGA Manager flow в PL обычно загружается не исходный
.bit, а бинарный.bin, подготовленный из.bitс помощьюbootgen. Именно такой.binфайл затем передаётся FPGA Manager черезfirmware/firmware-name.Для ZynqMP flow отличается: загрузка PL идёт через platform firmware, а поддерживаемый формат зависит от версии Xilinx/AMD software stack и используемого flow. В PetaLinux/AMD flow для Zynq UltraScale+ могут использоваться bitstream в
.binформате или.pdi, поэтому в этой статье я не буду смешивать Zynq-7000 и ZynqMP. Здесь рассматриваем Zynq-7000, где для FPGA Manager готовим именно.bin.
Для подготовки у себя я обычно использую следующий небольшой bash-скрипт (если что, его можно найти в репозитории в папке scripts):
#!/bin/bashshow_usage() { echo "Usage: $0 [options] <input.bit> [output.bin]" echo "" echo "Options:" echo " -a, --arch <arch> Архитектура: zynq, zynqmp (по умолчанию: zynqmp)" echo " -h, --help Показать эту справку" echo "" echo "Arguments:" echo " input.bit Входной bitstream файл" echo " output.bin (опционально) Имя выходного файла" echo "" echo "Examples:" echo " $0 design.bit # ZynqMP, output: design.bit.bin" echo " $0 design.bit fpga.bin # ZynqMP, output: fpga.bin" echo " $0 --arch zynq design.bit # Zynq-7000, output: design.bit.bin" echo " $0 -a zynq design.bit output.bin # Zynq-7000, output: output.bin" exit 0}ARCH="zynqmp"while [[ $# -gt 0 ]]; do case $1 in -a|--arch) ARCH="$2" shift 2 ;; -h|--help) show_usage ;; -*) echo "Error: Unknown option $1" show_usage ;; *) break ;; esacdoneif [ $# -lt 1 ]; then echo "Error: Missing input file" echo "" show_usagefiINPUT_BIT="$1"if [ ! -f "$INPUT_BIT" ]; then echo "Error: File '$INPUT_BIT' not found" exit 1fiif [ "$ARCH" != "zynq" ] && [ "$ARCH" != "zynqmp" ]; then echo "Error: Invalid architecture '$ARCH'" echo "Supported architectures: zynq, zynqmp" exit 1fiif [ $# -ge 2 ]; then OUTPUT_BIN="$2"else OUTPUT_BIN="${INPUT_BIT}.bin"fiecho "Converting bitstream:"echo " Input: $INPUT_BIT"echo " Output: $OUTPUT_BIN"echo " Target arch: $ARCH"BIF_FILE="bitstream_temp.bif"echo "all : { $INPUT_BIT }" > "$BIF_FILE"# Путь к bootgen (настройте под вашу установку Vivado)BOOTGEN="bootgen"# Если bootgen не в PATH, раскомментируйте и настройте путь:BOOTGEN="/home/fka/tools/Xilinx/2025.1/Vitis/bin/bootgen"if ! command -v $BOOTGEN &> /dev/null; then echo "Error: bootgen not found in PATH" echo "Please install Xilinx Vivado or set BOOTGEN variable to bootgen path" rm -f "$BIF_FILE" exit 1fiecho "Running bootgen with architecture: $ARCH"$BOOTGEN -image "$BIF_FILE" -arch "$ARCH" -process_bitstream binif [ $? -ne 0 ]; then echo "Error: bootgen conversion failed" rm -f "$BIF_FILE" exit 1fiTEMP_BIN="${INPUT_BIT}.bin"if [ "$TEMP_BIN" != "$OUTPUT_BIN" ]; then if [ -f "$TEMP_BIN" ]; then mv "$TEMP_BIN" "$OUTPUT_BIN" else echo "Error: Expected output file '$TEMP_BIN' not found" rm -f "$BIF_FILE" exit 1 fifirm -f "$BIF_FILE"echo "Conversion completed successfully: $OUTPUT_BIN"echo "Target architecture: $ARCH"
Достаточно вызвать этот скрипт, явно указав ему архитектуру zynq, и прокинув путь до bitstream файла с расширением .bit.
Обратите внимание, что в репозитории в скрипте дефолтная платформа — zynqmp.
Готовим ядро для работы с FPGA Manager
Для корректной загрузки bitstream из Linux необходимо, чтобы в ядре была включена поддержка FPGA Manager и, при необходимости, Device Tree Overlay. В большинстве defconfig для Zynq/ZynqMP эти опции уже включены, но лучше явно понимать, за что отвечает каждая из них.
В минимальном варианте для Zynq-7000 нам понадобится поддержка FPGA framework и драйвер FPGA Manager для Xilinx Zynq:
CONFIG_FPGA=y
Опция CONFIG_FPGA включает общий FPGA Configuration Framework в ядре Linux.
Этот фреймворк добавляет общий слой FPGA Manager и драйверы FPGA Manager, через которые ядро умеет программировать FPGA.
Сам FPGA Manager не привязан к конкретному производителю: он предоставляет общий интерфейс, а вся платформенно-зависимая работа выполняется нижележащим драйвером.
Для Zynq-7000 таким драйвером является CONFIG_FPGA_MGR_ZYNQ_FPGA. Он добавляет поддержку программирования PL-части Xilinx Zynq через механизм FPGA Manager.
На Zynq-7000 загрузка bitstream из PS в PL выполняется через блок DevCfg/PCAP, поэтому без этого драйвера интерфейс FPGA Manager для прошивки PL просто не появится.
Для Zynq UltraScale+ MPSoC используется другой драйвер:
CONFIG_FPGA_MGR_ZYNQMP_FPGA=y
Он также относится к FPGA Manager, но предназначен уже для ZynqMP. В отличие от Zynq-7000, там загрузка PL завязана на firmware-интерфейс платформы, поэтому детали отличаются. В этой статье основной фокус будет на Zynq-7000.
Если мы хотим не только загрузить bitstream, но и динамически добавить в Linux устройства, появившиеся в PL, нужна поддержка Device Tree Overlay:
CONFIG_OF_OVERLAY=y
Device Tree Overlay позволяет во время работы системы изменить live device tree: добавить новые узлы, изменить свойства существующих узлов и тем самым заставить Linux создать новые platform device. Это важно для случаев, когда в PL находится не просто мигающий светодиод, а, например, AXI GPIO, AXI DMA, BRAM-контроллер или собственный IP-блок.
Для сценария загрузки FPGA с DTS overlay также полезны:
CONFIG_FPGA_BRIDGE=yCONFIG_FPGA_REGION=yCONFIG_OF_FPGA_REGION=y
CONFIG_FPGA_REGION включает общий слой FPGA Region.
FPGA Region описывает реконфигурируемую область FPGA: это может быть как вся PL-часть, так и отдельная partial-reconfiguration область. Region связывает эту область с конкретным FPGA Manager и, при необходимости, с FPGA bridges, которые нужно отключить перед программированием и включить обратно после него.
CONFIG_OF_FPGA_REGION добавляет Device Tree-интеграцию для FPGA Region.
Этот слой позволяет описывать FPGA-регион в device tree и использовать свойства из DT/overlay, например firmware-name, fpga-mgr, fpga-bridges и флаги загрузки. В таком сценарии overlay может не только добавить описание новых устройств, появившихся в PL, но и передать ядру информацию для загрузки FPGA image в соответствующий FPGA-регион.
Иными словами, CONFIG_FPGA_REGION даёт общий механизм region/manager/bridges, а CONFIG_OF_FPGA_REGION позволяет управлять этим механизмом через Device Tree.
Если используется загрузка overlay через configfs, также нужен сам configfs:
CONFIG_CONFIGFS_FS=y
Configfs — это виртуальная файловая система, через которую userspace может создавать kernel objects с помощью обычных операций вроде mkdir, rmdir и записи в файлы. Именно такой подход часто используется для загрузки .dtbo в работающую систему:
mount -t configfs none /sys/kernel/configmkdir /sys/kernel/config/device-tree/overlays/blinkcat blink.dtbo > /sys/kernel/config/device-tree/overlays/blink/dtbo
Однако здесь есть важная оговорка. Наличие CONFIG_OF_OVERLAY само по себе ещё не гарантирует, что в системе появится путь /sys/kernel/config/device-tree/overlays. В некоторых vendor-ядрах, включая Xilinx/AMD flow, такой интерфейс есть.
В mainline-ядре ситуация может отличаться: поддержка overlay есть, но configfs-интерфейса для ручной загрузки .dtbo может не быть.
Поэтому для своей сборки нужно проверять не только .config, но и фактическое наличие каталога:
mount -t configfs none /sys/kernel/configls /sys/kernel/config/device-tree/overlays
Если такого каталога нет, проблема не в .dtbo, не в bitstream. Просто в текущем ядре нет соответствующего configfs-интерфейса для загрузки overlay.
Также в конфигурациях для Zynq/ZynqMP часто встречаются опции (на atlassian wiki явно указано как необходимое):
CONFIG_CMA=yCONFIG_DMA_CMA=y
Итого для простого примера с загрузкой bitstream через FPGA Manager на Zynq-7000 стоит проверить минимум:
CONFIG_FPGA=yCONFIG_FPGA_MGR_ZYNQ_FPGA=y
Для сценария с FPGA Region и Device Tree Overlay дополнительно понадобятся:
CONFIG_FPGA_BRIDGE=yCONFIG_FPGA_REGION=yCONFIG_OF_FPGA_REGION=yCONFIG_OF_OVERLAY=yCONFIG_CONFIGFS_FS=y
CONFIG_CMA и CONFIG_DMA_CMA не являются обязательными для самого факта загрузки bitstream через FPGA Manager.
А на целевой системе полезно сразу проверить:
zcat /proc/config.gz | grep -E 'CONFIG_FPGA|CONFIG_OF_OVERLAY|CONFIG_CONFIGFS|CONFIG_CMA|CONFIG_DMA_CMA'ls /sys/class/fpga_manager/mount -t configfs none /sys/kernel/configls /sys/kernel/config/
Если /sys/class/fpga_manager/fpga0 появился, значит FPGA Manager в системе есть. Если появился /sys/kernel/config/device-tree/overlays, значит можно пробовать грузить Device Tree Overlay через configfs.
Если второго каталога нет, но CONFIG_OF_OVERLAY=y, это не противоречие: overlay-механизм в ядре может быть включён, но пользовательский configfs-интерфейс для загрузки overlay может отсутствовать.
Готовим корневой device tree для работы с FPGA Manager
Во-первых, как уже говорилось выше, нам нужно обновить ps7_init, которые мы передаем во время сборки в U-Boot SPL.
Во-вторых, нам нужно будет обновить DTS, так как у нас изменились некоторые параметры в ProcessingSystem, которые будут также отражаться на нашем DTS.
Изначально я забыл про то, что настройке параметров тактирования у нас в DTS также меняются параметры clkc ноды, и начал пытаться прошить поверх образа с DTS без измененных параметров bitstream. Это приводило к тому, что у меня загружался bitstream в PL, начинали мигать диоды, но после этого Linux зависал намертво.
Как оказалось, в clkc ноде есть маска fclk-enable. Это маска включённых FCLK-выходов PS в описании clock controller. То есть 0x1 соответствует включённому FCLK_CLK0.
Изначально нода выглядела вот таким образом:
&clkc { fclk-enable = <0x0>; ps-clk-frequency = <33333333>;};
В корректной конфигурации, если у нас используется clk от PS части, она должна выглядеть так:
&clkc { fclk-enable = <0x1>; ps-clk-frequency = <33333333>;};
Проверяем загрузку bitstream с использованием fpgautil
В нашем образе уже присутствует приложение, которое позволяет нам загружать bitstream, и даже DTS overlay. Называется она fpgautil, это утилита от Xilinx, которая оборачивает работу с FPGA Manager, sysfs/configfs и DTO в более удобный интерфейс.
# fpgautil fpgautil: FPGA Utility for Loading/reading PL ConfigurationUsage:fpgautil -b <bin file path> -o <dtbo file path>Options: -b <binfile>(Bin file path) -o <dtbofile>(DTBO file path) -f <flags>Optional: <Bitstream type flags> f := <Full | Partial > -n <Fpga region info> FPGA Regions represent FPGA's and partial reconfiguration regions of FPGA's in the Device Tree Examples:(Load Full bitstream using Overlay)fpgautil -b top.bit.bin -o can.dtbo -f Full -n full (Load Partial bitstream using Overlay)fpgautil -b rm0.bit.bin -o rm0.dtbo -f Partial -n PR0(Load Full bitstream using sysfs interface)fpgautil -b top.bit.bin -f Full(Load Partial bitstream using sysfs interface)fpgautil -b rm0.bit.bin -f Partial(Remove Partial Overlay)fpgautil -R -n PR0(Remove Full Overlay)fpgautil -R -n fullNote: fpgautil -R is responsible for only removing the dtbo file from the livetree. it will not remove the PL logic from the FPGA region.
Пока что нас интересует только загрузка bitstream, без оверлея.
Я скопировал bitstream в /tmp/blink.bit.bin, после чего прошил наш битстрим:
# fpgautil -b /tmp/blink.bit.bin -f FullTime taken to load BIN is 69.000000 Milli SecondsBIN FILE loaded through FPGA manager successfully
Как можно увидеть, светодиоды начали мигать, а также в dmesg появилось сообщение об успешной прошивке от fpga_manager:
[222.499831] fpga_manager fpga0: writing blink.bit.bin to Xilinx Zynq FPGA Manager
Прошиваем bitstream через sysfs
Теперь повторим ту же загрузку напрямую через sysfs.
Делается это довольно просто:
# mkdir -p /lib/firmware# cp /tmp/blink.bit.bin /lib/firmware/# echo 0 > /sys/class/fpga_manager/fpga0/flags# echo blink.bit.bin > /sys/class/fpga_manager/fpga0/firmware# cat /sys/class/fpga_manager/fpga0/stateoperating
Состояние operating означает, что FPGA Manager успешно завершил загрузку bitstream, PL сконфигурирована и находится в рабочем состоянии. Это финальное нормальное состояние после успешной прошивки.
Если во время загрузки возникла бы ошибка, вместо operating можно было бы увидеть одно из ошибочных состояний.
Чуть подробнее по цепочке:
echo 0 > /sys/class/fpga_manager/fpga0/flags
0 означает обычную full reconfiguration, не partial. Потом:
echo blink.bit.bin > /sys/class/fpga_manager/fpga0/firmware
Ядро через firmware loader ищет файл blink.bit.bin в firmware path, обычно /lib/firmware, передаёт его FPGA Manager core, а тот вызывает platform-specific драйвер, у нас это Xilinx Zynq FPGA Manager через DevCfg/PCAP.
После успешной записи и завершения post-programming шагов state становится operating.
Kernel docs как раз описывают, что FPGA image может приходить как firmware file, а platform-specific детали скрыты в low-level driver ops.
Подробнее можно будет почитать тут:
https://www.kernel.org/doc/html/latest/driver-api/fpga/fpga-mgr.html
https://github.com/torvalds/linux/blob/master/drivers/fpga/zynq-fpga.c
https://github.com/torvalds/linux/blob/master/drivers/fpga/zynqmp-fpga.c
Реализация собственной утилиты fpga_loader
Теперь, когда мы руками прошили bitstream через fpgautil и напрямую через sysfs, можно завернуть этот процесс в небольшую C++-утилиту, чтобы потом это можно было удобно переиспользовать в других своих проектах.
fpgautil удобен для ручной проверки и bring-up. Но если загрузка PL должна быть частью основного приложения, у shell-обёртки быстро появляются минусы:
-
непонятно, установлен ли
fpgautilв rootfs; -
сложнее обрабатывать ошибки;
-
приходится парсить stdout/stderr;
-
сложнее тестировать пользовательскую логику;
-
появляется зависимость от конкретной userspace-утилиты;
-
приложение начинает зависеть от shell.
Поэтому я сделал небольшой проект fpga_loader: CLI-утилиту и одновременно C++-обёртку над стандартными интерфейсами Linux FPGA subsystem.
Репозиторий:
https://github.com/FernandesKA/fpga_loader
Идея простая, и состоит не в том чтобы заменить FPGA Manager или написать свой драйвер, а аккуратно обернуть уже существующие kernel-интерфейсы:
Пользовательское приложение / CLI ↓fpga_loader ↓sysfs FPGA Manager + configfs overlays ↓Linux FPGA subsystem ↓Zynq DevCfg / PCAP ↓ PL
То есть вся низкоуровневая работа всё равно остаётся в ядре. Пользовательская утилита только:
-
проверяет входные параметры;
-
копирует bitstream в firmware directory;
-
записывает flags в FPGA Manager, если такой sysfs-атрибут есть;
-
инициирует загрузку bitstream через
firmware_nameилиfirmware; -
проверяет итоговое состояние FPGA Manager;
-
при необходимости накладывает или удаляет Device Tree Overlay через configfs.
Структура проекта
На момент написания статьи структура проекта выглядит так:
.├── CMakeLists.txt├── inc│ ├── dt_overlay.hpp│ ├── file_utils.hpp│ └── fpga_manager.hpp├── scripts│ └── bit-to-bin.sh├── src│ ├── dt_overlay.cpp│ ├── file_utils.cpp│ ├── fpga_manager.cpp│ └── main.cpp└── tests ├── CMakeLists.txt ├── helpers.hpp ├── test_dt_overlay.cpp ├── test_file_utils.cpp └── test_fpga_manager.cpp
Логика специально вынесена из main.cpp в библиотечные классы. Это сделано для того, чтобы проект можно было использовать двумя способами:
-
как обычную CLI-утилиту;
-
как небольшую библиотеку внутри своего приложения.
CLI полезен для отладки на целевой плате:
fpga-loader statusfpga-loader /tmp/blink.bit.binfpga-loader -m overlay --dtbo /tmp/blink.dtbo --name blinkfpga-loader -m overlay --remove --name blink
А библиотечный вариант полезен, если загрузка FPGA должна быть частью вашего приложения. В текущей версии load() возвращает не просто bool, а LoadResult: код ошибки, текстовое описание и последнее состояние FPGA Manager.
#include <chrono>#include <iostream>#include "fpga_manager.hpp"int main(){ fpga::FpgaManagerConfig cfg; cfg.manager_path = "/sys/class/fpga_manager/fpga0"; cfg.firmware_dir = "/lib/firmware"; cfg.timeout = std::chrono::milliseconds(5000); cfg.verbose = true; fpga::FpgaManager mgr(cfg); auto result = mgr.load("/tmp/blink.bit.bin", fpga::FpgaFlagNone); if (!result) { std::cerr << "FPGA load failed: " << result.message << '\n'; return 1; } std::cout << "FPGA programmed, state=" << result.state << '\n'; return 0;}
Подключение через CMake выглядит обычным образом:
add_subdirectory(third_party/fpga_loader)target_link_libraries(my_app PRIVATE fpga::loader)
Класс FpgaManager
Основная часть работы находится в классе FpgaManager. Его задача — выполнить тот же набор операций, который мы выше делали руками через sysfs, но с обработкой ошибок.
В текущей реализации load() принимает путь к bitstream и флаги загрузки, а возвращает LoadResult:
enum class FpgaError { Ok, ManagerNotFound, BitstreamNotFound, FirmwareCopyFailed, FlagsWriteFailed, TriggerAttrNotFound, TriggerWriteFailed, StateError, Timeout,};struct LoadResult { FpgaError error = FpgaError::Ok; std::string message; std::string state; bool ok() const; explicit operator bool() const;};LoadResult load(const std::filesystem::path& bitstream, uint32_t flags = FpgaFlagNone);
Такой интерфейс удобнее для использования из основного приложения: можно не только понять, что загрузка не удалась, но и различить причину. Например, файл bitstream не найден, FPGA Manager отсутствует в sysfs, не удалось записать flags, не найден trigger-атрибут firmware_name/firmware, FPGA Manager ушёл в error-state или не перешёл в operating до таймаута.
Упрощённо логика load() выглядит так:
fpga::LoadResult FpgaManager::load(const std::filesystem::path& bitstream, uint32_t flags){ if (!available()) { return {FpgaError::ManagerNotFound, "fpga manager not found at " + cfg_.manager_path.string()}; } if (!std::filesystem::exists(bitstream)) { return {FpgaError::BitstreamNotFound, "bitstream not found: " + bitstream.string()}; } std::string firmware_name; if (!utils::copy_firmware(bitstream, cfg_.firmware_dir, firmware_name)) { return {FpgaError::FirmwareCopyFailed, "failed to copy bitstream to firmware directory"}; } if (auto r = write_flags(flags); !r) { return r; } if (auto r = trigger(firmware_name); !r) { return r; } return wait_operating();}
На целевой системе это превращается примерно в такую последовательность:
cp blink.bit.bin /lib/firmware/blink.bit.binecho 0 > /sys/class/fpga_manager/fpga0/flagsecho blink.bit.bin > /sys/class/fpga_manager/fpga0/firmwarecat /sys/class/fpga_manager/fpga0/state
Важный момент: в sysfs мы записываем не полный путь к файлу, а только имя firmware. Это связано с тем, что FPGA Manager использует kernel firmware loader. Поэтому файл должен лежать в firmware search path, обычно это /lib/firmware.
Если записать полный путь вроде /tmp/blink.bit.bin, можно получить ошибку загрузки firmware, потому что ядро будет искать firmware не так, как ожидает пользователь.
firmware_name и firmware
В разных ядрах имя sysfs-атрибута для запуска загрузки может отличаться. В текущей реализации fpga_loader сначала пробует firmware_name, а потом firmware:
for (const char* attr : {"firmware_name", "firmware"}) { auto node = cfg_.manager_path / attr; if (!std::filesystem::exists(node)) { continue; } return utils::write_sysfs(node, firmware_name);}
Это сделано специально, чтобы не прибивать утилиту гвоздями к одной версии ядра или одному vendor BSP. Если ни одного из этих атрибутов нет, утилита честно сообщает, что не нашла способ запустить bitstream-only загрузку через sysfs.
Проверка состояния FPGA Manager
После записи имени bitstream загрузка происходит внутри ядра. Пользовательская программа не должна считать операцию успешной сразу после записи в sysfs. Успех — это когда FPGA Manager перешёл в состояние operating.
В текущей реализации состояние опрашивается до таймаута. При успешном завершении возвращается LoadResult{FpgaError::Ok, ..., "operating"}. При ошибке возвращается конкретная причина:
fpga::LoadResult FpgaManager::wait_operating(){ auto deadline = std::chrono::steady_clock::now() + cfg_.timeout; while (std::chrono::steady_clock::now() < deadline) { std::string s = state(); if (s == "operating") { return {FpgaError::Ok, {}, s}; } if (s.find("error") != std::string::npos) { return {FpgaError::StateError, "FPGA manager entered error state: '" + s + "'", s}; } if (s == "unknown") { return {FpgaError::StateError, "FPGA manager state is 'unknown' after programming request", s}; } std::this_thread::sleep_for(std::chrono::milliseconds(50)); } std::string s = state(); return {FpgaError::Timeout, "timeout waiting for FPGA state 'operating'", s};}
Это полезно для диагностики. Если что-то пошло не так, FPGA Manager может показать, на каком этапе всё развалилось:
firmware request errorparse header errorwrite init errorwrite errorwrite complete error
По этим состояниям проще сузить область поиска: проблема может быть в firmware path, формате FPGA image, подготовке FPGA к программированию, передаче данных через PCAP или в финальном ожидании завершения конфигурации. Точную причину всё равно нужно смотреть, потому что state показывает только стадию, на которой произошла ошибка.
Флаги загрузки
FPGA Manager позволяет передать дополнительные флаги через файл:
/sys/class/fpga_manager/fpga0/flags
Для обычной полной реконфигурации используется 0:
echo 0 > /sys/class/fpga_manager/fpga0/flags
В коде это описано enum-ом, который повторяет значения FPGA_MGR_* из kernel header include/linux/fpga/fpga-mgr.h:
enum FpgaFlags : uint32_t { FpgaFlagNone = 0, FpgaFlagPartialReconfig = 1u << 0, // FPGA_MGR_PARTIAL_RECONFIG FpgaFlagExternalConfig = 1u << 1, // FPGA_MGR_EXTERNAL_CONFIG FpgaFlagEncryptedBitstream = 1u << 2, // FPGA_MGR_ENCRYPTED_BITSTREAM FpgaFlagBitstreamLsbFirst = 1u << 3, // FPGA_MGR_BITSTREAM_LSB_FIRST FpgaFlagCompressedBitstream = 1u << 4, // FPGA_MGR_COMPRESSED_BITSTREAM};
Для текущего примера с полной загрузкой blink.bit.bin нам нужен FpgaFlagNone.
Важно: частичная реконфигурация в этой статье не рассматривается. Наличие флага в API не означает, что partial reconfiguration автоматически заработает на любой сборке ядра, любом bitstream и любом device tree.
Обёртка над configfs для Device Tree Overlay
Вторая часть проекта — класс DtOverlay. Он работает не с FPGA Manager напрямую, а с configfs-интерфейсом Device Tree Overlay:
/sys/kernel/config/device-tree/overlays/
Ручной набор операций выглядит так:
mount -t configfs none /sys/kernel/configmkdir /sys/kernel/config/device-tree/overlays/blinkcat blink.dtbo > /sys/kernel/config/device-tree/overlays/blink/dtbo
Удаление overlay:
rmdir /sys/kernel/config/device-tree/overlays/blink
В C++ это можно завернуть в такой интерфейс:
fpga::DtOverlay overlay;if (!overlay.apply("blink", "/tmp/blink.dtbo")) { return 1;}if (!overlay.remove("blink")) { return 1;}
В текущей реализации apply():
-
проверяет, что configfs доступен;
-
при необходимости пытается смонтировать configfs;
-
проверяет наличие
.dtbo; -
создаёт каталог overlay;
-
записывает бинарный
.dtboв файлdtbo; -
читает
statusи ожидает состояниеapplied.
Упрощённо это выглядит так:
bool DtOverlay::apply(const std::string& name, const std::filesystem::path& dtbo_path, bool replace){ if (!ensure_mounted()) { return false; } if (overlay already exists) { if (!replace) { return false; } remove(name); } create overlay directory; write dtbo blob to <overlay>/dtbo; return status(name) == "applied";}
Здесь есть тонкость: запись в configfs — это не совсем обычное копирование файла. Мы пишем данные в специальный kernel object, и именно в момент записи dtbo ядро применяет overlay к live device tree. Если overlay некорректный, конфликтует с существующим деревом или ссылается на несуществующий target, ошибка появится именно на этом этапе.
Поэтому в реальной реализации нужно аккуратно обрабатывать ошибки:
-
нет
/sys/kernel/config/device-tree/overlays; -
configfs не смонтирован;
-
overlay с таким именем уже существует;
-
.dtboне найден; -
запись в
dtboзавершилась ошибкой; -
после записи
statusне сталapplied; -
при
--replaceстарый overlay нужно удалить перед установкой нового.
Два режима работы CLI
В итоге у CLI есть два основных режима.
Загрузка bitstream напрямую через FPGA Manager sysfs:
fpga-loader -m bitstream /tmp/blink.bit.bin
Так как bitstream — режим по умолчанию, можно короче:
fpga-loader /tmp/blink.bit.bin
Загрузка Device Tree Overlay:
fpga-loader -m overlay --dtbo /tmp/blink.dtbo
Также есть служебные команды:
fpga-loader statusfpga-loader -m overlay --remove --name blinkfpga-loader -m overlay --replace --dtbo /tmp/blink.dtbo --name blink
Пример загрузки bitstream:
# ./fpga-loader ./blink.bit.binFPGA programmed: state=operating
После этого можно проверить состояние руками:
# cat /sys/class/fpga_manager/fpga0/stateoperating
Команда status показывает не только состояние FPGA Manager, но и список активных overlays, если configfs доступен:
fpga-loader status
Сборка под целевую плату
Так как проект написан на C++17 и собирается через CMake, его можно собрать как на хосте, так и через Buildroot SDK.
Для обычной сборки:
cmake -B build -DCMAKE_BUILD_TYPE=Releasecmake --build build -j
Для кросс-компиляции через SDK:
source /path/to/buildroot-sdk/environment-setup-<tuple>cmake -B build -DCMAKE_BUILD_TYPE=Releasecmake --build build -j
После активации SDK CMake подхватит нужные переменные окружения: компилятор, sysroot, флаги сборки и linker flags.
Если нужно подключить проект как зависимость:
git submodule add https://github.com/FernandesKA/fpga\_loader third_party/fpga_loader
add_subdirectory(third_party/fpga_loader)target_link_libraries(my_target PRIVATE fpga::loader)
Скрипт для подготовки bitstream
В репозитории также есть скрипт:
scripts/bit-to-bin.sh
Он вызывает bootgen и преобразует .bit в .bit.bin. В текущей версии скрипта платформа по умолчанию — zynqmp, поэтому для этой статьи и Zynq-7000 архитектуру лучше указывать явно:
./scripts/bit-to-bin.sh --arch zynq design_1.bit
На выходе получится:
design_1.bit.bin
Именно этот файл дальше можно положить в /lib/firmware и загрузить через FPGA Manager.
Что проверено тестами
Так как настоящая загрузка FPGA требует железа, unit-тесты не должны зависеть от реальной платы. Иначе тесты будут запускаться только в тот момент, когда плата доступна, а выделять отдельную плату для тестов несколько проблематично.
Поэтому для тестов используется fake sysfs/configfs дерево во временной директории:
/tmp/fpga_loader_test/└── sys/ └── class/ └── fpga_manager/ └── fpga0/ ├── flags ├── firmware ├── firmware_name └── state
Тесты для FpgaManager проверяют:
-
что bitstream копируется в firmware directory;
-
что в
flagsзаписывается ожидаемое значение; -
что загрузка может запускаться через
firmware_name; -
что есть fallback на
firmware; -
что при ошибках возвращается ожидаемый
FpgaError; -
что ошибка корректно обрабатывается, если bitstream отсутствует;
-
что ошибка корректно обрабатывается, если нет
firmware_name/firmware; -
что ошибка корректно обрабатывается, если FPGA Manager не переходит в
operating.
Для DtOverlay fake configfs используется только для проверки пользовательской логики: отсутствие configfs, отсутствие .dtbo, уже существующий overlay, --replace, удаление overlay, чтение статуса и список overlays.
Есть важная оговорка: настоящий status=applied выставляет ядро после применения overlay. В обычной временной директории такого ядра, как ни странно, не живёт. Поэтому часть overlay-тестов проверяет, что код дошёл до создания каталога и записи dtbo, но полноценный успешный apply всё равно требует реального configfs или отдельного kernel stub.
Такой тест не проверяет сам PCAP и не доказывает, что FPGA реально прошилась. Но он проверяет пользовательскую логику: работу с путями, обработку ошибок и последовательность операций. Для userspace-обёртки это как раз то, что нужно.
Что проверено на железе
На моей плате проверен bitstream-only сценарий:
fpga-loader ./blink.bit.bincat /sys/class/fpga_manager/fpga0/state
После загрузки FPGA Manager переходит в operating, а светодиоды в PL начинают мигать.
Используемая платформа:
Board: RK-ZYNQ7020-FSoC: Zynq-7000 / XC7Z020Flow: Buildroot + U-Boot SPL + LinuxMethod: FPGA Manager sysfs, bitstream-onlyFormat: .bit.bin, подготовленный через bootgen
Overlay-сценарий в этой статье показан как интерфейсная часть fpga_loader и задел под следующий материал. Полноценный пример с AXI-устройством, fpga-region, firmware-name, загрузкой .dtbo и появлением platform device лучше разобрать отдельно, иначе статья станет довольно объёмной.
Ограничения
У инструмента есть понятные ограничения.
fpga_loader не валидирует содержимое bitstream. Он не знает, подходит ли bitstream к конкретной FPGA, совпадает ли версия Vivado, корректно ли разведены clock/reset, не конфликтует ли overlay с текущим device tree и не забыли ли мы включить нужный FCLK.
Он только использует стандартные механизмы Linux:
-
FPGA Manager через sysfs;
-
firmware loader;
-
Device Tree Overlay через configfs.
Если bitstream некорректный, проблема всё равно проявится на уровне FPGA Manager, драйвера или железа. Утилита может показать ошибку и состояние, но не превратит неправильный bitstream в правильный.
Также fpga_loader не делает безопасную остановку всего, что уже работает с PL. Если в PL есть AXI-периферия, DMA или драйверы, которые в момент reconfiguration продолжают к ней обращаться, можно получить зависание системы. Перед полной реконфигурацией нужно остановить userspace, остановить DMA, отвязать устройства или удалить overlay, и только после этого загружать новый bitstream.
Итог
В итоге получилась небольшая C++-обёртка над FPGA Manager и configfs overlay. Её можно использовать как самостоятельную CLI-утилиту для отладки или как библиотеку внутри основного приложения.
Для простого ручного bring-up вполне достаточно fpgautil и пары команд через sysfs. Но если загрузка PL становится частью проекта, лучше иметь нормальный программный интерфейс, тесты и контролируемую обработку ошибок.
Главная мысль здесь не в том, что fpga_loader делает что-то магическое. Он не заменяет FPGA Manager, не парсит bitstream и не лечит неправильный device tree. Он просто убирает shell-склейку из приложения и даёт небольшой проверяемый слой над sysfs/configfs.
Ещё раз продублирую ссылку на репозиторий:
https://github.com/FernandesKA/fpga_loader
Благодарю за внимание, и буду рад конструктивной критике!
ссылка на оригинал статьи https://habr.com/ru/articles/1052912/