Загружаем 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, настроенный в предыдущей статье. Для 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

Настройка FCLK_CLK0 в Vivado

Напишем небольшой модуль, который будет просто мигать диодом:

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

SystemVerilog модуль напрямую не добавился в BlockDesign, поэтому написал небольшой 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

Добавляем модуль в BlockDesign (ПКМ → Add module):

Добавление модуля в BlockDesign Vivado

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

BlockDesign с модулем blink

Констрейнты для светодиодов:

## 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 нужно пересобрать. Синтез, имплементация:

Успешная имплементация в Vivado

Генерируем bitstream. Экспортируем XSA вместе с bitstream: Export hardware → Include bitstream.

Экспорт XSA с bitstream в Vivado

Дальше подкидываем XSA в Buildroot: обновляем ps_init_gpl (c + h) и генерируем новый DTS.

Для Zynq-7000 в Xilinx/AMD FPGA Manager flow в PL загружается не исходный .bit, а бинарный .bin, подготовленный из .bit с помощью bootgen. Именно такой .bin файл затем передаётся FPGA Manager через firmware/firmware-name.

Для ZynqMP flow отличается: загрузка PL идёт через platform firmware, поэтому эти детали лучше разобрать отдельно. Здесь рассматриваем 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"
    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'"
    exit 1
fi

OUTPUT_BIN="${2:-${INPUT_BIT}.bin}"

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="/home/fka/tools/Xilinx/2025.1/Vitis/bin/bootgen"

if ! command -v $BOOTGEN &> /dev/null; then
    echo "Error: bootgen not found"
    rm -f "$BIF_FILE"
    exit 1
fi

$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" ] && [ -f "$TEMP_BIN" ]; then
    mv "$TEMP_BIN" "$OUTPUT_BIN"
fi

rm -f "$BIF_FILE"
echo "Conversion completed successfully: $OUTPUT_BIN"

Достаточно вызвать скрипт, явно указав архитектуру zynq:

./scripts/bit-to-bin.sh --arch zynq design_1.bit
В репозитории дефолтная платформа в скрипте — zynqmp, поэтому для Zynq-7000 архитектуру нужно указывать явно.

Готовим ядро для работы с FPGA Manager

В минимальном варианте для Zynq-7000 понадобится поддержка FPGA framework и драйвер FPGA Manager для Xilinx Zynq:

CONFIG_FPGA=y
CONFIG_FPGA_MGR_ZYNQ_FPGA=y

CONFIG_FPGA включает общий FPGA Configuration Framework. CONFIG_FPGA_MGR_ZYNQ_FPGA добавляет поддержку программирования PL через DevCfg/PCAP — без него интерфейс FPGA Manager для прошивки PL просто не появится.

Для ZynqMP используется другой драйвер: CONFIG_FPGA_MGR_ZYNQMP_FPGA=y. В этой статье основной фокус на Zynq-7000.

Для сценария с 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_FPGA_REGION включает общий слой FPGA Region — описывает реконфигурируемую область и связывает её с FPGA Manager и bridges. CONFIG_OF_FPGA_REGION добавляет Device Tree-интеграцию: позволяет описывать FPGA-регион в device tree и использовать свойства firmware-name, fpga-mgr, fpga-bridges.

CONFIG_CONFIGFS_FS нужен для загрузки overlay через configfs:

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. В mainline-ядре configfs-интерфейса для ручной загрузки .dtbo может не быть. Поэтому нужно проверять фактическое наличие каталога:

mount -t configfs none /sys/kernel/config
ls /sys/kernel/config/device-tree/overlays

На целевой системе полезно сразу проверить:

zcat /proc/config.gz | grep -E 'CONFIG_FPGA|CONFIG_OF_OVERLAY|CONFIG_CONFIGFS|CONFIG_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.

Критическое изменение в Device Tree

Изначально я забыл обновить DTS после изменения параметров тактирования в ProcessingSystem. Это приводило к тому, что загружался bitstream, начинали мигать диоды, но после этого Linux зависал намертво.

Как оказалось, в ноде clkc есть маска fclk-enable — маска включённых FCLK-выходов PS. 0x1 соответствует включённому FCLK_CLK0.

Изначально нода выглядела так:

&clkc {
    fclk-enable = <0x0>;
    ps-clk-frequency = <33333333>;
};

В корректной конфигурации, если используется CLK от PS:

&clkc {
    fclk-enable = <0x1>;
    ps-clk-frequency = <33333333>;
};

Проверяем загрузку bitstream с использованием fpgautil

В Buildroot-образе уже присутствует утилита 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>   FPGA Regions represent FPGA's
                             and partial reconfiguration regions

Examples:
(Load Full bitstream using Overlay)
fpgautil -b top.bit.bin -o can.dtbo -f Full -n full
(Load Full bitstream using sysfs interface)
fpgautil -b top.bit.bin -f Full
(Remove Full Overlay)
fpgautil -R -n full

Скопировал 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 появилось сообщение:

[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 сконфигурирована и находится в рабочем состоянии.

Разберём цепочку подробнее:

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. После успешной записи state становится operating.

Подробнее:

Реализация собственной утилиты fpga_loader

Теперь, когда мы руками прошили bitstream через fpgautil и напрямую через sysfs, можно завернуть этот процесс в небольшую C++-утилиту.

fpgautil удобен для ручной проверки и bring-up. Но если загрузка PL должна быть частью основного приложения, у shell-обёртки быстро появляются минусы:

  • непонятно, установлен ли fpgautil в rootfs;
  • сложнее обрабатывать ошибки;
  • приходится парсить stdout/stderr;
  • сложнее тестировать пользовательскую логику;
  • появляется зависимость от конкретной userspace-утилиты и shell.

Поэтому я сделал небольшой проект fpga_loader: CLI-утилиту и одновременно C++-обёртку над стандартными интерфейсами Linux FPGA subsystem.

Репозиторий: 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;
  4. инициирует загрузку через 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 в библиотечные классы, чтобы проект можно было использовать двумя способами: как 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. 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);

Упрощённо логика 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();
}

Важный момент: в sysfs записывается не полный путь к файлу, а только имя firmware. FPGA Manager использует kernel firmware loader, поэтому файл должен лежать в firmware search path — обычно /lib/firmware. Если записать полный путь вроде /tmp/blink.bit.bin, получим ошибку загрузки.

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.

Проверка состояния FPGA Manager

После записи имени bitstream загрузка происходит внутри ядра. Пользовательская программа не должна считать операцию успешной сразу после записи в sysfs — нужно дождаться состояния 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

Флаги загрузки

FPGA Manager позволяет передать флаги через /sys/class/fpga_manager/fpga0/flags. В коде это enum, повторяющий значения FPGA_MGR_* из kernel header:

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
};
Важно: частичная реконфигурация в этой статье не рассматривается. Наличие флага в API не означает, что partial reconfiguration автоматически заработает на любой сборке ядра.

Обёртка над configfs для Device Tree Overlay

Вторая часть проекта — класс DtOverlay. Он работает с configfs-интерфейсом Device Tree Overlay:

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, при необходимости монтирует его, создаёт каталог overlay и записывает бинарный .dtbo. Запись в configfs — это не обычное копирование файла: именно в момент записи dtbo ядро применяет overlay к live device tree.

Два режима работы CLI

Загрузка bitstream напрямую через FPGA Manager sysfs (режим по умолчанию):

fpga-loader /tmp/blink.bit.bin

Загрузка Device Tree Overlay:

fpga-loader -m overlay --dtbo /tmp/blink.dtbo --name blink

Служебные команды:

fpga-loader status
fpga-loader -m overlay --remove --name blink
fpga-loader -m overlay --replace --dtbo /tmp/blink.dtbo --name blink

Пример загрузки:

# ./fpga-loader ./blink.bit.bin
FPGA programmed: state=operating
# cat /sys/class/fpga_manager/fpga0/state
operating

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

Обычная сборка:

cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j

Кросс-компиляция через Buildroot SDK:

source /path/to/buildroot-sdk/environment-setup-<tuple>
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j

Подключение как зависимость:

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)

Что проверено тестами

Так как настоящая загрузка 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;
  • корректную обработку всех видов ошибок.

Для DtOverlay fake configfs проверяет пользовательскую логику: отсутствие configfs, отсутствие .dtbo, уже существующий overlay, --replace, удаление overlay. Настоящий status=applied выставляет ядро, поэтому часть overlay-тестов проверяет только, что код дошёл до записи dtbo.

Такой тест не проверяет сам PCAP и не доказывает, что FPGA реально прошилась. Но он проверяет пользовательскую логику: работу с путями, обработку ошибок и последовательность операций.

Что проверено на железе

На плате проверен bitstream-only сценарий:

fpga-loader ./blink.bit.bin
cat /sys/class/fpga_manager/fpga0/state

После загрузки FPGA Manager переходит в operating, светодиоды в PL начинают мигать.

ПараметрЗначение
BoardRK-ZYNQ7020-F
SoCZynq-7000 / XC7Z020
FlowBuildroot + U-Boot SPL + Linux
МетодFPGA Manager sysfs, bitstream-only
Формат.bit.bin, подготовленный через bootgen

Overlay-сценарий показан как интерфейсная часть fpga_loader и задел под следующий материал. Полноценный пример с AXI-устройством, fpga-region, загрузкой .dtbo и появлением platform device лучше разобрать отдельно.

Ограничения

fpga_loader не валидирует содержимое bitstream. Он не знает, подходит ли bitstream к конкретной FPGA, совпадает ли версия Vivado, корректно ли разведены clock/reset. Он только использует стандартные механизмы Linux: FPGA Manager через sysfs, firmware loader, Device Tree Overlay через configfs.

Также fpga_loader не делает безопасную остановку всего, что работает с PL. Если в PL есть AXI-периферия, DMA или драйверы, которые в момент reconfiguration продолжают к ней обращаться, можно получить зависание системы. Перед полной реконфигурацией нужно остановить userspace, DMA, отвязать устройства или удалить overlay.

Итог

Получилась небольшая C++-обёртка над FPGA Manager и configfs overlay. Её можно использовать как самостоятельную CLI-утилиту для отладки или как библиотеку внутри основного приложения.

Для простого ручного bring-up вполне достаточно fpgautil и пары команд через sysfs. Но если загрузка PL становится частью проекта, лучше иметь нормальный программный интерфейс, тесты и контролируемую обработку ошибок.

Главная мысль здесь не в том, что fpga_loader делает что-то магическое. Он не заменяет FPGA Manager, не парсит bitstream и не лечит неправильный device tree. Он просто убирает shell-склейку из приложения и даёт небольшой проверяемый слой над sysfs/configfs.

Репозиторий: github.com/FernandesKA/fpga_loader