Вход на сайт

Просмотр новости

Найдите то, что Вас интересует

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

Дата публикации: 28-06-2026 12:14:55

Добрый день!
В прошлой статье мы подготовили кастомную плату на Zynq-7000 для загрузки Linux: Buildroot, U-Boot SPL, device tree и минимальная rootfs уже работают. Но PL-часть при этом оставалась почти пустой.На первых этапах bring-up это нормально: bitstream обычно шьют через JTAG или кладут в BOOT.BIN, чтобы FPGA конфигурировалась ещё до старта Linux. Но для реального проекта такой подход быстро становится неудобным: иногда нужно менять PL-логику уже после загрузки системы.В этой статье разберём, как загрузить bitstream в Zynq-7000 из Linux через FPGA Manager, какие параметры ядра для этого нужны, и как завернуть загрузку в небольшую C++ утилиту. Читать далее

Основное содержимое страницы с новостью.

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

b04fa48159d50aa29ac2214acc382126.png

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


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)

182afac78d71a42f201477a8f88b353b.png

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

После чего BlockDesign будет выглядеть следующим образом.

e4659357a051e61e638c0a2d568e8926.png

В констрейнах я укажу используемые выводы для светодиодов (мы используем две штуки):

## 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, то тактовый сигнал на описанный выше модуль приходить не будет.

Синтезируем, имплементируем, вроде бы что-то собралось.

96009d000c30e0a8cb2329c0acb78f4c.png

Генерируем битстрим.

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

2640f6434deae42c7d9d3f70826bd5bf.png

Дальше нам нужно подкинуть наш 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=y

Device 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=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/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=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/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/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 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():

  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 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_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.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, подготовленный через 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

Благодарю за внимание, и буду рад конструктивной критике!

Схожие новости

#Наименование новостиТональностьИнформативностьДата публикации
1Анатомия процесса загрузки Linux — от инициализации ядра до systemd0524-06-2026
2Сборка Angie из исходных кодов0513-07-2026
3Переход на zVirt 5.0: что меняется на каждом узле и как не сорвать обновление0709-07-2026
4Загружаемся с Raspberry Pi Pico0509-06-2026
5Переход на zVirt 5.0: что меняется на каждом узле и как не сорвать обновление0709-07-2026
6Пишу алгоритм FFT на Си для процессора Эльбрус0712-06-2026
7Делаем свое контекстное меню для полей ввода в Qt на мобильных устройствах0528-06-2026
8Сложно о простом. Все, что бы вы хотели знать о SFP модулях. Часть 6. Топ-10 частых ошибок при выборе и эксплуатации SFP0509-07-2026
9Линукс на Intel 40040523-06-2026
10ВВОД В СИСТЕМУ «УМНЫЙ ДОМ» МОДУЛЯ GSM0509-07-2026

Классификация: Экономика. Схожих патентов: 0. Схожих новостей: 10. Тональность: 0. Информативность: 8. Источник: habr.com.