From 2bad4a745293123cecb0926852b0fe2b3a23206a Mon Sep 17 00:00:00 2001 From: David Garske Date: Wed, 16 Sep 2026 13:03:14 -0700 Subject: [PATCH] zynqmp: add non-cacheable DMA window and hal_dma_set_noncached() --- Makefile | 1 + docs/HAL.md | 16 +++ docs/Targets.md | 4 + hal/zynq.c | 91 ++++++++++++++++ hal/zynq.h | 33 +++++- hal/zynq.ld | 23 ++++ include/hal.h | 9 +- options.mk | 5 + src/libwolfboot.c | 10 ++ tools/unit-tests/Makefile | 5 + tools/unit-tests/unit-zynq-dma-range.c | 141 +++++++++++++++++++++++++ 11 files changed, 336 insertions(+), 2 deletions(-) create mode 100644 tools/unit-tests/unit-zynq-dma-range.c diff --git a/Makefile b/Makefile index 6deec40694..ea92e20626 100644 --- a/Makefile +++ b/Makefile @@ -727,6 +727,7 @@ $(LSCRIPT): $(LSCRIPT_IN) FORCE sed -e "s/@WOLFBOOT_STAGE1_FLASH_ADDR@/$(WOLFBOOT_STAGE1_FLASH_ADDR)/g" | \ sed -e "s/@WOLFBOOT_STAGE1_BASE_ADDR@/$(WOLFBOOT_STAGE1_BASE_ADDR)/g" | \ sed -e "s/@WOLFBOOT_LOAD_BASE@/$(WOLFBOOT_LOAD_BASE)/g" | \ + sed -e "s/@WOLFBOOT_DMA_BUFFER_ADDRESS@/$(WOLFBOOT_DMA_BUFFER_ADDRESS)/g" | \ sed -e "s/@BOOTLOADER_START@/$(BOOTLOADER_START)/g" | \ sed -e "s/@IMAGE_HEADER_SIZE@/$(IMAGE_HEADER_SIZE)/g" | \ sed -e "s/@WOLFBOOT_LOAD_ADDRESS@/$(WOLFBOOT_LOAD_ADDRESS)/g" | \ diff --git a/docs/HAL.md b/docs/HAL.md index 6cfabcccb5..f97881a051 100644 --- a/docs/HAL.md +++ b/docs/HAL.md @@ -146,6 +146,22 @@ implementation in new ports must return immediately without performing any actio if the content of the bootloader partition in the two banks already match. +### Optional support for non-coherent DMA + +`int hal_dma_set_noncached(uintptr_t start, uintptr_t end)` + +Re-attribute `[start, end)` as non-cacheable. Only needed by ports that run with the MMU and D-cache enabled and that hand memory to a bus master which is not coherent with the CPU caches, such as an Ethernet MAC driven from a hook. Returns 0 on success, or negative if the port cannot satisfy the request. + +A weak default in `src/libwolfboot.c` returns an error, so the symbol always links. It deliberately does **not** succeed silently: a no-op would leave the caller sharing write-back memory with a non-coherent master, which is the failure this function exists to prevent. + +A port implementing it must: + +- Round the range **outward** to whatever granule its translation tables can express. On ZynqMP (`hal/zynq.c`) that is a 2MB block, so the caller has to give the region an aligned block of its own rather than placing it next to other data. +- Clean and invalidate the affected range **before** changing the attribute, so a line still dirty at the switch cannot later be written back over what the bus master has since put there. +- Push the modified table entries out and invalidate the TLB at the exception level that owns the translation. + +Callers place their buffers with a dedicated linker section. The ZynqMP port provides `.dma_buffers` in `hal/zynq.ld`, based at `WOLFBOOT_DMA_BUFFER_ADDRESS` (default `0x8200000`, clear of the kernel, the FIT staging area and the DTS) and exporting `_dma_buffers_start` / `_dma_buffers_end`. + ### wolfHSM HAL extensions Refer to [wolfHSM.md](wolfHSM.md) for the wolfHSM-specific HAL functions and an overview of wolfHSM compatibility. diff --git a/docs/Targets.md b/docs/Targets.md index 3289930bcb..35104c71df 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -4189,6 +4189,10 @@ Key configuration options: Opt-in (off by default), wolfBoot can replay a board's U-Boot Ethernet PHY register sequence over the GEM MDIO management plane so the PHY is ready before the OS runs. Enable with `CFLAGS_EXTRA+=-DWOLFBOOT_ZYNQMP_PHY_INIT`. The default targets the ZCU102 on-board PHY (TI DP83867 at MDIO `0x0C` on GEM3, `0xFF0E0000`) and just reads the PHY ID as a diagnostic (printed with `DEBUG_UART=1`). A board supplies its own sequence by keeping its values in a small header selected with one line, `CFLAGS_EXTRA+=-DZYNQMP_PHY_INIT_HEADER='"myboard_phy.h"'`, where that header `#define`s any of `ZYNQMP_GEM_BASE`, `ZYNQMP_PHY_ADDR`, `ZYNQMP_PHY_GPIO_ADDR`, `ZYNQMP_GEM_MDC_DIV`, and the `{op, arg0, arg1}` step array `ZYNQMP_PHY_INIT_STEPS`; scalars can also be set directly with `-D`, and anything omitted falls back to the ZCU102 defaults (see `hal/zynq.h` and the commented example in `config/examples/zynqmp.config`). Where the PHY is behind the PL, the boot image must include the FPGA bitstream (bootgen `[destination_device=pl] system.bit`) or the transactions are no-ops. +### Non-cacheable DMA window + +The ZynqMP GEM is not coherent with the CPU caches, so a hook that drives it needs non-cacheable memory. `hal/zynq.ld` reserves a 2MB-aligned `.dma_buffers` region at `WOLFBOOT_DMA_BUFFER_ADDRESS` (default `0x8200000`) and `hal_dma_set_noncached()` re-attributes it at runtime; see [HAL.md](./HAL.md). + ### Building with Xilinx tools (Vitis IDE) See [IDE/XilinxSDK/README.md](/IDE/XilinxSDK/README.md) for using Xilinx IDE diff --git a/hal/zynq.c b/hal/zynq.c index 06c38cfde9..0f632d89dd 100644 --- a/hal/zynq.c +++ b/hal/zynq.c @@ -3063,4 +3063,95 @@ void sdhci_platform_dma_complete(void *buf, uint32_t sz, int is_write) #endif /* DISK_SDCARD || DISK_EMMC */ +#if defined(MMU) && defined(__WOLFBOOT) +/* wolfBoot maps DDR write-back in 2MB blocks from the static MMUTableL2. A + * non-coherent DMA master cannot share 8-byte descriptors through that: + * several fall in one cache line, so cleaning one clobbers its neighbours' + * ownership bits. Re-attribute the whole block Normal-NC instead. + * + * Valid block, AttrIndx=0 (MAIR[0] is Normal-NC), AF set, never executable. + * Shareability is ignored for Non-Cacheable memory. */ +#define ZYNQMP_L2_BLOCK_NORMAL_NC \ + (0x401ULL | (1ULL << 53) | (1ULL << 54)) + + +/* From src/boot_aarch64_start.S: four contiguous 512-entry tables off + * L1[0..3] mapping 0x0-0xFFFFFFFF, so the index is addr >> 21. */ +extern uint64_t MMUTableL2[]; + +/* Make the table entries [first,last] visible to the table walker, which may + * not snoop the caches: push them out before anything relies on them. */ +static void zynqmp_mmu_publish(uint64_t first, uint64_t last) +{ + uint64_t i; + + __asm__ volatile("dsb ishst" : : : "memory"); + for (i = first; i <= last; i++) { + __asm__ volatile("dc civac, %0" + : : "r"((uintptr_t)&MMUTableL2[i]) : "memory"); + } + __asm__ volatile("dsb sy" : : : "memory"); +} + +/* wolfBoot runs at EL3 as an FSBL replacement, at EL2 under BL31. */ +static void zynqmp_mmu_tlbi(void) +{ + switch (current_el()) { + case 3: + __asm__ volatile("tlbi alle3" : : : "memory"); + break; + case 2: + __asm__ volatile("tlbi alle2" : : : "memory"); + break; + default: + __asm__ volatile("tlbi vmalle1" : : : "memory"); + break; + } + __asm__ volatile("dsb sy" : : : "memory"); + __asm__ volatile("isb" : : : "memory"); +} + +/* Mark every 2MB block overlapping [start,end) Normal-NC. Cleans the range + * first: a line still dirty at the moment of the change could otherwise land + * on top of what the bus master has since written. */ +int hal_dma_set_noncached(uintptr_t start, uintptr_t end) +{ + uintptr_t addr; + uint64_t first, last, i; + + if (zynqmp_l2_block_range((uint64_t)start, (uint64_t)end, &first, &last) + != 0) { + return -1; + } + + /* Whole blocks: the attribute applies per block. */ + for (addr = (uintptr_t)(first << ZYNQMP_L2_BLOCK_SHIFT); + addr < (uintptr_t)((last + 1) << ZYNQMP_L2_BLOCK_SHIFT); + addr += CACHE_LINE_SIZE) { + __asm__ volatile("dc civac, %0" : : "r"(addr) : "memory"); + } + __asm__ volatile("dsb sy" : : : "memory"); + + /* Break-before-make: valid -> valid memory-type changes are CONSTRAINED + * UNPREDICTABLE on ARMv8-A. Nothing may touch the range while it is + * unmapped; the caller owns a dedicated region and wolfBoot's own code + * and data are in a different block. */ + for (i = first; i <= last; i++) { + MMUTableL2[i] = 0; + } + zynqmp_mmu_publish(first, last); + zynqmp_mmu_tlbi(); + + for (i = first; i <= last; i++) { + MMUTableL2[i] = (i << ZYNQMP_L2_BLOCK_SHIFT) + | ZYNQMP_L2_BLOCK_NORMAL_NC; + } + zynqmp_mmu_publish(first, last); + zynqmp_mmu_tlbi(); + + return 0; +} +#endif /* MMU && __WOLFBOOT */ + + #endif /* TARGET_zynq */ diff --git a/hal/zynq.h b/hal/zynq.h index 33c4ef1a9c..8e650246cb 100644 --- a/hal/zynq.h +++ b/hal/zynq.h @@ -689,6 +689,37 @@ #define CRL_APB_DBG_LPD_CTRL (CRL_APB_BASE + 0x00B0U) #define CRL_APB_RST_LPD_DBG (CRL_APB_BASE + 0x0240U) - +#ifndef __ASSEMBLER__ +#include +#include + +/* 2MB: the smallest granule this translation table can re-attribute. */ +#define ZYNQMP_L2_BLOCK_SHIFT 21 +/* Four contiguous 512-entry tables covering 0x0-0xFFFFFFFF. */ +#define ZYNQMP_L2_ENTRIES 2048 + +/* [start,end) to the inclusive 2MB block indices covering it. Split out so + * it can be unit tested: these indices decide which physical blocks get + * re-attributed. Returns 0, or -1 for an empty range or one past 4GB. */ +static inline int zynqmp_l2_block_range(uint64_t start, uint64_t end, + uint64_t* first, uint64_t* last) +{ + uint64_t f, l; + + if (end <= start || first == NULL || last == NULL) { + return -1; + } + f = start >> ZYNQMP_L2_BLOCK_SHIFT; + l = (end - 1) >> ZYNQMP_L2_BLOCK_SHIFT; + if (l >= ZYNQMP_L2_ENTRIES) { + return -1; + } + /* Only on success, so a caller that ignores the return does not act on + * half-written indices. */ + *first = f; + *last = l; + return 0; +} +#endif /* !__ASSEMBLER__ */ #endif /* _ZYNQMP_H_ */ diff --git a/hal/zynq.ld b/hal/zynq.ld index ba39422655..825fe498b1 100644 --- a/hal/zynq.ld +++ b/hal/zynq.ld @@ -19,6 +19,11 @@ MEMORY * Must match WOLFBOOT_ORIGIN in the target .config. */ psu_ddr_0_MEM_0 : ORIGIN = 0x8000000, LENGTH = 0x200000 + /* Non-cacheable DDR for bus-master DMA. Its own 2MB-aligned block: 2MB + * is the smallest granule the translation table can re-attribute, and it + * must stay clear of .text so the image hash is not run uncached. + * NOLOAD, so an unused region costs nothing. */ + psu_ddr_dma_MEM_0 : ORIGIN = @WOLFBOOT_DMA_BUFFER_ADDRESS@, LENGTH = 0x200000 psu_ddr_1_MEM_0 : ORIGIN = 0x800000000, LENGTH = 0x80000000 psu_ocm_ram_0_MEM_0 : ORIGIN = 0xFFFC0000, LENGTH = 0x40000 psu_qspi_linear_0_MEM_0 : ORIGIN = 0xC0000000, LENGTH = 0x20000000 @@ -293,6 +298,24 @@ _SDA_BASE_ = __sdata_start + ((__sbss_end - __sdata_start) / 2 ); _SDA2_BASE_ = __sdata2_start + ((__sbss2_end - __sdata2_start) / 2 ); +/* Its own MEMORY region so hal_dma_set_noncached() can mark the enclosing + * 2MB block Normal-NC without touching wolfBoot's code or data. */ +.dma_buffers (NOLOAD) : { + . = ALIGN(64); + _dma_buffers_start = .; + *(.dma_buffers) + *(.dma_buffers.*) + . = ALIGN(64); + _dma_buffers_end = .; +} > psu_ddr_dma_MEM_0 + +/* hal_dma_set_noncached() rounds the range outward to the enclosing 2MB + * block and re-attributes the whole block. A misaligned override would put + * the region in wolfBoot's own block, and the break-before-make step would + * unmap live code. Fail the build instead. */ +ASSERT((_dma_buffers_start & 0x1FFFFF) == 0, + "WOLFBOOT_DMA_BUFFER_ADDRESS must be 2MB aligned") + /* Generate Stack and Heap definitions */ .heap (NOLOAD) : { diff --git a/include/hal.h b/include/hal.h index f042023c66..19beba4800 100644 --- a/include/hal.h +++ b/include/hal.h @@ -60,7 +60,8 @@ void hal_deinit(); void hal_init(void); /* Timer functions (platform-specific, used for benchmarking) */ -#if defined(WOLFBOOT_UPDATE_DISK) || defined(BOOT_BENCHMARK) +#if defined(WOLFBOOT_UPDATE_DISK) || defined(BOOT_BENCHMARK) || \ + defined(PREBOOT_NETCHECK) uint64_t hal_get_timer_us(void); #endif @@ -113,6 +114,12 @@ void hal_cache_invalidate(void); int hal_flash_protect(haladdr_t address, int len); void hal_prepare_boot(void); +/* Re-attribute [start,end) non-cacheable, for memory shared with a + * non-coherent bus master. Returns 0, or negative if the port cannot; see + * docs/HAL.md. The weak default fails rather than doing nothing, so a caller + * never silently runs DMA through write-back memory. */ +int hal_dma_set_noncached(uintptr_t start, uintptr_t end); + #ifdef DUALBANK_SWAP void hal_flash_dualbank_swap(void); #endif diff --git a/options.mk b/options.mk index ce6915d098..28215931a8 100644 --- a/options.mk +++ b/options.mk @@ -1689,6 +1689,11 @@ ifeq ($(WOLFHSM_SERVER),1) endif +# Non-cacheable DDR carve-out for bus-master DMA (hal/zynq.ld). Substituted +# into the linker script only; code uses the _dma_buffers_start/_end symbols +# the script exports rather than this address. +WOLFBOOT_DMA_BUFFER_ADDRESS?=0x8200000 + # wolfBoot hooks framework # WOLFBOOT_HOOKS_FILE: path to a single .c file containing hook definitions WOLFBOOT_HOOKS_ENABLED := diff --git a/src/libwolfboot.c b/src/libwolfboot.c index 498171d7c8..e7fc3fd6fd 100644 --- a/src/libwolfboot.c +++ b/src/libwolfboot.c @@ -252,6 +252,16 @@ void WEAKFUNCTION hal_cache_invalidate(void) /* if cache flushing is required implement in hal */ } +/* Weak default; a port with the MMU and D-cache on overrides it. Fails + * rather than succeeding: a silent no-op would leave the caller sharing + * write-back memory with a non-coherent master. */ +int WEAKFUNCTION hal_dma_set_noncached(uintptr_t start, uintptr_t end) +{ + (void)start; + (void)end; + return -1; +} + #ifdef NVM_FLASH_WRITEONCE /* Some internal FLASH memory models don't allow * multiple writes after erase in the same page/area. diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 9306819684..119969b339 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -129,6 +129,7 @@ TESTS+=unit-rp2350-flash-write TESTS+=unit-fwtpm-rsp-overrun TESTS+=unit-fwtpm-cmd-toctou TESTS+=unit-fdt-memrsv-wrap +TESTS+=unit-zynq-dma-range TESTS+=unit-pkcs11_store-stalecache TESTS+=unit-aurix-erased-fill TESTS+=unit-aurix-erased-fill-invert @@ -508,6 +509,10 @@ unit-fwtpm-cmd-toctou: ../../include/target.h unit-fwtpm-cmd-toctou.c gcc -o $@ $^ $(CFLAGS) -I$(WOLFBOOT_LIB_WOLFTPM) \ -DWOLFTPM_USER_SETTINGS $(LDFLAGS) +# unit-zynq-dma-range: block-index math behind hal_dma_set_noncached() +unit-zynq-dma-range: unit-zynq-dma-range.c + gcc -o $@ $^ $(CFLAGS) $(LDFLAGS) + # unit-fdt-memrsv-wrap: layout validation in front of fdt_add_mem_rsv() # (F-11045). Links the real parser rather than extracting one function. unit-fdt-memrsv-wrap:CFLAGS+=-DWOLFBOOT_FDT diff --git a/tools/unit-tests/unit-zynq-dma-range.c b/tools/unit-tests/unit-zynq-dma-range.c new file mode 100644 index 0000000000..54d7f6eec0 --- /dev/null +++ b/tools/unit-tests/unit-zynq-dma-range.c @@ -0,0 +1,141 @@ +/* unit-zynq-dma-range.c + * + * 2MB block-index arithmetic behind hal_dma_set_noncached() on ZynqMP. These + * indices decide which physical blocks get re-attributed, so an off-by-one + * would silently re-map the wrong memory. The dc/tlbi around them cannot run + * on the host; this arithmetic can. + * + * 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 "../../hal/zynq.h" + +#define BLK (1ULL << 21) + +START_TEST(test_block_range_single_block) +{ + uint64_t first = 99, last = 99; + + /* The wolfBoot default DMA window: one 2MB block at 0x8200000. */ + ck_assert_int_eq(zynqmp_l2_block_range(0x8200000, 0x8400000, &first, &last), + 0); + ck_assert_uint_eq(first, 65); + ck_assert_uint_eq(last, 65); + + /* A sub-block range still names that one block, so a caller must own + * the whole thing. */ + ck_assert_int_eq(zynqmp_l2_block_range(0x8200000, 0x8200040, &first, &last), + 0); + ck_assert_uint_eq(first, 65); + ck_assert_uint_eq(last, 65); +} +END_TEST + +START_TEST(test_block_range_boundaries) +{ + uint64_t first = 0, last = 0; + + /* end is exclusive: one block, not two. */ + ck_assert_int_eq(zynqmp_l2_block_range(0, BLK, &first, &last), 0); + ck_assert_uint_eq(first, 0); + ck_assert_uint_eq(last, 0); + + /* one byte past the boundary pulls in the next block */ + ck_assert_int_eq(zynqmp_l2_block_range(0, BLK + 1, &first, &last), 0); + ck_assert_uint_eq(last, 1); + + /* an unaligned start rounds down to its containing block */ + ck_assert_int_eq(zynqmp_l2_block_range(BLK + 0x40, BLK + 0x80, &first, + &last), 0); + ck_assert_uint_eq(first, 1); + ck_assert_uint_eq(last, 1); + + /* the last block the table describes */ + ck_assert_int_eq(zynqmp_l2_block_range(0xFFE00000, 0x100000000ULL, &first, + &last), 0); + ck_assert_uint_eq(first, ZYNQMP_L2_ENTRIES - 1); + ck_assert_uint_eq(last, ZYNQMP_L2_ENTRIES - 1); +} +END_TEST + +START_TEST(test_block_range_rejects_bad_input) +{ + uint64_t first = 0, last = 0; + + /* empty and inverted ranges */ + ck_assert_int_eq(zynqmp_l2_block_range(BLK, BLK, &first, &last), -1); + ck_assert_int_eq(zynqmp_l2_block_range(0x8400000, 0x8200000, &first, &last), + -1); + + /* one byte past the 4GB the table covers */ + ck_assert_int_eq(zynqmp_l2_block_range(0xFFE00000, 0x100000001ULL, &first, + &last), -1); + /* and far past it */ + ck_assert_int_eq(zynqmp_l2_block_range(0x100000000ULL, 0x100200000ULL, + &first, &last), -1); + + ck_assert_int_eq(zynqmp_l2_block_range(0, BLK, NULL, &last), -1); + ck_assert_int_eq(zynqmp_l2_block_range(0, BLK, &first, NULL), -1); +} +END_TEST + +START_TEST(test_block_range_leaves_outputs_alone_on_error) +{ + uint64_t first = 0xAAAA, last = 0x5555; + + /* An out of range end is rejected only after the indices are computed, + * so check the caller's variables are still untouched. */ + ck_assert_int_eq(zynqmp_l2_block_range(0x100000000ULL, 0x100200000ULL, + &first, &last), -1); + ck_assert_uint_eq(first, 0xAAAA); + ck_assert_uint_eq(last, 0x5555); + + ck_assert_int_eq(zynqmp_l2_block_range(BLK, BLK, &first, &last), -1); + ck_assert_uint_eq(first, 0xAAAA); + ck_assert_uint_eq(last, 0x5555); +} +END_TEST + +static Suite *dma_range_suite(void) +{ + Suite *s = suite_create("zynq-dma-range"); + TCase *tc = tcase_create("zynq-dma-range"); + + tcase_add_test(tc, test_block_range_single_block); + tcase_add_test(tc, test_block_range_boundaries); + tcase_add_test(tc, test_block_range_rejects_bad_input); + tcase_add_test(tc, test_block_range_leaves_outputs_alone_on_error); + suite_add_tcase(s, tc); + return s; +} + +int main(void) +{ + int fails; + Suite *s = dma_range_suite(); + SRunner *sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + return fails; +}