From 4fedc93ed8b03507a684995383274ccd502368b3 Mon Sep 17 00:00:00 2001 From: David Garske Date: Tue, 18 Aug 2026 15:39:30 -0700 Subject: [PATCH] imx95_m7: fix console/status overlap, cache handoff, timebase and build wiring --- .github/workflows/test-configs.yml | 9 ++ Makefile | 6 + docs/Targets.md | 82 ++++++++++++++ hal/imx95_m7.c | 163 ++++++++++++++++----------- hal/imx95_m7.h | 172 +++++++++++++++++++++++++++++ hal/imx95_m7.ld | 11 ++ hal/uart/uart_drv_imx95_m7.c | 35 ++++-- test-app/Makefile | 3 + test-app/app_imx95_m7.c | 49 ++++++-- test-app/wolfcrypt_support.c | 70 +++++++----- 10 files changed, 489 insertions(+), 111 deletions(-) create mode 100644 hal/imx95_m7.h diff --git a/.github/workflows/test-configs.yml b/.github/workflows/test-configs.yml index 852139ab..1cad233d 100644 --- a/.github/workflows/test-configs.yml +++ b/.github/workflows/test-configs.yml @@ -385,6 +385,15 @@ jobs: config-file: ./config/examples/raspi3-encrypted.config make-args: wolfboot.bin CROSS_COMPILE=aarch64-linux-gnu- + imx95_m7_test: + uses: ./.github/workflows/test-build.yml + with: + arch: arm + # i.MX95 Cortex-M7: wolfBoot runs from ITCM (loaded by remoteproc) and + # the payload lives in DDR, so there is no contiguous factory.bin. + config-file: ./config/examples/imx95-m7.config + make-args: wolfboot.bin test-app/image_v1_signed.bin + tegra234_test: uses: ./.github/workflows/test-build.yml with: diff --git a/Makefile b/Makefile index 4489c08f..7639cbc9 100644 --- a/Makefile +++ b/Makefile @@ -381,6 +381,12 @@ ifeq ($(TARGET),cm4) MAIN_TARGET:=wolfboot.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) + MAIN_TARGET:=wolfboot.bin test-app/image_v1_signed.bin +endif + ifeq ($(TARGET),sim) CFLAGS+=-fno-pie LDFLAGS+=-no-pie diff --git a/docs/Targets.md b/docs/Targets.md index 5c65e895..d7ebc431 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -20,6 +20,7 @@ This README describes configuration of supported targets. * [Nordic nRF52840](#nordic-nrf52840) * [Nordic nRF5340](#nordic-nrf5340) * [Nordic nRF54L15](#nordic-nrf54l15) +* [NXP i.MX95 Cortex-M7](#nxp-imx95-cortex-m7) * [NXP iMX-RT](#nxp-imx-rt) * [NXP Kinetis](#nxp-kinetis) * [NXP Kinetis KL26Z](#nxp-kinetis-kl26z) @@ -9093,3 +9094,84 @@ Boot success marked. Version: 1 | `FLAGS_HOME` | Keep boot flags in internal flash (required when `EXT_FLASH=1`). | | `MAX3266X_TPU` | Enable TPU hardware SHA256 acceleration (requires `MSDK_DIR`). | | `MAX3266X_OLD` | Build TPU acceleration against the older, deprecated Maxim SDK tree instead of the modern MSDK. | + + +## 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. + +Validated on a Toradex SMARC i.MX95 module with `TARGET=imx95_m7`. + +### i.MX95: Memory layout + +wolfBoot is linked for the *core* view of TCM. `remoteproc` loads through the *system* view and `imx_rproc` translates between the two; linking for the system view produces an image that loads cleanly and faults on the first instruction fetch. + +| Region | Core view | System view | Size | +|--------|-----------|-------------|------| +| ITCM (wolfBoot text) | `0x00000000` | `0x203C0000` | 256 KiB | +| DTCM (wolfBoot data, app data/heap/stack) | `0x20000000` | `0x20400000` | 256 KiB | +| Reserved DDR (`memory@80000000`) | `0x80000000` | - | 16 MiB | + +The TCM split assumes `M7_CFG[TCM_SIZE] = 000b` (256 KiB + 256 KiB), the reset default. That field lives in `BLK_CTRL_Secure_AON` and is owned by the System Manager running on the M33, not by wolfBoot. + +The "flash" wolfBoot writes is ordinary DDR: writes are copies and erases are fills. Linux stages an update by writing the UPDATE partition before restarting the core. + +| Block | Address | Size | +|-------|---------|------| +| BOOT partition | `0x80100000` | 4 MiB | +| UPDATE partition | `0x80500000` | 4 MiB | +| SWAP | `0x80900000` | 16 KiB sector | +| Console header + ring | `0x80F00000` | 64 KiB | +| wolfBoot status block | `0x80F10000` | 4 words | +| Test-app status block | `0x80F10010` | 2 words | + +The RPMsg carveouts at `0x88000000` (vrings) and `0x88020000` (vdevbuffer) belong to the RPMsg transport and are deliberately not used for the console or status blocks. wolfBoot still carries the `remoteproc` resource table declaring them, because Linux looks for `.resource_table` in the ELF *it* loads - without it the kernel logs `No resource table in elf` and never creates the virtio device. + +### i.MX95: Building + +```sh +cp config/examples/imx95-m7.config .config +make +``` + +This produces `wolfboot.elf` (the image `remoteproc` loads) and `test-app/image_v1_signed.bin` (the payload Linux writes to `0x80100000`). There is no `factory.bin`: wolfBoot runs from ITCM and the payload lives in DDR, so there is no contiguous flash image to assemble. + +To build the test app with the wolfCrypt test suite and benchmark: + +```sh +make WOLFCRYPT_TEST=1 WOLFCRYPT_BENCHMARK=1 +``` + +Benchmark timing comes from the Cortex-M7 DWT cycle counter, scaled by `IMX95_M7_HZ` (800 MHz by default, matching this board's `clk_summary` entry `m7 800000000`). The M7 cannot read its own clock rate without an SCMI round trip to the System Manager, so if that rate is configured differently the constant must be overridden or every reported figure scales by the ratio: + +```sh +make WOLFCRYPT_BENCHMARK=1 CFLAGS_EXTRA=-DIMX95_M7_HZ=1000000000ULL +``` + +### i.MX95: Console and status + +The M7's LPUART is not routed to a host-accessible header on this carrier, and Linux owns all four LPUART instances, so the console is a ring buffer in the M7's own DDR window rather than a peripheral driver. Its header is: + +| Offset | Field | Meaning | +|--------|-------|---------| +| `0x00` | `magic` | `0x4E4F4357` - the stored bytes read `WCON` in a hexdump | +| `0x04` | `wr` | Total bytes ever written, monotonic within a run | +| `0x08` | `size` | Ring size in bytes; read it rather than assuming | +| `0x0C` | `rsvd` | Reserved | + +A reader tracks its own position `pos` and copies `wr - pos` bytes starting at `data[pos % size]`. Overrun is possible for very chatty output and is detected when `wr - pos > size`, so the reader reports the gap instead of printing corrupt text. + +Progress is also published as plain 32-bit words that Linux can poll with `devmem` without parsing console text: + +```sh +devmem 0x80F10000 # 0x57424F54 "WBOT" - wolfBoot status block valid +devmem 0x80F10004 # progress code: 1 = hal_init, 2 = hal_prepare_boot +devmem 0x80F10008 # DWT cycle count at hal_init +devmem 0x80F1000C # DWT cycle count at hal_prepare_boot +devmem 0x80F10010 # 0x41505031 "APP1" - the payload is running +devmem 0x80F10014 # heartbeat, incrementing +``` + +The difference between the two timestamps is the cost of everything wolfBoot does in between, which is dominated by signature verification. Note that these magics are spelled to read correctly as `devmem` 32-bit words, the opposite convention from the console magic, which is read from a hexdump of the ring. + +Both caches are enabled by `hal_init()`, which matters because verifying an image means hashing megabytes resident in DDR. The ARMv7-M default memory map marks `0x80000000-0x9FFFFFFF` as Normal write-through, so no MPU region is needed and M7 stores to the shared window still reach DDR; the HAL nevertheless cleans the affected lines explicitly so that behaviour is not left depending on an inherited attribute. diff --git a/hal/imx95_m7.c b/hal/imx95_m7.c index 2a527529..759d16e4 100644 --- a/hal/imx95_m7.c +++ b/hal/imx95_m7.c @@ -45,6 +45,7 @@ #include #include #include "image.h" +#include "hal/imx95_m7.h" /* --------------------------------------------------------------------------- * remoteproc resource table @@ -130,54 +131,50 @@ const struct imx95_rsc_table resource_table /* Erased state of the DDR-backed pseudo-flash. */ #define FLASH_ERASED_BYTE 0xFFU -/* Bounds of the M7's reserved DDR window. Anything outside it is rejected - * rather than silently corrupting memory that belongs to Linux. */ -#define M7_DDR_BASE 0x80000000UL -#define M7_DDR_SIZE 0x01000000UL - -/* Liveness marker. The M7 has no confirmed UART route on this carrier, so - * early bring-up reports progress through shared memory that Linux can read - * with devmem instead of over a serial port. +/* The M7 has no confirmed UART route on this carrier, so early bring-up + * reports progress through the shared status block that Linux can read with + * devmem instead of over a serial port. Its address, and the console ring + * immediately below it, live in hal/imx95_m7.h. * - * This lives inside the M7's own DDR window, immediately above the console - * ring at 0x80F00000, and deliberately NOT in the 0x88000000 block: the device - * tree reserves that for RPMsg (vdev0/vdev1 vrings, and a 1 MiB vdevbuffer at - * 0x88020000), so a status word there would be scribbling on buffers Linux - * owns as soon as RPMsg is brought up. */ -#define WOLFBOOT_STATUS_ADDR 0x80F10000UL -#define WOLFBOOT_STATUS_MAGIC 0x57424F54UL /* "WBOT" */ - -/* Cortex-M7 DWT cycle counter. With no usable console on this carrier the - * cycle counter is how the signature-verification cost gets measured: it is - * exact, free to read, and needs no peripheral. Timestamps are published - * alongside the status word for the A55 to read. */ -#define CORE_DEMCR (*(volatile uint32_t *)0xE000EDFCUL) -#define DEMCR_TRCENA (1UL << 24) -#define DWT_CTRL (*(volatile uint32_t *)0xE0001000UL) -#define DWT_CYCCNTENA (1UL << 0) -#define DWT_CYCCNT (*(volatile uint32_t *)0xE0001004UL) - -/* Cortex-M7 cache maintenance. Both caches are OFF out of reset, and this - * image runs its text from DDR, so leaving them off means every instruction - * fetch and data access goes to external memory - measured at ~664 cycles per - * byte for SHA-256, roughly fifty times slower than the core is capable of. + * Both caches are OFF out of reset. wolfBoot's own text runs from ITCM (see + * hal/imx95_m7.ld), so the I-cache buys it little; the D-cache is what matters, + * because verifying an image means hashing multiple megabytes that live in + * DDR - measured at ~664 cycles per byte for SHA-256 with the cache off, + * roughly fifty times slower than the core is capable of. * * The ARMv7-M default memory map already marks 0x80000000-0x9FFFFFFF as normal - * cacheable memory, so no MPU region is needed to make this take effect. */ -#define SCB_CCR (*(volatile uint32_t *)0xE000ED14UL) -#define SCB_CCSIDR (*(volatile uint32_t *)0xE000ED80UL) -#define SCB_CSSELR (*(volatile uint32_t *)0xE000ED84UL) -#define SCB_ICIALLU (*(volatile uint32_t *)0xE000EF50UL) -#define SCB_DCISW (*(volatile uint32_t *)0xE000EF60UL) -#define CCR_IC (1UL << 17) -#define CCR_DC (1UL << 16) + * write-through cacheable memory, so no MPU region is needed to make this take + * effect, and stores to the shared window still reach DDR. */ -#define DSB() __asm__ volatile ("dsb 0xF" ::: "memory") -#define ISB() __asm__ volatile ("isb 0xF" ::: "memory") +/* Fill sets/ways/shift from CCSIDR for the L1 data cache. The shifts are + * geometry-dependent (ARMv7-M DDI 0403: way field starts at 32-log2(assoc), + * set field at log2(line size)), so derive both rather than assuming the + * Cortex-M7's fixed 4-way, 32-byte-line configuration. */ +static void hal_dcache_geometry(uint32_t *sets, uint32_t *ways, + uint32_t *way_shift, uint32_t *set_shift) +{ + uint32_t ccsidr, tmp, shift; + + SCB_CSSELR = 0UL; /* select L1 data cache */ + DSB(); + ccsidr = SCB_CCSIDR; + + *sets = ((ccsidr >> 13) & 0x7FFFUL) + 1UL; + *ways = ((ccsidr >> 3) & 0x3FFUL) + 1UL; + *set_shift = (ccsidr & 0x7UL) + 4UL; + + shift = 32UL; + tmp = *ways - 1UL; + while (tmp != 0UL) { + shift--; + tmp >>= 1; + } + *way_shift = shift; +} static void hal_cache_enable(void) { - uint32_t ccsidr, sets, ways, s, w; + uint32_t sets, ways, way_shift, set_shift, s, w; /* I-cache: invalidate, then enable. */ DSB(); ISB(); @@ -188,15 +185,10 @@ static void hal_cache_enable(void) /* D-cache: invalidate every set/way before enabling, otherwise stale * lines from reset can be written back over live DDR. */ - SCB_CSSELR = 0UL; /* select L1 data cache */ - DSB(); - ccsidr = SCB_CCSIDR; - sets = (ccsidr >> 13) & 0x7FFFUL; - ways = (ccsidr >> 3) & 0x3FFUL; - for (s = 0; s <= sets; s++) { - for (w = 0; w <= ways; w++) { - /* DCISW: way in [31:30], set in [13:5] for a 32-byte line. */ - SCB_DCISW = ((w << 30) | (s << 5)); + hal_dcache_geometry(&sets, &ways, &way_shift, &set_shift); + for (s = 0; s < sets; s++) { + for (w = 0; w < ways; w++) { + SCB_DCISW = ((w << way_shift) | (s << set_shift)); } } DSB(); @@ -204,11 +196,23 @@ static void hal_cache_enable(void) DSB(); ISB(); } -static void hal_cycle_counter_start(void) +/* Clean every dirty line back to DDR and drop the I-cache. Used before handing + * off to an image wolfBoot may have just written through the D-cache: the + * Cortex-M7 instruction side does not snoop the data side, so without this the + * jump would rely on the default map happening to be write-through. */ +static void hal_cache_flush(void) { - CORE_DEMCR |= DEMCR_TRCENA; - DWT_CYCCNT = 0; - DWT_CTRL |= DWT_CYCCNTENA; + uint32_t sets, ways, way_shift, set_shift, s, w; + + hal_dcache_geometry(&sets, &ways, &way_shift, &set_shift); + for (s = 0; s < sets; s++) { + for (w = 0; w < ways; w++) { + SCB_DCCSW = ((w << way_shift) | (s << set_shift)); + } + } + DSB(); + SCB_ICIALLU = 0UL; + DSB(); ISB(); } static int hal_addr_is_valid(uint32_t address, int len) @@ -226,11 +230,23 @@ static int hal_addr_is_valid(uint32_t address, int len) return 1; } -#ifdef __WOLFBOOT +/* hal_init()/hal_prepare_boot() are deliberately NOT wrapped in + * #ifdef __WOLFBOOT: test-app/Makefile pulls ../hal/imx95_m7.o into APP_OBJS + * and rebuilds it in place with the application's flags, so an object that + * only defines them for the bootloader build would be clobbered and break the + * next wolfboot.elf link. The application simply never calls them, and + * --gc-sections drops them from its image. + * + * Progress codes stamped into the shared status block. Kept deliberately + * trivial: no locking, no ordering guarantees beyond the store itself. */ +#define HAL_STATUS_INIT 1U +#define HAL_STATUS_PREBOOT 2U -/* Record a progress code where the A55 side can see it. Kept deliberately - * trivial: no locking, no ordering guarantees beyond the write itself. */ -void hal_status(uint32_t code) +/* Record a progress code where the A55 side can see it. The block is only + * WOLFBOOT_STATUS_WORDS words wide and the word above it is the test app's + * liveness magic, so the timestamp slot is selected explicitly rather than + * indexed by the caller's code. */ +static void hal_status(uint32_t code) { volatile uint32_t *status = (volatile uint32_t *)WOLFBOOT_STATUS_ADDR; @@ -239,7 +255,18 @@ void hal_status(uint32_t code) /* status[2] is stamped at hal_init, status[3] at hal_prepare_boot; the * difference is the cost of everything wolfBoot does in between, which is * dominated by the signature verification. */ - status[code + 1] = DWT_CYCCNT; + switch (code) { + case HAL_STATUS_INIT: + status[2] = DWT_CYCCNT; + break; + case HAL_STATUS_PREBOOT: + status[3] = DWT_CYCCNT; + break; + default: + break; + } + imx95_dcache_clean((const void *)status, + WOLFBOOT_STATUS_WORDS * sizeof(uint32_t)); } void hal_init(void) @@ -248,19 +275,25 @@ void hal_init(void) * released: DDR by the OEI/SPL stage, the TCM split by the System Manager * on the M33 via M7_CFG[TCM_SIZE] in BLK_CTRL_Secure_AON. There is * nothing for wolfBoot to bring up here except the caches, which are off - * out of reset and which this image needs because it runs from DDR. */ + * out of reset and which this image needs because the partitions it hashes + * live in DDR. + * + * .bss and .data are handled by src/boot_arm.c:isr_reset(): remoteproc + * loads only PT_LOAD segments and .bss is NOBITS, so nothing external + * zeroes it before entry. */ hal_cache_enable(); - hal_cycle_counter_start(); - hal_status(1); + imx95_dwt_init(); + hal_status(HAL_STATUS_INIT); } void hal_prepare_boot(void) { - hal_status(2); + hal_status(HAL_STATUS_PREBOOT); + /* wolfBoot may have just written the image being booted into DDR through + * the D-cache; make that visible to the instruction side before jumping. */ + hal_cache_flush(); } -#endif /* __WOLFBOOT */ - int RAMFUNCTION hal_flash_write(uint32_t address, const uint8_t *data, int len) { if (data == NULL) diff --git a/hal/imx95_m7.h b/hal/imx95_m7.h new file mode 100644 index 00000000..3744f3a1 --- /dev/null +++ b/hal/imx95_m7.h @@ -0,0 +1,172 @@ +/* imx95_m7.h + * + * Shared definitions for the Cortex-M7 on the NXP i.MX95. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +/* The shared-memory layout below is read by the A55 cluster running Linux, so + * it is described in exactly one place: the HAL, the console driver and the + * test app all include this header rather than repeating the addresses. */ + +#ifndef IMX95_M7_H +#define IMX95_M7_H + +#include + +/* --------------------------------------------------------------------------- + * Memory map + * + * The M7's reserved DDR window is memory@80000000, 16 MiB. wolfBoot's + * partitions sit in the lower part of it (BOOT 0x80100000, UPDATE 0x80500000, + * SWAP 0x80900000); the shared window below occupies the top of it, clear of + * the RPMsg reservations at 0x88000000 which belong to the transport. + * + * 0x80F00000 console header (16 B) + ring (65520 B) + * 0x80F10000 wolfBoot status block (4 words) + * 0x80F10010 test-app status block (2 words) + * ------------------------------------------------------------------------- */ + +#define M7_DDR_BASE 0x80000000UL +#define M7_DDR_SIZE 0x01000000UL + +/* Console ring. CONSOLE_REGION is the whole 64 KiB page; the ring is what is + * left of it once the header is accounted for, so the console cannot run into + * the status block above it. Cortex-M7 has a hardware UDIV, so the + * non-power-of-two modulo in uart_tx() costs a few cycles, not a helper call. */ +#define CONSOLE_BASE 0x80F00000UL +#define CONSOLE_HDR_SIZE 16UL +#define CONSOLE_REGION 0x00010000UL +#define CONSOLE_SIZE (CONSOLE_REGION - CONSOLE_HDR_SIZE) +/* Stored bytes are 57 43 4F 4E, so a hexdump of the ring reads "WCON". */ +#define CONSOLE_MAGIC 0x4E4F4357UL + +/* Status blocks. These are read one 32-bit word at a time with devmem, which + * prints the word rather than its bytes, so the magics are spelled to read + * correctly in that view ("WBOT", "APP1") - the opposite convention from + * CONSOLE_MAGIC above, which is read from a hexdump. */ +#define WOLFBOOT_STATUS_ADDR 0x80F10000UL +#define WOLFBOOT_STATUS_WORDS 4U +#define WOLFBOOT_STATUS_MAGIC 0x57424F54UL /* "WBOT" */ + +#define APP_STATUS_ADDR (WOLFBOOT_STATUS_ADDR + (WOLFBOOT_STATUS_WORDS * 4UL)) +#define APP_STATUS_WORDS 2U +#define APP_STATUS_MAGIC 0x41505031UL /* "APP1" */ + +/* The ring and the status block used to overlap by exactly the header size. + * Fail the build rather than let the two drift apart again. */ +typedef char imx95_console_fits_below_status[ + ((CONSOLE_BASE + CONSOLE_HDR_SIZE + CONSOLE_SIZE) <= WOLFBOOT_STATUS_ADDR) + ? 1 : -1]; + +/* --------------------------------------------------------------------------- + * Barriers and cache maintenance + * + * Both L1 caches are off out of reset. Once they are on, note that "volatile" + * only binds the compiler: it places no barrier on the core, and the A55 is a + * genuinely separate observer of the shared window, so publish protocols need + * a real DMB. + * + * The ARMv7-M default memory map makes 0x80000000-0x9FFFFFFF Normal + * write-through, which is why M7 stores to the shared window reach DDR at all + * without maintenance. The explicit cleans below cost close to nothing on a + * write-through line and stop that inherited attribute from being load-bearing + * if a future MPU region, NO_MPU=0 or a different carveout base changes it. + * ------------------------------------------------------------------------- */ + +#define DMB() __asm__ volatile ("dmb 0xF" ::: "memory") +#define DSB() __asm__ volatile ("dsb 0xF" ::: "memory") +#define ISB() __asm__ volatile ("isb 0xF" ::: "memory") + +#define SCB_CCR (*(volatile uint32_t *)0xE000ED14UL) +#define SCB_CCSIDR (*(volatile uint32_t *)0xE000ED80UL) +#define SCB_CSSELR (*(volatile uint32_t *)0xE000ED84UL) +#define SCB_ICIALLU (*(volatile uint32_t *)0xE000EF50UL) +#define SCB_DCISW (*(volatile uint32_t *)0xE000EF60UL) +#define SCB_DCCMVAC (*(volatile uint32_t *)0xE000EF68UL) +#define SCB_DCCSW (*(volatile uint32_t *)0xE000EF6CUL) +#define CCR_IC (1UL << 17) +#define CCR_DC (1UL << 16) + +/* Cortex-M7 L1 line size. Also derived from CCSIDR where the set/way loops + * need it; this constant is only for maintenance by address. */ +#define IMX95_CACHE_LINE 32UL + +static inline void imx95_dcache_clean(const void *addr, uint32_t len) +{ + uint32_t line; + uint32_t end; + + if (len == 0) + return; + line = ((uint32_t)(uintptr_t)addr) & ~(IMX95_CACHE_LINE - 1UL); + end = (uint32_t)(uintptr_t)addr + len; + for (; line < end; line += IMX95_CACHE_LINE) { + SCB_DCCMVAC = line; + } + DSB(); +} + +/* --------------------------------------------------------------------------- + * DWT cycle counter + * + * With no console UART routed on this carrier the cycle counter is how boot + * cost gets measured: exact, free to read, and needing no peripheral. + * ------------------------------------------------------------------------- */ + +#define CORE_DEMCR (*(volatile uint32_t *)0xE000EDFCUL) +#define DEMCR_TRCENA (1UL << 24) +#define DWT_CTRL (*(volatile uint32_t *)0xE0001000UL) +#define DWT_CYCCNTENA (1UL << 0) +#define DWT_CYCCNT (*(volatile uint32_t *)0xE0001004UL) + +/* Idempotent: a second caller must not restart a counter someone else is + * already extending to 64 bits. */ +static inline void imx95_dwt_init(void) +{ + if ((DWT_CTRL & DWT_CYCCNTENA) != 0UL) + return; + CORE_DEMCR |= DEMCR_TRCENA; + DWT_CYCCNT = 0; + DWT_CTRL |= DWT_CYCCNTENA; +} + +/* Bounded spin. Wrap-safe for any delay shorter than a full 32-bit period + * (~5.4 s at 800 MHz) because the comparison is done on the difference. */ +static inline void imx95_delay_cycles(uint32_t cycles) +{ + uint32_t start = DWT_CYCCNT; + + while ((uint32_t)(DWT_CYCCNT - start) < cycles) + ; +} + +/* --------------------------------------------------------------------------- + * SysTick (test app only; wolfBoot itself runs with interrupts off) + * ------------------------------------------------------------------------- */ + +#define SYST_CSR (*(volatile uint32_t *)0xE000E010UL) +#define SYST_RVR (*(volatile uint32_t *)0xE000E014UL) +#define SYST_CVR (*(volatile uint32_t *)0xE000E018UL) +#define SYST_CSR_ENABLE (1UL << 0) +#define SYST_CSR_TICKINT (1UL << 1) +#define SYST_CSR_CLKSOURCE (1UL << 2) +#define SYST_RVR_MAX 0x00FFFFFFUL + +#endif /* IMX95_M7_H */ diff --git a/hal/imx95_m7.ld b/hal/imx95_m7.ld index f9843830..77ee4567 100644 --- a/hal/imx95_m7.ld +++ b/hal/imx95_m7.ld @@ -39,6 +39,15 @@ SECTIONS . = ALIGN(8); } > ITCM + /* Public keys emitted by the keytool (KEYSTORE_SECTION). Placed + * explicitly so it cannot land after _stored_data as an orphan. */ + .keystore : + { + . = ALIGN(8); + KEEP(*(.keystore*)) + . = ALIGN(8); + } > ITCM + .edidx : { . = ALIGN(4); @@ -52,6 +61,8 @@ SECTIONS _start_data = .; KEEP(*(.data*)) . = ALIGN(4); + KEEP(*(.ramcode)) + . = ALIGN(4); _end_data = .; } > DTCM diff --git a/hal/uart/uart_drv_imx95_m7.c b/hal/uart/uart_drv_imx95_m7.c index bb2995bb..0524582a 100644 --- a/hal/uart/uart_drv_imx95_m7.c +++ b/hal/uart/uart_drv_imx95_m7.c @@ -33,20 +33,24 @@ * The buffer sits at the top of the M7's 16 MiB carveout (memory@80000000), * clear of the wolfBoot partitions lower down (BOOT 0x80100000, UPDATE * 0x80500000, SWAP 0x80900000). It deliberately does NOT use the RPMsg - * vdevbuffer carveout, which belongs to the RPMsg transport. + * vdevbuffer carveout, which belongs to the RPMsg transport. The header plus + * the ring occupy exactly the 64 KiB page below the status block; see + * hal/imx95_m7.h, which holds the layout and asserts that they do not overlap. * * Reader contract: `wr` counts bytes ever written and never wraps within a run, * so a reader tracks its own position and copies (wr - pos) bytes from - * data[pos % size]. Overrun is possible and expected for very chatty output; - * the reader detects it when (wr - pos) > size and reports the gap rather than - * silently printing corrupt text. + * data[pos % size]. `size` is published in the header rather than assumed. + * Overrun is possible and expected for very chatty output; the reader detects + * it when (wr - pos) > size and reports the gap rather than silently printing + * corrupt text. + * + * Publish ordering is enforced with DMB, not by `volatile`: volatile binds the + * compiler only, while the A55 reading this ring is a separate observer of a + * Normal-memory region the M7 may reorder stores to. */ #include - -#define CONSOLE_BASE 0x80F00000UL -#define CONSOLE_MAGIC 0x4E4F4357UL /* "WCON" little-endian */ -#define CONSOLE_SIZE 0x10000UL /* 64 KiB of text */ +#include "hal/imx95_m7.h" struct console_hdr { volatile uint32_t magic; @@ -56,7 +60,7 @@ struct console_hdr { }; #define CONSOLE_HDR ((struct console_hdr *)CONSOLE_BASE) -#define CONSOLE_DATA ((volatile uint8_t *)(CONSOLE_BASE + sizeof(struct console_hdr))) +#define CONSOLE_DATA ((volatile uint8_t *)(CONSOLE_BASE + CONSOLE_HDR_SIZE)) int uart_init(uint32_t bitrate, uint8_t data, char parity, uint8_t stop) { @@ -69,9 +73,11 @@ int uart_init(uint32_t bitrate, uint8_t data, char parity, uint8_t stop) h->wr = 0; h->size = CONSOLE_SIZE; - /* Magic last: a reader that samples mid-init sees no valid console rather - * than a valid magic with a stale size. */ + /* Magic last, and behind a barrier: a reader that samples mid-init sees no + * valid console rather than a valid magic with a stale size. */ + DMB(); h->magic = CONSOLE_MAGIC; + imx95_dcache_clean((const void *)h, sizeof(*h)); return 0; } @@ -79,15 +85,20 @@ int uart_tx(const uint8_t c) { struct console_hdr *h = CONSOLE_HDR; uint32_t w; + uint32_t idx; if (h->magic != CONSOLE_MAGIC) (void)uart_init(0, 0, 'N', 0); w = h->wr; - CONSOLE_DATA[w % CONSOLE_SIZE] = c; + idx = w % CONSOLE_SIZE; + CONSOLE_DATA[idx] = c; + imx95_dcache_clean((const void *)&CONSOLE_DATA[idx], 1); /* Publish the byte before advancing the counter, so a reader never sees a * count covering a byte that has not been stored yet. */ + DMB(); h->wr = w + 1; + imx95_dcache_clean((const void *)h, sizeof(*h)); return 0; } diff --git a/test-app/Makefile b/test-app/Makefile index 0f233bbc..b39a1996 100644 --- a/test-app/Makefile +++ b/test-app/Makefile @@ -622,6 +622,9 @@ endif # arch.mk via CORTEX_M7. ifeq ($(TARGET),imx95_m7) CFLAGS+=-I.. + # The app owns isr_systick: it samples the DWT cycle counter often enough + # that the 32-bit counter cannot wrap unnoticed between timebase reads. + CFLAGS+=-DAPP_HAS_SYSTICK APP_OBJS+=../hal/uart/uart_drv_$(UART_TARGET).o LSCRIPT_TEMPLATE=ARM-imx95_m7.ld endif diff --git a/test-app/app_imx95_m7.c b/test-app/app_imx95_m7.c index 86c55d16..1551eb52 100644 --- a/test-app/app_imx95_m7.c +++ b/test-app/app_imx95_m7.c @@ -35,11 +35,13 @@ * wolfBoot writes its own progress codes at 0x80F10000, so a reader can tell * "bootloader ran" from "payload ran" with no peripheral wired up. The block * sits just above the console ring in the M7's DDR window, clear of the - * RPMsg reservations at 0x88000000. + * RPMsg reservations at 0x88000000. The addresses come from hal/imx95_m7.h, + * which is also where the two blocks are asserted not to overlap. */ #include #include "wolfboot/wolfboot.h" +#include "hal/imx95_m7.h" #if defined(WOLFCRYPT_TEST) || defined(WOLFCRYPT_BENCHMARK) #include @@ -52,12 +54,39 @@ int wolfcrypt_test(void *args); int benchmark_test(void *args); #endif -#define APP_STATUS_ADDR 0x80F10010UL -#define APP_STATUS_MAGIC 0x41505031UL /* "APP1" */ +/* A heartbeat visible to a devmem poller does not need to run at core speed; + * an unthrottled loop just burns DDR bandwidth the A55 cluster shares. */ +#define APP_HEARTBEAT_CYCLES 0x04000000UL /* ~84 ms at 800 MHz */ extern int uart_init(uint32_t bitrate, uint8_t data, char parity, uint8_t stop); extern void uart_write(const char *buf, unsigned int len); +#if defined(WOLFCRYPT_TEST) || defined(WOLFCRYPT_BENCHMARK) +/* Defined in test-app/wolfcrypt_support.c: folds the 32-bit DWT counter into + * the 64-bit timebase. Called often enough here that a wrap cannot be missed + * between two timebase reads, however far apart those are. */ +extern void imx95_m7_cyc_sample(void); + +static void systick_start(void) +{ + /* Full 24-bit reload: ~21 ms at 800 MHz, far inside the ~5.4 s CYCCNT + * wrap, and it needs no knowledge of the actual core clock. */ + SYST_RVR = SYST_RVR_MAX; + SYST_CVR = 0; + SYST_CSR = SYST_CSR_CLKSOURCE | SYST_CSR_TICKINT | SYST_CSR_ENABLE; +} +#endif + +/* The vector slot is claimed unconditionally (APP_HAS_SYSTICK in + * test-app/Makefile), so the handler must exist even in builds with no + * timebase to sample; SysTick is only started when there is one. */ +void isr_systick(void) +{ +#if defined(WOLFCRYPT_TEST) || defined(WOLFCRYPT_BENCHMARK) + imx95_m7_cyc_sample(); +#endif +} + /* Local, so the banner works in builds that do not link ../src/string.o. */ static void say(const char *s) { @@ -72,22 +101,21 @@ void main(void) { volatile uint32_t *status = (volatile uint32_t *)APP_STATUS_ADDR; uint32_t heartbeat = 0; - /* remoteproc loads only PT_LOAD segments, and .bss is NOBITS - nothing - * zeroes it before entry, so do it here before any static is read. */ - extern char _start_bss[], _end_bss[]; - char *p; - for (p = _start_bss; p < _end_bss; p++) - *p = 0; + /* .data and .bss are already handled by test-app/startup_arm.c:isr_reset(), + * which runs from this image's own vector table before main(). */ (void)uart_init(115200, 8, 'N', 1); status[0] = APP_STATUS_MAGIC; status[1] = heartbeat; + imx95_dcache_clean((const void *)status, + APP_STATUS_WORDS * sizeof(uint32_t)); say("\r\nwolfBoot test app: i.MX95 Cortex-M7\r\n"); #if defined(WOLFCRYPT_TEST) || defined(WOLFCRYPT_BENCHMARK) + systick_start(); wolfCrypt_Init(); #ifdef WOLFCRYPT_TEST say("\r\nRunning wolfCrypt tests...\r\n"); @@ -107,7 +135,10 @@ void main(void) /* Confirm this image booted so wolfBoot does not roll it back. */ wolfBoot_success(); + imx95_dwt_init(); while (1) { status[1] = ++heartbeat; + imx95_dcache_clean((const void *)&status[1], sizeof(uint32_t)); + imx95_delay_cycles(APP_HEARTBEAT_CYCLES); } } diff --git a/test-app/wolfcrypt_support.c b/test-app/wolfcrypt_support.c index a01b7adb..a9a5c6d6 100644 --- a/test-app/wolfcrypt_support.c +++ b/test-app/wolfcrypt_support.c @@ -47,44 +47,64 @@ #elif defined(TARGET_imx95_m7) /* Cortex-M7 DWT cycle counter. Exact, free to read, and needs no * peripheral - which matters here because the M7 has no console UART - * routed on this carrier. The core runs at 800 MHz (read from the board's - * clk_summary: "m7 800000000"), not assumed. CYCCNT is 32-bit and wraps - * every ~5.4 s at that rate, so accumulate into 64 bits on each read; - * benchmark intervals are shorter than one wrap, so sampling on every call - * is sufficient to catch it. */ + * routed on this carrier. + * + * IMX95_M7_HZ is a build-time constant, not a run-time reading: the M7 + * clock is owned by the System Manager on the M33 and the core cannot + * query it without an SCMI round trip. 800 MHz is what this board's + * clk_summary reports ("m7 800000000"); override with -DIMX95_M7_HZ if the + * System Manager is configured differently, or every reported figure + * scales by the ratio. + * + * CYCCNT is 32 bits and wraps every ~5.4 s at that rate, and the wrap + * accounting below can only recover one wrap per read. my_time() alone + * does not read often enough to guarantee that - it is called only when + * wolfCrypt validates a certificate date - so app_imx95_m7.c drives + * imx95_m7_cyc_sample() from SysTick to keep the invariant true. */ + #include "hal/imx95_m7.h" + + #ifndef IMX95_M7_HZ #define IMX95_M7_HZ 800000000ULL + #endif + static uint64_t m7_cyc_hi = 0; static uint32_t m7_cyc_last = 0; static int m7_cyc_inited = 0; + static uint64_t m7_start_ticks = 0; - static void m7_cyc_init(void); - + /* Reached from both thread and interrupt context, so the wrap accounting + * is a critical section. PRIMASK is saved and restored rather than + * unconditionally re-enabled, since the caller may already be masked. */ static uint64_t m7_get_ticks(void) { - uint32_t now; + uint32_t now, primask; + uint64_t ticks; + + __asm__ volatile ("mrs %0, primask" : "=r"(primask)); + __asm__ volatile ("cpsid i" ::: "memory"); if (!m7_cyc_inited) { - m7_cyc_init(); + imx95_dwt_init(); + m7_cyc_hi = 0; + m7_cyc_last = 0; m7_cyc_inited = 1; } - now = *(volatile uint32_t *)0xE0001004UL; /* DWT_CYCCNT */ - + now = DWT_CYCCNT; if (now < m7_cyc_last) m7_cyc_hi += 0x100000000ULL; /* wrapped since last read */ m7_cyc_last = now; - return m7_cyc_hi + now; + ticks = m7_cyc_hi + now; + + if ((primask & 1U) == 0U) + __asm__ volatile ("cpsie i" ::: "memory"); + return ticks; } - static void m7_cyc_init(void) + /* Called from the app's SysTick handler; see app_imx95_m7.c. */ + void imx95_m7_cyc_sample(void) { - *(volatile uint32_t *)0xE000EDFCUL |= (1UL << 24); /* DEMCR.TRCENA */ - *(volatile uint32_t *)0xE0001004UL = 0; /* DWT_CYCCNT = 0 */ - *(volatile uint32_t *)0xE0001000UL |= 1UL; /* DWT_CTRL.CYCCNTENA */ - m7_cyc_hi = 0; - m7_cyc_last = 0; + (void)m7_get_ticks(); } - - static uint64_t m7_start_ticks = 0; #elif defined(TARGET_nxp_t2080) || defined(TARGET_nxp_t1024) /* PPC time base register for accurate timing (e6500). */ static uint32_t ppc_tb_hz = 0; @@ -239,11 +259,11 @@ double current_time(int reset) (void)reset; return (double)SysTick_time_ms / 1000.0; #elif defined(TARGET_imx95_m7) - if (reset) { - m7_cyc_init(); - m7_cyc_inited = 1; - m7_start_ticks = 0; - } + /* Take a new origin rather than zeroing the counter: my_time() shares it + * and the benchmark resets once per algorithm, which would otherwise walk + * wolfCrypt's notion of wall-clock time backwards dozens of times a run. */ + if (reset) + m7_start_ticks = m7_get_ticks(); return (double)(m7_get_ticks() - m7_start_ticks) / (double)IMX95_M7_HZ; #elif defined(TARGET_nxp_t2080) || defined(TARGET_nxp_t1024) if (ppc_tb_hz == 0)