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

от автора

Добрый день!
В прошлой статье мы рассматривали настройку 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

То есть вся низкоуровневая работа всё равно остаётся в ядре. Пользовательская утилита только:

  1. проверяет входные параметры;

  2. копирует bitstream в firmware directory;

  3. записывает flags в FPGA Manager, если такой sysfs-атрибут есть;

  4. инициирует загрузку bitstream через firmware_name или firmware;

  5. проверяет итоговое состояние FPGA Manager;

  6. при необходимости накладывает или удаляет 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 в библиотечные классы. Это сделано для того, чтобы проект можно было использовать двумя способами:

  1. как обычную CLI-утилиту;

  2. как небольшую библиотеку внутри своего приложения.

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():

  1. проверяет, что configfs доступен;

  2. при необходимости пытается смонтировать configfs;

  3. проверяет наличие .dtbo;

  4. создаёт каталог overlay;

  5. записывает бинарный .dtbo в файл dtbo;

  6. читает 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/