From 3a4e990994680a094c11af31cf13da90baab5528 Mon Sep 17 00:00:00 2001 From: David Garske Date: Mon, 1 Jun 2026 15:24:38 -0700 Subject: [PATCH] Add wolfBoot FIT support for loading bitstream --- Makefile | 1 + arch.mk | 7 + config/examples/zynqmp.config | 8 + docs/Targets.md | 57 +++++++ hal/hal.c | 20 +++ hal/versal.c | 19 +++ hal/versal.its | 25 +++ hal/zynq.c | 54 ++++++ hal/zynq7000.c | 123 ++++++++++++++ hal/zynq7000.h | 60 ++++++- include/fdt.h | 19 ++- include/hal.h | 16 ++ include/hal_fpga.h | 60 +++++++ include/target.h.in | 5 + options.mk | 28 ++++ src/fdt.c | 176 +++++++++++++++++++- src/update_disk.c | 15 +- src/update_ram.c | 15 +- tools/fdt-parser/fdt-parser.c | 6 +- tools/unit-tests/Makefile | 7 + tools/unit-tests/unit-fit-fpga.c | 274 +++++++++++++++++++++++++++++++ 21 files changed, 983 insertions(+), 12 deletions(-) create mode 100644 include/hal_fpga.h create mode 100644 tools/unit-tests/unit-fit-fpga.c diff --git a/Makefile b/Makefile index 61954b9ebb..e247957fc4 100644 --- a/Makefile +++ b/Makefile @@ -653,6 +653,7 @@ include/target.h: $(TARGET_H_TEMPLATE) FORCE sed -e "s/@WOLFBOOT_LOAD_ADDRESS@/$(WOLFBOOT_LOAD_ADDRESS)/g" | \ sed -e "s/@WOLFBOOT_LOAD_DTS_ADDRESS@/$(WOLFBOOT_LOAD_DTS_ADDRESS)/g" | \ sed -e "s/@WOLFBOOT_LOAD_RAMDISK_ADDRESS@/$(WOLFBOOT_LOAD_RAMDISK_ADDRESS)/g" | \ + sed -e "s/@WOLFBOOT_LOAD_FPGA_ADDRESS@/$(WOLFBOOT_LOAD_FPGA_ADDRESS)/g" | \ sed -e "s|@WOLFBOOT_RAMBOOT_MAX_SIZE_DEFINE@|$(if $(strip $(WOLFBOOT_RAMBOOT_MAX_SIZE)),#define WOLFBOOT_RAMBOOT_MAX_SIZE $(WOLFBOOT_RAMBOOT_MAX_SIZE),/* WOLFBOOT_RAMBOOT_MAX_SIZE undefined */)|g" | \ sed -e "s/@WOLFBOOT_PARTITION_SELF_HEADER_ADDRESS@/$(WOLFBOOT_PARTITION_SELF_HEADER_ADDRESS)/g" \ > $@ diff --git a/arch.mk b/arch.mk index 1973d5fdc0..5f8bb8acd9 100644 --- a/arch.mk +++ b/arch.mk @@ -74,6 +74,8 @@ ifeq ($(ARCH),AARCH64) # Support detection and skip of U-Boot legacy header */ CFLAGS+=-DWOLFBOOT_UBOOT_LEGACY CFLAGS+=-DWOLFBOOT_DUALBOOT + # FPGA bitstream-from-FIT is supported via the PMU firmware + # (PM_FPGA_LOAD). Enable with FPGA_BITSTREAM=1 (opt-in). ifeq ($(HW_SHA3),1) # Use HAL for hash (see zynqmp.c) @@ -90,6 +92,9 @@ ifeq ($(ARCH),AARCH64) CFLAGS+=-DWOLFBOOT_DUALBOOT # Support detection and skip of U-Boot legacy header CFLAGS+=-DWOLFBOOT_UBOOT_LEGACY + # NOTE: FPGA_BITSTREAM is stubbed on Versal (PL config is a PDI loaded + # by the PLM via XilLoader Load-PDI IPI - not yet implemented). Leave + # FPGA_BITSTREAM off until that path lands. # PLM owns RVBAR on Versal in JTAG boot; skip RVBAR writes CFLAGS+=-DSKIP_RVBAR=1 # Disable SDMA for multi-block transfers - use PIO instead. @@ -350,6 +355,8 @@ ifeq ($(ARCH),ARM) # positive probability from ~2^-32 to ~2^-64, matching what U-Boot's # own mkimage/bootm does. CFLAGS+=-DWOLFBOOT_UBOOT_LEGACY + # FPGA bitstream-from-FIT is supported via the DevC/PCAP DMA engine + # (full bitstream). Enable with FPGA_BITSTREAM=1 (opt-in). endif ifeq ($(TARGET),va416x0) diff --git a/config/examples/zynqmp.config b/config/examples/zynqmp.config index 114a9eab42..a3ecd52024 100644 --- a/config/examples/zynqmp.config +++ b/config/examples/zynqmp.config @@ -81,6 +81,14 @@ WOLFBOOT_LOAD_DTS_ADDRESS?=0x11800000 WOLFBOOT_DTS_BOOT_ADDRESS?=0x7B0000 WOLFBOOT_DTS_UPDATE_ADDRESS?=0x39B0000 +# Optional: program the PL from an "fpga" sub-image in the FIT before boot +# (handed to the PMU firmware via PM_FPGA_LOAD). Requires GZIP=1 when the +# bitstream node uses compression="gzip" (the typical mkimage output). The +# fpga node usually has no `load` property, so set a DDR staging address +# clear of the FIT (0x10000000), kernel (0x200000), and DTS (0x11800000). +#FPGA_BITSTREAM?=1 +#WOLFBOOT_LOAD_FPGA_ADDRESS?=0x40000000 + CROSS_COMPILE=aarch64-none-elf- #CROSS_COMPILE=aarch64-linux-gnu- diff --git a/docs/Targets.md b/docs/Targets.md index 1581f3a224..036109d0e7 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -4070,6 +4070,63 @@ FDT: Set chosen (...), linux,initrd-start=1073741824 FDT: Set chosen (...), linux,initrd-end=... ``` +**FPGA bitstream from FIT** + +wolfBoot can program the PL (FPGA fabric) from an `fpga` sub-image carried in the same signed FIT, using the standard U-Boot convention: a sub-image with `type = "fpga"`, referenced from the configuration node via an optional `fpga = ""` property, with a `compatible` string naming the load method. The PL is programmed before the kernel/DTB are loaded so PL-dependent clocks and peripherals come up first. The outer wolfBoot signature authenticates the whole FIT (bitstream included), so no per-image hashing is required. + +Enable with `FPGA_BITSTREAM=1` (off by default). A failed PL load is fatal (`wolfBoot_panic`) unless `FPGA_NONFATAL=1` is also set, which downgrades it to a logged warning that continues the boot. + +As produced by `mkimage`, the fpga sub-image is typically gzip-compressed (`compression = "gzip"`) and carries NO `load` property (U-Boot decompresses it into a scratch buffer before programming). wolfBoot mirrors this: set `WOLFBOOT_LOAD_FPGA_ADDRESS` to a DDR staging address (clear of the FIT at `WOLFBOOT_LOAD_ADDRESS`, the kernel, and the DTS), and the bitstream is decompressed there before the PL is programmed. Build with `GZIP=1` (already default in the example configs). If the fpga node does provide its own `load`, leave `WOLFBOOT_LOAD_FPGA_ADDRESS=0` to honor it. + +```sh +cp config/examples/zynqmp.config .config +make FPGA_BITSTREAM=1 WOLFBOOT_LOAD_FPGA_ADDRESS=0x40000000 +``` + +Per-target support: +- **ZynqMP** (`TARGET=zynq`): supported. The bitstream (a bootgen `.bin` with the `66 55 99 aa` sync word, not a Vivado `.bit`) is staged in DDR and handed to the PMU firmware via the `PM_FPGA_LOAD` EEMI call (xilfpga over the CSU DMA / PCAP). The size is passed in bytes and the flags are 0 for a full legacy bitstream, matching stock Xilinx U-Boot; no software byte-swap is applied. +- **Zynq-7000** (`TARGET=zynq7000`): supported (full bitstream). Programmed directly through the DevC/PCAP DMA engine (UG585 ch.6). Partial reconfiguration is not yet implemented. +- **Versal** (`TARGET=versal`): not yet implemented - Versal programs the PL with a PDI loaded by the PLM (XilLoader Load-PDI IPI), not a raw bitstream. `hal_fpga_load` is a stub; leave `FPGA_BITSTREAM` off on Versal until that path lands. + +The `compatible` string selects full vs partial: any value containing `partial` requests partial reconfiguration, otherwise a full bitstream is loaded. Typical full-bitstream values are `u-boot,fpga-legacy`, `u-boot,zynqmp-fpga-ddrauth`, or `u-boot,zynqmp-fpga-enc`. + +Decompressed-size validation: the fpga node carries no explicit uncompressed-size property, but for `compression = "gzip"` the gzip trailer encodes it. wolfBoot verifies the gzip CRC32 and ISIZE (decompressed length) on completion and bounds the output by `WOLFBOOT_FIT_MAX_FPGA`, so a truncated or corrupt bitstream is rejected (fatal, or skipped under `FPGA_NONFATAL=1`). The byte count handed to `PM_FPGA_LOAD` is the validated decompressed length. Note that `WOLFBOOT_FIT_MAX_FPGA` is a compile-time cap only (set it with `CFLAGS_EXTRA+=-DWOLFBOOT_FIT_MAX_FPGA=...`; it defaults to `WOLFBOOT_FIT_MAX_DECOMP`, 256 MB) - it is a sanity ceiling, NOT the size of your DDR staging region. It does not by itself stop the bitstream from overrunning the kernel/DTB/FIT areas, so choosing a `WOLFBOOT_LOAD_FPGA_ADDRESS` clear of those regions (and, if you want a tight bound, lowering `WOLFBOOT_FIT_MAX_FPGA` to your staging window) is the integrator's responsibility. + +Multiple configurations: wolfBoot boots the FIT's `default` configuration. For a FIT that carries several boards' configurations selected at runtime (e.g. U-Boot's `bootm #conf-`), build with `FIT_CONFIG_SELECT=1` and provide a `hal_fit_config_name()` in your integration that returns the config node name to boot (e.g. `"conf-fcm"`), or NULL to fall back to `default`. wolfBoot ships only the weak default (returns NULL) - the board-detection logic (PS GPIO, CHIPID, etc.) is intentionally left to the integrator and is not part of upstream. + +Example FIT layout (matches `mkimage` output - gzip, no `load`): + +```dts +images { + kernel-1 { ... }; + fdt-1 { ... }; + fpga-1 { + description = "FPGA bitstream"; + data = /incbin/("system.bit.bin.gz"); /* gzip of bootgen .bin */ + type = "fpga"; + arch = "arm64"; /* informational; not read by wolfBoot */ + compression = "gzip"; + compatible = "u-boot,fpga-legacy"; + hash-1 { algo = "sha256"; }; /* covered by outer sig */ + }; +}; +configurations { + default = "conf-zcu102"; + conf-zcu102 { + kernel = "kernel-1"; + fdt = "fdt-1"; + fpga = "fpga-1"; + }; +}; +``` + +Successful programming prints (ZynqMP): +``` +FIT: programming FPGA 'fpga-1' (N bytes, full) +FPGA status: 0x... +FIT: FPGA programmed +``` + ## Xilinx Zynq-7000 (ZC702) diff --git a/hal/hal.c b/hal/hal.c index 5e2545554c..b2a73b6162 100644 --- a/hal/hal.c +++ b/hal/hal.c @@ -356,3 +356,23 @@ WEAKFUNCTION int hal_dice_get_attest_pubkey(uint8_t *buf, size_t *len) return -1; } #endif /* WOLFBOOT_DICE_HW */ + +#ifdef WOLFBOOT_FPGA_BITSTREAM +WEAKFUNCTION int hal_fpga_load(uint32_t flags, uintptr_t addr, size_t size) +{ + /* No FPGA loader for this target; implement in the target hal. */ + (void)flags; + (void)addr; + (void)size; + return -1; +} +#endif /* WOLFBOOT_FPGA_BITSTREAM */ + +#ifdef WOLFBOOT_FIT_CONFIG_SELECT +WEAKFUNCTION const char* hal_fit_config_name(void) +{ + /* Default: use the FIT's own `default` configuration. A target + * implements this to select a per-board configuration at runtime. */ + return NULL; +} +#endif /* WOLFBOOT_FIT_CONFIG_SELECT */ diff --git a/hal/versal.c b/hal/versal.c index 8be09fc5a7..29b5211547 100644 --- a/hal/versal.c +++ b/hal/versal.c @@ -1354,6 +1354,25 @@ int RAMFUNCTION hal_flash_erase(uintptr_t address, int len) return -1; } +#ifdef WOLFBOOT_FPGA_BITSTREAM +/* Versal programs the PL with a PDI (Programmable Device Image), not a raw + * bitstream. Runtime PL configuration is done by the PLM via the XilLoader + * "Load PDI" command (XLOADER_CMD_ID_LOAD_PDI), reached by building an IPI + * request to the PLM channel with the DDR PDI address/size and polling the + * PLM response. That path is not yet implemented; until it lands, leave + * FPGA_BITSTREAM disabled for Versal (the FIT loader treats a failed load + * as fatal unless FPGA_NONFATAL is set). */ +int hal_fpga_load(uint32_t flags, uintptr_t addr, size_t size) +{ + (void)flags; + (void)addr; + (void)size; + wolfBoot_printf("Versal FPGA/PDI load not implemented " + "(needs PLM XilLoader Load-PDI IPI)\n"); + return -1; +} +#endif /* WOLFBOOT_FPGA_BITSTREAM */ + /* ============================================================================ * External Flash Interface diff --git a/hal/versal.its b/hal/versal.its index 2b160cb3a1..f2aa203495 100644 --- a/hal/versal.its +++ b/hal/versal.its @@ -29,6 +29,30 @@ algo = "sha256"; }; }; + /* FPGA bitstream sub-image (requires FPGA_BITSTREAM=1). Add the + * "fpga = ..." reference to the configuration node below. The + * bitstream must be a bootgen .bin staged to its load address. + * NOTE: Versal PL config is a PDI loaded by the PLM (XilLoader + * Load-PDI IPI) - the wolfBoot Versal hal_fpga_load is currently + * a stub, so leave this disabled on Versal. On ZynqMP/Zynq-7000 + * this is the working convention. + * + * As emitted by mkimage the fpga node is usually gzip-compressed + * and has no "load" property; set WOLFBOOT_LOAD_FPGA_ADDRESS to a + * DDR staging address and build with GZIP=1. + * + * fpga-1 { + * description = "FPGA bitstream"; + * data = /incbin/("../system.bit.bin.gz"); + * type = "fpga"; + * arch = "arm64"; + * compression = "gzip"; + * compatible = "u-boot,fpga-legacy"; + * hash-1 { + * algo = "sha256"; + * }; + * }; + */ }; configurations { default = "conf1"; @@ -36,6 +60,7 @@ description = "Linux kernel and FDT blob"; kernel = "kernel-1"; fdt = "fdt-1"; + /* fpga = "fpga-1"; */ hash-1 { algo = "sha256"; }; diff --git a/hal/zynq.c b/hal/zynq.c index fb1262345d..cd12f18314 100644 --- a/hal/zynq.c +++ b/hal/zynq.c @@ -36,6 +36,7 @@ #include #include "image.h" #include "printf.h" +#include "hal_fpga.h" #include #include @@ -197,6 +198,13 @@ static void smc_call(struct pt_regs *args) #define PM_MMIO_WRITE 0x13 #define PM_MMIO_READ 0x14 +/* FPGA / PL programming (xilfpga via PMU firmware / TF-A) */ +#define PM_FPGA_LOAD 0x16 /* 22 */ +#define PM_FPGA_GET_STATUS 0x17 /* 23 */ +/* pm_fpga_load flags (bit 0 selects full vs partial bitstream) */ +#define XFPGA_FULLBIT_EN 0x0 +#define XFPGA_PARTIAL_EN 0x1 + /* AES */ /* requires PMU built with -DENABLE_SECURE_VAL=1 */ #define PM_SECURE_AES 0x2F @@ -1713,6 +1721,52 @@ int RAMFUNCTION hal_flash_erase(uintptr_t address, int len) return 0; } +#ifdef WOLFBOOT_FPGA_BITSTREAM +/* Program the PL by handing the bitstream to the PMU firmware (xilfpga) + * via the PM_FPGA_LOAD EEMI call. The bitstream must be a bootgen .bin + * resident in DDR; it is flushed from the D-cache so the CSU DMA sees + * the committed bytes. */ +int hal_fpga_load(uint32_t flags, uintptr_t addr, size_t size) +{ + uint32_t ret_payload[PM_ARGS_CNT]; + uint32_t pmflags; + + /* PM_FPGA_LOAD takes the bitstream size in BYTES (the PMU firmware + * divides by the word length internally for the CSU DMA). This + * matches stock Xilinx U-Boot (drivers/fpga/zynqmppl.c passes + * bsize verbatim). For a legacy full bitstream the flags argument + * is 0; bit 0 selects partial. */ + pmflags = (flags == HAL_FPGA_PARTIAL) ? XFPGA_PARTIAL_EN : XFPGA_FULLBIT_EN; + + /* Ensure the bitstream is committed to DDR before the CSU DMA reads it. */ + flush_dcache_range((unsigned long)addr, (unsigned long)(addr + size)); + + memset(ret_payload, 0, sizeof(ret_payload)); + /* arg0=addr_low, arg1=addr_high, arg2=size(bytes), arg3=flags */ + pmu_request(PM_FPGA_LOAD, + (uint32_t)(addr & 0xFFFFFFFF), (uint32_t)((uint64_t)addr >> 32), + (uint32_t)size, pmflags, ret_payload); + if (ret_payload[0] != 0) { + wolfBoot_printf("PM_FPGA_LOAD failed: %u\n", ret_payload[0]); + return -1; + } + + /* Confirm the PL reports configured (PCAP status). This is + * informational - the load already succeeded above - so a failed + * query is logged but does not fail the call. */ + memset(ret_payload, 0, sizeof(ret_payload)); + pmu_request(PM_FPGA_GET_STATUS, 0, 0, 0, 0, ret_payload); + if (ret_payload[0] == 0) { + wolfBoot_printf("FPGA status: 0x%x\n", ret_payload[1]); + } + else { + wolfBoot_printf("FPGA status query failed: %u\n", ret_payload[0]); + } + + return 0; +} +#endif /* WOLFBOOT_FPGA_BITSTREAM */ + /* Xilinx Write uses SPI mode and Page Program 0x02 */ /* Issues using write with QSPI mode */ int RAMFUNCTION ext_flash_write(uintptr_t address, const uint8_t *data, int len) diff --git a/hal/zynq7000.c b/hal/zynq7000.c index a6a9850f33..349758c5c7 100644 --- a/hal/zynq7000.c +++ b/hal/zynq7000.c @@ -27,6 +27,7 @@ #include "image.h" #include "printf.h" #include "hal/zynq7000.h" +#include "hal_fpga.h" #ifndef ARCH_ARM # error "wolfBoot zynq7000 HAL: wrong architecture selected. Please compile with ARCH=ARM." @@ -871,6 +872,128 @@ uint64_t hal_get_timer_us(void) return (count * 1000000ULL) / (uint64_t)Z7_GTIMER_FREQ_HZ; } +#ifdef WOLFBOOT_FPGA_BITSTREAM +/* PL programming timeout (microseconds). */ +#ifndef Z7_FPGA_TIMEOUT_US +#define Z7_FPGA_TIMEOUT_US 1000000ULL /* 1 second */ +#endif + +/* Clean the D-cache over [start, start+len) by MVA so the DevC DMA sees + * the committed bitstream bytes (DCCMVAC, L1 line = 32B). Mirrors the + * SDMA coherency path. */ +static void z7_dcache_clean_range(uintptr_t start, uint32_t len) +{ + uintptr_t addr; + uintptr_t end = (start + len + 31U) & ~31U; + start &= ~31U; + for (addr = start; addr < end; addr += 32U) { + __asm__ volatile("mcr p15, 0, %0, c7, c10, 1" : : "r"(addr) : "memory"); + } + __asm__ volatile("dsb sy" : : : "memory"); +} + +/* Program the PL from a bootgen .bin bitstream resident in DDR using the + * DevC PCAP DMA engine (UG585 ch.6 / Xilinx XDcfg full-bitstream flow). + * Only the full-bitstream path is implemented; partial reconfiguration + * returns an error. */ +int hal_fpga_load(uint32_t flags, uintptr_t addr, size_t size) +{ + uint32_t sts; + uint32_t words = (uint32_t)((size + 3U) / 4U); + uint64_t t0; + + if (flags == HAL_FPGA_PARTIAL) { + /* Partial reconfig leaves PROG_B asserted and routes via PCAP_PR; + * not implemented in this initial full-bitstream path. */ + wolfBoot_printf("Z7 FPGA: partial reconfig not implemented\n"); + return -1; + } + if (addr > 0xFFFFFFFFU || size == 0) { + return -1; + } + /* The DevC DMA descriptor encodes the "last" flag in the source + * address LSB, so the bitstream buffer must be word-aligned. */ + if ((addr & 0x3U) != 0U) { + wolfBoot_printf("Z7 FPGA: bitstream addr not word-aligned\n"); + return -1; + } + + /* 1. Unlock DevC and select PCAP (not ICAP). */ + Z7_DEVC_UNLOCK = Z7_DEVC_UNLOCK_KEY; + Z7_DEVC_CTRL |= (Z7_DEVC_CTRL_PCAP_MODE | Z7_DEVC_CTRL_PCAP_PR); + /* Disable internal PCAP loopback. */ + Z7_DEVC_MCTRL &= ~Z7_DEVC_MCTRL_PCAP_LPBK; + + /* 2. Clear sticky interrupts. */ + Z7_DEVC_INT_STS = Z7_DEVC_INT_ALL; + + /* 3. Pulse PROG_B low then high to clear the PL (full bitstream). */ + Z7_DEVC_CTRL &= ~Z7_DEVC_CTRL_PCFG_PROG_B; + t0 = hal_get_timer_us(); + while (Z7_DEVC_STATUS & Z7_DEVC_STATUS_PCFG_INIT) { + if (hal_get_timer_us() - t0 > Z7_FPGA_TIMEOUT_US) { + wolfBoot_printf("Z7 FPGA: timeout waiting PCFG_INIT clear\n"); + return -1; + } + } + Z7_DEVC_CTRL |= Z7_DEVC_CTRL_PCFG_PROG_B; + t0 = hal_get_timer_us(); + while (!(Z7_DEVC_STATUS & Z7_DEVC_STATUS_PCFG_INIT)) { + if (hal_get_timer_us() - t0 > Z7_FPGA_TIMEOUT_US) { + wolfBoot_printf("Z7 FPGA: timeout waiting PCFG_INIT set\n"); + return -1; + } + } + + /* 4. Clear interrupts. */ + Z7_DEVC_INT_STS = Z7_DEVC_INT_ALL; + + /* 5. Make the bitstream coherent in DDR for the DMA. Clean the same + * rounded-up word length the DMA reads (words * 4), not just the + * byte size, so a non-word-aligned tail is covered. (Xilinx .bin + * bitstreams are word streams, so this normally equals size.) */ + z7_dcache_clean_range(addr, words * 4U); + + /* 6. Program the DMA: src = DDR bitstream (LSB=1 marks last descriptor), + * dst = PCAP sentinel. Lengths are in 32-bit words. */ + Z7_DEVC_DMA_SRC = ((uint32_t)addr) | Z7_DEVC_DMA_LAST; + Z7_DEVC_DMA_DST = Z7_DEVC_DMA_DEST_PCAP; + Z7_DEVC_DMA_SRC_LEN = words; + Z7_DEVC_DMA_DST_LEN = words; + + /* 7. Wait for DMA done (and check error bits). */ + t0 = hal_get_timer_us(); + do { + sts = Z7_DEVC_INT_STS; + if (sts & Z7_DEVC_INT_ERR_MASK) { + wolfBoot_printf("Z7 FPGA: DMA error, INT_STS=0x%x\n", sts); + return -1; + } + if (hal_get_timer_us() - t0 > Z7_FPGA_TIMEOUT_US) { + wolfBoot_printf("Z7 FPGA: timeout waiting DMA done\n"); + return -1; + } + } while (!(sts & Z7_DEVC_INT_DMA_DONE)); + + /* 8. Wait for the PL to report configuration complete (and surface a + * config error immediately rather than spinning to the timeout). */ + t0 = hal_get_timer_us(); + do { + sts = Z7_DEVC_INT_STS; + if (sts & Z7_DEVC_INT_ERR_MASK) { + wolfBoot_printf("Z7 FPGA: config error, INT_STS=0x%x\n", sts); + return -1; + } + if (hal_get_timer_us() - t0 > Z7_FPGA_TIMEOUT_US) { + wolfBoot_printf("Z7 FPGA: timeout waiting PCFG_DONE\n"); + return -1; + } + } while (!(sts & Z7_DEVC_INT_PCFG_DONE)); + + return 0; +} +#endif /* WOLFBOOT_FPGA_BITSTREAM */ + #if defined(DISK_SDCARD) || defined(DISK_EMMC) /* ============================================================================ * SDHCI (SD Card / eMMC) Platform Support diff --git a/hal/zynq7000.h b/hal/zynq7000.h index cfbb46b946..c66c917c33 100644 --- a/hal/zynq7000.h +++ b/hal/zynq7000.h @@ -199,8 +199,66 @@ #define Z7_GTIMER_FREQ_HZ 333333333UL #endif -/* DevC (Device Configuration: AES + bitstream loader). UG585 ch.6. */ +/* DevC (Device Configuration: AES + bitstream loader). UG585 ch.6. + * Register offsets/bits mirror Xilinx xdevcfg_hw.h (XDCFG_*). */ #define Z7_DEVC_BASE 0xF8007000UL +#define Z7_DEVC_CTRL (*((volatile uint32_t*)(Z7_DEVC_BASE + 0x00))) +#define Z7_DEVC_LOCK (*((volatile uint32_t*)(Z7_DEVC_BASE + 0x04))) +#define Z7_DEVC_CFG (*((volatile uint32_t*)(Z7_DEVC_BASE + 0x08))) +#define Z7_DEVC_INT_STS (*((volatile uint32_t*)(Z7_DEVC_BASE + 0x0C))) +#define Z7_DEVC_INT_MASK (*((volatile uint32_t*)(Z7_DEVC_BASE + 0x10))) +#define Z7_DEVC_STATUS (*((volatile uint32_t*)(Z7_DEVC_BASE + 0x14))) +#define Z7_DEVC_DMA_SRC (*((volatile uint32_t*)(Z7_DEVC_BASE + 0x18))) +#define Z7_DEVC_DMA_DST (*((volatile uint32_t*)(Z7_DEVC_BASE + 0x1C))) +#define Z7_DEVC_DMA_SRC_LEN (*((volatile uint32_t*)(Z7_DEVC_BASE + 0x20))) +#define Z7_DEVC_DMA_DST_LEN (*((volatile uint32_t*)(Z7_DEVC_BASE + 0x24))) +#define Z7_DEVC_UNLOCK (*((volatile uint32_t*)(Z7_DEVC_BASE + 0x34))) +#define Z7_DEVC_MCTRL (*((volatile uint32_t*)(Z7_DEVC_BASE + 0x80))) + +/* CTRL bits */ +#define Z7_DEVC_CTRL_FORCE_RST 0x80000000U /* bit 31 */ +#define Z7_DEVC_CTRL_PCFG_PROG_B 0x40000000U /* bit 30 */ +#define Z7_DEVC_CTRL_PCAP_PR 0x08000000U /* bit 27: 1=PCAP, 0=ICAP */ +#define Z7_DEVC_CTRL_PCAP_MODE 0x04000000U /* bit 26: PCAP enable */ +#define Z7_DEVC_CTRL_QUARTER_RATE 0x02000000U /* bit 25 */ + +/* INT_STS bits */ +#define Z7_DEVC_INT_DMA_DONE 0x00002000U /* bit 13 */ +#define Z7_DEVC_INT_D_P_DONE 0x00001000U /* bit 12: DMA+PCAP done */ +#define Z7_DEVC_INT_PCFG_DONE 0x00000004U /* bit 2 */ +/* DMA / AXI / config error aggregate. Matches XDcfg + * XDCFG_IXR_ERROR_FLAGS_MASK (xdevcfg_hw.h): AXI WTO/WERR/RTO/RERR, + * RX_FIFO_OV, DMA_CMD_ERR, DMA_Q_OV, P2D_LEN_ERR, HMAC_ERR. */ +#define Z7_DEVC_INT_AXI_WTO 0x00800000U +#define Z7_DEVC_INT_AXI_WERR 0x00400000U +#define Z7_DEVC_INT_AXI_RTO 0x00200000U +#define Z7_DEVC_INT_AXI_RERR 0x00100000U +#define Z7_DEVC_INT_RX_FIFO_OV 0x00040000U +#define Z7_DEVC_INT_DMA_CMD_ERR 0x00008000U +#define Z7_DEVC_INT_DMA_Q_OV 0x00004000U +#define Z7_DEVC_INT_P2D_LEN_ERR 0x00000800U +#define Z7_DEVC_INT_HMAC_ERR 0x00000040U +#define Z7_DEVC_INT_ERR_MASK (Z7_DEVC_INT_AXI_WTO | Z7_DEVC_INT_AXI_WERR \ + | Z7_DEVC_INT_AXI_RTO | Z7_DEVC_INT_AXI_RERR \ + | Z7_DEVC_INT_RX_FIFO_OV \ + | Z7_DEVC_INT_DMA_CMD_ERR \ + | Z7_DEVC_INT_DMA_Q_OV \ + | Z7_DEVC_INT_P2D_LEN_ERR \ + | Z7_DEVC_INT_HMAC_ERR) /* 0x00F4C840 */ +#define Z7_DEVC_INT_ALL 0xFFFFFFFFU + +/* STATUS bits */ +#define Z7_DEVC_STATUS_DMA_CMD_FULL 0x80000000U /* bit 31 */ +#define Z7_DEVC_STATUS_PCFG_INIT 0x00000010U /* bit 4 */ + +/* MCTRL bits */ +#define Z7_DEVC_MCTRL_PCAP_LPBK 0x00000010U /* internal loopback */ + +/* UNLOCK magic and PCAP DMA sentinel (UG585 ch.6 / XDcfg). The DMA + * address LSB set to 1 marks the last (only) descriptor in the chain. */ +#define Z7_DEVC_UNLOCK_KEY 0x757BDF0DU +#define Z7_DEVC_DMA_DEST_PCAP 0xFFFFFFFFU +#define Z7_DEVC_DMA_LAST 0x00000001U /* GIC (PL390 / GIC-400 v1) - per-CPU interface and distributor. */ #define Z7_GIC_CPUIF_BASE 0xF8F00100UL diff --git a/include/fdt.h b/include/fdt.h index 69941c03ac..cd726f19c1 100644 --- a/include/fdt.h +++ b/include/fdt.h @@ -169,7 +169,15 @@ int fdt_shrink(void* fdt); /* FIT */ const char* fit_find_images(void* fdt, const char** pkernel, const char** pflat_dt, - const char** pramdisk); + const char** pramdisk, const char** pfpga); +/* Return a pointer to a subimage's "compatible" property, or NULL if + * absent. NOTE: "compatible" is a devicetree string-list (one or more + * NUL-separated strings); this returns a pointer to the raw property + * data, i.e. the FIRST string only. Callers that must inspect every + * entry (e.g. to detect "partial") have to obtain the property length + * via fdt_getprop() and scan the whole buffer - fit_load_fpga() does + * this internally rather than relying on this helper. */ +const char* fit_get_compatible(void* fdt, const char* image); void* fit_load_image(void* fdt, const char* image, int* lenp); void* fit_load_image_ex(void* fdt, const char* image, int* lenp, uint32_t out_max); /* Load (and, if compressed, decompress) a FIT subimage directly to a @@ -192,6 +200,15 @@ int fdt_fixup_initrd(void* fdt, uint64_t start, uint64_t size); int fit_load_ramdisk(void* fit, const char* ramdisk_node, void* dts_addr); #endif +#ifdef WOLFBOOT_FPGA_BITSTREAM +/* Locate a FIT fpga subimage, stage it to its `load` address (or use + * the in-FIT data pointer) and program the PL via hal_fpga_load(). + * Returns 0 on success (including when fpga_node is NULL), negative on + * failure. When WOLFBOOT_FPGA_NONFATAL is defined a programming failure + * is logged and 0 is returned instead. */ +int fit_load_fpga(void* fdt, const char* fpga_node); +#endif + #ifdef __cplusplus } #endif diff --git a/include/hal.h b/include/hal.h index bf2f5b138b..701e400144 100644 --- a/include/hal.h +++ b/include/hal.h @@ -108,6 +108,22 @@ void hal_prepare_boot(void); void *hal_get_dts_update_address(void); #endif +#ifdef WOLFBOOT_FIT_CONFIG_SELECT + /* + * Return the name of the FIT /configurations node to boot (e.g. + * "conf-fcm"), or NULL to use the FIT's own `default` configuration. + * Lets a target select a per-board configuration at runtime (board + * detection), the way U-Boot's `bootm #conf-` does. The + * weak default returns NULL. + */ + const char* hal_fit_config_name(void); +#endif + +/* FPGA load mode constants + hal_fpga_load() prototype (kept in a standalone + * header so the per-target HAL .c files can include just this, not all of + * hal.h). Gated internally by WOLFBOOT_FPGA_BITSTREAM. */ +#include "hal_fpga.h" + #if !defined(SPI_FLASH) && !defined(QSPI_FLASH) && !defined(OCTOSPI_FLASH) /* user supplied external flash interfaces */ int ext_flash_write(uintptr_t address, const uint8_t *data, int len); diff --git a/include/hal_fpga.h b/include/hal_fpga.h new file mode 100644 index 0000000000..a611b2f558 --- /dev/null +++ b/include/hal_fpga.h @@ -0,0 +1,60 @@ +/* hal_fpga.h + * + * FPGA/PL bitstream loading HAL interface. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* Split out from hal.h so the per-target HAL implementations (hal/zynq.c, + * hal/zynq7000.c, ...) can pull in just the FPGA flag constants and the + * hal_fpga_load() prototype without including hal.h, which also drags in the + * external-flash and wolfHSM interfaces. */ + +#ifndef H_HAL_FPGA_ +#define H_HAL_FPGA_ + +#ifdef WOLFBOOT_FPGA_BITSTREAM + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* FPGA load mode (flags argument to hal_fpga_load) */ +#define HAL_FPGA_FULL 0u /* full bitstream / device image */ +#define HAL_FPGA_PARTIAL 1u /* partial reconfiguration */ + +/* + * Program the PL/FPGA fabric from an in-DDR bitstream/PDI image. + * addr/size describe the staged image buffer; the implementation + * is responsible for any required cache maintenance before the + * configuration engine reads it. Returns 0 on success, negative on + * error. The weak default returns -1 (not implemented). + */ +int hal_fpga_load(uint32_t flags, uintptr_t addr, size_t size); + +#ifdef __cplusplus +} +#endif + +#endif /* WOLFBOOT_FPGA_BITSTREAM */ + +#endif /* H_HAL_FPGA_ */ diff --git a/include/target.h.in b/include/target.h.in index 59fbe910de..5429c4814f 100644 --- a/include/target.h.in +++ b/include/target.h.in @@ -162,5 +162,10 @@ * verbatim — do not relocate". */ #define WOLFBOOT_LOAD_RAMDISK_ADDRESS @WOLFBOOT_LOAD_RAMDISK_ADDRESS@ +/* DDR staging address for the (decompressed) FPGA bitstream (used when + * WOLFBOOT_FPGA_BITSTREAM is set; otherwise unused). 0 means "use the FIT + * image's `load` property verbatim". */ +#define WOLFBOOT_LOAD_FPGA_ADDRESS @WOLFBOOT_LOAD_FPGA_ADDRESS@ + #endif /* !H_TARGETS_TARGET_ */ diff --git a/options.mk b/options.mk index 52146818d1..f1f2ee40a4 100644 --- a/options.mk +++ b/options.mk @@ -916,6 +916,34 @@ ifeq ($(FIT_RAMDISK),1) CFLAGS+=-DWOLFBOOT_FIT_RAMDISK endif +# FPGA_BITSTREAM=1 enables loading an "fpga" sub-image from a FIT and +# programming the PL before booting (Xilinx ZynqMP/Zynq-7000; Versal is +# stubbed pending a PLM Load-PDI path). Off by default. +FPGA_BITSTREAM ?= 0 +ifeq ($(FPGA_BITSTREAM),1) + CFLAGS+=-DWOLFBOOT_FPGA_BITSTREAM +endif +# DDR staging address for the (decompressed) FPGA bitstream. The fpga +# sub-image typically has no `load` property and is gzip-compressed, so +# it is decompressed to this address before the PL is programmed. 0 (the +# default) means "honor the FIT's own `load` property instead". Pick a +# region clear of the FIT staging area (WOLFBOOT_LOAD_ADDRESS) and the +# kernel/DTB/ramdisk load addresses. +WOLFBOOT_LOAD_FPGA_ADDRESS ?= 0 +# FPGA_NONFATAL=1 downgrades a failed PL load from fatal (panic) to a +# logged warning that continues the boot. +FPGA_NONFATAL ?= 0 +ifeq ($(FPGA_NONFATAL),1) + CFLAGS+=-DWOLFBOOT_FPGA_NONFATAL +endif +# FIT_CONFIG_SELECT=1 lets the target pick a per-board FIT configuration +# at runtime (hal_fit_config_name) instead of always booting the FIT's +# `default` config - mirrors U-Boot's `bootm #conf-`. +FIT_CONFIG_SELECT ?= 0 +ifeq ($(FIT_CONFIG_SELECT),1) + CFLAGS+=-DWOLFBOOT_FIT_CONFIG_SELECT +endif + ifeq ($(ARMORED),1) CFLAGS+=-DWOLFBOOT_ARMORED endif diff --git a/src/fdt.c b/src/fdt.c index b31feee971..60aad0884f 100644 --- a/src/fdt.c +++ b/src/fdt.c @@ -807,18 +807,37 @@ int fdt_fixup_val64(void* fdt, int off, const char* node, const char* name, /* FIT Specific */ const char* fit_find_images(void* fdt, const char** pkernel, const char** pflat_dt, - const char** pramdisk) + const char** pramdisk, const char** pfpga) { const void* val; const char *conf = NULL, *kernel = NULL, *flat_dt = NULL, *ramdisk = NULL; + const char *fpga = NULL; int off, len = 0; - /* Find the default configuration (optional) */ + /* Find the configuration to boot (optional). A target may override the + * FIT's own `default` with a per-board selection (hal_fit_config_name). */ off = fdt_find_node_offset(fdt, -1, "configurations"); if (off > 0) { - val = fdt_getprop(fdt, off, "default", &len); - if (val != NULL && len > 0) { - conf = (const char*)val; +#ifdef WOLFBOOT_FIT_CONFIG_SELECT + conf = hal_fit_config_name(); + /* If the target selected a config that is not present in this FIT, + * fall back to the default rather than silently mis-selecting + * images via the type-based search below. */ + if (conf != NULL && fdt_find_node_offset(fdt, -1, conf) <= 0) { + wolfBoot_printf("FIT: configuration '%s' not found, " + "using default\n", conf); + conf = NULL; + } + if (conf != NULL) { + wolfBoot_printf("FIT: selected configuration '%s'\n", conf); + } + if (conf == NULL) +#endif + { + val = fdt_getprop(fdt, off, "default", &len); + if (val != NULL && len > 0) { + conf = (const char*)val; + } } } if (conf != NULL) { @@ -827,6 +846,7 @@ const char* fit_find_images(void* fdt, const char** pkernel, const char** pflat_ kernel = fdt_getprop(fdt, off, "kernel", &len); flat_dt = fdt_getprop(fdt, off, "fdt", &len); ramdisk = fdt_getprop(fdt, off, "ramdisk", &len); + fpga = fdt_getprop(fdt, off, "fpga", &len); } } if (kernel == NULL) { @@ -859,6 +879,16 @@ const char* fit_find_images(void* fdt, const char** pkernel, const char** pflat_ } } } + if (fpga == NULL) { + /* find node with "type" == fpga */ + off = fdt_find_prop_offset(fdt, -1, "type", "fpga"); + if (off > 0) { + val = fdt_get_name(fdt, off, &len); + if (val != NULL && len > 0) { + fpga = (const char*)val; + } + } + } if (pkernel) *pkernel = kernel; @@ -866,10 +896,34 @@ const char* fit_find_images(void* fdt, const char** pkernel, const char** pflat_ *pflat_dt = flat_dt; if (pramdisk) *pramdisk = ramdisk; + if (pfpga) + *pfpga = fpga; return conf; } +/* Returns a pointer to the first string of the node's "compatible" + * property (a NUL-separated DT string-list), or NULL. See the header for + * the multi-entry caveat. */ +const char* fit_get_compatible(void* fdt, const char* image) +{ + const char* val; + int off, len = 0; + + if (image == NULL) { + return NULL; + } + off = fdt_find_node_offset(fdt, -1, image); + if (off <= 0) { + return NULL; + } + val = (const char*)fdt_getprop(fdt, off, "compatible", &len); + if (val != NULL && len > 0) { + return val; + } + return NULL; +} + int fdt_fixup_initrd(void* fdt, uint64_t start, uint64_t size) { int off, ret; @@ -991,7 +1045,9 @@ static void* fit_load_image_inner(void* fdt, const char* image, int* lenp, int off, len = 0; const char *comp; int complen = 0; -#ifndef WOLFBOOT_GZIP +#ifdef WOLFBOOT_GZIP + BENCHMARK_DECLARE(); +#else (void)out_max; #endif @@ -1033,6 +1089,7 @@ static void* fit_load_image_inner(void* fdt, const char* image, int* lenp, int rc; wolfBoot_printf("Decompressing Image %s (gzip): " "%p -> %p (%d bytes)\n", image, data, load, len); + BENCHMARK_START(); rc = wolfBoot_gunzip((const uint8_t*)data, (uint32_t)len, (uint8_t*)load, out_max, &out_len); if (rc != 0) { @@ -1041,8 +1098,12 @@ static void* fit_load_image_inner(void* fdt, const char* image, int* lenp, return NULL; } len = (int)out_len; - wolfBoot_printf("Decompressed %s: %u bytes\n", image, + /* No trailing newline: BENCHMARK_END("") appends + * " ( ms)\r\n" under BOOT_BENCHMARK, or just "\r\n" + * otherwise. */ + wolfBoot_printf("Decompressed %s: %u bytes", image, out_len); + BENCHMARK_END(""); #else wolfBoot_printf("FIT: subimage '%s' has compression=" "\"gzip\" but WOLFBOOT_GZIP is not enabled in " @@ -1124,4 +1185,105 @@ void* fit_load_image_to(void* fdt, const char* image, void* dst, return fit_load_image_inner(fdt, image, lenp, dst_max, dst); } +#ifdef WOLFBOOT_FPGA_BITSTREAM +/* Minimal length-bounded substring search (strstr is not provided by + * wolfBoot's freestanding string.c). Searches the first hlen bytes of + * haystack for needle. hlen is an explicit length so this works over a + * DT "compatible" property, which is a list of NUL-separated strings: + * a needle with no embedded NUL (e.g. "partial") matches within any one + * entry, and a NUL separator can never be part of the match. Returns 1 + * if found. */ +static int fit_str_contains(const char* haystack, int hlen, const char* needle) +{ + int nlen, i; + + if (haystack == NULL || needle == NULL || hlen <= 0) { + return 0; + } + nlen = (int)strlen(needle); + if (nlen == 0 || nlen > hlen) { + return 0; + } + for (i = 0; i + nlen <= hlen; i++) { + if (strncmp(haystack + i, needle, (size_t)nlen) == 0) { + return 1; + } + } + return 0; +} + +/* Upper bound on the (decompressed) FPGA bitstream size. Defaults to the + * generic FIT decompression cap. The staging region at + * WOLFBOOT_LOAD_FPGA_ADDRESS must be at least this large. */ +#ifndef WOLFBOOT_FIT_MAX_FPGA +#define WOLFBOOT_FIT_MAX_FPGA WOLFBOOT_FIT_MAX_DECOMP +#endif + +int fit_load_fpga(void* fdt, const char* fpga_node) +{ + void* data; + const char* comp; + uint32_t flags = HAL_FPGA_FULL; + int len = 0; + int ret; + int coff; + int clen = 0; + BENCHMARK_DECLARE(); + + if (fpga_node == NULL) { + /* No fpga subimage present - nothing to do. */ + return 0; + } + + /* Stage the bitstream into DDR. In the common U-Boot convention the + * fpga sub-image carries no `load` property and is gzip-compressed + * (the bootloader decompresses it into a scratch buffer before + * programming the PL), so when WOLFBOOT_LOAD_FPGA_ADDRESS is set we + * decompress/copy straight to that dedicated staging address. If it + * is 0 we honor the FIT's own `load` property instead (fit_load_image + * fails closed for a compressed sub-image that has no destination). */ +#if defined(WOLFBOOT_LOAD_FPGA_ADDRESS) && (WOLFBOOT_LOAD_FPGA_ADDRESS != 0) + data = fit_load_image_to(fdt, fpga_node, + (void*)(uintptr_t)(WOLFBOOT_LOAD_FPGA_ADDRESS), + WOLFBOOT_FIT_MAX_FPGA, &len); +#else + data = fit_load_image(fdt, fpga_node, &len); +#endif + if (data == NULL || len <= 0) { + wolfBoot_printf("FIT: failed to load fpga '%s'\n", fpga_node); + return -1; + } + + /* Select full vs partial reconfiguration from the U-Boot-style + * "compatible" string (e.g. "...fpga-partial"). compatible is a DT + * string list (one or more NUL-separated entries), so scan the whole + * property rather than only its first string. Default is full. */ + comp = NULL; + coff = fdt_find_node_offset(fdt, -1, fpga_node); + if (coff > 0) { + comp = (const char*)fdt_getprop(fdt, coff, "compatible", &clen); + } + if (fit_str_contains(comp, clen, "partial")) { + flags = HAL_FPGA_PARTIAL; + } + + wolfBoot_printf("FIT: programming FPGA '%s' (%d bytes, %s)\n", + fpga_node, len, (flags == HAL_FPGA_PARTIAL) ? "partial" : "full"); + + BENCHMARK_START(); + ret = hal_fpga_load(flags, (uintptr_t)data, (size_t)len); + if (ret != 0) { + wolfBoot_printf("FIT: hal_fpga_load failed: %d\n", ret); +#ifdef WOLFBOOT_FPGA_NONFATAL + wolfBoot_printf("FIT: continuing without FPGA (non-fatal)\n"); + return 0; +#else + return -1; +#endif + } + BENCHMARK_END("FIT: FPGA programmed"); + return 0; +} +#endif /* WOLFBOOT_FPGA_BITSTREAM */ + #endif /* (MMU || WOLFBOOT_FDT) && !BUILD_LOADER_STAGE1 */ diff --git a/src/update_disk.c b/src/update_disk.c index 62631ae721..ca1d2f53ff 100644 --- a/src/update_disk.c +++ b/src/update_disk.c @@ -542,11 +542,24 @@ void RAMFUNCTION wolfBoot_start(void) if (wolfBoot_get_dts_size(load_address) > 0) { void* fit = (void*)load_address; const char *kernel = NULL, *flat_dt = NULL, *ramdisk = NULL; + const char *fpga = NULL; wolfBoot_printf("Flattened uImage Tree: Version %d, Size %d\n", fdt_version(fit), fdt_totalsize(fit)); - (void)fit_find_images(fit, &kernel, &flat_dt, &ramdisk); + (void)fit_find_images(fit, &kernel, &flat_dt, &ramdisk, &fpga); +#ifdef WOLFBOOT_FPGA_BITSTREAM + /* Program the PL before booting so PL-dependent clocks and + * peripherals are up first. */ + if (fpga != NULL) { + if (fit_load_fpga(fit, fpga) != 0) { + wolfBoot_printf("FIT: FPGA load failed\r\n"); + wolfBoot_panic(); + } + } +#else + (void)fpga; +#endif if (kernel != NULL) { void *new_load = fit_load_image(fit, kernel, NULL); if (new_load == NULL) { diff --git a/src/update_ram.c b/src/update_ram.c index 79292b2f65..1708674df5 100644 --- a/src/update_ram.c +++ b/src/update_ram.c @@ -448,11 +448,24 @@ void RAMFUNCTION wolfBoot_start(void) if (wolfBoot_get_dts_size(load_address) > 0) { void* fit = (void*)load_address; const char *kernel = NULL, *flat_dt = NULL, *ramdisk = NULL; + const char *fpga = NULL; wolfBoot_printf("Flattened uImage Tree: Version %d, Size %d\n", fdt_version(fit), fdt_totalsize(fit)); - (void)fit_find_images(fit, &kernel, &flat_dt, &ramdisk); + (void)fit_find_images(fit, &kernel, &flat_dt, &ramdisk, &fpga); +#ifdef WOLFBOOT_FPGA_BITSTREAM + /* Program the PL before booting so PL-dependent clocks and + * peripherals are up first. */ + if (fpga != NULL) { + if (fit_load_fpga(fit, fpga) != 0) { + wolfBoot_printf("FIT: FPGA load failed\n"); + wolfBoot_panic(); + } + } +#else + (void)fpga; +#endif if (kernel != NULL) { void *new_load = fit_load_image(fit, kernel, NULL); if (new_load == NULL) { diff --git a/tools/fdt-parser/fdt-parser.c b/tools/fdt-parser/fdt-parser.c index d9ac4da403..6bfa64b2c3 100644 --- a/tools/fdt-parser/fdt-parser.c +++ b/tools/fdt-parser/fdt-parser.c @@ -373,8 +373,9 @@ void dts_parse_fit_image(void* fit, const char* image, const char* desc) int dts_parse_fit(void* image) { const char *conf = NULL, *kernel = NULL, *flat_dt = NULL, *ramdisk = NULL; + const char *fpga = NULL; - conf = fit_find_images(image, &kernel, &flat_dt, &ramdisk); + conf = fit_find_images(image, &kernel, &flat_dt, &ramdisk, &fpga); if (conf != NULL) { printf("FIT: Found '%s' configuration\n", conf); dts_fit_image_item(image, fdt_find_node_offset(image, -1, conf), @@ -387,6 +388,9 @@ int dts_parse_fit(void* image) if (ramdisk != NULL) { dts_parse_fit_image(image, ramdisk, "Ramdisk"); } + if (fpga != NULL) { + dts_parse_fit_image(image, fpga, "FPGA"); + } return 0; } diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index ee0b6398e4..9a8a1f3ba0 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -60,6 +60,7 @@ TESTS:=unit-parser unit-fdt unit-extflash unit-string unit-spi-flash unit-aes128 TESTS+=unit-tpm-check-rot-auth TESTS+=unit-tpm-api-names TESTS+=unit-fit-gzip unit-fit-nogzip +TESTS+=unit-fit-fpga include unit-sign-encrypted-output.mkfrag @@ -338,6 +339,12 @@ unit-fit-nogzip: ../../include/target.h unit-fit-gzip.c -DWOLFBOOT_NO_PRINTF \ -ffunction-sections -fdata-sections $(LDFLAGS) -Wl,--gc-sections +# FIT fpga-subimage discovery (fit_find_images / fit_get_compatible). +unit-fit-fpga: ../../include/target.h unit-fit-fpga.c + gcc -o $@ unit-fit-fpga.c $(CFLAGS) -DWOLFBOOT_FDT \ + -DWOLFBOOT_NO_PRINTF \ + -ffunction-sections -fdata-sections $(LDFLAGS) -Wl,--gc-sections + unit-update-flash: ../../include/target.h unit-update-flash.c gcc -o $@ unit-update-flash.c ../../src/image.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) diff --git a/tools/unit-tests/unit-fit-fpga.c b/tools/unit-tests/unit-fit-fpga.c new file mode 100644 index 0000000000..9902d8a467 --- /dev/null +++ b/tools/unit-tests/unit-fit-fpga.c @@ -0,0 +1,274 @@ +/* unit-fit-fpga.c + * + * Unit tests for the FIT-image FPGA-subimage discovery added to + * fit_find_images() and fit_get_compatible() in src/fdt.c. The tests + * build minimal FIT blobs in memory (a tiny DTB writer below) and check + * that the fpga node is found via the configuration "fpga" property, via + * the type=="fpga" fallback, and that it is NULL when absent. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include +#include +#include + +#include "../../include/fdt.h" + +/* fdt.c calls wolfBoot_printf; supply a silent stub. */ +void wolfBoot_printf(const char *fmt, ...) +{ + (void)fmt; +} + +/* Pull in the production code under test (gated on WOLFBOOT_FDT). */ +#include "../../src/fdt.c" + +/* ------------------------------------------------------------------------- */ +/* Minimal big-endian DTB writer (FDT_* tokens come from fdt.h) */ +/* ------------------------------------------------------------------------- */ +static uint8_t g_struct[4096]; +static uint32_t g_struct_len; +static char g_strings[1024]; +static uint32_t g_strings_len; +static uint8_t g_blob[8192]; + +static void be32_put(uint8_t* p, uint32_t v) +{ + p[0] = (uint8_t)(v >> 24); + p[1] = (uint8_t)(v >> 16); + p[2] = (uint8_t)(v >> 8); + p[3] = (uint8_t)(v); +} + +static void struct_u32(uint32_t v) +{ + be32_put(&g_struct[g_struct_len], v); + g_struct_len += 4; +} + +/* Append name+nul, 4-byte aligned (used by node names). */ +static void struct_str(const char* s) +{ + uint32_t n = (uint32_t)strlen(s) + 1U; + memcpy(&g_struct[g_struct_len], s, n); + g_struct_len += n; + while (g_struct_len & 3U) { + g_struct[g_struct_len++] = 0; + } +} + +/* Intern a property name into the strings block, return its offset. */ +static uint32_t strings_off(const char* name) +{ + uint32_t n = (uint32_t)strlen(name) + 1U; + uint32_t off = g_strings_len; + memcpy(&g_strings[g_strings_len], name, n); + g_strings_len += n; + return off; +} + +static void node_begin(const char* name) +{ + struct_u32(FDT_BEGIN_NODE); + struct_str(name); +} + +static void node_end(void) +{ + struct_u32(FDT_END_NODE); +} + +static void prop_str(const char* name, const char* val) +{ + uint32_t vlen = (uint32_t)strlen(val) + 1U; + struct_u32(FDT_PROP); + struct_u32(vlen); + struct_u32(strings_off(name)); + memcpy(&g_struct[g_struct_len], val, vlen); + g_struct_len += vlen; + while (g_struct_len & 3U) { + g_struct[g_struct_len++] = 0; + } +} + +/* Assemble the header + reserve map + struct + strings into g_blob. */ +static void* fit_finish(void) +{ + uint32_t off_rsv = 40; /* header is 40 bytes (v17) */ + uint32_t off_struct = off_rsv + 16; /* one terminating rsv entry */ + uint32_t off_strings = off_struct + g_struct_len; + uint32_t total = off_strings + g_strings_len; + uint8_t* h = g_blob; + + memset(g_blob, 0, sizeof(g_blob)); + be32_put(h + 0, 0xD00DFEEDU); /* magic */ + be32_put(h + 4, total); /* totalsize */ + be32_put(h + 8, off_struct); /* off_dt_struct */ + be32_put(h + 12, off_strings); /* off_dt_strings */ + be32_put(h + 16, off_rsv); /* off_mem_rsvmap */ + be32_put(h + 20, 17); /* version */ + be32_put(h + 24, 16); /* last_comp_version */ + be32_put(h + 28, 0); /* boot_cpuid_phys */ + be32_put(h + 32, g_strings_len); /* size_dt_strings */ + be32_put(h + 36, g_struct_len); /* size_dt_struct */ + /* reserve map terminator already zeroed */ + memcpy(g_blob + off_struct, g_struct, g_struct_len); + memcpy(g_blob + off_strings, g_strings, g_strings_len); + return g_blob; +} + +static void fit_reset(void) +{ + g_struct_len = 0; + g_strings_len = 0; + memset(g_struct, 0, sizeof(g_struct)); + memset(g_strings, 0, sizeof(g_strings)); +} + +/* ------------------------------------------------------------------------- */ +/* Tests */ +/* ------------------------------------------------------------------------- */ + +/* Configuration node carries an explicit "fpga" reference. */ +START_TEST(test_fit_fpga_via_config) +{ + const char *kernel = NULL, *flat_dt = NULL, *ramdisk = NULL, *fpga = NULL; + const char* comp; + void* fit; + + fit_reset(); + struct_u32(FDT_BEGIN_NODE); struct_str(""); /* root */ + node_begin("images"); + node_begin("fpga-1"); + prop_str("type", "fpga"); + prop_str("compatible", "u-boot,zynqmp-fpga-ddrauth"); + node_end(); + node_end(); + node_begin("configurations"); + prop_str("default", "conf1"); + node_begin("conf1"); + prop_str("kernel", "kernel-1"); + prop_str("fpga", "fpga-1"); + node_end(); + node_end(); + node_end(); + struct_u32(FDT_END); + fit = fit_finish(); + + fit_find_images(fit, &kernel, &flat_dt, &ramdisk, &fpga); + ck_assert_ptr_nonnull(fpga); + ck_assert_str_eq(fpga, "fpga-1"); + + comp = fit_get_compatible(fit, fpga); + ck_assert_ptr_nonnull(comp); + ck_assert_str_eq(comp, "u-boot,zynqmp-fpga-ddrauth"); +} +END_TEST + +/* No "fpga" config property: fall back to a node with type=="fpga". */ +START_TEST(test_fit_fpga_via_type_fallback) +{ + const char *fpga = NULL; + void* fit; + + fit_reset(); + struct_u32(FDT_BEGIN_NODE); struct_str(""); + node_begin("images"); + node_begin("the-bitstream"); + prop_str("type", "fpga"); + node_end(); + node_end(); + node_end(); + struct_u32(FDT_END); + fit = fit_finish(); + + fit_find_images(fit, NULL, NULL, NULL, &fpga); + ck_assert_ptr_nonnull(fpga); + ck_assert_str_eq(fpga, "the-bitstream"); +} +END_TEST + +/* No fpga subimage at all: pfpga must be left NULL. */ +START_TEST(test_fit_fpga_absent) +{ + const char *fpga = (const char*)0x1; /* poison */ + void* fit; + + fit_reset(); + struct_u32(FDT_BEGIN_NODE); struct_str(""); + node_begin("images"); + node_begin("kernel-1"); + prop_str("type", "kernel"); + node_end(); + node_end(); + node_end(); + struct_u32(FDT_END); + fit = fit_finish(); + + fit_find_images(fit, NULL, NULL, NULL, &fpga); + ck_assert_ptr_null(fpga); +} +END_TEST + +/* fit_get_compatible returns NULL when the property is absent. */ +START_TEST(test_fit_compatible_absent) +{ + const char* comp; + void* fit; + + fit_reset(); + struct_u32(FDT_BEGIN_NODE); struct_str(""); + node_begin("images"); + node_begin("fpga-1"); + prop_str("type", "fpga"); + node_end(); + node_end(); + node_end(); + struct_u32(FDT_END); + fit = fit_finish(); + + comp = fit_get_compatible(fit, "fpga-1"); + ck_assert_ptr_null(comp); +} +END_TEST + +static Suite* fit_fpga_suite(void) +{ + Suite* s = suite_create("fit-fpga"); + TCase* tc = tcase_create("discovery"); + tcase_add_test(tc, test_fit_fpga_via_config); + tcase_add_test(tc, test_fit_fpga_via_type_fallback); + tcase_add_test(tc, test_fit_fpga_absent); + tcase_add_test(tc, test_fit_compatible_absent); + suite_add_tcase(s, tc); + return s; +} + +int main(void) +{ + int failed; + SRunner* sr = srunner_create(fit_fpga_suite()); + srunner_run_all(sr, CK_NORMAL); + failed = srunner_ntests_failed(sr); + srunner_free(sr); + return (failed == 0) ? 0 : 1; +}