📱 Подписаться
IT и цифровая трансформация

Загружаем bitstream из Linux через FPGA Manager на Zynq-7000

📰 Habr 👁️ 0 просмотров

FernandesKA4 часа назад

Загружаем bitstream из Linux через FPGA Manager на Zynq-7000

Уровень сложностиСреднийВремя на прочтение22 минОхват и читатели3.9KFPGA*Linux*Системное программирование*ТуториалДобрый день! В прошлой статье мы рассматривали настройку 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.Полезные материалы по теме:

• https://xilinx-wiki.atlassian.net/wiki/spaces/A/pages/18841645/Solution+Zynq+PL+Programming+With+FPGA+Manager
• https://xilinx-wiki.atlassian.net/wiki/spaces/A/pages/18841847/Solution+ZynqMP+PL+Programming

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 begin
if (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");
end

localparam 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) begin
if (!rst_n) begin
cnt <= '0;
led_raw <= 1'b0;
end else if (cnt == DIV_MAX[CNT_W-1:0]) begin
cnt <= '0;
led_raw <= ~led_raw;
end else begin
cnt <= cnt + 1'b1;
end
end

always_comb begin led_o = (LED_ACTIVE_HIGH) ? led_raw : ~led_raw; end

endmoduleДалее попробуем собрать модуль, как видим синтез проходит. Теперь нам нужно будет добавить этот модуль в 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/bash

show_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
;;
esac
done

if [ $# -lt 1 ]; then
echo "Error: Missing input file"
echo ""
show_usage
fi

INPUT_BIT="$1"

if [ ! -f "$INPUT_BIT" ]; then
echo "Error: File '$INPUT_BIT' not found"
exit 1
fi

if [ "$ARCH" != "zynq" ] && [ "$ARCH" != "zynqmp" ]; then
echo "Error: Invalid architecture '$ARCH'"
echo "Supported architectures: zynq, zynqmp"
exit 1
fi

if [ $# -ge 2 ]; then
OUTPUT_BIN="$2"
else
OUTPUT_BIN="${INPUT_BIT}.bin"
fi

echo "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 1
fi

echo "Running bootgen with architecture: $ARCH"
$BOOTGEN -image "$BIF_FILE" -arch "$ARCH" -process_bitstream bin

if [ $? -ne 0 ]; then
echo "Error: bootgen conversion failed"
rm -f "$BIF_FILE"
exit 1
fi

TEMP_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
fi
fi

rm -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=yDevice Tree Overlay позволяет во время работы системы изменить live device tree: добавить новые узлы, изменить свойства существующих узлов и тем самым заставить Linux создать новые platform device. Это важно для случаев, когда в PL находится не просто мигающий светодиод, а, например, AXI GPIO, AXI DMA, BRAM-контроллер или собственный IP-блок.

Для сценария загрузки FPGA с DTS overlay также полезны:

CONFIG_FPGA_BRIDGE=y
CONFIG_FPGA_REGION=y
CONFIG_OF_FPGA_REGION=yCONFIG_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=yConfigfs — это виртуальная файловая система, через которую userspace может создавать kernel objects с помощью обычных операций вроде mkdir, rmdir и записи в файлы. Именно такой подход часто используется для загрузки .dtbo в работающую систему:

mount -t configfs none /sys/kernel/config
mkdir /sys/kernel/config/device-tree/overlays/blink
cat 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/config
ls /sys/kernel/config/device-tree/overlaysЕсли такого каталога нет, проблема не в .dtbo, не в bitstream. Просто в текущем ядре нет соответствующего configfs-интерфейса для загрузки overlay.

Также в конфигурациях для Zynq/ZynqMP часто встречаются опции (на atlassian wiki явно указано как необходимое):

CONFIG_CMA=y
CONFIG_DMA_CMA=yИтого для простого примера с загрузкой bitstream через FPGA Manager на Zynq-7000 стоит проверить минимум:

CONFIG_FPGA=y
CONFIG_FPGA_MGR_ZYNQ_FPGA=yДля сценария с FPGA Region и Device Tree Overlay дополнительно понадобятся:

CONFIG_FPGA_BRIDGE=y
CONFIG_FPGA_REGION=y
CONFIG_OF_FPGA_REGION=y
CONFIG_OF_OVERLAY=y
CONFIG_CONFIGFS_FS=yCONFIG_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/config
ls /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 Configuration

Usage: 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 full
Note: 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 Full
Time taken to load BIN is 69.000000 Milli Seconds
BIN 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/state
operatingСостояние operating означает, что FPGA Manager успешно завершил загрузку bitstream, PL сконфигурирована и находится в рабочем состоянии. Это финальное нормальное состояние после успешной прошивки.

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

Чуть подробнее по цепочке:

echo 0 > /sys/class/fpga_manager/fpga0/flags0 означает обычную 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 status
fpga-loader /tmp/blink.bit.bin
fpga-loader -m overlay --dtbo /tmp/blink.dtbo --name blink
fpga-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.bin
echo 0 > /sys/class/fpga_manager/fpga0/flags
echo blink.bit.bin > /sys/class/fpga_manager/fpga0/firmware
cat /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 error
parse header error
write init error
write error
write 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/config
mkdir /sys/kernel/config/device-tree/overlays/blink
cat 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 status
fpga-loader -m overlay --remove --name blink
fpga-loader -m overlay --replace --dtbo /tmp/blink.dtbo --name blinkПример загрузки bitstream:

# ./fpga-loader ./blink.bit.bin
FPGA programmed: state=operatingПосле этого можно проверить состояние руками:

# cat /sys/class/fpga_manager/fpga0/state
operatingКоманда status показывает не только состояние FPGA Manager, но и список активных overlays, если configfs доступен:

fpga-loader status

Сборка под целевую плату

Так как проект написан на C++17 и собирается через CMake, его можно собрать как на хосте, так и через Buildroot SDK.

Для обычной сборки:

cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -jДля кросс-компиляции через SDK:

source /path/to/buildroot-sdk/environment-setup-<tuple>
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -jПосле активации SDK CMake подхватит нужные переменные окружения: компилятор, sysroot, флаги сборки и linker flags.

Если нужно подключить проект как зависимость:

git submodule add https://github.com/FernandesKA/fpga\_loader third_party/fpga_loaderadd_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.bin
cat /sys/class/fpga_manager/fpga0/stateПосле загрузки FPGA Manager переходит в operating, а светодиоды в PL начинают мигать.

Используемая платформа:

Board: RK-ZYNQ7020-F
SoC: Zynq-7000 / XC7Z020
Flow: Buildroot + U-Boot SPL + Linux
Method: FPGA Manager sysfs, bitstream-only
Format: .bit.bin, подготовленный через bootgenOverlay-сценарий в этой статье показан как интерфейсная часть 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

Благодарю за внимание, и буду рад конструктивной критике!Теги:• FpgaManager
• Xilinx
• zynq-7000
• RK-ZYNQ7020F_REV1_1
• Bitstream
• Devece Tree overlayХабы:• FPGA
• Linux
• Системное программирование

Получайте больше инсайтов о систематизации бизнеса

Подписывайтесь на Telegram-канал Business Operations — ежедневные материалы о бизнес-процессах, операционном управлении и повышении эффективности

💬 Подписаться на канал