diff --git a/.github/workflows/test-configs.yml b/.github/workflows/test-configs.yml index 4b973482ae..2e9ce817f5 100644 --- a/.github/workflows/test-configs.yml +++ b/.github/workflows/test-configs.yml @@ -433,6 +433,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 e2b174bf3d..e2e19bcb91 100644 --- a/.gitignore +++ b/.gitignore @@ -256,6 +256,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 6deec40694..f8167a330b 100644 --- a/Makefile +++ b/Makefile @@ -408,6 +408,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 6d405f03a7..d6dca1a827 100644 --- a/arch.mk +++ b/arch.mk @@ -231,6 +231,61 @@ ifeq ($(ARCH),AARCH64) # BOOT_EL1 itself is emitted by options.mk; nothing to add here. 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 + # LPUART1 console, shared with the stage 1 build. + OBJS+=hal/imx95_lpuart.o + # 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 + OBJS+=hal/imx95_usdhc.o + # 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 + # imx95_scmi.o carries the SCMI-over-MU client used by both of the above. + ifneq ($(filter 1,$(IMX95_SCMI_COLD_INIT) $(IMX95_INIT_M7)),) + OBJS+=hal/imx95_scmi.o + 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 @@ -257,7 +312,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..e8be12334d --- /dev/null +++ b/config/examples/imx95-a55.config @@ -0,0 +1,92 @@ +# 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. + +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 (hal/imx95_usdhc.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 + +# 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; the ostree= path pins the deployment. +# audit=0: the audit subsystem writes multi-line records to this console +# during late boot, interleaving with anything else printing there. Nothing +# in this configuration consumes them. +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 3289930bcb..9704cb330b 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -22,6 +22,7 @@ This README describes configuration of supported targets. * [Nordic nRF5340](#nordic-nrf5340) * [Nordic nRF54L15](#nordic-nrf54l15) * [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 Kinetis](#nxp-kinetis) * [NXP Kinetis KL26Z](#nxp-kinetis-kl26z) @@ -9447,6 +9448,33 @@ 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. SPL/ELE authenticate the container itself (AHAB), so the chain is ROM -> ELE -> SPL -> BL31 -> wolfBoot -> Linux once SRK fuses are programmed. 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. + +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, `hal/imx95_usdhc.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 (`hal/imx95_scmi.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. + ## 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..2f654db60d --- /dev/null +++ b/hal/imx95_a55.c @@ -0,0 +1,456 @@ +/* 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. SPL/ELE authenticate the container (anchored + * once SRK fuses are programmed). RAM-resident, no flash: the flash HAL is a + * no-op surface the core references unconditionally. */ + +#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_INIT_M7 +/* SCMI M7 power-domain on + TCM ECC scrub (hal/imx95_scmi.c). */ +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; +} + +/* -------------------------------------------------------------------------- + * Handoff recon: print the machine state BL31 handed us - entry EL, SCTLR + * MMU/cache bits, the x0 pointer - to confirm the BL33 entry contract. This is + * the whole point of the first bring-up image, so it is on by default. + * -------------------------------------------------------------------------- */ + +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 on this + * target: the DTS relocation target, the staging window and the kernel/ramdisk + * loads. hal_prepare_boot() cleans it as one range because the exit's set/way + * clean does not reach the DSU/L3. imx95_check_load_ranges() rejects a + * configuration whose load addresses fall outside it; a payload whose tail + * would run past the top needs the top raised, so keep the two 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 can reach: the ramdisk + * destination plus WOLFBOOT_FIT_MAX_RAMDISK (src/fdt.c, 256 MiB by default) + * lands at 0xCA000000 with this config. Sized past that so a full-size + * initramfs cannot leave its tail dirty, and 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. */ + +static int range_overlaps(uint64_t a_start, uint64_t a_len, + uint64_t b_start, uint64_t b_len) +{ + 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 destination wolfBoot copies a FIT sub-image to must sit in DRAM bank 0, + * inside the span hal_prepare_boot() cleans (or the copy is left dirty in the + * DSU/L3 and the kernel reads it with caches off), and clear of the reserved + * carveouts - vpu_boot starts exactly at the bottom of the flushed span, so + * being inside that span is not by itself proof of being safe. */ +static int imx95_check_dest(const char* name, uint64_t addr, uint64_t len) +{ + int ret = 0; + + if (addr < IMX95_DRAM_BASE || addr >= IMX95_DRAM_END) { + wolfBoot_printf("imx95: %s load address outside DRAM bank 0\n", name); + return -1; + } + if (addr < IMX95_PAYLOAD_FLUSH_BASE || addr >= IMX95_PAYLOAD_FLUSH_TOP) { + wolfBoot_printf("imx95: %s load address 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 other destinations this stage writes to. The kernel's own load + * address comes from the FIT rather than the build, so it is not checked + * here; keep it inside the same span. */ + 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 cleaning misses the A55's DSU system + * cache, so copies made with D-cache on can be stale at MMU-off. With the + * EL2 identity map in place this is load-bearing - the FIT sub-images were + * copied through the D-cache and Linux enters with caches off - so it is + * not an optimization to remove. The SCTLR.C test only skips the work for + * a configuration built without the MMU, where no line can be dirty. */ + 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: the boot and update addresses come from here rather + * than from linker-script partition symbols. The disk updater selects A/B by + * partition index and does not consult these, so they exist for the version + * helpers in libwolfboot.c and both name the one RAM staging window this + * target loads into. A RAM-update path would need two distinct windows. */ +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 written into the DTB. Default is the Toradex + * SMARC iMX95 module's address (OUI 00:14:2d; low bytes are the module serial), + * read from its config block. Override with -DIMX95_ETH0_MAC={...} per board. */ +#ifndef IMX95_ETH0_MAC +#define IMX95_ETH0_MAC { 0x00, 0x14, 0x2d, 0x88, 0x8d, 0x20 } +#endif + +/* Fix up /chosen and /memory on the (already verified) FIT DTB. */ +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. */ + (void)fdt_grow(&ctx, WOLFBOOT_FDT_FIXUP_HEADROOM); + + /* 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) { + fdt_setprop(&ctx, off, "device_type", "memory", 7); + } + } + if (off >= 0) { + uint64_t memreg[4]; + memreg[0] = cpu_to_fdt64(0x90000000ULL); /* bank0 start */ + memreg[1] = cpu_to_fdt64(0x70000000ULL); /* bank0 size */ + memreg[2] = cpu_to_fdt64(0x100000000ULL); /* bank1 start */ + memreg[3] = cpu_to_fdt64(0x180000000ULL); /* 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; + } + + /* Ethernet MAC: the ENETC ports are PCIe-enumerated and carry no + * local-mac-address in the OS DTB (U-Boot normally programs it from the + * Toradex config block), so Linux would pick a random MAC each boot and the + * DHCP address would float. Set the primary port (ethernet0 = end0) to the + * module's address. Non-fatal: networking is not boot-critical. */ + { + static const uint8_t eth0_mac[6] = IMX95_ETH0_MAC; + 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); + } + } + + /* 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; +} diff --git a/hal/imx95_a55.h b/hal/imx95_a55.h new file mode 100644 index 0000000000..ad21aab59b --- /dev/null +++ b/hal/imx95_a55.h @@ -0,0 +1,114 @@ +/* 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 (third image in AHAB container 2, entered by BL31). + * Included from assembly: integer expressions only. Constants consumed by + * src/boot_aarch64_start.S carry no UL suffix, which not every assembler + * accepts. + * Addresses measured on 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) + +/* 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). wolfBoot's console output is gone by + * the time Linux is up, so the verified-boot log cannot be shown on a display + * the board itself drives. Mirroring it into DDR keeps it readable after the + * handoff. The layout and the reader contract are the Cortex-M7 console's from + * hal/imx95_m7.h - magic, monotonic write count, ring size, then the data at + * +16, with byte n stored at data[n % size] - so one reader serves both cores. + * It sits in the tail of the M7 carveout, which the DTB keeps as + * reserved-memory, clear of that core's console page at 0x80F00000 and its + * status block at 0x80F10000. */ +#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 + +/* 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..05127596bf --- /dev/null +++ b/hal/imx95_a55_stage1.c @@ -0,0 +1,442 @@ +/* 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 image from AHAB container 0 into OCRAM and enters it + * at EL3. Everything before it - the ELE firmware, the System Manager on the + * M33 and the OEI that trains DDR - is still the SoC's own, because the ROM + * authenticates and starts them directly. What this replaces is the last image + * in that container, U-Boot SPL, whose whole job is to find the next container + * and load BL31, OP-TEE and BL33 out of it. + * + * DDR is already up by the time this runs: the OEI did it, and SPL only checks + * the power domain. So a stage 1 needs the boot device, the container parser, + * and the handful of platform pokes below that SPL also does. */ + +#include +#include +#include "printf.h" +#include "hal/imx95_a55.h" +#include "hal/imx95_ahab.h" + +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 WDG3_BASE 0x42490000UL +#define WDG4_BASE 0x424A0000UL +#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 GPIO2_BASE 0x43810000UL +#define GPIO3_BASE 0x43820000UL +#define GPIO4_BASE 0x43840000UL +#define GPIO5_BASE 0x43850000UL + +#define SMMU_BASE 0x490D0000UL +#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_BASE 0x47530000UL +#define ELE_MU_PAR (ELE_MU_BASE + 0x004) +#define ELE_MU_TCR (ELE_MU_BASE + 0x120) +#define ELE_MU_TSR (ELE_MU_BASE + 0x124) +#define ELE_MU_RCR (ELE_MU_BASE + 0x128) +#define ELE_MU_RSR (ELE_MU_BASE + 0x12C) +#define ELE_MU_TR(n) (ELE_MU_BASE + 0x200 + ((n) * 4)) +#define ELE_MU_RR(n) (ELE_MU_BASE + 0x280 + ((n) * 4)) +#define ELE_MU_SR (ELE_MU_BASE + 0x00C) +#define ELE_MU_SR_RDR (1UL << 6) /* receive data ready */ +#define ELE_MU_POLL_LOOPS 1000000UL + +#define ELE_VERSION 0x06U +#define ELE_CMD_TAG 0x17U +#define ELE_START_RNG 0xA3U +#define ELE_OK 0xD6U +#define ELE_MAX_MSG 8U + +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(SMMU_BASE + SMMU_GBPA) & SMMU_GBPA_UPDATE) == 0UL) + break; + } + if (n == SMMU_POLL_LOOPS) + return -1; + + /* Use incoming SHCFG attributes */ + wr(SMMU_BASE + SMMU_GBPA, SMMU_GBPA_SHCFG_IN | SMMU_GBPA_UPDATE); + for (n = 0; n < SMMU_POLL_LOOPS; n++) { + if ((rd(SMMU_BASE + SMMU_GBPA) & SMMU_GBPA_UPDATE) == 0UL) + break; + } + if (n == SMMU_POLL_LOOPS) + return -1; + + wr(SMMU_BASE + SMMU_CR0, 0); + for (n = 0; n < SMMU_POLL_LOOPS; n++) { + if (rd(SMMU_BASE + SMMU_CR0_ACK) == 0UL) + break; + } + if (n == SMMU_POLL_LOOPS) + return -1; + + return 0; +} + +/* One ELE message: words are handed over one transmit register at a time and + * the reply comes back the same way. msg[0] carries version, word count, + * command and tag; the first reply word after it is the status. */ +static int ele_call(uint32_t *msg, uint32_t nwords) +{ + uint32_t tr_num, rr_num, i, n, sr; + + if (nwords == 0U || nwords > ELE_MAX_MSG) + return -1; + + tr_num = rd(ELE_MU_PAR) & 0xFFU; + rr_num = (rd(ELE_MU_PAR) >> 8) & 0xFFU; + if (tr_num == 0U || rr_num == 0U) + return -1; + + /* 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)); + } + + for (i = 0; i < nwords; i++) { + for (n = 0; n < ELE_MU_POLL_LOOPS; n++) { + sr = rd(ELE_MU_TSR); + if ((sr & (1UL << (i % tr_num))) != 0UL) + break; + } + if (n == ELE_MU_POLL_LOOPS) + return -1; + wr(ELE_MU_TR(i % tr_num), msg[i]); + } + + for (i = 0; i < nwords; i++) { + for (n = 0; n < ELE_MU_POLL_LOOPS; n++) { + sr = rd(ELE_MU_RSR); + if ((sr & (1UL << (i % rr_num))) != 0UL) + break; + } + if (n == ELE_MU_POLL_LOOPS) + return -1; + msg[i] = rd(ELE_MU_RR(i % rr_num)); + /* The reply header says how many words the ELE is actually sending. */ + if (i == 0U) { + nwords = (msg[0] >> 8) & 0xFFU; + if (nwords == 0U || nwords > ELE_MAX_MSG) + return -1; + } + } + + if (nwords < 2U) + return -1; + if ((msg[1] & 0xFFU) != ELE_OK) + return -1; + return 0; +} + +/* 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(msg, 1); +} + +/* Everything SPL does between the console coming up and the container load. */ +int imx95_stage1_platform_init(void) +{ + int ret; + + wdog_disable(WDG3_BASE); + wdog_disable(WDG4_BASE); + + gpio_reset(GPIO2_BASE); + gpio_reset(GPIO3_BASE); + gpio_reset(GPIO4_BASE); + gpio_reset(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"); + + return 0; +} + +/* Bring up the controller the ROM booted from and point ordinary reads at the + * same place the ROM read the containers from. On eMMC that is a boot + * partition, selected by the same EXT_CSD field that told the ROM which one to + * use; on SD the containers sit in 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 + if (imx95_usdhc2_cold_init() != 0) + return -1; + 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) +{ + wolfBoot_printf("stage1: exception ESR=0x%x%x ELR=0x%x%x FAR=0x%x%x\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 loading this image would land on top of the code doing the + * loading. In the boot chain nothing does - the stage runs from OCRAM and the + * images it loads go to DRAM - but a stage 1 linked into DRAM for bring-up is + * reached by being an image of the very container it then walks. */ +static int stage1_overlaps_self(uint64_t dst, uint32_t size) +{ + uint64_t lo = (uint64_t)(uintptr_t)_start_text; + uint64_t hi = (uint64_t)(uintptr_t)END_STACK; + + return (dst < hi) && ((dst + size) > lo); +} + +/* 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; + + if (imx95_scmi_uart_clk_init() != 0) { + /* Nothing can be reported: this is what the console runs on. */ + return -1; + } + uart_init(); + wolfBoot_printf("\nwolfBoot stage 1: NXP i.MX95 Cortex-A55\n"); + + if (imx95_stage1_platform_init() != 0) + return -1; + + if (imx95_stage1_disk_init() != 0) { + wolfBoot_printf("stage1: boot device init failed\n"); + return -1; + } + + /* 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; + } + next = imx95_ahab_next(&ctnr); + + if (imx95_ahab_parse(&ctnr, next, stage1_read, NULL) != 0) { + wolfBoot_printf("stage1: no container at 0x%x\n", (unsigned)next); + return -1; + } + + for (i = 0; i < ctnr.count; i++) { + if (ctnr.img[i].size == 0U) + continue; + if (stage1_overlaps_self(ctnr.img[i].dst, ctnr.img[i].size)) { + wolfBoot_printf("stage1: skipping image %u at 0x%x (that is us)\n", + (unsigned)i, (unsigned)ctnr.img[i].dst); + continue; + } + 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); + return -1; + } + if (bl31_entry == 0U) + bl31_entry = ctnr.img[i].entry; + } + + if (bl31_entry == 0U) { + wolfBoot_printf("stage1: container has no entry point\n"); + return -1; + } + + 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; +} + +int imx95_stage1_main(void) +{ + int ret = stage1_boot(); + +#ifdef IMX95_STAGE1_PASSTHROUGH + /* Bring-up build: this stage is an extra image in a container that some + * other loader already staged completely, so its job is to be invisible + * when it fails. Production does not define this - there a failure means + * nothing has been loaded, and stopping is the only honest 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..6c857e310e --- /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 image here and enters it at EL3. LENGTH is the + * same 192 KiB budget U-Boot gives SPL, so growth is a link error rather + * than a silent overrun into the stack below. Both come from + * WOLFBOOT_STAGE1_BASE_ADDR / WOLFBOOT_STAGE1_SIZE, so the same sources + * can also be linked somewhere else for bring-up. */ + 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, above the loaded image and below the end of the region. At the + * OCRAM base this is 0x204D6000, the same address U-Boot SPL uses, so it is + * known to be clear of anything the ROM or the OEI left behind. */ +END_STACK = @WOLFBOOT_STAGE1_BASE_ADDR@ + 0x56000; diff --git a/hal/imx95_a55_stage1_start.S b/hal/imx95_a55_stage1_start.S new file mode 100644 index 0000000000..be2feb4891 --- /dev/null +++ b/hal/imx95_a55_stage1_start.S @@ -0,0 +1,160 @@ +/* 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 was + * using. Everything this stage touches is either Device memory or DRAM that a + * later stage reads with its own caches off, so the MMU and both caches are + * turned off up front and left off: no maintenance to get wrong, and every + * store has reached memory by the time BL31 is entered. What the previous + * stage left dirty is written back first - turning the cache off does not do + * that, and anything still in it would simply 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 _stage1_flush_dcache + + /* 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 + +/* Clean and invalidate every data cache level to the point of coherency, by + * set/way. Only correct because this runs on the one core that is out of + * reset; it is the standard sequence for the moment before the cache is + * turned off. */ +_stage1_flush_dcache: + dsb sy + mrs x0, CLIDR_EL1 + ubfx w2, w0, #24, #3 /* level of coherency */ + cbz w2, 5f + mov w1, #0 /* level iterator */ +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 /* last set */ + ubfx w4, w4, #3, #10 /* last 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 CISW, 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 +5: dsb sy + isb + ret + + /* Place the literal pool here rather than letting the assembler put it + * after the vector table, where it would sit in the padding of a vector + * entry. */ + .ltorg + +/* 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..cb66fe3b7f --- /dev/null +++ b/hal/imx95_ahab.c @@ -0,0 +1,155 @@ +/* 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 a set of AHAB containers: container 0 carries the ELE + * firmware, the System Manager and the first A55 image, the next one BL31, + * OP-TEE and BL33. Nothing records where that next container starts - it is + * the end of the previous one (the furthest of its header, its images and its + * signature block) rounded up. Only offsets are read here; the signature block + * belongs to the ELE, which verifies containers in hardware. */ + +#include +#include +#include "hal/imx95_ahab.h" + +/* Container header, all little-endian: + * +0 version +1 length (16-bit) +3 tag + * +4 flags + * +8 sw_version (16-bit) +10 fuse_version +11 num_images + * +12 signature block offset (16-bit) + * followed by num_images 128-byte image entries: + * +0 offset +4 size +8 dst (64-bit) +16 entry (64-bit) +24 flags + * then hash and IV, which only the ELE looks at. */ +#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 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))); + struct imx95_ahab_image *img; + const uint8_t *e; + uint32_t count, sig_off, sig_len, end, i; + + if (ctnr == NULL || 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; + + if (hdr[3] != IMX95_AHAB_TAG || hdr[0] != IMX95_AHAB_VERSION) + return -1; + + count = hdr[11]; + if (count == 0U || count > (uint32_t)IMX95_AHAB_MAX_IMAGES) + return -1; + + ctnr->base = base; + ctnr->count = count; + + /* 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 = rd16(&hdr[1]); + + for (i = 0; i < count; i++) { + e = &hdr[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; + } + + sig_off = rd16(&hdr[12]); + if (sig_off != 0U) { + if ((sig_off + 4U) > (uint32_t)sizeof(hdr)) + return -1; + sig_len = rd16(&hdr[sig_off + 1]); + if ((sig_off + sig_len) > end) + end = sig_off + sig_len; + } + + if (end > (0xFFFFFFFFU - base)) + return -1; + ctnr->size = end; + return 0; +} + +/* Byte offset of the container that follows this one. */ +uint32_t imx95_ahab_next(const struct imx95_ahab_container *ctnr) +{ + uint32_t end = ctnr->base + ctnr->size; + + return (end + (IMX95_AHAB_ALIGN - 1U)) & ~(IMX95_AHAB_ALIGN - 1U); +} + +/* Copy one image from the boot device to dst. Image offsets and sizes are + * block-aligned as the image tool emits them; 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; + + 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; + if ((img->size % IMX95_AHAB_BLOCK) != 0U) + return -1; + + return read_cb(ctx, ctnr->base + img->offset, img->size, dst); +} diff --git a/hal/imx95_ahab.h b/hal/imx95_ahab.h new file mode 100644 index 0000000000..dbc939ec94 --- /dev/null +++ b/hal/imx95_ahab.h @@ -0,0 +1,81 @@ +/* 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. */ +#define IMX95_AHAB_HDR_BYTES 1024 +#define IMX95_AHAB_MAX_IMAGES 7 + +/* Containers start on a 1 KiB boundary on every i.MX95 boot device. */ +#define IMX95_AHAB_ALIGN 1024U + +/* 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 count; + struct imx95_ahab_image img[IMX95_AHAB_MAX_IMAGES]; +}; + +/* 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); +uint32_t imx95_ahab_next(const struct imx95_ahab_container *ctnr); +int imx95_ahab_load(const struct imx95_ahab_container *ctnr, uint32_t index, + void *dst, imx95_ahab_read_cb read_cb, void *ctx); + +#endif /* IMX95_AHAB_H */ diff --git a/hal/imx95_lpuart.c b/hal/imx95_lpuart.c new file mode 100644 index 0000000000..154fbec53a --- /dev/null +++ b/hal/imx95_lpuart.c @@ -0,0 +1,105 @@ +/* imx95_lpuart.c + * + * LPUART1 console for wolfBoot 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 + */ + +#include +#include "printf.h" +#include "hal/imx95_a55.h" + +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; +} + +#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; + + if (!ring_ready) { + /* Publish the magic last: until it is set a reader treats the region + * as absent rather than reading a stale count from the last boot. */ + 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 */ 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) diff --git a/hal/imx95_scmi.c b/hal/imx95_scmi.c new file mode 100644 index 0000000000..6175d81fb5 --- /dev/null +++ b/hal/imx95_scmi.c @@ -0,0 +1,433 @@ +/* imx95_scmi.c + * + * Minimal SCMI-over-MU client for the NXP i.MX95 Cortex-A55 (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 + */ + +/* With no U-Boot ahead of wolfBoot, clocks, pinmux and power domains are still + * owned by the M33 System Manager and must be requested over SCMI; the pad + * registers data-abort on direct access from BL33. This is the smallest client + * that does it: SMT transport over MU2, then clock, pinctrl and power calls. + * Cross-checked against U-Boot drivers/firmware/scmi, clk/clk_scmi.c and + * pinctrl/pinctrl-scmi.c. */ + +#include +#include "printf.h" +#include "hal/imx95_a55.h" + +#if defined(IMX95_SCMI_COLD_INIT) || defined(IMX95_INIT_M7) || \ + defined(IMX95_STAGE1) + +#define MU2_BASE 0x445B0000UL +#define MU2_GCR (MU2_BASE + 0x114) /* set BIT0: ring A2P doorbell */ +#define MU2_GSR (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 + +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; } + +/* One synchronous SCMI command. payload[] holds n_in request words on entry + * and n_out response words on return, word 0 being the SCMI status. The two + * counts differ per message - POWER_STATE_GET takes one word and answers with + * two - and sending the wrong number makes the System Manager read a field + * from the wrong offset. Returns 0 on success. */ +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 (rd(sh + SMT_CHAN_STATUS) & CHAN_FREE) + break; + } + if (n == SCMI_TIMEOUT) + return -1; + + wr(sh + SMT_FLAGS, 0); /* poll, no interrupt */ + wr(sh + SMT_LENGTH, 4 + n_in * 4); /* header word + payload */ + wr(sh + SMT_MSG_HEADER, (proto << 10) | (msg_id & 0xFF)); + for (i = 0; i < n_in; i++) + wr(sh + SMT_PAYLOAD + i * 4, payload[i]); + + /* Hand the channel to the SM and ring the doorbell. */ + wr(sh + SMT_CHAN_STATUS, rd(sh + SMT_CHAN_STATUS) & ~CHAN_FREE); + wr(MU2_GCR, rd(MU2_GCR) | 0x1); + + /* The SM sets FREE again when the response is in place. */ + for (n = 0; n < SCMI_TIMEOUT; n++) { + if (rd(sh + SMT_CHAN_STATUS) & CHAN_FREE) + break; + } + if (n == SCMI_TIMEOUT) + return -2; + wr(MU2_GSR, 0x1); /* clear GIR0 ack */ + + for (i = 0; i < n_out; i++) + payload[i] = rd(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 on the SMARC, not controller pins: + * their pads must first be SCMI-muxed to GPIO3 (ALT5), then driven by direct + * RGPIO3 MMIO (which is permitted from BL33). Bits 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_BASE 0x43820000UL +#define RGPIO3_PDOR (RGPIO3_BASE + 0x40) +#define RGPIO3_PDDR (RGPIO3_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 register offset, ALT mode, pad conf}. The six + * data/clk/cmd pins take the uSDHC2 function (ALT0); the three carrier GPIO + * signals take GPIO3 (ALT5). Values are the SMARC usdhc2/cd/pwr-en/vsel + * 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 = rd(RGPIO3_PDDR); + pddr &= ~(1U << GPIO3_CD); + pddr |= (1U << GPIO3_VSEL) | (1U << GPIO3_PWR); + wr(RGPIO3_PDDR, pddr); + + /* Select 3.3V I/O (VSEL low) before powering the card. */ + pdor = rd(RGPIO3_PDOR); + pdor &= ~(1U << GPIO3_VSEL); + wr(RGPIO3_PDOR, pdor); + + /* Power on, then wait past the regulator startup delay (DTS + * startup-delay-us 20000). Caches are off here, so the loop count is + * deliberately generous. */ + pdor |= (1U << GPIO3_PWR); + wr(RGPIO3_PDOR, pdor); + delay_loops(40000000U); + + SCMI_DBG("scmi: GPIO3 PDDR=0x%x PDOR=0x%x\n", + (unsigned)rd(RGPIO3_PDDR), (unsigned)rd(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). */ +#define IMX95_M7_ITCM_SYS 0x203C0000UL +#define IMX95_M7_DTCM_SYS 0x20400000UL +#define IMX95_M7_TCM_BYTES 0x40000UL + +/* Match U-Boot's power_on_m7(): power up the M7 mix over SCMI, then scrub its + * TCM so that never-written words carry valid ECC. U-Boot does this at board + * init, so wolfBoot must too when it replaces U-Boot as BL33 on a board whose + * Cortex-M7 is launched later by Linux remoteproc. */ +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_BYTES / 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. The ROM has already read the first container + * through it, so this only makes the state explicit rather than depending on + * whatever the ROM happened to leave behind. Eight data lines, no card detect + * and no card power to switch - it is soldered down. */ +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 hal/imx95_usdhc.c 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 */ diff --git a/hal/imx95_usdhc.c b/hal/imx95_usdhc.c new file mode 100644 index 0000000000..9637caf424 --- /dev/null +++ b/hal/imx95_usdhc.c @@ -0,0 +1,991 @@ +/* imx95_usdhc.c + * + * Minimal uSDHC (SD card) driver for wolfBoot 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 + */ + +/* Minimal SD-only driver for the i.MX uSDHC (i.MX 95 RM; same IP as i.MX + * 6/7/8). NOT SDHCI-register-compatible, so src/sdhci.c does not apply. + * PIO reads at 25 MHz default speed, no DMA/tuning/1.8V; disk_write() is + * not implemented (updates are staged by the OS). Module clock and pinmux + * must already be up (SPL/prior stage); only SYS_CTRL dividers are set. */ + +#include +#include +#include "printf.h" +#include "hal/imx95_a55.h" +#ifdef IMX95_EMMC_PROBE +#include "hal/imx95_ahab.h" +#endif + +#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 rd(uint32_t off) +{ + return *(volatile uint32_t*)(usdhc_base + off); +} + +static inline void 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 ((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 ((rd(off) & mask) != 0) + return 0; + } + return -1; +} + +static void usdhc_reset(uint32_t bits) +{ + wr(USDHC_SYS_CTRL, 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 does not touch the vendor registers, so a controller the boot ROM has + * already driven comes back still in its fast-boot mode, with the HS400 DLL + * and tuning it selected. The first command then never completes. U-Boot's + * esdhc_init() puts the same five registers back by hand for this reason; a + * stage that runs after U-Boot inherits them already clean, which is why this + * only shows up when nothing ran first. */ +/* The 80 startup clocks a card needs before its first command. The SD clock + * is normally gated when idle, so it is forced on across the sequence the way + * U-Boot's esdhc_init() does - INITA on its own is not enough on a controller + * the boot ROM has already used. */ +static void usdhc_init_clocks(void) +{ + volatile uint32_t d; + + wr(USDHC_VEND_SPEC, rd(USDHC_VEND_SPEC) | VEND_SPEC_FRC_SDCLK_ON); + wr(USDHC_SYS_CTRL, rd(USDHC_SYS_CTRL) | SYS_CTRL_INITA); + (void)usdhc_wait_clear(USDHC_SYS_CTRL, SYS_CTRL_INITA); + /* Caches are off in this stage, so a plain loop is well over the 1 ms + * U-Boot waits here. */ + for (d = 0; d < 200000U; d++) { } + wr(USDHC_VEND_SPEC, rd(USDHC_VEND_SPEC) & ~VEND_SPEC_FRC_SDCLK_ON); +} + +static void usdhc_vendor_reset(void) +{ + wr(USDHC_MMC_BOOT, 0); + wr(USDHC_MIX_CTRL, 0); + wr(USDHC_CLK_TUNE_CTRL, 0); + wr(USDHC_DLL_CTRL, 0); + wr(USDHC_VEND_SPEC, VEND_SPEC_INIT); +} + +static void usdhc_set_clock(int speed) +{ + uint32_t v; + + v = 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 */ + 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.) */ + wr(USDHC_VEND_SPEC, 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 */ + 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; + wr(USDHC_MIX_CTRL, mix); + } + else { + 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; + + wr(USDHC_CMD_ARG, arg); + wr(USDHC_CMD_XFR_TYP, xfr); + + /* Wait for command complete or error */ + for (n = 0; n < USDHC_TIMEOUT_LOOPS; n++) { + st = rd(USDHC_INT_STATUS); + if (st & INT_CMD_ERRS) { + wr(USDHC_INT_STATUS, INT_CMD_ERRS | INT_CC); + usdhc_reset(SYS_CTRL_RSTC); + return (st & INT_CTOE) ? -2 : -3; + } + if (st & INT_CC) { + wr(USDHC_INT_STATUS, INT_CC); + return 0; + } + } + usdhc_reset(SYS_CTRL_RSTC); + return -1; +} + +/* --- Card bring-up ------------------------------------------------------- */ + +/* CMD6 SWITCH_FUNC, mode 1 (set), function group 1 = 1 (high speed). The card + * answers with a 512-bit status; bits 379:376 hold the function it actually + * selected for group 1, which is the low nibble of the 17th byte received. + * Returns 0 only when the card confirms high speed. */ +static int sd_switch_high_speed(void) +{ + uint32_t sw[16]; + const uint8_t *b = (const uint8_t *)sw; + uint32_t n, st, i; + + wr(USDHC_BLK_ATT, (1UL << 16) | 64UL); + 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 = rd(USDHC_INT_STATUS); + if (st & INT_DATA_ERRS) { + wr(USDHC_INT_STATUS, INT_DATA_ERRS); + goto restore; + } + if (rd(USDHC_PRES_STATE) & PRES_BREN) + break; + } + if (n == USDHC_TIMEOUT_LOOPS) + goto restore; + wr(USDHC_INT_STATUS, INT_BRR); + for (i = 0; i < 16; i++) + sw[i] = rd(USDHC_DATA_BUFF_ACC); + if (usdhc_wait_set(USDHC_INT_STATUS, INT_TC) != 0) + goto restore; + wr(USDHC_INT_STATUS, INT_TC); + + /* Restore the block geometry the read path expects. */ + wr(USDHC_WTMK_LVL, (128UL << 16) | 128UL); + 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); + wr(USDHC_WTMK_LVL, (128UL << 16) | 128UL); + wr(USDHC_BLK_ATT, (1UL << 16) | SD_BLOCK_SIZE); + return -1; +} + +#if defined(DISK_EMMC) || defined(IMX95_EMMC_PROBE) + +/* EXT_CSD byte 179. Bits [2:0] select which partition ordinary read commands + * address; bits [5:3] select which one the boot ROM loads from. Only the + * access bits may be touched here - rewriting 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; + + 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 = rd(USDHC_INT_STATUS); + if (st & INT_DATA_ERRS) { + wr(USDHC_INT_STATUS, INT_DATA_ERRS); + usdhc_reset(SYS_CTRL_RSTC | SYS_CTRL_RSTD); + return -1; + } + if (rd(USDHC_PRES_STATE) & PRES_BREN) + break; + } + if (n == USDHC_TIMEOUT_LOOPS) + return -1; + wr(USDHC_INT_STATUS, INT_BRR); + for (i = 0; i < SD_BLOCK_SIZE / 4; i++) + out[i] = rd(USDHC_DATA_BUFF_ACC); + if (usdhc_wait_set(USDHC_INT_STATUS, INT_TC) != 0) + return -1; + 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(); + wr(USDHC_PROT_CTRL, PROT_CTRL_EMODE_LE | PROT_CTRL_DTW_1BIT | + PROT_CTRL_CDTL | PROT_CTRL_CDSS); + wr(USDHC_WTMK_LVL, (128UL << 16) | 128UL); + wr(USDHC_INT_STATUS_EN, 0xFFFFFFFFUL); + 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)rd(USDHC_SYS_CTRL), (unsigned)rd(USDHC_PRES_STATE), + (unsigned)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)rd(USDHC_PRES_STATE), + (unsigned)rd(USDHC_INT_STATUS)); + return -1; + } + rsp = 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) { + wr(USDHC_PROT_CTRL, + (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. */ + 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 */ + wr(USDHC_WTMK_LVL, (128UL << 16) | 128UL); + /* Enable status bits (polled; signals stay off) */ + wr(USDHC_INT_STATUS_EN, 0xFFFFFFFFUL); + 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)rd(USDHC_SYS_CTRL), (unsigned)rd(USDHC_PRES_STATE), + (int)((rd(USDHC_PRES_STATE) >> 3) & 1U), + (int)((rd(USDHC_PRES_STATE) >> 16) & 1U), + (unsigned)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)rd(USDHC_CMD_RSP0)); + if (ret != 0) { + wolfBoot_printf("usdhc: CMD8 failed (%d) - SD v1 card?\n", ret); + return -1; + } + if ((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 = 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)(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; + wr(USDHC_PROT_CTRL, + (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 bus ceiling from 25 to 50 MHz, which is most of + * the read time on an image of any size. CMD6 returns a 64-byte status; + * only switch the controller after the card confirms it took the mode, + * because driving 50 MHz at a card still in default speed corrupts data + * silently. A card that declines simply stays at 25 MHz. */ + 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; + } + + 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 = rd(USDHC_INT_STATUS); + if (st & INT_DATA_ERRS) { + wr(USDHC_INT_STATUS, INT_DATA_ERRS); + usdhc_reset(SYS_CTRL_RSTC | SYS_CTRL_RSTD); + return -1; + } + if (rd(USDHC_PRES_STATE) & PRES_BREN) + break; + } + if (n == USDHC_TIMEOUT_LOOPS) { + usdhc_reset(SYS_CTRL_RSTC | SYS_CTRL_RSTD); + return -1; + } + wr(USDHC_INT_STATUS, INT_BRR); + for (w = 0; w < SD_BLOCK_SIZE / 4; w++) + *out++ = 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; + } + 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. */ +#define IOMUXC_BASE 0x443C0000UL +#define RGPIO3_BASE 0x43820000UL /* PDOR 0x40 PSOR 0x44 PDDR 0x54 */ + +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)rd(USDHC_SYS_CTRL), (unsigned)rd(USDHC_PRES_STATE), + (unsigned)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)rd(USDHC_PRES_STATE), + (int)((rd(USDHC_PRES_STATE) >> 3) & 1U)); + + wolfBoot_printf("probe: reading IOMUXC\n"); + for (i = 0; i < 6U; i++) { + v = *(volatile uint32_t*)(IOMUXC_BASE + mux_off[i]); + wolfBoot_printf("probe: mux[%08x]=%08x cfg=%08x\n", + (unsigned)mux_off[i], (unsigned)v, + (unsigned)*(volatile uint32_t*)(IOMUXC_BASE + cfg_off[i])); + } + + wolfBoot_printf("probe: writing IOMUXC (mode0, conf 0x138e/0x158e)\n"); + for (i = 0; i < 6U; i++) { + *(volatile uint32_t*)(IOMUXC_BASE + mux_off[i]) = 0U; + *(volatile uint32_t*)(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*)(IOMUXC_BASE + 0x1a4)); + + wolfBoot_printf("probe: reading GPIO3\n"); + wolfBoot_printf("probe: PDOR=%08x PDDR=%08x\n", + (unsigned)*(volatile uint32_t*)(RGPIO3_BASE + 0x40), + (unsigned)*(volatile uint32_t*)(RGPIO3_BASE + 0x54)); + wolfBoot_printf("probe: card power on (GPIO3.7)\n"); + *(volatile uint32_t*)(RGPIO3_BASE + 0x54) |= (1U << 7); /* PDDR out */ + *(volatile uint32_t*)(RGPIO3_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); +} + +/* Read the SoC's own boot containers out of an eMMC boot partition and report + * what is there. This is how the eMMC path is proven before anything depends + * on it: it runs inside an SD-booting image, against the other controller, and + * restores the SD selection afterwards, so a failure here cannot disturb the + * boot in progress. Nothing is loaded - only the offsets are walked. */ +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; + 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)); + } + off = imx95_ahab_next(&ctnr); + } + } + (void)imx95_emmc_select_partition(0); + +restore: + usdhc_base = saved_base; + card_ready = saved_ready; + card_rca = saved_rca; + card_high_cap = saved_cap; +} +#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; + + 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/include/hal.h b/include/hal.h index f042023c66..30d2e15f8d 100644 --- a/include/hal.h +++ b/include/hal.h @@ -71,9 +71,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) diff --git a/options.mk b/options.mk index ce6915d098..bfe3f7698b 100644 --- a/options.mk +++ b/options.mk @@ -802,10 +802,11 @@ 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 selects which one: the Cadence SDHCI driver (src/sdhci.c, the +# default), the Freescale eSDHC driver (hal/nxp_esdhc.o) or the i.MX uSDHC +# driver (hal/imx95_usdhc.o) - the latter two are added by the target's arch.mk +# block, which also sets DISK_DRIVER. 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..57d4e7d0f0 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,12 @@ 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. */ + if (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 +213,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_start.S b/src/boot_aarch64_start.S index 3334d6e553..4665cad140 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 @@ -595,6 +599,10 @@ invalidatecaches_end: * ENETC) needs Outer-Shareable to snoop the A72 caches; Inner-Shareable only * covers the CPU cluster. */ .set MemoryOSH, 0x405 | (2 << 8) | (0x0) +/* Normal Non-Cacheable, inner shareable (MAIR index 0). For DRAM shared with + * a core or an agent outside this cluster's coherency, where a cached copy + * would simply not be seen. */ +.set MemoryNC, 0x401 | (3 << 8) | (0x0) #ifndef TARGET_nxp_ls1028a .section .mmu_tbl0,"a" @@ -1325,19 +1333,66 @@ _cm4_entry: b 7b 8: -#ifdef TARGET_tegra234 - /* Use the reserved in-DRAM stack from hal/tegra234.ld. The generic path - * sets sp to the image base, which on this target is the BL33 load address - * - growing down from there runs into memory wolfBoot does not own. */ +#if defined(TARGET_tegra234) || defined(TARGET_imx95_a55) + /* Use the reserved in-DRAM stack from the target's linker script. The + * generic path sets sp to the image base, which on these targets is the + * BL33 load address - growing down from there runs into memory wolfBoot + * does not own (OP-TEE, on i.MX95). */ ldr x1, =END_STACK #endif mov sp, x1 // set stack pointer -#ifdef TARGET_tegra234 +#if defined(TARGET_tegra234) || 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 as Normal cacheable before entering C. BL31 hands over + * with the MMU and caches off, so wolfBoot would otherwise hash a FIT one + * uncached DRAM access at a time. Everything below runs at EL2. + * + * Gated on EL2_HYPERVISOR because that is what compiles the teardown the + * ARM64 boot protocol requires (el2_flush_disable_mmu_and_boot, called from + * do_boot). Turning the MMU on without it would hand Linux a live MMU and + * dirty caches, so the two must not be able to drift apart. */ + dsb sy + isb + tlbi alle2 + dsb sy + isb + ic iallu /* the I-cache is about to be enabled */ + dsb sy + isb + /* The cache RAMs hold random tags out of a cold reset and the previous + * stage cleaned what it owned before turning the cache off, so invalidate + * rather than clean: there is nothing of ours to preserve, 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. @@ -1414,6 +1469,129 @@ 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 every data cache level by set/way. Used once, before the D-cache + * is enabled: out of a cold reset the cache RAMs hold random tags and dirty + * bits, and the stage that handed over cleaned what it owned before turning + * the cache off, so there is nothing to preserve 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 translation tables (4KB granule, 40-bit VA/PA, T0SZ=24). + * + * wolfBoot ran MMU-off here originally, which cost far more than it saved: the + * SHA-256 over a FIT is a DRAM round trip per load, and arch.mk has to keep the + * target off the wolfcrypt ARM assembly because its NEON loads fault without a + * map. An identity map with DRAM marked Normal cacheable fixes both. + * + * 0x00000000-0x7FFFFFFF Device (boot ROM, OCRAM, M7 TCM system view, + * LPUART1 0x44380000, MU2 0x445B0000 and its + * SCMI shared buffer, RGPIO3 0x43820000, + * uSDHC, PCIe/ENETC 0x4CA00000) + * 0x80000000-0xBFFFFFFF DRAM, via L2 so the shared carveout can differ + * 0xC0000000-0xFFFFFFFF DRAM Normal WB cacheable + * 0x100000000-0x27FFFFFFF DRAM bank 1 (6 GiB) Normal WB cacheable + * everything above invalid - a stray access should fault, not wander + * + * The SCMI shared buffer stays Device: the System Manager on the M33 writes it + * from outside this cluster's coherency, so a cached view could be stale. + * ========================================================================== */ +/* 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 carveout the DTB reserves for the Cortex-M7. It + * holds that core's console ring, wolfBoot's own log ring and the M7 payload + * Linux loads later, all read by software this cache cannot reach. Normal + * Non-Cacheable so a write is visible without maintenance. */ +.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 */ @@ -1746,6 +1924,91 @@ el2_flush_and_disable_mmu: isb ret + +/* el2_flush_disable_mmu_and_boot(entry, dts): fused flush + MMU-off + jump + * (x0 = dts, x1-x3 zeroed) touching NO memory after the disable. With the + * D-cache off, a stack reload of entry/dts between a flush call and a + * separate C jump can read stale DRAM (set/way misses a DSU system cache). + * The set/way loop clobbers x0-x11 only; x20/x21 carry the values. */ +.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 */ #if defined(WOLFBOOT_ZYNQMP_FSBL) diff --git a/stage1/Makefile b/stage1/Makefile index 1a70031f60..9926e501c1 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,26 @@ 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 \ + ./hal/imx95_ahab.o \ + ./hal/imx95_lpuart.o \ + ./hal/imx95_scmi.o \ + ./hal/imx95_usdhc.o \ + ./src/string.o + CFLAGS+=-DIMX95_STAGE1 + ASFLAGS+=-DIMX95_STAGE1 + # 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 +100,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 9306819684..357508781a 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -72,6 +72,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 \ @@ -713,6 +714,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 diff --git a/tools/unit-tests/unit-imx95-ahab.c b/tools/unit-tests/unit-imx95-ahab.c new file mode 100644 index 0000000000..4177219063 --- /dev/null +++ b/tools/unit-tests/unit-imx95-ahab.c @@ -0,0 +1,310 @@ +/* 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; + + setup_valid(); + ck_assert_int_eq(imx95_ahab_parse(&c, IMX95_AHAB_MMC_OFFSET, dev_read, + NULL), 0); + ck_assert_uint_eq(imx95_ahab_next(&c), IMX95_AHAB_MMC_OFFSET + 0x3000); + + /* An end that is not already aligned must round up, not truncate. */ + c.size = 0x3001; + ck_assert_uint_eq(imx95_ahab_next(&c), + (IMX95_AHAB_MMC_OFFSET + 0x3400)); +} +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); + c.img[0].offset = 0x1000; + c.img[0].size = 0x101; + ck_assert_int_lt(imx95_ahab_load(&c, 0, out, dev_read, NULL), 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); + 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; +}