From 0f099cf46817ed313646ce884e048005db82f92e Mon Sep 17 00:00:00 2001 From: David Garske Date: Fri, 18 Sep 2026 08:55:47 -0700 Subject: [PATCH] Add NXP i.MX 8QuadMax MEK port including BL33 support --- .github/workflows/test-configs.yml | 63 + .gitignore | 7 + Makefile | 7 + arch.mk | 73 + config/examples/imx8qm-mek-qspi.config | 37 + config/examples/imx8qm-mek.config | 41 + docs/Targets.md | 281 +++ hal/cm4.c | 68 +- hal/imx8qm.c | 1721 ++++++++++++++++++ hal/imx8qm.h | 340 ++++ hal/imx8qm.ld | 74 + hal/tegra234.c | 33 +- hal/versal.c | 19 +- include/aarch64_arch.h | 111 ++ src/boot_aarch64_start.S | 96 +- src/sdhci.c | 194 +- src/update_disk.c | 3 + test-app/app_imx8qm.c | 111 ++ test-app/boot_arm64_start.S | 3 +- tools/scripts/imx8qm/imx8qm-ahab-sign.sh | 115 ++ tools/scripts/imx8qm/imx8qm-flash.sh | 76 + tools/scripts/imx8qm/imx8qm-mkflashbin.sh | 166 ++ tools/unit-tests/Makefile | 77 +- tools/unit-tests/unit-imx8qm-flexspi-write.c | 512 ++++++ tools/unit-tests/unit-imx8qm-usdhc-shim.c | 537 ++++++ tools/unit-tests/unit-sdhci-cmd-inhibit.c | 369 ++++ tools/unit-tests/unit-update-disk.c | 27 + 27 files changed, 5004 insertions(+), 157 deletions(-) create mode 100644 config/examples/imx8qm-mek-qspi.config create mode 100644 config/examples/imx8qm-mek.config create mode 100644 hal/imx8qm.c create mode 100644 hal/imx8qm.h create mode 100644 hal/imx8qm.ld create mode 100644 include/aarch64_arch.h create mode 100644 test-app/app_imx8qm.c create mode 100755 tools/scripts/imx8qm/imx8qm-ahab-sign.sh create mode 100755 tools/scripts/imx8qm/imx8qm-flash.sh create mode 100755 tools/scripts/imx8qm/imx8qm-mkflashbin.sh create mode 100644 tools/unit-tests/unit-imx8qm-flexspi-write.c create mode 100644 tools/unit-tests/unit-imx8qm-usdhc-shim.c create mode 100644 tools/unit-tests/unit-sdhci-cmd-inhibit.c diff --git a/.github/workflows/test-configs.yml b/.github/workflows/test-configs.yml index 4b973482ae..fb48b6e350 100644 --- a/.github/workflows/test-configs.yml +++ b/.github/workflows/test-configs.yml @@ -461,6 +461,69 @@ jobs: config-file: ./config/examples/tegra234-sdcard.config make-args: wolfboot.bin test-app/image_v1_signed.bin CROSS_COMPILE=aarch64-linux-gnu- + imx8qm_test: + uses: ./.github/workflows/test-build-aarch64.yml + with: + arch: aarch64 + # Bundled payload in DRAM, no storage driver. The target's full + # MAIN_TARGET and what the packaging script builds. + config-file: ./config/examples/imx8qm-mek.config + make-args: wolfboot.bin test-app/image_v1_signed.bin + + imx8qm_linux_test: + uses: ./.github/workflows/test-build-aarch64.yml + with: + arch: aarch64 + # Linux FIT off the SD card: MMU, EL2->EL1 and the DTB code path. + config-file: ./config/examples/imx8qm-mek.config + make-args: DISK_SDCARD=1 IMX8QM_MMU=1 EL2_HYPERVISOR=1 BOOT_EL1=1 wolfboot.bin test-app/image_v1_signed.bin + + imx8qm_sdcard_test: + uses: ./.github/workflows/test-build-aarch64.yml + with: + arch: aarch64 + # SD card (uSDHC2): keeps the uSDHC-to-SDHCI register shim compiled. + config-file: ./config/examples/imx8qm-mek.config + make-args: DISK_SDCARD=1 wolfboot.bin test-app/image_v1_signed.bin + + imx8qm_emmc_test: + uses: ./.github/workflows/test-build-aarch64.yml + with: + arch: aarch64 + # eMMC (uSDHC1): the only imx8qm build that compiles the DISK_EMMC + # branches of src/sdhci.c. + config-file: ./config/examples/imx8qm-mek.config + make-args: DISK_EMMC=1 wolfboot.bin test-app/image_v1_signed.bin + + imx8qm_sdcard_fs_test: + uses: ./.github/workflows/test-build-aarch64.yml + with: + arch: aarch64 + # Keeps the read-only filesystem layer (src/disk_fs.c, src/fat32.c, + # src/ext4.c) compiled against this target. + config-file: ./config/examples/imx8qm-mek.config + make-args: DISK_SDCARD=1 DISK_FS=both wolfboot.bin test-app/image_v1_signed.bin + + imx8qm_qspi_test: + uses: ./.github/workflows/test-build-aarch64.yml + with: + arch: aarch64 + # FlexSPI0 serial NOR: the only imx8qm build that compiles the + # ext_flash_* FlexSPI driver and the partition-based update flow. Its + # memory map differs enough to warrant its own config. + config-file: ./config/examples/imx8qm-mek-qspi.config + make-args: wolfboot.bin test-app/image_v1_signed.bin + + imx8qm_no_scu_test: + uses: ./.github/workflows/test-build-aarch64.yml + with: + arch: aarch64 + # arch.mk sets IMX8QM_SCU ?= 1, so every other imx8qm job already compiles + # the SCU (MU1_A) client. This is the one job that builds it OUT, which is + # the chainload path and is otherwise never compiled in CI. + config-file: ./config/examples/imx8qm-mek.config + make-args: DISK_SDCARD=1 IMX8QM_SCU=0 wolfboot.bin test-app/image_v1_signed.bin + cm4_test: uses: ./.github/workflows/test-build-aarch64.yml with: diff --git a/.gitignore b/.gitignore index e2b174bf3d..eebee378f9 100644 --- a/.gitignore +++ b/.gitignore @@ -481,11 +481,15 @@ tools/unit-tests/unit-dice-token-size tools/unit-tests/unit-efi-x86-open-image tools/unit-tests/unit-flash-write-cc26x2 tools/unit-tests/unit-ls1028a-xspi-write +tools/unit-tests/unit-imx8qm-usdhc-shim +tools/unit-tests/unit-imx8qm-flexspi-write tools/unit-tests/unit-nvm-cache-scrub tools/unit-tests/unit-p1021-fcm-bytes tools/unit-tests/unit-p1021-qe-firmware tools/unit-tests/unit-pic32-pfswap tools/unit-tests/unit-pkcs11-nsc-zeroize +tools/unit-tests/unit-sdhci-cmd-inhibit +tools/unit-tests/unit-sdhci-cmd-inhibit-uhs tools/unit-tests/unit-sdhci-uhs-recover tools/unit-tests/unit-sdhci-wait-busy tools/unit-tests/unit-stm32l5-write @@ -555,3 +559,6 @@ tools/fs-test/fixtures/ # x86 FSP QEMU filesystem test images (make_hd.sh FS=1) fs_a.img fs_b.img +tools/unit-tests/aarch64_arch_host.h +tools/unit-tests/imx8qm_usdhc_extract.h +tools/unit-tests/imx8qm_flexspi_extract.h diff --git a/Makefile b/Makefile index 6deec40694..fde2838558 100644 --- a/Makefile +++ b/Makefile @@ -412,6 +412,13 @@ ifeq ($(TARGET),cm4) MAIN_TARGET:=wolfboot.bin endif +# i.MX 8QuadMax runs as the bare-metal BL33 loaded into DRAM by ATF, so there +# is no contiguous flash factory.bin. Build the bootloader plus the signed +# payload, which the packaging script assembles into the BL33 image. +ifeq ($(TARGET),imx8qm) + MAIN_TARGET:=wolfboot.bin test-app/image_v1_signed.bin +endif + # i.MX95 M7 runs from ITCM (loaded by the Linux remoteproc driver); the payload # lives in DDR at 0x80100000, so there is no contiguous flash image to assemble. ifeq ($(TARGET),imx95_m7) diff --git a/arch.mk b/arch.mk index 6d405f03a7..ef5fa53c25 100644 --- a/arch.mk +++ b/arch.mk @@ -231,6 +231,79 @@ ifeq ($(ARCH),AARCH64) # BOOT_EL1 itself is emitted by options.mk; nothing to add here. endif + ifeq ($(TARGET),imx8qm) + # Bare-metal wolfBoot as BL33, replacing U-Boot in the NXP boot container. + # SCFW trains DDR before any A-core runs. A53 is the lowest common + # denominator across the cluster pair; -mstrict-align because the MMU is off. + ARCH_FLAGS=-mcpu=cortex-a53+crypto -march=armv8-a+crypto -mstrict-align + CFLAGS+=$(ARCH_FLAGS) -DCORTEX_A53 + # BL33 entry address. Must agree with ORIGIN in hal/imx8qm.ld. + WOLFBOOT_ORIGIN=0x80020000 + # The RVBAR write in the full startup path is a ZynqMP register. + CFLAGS+=-DSKIP_RVBAR=1 + # On by default: the SCU owns the console's power, clock and pads, so this + # is not optional at real BL33 entry. + IMX8QM_SCU ?= 1 + ifeq ($(IMX8QM_SCU),1) + CFLAGS+=-DIMX8QM_SCU=1 + endif + # Cacheable DRAM for load-and-verify, torn down before handoff. + ifeq ($(IMX8QM_MMU),1) + # The teardown needs the stack-free assembly routine in + # src/boot_aarch64_start.S, which this selects. + CFLAGS+=-DIMX8QM_MMU -DWOLFBOOT_AARCH64_MMU_TEARDOWN + # With the MMU on, DRAM is Normal cacheable and the wolfcrypt ARMv8 + # assembly is safe: its NEON multi-register loads are only a problem + # while memory is Device-typed, which is why the shared AArch64 block + # defaults NO_ARM_ASM=1. Worth having, because SHA-384 dominates this + # target's boot: 4011 ms to 557 ms over a 32 MB image, hardware + # measured, with the signature still verifying. Set NO_ARM_ASM=1 to + # opt back out. This is first, so it wins the later ?= default. + NO_ARM_ASM ?= 0 + endif + # Opt-in: pin the SD node to 3.3V high-speed and cap its clock. Off by + # default, since the board runs DDR50 at 50 MHz. Set it to keep Linux out + # of UHS, which is what makes a warm reboot work: once the card is at + # 1.8V, a reboot does no VDD cycle and wolfBoot's 3.3V CMD8 times out. + ifeq ($(IMX8QM_SD_NO_UHS),1) + CFLAGS+=-DIMX8QM_SD_NO_UHS + endif + # uSDHC board facts. Here rather than in the config because a command-line + # CFLAGS_EXTRA= would replace the config's, whereas CFLAGS is additive. + ifneq ($(filter 1,$(DISK_SDCARD) $(DISK_EMMC)),) + # Card detect is lsio_gpio5[22], so PRES_STATE bit 16 never sets. + # SDMA is on: the read is the dominant boot cost and PIO caps it near + # 1.6 MB/s regardless of bus clock. IMX8QM_SDHCI_PIO=1 falls back. + CFLAGS+=-DSDHCI_FORCE_CARD_DETECT + ifeq ($(IMX8QM_SDHCI_PIO),1) + CFLAGS+=-DSDHCI_SDMA_DISABLED + endif + # Read the payload in 64 KB chunks rather than the 512-byte default. + # A 32 MB image is 63083 separate commands at 512 bytes, and every one + # of them is below SDHCI_DMA_THRESHOLD (4 KB), so the transfer never + # reaches the SDMA path and pays full per-command overhead instead. + DISK_BLOCK_SIZE ?= 65536 + CFLAGS+=-DDISK_BLOCK_SIZE=$(DISK_BLOCK_SIZE) + # Clock ceiling per medium: the SD is a removable slot whose sustained + # reads fail CRC above 25 MHz here; the soldered 8-bit eMMC has no such + # limit and takes the 3.3V high-speed rate. DISK_EMMC wins if both are + # set, matching the base-address selection in hal/imx8qm.c. + ifeq ($(DISK_EMMC),1) + IMX8QM_USDHC_MAX_CLK_KHZ ?= 52000 + else ifeq ($(IMX8QM_SD_NO_UHS),1) + IMX8QM_USDHC_MAX_CLK_KHZ ?= 25000 + else + IMX8QM_USDHC_MAX_CLK_KHZ ?= 50000 + endif + CFLAGS+=-DIMX8QM_USDHC_MAX_CLK_KHZ=$(IMX8QM_USDHC_MAX_CLK_KHZ) + endif + # MMU/WOLFBOOT_FDT/DUALBOOT come from the shared AARCH64 block and only pull + # in the FDT codepath; imx8qm still runs MMU-off, 1:1. + ifeq ($(EL2_HYPERVISOR),1) + CFLAGS+=-DEL2_HYPERVISOR=1 + 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 diff --git a/config/examples/imx8qm-mek-qspi.config b/config/examples/imx8qm-mek-qspi.config new file mode 100644 index 0000000000..6eb674a3c5 --- /dev/null +++ b/config/examples/imx8qm-mek-qspi.config @@ -0,0 +1,37 @@ +# NXP i.MX 8QuadMax MEK - bare-metal BL33, FlexSPI0 serial NOR boot. Separate +# from imx8qm-mek.config because this is a different memory map, not a toggle. +# Set SW2 to Octal SPI (POS-4 and POS-5 ON). +# +# Read, erase and program are hardware-validated against the on-board NOR. +# See the FlexSPI notes in docs/Targets.md. +ARCH=AARCH64 +TARGET=imx8qm +SIGN?=ECC384 +HASH?=SHA384 +DEBUG?=1 +DEBUG_UART?=1 +SPMATH?=1 +SKIP_GIC_INIT?=1 + +# wolfBoot runs from DRAM as BL33; the image is copied out of NOR before verify. +EXT_FLASH=1 +SPI_FLASH=0 +NO_XIP=1 +# FlexSPI0 AHB (memory-mapped read) window. +ARCH_FLASH_OFFSET?=0x08000000 +# MT35XU512ABA: 128 KiB sectors per "sf probe". Partitions are aligned to it, +# and the first 1 MB is left free for the boot container at offset 0. +WOLFBOOT_SECTOR_SIZE=0x20000 +WOLFBOOT_PARTITION_SIZE?=0x400000 +WOLFBOOT_PARTITION_BOOT_ADDRESS?=0x08100000 +WOLFBOOT_PARTITION_UPDATE_ADDRESS?=0x08500000 +# Unused: arch.mk selects update_ram.o for non-disk aarch64 targets. +WOLFBOOT_PARTITION_SWAP_ADDRESS?=0x08900000 +# Needed only because this config uses real partitions. +WOLFBOOT_DTS_BOOT_ADDRESS?=0x08A00000 +WOLFBOOT_DTS_UPDATE_ADDRESS?=0x08B00000 + +# Staging must clear the DTB's carveouts and the CMA pool at 0xc0000000. +WOLFBOOT_RAMBOOT_MAX_SIZE=0x4000000 +WOLFBOOT_LOAD_ADDRESS?=0xA0000000 +WOLFBOOT_LOAD_DTS_ADDRESS?=0xA8000000 diff --git a/config/examples/imx8qm-mek.config b/config/examples/imx8qm-mek.config new file mode 100644 index 0000000000..96d1d42957 --- /dev/null +++ b/config/examples/imx8qm-mek.config @@ -0,0 +1,41 @@ +# NXP i.MX 8QuadMax MEK - bare-metal BL33, replacing U-Boot in the NXP boot +# container. As shipped: payload bundled in BL33, verified in DRAM, booted at +# EL2. NOR has its own memory map: imx8qm-mek-qspi.config. +# +# Variants are make options, not separate configs, and are documented with their +# effects in docs/Targets.md. Do not uncomment them here: +# DISK_SDCARD=1 DISK_EMMC=1 DISK_FS=fat32|ext4|both IMX8QM_MMU=1 +# EL2_HYPERVISOR=1 BOOT_EL1=1 IMX8QM_SCU=0 NO_ARM_ASM=1 +# IMX8QM_USDHC_MAX_CLK_KHZ= IMX8QM_SD_NO_UHS=1 DISK_BLOCK_SIZE= +# IMX8QM_SDHCI_PIO=1 BOOT_BENCHMARK=1 +# e.g. make DISK_SDCARD=1 IMX8QM_MMU=1 EL2_HYPERVISOR=1 BOOT_EL1=1 # Linux FIT +# +# Never pass CFLAGS_EXTRA= on the command line - it replaces, not appends. +ARCH=AARCH64 +TARGET=imx8qm +SIGN?=ECC384 +HASH?=SHA384 +DEBUG?=1 +DEBUG_UART?=1 +SPMATH?=1 +NO_XIP?=1 +SKIP_GIC_INIT?=1 +WOLFBOOT_SECTOR_SIZE?=0x1000 +WOLFBOOT_NO_PARTITIONS=1 +# Bounds the test-app link; a disk build reads its payload off the medium. +WOLFBOOT_PARTITION_SIZE?=0x100000 + +# A/B slots by GPT label, so they survive repartitioning. Disk builds only. +CFLAGS_EXTRA+=-DBOOT_LABEL_A='"boot_a"' +CFLAGS_EXTRA+=-DBOOT_LABEL_B='"boot_b"' +#CFLAGS_EXTRA+=-DBOOT_PART_A=0 +#CFLAGS_EXTRA+=-DBOOT_PART_B=1 +#CFLAGS_EXTRA+=-DBOOT_FILE_A='"/boot/fitImage_A.itb"' # with DISK_FS +#CFLAGS_EXTRA+=-DBOOT_FILE_B='"/boot/fitImage_B.itb"' +#WOLFBOOT_FS_CACHE_SIZE=512 + +# Staging must clear the DTB's carveouts (~0x90000000-0x9c000000) and the CMA +# pool at 0xc0000000. Keep LOAD_DTS_ADDRESS above LOAD_ADDRESS + RAMBOOT_MAX. +WOLFBOOT_RAMBOOT_MAX_SIZE=0x4000000 +WOLFBOOT_LOAD_ADDRESS?=0xA0000000 +WOLFBOOT_LOAD_DTS_ADDRESS?=0xA8000000 diff --git a/docs/Targets.md b/docs/Targets.md index 3289930bcb..8ad5714b5f 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -21,6 +21,7 @@ This README describes configuration of supported targets. * [Nordic nRF52840](#nordic-nrf52840) * [Nordic nRF5340](#nordic-nrf5340) * [Nordic nRF54L15](#nordic-nrf54l15) +* [NXP i.MX 8QuadMax](#nxp-imx-8quadmax) * [NXP i.MX95 Cortex-M7](#nxp-imx95-cortex-m7) * [NXP iMX-RT](#nxp-imx-rt) * [NXP Kinetis](#nxp-kinetis) @@ -9357,6 +9358,286 @@ Boot success marked. Version: 1 | `MAX3266X_OLD` | Build TPU acceleration against the older, deprecated Maxim SDK tree instead of the modern MSDK. | +## NXP i.MX 8QuadMax + +wolfBoot runs on the NXP i.MX 8QuadMax (MIMX8QM: 2x Cortex-A72 + 4x Cortex-A53) as the bare-metal **BL33** firmware stage, replacing U-Boot inside the NXP boot container. Developed against the i.MX 8QuadMax Multisensory Enablement Kit (MCIMX8QM-CPU, "MEK"). + +The boot ROM runs on the Cortex-M4 System Controller Unit (SCU), and the SCU firmware trains DDR before any application core executes, so there is no SPL stage and no DDR training blob on the A-cores. By the time wolfBoot is entered, DRAM is up and ATF has configured EL3: + +``` +Boot ROM (on the SCU Cortex-M4) + -> SCFW (scfw_tcm.bin) DDR init, power/clock/pad ownership + -> SECO firmware AHAB container authentication + -> ATF BL31 (bl31.bin) EL3, PSCI, SMP + -> BL33 = wolfBoot EL2 non-secure, entered at 0x80020000 + -> verify -> Linux FIT (EL2->EL1, DTB in x0) or bare-metal payload +``` + +`imx-mkimage` combines SCFW, the SECO container, ATF BL31 and BL33 into a single `flash.bin`. wolfBoot links at `0x80020000`, where `u-boot.bin` links on this SoC (`imx8qm_mek_defconfig`, `CONFIG_TEXT_BASE`), so it drops into the BL33 slot with no change to the stock recipe. wolfBoot runs MMU-off and 1:1 physical, which is what the arm64 Linux boot protocol expects at handoff. + +Two signature layers are in play. **AHAB** authenticates the boot container - SCFW, ATF and wolfBoot itself - against a Super Root Key hash in SoC fuses, before any A-core runs. **wolfBoot** authenticates the OS or application image it goes on to boot. In the bundled configurations the payload sits inside the BL33 image and is covered by both; a DTB bundled at `IMX8QM_DTB_OFFSET` is covered by AHAB only, not by wolfBoot's payload signature. + +### Example configurations + +Two configs, because there are two memory maps. Everything else is a build option: + +| Config | Memory map | +|---|---| +| [/config/examples/imx8qm-mek.config](/config/examples/imx8qm-mek.config) | The template. `WOLFBOOT_NO_PARTITIONS`, payload staged in DRAM. Covers the bundled bring-up build and every disk variant below. | +| [/config/examples/imx8qm-mek-qspi.config](/config/examples/imx8qm-mek-qspi.config) | Boot/update/swap partitions in the on-board MT35XU512ABA serial NOR on FlexSPI0, with the partition-based A/B update flow. | + +Variants are selected on the make command line, so CI and a local build agree: + +| Build | Boot path | +|---|---| +| `make` | Verify a payload bundled into the BL33 image in DRAM and boot it at EL2. No storage driver. | +| `make DISK_SDCARD=1` | Read the signed image from the SD card socket (uSDHC2). | +| `make DISK_EMMC=1` | Read the signed image from the soldered 8-bit eMMC (uSDHC1). | +| `make DISK_SDCARD=1 IMX8QM_MMU=1 EL2_HYPERVISOR=1 BOOT_EL1=1` | The Linux FIT path: drop EL2 -> EL1 and hand off with the DTB in `x0`. | +| `... DISK_FS=fat32\|ext4\|both` | Read the image from a file on a read-only FAT32 or ext4 filesystem rather than a raw partition offset. See [compile.md](compile.md). | +| `... IMX8QM_SCU=0` | Chainload: assume an earlier stage already powered, clocked and pad-muxed the console and the boot device. | +| `... IMX8QM_SD_NO_UHS=1` | Pin the SD node to 3.3V high-speed (`no-1-8-v` plus `max-frequency`) and drop wolfBoot's own ceiling to 25 MHz. Off by default; the board runs the card at DDR50, 1.8V, 50 MHz. Set it to keep Linux out of UHS, which is what makes a warm reboot work - see "Known limitations". | +| `... IMX8QM_USDHC_MAX_CLK_KHZ=` | Override the uSDHC clock ceiling. `arch.mk` defaults it per medium: 50000 for the SD socket (25000 with `IMX8QM_SD_NO_UHS=1`) and 52000 for the eMMC. | +| `... IMX8QM_SDHCI_PIO=1` | Force the PIO path instead of SDMA, for debugging. | +| `... BOOT_BENCHMARK=1` | Time the payload read, the hash and the signature check. | +| `... NO_ARM_ASM=1` | Turn the wolfcrypt ARMv8 assembly back off. It is on by default when `IMX8QM_MMU=1`. | + +`IMX8QM_MMU=1` is required for a Linux-sized payload. MMU-off DRAM is uncached, and hashing a 32 MB FIT that way takes over fifteen minutes against about thirty seconds with the MMU on. The Linux variant sets it, and `arch.mk` then defaults `NO_ARM_ASM=0`, because the NEON hazard behind the shared AArch64 default only applies while memory is Device-typed. + +Do not pass `CFLAGS_EXTRA=` on the make command line. `options.mk` does `CFLAGS+=$(CFLAGS_EXTRA)` with no `override`, so a command-line value replaces every `CFLAGS_EXTRA+=` in the config and in `arch.mk`. The uSDHC board flags (`SDHCI_FORCE_CARD_DETECT`, `IMX8QM_USDHC_MAX_CLK_KHZ`, `DISK_BLOCK_SIZE`) live in `arch.mk` for that reason. + +The disk builds expect two boot partitions labelled `boot_a` and `boot_b`, each holding a signed image at raw offset 0. They are selected by label, not by index, so the layout survives repartitioning; `BOOT_PART_A`/`BOOT_PART_B` are available (commented out) for media with no labels. + +### Building + +No hardware is needed to compile. + +``` +cp config/examples/imx8qm-mek.config .config +make distclean +make keytools +make +``` + +Append variant options to the final `make`, for example `make DISK_SDCARD=1 IMX8QM_MMU=1 EL2_HYPERVISOR=1 BOOT_EL1=1`. + +This produces `wolfboot.bin` (the BL33 bootloader) and `test-app/image_v1_signed.bin` (the signed payload). There is no contiguous `factory.bin`: wolfBoot is loaded into DRAM by ATF, so there is no flash image to assemble. + +### Image layout + +In the no-storage-driver configurations the packaging script bundles the payload and, for a Linux boot, the device tree into the BL33 image at fixed offsets (`hal/imx8qm.h`): + +``` +0x80020000 +--------------------------+ + | wolfBoot (BL33) | linker cap: 2 MB +0x80220000 +--------------------------+ IMX8QM_BUNDLE_OFFSET + | signed payload | +0x80320000 +--------------------------+ IMX8QM_DTB_OFFSET + | device tree (optional) | +0x80420000 +--------------------------+ IMX8QM_BL33_MAX_SIZE +``` + +`hal/imx8qm.ld` sets the memory region length to the payload offset, so wolfBoot growing into the payload slot is a link error rather than a silently mis-assembled image. + +BL33 lands at `0x80020000` because `imx-mkimage`'s recipe concatenates the A-core stages: `bl31.bin` at offset 0, padding to 128 KB, then BL33, with the container loaded at `0x80000000`. BL31 must fit the 128 KB slot. + +### Building the boot container + +`tools/scripts/imx8qm/imx8qm-mkflashbin.sh` builds wolfBoot and the signed payload against one freshly generated key, bundles them, and hands the result to `imx-mkimage` as the BL33 input: + +``` +IMX_MKIMAGE=/path/to/imx-mkimage \ +IMX_FIRMWARE=/path/to/firmware \ + tools/scripts/imx8qm/imx8qm-mkflashbin.sh [device-tree.dtb] +``` + +`IMX_FIRMWARE` must hold three binaries from the NXP BSP, none of which are redistributable. Without `IMX_MKIMAGE`/`IMX_FIRMWARE` the script stops after producing the BL33 bundle in `wolfboot.bin`, which can be passed to `imx-mkimage` by hand in place of `u-boot.bin`. + +| File | Source (LF v6.1.22-2.0.0) | +|---|---| +| `scfw_tcm.bin` | `imx-sc-firmware-1.15.0.bin`, member `mx8qm-mek-scfw-tcm.bin` | +| `mx8qmb0-ahab-container.img` | `imx-seco-5.9.0.bin`, under `firmware/seco/` | +| `bl31.bin` | built from `nxp-imx/imx-atf`, tag `lf-6.1.22-2.0.0` | + +The two archives are self-extracting scripts behind an NXP EULA prompt: + +``` +wget https://www.nxp.com/lgfiles/NMG/MAD/YOCTO/imx-sc-firmware-1.15.0.bin +wget https://www.nxp.com/lgfiles/NMG/MAD/YOCTO/imx-seco-5.9.0.bin +sh imx-sc-firmware-1.15.0.bin --auto-accept +sh imx-seco-5.9.0.bin --auto-accept +``` + +sha256, from the recipes: `imx-sc-firmware-1.15.0.bin` = `1272ac5c31a88017ef548721f3acf930a7eda6ac73aa9f41b5f0cade9d5c0e5f`, `imx-seco-5.9.0.bin` = `c3bd761f457e939035b01a0ab36e79064a2a1bc6c3cdb3cd847f7f38df0964df`. The SECO container is not in `firmware-imx`, which carries only DDR, HDMI, VPU and SDMA firmware; it comes from the separate `imx-seco` recipe. + +BL31 builds locally: + +``` +git clone https://github.com/nxp-imx/imx-atf.git +git -C imx-atf checkout lf-6.1.22-2.0.0 +make -C imx-atf PLAT=imx8qm bl31 CROSS_COMPILE=aarch64-none-elf- +# -> imx-atf/build/imx8qm/release/bl31.bin, ~41 KB +``` + +**Use the Linux BSP's `bl31.bin`.** The BSPs ship two ATF builds of the same size for this SoC, and a container built with the Android one produces no console output at all: BL31 runs before BL33, so wolfBoot never starts. A size check does not catch it. Identify the build first, and let the packaging script copy `bl31.bin` from `$IMX_FIRMWARE` rather than relying on whatever is staged in `imx-mkimage/iMX8QM/`: + +``` +strings bl31.bin | grep '^v2\.' +v2.8(release): <- use this +v2.8(release):android-13.0.0_2.0.0-rc1-1-g99195a23d <- will not boot +``` + +### Hardware setup (MEK) + +Boot mode is set on **SW2**. Positions are ON where listed, OFF everywhere else: + +| Boot device | POS-1 | POS-2 | POS-3 | POS-4 | POS-5 | POS-6 | +|---|---|---|---|---|---|---| +| Boot from fuse | OFF | OFF | OFF | OFF | OFF | OFF | +| Serial download (SDP) | OFF | OFF | **ON** | OFF | OFF | OFF | +| eMMC0 (uSDHC1) | OFF | OFF | OFF | **ON** | OFF | OFF | +| SD1 (uSDHC2) | OFF | OFF | **ON** | **ON** | OFF | OFF | +| Octal SPI (FlexSPI0) | OFF | OFF | OFF | **ON** | **ON** | OFF | + +From Table 3 of the i.MX 8QuadMax MEK Quick Start Guide, restated per switch position because the guide prints that table POS-6 first. Serial download and eMMC0 are mirror images of each other, so go by position number rather than by a bit pattern. + +The micro-B debug port (J18) drives an FT4232H, which enumerates four serial ports, one per FTDI channel. The Cortex-A console (LPUART0) is one of them; which tty it lands on depends on enumeration order, so identify it by watching per-port byte counts across a boot. Console settings are 115200 8N1. + +Power is 12 V into the 4-pin DIN connector J16. The board starts booting as soon as power is applied; SW1 need not be pressed. SW3 is reset. + +### Programming + +SDP loads the container straight into RAM over USB and writes nothing to the board, so a power cycle returns to the previous image. Set SW2 to serial download (POS-3) and connect J17: + +``` +tools/scripts/imx8qm/imx8qm-flash.sh sdp flash.bin +``` + +To boot from a microSD card, write the container at the 32 KB offset the boot ROM reads from and set SW2 to SD1 (POS-3 and POS-4): + +``` +tools/scripts/imx8qm/imx8qm-flash.sh sd /dev/sdX flash.bin +``` + +The script refuses any target that is not a removable device and asks for confirmation, because the container is written to raw sectors with no filesystem in the way. It does not write the on-board eMMC or the FlexSPI NOR; do those from a system already running on the board, with SDP as the recovery path. + +### Booting from eMMC + +Set SW2 to eMMC0 (POS-4) and build with `DISK_EMMC=1`. + +**The ROM boots the eMMC from its boot partition, not the user area.** A fresh MEK ships with `EXT_CSD[179] PARTITION_CONFIG` set to `BOOT_PARTITION_ENABLE: 0x1`, meaning `mmcblk0boot0`, which holds the factory NXP U-Boot. A container written to the user area at 32 KiB, where SD boot expects it, is never read, and the board comes up in the stock U-Boot with nothing to say wolfBoot was skipped. + +Point the ROM at the user area instead: + +``` +=> mmc partconf 0 0 7 0 # BOOT_PARTITION_ENABLE=7, user area +=> mmc partconf 0 # read it back +=> mmc partconf 0 0 1 0 # revert to boot partition 1 +``` + +`mmc partconf` is a U-Boot command, so once wolfBoot has replaced U-Boot the setting can only be changed from an OS shipping `mmc-utils`, which the stock NXP image does not. The alternative is to write the container into `mmcblk0boot0` itself, at offset 0 rather than 32 KiB, after clearing `/sys/block/mmcblk0boot0/force_ro`; that overwrites the factory bootloader. + +To confirm which copy the ROM used, look for the container tag from Linux. `00 a0 01 87` is a valid container header: + +``` +dd if=/dev/mmcblk0boot0 bs=1 skip=1024 count=4 | od -An -tx1 # boot partition +dd if=/dev/mmcblk0 bs=1 skip=33792 count=4 | od -An -tx1 # user area +``` + +With the user area selected, the ROM fetches the container from eMMC, and wolfBoot reads the eMMC GPT and boots a signed FIT from `boot_a`. + +### System Controller (SCU) + +The SCU owns power, clocks and pad mux; nothing else can turn a peripheral on. Powering and clocking a block is not sufficient - its pads must be routed with `sc_pad_set` and its LPCG cell opened, or the peripheral accepts register writes and drives nothing. This applies to the console as much as to storage, which is why `arch.mk` sets `IMX8QM_SCU ?= 1`. + +wolfBoot opens the MU1_A mailbox and brings up the resources it uses (`sc_pm_set_resource_power_mode`, `sc_pm_set_clock_rate`, `sc_pm_clock_enable`, `sc_pad_set`, plus the LPCG cell). MU1_A is the channel free for BL33: the upstream device tree binds `fsl,imx-scu` to `lsio_mu1`, while TF-A's `plat/imx/imx8qm` uses MU0. RPC framing, resource IDs and payload layouts follow U-Boot's `drivers/misc/imx8/scu_api.c` and `dt-bindings/firmware/imx/rsrc.h`. + +`IMX8QM_SCU=0` assumes an earlier stage left the console and boot device powered and clocked, and touches none of it. Valid when chainloading from U-Boot. Compiled in CI by the `imx8qm_no_scu_test` job. + +BL33 also owns SMMU bring-up, in two parts. Power `SC_R_SMMU` and set `sCR0.CLIENTPD`, as U-Boot does in `arch/arm/mach-imx/imx8/cpu.c`, or the OS aborts reading `SMMU_IDR0` off an unpowered block. Then publish each bus master's stream ID with `sc_rm_set_master_sid()`: the ID a master emits is owned by the SCU's resource manager, not by the SMMU or the device tree, so without it the OS programs the SMMU for the `iommus` ID, the master emits something else, and its DMA faults. Both uSDHC controllers are published, not only the one a given build boots from, because the OS brings up every controller the device tree enables. + +`hal_dts_fixup()` supplies what U-Boot would have: `/chosen/bootargs`, the real two-bank `/memory` map (2 GB at `0x80000000`, 4 GB at `0x880000000`), and optionally `no-1-8-v` plus `max-frequency` on the SD node. Without it the OS comes up with 1 GB and an empty command line. RAM staging sits at `0xa0000000` and `0xa8000000`, in the gap between the stock DTB's carveouts (roughly `0x90000000` to `0x9c000000`) and the CMA pool at `0xc0000000`. An overlap raises no error: the OS reserves the device tree it was handed, so the colliding carveout's reservation fails and whichever driver owned that region faults later. + +The MMU teardown before handoff must not be written in C. A set/way cache walk in C keeps its loop counters in stack slots and re-dirties lines behind itself; clearing `SCTLR_EL2.C` then strands them while later stack reads go to DRAM. `hal_prepare_boot()` calls the stack-free assembly `el2_flush_and_disable_mmu()` in `src/boot_aarch64_start.S`. + +### SD / eMMC driver + +i.MX uSDHC is the little-endian descendant of the QorIQ eSDHC and is close to, but not the same as, standard SDHCI. Rather than duplicating the card-initialization state machines, this target reuses the generic driver in `src/sdhci.c` and translates the register map in `hal/imx8qm.c` (`sdhci_reg_read`/`sdhci_reg_write`), as `hal/cm4.c` and `hal/tegra234.c` do for their controllers. Four differences are handled there: + +1. The command register's transfer-mode half lives in `MIX_CTRL` (0x48), not in the low half of 0x0C. The command half at 0x0C is bit-identical to standard SDHCI. +2. `PROT_CTRL` encodes bus width as a 2-bit field and has no bus-power or bus-voltage fields. +3. `SYS_CTRL` uses a DVS/SDCLKFS divider pair rather than the standard 10-bit divisor. The reset and data-timeout fields do line up and pass through untranslated. +4. There is no error-interrupt summary bit, and the DMA error moves from bit 25 to bit 28. The summary is synthesized on read, and must account for the uSDHC-only tuning error `TNE`, which has no standard bit to map onto: left out of the summary a latched tuning error is invisible to the generic driver and unclearable by it, so the driver waits on a transfer that never completes. + +SDMA is on, and the shim points the driver's SDMA address register at uSDHC's `DS_ADDR` rather than the ADMA2 descriptor pointer. `arch.mk` reads the payload in 64 KB blocks, because `DISK_BLOCK_SIZE` otherwise defaults below `SDHCI_DMA_THRESHOLD` and every transfer silently takes the PIO path regardless of bus clock. DMA lands in DRAM behind the cache, so the HAL implements `sdhci_platform_dma_prepare()`/`sdhci_platform_dma_complete()` as range-based clean and invalidate. + +### FlexSPI serial NOR + +The MEK fits an MT35XU512ABA (64 MB octal NOR) on FlexSPI0. Reads come from the memory-mapped AHB window at `0x08000000`; erase and page program go through the LUT-driven IP command path. This is the same IP as the Layerscape LS1028A "XSPI" block, and the driver follows `hal/nxp_ls1028a.c`. + +All LUT sequences use single-pad (1-1-1) SPI with 4-byte addressing: the part powers up in extended SPI mode, and 64 MB is past the 16 MB limit of 3-byte addressing. Octal mode would need a mode-register write first. + +Reads through the AHB window are served from prefetch buffers that an IP-path command does not invalidate, so without an explicit flush the window keeps returning the previous contents while the device itself is correct. `ext_flash_write()` and `ext_flash_erase()` therefore end with a controller software reset, which flushes the buffers and leaves the configuration registers and the LUT in place. + +The QSPI config leaves the first 1 MB of the NOR free for the boot container the ROM reads from offset 0, and places the boot, update and swap partitions above it. + +### AHAB secure boot + +AHAB is the SoC's own secure boot, the layer below wolfBoot's signature checking. A container built by `imx8qm-mkflashbin.sh` is unsigned and boots on a board whose SRK fuses are blank ("open" lifecycle). To sign one: + +``` +CST_PATH=/path/to/cst- \ +SRK_TABLE=/path/to/SRK_1_2_3_4_table.bin \ +SRK_KEY=/path/to/SRK1_..._ca_crt.pem \ +CERT_KEY=/path/to/SGK1_..._usr_crt.pem \ + tools/scripts/imx8qm/imx8qm-ahab-sign.sh flash.bin [mkimage.log] +``` + +The SRK table and keys come from the Code Signing Tool's own PKI scripts; this script does not generate them, because the key material outlives any one build. Passing the saved `imx-mkimage` output as the second argument lets the script use the container and signature-block offsets it printed instead of the stock ones. + +Use `keys/ahab_pki_tree.sh`, not `hab4_pki_tree.sh`: the latter is the older HAB4 flow for i.MX6/7 and produces keys AHAB will not accept. It reads its password from `keys/key_pass.txt` (the password repeated on two lines) and prompts for the rest; for this part the answers are a new CA, elliptic curve, `p384`, `sha384`, a duration, and SRK certificates with the CA flag set. `srktool` then needs `--ahab_ver`: + +``` +srktool --ahab_ver --table SRK_1_2_3_4_table.bin --efuses SRK_1_2_3_4_fuse.bin \ + --sign_digest sha512 --fuse_format 1 \ + --certs SRK1_...,SRK2_...,SRK3_...,SRK4_... +``` + +The signing certificates that script emits are named `SGK_1_...`, with the extra index, which is what `CERT_KEY` wants. + +`CERT_KEY` is mandatory. `ahab_pki_tree.sh` sets the CA flag on the SRKs, and a CA-flagged SRK may only sign another key, never data, so the CSF installs an SGK certificate and signs the container with that. The script emits the `[Install Certificate]` form for this reason, and CST requires a `Permissions` bitmask alongside the certificate; it defaults to `0x1` and `CERT_PERMISSIONS` overrides it. Installing the certificate costs about 220 bytes of signature block, which stays clear of the first payload at `0x13000`. + +Signing changes nothing on an open part; it becomes required once the SRK hash is fused and the part is closed. An open part does not verify the signature, so booting a signed container there shows only that the container is still well formed. The useful check at that stage is that the container and signature-block headers stay self-consistent and no payload is overlapped. `SRK_1_2_3_4_fuse.bin` holds the hash that would be fused; nothing in this flow writes it. + +> **Fusing is irreversible.** Burning the SRK hash and closing the part are one-way operations that permanently reject any container not signed by the matching key. A wrong hash, a lost private key, or a container never verified to boot while the part was open leaves the board with no recovery path, SDP included. Confirm the signed container boots on the open part, back up the key material, then fuse. wolfBoot ships no script that burns fuses; use NXP's own tooling. + +### Peripheral map + +Register bases used by the HAL, from the upstream device tree (`imx8-ss-{lsio,conn,dma}.dtsi`, `imx8qm-mek.dts`): + +| Block | Address | Note | +|---|---|---| +| LPUART0 | `0x5A060000` | console (`stdout-path`) | +| uSDHC1 | `0x5B010000` | eMMC, 8-bit, non-removable | +| uSDHC2 | `0x5B020000` | SD card, 4-bit | +| FlexSPI0 registers | `0x5D120000` | MT35XU512ABA, 64 MB octal NOR | +| FlexSPI0 AHB window | `0x08000000` | memory-mapped reads | +| MU1_A | `0x5D1C0000` | mailbox to the System Controller | +| DRAM | `0x80000000` | trained by SCFW before BL33 runs | + +### Known limitations + +- **A warm reboot cannot recover an SD card left in UHS mode, and no software fix is possible on this board.** Once Linux has taken the card to 1.8V signalling (`timing spec: sd uhs SDR104`, `signal voltage: 1.80 V` in `/sys/kernel/debug/mmc1/ios`), a `reboot` performs no VDD cycle, so the card is still in its UHS state when wolfBoot starts and does not answer `CMD8`. A card that has negotiated UHS-I returns to the initial 3.3V state only when VDD is removed; `CMD0` does not do it. On the MEK the card supply is a fixed rail, and the uSDHC shim's bus-power and bus-voltage bits are shadowed rather than driven (`hal/imx8qm.c`), so `sdhci_set_power()` cannot produce a VDD cycle. Hardware-checked: switching the host to 1.8V to meet the card does not make it answer `CMD8` either, and initialization then fails at `CMD55`. + What wolfBoot does now is fail cleanly instead of hanging. `src/sdhci.c` used to wait for Command Inhibit with an unbounded loop, so the first failed command never returned and the boot stopped with no further output; the wait is bounded and resets the command line, so the failure is reported and reaches `wolfBoot_panic()`. Recovery is a **power cycle**, which always works, or `IMX8QM_SD_NO_UHS=1`, which avoids the situation by keeping Linux out of UHS. `src/sdhci.c` also carries an opt-in `SDHCI_UHS_RECOVER_ON_INIT`, which switches the host to 1.8V and retries `CMD8` once; it is off by default and does **not** help on this board, but is kept for hosts that can drive bus power. The eMMC has the same exposure at 1.8V for HS200/HS400. +- **Failover cannot cross a version boundary downwards.** This is the anti-rollback policy rather than a gap: the retry is refused by the `ALLOW_DOWNGRADE` guard (`Rollback to lower version not allowed`) whenever the fallback slot carries a lower version, so equal-version slots are what make failover complete. +- **AHAB has not been exercised against a closed part.** Signing is exercised and the signed container boots on an open part, but no board here has fused SRKs, so enforcement is unproven. Fusing is irreversible. +- **The QSPI config's swap partition is reserved but unused.** The shared AArch64 block in `arch.mk` selects `src/update_ram.o` for every non-disk aarch64 target, which version-selects and RAM-boots rather than running the sector-swap update and rollback flow. +- The FIT's kernel load address is not 2 MB aligned, so the kernel relocates itself and warns. That belongs in the FIT, not in wolfBoot. + +### Debugging + +`-DDEBUG_SDHCI -DDEBUG_DISK -DDEBUG_GPT` add command-level tracing to the storage paths, and `-DDEBUG_FS` traces the filesystem layer when `DISK_FS` is enabled. + ## NXP i.MX95 Cortex-M7 The i.MX95 pairs an A55 cluster running Linux with a real-time Cortex-M7 and a Cortex-M33 System Manager. The M7 has no dedicated flash: it is loaded into TCM by the Linux `remoteproc` driver on the A55 side, so wolfBoot is the ELF that `remoteproc` loads, and the images it verifies live in the DDR region the device tree reserves for the M7. diff --git a/hal/cm4.c b/hal/cm4.c index 566dc2e003..e2095e904d 100644 --- a/hal/cm4.c +++ b/hal/cm4.c @@ -564,6 +564,15 @@ void* _sbrk(int incr) #endif /* HAVE_FIPS */ #if defined(CM4_USE_MMU) +/* Set/way cache maintenance comes from the shared AArch64 helpers. The + * include sits inside this guard because the host unit test compiles this + * whole file, where the header's TIMER_CLK_FREQ requirement does not apply; + * cm4 keeps its own hal_get_timer_us(), which needs 128-bit math. */ +#ifndef TIMER_CLK_FREQ +#define TIMER_CLK_FREQ BCM2711_TIMER_CLK_FREQ +#endif +#include "aarch64_arch.h" + /* Minimal identity-mapped MMU + caches for the CM4. wolfBoot's simple startup * runs with the MMU off, so all memory is Device-nGnRnE, which faults on the * unaligned / 128-bit SIMD accesses that the FIPS module, newlib printf, and @@ -580,44 +589,6 @@ void* _sbrk(int incr) static volatile uint64_t cm4_l1_table[512] __attribute__((aligned(4096))); -/* Data-cache maintenance by set/way over all levels to the point of coherency. - * clean != 0 -> clean+invalidate (dc cisw); else invalidate-only (dc isw). */ -static void cm4_dcache_maint(int clean) -{ - uint64_t clidr, ccsidr; - unsigned int level, loc, ctype, linesize, ways, sets, way, set, wayshift; - - __asm__ volatile("dsb sy"); - __asm__ volatile("mrs %0, clidr_el1" : "=r"(clidr)); - loc = (unsigned int)((clidr >> 24) & 0x7); /* Level of Coherency */ - for (level = 0; level < loc; level++) { - ctype = (unsigned int)((clidr >> (level * 3)) & 0x7); - if (ctype < 2) /* no data/unified cache at this level */ - continue; - __asm__ volatile("msr csselr_el1, %0" :: "r"((uint64_t)(level << 1))); - __asm__ volatile("isb"); - __asm__ volatile("mrs %0, ccsidr_el1" : "=r"(ccsidr)); - linesize = (unsigned int)(ccsidr & 0x7) + 4; /* log2(bytes) */ - ways = (unsigned int)((ccsidr >> 3) & 0x3FF); /* assoc - 1 */ - sets = (unsigned int)((ccsidr >> 13) & 0x7FFF); /* sets - 1 */ - /* __builtin_clz(0) is UB; a direct-mapped cache (ways==0) never uses - * the way field (way stays 0), so the shift amount is irrelevant. */ - wayshift = (ways == 0) ? 32u : (unsigned int)__builtin_clz(ways); - for (set = 0; set <= sets; set++) { - for (way = 0; way <= ways; way++) { - uint64_t val = ((uint64_t)(level << 1)) - | ((uint64_t)way << wayshift) - | ((uint64_t)set << linesize); - if (clean) - __asm__ volatile("dc cisw, %0" :: "r"(val)); - else - __asm__ volatile("dc isw, %0" :: "r"(val)); - } - } - } - __asm__ volatile("dsb sy"); - __asm__ volatile("isb"); -} /* MMU/cache setup uses EL2 system registers; wolfBoot enters at EL2 on the CM4. * Guard against an EL1 entry (a custom armstub) so the msr *_el2 below do not @@ -661,7 +632,7 @@ void cm4_mmu_enable(void) __asm__ volatile("dsb sy"); /* Invalidate the D-cache (and I-cache) before enabling them, so no stale * lines left by an earlier boot stage surface once caching is on. */ - cm4_dcache_maint(0); + aarch64_dcache_maint(0); __asm__ volatile("ic iallu"); __asm__ volatile("dsb sy"); __asm__ volatile("isb"); @@ -683,14 +654,14 @@ void cm4_mmu_disable(void) cm4_require_el2(); /* Flush the loaded app to DRAM WHILE the D-cache is still enabled, then * disable M/C/I together. The "textbook" order (clear SCTLR.C first, then - * flush) is UNSAFE here: cm4_dcache_maint() and this function use the stack, + * flush) is UNSAFE here: aarch64_dcache_maint() and this function use the stack, * and once C is cleared, stack reads bypass the cache and return stale DRAM * (the dirty lines - including this function's spilled return address - are * not yet written back), so the function would return to garbage. That * order is only safe in a pure-asm flush with no stack use (U-Boot). What * must be coherent for the application is the loaded image, and it is fully * flushed here with caches on. */ - cm4_dcache_maint(1); /* clean+invalidate: flush the loaded app to memory */ + aarch64_dcache_maint(1); /* clean+invalidate: flush the app to memory */ __asm__ volatile("mrs %0, sctlr_el2" : "=r"(sctlr)); sctlr &= ~((1UL << 0) | (1UL << 2) | (1UL << 12)); /* clear M, C, I */ __asm__ volatile("msr sctlr_el2, %0" :: "r"(sctlr)); @@ -703,14 +674,17 @@ void cm4_mmu_disable(void) #endif /* CM4_USE_MMU */ #if defined(DEBUG) && defined(DEBUG_UART) -/* CM4 bring-up diagnostic: exception handler invoked from cm4_vectors in +/* CM4 bring-up diagnostic: exception handler invoked from simple_el2_vectors in * src/boot_aarch64_start.S. Dumps the fault syndrome so a data/instruction * abort shows up over UART instead of hanging silently. Built only with - * DEBUG + DEBUG_UART. ESR_EL2[31:26] = exception class. */ -void cm4_fault_handler(unsigned long esr, unsigned long elr, unsigned long far); -void cm4_fault_handler(unsigned long esr, unsigned long elr, unsigned long far) -{ - wolfBoot_printf("\n*** CM4 EXCEPTION ***\n"); + * DEBUG + DEBUG_UART. ESR_EL2[31:26] = exception class; vector is the 0-15 slot + * in the table, so >= 8 means the fault came from a lower EL. */ +void simple_el2_fault_handler(unsigned long esr, unsigned long elr, + unsigned long far, unsigned long vector); +void simple_el2_fault_handler(unsigned long esr, unsigned long elr, + unsigned long far, unsigned long vector) +{ + wolfBoot_printf("\n*** CM4 EXCEPTION *** vector=%d\n", (int)vector); wolfBoot_printf("ESR_EL2=0x%08x EC=0x%02x\n", (unsigned)esr, (unsigned)((esr >> 26) & 0x3F)); wolfBoot_printf("ELR_EL2=0x%08x%08x\n", diff --git a/hal/imx8qm.c b/hal/imx8qm.c new file mode 100644 index 0000000000..1c996aa18c --- /dev/null +++ b/hal/imx8qm.c @@ -0,0 +1,1721 @@ +/* imx8qm.c + * + * 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 + */ + +/* NXP i.MX 8QuadMax (MCIMX8QM-MEK): bare-metal wolfBoot as BL33, replacing + * U-Boot in the NXP boot container. DDR is trained by SCFW. See docs/Targets.md. */ + +#include +#include +#include +#include + +#if defined(DEBUG_UART) + #define PRINTF_ENABLED +#endif + +#include "image.h" +#include "printf.h" +#include "hal/imx8qm.h" +#if defined(DISK_SDCARD) || defined(DISK_EMMC) +#include "sdhci.h" +#endif + +#ifndef ARCH_AARCH64 +# error "wolfBoot imx8qm HAL: wrong architecture. Compile with ARCH=AARCH64." +#endif + +/* Handoff x0 from the .boot stub: the DTB/params pointer per the arm64 boot + * protocol. Nonzero initializer keeps it in .data, clear of the BSS wipe. */ +volatile uint64_t boot_handoff_x0 = 0xFFFFFFFFFFFFFFFFULL; + +/* Flattened Device Tree magic 0xd00dfeed, stored big-endian -> reads back as + * 0xedfe0dd0 on this little-endian core. */ +#define FDT_MAGIC_LE 0xedfe0dd0u + +/* MMIO is Device-nGnRnE (MMU off), so accesses are strongly ordered by the + * memory type and need no barriers. Mapping these Normal would break that. + * Kept as functions, not open-coded casts: the uSDHC and FlexSPI unit tests + * extract those drivers from this file and substitute their own. */ +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; } + +/* Generic timer and set/way cache maintenance come from the shared AArch64 + * helpers; only the two HAL entry points are defined here, because + * include/hal.h gives them external linkage. */ +#include "aarch64_arch.h" + +uint64_t hal_get_timer_us(void) +{ + return (timer_get_count() * 1000000ULL) / timer_get_freq(); +} + +void hal_delay_us(uint32_t us) +{ + uint64_t deadline = timer_deadline_us(us); + + while (!timer_expired(deadline)) + ; +} + +#if defined(DISK_SDCARD) || defined(DISK_EMMC) +#if defined(DISK_EMMC) +#define IMX8QM_USDHC_BASE IMX8QM_USDHC1_BASE /* eMMC, 8-bit */ +#else +#define IMX8QM_USDHC_BASE IMX8QM_USDHC2_BASE /* SD card, 4-bit */ +#endif +#endif + +/* Set once imx8qm_scu_init() has brought the console's clock up, so + * uart_init() knows the LPUART root clock rate and can program the divisor. */ +static int imx8qm_scu_ready; + +/* Console LPUART0. With the SCU client wolfBoot knows the root clock and programs + * BAUD; without it the rate is unreadable, so only TE/RE are enabled. */ + +#ifndef LPUART_TX_TIMEOUT_US +#define LPUART_TX_TIMEOUT_US 10000 +#endif + +#if defined(DEBUG_UART) +void uart_init(void) +{ + uintptr_t b = IMX8QM_LPUART0_BASE; + uint32_t ctrl = rd32(b + LPUART_CTRL); + uint32_t baud; + + /* Only program BAUD when wolfBoot brought the port up, so the root clock + * rate is known. Otherwise a prior stage set it and it is not knowable. */ + if (imx8qm_scu_ready) { + wr32(b + LPUART_CTRL, 0); /* TE/RE off while BAUD changes */ + baud = rd32(b + LPUART_BAUD); + baud &= ~(uint32_t)(LPUART_BAUD_OSR_MASK | LPUART_BAUD_SBR_MASK | + LPUART_BAUD_M10 | LPUART_BAUD_SBNS); + baud |= IMX8QM_LPUART_BAUD_115200; /* 8N1 at 80 MHz */ + wr32(b + LPUART_BAUD, baud); + wr32(b + LPUART_CTRL, LPUART_CTRL_TE | LPUART_CTRL_RE); + return; + } + + if ((ctrl & LPUART_CTRL_TE) == 0) { + wr32(b + LPUART_CTRL, ctrl | LPUART_CTRL_TE | LPUART_CTRL_RE); + } +} + +static void lpuart_putc(char c) +{ + uintptr_t b = IMX8QM_LPUART0_BASE; + uint64_t deadline = timer_deadline_us(LPUART_TX_TIMEOUT_US); + + while ((rd32(b + LPUART_STAT) & LPUART_STAT_TDRE) == 0) { + if (timer_expired(deadline)) + return; /* drop the character; do not stall the boot */ + } + wr32(b + LPUART_DATA, (uint32_t)(uint8_t)c); +} + +void uart_write(const char* buf, unsigned int sz) +{ + unsigned int i; + + for (i = 0; i < sz; i++) { + if (buf[i] == '\n') + lpuart_putc('\r'); + lpuart_putc(buf[i]); + } +} +#endif /* DEBUG_UART */ + +/* System Controller (SCU) client over MU1_A, the channel free for BL33 (Linux and + * U-Boot bind lsio_mu1, TF-A uses MU0). RPC framing per U-Boot scu_api.c. */ + +#ifdef IMX8QM_SCU + +/* SCU resources this HAL may drive, and the config that needs each one. */ +struct imx8qm_resource { + uint16_t rsrc; + uint8_t has_clock; /* 1 if a peripheral clock must be set + enabled */ + uint32_t clock_hz; +}; + +static const struct imx8qm_resource imx8qm_resources[] = { + { SC_R_UART_0, 1, IMX8QM_UART_CLK_HZ }, +#if defined(DISK_EMMC) + { SC_R_SDHC_0, 1, IMX8QM_USDHC_PERCLK_HZ }, +#endif +#if defined(DISK_SDCARD) + { SC_R_SDHC_1, 1, IMX8QM_USDHC_PERCLK_HZ }, +#endif +#if defined(EXT_FLASH) + { SC_R_FSPI_0, 1, 100000000 }, +#endif +}; + +#define IMX8QM_NUM_RESOURCES \ + (int)(sizeof(imx8qm_resources) / sizeof(imx8qm_resources[0])) + + +/* SCFW RPC framing. A message is a header word followed by up to 7 payload + * words, moved through the MU's four transmit/receive registers. */ +#define SC_RPC_VERSION 1U +#define SC_RPC_SVC_PM 2U +#define SC_PM_FUNC_SET_RESOURCE_POWER_MODE 3U +#define SC_PM_FUNC_SET_CLOCK_RATE 5U +#define SC_PM_FUNC_GET_CLOCK_RATE 6U +#define SC_PM_FUNC_CLOCK_ENABLE 7U +#define SC_RPC_SVC_RM 3U +#define SC_RM_FUNC_SET_MASTER_SID 11U + +#ifndef SC_MU_TIMEOUT_US +#define SC_MU_TIMEOUT_US 1000000 +#endif + +/* Words per RPC message, header included (SCFW SC_RPC_MAX_MSG). */ +#define SC_RPC_MAX_MSG 8 + +#define SC_RPC_HEADER(size, svc, func) \ + ((uint32_t)SC_RPC_VERSION | ((uint32_t)(size) << 8) | \ + ((uint32_t)(svc) << 16) | ((uint32_t)(func) << 24)) + +/* Move one 32-bit word into MU transmit register n, waiting for it to drain. + * Returns 0 on success, -1 if the SCU stopped consuming. */ +static int mu_send_word(int n, uint32_t val) +{ + uintptr_t b = IMX8QM_LSIO_MU1A_BASE; + uint64_t deadline = timer_deadline_us(SC_MU_TIMEOUT_US); + + while ((rd32(b + MU_SR) & (MU_SR_TE0 >> n)) == 0) { + if (timer_expired(deadline)) + return -1; + } + wr32(b + MU_TR0 + (uintptr_t)n * 4, val); + return 0; +} + +static int mu_recv_word(int n, uint32_t* val) +{ + uintptr_t b = IMX8QM_LSIO_MU1A_BASE; + uint64_t deadline = timer_deadline_us(SC_MU_TIMEOUT_US); + + while ((rd32(b + MU_SR) & (MU_SR_RF0 >> n)) == 0) { + if (timer_expired(deadline)) + return -1; + } + *val = rd32(b + MU_RR0 + (uintptr_t)n * 4); + return 0; +} + +/* One SCFW RPC; "size" counts header + payload in words. Every reply word must be + * drained or the next call reads a leftover as its reply header. */ +/* Drop any pending reply words after a failed transfer, or every later RPC is + * misaligned. Bounded so a mailbox the SCU keeps refilling cannot spin. */ +static void mu_drain_rx(void) +{ + uintptr_t b = IMX8QM_LSIO_MU1A_BASE; + uint32_t discard; + int pass, n; + + for (pass = 0; pass < 8; pass++) { + int drained = 0; + for (n = 0; n < 4; n++) { + if ((rd32(b + MU_SR) & (MU_SR_RF0 >> n)) != 0) { + discard = rd32(b + MU_RR0 + (uintptr_t)n * 4); + (void)discard; + drained = 1; + } + } + if (!drained) + return; + } +} + +static int sc_rpc_call(uint32_t* msg, int size) +{ + int i, reply_size; + uint32_t reply = 0, discard; + + for (i = 0; i < size; i++) { + if (mu_send_word(i % 4, msg[i]) != 0) { + mu_drain_rx(); + return -1; + } + } + if (mu_recv_word(0, &reply) != 0) { + mu_drain_rx(); + return -1; + } + + /* The size field is a byte, so a malformed or mis-framed header can claim + * up to 255 words. Each missing word then costs a full SC_MU_TIMEOUT_US + * in mu_recv_word(), turning one bad reply into minutes of stalled boot. + * The protocol caps a message at SC_RPC_MAX_MSG words and nothing here + * sends or expects more than three. */ + reply_size = (int)((reply >> 8) & 0xFF); + if (reply_size < 1 || reply_size > SC_RPC_MAX_MSG) { + mu_drain_rx(); + return -1; + } + for (i = 1; i < reply_size; i++) { + if (mu_recv_word(i % 4, &discard) != 0) { + mu_drain_rx(); + return -1; + } + } + /* Result is a signed 8-bit status in the function byte of the reply. */ + return (int8_t)((reply >> 24) & 0xFF); +} + +static int sc_pm_set_resource_power_mode(uint16_t rsrc, uint8_t mode) +{ + uint32_t msg[2]; + + /* Data layout: resource at byte 0, power mode at byte 2. */ + msg[0] = SC_RPC_HEADER(2, SC_RPC_SVC_PM, + SC_PM_FUNC_SET_RESOURCE_POWER_MODE); + msg[1] = (uint32_t)rsrc | ((uint32_t)mode << 16); + return sc_rpc_call(msg, 2); +} + +static int sc_pm_set_clock_rate(uint16_t rsrc, uint8_t clk, uint32_t rate) +{ + uint32_t msg[3]; + + /* Rate at byte 0 (a whole word), resource at byte 4, clock type at byte 6. The SCU + * replies with the rate it programmed; sc_rpc_call drains it unused. */ + msg[0] = SC_RPC_HEADER(3, SC_RPC_SVC_PM, SC_PM_FUNC_SET_CLOCK_RATE); + msg[1] = rate; + msg[2] = (uint32_t)rsrc | ((uint32_t)clk << 16); + return sc_rpc_call(msg, 3); +} + + +static int sc_pm_clock_enable(uint16_t rsrc, uint8_t clk, uint8_t enable) +{ + uint32_t msg[3]; + + /* Data layout: resource at byte 0, clock type at byte 2, enable at byte 3, + * autogate at byte 4 (i.e. the start of the second data word). */ + msg[0] = SC_RPC_HEADER(3, SC_RPC_SVC_PM, SC_PM_FUNC_CLOCK_ENABLE); + msg[1] = (uint32_t)rsrc | ((uint32_t)clk << 16) | + ((uint32_t)enable << 24); + msg[2] = 0; /* autogate off */ + return sc_rpc_call(msg, 3); +} + +#define SC_RPC_SVC_PAD 6U +#define PAD_FUNC_SET 15U + +/* The ID a master emits belongs to the SCU resource manager, not the SMMU or the + * DTB; without this the OS programs the SMMU for the "iommus" ID and DMA faults. */ +static int sc_rm_set_master_sid(uint16_t rsrc, uint16_t sid) +{ + uint32_t msg[2]; + + /* Data layout: resource at byte 0, stream ID at byte 2. */ + msg[0] = SC_RPC_HEADER(2, SC_RPC_SVC_RM, SC_RM_FUNC_SET_MASTER_SID); + msg[1] = (uint32_t)rsrc | ((uint32_t)sid << 16); + return sc_rpc_call(msg, 2); +} + +/* The caller must OR in the IFMUX/GP enables, or the mux is configured but not + * driven (as U-Boot does in imx8_iomux_setup_pad()). */ +static int sc_pad_set(uint16_t pad, uint32_t val) +{ + uint32_t msg[3]; + + /* Data layout: the control word at byte 0, the pad id at byte 4. */ + msg[0] = SC_RPC_HEADER(3, SC_RPC_SVC_PAD, PAD_FUNC_SET); + msg[1] = val; + msg[2] = (uint32_t)pad; + return sc_rpc_call(msg, 3); +} + +/* The clock-gate cell in front of the peripheral, separate from the SCU ungate. + * Written twice with a delay, per the documented LPCG erratum. */ +#ifndef LPCG_SETTLE_TIMEOUT_US +#define LPCG_SETTLE_TIMEOUT_US 10000 +#endif + +static void lpcg_all_clock_on(uintptr_t lpcg) +{ + uint64_t deadline; + + wr32(lpcg, LPCG_ALL_CLOCK_ON); + hal_delay_us(10); + wr32(lpcg, LPCG_ALL_CLOCK_ON); + + deadline = timer_deadline_us(LPCG_SETTLE_TIMEOUT_US); + while ((rd32(lpcg) & LPCG_ALL_CLOCK_STOP) != 0) { + if (timer_expired(deadline)) + return; + } +} + +/* Failures are buffered, not printed: this runs before the console is up. */ +#define SCU_MAX_FAILURES 8 +/* is_pad distinguishes the two SCU id namespaces: a pad id and a resource id + * of the same value are different things, and reporting both as "resource" + * sends the reader to the wrong table. */ +static struct { uint16_t id; uint8_t is_pad; const char* what; int err; } + scu_failures[SCU_MAX_FAILURES]; +static int scu_nfailures; + +static void scu_note_failure_id(uint16_t id, uint8_t is_pad, const char* what, + int err) +{ + if (scu_nfailures < SCU_MAX_FAILURES) { + scu_failures[scu_nfailures].id = id; + scu_failures[scu_nfailures].is_pad = is_pad; + scu_failures[scu_nfailures].what = what; + scu_failures[scu_nfailures].err = err; + scu_nfailures++; + } +} + +static void scu_note_failure(uint16_t rsrc, const char* what, int err) +{ + scu_note_failure_id(rsrc, 0, what, err); +} + +static void scu_note_pad_failure(uint16_t pad, const char* what, int err) +{ + scu_note_failure_id(pad, 1, what, err); +} + +/* Failures are reported but not fatal: the ROM may already have brought a + * resource up. */ +#ifndef SMMU_CLIENTPD_TRIES +#define SMMU_CLIENTPD_TRIES 10 +#endif + +static void imx8qm_scu_init(void) +{ + int i, ret; + + for (i = 0; i < IMX8QM_NUM_RESOURCES; i++) { + const struct imx8qm_resource* r = &imx8qm_resources[i]; + + ret = sc_pm_set_resource_power_mode(r->rsrc, SC_PM_PW_MODE_ON); + if (ret != 0) { + scu_note_failure(r->rsrc, "power on", ret); + continue; + } + if (r->has_clock) { + ret = sc_pm_set_clock_rate(r->rsrc, SC_PM_CLK_PER, r->clock_hz); + if (ret != 0) + scu_note_failure(r->rsrc, "set rate", ret); + ret = sc_pm_clock_enable(r->rsrc, SC_PM_CLK_PER, 1); + if (ret != 0) + scu_note_failure(r->rsrc, "clk enable", ret); + } + } + + /* Pads and LPCG: without both, LPUART0 accepts writes and drives nothing. */ + ret = sc_pad_set(SC_P_UART0_RX, + IMX8QM_UART_PAD_CTRL | PADRING_IFMUX_EN | PADRING_GP_EN); + if (ret != 0) + scu_note_pad_failure(SC_P_UART0_RX, "pad set", ret); + ret = sc_pad_set(SC_P_UART0_TX, + IMX8QM_UART_PAD_CTRL | PADRING_IFMUX_EN | PADRING_GP_EN); + if (ret != 0) + scu_note_pad_failure(SC_P_UART0_TX, "pad set", ret); + + lpcg_all_clock_on(IMX8QM_LPUART0_LPCG); + +#if defined(DISK_SDCARD) || defined(DISK_EMMC) + /* Same for the storage controller, or it reads back plausible register + * values and never talks to the card. */ + { + /* Both pad sets, not just the boot medium's: the OS brings up every uSDHC its DTB + * enables, and routing only one left an eMMC-booted kernel with no SD slot. */ + static const struct { uint16_t pad; uint32_t cfg; } sd_pads[] = { + /* uSDHC1: soldered 8-bit eMMC */ + { SC_P_EMMC0_CLK, IMX8QM_SD_PAD_CLK_CTRL }, + { SC_P_EMMC0_CMD, IMX8QM_SD_PAD_CTRL }, + { SC_P_EMMC0_DATA0, IMX8QM_SD_PAD_CTRL }, + { SC_P_EMMC0_DATA1, IMX8QM_SD_PAD_CTRL }, + { SC_P_EMMC0_DATA2, IMX8QM_SD_PAD_CTRL }, + { SC_P_EMMC0_DATA3, IMX8QM_SD_PAD_CTRL }, + { SC_P_EMMC0_DATA4, IMX8QM_SD_PAD_CTRL }, + { SC_P_EMMC0_DATA5, IMX8QM_SD_PAD_CTRL }, + { SC_P_EMMC0_DATA6, IMX8QM_SD_PAD_CTRL }, + { SC_P_EMMC0_DATA7, IMX8QM_SD_PAD_CTRL }, + { SC_P_EMMC0_STROBE, 0x00000041 }, + { SC_P_EMMC0_RESET_B, IMX8QM_SD_PAD_CTRL }, + /* uSDHC2: 4-bit SD socket */ + { SC_P_USDHC1_CLK, IMX8QM_SD_PAD_CLK_CTRL }, + { SC_P_USDHC1_CMD, IMX8QM_SD_PAD_CTRL }, + { SC_P_USDHC1_DATA0, IMX8QM_SD_PAD_CTRL }, + { SC_P_USDHC1_DATA1, IMX8QM_SD_PAD_CTRL }, + { SC_P_USDHC1_DATA2, IMX8QM_SD_PAD_CTRL }, + { SC_P_USDHC1_DATA3, IMX8QM_SD_PAD_CTRL }, + { SC_P_USDHC1_VSELECT, IMX8QM_SD_PAD_CTRL }, + }; + unsigned int pad_i; + + for (pad_i = 0; pad_i < sizeof(sd_pads)/sizeof(sd_pads[0]); pad_i++) { + ret = sc_pad_set(sd_pads[pad_i].pad, + sd_pads[pad_i].cfg | PADRING_IFMUX_EN | PADRING_GP_EN); + if (ret != 0) + scu_note_pad_failure(sd_pads[pad_i].pad, "sd pad set", ret); + } + lpcg_all_clock_on(IMX8QM_USDHC_LPCG(IMX8QM_USDHC_BASE)); + + /* Both controllers: the OS brings up every uSDHC the device tree + * enables, and one with no published ID is unusable by it. */ + ret = sc_rm_set_master_sid(SC_R_SDHC_0, IMX8QM_USDHC_SMMU_SID); + if (ret != 0) + scu_note_failure(SC_R_SDHC_0, "set master sid", ret); + ret = sc_rm_set_master_sid(SC_R_SDHC_1, IMX8QM_USDHC_SMMU_SID); + if (ret != 0) + scu_note_failure(SC_R_SDHC_1, "set master sid", ret); + } +#endif + + /* BL33 owns SMMU bring-up: the OS aborts reading SMMU_IDR0 off an + * unpowered block. Write guarded on the power call succeeding. */ + ret = sc_pm_set_resource_power_mode(SC_R_SMMU, SC_PM_PW_MODE_ON); + if (ret != 0) { + scu_note_failure(SC_R_SMMU, "power on", ret); + } + else { + /* Read back: a dropped write leaves translation on for every client. */ + int tries; + + for (tries = 0; tries < SMMU_CLIENTPD_TRIES; tries++) { + wr32(IMX8QM_SMMU_BASE + IMX8QM_SMMU_sCR0, + IMX8QM_SMMU_sCR0_CLIENTPD); + if ((rd32(IMX8QM_SMMU_BASE + IMX8QM_SMMU_sCR0) + & IMX8QM_SMMU_sCR0_CLIENTPD) != 0) { + break; + } + hal_delay_us(1000); + } + if (tries == SMMU_CLIENTPD_TRIES) { + scu_note_failure(SC_R_SMMU, "sCR0 CLIENTPD did not stick", 0); + } + } + +#if defined(EXT_FLASH) + /* flexspi0grp is one contiguous pad run, all at mux 0. */ + { + uint16_t pad; + + for (pad = SC_P_QSPI0A_DATA0; pad <= SC_P_QSPI0B_SS1_B; pad++) { + ret = sc_pad_set(pad, IMX8QM_FLEXSPI_PAD_CTRL | + PADRING_IFMUX_EN | PADRING_GP_EN); + if (ret != 0) + scu_note_pad_failure(pad, "fspi pad set", ret); + } + lpcg_all_clock_on(IMX8QM_FLEXSPI0_LPCG); + } +#endif + + imx8qm_scu_ready = 1; +} +/* Print anything imx8qm_scu_init() could not do, now that the console is up. */ +static void imx8qm_scu_report(void) +{ + int i; + + for (i = 0; i < scu_nfailures; i++) { + wolfBoot_printf("imx8qm scu: %s %u %s failed (%d)\n", + scu_failures[i].is_pad ? "pad" : "resource", + (unsigned)scu_failures[i].id, scu_failures[i].what, + scu_failures[i].err); + } +} +#else /* !IMX8QM_SCU */ +static void imx8qm_scu_init(void) +{ + (void)imx8qm_scu_ready; + /* The prior stage left the console and boot device powered and clocked. Build with + * IMX8QM_SCU=1 to have wolfBoot bring them up itself. */ +} + +static void imx8qm_scu_report(void) +{ +} +#endif /* IMX8QM_SCU */ + +/* Partition / load addresses. In the no-storage configs the payload and DTB are + * bundled into the BL33 image at the hal/imx8qm.h offsets. */ + +void* hal_get_primary_address(void) +{ + return (void*)(uintptr_t)(IMX8QM_BL33_BASE + IMX8QM_BUNDLE_OFFSET); +} + +/* The packaging script writes only the boot payload, so no update partition exists; + * return the reserved DRAM staging address to give the update API a valid target. */ +void* hal_get_update_address(void) +{ + return (void*)(uintptr_t)IMX8QM_UPDATE_ADDR; +} + +void* hal_get_dts_address(void) +{ + uintptr_t dtb = (uintptr_t)(IMX8QM_BL33_BASE + IMX8QM_DTB_OFFSET); + uint32_t totalsize; + + /* Only claim a bundled DTB; otherwise this offset is empty and x0 would be garbage. + * A bundled DTB is covered by AHAB, not by wolfBoot's payload signature. */ + if (rd32(dtb) != FDT_MAGIC_LE) + return NULL; + /* totalsize (big-endian, at offset 4) must fit in what is left of the + * BL33 image: update_ram copies that many bytes. */ + totalsize = __builtin_bswap32(rd32(dtb + 4)); + if (totalsize < 8 || + totalsize > (IMX8QM_BL33_MAX_SIZE - IMX8QM_DTB_OFFSET)) + return NULL; + return (void*)dtb; +} + +void* hal_get_dts_update_address(void) +{ + return NULL; /* Not yet supported */ +} + +/* FlexSPI0 serial NOR (MT35XU512ABA, 64 MB): AHB-window reads, LUT-driven IP-path + * erase/program. Single-pad sequences, 4-byte addressing (64 MB is past 3 bytes). */ + +#ifdef EXT_FLASH + +#define FSPI_REG(off) (IMX8QM_FLEXSPI0_BASE + (off)) +#define FSPI_LUT(n) FSPI_REG(FLEXSPI_LUT0 + ((n) * 4)) + +/* One LUT instruction: opcode[15:10] | pad[9:8] | operand[7:0]. Two per word. */ +#define LUT_INSTR(op, pad, operand) \ + ((uint16_t)(((op) << 10) | ((pad) << 8) | ((operand) & 0xFF))) +#define LUT_WORD(i0, i1) ((uint32_t)(i0) | ((uint32_t)(i1) << 16)) + +/* Command-done / error bits are write-1-to-clear in INTR. */ +#define FSPI_IP_ERR (FLEXSPI_INTR_IPCMDGE | FLEXSPI_INTR_IPCMDERR) + +/* Bound every IP command so a mis-configured controller fails the boot with an + * error instead of hanging inside the flash driver. */ +#ifndef FSPI_IPCMD_TIMEOUT_US +#define FSPI_IPCMD_TIMEOUT_US 1000000 +#endif +/* A full-chip erase is never issued here; the longest single operation is a + * 128 KB sector erase, spec'd in the hundreds of milliseconds. */ +#ifndef FSPI_WIP_TIMEOUT_US +#define FSPI_WIP_TIMEOUT_US 5000000 +#endif + +static int flexspi_ready; + +/* IPCR0 takes the offset within the device, not the AHB address (as Linux + * spi-nxp-fspi.c does). Accept either so callers can pass a partition address. */ +static uint32_t flexspi_dev_off(uintptr_t address) +{ + if (address >= IMX8QM_FLEXSPI0_AHB_BASE) + address -= IMX8QM_FLEXSPI0_AHB_BASE; + return (uint32_t)address; +} + +static void flexspi_lut_unlock(void) +{ + wr32(FSPI_REG(FLEXSPI_LUTKEY), FLEXSPI_LUTKEY_VALUE); + wr32(FSPI_REG(FLEXSPI_LUTCR), FLEXSPI_LUTCR_UNLOCK); +} + +static void flexspi_lut_lock(void) +{ + wr32(FSPI_REG(FLEXSPI_LUTKEY), FLEXSPI_LUTKEY_VALUE); + wr32(FSPI_REG(FLEXSPI_LUTCR), FLEXSPI_LUTCR_LOCK); +} + +static void flexspi_lut_setup(void) +{ + int i; + + flexspi_lut_unlock(); + + /* Zero every sequence slot this driver owns, so a stale LUT left by the + * boot ROM cannot be executed by a sequence index we did not program. */ + for (i = 0; i < 5 * 4; i++) + wr32(FSPI_LUT(i), 0); + + /* READ (0x13): CMD, 32-bit address, then read. Used by the AHB window. */ + wr32(FSPI_LUT(FLEXSPI_LUT_SEQ_READ * 4 + 0), + LUT_WORD(LUT_INSTR(FLEXSPI_LUT_CMD, FLEXSPI_LUT_PAD1, + FLEXSPI_NOR_CMD_READ_4B), + LUT_INSTR(FLEXSPI_LUT_RADDR, FLEXSPI_LUT_PAD1, 32))); + wr32(FSPI_LUT(FLEXSPI_LUT_SEQ_READ * 4 + 1), + LUT_WORD(LUT_INSTR(FLEXSPI_LUT_READ, FLEXSPI_LUT_PAD1, 4), + LUT_INSTR(FLEXSPI_LUT_STOP, FLEXSPI_LUT_PAD1, 0))); + + /* WREN (0x06) */ + wr32(FSPI_LUT(FLEXSPI_LUT_SEQ_WREN * 4 + 0), + LUT_WORD(LUT_INSTR(FLEXSPI_LUT_CMD, FLEXSPI_LUT_PAD1, + FLEXSPI_NOR_CMD_WREN), + LUT_INSTR(FLEXSPI_LUT_STOP, FLEXSPI_LUT_PAD1, 0))); + + /* RDSR (0x05): one status byte */ + wr32(FSPI_LUT(FLEXSPI_LUT_SEQ_RDSR * 4 + 0), + LUT_WORD(LUT_INSTR(FLEXSPI_LUT_CMD, FLEXSPI_LUT_PAD1, + FLEXSPI_NOR_CMD_RDSR), + LUT_INSTR(FLEXSPI_LUT_READ, FLEXSPI_LUT_PAD1, 1))); + + /* Sector erase (0xDC), 128 KB - see FLEXSPI_NOR_SECTOR_SIZE */ + wr32(FSPI_LUT(FLEXSPI_LUT_SEQ_SE * 4 + 0), + LUT_WORD(LUT_INSTR(FLEXSPI_LUT_CMD, FLEXSPI_LUT_PAD1, + FLEXSPI_NOR_CMD_SE_4B), + LUT_INSTR(FLEXSPI_LUT_RADDR, FLEXSPI_LUT_PAD1, 32))); + + /* Page program (0x12) */ + wr32(FSPI_LUT(FLEXSPI_LUT_SEQ_PP * 4 + 0), + LUT_WORD(LUT_INSTR(FLEXSPI_LUT_CMD, FLEXSPI_LUT_PAD1, + FLEXSPI_NOR_CMD_PP_4B), + LUT_INSTR(FLEXSPI_LUT_RADDR, FLEXSPI_LUT_PAD1, 32))); + wr32(FSPI_LUT(FLEXSPI_LUT_SEQ_PP * 4 + 1), + LUT_WORD(LUT_INSTR(FLEXSPI_LUT_WRITE, FLEXSPI_LUT_PAD1, 4), + LUT_INSTR(FLEXSPI_LUT_STOP, FLEXSPI_LUT_PAD1, 0))); + + flexspi_lut_lock(); +} + +static void flexspi_init(void) +{ + uint64_t deadline; + int i; + + /* Software reset, then hold the module disabled while it is configured. */ + wr32(FSPI_REG(FLEXSPI_MCR0), rd32(FSPI_REG(FLEXSPI_MCR0)) | + FLEXSPI_MCR0_SWRESET); + deadline = timer_deadline_us(FSPI_IPCMD_TIMEOUT_US); + while ((rd32(FSPI_REG(FLEXSPI_MCR0)) & FLEXSPI_MCR0_SWRESET) != 0) { + if (timer_expired(deadline)) + break; + } + wr32(FSPI_REG(FLEXSPI_MCR0), FLEXSPI_MCR0_MDIS); + + /* Bypass the DLL: at these single-pad SDR rates the delay line is unnecessary, and + * overriding it removes a lock step that can fail before the flash is probed. */ + wr32(FSPI_REG(FLEXSPI_DLLACR), FLEXSPI_DLLCR_OVRDEN); + wr32(FSPI_REG(FLEXSPI_DLLBCR), FLEXSPI_DLLCR_OVRDEN); + + /* Give each chip select its own configuration rather than mirroring A1. */ + wr32(FSPI_REG(FLEXSPI_MCR2), rd32(FSPI_REG(FLEXSPI_MCR2)) & + ~(uint32_t)FLEXSPI_MCR2_SAMEDEVEN); + + /* Flash A1 size, in KB. Only A1 is populated on the MEK; leaving the + * other three at 0 keeps their windows closed. */ + wr32(FSPI_REG(FLEXSPI_FLSHA1CR0), IMX8QM_FLEXSPI0_SIZE / 1024); + + /* Point AHB reads at the READ sequence. */ + wr32(FSPI_REG(FLEXSPI_FLSHA1CR2), + (uint32_t)FLEXSPI_LUT_SEQ_READ << FLEXSPI_FLSHCR2_ARDSEQID_S); + + /* Give all of the AHB RX buffer space to the last buffer, which is the + * one used by ordinary AHB reads. */ + for (i = 0; i < 7; i++) + wr32(FSPI_REG(FLEXSPI_AHBRXBUF0CR0 + i * 4), 0); + + wr32(FSPI_REG(FLEXSPI_AHBCR), FLEXSPI_AHBCR_PREFETCH); + + /* Enable the module with generous arbitration timeouts. */ + wr32(FSPI_REG(FLEXSPI_MCR0), + FLEXSPI_MCR0_AHBGRANTWAIT | FLEXSPI_MCR0_IPGRANTWAIT); + + flexspi_lut_setup(); + flexspi_ready = 1; +} + +/* Run one IP command sequence. Returns 0 on success, -1 on error/timeout. */ +static int flexspi_ip_cmd(uint32_t seq, uint32_t dev_off, uint32_t data_size) +{ + uint64_t deadline; + uint32_t intr; + + wr32(FSPI_REG(FLEXSPI_INTR), FLEXSPI_INTR_IPCMDDONE | FSPI_IP_ERR); + wr32(FSPI_REG(FLEXSPI_IPCR0), dev_off); + wr32(FSPI_REG(FLEXSPI_IPCR1), (seq << 16) | data_size); + wr32(FSPI_REG(FLEXSPI_IPCMD), FLEXSPI_IPCMD_TRG); + + deadline = timer_deadline_us(FSPI_IPCMD_TIMEOUT_US); + for (;;) { + intr = rd32(FSPI_REG(FLEXSPI_INTR)); + if ((intr & (FLEXSPI_INTR_IPCMDDONE | FSPI_IP_ERR)) != 0) + break; + if (timer_expired(deadline)) { + wolfBoot_printf("imx8qm fspi: IP command %u timed out\n", + (unsigned)seq); + return -1; + } + } + wr32(FSPI_REG(FLEXSPI_INTR), intr & (FLEXSPI_INTR_IPCMDDONE | FSPI_IP_ERR)); + if ((intr & FSPI_IP_ERR) != 0) { + wolfBoot_printf("imx8qm fspi: IP command %u error (INTR 0x%08x)\n", + (unsigned)seq, (unsigned)intr); + return -1; + } + return 0; +} + +static int flexspi_write_en(uint32_t dev_off) +{ + wr32(FSPI_REG(FLEXSPI_IPTXFCR), FLEXSPI_IPTXFCR_CLR); + return flexspi_ip_cmd(FLEXSPI_LUT_SEQ_WREN, dev_off, 0); +} + +static int flexspi_read_sr(uint32_t dev_off, uint8_t* sr) +{ + uint32_t data; + + wr32(FSPI_REG(FLEXSPI_IPRXFCR), FLEXSPI_IPRXFCR_CLR); + if (flexspi_ip_cmd(FLEXSPI_LUT_SEQ_RDSR, dev_off, 1) != 0) + return -1; + data = rd32(FSPI_REG(FLEXSPI_RFDR0)); + *sr = (uint8_t)(data & 0xFF); + wr32(FSPI_REG(FLEXSPI_IPRXFCR), FLEXSPI_IPRXFCR_CLR); + wr32(FSPI_REG(FLEXSPI_INTR), FLEXSPI_INTR_IPRXWA); + return 0; +} + +/* Wait out the device's program/erase cycle. IPCMDDONE only means the controller + * stopped driving; the device holds WIP and ignores Write Enable until it clears. */ +static int flexspi_wait_ready(uint32_t dev_off) +{ + uint64_t deadline = timer_deadline_us(FSPI_WIP_TIMEOUT_US); + uint8_t sr = 0; + + do { + if (flexspi_read_sr(dev_off, &sr) != 0) + return -1; + if ((sr & FLEXSPI_NOR_SR_WIP) == 0) + return 0; + } while (!timer_expired(deadline)); + + wolfBoot_printf("imx8qm fspi: WIP never cleared at 0x%08x\n", + (unsigned)dev_off); + return -1; +} + +/* AHB reads come from prefetch buffers an IP-path erase/program does not invalidate, + * so the window returns stale data. A software reset flushes them, keeping the LUT. */ +static void flexspi_ahb_flush(void) +{ + uint64_t deadline; + + wr32(FSPI_REG(FLEXSPI_MCR0), rd32(FSPI_REG(FLEXSPI_MCR0)) | + FLEXSPI_MCR0_SWRESET); + deadline = timer_deadline_us(FSPI_IPCMD_TIMEOUT_US); + while ((rd32(FSPI_REG(FLEXSPI_MCR0)) & FLEXSPI_MCR0_SWRESET) != 0) { + if (timer_expired(deadline)) + break; + } +} + +void ext_flash_unlock(void) +{ + if (!flexspi_ready) + flexspi_init(); +} + +void ext_flash_lock(void) +{ +} + +int ext_flash_read(uintptr_t address, uint8_t *data, int len) +{ + uintptr_t src; + + if (len < 0) + return -1; + if (!flexspi_ready) + flexspi_init(); + + /* Reads come from the memory-mapped AHB window. Accept either a device + * offset or an AHB address. */ + src = address; + if (src < IMX8QM_FLEXSPI0_AHB_BASE) + src += IMX8QM_FLEXSPI0_AHB_BASE; + /* Compared by subtraction rather than src + len: the addition wraps for a + * src near the top of the address space and would then pass a check it + * should fail. Both operands below are known not to wrap. */ + if ((src < IMX8QM_FLEXSPI0_AHB_BASE) || + (src > (uintptr_t)(IMX8QM_FLEXSPI0_AHB_BASE + IMX8QM_FLEXSPI0_SIZE)) || + ((uintptr_t)len > + (uintptr_t)(IMX8QM_FLEXSPI0_AHB_BASE + IMX8QM_FLEXSPI0_SIZE) - src)) + return -1; + + memcpy(data, (const void*)src, (size_t)len); + return len; +} + +int ext_flash_write(uintptr_t address, const uint8_t *data, int len) +{ + uint32_t off = flexspi_dev_off(address); + uint32_t remaining = (uint32_t)len; + uint32_t i, chunk, page_room, filled; + uint64_t deadline; + + if (len < 0) + return -1; + if (!flexspi_ready) + flexspi_init(); + /* Subtraction, not off + remaining: the addition wraps and would then + * pass a check it should fail. Same reasoning as ext_flash_read(). */ + if ((off > IMX8QM_FLEXSPI0_SIZE) || + (remaining > IMX8QM_FLEXSPI0_SIZE - off)) + return -1; + + while (remaining > 0) { + /* A page program must not cross a page boundary: the device's write pointer wraps to + * the start of the page and the excess bytes clobber preceding data. */ + page_room = FLEXSPI_NOR_PAGE_SIZE - (off % FLEXSPI_NOR_PAGE_SIZE); + chunk = remaining > page_room ? page_room : remaining; + + /* The device clears its write-enable latch after every program, so + * this is per page rather than once up front. */ + wr32(FSPI_REG(FLEXSPI_IPTXFCR), FLEXSPI_IPTXFCR_CLR); + if (flexspi_write_en(off) != 0) + return -1; + + /* Fill the IP TX FIFO a watermark at a time before triggering. */ + wr32(FSPI_REG(FLEXSPI_IPCR0), off); + filled = 0; + while (filled < chunk) { + uint32_t burst = chunk - filled; + + if (burst > FLEXSPI_IP_WM_BYTES) + burst = FLEXSPI_IP_WM_BYTES; + + deadline = timer_deadline_us(FSPI_IPCMD_TIMEOUT_US); + while ((rd32(FSPI_REG(FLEXSPI_INTR)) & FLEXSPI_INTR_IPTXWE) == 0) { + if (timer_expired(deadline)) { + wolfBoot_printf("imx8qm fspi: TX FIFO never drained\n"); + return -1; + } + } + for (i = 0; i < burst; i += 4) { + uint32_t word = 0; + uint32_t n = (burst - i) < 4 ? (burst - i) : 4; + + memcpy(&word, data + filled + i, n); + wr32(FSPI_REG(FLEXSPI_TFDR0 + i), word); + } + wr32(FSPI_REG(FLEXSPI_INTR), FLEXSPI_INTR_IPTXWE); + filled += burst; + } + + if (flexspi_ip_cmd(FLEXSPI_LUT_SEQ_PP, off, chunk) != 0) + return -1; + wr32(FSPI_REG(FLEXSPI_IPTXFCR), FLEXSPI_IPTXFCR_CLR); + if (flexspi_wait_ready(off) != 0) + return -1; + + data += chunk; + off += chunk; + remaining -= chunk; + } + flexspi_ahb_flush(); + return len; +} + +int ext_flash_erase(uintptr_t address, int len) +{ + uint32_t off = flexspi_dev_off(address); + uint32_t end; + + if (len <= 0) + return -1; + if (!flexspi_ready) + flexspi_init(); + + /* Subtraction, not off + len, for the reason given in ext_flash_read(). */ + if ((off > IMX8QM_FLEXSPI0_SIZE) || + ((uint32_t)len > IMX8QM_FLEXSPI0_SIZE - off)) + return -1; + end = off + (uint32_t)len; + + /* Erase every sector the range touches, including a partial head or tail: + * the caller is entitled to pass an unaligned address and length. */ + off -= off % FLEXSPI_NOR_SECTOR_SIZE; + while (off < end) { + if (flexspi_write_en(off) != 0) + return -1; + if (flexspi_ip_cmd(FLEXSPI_LUT_SEQ_SE, off, 0) != 0) + return -1; + if (flexspi_wait_ready(off) != 0) + return -1; + off += FLEXSPI_NOR_SECTOR_SIZE; + } + flexspi_ahb_flush(); + return len; +} +#endif /* EXT_FLASH */ + +/* uSDHC -> SDHCI shim (src/sdhci.c uses only sdhci_reg_read/write): transfer mode in + * MIX_CTRL, width in PROT_CTRL, DVS/SDCLKFS divider, no error summary, DMA err 25->28. */ + +#if defined(DISK_SDCARD) || defined(DISK_EMMC) + +/* Not exported by include/sdhci.h; every in-tree SDHCI platform defines it + * locally (hal/versal.c, hal/zynq7000.c, hal/cm4.h). */ +#ifndef CADENCE_SRS_OFFSET +#define CADENCE_SRS_OFFSET 0x200 +#endif + +#ifndef USDHC_RESET_TIMEOUT_US +#define USDHC_RESET_TIMEOUT_US 1000000 +#endif +#ifndef USDHC_INIT_TIMEOUT_US +#define USDHC_INIT_TIMEOUT_US 100000 +#endif +#ifndef USDHC_CLK_STABLE_TIMEOUT_US +#define USDHC_CLK_STABLE_TIMEOUT_US 100000 +#endif + +/* Standard-SDHCI offsets, as seen after the Cadence SRS base is removed. */ +#define STD_BLK 0x04 +#define STD_CMD 0x0C +#define STD_PRES_STATE 0x24 +#define STD_HOST_CTRL1 0x28 +#define STD_CLOCK_CTRL 0x2C +#define STD_INT_STATUS 0x30 +#define STD_INT_STATUS_EN 0x34 +#define STD_INT_SIGNAL_EN 0x38 +#define STD_HOST_CTRL2 0x3C +#define STD_CAPS1 0x40 +#define STD_CAPS2 0x44 +#define STD_MAX_CURRENT 0x48 +#define STD_ADMA_ADDR_LO 0x58 +#define STD_ADMA_ADDR_HI 0x5C + +/* uSDHC-only error bits with no standard-SDHCI position. */ +#define USDHC_INT_DMAE (1U << 28) +#define USDHC_INT_TNE (1U << 26) + +/* Fields the driver reads back that uSDHC does not implement. */ +static uint32_t srs10_shadow; /* bus power / bus voltage / high speed */ +static uint32_t srs15_shadow; /* host control 2 */ +/* The rate sdhci_platform_set_clock() achieved, reported as the base so the generic + * driver's divider is 1. NOT the CAPS1 base field, which stays the source rate. */ +static uint32_t usdhc_achieved_clk_khz = IMX8QM_USDHC_PERCLK_HZ / 1000; + +uint32_t sdhci_reg_read(uint32_t offset) +{ + uintptr_t b = IMX8QM_USDHC_BASE; + uint32_t std, v, raw; + + if (offset < CADENCE_SRS_OFFSET) { + /* Cadence HRS range: uSDHC has no equivalent. Report the PHY + * handshake as acknowledged so the driver's wait loops still exit. */ + return (offset == SDHCI_HRS04) ? SDHCI_HRS04_UIS_ACK : 0; + } + std = offset - CADENCE_SRS_OFFSET; + + switch (std) { + case STD_PRES_STATE: + v = rd32(b + USDHC_PRES_STATE); + /* Card state stable has no uSDHC bit; the card-inserted bit is + * already debounced, so report it as always stable. */ + v |= SDHCI_SRS09_CSS; + /* DAT0 level moves from the DLSL field (bit 24) to bit 20. */ + if ((v & USDHC_PRES_DLSL_DAT0) != 0) + v |= SDHCI_SRS09_DAT0_LVL; + else + v &= ~SDHCI_SRS09_DAT0_LVL; + return v; + + case STD_HOST_CTRL1: + v = srs10_shadow & ~(uint32_t)(SDHCI_SRS10_DTW | SDHCI_SRS10_EDTW); + switch (rd32(b + USDHC_PROT_CTRL) & USDHC_PROT_DTW_MASK) { + case USDHC_PROT_DTW_4BIT: v |= SDHCI_SRS10_DTW; break; + case USDHC_PROT_DTW_8BIT: v |= SDHCI_SRS10_EDTW; break; + default: break; + } + return v; + + case STD_CLOCK_CTRL: + /* No internal clock-enable or stable bit; report both satisfied. */ + v = rd32(b + USDHC_SYS_CTRL) & + (uint32_t)(USDHC_SYS_DTOCV_MASK | USDHC_SYS_RSTA | + USDHC_SYS_RSTC | USDHC_SYS_RSTD); + return v | SDHCI_SRS11_ICE | SDHCI_SRS11_ICS | SDHCI_SRS11_SDCE; + + case STD_INT_STATUS: + case STD_INT_STATUS_EN: + case STD_INT_SIGNAL_EN: + raw = rd32(b + std); + v = raw; + if ((raw & USDHC_INT_DMAE) != 0) + v |= SDHCI_SRS12_EADMA; + /* Hide uSDHC-only bits and synthesize the summary the driver polls. TNE has no standard + * bit, so test the raw value: otherwise a latched tuning error stalls the transfer. */ + v &= ~(uint32_t)(USDHC_INT_DMAE | USDHC_INT_TNE); + if (std == STD_INT_STATUS && + (((v & SDHCI_SRS12_ERR_STAT) != 0) || + ((raw & USDHC_INT_TNE) != 0))) { + v |= SDHCI_SRS12_EINT; + } + return v; + + case STD_HOST_CTRL2: + return srs15_shadow; + + case STD_CAPS1: + /* uSDHC zeroes these; synthesize a 50 MHz timeout clock and the + * real peripheral clock. 64-bit addressing masked off (PIO). */ + v = rd32(b + USDHC_HOST_CTRL_CAP) & + (uint32_t)(USDHC_CAP_VS33 | USDHC_CAP_VS30 | USDHC_CAP_VS18); + v |= (50U << SDHCI_SRS16_TCF_SHIFT) & SDHCI_SRS16_TCF_MASK; + v |= SDHCI_SRS16_TCU; /* timeout clock is in MHz */ + /* Constant source clock in MHz; the achieved rate would truncate to 0 MHz once the + * 400 kHz identification clock is programmed. */ + v |= ((IMX8QM_USDHC_PERCLK_HZ / 1000000U) + << SDHCI_SRS16_BCSDCLK_SHIFT) & + SDHCI_SRS16_BCSDCLK_MASK; + return v; + + case STD_CAPS2: + return 0; + + case STD_ADMA_ADDR_LO: + /* SDMA, not ADMA2: PROT_CTRL selects DMASEL=simple, so SRS22 is uSDHC's DS_ADDR (the + * running address the boundary handler rewrites to resume), not ADMA_SYS_ADDR. */ + return rd32(b + USDHC_DS_ADDR); + + case STD_ADMA_ADDR_HI: + return 0; /* 32-bit ADMA only */ + + case STD_MAX_CURRENT: + /* uSDHC has no max-current register, and this offset is its live + * MIX_CTRL. Passing that through would feed transfer-mode bits to + * the driver's XPC decision as if they were milliamps. Report no + * capability, as STD_CAPS2 does, which leaves XPC off. */ + return 0; + + default: + return rd32(b + std); + } +} + +void sdhci_reg_write(uint32_t offset, uint32_t val) +{ + uintptr_t b = IMX8QM_USDHC_BASE; + uint32_t std, v, mix; + + if (offset < CADENCE_SRS_OFFSET) { + /* The only Cadence host register the driver writes is the software + * reset; route it to the uSDHC reset-all bit. */ + if (offset == SDHCI_HRS00 && (val & SDHCI_HRS00_SWR) != 0) + wr32(b + USDHC_SYS_CTRL, + rd32(b + USDHC_SYS_CTRL) | USDHC_SYS_RSTA); + return; + } + std = offset - CADENCE_SRS_OFFSET; + + switch (std) { + case STD_BLK: + /* Bit 12 is block size here, SDMA boundary in standard SDHCI. */ + wr32(b + USDHC_BLK_ATT, + (val & 0xFFFF0000U) | (val & 0x0FFFU)); + return; + + case STD_CMD: + /* MIX_CTRL first: writing CMD_XFR_TYP starts the command. Bits + * above 5 of MIX_CTRL have no standard equivalent; preserve. */ + mix = rd32(b + USDHC_MIX_CTRL) & ~(uint32_t)USDHC_MIX_CTRL_XFER_MASK; + mix |= val & USDHC_MIX_CTRL_XFER_MASK; + wr32(b + USDHC_MIX_CTRL, mix); + + v = val & 0xFFFF0000U; + /* The Cadence response-check bits alias to "48-bit with busy". SD data commands are + * R1, not R1b, so a busy check would wait on DAT0 forever; downgrade. */ + if ((v & USDHC_XFR_DPSEL) != 0 && + (v & USDHC_XFR_RSPTYP_MASK) == USDHC_XFR_RSPTYP_48B) { + v = (v & ~(uint32_t)USDHC_XFR_RSPTYP_MASK) | + USDHC_XFR_RSPTYP_48; + } + wr32(b + USDHC_CMD_XFR_TYP, v); + return; + + case STD_HOST_CTRL1: + srs10_shadow = val; + v = rd32(b + USDHC_PROT_CTRL) & + ~(uint32_t)(USDHC_PROT_DTW_MASK | USDHC_PROT_DMASEL_MASK); + if ((val & SDHCI_SRS10_EDTW) != 0) + v |= USDHC_PROT_DTW_8BIT; + else if ((val & SDHCI_SRS10_DTW) != 0) + v |= USDHC_PROT_DTW_4BIT; + else + v |= USDHC_PROT_DTW_1BIT; + /* Standard SDHCI selects the DMA engine in bits 3:4; uSDHC uses + * bits 8:9. Only simple (SDMA) is ever requested here. */ + v |= USDHC_PROT_DMASEL_SIMPLE; + wr32(b + USDHC_PROT_CTRL, v); + return; + + case STD_CLOCK_CTRL: + /* Only the shared fields; the divider is owned by + * sdhci_platform_set_clock, so drop the standard divisor bits. */ + v = rd32(b + USDHC_SYS_CTRL) & ~(uint32_t)USDHC_SYS_DTOCV_MASK; + v |= val & USDHC_SYS_DTOCV_MASK; + v |= val & (uint32_t)(USDHC_SYS_RSTA | USDHC_SYS_RSTC | + USDHC_SYS_RSTD); + wr32(b + USDHC_SYS_CTRL, v); + return; + + case STD_INT_STATUS: + /* Write-1-to-clear. Map the standard ADMA error bit onto the uSDHC one and clear TNE + * with any error or the summary: the driver never sees TNE, so this is its only exit. */ + v = val; + if ((val & SDHCI_SRS12_EADMA) != 0) + v |= USDHC_INT_DMAE; + if ((val & (SDHCI_SRS12_ERR_STAT | SDHCI_SRS12_EINT)) != 0) + v |= USDHC_INT_TNE; + wr32(b + USDHC_INT_STATUS, v & ~(uint32_t)SDHCI_SRS12_EINT); + return; + + case STD_INT_STATUS_EN: + case STD_INT_SIGNAL_EN: + /* On uSDHC a status bit does not latch unless its enable is set, so the uSDHC-only + * error enables must be added to the mask the generic driver builds. */ + v = val & ~(uint32_t)SDHCI_SRS12_EINT; + if ((val & SDHCI_SRS12_ERR_STAT) != 0) + v |= USDHC_INT_DMAE | USDHC_INT_TNE; + wr32(b + std, v); + return; + + case STD_HOST_CTRL2: + srs15_shadow = val; + /* 1.8V signaling lives in VEND_SPEC on uSDHC. */ + v = rd32(b + USDHC_VEND_SPEC); + if ((val & SDHCI_SRS15_V18SE) != 0) + v |= USDHC_VEND_SPEC_VSELECT; + else + v &= ~(uint32_t)USDHC_VEND_SPEC_VSELECT; + wr32(b + USDHC_VEND_SPEC, v); + return; + + case STD_CAPS1: + case STD_CAPS2: + return; /* read-only */ + + case STD_ADMA_ADDR_LO: + wr32(b + USDHC_DS_ADDR, val); /* see the read side */ + return; + + case STD_ADMA_ADDR_HI: + return; /* 32-bit ADMA only */ + + default: + wr32(b + std, val); + return; + } +} + +void sdhci_platform_init(void) +{ + uintptr_t b = IMX8QM_USDHC_BASE; + uint64_t deadline; + uint32_t v; + + /* Reset the controller, then apply the settings that survive it. */ + wr32(b + USDHC_SYS_CTRL, rd32(b + USDHC_SYS_CTRL) | USDHC_SYS_RSTA); + deadline = timer_deadline_us(USDHC_RESET_TIMEOUT_US); + while ((rd32(b + USDHC_SYS_CTRL) & USDHC_SYS_RSTA) != 0) { + if (timer_expired(deadline)) { + wolfBoot_printf("imx8qm usdhc: reset-all did not clear\n"); + break; + } + } + + /* Little-endian data port, card-detect from DAT3 disabled (the MEK wires + * a dedicated card-detect GPIO, and on eMMC there is nothing to detect). */ + v = rd32(b + USDHC_PROT_CTRL); + v = (v & ~(uint32_t)USDHC_PROT_EMODE_MASK) | USDHC_PROT_EMODE_LE; + v &= ~(uint32_t)USDHC_PROT_D3CD; + wr32(b + USDHC_PROT_CTRL, v); + + /* Move a whole 512-byte block per watermark event, so the PIO loop sees one Buffer + * Read Ready per block rather than one every 32 bytes. */ + wr32(b + USDHC_WTMK_LVL, + ((uint32_t)USDHC_WTMK_BLOCK_WORDS << USDHC_WTMK_RD_SHIFT) | + ((uint32_t)USDHC_WTMK_BLOCK_WORDS << USDHC_WTMK_WR_SHIFT)); + + /* Send the 80 initialization clocks the card needs before CMD0. */ + wr32(b + USDHC_SYS_CTRL, rd32(b + USDHC_SYS_CTRL) | USDHC_SYS_INITA); + deadline = timer_deadline_us(USDHC_INIT_TIMEOUT_US); + while ((rd32(b + USDHC_SYS_CTRL) & USDHC_SYS_INITA) != 0) { + if (timer_expired(deadline)) + break; + } +} + +/* uSDHC divides by (SDCLKFS prescaler) * (DVS + 1). Reporting the requested + * rate back as the base clock makes the generic driver's divider settle on 1. */ +uint32_t sdhci_platform_set_clock(uint32_t clock_khz, uint32_t base_clk_khz) +{ + uintptr_t b = IMX8QM_USDHC_BASE; + uint32_t target_hz, pre, dvs, v; + uint64_t deadline; + + (void)base_clk_khz; + + if (clock_khz == 0) + return 0; + + /* Optional ceiling: sustained reads fail CRC at the 50 MHz the generic + * driver steps to. Raising it needs uSDHC delay-line tuning. */ +#ifdef IMX8QM_USDHC_MAX_CLK_KHZ + if (clock_khz > IMX8QM_USDHC_MAX_CLK_KHZ) + clock_khz = IMX8QM_USDHC_MAX_CLK_KHZ; +#endif + + target_hz = clock_khz * 1000U; + + /* Smallest prescaler/divisor pair not exceeding the requested rate; the prescaler is + * a power of two from 1 to 256 and the divisor runs 1..16. */ + for (pre = 1; pre <= 256; pre <<= 1) { + for (dvs = 1; dvs <= 16; dvs++) { + if ((IMX8QM_USDHC_PERCLK_HZ / (pre * dvs)) <= target_hz) + goto found; + } + } + pre = 256; + dvs = 16; +found: + v = rd32(b + USDHC_SYS_CTRL) & + ~(uint32_t)(USDHC_SYS_DVS_MASK | USDHC_SYS_SDCLKFS_MASK); + /* SDCLKFS is a one-hot prescaler code: 0x01 = divide by 2, and each + * further bit doubles it. A prescaler of 1 is encoded as 0. */ + v |= ((pre >> 1) << USDHC_SYS_SDCLKFS_SHIFT) & USDHC_SYS_SDCLKFS_MASK; + v |= ((dvs - 1) << USDHC_SYS_DVS_SHIFT) & USDHC_SYS_DVS_MASK; + wr32(b + USDHC_SYS_CTRL, v); + + /* Wait for the divided clock to settle before any command is issued. */ + deadline = timer_deadline_us(USDHC_CLK_STABLE_TIMEOUT_US); + while ((rd32(b + USDHC_PRES_STATE) & USDHC_PRES_SDSTB) == 0) { + if (timer_expired(deadline)) { + wolfBoot_printf("imx8qm usdhc: SD clock never stabilized\n"); + return 0; + } + } + + usdhc_achieved_clk_khz = (IMX8QM_USDHC_PERCLK_HZ / (pre * dvs)) / 1000U; + return usdhc_achieved_clk_khz; +} + +void sdhci_platform_irq_init(void) +{ + /* Polled mode: no GIC wiring needed for the boot path. */ +} + +void sdhci_platform_set_bus_mode(int is_emmc) +{ + /* The bus width is driven through Host Control 1, which the shim + * translates; there is no separate eMMC/SD mode select on uSDHC. */ + (void)is_emmc; +} + +#endif /* DISK_SDCARD || DISK_EMMC */ + + +/* Identity MMU (IMX8QM_MMU): MMU-off DRAM is uncached, so hashing is impractical. Maps + * the low 4 GB Normal cacheable, torn down in hal_prepare_boot(). Follows hal/cm4.c. */ + +#ifdef IMX8QM_MMU + +/* Normal = AttrIdx0 + AF + inner-shareable; Device = AttrIdx1 + AF + XN, since + * an instruction fetch from Device memory is CONSTRAINED UNPREDICTABLE. */ +#define MMU_BLOCK_NORMAL 0x0000000000000701ULL +#define MMU_BLOCK_DEVICE (0x0000000000000405ULL | (1ULL << 54) | (1ULL << 53)) + +static volatile uint64_t imx8qm_l1_table[512] __attribute__((aligned(4096))); + +#if (defined(DISK_SDCARD) || defined(DISK_EMMC)) && !defined(SDHCI_SDMA_DISABLED) +/* DMA cache maintenance for the SDMA path by address range, not the set/way walk used + * at handoff, which would cost more per transfer than the DMA saves. No-op MMU-off. */ +static void imx8qm_dcache_range(uintptr_t start, uint32_t sz, int invalidate) +{ + uintptr_t line, end; + uint64_t ctr; + unsigned int dminline; + + if (sz == 0) + return; + /* CTR_EL0.DminLine is log2 of the smallest data cache line, in words. */ + __asm__ volatile("mrs %0, ctr_el0" : "=r"(ctr)); + dminline = (unsigned int)((ctr >> 16) & 0xF); + line = (uintptr_t)4 << dminline; + + end = (start + sz + line - 1) & ~(line - 1); + start &= ~(line - 1); + __asm__ volatile("dsb sy"); + for (; start < end; start += line) { + if (invalidate) + __asm__ volatile("dc ivac, %0" :: "r"(start) : "memory"); + else + __asm__ volatile("dc civac, %0" :: "r"(start) : "memory"); + } + __asm__ volatile("dsb sy"); + __asm__ volatile("isb"); +} + +void sdhci_platform_dma_prepare(void* buf, uint32_t sz, int is_write) +{ + /* Outbound data must reach memory before the controller reads it. Clean on inbound + * too: a dirty line over the buffer could evict onto data the controller wrote. */ + imx8qm_dcache_range((uintptr_t)buf, sz, 0); + (void)is_write; +} + +void sdhci_platform_dma_complete(void* buf, uint32_t sz, int is_write) +{ + /* Inbound data landed in memory behind the cache's back; drop any stale + * lines so the CPU reads what the controller wrote. */ + if (!is_write) + imx8qm_dcache_range((uintptr_t)buf, sz, 1); +} +#endif /* (DISK_SDCARD || DISK_EMMC) && !SDHCI_SDMA_DISABLED */ + + +/* The registers below are EL2 ones. ATF enters BL33 at EL2 on this SoC, but + * check rather than trap silently if that ever changes. */ +static int imx8qm_mmu_at_el2(void) +{ + unsigned long el; + + __asm__ volatile("mrs %0, CurrentEL" : "=r"(el)); + if (((el >> 2) & 0x3) != 2) { + wolfBoot_printf("imx8qm: MMU needs EL2, running at EL%d; leaving it off\n", + (int)((el >> 2) & 0x3)); + return 0; + } + return 1; +} + +static int imx8qm_mmu_on; + +void imx8qm_mmu_enable(void) +{ + unsigned long sctlr; + int i; + + if (!imx8qm_mmu_at_el2()) + return; + + /* Low 4 GB in 1 GB blocks: DRAM (>= 0x80000000) Normal, the rest Device. */ + for (i = 0; i < 4; i++) { + uint64_t base = (uint64_t)i << 30; + imx8qm_l1_table[i] = base | + ((base >= IMX8QM_DRAM_BASE) ? MMU_BLOCK_NORMAL : MMU_BLOCK_DEVICE); + } + + /* Attr0 = 0xFF Normal write-back write-allocate, Attr1 = 0x00 Device. */ + __asm__ volatile("msr mair_el2, %0" :: "r"(0x00000000000000FFUL)); + __asm__ volatile("msr ttbr0_el2, %0" + :: "r"((uint64_t)(uintptr_t)imx8qm_l1_table)); + /* T0SZ=32 (32-bit VA), 4 KB granule, cacheable inner-shareable table + * walks. Bits 31 and 23 are RES1 for TCR_EL2 when E2H is 0. */ + __asm__ volatile("msr tcr_el2, %0" + :: "r"(0x0000000000013520UL | (1UL << 31) | (1UL << 23))); + __asm__ volatile("isb"); + __asm__ volatile("tlbi alle2"); + __asm__ volatile("dsb sy"); + + /* Drop anything an earlier stage left in the caches before turning them + * on, so no stale line surfaces once caching is live. */ + aarch64_dcache_maint(0); + __asm__ volatile("ic iallu"); + __asm__ volatile("dsb sy"); + __asm__ volatile("isb"); + + __asm__ volatile("mrs %0, sctlr_el2" : "=r"(sctlr)); + sctlr |= (1UL << 0) | (1UL << 2) | (1UL << 12); /* M | C | I */ + __asm__ volatile("msr sctlr_el2, %0" :: "r"(sctlr)); + __asm__ volatile("isb"); + imx8qm_mmu_on = 1; +} + +/* In src/boot_aarch64_start.S and deliberately in assembly: it touches no stack. A C + * set/way walk re-dirties lines through its stack counters. See docs/Targets.md. */ +extern void el2_flush_and_disable_mmu(void); + +void imx8qm_mmu_disable(void) +{ + if (!imx8qm_mmu_on) + return; + + /* Cleared before the teardown so the store is written back with everything else; + * afterwards the D-cache is off and it would reach DRAM either way. */ + imx8qm_mmu_on = 0; + el2_flush_and_disable_mmu(); + /* With M clear the stale EL2 mappings can no longer be used, but drop them + * anyway so nothing inherits them. Register-only, so no stack hazard. */ + __asm__ volatile("tlbi alle2"); + __asm__ volatile("dsb sy"); + __asm__ volatile("isb"); +} +#endif /* IMX8QM_MMU */ + + +/* EL2 exception report */ + +#if defined(DEBUG) && defined(DEBUG_UART) + +/* Image bounds from hal/imx8qm.ld, used to say whether the faulting PC is + * inside wolfBoot itself. */ +extern uint8_t _start_text[]; +extern uint8_t _end[]; + +/* From simple_el2_fault_common (src/boot_aarch64_start.S); the vector halts in + * wfi after this. Slots 0-7 are current EL (wolfBoot), 8-15 lower EL. */ +void simple_el2_fault_handler(unsigned long esr, unsigned long elr, + unsigned long far, unsigned long vector); +void simple_el2_fault_handler(unsigned long esr, unsigned long elr, + unsigned long far, unsigned long vector) +{ + const char* from; + uintptr_t pc; + + from = (vector >= 8) ? "lower EL (payload)" : "current EL (wolfBoot)"; + pc = (uintptr_t)elr; + + wolfBoot_printf("\n*** i.MX8QM EL2 EXCEPTION ***\n"); + wolfBoot_printf("vector=%d from %s\n", (int)vector, from); + wolfBoot_printf("ESR=0x%08x EC=0x%02x ISS=0x%06x\n", + (uint32_t)esr, (uint32_t)((esr >> 26) & 0x3F), + (uint32_t)(esr & 0x1FFFFFUL)); + wolfBoot_printf("ELR=0x%08x%08x\n", + (uint32_t)(elr >> 32), (uint32_t)(elr & 0xFFFFFFFFUL)); + wolfBoot_printf("FAR=0x%08x%08x\n", + (uint32_t)(far >> 32), (uint32_t)(far & 0xFFFFFFFFUL)); + if (pc >= (uintptr_t)_start_text && pc < (uintptr_t)_end) { + wolfBoot_printf("ELR is inside wolfBoot (+0x%x from 0x%08x)\n", + (uint32_t)(pc - (uintptr_t)_start_text), + (uint32_t)(uintptr_t)_start_text); + } + else { + wolfBoot_printf("ELR is outside wolfBoot [0x%08x-0x%08x)\n", + (uint32_t)(uintptr_t)_start_text, (uint32_t)(uintptr_t)_end); + } +} +#endif /* DEBUG && DEBUG_UART */ + +/* Device tree fixups. The stock imx8qm-mek.dtb relies on U-Boot patching it: a 1 GB + * placeholder /memory and no /chosen/bootargs. wolfBoot does the same fixups. */ + +#if defined(MMU) && defined(WOLFBOOT_FDT) + +#include "fdt.h" + +/* Console LPUART0 (ttyLP0); root defaults to the SD card's third partition (uSDHC2 is + * mmcblk1, uSDHC1 the eMMC). Override LINUX_BOOTARGS or LINUX_BOOTARGS_ROOT. */ +#ifndef LINUX_BOOTARGS +#ifndef LINUX_BOOTARGS_ROOT +#define LINUX_BOOTARGS_ROOT "/dev/mmcblk1p3" +#endif +#define LINUX_BOOTARGS \ + "console=ttyLP0,115200 earlycon root=" LINUX_BOOTARGS_ROOT \ + " rootfstype=ext4 rootwait rw" +#endif + +/* Matching U-Boot's PHYS_SDRAM_1 / PHYS_SDRAM_2. Constants because the MEK is a + * fixed 6 GB board; override from the config for a different fit. */ +#ifndef IMX8QM_DRAM_BANK0_BASE +#define IMX8QM_DRAM_BANK0_BASE 0x0000000080000000ULL +#endif +#ifndef IMX8QM_DRAM_BANK0_SIZE +#define IMX8QM_DRAM_BANK0_SIZE 0x0000000080000000ULL /* 2 GB */ +#endif +#ifndef IMX8QM_DRAM_BANK1_BASE +#define IMX8QM_DRAM_BANK1_BASE 0x0000000880000000ULL +#endif +#ifndef IMX8QM_DRAM_BANK1_SIZE +#define IMX8QM_DRAM_BANK1_SIZE 0x0000000100000000ULL /* 4 GB */ +#endif + +/* Full path of the SD slot's controller node in the stock imx8qm-mek.dtb + * (uSDHC2; uSDHC1 at 5b010000 is the eMMC and must keep 1.8V for HS200/HS400). */ +#ifndef IMX8QM_SD_DT_PATH +#define IMX8QM_SD_DT_PATH "/bus@5b000000/mmc@5b020000" +#endif + +/* Bus clock ceiling published to the OS for the SD slot. Matches the cap + * wolfBoot's own driver already applies (IMX8QM_USDHC_MAX_CLK_KHZ). */ +#ifndef IMX8QM_SD_DT_MAX_FREQ_HZ +#define IMX8QM_SD_DT_MAX_FREQ_HZ 25000000U +#endif + +int hal_dts_fixup(void* dts_addr, uint32_t capacity) +{ + fdt_ctx ctx; + uint64_t reg[4]; + int off; + int ret; + + ret = fdt_open(&ctx, dts_addr, capacity); + if (ret != 0) { + wolfBoot_printf("FDT: invalid header (%d)\n", ret); + return ret; + } + + /* Both fixups add or grow properties, so make room first. */ + ret = fdt_grow(&ctx, WOLFBOOT_FDT_FIXUP_HEADROOM); + if (ret != 0) { + wolfBoot_printf("FDT: no headroom for fixups (%d)\n", ret); + return ret; + } + + /* /chosen/bootargs. Create /chosen only if genuinely absent; any other negative return + * is a malformed FDT, surfaced rather than masked by a later add_subnode failure. */ + 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: /chosen error (%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; + } + + /* Opt-in "no-1-8-v" plus a clock cap for a path that cannot carry UHS-I, such as an SD + * multiplexer. Off by default. The eMMC node is untouched; it needs 1.8V for HS200/400. */ +#if (defined(DISK_SDCARD) || defined(DISK_EMMC)) && defined(IMX8QM_SD_NO_UHS) + off = fdt_path_offset(&ctx, IMX8QM_SD_DT_PATH); + if (off < 0) { + wolfBoot_printf("FDT: %s not found (%d); SD left UHS-capable\n", + IMX8QM_SD_DT_PATH, off); + } + else { + /* Empty property: presence is the flag. */ + ret = fdt_setprop(&ctx, off, "no-1-8-v", NULL, 0); + if (ret != 0) { + wolfBoot_printf("FDT: no-1-8-v fixup failed (%d)\n", ret); + return ret; + } + /* Cap the clock too: "no-1-8-v" only rules out UHS, leaving 50 MHz high-speed, which + * this slot is also unreliable at. Uncapped, root mounts and then fails under load. */ + { + uint32_t hz = cpu_to_fdt32(IMX8QM_SD_DT_MAX_FREQ_HZ); + + ret = fdt_setprop(&ctx, off, "max-frequency", &hz, + (int)sizeof(hz)); + if (ret != 0) { + wolfBoot_printf("FDT: max-frequency fixup failed (%d)\n", ret); + return ret; + } + } + wolfBoot_printf("FDT: SD pinned to 3.3V, max %u Hz\n", + (unsigned)IMX8QM_SD_DT_MAX_FREQ_HZ); + } +#endif + + /* Located by device_type, not unit address. Root is #address/#size-cells + * = 2, so each bank is a pair of big-endian 64-bit words. */ + off = fdt_find_devtype(&ctx, 0, "memory"); + if (off < 0) { + /* Not fatal on its own: a DTB that already describes memory correctly + * (or a payload that does not need it) still boots. Say so loudly. */ + wolfBoot_printf("FDT: no /memory node (%d); leaving memory as-is\n", + off); + return 0; + } + reg[0] = cpu_to_fdt64(IMX8QM_DRAM_BANK0_BASE); + reg[1] = cpu_to_fdt64(IMX8QM_DRAM_BANK0_SIZE); + reg[2] = cpu_to_fdt64(IMX8QM_DRAM_BANK1_BASE); + reg[3] = cpu_to_fdt64(IMX8QM_DRAM_BANK1_SIZE); + ret = fdt_setprop(&ctx, off, "reg", reg, (int)sizeof(reg)); + if (ret != 0) { + wolfBoot_printf("FDT: memory fixup failed (%d)\n", ret); + return ret; + } + wolfBoot_printf("FDT: memory 0x%08x+0x%08x, 0x%x_%08x+0x%x_%08x\n", + (uint32_t)IMX8QM_DRAM_BANK0_BASE, (uint32_t)IMX8QM_DRAM_BANK0_SIZE, + (uint32_t)(IMX8QM_DRAM_BANK1_BASE >> 32), + (uint32_t)IMX8QM_DRAM_BANK1_BASE, + (uint32_t)(IMX8QM_DRAM_BANK1_SIZE >> 32), + (uint32_t)IMX8QM_DRAM_BANK1_SIZE); + return 0; +} +#endif /* MMU && WOLFBOOT_FDT */ + +/* Public HAL entry points */ + +void hal_init(void) +{ + /* Must run first: the SCU powers the console too, so a banner printed + * before this goes nowhere. Failures are buffered and printed below. */ + imx8qm_scu_init(); +#if defined(DEBUG_UART) + uart_init(); + wolfBoot_printf("wolfBoot i.MX 8QuadMax (bare-metal BL33)\n"); + imx8qm_scu_report(); +#endif +#ifdef IMX8QM_MMU + /* After the console, so a refusal can be reported. Verification of a large + * image is impractically slow without this; see the MMU section above. */ + imx8qm_mmu_enable(); +#endif +} + +void hal_prepare_boot(void) +{ + /* No D-cache maintenance needed (MMU off), but SCTLR.I is IMPLEMENTATION + * DEFINED at BL33 entry, so invalidate the I-cache before branching. */ +#if defined(DISK_SDCARD) || defined(DISK_EMMC) + /* Quiesce the controller so the OS does not inherit a uSDHC this + * firmware left clocked and running. */ + sdhci_shutdown(); +#endif +#ifdef IMX8QM_MMU + /* Writes the verified payload back to DRAM and returns the core to the + * MMU-off, 1:1 state the arm64 boot protocol expects. */ + imx8qm_mmu_disable(); +#endif + __asm__ volatile( + "dsb sy \n" + "ic iallu \n" + "dsb sy \n" + "isb \n" + ::: "memory"); +} + +/* No internal flash: wolfBoot is loaded into DRAM by an earlier stage and the serial + * NOR is reached via ext_flash_* above. Provide the required no-op HAL surface. */ +int RAMFUNCTION hal_flash_write(uintptr_t address, const uint8_t *data, int len) +{ + (void)address; (void)data; (void)len; + return 0; +} + +void RAMFUNCTION hal_flash_unlock(void) +{ +} + +void RAMFUNCTION hal_flash_lock(void) +{ +} + +int RAMFUNCTION hal_flash_erase(uintptr_t address, int len) +{ + (void)address; (void)len; + return 0; +} diff --git a/hal/imx8qm.h b/hal/imx8qm.h new file mode 100644 index 0000000000..0371407bd7 --- /dev/null +++ b/hal/imx8qm.h @@ -0,0 +1,340 @@ +/* imx8qm.h + * + * 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 + */ + +/* NXP i.MX 8QuadMax (MIMX8QM) definitions for the MCIMX8QM-MEK; register bases from + * the DTB. boot_aarch64_start.S includes this, so keep assembly off the C-only pads. */ + +#ifndef _IMX8QM_H_ +#define _IMX8QM_H_ + +/* Simple AArch64 startup: SCFW trains DDR and ATF BL31 has already run, so no + * builtin EL3/MMU init is needed here (same model as tegra234). */ +#define USE_BUILTIN_STARTUP +#define USE_SIMPLE_STARTUP + +/* BL33 image layout. ATF enters at 0x80020000, where u-boot.bin links, so the stock + * imx-mkimage recipe fits. Keep in sync with imx8qm.ld and imx8qm-mkflashbin.sh. */ +#define IMX8QM_BL33_BASE 0x80020000 +/* Low DRAM window, used to sanity-check handoff pointers */ +#define IMX8QM_DRAM_BASE 0x80000000 +#define IMX8QM_DRAM_END 0x100000000 +#define IMX8QM_BUNDLE_OFFSET 0x200000 /* 2 MB: signed payload */ +#define IMX8QM_DTB_OFFSET 0x300000 /* 3 MB: device tree */ +#define IMX8QM_BL33_MAX_SIZE 0x400000 /* container image cap */ + +/* Update partition, staged in low DRAM above the BL33 image. A constant, not a linker + * symbol: an ADRP against an absolute symbol this far out exceeds its +/-4GB reach. */ +#define IMX8QM_UPDATE_ADDR 0x88000000 + +/* Generic timer fallback if firmware left CNTFRQ_EL0 unprogrammed; the i.MX8QM system + * counter runs at 8 MHz. */ +#ifndef TIMER_CLK_FREQ +#define TIMER_CLK_FREQ 8000000 +#endif + +/* Console LPUART0: stdout-path in imx8qm-mek.dts, J11 FTDI interface 0, 115200 8N1. + * 32-bit little-endian map (UPIO_MEM32) with reg_off 0x10, folded into the offsets. */ +#define IMX8QM_LPUART0_BASE 0x5A060000 +#define LPUART_BAUD 0x10 +#define LPUART_STAT 0x14 +#define LPUART_CTRL 0x18 +#define LPUART_DATA 0x1C + +#define LPUART_STAT_TDRE (1 << 23) /* TX data register empty */ +#define LPUART_STAT_TC (1 << 22) /* transmit complete */ +#define LPUART_STAT_RDRF (1 << 21) /* RX data register full */ +#define LPUART_CTRL_TE (1 << 19) /* transmitter enable */ +#define LPUART_CTRL_RE (1 << 18) /* receiver enable */ + +/* uSDHC: usdhc1 is the 8-bit eMMC, usdhc2 the 4-bit SD socket (card detect on + * lsio_gpio5[22]). One is compiled in at a time via DISK_EMMC / DISK_SDCARD. */ +#define IMX8QM_USDHC1_BASE 0x5B010000 /* eMMC, 8-bit */ +#define IMX8QM_USDHC2_BASE 0x5B020000 /* SD card, 4-bit */ + +/* uSDHC keeps eSDHC's register layout and adds MIX_CTRL, which holds the + * transfer-mode half standard SDHCI keeps in the command register. */ +#define USDHC_DS_ADDR 0x00 +#define USDHC_BLK_ATT 0x04 +#define USDHC_CMD_ARG 0x08 +#define USDHC_CMD_XFR_TYP 0x0C +#define USDHC_CMD_RSP0 0x10 +#define USDHC_DATA_BUFF_ACC_PORT 0x20 +#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_HOST_CTRL_CAP 0x40 +#define USDHC_WTMK_LVL 0x44 +#define USDHC_MIX_CTRL 0x48 +#define USDHC_VEND_SPEC 0xC0 + +/* PRES_STATE */ +#define USDHC_PRES_CIHB (1 << 0) /* command inhibit (CMD) */ +#define USDHC_PRES_SDSTB (1 << 3) /* SD clock stable */ +#define USDHC_PRES_DLSL_DAT0 (1 << 24) /* DAT0 line signal level */ + +/* PROT_CTRL */ +#define USDHC_PROT_DTW_MASK (0x3 << 1) +#define USDHC_PROT_DTW_1BIT (0x0 << 1) +#define USDHC_PROT_DTW_4BIT (0x1 << 1) +#define USDHC_PROT_DTW_8BIT (0x2 << 1) +#define USDHC_PROT_D3CD (1 << 3) +#define USDHC_PROT_EMODE_MASK (0x3 << 4) +#define USDHC_PROT_EMODE_LE (0x2 << 4) /* little-endian data port */ +#define USDHC_PROT_DMASEL_MASK (0x3 << 8) +#define USDHC_PROT_DMASEL_SIMPLE (0x0 << 8) + +/* SYS_CTRL. Reset and data-timeout sit at the same bits as SDHCI's SRS11 and pass + * through the shim; the DVS/SDCLKFS clock divider does not and is programmed directly. */ +#define USDHC_SYS_DVS_SHIFT 4 +#define USDHC_SYS_DVS_MASK (0xF << 4) +#define USDHC_SYS_SDCLKFS_SHIFT 8 +#define USDHC_SYS_SDCLKFS_MASK (0xFF << 8) +#define USDHC_SYS_DTOCV_SHIFT 16 +#define USDHC_SYS_DTOCV_MASK (0xF << 16) +#define USDHC_SYS_RSTA (1 << 24) /* reset all */ +#define USDHC_SYS_RSTC (1 << 25) /* reset command line */ +#define USDHC_SYS_RSTD (1 << 26) /* reset data line */ +#define USDHC_SYS_INITA (1 << 27) /* send 80 init clocks */ + +/* CMD_XFR_TYP (upper half is bit-identical to standard SDHCI's command half) */ +#define USDHC_XFR_RSPTYP_MASK (0x3 << 16) +#define USDHC_XFR_RSPTYP_48 (0x2 << 16) +#define USDHC_XFR_RSPTYP_48B (0x3 << 16) +#define USDHC_XFR_DPSEL (1 << 21) /* data present */ + +/* MIX_CTRL, the transfer-mode half. Bits 0,1,2,4,5 match standard SDHCI; the tuning + * and DDR bits above them have no equivalent and must be preserved. */ +#define USDHC_MIX_CTRL_XFER_MASK 0x3F + +/* HOST_CTRL_CAP: the voltage-support bits are at the standard positions. */ +#define USDHC_CAP_VS33 (1 << 24) +#define USDHC_CAP_VS30 (1 << 25) +#define USDHC_CAP_VS18 (1 << 26) + +/* WTMK_LVL burst watermarks, in 4-byte words. A full 512-byte block so the PIO loop + * sees one Buffer Read Ready per block rather than one per 32 bytes. */ +#define USDHC_WTMK_RD_SHIFT 0 +#define USDHC_WTMK_WR_SHIFT 16 +#define USDHC_WTMK_BLOCK_WORDS 0x80 + +/* VEND_SPEC */ +#define USDHC_VEND_SPEC_VSELECT (1 << 1) /* 1.8V signaling */ + +/* uSDHC per clock, owned by the SCU; 198 MHz is what the NXP BSP programs. With + * IMX8QM_SCU=1 the real rate is read back and this is only the fallback. */ +#ifndef IMX8QM_USDHC_PERCLK_HZ +#define IMX8QM_USDHC_PERCLK_HZ 198000000 +#endif + +/* FlexSPI0: MT35XU512ABA 64 MB octal NOR. AHB read window plus the LUT-driven IP + * command path for erase/program; same IP as the Layerscape XSPI (nxp_ls1028a.c). */ +#define IMX8QM_FLEXSPI0_BASE 0x5D120000 /* controller registers */ +#define IMX8QM_FLEXSPI0_AHB_BASE 0x08000000 /* memory-mapped read window */ +#define IMX8QM_FLEXSPI0_SIZE 0x4000000 /* 64 MB */ + +#define FLEXSPI_MCR0 0x00 +#define FLEXSPI_MCR2 0x08 +#define FLEXSPI_AHBCR 0x0C +#define FLEXSPI_INTR 0x14 +#define FLEXSPI_LUTKEY 0x18 +#define FLEXSPI_LUTCR 0x1C +#define FLEXSPI_AHBRXBUF0CR0 0x20 +#define FLEXSPI_FLSHA1CR0 0x60 +#define FLEXSPI_FLSHA1CR2 0x80 +#define FLEXSPI_IPCR0 0xA0 +#define FLEXSPI_IPCR1 0xA4 +#define FLEXSPI_IPCMD 0xB0 +#define FLEXSPI_IPRXFCR 0xB8 +#define FLEXSPI_IPTXFCR 0xBC +#define FLEXSPI_RFDR0 0x100 +#define FLEXSPI_TFDR0 0x180 +#define FLEXSPI_LUT0 0x200 + +#define FLEXSPI_MCR0_SWRESET (1 << 0) +#define FLEXSPI_MCR0_MDIS (1 << 1) +#define FLEXSPI_LUTKEY_VALUE 0x5AF05AF0 +#define FLEXSPI_LUTCR_LOCK 0x01 +#define FLEXSPI_LUTCR_UNLOCK 0x02 +#define FLEXSPI_IPCMD_TRG (1 << 0) +#define FLEXSPI_INTR_IPCMDDONE (1 << 0) +#define FLEXSPI_INTR_IPCMDGE (1 << 1) +#define FLEXSPI_INTR_IPCMDERR (1 << 3) +#define FLEXSPI_IPRXFCR_CLR (1 << 0) +#define FLEXSPI_IPTXFCR_CLR (1 << 0) +#define FLEXSPI_DLLACR 0xC0 +#define FLEXSPI_DLLBCR 0xC4 +#define FLEXSPI_DLLCR_OVRDEN (1 << 8) +#define FLEXSPI_MCR0_AHBGRANTWAIT (0xFF << 24) +#define FLEXSPI_MCR0_IPGRANTWAIT (0xFF << 16) +#define FLEXSPI_MCR2_SAMEDEVEN (1 << 15) +#define FLEXSPI_AHBCR_PREFETCH (1 << 5) +#define FLEXSPI_FLSHCR2_ARDSEQID_S 0 +#define FLEXSPI_INTR_IPRXWA (1 << 5) +#define FLEXSPI_INTR_IPTXWE (1 << 6) +/* IP TX/RX FIFO watermark, in 8-byte units of (value + 1). Left at the reset + * value of 0, so the IP fill loop below moves 8 bytes per watermark event. */ +#define FLEXSPI_IP_WM_BYTES 8 + +/* LUT sequence slots used by the HAL. Each sequence is 4 LUT words. */ +#define FLEXSPI_LUT_SEQ_READ 0 +#define FLEXSPI_LUT_SEQ_WREN 1 +#define FLEXSPI_LUT_SEQ_RDSR 2 +#define FLEXSPI_LUT_SEQ_SE 3 /* 128 KB sector erase (0xDC) */ +#define FLEXSPI_LUT_SEQ_PP 4 /* page program */ + +/* LUT instruction opcodes (FlexSPI "INSTR" field) */ +#define FLEXSPI_LUT_CMD 0x01 +#define FLEXSPI_LUT_RADDR 0x02 +#define FLEXSPI_LUT_READ 0x09 +#define FLEXSPI_LUT_WRITE 0x08 +#define FLEXSPI_LUT_STOP 0x00 + +/* LUT pad counts */ +#define FLEXSPI_LUT_PAD1 0 + +/* MT35XU512ABA / generic SPI NOR opcodes. 4-byte addressing is used + * throughout: the part is 64 MB, past the 16 MB 3-byte limit. */ +#define FLEXSPI_NOR_CMD_READ_4B 0x13 /* READ, 4-byte address */ +#define FLEXSPI_NOR_CMD_PP_4B 0x12 /* PAGE PROGRAM, 4-byte address */ +#define FLEXSPI_NOR_CMD_SE_4B 0xDC /* SECTOR ERASE 128K, 4-byte addr */ +#define FLEXSPI_NOR_CMD_WREN 0x06 +#define FLEXSPI_NOR_CMD_RDSR 0x05 +#define FLEXSPI_NOR_SR_WIP 0x01 + +/* The part also documents a 4 KB subsector erase (0x21), but 128 KB is advertised and + * is what U-Boot and Linux use. Being wrong the unsafe way is unrecoverable. */ +#define FLEXSPI_NOR_SECTOR_SIZE 0x20000 +#define FLEXSPI_NOR_PAGE_SIZE 0x100 /* confirmed by "sf probe" */ + +/* Messaging Unit: A-core software reaches the SCU over MU1_A, the channel free for + * BL33 (TF-A uses MU0). Only compiled with IMX8QM_SCU=1. */ +#define IMX8QM_LSIO_MU1A_BASE 0x5D1C0000 + +#define MU_TR0 0x00 /* transmit registers */ +#define MU_RR0 0x10 /* receive registers */ +#define MU_SR 0x20 /* status */ + +#define MU_SR_TE0 (1 << 23) /* TR0 empty */ +#define MU_SR_RF0 (1 << 27) /* RR0 full */ + +/* SCU resources this HAL may touch (subset of the SCFW sc_rsrc_t enum). + * Values match include/dt-bindings/firmware/imx/rsrc.h upstream. */ +/* System MMU (MMU-500). BL33 must power it and set sCR0.CLIENTPD, or the OS + * aborts on its first ID-register read. */ +#define SC_R_SMMU 17 +/* SMMU stream ID both uSDHC controllers present, from "iommus" on mmc@5b010000 and + * mmc@5b020000 (<&smmu 0x11 0x7f80>). Identical, hence one Linux iommu group. */ +#define IMX8QM_USDHC_SMMU_SID 0x11 +#define IMX8QM_SMMU_BASE 0x51400000 +/* sCR0 is at offset 0; CLIENTPD bypasses translation for all clients. */ +#define IMX8QM_SMMU_sCR0 0x00 +#define IMX8QM_SMMU_sCR0_CLIENTPD (1U << 0) + +#define SC_R_SDHC_0 248 /* uSDHC1 - eMMC */ +#define SC_R_SDHC_1 249 /* uSDHC2 - SD card */ +#define SC_R_FSPI_0 237 +#define SC_R_UART_0 57 + +/* SCU clock types (sc_pm_clk_t) */ +#define SC_PM_CLK_PER 2 + +/* Pad mux: powering and clocking LPUART0 is not enough, its pads must be + * routed too. Values follow U-Boot's imx8qm_mek UART_PAD_CTRL. */ +#define SC_P_UART0_RX 21 +#define SC_P_UART0_TX 22 +#define SC_PAD_CONFIG_OUT_IN 3 +#define SC_PAD_ISO_OFF 0 +#define SC_PAD_28FDSOI_DSE_DV_HIGH 0 +#define SC_PAD_28FDSOI_PS_PU 1 +#define PADRING_CONFIG_SHIFT 25 +#define PADRING_LPCONFIG_SHIFT 23 +#define PADRING_PULL_SHIFT 5 +#define PADRING_DSE_SHIFT 0 +#define PADRING_IFMUX_EN (1U << 31) +#define PADRING_GP_EN (1U << 30) +#define IMX8QM_UART_PAD_CTRL \ + (((uint32_t)SC_PAD_CONFIG_OUT_IN << PADRING_CONFIG_SHIFT) | \ + ((uint32_t)SC_PAD_ISO_OFF << PADRING_LPCONFIG_SHIFT) | \ + ((uint32_t)SC_PAD_28FDSOI_DSE_DV_HIGH << PADRING_DSE_SHIFT) | \ + ((uint32_t)SC_PAD_28FDSOI_PS_PU << PADRING_PULL_SHIFT)) + +/* uSDHC pads from the MEK pin groups, all mux function 0; note USDHC1_DATA2 is 227, + * not 226. The pad ring names the SD slot USDHC1_* but it is uSDHC2: follow the base. */ +#define SC_P_USDHC1_VSELECT 159 +#define SC_P_USDHC1_CLK 222 +#define SC_P_USDHC1_CMD 223 +#define SC_P_USDHC1_DATA0 224 +#define SC_P_USDHC1_DATA1 225 +#define SC_P_USDHC1_DATA2 227 +#define SC_P_USDHC1_DATA3 228 + +#define SC_P_EMMC0_CLK 209 +#define SC_P_EMMC0_CMD 210 +#define SC_P_EMMC0_DATA0 211 +#define SC_P_EMMC0_DATA1 212 +#define SC_P_EMMC0_DATA2 213 +#define SC_P_EMMC0_DATA3 214 +#define SC_P_EMMC0_DATA4 215 +#define SC_P_EMMC0_DATA5 216 +#define SC_P_EMMC0_DATA6 217 +#define SC_P_EMMC0_DATA7 218 +#define SC_P_EMMC0_STROBE 219 +#define SC_P_EMMC0_RESET_B 220 + +#define IMX8QM_SD_PAD_CLK_CTRL 0x06000041 /* clock/strobe pads */ +#define IMX8QM_SD_PAD_CTRL 0x00000021 /* cmd/data/vselect pads */ + +/* uSDHC low-power clock gating cells, one per controller, laid out with the + * same stride as the controllers themselves. */ +#define IMX8QM_USDHC0_LPCG 0x5B200000 +#define IMX8QM_USDHC_LPCG_STRIDE 0x10000 +#define IMX8QM_USDHC_LPCG(base) \ + (IMX8QM_USDHC0_LPCG + \ + ((((uintptr_t)(base)) - IMX8QM_USDHC1_BASE) / IMX8QM_USDHC_LPCG_STRIDE) \ + * IMX8QM_USDHC_LPCG_STRIDE) + +/* Low Power Clock Gating. sc_pm_clock_enable() ungates at the SCU; the peripheral's + * own LPCG cell still needs opening, with the double-write the reference driver uses. */ +#define IMX8QM_LPUART0_LPCG 0x5A460000 +/* FlexSPI0. Pads and LPCG from imx8qm-mek.dtb (flexspi0grp: pads 181-196, mux 0, + * pad control 0x6000021) and U-Boot's imx8qm_lpcg.h (FSPI_0_LPCG). */ +#define IMX8QM_FLEXSPI0_LPCG 0x5D520000 +#define SC_P_QSPI0A_DATA0 181 +#define SC_P_QSPI0B_SS1_B 196 +#define IMX8QM_FLEXSPI_PAD_CTRL 0x06000021 +#define LPCG_ALL_CLOCK_ON 0x22222222 +#define LPCG_ALL_CLOCK_STOP 0x88888888 + +/* LPUART0 root clock the BSP programs, and the resulting 115200 divisor. + * OSR 8 / SBR 87 lands within 0.25% of 115200 from 80 MHz. */ +#define IMX8QM_UART_CLK_HZ 80000000 +#define IMX8QM_LPUART_BAUD_115200 ((7U << 24) | 87U) +#define LPUART_BAUD_OSR_MASK (0x1FU << 24) +#define LPUART_BAUD_SBR_MASK 0x1FFFU +#define LPUART_BAUD_M10 (1U << 29) +#define LPUART_BAUD_SBNS (1U << 13) + +/* SCU power modes (sc_pm_power_mode_t) */ +#define SC_PM_PW_MODE_ON 3 + +#endif /* _IMX8QM_H_ */ diff --git a/hal/imx8qm.ld b/hal/imx8qm.ld new file mode 100644 index 0000000000..b8476d3393 --- /dev/null +++ b/hal/imx8qm.ld @@ -0,0 +1,74 @@ +/* imx8qm.ld - bare-metal link map for the i.MX 8QuadMax BL33 firmware target. + * + * DRAM-resident, boot-from-DRAM AArch64 map (same style as hal/tegra234.ld). + * Keep the addresses here in sync with hal/imx8qm.h. + */ +MEMORY +{ + /* ATF enters BL33 at 0x80020000, where u-boot.bin links on this SoC. Not + * position-independent. LENGTH is the payload offset, so growing into the + * bundled payload is a link error. */ + DDR_MEM(rwx): ORIGIN = 0x80020000, LENGTH = 0x200000 +} +ENTRY(_vector_table); + +SECTIONS +{ + .text : + { + _start_text = .; + KEEP(*(.boot*)) + *(.text*) + *(.rodata*) + *(.note.*) + . = ALIGN(4); + _end_text = .; + } > DDR_MEM + .ARM.exidx : + { + . = 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 + + .bss (NOLOAD) : + { + _start_bss = .; + __bss_start__ = .; + *(.bss*) + *(COMMON) + . = ALIGN(4); + _end_bss = .; + __bss_end__ = .; + } > DDR_MEM + + /* Image-relative stack, used by the imx8qm sp fixup in + * src/boot_aarch64_start.S. */ + .stack (NOLOAD) : + { + . = ALIGN(16); + . += 0x40000; /* 256 KB */ + . = ALIGN(16); + END_STACK = .; + /* Top of the runtime footprint, so above the stack, not the end of + * .bss: src/update_ram.c uses it for the RAM-boot overlap guard. */ + _end = .; + } > DDR_MEM + . = ALIGN(4); +} + +/* No partition symbols here: this target builds WOLFBOOT_NO_PARTITIONS, so the + * boot/update/DTB addresses come from hal_get_*_address() in hal/imx8qm.c + * (constants in hal/imx8qm.h) and the RAM staging targets come from + * WOLFBOOT_LOAD_ADDRESS / WOLFBOOT_LOAD_DTS_ADDRESS in the config. Defining + * them twice only invites the two copies to disagree. */ diff --git a/hal/tegra234.c b/hal/tegra234.c index e2b2cd9ce4..4eca1add5c 100644 --- a/hal/tegra234.c +++ b/hal/tegra234.c @@ -70,41 +70,16 @@ static inline void dmb_sy(void) { __asm__ volatile("dmb sy" ::: "memory"); } 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; } -/* -------------------------------------------------------------------------- - * ARMv8 generic timer (architectural; no SoC register needed) - * -------------------------------------------------------------------------- */ - -static inline uint64_t timer_get_count(void) -{ - uint64_t cntpct; - __asm__ volatile("mrs %0, cntpct_el0" : "=r" (cntpct)); - return cntpct; -} - -static inline uint64_t timer_get_freq(void) -{ - uint64_t cntfrq; - __asm__ volatile("mrs %0, cntfrq_el0" : "=r" (cntfrq)); - return cntfrq ? cntfrq : TIMER_CLK_FREQ; -} +/* Generic timer primitives come from the shared AArch64 helpers; only the two + * HAL entry points are defined here, because include/hal.h gives them + * external linkage. */ +#include "aarch64_arch.h" uint64_t hal_get_timer_us(void) { return (timer_get_count() * 1000000ULL) / timer_get_freq(); } -/* Deadline helpers for the polling loops below: one division at setup, none - * in the loop itself. */ -static uint64_t timer_deadline_us(uint32_t us) -{ - return timer_get_count() + (((uint64_t)us * timer_get_freq()) / 1000000ULL); -} - -static int timer_expired(uint64_t deadline) -{ - return timer_get_count() > deadline; -} - void hal_delay_us(uint32_t us) { uint64_t deadline = timer_deadline_us(us); diff --git a/hal/versal.c b/hal/versal.c index ce0c5ce472..b65cf38a7f 100644 --- a/hal/versal.c +++ b/hal/versal.c @@ -240,21 +240,10 @@ void uart_write(const char *buf, uint32_t len) * ============================================================================ */ -/* Get current timer count (physical counter) */ -static inline uint64_t timer_get_count(void) -{ - uint64_t cntpct; - __asm__ volatile("mrs %0, cntpct_el0" : "=r" (cntpct)); - return cntpct; -} - -/* Get timer frequency with fallback to TIMER_CLK_FREQ if not configured */ -static inline uint64_t timer_get_freq(void) -{ - uint64_t cntfrq; - __asm__ volatile("mrs %0, cntfrq_el0" : "=r" (cntfrq)); - return cntfrq ? cntfrq : TIMER_CLK_FREQ; -} +/* The counter accessors come from the shared AArch64 helpers. The three + * conversions below stay local: each picks a different way to avoid overflow, + * and the header deliberately takes no position on that. */ +#include "aarch64_arch.h" /* Get current time in milliseconds */ uint64_t hal_timer_ms(void) diff --git a/include/aarch64_arch.h b/include/aarch64_arch.h new file mode 100644 index 0000000000..2c7c81e0f7 --- /dev/null +++ b/include/aarch64_arch.h @@ -0,0 +1,111 @@ +/* aarch64_arch.h + * + * 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 + */ + +/* Architectural AArch64 helpers that carry no SoC-specific state: the generic + * timer and set/way data-cache maintenance. Several HALs each grew their own + * copy of these; this is the shared home for them. Inline, so including it + * changes nothing for a HAL that does not call them. + * + * Define TIMER_CLK_FREQ before including for the rate to assume when firmware + * left CNTFRQ_EL0 unprogrammed. */ + +#ifndef _AARCH64_ARCH_H_ +#define _AARCH64_ARCH_H_ + +#include + +#ifndef TIMER_CLK_FREQ +#error "TIMER_CLK_FREQ must be defined before including aarch64_arch.h" +#endif + +/* CNTPCT_EL0, the architectural counter. */ +static inline uint64_t timer_get_count(void) +{ + uint64_t cntpct; + __asm__ volatile("mrs %0, cntpct_el0" : "=r" (cntpct)); + return cntpct; +} + +static inline uint64_t timer_get_freq(void) +{ + uint64_t cntfrq; + __asm__ volatile("mrs %0, cntfrq_el0" : "=r" (cntfrq)); + return cntfrq ? cntfrq : TIMER_CLK_FREQ; +} + +/* Deadline helpers for polling loops: one division at setup, none in the + * loop itself. */ +static inline uint64_t timer_deadline_us(uint32_t us) +{ + return timer_get_count() + (((uint64_t)us * timer_get_freq()) / 1000000ULL); +} + +/* Strictly greater, not >=, and deliberately so. A deadline is computed from + * a count taken at an arbitrary point within a tick, so the first tick is + * already partly spent. Exiting at == would let hal_delay_us() return early + * by up to one tick, which on a 1 MHz generic timer is the whole of a + * requested 1 us delay. Waiting one extra tick is the safe direction for a + * delay and costs a timeout nothing. */ +static inline int timer_expired(uint64_t deadline) +{ + return timer_get_count() > deadline; +} + +/* Data-cache maintenance by set/way over every level to the point of + * coherency. clean != 0 -> clean+invalidate, else invalidate only. */ +static inline void aarch64_dcache_maint(int clean) +{ + uint64_t clidr, ccsidr; + unsigned int level, loc, ctype, linesize, ways, sets, way, set, wayshift; + + __asm__ volatile("dsb sy"); + __asm__ volatile("mrs %0, clidr_el1" : "=r"(clidr)); + loc = (unsigned int)((clidr >> 24) & 0x7); + for (level = 0; level < loc; level++) { + ctype = (unsigned int)((clidr >> (level * 3)) & 0x7); + if (ctype < 2) /* no data or unified cache at this level */ + continue; + __asm__ volatile("msr csselr_el1, %0" :: "r"((uint64_t)(level << 1))); + __asm__ volatile("isb"); + __asm__ volatile("mrs %0, ccsidr_el1" : "=r"(ccsidr)); + linesize = (unsigned int)(ccsidr & 0x7) + 4; + ways = (unsigned int)((ccsidr >> 3) & 0x3FF); + sets = (unsigned int)((ccsidr >> 13) & 0x7FFF); + /* __builtin_clz(0) is undefined; a direct-mapped cache never uses the + * way field, so the shift amount does not matter there. */ + wayshift = (ways == 0) ? 32u : (unsigned int)__builtin_clz(ways); + for (set = 0; set <= sets; set++) { + for (way = 0; way <= ways; way++) { + uint64_t val = ((uint64_t)(level << 1)) + | ((uint64_t)way << wayshift) + | ((uint64_t)set << linesize); + if (clean) + __asm__ volatile("dc cisw, %0" :: "r"(val)); + else + __asm__ volatile("dc isw, %0" :: "r"(val)); + } + } + } + __asm__ volatile("dsb sy"); + __asm__ volatile("isb"); +} + +#endif /* _AARCH64_ARCH_H_ */ diff --git a/src/boot_aarch64_start.S b/src/boot_aarch64_start.S index 3334d6e553..f88c5b7f6f 100644 --- a/src/boot_aarch64_start.S +++ b/src/boot_aarch64_start.S @@ -46,6 +46,10 @@ #include "hal/cm4.h" #endif +#ifdef TARGET_imx8qm +#include "hal/imx8qm.h" +#endif + /* GICv2 Register Offsets */ #ifndef GICD_BASE #define GICD_BASE 0xF9010000 @@ -1302,12 +1306,14 @@ _cm4_entry: // EL == 2? mov x2, #3 << 20 msr cptr_el2, x2 /* Enable FP/SIMD */ -#if defined(TARGET_cm4) +#if defined(TARGET_cm4) || defined(TARGET_imx8qm) /* Always install a valid EL2 vector base: the reset VBAR_EL2 is UNKNOWN, so * without this a fault in the parsing-heavy boot code branches to an * undefined address. The table halts cleanly (and, under DEBUG+DEBUG_UART, - * dumps ESR/ELR/FAR first). */ - ldr x2, =cm4_vectors + * dumps ESR/ELR/FAR first). el2_to_el1_boot copies VBAR_EL2 into VBAR_EL1, + * so this also covers the window between the drop to EL1 and the payload + * installing its own vectors. */ + ldr x2, =simple_el2_vectors msr VBAR_EL2, x2 isb /* ensure new vector base is in effect */ #endif @@ -1325,14 +1331,13 @@ _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_imx8qm) + /* Reserved in-DRAM stack from the linker script: the generic path sets sp + * to the image base, which here is the BL33 load address. */ ldr x1, =END_STACK #endif mov sp, x1 // set stack pointer -#ifdef TARGET_tegra234 +#if defined(TARGET_tegra234) || defined(TARGET_imx8qm) /* 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 @@ -1342,48 +1347,53 @@ _cm4_entry: bl boot_entry_C // boot_entry_C never returns b 7b // go to sleep anyhow in case. -#if defined(TARGET_cm4) +#if defined(TARGET_cm4) || defined(TARGET_imx8qm) /* Minimal always-on EL2 exception vectors. On any exception (data/instruction * abort, etc.) halt cleanly rather than branch to the UNKNOWN reset VBAR_EL2. - * Under DEBUG+DEBUG_UART the handler additionally dumps ESR/ELR/FAR over UART. */ -.macro CM4_VEC label + * Under DEBUG+DEBUG_UART the handler additionally dumps ESR/ELR/FAR over UART. + * + * Each entry passes its slot number, which distinguishes a fault inside + * wolfBoot (slots 0-7) from one in the payload after the drop to EL1 (8-15). */ +.macro EL2_VEC label, idx .align 7 \label: - b cm4_fault_common + mov x3, #\idx + b simple_el2_fault_common .endm .align 11 -.global cm4_vectors -cm4_vectors: - CM4_VEC cm4_v0 /* Current EL, SP0: Sync */ - CM4_VEC cm4_v1 /* IRQ */ - CM4_VEC cm4_v2 /* FIQ */ - CM4_VEC cm4_v3 /* SError */ - CM4_VEC cm4_v4 /* Current EL, SPx: Sync */ - CM4_VEC cm4_v5 /* IRQ */ - CM4_VEC cm4_v6 /* FIQ */ - CM4_VEC cm4_v7 /* SError */ - CM4_VEC cm4_v8 /* Lower EL, AArch64: Sync */ - CM4_VEC cm4_v9 /* IRQ */ - CM4_VEC cm4_v10 /* FIQ */ - CM4_VEC cm4_v11 /* SError */ - CM4_VEC cm4_v12 /* Lower EL, AArch32: Sync */ - CM4_VEC cm4_v13 /* IRQ */ - CM4_VEC cm4_v14 /* FIQ */ - CM4_VEC cm4_v15 /* SError */ - -cm4_fault_common: +.global simple_el2_vectors +simple_el2_vectors: + EL2_VEC el2_v0, 0 /* Current EL, SP0: Sync */ + EL2_VEC el2_v1, 1 /* IRQ */ + EL2_VEC el2_v2, 2 /* FIQ */ + EL2_VEC el2_v3, 3 /* SError */ + EL2_VEC el2_v4, 4 /* Current EL, SPx: Sync */ + EL2_VEC el2_v5, 5 /* IRQ */ + EL2_VEC el2_v6, 6 /* FIQ */ + EL2_VEC el2_v7, 7 /* SError */ + EL2_VEC el2_v8, 8 /* Lower EL, AArch64: Sync */ + EL2_VEC el2_v9, 9 /* IRQ */ + EL2_VEC el2_v10, 10 /* FIQ */ + EL2_VEC el2_v11, 11 /* SError */ + EL2_VEC el2_v12, 12 /* Lower EL, AArch32: Sync */ + EL2_VEC el2_v13, 13 /* IRQ */ + EL2_VEC el2_v14, 14 /* FIQ */ + EL2_VEC el2_v15, 15 /* SError */ + +simple_el2_fault_common: #if defined(DEBUG) && defined(DEBUG_UART) /* Switch to a dedicated fault stack before calling the C handler: the * exception may have been taken with SP unset (the window before 'mov sp,x1' * or on a secondary core that never sets SP) or with SP overflowed, and - * cm4_fault_handler needs a valid stack for its wolfBoot_printf calls. */ - ldr x3, =cm4_fault_stack_top - mov sp, x3 + * the handler needs a valid stack. x3 already carries the vector slot, so + * read the system registers into x0-x2 without disturbing it. */ + ldr x4, =simple_el2_fault_stack_top + mov sp, x4 mrs x0, esr_el2 mrs x1, elr_el2 mrs x2, far_el2 - bl cm4_fault_handler /* void cm4_fault_handler(esr, elr, far) */ + bl simple_el2_fault_handler /* void (esr, elr, far, vector) */ #endif 9: wfi b 9b @@ -1391,12 +1401,12 @@ cm4_fault_common: #if defined(DEBUG) && defined(DEBUG_UART) .section ".bss" .balign 16 -cm4_fault_stack: +simple_el2_fault_stack: .skip 1024 -cm4_fault_stack_top: +simple_el2_fault_stack_top: .section ".boot" #endif -#endif /* TARGET_cm4 */ +#endif /* TARGET_cm4 || TARGET_imx8qm */ #if defined(TARGET_cm4) /* Firmware-provided DTB pointer, stored at _cm4_entry. Read by hal_get_boot_dts() * (hal/cm4.c) for the kernel-only FIT boot path. In .data so it is not subject to @@ -1665,7 +1675,11 @@ el2_to_el1_boot: * AAPCS64: clobbers x0-x7, x9-x11; x30 (LR) is preserved because the * set/way loop body does not touch it. */ -#if defined(EL2_HYPERVISOR) && EL2_HYPERVISOR == 1 +/* Also built where a HAL tears its own identity MMU down before handoff and + * needs this routine's stack-free property, which is orthogonal to whether the + * build hands off at EL1. Such a HAL sets WOLFBOOT_AARCH64_MMU_TEARDOWN. */ +#if (defined(EL2_HYPERVISOR) && EL2_HYPERVISOR == 1) || \ + defined(WOLFBOOT_AARCH64_MMU_TEARDOWN) .global el2_flush_and_disable_mmu el2_flush_and_disable_mmu: /* ---- 1. Clean & invalidate entire data cache to PoC by set/way ---- @@ -1746,7 +1760,7 @@ el2_flush_and_disable_mmu: isb ret -#endif /* EL2_HYPERVISOR */ +#endif /* EL2_HYPERVISOR || WOLFBOOT_AARCH64_MMU_TEARDOWN */ #if defined(WOLFBOOT_ZYNQMP_FSBL) /* diff --git a/src/sdhci.c b/src/sdhci.c index 0bb7da8e88..51a9c179a9 100644 --- a/src/sdhci.c +++ b/src/sdhci.c @@ -395,6 +395,12 @@ static int sdhci_set_power(uint32_t voltage) * short probe read usually succeeds. So this is driven from an actual data * failure: switch the host to meet the card, then let the caller retry. * + * This does not help a card whose *initialization* fails for the same reason: + * a card that has negotiated UHS-I returns to 3.3V only on a VDD cycle, and + * on a board where the card supply is a fixed rail nothing in software can + * produce one. Hardware-checked on an i.MX 8QuadMax MEK: switching the host + * to 1.8V does not make the card answer CMD8 after a warm reset. + * * This restores signaling the card is already using rather than initiating a * voltage switch, so no CMD11 sequence is involved. It runs at most once per * boot, and only after a transfer has already failed. @@ -585,6 +591,72 @@ static uint32_t sdhci_get_response_type(uint8_t resp_type) #define DEVICE_BUSY 1 +/* Register-read budget for the inhibit/reset waits below. Large, because it + * bounds a wedged controller rather than a normal one; the loops that spend it + * pet the watchdog, or on a platform whose watchdog cannot be disabled (the + * PolarFire MSS pair) error recovery would be cut short by a chip reset. */ +#ifndef SDHCI_INHIBIT_TIMEOUT +#define SDHCI_INHIBIT_TIMEOUT 1000000U +#endif + +/* SRS12 error bits from the last command. sdhci_cmd() clears SRS12 as soon as + * the command returns, so a caller that needs to tell a silent card from a + * broken link has to be handed the bits rather than read them back. */ +static uint32_t g_last_cmd_err; + +/* Reset data and command lines to recover from errors. + * + * Returns 0 once the lines are idle, -1 if either the reset or the inhibit + * bits never clear. The post-reset wait belongs here rather than at the call + * sites: a bounded reset that leaves the caller spinning on CICMD/CIDAT moves + * the hang instead of removing it. */ +static int sdhci_reset_lines(void) +{ + uint32_t to = SDHCI_INHIBIT_TIMEOUT; + + sdhci_reg_or(SDHCI_SRS11, SDHCI_SRS11_RESET_DAT_CMD); + while ((SDHCI_REG(SDHCI_SRS11) & SDHCI_SRS11_RESET_DAT_CMD) != 0 && + to > 0) { + sdhci_platform_wdt_pet(); + to--; + } + if ((SDHCI_REG(SDHCI_SRS11) & SDHCI_SRS11_RESET_DAT_CMD) != 0) { + return -1; + } + to = SDHCI_INHIBIT_TIMEOUT; + while ((SDHCI_REG(SDHCI_SRS09) & + (SDHCI_SRS09_CICMD | SDHCI_SRS09_CIDAT)) != 0 && to > 0) { + sdhci_platform_wdt_pet(); + to--; + } + if ((SDHCI_REG(SDHCI_SRS09) & + (SDHCI_SRS09_CICMD | SDHCI_SRS09_CIDAT)) != 0) { + return -1; + } + return 0; +} + +/* Wait for the command line to leave the inhibited state. + * + * A command that timed out leaves Command Inhibit (CMD) set, and only a + * CMD-line reset clears it. An unbounded wait here therefore hangs the boot + * on the first failed command instead of returning the error: the caller + * never sees it, so nothing can retry or recover. Bound the wait and reset + * the lines if it does not clear. */ +static int sdhci_wait_cmd_idle(void) +{ + uint32_t to = SDHCI_INHIBIT_TIMEOUT; + + while ((SDHCI_REG(SDHCI_SRS09) & SDHCI_SRS09_CICMD) != 0 && to > 0) { + sdhci_platform_wdt_pet(); + to--; + } + if ((SDHCI_REG(SDHCI_SRS09) & SDHCI_SRS09_CICMD) != 0) { + return sdhci_reset_lines(); + } + return 0; +} + static int sdhci_send_cmd_internal(uint32_t cmd_type, uint32_t cmd_index, uint32_t cmd_arg, uint8_t resp_type) { @@ -592,13 +664,20 @@ static int sdhci_send_cmd_internal(uint32_t cmd_type, uint32_t cmd_reg; uint32_t timeout = 0x000FFFFF; + g_last_cmd_err = 0; + #ifdef DEBUG_SDHCI wolfBoot_printf("sdhci_send_cmd: cmd_index: %d, cmd_arg: %08X, resp_type: %d\n", cmd_index, cmd_arg, resp_type); #endif - /* wait for command line to be idle */ - while ((SDHCI_REG(SDHCI_SRS09) & SDHCI_SRS09_CICMD) != 0); + /* wait for command line to be idle. A line that will not come back + * cannot carry a command, so fail rather than write SRS03 into it. */ + if (sdhci_wait_cmd_idle() != 0) { + wolfBoot_printf("sdhci_send_cmd: cmd %u: command line stuck busy\n", + cmd_index); + return -1; + } /* set command argument and command transfer registers */ SDHCI_REG_SET(SDHCI_SRS02, cmd_arg); @@ -623,11 +702,22 @@ static int sdhci_send_cmd_internal(uint32_t cmd_type, wolfBoot_printf("sdhci_send_cmd: cmd %u arg 0x%08X resp %u: " "error SRS12=0x%08X\n", cmd_index, cmd_arg, resp_type, SDHCI_REG(SDHCI_SRS12)); + g_last_cmd_err = SDHCI_REG(SDHCI_SRS12) & SDHCI_SRS12_ERR_STAT; status = -1; /* error */ } SDHCI_REG_SET(SDHCI_SRS12, SDHCI_SRS12_CC); /* clear command complete */ - while ((SDHCI_REG(SDHCI_SRS09) & SDHCI_SRS09_CICMD) != 0); + if (sdhci_wait_cmd_idle() != 0) { + /* Reported even when the command itself already failed: a line that + * will not go idle afterwards is a separate fault, and it is the + * next command that pays for it. g_last_cmd_err stays 0 here - the + * controller raised no error, the line simply never released. */ + wolfBoot_printf("sdhci_send_cmd: cmd %u: command line stuck busy " + "after completion\n", cmd_index); + if (status == 0) { + status = -1; + } + } if (status == 0) { /* check for device busy */ @@ -705,6 +795,22 @@ static int sdhci_wait_busy(int check_dat0) void sdhci_shutdown(void) { uint32_t to = 100000U; + +#ifdef DISK_SDCARD + /* If sdhci_uhs_recover() moved the host to 1.8V, put it back before handing + * over: a software reset does not clear 1.8V Signaling Enable, so the next + * stage would inherit a 1.8V host and an OS that powers the card at 3.3V + * could not talk to it. 3.3V is right even though the card is still at + * 1.8V - the card returns only on a VDD cycle, which is the next stage's + * job (Linux does it via vmmc-supply), and that pair is what a cold boot + * looks like. Clock left stopped; RESET_ALL follows. */ + if (g_uhs_recovered) { + sdhci_reg_and(SDHCI_SRS11, ~(uint32_t)SDHCI_SRS11_SDCE); + sdhci_reg_and(SDHCI_SRS15, ~(uint32_t)SDHCI_SRS15_V18SE); + udelay(5000); /* let the level shifter settle */ + } +#endif + sdhci_reg_or(SDHCI_SRS11, SDHCI_SRS11_RESET_ALL); while ((SDHCI_REG(SDHCI_SRS11) & SDHCI_SRS11_RESET_ALL) != 0U && to > 0U) { @@ -716,13 +822,6 @@ void sdhci_shutdown(void) } } -/* Reset data and command lines to recover from errors */ -static inline void sdhci_reset_lines(void) -{ - sdhci_reg_or(SDHCI_SRS11, SDHCI_SRS11_RESET_DAT_CMD); - while (SDHCI_REG(SDHCI_SRS11) & SDHCI_SRS11_RESET_DAT_CMD); -} - /* ============================================================================ * Response Parsing Helper * ============================================================================ */ @@ -763,6 +862,10 @@ static uint32_t sdhci_get_response_bits(int from, int count) static int sdcard_power_init_seq(uint32_t voltage) { int retries; + /* SRS12 errors from the CMD8 attempt, snapshotted at the call: any later + * command overwrites g_last_cmd_err, and on the UHS retry path a CMD0 + * runs in between. */ + uint32_t cmd8_err = 0; /* Set power to specified voltage */ int status = sdhci_set_power(voltage); #ifdef DEBUG_SDHCI @@ -804,6 +907,57 @@ static int sdcard_power_init_seq(uint32_t voltage) /* send the operating conditions command */ status = sdhci_cmd(SD_CMD8_SEND_IF_COND, SD_IF_COND_27V_33V, SDHCI_RESP_R7); + cmd8_err = g_last_cmd_err; +#if defined(DISK_SDCARD) && defined(SDHCI_UHS_RECOVER_ON_INIT) + if (status != 0) { + /* Opt-in. A card a previous stage left in UHS-I is at 1.8V and + * does not answer a 3.3V CMD8, so meet the card and retry once. + * + * Off by default because it cannot work where software has no way + * to cycle card VDD, which is the only thing that returns a UHS-I + * card to 3.3V. Hardware-checked ineffective on the i.MX 8QuadMax + * MEK, whose card supply is a fixed rail; kept for hosts that do + * drive bus power, where it is untested. */ + if (sdhci_uhs_recover() == 0) { + status = sdhci_cmd(MMC_CMD0_GO_IDLE, 0, SDHCI_RESP_NONE); + if (status == 0) { + udelay(200); + status = sdhci_cmd(SD_CMD8_SEND_IF_COND, + SD_IF_COND_27V_33V, SDHCI_RESP_R7); + cmd8_err = g_last_cmd_err; + } + if (status != 0) { + /* Not a UHS card. Restore 3.3V and put the card back in + * idle, since it saw a CMD0 at the wrong signaling. */ + sdhci_uhs_recover_rollback(); + (void)sdhci_cmd(MMC_CMD0_GO_IDLE, 0, SDHCI_RESP_NONE); + udelay(200); + } + } + } +#endif + if (status != 0) { + /* An SD v1.x card does not implement CMD8 and never answers it. + * Nothing below reads the CMD8 response - ACMD41 carries HCS, + * which a v1.x card ignores - so a missing answer is not fatal. + * Treating it as fatal rejected every legacy card. + * + * Only silence is excused. A command timeout on its own is a card + * that did not answer; a CRC, end-bit or index error means the + * card DID answer and the link mangled it, and continuing then + * would run the whole init over a known-broken bus. */ + if ((cmd8_err & SDHCI_SRS12_ECT) != 0 && + (cmd8_err & (SDHCI_SRS12_ECCRC | SDHCI_SRS12_ECEB | + SDHCI_SRS12_ECI)) == 0) { + wolfBoot_printf("SD: no CMD8 response, continuing as v1.x " + "card\n"); + status = 0; + } + else { + wolfBoot_printf("SD: CMD8 failed, SRS12 errors 0x%08X\n", + cmd8_err); + } + } } return status; } @@ -1406,11 +1560,12 @@ static int sdhci_transfer(int dir, uint32_t cmd_index, uint32_t block_addr, return status; } - /* Reset data and command lines */ - sdhci_reset_lines(); - - /* Wait for command and data line busy to clear */ - while ((SDHCI_REG(SDHCI_SRS09) & (SDHCI_SRS09_CICMD | SDHCI_SRS09_CIDAT)) != 0); + /* Reset data and command lines, which also waits for the inhibit bits + * to clear. A stuck line is reported, not spun on. */ + if (sdhci_reset_lines() != 0) { + wolfBoot_printf("sdhci_transfer: lines stuck busy after reset\n"); + return -1; + } /* Setup default transfer block count and block size */ bcr_reg = (block_count << SDHCI_SRS01_BCCT_SHIFT) | sz; @@ -1572,7 +1727,8 @@ static int sdhci_transfer(int dir, uint32_t cmd_index, uint32_t block_addr, /* Between-block workaround for the Arasan EMMC2 block. This is * a controller quirk, NOT a generic SD behavior, so it is scoped * rather than keyed off DISK_SDCARD: that macro is also set by - * the polarfire/zynqmp/versal/zynq7000/tegra234 SD targets, + * the polarfire/zynqmp/versal/zynq7000/tegra234/imx8qm SD + * targets, * whose multi-block PIO reads are already validated without it. * Platforms needing the quirk opt in with SDHCI_PIO_BRR_CLEAR * (the CM4 microSD path, which drives the same EMMC2 block). @@ -1704,8 +1860,10 @@ static int sdhci_transfer(int dir, uint32_t cmd_index, uint32_t block_addr, (dir == SDHCI_DIR_READ) ? "read" : "write", status); #endif - /* Clear status interrupts (except current limit, card interrupt/removal/insert) */ - sdhci_reset_lines(); + /* Clear status interrupts (except current limit, card interrupt/removal/insert). + * Teardown: the transfer result is already decided, so a stuck line here + * cannot change it and the next command reports it. */ + (void)sdhci_reset_lines(); SDHCI_REG_SET(SDHCI_SRS12, ~(SDHCI_SRS12_ECL | SDHCI_SRS12_CINT | SDHCI_SRS12_CR | SDHCI_SRS12_CIN)); diff --git a/src/update_disk.c b/src/update_disk.c index 0d77858c6d..207bb2f583 100644 --- a/src/update_disk.c +++ b/src/update_disk.c @@ -769,6 +769,9 @@ void RAMFUNCTION wolfBoot_start(void) continue; } os_image.fw_base = (uint8_t*)load_address; + /* Now in RAM: an EXT_FLASH build would otherwise verify through + * ext_flash_check_read() and hash flash, not the staged image. */ + os_image.not_ext = 1; #ifndef WOLFBOOT_SKIP_BOOT_VERIFY wolfBoot_printf("Checking image integrity..."); diff --git a/test-app/app_imx8qm.c b/test-app/app_imx8qm.c new file mode 100644 index 0000000000..5009a33ce4 --- /dev/null +++ b/test-app/app_imx8qm.c @@ -0,0 +1,111 @@ +/* app_imx8qm.c + * + * Test bare-metal boot application + * + * 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 "wolfboot/wolfboot.h" + +#ifdef TARGET_imx8qm + +#include "hal/imx8qm.h" + +/* Same LPUART0 console as wolfBoot (hal/imx8qm.h). The MMU is off here, so drive the + * registers directly and leave the baud rate SCFW/ATF set up alone. */ +#define LPUART_REG(off) \ + (*(volatile uint32_t*)(uintptr_t)(IMX8QM_LPUART0_BASE + (off))) + +/* Bounded spin: no timer is set up in this payload, so a plain counter is + * enough to keep a stalled console from hanging the app mid-message. */ +#define LPUART_TX_SPIN_MAX 1000000 + +static void uart_putc(char c) +{ + unsigned int spin = 0; + + while ((LPUART_REG(LPUART_STAT) & LPUART_STAT_TDRE) == 0) { + if (++spin > LPUART_TX_SPIN_MAX) + return; /* drop the character rather than stall forever */ + } + LPUART_REG(LPUART_DATA) = (uint32_t)(uint8_t)c; +} + +static void uart_puts(const char* s) +{ + while (*s != '\0') { + if (*s == '\n') + uart_putc('\r'); + uart_putc(*s++); + } +} + +static void uart_puthex(uint64_t v) +{ + int i; + uart_puts("0x"); + for (i = 60; i >= 0; i -= 4) + uart_putc("0123456789abcdef"[(v >> i) & 0xF]); +} + +/* FDT magic 0xd00dfeed, stored big-endian -> reads as 0xedfe0dd0 here */ +#define FDT_MAGIC_LE 0xedfe0dd0u + +/* The DTB pointer arrives as the first argument: el2_to_el1_boot() leaves it in x0 and + * boot_arm64_start.S forwards it per AAPCS. A parameter stays correct at any -O. */ +void __attribute__((section(".boot"))) main(uint64_t dtb) { + uint64_t el; + + __asm__ volatile("mrs %0, CurrentEL" : "=r"(el)); + el >>= 2; + + uart_puts("\n*** wolfBoot i.MX 8QuadMax payload: verified + booted ***\n"); + + /* Prove which EL the payload was entered at, and that a valid FDT arrived + * in x0: the full arm64 boot contract for a signed payload. */ + uart_puts(" CurrentEL = EL"); + uart_putc('0' + (char)(el & 3)); + uart_puts("\n"); + + uart_puts(" DTB (x0) = "); + uart_puthex(dtb); + uart_puts("\n DTB magic = "); + /* Range- and alignment-check before dereferencing: MMU-off this is Device memory, + * where an unaligned access faults. A stray pointer should be reported, not taken. */ + if (dtb < IMX8QM_DRAM_BASE || dtb > (IMX8QM_DRAM_END - 4) || + (dtb & 0x3) != 0) { + uart_puts("(pointer not in DRAM or misaligned)\n"); + } + else if (*((volatile uint32_t*)(uintptr_t)dtb) == FDT_MAGIC_LE) { + uart_puts("OK (0xd00dfeed)\n"); + } + else { + uart_puthex(*((volatile uint32_t*)(uintptr_t)dtb)); + uart_puts(" (BAD)\n"); + } + + uart_puts("*** payload complete: parked ***\n"); + + /* Wait for reboot */ + while(1) + ; +} +#endif /** TARGET_imx8qm **/ diff --git a/test-app/boot_arm64_start.S b/test-app/boot_arm64_start.S index 0c76fff905..d5f6cfbaf3 100644 --- a/test-app/boot_arm64_start.S +++ b/test-app/boot_arm64_start.S @@ -61,7 +61,8 @@ _start: 4: /* Restore x0 and jump to main - never returns. main(void) ignores it, but - * the tegra234 payload reads x0 to recover the DTB pointer. */ + * the tegra234 and imx8qm payloads read x0 to recover the DTB + * pointer. */ mov x0, x19 bl main diff --git a/tools/scripts/imx8qm/imx8qm-ahab-sign.sh b/tools/scripts/imx8qm/imx8qm-ahab-sign.sh new file mode 100755 index 0000000000..d0d4da1cc7 --- /dev/null +++ b/tools/scripts/imx8qm/imx8qm-ahab-sign.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# +# imx8qm-ahab-sign.sh - AHAB-sign an i.MX 8QuadMax boot container. +# +# AHAB is the SoC's own secure boot: the SECO firmware authenticates the boot +# container (SCFW + ATF + wolfBoot-as-BL33) against a Super Root Key hash burnt +# into fuses, before any A-core runs. It is the layer *below* wolfBoot's own +# signature checking, and the two are independent: +# +# AHAB -> authenticates the container, i.e. wolfBoot itself +# wolfBoot -> authenticates the OS/application image it goes on to boot +# +# A flash.bin built by imx8qm-mkflashbin.sh is unsigned ("open" lifecycle) and +# boots on a board whose SRK fuses are blank. Signing it is what makes it boot +# on a closed part. +# +# This script covers the signing step only. Burning the SRK hash and closing +# the part are deliberately NOT automated: both are one-way fuse operations +# that permanently reject any container not signed by the matching key, and a +# mistake bricks the board. See docs/Targets.md for that procedure. +# +# Usage: +# CST_PATH=/path/to/cst- \ +# SRK_TABLE=/path/to/SRK_1_2_3_4_table.bin \ +# SRK_KEY=/path/to/SRK1_sha384_secp384r1_v3_ca_crt.pem \ +# CERT_KEY=/path/to/SGK1_1_sha384_secp384r1_v3_usr_crt.pem \ +# tools/scripts/imx8qm/imx8qm-ahab-sign.sh flash.bin [mkimage.log] +# +# The SRK table and keys come from the Code Signing Tool's own PKI scripts; +# this script does not generate them, because the key material must outlive +# any one build. Use keys/ahab_pki_tree.sh, not hab4_pki_tree.sh: the latter +# is the older HAB4 flow for i.MX6/7 and produces keys AHAB will not accept. +# srktool needs --ahab_ver for the table. Note the SGK certificates the AHAB +# script emits are named SGK_1_..., with the extra index. +# +# CERT_KEY is mandatory: ahab_pki_tree.sh sets the CA flag on the SRKs, and a +# CA-flagged SRK may only sign another key, never data, so the container is +# signed by an SGK installed via [Install Certificate]. CST requires a +# Permissions bitmask with it; CERT_PERMISSIONS overrides the 0x1 default. +set -e + +IMAGE="${1:-flash.bin}" +MKIMAGE_LOG="${2:-}" + +for v in CST_PATH SRK_TABLE SRK_KEY CERT_KEY; do + # Indirect expansion, not eval: eval on environment-controlled content + # would execute a value containing $(...). + if [ -z "${!v}" ]; then + echo "ERROR: $v is not set. See the header of this script." >&2 + exit 1 + fi +done +[ -f "$IMAGE" ] || { echo "ERROR: no such image: $IMAGE" >&2; exit 1; } +[ -f "$SRK_TABLE" ] || { echo "ERROR: no such SRK table: $SRK_TABLE" >&2; exit 1; } + +CST="$CST_PATH/linux64/bin/cst" +[ -x "$CST" ] || { echo "ERROR: cst not executable: $CST" >&2; exit 1; } + +# imx-mkimage prints the container offsets the CSF has to reference, e.g. +# CST: CONTAINER 0 offset: 0x400 +# CST: CONTAINER 0: Signature Block: offset is at 0x590 +# Take them from a saved build log if one was given, otherwise fall back to +# the offsets the stock iMX8QM recipe produces. +if [ -n "$MKIMAGE_LOG" ] && [ -f "$MKIMAGE_LOG" ]; then + CONTAINER_OFFSET=$(grep -m1 'CST: CONTAINER 0 offset:' "$MKIMAGE_LOG" \ + | grep -oE '0x[0-9a-fA-F]+') + SIGBLK_OFFSET=$(grep -m1 'CST: CONTAINER 0: Signature Block: offset is at' \ + "$MKIMAGE_LOG" | grep -oE '0x[0-9a-fA-F]+') +fi +CONTAINER_OFFSET="${CONTAINER_OFFSET:-0x400}" +SIGBLK_OFFSET="${SIGBLK_OFFSET:-0x590}" + +echo "Signing $IMAGE" +echo " container offset: $CONTAINER_OFFSET" +echo " signature block offset: $SIGBLK_OFFSET" +echo " SRK index: ${SRK_INDEX:-0}" + +# Not "mktemp -t