From e85cf3c289464a99c8176808b5309876dcfc48e9 Mon Sep 17 00:00:00 2001 From: David Garske Date: Fri, 18 Sep 2026 16:12:50 -0700 Subject: [PATCH 1/3] hal: keep BENCHMARK_END valid on a build with printf compiled out --- include/hal.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/include/hal.h b/include/hal.h index 55d93230ab..5f6d0973a3 100644 --- a/include/hal.h +++ b/include/hal.h @@ -77,9 +77,13 @@ uint64_t hal_get_timer_us(void); #ifdef BOOT_BENCHMARK #define BENCHMARK_DECLARE() uint64_t _boot_bench_start #define BENCHMARK_START() (_boot_bench_start = hal_get_timer_us()) + /* wolfBoot_printf compiles to nothing on a build without printf, which + * leaves _elapsed_ms unreferenced; keep it used so the macro is valid + * there too. */ #define BENCHMARK_END(msg) do { \ uint64_t _elapsed_ms = (hal_get_timer_us() - _boot_bench_start) / 1000; \ wolfBoot_printf(msg " (%lu ms)\r\n", (unsigned long)_elapsed_ms); \ + (void)_elapsed_ms; \ } while(0) #else #define BENCHMARK_DECLARE() do {} while(0) From 23a45d8d30ffd1859cfb8c43cc7bc6c9b1f60c80 Mon Sep 17 00:00:00 2001 From: David Garske Date: Fri, 18 Sep 2026 11:58:11 -0700 Subject: [PATCH 2/3] imx95_m7: do not issue D-cache clean by address while the cache is disabled --- hal/imx95_m7.c | 11 +++++++++++ hal/imx95_m7.h | 14 ++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/hal/imx95_m7.c b/hal/imx95_m7.c index 68fd0cc270..33b908c605 100644 --- a/hal/imx95_m7.c +++ b/hal/imx95_m7.c @@ -303,6 +303,17 @@ void hal_init(void) hal_status(HAL_STATUS_INIT); } +#if defined(WOLFBOOT_UPDATE_DISK) || defined(BOOT_BENCHMARK) +/* Microseconds from the DWT cycle counter hal_init() starts. The counter is + * 32-bit and wraps about every 5.4 s at 800 MHz, which bounds any single + * measurement rather than the boot as a whole - each BENCHMARK_END subtracts + * two reads, so a wrap only matters for an interval longer than that. */ +uint64_t hal_get_timer_us(void) +{ + return (uint64_t)DWT_CYCCNT / (IMX95_M7_CORE_HZ / 1000000UL); +} +#endif + void hal_prepare_boot(void) { hal_status(HAL_STATUS_PREBOOT); diff --git a/hal/imx95_m7.h b/hal/imx95_m7.h index 3744f3a187..5bd175e0a9 100644 --- a/hal/imx95_m7.h +++ b/hal/imx95_m7.h @@ -115,6 +115,14 @@ static inline void imx95_dcache_clean(const void *addr, uint32_t len) if (len == 0) return; + /* Nothing to clean while the cache is off - every store already reached + * memory. It is also unsafe: the M7 does maintenance by address even with + * the cache disabled, and out of a cold reset the cache RAMs hold random + * tags and dirty bits until hal_cache_enable() invalidates them, so a hit + * writes garbage to a random address and the bus error comes back as an + * imprecise BusFault. */ + if ((SCB_CCR & CCR_DC) == 0UL) + return; line = ((uint32_t)(uintptr_t)addr) & ~(IMX95_CACHE_LINE - 1UL); end = (uint32_t)(uintptr_t)addr + len; for (; line < end; line += IMX95_CACHE_LINE) { @@ -147,6 +155,12 @@ static inline void imx95_dwt_init(void) DWT_CTRL |= DWT_CYCCNTENA; } +/* The M7 core clock on this part. Only used to turn cycle counts into + * microseconds for the boot benchmark; override for a different clock. */ +#ifndef IMX95_M7_CORE_HZ +#define IMX95_M7_CORE_HZ 800000000UL +#endif + /* Bounded spin. Wrap-safe for any delay shorter than a full 32-bit period * (~5.4 s at 800 MHz) because the comparison is done on the difference. */ static inline void imx95_delay_cycles(uint32_t cycles) From 7a73c95f2055d7154100c898351f5f2135aff649 Mon Sep 17 00:00:00 2001 From: David Garske Date: Wed, 30 Sep 2026 09:28:42 -0700 Subject: [PATCH 3/3] imx95_a55: add the Cortex-A55 target and a stage 1 replacing U-Boot SPL --- .github/workflows/test-configs.yml | 27 + .gitignore | 1 + Makefile | 6 + arch.mk | 54 +- config/examples/imx95-a55.config | 100 ++ docs/Targets.md | 68 + hal/imx95_a55.c | 1958 ++++++++++++++++++++++++++++ hal/imx95_a55.h | 148 +++ hal/imx95_a55.ld | 94 ++ hal/imx95_a55_stage1.c | 747 +++++++++++ hal/imx95_a55_stage1.ld | 51 + hal/imx95_a55_stage1_start.S | 109 ++ hal/imx95_ahab.c | 345 +++++ hal/imx95_ahab.h | 123 ++ hal/imx95_m7.c | 7 +- hal/imx95_m7.h | 9 +- include/hal.h | 1 + options.mk | 7 +- src/boot_aarch64.c | 19 +- src/boot_aarch64_cache.S | 74 ++ src/boot_aarch64_start.S | 287 +++- src/fdt.c | 18 + stage1/Makefile | 46 +- tools/unit-tests/Makefile | 22 +- tools/unit-tests/unit-imx95-ahab.c | 530 ++++++++ 25 files changed, 4774 insertions(+), 77 deletions(-) create mode 100644 config/examples/imx95-a55.config create mode 100644 hal/imx95_a55.c create mode 100644 hal/imx95_a55.h create mode 100644 hal/imx95_a55.ld create mode 100644 hal/imx95_a55_stage1.c create mode 100644 hal/imx95_a55_stage1.ld create mode 100644 hal/imx95_a55_stage1_start.S create mode 100644 hal/imx95_ahab.c create mode 100644 hal/imx95_ahab.h create mode 100644 src/boot_aarch64_cache.S create mode 100644 tools/unit-tests/unit-imx95-ahab.c diff --git a/.github/workflows/test-configs.yml b/.github/workflows/test-configs.yml index 825ec1f000..ce466a5331 100644 --- a/.github/workflows/test-configs.yml +++ b/.github/workflows/test-configs.yml @@ -454,6 +454,33 @@ jobs: config-file: ./config/examples/imx95-m7.config make-args: wolfboot.bin test-app/image_v1_signed.bin + imx95_a55_test: + uses: ./.github/workflows/test-build.yml + with: + arch: aarch64 + # i.MX95 Cortex-A55 as BL33. The payload is a Linux FIT on the boot + # medium, so there is no test app and no contiguous factory.bin. + config-file: ./config/examples/imx95-a55.config + make-args: wolfboot.bin CROSS_COMPILE=aarch64-linux-gnu- + + imx95_a55_stage1_emmc_test: + uses: ./.github/workflows/test-build.yml + with: + arch: aarch64 + # The stage 1 that replaces U-Boot SPL, reading the containers from the + # eMMC boot partition. Nothing else compiles the AHAB parser or the + # OCRAM link, so this keeps both from rotting. + config-file: ./config/examples/imx95-a55.config + make-args: -C stage1 DISK_EMMC=1 DISK_SDCARD=0 loader_stage1.bin CROSS_COMPILE=aarch64-linux-gnu- + + imx95_a55_stage1_sd_test: + uses: ./.github/workflows/test-build.yml + with: + arch: aarch64 + # Same stage 1 against the SD controller rather than the eMMC one. + config-file: ./config/examples/imx95-a55.config + make-args: -C stage1 DISK_SDCARD=1 DISK_EMMC=0 loader_stage1.bin CROSS_COMPILE=aarch64-linux-gnu- + tegra234_test: uses: ./.github/workflows/test-build.yml with: diff --git a/.gitignore b/.gitignore index 2cb4a18e3c..ef2f13a726 100644 --- a/.gitignore +++ b/.gitignore @@ -258,6 +258,7 @@ tools/unit-tests/unit-va416x0-fram tools/unit-tests/unit-wolfhsm_flash_hal tools/unit-tests/unit-ubootenv tools/unit-tests/unit-cm4-sdhci +tools/unit-tests/unit-imx95-ahab tools/unit-tests/unit-cm4-rauc-slot tools/unit-tests/__pycache__/* diff --git a/Makefile b/Makefile index ce668f0e84..c184752ad7 100644 --- a/Makefile +++ b/Makefile @@ -431,6 +431,12 @@ ifeq ($(TARGET),tegra234) MAIN_TARGET:=wolfboot.bin test-app/image_v1_signed.bin endif +# i.MX95 A55 is a RAM-resident BL33 loaded by SPL inside an AHAB container, so +# there is no contiguous flash image to assemble into a factory.bin. +ifeq ($(TARGET),imx95_a55) + MAIN_TARGET:=wolfboot.bin +endif + ifeq ($(TARGET),cm4) MAIN_TARGET:=wolfboot.bin endif diff --git a/arch.mk b/arch.mk index 88725b9d41..e905b8620f 100644 --- a/arch.mk +++ b/arch.mk @@ -75,7 +75,7 @@ ifeq ($(ARCH),AARCH64) # Skip the bare-metal reset (boot_aarch64_start.S) and EL2/GIC glue. OBJS+=src/boot_aarch64_efi.o else - OBJS+=src/boot_aarch64.o src/boot_aarch64_start.o + OBJS+=src/boot_aarch64.o src/boot_aarch64_start.o src/boot_aarch64_cache.o endif ifeq ($(TARGET),zynq) @@ -308,6 +308,56 @@ ifeq ($(ARCH),AARCH64) endif endif + ifeq ($(TARGET),imx95_a55) + # NXP i.MX95 Cortex-A55 cluster, running as BL33 in AHAB container 2. + # -mstrict-align because this stage runs with the MMU off, so every access + # is Device-nGnRnE and unaligned ones fault. + ARCH_FLAGS=-mcpu=cortex-a55+crypto -march=armv8.2-a+crypto -mstrict-align + CFLAGS+=$(ARCH_FLAGS) -DCORTEX_A55 + LDFLAGS+=$(ARCH_FLAGS) + # BL31 loads and enters this image here; must match ORIGIN in + # hal/imx95_a55.ld and IMX95_BL33_BASE in hal/imx95_a55.h. + WOLFBOOT_ORIGIN=0x90200000 + # Console, SCMI client and uSDHC driver all live in hal/imx95_a55.c, so none + # needs its own object; the switches below pick which paths compile in. + # Bring-up aid: print the entry EL, SCTLR and handoff x0 that BL31 passed. + ifeq ($(IMX95_HANDOFF_DUMP),1) + CFLAGS+=-DIMX95_HANDOFF_DUMP + endif + # i.MX95 is GICv3 and BL31 already configured it. EL2_HYPERVISOR is not a + # generic options.mk variable, so it is emitted here as tegra234 does. + ifeq ($(EL2_HYPERVISOR),1) + CFLAGS+=-DEL2_HYPERVISOR=1 + endif + # i.MX uSDHC is not SDHCI-compatible: skip src/sdhci.c, use our driver. + ifneq ($(filter 1,$(DISK_SDCARD) $(DISK_EMMC)),) + DISK_DRIVER=usdhc + # SCMI cold-init of uSDHC2 (clock+pinmux) when no prior stage did it. + ifeq ($(IMX95_SCMI_COLD_INIT),1) + CFLAGS+=-DIMX95_SCMI_COLD_INIT + endif + endif + # SCMI M7 power-on + TCM ECC scrub (U-Boot's power_on_m7 equivalent) for a + # Cortex-M7 that Linux launches later. Independent of the disk driver. + ifeq ($(IMX95_INIT_M7),1) + CFLAGS+=-DIMX95_INIT_M7 + endif + # One-shot diagnostic: read the AHAB container out of the eMMC boot + # partitions and report it, from inside an image that boots from SD. + ifeq ($(IMX95_EMMC_PROBE),1) + CFLAGS+=-DIMX95_EMMC_PROBE + OBJS+=hal/imx95_ahab.o + endif + # Mirror the console into a DDR ring that survives the handoff to Linux. + ifeq ($(IMX95_LOG_RING),1) + CFLAGS+=-DIMX95_LOG_RING + endif + # Verbose per-step trace of the SCMI cold-init and SD bring-up. + ifeq ($(IMX95_SCMI_DEBUG),1) + CFLAGS+=-DIMX95_SCMI_DEBUG + endif + endif + ifeq ($(TARGET),cm4) # Raspberry Pi Compute Module 4 - Broadcom BCM2711, Cortex-A72 ARCH_FLAGS=-mcpu=cortex-a72+crypto -march=armv8-a+crypto -mtune=cortex-a72 @@ -334,7 +384,7 @@ ifeq ($(ARCH),AARCH64) # asm (sp_arm64.c / WOLFSSL_SP_ARM64_ASM); that is enabled independently on # __aarch64__ in include/user_settings.h, so a non-FIPS cm4 build still links # sp_arm64 asm. - ifeq ($(filter zynq versal nxp_ls1028a,$(TARGET)),) + ifeq ($(filter zynq versal nxp_ls1028a imx95_a55,$(TARGET)),) NO_ARM_ASM?=1 endif diff --git a/config/examples/imx95-a55.config b/config/examples/imx95-a55.config new file mode 100644 index 0000000000..e7811a35d9 --- /dev/null +++ b/config/examples/imx95-a55.config @@ -0,0 +1,100 @@ +# NXP i.MX95 Cortex-A55 - wolfBoot as BL33 (replaces U-Boot proper): +# ROM -> ELE -> SM -> OEI -> SPL [c1] -> BL31 -> OP-TEE -> wolfBoot -> Linux +# wolfBoot takes U-Boot's slot in AHAB container 2 at 0x90200000; the other +# container images are the vendor's, reused verbatim. +# +# `make stage1` from this same config also replaces SPL, and then the chain is +# ROM -> ELE -> SM -> OEI -> wolfBoot [c1] -> BL31 -> OP-TEE -> wolfBoot +# with stage 1 making the ELE call that authenticates container 2 that SPL used +# to make (IMX95_AHAB_AUTH, on by default). See docs/Targets.md. + +ARCH?=AARCH64 +TARGET?=imx95_a55 + +# ML-DSA-87, matching the imx95_m7 port. Header fits sig (4627) + pubkey. +SIGN?=ML_DSA +ML_DSA_LEVEL?=5 +HASH?=SHA256 +# Required: unset, ML_DSA_IMAGE_SIGNATURE_SIZE expands empty -> image.c errors. +IMAGE_SIGNATURE_SIZE?=4627 +IMAGE_HEADER_SIZE?=12288 + +DEBUG?=0 +DEBUG_SYMBOLS=1 +DEBUG_UART?=1 +# Print entry EL, SCTLR, MPIDR, CNTFRQ and the handoff x0 pointer. +IMX95_HANDOFF_DUMP?=1 +# Report how long each boot step takes, in ms, from the generic timer. +BOOT_BENCHMARK?=1 +# Also mirror the console into a DDR ring, so the verified-boot log can be read +# from Linux (and shown on a display) long after the UART output has scrolled. +IMX95_LOG_RING?=1 + +V?=0 +SPMATH?=1 +NO_ASM?=0 + +# RAM-resident: BL31 loads and enters this image, so there is nothing to copy +# from a stored .data and no XIP flash behind it. +NO_XIP?=1 + +# GICv3, configured by BL31; gicv2_init_secure() would abort on an unmapped +# ZynqMP address. +SKIP_GIC_INIT?=1 + +# Stay at EL2 (Linux wants it for KVM). Do NOT set BOOT_EL1: el2_to_el1_boot +# skips cache/MMU cleanup and would hand Linux stale DRAM. +EL2_HYPERVISOR=1 + +# No flash: boot/update addresses come from the HAL. +WOLFBOOT_NO_PARTITIONS=1 + +# Must match ORIGIN in hal/imx95_a55.ld and IMX95_BL33_BASE in hal/imx95_a55.h. +WOLFBOOT_ORIGIN=0x90200000 + +# RAM staging at 0xA1000000: clear of the M7/BL31/OP-TEE/ELE/vpu carveouts; +# hal_init() asserts this via imx95_check_load_ranges(). +WOLFBOOT_LOAD_ADDRESS?=0xA1000000 +WOLFBOOT_LOAD_DTS_ADDRESS?=0xA0800000 + +# Bounds the image before authentication; required without fixed partitions. +WOLFBOOT_RAMBOOT_MAX_SIZE=0x10000000 + +# Required for the test-app link even with WOLFBOOT_NO_PARTITIONS=1. +WOLFBOOT_SECTOR_SIZE?=0x4000 +WOLFBOOT_PARTITION_SIZE?=0x4000000 +WOLFBOOT_PARTITION_BOOT_ADDRESS?=0xA1000000 + +# Toolchain +USE_GCC=1 +CROSS_COMPILE?=aarch64-none-elf- + +# Storage: carrier SD on uSDHC2 (uSDHC driver in hal/imx95_a55.c; i.MX uSDHC, not SDHCI). +# Boot containers stay on eMMC. +DISK_SDCARD?=1 +# Cold start: with no U-Boot ahead of wolfBoot the uSDHC2 clock and pinmux are +# still owned by the System Manager and must be requested over SCMI. +IMX95_SCMI_COLD_INIT?=1 +# Power the Cortex-M7 mix and scrub its TCM, as U-Boot's board init does, for +# boards that launch an M7 image from Linux. +IMX95_INIT_M7?=1 +# A/B slots by 0-based index into the parsed partition table. +CFLAGS_EXTRA+=-DBOOT_PART_A=1 -DBOOT_PART_B=2 +CFLAGS_EXTRA+=-DDISK_BLOCK_SIZE=0x10000 +# This module's ethernet MAC (OUI 00:14:2d, low bytes are the module serial), +# from its Toradex config block. The ENETC ports are PCIe-enumerated and carry +# no local-mac-address in the OS DTB, so without this Linux picks a random MAC +# each boot and the DHCP address floats. Replace it for a different module. +CFLAGS_EXTRA+=-DIMX95_ETH0_MAC=0x00142d888d20 + +# Linux FIT boot: kernel at 0xB2000000, ramdisk at 0xBA000000 - both above +# the staging window so the copies cannot overlap the staged FIT. +FIT_RAMDISK?=1 +WOLFBOOT_LOAD_RAMDISK_ADDRESS?=0xBA000000 +# OS cmdline plus serial console; ostree= pins the deployment. audit=0 because +# the audit subsystem interleaves records with anything else on this console. +CFLAGS_EXTRA+=-DLINUX_BOOTARGS='"earlycon console=ttyLP1,115200 audit=0 root=LABEL=otaroot rootfstype=ext4 logo.nologo ostree=/ostree/boot.1/torizon/d976a72bdc954405cfbe989bae393ad9da0b14b5dc0c1328c9b19e88201745a6/0"' + +# --- Later phases (not yet implemented) ------------------------------------- +#ELF?=1 +#GZIP?=1 diff --git a/docs/Targets.md b/docs/Targets.md index 7a7710f767..e3eacd7c6f 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -23,6 +23,7 @@ This README describes configuration of supported targets. * [Nordic nRF54L15](#nordic-nrf54l15) * [NXP i.MX 8QuadMax](#nxp-imx-8quadmax) * [NXP i.MX95 Cortex-M7](#nxp-imx95-cortex-m7) +* [NXP i.MX95 Cortex-A55 (BL33)](#nxp-imx95-cortex-a55-bl33) * [NXP iMX-RT](#nxp-imx-rt) * [NXP i.MX RT700](#nxp-imx-rt700) * [NXP Kinetis](#nxp-kinetis) @@ -9902,6 +9903,73 @@ The difference between the two timestamps is the cost of everything wolfBoot doe Both caches are enabled by `hal_init()`, which matters because verifying an image means hashing megabytes resident in DDR. The ARMv7-M default memory map marks `0x80000000-0x9FFFFFFF` as Normal write-through, so no MPU region is needed and M7 stores to the shared window still reach DDR; the HAL nevertheless cleans the affected lines explicitly so that behaviour is not left depending on an inherited attribute. +## NXP i.MX95 Cortex-A55 (BL33) + +wolfBoot replaces U-Boot proper on the i.MX95's Cortex-A55 cluster: it is the third image in AHAB container 2 (after BL31 and OP-TEE), entered by BL31 at `0x90200000` in NS-EL2, where it verifies a Linux FIT (kernel + DTB + initramfs) with ML-DSA-87 and boots it at EL2. NXP also documents an OEM PQC SRK hybrid (ML-DSA) AHAB flow, so the container half of the chain can be post-quantum as well. + +Which component authenticates the containers depends on which slot wolfBoot occupies, and the two cases are not the same: + +- **wolfBoot as BL33 only** (this section). U-Boot SPL is still in container 0 and still calls the ELE to authenticate container 2 before BL31 runs, so the chain is ROM -> ELE -> SPL -> BL31 -> wolfBoot -> Linux once SRK fuses are programmed. +- **wolfBoot as stage 1 as well** (next section). SPL is gone, so wolfBoot performs that ELE call itself. `IMX95_AHAB_AUTH` is what does it, and it is on by default; the chain becomes ROM -> ELE -> wolfBoot stage 1 -> BL31 -> wolfBoot BL33 -> Linux. + +In both cases the ROM and the ELE authenticate container 0 before any A55 code runs, so whichever image sits in the SPL slot is itself verified. + +Validated on a Toradex SMARC iMX95 with `TARGET=imx95_a55` (`config/examples/imx95-a55.config`): full boot to Linux userspace with the FIT on the carrier SD (uSDHC2, the uSDHC driver in `hal/imx95_a55.c` - i.MX uSDHC, not SDHCI-compatible). + +Notes: +- The FIT DTB gets `/chosen` bootargs, initrd properties and a `/memory` node from `hal_dts_fixup()`; deployment DTBs commonly ship without `/memory` (the bootloader is expected to add it) and the kernel hangs silently without one. +- The EL2 exit is a fused asm routine (flush, DAIF mask, TLB invalidate, jump) that touches no memory after the D-cache goes off; set/way cleaning does not reach the A55 cluster's DSU system cache, so payload ranges are also cleaned by VA. +- Use plain `earlycon` (DTB-derived); an explicit `earlycon=lpuart32,mmio32,` uses the wrong register layout on i.MX and silences all console output. +- uSDHC2's clock, pinmux and card power are owned by the M33 System Manager, and its pad registers data-abort on direct access from BL33. `IMX95_SCMI_COLD_INIT=1` brings them up over SCMI (the SCMI client in `hal/imx95_a55.c`), which is what lets wolfBoot boot from SD on a cold power-on rather than only after a stage that already initialized the controller. +- `IMX95_INIT_M7=1` powers the Cortex-M7 mix and scrubs its TCM for ECC, matching what U-Boot's board init does, for boards that launch an M7 image from Linux later. + +### Stage 1: replacing U-Boot SPL + +The boot device holds a set of AHAB containers. The first one is what the boot ROM reads: it carries the ELE firmware, the M33 System Manager, the OEI that trains DDR, and one A55 image loaded into OCRAM at `0x20480000`. That last image is U-Boot SPL, and its whole job is to find the next container and load BL31, OP-TEE and BL33 out of it. wolfBoot can take that slot instead: + +``` +cp config/examples/imx95-a55.config .config +make # BL33, loaded into DRAM by BL31 +make stage1 DISK_EMMC=1 # stage1/loader_stage1.bin, the SPL slot +``` + +DDR is up before stage 1 runs, because the OEI did it, so stage 1 only needs the boot device and the container walk. Nothing records where the next container starts: it is derived by taking the end of the first container - the furthest of its header, its images and its signature block - and rounding up to 1 KiB. `hal/imx95_ahab.c` is that walk, and `hal/imx95_a55_stage1.c` is the rest: the watchdog, GPIO and SMMU state a warm reset out of Linux leaves behind, the SCMI calls for the console clock and the A55 performance level, and the ELE call that starts its random generator. + +Two things differ from the BL33 build. The console is programmed rather than inherited, since nothing has configured LPUART1 yet. And on eMMC the containers live in a boot partition, not the user area: `imx95_emmc_boot_partition()` reads back the same `PARTITION_CONFIG` field that told the ROM which one to load from, and reads follow it. + +#### What stage 1 authenticates + +Taking SPL's slot means taking over the step SPL performed. The boot ROM and the ELE authenticate container 0, so stage 1 is verified code, but the container it goes on to load BL31, OP-TEE and BL33 out of is a separate one that nothing has checked yet. `IMX95_AHAB_AUTH=1` (the default) makes stage 1 ask the ELE to check it, the same three calls U-Boot's SPL makes: + +1. The container's header, image table and signature block are staged into DDR at `0x90000000` - NXP's own `IMG_CONTAINER_BASE` for this SoC - because the ELE reads the address it is given over its own master port and requires DDR. Every value stage 1 then acts on comes from those staged bytes; the medium is not consulted for the table again. +2. `ELE_OEM_CNTN_AUTH_REQ` authenticates the header. +3. Each image is loaded to its destination and then checked in place with `ELE_VERIFY_IMAGE_REQ`, one at a time so a failure names the image. `ELE_RELEASE_CONTAINER_REQ` follows, before the jump. + +Any failure stops the boot. `IMX95_AHAB_AUTH=0` builds without it, for bring-up on a part whose containers are not signed; it cannot be combined with `IMX95_STAGE1_PASSTHROUGH` or `IMX95_STAGE1_ALLOW_SELF_OVERLAP`, both of which would carry on past a refusal, and the build fails if you try. + +Separately, and regardless of that flag, stage 1 bounds every destination *before* writing any of them. An image is streamed straight from the boot device into the address the container names, so a check made afterwards would come too late to matter: by then the bytes have landed. Destinations must lie inside DDR, must not overlap the staging window, this loader, the M7 carveout, the ELE shared-memory region or the VPU boot area, must not overlap each other once rounded up to the block size, and image 0 must be an A55 executable whose entry point lies inside its own bytes. + +Two limits are worth stating plainly: + +- **On an open part AHAB reports success without enforcing a signature.** That is why an unsigned container boots at all. What authentication buys there is an ELE-checked hash of each image against a table nobody signed - integrity, not authenticity. Build with `IMX95_AHAB_LIFECYCLE=1` to have stage 1 print the lifecycle, so the difference is visible rather than implied; it is off by default because nothing else on this platform reads the fuse shadow block from the slot SPL occupies. Enforcement begins when the SRK hash is fused; see the i.MX8QuadMax AHAB section above for what that costs and why it is irreversible. +- **Authenticating the container does not prevent rollback.** The offset of the next container is derived by walking container 0, which stage 1 reads off the medium and nothing re-authenticates, so a different but validly signed older container set would authenticate happily. That needs `sw_version`/`fuse_version` policy or the AHAB monotonic counter, neither of which is implemented here. + +#### Stage 1 build options + +Stage 1 is built by `make -C stage1` and takes its own options, separately from the BL33 build. Pass them on the command line. + +| Option | Default | Effect | +|---|---|---| +| `IMX95_AHAB_AUTH` | `1` | Ask the ELE to authenticate the container set stage 1 loads BL31, OP-TEE and BL33 from, as described above. Setting it to `0` builds without those calls, for bring-up on a part whose containers are not signed. | +| `IMX95_AHAB_LIFECYCLE` | `0` | Read and print the part's lifecycle from the fuse shadow block. Off by default: U-Boot reads that block only from U-Boot proper and only under `CONFIG_AHAB_BOOT`, so whether it answers the A55 this early is untested per part, and a diagnostic must not be able to cost a boot. | +| `IMX95_STAGE1_PASSTHROUGH` | unset | Bring-up only. Enter BL31 even when the container load failed, for a board where some other loader has already staged the whole set and stage 1 only needs to be invisible when it cannot help. Refused at compile time together with `IMX95_AHAB_AUTH`, because returning an error is then indistinguishable from the jump. | +| `IMX95_STAGE1_ALLOW_SELF_OVERLAP` | unset | Bring-up only. Skip an image whose destination lands on the running loader instead of refusing the boot, which a stage 1 linked into DRAM needs in order to skip its own entry. Refused at compile time together with `IMX95_AHAB_AUTH`. | +| `IMX95_LOG_RING` | `0` | Mirror the console into a DDR ring that survives the handoff, so stage 1's output can be read back from Linux with `memtool con`. Stage 1 starts the ring and BL33 appends to it, so one address holds both. | +| `IMX95_LOG_RING_BASE` | `0x80F20000` | Where that ring lives. Change it only to move the ring out of the way of something else; stage 1 and BL33 must agree on it. | +| `IMX95_EMMC_PROBE` | `0` | One-shot diagnostic that walks the eMMC boot partitions and reports the AHAB containers it finds, from an image booting off SD. | + +The two bring-up options exist to get a new board talking; neither belongs in a shipped configuration, and the build refuses to combine either with authentication rather than letting a boot continue past a refusal. + ## TI C2000 C28x (LAUNCHXL-F28P55X) wolfBoot runs on the Texas Instruments C2000 C28x DSP (TMS320F28P550SJ, 150 MHz) as a secure execute-in-place (XIP) bootloader. The C28x is word-addressed with `CHAR_BIT == 16` (no 8-bit type -- each octet occupies one 16-bit cell), built with the TI `cl2000` toolchain against wolfSSL's wide-byte (`CHAR_BIT != 8`) support. diff --git a/hal/imx95_a55.c b/hal/imx95_a55.c new file mode 100644 index 0000000000..20108b3ed5 --- /dev/null +++ b/hal/imx95_a55.c @@ -0,0 +1,1958 @@ +/* imx95_a55.c + * + * HAL for the Cortex-A55 cluster on the NXP i.MX95, running as BL33. + * + * 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 + */ + +/* wolfBoot as BL33: third image in AHAB container 2, entered by BL31 at + * IMX95_BL33_BASE in NS-EL2. RAM-resident, so the flash HAL below is a no-op + * surface the core references unconditionally. */ + +#include +#include +#include +#include +#include + +#if defined(DEBUG_UART) + #define PRINTF_ENABLED +#endif + +#include "image.h" +#include "loader.h" +#include "printf.h" +#include "hal/imx95_a55.h" +#ifdef IMX95_EMMC_PROBE +#include "hal/imx95_ahab.h" +#endif + +#ifdef IMX95_INIT_M7 +/* SCMI M7 power-domain on + TCM ECC scrub, in the SCMI section below. */ +extern int imx95_m7_tcm_init(void); +#endif + +/* x0 at entry (captured by the .boot stub); .data so BSS clear spares it. */ +volatile uint64_t boot_handoff_x0 = 0xFFFFFFFFFFFFFFFFULL; + +static inline uint32_t rd32(uintptr_t a) +{ + return *(volatile uint32_t*)a; +} + +static inline void wr32(uintptr_t a, uint32_t v) +{ + *(volatile uint32_t*)a = v; +} + +#ifndef BUILD_LOADER_STAGE1 +/* To the drivers further down, everything is the wolfBoot HAL API this image + * presents as BL33. Stage 1 links this file for those drivers but is not a + * wolfBoot instance: it has no partitions, FIT or device tree, and its target.h + * does not define the load addresses here. The PPC targets gate the same way. */ + +/* Handoff recon: the entry EL, SCTLR MMU/cache bits and x0 BL31 passed, to + * confirm the BL33 entry contract. On by default; it is cheap and diagnostic. */ + +static inline uint64_t read_current_el(void) +{ + uint64_t v; + __asm__ volatile("mrs %0, CurrentEL" : "=r"(v)); + return (v >> 2) & 0x3; +} + +/* Read the SCTLR of the current EL (a higher EL's would trap). */ +static uint64_t read_current_sctlr(uint64_t el) +{ + uint64_t v = 0; + + switch (el) { + case 3: __asm__ volatile("mrs %0, sctlr_el3" : "=r"(v)); break; + case 2: __asm__ volatile("mrs %0, sctlr_el2" : "=r"(v)); break; + default: __asm__ volatile("mrs %0, sctlr_el1" : "=r"(v)); break; + } + return v; +} + +#if defined(DEBUG_UART) && defined(IMX95_HANDOFF_DUMP) + +static inline uint64_t read_mpidr(void) +{ + uint64_t v; + __asm__ volatile("mrs %0, mpidr_el1" : "=r"(v)); + return v; +} + +/* Two 32-bit halves: the small printf may lack long long. */ +static void dump64(const char* name, uint64_t v) +{ + wolfBoot_printf("%s0x%08x%08x\n", name, + (uint32_t)(v >> 32), (uint32_t)(v & 0xFFFFFFFFUL)); +} + +static void imx95_handoff_dump(void) +{ + uint64_t el = read_current_el(); + uint64_t sctlr = read_current_sctlr(el); + uint64_t x0 = boot_handoff_x0; + uint64_t cntfrq; + const uint8_t* p; + int i; + + __asm__ volatile("mrs %0, cntfrq_el0" : "=r"(cntfrq)); + + wolfBoot_printf("i.MX95 A55 handoff recon:\n"); + wolfBoot_printf(" CurrentEL: EL%d\n", (int)el); + dump64(" SCTLR_ELx: ", sctlr); + wolfBoot_printf(" MMU=%d I$=%d D$=%d\n", + (int)(sctlr & 0x1), (int)((sctlr >> 12) & 0x1), + (int)((sctlr >> 2) & 0x1)); + dump64(" MPIDR_EL1: ", read_mpidr()); + dump64(" CNTFRQ_EL0: ", cntfrq); + dump64(" handoff x0: ", x0); + + /* Only deref x0 if it lands in DRAM (a DTB starts d00dfeed BE). */ + if (x0 >= IMX95_DRAM_BASE && x0 <= (IMX95_DRAM_END - 16)) { + p = (const uint8_t*)(uintptr_t)x0; + wolfBoot_printf(" [x0] first 16 bytes:\n "); + for (i = 0; i < 16; i++) + wolfBoot_printf("%02x ", p[i]); + wolfBoot_printf("\n"); + } + else { + wolfBoot_printf(" [x0] not a plausible DRAM pointer; skipping dump\n"); + } +} +#endif /* DEBUG_UART && IMX95_HANDOFF_DUMP */ + +/* Span covering every address a verified FIT sub-image is copied to, cleaned as + * one range by hal_prepare_boot() because the exit's set/way clean misses the + * DSU/L3. imx95_check_load_ranges() rejects loads outside it; keep them in step. */ +#ifndef IMX95_PAYLOAD_FLUSH_BASE +#define IMX95_PAYLOAD_FLUSH_BASE 0xA0000000UL +#endif +#ifndef IMX95_PAYLOAD_FLUSH_TOP +/* Must stay above the highest address a FIT sub-image reaches: ramdisk dest plus + * WOLFBOOT_FIT_MAX_RAMDISK lands at 0xCA000000 here, so a full-size initramfs + * cannot leave its tail dirty. Still inside DRAM bank 0. */ +#define IMX95_PAYLOAD_FLUSH_TOP 0xD0000000UL +#endif + +/* Staging-window sanity: overlap with the BL31/OP-TEE/M7 carveouts corrupts + * them silently; fail loudly at boot instead. */ + +/* Same contract as its twin in hal/imx95_ahab.c: an empty range overlaps + * nothing, and callers hand in wrap-free extents (imx95_check_dest() rejects a + * wrapping addr+len before it gets here). */ +static int range_overlaps(uint64_t a_start, uint64_t a_len, + uint64_t b_start, uint64_t b_len) +{ + if (a_len == 0U || b_len == 0U) + return 0; + return (a_start < (b_start + b_len)) && (b_start < (a_start + a_len)); +} + +static int imx95_check_one(const char* name, uint64_t load, uint64_t len, + uint64_t res, uint64_t res_len) +{ + if (range_overlaps(load, len, res, res_len)) { + wolfBoot_printf("imx95: staging window overlaps %s\n", name); + return -1; + } + return 0; +} + +/* A FIT sub-image destination must be in DRAM bank 0, inside the span + * hal_prepare_boot() cleans, and clear of the carveouts - vpu_boot starts at the + * bottom of that span, so being inside it is not proof of being safe. */ +static int imx95_check_dest(const char* name, uint64_t addr, uint64_t len) +{ + uint64_t end; + int ret = 0; + + if (len == 0UL || addr > (UINT64_MAX - len)) { + wolfBoot_printf("imx95: %s load range is empty or wraps\n", name); + return -1; + } + end = addr + len; + if (addr < IMX95_DRAM_BASE || end > IMX95_DRAM_END) { + wolfBoot_printf("imx95: %s load range outside DRAM bank 0\n", name); + return -1; + } + /* The end matters as much as the start: hal_prepare_boot() cleans only this + * span by VA, so a tail outside it reaches Linux un-cleaned at MMU-off. */ + if (addr < IMX95_PAYLOAD_FLUSH_BASE || end > IMX95_PAYLOAD_FLUSH_TOP) { + wolfBoot_printf("imx95: %s load range outside the flushed span\n", + name); + return -1; + } + ret |= imx95_check_one(name, addr, len, IMX95_BL31_BASE, 0x200000UL); + ret |= imx95_check_one(name, addr, len, + IMX95_OPTEE_BASE, IMX95_OPTEE_SIZE); + ret |= imx95_check_one(name, addr, len, + IMX95_OPTEE_SHM_BASE, IMX95_OPTEE_SHM_SIZE); + ret |= imx95_check_one(name, addr, len, + IMX95_M7_DDR_BASE, IMX95_M7_DDR_SIZE); + ret |= imx95_check_one(name, addr, len, + IMX95_ELE_SHM_BASE, IMX95_ELE_SHM_SIZE); + ret |= imx95_check_one(name, addr, len, + IMX95_VPU_BOOT_BASE, IMX95_VPU_BOOT_SIZE); + return ret; +} + +int imx95_check_load_ranges(void) +{ + uint64_t load = (uint64_t)WOLFBOOT_LOAD_ADDRESS; + uint64_t len = (uint64_t)WOLFBOOT_RAMBOOT_MAX_SIZE; + extern uint8_t _end[]; + uint64_t self = (uint64_t)IMX95_BL33_BASE; + uint64_t self_len = (uint64_t)(uintptr_t)_end - self; + int ret = 0; + + ret |= imx95_check_one("BL31", load, len, IMX95_BL31_BASE, 0x200000UL); + ret |= imx95_check_one("OP-TEE", load, len, + IMX95_OPTEE_BASE, IMX95_OPTEE_SIZE); + ret |= imx95_check_one("OP-TEE shm", load, len, + IMX95_OPTEE_SHM_BASE, IMX95_OPTEE_SHM_SIZE); + ret |= imx95_check_one("M7 carveout", load, len, + IMX95_M7_DDR_BASE, IMX95_M7_DDR_SIZE); + ret |= imx95_check_one("ELE shared buffer", load, len, + IMX95_ELE_SHM_BASE, IMX95_ELE_SHM_SIZE); + ret |= imx95_check_one("vpu_boot", load, len, + IMX95_VPU_BOOT_BASE, IMX95_VPU_BOOT_SIZE); + ret |= imx95_check_one("wolfBoot itself", load, len, self, self_len); + + if (load < IMX95_DRAM_BASE || (load + len) > IMX95_DRAM_END) { + wolfBoot_printf("imx95: staging window outside DRAM bank 0\n"); + ret = -1; + } + + /* The destinations known from the build. The kernel, DTB and ramdisk come + * from the FIT's own load properties instead, so wolfBoot_fit_memcpy() + * bounds those at the point of copy. */ + ret |= imx95_check_dest("staging", (uint64_t)WOLFBOOT_LOAD_ADDRESS, + (uint64_t)WOLFBOOT_RAMBOOT_MAX_SIZE); + ret |= imx95_check_dest("DTS", (uint64_t)WOLFBOOT_LOAD_DTS_ADDRESS, + (uint64_t)WOLFBOOT_DTS_MAX_SIZE); +#ifdef WOLFBOOT_LOAD_RAMDISK_ADDRESS + /* The ramdisk extent is not known until the FIT is parsed; check the + * destination itself so a configuration that aims it at a carveout fails + * at boot rather than corrupting one. */ + ret |= imx95_check_dest("ramdisk", + (uint64_t)WOLFBOOT_LOAD_RAMDISK_ADDRESS, 1UL); +#endif + return ret; +} + +/* -------------------------------------------------------------------------- + * HAL surface + * -------------------------------------------------------------------------- */ + +void hal_init(void) +{ +#if defined(DEBUG_UART) + uart_init(); + wolfBoot_printf("\nwolfBoot: NXP i.MX95 Cortex-A55 (BL33)\n"); +#endif +#if defined(DEBUG_UART) && defined(IMX95_HANDOFF_DUMP) + imx95_handoff_dump(); +#endif + if (imx95_check_load_ranges() != 0) { + wolfBoot_printf("imx95: refusing to boot with an unsafe load window\n"); + wolfBoot_panic(); + } +#ifdef IMX95_INIT_M7 + /* U-Boot powers up the M7 mix and scrubs its TCM (ECC) at board init; as + * its BL33 replacement wolfBoot must too. Non-fatal: the A55 boot proceeds + * even if the M7 bring-up fails. */ + (void)imx95_m7_tcm_init(); +#endif +} + +/* src/boot_aarch64_start.S: clean+invalidate by VA. */ +extern void flush_dcache_range(uintptr_t start, uintptr_t end); + +#if defined(WOLFBOOT_UPDATE_DISK) || defined(BOOT_BENCHMARK) +/* Microseconds from the ARM generic timer. CNTFRQ_EL0 is set up by the stages + * ahead of wolfBoot (24 MHz on this part); fall back to that rather than + * dividing by zero if a platform ever leaves it clear. */ +uint64_t hal_get_timer_us(void) +{ + uint64_t cnt, frq; + + __asm__ volatile("isb" : : : "memory"); + __asm__ volatile("mrs %0, cntpct_el0" : "=r"(cnt)); + __asm__ volatile("mrs %0, cntfrq_el0" : "=r"(frq)); + if (frq == 0) + frq = 24000000ULL; + return (cnt * 1000000ULL) / frq; +} +#endif + +void hal_prepare_boot(void) +{ + uint64_t sctlr; + + /* Clean payload DRAM by VA: set/way misses the A55's DSU cache, so copies + * made with D-cache on can be stale at MMU-off. Load-bearing, not an + * optimization. The SCTLR.C test only skips it on an MMU-less build. */ + sctlr = read_current_sctlr(read_current_el()); + if ((sctlr & SCTLR_C) == 0) + return; + + flush_dcache_range((uintptr_t)IMX95_PAYLOAD_FLUSH_BASE, + (uintptr_t)IMX95_PAYLOAD_FLUSH_TOP); +} + +/* No flash on this stage. wolfBoot is RAM-resident and its images arrive from + * a storage driver, so these exist only to satisfy the core's unconditional + * references. */ +int RAMFUNCTION hal_flash_write(uintptr_t address, const uint8_t *data, int len) +{ + (void)address; (void)data; (void)len; + return 0; +} + +int RAMFUNCTION hal_flash_erase(uintptr_t address, int len) +{ + (void)address; (void)len; + return 0; +} + +void RAMFUNCTION hal_flash_unlock(void) +{ +} + +void RAMFUNCTION hal_flash_lock(void) +{ +} + +/* WOLFBOOT_NO_PARTITIONS: boot/update addresses come from here, not linker + * symbols. The disk updater picks A/B by partition index and ignores these, so + * they serve libwolfboot.c's version helpers and both name one staging window. */ +void* hal_get_primary_address(void) +{ + return (void*)(uintptr_t)WOLFBOOT_LOAD_ADDRESS; +} + +void* hal_get_update_address(void) +{ + return (void*)(uintptr_t)WOLFBOOT_LOAD_ADDRESS; +} + +#if defined(__WOLFBOOT) && defined(LINUX_BOOTARGS) +#include "fdt.h" + +/* Primary ethernet (end0) MAC as a 48-bit value, e.g. + * -DIMX95_ETH0_MAC=0x00142d888d20 (see config/examples/imx95-a55.config). + * Undefined means skip the fixup: shipping one module's address as every + * board's default would put duplicate MACs on a network. */ + +/* Fix up /chosen and /memory on the (already verified) FIT DTB. */ +/* Every FIT sub-image destination comes from that image's own `load` property, + * so it is bounded here rather than assumed from the build: this hook is the + * only place the kernel and DTB destinations are known. The caller fails closed + * on a negative return. */ +/* Shared by both FIT write paths. The FIT is still live in the staging window + * and is the copy's source, so a destination inside it would overwrite the + * bytes being read. That is checked here and not in imx95_check_dest(), which + * is also asked to validate the staging window itself and would then reject its + * own configuration. */ +static int imx95_fit_dest_ok(uint64_t dst, uint64_t len) +{ + if (imx95_check_dest("FIT sub-image", dst, len) != 0) { + return -1; + } + if (range_overlaps(dst, len, (uint64_t)WOLFBOOT_LOAD_ADDRESS, + (uint64_t)WOLFBOOT_RAMBOOT_MAX_SIZE)) { + wolfBoot_printf("imx95: FIT sub-image would land in the staging " + "window\n"); + return -1; + } + return 0; +} + +int wolfBoot_fit_memcpy(void *dst, const void *src, uint32_t len) +{ + if (imx95_fit_dest_ok((uint64_t)(uintptr_t)dst, (uint64_t)len) != 0) { + return -1; + } + memcpy(dst, src, len); + return 0; +} + +/* The gzip path writes straight to the FIT-declared address, so it needs the + * same bound as the copy. len is the decompressor's output ceiling. */ +int wolfBoot_fit_check_dest(void *dst, uint32_t len) +{ + return imx95_fit_dest_ok((uint64_t)(uintptr_t)dst, (uint64_t)len); +} + +int hal_dts_fixup(void* dts_addr, uint32_t capacity) +{ + fdt_ctx ctx; + int off, ret; + + ret = fdt_open(&ctx, dts_addr, capacity); + if (ret != 0) { + wolfBoot_printf("FDT: invalid header (%d)\n", ret); + return ret; + } + /* Headroom so the next stage sees room to grow. */ + ret = fdt_grow(&ctx, WOLFBOOT_FDT_FIXUP_HEADROOM); + if (ret != 0) { + wolfBoot_printf("FDT: no headroom for fixups (%d)\n", ret); + return ret; + } + + /* Root child only: a nested node named "chosen" must not be matched. */ + off = fdt_subnode_offset(&ctx, 0, "chosen"); + if (off == -FDT_ERR_NOTFOUND) + off = fdt_add_subnode(&ctx, 0, "chosen"); + if (off < 0) { + wolfBoot_printf("FDT: no /chosen (%d)\n", off); + return off; + } + ret = fdt_fixup_str(&ctx, off, "chosen", "bootargs", LINUX_BOOTARGS); + if (ret != 0) { + wolfBoot_printf("FDT: bootargs fixup failed (%d)\n", ret); + return ret; + } + + /* /memory: the OS deployment DTB ships without one (the bootloader + * normally adds it); a kernel with no memory node hangs before earlycon. + * Banks match U-Boot's fixup for this board. */ + off = fdt_find_devtype(&ctx, -1, "memory"); + if (off < 0) { + off = fdt_add_subnode(&ctx, 0, "memory"); + if (off >= 0) { + /* Without device_type Linux does not read the node as memory, so + * falling through to write reg would cause the silent pre-earlycon + * hang this fixup exists to avoid. */ + ret = fdt_setprop(&ctx, off, "device_type", "memory", 7); + if (ret != 0) { + wolfBoot_printf("FDT: /memory device_type failed (%d)\n", ret); + return ret; + } + } + } + if (off >= 0) { + uint64_t memreg[4]; + /* Override per module: DRAM population varies by SKU. */ + memreg[0] = cpu_to_fdt64(IMX95_DRAM_BANK0_BASE); + memreg[1] = cpu_to_fdt64(IMX95_DRAM_BANK0_SIZE); + memreg[2] = cpu_to_fdt64(IMX95_DRAM_BANK1_BASE); + memreg[3] = cpu_to_fdt64(IMX95_DRAM_BANK1_SIZE); + ret = fdt_setprop(&ctx, off, "reg", memreg, sizeof(memreg)); + if (ret != 0) { + wolfBoot_printf("FDT: /memory reg failed (%d)\n", ret); + return ret; + } + } + else { + /* Fail closed: a kernel with no RAM description hangs silently. */ + wolfBoot_printf("FDT: cannot create /memory (%d)\n", off); + return off; + } + + /* The ENETC ports are PCIe-enumerated with no local-mac-address in the OS + * DTB, so Linux would pick a random MAC each boot and the DHCP address would + * float. Sets ethernet0 only, and is non-fatal. */ +#ifdef IMX95_ETH0_MAC + { + static const uint8_t eth0_mac[6] = { + (uint8_t)((IMX95_ETH0_MAC >> 40) & 0xFFU), + (uint8_t)((IMX95_ETH0_MAC >> 32) & 0xFFU), + (uint8_t)((IMX95_ETH0_MAC >> 24) & 0xFFU), + (uint8_t)((IMX95_ETH0_MAC >> 16) & 0xFFU), + (uint8_t)((IMX95_ETH0_MAC >> 8) & 0xFFU), + (uint8_t)(IMX95_ETH0_MAC & 0xFFU) + }; + off = fdt_path_offset(&ctx, "/soc/pcie@4ca00000/ethernet@0,0"); + if (off >= 0) { + ret = fdt_setprop(&ctx, off, "local-mac-address", eth0_mac, + sizeof(eth0_mac)); + if (ret != 0) + wolfBoot_printf("FDT: eth0 MAC fixup failed (%d)\n", ret); + } + else { + wolfBoot_printf("FDT: eth0 node not found (%d)\n", off); + } + } +#endif /* IMX95_ETH0_MAC */ + + /* Last DTB write before the jump, after hal_prepare_boot()'s flush; + * set/way cleaning in do_boot misses the DSU system cache, so clean + * the blob by VA here or the kernel can read stale /chosen edits. */ + flush_dcache_range((uintptr_t)dts_addr, + (uintptr_t)dts_addr + fdt_size(&ctx)); + return 0; +} +#endif /* __WOLFBOOT && LINUX_BOOTARGS */ + +/* The DTB travels inside the verified FIT rather than as a separate image, so + * there is no standalone DTS partition. Returning NULL is supported - the + * updater treats it as "no external device tree". */ +void* hal_get_dts_address(void) +{ + return NULL; +} + +void* hal_get_dts_update_address(void) +{ + return NULL; +} +#endif /* !BUILD_LOADER_STAGE1 */ + + +/* LPUART1 console. A55-only, shared with the stage 1 loader. */ + +#if defined(DEBUG_UART) + +/* Stage 1 runs before anything has touched the port, so it programs it; as + * BL33 the earlier stages left it clocked and at 115200, and rewriting BAUD + * would need the reference rate the System Manager owns. */ +void uart_init(void) +{ +#ifdef IMX95_STAGE1 + /* 24 MHz / (16 * 13) = 115385, inside what an 8N1 receiver tolerates. */ + wr32(IMX95_LPUART1_BASE + LPUART_CTRL_OFF, 0); + wr32(IMX95_LPUART1_BASE + LPUART_BAUD_OFF, + ((LPUART_BAUD_OSR - 1UL) << 24) | LPUART_BAUD_SBR); + wr32(IMX95_LPUART1_BASE + LPUART_CTRL_OFF, LPUART_CTRL_TE); +#endif +} + +static void uart_tx(char c) +{ + while ((rd32(IMX95_LPUART1_BASE + LPUART_STAT_OFF) & LPUART_STAT_TDRE) == 0) + ; + wr32(IMX95_LPUART1_BASE + LPUART_DATA_OFF, (uint32_t)(uint8_t)c); +} + +#ifdef IMX95_LOG_RING +/* The ring is in the M7 carveout, which the EL2 map marks Normal + * Non-Cacheable so readers outside this cluster's coherency see it without + * maintenance. */ +static void log_ring_putc(char c) +{ + static int ring_ready; + volatile uint32_t *hdr = (volatile uint32_t *)(uintptr_t)IMX95_LOG_RING_BASE; + volatile uint8_t *data = + (volatile uint8_t *)(uintptr_t)(IMX95_LOG_RING_BASE + IMX95_LOG_RING_HDR); + uint32_t wr; + int inherit = 0; + + if (!ring_ready) { +#ifndef IMX95_STAGE1 + /* Stage 1 starts the ring clean and later images continue it, or the + * second writer resets the counter and the first image's output is + * lost. A cold boot leaves garbage, so magic and size must match. */ + if (hdr[0] == (uint32_t)IMX95_LOG_RING_MAGIC && + hdr[2] == (uint32_t)IMX95_LOG_RING_SIZE) + inherit = 1; +#endif + if (!inherit) { + /* Publish the magic last: until it is set a reader treats the + * region as absent rather than reading a stale count. */ + hdr[1] = 0; + hdr[2] = (uint32_t)IMX95_LOG_RING_SIZE; + hdr[3] = 0; + hdr[0] = (uint32_t)IMX95_LOG_RING_MAGIC; + } + ring_ready = 1; + } + wr = hdr[1]; + data[wr % (uint32_t)IMX95_LOG_RING_SIZE] = (uint8_t)c; + hdr[1] = wr + 1; /* monotonic: the reader derives wrap from it */ +} +#endif /* IMX95_LOG_RING */ + +/* printf does not expand newlines. */ +void uart_write(const char* buf, unsigned int sz) +{ + unsigned int i; + + for (i = 0; i < sz; i++) { + if (buf[i] == '\n') + uart_tx('\r'); + uart_tx(buf[i]); +#ifdef IMX95_LOG_RING + log_ring_putc(buf[i]); +#endif + } + while ((rd32(IMX95_LPUART1_BASE + LPUART_STAT_OFF) & LPUART_STAT_TC) == 0) + ; +} + +#endif /* DEBUG_UART */ + + +/* SCMI client over MU2: clocks, pinmux, power. A55-only, shared with the stage 1 loader. */ + +/* Without U-Boot ahead of wolfBoot, clocks, pinmux and power belong to the M33 + * System Manager and must be requested over SCMI - the pad registers data-abort + * on direct access from BL33. SMT transport over MU2, then clock/pinctrl/power. */ + + +#if defined(IMX95_SCMI_COLD_INIT) || defined(IMX95_INIT_M7) || \ + defined(IMX95_STAGE1) + +#define MU2_GCR (IMX95_MU2_BASE + 0x114) /* set BIT0: ring A2P doorbell */ +#define MU2_GSR (IMX95_MU2_BASE + 0x118) /* BIT0: GIR0 ack (write 1 to clr) */ +#define SCMI_SHMEM 0x445B1000UL /* scmi_buf0, 1 KiB */ + +/* SMT header offsets within the shared buffer */ +#define SMT_CHAN_STATUS 0x04 +#define SMT_FLAGS 0x10 +#define SMT_LENGTH 0x14 +#define SMT_MSG_HEADER 0x18 +#define SMT_PAYLOAD 0x1C +#define CHAN_FREE 0x1UL + +#define SCMI_PROTO_PERF 0x13 +#define SCMI_PROTO_CLOCK 0x14 +#define SCMI_PROTO_PINCTRL 0x19 +#define SCMI_PROTO_POWER 0x11 +#define CLOCK_RATE_SET 0x5 +#define CLOCK_RATE_GET 0x6 +#define CLOCK_CONFIG_SET 0x7 +#define CLOCK_PARENT_SET 0xD +#define CLOCK_RATE_ROUND_CLOSEST (1UL << 3) +#define PINCTRL_CONFIG_SET 0x6 +#define PWD_STATE_SET 0x4 +#define PWD_STATE_GET 0x5 +#define PERF_LEVEL_SET 0x7 +#define PINCTRL_TYPE_MUX 192 +#define PINCTRL_TYPE_CONFIG 193 + +/* Power domain id for the M7 mix (dt-bindings/power/fsl,imx95-power.h). */ +#define IMX95_PD_DDR 12 +#define IMX95_PD_M7 17 + +/* Performance domain 8 is the A55 cluster; level 3 is its top operating + * point. U-Boot's set_arm_core_max_clk() uses the same pair. */ +#define IMX95_PERF_DOM_ARM 8 +#define IMX95_PERF_LVL_MAX 3 + +/* Pad ALT mode passed as the SCMI MUX value: 0 = uSDHC2 function, 5 = GPIO3. */ +#define PAD_ALT_LPUART1 0 +#define PAD_ALT_USDHC1 0 +#define PAD_ALT_USDHC2 0 +#define PAD_ALT_GPIO 5 + +#define IMX95_CLK_24M 2 +#define IMX95_CLK_SYSPLL1_PFD1 9 +#define IMX95_CLK_LPUART1 52 /* IMX95_CCM_NUM_CLK_SRC(41) + 11 */ +#define IMX95_CLK_USDHC1 158 /* IMX95_CCM_NUM_CLK_SRC(41) + 117 */ +#define IMX95_CLK_USDHC2 159 /* IMX95_CCM_NUM_CLK_SRC(41) + 118 */ + +#define SCMI_TIMEOUT 2000000 + +/* Build with -DIMX95_SCMI_DEBUG for a per-step trace of the cold-init. */ +#ifdef IMX95_SCMI_DEBUG +#define SCMI_DBG(...) wolfBoot_printf(__VA_ARGS__) +#else +#define SCMI_DBG(...) do { } while (0) +#endif + + +/* One synchronous SCMI command. payload[] holds n_in request words in and n_out + * response words out, word 0 the status. The counts differ per message, and a + * wrong one makes the System Manager read a field from the wrong offset. */ +static int scmi_cmd(uint32_t proto, uint32_t msg_id, + uint32_t* payload, uint32_t n_in, uint32_t n_out) +{ + uintptr_t sh = SCMI_SHMEM; + uint32_t n, i; + + /* Wait for the channel to be free. */ + for (n = 0; n < SCMI_TIMEOUT; n++) { + if (rd32(sh + SMT_CHAN_STATUS) & CHAN_FREE) + break; + } + if (n == SCMI_TIMEOUT) + return -1; + + wr32(sh + SMT_FLAGS, 0); /* poll, no interrupt */ + wr32(sh + SMT_LENGTH, 4 + n_in * 4); /* header word + payload */ + wr32(sh + SMT_MSG_HEADER, (proto << 10) | (msg_id & 0xFF)); + for (i = 0; i < n_in; i++) + wr32(sh + SMT_PAYLOAD + i * 4, payload[i]); + + /* Hand the channel to the SM and ring the doorbell. */ + wr32(sh + SMT_CHAN_STATUS, rd32(sh + SMT_CHAN_STATUS) & ~CHAN_FREE); + wr32(MU2_GCR, rd32(MU2_GCR) | 0x1); + + /* The SM sets FREE again when the response is in place. */ + for (n = 0; n < SCMI_TIMEOUT; n++) { + if (rd32(sh + SMT_CHAN_STATUS) & CHAN_FREE) + break; + } + if (n == SCMI_TIMEOUT) + return -2; + wr32(MU2_GSR, 0x1); /* clear GIR0 ack */ + + for (i = 0; i < n_out; i++) + payload[i] = rd32(sh + SMT_PAYLOAD + i * 4); + return (int)payload[0]; /* SCMI status, 0 = OK */ +} + +#if (defined(DISK_SDCARD) && defined(IMX95_SCMI_COLD_INIT)) || \ + defined(IMX95_STAGE1) +static int scmi_clock_enable(uint32_t clock_id) +{ + uint32_t p[3]; + p[0] = clock_id; + p[1] = 1; /* attributes: enable */ + p[2] = 0; /* oem_config_val */ + return scmi_cmd(SCMI_PROTO_CLOCK, CLOCK_CONFIG_SET, p, 3, 1); +} + +/* One pad: MUX(=mux) + CONFIG(=conf). identifier is the mux register offset/4 + * (the SM's pin numbering); function_id 0xFFFFFFFF, attributes = ncfgs<<2. */ +static int scmi_pin_config(uint32_t mux_ofs, uint32_t mux, uint32_t conf) +{ + uint32_t p[7]; + p[0] = mux_ofs / 4; /* identifier */ + p[1] = 0xFFFFFFFF; /* function_id */ + p[2] = (uint32_t)(2 << 2); /* attributes: 2 configs */ + p[3] = PINCTRL_TYPE_MUX; p[4] = mux; + p[5] = PINCTRL_TYPE_CONFIG; p[6] = conf; + return scmi_cmd(SCMI_PROTO_PINCTRL, PINCTRL_CONFIG_SET, p, 7, 1); +} + +/* Three uSDHC2 signals are carrier GPIOs, not controller pins: SCMI-mux the pads + * to GPIO3 (ALT5), then drive RGPIO3 directly. From the SMARC DTS: + * GPIO3.0 SD2_CD_B card detect (input, active low) + * GPIO3.7 SD2_RESET_B SDIO_PWR_EN (output high = card power on) + * GPIO3.19 SD2_VSELECT PMIC_SD2_VSEL (output low = 3.3V, high = 1.8V) */ +#define RGPIO3_PDOR (IMX95_GPIO3_BASE + 0x40) +#define RGPIO3_PDDR (IMX95_GPIO3_BASE + 0x54) +#define GPIO3_CD 0U +#define GPIO3_PWR 7U +#define GPIO3_VSEL 19U + +static void delay_loops(uint32_t loops) +{ + volatile uint32_t d = loops; + while (d-- > 0U) { } +} + +int imx95_usdhc2_cold_init(void) +{ + /* All nine uSDHC2 pads: {mux offset, ALT mode, pad conf}. Data/clk/cmd take + * ALT0, the three carrier GPIOs ALT5. From the SMARC pinctrl groups. */ + static const uint32_t pads[9][3] = { + { 0x1A4, PAD_ALT_USDHC2, 0x158e }, /* SD2_CLK */ + { 0x1A8, PAD_ALT_USDHC2, 0x138e }, /* SD2_CMD */ + { 0x1AC, PAD_ALT_USDHC2, 0x138e }, /* SD2_DATA0 */ + { 0x1B0, PAD_ALT_USDHC2, 0x138e }, /* SD2_DATA1 */ + { 0x1B4, PAD_ALT_USDHC2, 0x138e }, /* SD2_DATA2 */ + { 0x1B8, PAD_ALT_USDHC2, 0x138e }, /* SD2_DATA3 */ + { 0x1A0, PAD_ALT_GPIO, 0x1100 }, /* SD2_CD_B -> GPIO3.0 */ + { 0x1BC, PAD_ALT_GPIO, 0x011e }, /* SD2_RESET_B -> GPIO3.7 */ + { 0x154, PAD_ALT_GPIO, 0x0004 }, /* SD2_VSELECT -> GPIO3.19 */ + }; + int i, ret; + uint32_t pddr, pdor; + + ret = scmi_clock_enable(IMX95_CLK_USDHC2); + SCMI_DBG("scmi: clock enable(%d) -> %d\n", IMX95_CLK_USDHC2, ret); + if (ret != 0) { + wolfBoot_printf("scmi: uSDHC2 clock enable failed (%d)\n", ret); + return ret; + } + + for (i = 0; i < 9; i++) { + ret = scmi_pin_config(pads[i][0], pads[i][1], pads[i][2]); + SCMI_DBG("scmi: pad 0x%x mux %d -> %d\n", + (unsigned)pads[i][0], (int)pads[i][1], ret); + if (ret != 0) { + wolfBoot_printf("scmi: pad 0x%x config failed (%d)\n", + (unsigned)pads[i][0], ret); + return ret; + } + } + + /* Card detect as input; voltage-select and power-enable as outputs. */ + pddr = rd32(RGPIO3_PDDR); + pddr &= ~(1U << GPIO3_CD); + pddr |= (1U << GPIO3_VSEL) | (1U << GPIO3_PWR); + wr32(RGPIO3_PDDR, pddr); + + /* Select 3.3V I/O (VSEL low) before powering the card. */ + pdor = rd32(RGPIO3_PDOR); + pdor &= ~(1U << GPIO3_VSEL); + wr32(RGPIO3_PDOR, pdor); + + /* Power on, then wait past the regulator startup delay (DTS + * startup-delay-us 20000). The count is generous enough to clear 20 ms + * either way: stage 1 runs this with caches off, BL33 with them on. */ + pdor |= (1U << GPIO3_PWR); + wr32(RGPIO3_PDOR, pdor); + delay_loops(40000000U); + + SCMI_DBG("scmi: GPIO3 PDDR=0x%x PDOR=0x%x\n", + (unsigned)rd32(RGPIO3_PDDR), (unsigned)rd32(RGPIO3_PDOR)); + wolfBoot_printf("scmi: uSDHC2 clock+pinmux+power up\n"); + return 0; +} +#endif /* DISK_SDCARD && IMX95_SCMI_COLD_INIT */ + +#ifdef IMX95_INIT_M7 +/* Cortex-M7 TCM system-view bases and size (256 KiB each at TCM_SIZE=000b). */ + +/* Match U-Boot's power_on_m7(): power up the M7 mix over SCMI, then scrub its TCM + * so never-written words carry valid ECC. Needed because wolfBoot replaces the + * U-Boot board init on a board whose M7 Linux remoteproc launches later. */ +int imx95_m7_tcm_init(void) +{ + uint32_t p[3]; + volatile uint32_t *w; + uint32_t i, words; + int ret; + + /* SCMI power domain: STATE_SET {flags=0, domain_id=M7, pstate=0=on}. */ + p[0] = 0; + p[1] = IMX95_PD_M7; + p[2] = 0; + ret = scmi_cmd(SCMI_PROTO_POWER, PWD_STATE_SET, p, 3, 1); + SCMI_DBG("scmi: M7 power-on -> %d\n", ret); + if (ret != 0) { + wolfBoot_printf("scmi: M7 power domain on failed (%d)\n", ret); + return ret; + } + + /* Scrub ITCM + DTCM to initialize ECC. Word writes: the TCM system view is + * mapped Device, where an unaligned or wider access would fault. */ + words = (uint32_t)(IMX95_M7_TCM_SIZE / 4U); + w = (volatile uint32_t *)IMX95_M7_ITCM_SYS; + for (i = 0; i < words; i++) + w[i] = 0U; + w = (volatile uint32_t *)IMX95_M7_DTCM_SYS; + for (i = 0; i < words; i++) + w[i] = 0U; + + wolfBoot_printf("scmi: M7 powered, TCM ECC initialized\n"); + return 0; +} +#endif /* IMX95_INIT_M7 */ + +#ifdef IMX95_STAGE1 +/* Report what the System Manager currently has a clock running at. */ +static uint32_t scmi_clock_rate(uint32_t clock_id) +{ + uint32_t p[3]; + + p[0] = clock_id; + if (scmi_cmd(SCMI_PROTO_CLOCK, CLOCK_RATE_GET, p, 1, 3) != 0) + return 0; + return p[1]; +} + +/* Parent a peripheral clock and give it a rate, then enable it. Enabling alone + * leaves whatever the previous owner set, which is invisible when a stage runs + * after U-Boot SPL and fatal when it runs instead of it. */ +static int scmi_clock_setup(uint32_t clock_id, uint32_t parent_id, + uint32_t rate) +{ + uint32_t p[4]; + int ret; + + p[0] = clock_id; + p[1] = 0; /* attributes: off while reparenting */ + p[2] = 0; + ret = scmi_cmd(SCMI_PROTO_CLOCK, CLOCK_CONFIG_SET, p, 3, 1); + if (ret != 0) + return ret; + + p[0] = clock_id; + p[1] = parent_id; + ret = scmi_cmd(SCMI_PROTO_CLOCK, CLOCK_PARENT_SET, p, 2, 1); + if (ret != 0) + return ret; + + p[0] = CLOCK_RATE_ROUND_CLOSEST; + p[1] = clock_id; + p[2] = rate; + p[3] = 0; + ret = scmi_cmd(SCMI_PROTO_CLOCK, CLOCK_RATE_SET, p, 4, 1); + if (ret != 0) + return ret; + + return scmi_clock_enable(clock_id); +} + +/* uSDHC1 is the on-module eMMC, already used by the ROM, so this only makes the + * state explicit instead of inheriting it. Eight data lines, soldered down, so + * no card detect and no card power. */ +int imx95_usdhc1_cold_init(void) +{ + static const uint32_t pads[11][2] = { + { 0x128, 0x158e }, /* SD1_CLK */ + { 0x12C, 0x138e }, /* SD1_CMD */ + { 0x130, 0x138e }, /* SD1_DATA0 */ + { 0x134, 0x138e }, /* SD1_DATA1 */ + { 0x138, 0x138e }, /* SD1_DATA2 */ + { 0x13C, 0x138e }, /* SD1_DATA3 */ + { 0x140, 0x138e }, /* SD1_DATA4 */ + { 0x144, 0x138e }, /* SD1_DATA5 */ + { 0x148, 0x138e }, /* SD1_DATA6 */ + { 0x14C, 0x138e }, /* SD1_DATA7 */ + { 0x150, 0x158e } /* SD1_STROBE */ + }; + uint32_t was; + int i, ret; + + /* The divider in the uSDHC section below is written against a 400 MHz module + * clock, which is what U-Boot's init_clk_usdhc() sets. */ + was = scmi_clock_rate(IMX95_CLK_USDHC1); + ret = scmi_clock_setup(IMX95_CLK_USDHC1, IMX95_CLK_SYSPLL1_PFD1, + 400000000UL); + if (ret != 0) { + wolfBoot_printf("scmi: uSDHC1 clock setup failed (%d)\n", ret); + return ret; + } + wolfBoot_printf("scmi: uSDHC1 clock %u -> %u Hz\n", + (unsigned)was, (unsigned)scmi_clock_rate(IMX95_CLK_USDHC1)); + + for (i = 0; i < 11; i++) { + ret = scmi_pin_config(pads[i][0], PAD_ALT_USDHC1, pads[i][1]); + if (ret != 0) { + wolfBoot_printf("scmi: pad 0x%x config failed (%d)\n", + (unsigned)pads[i][0], ret); + return ret; + } + } + + wolfBoot_printf("scmi: uSDHC1 clock+pinmux up\n"); + return 0; +} + +/* Parent the console UART to the 24 MHz oscillator and enable it. Stage 1 runs + * before anything else has touched the clock tree, so the console is silent + * until this has been done. Mirrors U-Boot's init_uart_clk(). */ +int imx95_scmi_uart_clk_init(void) +{ + /* SMARC SER1 on the AONMIX LPUART1 pads, both at ALT0. */ + static const uint32_t pads[2][2] = { + { 0x1D0, 0x31e }, /* UART1_RXD */ + { 0x1D4, 0x31e } /* UART1_TXD */ + }; + int i, ret; + + for (i = 0; i < 2; i++) { + ret = scmi_pin_config(pads[i][0], PAD_ALT_LPUART1, pads[i][1]); + if (ret != 0) + return ret; + } + + return scmi_clock_setup(IMX95_CLK_LPUART1, IMX95_CLK_24M, 24000000UL); +} + +/* Raise the A55 cluster to its maximum operating point. Nothing before stage 1 + * does this, so without it the whole boot runs at the reset rate. */ +int imx95_scmi_arm_max_clk(void) +{ + uint32_t p[2]; + + p[0] = IMX95_PERF_DOM_ARM; + p[1] = IMX95_PERF_LVL_MAX; + return scmi_cmd(SCMI_PROTO_PERF, PERF_LEVEL_SET, p, 2, 1); +} + +/* 1 when the DDR mix is powered, 0 when it is off, negative if the System + * Manager would not answer. The OEI is what brings DDR up, so an off domain + * means it did not run and nothing loaded into DRAM would survive. */ +int imx95_scmi_ddr_powered(void) +{ + uint32_t p[2]; + int ret; + + p[0] = IMX95_PD_DDR; + ret = scmi_cmd(SCMI_PROTO_POWER, PWD_STATE_GET, p, 1, 2); + if (ret != 0) + return ret; + /* Reply word 1 is the power state; bit 30 set means off. */ + return ((p[1] & (1UL << 30)) != 0UL) ? 0 : 1; +} +#endif /* IMX95_STAGE1 */ + +#endif /* IMX95_SCMI_COLD_INIT || IMX95_INIT_M7 || IMX95_STAGE1 */ + + +/* uSDHC: eMMC and carrier SD. A55-only, shared with the stage 1 loader. */ + +/* Minimal i.MX uSDHC driver (same IP as i.MX 6/7/8), NOT SDHCI-register + * compatible so src/sdhci.c does not apply. PIO, no DMA/tuning/1.8V, no + * disk_write(). Module clock and pinmux must already be up. */ + + +#if defined(IMX95_SCMI_COLD_INIT) && defined(DISK_SDCARD) +extern int imx95_usdhc2_cold_init(void); +#endif + +#if defined(DISK_SDCARD) || defined(DISK_EMMC) + +/* uSDHC1 carries the eMMC on this module, uSDHC2 the carrier SD slot. A build + * selects one: the two are separate controllers and the driver keeps a single + * card's state. */ +#ifndef USDHC_BASE +#ifdef DISK_EMMC +#define USDHC_BASE IMX95_USDHC1_BASE /* eMMC (uSDHC1) */ +#else +#define USDHC_BASE IMX95_USDHC2_BASE /* carrier SD (uSDHC2) */ +#endif +#endif + +/* --- uSDHC registers (offsets from the instance base) -------------------- */ +#define USDHC_DS_ADDR 0x00 /* DMA system address */ +#define USDHC_BLK_ATT 0x04 /* block size / count */ +#define USDHC_CMD_ARG 0x08 +#define USDHC_CMD_XFR_TYP 0x0C +#define USDHC_CMD_RSP0 0x10 +#define USDHC_CMD_RSP1 0x14 +#define USDHC_CMD_RSP2 0x18 +#define USDHC_CMD_RSP3 0x1C +#define USDHC_DATA_BUFF_ACC 0x20 /* PIO data port */ +#define USDHC_PRES_STATE 0x24 +#define USDHC_PROT_CTRL 0x28 +#define USDHC_SYS_CTRL 0x2C +#define USDHC_INT_STATUS 0x30 +#define USDHC_INT_STATUS_EN 0x34 +#define USDHC_INT_SIGNAL_EN 0x38 +#define USDHC_AUTOCMD12_ERR 0x3C +#define USDHC_HOST_CTRL_CAP 0x40 +#define USDHC_WTMK_LVL 0x44 +#define USDHC_MIX_CTRL 0x48 +#define USDHC_DLL_CTRL 0x60 +#define USDHC_CLK_TUNE_CTRL 0x68 /* CLK_TUNE_CTRL_STATUS */ +#define USDHC_VEND_SPEC 0xC0 +#define USDHC_MMC_BOOT 0xC4 +/* VEND_SPEC reset value: clock gates on, 3.3V signaling. */ +#define VEND_SPEC_INIT 0x20007809UL +#define VEND_SPEC_FRC_SDCLK_ON (1UL << 8) +#define VEND_SPEC_IPGEN (1UL << 11) /* IPG clock always on */ +#define VEND_SPEC_HCKEN (1UL << 12) /* AHB clock always on */ +#define VEND_SPEC_PEREN (1UL << 13) /* peripheral clock on */ +#define VEND_SPEC_CKEN (1UL << 14) /* SD clock on */ + +/* CMD_XFR_TYP fields */ +#define CMD_XFR_CMDINX(c) (((uint32_t)(c) & 0x3F) << 24) +#define CMD_XFR_CMDTYP_ABORT (3UL << 22) +#define CMD_XFR_DPSEL (1UL << 21) /* data present */ +#define CMD_XFR_CICEN (1UL << 20) /* check index */ +#define CMD_XFR_CCCEN (1UL << 19) /* check CRC */ +#define CMD_XFR_RSPTYP_NONE (0UL << 16) +#define CMD_XFR_RSPTYP_136 (1UL << 16) +#define CMD_XFR_RSPTYP_48 (2UL << 16) +#define CMD_XFR_RSPTYP_48B (3UL << 16) /* 48 with busy */ + +/* MIX_CTRL fields */ +#define MIX_CTRL_DMAEN (1UL << 0) +#define MIX_CTRL_BCEN (1UL << 1) /* block count enable */ +#define MIX_CTRL_AC12EN (1UL << 2) /* auto CMD12 */ +#define MIX_CTRL_DTDSEL_READ (1UL << 4) /* data direction: read */ +#define MIX_CTRL_MSBSEL (1UL << 5) /* multi block */ + +/* PRES_STATE fields */ +#define PRES_CIHB (1UL << 0) /* command inhibit (cmd line) */ +#define PRES_CDIHB (1UL << 1) /* command inhibit (data line) */ +#define PRES_DLA (1UL << 2) /* data line active */ +#define PRES_SDSTB (1UL << 3) /* SD clock stable */ +#define PRES_BREN (1UL << 11) /* buffer read enable */ +#define PRES_CINST (1UL << 16) /* card inserted */ + +/* PROT_CTRL fields */ +#define PROT_CTRL_DTW_MASK (3UL << 1) +#define PROT_CTRL_DTW_1BIT (0UL << 1) +#define PROT_CTRL_DTW_4BIT (1UL << 1) +#define PROT_CTRL_EMODE_LE (2UL << 4) /* little-endian mode */ +#define PROT_CTRL_CDTL (1UL << 6) /* card detect test level */ +#define PROT_CTRL_CDSS (1UL << 7) /* card detect source = test */ + +/* SYS_CTRL fields */ +#define SYS_CTRL_DVS_SHIFT 4 /* divisor: 1..16 */ +#define SYS_CTRL_SDCLKFS_SHIFT 8 /* prescaler: 2^n */ +#define SYS_CTRL_DTOCV_SHIFT 16 /* data timeout counter */ +#define SYS_CTRL_INITA (1UL << 27) /* send 80 init clocks */ +#define SYS_CTRL_RSTA (1UL << 24) /* reset all */ +#define SYS_CTRL_RSTC (1UL << 25) /* reset cmd */ +#define SYS_CTRL_RSTD (1UL << 26) /* reset data */ +#define SYS_CTRL_RSTT (1UL << 28) /* reset tuning */ + +/* INT_STATUS fields */ +#define INT_CC (1UL << 0) /* command complete */ +#define INT_TC (1UL << 1) /* transfer complete */ +#define INT_BRR (1UL << 5) /* buffer read ready */ +#define INT_CTOE (1UL << 16) /* command timeout */ +#define INT_CCE (1UL << 17) /* command CRC error */ +#define INT_CEBE (1UL << 18) /* command end bit error */ +#define INT_CIE (1UL << 19) /* command index error */ +#define INT_DTOE (1UL << 20) /* data timeout */ +#define INT_DCE (1UL << 21) /* data CRC error */ +#define INT_DEBE (1UL << 22) /* data end bit error */ +#define INT_CMD_ERRS (INT_CTOE | INT_CCE | INT_CEBE | INT_CIE) +#define INT_DATA_ERRS (INT_DTOE | INT_DCE | INT_DEBE) + +/* SD commands used */ +#define SD_CMD0_GO_IDLE 0 +#define SD_CMD2_ALL_SEND_CID 2 +#define SD_CMD3_SEND_REL_ADDR 3 +#define SD_CMD6_SWITCH 6 +#define MMC_CMD1_SEND_OP_COND 1 +#define MMC_CMD8_SEND_EXT_CSD 8 +#define SD_CMD7_SELECT 7 +#define SD_CMD8_SEND_IF_COND 8 +#define SD_CMD9_SEND_CSD 9 +#define SD_CMD12_STOP 12 +#define SD_CMD16_SET_BLOCKLEN 16 +#define SD_CMD17_READ_SINGLE 17 +#define SD_CMD18_READ_MULTIPLE 18 +#define SD_CMD55_APP_CMD 55 +#define SD_ACMD6_SET_BUS_WIDTH 6 +#define SD_ACMD41_OP_COND 41 + +#define SD_BLOCK_SIZE 512 +/* Multi-block cap: BLK_ATT count is 16-bit; stay well under it. */ +#define SD_MAX_BLOCKS 1024 + +#define USDHC_TIMEOUT_LOOPS 1000000 + +/* Build with -DIMX95_SCMI_DEBUG for a per-step trace of the SD bring-up. */ +#ifdef IMX95_SCMI_DEBUG +#define DISK_DBG(...) wolfBoot_printf(__VA_ARGS__) +#else +#define DISK_DBG(...) do { } while (0) +#endif + +static int card_rca; /* relative card address, from CMD3 */ +static int card_high_cap; /* SDHC/SDXC: block addressing */ +static int card_ready; + +/* Which controller the calls below talk to. A variable rather than the macro + * so one image can reach both instances - the eMMC on uSDHC1 and the carrier + * SD on uSDHC2 are the same IP at different addresses. */ +static uintptr_t usdhc_base = USDHC_BASE; + +static inline uint32_t usdhc_rd(uint32_t off) +{ + return *(volatile uint32_t*)(usdhc_base + off); +} + +static inline void usdhc_wr(uint32_t off, uint32_t v) +{ + *(volatile uint32_t*)(usdhc_base + off) = v; +} + +/* --- Low level ----------------------------------------------------------- */ + +static int usdhc_wait_clear(uint32_t off, uint32_t mask) +{ + uint32_t n; + + for (n = 0; n < USDHC_TIMEOUT_LOOPS; n++) { + if ((usdhc_rd(off) & mask) == 0) + return 0; + } + return -1; +} + +static int usdhc_wait_set(uint32_t off, uint32_t mask) +{ + uint32_t n; + + for (n = 0; n < USDHC_TIMEOUT_LOOPS; n++) { + if ((usdhc_rd(off) & mask) != 0) + return 0; + } + return -1; +} + +static void usdhc_reset(uint32_t bits) +{ + usdhc_wr(USDHC_SYS_CTRL, usdhc_rd(USDHC_SYS_CTRL) | bits); + (void)usdhc_wait_clear(USDHC_SYS_CTRL, bits); +} + +/* Divider = prescaler (2^n, field 0x01..0x80) x divisor (1..16). At the + * 400 MHz module clock default: /256/4 = ~390 kHz identification, /2/8 = + * 25 MHz default speed, /2/4 = 50 MHz once the card is in high-speed mode. */ +#define USDHC_CLK_ID 0 +#define USDHC_CLK_25MHZ 1 +#define USDHC_CLK_50MHZ 2 + +/* RSTA leaves the vendor registers alone, so a controller the boot ROM already + * drove stays in fast-boot mode with its HS400 tuning and the first command + * never completes. U-Boot's esdhc_init() restores the same five by hand. */ +/* The 80 startup clocks a card needs before its first command. The SD clock is + * forced on across it: INITA alone is not enough after the boot ROM. */ +static void usdhc_init_clocks(void) +{ + volatile uint32_t d; + + usdhc_wr(USDHC_VEND_SPEC, usdhc_rd(USDHC_VEND_SPEC) | VEND_SPEC_FRC_SDCLK_ON); + usdhc_wr(USDHC_SYS_CTRL, usdhc_rd(USDHC_SYS_CTRL) | SYS_CTRL_INITA); + (void)usdhc_wait_clear(USDHC_SYS_CTRL, SYS_CTRL_INITA); + /* Belt to the INITA self-clear above, which is the real 80-clock wait. + * Generous either way: stage 1 runs uncached, BL33 cached. */ + for (d = 0; d < 200000U; d++) { } + usdhc_wr(USDHC_VEND_SPEC, usdhc_rd(USDHC_VEND_SPEC) & ~VEND_SPEC_FRC_SDCLK_ON); +} + +static void usdhc_vendor_reset(void) +{ + usdhc_wr(USDHC_MMC_BOOT, 0); + usdhc_wr(USDHC_MIX_CTRL, 0); + usdhc_wr(USDHC_CLK_TUNE_CTRL, 0); + usdhc_wr(USDHC_DLL_CTRL, 0); + usdhc_wr(USDHC_VEND_SPEC, VEND_SPEC_INIT); +} + +static void usdhc_set_clock(int speed) +{ + uint32_t v; + + v = usdhc_rd(USDHC_SYS_CTRL); + v &= ~((0xFFUL << SYS_CTRL_SDCLKFS_SHIFT) | (0xFUL << SYS_CTRL_DVS_SHIFT) + | (0xFUL << SYS_CTRL_DTOCV_SHIFT)); + if (speed == USDHC_CLK_ID) { + v |= (0x80UL << SYS_CTRL_SDCLKFS_SHIFT); /* /256 */ + v |= (0x3UL << SYS_CTRL_DVS_SHIFT); /* /4 -> ~390 kHz */ + } + else if (speed == USDHC_CLK_50MHZ) { + v |= (0x01UL << SYS_CTRL_SDCLKFS_SHIFT); /* /2 */ + v |= (0x3UL << SYS_CTRL_DVS_SHIFT); /* /4 -> 50 MHz */ + } + else { + v |= (0x01UL << SYS_CTRL_SDCLKFS_SHIFT); /* /2 */ + v |= (0x7UL << SYS_CTRL_DVS_SHIFT); /* /8 -> 25 MHz */ + } + v |= (0xEUL << SYS_CTRL_DTOCV_SHIFT); /* max data timeout */ + usdhc_wr(USDHC_SYS_CTRL, v); + /* Enable the IPG/AHB/peripheral/SD clock gates: after a cold reset these + * are off, so SDSTB never sets and no command can run. (A warm handoff + * from a prior stage leaves them on; setting them again is harmless.) */ + usdhc_wr(USDHC_VEND_SPEC, usdhc_rd(USDHC_VEND_SPEC) | + VEND_SPEC_IPGEN | VEND_SPEC_HCKEN | VEND_SPEC_PEREN | VEND_SPEC_CKEN); + (void)usdhc_wait_set(USDHC_PRES_STATE, PRES_SDSTB); +} + +/* Send a command; response left in CMD_RSP0..3. Returns 0 on success. */ +static int usdhc_cmd(uint32_t idx, uint32_t arg, int rsp136, int rsp_busy, + int data, int multi, int check_crc_idx) +{ + uint32_t xfr; + uint32_t st; + uint32_t n; + + if (usdhc_wait_clear(USDHC_PRES_STATE, PRES_CIHB) != 0) + return -1; + if ((data || rsp_busy) && + usdhc_wait_clear(USDHC_PRES_STATE, PRES_CDIHB) != 0) + return -1; + + /* Clear stale status */ + usdhc_wr(USDHC_INT_STATUS, 0xFFFFFFFFUL); + + /* MIX_CTRL first: the CMD_XFR_TYP write launches the command. */ + if (data) { + uint32_t mix = MIX_CTRL_DTDSEL_READ; + if (multi) + mix |= MIX_CTRL_MSBSEL | MIX_CTRL_BCEN | MIX_CTRL_AC12EN; + usdhc_wr(USDHC_MIX_CTRL, mix); + } + else { + usdhc_wr(USDHC_MIX_CTRL, 0); + } + + xfr = CMD_XFR_CMDINX(idx); + if (rsp136) + xfr |= CMD_XFR_RSPTYP_136; + else if (rsp_busy) + xfr |= CMD_XFR_RSPTYP_48B; + else if (idx == SD_CMD0_GO_IDLE) + xfr |= CMD_XFR_RSPTYP_NONE; /* CMD0 has no response */ + else + xfr |= CMD_XFR_RSPTYP_48; + if (check_crc_idx) + xfr |= CMD_XFR_CICEN | CMD_XFR_CCCEN; + if (data) + xfr |= CMD_XFR_DPSEL; + + usdhc_wr(USDHC_CMD_ARG, arg); + usdhc_wr(USDHC_CMD_XFR_TYP, xfr); + + /* Wait for command complete or error */ + for (n = 0; n < USDHC_TIMEOUT_LOOPS; n++) { + st = usdhc_rd(USDHC_INT_STATUS); + if (st & INT_CMD_ERRS) { + usdhc_wr(USDHC_INT_STATUS, INT_CMD_ERRS | INT_CC); + usdhc_reset(SYS_CTRL_RSTC); + return (st & INT_CTOE) ? -2 : -3; + } + if (st & INT_CC) { + usdhc_wr(USDHC_INT_STATUS, INT_CC); + return 0; + } + } + usdhc_reset(SYS_CTRL_RSTC); + return -1; +} + +/* --- Card bring-up ------------------------------------------------------- */ + +/* CMD6 SWITCH_FUNC, mode 1, group 1 = high speed. The 512-bit status answers with + * the function actually selected in bits 379:376 - the low nibble of the 17th + * byte. Returns 0 only when the card confirms it. */ +static int sd_switch_high_speed(void) +{ + uint32_t sw[16]; + const uint8_t *b = (const uint8_t *)sw; + uint32_t n, st, i; + + usdhc_wr(USDHC_BLK_ATT, (1UL << 16) | 64UL); + usdhc_wr(USDHC_WTMK_LVL, (16UL << 16) | 16UL); + + if (usdhc_cmd(SD_CMD6_SWITCH, 0x80FFFFF1UL, 0, 0, 1, 0, 1) != 0) + goto restore; + + for (n = 0; n < USDHC_TIMEOUT_LOOPS; n++) { + st = usdhc_rd(USDHC_INT_STATUS); + if (st & INT_DATA_ERRS) { + usdhc_wr(USDHC_INT_STATUS, INT_DATA_ERRS); + goto restore; + } + if (usdhc_rd(USDHC_PRES_STATE) & PRES_BREN) + break; + } + if (n == USDHC_TIMEOUT_LOOPS) + goto restore; + usdhc_wr(USDHC_INT_STATUS, INT_BRR); + for (i = 0; i < 16; i++) + sw[i] = usdhc_rd(USDHC_DATA_BUFF_ACC); + if (usdhc_wait_set(USDHC_INT_STATUS, INT_TC) != 0) + goto restore; + usdhc_wr(USDHC_INT_STATUS, INT_TC); + + /* Restore the block geometry the read path expects. */ + usdhc_wr(USDHC_WTMK_LVL, (128UL << 16) | 128UL); + usdhc_wr(USDHC_BLK_ATT, (1UL << 16) | SD_BLOCK_SIZE); + return ((b[16] & 0x0FU) == 1U) ? 0 : -1; + +restore: + usdhc_reset(SYS_CTRL_RSTC | SYS_CTRL_RSTD); + usdhc_wr(USDHC_WTMK_LVL, (128UL << 16) | 128UL); + usdhc_wr(USDHC_BLK_ATT, (1UL << 16) | SD_BLOCK_SIZE); + return -1; +} + +#if defined(DISK_EMMC) || defined(IMX95_EMMC_PROBE) + +/* EXT_CSD byte 179: [2:0] picks the partition ordinary reads address, [5:3] the + * one the boot ROM loads from. Only the access bits are touched here - the + * enable bits would change where the SoC boots from next reset. */ +#define EXT_CSD_PARTITION_CONFIG 179 +#define EXT_CSD_BUS_WIDTH 183 +#define EXT_CSD_PART_ACCESS_MASK 0x07U +#define EXT_CSD_BUS_WIDTH_4BIT 1 +#define MMC_SWITCH_WRITE_BYTE (3UL << 24) + +static uint8_t emmc_part_config; /* EXT_CSD[179] as read at init */ + +/* CMD6 in the "write byte" form: index in [23:16], value in [15:8]. R1b, so + * the card holds DAT0 low while it applies the change. */ +static int emmc_switch(uint32_t index, uint32_t value) +{ + uint32_t arg = MMC_SWITCH_WRITE_BYTE | (index << 16) | (value << 8); + + return usdhc_cmd(SD_CMD6_SWITCH, arg, 0, 1, 0, 0, 1); +} + +/* CMD8 on eMMC returns the 512-byte EXT_CSD as a data block, unlike the SD + * CMD8 which is an interface-condition check with no data. */ +static int emmc_read_ext_csd(uint8_t *buf) +{ + uint32_t n, st, i; + uint32_t *out = (uint32_t *)(void *)buf; + + usdhc_wr(USDHC_BLK_ATT, (1UL << 16) | SD_BLOCK_SIZE); + if (usdhc_cmd(MMC_CMD8_SEND_EXT_CSD, 0, 0, 0, 1, 0, 1) != 0) + return -1; + + for (n = 0; n < USDHC_TIMEOUT_LOOPS; n++) { + st = usdhc_rd(USDHC_INT_STATUS); + if (st & INT_DATA_ERRS) { + usdhc_wr(USDHC_INT_STATUS, INT_DATA_ERRS); + usdhc_reset(SYS_CTRL_RSTC | SYS_CTRL_RSTD); + return -1; + } + if (usdhc_rd(USDHC_PRES_STATE) & PRES_BREN) + break; + } + if (n == USDHC_TIMEOUT_LOOPS) + return -1; + usdhc_wr(USDHC_INT_STATUS, INT_BRR); + for (i = 0; i < SD_BLOCK_SIZE / 4; i++) + out[i] = usdhc_rd(USDHC_DATA_BUFF_ACC); + if (usdhc_wait_set(USDHC_INT_STATUS, INT_TC) != 0) + return -1; + usdhc_wr(USDHC_INT_STATUS, INT_TC); + return 0; +} + +/* Point ordinary reads at the user area (0), boot0 (1) or boot1 (2). The boot + * partitions are where the SoC's own boot containers live, so this is what + * lets wolfBoot read them. */ +int imx95_emmc_select_partition(int part) +{ + uint8_t cfg; + + if (!card_ready) + return -1; + if (part < 0 || part > 2) + return -1; + + cfg = (uint8_t)((emmc_part_config & (uint8_t)~EXT_CSD_PART_ACCESS_MASK) | + (uint8_t)part); + if (emmc_switch(EXT_CSD_PARTITION_CONFIG, cfg) != 0) { + wolfBoot_printf("emmc: partition switch to %d failed\n", part); + return -1; + } + emmc_part_config = cfg; + return 0; +} + +/* Which boot partition the SoC loads from, 1 for boot0 or 2 for boot1, taken + * from the enable field the ROM itself reads. 0 means none is enabled. */ +int imx95_emmc_boot_partition(void) +{ + if (!card_ready) + return -1; + return (int)((emmc_part_config >> 3) & 0x07U); +} + +static int emmc_card_init(void) +{ + static uint8_t ext_csd[SD_BLOCK_SIZE] __attribute__((aligned(4))); + uint32_t rsp; + uint32_t n; + int ret; + + card_rca = 1; /* the host assigns it on eMMC, unlike SD */ + card_high_cap = 0; + + usdhc_reset(SYS_CTRL_RSTA | SYS_CTRL_RSTT); + usdhc_vendor_reset(); + usdhc_wr(USDHC_PROT_CTRL, PROT_CTRL_EMODE_LE | PROT_CTRL_DTW_1BIT | + PROT_CTRL_CDTL | PROT_CTRL_CDSS); + usdhc_wr(USDHC_WTMK_LVL, (128UL << 16) | 128UL); + usdhc_wr(USDHC_INT_STATUS_EN, 0xFFFFFFFFUL); + usdhc_wr(USDHC_INT_SIGNAL_EN, 0); + + usdhc_set_clock(USDHC_CLK_ID); + usdhc_init_clocks(); + + DISK_DBG("emmc: SYS_CTRL=%08x PRES=%08x VEND=%08x\n", + (unsigned)usdhc_rd(USDHC_SYS_CTRL), (unsigned)usdhc_rd(USDHC_PRES_STATE), + (unsigned)usdhc_rd(USDHC_VEND_SPEC)); + + (void)usdhc_cmd(SD_CMD0_GO_IDLE, 0, 0, 0, 0, 0, 0); + + /* CMD1 rather than ACMD41, and with the sector-address bit set: parts this + * size are always block addressed, and asking for byte addressing would be + * refused. R3 carries no CRC. */ + rsp = 0; + for (n = 0; n < 1000; n++) { + ret = usdhc_cmd(MMC_CMD1_SEND_OP_COND, 0x40FF8080UL, 0, 0, 0, 0, 0); + if (ret != 0) { + wolfBoot_printf("emmc: CMD1 failed (%d) PRES=%08x INT=%08x\n", + ret, (unsigned)usdhc_rd(USDHC_PRES_STATE), + (unsigned)usdhc_rd(USDHC_INT_STATUS)); + return -1; + } + rsp = usdhc_rd(USDHC_CMD_RSP0); + if (rsp & 0x80000000UL) + break; + } + if (!(rsp & 0x80000000UL)) { + wolfBoot_printf("emmc: card stuck busy in CMD1 (OCR 0x%08x)\n", + (unsigned)rsp); + return -1; + } + card_high_cap = (rsp & 0x40000000UL) ? 1 : 0; + + ret = usdhc_cmd(SD_CMD2_ALL_SEND_CID, 0, 1, 0, 0, 0, 0); + if (ret != 0) { + wolfBoot_printf("emmc: CMD2 failed (%d)\n", ret); + return -1; + } + /* The host chooses the address on eMMC and tells the card. */ + ret = usdhc_cmd(SD_CMD3_SEND_REL_ADDR, (uint32_t)card_rca << 16, + 0, 0, 0, 0, 1); + if (ret != 0) { + wolfBoot_printf("emmc: CMD3 failed (%d)\n", ret); + return -1; + } + (void)usdhc_cmd(SD_CMD9_SEND_CSD, (uint32_t)card_rca << 16, 1, 0, 0, 0, 0); + ret = usdhc_cmd(SD_CMD7_SELECT, (uint32_t)card_rca << 16, 0, 1, 0, 0, 1); + if (ret != 0) { + wolfBoot_printf("emmc: CMD7 failed (%d)\n", ret); + return -1; + } + + usdhc_set_clock(USDHC_CLK_25MHZ); + + /* 4-bit: the SMARC carrier routes four eMMC data lines, and the wider bus + * is a device-side setting the card has to be told about. */ + if (emmc_switch(EXT_CSD_BUS_WIDTH, EXT_CSD_BUS_WIDTH_4BIT) == 0) { + usdhc_wr(USDHC_PROT_CTRL, + (usdhc_rd(USDHC_PROT_CTRL) & ~PROT_CTRL_DTW_MASK) | PROT_CTRL_DTW_4BIT); + } + + (void)usdhc_cmd(SD_CMD16_SET_BLOCKLEN, SD_BLOCK_SIZE, 0, 0, 0, 0, 1); + + if (emmc_read_ext_csd(ext_csd) != 0) { + wolfBoot_printf("emmc: EXT_CSD read failed\n"); + return -1; + } + emmc_part_config = ext_csd[EXT_CSD_PARTITION_CONFIG]; + + wolfBoot_printf("emmc: ready, rca=0x%x part_config=0x%02x\n", + (unsigned)card_rca, (unsigned)emmc_part_config); + return 0; +} + +#endif /* DISK_EMMC || IMX95_EMMC_PROBE */ + +static int sd_card_init(void) +{ + uint32_t rsp; + uint32_t n; + int ret; + + card_rca = 0; + card_high_cap = 0; + + usdhc_reset(SYS_CTRL_RSTA | SYS_CTRL_RSTT); + usdhc_vendor_reset(); + + /* CDTL+CDSS force card-present: boards routing CD to a GPIO leave the + * dedicated CD pad floating, so CINST never sets. The card answering + * CMD8/ACMD41 is the real presence check. */ + usdhc_wr(USDHC_PROT_CTRL, PROT_CTRL_EMODE_LE | PROT_CTRL_DTW_1BIT | + PROT_CTRL_CDTL | PROT_CTRL_CDSS); + /* PIO watermark: one 512-byte block = 128 words on both sides */ + usdhc_wr(USDHC_WTMK_LVL, (128UL << 16) | 128UL); + /* Enable status bits (polled; signals stay off) */ + usdhc_wr(USDHC_INT_STATUS_EN, 0xFFFFFFFFUL); + usdhc_wr(USDHC_INT_SIGNAL_EN, 0); + + usdhc_set_clock(USDHC_CLK_ID); + + DISK_DBG("usdhc: after clk SYS_CTRL=%08x PRES=%08x (SDSTB=%d CINST=%d) CAP=%08x\n", + (unsigned)usdhc_rd(USDHC_SYS_CTRL), (unsigned)usdhc_rd(USDHC_PRES_STATE), + (int)((usdhc_rd(USDHC_PRES_STATE) >> 3) & 1U), + (int)((usdhc_rd(USDHC_PRES_STATE) >> 16) & 1U), + (unsigned)usdhc_rd(USDHC_HOST_CTRL_CAP)); + + /* Emit the 80 startup clocks a cold card needs before CMD0/CMD8. */ + usdhc_init_clocks(); + + /* CMD0: idle */ + (void)usdhc_cmd(SD_CMD0_GO_IDLE, 0, 0, 0, 0, 0, 0); + + /* CMD8 voltage check (2.7-3.6V, pattern 0xAA). SD v1 not supported. */ + ret = usdhc_cmd(SD_CMD8_SEND_IF_COND, 0x1AA, 0, 0, 0, 0, 1); + DISK_DBG("usdhc: CMD8 -> %d RSP0=%08x\n", ret, (unsigned)usdhc_rd(USDHC_CMD_RSP0)); + if (ret != 0) { + wolfBoot_printf("usdhc: CMD8 failed (%d) - SD v1 card?\n", ret); + return -1; + } + if ((usdhc_rd(USDHC_CMD_RSP0) & 0xFF) != 0xAA) { + wolfBoot_printf("usdhc: CMD8 pattern mismatch\n"); + return -1; + } + + /* ACMD41 with HCS until the card leaves busy (bit 31 set). */ + for (n = 0; n < 1000; n++) { + ret = usdhc_cmd(SD_CMD55_APP_CMD, 0, 0, 0, 0, 0, 1); + if (ret != 0) + return -1; + /* ACMD41 response (R3) has no CRC; disable the checks */ + ret = usdhc_cmd(SD_ACMD41_OP_COND, 0x40300000UL, 0, 0, 0, 0, 0); + if (ret != 0) + return -1; + rsp = usdhc_rd(USDHC_CMD_RSP0); + if (rsp & 0x80000000UL) + break; + } + DISK_DBG("usdhc: ACMD41 done after %u loops, OCR=%08x\n", + (unsigned)n, (unsigned)rsp); + if (!(rsp & 0x80000000UL)) { + wolfBoot_printf("usdhc: card stuck busy in ACMD41\n"); + return -1; + } + card_high_cap = (rsp & 0x40000000UL) ? 1 : 0; + + /* CMD2 (CID, R2/136) then CMD3 (RCA) */ + if (usdhc_cmd(SD_CMD2_ALL_SEND_CID, 0, 1, 0, 0, 0, 0) != 0) + return -1; + if (usdhc_cmd(SD_CMD3_SEND_REL_ADDR, 0, 0, 0, 0, 0, 1) != 0) + return -1; + card_rca = (int)(usdhc_rd(USDHC_CMD_RSP0) >> 16); + + /* CMD9 (CSD): unparsed, but some cards require it before select */ + (void)usdhc_cmd(SD_CMD9_SEND_CSD, (uint32_t)card_rca << 16, 1, 0, 0, 0, 0); + + /* CMD7: select the card (R1b) */ + if (usdhc_cmd(SD_CMD7_SELECT, (uint32_t)card_rca << 16, 0, 1, 0, 0, 1) + != 0) + return -1; + + /* Transfer clock, then 4-bit bus (ACMD6 arg 2) */ + usdhc_set_clock(USDHC_CLK_25MHZ); + if (usdhc_cmd(SD_CMD55_APP_CMD, (uint32_t)card_rca << 16, 0, 0, 0, 0, 1) + != 0) + return -1; + if (usdhc_cmd(SD_ACMD6_SET_BUS_WIDTH, 2, 0, 0, 0, 0, 1) != 0) + return -1; + usdhc_wr(USDHC_PROT_CTRL, + (usdhc_rd(USDHC_PROT_CTRL) & ~PROT_CTRL_DTW_MASK) | PROT_CTRL_DTW_4BIT); + /* keep CDTL/CDSS asserted; RSTA is the only thing that clears them */ + + /* CMD16: 512-byte blocks (no-op for high capacity, harmless) */ + (void)usdhc_cmd(SD_CMD16_SET_BLOCKLEN, SD_BLOCK_SIZE, 0, 0, 0, 0, 1); + + /* High speed doubles the ceiling to 50 MHz. Switch the controller only + * after the card confirms CMD6, because driving 50 MHz at a card still in + * default speed corrupts data silently; one that declines stays at 25. */ + if (sd_switch_high_speed() == 0) { + usdhc_set_clock(USDHC_CLK_50MHZ); + wolfBoot_printf("usdhc: high speed (50 MHz)\n"); + } + + wolfBoot_printf("usdhc: SD card ready, rca=0x%x %s\n", + (unsigned)card_rca, card_high_cap ? "(high capacity)" : ""); + return 0; +} + +/* Read 'blocks' full blocks starting at 'lba' into buf via PIO. */ +/* Destination must be 4-byte aligned: PIO drains the FIFO as words and this + * stage is built -mstrict-align, where unaligned stores fault. disk_read() + * routes unaligned callers through the bounce block. */ +static int sd_read_blocks(uint32_t lba, uint32_t blocks, uint8_t *buf) +{ + uint32_t arg; + uint32_t *out = (uint32_t*)(void*)buf; + uint32_t b, w, st, n; + int multi = (blocks > 1); + int cmd = multi ? SD_CMD18_READ_MULTIPLE : SD_CMD17_READ_SINGLE; + + /* A standard-capacity card takes a byte address, so its last addressable + * block is the one whose byte offset still fits the 32-bit argument. + * Refuse rather than wrap into an unrelated sector. */ + if (!card_high_cap) { + if (lba > (0xFFFFFFFFUL / SD_BLOCK_SIZE)) + return -1; + arg = lba * SD_BLOCK_SIZE; + } + else { + arg = lba; + } + + usdhc_wr(USDHC_BLK_ATT, ((uint32_t)blocks << 16) | SD_BLOCK_SIZE); + + if (usdhc_cmd((uint32_t)cmd, arg, 0, 0, 1, multi, 1) != 0) + return -1; + + for (b = 0; b < blocks; b++) { + /* Wait for one block in the FIFO */ + for (n = 0; n < USDHC_TIMEOUT_LOOPS; n++) { + st = usdhc_rd(USDHC_INT_STATUS); + if (st & INT_DATA_ERRS) { + usdhc_wr(USDHC_INT_STATUS, INT_DATA_ERRS); + usdhc_reset(SYS_CTRL_RSTC | SYS_CTRL_RSTD); + return -1; + } + if (usdhc_rd(USDHC_PRES_STATE) & PRES_BREN) + break; + } + if (n == USDHC_TIMEOUT_LOOPS) { + usdhc_reset(SYS_CTRL_RSTC | SYS_CTRL_RSTD); + return -1; + } + usdhc_wr(USDHC_INT_STATUS, INT_BRR); + for (w = 0; w < SD_BLOCK_SIZE / 4; w++) + *out++ = usdhc_rd(USDHC_DATA_BUFF_ACC); + } + + /* Transfer complete (auto CMD12 covers the multi-block stop) */ + if (usdhc_wait_set(USDHC_INT_STATUS, INT_TC) != 0) { + usdhc_reset(SYS_CTRL_RSTC | SYS_CTRL_RSTD); + return -1; + } + usdhc_wr(USDHC_INT_STATUS, INT_TC); + return 0; +} + +/* --- wolfBoot disk interface --------------------------------------------- */ + +#ifdef IMX95_COLD_PROBE +/* Cold-state prober: report exactly what a cold uSDHC2 needs, one marker + * line before each access so a TRDC abort (caught by the debug vectors) + * names the blocked register. Debug builds only. */ +static void probe_delay(uint32_t loops) +{ + volatile uint32_t n = loops; + while (n-- > 0U) { __asm__ volatile("nop"); } +} + +void imx95_usdhc_cold_probe(void) +{ + static const uint32_t mux_off[6] = + { 0x1a4, 0x1a8, 0x1ac, 0x1b0, 0x1b4, 0x1b8 }; + static const uint32_t cfg_off[6] = + { 0x3a8, 0x3ac, 0x3b0, 0x3b4, 0x3b8, 0x3bc }; + uint32_t i, v; + + wolfBoot_printf("probe: SYS_CTRL=%08x PRES=%08x CAP=%08x\n", + (unsigned)usdhc_rd(USDHC_SYS_CTRL), (unsigned)usdhc_rd(USDHC_PRES_STATE), + (unsigned)usdhc_rd(USDHC_HOST_CTRL_CAP)); + usdhc_reset(SYS_CTRL_RSTA); + usdhc_set_clock(USDHC_CLK_ID); + wolfBoot_printf("probe: after clk PRES=%08x (SDSTB=%d)\n", + (unsigned)usdhc_rd(USDHC_PRES_STATE), + (int)((usdhc_rd(USDHC_PRES_STATE) >> 3) & 1U)); + + wolfBoot_printf("probe: reading IOMUXC\n"); + for (i = 0; i < 6U; i++) { + v = *(volatile uint32_t*)(IMX95_IOMUXC_BASE + mux_off[i]); + wolfBoot_printf("probe: mux[%08x]=%08x cfg=%08x\n", + (unsigned)mux_off[i], (unsigned)v, + (unsigned)*(volatile uint32_t*)(IMX95_IOMUXC_BASE + cfg_off[i])); + } + + wolfBoot_printf("probe: writing IOMUXC (mode0, conf 0x138e/0x158e)\n"); + for (i = 0; i < 6U; i++) { + *(volatile uint32_t*)(IMX95_IOMUXC_BASE + mux_off[i]) = 0U; + *(volatile uint32_t*)(IMX95_IOMUXC_BASE + cfg_off[i]) = + (i == 0U) ? 0x158eU : 0x138eU; /* CLK gets the stronger conf */ + } + wolfBoot_printf("probe: mux write ok, readback mux[0]=%08x\n", + (unsigned)*(volatile uint32_t*)(IMX95_IOMUXC_BASE + 0x1a4)); + + wolfBoot_printf("probe: reading GPIO3\n"); + wolfBoot_printf("probe: PDOR=%08x PDDR=%08x\n", + (unsigned)*(volatile uint32_t*)(IMX95_GPIO3_BASE + 0x40), + (unsigned)*(volatile uint32_t*)(IMX95_GPIO3_BASE + 0x54)); + wolfBoot_printf("probe: card power on (GPIO3.7)\n"); + *(volatile uint32_t*)(IMX95_GPIO3_BASE + 0x54) |= (1U << 7); /* PDDR out */ + *(volatile uint32_t*)(IMX95_GPIO3_BASE + 0x44) = (1U << 7); /* PSOR high */ + probe_delay(20000000U); /* > startup-delay-us at ~1.8 GHz */ + + wolfBoot_printf("probe: retry enumeration\n"); + card_ready = 0; + if (sd_card_init() == 0) { + wolfBoot_printf("probe: SD ENUMERATED after cold init\n"); + card_ready = 1; + } + else { + wolfBoot_printf("probe: still failing after pinmux+power\n"); + } +} +#endif /* IMX95_COLD_PROBE */ + +#ifdef IMX95_EMMC_PROBE +/* Block-granular read of the currently selected eMMC partition, in the shape + * the container parser asks for. */ +static int emmc_ahab_read(void *ctx, uint32_t off, uint32_t len, void *buf) +{ + (void)ctx; + if ((off % SD_BLOCK_SIZE) != 0U || (len % SD_BLOCK_SIZE) != 0U) + return -1; + return sd_read_blocks(off / SD_BLOCK_SIZE, len / SD_BLOCK_SIZE, + (uint8_t *)buf); +} + +/* Walk the SoC's boot containers in an eMMC boot partition and report them. Runs + * inside an SD-booting image and restores the SD selection after, so a failure + * cannot disturb the boot in progress. Nothing is loaded. */ +void imx95_emmc_probe(void) +{ + static struct imx95_ahab_container ctnr; + uintptr_t saved_base = usdhc_base; + int saved_ready = card_ready; + int saved_rca = card_rca; + int saved_cap = card_high_cap; + /* emmc_card_init() and imx95_emmc_select_partition() below both rewrite + * this, so it belongs in the same save/restore as the rest. */ + uint8_t saved_part_cfg = emmc_part_config; + uint32_t off, i; + int part, n; + + usdhc_base = IMX95_USDHC1_BASE; + card_ready = 0; + + if (emmc_card_init() != 0) { + wolfBoot_printf("emmc probe: init failed\n"); + goto restore; + } + card_ready = 1; + + for (part = 1; part <= 2; part++) { + if (imx95_emmc_select_partition(part) != 0) + continue; + off = IMX95_AHAB_MMC_OFFSET; + for (n = 0; n < 2; n++) { + if (imx95_ahab_parse(&ctnr, off, emmc_ahab_read, NULL) != 0) { + wolfBoot_printf("emmc probe: boot%d no container at 0x%x\n", + part - 1, (unsigned)off); + break; + } + wolfBoot_printf("emmc probe: boot%d ctnr%d @0x%x size=0x%x images=%u\n", + part - 1, n, (unsigned)ctnr.base, (unsigned)ctnr.size, + (unsigned)ctnr.count); + for (i = 0; i < ctnr.count; i++) { + wolfBoot_printf(" img%u +0x%x size=0x%x dst=0x%x core=%u\n", + (unsigned)i, (unsigned)ctnr.img[i].offset, + (unsigned)ctnr.img[i].size, + (unsigned)ctnr.img[i].dst, + (unsigned)IMX95_AHAB_CORE(ctnr.img[i].flags)); + } + if (imx95_ahab_next(&ctnr, &off) != 0) { + wolfBoot_printf(" no container follows this one\n"); + break; + } + } + } + (void)imx95_emmc_select_partition(0); + +restore: + usdhc_base = saved_base; + card_ready = saved_ready; + card_rca = saved_rca; + card_high_cap = saved_cap; + emmc_part_config = saved_part_cfg; +} +#endif /* IMX95_EMMC_PROBE */ + +int disk_init(int drv) +{ + (void)drv; + if (card_ready) + return 0; +#if defined(IMX95_SCMI_COLD_INIT) && defined(DISK_SDCARD) + /* Brings up uSDHC2's clock, pads and card power. uSDHC1 (eMMC) would need + * its own equivalent to run from a cold power-on; today an eMMC build + * relies on the stage ahead of wolfBoot having initialized it. */ + if (imx95_usdhc2_cold_init() != 0) + return -1; +#endif +#ifdef IMX95_COLD_PROBE + imx95_usdhc_cold_probe(); + if (card_ready) + return 0; +#endif +#ifdef IMX95_EMMC_PROBE + imx95_emmc_probe(); +#endif +#ifdef DISK_EMMC + if (emmc_card_init() != 0) + return -1; +#else + if (sd_card_init() != 0) + return -1; +#endif + card_ready = 1; + return 0; +} + +/* Byte-addressed (src/disk.c passes byte offsets); unaligned head/tail go + * through a bounce block. */ +int disk_read(int drv, uint64_t start, uint32_t count, uint8_t *buf) +{ + static uint8_t bounce[SD_BLOCK_SIZE] __attribute__((aligned(4))); + uint32_t lba, off, chunk, blocks; + uint32_t done = 0; + uint32_t i; + uint64_t lba64; + + (void)drv; + if (!card_ready) + return -1; + /* The byte count comes back through an int, so a request too large to + * report must be refused rather than answered with a negative length. */ + if (count > (uint32_t)INT_MAX) + return -1; + + while (done < count) { + /* The card command argument is 32-bit. A partition table that puts an + * image past that limit must fail the read, not silently wrap round to + * an in-range sector and hand back the wrong bytes. */ + lba64 = (start + done) / SD_BLOCK_SIZE; + if (lba64 > 0xFFFFFFFFULL) + return -1; + lba = (uint32_t)lba64; + off = (uint32_t)((start + done) % SD_BLOCK_SIZE); + + if (off != 0 || (count - done) < SD_BLOCK_SIZE || + (((uintptr_t)buf + done) & 3U) != 0U) { + /* Partial block through the bounce buffer */ + chunk = SD_BLOCK_SIZE - off; + if (chunk > (count - done)) + chunk = count - done; + if (sd_read_blocks(lba, 1, bounce) != 0) + return -1; + for (i = 0; i < chunk; i++) + buf[done + i] = bounce[off + i]; + done += chunk; + } + else { + /* Whole blocks straight into the destination */ + blocks = (count - done) / SD_BLOCK_SIZE; + if (blocks > SD_MAX_BLOCKS) + blocks = SD_MAX_BLOCKS; + if (sd_read_blocks(lba, blocks, buf + done) != 0) + return -1; + done += blocks * SD_BLOCK_SIZE; + } + } + return (int)done; +} + +/* Not implemented: nothing in the boot path writes. */ +int disk_write(int drv, uint64_t start, uint32_t count, const uint8_t *buf) +{ + (void)drv; (void)start; (void)count; (void)buf; + return -1; +} + +/* Quiesce so the OS driver starts from reset state. */ +void disk_close(int drv) +{ + (void)drv; + if (card_ready) { + usdhc_reset(SYS_CTRL_RSTC | SYS_CTRL_RSTD); + card_ready = 0; + } +} + +#endif /* DISK_SDCARD || DISK_EMMC */ diff --git a/hal/imx95_a55.h b/hal/imx95_a55.h new file mode 100644 index 0000000000..4ba32634c1 --- /dev/null +++ b/hal/imx95_a55.h @@ -0,0 +1,148 @@ +/* imx95_a55.h + * + * Hardware definitions for the Cortex-A55 cluster on the NXP i.MX95. + * + * 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 + */ + +/* wolfBoot as BL33, entered by BL31. Included from assembly, so integer + * expressions only, and constants used by boot_aarch64_start.S carry no UL - + * not every assembler accepts it. Addresses from a Toradex SMARC iMX95. */ + +#ifndef _IMX95_A55_H_ +#define _IMX95_A55_H_ + +/* Simple startup stub: DDR and EL3 setup already done by OEI/BL31 (same + * model as tegra234). */ +#define USE_BUILTIN_STARTUP +#define USE_SIMPLE_STARTUP + +/* Boot chain: ROM -> ELE -> SM -> OEI -> SPL [c1] -> BL31 -> OP-TEE -> + * wolfBoot [c2]. Load addresses fixed by the vendor imx-mkimage layout. */ +#define IMX95_BL31_BASE 0x8A200000UL +#define IMX95_OPTEE_BASE 0x8C000000UL +#define IMX95_OPTEE_SIZE 0x02000000UL /* 32 MiB */ +#define IMX95_OPTEE_SHM_BASE 0x8E000000UL +#define IMX95_OPTEE_SHM_SIZE 0x00200000UL /* 2 MiB */ +#define IMX95_BL33_BASE 0x90200000UL + +/* SCTLR_ELx.C - data cache enable. */ +#define SCTLR_C (1UL << 2) + +/* DRAM bank 0; below 0x90000000 is carved out for M7/BL31/OP-TEE. */ +#define IMX95_DRAM_BASE 0x90000000UL +#define IMX95_DRAM_SIZE 0x70000000UL /* 1.75 GiB, bank 0 */ +#define IMX95_DRAM_END (IMX95_DRAM_BASE + IMX95_DRAM_SIZE) + +/* All of DDR bank 0 including the low carveouts. IMX95_DRAM_BASE is what wolfBoot + * may allocate; container images land below it (BL31, OP-TEE), so a stage 1 + * bounds image destinations against this wider window. */ +#define IMX95_DDR_BASE 0x80000000UL +#define IMX95_DDR_SIZE (IMX95_DRAM_END - IMX95_DDR_BASE) + +/* Where the container is staged for the ELE, which reads over its own master port + * and requires DDR, so it cannot be authenticated in the OCRAM stage 1 parsed it + * in. NXP's IMG_CONTAINER_BASE; nothing may load over it until released. */ +#define IMX95_AHAB_STAGE_BASE 0x90000000UL +#define IMX95_AHAB_STAGE_SIZE 0x00010000UL /* 64 KiB, the header's max span */ + +/* Reserved windows inside bank 0 that staging must not overlap. */ +#define IMX95_M7_DDR_BASE 0x80000000UL +#define IMX95_M7_DDR_SIZE 0x01000000UL /* 16 MiB */ +#define IMX95_ELE_SHM_BASE 0x9C300000UL +#define IMX95_ELE_SHM_SIZE 0x00100000UL /* 1 MiB */ +#define IMX95_VPU_BOOT_BASE 0xA0000000UL +#define IMX95_VPU_BOOT_SIZE 0x00100000UL /* 1 MiB */ + +/* Optional DDR log ring (IMX95_LOG_RING): mirrors the console so the boot log + * survives the handoff to Linux. Same layout as the M7 console in + * hal/imx95_m7.h, in the M7 carveout tail clear of its pages. */ +#ifndef IMX95_LOG_RING_BASE +#define IMX95_LOG_RING_BASE 0x80F20000UL +#endif +#define IMX95_LOG_RING_HDR 16UL +#define IMX95_LOG_RING_REGION 0x00010000UL /* 64 KiB page */ +#define IMX95_LOG_RING_SIZE (IMX95_LOG_RING_REGION - IMX95_LOG_RING_HDR) +#define IMX95_LOG_RING_MAGIC 0x4E4F4357UL /* "WCON" */ + +/* Console: LPUART1 (ttyLP1), already clocked/pinmuxed at 115200 by the + * prior stages; BAUD is not reprogrammed (its ref clock is SM-owned). + * NXP LPUART v2 register block (same layout as hal/s32k1xx.h). */ +#define IMX95_LPUART1_BASE 0x44380000 + +#define LPUART_BAUD_OFF 0x10 +#define LPUART_STAT_OFF 0x14 +#define LPUART_CTRL_OFF 0x18UL +#define LPUART_DATA_OFF 0x1C + +#define LPUART_STAT_TDRE (1UL << 23) /* Transmit Data Register Empty */ +#define LPUART_STAT_TC (1UL << 22) /* Transmission Complete */ +#define LPUART_CTRL_TE (1UL << 19) /* Transmitter Enable */ + +/* Stage 1 programs the port itself: 24 MHz reference, oversample 16. */ +#define LPUART_BAUD_OSR 16UL +#define LPUART_BAUD_SBR 13UL + +/* Storage: uSDHC1 = module eMMC, uSDHC2 = carrier SD slot. */ +#define IMX95_USDHC1_BASE 0x42850000UL /* eMMC */ +#define IMX95_USDHC2_BASE 0x42860000UL /* carrier SD */ + +#ifndef __ASSEMBLER__ +/* Point ordinary reads at the eMMC user area (0), boot0 (1) or boot1 (2). + * The SoC's own boot containers live in the boot partitions. */ +int imx95_emmc_select_partition(int part); +int imx95_emmc_boot_partition(void); +#endif + +/* The /memory banks wolfBoot patches into the DTB. Defaults are the Toradex + * SMARC iMX95 8 GiB module; override for a different DRAM population. */ +#ifndef IMX95_DRAM_BANK0_BASE +#define IMX95_DRAM_BANK0_BASE 0x90000000ULL +#endif +#ifndef IMX95_DRAM_BANK0_SIZE +#define IMX95_DRAM_BANK0_SIZE 0x70000000ULL +#endif +#ifndef IMX95_DRAM_BANK1_BASE +#define IMX95_DRAM_BANK1_BASE 0x100000000ULL +#endif +#ifndef IMX95_DRAM_BANK1_SIZE +#define IMX95_DRAM_BANK1_SIZE 0x180000000ULL +#endif + +/* SoC block bases, shared rather than repeated per driver - the GPIO block below + * was previously spelled three times under two names. Register offsets, bit + * fields and unlock keys stay with their driver. */ +#define IMX95_WDG3_BASE 0x42490000UL +#define IMX95_WDG4_BASE 0x424A0000UL +#define IMX95_GPIO2_BASE 0x43810000UL +#define IMX95_GPIO3_BASE 0x43820000UL +#define IMX95_GPIO4_BASE 0x43840000UL +#define IMX95_GPIO5_BASE 0x43850000UL +#define IMX95_IOMUXC_BASE 0x443C0000UL +#define IMX95_MU2_BASE 0x445B0000UL /* SCMI transport to the SM */ +#define IMX95_ELE_MU_BASE 0x47530000UL /* ELE MU1, the slot SPL uses */ +#define IMX95_FSB_BASE 0x47510000UL /* fuse shadow block */ +#define IMX95_SMMU_BASE 0x490D0000UL + +/* M7 TCMs through the system aperture (M7 links for its core view). */ +#define IMX95_M7_ITCM_SYS 0x203C0000UL /* M7 core view 0x00000000 */ +#define IMX95_M7_DTCM_SYS 0x20400000UL /* M7 core view 0x20000000 */ +#define IMX95_M7_TCM_SIZE 0x00040000UL /* 256 KiB each */ + +#endif /* _IMX95_A55_H_ */ diff --git a/hal/imx95_a55.ld b/hal/imx95_a55.ld new file mode 100644 index 0000000000..0a389ec083 --- /dev/null +++ b/hal/imx95_a55.ld @@ -0,0 +1,94 @@ +/* imx95_a55.ld - wolfBoot as BL33, DRAM-resident (shape of tegra234.ld). + * Keep addresses in sync with hal/imx95_a55.h. */ +MEMORY +{ + /* BL31 enters at IMX95_BL33_BASE (not position independent). LENGTH + * bounds wolfBoot's footprint so growth is a link error, not silent + * overlap with the staging window above. */ + DDR_MEM(rwx): ORIGIN = 0x90200000, LENGTH = 0x200000 +} +ENTRY(_vector_table); + +SECTIONS +{ + .text : + { + _start_text = .; + KEEP(*(.boot*)) + *(.text*) + *(.rodata*) + *(.note.*) + . = ALIGN(4); + _end_text = .; + } > DDR_MEM + + .edidx : + { + . = ALIGN(4); + *(.ARM.exidx*) + } > DDR_MEM + + .data : + { + PROVIDE(_stored_data = .); /* XIP builds copy .data from here */ + _start_data = .; + KEEP(*(.data*)) + . = ALIGN(4); + KEEP(*(.ramcode)) + . = ALIGN(4); + _end_data = .; + } > DDR_MEM + + /* Translation tables for the EL2 identity map (src/boot_aarch64_start.S). + * 4 KiB alignment is architectural: TTBR0_EL2 ignores the low bits. */ + .mmu_tbl0 (ALIGN(4096)) : + { + __mmu_tbl0_start = .; + *(.mmu_tbl0) + __mmu_tbl0_end = .; + } > DDR_MEM + + .mmu_tbl1 (ALIGN(4096)) : + { + __mmu_tbl1_start = .; + *(.mmu_tbl1) + __mmu_tbl1_end = .; + } > DDR_MEM + + .mmu_tbl2 (ALIGN(4096)) : + { + __mmu_tbl2_start = .; + *(.mmu_tbl2) + __mmu_tbl2_end = .; + } > DDR_MEM + + .bss (NOLOAD) : + { + _start_bss = .; + __bss_start__ = .; + *(.bss*) + *(COMMON) + . = ALIGN(4); + _end_bss = .; + __bss_end__ = .; + } > DDR_MEM + + /* Dedicated stack: sp at the image base would grow down into OP-TEE, + * so startup uses END_STACK. 256 KiB covers ML-DSA-87 verify plus the + * on-stack header buffer. */ + .stack (NOLOAD) : + { + . = ALIGN(16); + _stack_bottom = .; + . += 0x40000; /* 256 KB */ + . = ALIGN(16); + END_STACK = .; + /* _end = top of the runtime footprint (above the stack); used by + * the RAM-boot overlap guard and the load-range check. */ + _end = .; + } > DDR_MEM + . = ALIGN(4); +} + +/* No partition symbols: WOLFBOOT_NO_PARTITIONS - addresses come from the + * HAL and config. */ diff --git a/hal/imx95_a55_stage1.c b/hal/imx95_a55_stage1.c new file mode 100644 index 0000000000..bdba300c9c --- /dev/null +++ b/hal/imx95_a55_stage1.c @@ -0,0 +1,747 @@ +/* imx95_a55_stage1.c + * + * wolfBoot stage 1 for the NXP i.MX95 Cortex-A55, in place of U-Boot SPL. + * + * 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 + */ + +/* The boot ROM loads this from AHAB container 0 into OCRAM and enters at EL3, + * replacing U-Boot SPL: find the next container and load BL31, OP-TEE and BL33. + * DDR is already up from the OEI. */ + +#include +#include +#include "printf.h" +#include "hal/imx95_a55.h" +#include "hal/imx95_ahab.h" + +/* Both bring-up switches below defeat authentication outright, so neither may be + * combined with it: PASSTHROUGH jumps to BL31 after a reported failure, turning + * "refuse to boot" into "boot the thing that just failed". */ +#if defined(IMX95_AHAB_AUTH) && defined(IMX95_STAGE1_PASSTHROUGH) +#error "IMX95_STAGE1_PASSTHROUGH enters BL31 after a failed load; it cannot be used with IMX95_AHAB_AUTH" +#endif +#if defined(IMX95_AHAB_AUTH) && defined(IMX95_STAGE1_ALLOW_SELF_OVERLAP) +#error "IMX95_STAGE1_ALLOW_SELF_OVERLAP skips an image the container asked to load; it cannot be used with IMX95_AHAB_AUTH" +#endif + +static inline uint32_t rd(uintptr_t a) { return *(volatile uint32_t *)a; } +static inline void wr(uintptr_t a, uint32_t v) { *(volatile uint32_t *)a = v; } + +/* Watchdogs 3 and 4 are left running by a warm reset out of Linux. */ +#define WDOG_CS 0x00 +#define WDOG_CNT 0x04 +#define WDOG_TOVAL 0x08 +#define WDOG_WIN 0x0C +#define WDOG_CS_EN (1UL << 7) +#define WDOG_CS_ULK (1UL << 11) +#define WDOG_CS_RCS (1UL << 10) +#define WDOG_REFRESH_WORD 0xB480A602UL +#define WDOG_UNLOCK_WORD 0xD928C520UL +#define WDOG_POLL_LOOPS 1000000UL + +/* GPIO2..5 are in the domains this core owns; a warm reset can leave pins + * driven, so the interrupt and data registers are cleared. */ +#define SMMU_CR0 0x20 +#define SMMU_CR0_ACK 0x24 +#define SMMU_GBPA 0x44 +#define SMMU_GBPA_UPDATE (1UL << 31) +#define SMMU_GBPA_SHCFG_IN (1UL << 12) +#define SMMU_POLL_LOOPS 1000000UL + +/* EdgeLock Enclave message unit. Stage 1 owns MU1; the later stages use MU3. */ +#define ELE_MU_PAR (IMX95_ELE_MU_BASE + 0x004) +#define ELE_MU_TCR (IMX95_ELE_MU_BASE + 0x120) +#define ELE_MU_TSR (IMX95_ELE_MU_BASE + 0x124) +#define ELE_MU_RCR (IMX95_ELE_MU_BASE + 0x128) +#define ELE_MU_RSR (IMX95_ELE_MU_BASE + 0x12C) +#define ELE_MU_TR(n) (IMX95_ELE_MU_BASE + 0x200 + ((n) * 4)) +#define ELE_MU_RR(n) (IMX95_ELE_MU_BASE + 0x280 + ((n) * 4)) +#define ELE_MU_SR (IMX95_ELE_MU_BASE + 0x00C) +#define ELE_MU_SR_RDR (1UL << 6) /* receive data ready */ + +/* A loop count is the wrong unit here: AHAB authentication hashes the whole + * image inside the enclave, so the wait is bounded in seconds, not iterations. */ +#define ELE_TIMEOUT_MS 5000UL + +/* Enough iterations to see a 24 MHz counter tick, and - for the no-clock + * fallback - a spin long enough to cover an enclave hashing an image. */ +#define ELE_TIMER_PROBE_LOOPS 1000UL +#define ELE_MU_POLL_LOOPS 100000000UL + +#define ELE_VERSION 0x06U +#define ELE_CMD_TAG 0x17U +#define ELE_RESP_TAG 0xE1U +#define ELE_START_RNG 0xA3U +#define ELE_OK 0xD6U +#define ELE_MAX_MSG 8U + +/* AHAB container authentication, exactly as U-Boot's SPL issues it. */ +#define ELE_CNTR_AUTH 0x87U +#define ELE_VERIFY_IMAGE 0x88U +#define ELE_RELEASE_CNTR 0x89U + +/* Indications worth naming: the rest are reported as a raw byte. */ +#define ELE_IND_NO_AUTH 0xEEU /* container carries no signature */ +#define ELE_IND_BAD_SIG 0xF0U +#define ELE_IND_LIFECYCLE 0xF2U +#define ELE_IND_BAD_CNTR 0xF7U +#define ELE_IND_RNG_STOPPED 0xB8U +#define ELE_IND_BAD_OP 0xC0U /* e.g. release with no context held */ + +/* An ELE call fails two ways. A clean negative reply leaves the mailbox usable, + * so a release can follow. A protocol failure - timeout, or a reply not matching + * what was sent - does not: a later message could consume the abandoned reply. */ +#define ELE_CALL_OK 0 +#define ELE_CALL_REJECTED (-1) +#define ELE_CALL_BROKEN (-2) + +/* Lifecycle, so every boot says which one it is rather than leaving an "AHAB + * OK" line to imply more than it means. */ +#define FSB_LC_OFFSET 0x414U +#define FSB_LC_MASK 0x3FFU +#define LC_OEM_OPEN 0x10U +#define LC_OEM_SWC 0x20U /* secure world closed */ +#define LC_OEM_CLOSED 0x40U + +extern int imx95_scmi_arm_max_clk(void); +extern int imx95_scmi_ddr_powered(void); +extern int imx95_scmi_uart_clk_init(void); +extern int imx95_usdhc1_cold_init(void); +extern int imx95_usdhc2_cold_init(void); +extern void uart_init(void); +extern int disk_init(int drv); +extern int disk_read(int drv, uint64_t start, uint32_t count, uint8_t *buf); + +/* Every wait in this stage is bounded: a bootloader that spins on a status bit + * that never arrives is harder to diagnose than one that reports and moves on, + * and there is no watchdog left to rescue it. */ +static int wdog_wait(uintptr_t base, uint32_t bit) +{ + uint32_t n; + + for (n = 0; n < WDOG_POLL_LOOPS; n++) { + if ((rd(base + WDOG_CS) & bit) != 0UL) + return 0; + } + return -1; +} + +static void wdog_disable(uintptr_t base) +{ + uint32_t cs = rd(base + WDOG_CS); + + if ((cs & WDOG_CS_EN) == 0UL) + return; + + wr(base + WDOG_CNT, WDOG_REFRESH_WORD); + if ((cs & WDOG_CS_ULK) == 0UL) { + wr(base + WDOG_CNT, WDOG_UNLOCK_WORD); + if (wdog_wait(base, WDOG_CS_ULK) != 0) { + wolfBoot_printf("stage1: watchdog 0x%x did not unlock\n", + (unsigned)base); + return; + } + } + wr(base + WDOG_WIN, 0); + wr(base + WDOG_TOVAL, 0x400); + wr(base + WDOG_CS, 0x2120); + if (wdog_wait(base, WDOG_CS_RCS) != 0) + wolfBoot_printf("stage1: watchdog 0x%x did not acknowledge\n", + (unsigned)base); +} + +static void gpio_reset(uintptr_t base) +{ + wr(base + 0x10, 0); + wr(base + 0x14, 0); + wr(base + 0x18, 0); + wr(base + 0x1C, 0); +} + +/* Linux can be reset while it is in the middle of disabling the SMMU, so this + * is done unconditionally rather than after a state check. */ +static int smmu_disable(void) +{ + uint32_t n; + + for (n = 0; n < SMMU_POLL_LOOPS; n++) { + if ((rd(IMX95_SMMU_BASE + SMMU_GBPA) & SMMU_GBPA_UPDATE) == 0UL) + break; + } + if (n == SMMU_POLL_LOOPS) + return -1; + + /* Use incoming SHCFG attributes */ + wr(IMX95_SMMU_BASE + SMMU_GBPA, SMMU_GBPA_SHCFG_IN | SMMU_GBPA_UPDATE); + for (n = 0; n < SMMU_POLL_LOOPS; n++) { + if ((rd(IMX95_SMMU_BASE + SMMU_GBPA) & SMMU_GBPA_UPDATE) == 0UL) + break; + } + if (n == SMMU_POLL_LOOPS) + return -1; + + wr(IMX95_SMMU_BASE + SMMU_CR0, 0); + for (n = 0; n < SMMU_POLL_LOOPS; n++) { + if (rd(IMX95_SMMU_BASE + SMMU_CR0_ACK) == 0UL) + break; + } + if (n == SMMU_POLL_LOOPS) + return -1; + + return 0; +} + +/* The generic timer is readable at EL3 regardless of MMU state, so it is the + * only clock available to bound a wait in this stage - but see timer_deadline() + * for why being readable does not mean it is counting. */ +static uint64_t timer_ticks(void) +{ + uint64_t v; + + __asm__ volatile("isb" ::: "memory"); + __asm__ volatile("mrs %0, cntpct_el0" : "=r"(v)); + return v; +} + +/* Returns 0 when the system counter is not advancing, which mu_wait() reads as + * "no clock". Nothing below BL31 guarantees the counter is enabled, and a + * deadline a stalled CNTPCT_EL0 never reaches is not a bound at all. */ +static uint64_t timer_deadline(uint32_t ms) +{ + uint64_t hz, t0; + uint32_t n; + + __asm__ volatile("mrs %0, cntfrq_el0" : "=r"(hz)); + if (hz == 0ULL) + hz = 24000000ULL; /* the SoC's fixed rate, if CNTFRQ is unset */ + + t0 = timer_ticks(); + for (n = 0; n < ELE_TIMER_PROBE_LOOPS; n++) { + if (timer_ticks() != t0) + return t0 + ((hz / 1000ULL) * (uint64_t)ms); + } + return 0; +} + +/* Wait for a status bit, bounded by wall time where there is a clock to read + * and by iterations where there is not. */ +static int mu_wait(uintptr_t reg, uint32_t bit, uint64_t deadline) +{ + uint32_t n; + + if (deadline == 0ULL) { + for (n = 0; n < ELE_MU_POLL_LOOPS; n++) { + if ((rd(reg) & bit) != 0UL) + return 0; + } + return -1; + } + do { + if ((rd(reg) & bit) != 0UL) + return 0; + } while (timer_ticks() < deadline); + return -1; +} + +/* One ELE message, a word per transmit register. msg[0] is version, word count, + * command and tag; the next reply word is the status. The reply's command is + * checked so an abandoned response is not misread; resp's [15:8] says why. */ +static int ele_call(uint32_t cmd, uint32_t *msg, uint32_t nwords, + uint32_t *resp) +{ + uint32_t tr_num, rr_num, i, n, reply_words; + uint64_t deadline; + + if (resp != NULL) + *resp = 0; + if (nwords == 0U || nwords > ELE_MAX_MSG) + return ELE_CALL_BROKEN; + + tr_num = rd(ELE_MU_PAR) & 0xFFU; + rr_num = (rd(ELE_MU_PAR) >> 8) & 0xFFU; + /* PAR is a hardware constant, but an unclocked MU reads back as all-ones + * and these index TR/RR, so a bad count must not reach a register offset. */ + if (tr_num == 0U || rr_num == 0U || + tr_num > ELE_MAX_MSG || rr_num > ELE_MAX_MSG) + return ELE_CALL_BROKEN; + + /* Drain anything a previous owner left behind, bounded in case the mailbox + * keeps re-asserting. */ + wr(ELE_MU_TCR, 0); + wr(ELE_MU_RCR, 0); + for (n = 0; n < ELE_MAX_MSG; n++) { + if ((rd(ELE_MU_SR) & ELE_MU_SR_RDR) == 0UL) + break; + for (i = 0; i < rr_num; i++) + (void)rd(ELE_MU_RR(i)); + } + + /* The enclave reads the staged container over its own master port, so the + * stores that placed it must be observable before the doorbell. */ + __asm__ volatile("dsb sy" ::: "memory"); + + deadline = timer_deadline(ELE_TIMEOUT_MS); + + for (i = 0; i < nwords; i++) { + if (mu_wait(ELE_MU_TSR, 1UL << (i % tr_num), deadline) != 0) + return ELE_CALL_BROKEN; + wr(ELE_MU_TR(i % tr_num), msg[i]); + } + + reply_words = nwords; + for (i = 0; i < reply_words; i++) { + if (mu_wait(ELE_MU_RSR, 1UL << (i % rr_num), deadline) != 0) + return ELE_CALL_BROKEN; + msg[i] = rd(ELE_MU_RR(i % rr_num)); + /* The reply header says how many words the ELE is actually sending. */ + if (i == 0U) { + reply_words = (msg[0] >> 8) & 0xFFU; + if (reply_words < 2U || reply_words > ELE_MAX_MSG) + return ELE_CALL_BROKEN; + /* A reply that is not this command's reply means the mailbox is + * out of step; reading further would compound the confusion. */ + if (((msg[0] >> 24) & 0xFFU) != ELE_RESP_TAG) + return ELE_CALL_BROKEN; + if (((msg[0] >> 16) & 0xFFU) != cmd) + return ELE_CALL_BROKEN; + } + } + + if (resp != NULL) + *resp = msg[1]; + if ((msg[1] & 0xFFU) != ELE_OK) + return ELE_CALL_REJECTED; + return ELE_CALL_OK; +} + +static void ele_report(const char *what, uint32_t resp) +{ + wolfBoot_printf("stage1: ELE %s failed, status 0x%x (indication 0x%x)\n", + what, (unsigned)resp, (unsigned)((resp >> 8) & 0xFFU)); +} + +/* Starts the ELE's random generator. Later stages - OP-TEE and Linux - expect + * it to be running, and only the first caller after reset may start it. */ +static int ele_start_rng(void) +{ + uint32_t msg[ELE_MAX_MSG]; + + msg[0] = (uint32_t)ELE_VERSION | ((uint32_t)1 << 8) | + ((uint32_t)ELE_START_RNG << 16) | ((uint32_t)ELE_CMD_TAG << 24); + return ele_call(ELE_START_RNG, msg, 1, NULL); +} + +#ifdef IMX95_AHAB_LIFECYCLE +/* Which lifecycle the part is in: on an open part AHAB reports success without + * enforcing a signature, so the distinction is the difference between + * integrity and authenticity. */ +static uint32_t ele_lifecycle(void) +{ + return rd(IMX95_FSB_BASE + FSB_LC_OFFSET) & FSB_LC_MASK; +} + +static void ele_report_lifecycle(uint32_t lc) +{ + const char *name = "unknown"; + + if (lc == LC_OEM_OPEN) + name = "OEM open - AHAB does not enforce signatures"; + else if (lc == LC_OEM_SWC) + name = "OEM secure world closed"; + else if (lc == LC_OEM_CLOSED) + name = "OEM closed"; + wolfBoot_printf("stage1: lifecycle 0x%x (%s)\n", (unsigned)lc, name); +} +#endif /* IMX95_AHAB_LIFECYCLE */ + +#ifdef IMX95_AHAB_AUTH +/* Hand the ELE the staged container. Everything it goes on to verify is taken + * from those bytes, not from the medium they came off. */ +static int ele_auth_container(uint64_t addr, uint32_t *resp) +{ + uint32_t msg[ELE_MAX_MSG]; + + msg[0] = (uint32_t)ELE_VERSION | ((uint32_t)3 << 8) | + ((uint32_t)ELE_CNTR_AUTH << 16) | ((uint32_t)ELE_CMD_TAG << 24); + msg[1] = (uint32_t)(addr >> 32); + msg[2] = (uint32_t)addr; + return ele_call(ELE_CNTR_AUTH, msg, 3, resp); +} + +/* Verify one image where it now sits. The payload is a bitmask, not an index, + * but it is sent one bit at a time so a failure names the image. */ +static int ele_verify_image(uint32_t index, uint32_t *resp) +{ + uint32_t msg[ELE_MAX_MSG]; + + msg[0] = (uint32_t)ELE_VERSION | ((uint32_t)2 << 8) | + ((uint32_t)ELE_VERIFY_IMAGE << 16) | ((uint32_t)ELE_CMD_TAG << 24); + msg[1] = 1UL << index; + return ele_call(ELE_VERIFY_IMAGE, msg, 2, resp); +} + +/* The enclave holds one authentication context at a time, so this has to run + * before the jump as well as on every failure that left the mailbox usable. */ +static int ele_release_container(uint32_t *resp) +{ + uint32_t msg[ELE_MAX_MSG]; + + msg[0] = (uint32_t)ELE_VERSION | ((uint32_t)1 << 8) | + ((uint32_t)ELE_RELEASE_CNTR << 16) | ((uint32_t)ELE_CMD_TAG << 24); + return ele_call(ELE_RELEASE_CNTR, msg, 1, resp); +} +#endif /* IMX95_AHAB_AUTH */ + +/* Everything SPL does between the console coming up and the container load. */ +int imx95_stage1_platform_init(void) +{ + int ret; + + wdog_disable(IMX95_WDG3_BASE); + wdog_disable(IMX95_WDG4_BASE); + + gpio_reset(IMX95_GPIO2_BASE); + gpio_reset(IMX95_GPIO3_BASE); + gpio_reset(IMX95_GPIO4_BASE); + gpio_reset(IMX95_GPIO5_BASE); + + if (smmu_disable() != 0) + wolfBoot_printf("stage1: SMMU disable timed out\n"); + + /* Without this the A55 cluster stays at its reset rate for the whole of + * the boot, which is the single biggest thing this stage controls. */ + if (imx95_scmi_arm_max_clk() != 0) + wolfBoot_printf("stage1: ARM clock set failed\n"); + + /* Only a definitive "off" is fatal. A query the System Manager refuses says + * nothing about DDR, and stopping the boot over it would be worse than the + * problem it is meant to catch. */ + ret = imx95_scmi_ddr_powered(); + if (ret == 0) { + wolfBoot_printf("stage1: DDR is powered off - OEI did not run\n"); + return -1; + } + if (ret < 0) + wolfBoot_printf("stage1: DDR power state unavailable (%d)\n", ret); + + if (ele_start_rng() != 0) { + wolfBoot_printf("stage1: ELE RNG start failed\n"); +#ifdef IMX95_AHAB_AUTH + /* Without it the enclave answers ELE_IND_RNG_STOPPED to the services + * that need it, authentication among them, so this stops being a + * warning once the boot depends on the ELE answering. */ + return -1; +#endif + } + + return 0; +} + +/* Bring up the controller the ROM booted from and point ordinary reads where it + * read the containers: on eMMC a boot partition, from the same EXT_CSD field the + * ROM used; on SD the raw area ahead of the partitions. */ +static int imx95_stage1_disk_init(void) +{ +#ifdef DISK_EMMC + int part; + + if (imx95_usdhc1_cold_init() != 0) + return -1; + if (disk_init(0) != 0) + return -1; + part = imx95_emmc_boot_partition(); + if (part < 1) { + wolfBoot_printf("stage1: no eMMC boot partition enabled\n"); + return -1; + } + wolfBoot_printf("stage1: eMMC boot%d\n", part - 1); + return imx95_emmc_select_partition(part); +#else + /* disk_init() runs the uSDHC2 cold-init itself when IMX95_SCMI_COLD_INIT is + * set, and repeating it would repeat the regulator settle delay. */ +#if !(defined(IMX95_SCMI_COLD_INIT) && defined(DISK_SDCARD)) + if (imx95_usdhc2_cold_init() != 0) + return -1; +#endif + return disk_init(0); +#endif +} + +/* Reads len bytes at byte offset off from the boot device. Both are multiples + * of the block size; disk_read() takes byte units and answers with how many it + * read, so a short read is a failure here. */ +static int stage1_read(void *ctx, uint32_t off, uint32_t len, void *buf) +{ + (void)ctx; + if (disk_read(0, (uint64_t)off, len, (uint8_t *)buf) != (int)len) + return -1; + return 0; +} + +/* Called from the exception vectors with the syndrome, the faulting + * instruction and the faulting address. */ +void imx95_stage1_fault(uint64_t esr, uint64_t elr, uint64_t far) +{ + /* Two halves, because the small printf may lack long long. The low word + * needs the width or its leading zeros are dropped and the halves run + * together, which misreports the register exactly when it matters. */ + wolfBoot_printf("stage1: exception ESR=0x%08x%08x ELR=0x%08x%08x " + "FAR=0x%08x%08x\n", + (unsigned)(esr >> 32), (unsigned)esr, + (unsigned)(elr >> 32), (unsigned)elr, + (unsigned)(far >> 32), (unsigned)far); +} + +extern uint8_t _start_text[]; +extern uint8_t END_STACK[]; + +/* True when an image would land on the code loading it. Nothing does in the real + * chain (OCRAM loader, DRAM images), but a stage 1 linked into DRAM for bring-up + * is itself an image of the container it walks. */ +/* True when loading this image would land on the code doing the loading. The + * padded length is what matters, so the arithmetic lives in imx95_ahab.c where + * the host tests can reach it. */ +static int stage1_overlaps_self(uint64_t dst, uint32_t size) +{ + return imx95_ahab_overlaps(dst, size, (uint64_t)(uintptr_t)_start_text, + (uint64_t)(uintptr_t)END_STACK); +} + +/* Windows no container image may load over. Staging is first because it is the + * least obvious: the ELE keeps reading it while the authentication is + * outstanding, so an image there would replace the blessed bytes. */ +static const struct imx95_ahab_region stage1_excl[] = { + { IMX95_AHAB_STAGE_BASE, IMX95_AHAB_STAGE_SIZE }, + { IMX95_OPTEE_SHM_BASE, IMX95_OPTEE_SHM_SIZE }, + { IMX95_M7_DDR_BASE, IMX95_M7_DDR_SIZE }, + { IMX95_ELE_SHM_BASE, IMX95_ELE_SHM_SIZE }, + { IMX95_VPU_BOOT_BASE, IMX95_VPU_BOOT_SIZE }, +}; + +#define STAGE1_EXCL_COUNT \ + ((uint32_t)(sizeof(stage1_excl) / sizeof(stage1_excl[0]))) + +/* Stage the container header, table and signature block in DDR and parse from + * there: the ELE requires DDR, and every value acted on must come from the same + * bytes it saw, since a medium can answer differently the second time. */ +static int stage1_stage_container(uint32_t base, + struct imx95_ahab_container *ctnr) +{ + uint8_t *stage = (uint8_t *)(uintptr_t)IMX95_AHAB_STAGE_BASE; + uint32_t span, want; + + if (stage1_read(NULL, base, (uint32_t)IMX95_AHAB_HDR_BYTES, stage) != 0) + return -1; + if (imx95_ahab_parse_buf(ctnr, base, stage, + (uint32_t)IMX95_AHAB_HDR_BYTES) != 0) + return -1; + if (imx95_ahab_span(ctnr, &span) != 0) + return -1; + + if (span > (uint32_t)IMX95_AHAB_HDR_BYTES) { + /* Re-read the whole span in one go. Splicing the first read to a tail + * would give two reads of one structure, which is a hole on a medium + * that need not answer consistently. */ + want = (span + (IMX95_AHAB_BLOCK - 1U)) & ~(IMX95_AHAB_BLOCK - 1U); + if (want > (uint32_t)IMX95_AHAB_STAGE_SIZE) + return -1; + if (stage1_read(NULL, base, want, stage) != 0) + return -1; + if (imx95_ahab_parse_buf(ctnr, base, stage, span) != 0) + return -1; + } + + return 0; +} + +/* Load every image of the container that follows the one the ROM booted from, + * then enter the first one loaded. That is BL31, which knows where OP-TEE and + * BL33 were placed; this is the same handoff U-Boot SPL makes. */ +static int stage1_boot(void) +{ + static struct imx95_ahab_container ctnr; + void (*entry)(void); + uint64_t bl31_entry = 0; + uint32_t next, i; +#ifdef IMX95_AHAB_AUTH + uint32_t resp = 0; + int ret, held = 0; +#endif + + if (imx95_scmi_uart_clk_init() != 0) { + /* Nothing can be reported: this is what the console runs on. */ + return -1; + } +#if defined(DEBUG_UART) + uart_init(); + wolfBoot_printf("\nwolfBoot stage 1: NXP i.MX95 Cortex-A55\n"); +#endif + + if (imx95_stage1_platform_init() != 0) + return -1; + + if (imx95_stage1_disk_init() != 0) { + wolfBoot_printf("stage1: boot device init failed\n"); + return -1; + } + +#ifdef IMX95_AHAB_LIFECYCLE + /* Opt-in: U-Boot reads the lifecycle only from U-Boot proper and only under + * CONFIG_AHAB_BOOT, so whether the FSB answers the A55 in SPL's slot is + * untested per part, and a diagnostic must not cost a working boot. */ + ele_report_lifecycle(ele_lifecycle()); +#endif + + /* Container 0 is the one the ROM read; the next one starts where it ends. */ + if (imx95_ahab_parse(&ctnr, IMX95_AHAB_MMC_OFFSET, stage1_read, NULL) != 0) { + wolfBoot_printf("stage1: no container at 0x%x\n", + (unsigned)IMX95_AHAB_MMC_OFFSET); + return -1; + } + if (imx95_ahab_next(&ctnr, &next) != 0) { + wolfBoot_printf("stage1: container 0 does not lead anywhere\n"); + return -1; + } + + if (stage1_stage_container(next, &ctnr) != 0) { + wolfBoot_printf("stage1: no container at 0x%x\n", (unsigned)next); + return -1; + } + +#ifdef IMX95_AHAB_AUTH + ret = ele_auth_container((uint64_t)IMX95_AHAB_STAGE_BASE, &resp); + if (ret == ELE_CALL_REJECTED && + ((resp >> 8) & 0xFFU) == (uint32_t)ELE_IND_BAD_OP) { + /* A context an interrupted boot left held. Release once, retry once. */ + (void)ele_release_container(NULL); + ret = ele_auth_container((uint64_t)IMX95_AHAB_STAGE_BASE, &resp); + } + if (ret != ELE_CALL_OK) { + ele_report("container authenticate", resp); + /* Only a clean refusal leaves the mailbox usable enough to release on. */ + if (ret == ELE_CALL_REJECTED) + (void)ele_release_container(NULL); + return -1; + } + held = 1; + wolfBoot_printf("stage1: container authenticated by ELE\n"); +#endif + + /* Everything below acts on the staged copy, and destinations are checked in + * full before any byte is written: an image streams straight into img->dst, + * so by the time the ELE could object the bytes have landed. */ + if (imx95_ahab_check_dst(&ctnr, (uint64_t)IMX95_DDR_BASE, + (uint64_t)IMX95_DDR_SIZE, + stage1_excl, STAGE1_EXCL_COUNT) != 0) { + wolfBoot_printf("stage1: container has an unusable load address\n"); + goto release; + } + if (imx95_ahab_check_exec(&ctnr, 0) != 0) { + wolfBoot_printf("stage1: image 0 is not an A55 executable\n"); + goto release; + } + + for (i = 0; i < ctnr.count; i++) { + if (stage1_overlaps_self(ctnr.img[i].dst, ctnr.img[i].size)) { +#ifdef IMX95_STAGE1_ALLOW_SELF_OVERLAP + /* Bring-up only: a stage 1 linked into DRAM is itself an image of + * the container it walks, so it has to skip its own entry. */ + wolfBoot_printf("stage1: skipping image %u at 0x%x (that is us)\n", + (unsigned)i, (unsigned)ctnr.img[i].dst); + if (i == 0U) { + wolfBoot_printf("stage1: image 0 skipped, nothing to enter\n"); + goto release; + } + continue; +#else + /* A container asking to be written over the running loader is a + * hostile-container signal, not something to work around. */ + wolfBoot_printf("stage1: image %u would overwrite this loader\n", + (unsigned)i); + goto release; +#endif + } + wolfBoot_printf("stage1: image %u -> 0x%x, %u bytes\n", + (unsigned)i, (unsigned)ctnr.img[i].dst, + (unsigned)ctnr.img[i].size); + if (imx95_ahab_load(&ctnr, i, (void *)(uintptr_t)ctnr.img[i].dst, + stage1_read, NULL) != 0) { + wolfBoot_printf("stage1: image %u load failed\n", (unsigned)i); + goto release; + } +#ifdef IMX95_AHAB_AUTH + /* Verify where it now sits, one image at a time so a failure names it. */ + ret = ele_verify_image(i, &resp); + if (ret != ELE_CALL_OK) { + wolfBoot_printf("stage1: image %u failed verification\n", + (unsigned)i); + ele_report("image verify", resp); + if (ret == ELE_CALL_BROKEN) + held = 0; /* mailbox is out of step; send nothing more */ + goto release; + } +#endif + } + + bl31_entry = ctnr.img[0].entry; + +#ifdef IMX95_AHAB_AUTH + /* Release before the jump, not on the way out of an error path: the success + * path never returns, and the enclave holds one context at a time. */ + if (held) { + if (ele_release_container(&resp) != ELE_CALL_OK) + ele_report("container release", resp); + held = 0; + } +#endif + + wolfBoot_printf("stage1: entering BL31 at 0x%x\n", (unsigned)bl31_entry); + + /* Caches are off, so the loads above are already in memory. */ + __asm__ volatile("dsb sy" ::: "memory"); + __asm__ volatile("isb" ::: "memory"); + + entry = (void (*)(void))(uintptr_t)bl31_entry; + entry(); + return -1; + +release: +#ifdef IMX95_AHAB_AUTH + if (held) + (void)ele_release_container(NULL); +#endif + return -1; +} + +int imx95_stage1_main(void) +{ + int ret = stage1_boot(); + +#ifdef IMX95_STAGE1_PASSTHROUGH + /* Bring-up only: here this stage is an extra image in a container another + * loader already staged, so it should be invisible when it fails. In + * production a failure means nothing loaded, and stopping is the answer. */ + void (*bl31)(void) = (void (*)(void))(uintptr_t)IMX95_BL31_BASE; + + wolfBoot_printf("stage1: passing through to BL31\n"); + bl31(); +#endif + return ret; +} diff --git a/hal/imx95_a55_stage1.ld b/hal/imx95_a55_stage1.ld new file mode 100644 index 0000000000..acd474e55b --- /dev/null +++ b/hal/imx95_a55_stage1.ld @@ -0,0 +1,51 @@ +/* imx95_a55_stage1.ld - wolfBoot stage 1, OCRAM-resident, in place of the + * U-Boot SPL image in AHAB container 0. Addresses match what the image tool + * records for that container entry. */ +MEMORY +{ + /* The boot ROM loads this here and enters at EL3. LENGTH is the same 192 KiB + * budget U-Boot gives SPL, so growth past it is a link error. The stack is + * NOT inside this region; see END_STACK. */ + OCRAM(rwx): ORIGIN = @WOLFBOOT_STAGE1_BASE_ADDR@, LENGTH = @WOLFBOOT_STAGE1_SIZE@ +} +ENTRY(_boot); + +SECTIONS +{ + .text : + { + _start_text = .; + KEEP(*(.boot*)) + *(.text*) + *(.rodata*) + . = ALIGN(8); + _end_text = .; + } > OCRAM + + .data : + { + _start_data = .; + KEEP(*(.data*)) + . = ALIGN(8); + _end_data = .; + } > OCRAM + + .bss (NOLOAD) : + { + . = ALIGN(8); + _start_bss = .; + __bss_start__ = .; + *(.bss*) + *(COMMON) + . = ALIGN(8); + _end_bss = .; + __bss_end__ = .; + } > OCRAM +} + +/* Stack top, deliberately ABOVE the MEMORY region, in the OCRAM that extends + * past this image's budget - 0x204D6000 at the OCRAM base, the address U-Boot + * SPL uses. A bare symbol gets no bounds check, hence the assert below. */ +END_STACK = @WOLFBOOT_STAGE1_BASE_ADDR@ + 0x56000; +ASSERT(END_STACK > @WOLFBOOT_STAGE1_BASE_ADDR@ + @WOLFBOOT_STAGE1_SIZE@, + "stage 1 stack top must stay above the OCRAM image region") diff --git a/hal/imx95_a55_stage1_start.S b/hal/imx95_a55_stage1_start.S new file mode 100644 index 0000000000..731efbc0bc --- /dev/null +++ b/hal/imx95_a55_stage1_start.S @@ -0,0 +1,109 @@ +/* imx95_a55_stage1_start.S - entry for the wolfBoot i.MX95 stage 1. + * + * 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 + */ + +/* Entered at EL3 with whatever MMU and cache state the previous stage left. The + * MMU and both caches go off up front and stay off, after writing back what was + * dirty - turning the cache off does not do that, and those lines would be lost. */ + + .section ".boot","ax" + .global _boot +_boot: + /* Vectors first, so a fault during setup stops here instead of running + * into whatever the previous stage left at address 0. */ + adr x0, _stage1_vectors + msr VBAR_EL3, x0 + isb + + /* Let the console's printf use SIMD: its variadic prologue saves q0-q7, + * which traps unless EL3 has FP enabled. The previous stage may or may not + * have done this. */ + msr CPTR_EL3, xzr + isb + + bl invalidate_dcaches + + /* SCTLR_EL3: clear M (MMU), C (data cache) and I (instruction cache). */ + mrs x0, SCTLR_EL3 + mov x1, #1 /* M */ + orr x1, x1, #(1 << 2) /* C */ + orr x1, x1, #(1 << 12) /* I */ + bic x0, x0, x1 + msr SCTLR_EL3, x0 + isb + + tlbi alle3 + ic iallu + dsb sy + isb + + ldr x0, =END_STACK + mov sp, x0 + + /* Zero .bss. The linker script aligns both ends to 8. */ + ldr x0, =_start_bss + ldr x1, =_end_bss +1: cmp x0, x1 + b.hs 2f + str xzr, [x0], #8 + b 1b + +2: bl imx95_stage1_main + /* Only reached if the load failed; imx95_stage1_main does not return on + * success. */ +_stage1_halt: + wfi + b _stage1_halt + +/* Every exception reports itself and then halts. The console is up for all but + * the first few instructions, and a fault that names its cause is worth far + * more during bring-up than a silent spin. */ +_stage1_fault: + mrs x0, ESR_EL3 + mrs x1, ELR_EL3 + mrs x2, FAR_EL3 + ldr x3, =END_STACK + mov sp, x3 + bl imx95_stage1_fault + b _stage1_halt + + .macro stage1_vector + .align 7 + b _stage1_fault + .endm + + .align 11 +_stage1_vectors: + stage1_vector /* Current EL, SP0: Synchronous */ + stage1_vector /* IRQ */ + stage1_vector /* FIQ */ + stage1_vector /* SError */ + stage1_vector /* Current EL, SPx: Synchronous */ + stage1_vector /* IRQ */ + stage1_vector /* FIQ */ + stage1_vector /* SError */ + stage1_vector /* Lower EL, AArch64 Synchronous */ + stage1_vector /* IRQ */ + stage1_vector /* FIQ */ + stage1_vector /* SError */ + stage1_vector /* Lower EL, AArch32 Synchronous */ + stage1_vector /* IRQ */ + stage1_vector /* FIQ */ + stage1_vector /* SError */ diff --git a/hal/imx95_ahab.c b/hal/imx95_ahab.c new file mode 100644 index 0000000000..40dbd654b5 --- /dev/null +++ b/hal/imx95_ahab.c @@ -0,0 +1,345 @@ +/* imx95_ahab.c + * + * AHAB container-set parsing for the NXP i.MX95. + * + * 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 + */ + +/* The boot device holds AHAB containers: 0 has the ELE firmware, System Manager + * and first A55 image, the next BL31, OP-TEE and BL33. Nothing records where the + * next starts - it is the end of the previous, rounded up. */ + +#include +#include +#include "hal/imx95_ahab.h" + +/* Header, LE: +0 ver, +1 len16, +3 tag, +4 flags, +8 sw_ver16, +10 fuse_ver, + * +11 num_images, +12 sig off16. Then 128-byte entries: +0 offset, +4 size, + * +8 dst64, +16 entry64, +24 flags, then hash and IV, read only by the ELE. */ +#define AHAB_HDR_SIZE 16U +#define AHAB_IMG_ENTRY_SIZE 128U + +static uint32_t rd16(const uint8_t *p) +{ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8); +} + +static uint32_t rd32(const uint8_t *p) +{ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} + +static uint64_t rd64(const uint8_t *p) +{ + return (uint64_t)rd32(p) | ((uint64_t)rd32(p + 4) << 32); +} + +/* Parse a container already in memory. Separate from the reading path because + * once the ELE has blessed a staged copy the table must come from that copy - + * a medium can answer differently the second time. */ +int imx95_ahab_parse_buf(struct imx95_ahab_container *ctnr, uint32_t base, + const uint8_t *buf, uint32_t len) +{ + struct imx95_ahab_image *img; + const uint8_t *e; + uint32_t count, table_end, sig_off, sig_len, end, i; + + if (ctnr == NULL || buf == NULL) + return -1; + if ((base % IMX95_AHAB_ALIGN) != 0U) + return -1; + if (len < AHAB_HDR_SIZE) + return -1; + + if (buf[3] != IMX95_AHAB_TAG || buf[0] != IMX95_AHAB_VERSION) + return -1; + + count = buf[11]; + if (count == 0U || count > (uint32_t)IMX95_AHAB_MAX_IMAGES) + return -1; + + /* The image table has to be inside the bytes handed in, or the entries + * below would be read from beyond the buffer. */ + table_end = AHAB_HDR_SIZE + (count * AHAB_IMG_ENTRY_SIZE); + if (table_end > len) + return -1; + + ctnr->base = base; + ctnr->count = count; + ctnr->hdr_len = rd16(&buf[1]); + + /* An entry parked beyond the header's own length would sit outside what a + * signature covers, so the table has to fit within it. */ + if (table_end > ctnr->hdr_len) + return -1; + + /* The container ends at the furthest of the header, every image and the + * signature block - the same rule U-Boot's get_container_size() uses. */ + end = ctnr->hdr_len; + + for (i = 0; i < count; i++) { + e = &buf[AHAB_HDR_SIZE + (i * AHAB_IMG_ENTRY_SIZE)]; + img = &ctnr->img[i]; + img->offset = rd32(e); + img->size = rd32(e + 4); + img->dst = rd64(e + 8); + img->entry = rd64(e + 16); + img->flags = rd32(e + 24); + + /* A truncated or hostile entry must not wrap the end calculation and + * make the container look smaller than it is. */ + if (img->size > (0xFFFFFFFFU - img->offset)) + return -1; + if ((img->offset + img->size) > end) + end = img->offset + img->size; + } + + ctnr->sig_end = 0U; + sig_off = rd16(&buf[12]); + if (sig_off != 0U) { + if ((sig_off + 4U) > len) + return -1; + /* The table must stop before the signature block starts, or entries + * would overlap the very bytes that authenticate them. */ + if (table_end > sig_off) + return -1; + sig_len = rd16(&buf[sig_off + 1]); + if (sig_len > (0xFFFFFFFFU - sig_off)) + return -1; + ctnr->sig_end = sig_off + sig_len; + if (ctnr->sig_end > end) + end = ctnr->sig_end; + } + + if (end > (0xFFFFFFFFU - base)) + return -1; + ctnr->size = end; + return 0; +} + +/* Parse the container at byte offset base. On success ctnr describes it and + * ctnr->size is the number of bytes it occupies from base. */ +int imx95_ahab_parse(struct imx95_ahab_container *ctnr, uint32_t base, + imx95_ahab_read_cb read_cb, void *ctx) +{ + static uint8_t hdr[IMX95_AHAB_HDR_BYTES] __attribute__((aligned(4))); + + if (read_cb == NULL) + return -1; + if ((base % IMX95_AHAB_ALIGN) != 0U) + return -1; + if (read_cb(ctx, base, (uint32_t)sizeof(hdr), hdr) != 0) + return -1; + + return imx95_ahab_parse_buf(ctnr, base, hdr, (uint32_t)sizeof(hdr)); +} + +/* Byte offset of the next container. The round-up is overflow-guarded: a + * container ending within IMX95_AHAB_ALIGN-1 of 4 GiB wrapped to 0, an aligned + * and accepted offset, so the next parse read the partition table. */ +int imx95_ahab_next(const struct imx95_ahab_container *ctnr, uint32_t *next) +{ + uint32_t end; + + if (ctnr == NULL || next == NULL) + return -1; + + end = ctnr->base + ctnr->size; + if (end > (0xFFFFFFFFU - (IMX95_AHAB_ALIGN - 1U))) + return -1; + + end = (end + (IMX95_AHAB_ALIGN - 1U)) & ~(IMX95_AHAB_ALIGN - 1U); + + /* A container that does not advance would make the walk loop forever. */ + if (end <= ctnr->base) + return -1; + + *next = end; + return 0; +} + +/* Bytes the ELE has to be given: the header, its table and its signature + * block. Deliberately not ctnr->size, which spans the image payloads and on a + * real container is tens of megabytes. */ +int imx95_ahab_span(const struct imx95_ahab_container *ctnr, uint32_t *span) +{ + uint32_t n; + + if (ctnr == NULL || span == NULL) + return -1; + + n = ctnr->hdr_len; + if (ctnr->sig_end > n) + n = ctnr->sig_end; + if ((AHAB_HDR_SIZE + (ctnr->count * AHAB_IMG_ENTRY_SIZE)) > n) + n = AHAB_HDR_SIZE + (ctnr->count * AHAB_IMG_ENTRY_SIZE); + + /* Attacker-controlled at this point: clamp before anything copies it. */ + if (n == 0U || n > IMX95_AHAB_SPAN_MAX) + return -1; + + *span = n; + return 0; +} + +/* The ELE hashes exactly size bytes but the device transfers whole blocks, so + * this rounded length is what gets written and must be bounds-checked. U-Boot + * rounds the same way in read_auth_image(). */ +/* True when the block-padded transfer to dst would touch [lo, hi). The padding + * is the point: imx95_ahab_load() writes the rounded length, so an image ending + * just below lo can still have its tail land inside. A rounded length of 0 is a + * round-up overflow and counts as an overlap. */ +int imx95_ahab_overlaps(uint64_t dst, uint32_t size, uint64_t lo, uint64_t hi) +{ + uint32_t len = imx95_ahab_load_len(size); + + if (len == 0U) + return 1; + if (dst > (UINT64_MAX - (uint64_t)len)) + return 1; + return (dst < hi) && ((dst + (uint64_t)len) > lo); +} + +uint32_t imx95_ahab_load_len(uint32_t size) +{ + uint32_t rem = size % IMX95_AHAB_BLOCK; + + if (rem == 0U) + return size; + if (size > (0xFFFFFFFFU - (IMX95_AHAB_BLOCK - rem))) + return 0U; + return size + (IMX95_AHAB_BLOCK - rem); +} + +static int range_overlaps(uint64_t a_base, uint64_t a_len, + uint64_t b_base, uint64_t b_len) +{ + if (a_len == 0U || b_len == 0U) + return 0; + return (a_base < (b_base + b_len)) && (b_base < (a_base + a_len)); +} + +/* Reject every unsafe destination before writing any of them: an image streams + * straight into img->dst, so by the time the ELE could object the bytes have + * landed. Runs whether or not authentication is enabled. */ +int imx95_ahab_check_dst(const struct imx95_ahab_container *ctnr, + uint64_t dram_base, uint64_t dram_size, + const struct imx95_ahab_region *excl, + uint32_t excl_count) +{ + const struct imx95_ahab_image *img; + uint64_t len, other_len; + uint32_t i, j; + + if (ctnr == NULL) + return -1; + if (dram_size == 0U || dram_base > (0xFFFFFFFFFFFFFFFFULL - dram_size)) + return -1; + if (excl_count != 0U && excl == NULL) + return -1; + + for (i = 0; i < ctnr->count; i++) { + img = &ctnr->img[i]; + + /* A zero-length image is not something a real container carries, and + * skipping one would desynchronize "loaded" from "verified". */ + if (img->size == 0U) + return -1; + + len = (uint64_t)imx95_ahab_load_len(img->size); + if (len == 0U) + return -1; + if (img->dst > (0xFFFFFFFFFFFFFFFFULL - len)) + return -1; + + /* Inside DRAM, all of it. */ + if (img->dst < dram_base) + return -1; + if ((img->dst + len) > (dram_base + dram_size)) + return -1; + + /* Clear of the staging window, this loader, and the carveouts their + * owners are already using. */ + for (j = 0; j < excl_count; j++) { + if (range_overlaps(img->dst, len, excl[j].base, excl[j].size)) + return -1; + } + + /* Disjoint from every other image, or a later load would silently + * replace one the ELE has already verified. */ + for (j = 0; j < i; j++) { + other_len = (uint64_t)imx95_ahab_load_len(ctnr->img[j].size); + if (range_overlaps(img->dst, len, ctnr->img[j].dst, other_len)) + return -1; + } + } + + return 0; +} + +/* The image stage 1 is going to branch into has to be an A55 executable, and + * its entry has to be inside the bytes that were loaded and verified. */ +int imx95_ahab_check_exec(const struct imx95_ahab_container *ctnr, + uint32_t index) +{ + const struct imx95_ahab_image *img; + + if (ctnr == NULL || index >= ctnr->count) + return -1; + + img = &ctnr->img[index]; + if (IMX95_AHAB_TYPE(img->flags) != IMX95_AHAB_TYPE_EXEC) + return -1; + if (IMX95_AHAB_CORE(img->flags) != IMX95_AHAB_CORE_A55) + return -1; + if (img->entry < img->dst) + return -1; + if (img->entry >= (img->dst + (uint64_t)img->size)) + return -1; + + return 0; +} + +/* Copy one image from the boot device to dst. The offset must be block-aligned + * as the image tool emits it; a container that is not is rejected rather than + * silently loaded from the wrong place. */ +int imx95_ahab_load(const struct imx95_ahab_container *ctnr, uint32_t index, + void *dst, imx95_ahab_read_cb read_cb, void *ctx) +{ + const struct imx95_ahab_image *img; + uint32_t len; + + if (ctnr == NULL || dst == NULL || read_cb == NULL) + return -1; + if (index >= ctnr->count) + return -1; + + img = &ctnr->img[index]; + if (img->size == 0U) + return -1; + if (((ctnr->base + img->offset) % IMX95_AHAB_BLOCK) != 0U) + return -1; + + len = imx95_ahab_load_len(img->size); + if (len == 0U) + return -1; + + return read_cb(ctx, ctnr->base + img->offset, len, dst); +} diff --git a/hal/imx95_ahab.h b/hal/imx95_ahab.h new file mode 100644 index 0000000000..d54855e99c --- /dev/null +++ b/hal/imx95_ahab.h @@ -0,0 +1,123 @@ +/* imx95_ahab.h + * + * AHAB container-set parsing for the NXP i.MX95. + * + * 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 + */ + +#ifndef IMX95_AHAB_H +#define IMX95_AHAB_H + +#include + +#define IMX95_AHAB_BLOCK 512U + +/* Two sectors hold the header plus seven image entries, more than any + * container the SoC's own boot flow uses. */ +/* Bytes read to parse a container in place. Has to cover the header, the image + * table (16 + 7*128 = 912) and the signature block, whose offset is a 16-bit + * field: 1 KiB was too tight for a container that pads before its signature. */ +#define IMX95_AHAB_HDR_BYTES 4096 +#define IMX95_AHAB_MAX_IMAGES 7 + +/* Containers start on a 1 KiB boundary. NXP's container_hdr_alignment() reports + * 16 KiB for anything but i.MX95 A0, yet these parts place container 1 at + * 0xCF800, which is 1 KiB aligned and not 16 KiB. The silicon wins. */ +#define IMX95_AHAB_ALIGN 1024U + +/* Most the ELE will ever be handed to authenticate. The header's length field + * is 16-bit, and NXP bounds its own staging window the same way + * (IMG_CONTAINER_END_BASE = base + 0xFFFF). */ +#define IMX95_AHAB_SPAN_MAX 0x10000U + +/* Where the ROM reads the first container from on eMMC/SD. */ +#define IMX95_AHAB_MMC_OFFSET 0x8000U + +#define IMX95_AHAB_TAG 0x87U +#define IMX95_AHAB_VERSION 0x02U + +/* Image flags: type in [3:0], the core that runs it in [7:4]. */ +#define IMX95_AHAB_TYPE(f) ((uint32_t)(f) & 0x0FU) +#define IMX95_AHAB_CORE(f) (((uint32_t)(f) >> 4) & 0x0FU) + +#define IMX95_AHAB_TYPE_EXEC 0x03U /* plain executable */ +#define IMX95_AHAB_TYPE_ELE 0x05U /* EdgeLock Enclave firmware */ + +#define IMX95_AHAB_CORE_M33 0x01U +#define IMX95_AHAB_CORE_A55 0x02U + +struct imx95_ahab_image { + uint64_t dst; /* where the image is loaded */ + uint64_t entry; /* where execution starts */ + uint32_t offset; /* byte offset from the container header */ + uint32_t size; + uint32_t flags; +}; + +struct imx95_ahab_container { + uint32_t base; /* byte offset of the header on the boot device */ + uint32_t size; /* header through the last byte the container owns */ + uint32_t hdr_len; /* the header's own length field, without payloads */ + uint32_t sig_end; /* end of the signature block, 0 when there is none */ + uint32_t count; + struct imx95_ahab_image img[IMX95_AHAB_MAX_IMAGES]; +}; + +/* A region an image must not be loaded over. */ +struct imx95_ahab_region { + uint64_t base; + uint64_t size; +}; + +/* Reads len bytes at byte offset off from the boot device into buf. Both off + * and len are multiples of the device block size. Returns 0 on success. */ +typedef int (*imx95_ahab_read_cb)(void *ctx, uint32_t off, uint32_t len, + void *buf); + +int imx95_ahab_parse(struct imx95_ahab_container *ctnr, uint32_t base, + imx95_ahab_read_cb read_cb, void *ctx); +int imx95_ahab_parse_buf(struct imx95_ahab_container *ctnr, uint32_t base, + const uint8_t *buf, uint32_t len); +int imx95_ahab_next(const struct imx95_ahab_container *ctnr, uint32_t *next); +int imx95_ahab_load(const struct imx95_ahab_container *ctnr, uint32_t index, + void *dst, imx95_ahab_read_cb read_cb, void *ctx); + +/* Bytes the ELE must be given to authenticate this container: the header, its + * image table and its signature block, but none of the image payloads. */ +int imx95_ahab_span(const struct imx95_ahab_container *ctnr, uint32_t *span); + +/* Length actually written when loading an image. The ELE hashes size, but a + * block device transfers whole blocks, so this is the extent that has to be + * bounds-checked. */ +uint32_t imx95_ahab_load_len(uint32_t size); +int imx95_ahab_overlaps(uint64_t dst, uint32_t size, uint64_t lo, uint64_t hi); + +/* Reject every image destination that cannot be safely written, before any of + * them is. Images must land inside [dram_base, dram_base + dram_size), must not + * touch any region in excl[], and must not overlap each other. */ +int imx95_ahab_check_dst(const struct imx95_ahab_container *ctnr, + uint64_t dram_base, uint64_t dram_size, + const struct imx95_ahab_region *excl, + uint32_t excl_count); + +/* Confirm one image is an A55 executable whose entry lies inside itself. */ +int imx95_ahab_check_exec(const struct imx95_ahab_container *ctnr, + uint32_t index); + +#endif /* IMX95_AHAB_H */ diff --git a/hal/imx95_m7.c b/hal/imx95_m7.c index 33b908c605..f7ee770d3c 100644 --- a/hal/imx95_m7.c +++ b/hal/imx95_m7.c @@ -304,10 +304,9 @@ void hal_init(void) } #if defined(WOLFBOOT_UPDATE_DISK) || defined(BOOT_BENCHMARK) -/* Microseconds from the DWT cycle counter hal_init() starts. The counter is - * 32-bit and wraps about every 5.4 s at 800 MHz, which bounds any single - * measurement rather than the boot as a whole - each BENCHMARK_END subtracts - * two reads, so a wrap only matters for an interval longer than that. */ +/* Microseconds from the DWT cycle counter hal_init() starts. 32-bit, wrapping + * every ~5.4 s at 800 MHz, which bounds a single measurement rather than the + * boot: BENCHMARK_END subtracts two reads. */ uint64_t hal_get_timer_us(void) { return (uint64_t)DWT_CYCCNT / (IMX95_M7_CORE_HZ / 1000000UL); diff --git a/hal/imx95_m7.h b/hal/imx95_m7.h index 5bd175e0a9..5392f2c114 100644 --- a/hal/imx95_m7.h +++ b/hal/imx95_m7.h @@ -115,12 +115,9 @@ static inline void imx95_dcache_clean(const void *addr, uint32_t len) if (len == 0) return; - /* Nothing to clean while the cache is off - every store already reached - * memory. It is also unsafe: the M7 does maintenance by address even with - * the cache disabled, and out of a cold reset the cache RAMs hold random - * tags and dirty bits until hal_cache_enable() invalidates them, so a hit - * writes garbage to a random address and the bus error comes back as an - * imprecise BusFault. */ + /* Nothing to clean with the cache off, and unsafe: the M7 maintains by + * address regardless, and out of cold reset the cache RAMs hold random tags + * and dirty bits, so a hit writes garbage and faults imprecisely. */ if ((SCB_CCR & CCR_DC) == 0UL) return; line = ((uint32_t)(uintptr_t)addr) & ~(IMX95_CACHE_LINE - 1UL); diff --git a/include/hal.h b/include/hal.h index 5f6d0973a3..6ae032f4de 100644 --- a/include/hal.h +++ b/include/hal.h @@ -40,6 +40,7 @@ extern void do_boot(const uint32_t *app_offset, const uint32_t* dts_offset); * Returns 0 on success or a negative value if the copy failed, so callers can * fail closed instead of running on stale data. */ extern int wolfBoot_fit_memcpy(void *dst, const void *src, uint32_t len); +extern int wolfBoot_fit_check_dest(void *dst, uint32_t len); #else extern void do_boot(const uint32_t *app_offset); #endif diff --git a/options.mk b/options.mk index a99c556876..6a07b39474 100644 --- a/options.mk +++ b/options.mk @@ -808,10 +808,9 @@ ifeq ($(DISK_EMMC),1) endif # Add the SD/eMMC block driver if SD card or eMMC is enabled (only once). -# DISK_DRIVER selects which one: the Cadence SDHCI driver (src/sdhci.c, -# the default) or the Freescale eSDHC driver (hal/nxp_esdhc.o, added by -# the target's arch.mk block, which also sets DISK_DRIVER=esdhc). Exactly -# one may link: both define the disk_* entry points. +# DISK_DRIVER picks one: Cadence SDHCI (src/sdhci.c, default), Freescale eSDHC +# (hal/nxp_esdhc.o) or i.MX uSDHC (hal/imx95_a55.c), set by the target's arch.mk. +# Exactly one may link: they all define the disk_* entry points. DISK_DRIVER?=cadence ifneq ($(filter 1,$(DISK_SDCARD) $(DISK_EMMC)),) ifeq ($(DISK_DRIVER),cadence) diff --git a/src/boot_aarch64.c b/src/boot_aarch64.c index f60e9805fd..c4b97414ca 100644 --- a/src/boot_aarch64.c +++ b/src/boot_aarch64.c @@ -59,6 +59,8 @@ extern void gicv2_init_secure(void); * boot_aarch64_start.S (e.g. via hal/zynq.h on ZynqMP). */ #if defined(EL2_HYPERVISOR) && EL2_HYPERVISOR == 1 extern void el2_flush_and_disable_mmu(void); +extern void el2_flush_disable_mmu_and_boot(uintptr_t entry, uintptr_t dts) + __attribute__((noreturn)); #endif /* Clean & invalidate the data cache over [start,end) (in boot_aarch64_start.S). @@ -160,8 +162,15 @@ void RAMFUNCTION do_boot(const uint32_t *app_offset) #ifdef MMU wolfBoot_printf("do_boot: dts=0x%08x\n", (uint32_t)(uintptr_t)dts_offset); /* WOLFBOOT_DTS_MAX_SIZE is this target's DTS staging-window size - * (see include/fdt.h); it bounds the fixups below. */ - hal_dts_fixup((uint32_t*)dts_offset, WOLFBOOT_DTS_MAX_SIZE); + * (see include/fdt.h); it bounds the fixups below. A failed fixup must + * not boot with an unpatched DTB - Linux may lack its memory node. */ + /* Only when there is a tree: update_ram.c passes NULL when the payload has + * none, and a real hal_dts_fixup() reports failure on NULL. */ + if ((dts_offset != NULL) && + (hal_dts_fixup((uint32_t*)dts_offset, WOLFBOOT_DTS_MAX_SIZE) != 0)) { + wolfBoot_printf("hal_dts_fixup failed; aborting boot\n"); + wolfBoot_panic(); + } #endif #ifndef SKIP_GIC_INIT @@ -207,7 +216,11 @@ void RAMFUNCTION do_boot(const uint32_t *app_offset) #if defined(MMU) && defined(EL2_HYPERVISOR) && EL2_HYPERVISOR == 1 if (current_el() == 2) { wolfBoot_printf("do_boot: flushing caches, disabling MMU\n"); - el2_flush_and_disable_mmu(); + /* Fused flush+disable+jump: a stack reload between a separate + * flush and jump can read stale DRAM once the D-cache is off + * (set/way misses a DSU system cache). See the asm routine. */ + el2_flush_disable_mmu_and_boot((uintptr_t)app_offset, + (uintptr_t)dts_offset); } #endif diff --git a/src/boot_aarch64_cache.S b/src/boot_aarch64_cache.S new file mode 100644 index 0000000000..44d67624e8 --- /dev/null +++ b/src/boot_aarch64_cache.S @@ -0,0 +1,74 @@ +/* boot_aarch64_cache.S - shared AArch64 set/way data cache maintenance. + * + * 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 + */ + +/* Clean and invalidate all data cache levels to the PoC by set/way. Valid only + * on a single core out of reset, immediately before the cache is turned off - + * which does not write dirty lines back by itself. Its own translation unit so a + * loader that links none of the rest of boot_aarch64_start.S can still call it. */ + .section ".text","ax" + .globl invalidate_dcaches +invalidate_dcaches: + dmb ISH + mrs x0, CLIDR_EL1 /* x0 = CLIDR */ + ubfx w2, w0, #24, #3 /* w2 = CLIDR.LoC */ + cmp w2, #0 /* LoC is 0? */ + b.eq invalidatecaches_end /* No cleaning required and enable MMU */ + mov w1, #0 /* w1 = level iterator */ + +invalidatecaches_flush_level: + add w3, w1, w1, lsl #1 /* w3 = w1 * 3 (right-shift for cache type) */ + lsr w3, w0, w3 /* w3 = w0 >> w3 */ + ubfx w3, w3, #0, #3 /* w3 = cache type of this level */ + cmp w3, #2 /* No cache at this level? */ + b.lt invalidatecaches_next_level + + lsl w4, w1, #1 + msr CSSELR_EL1, x4 /* Select current cache level in CSSELR */ + isb /* ISB required to reflect new CSIDR */ + mrs x4, CCSIDR_EL1 /* w4 = CSIDR */ + + ubfx w3, w4, #0, #3 + add w3, w3, #2 /* w3 = log2(line size) */ + ubfx w5, w4, #13, #15 + ubfx w4, w4, #3, #10 /* w4 = Way number */ + clz w6, w4 /* w6 = 32 - log2(number of ways) */ + +invalidatecaches_flush_set: + mov w8, w4 /* w8 = Way number */ +invalidatecaches_flush_way: + lsl w7, w1, #1 /* Fill level field */ + lsl w9, w5, w3 + orr w7, w7, w9 /* Fill index field */ + lsl w9, w8, w6 + orr w7, w7, w9 /* Fill way field */ + dc CISW, x7 /* Invalidate by set/way to point of coherency */ + subs w8, w8, #1 /* Decrement way */ + b.ge invalidatecaches_flush_way + subs w5, w5, #1 /* Descrement set */ + b.ge invalidatecaches_flush_set + +invalidatecaches_next_level: + add w1, w1, #1 /* Next level */ + cmp w2, w1 + b.gt invalidatecaches_flush_level + +invalidatecaches_end: + ret diff --git a/src/boot_aarch64_start.S b/src/boot_aarch64_start.S index e9c99d44be..937d03a557 100644 --- a/src/boot_aarch64_start.S +++ b/src/boot_aarch64_start.S @@ -42,6 +42,10 @@ #include "hal/tegra234.h" #endif +#ifdef TARGET_imx95_a55 +#include "hal/imx95_a55.h" +#endif + #ifdef TARGET_cm4 #include "hal/cm4.h" #endif @@ -497,53 +501,8 @@ error: b error -invalidate_dcaches: - dmb ISH - mrs x0, CLIDR_EL1 /* x0 = CLIDR */ - ubfx w2, w0, #24, #3 /* w2 = CLIDR.LoC */ - cmp w2, #0 /* LoC is 0? */ - b.eq invalidatecaches_end /* No cleaning required and enable MMU */ - mov w1, #0 /* w1 = level iterator */ - -invalidatecaches_flush_level: - add w3, w1, w1, lsl #1 /* w3 = w1 * 3 (right-shift for cache type) */ - lsr w3, w0, w3 /* w3 = w0 >> w3 */ - ubfx w3, w3, #0, #3 /* w3 = cache type of this level */ - cmp w3, #2 /* No cache at this level? */ - b.lt invalidatecaches_next_level - - lsl w4, w1, #1 - msr CSSELR_EL1, x4 /* Select current cache level in CSSELR */ - isb /* ISB required to reflect new CSIDR */ - mrs x4, CCSIDR_EL1 /* w4 = CSIDR */ - - ubfx w3, w4, #0, #3 - add w3, w3, #2 /* w3 = log2(line size) */ - ubfx w5, w4, #13, #15 - ubfx w4, w4, #3, #10 /* w4 = Way number */ - clz w6, w4 /* w6 = 32 - log2(number of ways) */ - -invalidatecaches_flush_set: - mov w8, w4 /* w8 = Way number */ -invalidatecaches_flush_way: - lsl w7, w1, #1 /* Fill level field */ - lsl w9, w5, w3 - orr w7, w7, w9 /* Fill index field */ - lsl w9, w8, w6 - orr w7, w7, w9 /* Fill way field */ - dc CISW, x7 /* Invalidate by set/way to point of coherency */ - subs w8, w8, #1 /* Decrement way */ - b.ge invalidatecaches_flush_way - subs w5, w5, #1 /* Descrement set */ - b.ge invalidatecaches_flush_set - -invalidatecaches_next_level: - add w1, w1, #1 /* Next level */ - cmp w2, w1 - b.gt invalidatecaches_flush_level - -invalidatecaches_end: - ret + + @@ -599,7 +558,6 @@ invalidatecaches_end: * ENETC) needs Outer-Shareable to snoop the A72 caches; Inner-Shareable only * covers the CPU cluster. */ .set MemoryOSH, 0x405 | (2 << 8) | (0x0) - #ifndef TARGET_nxp_ls1028a .section .mmu_tbl0,"a" @@ -1371,18 +1329,59 @@ _cm4_entry: b 7b 8: -#if defined(TARGET_tegra234) || defined(TARGET_imx8qm) +#if defined(TARGET_tegra234) || defined(TARGET_imx8qm) || defined(TARGET_imx95_a55) /* Reserved in-DRAM stack from the linker script: the generic path sets sp * to the image base, which here is the BL33 load address. */ ldr x1, =END_STACK #endif mov sp, x1 // set stack pointer -#if defined(TARGET_tegra234) || defined(TARGET_imx8qm) +#if defined(TARGET_tegra234) || defined(TARGET_imx8qm) || defined(TARGET_imx95_a55) /* Stash the handoff x0 (DTB pointer, saved in x21 at reset) in a .data * global so C can read it after boot_entry_C clears BSS. */ adrp x2, boot_handoff_x0 add x2, x2, :lo12:boot_handoff_x0 str x21, [x2] +#endif +#if defined(TARGET_imx95_a55) && !defined(NO_MMU) && \ + defined(EL2_HYPERVISOR) && EL2_HYPERVISOR == 1 + /* Identity-map DRAM Normal cacheable before C, or the FIT is hashed one + * uncached access at a time. Runs at EL2. Gated on EL2_HYPERVISOR because + * that compiles the teardown the ARM64 boot protocol needs, so turning the + * MMU on cannot drift apart from handing Linux a clean one. */ + dsb sy + isb + tlbi alle2 + dsb sy + isb + ic iallu /* the I-cache is about to be enabled */ + dsb sy + isb + /* Invalidate rather than clean: out of cold reset the cache RAMs hold random + * tags, the previous stage already cleaned what it owned, and a stale dirty + * line would later be written back over live DRAM. */ + bl imx95_dcache_invalidate_all + dsb sy + isb + + ldr x1, =MMUTableL0 + msr TTBR0_EL2, x1 + /* MAIR: 0 Normal NC, 1 Normal WB/WA/RA, 2 Device-nGnRnE, 3 Device-nGnRE, + * 4 Normal WT/WA/RA - the indices the block attributes above use. */ + ldr x1, =0x000000BB0400FF44 + msr MAIR_EL2, x1 + /* 4KB granule, T0SZ=24 (40-bit VA), PS=010 (40-bit PA), inner-shareable + * WB walks. */ + ldr x1, =0x80823518 + msr TCR_EL2, x1 + isb + + mrs x1, SCTLR_EL2 + orr x1, x1, #(1 << 12) /* I-cache */ + orr x1, x1, #(1 << 2) /* D-cache */ + orr x1, x1, #(1 << 0) /* MMU */ + msr SCTLR_EL2, x1 + dsb sy + isb #endif bl boot_entry_C // boot_entry_C never returns b 7b // go to sleep anyhow in case. @@ -1464,6 +1463,111 @@ cm4_fw_dtb: _vector_table: bl boot_entry_C // boot_entry_C never returns #endif + +#if defined(TARGET_imx95_a55) && !defined(NO_MMU) && \ + defined(EL2_HYPERVISOR) && EL2_HYPERVISOR == 1 +/* Invalidate all data cache levels by set/way, once before the D-cache is + * enabled: out of cold reset the cache RAMs hold random tags and dirty bits, and + * a stale dirty line would otherwise be written back over live DRAM. */ +.section ".boot" +.globl imx95_dcache_invalidate_all +imx95_dcache_invalidate_all: + dmb ISH + mrs x0, CLIDR_EL1 + ubfx w2, w0, #24, #3 /* LoC */ + cmp w2, #0 + b.eq 9f + mov w1, #0 /* level */ +1: add w3, w1, w1, lsl #1 + lsr w3, w0, w3 + ubfx w3, w3, #0, #3 /* cache type at this level */ + cmp w3, #2 + b.lt 4f + lsl w4, w1, #1 + msr CSSELR_EL1, x4 + isb + mrs x4, CCSIDR_EL1 + ubfx w3, w4, #0, #3 + add w3, w3, #2 /* log2(line size) */ + ubfx w5, w4, #13, #15 /* max set */ + ubfx w4, w4, #3, #10 /* max way */ + clz w6, w4 +2: mov w8, w4 +3: lsl w7, w1, #1 + lsl w9, w5, w3 + orr w7, w7, w9 + lsl w9, w8, w6 + orr w7, w7, w9 + dc ISW, x7 + subs w8, w8, #1 + b.ge 3b + subs w5, w5, #1 + b.ge 2b +4: add w1, w1, #1 + cmp w2, w1 + b.gt 1b +9: dsb sy + isb + ret + +/* i.MX95 identity tables (4KB granule, 40-bit VA/PA, T0SZ=24). DRAM is Normal + * cacheable so wolfcrypt's NEON loads do not fault; MU2's SCMI buffer stays + * Device, as the M33 writes it outside this cluster's coherency. 0-0x7FFFFFFF + * Device; 0x80000000-0xBFFFFFFF DRAM via L2 so the carveout can differ; + * 0xC0000000-0x27FFFFFFF DRAM cacheable; above invalid. */ +/* Block descriptors: valid + AF, with the MAIR index in bits [4:2] and the + * shareability in [9:8]. Device blocks are also PXN/UXN. */ +.set imx95_Memory, 0x405 | (3 << 8) /* Normal WB, ISH */ +.set imx95_MemoryNC, 0x401 | (3 << 8) /* Normal NC, ISH */ +.set imx95_Device, 0x409 | (1 << 53) | (1 << 54) /* Device-nGnRnE */ + +.section .mmu_tbl0,"a" +MMUTableL0: +.set SECT, MMUTableL1 +.8byte SECT + 0x3 /* 0x00_0000_0000 - 0x7F_FFFF_FFFF -> L1 */ +.8byte 0x0 /* 0x80_0000_0000 - 0xFF_FFFF_FFFF invalid */ + +.section .mmu_tbl1,"a" +MMUTableL1: +.set SECT, 0x0 +.8byte SECT + imx95_Device /* 0x00000000 - 0x3FFFFFFF */ +.set SECT, SECT+0x40000000 +.8byte SECT + imx95_Device /* 0x40000000 - 0x7FFFFFFF peripherals */ +.set SECT, MMUTableL2 +.8byte SECT + 0x3 /* 0x80000000 - 0xBFFFFFFF -> L2 */ +.set SECT, 0xC0000000 +.8byte SECT + imx95_Memory /* 0xC0000000 - 0xFFFFFFFF DRAM */ +.set SECT, 0x100000000 +.rept 6 /* 0x1_00000000 - 0x2_7FFFFFFF DRAM bank 1 */ +.8byte SECT + imx95_Memory +.set SECT, SECT+0x40000000 +.endr +.rept 502 /* remainder of the 512 GiB L1: unmapped */ +.8byte 0x0 +.endr + +.section .mmu_tbl2,"a" +MMUTableL2: +/* 0x80000000 + 16 MiB: the DTB's Cortex-M7 carveout, holding that core's console + * ring, wolfBoot's log ring and the payload Linux loads later - all read by + * software this cache cannot reach, so Normal Non-Cacheable. */ +.set SECT, 0x80000000 +.rept 8 /* 8 x 2 MiB = 16 MiB */ +.8byte SECT + imx95_MemoryNC +.set SECT, SECT+0x200000 +.endr +.rept 504 /* rest of the 1 GiB block: normal DRAM */ +.8byte SECT + imx95_Memory +.set SECT, SECT+0x200000 +.endr + + +/* Back to the section the surrounding startup code lives in: the routines that + * follow carry no .section of their own and would otherwise be assembled into + * the table above. */ +.section ".boot" +#endif /* TARGET_imx95_a55 && !NO_MMU */ + #endif /* !USE_BUILTIN_STARTUP */ @@ -1800,6 +1904,89 @@ el2_flush_and_disable_mmu: isb ret + +/* el2_flush_disable_mmu_and_boot(entry, dts): fused flush, MMU-off and jump + * touching NO memory after the disable - with the D-cache off, a stack reload + * between a separate flush and jump can read stale DRAM. x20/x21 carry them. */ +.global el2_flush_disable_mmu_and_boot +el2_flush_disable_mmu_and_boot: + /* arm64 boot protocol: all DAIF exceptions masked at kernel entry. */ + msr daifset, #0xf + mov x20, x0 /* entry */ + mov x21, x1 /* dts */ + bl el2_flush_and_disable_mmu + /* MMU off; registers only from here. Drop stale EL2 TLB entries. */ + tlbi alle2 + dsb sy + isb +#if defined(TARGET_imx95_a55) && defined(IMX95_HANDOFF_DUMP) + /* Debug crash catcher: report early payload faults via LPUART. */ + adr x9, imx95_catch_vectors + msr vbar_el2, x9 + isb +#endif + mov x0, x21 + mov x1, xzr + mov x2, xzr + mov x3, xzr + br x20 + +#if defined(TARGET_imx95_a55) && defined(IMX95_HANDOFF_DUMP) +/* EL2 crash catcher: dump ESR/ELR/FAR over the polled LPUART and park. + * Registers only - safe with MMU and caches off. */ +.macro IMX95_CATCH_PUTC reg +9: ldr w11, [x26] + tbz w11, #23, 9b + str \reg, [x27] +.endm + +.balign 2048 +imx95_catch_vectors: + .rept 16 + b imx95_catch_common + .balign 0x80 + .endr + +imx95_catch_common: + ldr x26, =(IMX95_LPUART1_BASE + LPUART_STAT_OFF) + ldr x27, =(IMX95_LPUART1_BASE + LPUART_DATA_OFF) + /* "\r\nVEC " */ + mov w10, #'\r' + IMX95_CATCH_PUTC w10 + mov w10, #'\n' + IMX95_CATCH_PUTC w10 + mov w10, #'V' + IMX95_CATCH_PUTC w10 + mov w10, #'E' + IMX95_CATCH_PUTC w10 + mov w10, #'C' + IMX95_CATCH_PUTC w10 + mrs x12, esr_el2 + bl imx95_catch_hex + mrs x12, elr_el2 + bl imx95_catch_hex + mrs x12, far_el2 + bl imx95_catch_hex +1: wfe + b 1b + +/* print ' ' + x12 as 16 hex digits; clobbers x10-x14, uses x30 */ +imx95_catch_hex: + mov w10, #' ' + IMX95_CATCH_PUTC w10 + mov x13, #60 +2: lsr x14, x12, x13 + and x14, x14, #0xf + cmp x14, #10 + b.lt 3f + add w10, w14, #('a' - 10) + b 4f +3: add w10, w14, #'0' +4: IMX95_CATCH_PUTC w10 + subs x13, x13, #4 + b.ge 2b + ret +#endif /* TARGET_imx95_a55 && IMX95_HANDOFF_DUMP */ #endif /* EL2_HYPERVISOR || WOLFBOOT_AARCH64_MMU_TEARDOWN */ #if defined(WOLFBOOT_ZYNQMP_FSBL) diff --git a/src/fdt.c b/src/fdt.c index 6e9c25b00b..be18b3e54c 100644 --- a/src/fdt.c +++ b/src/fdt.c @@ -1735,6 +1735,19 @@ int __attribute__((weak)) wolfBoot_fit_memcpy(void *dst, const void *src, return 0; } +/* Weak destination check for a FIT subimage load. wolfBoot_fit_memcpy() above + * lets a target police the plain copy, but the gzip path writes to the + * FIT-declared address directly, so that write needs its own check. Default + * accepts everything, which is the behavior before this hook existed. Called + * with the decompressor's output ceiling, not the eventual length, because the + * real length is not known until the stream has been inflated. */ +int __attribute__((weak)) wolfBoot_fit_check_dest(void *dst, uint32_t len) +{ + (void)dst; + (void)len; + return 0; +} + /* Inner implementation shared by fit_load_image_ex and fit_load_image_to. * When dst_override is non-NULL it replaces the FIT image's `load` * property as the destination, so a compressed (gzip) payload is @@ -1846,6 +1859,11 @@ static void* fit_load_image_inner(fdt_ctx* ctx, const char* image, int* lenp, wolfBoot_printf("gzip: no _start_text, output not " "bounded by the wolfBoot image\n"); } + if (wolfBoot_fit_check_dest(load, gz_max) != 0) { + wolfBoot_printf("FIT: %s decompression destination %p " + "rejected\n", image, load); + return NULL; + } wolfBoot_printf("Decompressing Image %s (gzip): " "%p -> %p (%d bytes)\n", image, data, load, len); BENCHMARK_START(); diff --git a/stage1/Makefile b/stage1/Makefile index cc5a5ea0e7..a3ba62423e 100644 --- a/stage1/Makefile +++ b/stage1/Makefile @@ -9,6 +9,10 @@ include ../tools/config.mk ## Initializers WOLFBOOT_ROOT?=$(PWD) +# Set by the top-level Makefile; defaulted here so this sub-make also works +# when it is invoked directly. +WOLFBOOT_LIB_WOLFSSL?=$(abspath ../lib/wolfssl) +WOLFBOOT_LIB_WOLFTPM?=$(abspath ../lib/wolfTPM) CFLAGS:=-D"__WOLFBOOT" CFLAGS+=-Werror -Wextra -nostdlib LDFLAGS:= @@ -52,6 +56,40 @@ include ../arch.mk # Parse config options include ../options.mk +# NXP i.MX95: stage 1 replaces U-Boot SPL in AHAB container 0. It is a +# standalone image with its own entry, so the object list is stated outright +# rather than inherited from the BL33 build. +ifeq ($(TARGET),imx95_a55) + OBJS:= \ + ./hal/imx95_a55_stage1.o \ + ./hal/imx95_a55_stage1_start.o \ + ./src/boot_aarch64_cache.o \ + ./hal/imx95_ahab.o \ + ./hal/imx95_a55.o \ + ./src/string.o + CFLAGS+=-DIMX95_STAGE1 + ASFLAGS+=-DIMX95_STAGE1 + # Ask the ELE to authenticate the container this stage loads BL31, OP-TEE and + # BL33 out of. On by default: taking U-Boot SPL's slot means taking over the + # step SPL performed, and without it the AHAB chain stops at this stage. Set + # to 0 only for bring-up on a part whose containers are not signed. + IMX95_AHAB_AUTH?=1 + ifeq ($(IMX95_AHAB_AUTH),1) + CFLAGS+=-DIMX95_AHAB_AUTH + endif + # Read and print the part's lifecycle from the fuse shadow block. Off by + # default: no other code on this platform touches the FSB from the slot SPL + # occupies, so a part that does not answer there would lose the boot to a + # line of diagnostics. + ifeq ($(IMX95_AHAB_LIFECYCLE),1) + CFLAGS+=-DIMX95_AHAB_LIFECYCLE + endif + # OCRAM, where the boot ROM loads the container's A55 image. Overridable so + # the same build can be linked elsewhere for bring-up. + WOLFBOOT_STAGE1_BASE_ADDR?=0x20480000 + WOLFBOOT_STAGE1_SIZE?=0x30000 +endif + ifeq ($(WOLFTPM),1) LS1_OBJS += \ tpm_io.o \ @@ -76,9 +114,15 @@ CFLAGS+=-D"WOLFBOOT_TPM_NO_CHG_PLAT_AUTH" # Setup default optimizations (for GCC) ifeq ($(USE_GCC_HEADLESS),1) - CFLAGS+=-Wall -Wextra -Wno-main -Wno-unused + # -ffreestanding is not optional: without it the compiler recognizes the + # byte-scan loop in src/string.c as the strlen idiom and rewrites it into a + # call to strlen, so strlen becomes a branch to itself. -nostartfiles keeps + # the C runtime's startup objects, and the libc they drag in, out of the + # link. The top-level Makefile passes both; this one has to as well. + CFLAGS+=-Wall -Wextra -Wno-main -Wno-unused -ffreestanding CFLAGS+=-ffunction-sections -fdata-sections LDFLAGS+=-Wl,-gc-sections -Wl,-Map=loader_stage1.map + LDFLAGS+=-ffreestanding -nostartfiles LSCRIPT_FLAGS+=-T $(LSCRIPT) OBJCOPY_FLAGS+=--gap-fill $(FILL_BYTE) endif diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 675597c549..2346e1ee63 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -73,6 +73,7 @@ TESTS:=unit-parser unit-parser-large-header unit-fdt unit-extflash unit-string \ unit-image-dts-sha384 unit-image-dts-sha3-384 unit-store-sbrk \ unit-tpm-blob unit-policy-create unit-policy-sign unit-rot-auth unit-sdhci-response-bits \ unit-sdhci-disk-unaligned unit-sdhci-dma-error unit-cm4-sdhci unit-cm4-rauc-slot \ + unit-imx95-ahab \ unit-sign-encrypted-output \ unit-sign-hybrid-keyload \ unit-sign-header-failure \ @@ -755,6 +756,10 @@ unit-cm4-sdhci: ../../include/target.h unit-cm4-sdhci.c gcc -o $@ $^ $(CFLAGS) -I../.. -ffunction-sections -fdata-sections $(LDFLAGS) \ -Wl,--gc-sections +unit-imx95-ahab: ../../include/target.h unit-imx95-ahab.c + gcc -o $@ unit-imx95-ahab.c $(CFLAGS) -I../.. -ffunction-sections \ + -fdata-sections $(LDFLAGS) -Wl,--gc-sections + unit-cm4-rauc-slot: ../../include/target.h unit-cm4-rauc-slot.c ../../src/ubootenv.c gcc -o $@ unit-cm4-rauc-slot.c ../../src/ubootenv.c $(CFLAGS) -I../.. \ -ffunction-sections -fdata-sections $(LDFLAGS) -Wl,--gc-sections @@ -1320,15 +1325,18 @@ versal_host.c: ../../hal/versal.c versal_host.h aarch64_arch_host.h # The anchors are literal source lines, so a refactor can move them. "test -s" # catches an anchor that stops matching, but not a range that still matches # and silently truncates, so each rule also asserts the entry points its test -# drives are present. +# drives are present. Two details of that form matter: the assertion exits +# rather than returning false, since a false in a for-body is discarded by the +# next iteration's status and only the last item would fail the build; and it +# matches the call syntax, since a bare name still matches a renamed one. imx8qm_usdhc_extract.h: ../../hal/imx8qm.c sed -n '/^#ifndef CADENCE_SRS_OFFSET$$/,/^#endif \/\* DISK_SDCARD || DISK_EMMC \*\/$$/p' $< \ | sed '/^#endif \/\* DISK_SDCARD || DISK_EMMC \*\/$$/d' > $@ test -s $@ @! grep -q '__asm__' $@ || { echo "$@: unexpected asm in the extracted shim"; false; } - @for f in sdhci_reg_read sdhci_reg_write sdhci_platform_init \ - sdhci_platform_set_clock; do \ - grep -q "$$f" $@ || { echo "$@: $$f missing - the extraction anchors no longer bracket the whole shim"; false; }; \ + @for f in 'sdhci_reg_read(' 'sdhci_reg_write(' 'sdhci_platform_init(' \ + 'sdhci_platform_set_clock('; do \ + grep -qF "$$f" $@ || { echo "$@: '$$f' missing - the extraction anchors no longer bracket the whole shim"; exit 1; }; \ done unit-imx8qm-usdhc-shim: unit-imx8qm-usdhc-shim.c imx8qm_usdhc_extract.h @@ -1342,9 +1350,9 @@ imx8qm_flexspi_extract.h: ../../hal/imx8qm.c | sed '/^#endif \/\* EXT_FLASH \*\/$$/d' > $@ test -s $@ @! grep -q '__asm__' $@ || { echo "$@: unexpected asm in the extracted driver"; false; } - @for f in flexspi_init ext_flash_read ext_flash_write ext_flash_erase \ - ext_flash_unlock; do \ - grep -q "$$f" $@ || { echo "$@: $$f missing - the extraction anchors no longer bracket the whole driver"; false; }; \ + @for f in 'flexspi_init(' 'ext_flash_read(' 'ext_flash_write(' \ + 'ext_flash_erase(' 'ext_flash_unlock('; do \ + grep -qF "$$f" $@ || { echo "$@: '$$f' missing - the extraction anchors no longer bracket the whole driver"; exit 1; }; \ done unit-imx8qm-flexspi-write: unit-imx8qm-flexspi-write.c imx8qm_flexspi_extract.h diff --git a/tools/unit-tests/unit-imx95-ahab.c b/tools/unit-tests/unit-imx95-ahab.c new file mode 100644 index 0000000000..2ec9d7caec --- /dev/null +++ b/tools/unit-tests/unit-imx95-ahab.c @@ -0,0 +1,530 @@ +/* unit-imx95-ahab.c + * + * Unit tests for the i.MX95 AHAB container parser in hal/imx95_ahab.c. The + * parser decides what the stage 1 loads and where it jumps, from a structure + * read off the boot device, so the bounds and overflow guards matter as much + * as the happy path. + * + * 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 "../../hal/imx95_ahab.c" + +/* A boot device made of host memory. Big enough for two containers and the + * images the second one points at. */ +#define DEV_BYTES (512U * 1024U) +static uint8_t dev[DEV_BYTES]; + +static int dev_read(void *ctx, uint32_t off, uint32_t len, void *buf) +{ + (void)ctx; + if ((off % IMX95_AHAB_BLOCK) != 0U || (len % IMX95_AHAB_BLOCK) != 0U) + return -1; + if ((off + len) > DEV_BYTES) + return -1; + memcpy(buf, &dev[off], len); + return 0; +} + +/* A read that fails every time, to prove a device error is not mistaken for + * an absent container. */ +static int dev_read_fail(void *ctx, uint32_t off, uint32_t len, void *buf) +{ + (void)ctx; (void)off; (void)len; (void)buf; + return -1; +} + +static void put16(uint32_t off, uint32_t v) +{ + dev[off] = (uint8_t)(v & 0xFF); + dev[off + 1] = (uint8_t)((v >> 8) & 0xFF); +} + +static void put32(uint32_t off, uint32_t v) +{ + int i; + for (i = 0; i < 4; i++) + dev[off + i] = (uint8_t)((v >> (8 * i)) & 0xFF); +} + +static void put64(uint32_t off, uint64_t v) +{ + put32(off, (uint32_t)v); + put32(off + 4, (uint32_t)(v >> 32)); +} + +/* Lay down a container header at base with count image entries. */ +static void make_container(uint32_t base, uint32_t count, uint32_t sig_off) +{ + uint32_t length = AHAB_HDR_SIZE + (count * AHAB_IMG_ENTRY_SIZE); + + memset(&dev[base], 0, IMX95_AHAB_HDR_BYTES); + dev[base + 0] = IMX95_AHAB_VERSION; + put16(base + 1, length); + dev[base + 3] = IMX95_AHAB_TAG; + dev[base + 11] = (uint8_t)count; + put16(base + 12, sig_off); +} + +static void make_image(uint32_t base, uint32_t idx, uint32_t off, uint32_t size, + uint64_t dst, uint64_t entry, uint32_t flags) +{ + uint32_t e = base + AHAB_HDR_SIZE + (idx * AHAB_IMG_ENTRY_SIZE); + + put32(e + 0, off); + put32(e + 4, size); + put64(e + 8, dst); + put64(e + 16, entry); + put32(e + 24, flags); +} + +static void setup_valid(void) +{ + memset(dev, 0, sizeof(dev)); + make_container(IMX95_AHAB_MMC_OFFSET, 2, 0); + /* Two images, the furthest ending at 0x3000 from the header. */ + make_image(IMX95_AHAB_MMC_OFFSET, 0, 0x1000, 0x1000, + 0x8A200000ULL, 0x8A200000ULL, 0x03U | (0x02U << 4)); + make_image(IMX95_AHAB_MMC_OFFSET, 1, 0x2000, 0x1000, + 0x90200000ULL, 0x90200000ULL, 0x03U | (0x02U << 4)); + /* Recognisable payloads for the load test. */ + memset(&dev[IMX95_AHAB_MMC_OFFSET + 0x1000], 0xA5, 0x1000); + memset(&dev[IMX95_AHAB_MMC_OFFSET + 0x2000], 0x5A, 0x1000); +} + +START_TEST(test_parse_valid) +{ + struct imx95_ahab_container c; + + setup_valid(); + ck_assert_int_eq(imx95_ahab_parse(&c, IMX95_AHAB_MMC_OFFSET, dev_read, + NULL), 0); + ck_assert_uint_eq(c.base, IMX95_AHAB_MMC_OFFSET); + ck_assert_uint_eq(c.count, 2); + ck_assert_uint_eq(c.size, 0x3000); + ck_assert_uint_eq(c.img[0].dst, 0x8A200000ULL); + ck_assert_uint_eq(c.img[1].offset, 0x2000); + ck_assert_uint_eq(IMX95_AHAB_TYPE(c.img[0].flags), IMX95_AHAB_TYPE_EXEC); + ck_assert_uint_eq(IMX95_AHAB_CORE(c.img[0].flags), IMX95_AHAB_CORE_A55); +} +END_TEST + +/* The whole point of the walk: the next container is the end of this one + * rounded up, because nothing records where it starts. */ +START_TEST(test_next_rounds_up) +{ + struct imx95_ahab_container c; + + uint32_t next; + + setup_valid(); + ck_assert_int_eq(imx95_ahab_parse(&c, IMX95_AHAB_MMC_OFFSET, dev_read, + NULL), 0); + ck_assert_int_eq(imx95_ahab_next(&c, &next), 0); + ck_assert_uint_eq(next, IMX95_AHAB_MMC_OFFSET + 0x3000); + + /* An end that is not already aligned must round up, not truncate. */ + c.size = 0x3001; + ck_assert_int_eq(imx95_ahab_next(&c, &next), 0); + ck_assert_uint_eq(next, (IMX95_AHAB_MMC_OFFSET + 0x3400)); + + /* The round-up must not wrap. An end inside the last IMX95_AHAB_ALIGN-1 + * bytes of the address space used to come back as 0, which is aligned and + * would have been accepted as the next container's offset. */ + c.base = 0xFFFFF800U; + c.size = 0x7FFU; + ck_assert_int_lt(imx95_ahab_next(&c, &next), 0); + + c.base = 0U; + c.size = 0xFFFFFFFFU; + ck_assert_int_lt(imx95_ahab_next(&c, &next), 0); + + /* A container that does not advance would loop the walk forever. */ + c.base = IMX95_AHAB_MMC_OFFSET; + c.size = 0U; + ck_assert_int_lt(imx95_ahab_next(&c, &next), 0); + + ck_assert_int_lt(imx95_ahab_next(NULL, &next), 0); + ck_assert_int_lt(imx95_ahab_next(&c, NULL), 0); +} +END_TEST + +/* The signature block can be the furthest thing in the container. */ +START_TEST(test_size_covers_signature_block) +{ + struct imx95_ahab_container c; + + setup_valid(); + make_container(IMX95_AHAB_MMC_OFFSET, 1, 0x120); + make_image(IMX95_AHAB_MMC_OFFSET, 0, 0x200, 0x200, 0, 0, 0); + put16(IMX95_AHAB_MMC_OFFSET + 0x120 + 1, 0x300); /* sig block length */ + ck_assert_int_eq(imx95_ahab_parse(&c, IMX95_AHAB_MMC_OFFSET, dev_read, + NULL), 0); + ck_assert_uint_eq(c.size, 0x120 + 0x300); +} +END_TEST + +START_TEST(test_reject_bad_tag_and_version) +{ + struct imx95_ahab_container c; + + setup_valid(); + dev[IMX95_AHAB_MMC_OFFSET + 3] = 0x00; + ck_assert_int_lt(imx95_ahab_parse(&c, IMX95_AHAB_MMC_OFFSET, dev_read, + NULL), 0); + + setup_valid(); + dev[IMX95_AHAB_MMC_OFFSET + 0] = 0x01; + ck_assert_int_lt(imx95_ahab_parse(&c, IMX95_AHAB_MMC_OFFSET, dev_read, + NULL), 0); +} +END_TEST + +START_TEST(test_reject_bad_image_count) +{ + struct imx95_ahab_container c; + + setup_valid(); + dev[IMX95_AHAB_MMC_OFFSET + 11] = 0; + ck_assert_int_lt(imx95_ahab_parse(&c, IMX95_AHAB_MMC_OFFSET, dev_read, + NULL), 0); + + setup_valid(); + dev[IMX95_AHAB_MMC_OFFSET + 11] = IMX95_AHAB_MAX_IMAGES + 1; + ck_assert_int_lt(imx95_ahab_parse(&c, IMX95_AHAB_MMC_OFFSET, dev_read, + NULL), 0); +} +END_TEST + +/* An entry whose offset + size wraps must not make the container look + * smaller than it is, which would place the next container inside this one. */ +START_TEST(test_reject_offset_size_overflow) +{ + struct imx95_ahab_container c; + + setup_valid(); + make_image(IMX95_AHAB_MMC_OFFSET, 0, 0xFFFFFF00U, 0x200, 0, 0, 0); + ck_assert_int_lt(imx95_ahab_parse(&c, IMX95_AHAB_MMC_OFFSET, dev_read, + NULL), 0); +} +END_TEST + +START_TEST(test_reject_signature_block_out_of_range) +{ + struct imx95_ahab_container c; + + setup_valid(); + put16(IMX95_AHAB_MMC_OFFSET + 12, IMX95_AHAB_HDR_BYTES); + ck_assert_int_lt(imx95_ahab_parse(&c, IMX95_AHAB_MMC_OFFSET, dev_read, + NULL), 0); +} +END_TEST + +START_TEST(test_reject_unaligned_base_and_read_failure) +{ + struct imx95_ahab_container c; + + setup_valid(); + ck_assert_int_lt(imx95_ahab_parse(&c, IMX95_AHAB_MMC_OFFSET + 1, dev_read, + NULL), 0); + ck_assert_int_lt(imx95_ahab_parse(&c, IMX95_AHAB_MMC_OFFSET, dev_read_fail, + NULL), 0); + ck_assert_int_lt(imx95_ahab_parse(NULL, IMX95_AHAB_MMC_OFFSET, dev_read, + NULL), 0); + ck_assert_int_lt(imx95_ahab_parse(&c, IMX95_AHAB_MMC_OFFSET, NULL, + NULL), 0); +} +END_TEST + +START_TEST(test_load_copies_image) +{ + struct imx95_ahab_container c; + static uint8_t out[0x1000]; + + setup_valid(); + ck_assert_int_eq(imx95_ahab_parse(&c, IMX95_AHAB_MMC_OFFSET, dev_read, + NULL), 0); + memset(out, 0, sizeof(out)); + ck_assert_int_eq(imx95_ahab_load(&c, 1, out, dev_read, NULL), 0); + ck_assert_uint_eq(out[0], 0x5A); + ck_assert_uint_eq(out[sizeof(out) - 1], 0x5A); +} +END_TEST + +START_TEST(test_load_rejects_bad_requests) +{ + struct imx95_ahab_container c; + static uint8_t out[0x1000]; + + setup_valid(); + ck_assert_int_eq(imx95_ahab_parse(&c, IMX95_AHAB_MMC_OFFSET, dev_read, + NULL), 0); + /* Index past the end of the array. */ + ck_assert_int_lt(imx95_ahab_load(&c, c.count, out, dev_read, NULL), 0); + ck_assert_int_lt(imx95_ahab_load(&c, 0, NULL, dev_read, NULL), 0); + + /* A zero-length image has nothing to load. */ + c.img[0].size = 0; + ck_assert_int_lt(imx95_ahab_load(&c, 0, out, dev_read, NULL), 0); + + /* Offsets the image tool never emits are refused rather than loaded from + * the wrong place. */ + c.img[0].size = 0x1000; + c.img[0].offset = 0x1001; + ck_assert_int_lt(imx95_ahab_load(&c, 0, out, dev_read, NULL), 0); + + /* A non-block-multiple size is read rounded up, as U-Boot's + * read_auth_image() does. Those extra bytes are why the destination checks + * bound the rounded-up extent rather than size. */ + c.img[0].offset = 0x1000; + c.img[0].size = 0x101; + ck_assert_int_eq(imx95_ahab_load(&c, 0, out, dev_read, NULL), 0); + ck_assert_uint_eq(imx95_ahab_load_len(0x101), 0x200); + ck_assert_uint_eq(imx95_ahab_load_len(0x200), 0x200); + ck_assert_uint_eq(imx95_ahab_load_len(0), 0); + /* Rounding up must not wrap. */ + ck_assert_uint_eq(imx95_ahab_load_len(0xFFFFFF01U), 0U); +} +END_TEST + +/* The self-overlap test has to use the length that is actually written, not the + * recorded size: imx95_ahab_load() block-pads the transfer, so an image ending + * just below a protected region can still have its tail land inside it. */ +START_TEST(test_overlaps_uses_padded_length) +{ + const uint64_t lo = 0x20480000UL; /* stand-ins for _start_text .. */ + const uint64_t hi = 0x204D6000UL; /* .. END_STACK */ + + /* Ends exactly at lo with an already-aligned size: no overlap, and the + * padding changes nothing because there is none. */ + ck_assert_int_eq(imx95_ahab_overlaps(lo - 0x200UL, 0x200U, lo, hi), 0); + /* The case that matters. Each of these ends exactly at lo when the recorded + * size is taken literally - which an unrounded check reads as "clear" - but + * the padded transfer runs 0x100 bytes past it, into the loader. */ + ck_assert_int_eq(imx95_ahab_overlaps(lo - 0x100UL, 0x100U, lo, hi), 1); + ck_assert_int_eq(imx95_ahab_overlaps(lo - 0x300UL, 0x300U, lo, hi), 1); + ck_assert_int_eq(imx95_ahab_overlaps(lo - 0x500UL, 0x500U, lo, hi), 1); + /* Wholly below, and wholly above. */ + ck_assert_int_eq(imx95_ahab_overlaps(lo - 0x10000UL, 0x200U, lo, hi), 0); + ck_assert_int_eq(imx95_ahab_overlaps(hi, 0x200U, lo, hi), 0); + /* Inside. */ + ck_assert_int_eq(imx95_ahab_overlaps(lo, 0x200U, lo, hi), 1); + /* A size whose round-up overflows is refused rather than wrapped. */ + ck_assert_int_eq(imx95_ahab_overlaps(lo, 0xFFFFFF01U, lo, hi), 1); + /* So is a destination whose padded end would wrap 64 bits. */ + ck_assert_int_eq(imx95_ahab_overlaps(0xFFFFFFFFFFFFFF00ULL, 0x200U, + lo, hi), 1); +} +END_TEST + +/* The image table has to live inside the bytes a signature covers. */ +START_TEST(test_parse_rejects_table_outside_header) +{ + struct imx95_ahab_container c; + + setup_valid(); + /* Shrink the header's own length so the two entries no longer fit. */ + put16(IMX95_AHAB_MMC_OFFSET + 1, AHAB_HDR_SIZE + AHAB_IMG_ENTRY_SIZE); + ck_assert_int_lt(imx95_ahab_parse(&c, IMX95_AHAB_MMC_OFFSET, dev_read, + NULL), 0); + + /* A signature block starting inside the table is equally wrong: the + * entries would overlap the bytes that authenticate them. */ + setup_valid(); + put16(IMX95_AHAB_MMC_OFFSET + 12, AHAB_HDR_SIZE + 4U); + ck_assert_int_lt(imx95_ahab_parse(&c, IMX95_AHAB_MMC_OFFSET, dev_read, + NULL), 0); +} +END_TEST + +/* The span handed to the ELE covers the header, table and signature block, and + * never the image payloads. */ +START_TEST(test_span_bounds) +{ + struct imx95_ahab_container c; + uint32_t span; + + setup_valid(); + ck_assert_int_eq(imx95_ahab_parse(&c, IMX95_AHAB_MMC_OFFSET, dev_read, + NULL), 0); + ck_assert_int_eq(imx95_ahab_span(&c, &span), 0); + /* Two entries plus the header, not the 0x3000 the payloads reach. */ + ck_assert_uint_eq(span, AHAB_HDR_SIZE + (2U * AHAB_IMG_ENTRY_SIZE)); + ck_assert_uint_lt(span, c.size); + + /* Anything past the staging window is refused rather than truncated. */ + c.hdr_len = IMX95_AHAB_SPAN_MAX + 1U; + ck_assert_int_lt(imx95_ahab_span(&c, &span), 0); + c.hdr_len = IMX95_AHAB_SPAN_MAX; + ck_assert_int_eq(imx95_ahab_span(&c, &span), 0); + + /* A header that understates its own length must not shrink the span below + * the table it declares, or the ELE would be given fewer bytes than the + * entries stage 1 goes on to use. */ + setup_valid(); + ck_assert_int_eq(imx95_ahab_parse(&c, IMX95_AHAB_MMC_OFFSET, dev_read, + NULL), 0); + c.hdr_len = AHAB_HDR_SIZE; + c.sig_end = 0U; + ck_assert_int_eq(imx95_ahab_span(&c, &span), 0); + ck_assert_uint_eq(span, AHAB_HDR_SIZE + (c.count * AHAB_IMG_ENTRY_SIZE)); + + /* The signature block can be the furthest thing, and then it sets it. */ + c.sig_end = 0x800U; + ck_assert_int_eq(imx95_ahab_span(&c, &span), 0); + ck_assert_uint_eq(span, 0x800U); + + ck_assert_int_lt(imx95_ahab_span(NULL, &span), 0); + ck_assert_int_lt(imx95_ahab_span(&c, NULL), 0); +} +END_TEST + +/* Destinations are checked before a single byte is written, because an image is + * streamed straight into img->dst. */ +START_TEST(test_check_dst) +{ + struct imx95_ahab_container c; + struct imx95_ahab_region excl[1]; + const uint64_t dram = 0x80000000ULL; + const uint64_t dram_sz = 0x80000000ULL; + + setup_valid(); + ck_assert_int_eq(imx95_ahab_parse(&c, IMX95_AHAB_MMC_OFFSET, dev_read, + NULL), 0); + + excl[0].base = 0x90000000ULL; /* the staging window */ + excl[0].size = 0x10000ULL; + ck_assert_int_eq(imx95_ahab_check_dst(&c, dram, dram_sz, excl, 1), 0); + + /* Outside DRAM, below and above. */ + c.img[0].dst = 0x20480000ULL; + ck_assert_int_lt(imx95_ahab_check_dst(&c, dram, dram_sz, excl, 1), 0); + c.img[0].dst = dram + dram_sz - 0x100ULL; + ck_assert_int_lt(imx95_ahab_check_dst(&c, dram, dram_sz, excl, 1), 0); + + /* Landing on the staged container would rewrite what the ELE blessed. */ + c.img[0].dst = 0x90000000ULL; + ck_assert_int_lt(imx95_ahab_check_dst(&c, dram, dram_sz, excl, 1), 0); + + /* Two images may not overlap, or a later load replaces a verified one. */ + c.img[0].dst = 0x8A200000ULL; + c.img[1].dst = 0x8A200800ULL; + ck_assert_int_lt(imx95_ahab_check_dst(&c, dram, dram_sz, excl, 1), 0); + + /* Overlap only in the block padding still counts. */ + c.img[0].dst = 0x8A200000ULL; + c.img[0].size = 0x1001U; /* rounds up to 0x1200 */ + c.img[1].dst = 0x8A201100ULL; + ck_assert_int_lt(imx95_ahab_check_dst(&c, dram, dram_sz, excl, 1), 0); + + /* dst + length must not wrap 64-bit. */ + setup_valid(); + ck_assert_int_eq(imx95_ahab_parse(&c, IMX95_AHAB_MMC_OFFSET, dev_read, + NULL), 0); + c.img[0].dst = 0xFFFFFFFFFFFFF000ULL; + ck_assert_int_lt(imx95_ahab_check_dst(&c, dram, dram_sz, excl, 1), 0); + + /* A zero-length image is not something a real container carries. */ + setup_valid(); + ck_assert_int_eq(imx95_ahab_parse(&c, IMX95_AHAB_MMC_OFFSET, dev_read, + NULL), 0); + c.img[0].size = 0U; + ck_assert_int_lt(imx95_ahab_check_dst(&c, dram, dram_sz, excl, 1), 0); + + ck_assert_int_lt(imx95_ahab_check_dst(NULL, dram, dram_sz, excl, 1), 0); + ck_assert_int_lt(imx95_ahab_check_dst(&c, dram, 0U, excl, 1), 0); + ck_assert_int_lt(imx95_ahab_check_dst(&c, dram, dram_sz, NULL, 1), 0); +} +END_TEST + +/* The image stage 1 branches into must be an A55 executable entered inside + * itself. */ +START_TEST(test_check_exec) +{ + struct imx95_ahab_container c; + + setup_valid(); + ck_assert_int_eq(imx95_ahab_parse(&c, IMX95_AHAB_MMC_OFFSET, dev_read, + NULL), 0); + ck_assert_int_eq(imx95_ahab_check_exec(&c, 0), 0); + + /* ELE firmware is not something the A55 branches into. */ + c.img[0].flags = IMX95_AHAB_TYPE_ELE | (IMX95_AHAB_CORE_A55 << 4); + ck_assert_int_lt(imx95_ahab_check_exec(&c, 0), 0); + + /* Nor is an M33 image. */ + c.img[0].flags = IMX95_AHAB_TYPE_EXEC | (IMX95_AHAB_CORE_M33 << 4); + ck_assert_int_lt(imx95_ahab_check_exec(&c, 0), 0); + + /* An entry outside the image's own bytes is the interesting case: it is + * what lets a signed-but-weird container redirect the branch. */ + c.img[0].flags = IMX95_AHAB_TYPE_EXEC | (IMX95_AHAB_CORE_A55 << 4); + c.img[0].entry = c.img[0].dst - 4ULL; + ck_assert_int_lt(imx95_ahab_check_exec(&c, 0), 0); + c.img[0].entry = c.img[0].dst + c.img[0].size; + ck_assert_int_lt(imx95_ahab_check_exec(&c, 0), 0); + c.img[0].entry = c.img[0].dst + c.img[0].size - 4ULL; + ck_assert_int_eq(imx95_ahab_check_exec(&c, 0), 0); + + ck_assert_int_lt(imx95_ahab_check_exec(NULL, 0), 0); + ck_assert_int_lt(imx95_ahab_check_exec(&c, c.count), 0); +} +END_TEST + +static Suite *imx95_ahab_suite(void) +{ + Suite *s = suite_create("imx95-ahab"); + TCase *tc = tcase_create("container_parse"); + + tcase_add_test(tc, test_parse_valid); + tcase_add_test(tc, test_next_rounds_up); + tcase_add_test(tc, test_size_covers_signature_block); + tcase_add_test(tc, test_reject_bad_tag_and_version); + tcase_add_test(tc, test_reject_bad_image_count); + tcase_add_test(tc, test_reject_offset_size_overflow); + tcase_add_test(tc, test_reject_signature_block_out_of_range); + tcase_add_test(tc, test_reject_unaligned_base_and_read_failure); + tcase_add_test(tc, test_load_copies_image); + tcase_add_test(tc, test_load_rejects_bad_requests); + tcase_add_test(tc, test_parse_rejects_table_outside_header); + tcase_add_test(tc, test_span_bounds); + tcase_add_test(tc, test_check_dst); + tcase_add_test(tc, test_check_exec); + tcase_add_test(tc, test_overlaps_uses_padded_length); + suite_add_tcase(s, tc); + + return s; +} + +int main(void) +{ + int fails; + Suite *s = imx95_ahab_suite(); + SRunner *sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + + return fails; +}