В прошлой статье мы рассматривали настройку 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
Напишем небольшой модуль, который будет просто мигать диодом:
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):
Соединяем клоки, reset, выводим 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 нужно пересобрать. Синтез, имплементация:
Генерируем bitstream. Экспортируем XSA вместе с bitstream: Export hardware → Include bitstream.
Дальше подкидываем 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
Вся низкоуровневая работа остаётся в ядре. Пользовательская утилита только:
- проверяет входные параметры;
- копирует bitstream в firmware directory;
- записывает flags в FPGA Manager;
- инициирует загрузку через
firmware_nameилиfirmware; - проверяет итоговое состояние FPGA Manager;
- при необходимости накладывает или удаляет Device Tree Overlay через configfs.
Структура проекта
.
├── CMakeLists.txt
├── inc
│ ├── dt_overlay.hpp
│ ├── file_utils.hpp
│ └── fpga_manager.hpp
├── scripts
│ └── bit-to-bin.sh
├── src
│ ├── dt_overlay.cpp
│ ├── file_utils.cpp
│ ├── fpga_manager.cpp
│ └── main.cpp
└── tests
├── CMakeLists.txt
├── helpers.hpp
├── test_dt_overlay.cpp
├── test_file_utils.cpp
└── test_fpga_manager.cpp
Логика вынесена из main.cpp в библиотечные классы, чтобы проект можно было использовать двумя способами: как CLI-утилиту и как библиотеку внутри своего приложения.
CLI удобен для отладки на целевой плате:
fpga-loader status
fpga-loader /tmp/blink.bit.bin
fpga-loader -m overlay --dtbo /tmp/blink.dtbo --name blink
fpga-loader -m overlay --remove --name blink
Библиотечный вариант полезен, если загрузка FPGA должна быть частью приложения. load() возвращает не просто bool, а LoadResult: код ошибки, текстовое описание и последнее состояние FPGA Manager:
#include <chrono>
#include <iostream>
#include "fpga_manager.hpp"
int main()
{
fpga::FpgaManagerConfig cfg;
cfg.manager_path = "/sys/class/fpga_manager/fpga0";
cfg.firmware_dir = "/lib/firmware";
cfg.timeout = std::chrono::milliseconds(5000);
cfg.verbose = true;
fpga::FpgaManager mgr(cfg);
auto result = mgr.load("/tmp/blink.bit.bin", fpga::FpgaFlagNone);
if (!result) {
std::cerr << "FPGA load failed: " << result.message << '\n';
return 1;
}
std::cout << "FPGA programmed, state=" << result.state << '\n';
return 0;
}
Подключение через CMake:
add_subdirectory(third_party/fpga_loader)
target_link_libraries(my_app PRIVATE fpga::loader)
Класс FpgaManager
Основная часть работы — в классе FpgaManager. 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 начинают мигать.
| Параметр | Значение |
|---|---|
| Board | RK-ZYNQ7020-F |
| SoC | Zynq-7000 / XC7Z020 |
| Flow | Buildroot + 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