From c61d3ed999d1d52129e01bd1a4e8c8982e79d8a6 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 20:52:35 +0100 Subject: [PATCH 1/4] Keep a rollback on the original image across a power cut --- .gitignore | 1 + config/examples/stm32c0-rsa2048.config | 8 +- docs/Targets.md | 6 +- src/update_flash.c | 151 ++++- tools/scripts/sim-update-powerfail-resume.sh | 46 +- tools/unit-tests/Makefile | 11 + tools/unit-tests/unit-mock-flash.c | 118 +++- tools/unit-tests/unit-update-flash.c | 602 ++++++++++++++++++- 8 files changed, 872 insertions(+), 71 deletions(-) diff --git a/.gitignore b/.gitignore index 145518e660..b120c12113 100644 --- a/.gitignore +++ b/.gitignore @@ -181,6 +181,7 @@ tools/unit-tests/unit-string tools/unit-tests/unit-update-flash tools/unit-tests/unit-update-flash-enc tools/unit-tests/unit-update-flash-db +tools/unit-tests/unit-update-flash-int-invert tools/unit-tests/unit-update-flash-enc-full tools/unit-tests/unit-update-flash-enc-intswap tools/unit-tests/unit-update-ram diff --git a/config/examples/stm32c0-rsa2048.config b/config/examples/stm32c0-rsa2048.config index 979b33db9b..bf90caeb2c 100644 --- a/config/examples/stm32c0-rsa2048.config +++ b/config/examples/stm32c0-rsa2048.config @@ -28,12 +28,12 @@ CFLAGS_EXTRA+=-DUSE_SLOW_SHA256 #CFLAGS_EXTRA+=-DFLASH_SECURABLE_MEMORY_SUPPORT -#Partition: Boot: 12KB, App, 8KB +#Partition: Boot: 14KB, App, 8KB WOLFBOOT_PARTITION_SIZE?=0x2000 WOLFBOOT_SECTOR_SIZE?=0x800 -#Max WOLFBOOT size is 10KB -WOLFBOOT_PARTITION_BOOT_ADDRESS?=0x08003000 -WOLFBOOT_PARTITION_UPDATE_ADDRESS?=0x08005000 +#Max WOLFBOOT size is 14KB +WOLFBOOT_PARTITION_BOOT_ADDRESS?=0x08003800 +WOLFBOOT_PARTITION_UPDATE_ADDRESS?=0x08005800 WOLFBOOT_PARTITION_SWAP_ADDRESS?=0x08007800 #Example debugging partition for larger wolfBoot diff --git a/docs/Targets.md b/docs/Targets.md index ac736baf21..18a1076bc5 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -653,15 +653,15 @@ with ED25519 or LMS-8-10-1: with RSA2048: - Sector size: 2KB -- Wolfboot partition size: 12KB +- Wolfboot partition size: 14KB - Application partition size: 8 KB - Swap size 2KB ```C #define WOLFBOOT_SECTOR_SIZE 0x800 /* 2 KB */ -#define WOLFBOOT_PARTITION_BOOT_ADDRESS 0x08003000 /* at 12KB */ +#define WOLFBOOT_PARTITION_BOOT_ADDRESS 0x08003800 /* at 14KB */ #define WOLFBOOT_PARTITION_SIZE 0x2000 /* 8 KB */ -#define WOLFBOOT_PARTITION_UPDATE_ADDRESS 0x08005000 /* at 20KB */ +#define WOLFBOOT_PARTITION_UPDATE_ADDRESS 0x08005800 /* at 22KB */ #define WOLFBOOT_PARTITION_SWAP_ADDRESS 0x08007800 /* at 30KB */ ``` diff --git a/src/update_flash.c b/src/update_flash.c index 05eb937a16..18b316c1f0 100644 --- a/src/update_flash.c +++ b/src/update_flash.c @@ -159,6 +159,7 @@ static void RAMFUNCTION wolfBoot_erase_bootloader(uint32_t len) #include + #ifdef WOLFBOOT_SELF_HEADER #ifdef NVM_FLASH_ECC /* ECC flash only erases whole sectors */ @@ -359,6 +360,27 @@ void RAMFUNCTION wolfBoot_check_self_update(void) } #endif /* RAM_CODE for self_update */ +/* Reading a write-once trailer may erase its stale copy */ +static void RAMFUNCTION trailer_read_unlock(void) +{ +#ifdef NVM_FLASH_WRITEONCE + hal_flash_unlock(); +#ifdef EXT_FLASH + ext_flash_unlock(); +#endif +#endif +} + +static void RAMFUNCTION trailer_read_lock(void) +{ +#ifdef NVM_FLASH_WRITEONCE +#ifdef EXT_FLASH + ext_flash_lock(); +#endif + hal_flash_lock(); +#endif +} + #ifndef WOLFBOOT_SELF_UPDATE_MONOLITHIC /* The swap-based update machinery (wolfBoot_copy_sector, wolfBoot_update, etc.) * is not used in monolithic self-update mode. */ @@ -581,6 +603,68 @@ static int RAMFUNCTION wolfBoot_backup_last_boot_sector(uint32_t sector) # define TRAILER_OFFSET_WORDS 0 #endif +#ifndef EXT_ENCRYPTED +/* Version of the BOOT sector that the final erase backed up to SWAP, -1 when + * that copy holds no complete header with a version */ +static int RAMFUNCTION wolfBoot_swap_backup_version(uint32_t *ver) +{ + static uint8_t hdr[IMAGE_HEADER_SIZE] XALIGNED(4); + struct wolfBoot_image swap; + uint8_t *field = NULL; + + wolfBoot_open_image(&swap, PART_SWAP); +#ifdef NVM_FLASH_ECC + /* A read of erased ECC flash can fault */ + if (wb_flash_is_erased(&swap, 0, sizeof(hdr)) != 0) + return -1; +#endif +#if defined(EXT_FLASH) && PARTN_IS_EXT(PART_SWAP) + if (ext_flash_read((uintptr_t)swap.hdr, hdr, sizeof(hdr)) != + (int)sizeof(hdr)) + return -1; +#else + memcpy(hdr, swap.hdr, sizeof(hdr)); +#endif + if ((WOLFBOOT_HDR_GET_U32(hdr) != WOLFBOOT_MAGIC) || + (wolfBoot_find_header(hdr + IMAGE_HEADER_OFFSET, HDR_VERSION, + &field) != WOLFBOOT_HDR_U32_SZ) || (field == NULL)) { + return -1; + } + *ver = wolfBoot_get_blob_version(hdr); + return 0; +} + +/* After a swap, UPDATE holds the image that was in BOOT: the swap was a + * rollback unless it installed a newer image. A resume has no BOOT state to + * go by, so UPDATE, possibly external, must hold an authentic image newer + * than BOOT. A resume that restores the header sector takes the BOOT version + * from the SWAP backup, complete before FINAL_FLAGS. An unknown version is + * not a rollback. */ +static int RAMFUNCTION wolfBoot_swap_was_rollback(int resume, int from_swap) +{ + struct wolfBoot_image img; + uint32_t cur = 0; + uint32_t ver; + int known; + + known = (wolfBoot_open_image(&img, PART_UPDATE) == 0); + if (known && resume) { + known = (wolfBoot_verify_integrity(&img) == 0) && + (wolfBoot_verify_authenticity(&img) == 0); + } + if (known && from_swap) + known = (wolfBoot_swap_backup_version(&cur) == 0); + else if (known && (wolfBoot_open_image(&img, PART_BOOT) == 0)) + cur = wolfBoot_current_firmware_version(); + else + known = 0; + if (!known) + return 0; + ver = wolfBoot_update_firmware_version(); + return (ver > cur) || (!resume && (ver == cur)); +} +#endif + /** * @brief Performs the final swap and erase operations during a secure update, * ensuring that if power is lost during the update, the process can be resumed @@ -592,7 +676,7 @@ static int RAMFUNCTION wolfBoot_backup_last_boot_sector(uint32_t sector) * 2. Setting a magic trailer value to mark the swap as in progress * 3. Erasing the last sector(s) of the boot partition (where partition state is stored) * 4. Restoring the saved staging sector from swap back to boot - * 5. Setting the boot partition state to TESTING + * 5. Setting the boot partition state to TESTING, or SUCCESS after a rollback * 6. Erasing the last sector(s) of the update partition * * The staging sector (tmpBootPos) is positioned right before the final sectors @@ -605,9 +689,11 @@ static int RAMFUNCTION wolfBoot_backup_last_boot_sector(uint32_t sector) * * @param resume If 1, checks for interrupted swap and resumes it; if 0, starts * new swap + * @param rollback 1 when the swap restored the previous image; a resume works + * it out again * @return 0 on success, negative value if no swap needed or on error */ -static int RAMFUNCTION wolfBoot_swap_and_final_erase(int resume) +static int RAMFUNCTION wolfBoot_swap_and_final_erase(int resume, int rollback) { struct wolfBoot_image boot[1]; struct wolfBoot_image update[1]; @@ -625,7 +711,9 @@ static int RAMFUNCTION wolfBoot_swap_and_final_erase(int resume) wolfBoot_open_image(boot, PART_BOOT); wolfBoot_open_image(update, PART_UPDATE); wolfBoot_open_image(swap, PART_SWAP); + trailer_read_unlock(); wolfBoot_get_partition_state(PART_UPDATE, &updateState); + trailer_read_lock(); /* Read the trailer from the staging sector to check if we're resuming an * interrupted operation */ @@ -657,6 +745,13 @@ static int RAMFUNCTION wolfBoot_swap_and_final_erase(int resume) wolfBoot_zeroize(tmpBuffer, sizeof(tmpBuffer)); return -1; } +#if !defined(ALLOW_DOWNGRADE) && !defined(DELTA_UPDATES) && \ + !defined(EXT_ENCRYPTED) + /* A downgrade would look the same, a delta update keeps the patch in + * UPDATE and encrypted headers need their IV mapping: those keep TESTING */ + if (resume == 1) + rollback = wolfBoot_swap_was_rollback(1, tmpBootPos == 0); +#endif hal_flash_unlock(); #ifdef EXT_FLASH @@ -749,8 +844,10 @@ static int RAMFUNCTION wolfBoot_swap_and_final_erase(int resume) wb_flash_erase(boot, tmpBootPos, WOLFBOOT_SECTOR_SIZE); } - /* Mark boot partition as TESTING - this tells bootloader to fallback if update fails */ - wolfBoot_set_partition_state(PART_BOOT, IMG_STATE_TESTING); + /* Mark boot partition as TESTING - this tells bootloader to fallback if + * update fails. A rollback restores the confirmed image. */ + wolfBoot_set_partition_state(PART_BOOT, rollback ? IMG_STATE_SUCCESS : + IMG_STATE_TESTING); /* Erase the last sector(s) of update partition */ /* This resets the update partition state to IMG_STATE_NEW */ @@ -1080,7 +1177,7 @@ static int wolfBoot_delta_update(struct wolfBoot_image *boot, /* start re-entrant final erase, return code is only for resumption in * wolfBoot_start */ if (ret == 0) { - wolfBoot_swap_and_final_erase(0); + wolfBoot_swap_and_final_erase(0, 0); } #endif /* encryption key was not erased, will be erased by success */ @@ -1478,7 +1575,9 @@ static int RAMFUNCTION wolfBoot_update(int fallback_allowed) if (update_open < 0) { /* A power cut mid-swap may have erased the UPDATE header, try to * resume using the copy in SWAP */ + trailer_read_unlock(); update.fw_size = wolfBoot_swap_update_size(fallback_allowed); + trailer_read_lock(); if (update.fw_size == 0) { #ifdef EXT_ENCRYPTED wolfBoot_enable_fallback_iv(0); @@ -1543,12 +1642,17 @@ static int RAMFUNCTION wolfBoot_update(int fallback_allowed) cur_ver = wolfBoot_current_firmware_version(); upd_ver = wolfBoot_update_firmware_version(); + trailer_read_unlock(); #ifndef DISABLE_BACKUP bootStateRet = wolfBoot_get_partition_state(PART_BOOT, &bootState); +#if defined(EXT_ENCRYPTED) || defined(CUSTOM_PARTITION_TRAILER) + /* Compared before the swap moves the headers: a custom trailer records + * the result, and encrypted headers are not re-read after the swap */ if ((bootStateRet == 0) && (bootState == IMG_STATE_TESTING) && (fallback_allowed != 0) && (cur_ver >= upd_ver)) { rollback_needed = 1; } +#endif #endif wolfBoot_get_update_sector_flag(0, &flag); @@ -1559,6 +1663,7 @@ static int RAMFUNCTION wolfBoot_update(int fallback_allowed) &updateState) == 0) && (updateState == IMG_STATE_ROLLBACK); } #endif + trailer_read_lock(); /* Check the first sector to detect interrupted update */ if (flag == SECT_FLAG_NEW) { if (((update_type & HDR_IMG_TYPE_PART_MASK) != HDR_IMG_TYPE_APP) || @@ -1813,26 +1918,27 @@ static int RAMFUNCTION wolfBoot_update(int fallback_allowed) hal_flash_lock(); #if !defined(CUSTOM_PARTITION_TRAILER) +#ifndef EXT_ENCRYPTED + /* Which image is older is only certain once the swap is done, also when + * it resumed after a power cut */ + if ((bootStateRet == 0) && (bootState == IMG_STATE_TESTING) && + (fallback_allowed != 0) && wolfBoot_swap_was_rollback(0, 0)) { + rollback_needed = 1; +#ifdef WOLFBOOT_PERSIST_FAILURE_STATUS + /* The image that failed to confirm is in UPDATE now */ + cur_ver = wolfBoot_update_firmware_version(); +#endif + } +#endif /* start re-entrant final erase, return code is only for resumption in * wolfBoot_start */ - ret = wolfBoot_swap_and_final_erase(0); + ret = wolfBoot_swap_and_final_erase(0, rollback_needed); if (ret != 0) return ret; -#ifndef DISABLE_BACKUP - if (rollback_needed) { - hal_flash_unlock(); -#ifdef EXT_FLASH - ext_flash_unlock(); -#endif - wolfBoot_set_partition_state(PART_BOOT, IMG_STATE_SUCCESS); -#ifdef EXT_FLASH - ext_flash_lock(); -#endif - hal_flash_lock(); #ifdef WOLFBOOT_PERSIST_FAILURE_STATUS + if (rollback_needed) { wolfBoot_record_failure(WOLFBOOT_FAILURE_PHASE_ROLLBACK, WOLFBOOT_FAILURE_CAUSE_NOT_CONFIRMED, PART_BOOT, cur_ver); -#endif } #endif #else @@ -2155,7 +2261,14 @@ void RAMFUNCTION wolfBoot_start(void) #if !defined(DISABLE_BACKUP) && !defined(CUSTOM_PARTITION_TRAILER) /* resume the final erase in case the power failed before it finished */ - resumedFinalErase = wolfBoot_swap_and_final_erase(1); + resumedFinalErase = wolfBoot_swap_and_final_erase(1, 0); + /* A resumed rollback leaves BOOT confirmed instead of TESTING, also when + * only its last cleanup erase failed */ + if ((bootRet == 0) && (bootState == IMG_STATE_TESTING)) { + trailer_read_unlock(); + bootRet = wolfBoot_get_partition_state(PART_BOOT, &bootState); + trailer_read_lock(); + } if ((resumedFinalErase != 0) || ((bootRet == 0) && (bootState == IMG_STATE_TESTING))) #elif !defined(DISABLE_BACKUP) diff --git a/tools/scripts/sim-update-powerfail-resume.sh b/tools/scripts/sim-update-powerfail-resume.sh index 72db4f1d73..a3a9a9cd06 100755 --- a/tools/scripts/sim-update-powerfail-resume.sh +++ b/tools/scripts/sim-update-powerfail-resume.sh @@ -47,26 +47,38 @@ if [ "x$V" != "x1" ]; then fi fi -# make sure it can double revert -./wolfboot.elf powerfail 0 get_version 2>/dev/null -./wolfboot.elf powerfail 15000 get_version 2>/dev/null -./wolfboot.elf powerfail 18000 get_version 2>/dev/null -./wolfboot.elf powerfail 1a000 get_version 2>/dev/null -# fail on the last sector to stop the encrypt key save and state update -./wolfboot.elf powerfail 3e000 get_version 2>/dev/null -# may not trigger on non NVM_FLASH_WRITEONCE -V=`./wolfboot.elf powerfail 3f000 get_version` 2>/dev/null -if [ "x$V" != "x2" ]; then - V=`./wolfboot.elf get_version 2>/dev/null` - # if we failed on the final boot state write we need to double fallback - if [ "x$V" == "x1" ]; then +if grep -Eq '^(ENCRYPT|DELTA_UPDATES|ALLOW_DOWNGRADE)\??=1' .config; then + # These builds cannot tell a resumed fallback from an update, so a reset + # in it can leave v1 in TESTING: make sure it can double revert + ./wolfboot.elf powerfail 0 get_version 2>/dev/null + ./wolfboot.elf powerfail 15000 get_version 2>/dev/null + ./wolfboot.elf powerfail 18000 get_version 2>/dev/null + ./wolfboot.elf powerfail 1a000 get_version 2>/dev/null + # fail on the last sector to stop the encrypt key save and state update + ./wolfboot.elf powerfail 3e000 get_version 2>/dev/null + # may not trigger on non NVM_FLASH_WRITEONCE + V=`./wolfboot.elf powerfail 3f000 get_version` 2>/dev/null + if [ "x$V" != "x2" ]; then V=`./wolfboot.elf get_version 2>/dev/null` + # if we failed on the final boot state write we need to double fallback + if [ "x$V" == "x1" ]; then + V=`./wolfboot.elf get_version 2>/dev/null` + fi fi -fi -if [ "x$V" != "x2" ]; then - echo "Failed update (V: $V)" - exit 1 + if [ "x$V" != "x2" ]; then + echo "Failed update (V: $V)" + exit 1 + fi +else + # The fallback confirmed v1: the same resets find nothing to resume + for addr in 0 15000 18000 1a000 3e000 3f000; do + V=`./wolfboot.elf powerfail $addr get_version 2>/dev/null` + if [ "x$V" != "x1" ]; then + echo "Fallback did not stay on v1 (V: $V)" + exit 1 + fi + done fi echo Test successful. diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 7bae2327bd..14e7924704 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -49,6 +49,8 @@ CFLAGS+=-g -ggdb CFLAGS+=-fprofile-arcs CFLAGS+=-ftest-coverage CFLAGS+=--coverage +# The tests do not use threads: plain counters, even with -pthread +CFLAGS+=-fprofile-update=single CFLAGS+=-DUNIT_TEST_COVERAGE CFLAGS+=-DUNIT_TEST -DWOLFSSL_USER_SETTINGS # Pin the standard: GCC 14+ defaults to gnu23, where glibc string.h defines @@ -75,6 +77,7 @@ TESTS:=unit-parser unit-parser-large-header unit-fdt unit-extflash unit-string \ unit-enc-nvm-flagshome unit-delta unit-gzip unit-update-flash unit-update-flash-delta \ unit-update-flash-db \ unit-update-flash-hook \ + unit-update-flash-int-invert \ unit-update-flash-self-update \ unit-update-flash-self-update-ecc \ unit-update-flash-self-update-monolithic \ @@ -404,6 +407,11 @@ unit-nsc-update:CFLAGS+=-DMOCK_PARTITIONS -D__WOLFBOOT -DTZEN \ -DWOLFBOOT_NO_SIGN -DUNIT_TEST_AUTH \ -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED \ -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE +# Write-once trailers with inverted flags and every partition in internal flash +unit-update-flash-int-invert:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN -DUNIT_TEST_AUTH \ + -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED \ + -DNVM_FLASH_WRITEONCE -DWOLFBOOT_FLAGS_INVERT=1 -DFLASHBUFFER_SIZE=512 \ + -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE unit-update-flash-hook:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN -DUNIT_TEST_AUTH \ -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED -DEXT_FLASH -DPART_UPDATE_EXT -DPART_SWAP_EXT \ -DWOLFBOOT_HOOK_BOOT -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT \ @@ -1231,6 +1239,9 @@ unit-fit-fpga: ../../include/target.h unit-fit-fpga.c unit-update-flash: ../../include/target.h unit-update-flash.c gcc -o $@ unit-update-flash.c ../../src/image.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) +unit-update-flash-int-invert: ../../include/target.h unit-update-flash.c + gcc -o $@ unit-update-flash.c ../../src/image.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) + # DISABLE_BACKUP runtime variant: direct update->BOOT copy without the # power-fail-safe swap, to test the update-consumption behavior of that path. unit-update-flash-custom-trailer: ../../include/target.h unit-update-flash.c diff --git a/tools/unit-tests/unit-mock-flash.c b/tools/unit-tests/unit-mock-flash.c index 0af2e81eea..11707697f0 100644 --- a/tools/unit-tests/unit-mock-flash.c +++ b/tools/unit-tests/unit-mock-flash.c @@ -35,6 +35,56 @@ static int erased_nvm_bank0 = 0; static int erased_nvm_bank1 = 0; static int erased_vault = 0; static int hal_flash_write_fail = 0; + +#include +/* Power cut: flash operation number mock_powercut_at (from 0) longjmp()s to + * mock_powercut_jmp before it runs, or after only its first half, its second + * half, or all but its last word ran (mock_powercut_torn 1 to 3) */ +static int mock_powercut_at = -1; +static int mock_powercut_torn; +static int mock_powercut_pending; +static int mock_flash_ops; +/* Address and length of the operation the last cut hit */ +static uintptr_t mock_powercut_addr; +static int mock_powercut_size; +static jmp_buf mock_powercut_jmp; +static int hal_flash_write_fail_ret = -1; +static int hal_flash_erase_fail; +static int hal_flash_erase_fail_ret = -1; + +/* Returns how much of an operation of len bytes runs before the cut, from + * *skip bytes into it */ +static int mock_powercut_len(uintptr_t address, int len, int *skip) +{ + *skip = 0; + if ((mock_powercut_at >= 0) && (mock_flash_ops++ == mock_powercut_at)) { + mock_powercut_at = -1; + mock_powercut_addr = address; + mock_powercut_size = len; + if (!mock_powercut_torn) + longjmp(mock_powercut_jmp, 1); + mock_powercut_pending = 1; + switch (mock_powercut_torn) { + case 1: + return len / 2; + case 2: + *skip = len / 2; + break; + default: + return (len > 4) ? (len - 4) : 0; + } + return len - *skip; + } + return len; +} + +static void mock_powercut_finish(void) +{ + if (mock_powercut_pending) { + mock_powercut_pending = 0; + longjmp(mock_powercut_jmp, 1); + } +} const char *argv0; #ifdef NVM_FLASH_ECC @@ -198,11 +248,18 @@ void hal_init(void) int hal_flash_write(haladdr_t address, const uint8_t *data, int len) { int i; + int skip; uint8_t *a = (uint8_t *)(uintptr_t)address; ck_assert_msg(!locked, "Attempting to write to a locked FLASH"); - if (hal_flash_write_fail) { - hal_flash_write_fail = 0; - return -1; + len = mock_powercut_len((uintptr_t)address, len, &skip); + address += skip; + data += skip; + a += skip; + /* hal_flash_write_fail = n fails the n-th write from now */ + if ((hal_flash_write_fail > 0) && (--hal_flash_write_fail == 0)) { + i = hal_flash_write_fail_ret; + hal_flash_write_fail_ret = -1; + return i; } if ((address >= WOLFBOOT_PARTITION_SWAP_ADDRESS) && (address < WOLFBOOT_PARTITION_SWAP_ADDRESS + WOLFBOOT_SECTOR_SIZE)) { @@ -249,11 +306,28 @@ int hal_flash_write(haladdr_t address, const uint8_t *data, int len) } } #endif + mock_powercut_finish(); return 0; } +/* Internal flash erases to FLASH_BYTE_ERASED, external flash to 0xFF */ +#ifdef PART_UPDATE_EXT +#define MOCK_UPDATE_ERASED 0xFF +#else +#define MOCK_UPDATE_ERASED FLASH_BYTE_ERASED +#endif int hal_flash_erase(haladdr_t address, int len) { + int skip; + int ret; ck_assert_msg(!locked, "Attempting to erase a locked FLASH"); + len = mock_powercut_len((uintptr_t)address, len, &skip); + address += skip; + /* hal_flash_erase_fail = n fails the n-th erase from now */ + if ((hal_flash_erase_fail > 0) && (--hal_flash_erase_fail == 0)) { + ret = hal_flash_erase_fail_ret; + hal_flash_erase_fail_ret = -1; + return ret; + } if ((address >= WOLFBOOT_PARTITION_BOOT_ADDRESS) && (address < WOLFBOOT_PARTITION_BOOT_ADDRESS + WOLFBOOT_PARTITION_SIZE)) { #ifdef NVM_FLASH_ECC @@ -261,7 +335,7 @@ int hal_flash_erase(haladdr_t address, int len) return -1; #endif erased_boot++; - memset((void*)(uintptr_t)address, 0xFF, len); + memset((void*)(uintptr_t)address, FLASH_BYTE_ERASED, len); if (address >= WOLFBOOT_PARTITION_BOOT_ADDRESS + WOLFBOOT_PARTITION_SIZE - WOLFBOOT_SECTOR_SIZE) { erased_nvm_bank0++; } else if (address >= WOLFBOOT_PARTITION_BOOT_ADDRESS + WOLFBOOT_PARTITION_SIZE - 2 * WOLFBOOT_SECTOR_SIZE) { @@ -270,7 +344,7 @@ int hal_flash_erase(haladdr_t address, int len) } else if ((address >= WOLFBOOT_PARTITION_UPDATE_ADDRESS) && (address < WOLFBOOT_PARTITION_UPDATE_ADDRESS + WOLFBOOT_PARTITION_SIZE)) { erased_update++; - memset((void *)(uintptr_t)address, 0xFF, len); + memset((void *)(uintptr_t)address, MOCK_UPDATE_ERASED, len); if (address >= WOLFBOOT_PARTITION_UPDATE_ADDRESS + WOLFBOOT_PARTITION_SIZE - WOLFBOOT_SECTOR_SIZE) { erased_nvm_bank0++; } else if (address >= WOLFBOOT_PARTITION_UPDATE_ADDRESS + WOLFBOOT_PARTITION_SIZE - 2 * WOLFBOOT_SECTOR_SIZE) { @@ -279,7 +353,7 @@ int hal_flash_erase(haladdr_t address, int len) } else if ((address >= WOLFBOOT_PARTITION_SWAP_ADDRESS) && (address < WOLFBOOT_PARTITION_SWAP_ADDRESS + WOLFBOOT_SECTOR_SIZE)) { erased_swap++; - memset((void *)(uintptr_t)address, 0xFF, len); + memset((void *)(uintptr_t)address, FLASH_BYTE_ERASED, len); #ifdef MOCK_KEYVAULT } else if ((address >= (uintptr_t)vault_base) && (address < (uintptr_t)vault_base + keyvault_size)) { int n = vault_flash_op_len(len); @@ -304,6 +378,7 @@ int hal_flash_erase(haladdr_t address, int len) fail("Invalid address\n"); return -1; } + mock_powercut_finish(); return 0; } void hal_flash_unlock(void) @@ -337,6 +412,9 @@ void hal_prepare_boot(void) int ext_flash_erase(uintptr_t address, int len) { + int skip; + len = mock_powercut_len((uintptr_t)address, len, &skip); + address += skip; #ifdef PART_BOOT_EXT if ((address >= WOLFBOOT_PARTITION_BOOT_ADDRESS) && (address < WOLFBOOT_PARTITION_BOOT_ADDRESS + WOLFBOOT_PARTITION_SIZE)) { @@ -366,15 +444,21 @@ int ext_flash_erase(uintptr_t address, int len) fail("Invalid address: %p\n", address); return -1; } + mock_powercut_finish(); return 0; } int ext_flash_write(uintptr_t address, const uint8_t *data, int len) { int i; + int skip; uint8_t *a = (uint8_t *)address; ck_assert_msg(!ext_locked, "Attempting to write to a locked FLASH"); ck_assert_msg(len >= 0, "ext_flash_write invalid len %d", len); + len = mock_powercut_len((uintptr_t)address, len, &skip); + address += skip; + data += skip; + a += skip; if (ext_flash_write_fail) { ext_flash_write_fail = 0; return -1; @@ -397,6 +481,7 @@ int ext_flash_write(uintptr_t address, const uint8_t *data, int len) for (i = 0; i < len; i++) { a[i] = data[i]; } + mock_powercut_finish(); return 0; } @@ -405,6 +490,8 @@ int ext_flash_write(uintptr_t address, const uint8_t *data, int len) * short positive read, F-12065). Other lengths read in full. */ int mock_ext_flash_short_len = 0; int mock_ext_flash_short_bytes = 0; +/* When not 0, only reads from this address are short */ +uintptr_t mock_ext_flash_short_addr = 0; /* Records the source address of the largest ext_flash_read() call (the * image load to RAM), so a test can verify which partition was booted. */ @@ -417,7 +504,9 @@ int ext_flash_read(uintptr_t address, uint8_t *data, int len) int ret = len; uint8_t *a = (uint8_t *)address; - if (mock_ext_flash_short_len == len && mock_ext_flash_short_bytes > 0) + if (mock_ext_flash_short_len == len && mock_ext_flash_short_bytes > 0 && + (mock_ext_flash_short_addr == 0 || + mock_ext_flash_short_addr == address)) ret = len - mock_ext_flash_short_bytes; if (ret > mock_max_read_len) { mock_max_read_len = ret; @@ -451,10 +540,12 @@ static int mmap_file(const char *path, uint8_t *address, uint32_t len, uint8_t** ret_address) { struct stat st = { 0 }; + uint32_t fill[256]; uint8_t *mmaped_addr; int ret; int fd; int i; + int chunk = 0; if (path == NULL) return -1; @@ -465,9 +556,16 @@ static int mmap_file(const char *path, uint8_t *address, uint32_t len, return -1; } fprintf(stderr, "Open file: %s success.\n", path); - for (i = 0; i < len; i+=4) { - const uint32_t erased_word = 0xBADBADBA; - write(fd, &erased_word, 4); + for (i = 0; i < (int)(sizeof(fill) / sizeof(fill[0])); i++) + fill[i] = 0xBADBADBA; + /* Whole words, in chunks: the power-cut sweeps map the files thousands + * of times */ + for (i = 0; i < (int)len; i += chunk) { + chunk = (int)sizeof(fill); + if ((int)len - i < chunk) + chunk = (((int)len - i) + 3) & ~3; + if (write(fd, fill, chunk) != chunk) + break; } lseek(fd, SEEK_SET, 0); diff --git a/tools/unit-tests/unit-update-flash.c b/tools/unit-tests/unit-update-flash.c index a962ecbdaf..358b534b8f 100644 --- a/tools/unit-tests/unit-update-flash.c +++ b/tools/unit-tests/unit-update-flash.c @@ -140,11 +140,43 @@ static uint16_t host_to_img_u16(uint16_t val) * encryption-aware writer: in EXT_ENCRYPTED builds a raw write would leave * plaintext in flash that the bootloader then decrypts. The swap partition * is written raw, since its content is already encrypted when staged. */ +#ifndef EXT_FLASH +/* UPDATE in internal flash: the mock's ext_flash_* calls write the same memory */ +static int ext_flash_check_write(uintptr_t addr, const uint8_t *data, int len) +{ + return ext_flash_write(addr, data, len); +} + +static int ext_flash_check_read(uintptr_t addr, uint8_t *data, int len) +{ + return ext_flash_read(addr, data, len); +} +#endif + static int update_part_write(uintptr_t addr, const void *buf, int len) { return ext_flash_check_write(addr, buf, len); } +/* The UPDATE flags sit in the UPDATE partition, internal or external */ +static void update_flags_unlock(void) +{ +#ifndef EXT_FLASH + hal_flash_unlock(); +#else + ext_flash_unlock(); +#endif +} + +static void update_flags_lock(void) +{ +#ifndef EXT_FLASH + hal_flash_lock(); +#else + ext_flash_lock(); +#endif +} + static void ext_flash_write_le16(uintptr_t addr, uint16_t val) { uint8_t le[2]; @@ -435,6 +467,7 @@ static int add_payload_type(uint8_t part, uint32_t version, uint32_t size, int ret; wc_Sha256 sha; uint8_t digest[SHA256_DIGEST_SIZE]; + uint8_t hdr_pad[IMAGE_HEADER_SIZE]; ret = wc_InitSha256_ex(&sha, NULL, INVALID_DEVID); if (ret != 0) @@ -447,6 +480,9 @@ static int add_payload_type(uint8_t part, uint32_t version, uint32_t size, hal_flash_unlock(); + /* The sign tool pads the header with 0xFF, whatever flash erases to */ + memset(hdr_pad, 0xFF, sizeof(hdr_pad)); + hal_flash_write((uintptr_t)base, hdr_pad, sizeof(hdr_pad)); hal_flash_write((uintptr_t)base, (void *)&magic, 4); printf("Written magic: 0x%08X\n", magic); @@ -516,9 +552,9 @@ START_TEST (test_self_update_sameversion_erased) clear_erase_stats(); add_payload_type(PART_UPDATE, WOLFBOOT_VERSION, TEST_SIZE_SMALL, HDR_IMG_TYPE_WOLFBOOT | HDR_IMG_TYPE_AUTH); - ext_flash_unlock(); + update_flags_unlock(); wolfBoot_set_partition_state(PART_UPDATE, IMG_STATE_UPDATING); - ext_flash_lock(); + update_flags_lock(); wolfBoot_check_self_update(); @@ -537,9 +573,9 @@ START_TEST (test_self_update_oldversion_erased) clear_erase_stats(); add_payload_type(PART_UPDATE, WOLFBOOT_VERSION - 1, TEST_SIZE_SMALL, HDR_IMG_TYPE_WOLFBOOT | HDR_IMG_TYPE_AUTH); - ext_flash_unlock(); + update_flags_unlock(); wolfBoot_set_partition_state(PART_UPDATE, IMG_STATE_UPDATING); - ext_flash_lock(); + update_flags_lock(); wolfBoot_check_self_update(); @@ -584,9 +620,9 @@ START_TEST (test_self_update_newversion_copies_and_reboots) clear_erase_stats(); add_payload_type(PART_UPDATE, WOLFBOOT_VERSION + 1, TEST_SIZE_SMALL, HDR_IMG_TYPE_WOLFBOOT | HDR_IMG_TYPE_AUTH); - ext_flash_unlock(); + update_flags_unlock(); wolfBoot_set_partition_state(PART_UPDATE, IMG_STATE_UPDATING); - ext_flash_lock(); + update_flags_lock(); wolfBoot_check_self_update(); @@ -1090,7 +1126,7 @@ START_TEST (test_final_swap_propagates_encrypt_key_persist_failure) hal_flash_lock(); mock_set_encrypt_key_ret = -5; - ret = wolfBoot_swap_and_final_erase(1); + ret = wolfBoot_swap_and_final_erase(1, 0); ck_assert_int_eq(ret, -5); ck_assert_int_eq(mock_set_encrypt_key_calls, 1); @@ -1110,7 +1146,7 @@ START_TEST (test_final_swap_propagates_encrypt_key_read_failure) add_payload(PART_UPDATE, 2, TEST_SIZE_SMALL); mock_get_encrypt_key_ret = -7; - ret = wolfBoot_swap_and_final_erase(0); + ret = wolfBoot_swap_and_final_erase(0, 0); ck_assert_int_eq(ret, -7); ck_assert_int_eq(mock_set_encrypt_key_calls, 0); @@ -1144,11 +1180,16 @@ START_TEST (test_final_swap_aborts_on_backup_copy_failure) add_payload(PART_BOOT, 1, 30700); +#ifdef PART_SWAP_EXT ext_flash_write_fail = 1; - ret = wolfBoot_swap_and_final_erase(0); +#else + hal_flash_write_fail = 1; +#endif + ret = wolfBoot_swap_and_final_erase(0, 0); ck_assert_int_lt(ret, 0); ck_assert_int_eq(ext_flash_write_fail, 0); + ck_assert_int_eq(hal_flash_write_fail, 0); /* The update state must be unset: a failed backup must not leave the * update partition marked FINAL_FLAGS. */ ck_assert_int_ne(wolfBoot_get_partition_state(PART_UPDATE, &update_state), @@ -1176,12 +1217,12 @@ START_TEST (test_final_swap_aborts_on_restore_copy_failure) add_payload(PART_BOOT, 1, 30700); - ext_flash_unlock(); + update_flags_unlock(); wolfBoot_set_partition_state(PART_UPDATE, IMG_STATE_FINAL_FLAGS); - ext_flash_lock(); + update_flags_lock(); hal_flash_write_fail = 1; - ret = wolfBoot_swap_and_final_erase(1); + ret = wolfBoot_swap_and_final_erase(1, 0); ck_assert_int_lt(ret, 0); ck_assert_int_eq(hal_flash_write_fail, 0); @@ -1197,6 +1238,105 @@ START_TEST (test_final_swap_aborts_on_restore_copy_failure) cleanup_flash(); } END_TEST + +#if !defined(DELTA_UPDATES) && !defined(ALLOW_DOWNGRADE) +/* The rollback check with BOOT sector 0 erased, or torn after the magic and + * size, as a cut while a resumed final erase restores it leaves it. With + * from_swap the BOOT version comes from the SWAP backup of that sector. */ +static int rollback_check(uint32_t boot_ver, uint32_t update_ver, + int swap_valid, int short_read, int torn_header, int from_swap) +{ + static uint8_t hdr_sector[WOLFBOOT_SECTOR_SIZE]; + struct wolfBoot_image swap; + int ret; + + reset_mock_stats(); + prepare_flash(); + add_payload(PART_BOOT, boot_ver, TEST_SIZE_SMALL); + add_payload(PART_UPDATE, update_ver, TEST_SIZE_SMALL); + memcpy(hdr_sector, (const void *)(uintptr_t)WOLFBOOT_PARTITION_BOOT_ADDRESS, + sizeof(hdr_sector)); + wolfBoot_open_image(&swap, PART_SWAP); + hal_flash_unlock(); + ext_flash_unlock(); + wb_flash_erase(&swap, 0, WOLFBOOT_SECTOR_SIZE); + if (swap_valid) + wb_flash_write(&swap, 0, hdr_sector, WOLFBOOT_SECTOR_SIZE); + hal_flash_erase(WOLFBOOT_PARTITION_BOOT_ADDRESS, WOLFBOOT_SECTOR_SIZE); + if (torn_header) + hal_flash_write(WOLFBOOT_PARTITION_BOOT_ADDRESS, hdr_sector, 8); + ext_flash_lock(); + hal_flash_lock(); +#ifdef PART_SWAP_EXT + if (short_read) { + /* SWAP only: UPDATE is read whole to verify it first */ + mock_ext_flash_short_len = IMAGE_HEADER_SIZE; + mock_ext_flash_short_bytes = 4; + mock_ext_flash_short_addr = (uintptr_t)swap.hdr; + } +#else + (void)short_read; +#endif + ret = wolfBoot_swap_was_rollback(1, from_swap); + mock_ext_flash_short_len = 0; + mock_ext_flash_short_bytes = 0; + mock_ext_flash_short_addr = 0; + cleanup_flash(); + return ret; +} + +START_TEST (test_swap_was_rollback_without_boot_header) +{ + /* from the backup: an update, a rollback, a rollback to version 0 */ + ck_assert_int_eq(rollback_check(2, 1, 1, 0, 0, 1), 0); + ck_assert_int_eq(rollback_check(1, 2, 1, 0, 0, 1), 1); + ck_assert_int_eq(rollback_check(0, 1, 1, 0, 0, 1), 1); + ck_assert_int_eq(rollback_check(1, 2, 1, 0, 1, 1), 1); + /* unknown: no backup, an erased BOOT header, a short read */ + ck_assert_int_eq(rollback_check(1, 2, 0, 0, 0, 1), 0); + ck_assert_int_eq(rollback_check(1, 2, 1, 0, 0, 0), 0); +#ifdef PART_SWAP_EXT + ck_assert_int_eq(rollback_check(1, 2, 1, 1, 0, 1), 0); +#endif +} +END_TEST + +/* The direction check from intact headers, with one UPDATE byte changed in + * the header or the payload when tamper_at is not 0 */ +static int rollback_check_intact(uint32_t boot_ver, uint32_t update_ver, + uint32_t tamper_at, int resume) +{ + int ret; + + reset_mock_stats(); + prepare_flash(); + add_payload(PART_BOOT, boot_ver, TEST_SIZE_SMALL); + add_payload(PART_UPDATE, update_ver, TEST_SIZE_SMALL); + if (tamper_at != 0) + *(volatile uint8_t *)(uintptr_t)(MOCK_ADDRESS_UPDATE + tamper_at) ^= + 0x5A; + ret = wolfBoot_swap_was_rollback(resume, 0); + cleanup_flash(); + return ret; +} + +/* UPDATE may sit in external flash: only an authentic image newer than BOOT + * makes a resume finish as a rollback and confirm BOOT */ +START_TEST (test_swap_was_rollback_needs_authentic_newer_update) +{ + ck_assert_int_eq(rollback_check_intact(1, 2, 0, 1), 1); + ck_assert_int_eq(rollback_check_intact(2, 1, 0, 1), 0); + /* a copy of the installed image */ + ck_assert_int_eq(rollback_check_intact(2, 2, 0, 1), 0); + /* a changed header, as for a forged version, and a changed payload */ + ck_assert_int_eq(rollback_check_intact(1, 2, 16, 1), 0); + ck_assert_int_eq(rollback_check_intact(1, 2, IMAGE_HEADER_SIZE + 16, 1), + 0); + /* a fallback, decided from the BOOT state, keeps equal versions */ + ck_assert_int_eq(rollback_check_intact(2, 2, 0, 0), 1); +} +END_TEST +#endif #endif /* !DISABLE_BACKUP && !CUSTOM_PARTITION_TRAILER && !EXT_ENCRYPTED */ /* A completed DISABLE_BACKUP update must consume the update partition: @@ -1539,16 +1679,16 @@ START_TEST(test_rollback_resume_from_swapping_flag) resume_setup(); wolfBoot_start(); ck_assert_uint_eq(wolfBoot_current_firmware_version(), 2); - ext_flash_unlock(); #ifdef CUSTOM_PARTITION_TRAILER /* written by the rollback before its first sector */ hal_flash_unlock(); wolfBoot_set_partition_state(PART_UPDATE, IMG_STATE_ROLLBACK); hal_flash_lock(); #else + update_flags_unlock(); wolfBoot_set_partition_state(PART_UPDATE, IMG_STATE_UPDATING); + update_flags_lock(); #endif - ext_flash_lock(); cut_after_sector0_backup_erase(); wolfBoot_start(); ck_assert(!wolfBoot_panicked); @@ -1633,7 +1773,7 @@ static void swap_copy_set_u32(uint32_t off, uint32_t val) wolfBoot_open_image(&swap, PART_SWAP); hal_flash_unlock(); ext_flash_unlock(); - wb_flash_write(&swap, off, &word, sizeof(word)); + wb_flash_write(&swap, off, (uint8_t *)&word, sizeof(word)); ext_flash_lock(); hal_flash_lock(); } @@ -1758,6 +1898,408 @@ START_TEST (test_update_then_rollback_smaller) roundtrip_run(TEST_SIZE_SMALL / 2); } END_TEST + +#if !defined(DISABLE_BACKUP) && !defined(DELTA_UPDATES) && \ + !defined(EXT_ENCRYPTED) && !defined(CUSTOM_PARTITION_TRAILER) +/* A power cycle loses RAM state and keeps flash */ +static void mock_power_cycle(void) +{ + locked = 1; + ext_flash_reset_lock(); +#ifdef NVM_FLASH_WRITEONCE + nvm_cached_sector = 0; +#endif + wolfBoot_panicked = 0; + wolfBoot_staged_ok = 0; +} + +/* Power-cut runs start from a v1 with no trailer, or from a confirmed v1 */ +static int powercut_confirmed; + +/* Size of the original v1 image: the largest update starts from a largest + * original, so a rollback also restores image data in the staging sector */ +static uint32_t powercut_boot_size(uint32_t update_size) +{ + return (update_size == MAX_UPDATE_SIZE) ? MAX_UPDATE_SIZE : TEST_SIZE_SMALL; +} + +/* Each start state is built and hashed once, then copied in for every cut */ +static struct { + uint32_t update_size; + int confirmed; + uint8_t boot[WOLFBOOT_PARTITION_SIZE]; + uint8_t update[WOLFBOOT_PARTITION_SIZE]; +} powercut_cache[8]; +static int powercut_cached; + +static void powercut_setup(uint32_t update_size) +{ + int i; + + reset_mock_stats(); + prepare_flash(); + for (i = 0; i < powercut_cached; i++) { + if ((powercut_cache[i].update_size == update_size) && + (powercut_cache[i].confirmed == powercut_confirmed)) + break; + } + if (i < powercut_cached) { + memcpy((void *)(uintptr_t)MOCK_ADDRESS_BOOT, powercut_cache[i].boot, + WOLFBOOT_PARTITION_SIZE); + memcpy((void *)(uintptr_t)MOCK_ADDRESS_UPDATE, + powercut_cache[i].update, WOLFBOOT_PARTITION_SIZE); + } + else { + add_payload(PART_BOOT, 1, powercut_boot_size(update_size)); + add_payload(PART_UPDATE, 2, update_size); + if (powercut_confirmed) { + hal_flash_unlock(); + wolfBoot_set_partition_state(PART_BOOT, IMG_STATE_SUCCESS); + hal_flash_lock(); + } + if (powercut_cached < (int)(sizeof(powercut_cache) / + sizeof(powercut_cache[0]))) { + powercut_cache[i].update_size = update_size; + powercut_cache[i].confirmed = powercut_confirmed; + memcpy(powercut_cache[i].boot, + (const void *)(uintptr_t)MOCK_ADDRESS_BOOT, + WOLFBOOT_PARTITION_SIZE); + memcpy(powercut_cache[i].update, + (const void *)(uintptr_t)MOCK_ADDRESS_UPDATE, + WOLFBOOT_PARTITION_SIZE); + powercut_cached++; + } + } + mock_power_cycle(); +} + +static void boot_after_cut(int n, const char *step) +{ + mock_power_cycle(); + wolfBoot_start(); + ck_assert_msg(!wolfBoot_panicked && wolfBoot_staged_ok, + "no image boots after a power cut at flash op %d (%s)", n, step); +} + +/* Cut power before flash operation n of an update, then boot twice. The + * unconfirmed update must roll back to the original image, byte for byte. + * Returns 0 once n is past the last flash operation of the update. */ +static int powercut_update_run(uint32_t update_size, int n) +{ + uint32_t total_size = powercut_boot_size(update_size) + IMAGE_HEADER_SIZE; + int cut; + + powercut_setup(update_size); + memcpy(roundtrip_boot_snap, + (const void *)(uintptr_t)WOLFBOOT_PARTITION_BOOT_ADDRESS, total_size); + wolfBoot_update_trigger(); + mock_flash_ops = 0; + mock_powercut_at = n; + cut = setjmp(mock_powercut_jmp); + if (cut == 0) { + wolfBoot_start(); + mock_powercut_at = -1; + } + else { + boot_after_cut(n, "update"); + boot_after_cut(n, "rollback"); + ck_assert_msg(wolfBoot_current_firmware_version() == 1, + "update of %u bytes cut at flash op %d (torn mode %d, v1 %s): " + "version %u after rollback", update_size, n, mock_powercut_torn, + powercut_confirmed ? "confirmed" : "unconfirmed", + wolfBoot_current_firmware_version()); + ck_assert_int_eq(memcmp((const void *)(uintptr_t) + WOLFBOOT_PARTITION_BOOT_ADDRESS, roundtrip_boot_snap, + total_size), 0); + } + cleanup_flash(); + return cut; +} + +/* Install the update, then cut power during flash operation n of + * wolfBoot_success(). The next boot keeps the update if the confirmation + * landed and rolls back otherwise, and the boot after that agrees. */ +static int powercut_confirm_run(uint32_t update_size, int n) +{ + uint8_t st = 0; + uint32_t ver; + int committed; + int cut; + + powercut_setup(update_size); + wolfBoot_update_trigger(); + wolfBoot_start(); + ck_assert_uint_eq(wolfBoot_current_firmware_version(), 2); + mock_flash_ops = 0; + mock_powercut_at = n; + cut = setjmp(mock_powercut_jmp); + if (cut == 0) { + wolfBoot_success(); + mock_powercut_at = -1; + ck_assert_int_eq(wolfBoot_get_partition_state(PART_BOOT, &st), 0); + ck_assert_uint_eq(st, IMG_STATE_SUCCESS); + } + else { + /* The state the next boot reads: only a committed SUCCESS keeps v2 */ + mock_power_cycle(); + hal_flash_unlock(); + committed = (wolfBoot_get_partition_state(PART_BOOT, &st) == 0) && + (st == IMG_STATE_SUCCESS); + hal_flash_lock(); + boot_after_cut(n, "confirm"); + ver = wolfBoot_current_firmware_version(); + ck_assert_msg(ver == (committed ? 2U : 1U), + "update of %u bytes, confirm cut at flash op %d: version %u with " + "the confirmation %s", update_size, n, ver, + committed ? "committed" : "not committed"); + boot_after_cut(n, "confirm reboot"); + ck_assert_msg(wolfBoot_current_firmware_version() == ver, + "update of %u bytes, confirm cut at flash op %d: version %u, " + "then %u", update_size, n, ver, + wolfBoot_current_firmware_version()); + } + cleanup_flash(); + return cut; +} + +/* True when the last cut hit the trailer sectors of BOOT or UPDATE */ +static int powercut_in_trailer(void) +{ + const uintptr_t base[2] = { WOLFBOOT_PARTITION_BOOT_ADDRESS, + WOLFBOOT_PARTITION_UPDATE_ADDRESS }; + uintptr_t start; + int i; + + for (i = 0; i < 2; i++) { + start = base[i] + WOLFBOOT_PARTITION_SIZE - TRAILER_LEN; + if ((mock_powercut_addr < start + TRAILER_LEN) && + (mock_powercut_addr + (uintptr_t)mock_powercut_size > start)) + return 1; + } + return 0; +} + +/* A cut in a trailer write or erase runs in all four torn modes. Data + * copies are redone on resume, so their torn mode turns with the cut point + * to keep the sweeps fast. */ +static int powercut_points(int (*run)(uint32_t, int), uint32_t update_size, + int offset) +{ + int n = 0; + int more; + int k; + + do { + mock_powercut_torn = (n + offset) % 4; + more = run(update_size, n); + if (more && powercut_in_trailer()) { + for (k = 1; k < 4; k++) { + mock_powercut_torn = (n + offset + k) % 4; + ck_assert_int_ne(run(update_size, n), 0); + } + } + n++; + } while (more); + mock_powercut_torn = 0; + return n - 1; +} + +/* Sizes spanning an even and an odd number of sectors, and the largest + * update, which overlaps the staging sector */ +static const uint32_t powercut_sizes[] = { + TEST_SIZE_LARGE, TEST_SIZE_LARGE + WOLFBOOT_SECTOR_SIZE, MAX_UPDATE_SIZE +}; + +/* Every size, from an unconfirmed and a confirmed v1, with the cut operation + * not started or only partly done */ +static void powercut_all(int (*run)(uint32_t, int)) +{ + unsigned int i; + + for (powercut_confirmed = 0; powercut_confirmed < 2; powercut_confirmed++) { + for (i = 0; i < sizeof(powercut_sizes) / sizeof(powercut_sizes[0]); + i++) { + ck_assert_int_gt(powercut_points(run, powercut_sizes[i], + (int)i + powercut_confirmed), 0); + } + } + powercut_confirmed = 0; +} + +/* The sweeps boot thousands of times: keep their wolfBoot_printf log, which + * goes to stderr, out of the output. Failures are still reported by check. */ +static int powercut_stderr = -1; + +static void powercut_quiet(void) +{ + int null_fd = open("/dev/null", O_WRONLY); + + fflush(stderr); + powercut_stderr = dup(STDERR_FILENO); + if (null_fd >= 0) { + dup2(null_fd, STDERR_FILENO); + close(null_fd); + } + /* unbuffered, every line would still cost a write */ + setvbuf(stderr, NULL, _IOFBF, 1 << 16); +} + +static void powercut_loud(void) +{ + fflush(stderr); + setvbuf(stderr, NULL, _IONBF, 0); + if (powercut_stderr >= 0) { + dup2(powercut_stderr, STDERR_FILENO); + close(powercut_stderr); + powercut_stderr = -1; + } +} + +START_TEST (test_update_power_cut_rolls_back) +{ + powercut_all(powercut_update_run); +} +END_TEST + +START_TEST (test_confirm_power_cut_keeps_one_image) +{ + powercut_all(powercut_confirm_run); +} +END_TEST + +/* Install the update, leave it unconfirmed, then cut power before flash + * operation n of the rollback. Every later boot must stay on the original. */ +static int powercut_rollback_run(uint32_t update_size, int n) +{ + uint32_t total_size = powercut_boot_size(update_size) + IMAGE_HEADER_SIZE; + int boot; + int cut; + + powercut_setup(update_size); + memcpy(roundtrip_boot_snap, + (const void *)(uintptr_t)WOLFBOOT_PARTITION_BOOT_ADDRESS, total_size); + wolfBoot_update_trigger(); + wolfBoot_start(); + ck_assert_uint_eq(wolfBoot_current_firmware_version(), 2); + mock_power_cycle(); + mock_flash_ops = 0; + mock_powercut_at = n; + cut = setjmp(mock_powercut_jmp); + if (cut == 0) { + wolfBoot_start(); + mock_powercut_at = -1; + ck_assert_uint_eq(wolfBoot_current_firmware_version(), 1); + } + else { + for (boot = 0; boot < 3; boot++) { + boot_after_cut(n, "rollback"); + ck_assert_msg(wolfBoot_current_firmware_version() == 1, + "update of %u bytes, rollback cut at flash op %d (torn mode " + "%d, v1 %s): version %u on boot %d", update_size, n, + mock_powercut_torn, + powercut_confirmed ? "confirmed" : "unconfirmed", + wolfBoot_current_firmware_version(), boot + 1); + } + ck_assert_int_eq(memcmp((const void *)(uintptr_t) + WOLFBOOT_PARTITION_BOOT_ADDRESS, roundtrip_boot_snap, + total_size), 0); + } + cleanup_flash(); + return cut; +} + +/* A rollback to a version 0 image is confirmed, like any other */ +START_TEST (test_rollback_to_version_zero_stays) +{ + uint8_t st = 0; + int boot; + + reset_mock_stats(); + prepare_flash(); + add_payload(PART_BOOT, 0, TEST_SIZE_SMALL); + add_payload(PART_UPDATE, 1, TEST_SIZE_LARGE); + hal_flash_unlock(); + wolfBoot_set_partition_state(PART_BOOT, IMG_STATE_SUCCESS); + hal_flash_lock(); + wolfBoot_update_trigger(); + wolfBoot_start(); + ck_assert_uint_eq(wolfBoot_current_firmware_version(), 1); + for (boot = 0; boot < 3; boot++) { + boot_after_cut(0, "rollback to version 0"); + ck_assert_msg(wolfBoot_current_firmware_version() == 0, + "version %u on boot %d", wolfBoot_current_firmware_version(), + boot + 1); + } + hal_flash_unlock(); + ck_assert_int_eq(wolfBoot_get_partition_state(PART_BOOT, &st), 0); + hal_flash_lock(); + ck_assert_uint_eq(st, IMG_STATE_SUCCESS); + cleanup_flash(); +} +END_TEST + +START_TEST (test_rollback_power_cut_keeps_original) +{ + powercut_all(powercut_rollback_run); +} +END_TEST + +/* Install the update, leave it unconfirmed, then fail erase k of the rollback + * boot. The boots after it must all run the original image, byte for byte. + * Returns 0 once k is past the last erase of the rollback. */ +static int rollback_erase_fail_run(uint32_t update_size, int k) +{ + uint32_t total_size = powercut_boot_size(update_size) + IMAGE_HEADER_SIZE; + int boot; + int failed; + + powercut_setup(update_size); + memcpy(roundtrip_boot_snap, + (const void *)(uintptr_t)WOLFBOOT_PARTITION_BOOT_ADDRESS, total_size); + wolfBoot_update_trigger(); + wolfBoot_start(); + ck_assert_uint_eq(wolfBoot_current_firmware_version(), 2); + mock_power_cycle(); + hal_flash_erase_fail = k; + hal_flash_erase_fail_ret = -1; + wolfBoot_start(); + failed = (hal_flash_erase_fail == 0); + hal_flash_erase_fail = 0; + if (failed) { + for (boot = 0; boot < 3; boot++) { + boot_after_cut(k, "rollback erase failure"); + ck_assert_msg(wolfBoot_current_firmware_version() == 1, + "update of %u bytes, rollback erase %d failed (v1 %s): " + "version %u on boot %d", update_size, k, + powercut_confirmed ? "confirmed" : "unconfirmed", + wolfBoot_current_firmware_version(), boot + 1); + } + ck_assert_int_eq(memcmp((const void *)(uintptr_t) + WOLFBOOT_PARTITION_BOOT_ADDRESS, roundtrip_boot_snap, + total_size), 0); + } + cleanup_flash(); + return failed; +} + +START_TEST (test_rollback_erase_failure_keeps_original) +{ + unsigned int i; + int k; + + for (powercut_confirmed = 0; powercut_confirmed < 2; powercut_confirmed++) { + for (i = 0; i < sizeof(powercut_sizes) / sizeof(powercut_sizes[0]); + i++) { + for (k = 1; rollback_erase_fail_run(powercut_sizes[i], k); k++) + ; + ck_assert_int_gt(k, 1); + } + } + powercut_confirmed = 0; +} +END_TEST + +#endif #endif /* !DISABLE_BACKUP */ START_TEST (test_forward_update_tolarger) { @@ -2065,10 +2607,10 @@ START_TEST (test_swap_resume_noop) { reset_mock_stats(); prepare_flash(); - ext_flash_unlock(); + update_flags_unlock(); wolfBoot_set_partition_state(PART_UPDATE, IMG_STATE_NEW); - ext_flash_lock(); - ck_assert_int_eq(wolfBoot_swap_and_final_erase(1), -1); + update_flags_lock(); + ck_assert_int_eq(wolfBoot_swap_and_final_erase(1, 0), -1); cleanup_flash(); } END_TEST @@ -2601,6 +3143,10 @@ Suite *wolfboot_suite(void) TCase *zero_size_update = tcase_create("Zero size update"); TCase *invalid_sha = tcase_create("Invalid SHA digest"); TCase *emergency_rollback = tcase_create("Emergency rollback"); +#if !defined(DISABLE_BACKUP) && !defined(DELTA_UPDATES) && \ + !defined(EXT_ENCRYPTED) && !defined(CUSTOM_PARTITION_TRAILER) + TCase *power_cut = tcase_create("Power cut during update"); +#endif TCase *emergency_rollback_failure_due_to_bad_update = tcase_create("Emergency rollback failure due to bad update"); TCase *empty_boot_partition_update = tcase_create("Empty boot partition update"); TCase *empty_boot_but_update_sha_corrupted_denied = tcase_create("Empty boot partition but update SHA corrupted"); @@ -2660,6 +3206,12 @@ Suite *wolfboot_suite(void) !defined(EXT_ENCRYPTED) tcase_add_test(forward_update_samesize, test_final_swap_aborts_on_backup_copy_failure); tcase_add_test(forward_update_samesize, test_final_swap_aborts_on_restore_copy_failure); +#if !defined(DELTA_UPDATES) && !defined(ALLOW_DOWNGRADE) + tcase_add_test(forward_update_samesize, + test_swap_was_rollback_without_boot_header); + tcase_add_test(forward_update_samesize, + test_swap_was_rollback_needs_authentic_newer_update); +#endif #endif #if defined(DISABLE_BACKUP) && !defined(CUSTOM_PARTITION_TRAILER) tcase_add_test(forward_update_samesize, @@ -2711,6 +3263,16 @@ Suite *wolfboot_suite(void) tcase_add_test(emergency_rollback, test_update_then_rollback_samesize); tcase_add_test(emergency_rollback, test_update_then_rollback_larger); tcase_add_test(emergency_rollback, test_update_then_rollback_smaller); +#endif +#if !defined(DISABLE_BACKUP) && !defined(DELTA_UPDATES) && \ + !defined(EXT_ENCRYPTED) && !defined(CUSTOM_PARTITION_TRAILER) + tcase_add_test(power_cut, test_update_power_cut_rolls_back); + tcase_add_test(power_cut, test_confirm_power_cut_keeps_one_image); + tcase_add_test(power_cut, test_rollback_power_cut_keeps_original); + tcase_add_test(power_cut, test_rollback_to_version_zero_stays); + tcase_add_test(power_cut, test_rollback_erase_failure_keeps_original); + tcase_add_checked_fixture(power_cut, powercut_quiet, powercut_loud); + tcase_set_timeout(power_cut, 600); #endif tcase_add_test(emergency_rollback_failure_due_to_bad_update, test_emergency_rollback_failure_due_to_bad_update); tcase_add_test(empty_boot_partition_update, test_empty_boot_partition_update); @@ -2770,6 +3332,10 @@ Suite *wolfboot_suite(void) suite_add_tcase(s, zero_size_update); suite_add_tcase(s, invalid_sha); suite_add_tcase(s, emergency_rollback); +#if !defined(DISABLE_BACKUP) && !defined(DELTA_UPDATES) && \ + !defined(EXT_ENCRYPTED) && !defined(CUSTOM_PARTITION_TRAILER) + suite_add_tcase(s, power_cut); +#endif suite_add_tcase(s, emergency_rollback_failure_due_to_bad_update); suite_add_tcase(s, empty_boot_partition_update); suite_add_tcase(s, empty_boot_but_update_sha_corrupted_denied); From c89fb0c0d7f6320fe011575cbce5a96004eaee91 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 20:52:47 +0100 Subject: [PATCH 2/4] Keep the FLAGS_HOME final swap resumable across a power cut --- .gitignore | 2 + include/image.h | 7 ++ src/libwolfboot.c | 135 ++++++++++++++++++++ src/update_flash.c | 19 +++ tools/unit-tests/Makefile | 19 +++ tools/unit-tests/unit-update-flash.c | 180 +++++++++++++++++++++++++-- 6 files changed, 355 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index b120c12113..97aa2ac231 100644 --- a/.gitignore +++ b/.gitignore @@ -181,6 +181,8 @@ tools/unit-tests/unit-string tools/unit-tests/unit-update-flash tools/unit-tests/unit-update-flash-enc tools/unit-tests/unit-update-flash-db +tools/unit-tests/unit-update-flash-flagshome +tools/unit-tests/unit-update-flash-invert tools/unit-tests/unit-update-flash-int-invert tools/unit-tests/unit-update-flash-enc-full tools/unit-tests/unit-update-flash-enc-intswap diff --git a/include/image.h b/include/image.h index c3ae1f81e0..9306ee2ff1 100644 --- a/include/image.h +++ b/include/image.h @@ -1739,6 +1739,13 @@ int wolfBoot_set_update_sector_flag(uint16_t sector, uint8_t newflag); int wolfBoot_image_compression(struct wolfBoot_image *img, uint32_t *uncompressed_sz, uint32_t *window_bits); #endif +#if defined(FLAGS_HOME) && defined(NVM_FLASH_WRITEONCE) && \ + !defined(EXT_ENCRYPTED) && !defined(PART_BOOT_EXT) +/* The UPDATE flags share the BOOT trailer: a swap ends in one trailer write */ +#define WOLFBOOT_SHARED_TRAILER +int wolfBoot_flags_home_reset_boot(void); +int wolfBoot_flags_home_set_boot(uint8_t boot_state); +#endif #ifdef WOLFBOOT_ELF_FLASH_SCATTER /* Support for ELF scatter/gather format */ diff --git a/src/libwolfboot.c b/src/libwolfboot.c index 40ba5e0e78..c3bad98840 100644 --- a/src/libwolfboot.c +++ b/src/libwolfboot.c @@ -359,6 +359,29 @@ static uint8_t get_base_offset(uint8_t *base, uintptr_t off) return *(uint8_t*)((uintptr_t)base - off); /* ignore array bounds error */ } +#if defined(FLAGS_HOME) && defined(NVM_FLASH_WRITEONCE) +/* Whether shared copy sel holds FINAL_FLAGS, the final swap's resume record */ +static int RAMFUNCTION flags_home_final(int sel) +{ + uintptr_t end = PART_UPDATE_ENDFLAGS - ((uintptr_t)sel * WOLFBOOT_SECTOR_SIZE); + + return (*((uint32_t*)(end - sizeof(uint32_t))) == WOLFBOOT_MAGIC_TRAIL) && + (*((uint8_t*)(end - sizeof(uint32_t) - 1U)) == IMG_STATE_FINAL_FLAGS); +} + +/* BOOT record of shared copy sel: 0 no magic, 1 magic only, 2 magic and state */ +static int RAMFUNCTION flags_home_boot_record(int sel) +{ + uintptr_t end = PART_BOOT_ENDFLAGS - ((uintptr_t)sel * WOLFBOOT_SECTOR_SIZE); + + if (*((uint32_t*)(end - sizeof(uint32_t))) != WOLFBOOT_MAGIC_TRAIL) + return 0; + if (*((uint8_t*)(end - sizeof(uint32_t) - 1U)) == FLASH_BYTE_ERASED) + return 1; + return 2; +} +#endif + #ifdef __CCRX__ #pragma section FRAM #endif @@ -395,6 +418,25 @@ static int RAMFUNCTION nvm_select_fresh_sector(int part) #endif } +#if defined(FLAGS_HOME) && defined(NVM_FLASH_WRITEONCE) + /* A shared copy written last ends with the BOOT magic: a copy without it + * does not replace one that holds a whole BOOT record, unless it holds + * FINAL_FLAGS, which a torn erase of the old copy must not hide */ + if (part == PART_UPDATE) { + int rec_0 = flags_home_boot_record(0); + int rec_1 = flags_home_boot_record(1); + + if ((rec_0 == 2) && (rec_1 == 0) && !flags_home_final(1)) { + sel = 0; + goto finish; + } + if ((rec_1 == 2) && (rec_0 == 0) && !flags_home_final(0)) { + sel = 1; + goto finish; + } + } +#endif + /* check magic in case the sector is corrupt */ word_0 = *((uint32_t*)((uintptr_t)base - sizeof(uint32_t))); word_1 = *((uint32_t*)((uintptr_t)base - (WOLFBOOT_SECTOR_SIZE + sizeof(uint32_t)))); @@ -536,6 +578,99 @@ static int RAMFUNCTION partition_magic_write(uint8_t part, uintptr_t addr) nvm_cache_scrub(); return ret; } + +#ifdef WOLFBOOT_SHARED_TRAILER +/* Write NVM_CACHE over the spare copy of the trailer, then erase copy sel */ +static int RAMFUNCTION nvm_cache_move(uintptr_t last, int sel) +{ + uintptr_t addr_read = last - ((uintptr_t)sel * WOLFBOOT_SECTOR_SIZE); + uintptr_t addr_write = last - ((uintptr_t)(!sel) * WOLFBOOT_SECTOR_SIZE); + int ret; +#if FLASHBUFFER_SIZE != WOLFBOOT_SECTOR_SIZE + uintptr_t off = 0; +#endif + + ret = hal_flash_erase(addr_write, WOLFBOOT_SECTOR_SIZE); +#if FLASHBUFFER_SIZE != WOLFBOOT_SECTOR_SIZE + while ((off < WOLFBOOT_SECTOR_SIZE) && (ret == 0)) { + ret = hal_flash_write(addr_write + off, NVM_CACHE + off, + FLASHBUFFER_SIZE); + off += FLASHBUFFER_SIZE; + } +#else + if (ret == 0) { + ret = hal_flash_write(addr_write, NVM_CACHE, NVM_CACHE_SIZE); + } +#endif + if (ret == 0) { + ret = hal_flash_erase(addr_read, WOLFBOOT_SECTOR_SIZE); + } + return ret; +} + +/* Rewrite the trailer that also holds the UPDATE flags: either keep those + * flags and clear the BOOT magic and state, or commit a BOOT state and clear + * them. The spare copy is written before the old one is erased. */ +static int RAMFUNCTION flags_home_rewrite(int commit, uint8_t boot_state) +{ + uintptr_t last = (PART_BOOT_ENDFLAGS - 1U) & + ~((uintptr_t)WOLFBOOT_SECTOR_SIZE - 1U); + uintptr_t off = PART_BOOT_ENDFLAGS - sizeof(uint32_t) - last; + int sel; + int pass; + int ret = 0; + + sel = nvm_select_fresh_sector(PART_UPDATE); + /* A trailer without the UPDATE magic must end in the last sector, the + * one nvm_select_fresh_sector() keeps when no magic is found */ + for (pass = (commit && (sel == 0)) ? 0 : 1; (pass < 2) && (ret == 0); + pass++) { + XMEMCPY(NVM_CACHE, + (void *)(last - ((uintptr_t)sel * WOLFBOOT_SECTOR_SIZE)), + NVM_CACHE_SIZE); + if ((pass == 1) && commit) { + XMEMSET(NVM_CACHE, FLASH_BYTE_ERASED, NVM_CACHE_SIZE); + XMEMCPY(NVM_CACHE + off, &wolfboot_magic_trail, sizeof(uint32_t)); + NVM_CACHE[off - 1U] = boot_state; + } + else if (pass == 1) { + XMEMSET(NVM_CACHE + off - 1U, FLASH_BYTE_ERASED, + sizeof(uint32_t) + 1U); + } + ret = nvm_cache_move(last, sel); + sel = !sel; + } + nvm_cached_sector = sel; + return (ret == 0) ? 0 : -1; +} + +/** + * @brief Clear the BOOT state and keep the UPDATE flags, FINAL_FLAGS included. + * + * With FLAGS_HOME the UPDATE flags live in the BOOT trailer, so the final swap + * cannot erase it without losing its own resume record. + * + * @return 0 on success, -1 on failure. + */ +int RAMFUNCTION wolfBoot_flags_home_reset_boot(void) +{ + return flags_home_rewrite(0, 0); +} + +/** + * @brief Set the BOOT state and clear the UPDATE flags in one rewrite. + * + * The final swap commits TESTING after an update and SUCCESS after a + * rollback. + * + * @param[in] boot_state New BOOT partition state. + * @return 0 on success, -1 on failure. + */ +int RAMFUNCTION wolfBoot_flags_home_set_boot(uint8_t boot_state) +{ + return flags_home_rewrite(1, boot_state); +} +#endif /* WOLFBOOT_SHARED_TRAILER */ #ifdef __CCRX__ #pragma section #endif diff --git a/src/update_flash.c b/src/update_flash.c index 18b316c1f0..fd67504009 100644 --- a/src/update_flash.c +++ b/src/update_flash.c @@ -798,8 +798,13 @@ static int RAMFUNCTION wolfBoot_swap_and_final_erase(int resume, int rollback) wb_flash_write(boot, tmpBootPos, (void*)tmpBuffer, sizeof(tmpBuffer)); } #endif +#ifdef WOLFBOOT_SHARED_TRAILER + /* Keep FINAL_FLAGS to resume; a rollback keeps BOOT TESTING as well */ + ret = rollback ? 0 : wolfBoot_flags_home_reset_boot(); +#else /* Erase the last sector(s) of boot partition (where partition state is stored) */ ret = wb_trailer_clear(boot, PART_BOOT); +#endif if (ret < 0) { #ifdef EXT_FLASH ext_flash_lock(); @@ -846,8 +851,22 @@ static int RAMFUNCTION wolfBoot_swap_and_final_erase(int resume, int rollback) /* Mark boot partition as TESTING - this tells bootloader to fallback if * update fails. A rollback restores the confirmed image. */ +#ifdef WOLFBOOT_SHARED_TRAILER + /* FINAL_FLAGS stays set until this write replaces it */ + ret = wolfBoot_flags_home_set_boot(rollback ? IMG_STATE_SUCCESS : + IMG_STATE_TESTING); + if (ret < 0) { +#ifdef EXT_FLASH + ext_flash_lock(); +#endif + hal_flash_lock(); + wolfBoot_zeroize(tmpBuffer, sizeof(tmpBuffer)); + return ret; + } +#else wolfBoot_set_partition_state(PART_BOOT, rollback ? IMG_STATE_SUCCESS : IMG_STATE_TESTING); +#endif /* Erase the last sector(s) of update partition */ /* This resets the update partition state to IMG_STATE_NEW */ diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 14e7924704..be9cc5b5d6 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -77,6 +77,7 @@ TESTS:=unit-parser unit-parser-large-header unit-fdt unit-extflash unit-string \ unit-enc-nvm-flagshome unit-delta unit-gzip unit-update-flash unit-update-flash-delta \ unit-update-flash-db \ unit-update-flash-hook \ + unit-update-flash-flagshome unit-update-flash-invert \ unit-update-flash-int-invert \ unit-update-flash-self-update \ unit-update-flash-self-update-ecc \ @@ -407,6 +408,18 @@ unit-nsc-update:CFLAGS+=-DMOCK_PARTITIONS -D__WOLFBOOT -DTZEN \ -DWOLFBOOT_NO_SIGN -DUNIT_TEST_AUTH \ -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED \ -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE +# Write-once FLAGS_HOME trailers with SWAP in internal flash, with the default +# and the inverted flag polarity; the inverted one also writes trailers in +# chunks smaller than a sector +unit-update-flash-flagshome:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN -DUNIT_TEST_AUTH \ + -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED -DEXT_FLASH -DPART_UPDATE_EXT \ + -DNVM_FLASH_WRITEONCE -DFLAGS_HOME=1 \ + -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE +unit-update-flash-invert:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN -DUNIT_TEST_AUTH \ + -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED -DEXT_FLASH -DPART_UPDATE_EXT \ + -DNVM_FLASH_WRITEONCE -DWOLFBOOT_FLAGS_INVERT=1 -DFLAGS_HOME=1 \ + -DFLASHBUFFER_SIZE=512 \ + -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE # Write-once trailers with inverted flags and every partition in internal flash unit-update-flash-int-invert:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN -DUNIT_TEST_AUTH \ -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED \ @@ -1239,6 +1252,12 @@ unit-fit-fpga: ../../include/target.h unit-fit-fpga.c unit-update-flash: ../../include/target.h unit-update-flash.c gcc -o $@ unit-update-flash.c ../../src/image.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) +unit-update-flash-flagshome: ../../include/target.h unit-update-flash.c + gcc -o $@ unit-update-flash.c ../../src/image.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) + +unit-update-flash-invert: ../../include/target.h unit-update-flash.c + gcc -o $@ unit-update-flash.c ../../src/image.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) + unit-update-flash-int-invert: ../../include/target.h unit-update-flash.c gcc -o $@ unit-update-flash.c ../../src/image.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) diff --git a/tools/unit-tests/unit-update-flash.c b/tools/unit-tests/unit-update-flash.c index 358b534b8f..3957c012e2 100644 --- a/tools/unit-tests/unit-update-flash.c +++ b/tools/unit-tests/unit-update-flash.c @@ -158,10 +158,11 @@ static int update_part_write(uintptr_t addr, const void *buf, int len) return ext_flash_check_write(addr, buf, len); } -/* The UPDATE flags sit in the UPDATE partition, internal or external */ +/* The UPDATE flags are in internal flash without EXT_FLASH, and with + * FLAGS_HOME, which keeps them in the BOOT trailer */ static void update_flags_unlock(void) { -#ifndef EXT_FLASH +#if defined(FLAGS_HOME) || !defined(EXT_FLASH) hal_flash_unlock(); #else ext_flash_unlock(); @@ -170,13 +171,20 @@ static void update_flags_unlock(void) static void update_flags_lock(void) { -#ifndef EXT_FLASH +#if defined(FLAGS_HOME) || !defined(EXT_FLASH) hal_flash_lock(); #else ext_flash_lock(); #endif } +#ifdef FLAGS_HOME +/* The BOOT trailer also holds the UPDATE flags, which a refused resume clears */ +#define BOOT_IMAGE_AREA (WOLFBOOT_PARTITION_SIZE - 2 * WOLFBOOT_SECTOR_SIZE) +#else +#define BOOT_IMAGE_AREA WOLFBOOT_PARTITION_SIZE +#endif + static void ext_flash_write_le16(uintptr_t addr, uint16_t val) { uint8_t le[2]; @@ -1217,17 +1225,26 @@ START_TEST (test_final_swap_aborts_on_restore_copy_failure) add_payload(PART_BOOT, 1, 30700); + /* Start confirmed, so a skipped BOOT state reset leaves SUCCESS behind */ + hal_flash_unlock(); + wolfBoot_set_partition_state(PART_BOOT, IMG_STATE_SUCCESS); + hal_flash_lock(); update_flags_unlock(); wolfBoot_set_partition_state(PART_UPDATE, IMG_STATE_FINAL_FLAGS); update_flags_lock(); +#ifdef WOLFBOOT_SHARED_TRAILER + /* The BOOT state reset rewrites the shared trailer before the restore */ + hal_flash_write_fail = 1 + (WOLFBOOT_SECTOR_SIZE / FLASHBUFFER_SIZE); +#else hal_flash_write_fail = 1; +#endif ret = wolfBoot_swap_and_final_erase(1, 0); ck_assert_int_lt(ret, 0); ck_assert_int_eq(hal_flash_write_fail, 0); - /* The boot state must be unset: the state sector was erased before the - * restore and TESTING must not be written after a failed restore. */ + /* The boot state must be reset before the restore, and TESTING must not + * be written after a failed restore. */ ck_assert_int_ne(wolfBoot_get_partition_state(PART_BOOT, &boot_state), 0); /* The backup completed, so FINAL_FLAGS is set: the next boot's resume * path re-enters and retries the restore from SWAP. */ @@ -1738,7 +1755,7 @@ static void resume_with_version(uint32_t boot_ver, uint32_t update_ver) ck_assert_uint_eq(wolfBoot_current_firmware_version(), boot_ver); ck_assert_int_eq( memcmp((const void*)(uintptr_t)WOLFBOOT_PARTITION_BOOT_ADDRESS, - resume_boot_snap, WOLFBOOT_PARTITION_SIZE), + resume_boot_snap, BOOT_IMAGE_AREA), 0); cleanup_flash(); } @@ -1808,7 +1825,7 @@ static void resume_with_bad_swap_field(uint32_t off, uint32_t val) ck_assert_int_lt(wolfBoot_update(0), 0); ck_assert_int_eq( memcmp((const void*)(uintptr_t)WOLFBOOT_PARTITION_BOOT_ADDRESS, - resume_boot_snap, WOLFBOOT_PARTITION_SIZE), + resume_boot_snap, BOOT_IMAGE_AREA), 0); cleanup_flash(); } @@ -2299,6 +2316,148 @@ START_TEST (test_rollback_erase_failure_keeps_original) } END_TEST + +#if defined(FLAGS_HOME) && defined(NVM_FLASH_WRITEONCE) +/* A power cut while the update trigger rewrites the shared trailer must not + * lose the BOOT state held in the same trailer */ +START_TEST (test_flags_home_trigger_power_cut_keeps_boot_state) +{ + uint8_t st = 0; + int torn; + int n; + + powercut_confirmed = 1; + for (torn = 0; torn < 4; torn++) { + mock_powercut_torn = torn; + for (n = 0; ; n++) { + powercut_setup(TEST_SIZE_LARGE); + mock_flash_ops = 0; + mock_powercut_at = n; + if (setjmp(mock_powercut_jmp) == 0) { + wolfBoot_update_trigger(); + mock_powercut_at = -1; + cleanup_flash(); + break; + } + mock_power_cycle(); + hal_flash_unlock(); + ck_assert_msg((wolfBoot_get_partition_state(PART_BOOT, &st) == 0) && + (st == IMG_STATE_SUCCESS), "trigger cut at flash op %d (torn " + "mode %d) lost the BOOT state", n, torn); + hal_flash_lock(); + cleanup_flash(); + } + ck_assert_int_gt(n, 0); + } + powercut_confirmed = 0; + mock_powercut_torn = 0; +} +END_TEST +#endif + +#ifdef WOLFBOOT_SHARED_TRAILER +/* A torn erase of the old copy can keep its last 8 bytes, a whole BOOT + * record, next to the new copy holding FINAL_FLAGS, which must stay in use */ +START_TEST (test_flags_home_torn_erase_keeps_final_flags) +{ + static uint8_t tail[8]; + uintptr_t last = (PART_BOOT_ENDFLAGS - 1U) & + ~((uintptr_t)WOLFBOOT_SECTOR_SIZE - 1U); + uintptr_t old_copy; + uint8_t st = 0; + + reset_mock_stats(); + prepare_flash(); + add_payload(PART_BOOT, 1, TEST_SIZE_SMALL); + hal_flash_unlock(); + wolfBoot_set_partition_state(PART_BOOT, IMG_STATE_SUCCESS); + wolfBoot_set_partition_state(PART_UPDATE, IMG_STATE_FINAL_FLAGS); + old_copy = last - ((uintptr_t)nvm_select_fresh_sector(PART_UPDATE) * + WOLFBOOT_SECTOR_SIZE); + memcpy(tail, (const void *)(old_copy + WOLFBOOT_SECTOR_SIZE - sizeof(tail)), + sizeof(tail)); + ck_assert_int_eq(wolfBoot_flags_home_reset_boot(), 0); + hal_flash_write(old_copy + WOLFBOOT_SECTOR_SIZE - sizeof(tail), tail, + sizeof(tail)); + hal_flash_lock(); + mock_power_cycle(); + hal_flash_unlock(); + ck_assert_int_eq(wolfBoot_get_partition_state(PART_UPDATE, &st), 0); + ck_assert_uint_eq(st, IMG_STATE_FINAL_FLAGS); + hal_flash_lock(); + cleanup_flash(); +} +END_TEST + +/* A failed write or erase in either shared-trailer commit is reported as -1, + * whatever the driver returned, and the trailer holds either FINAL_FLAGS or + * the whole committed state */ +static int shared_trailer_fail_at(int erase, int code, int commit, int k) +{ + uint8_t st = 0; + int left; + int ret; + + reset_mock_stats(); + prepare_flash(); + add_payload(PART_BOOT, 1, TEST_SIZE_SMALL); + hal_flash_unlock(); + wolfBoot_set_partition_state(PART_UPDATE, IMG_STATE_FINAL_FLAGS); + if (erase) { + hal_flash_erase_fail = k; + hal_flash_erase_fail_ret = code; + } + else { + hal_flash_write_fail = k; + hal_flash_write_fail_ret = code; + } + ret = commit ? wolfBoot_flags_home_set_boot(IMG_STATE_TESTING) : + wolfBoot_flags_home_reset_boot(); + left = erase ? hal_flash_erase_fail : hal_flash_write_fail; + hal_flash_erase_fail = 0; + hal_flash_write_fail = 0; + if (left != 0) { + /* fewer than k operations: the commit ran to the end */ + ck_assert_int_eq(ret, 0); + } + else { + ck_assert_int_eq(ret, -1); + if (wolfBoot_get_partition_state(PART_UPDATE, &st) == 0) { + ck_assert_uint_eq(st, IMG_STATE_FINAL_FLAGS); + } + else { + /* only the erase of the old copy failed */ + ck_assert(commit); + ck_assert_int_eq(wolfBoot_get_partition_state(PART_BOOT, &st), 0); + ck_assert_uint_eq(st, IMG_STATE_TESTING); + } + } + hal_flash_lock(); + cleanup_flash(); + return left == 0; +} + +START_TEST (test_shared_trailer_flash_error_reported) +{ + static const int codes[2] = { -1, 0x00520005 }; /* positive driver status */ + int erase; + int code; + int commit; + int k; + + for (erase = 0; erase < 2; erase++) { + for (code = 0; code < 2; code++) { + for (commit = 0; commit < 2; commit++) { + for (k = 1; shared_trailer_fail_at(erase, codes[code], commit, + k); k++) + ; + ck_assert_int_gt(k, 1); + } + } + } +} +END_TEST +#endif #endif #endif /* !DISABLE_BACKUP */ @@ -3271,6 +3430,13 @@ Suite *wolfboot_suite(void) tcase_add_test(power_cut, test_rollback_power_cut_keeps_original); tcase_add_test(power_cut, test_rollback_to_version_zero_stays); tcase_add_test(power_cut, test_rollback_erase_failure_keeps_original); +#if defined(FLAGS_HOME) && defined(NVM_FLASH_WRITEONCE) + tcase_add_test(power_cut, test_flags_home_trigger_power_cut_keeps_boot_state); +#endif +#ifdef WOLFBOOT_SHARED_TRAILER + tcase_add_test(power_cut, test_flags_home_torn_erase_keeps_final_flags); + tcase_add_test(power_cut, test_shared_trailer_flash_error_reported); +#endif tcase_add_checked_fixture(power_cut, powercut_quiet, powercut_loud); tcase_set_timeout(power_cut, 600); #endif From d7af018c61c9aa575e6f7219a5fd0a64febfdf3e Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 21 Oct 2026 22:20:22 +0100 Subject: [PATCH 3/4] Add CY8CEVAL-062S2 support and bring the PSoC 6 port to parity --- .github/workflows/test-build-psoc6.yml | 35 +- .github/workflows/test-configs.yml | 65 ++ .github/workflows/test-units.yml | 10 +- .gitignore | 9 + arch.mk | 63 +- config/examples/cypsoc6-qspi.config | 41 + config/examples/cypsoc6.config | 33 +- docs/Targets.md | 195 +++-- hal/psoc6.c | 875 ++++++++++----------- hal/psoc6.h | 97 +++ hal/psoc6.ld | 23 +- include/hal.h | 4 + src/boot_arm.c | 4 + test-app/ARM-psoc6-cm4.ld | 97 +++ test-app/ARM-psoc6.ld | 2 +- test-app/Makefile | 48 +- test-app/app_psoc6.c | 49 +- test-app/startup_arm.c | 9 +- tools/ci/fetch-psoc6-pdl.sh | 26 + tools/config.mk | 11 +- tools/scripts/check-ramcode.py | 391 +++++++++ tools/unit-tests/Makefile | 40 +- tools/unit-tests/cmsis_stub/core_cm0plus.h | 135 ++++ tools/unit-tests/unit-psoc6-boot.c | 696 ++++++++++++++++ tools/unit-tests/unit-psoc6-flash.c | 506 ++++++++++++ tools/unit-tests/unit-psoc6-qspi.c | 319 ++++++++ 26 files changed, 3172 insertions(+), 611 deletions(-) create mode 100644 config/examples/cypsoc6-qspi.config create mode 100644 hal/psoc6.h create mode 100644 test-app/ARM-psoc6-cm4.ld create mode 100755 tools/ci/fetch-psoc6-pdl.sh create mode 100755 tools/scripts/check-ramcode.py create mode 100644 tools/unit-tests/cmsis_stub/core_cm0plus.h create mode 100644 tools/unit-tests/unit-psoc6-boot.c create mode 100644 tools/unit-tests/unit-psoc6-flash.c create mode 100644 tools/unit-tests/unit-psoc6-qspi.c diff --git a/.github/workflows/test-build-psoc6.yml b/.github/workflows/test-build-psoc6.yml index 1a89e7c698..4a123db217 100644 --- a/.github/workflows/test-build-psoc6.yml +++ b/.github/workflows/test-build-psoc6.yml @@ -1,4 +1,4 @@ -name: Wolfboot Reusable Build Workflow for Cypress PSoC6 +name: Wolfboot Reusable Build Workflow for Infineon PSoC 6 on: @@ -13,6 +13,10 @@ on: make-args: required: false type: string + ramcode-check: + required: false + type: boolean + default: false jobs: @@ -27,31 +31,12 @@ jobs: with: submodules: true - - uses: actions/checkout@v4 - with: - repository: Infineon/psoc6pdl - path: lib/psoc6pdl - - - uses: actions/checkout@v4 - with: - repository: Infineon/TARGET_CY8CKIT-062S2-43012 - path: lib/TARGET_CY8CKIT-062S2-43012 - - - uses: actions/checkout@v4 - with: - repository: Infineon/core-lib - path: lib/core-lib - - name: Trust workspace run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - - name: Patch cy_syslib.c to declare cy_delay32kMs + - name: Fetch Infineon PSoC 6 libraries run: | - # Add extern declaration for cy_delay32kMs directly in cy_syslib.c - # Find the line with the include and add the extern declaration after it - awk '/#include "cy_syslib.h"/ {print; print ""; print "/* Extern declaration for delay variable */"; print "extern uint32_t cy_delay32kMs;"; next} 1' \ - lib/psoc6pdl/drivers/source/cy_syslib.c > /tmp/cy_syslib_patched.c - mv /tmp/cy_syslib_patched.c lib/psoc6pdl/drivers/source/cy_syslib.c + tools/ci/fetch-psoc6-pdl.sh lib - name: make distclean run: | @@ -68,3 +53,9 @@ jobs: - name: Build wolfboot run: | make ${{inputs.make-args}} V=1 + + - name: Check the self-update code is all in RAM + if: ${{ inputs.ramcode-check }} + run: | + python3 tools/scripts/check-ramcode.py --selftest + python3 tools/scripts/check-ramcode.py wolfboot.elf wolfBoot_self_update 0x08000000 0x08060000 diff --git a/.github/workflows/test-configs.yml b/.github/workflows/test-configs.yml index 452ebc6a5f..e4f6117516 100644 --- a/.github/workflows/test-configs.yml +++ b/.github/workflows/test-configs.yml @@ -20,6 +20,71 @@ jobs: arch: arm config-file: ./config/examples/cypsoc6.config + cypsoc6_eval_test: + uses: ./.github/workflows/test-build-psoc6.yml + with: + arch: arm + config-file: ./config/examples/cypsoc6.config + make-args: PSOC6_BOARD=CY8CEVAL_062S2 + + cypsoc6_nouart_test: + uses: ./.github/workflows/test-build-psoc6.yml + with: + arch: arm + config-file: ./config/examples/cypsoc6.config + make-args: DEBUG_UART=0 + + cypsoc6_crypto_test: + uses: ./.github/workflows/test-build-psoc6.yml + with: + arch: arm + config-file: ./config/examples/cypsoc6.config + make-args: PSOC6_CRYPTO=1 + + cypsoc6_cm4_test: + uses: ./.github/workflows/test-build-psoc6.yml + with: + arch: arm + config-file: ./config/examples/cypsoc6.config + make-args: PSOC6_CM4_APP=1 IMAGE_HEADER_SIZE=1024 + + cypsoc6_ramcode_test: + uses: ./.github/workflows/test-build-psoc6.yml + with: + arch: arm + config-file: ./config/examples/cypsoc6.config + make-args: RAM_CODE=1 + ramcode-check: true + + cypsoc6_dualbank_test: + uses: ./.github/workflows/test-build-psoc6.yml + with: + arch: arm + config-file: ./config/examples/cypsoc6.config + make-args: DUALBANK_SWAP=1 WOLFBOOT_PARTITION_UPDATE_ADDRESS=0x12020000 WOLFBOOT_PARTITION_SWAP_ADDRESS=0xFFFFFFFF + + cypsoc6_qspi_test: + uses: ./.github/workflows/test-build-psoc6.yml + with: + arch: arm + config-file: ./config/examples/cypsoc6-qspi.config + make-args: PSOC6_BOARD=CY8CEVAL_062S2 + + cypsoc6_qspi_ramcode_test: + uses: ./.github/workflows/test-build-psoc6.yml + with: + arch: arm + config-file: ./config/examples/cypsoc6-qspi.config + make-args: PSOC6_BOARD=CY8CEVAL_062S2 RAM_CODE=1 + ramcode-check: true + + cypsoc6_qspi_cm4_test: + uses: ./.github/workflows/test-build-psoc6.yml + with: + arch: arm + config-file: ./config/examples/cypsoc6-qspi.config + make-args: PSOC6_BOARD=CY8CEVAL_062S2 PSOC6_CM4_APP=1 IMAGE_HEADER_SIZE=1024 + hifive1_test: uses: ./.github/workflows/test-build-riscv.yml with: diff --git a/.github/workflows/test-units.yml b/.github/workflows/test-units.yml index 37b70fed73..9a958d8b54 100644 --- a/.github/workflows/test-units.yml +++ b/.github/workflows/test-units.yml @@ -29,10 +29,16 @@ jobs: run: | tools/ci/fetch-mcuxsdk-headers.sh "$GITHUB_WORKSPACE/mcuxsdk" + - name: Fetch Infineon PSoC 6 PDL headers + run: | + tools/ci/fetch-psoc6-pdl.sh "$GITHUB_WORKSPACE/psoc6libs" + - name: Build unit tests run: | - make -C tools/unit-tests MCUXSDK="$GITHUB_WORKSPACE/mcuxsdk" + make -C tools/unit-tests MCUXSDK="$GITHUB_WORKSPACE/mcuxsdk" \ + PSOC6_LIBS="$GITHUB_WORKSPACE/psoc6libs" - name: Run unit tests run: | - make -C tools/unit-tests run MCUXSDK="$GITHUB_WORKSPACE/mcuxsdk" + make -C tools/unit-tests run MCUXSDK="$GITHUB_WORKSPACE/mcuxsdk" \ + PSOC6_LIBS="$GITHUB_WORKSPACE/psoc6libs" diff --git a/.gitignore b/.gitignore index 97aa2ac231..d83bb976e5 100644 --- a/.gitignore +++ b/.gitignore @@ -146,6 +146,9 @@ tools/unit-tests/unit-delta tools/unit-tests/unit-psoc-c3-divider tools/unit-tests/unit-psoc-e8-tz tools/unit-tests/unit-psoc-e8-flash +tools/unit-tests/unit-psoc6-flash +tools/unit-tests/unit-psoc6-qspi +tools/unit-tests/unit-psoc6-boot tools/unit-tests/unit-disk tools/unit-tests/unit-fs-probe tools/unit-tests/unit-fat32 @@ -431,6 +434,12 @@ lib/r_config lib/r_flash_rx lib/r_tsip_rx +# Infineon PSoC 6 libraries +lib/mtb-pdl-cat1 +lib/mtb-template-cat1 +lib/cmsis +lib/core-lib + Debug/ Release/ language.settings.xml diff --git a/arch.mk b/arch.mk index e4f8390704..fb55d422b6 100644 --- a/arch.mk +++ b/arch.mk @@ -2458,25 +2458,59 @@ endif ifeq ($(TARGET),psoc6) CORTEX_M0=1 - OBJS+=\ + PSOC6_BOARD?=CY8CKIT_062S2_43012 + # The boot ROM's flash NMI handler reprograms the CM0+ MPU for itself and + # cannot run under wolfBoot's map, which marks the ROM execute-never. + ifneq ($(NO_MPU),1) + $(error TARGET=psoc6 requires NO_MPU=1) + endif + # Main and work flash erase to 0x00 + ifneq ($(FLAGS_INVERT),1) + $(error TARGET=psoc6 requires FLAGS_INVERT=1) + endif + # Rewriting a programmed row erases it first, so trailer updates need the + # two-sector trailer to survive a power cut + ifneq ($(NVM_FLASH_WRITEONCE),1) + $(error TARGET=psoc6 requires NVM_FLASH_WRITEONCE=1) + endif + CYPRESS_PDL_DEV:=$(CYPRESS_PDL)/devices/COMPONENT_CAT1A + CYPRESS_PDL_TEMPLATE:=$(CYPRESS_TEMPLATE_LIB)/files/templates/cat1a/COMPONENT_MTB + PSOC6_PDL_OBJS:=\ $(CYPRESS_PDL)/drivers/source/cy_flash.o \ $(CYPRESS_PDL)/drivers/source/cy_ipc_pipe.o \ $(CYPRESS_PDL)/drivers/source/cy_ipc_sema.o \ $(CYPRESS_PDL)/drivers/source/cy_ipc_drv.o \ - $(CYPRESS_PDL)/drivers/source/cy_device.o \ $(CYPRESS_PDL)/drivers/source/cy_sysclk.o \ $(CYPRESS_PDL)/drivers/source/cy_sysint.o \ $(CYPRESS_PDL)/drivers/source/cy_syslib.o \ - $(CYPRESS_PDL)/drivers/source/cy_ble_clk.o \ - $(CYPRESS_PDL)/drivers/source/cy_wdt.o \ - $(CYPRESS_PDL)/drivers/source/TOOLCHAIN_GCC_ARM/cy_syslib_gcc.o \ - $(CYPRESS_PDL)/devices/templates/COMPONENT_MTB/COMPONENT_CM0P/system_psoc6_cm0plus.o + $(CYPRESS_PDL)/drivers/source/cy_gpio.o \ + $(CYPRESS_PDL)/drivers/source/TOOLCHAIN_GCC_ARM/cy_syslib_ext.o \ + $(CYPRESS_PDL_DEV)/source/cy_device.o + OBJS+=$(PSOC6_PDL_OBJS) \ + $(CYPRESS_PDL_TEMPLATE)/COMPONENT_CM0P/system_psoc6_cm0plus.o + PSOC6_UART_OBJS:=\ + $(CYPRESS_PDL)/drivers/source/cy_scb_common.o \ + $(CYPRESS_PDL)/drivers/source/cy_scb_uart.o + PSOC6_SMIF_OBJS:=\ + $(CYPRESS_PDL)/drivers/source/cy_smif.o \ + $(CYPRESS_PDL)/drivers/source/cy_smif_memslot.o \ + $(CYPRESS_PDL)/drivers/source/cy_smif_sfdp.o + ifeq ($(DEBUG_UART),1) + OBJS+=$(PSOC6_UART_OBJS) + endif + ifeq ($(BOOT_BENCHMARK),1) + OBJS+=$(CYPRESS_PDL)/drivers/source/cy_tcpwm_counter.o + endif + ifeq ($(EXT_FLASH),1) + OBJS+=$(PSOC6_SMIF_OBJS) + endif PSOC6_CRYPTO_OBJS=\ $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/port/cypress/psoc6_crypto.o \ $(CYPRESS_PDL)/drivers/source/cy_crypto_core_vu.o \ $(CYPRESS_PDL)/drivers/source/cy_crypto_core_ecc_domain_params.o \ $(CYPRESS_PDL)/drivers/source/cy_crypto_core_ecc_nist_p.o \ $(CYPRESS_PDL)/drivers/source/cy_crypto_core_ecc_ecdsa.o \ + $(CYPRESS_PDL)/drivers/source/cy_crypto_core_ecc_key_gen.o \ $(CYPRESS_PDL)/drivers/source/cy_crypto_core_sha_v2.o \ $(CYPRESS_PDL)/drivers/source/cy_crypto_core_sha_v1.o \ $(CYPRESS_PDL)/drivers/source/cy_crypto_core_mem_v2.o \ @@ -2486,15 +2520,20 @@ ifeq ($(TARGET),psoc6) $(CYPRESS_PDL)/drivers/source/cy_crypto.o CFLAGS+=\ - -I$(CYPRESS_PDL)/devices/include \ -I$(CYPRESS_PDL)/drivers/include \ - -I$(CYPRESS_PDL)/cmsis/include \ - -I$(CYPRESS_TARGET_LIB) \ + -I$(CYPRESS_PDL_DEV)/include \ + -I$(CYPRESS_PDL_DEV)/include/ip \ + -I$(CYPRESS_PDL_TEMPLATE) \ + -I$(CYPRESS_CMSIS)/Core/Include \ -I$(CYPRESS_CORE_LIB)/include \ - -I$(CYPRESS_PDL)/devices/include/ip \ - -I$(CYPRESS_PDL)/devices/templates/COMPONENT_MTB \ - -DCY8C624ABZI_D44 + -DCY8C624ABZI_S2D44 \ + -DPSOC6_BOARD_$(PSOC6_BOARD) ARCH_FLASH_OFFSET=0x10000000 + # Copies then program whole erased rows instead of rewriting half rows + CFLAGS+=-DFLASHBUFFER_SIZE=512 + ifeq ($(PSOC6_CM4_APP),1) + CFLAGS+=-DPSOC6_CM4_APP -DWOLFBOOT_BOOT_REMOTE_CORE + endif ifneq ($(PSOC6_CRYPTO),0) CFLAGS+=-DWOLFSSL_PSOC6_CRYPTO OBJS+=$(PSOC6_CRYPTO_OBJS) diff --git a/config/examples/cypsoc6-qspi.config b/config/examples/cypsoc6-qspi.config new file mode 100644 index 0000000000..d8da965666 --- /dev/null +++ b/config/examples/cypsoc6-qspi.config @@ -0,0 +1,41 @@ +ARCH?=ARM +TARGET?=psoc6 +PSOC6_BOARD?=CY8CKIT_062S2_43012 +SIGN?=ECC256 +HASH?=SHA256 +CYPRESS_PDL?=$(WOLFBOOT_ROOT)/lib/mtb-pdl-cat1 +CYPRESS_CMSIS?=$(WOLFBOOT_ROOT)/lib/cmsis +CYPRESS_TEMPLATE_LIB?=$(WOLFBOOT_ROOT)/lib/mtb-template-cat1 +CYPRESS_CORE_LIB?=$(WOLFBOOT_ROOT)/lib/core-lib +DEBUG?=0 +DEBUG_UART?=1 +VTOR?=1 +NO_MPU?=1 +FLAGS_INVERT?=1 +CORTEX_M0?=1 +NO_ASM?=0 +EXT_FLASH?=1 +NO_SWAP_EXT?=1 +FLAGS_HOME?=1 +SPI_FLASH?=0 +NO_XIP?=0 +UART_FLASH?=0 +ALLOW_DOWNGRADE?=0 +NVM_FLASH_WRITEONCE?=1 +WOLFBOOT_VERSION?=0 +V?=0 +SPMATH?=1 +RAM_CODE?=0 +DUALBANK_SWAP?=0 +PKA?=0 +WOLFTPM?=0 +PSOC6_CRYPTO?=0 +# Sectors match the 256 KB QSPI erase sector. wolfBoot 0x10000000 (256 KB), +# BOOT 0x10040000 (1.5 MB) and SWAP 0x101C0000 (256 KB) in internal flash, +# UPDATE 0x18000000 (1.5 MB) in QSPI. QSPI erases to 0xFF, so FLAGS_HOME +# keeps every flag in the BOOT trailer. +WOLFBOOT_PARTITION_SIZE?=0x180000 +WOLFBOOT_SECTOR_SIZE?=0x40000 +WOLFBOOT_PARTITION_BOOT_ADDRESS?=0x10040000 +WOLFBOOT_PARTITION_UPDATE_ADDRESS?=0x18000000 +WOLFBOOT_PARTITION_SWAP_ADDRESS?=0x101C0000 diff --git a/config/examples/cypsoc6.config b/config/examples/cypsoc6.config index 1b91550cd8..f3e548c7c5 100644 --- a/config/examples/cypsoc6.config +++ b/config/examples/cypsoc6.config @@ -1,12 +1,17 @@ ARCH?=ARM TARGET?=psoc6 -SIGN?=ED25519 +PSOC6_BOARD?=CY8CKIT_062S2_43012 +SIGN?=ECC256 HASH?=SHA256 -CYPRESS_PDL?=$(WOLFBOOT_ROOT)/lib/psoc6pdl -CYPRESS_TARGET_LIB?=$(WOLFBOOT_ROOT)/lib/TARGET_CY8CKIT-062S2-43012 +CYPRESS_PDL?=$(WOLFBOOT_ROOT)/lib/mtb-pdl-cat1 +CYPRESS_CMSIS?=$(WOLFBOOT_ROOT)/lib/cmsis +CYPRESS_TEMPLATE_LIB?=$(WOLFBOOT_ROOT)/lib/mtb-template-cat1 CYPRESS_CORE_LIB?=$(WOLFBOOT_ROOT)/lib/core-lib -DEBUG?=1 +DEBUG?=0 +DEBUG_UART?=1 VTOR?=1 +NO_MPU?=1 +FLAGS_INVERT?=1 CORTEX_M0?=1 NO_ASM?=0 EXT_FLASH?=0 @@ -14,7 +19,7 @@ SPI_FLASH?=0 NO_XIP?=0 UART_FLASH?=0 ALLOW_DOWNGRADE?=0 -NVM_FLASH_WRITEONCE?=0 +NVM_FLASH_WRITEONCE?=1 WOLFBOOT_VERSION?=0 V?=0 SPMATH?=1 @@ -23,16 +28,10 @@ DUALBANK_SWAP?=0 PKA?=0 WOLFTPM?=0 PSOC6_CRYPTO?=0 -WOLFBOOT_PARTITION_SIZE?=0x80000 -WOLFBOOT_SECTOR_SIZE?=512 -WOLFBOOT_PARTITION_BOOT_ADDRESS?=0x10080000 +# wolfBoot 0x10000000 (128 KB), BOOT 0x10020000 and UPDATE 0x10100000 +# (896 KB each), SWAP 0x101E0000 (one 4 KB subsector) +WOLFBOOT_PARTITION_SIZE?=0xE0000 +WOLFBOOT_SECTOR_SIZE?=0x1000 +WOLFBOOT_PARTITION_BOOT_ADDRESS?=0x10020000 WOLFBOOT_PARTITION_UPDATE_ADDRESS?=0x10100000 -WOLFBOOT_PARTITION_SWAP_ADDRESS?=0x10010000 - -# cypsoc6 places the swap sector (0x10010000) inside the bootloader -# write-protect region [0x10000000, 0x10080000). This is a deliberate -# peculiarity of this target: hal_flash_protect() is the weak no-op -# (psoc6 does not override it), so the overlap is harmless at runtime. -# Allow the overlap so the partition-geometry #error guards in -# include/target.h.in do not fire for this config. -WOLFBOOT_ALLOW_PART_OVERLAP = 1 +WOLFBOOT_PARTITION_SWAP_ADDRESS?=0x101E0000 diff --git a/docs/Targets.md b/docs/Targets.md index 18a1076bc5..4d842eed1c 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -9,7 +9,7 @@ This README describes configuration of supported targets. * [Analog Devices MAX32666](#analog-devices-max32666) * [Cortex-A53 / Raspberry PI 3](#cortex-a53--raspberry-pi-3-experimental) * [Cortex-A72 / Raspberry Pi Compute Module 4](#cortex-a72--raspberry-pi-compute-module-4-bcm2711) -* [Cypress PSoC-6](#cypress-psoc-6) +* [Infineon PSoC 6](#infineon-psoc-6) * [Infineon PSOC Control C3](#infineon-psoc-control-c3) * [Infineon PSOC Edge E8](#infineon-psoc-edge-e8) * [Infineon AURIX TC3xx](#infineon-aurix-tc3xx) @@ -4934,131 +4934,162 @@ sudo fdisk -l /dev/sdX | SD1 | 1 1 1 0 | off, off, off, on | -## Cypress PSoC-6 +## Infineon PSoC 6 -The Cypress PSoC 62S2 is a dual-core Cortex-M4 & Cortex-M0+ MCU. The secure boot process is managed by the M0+. -WolfBoot can be compiled as second stage flash bootloader to manage application verification and firmware updates. +The PSoC 62 (CY8C624ABZI-S2D44) has a Cortex-M0+ and a Cortex-M4F at 100 MHz, +2 MB of flash at `0x10000000` (512-byte rows that erase to `0x00`), a 32 KB work +flash at `0x14000000` and 1 MB of SRAM. The boot ROM starts the CM0+, which runs +wolfBoot. The verified application runs on the CM0+, or on the CM4 when built +with `PSOC6_CM4_APP=1`. Tested on the CY8CEVAL-062S2 evaluation kit with its +on-board KitProg3. The CY8CKIT-062S2-43012 uses the same configurations. -### Building - -The following configuration has been tested using PSoC 62S2 Wi-Fi BT Pioneer Kit (CY8CKIT-052S2-43012). - -#### Target specific requirements +The port builds against the Infineon mtb-pdl-cat1 driver library, the +mtb-template-cat1 system files, core-lib and cmsis. Fetch the pinned releases +into `lib/`, where the configs expect them: -wolfBoot uses the following components to access peripherals on the PSoC: +``` +tools/ci/fetch-psoc6-pdl.sh lib +``` - * [Cypress Core Library](https://github.com/cypresssemiconductorco/core-lib) - * [PSoC 6 Peripheral Driver Library](https://github.com/cypresssemiconductorco/psoc6pdl) - * [CY8CKIT-062S2-43012 BSP](https://github.com/cypresssemiconductorco/TARGET_CY8CKIT-062S2-43012) +### PSoC 6: Configuring and compiling -Cypress provides a [customized OpenOCD](https://github.com/cypresssemiconductorco/Openocd) for programming the flash and -debugging. +``` +cp config/examples/cypsoc6.config .config +make PSOC6_BOARD=CY8CEVAL_062S2 +``` -### Clock settings +`PSOC6_BOARD` selects the kit's console and LED pins: `CY8CKIT_062S2_43012` +(the default) or `CY8CEVAL_062S2`. Pass it to `make` or set it in `.config`. -wolfBoot configures PLL1 to run at 100 MHz and is driving `CLK_FAST`, `CLK_PERI`, and `CLK_SLOW` at that frequency. +The configuration files are: +- `cypsoc6.config`: all partitions in internal flash, ECC256 and SHA256, console + on the KitProg3 USB-UART. +- `cypsoc6-qspi.config`: UPDATE partition in the kits' 512 Mb QSPI NOR flash. -#### Build configuration +Every PSoC 6 build needs three settings, and the build fails without them: +- `NO_MPU=1`: the boot ROM performs flash operations in a CM0+ NMI handler that + cannot run under wolfBoot's MPU map. +- `FLAGS_INVERT=1`: the flash erases to `0x00`. +- `NVM_FLASH_WRITEONCE=1`: a programmed row can only be rewritten by erasing it + first, so the update flags alternate between the last two sectors of a + partition and a power cut never loses them. -The following configuration has been tested on the PSoC CY8CKIT-62S2-43012: +These options are make arguments on top of a configuration: +- `PSOC6_CRYPTO=1`: SHA-256 and ECDSA P-256 verification on the crypto block. +- `PSOC6_CM4_APP=1 IMAGE_HEADER_SIZE=1024`: boot the application on the CM4. + The 1 KB header keeps the CM4 vector table aligned. +- `RAM_CODE=1`: allow a bootloader self-update (`sign --wolfboot-update`). +- `BOOT_BENCHMARK=1`: print the hash and signature times from a 1 MHz TCPWM timer. +- `DEBUG_UART=0`: remove the console. +- `DUALBANK_SWAP=1 WOLFBOOT_PARTITION_UPDATE_ADDRESS=0x12020000 + WOLFBOOT_PARTITION_SWAP_ADDRESS=0xFFFFFFFF`: hardware bank swap instead of a + swap partition. -``` -make TARGET=psoc6 \ - CYPRESS_PDL=./lib/psoc6pdl \ - CYPRESS_TARGET_LIB=./lib/TARGET_CY8CKIT-062S2-43012 \ - CYPRESS_CORE_LIB=./lib/core-lib \ - WOLFBOOT_SECTOR_SIZE=4096 -``` +Flash layout of `cypsoc6.config`: -Note: A reference `.config` can be found in [/config/examples/cypsoc6.config](/config/examples/cypsoc6.config). +| Region | Address | Size | +| -------- | ------------ | ----------------- | +| wolfBoot | `0x10000000` | 128 KB | +| BOOT | `0x10020000` | 896 KB | +| UPDATE | `0x10100000` | 896 KB | +| SWAP | `0x101E0000` | 4 KB | -Hardware acceleration is enable by default using psoc6 crypto hw support. +`cypsoc6-qspi.config` uses the QSPI flash's 256 KB erase sectors as wolfBoot +sectors: wolfBoot at `0x10000000` (256 KB), BOOT at `0x10040000` (1.5 MB), SWAP at +`0x101C0000` (256 KB), and UPDATE at `0x18000000` (1.5 MB, QSPI). The QSPI flash +erases to `0xFF`, so `FLAGS_HOME=1` keeps all the update flags in the BOOT +trailer. The trailer takes the last two 256 KB sectors of BOOT, so images can be +up to 1 MB, and wolfBoot uses 264 KB of SRAM for its trailer cache. -To compile with hardware acceleration disabled, use the option +With `DUALBANK_SWAP=1` the flash is split into two 1 MB banks: BOOT at +`0x10020000`, and UPDATE at `0x12020000` in the other bank. On the first boot +wolfBoot copies itself into the second bank. An update swaps the bank mapping and +records it in the work flash. -`PSOC6_CRYPTO=0` +With `PSOC6_CM4_APP=1`, wolfBoot verifies the image on the CM0+ and starts the +CM4 at the image's vector table. The CM0+ then sleeps, but it stays available to +service the boot ROM's flash calls, so the CM4 application can call +`wolfBoot_success()` and stage updates. The CM4 application uses SRAM from +`0x08060000` and leaves wolfBoot's 384 KB of SRAM at `0x08000000` untouched. +Run `make clean` when changing `PSOC6_CM4_APP`: wolfBoot and the application are +compiled differently for each core, and existing objects are not rebuilt. -in your wolfBoot configuration. +### PSoC 6: Loading the firmware -#### External Flash Support - -PSoC6 supports external QSPI flash for firmware storage. To enable: +pyOCD supports the device (`cy8c6xxa`) through the KitProg3 in CMSIS-DAP mode: ``` -make TARGET=psoc6 \ - EXT_FLASH=1 \ - CYPRESS_PDL=./lib/psoc6pdl \ - CYPRESS_TARGET_LIB=./lib/TARGET_CY8CKIT-062S2-43012 \ - CYPRESS_CORE_LIB=./lib/core-lib \ - WOLFBOOT_SECTOR_SIZE=4096 +pip install pyocd +pyocd flash -t cy8c6xxa -a 0x10000000 -e sector wolfboot.bin +pyocd flash -t cy8c6xxa -a 0x10020000 -e sector test-app/image_v1_signed.bin ``` -External flash uses a temporary sector backup mechanism to handle the larger erase size (for example, 0x40000-byte sectors on an S25FL512S device) compared to internal flash rows. The backup sector is automatically managed in external flash beyond the update partition. - -#### Dual-Bank Flash Swap - -PSoC6 supports hardware-assisted bank swapping for faster and more reliable firmware updates: +The console is the KitProg3 USB-UART at 115200 8N1: ``` -make TARGET=psoc6 \ - DUALBANK_SWAP=1 \ - CYPRESS_PDL=./lib/psoc6pdl \ - CYPRESS_TARGET_LIB=./lib/TARGET_CY8CKIT-062S2-43012 \ - CYPRESS_CORE_LIB=./lib/core-lib \ - WOLFBOOT_SECTOR_SIZE=4096 +Booting version: 0x1 +Checking integrity...done +Verifying signature...done +Hello from PSoC6 CM0+ firmware version 1 ``` -#### OpenOCD installation - -Compile and install the customized OpenOCD. +### PSoC 6: Testing firmware update -Use the following configuration file when running `openocd` to connect to the PSoC6 board: +Sign version 2 and write it to the UPDATE partition. Then mark it for update by +writing the last 4 KB sector of the partition: the erased value `0x00`, +followed by `IMG_STATE_UPDATING` (`0x8F` with inverted flags) and the `BOOT` +magic. An application does the same with `wolfBoot_update_trigger()`. ``` -### openocd.cfg for PSoC-62S2 - -source [find interface/kitprog3.cfg] -transport select swd -adapter speed 1000 -source [find target/psoc6_2m.cfg] -init -reset init +tools/keytools/sign --ecc256 --sha256 test-app/image.bin wolfboot_signing_private_key.der 2 +pyocd flash -t cy8c6xxa -a 0x10100000 -e sector test-app/image_v2_signed.bin +python3 -c "import sys; sys.stdout.buffer.write(bytes(4091) + b'\x8fBOOT')" > trigger.bin +pyocd flash -t cy8c6xxa -a 0x101DF000 -e sector trigger.bin ``` -### Loading the firmware +The sign tool takes the header size from the environment, so sign a +`PSOC6_CM4_APP=1` build with the 1 KB header it was built with: -To upload `factory.bin` to the device with OpenOCD, connect the device, -run OpenOCD with the configuration from the previous section, then connect -to the local openOCD server running on TCP port 4444 using `telnet localhost 4444`. +``` +IMAGE_HEADER_SIZE=1024 tools/keytools/sign --ecc256 --sha256 test-app/image.bin wolfboot_signing_private_key.der 2 +``` -From the telnet console, type: +wolfBoot swaps the images, boots version 2, and the application confirms it: -`program factory.bin 0x10000000` +``` +Hello from PSoC6 CM0+ firmware version 2 +Update successful, firmware version 2 confirmed +``` -When the transfer is finished, you can either close openOCD or start a debugging session. +If version 2 does not call `wolfBoot_success()`, the next reset rolls back to +version 1. -### Debugging +With `cypsoc6-qspi.config`, write the image to `0x18000000`. The trigger +goes at the end of BOOT instead (`FLAGS_HOME`), 8 bytes below the BOOT flags. +It replaces both copies of the write-once trailer, the two last BOOT sectors: -Debugging with OpenOCD: - -Use the OpenOCD configuration from the previous sections to run OpenOCD. +``` +pyocd flash -t cy8c6xxa -a 0x18000000 -e sector test-app/image_v2_signed.bin +python3 -c "import sys; sys.stdout.buffer.write(bytes(0x80000 - 13) + b'\x8fBOOT' + bytes(8))" > trigger.bin +pyocd flash -t cy8c6xxa -a 0x10140000 -e sector trigger.bin +``` -From another console, connect using gdb, e.g.: +With `DUALBANK_SWAP=1`, the higher version boots, so no trigger is +needed. The debugger sees the single-bank map, in which the UPDATE partition +starts at `0x10120000` while the original mapping is active: ``` -arm-none-eabi-gdb -(gdb) target remote:3333 +pyocd flash -t cy8c6xxa -a 0x10120000 -e sector test-app/image_v2_signed.bin ``` -To reset the board to start from the M0+ flash bootloader position (wolfBoot reset handler), use -the monitor command sequence below: +### PSoC 6: Debugging ``` -(gdb) mon init -(gdb) mon reset init -(gdb) mon psoc6 reset_halt +pyocd gdbserver -t cy8c6xxa +arm-none-eabi-gdb wolfboot.elf -ex "target remote :3333" ``` +pyOCD serves the CM4 on port 3334. ## Microchip SAMA5D3 diff --git a/hal/psoc6.c b/hal/psoc6.c index a9b9007063..669abcf703 100644 --- a/hal/psoc6.c +++ b/hal/psoc6.c @@ -26,29 +26,45 @@ /* Project includes */ #include "image.h" #include "printf.h" -#include "psoc6_02_config.h" #include +#include "hal.h" +#include "hal/psoc6.h" #ifdef WOLFSSL_PSOC6_CRYPTO #include "wolfssl/wolfcrypt/port/cypress/psoc6_crypto.h" #endif +#include "cy_device_headers.h" #include "cy_flash.h" +#include "cy_gpio.h" #include "cy_sysclk.h" #include "cy_syslib.h" +#ifdef DEBUG_UART +#include "cy_scb_uart.h" +#endif +#ifdef BOOT_BENCHMARK +#include "cy_tcpwm_counter.h" +#endif +#include "cy_sysint.h" +#ifdef EXT_FLASH +#include "cy_smif_memslot.h" +#endif #define EMULATED_EEPROM_START (CY_EM_EEPROM_BASE) #define ROW_SIZE (CY_FLASH_SIZEOF_ROW) #define FLASH_SUBSECTOR_SIZE (CY_FLASH_SIZEOF_ROW * 8) #define FLASH_SECTOR_SIZE (CY_FLASH_SIZEOF_ROW * CPUSS_FLASHC_SONOS_MAIN_ROWS) #define FLASH_BASE_ADDRESS (CY_FLASH_BASE) -#define FLASH_SIZE (CY_FLASH_SIZE) -#define CPU_FREQ (100000000) /* Magic number constants */ -#define EXT_FLASH_TMP_MAGIC (0x4C465845U) /* 'EXFL' */ #define BANKMODE_MAGIC (0x4B424C44U) /* 'DLBK' */ -#define EXT_FLASH_WORD_ERASED_VALUE (0xFFFFFFFFU) + +/* NVM_FLASH_WRITEONCE keeps a whole trailer, one flag nibble per sector, in + * one sector */ +#if (((WOLFBOOT_PARTITION_SIZE) / (WOLFBOOT_SECTOR_SIZE)) / 2) + 16 > \ + (WOLFBOOT_SECTOR_SIZE) + #error "WOLFBOOT_SECTOR_SIZE is too small for the partition trailer" +#endif #ifdef EXT_FLASH @@ -113,24 +129,6 @@ static cy_stc_smif_block_config_t smifBlockConfig_sfdp = { static cy_stc_smif_block_config_t* smif_blk_config; -#define TMP_SECTOR_INFO_ADDR EMULATED_EEPROM_START - -/** - * Address of the temporary sector in external flash. - * - * External flash has a larger minimum erase size (determined at runtime via - * SFDP, e.g. 0x40000 bytes for S25FL512S) than internal flash rows. Since - * read-modify-erase-write cannot use SRAM as a temporary buffer due to size - * constraints, we use a dedicated sector in external flash for temporary - * storage during sector updates. - */ -#define SWAP_TMP_SECTOR_ADDR (WOLFBOOT_PARTITION_UPDATE_ADDRESS + CY_FLASH_SIZE) - -typedef struct tmp_sector_info { - uint32_t magic; - uint32_t sector_address; -} tmp_sector_info; - static SMIF_Type* QSPIPort = SMIF0; /* @@ -283,39 +281,29 @@ static const cy_stc_smif_config_t QSPI_config = { .blockEvent = (uint32_t)CY_SMIF_BUS_ERROR, }; -#if CY_CPU_CORTEX_M0P -/* CM0+ SMIF interrupt configuration */ -static cy_stc_sysint_t smifIntConfig = { - /* ATTENTION: make sure proper Interrupts configured for CM0p or M4 cores */ - .intrSrc = NvicMux7_IRQn, - .cm0pSrc = smif_interrupt_IRQn, - .intrPriority = 1, -}; -#endif - -#if CY_CPU_CORTEX_M0P -static void Isr_SMIF(void) -{ - Cy_SMIF_Interrupt(QSPIPort, &smif_context); -} -#endif - /** * @brief Initialize external QSPI flash hardware * - * Configures GPIO pins, SMIF clock, interrupt, and initializes the SMIF - * block and external memory device. + * Configures GPIO pins and the SMIF clock, and initializes the SMIF + * block and external memory device. Only blocking SMIF calls are used, so + * no SMIF interrupt is set up. * * Initialization steps: * 1. Configure QSPI data, clock, and slave-select GPIO pins * 2. Enable SMIF HF clock (CLK_HF2) with divide-by-4 - * 3. Setup SMIF interrupt for CM0+ - * 4. Initialize SMIF block in normal mode - * 5. Enable SMIF block - * 6. Initialize external memory configuration with retry + * 3. Initialize SMIF block in normal mode + * 4. Enable SMIF block + * 5. Initialize external memory configuration with retry * * @return CY_SMIF_SUCCESS on success, error code on failure */ +/* ext_flash_erase() erases whole sectors, so they must not exceed a wolfBoot + * sector */ +static int psoc6_qspi_geometry_ok(uint32_t erase_size) +{ + return (erase_size != 0U) && ((WOLFBOOT_SECTOR_SIZE % erase_size) == 0U); +} + cy_en_smif_status_t hal_ext_flash_init(void) { cy_en_smif_status_t st; @@ -349,28 +337,17 @@ cy_en_smif_status_t hal_ext_flash_init(void) CY_SMIF_SYSCLK_HFCLK_DIVIDER); (void)Cy_SysClk_ClkHfEnable(CY_SYSCLK_CLKHF_IN_CLKPATH2); - /* Step 3: Setup the interrupt for the SMIF block. For the CM0+ there - * is a two stage process to setup the interrupts. */ -#if CY_CPU_CORTEX_M0P - (void)Cy_SysInt_Init(&smifIntConfig, Isr_SMIF); -#endif - - /* Step 4: Initialize SMIF block */ + /* Step 3: Initialize SMIF block */ st = Cy_SMIF_Init(QSPIPort, &QSPI_config, CY_SMIF_INIT_TIMEOUT_US, &smif_context); if (st != CY_SMIF_SUCCESS) { return st; } -#if CY_CPU_CORTEX_M0P - NVIC_EnableIRQ( - smifIntConfig.intrSrc); /* Finally, Enable the SMIF interrupt */ -#endif - - /* Step 5: Enable SMIF block */ + /* Step 4: Enable SMIF block */ Cy_SMIF_Enable(QSPIPort, &smif_context); - /* Step 6: Initialize external memory via SFDP with retry */ + /* Step 5: Initialize external memory via SFDP with retry */ smif_blk_config = &smifBlockConfig_sfdp; try_count = CY_SMIF_INIT_TRY_COUNT; do { @@ -381,16 +358,23 @@ cy_en_smif_status_t hal_ext_flash_init(void) } } while ((st != CY_SMIF_SUCCESS) && (try_count > 0U)); + if ((st == CY_SMIF_SUCCESS) && !psoc6_qspi_geometry_ok( + smif_blk_config->memConfig[0]->deviceCfg->eraseSize)) { + wolfBoot_printf("QSPI erase size 0x%x does not divide " + "WOLFBOOT_SECTOR_SIZE\n", + smif_blk_config->memConfig[0]->deviceCfg->eraseSize); + st = CY_SMIF_BAD_PARAM; + } return st; } +#ifdef __WOLFBOOT /** * @brief Deinitialize external QSPI flash hardware * * Reverses the initialization performed by hal_ext_flash_init(). * Deinitializes SMIF memory, disables the SMIF block, disables the - * HF clock, disconnects interrupts (on CM0+), and deinitializes - * GPIO ports. Matches mcuboot's qspi_deinit() sequence. + * HF clock, and deinitializes GPIO ports. */ static void hal_ext_flash_deinit(void) { @@ -399,12 +383,6 @@ static void hal_ext_flash_deinit(void) (void)Cy_SysClk_ClkHfDisable(CY_SYSCLK_CLKHF_IN_CLKPATH2); -#if CY_CPU_CORTEX_M0P - NVIC_DisableIRQ(smifIntConfig.intrSrc); - Cy_SysInt_DisconnectInterruptSource(smifIntConfig.intrSrc, - smifIntConfig.cm0pSrc); -#endif - Cy_GPIO_Port_Deinit(qspi_SS_Port); Cy_GPIO_Port_Deinit(qspi_SCK_Port); Cy_GPIO_Port_Deinit(qspi_D0_Port); @@ -412,29 +390,221 @@ static void hal_ext_flash_deinit(void) Cy_GPIO_Port_Deinit(qspi_D2_Port); Cy_GPIO_Port_Deinit(qspi_D3_Port); } +#endif #endif /* EXT_FLASH */ #ifdef DUALBANK_SWAP -#define FLASH_BANK1_BASE (0x10000000) /* Base address of Flash Bank1 */ -#define FLASH_BANK2_BASE (0x12000000) /* Base address of Flash Bank2 */ +#define FLASH_BANK1_BASE (PSOC6_FLASH_BASE) +/* Second bank in the single-bank map, before dual-bank mode is enabled */ +#define FLASH_BANK2_COPY (PSOC6_FLASH_BASE + PSOC6_FLASH_BANK_SIZE) typedef struct active_bank { uint32_t magic; - uint32_t mapping; /* Changed from cy_en_maptype_t to uint32_t for 32-bit - alignment */ + uint32_t mapping; + uint32_t seq; + uint32_t check; } active_bank; -/* EXT_FLASH and DUALBANK_SWAP are mutually exclusive, so its safe to overwrite - * the data at same address. */ -#define ACTIVE_BANK_MODE_DATA_ADDRESS (EMULATED_EEPROM_START) +/* Two records in separate work-flash rows: a swap writes the older one, so a + * power cut never leaves the device without a valid mapping */ +#define BANK_RECORD_SLOTS 2U +#define BANK_RECORD_ADDR(slot) (EMULATED_EEPROM_START + ((slot) * ROW_SIZE)) int RAMFUNCTION bankmode_init(void); #endif /* Global write buffer to avoid stack allocation and enable RAMFUNCTION usage */ -static uint8_t psoc6_write_buffer[ROW_SIZE]; +static uint8_t psoc6_write_buffer[ROW_SIZE] __attribute__((aligned(4))); + +#if defined(PSOC6_CM4_APP) && \ + (((WOLFBOOT_PARTITION_BOOT_ADDRESS + IMAGE_HEADER_SIZE) % \ + PSOC6_CM4_VTOR_ALIGN) != 0) +#error "PSOC6_CM4_APP needs a 1 KB aligned image: set IMAGE_HEADER_SIZE=1024" +#endif + +static int RAMFUNCTION psoc6_in_region(uint32_t address, uint32_t len, + uint32_t base, uint32_t size) +{ + return (address >= base) && (len <= size) && + ((address - base) <= (size - len)); +} + +/* ProgramRow needs an erased row; WriteRow erases first */ +static int RAMFUNCTION psoc6_write_row(uint32_t row) +{ + const uint32_t *cur = (const uint32_t*)PSOC6_FLASH_PTR(row); + const uint32_t *buf = (const uint32_t*)psoc6_write_buffer; + uint32_t i; + int same = 1; + int erased = 1; + int ret = 0; + + for (i = 0; i < (ROW_SIZE / sizeof(uint32_t)); i++) { + if (cur[i] != buf[i]) { + same = 0; + } + if (cur[i] != 0U) { + erased = 0; + } + } + if (!same) { + if (erased) { + ret = (int)Cy_Flash_ProgramRow(row, buf); + } + else { + ret = (int)Cy_Flash_WriteRow(row, buf); + } + Cy_SysLib_ClearFlashCacheAndBuffer(); + } + /* wolfBoot checks for negative errors; PDL status codes are positive */ + return (ret == 0) ? 0 : -1; +} + +static int RAMFUNCTION psoc6_is_erased(uint32_t address, uint32_t len) +{ + const uint32_t *p = (const uint32_t*)PSOC6_FLASH_PTR(address); + uint32_t i; + + for (i = 0; i < (len / sizeof(uint32_t)); i++) { + if (p[i] != 0U) { + return 0; + } + } + return 1; +} + +int RAMFUNCTION psoc6_flash_range_ok(uint32_t address, uint32_t len) +{ + int ok = 0; + + if (len == 0U) { + ok = 0; + } + else if (psoc6_in_region(address, len, PSOC6_FLASH_BASE, + PSOC6_FLASH_SIZE) || + psoc6_in_region(address, len, PSOC6_WFLASH_BASE, PSOC6_WFLASH_SIZE)) { + ok = 1; + } +#ifdef DUALBANK_SWAP + else if (psoc6_in_region(address, len, PSOC6_FLASH_BANK2_BASE, + PSOC6_FLASH_BANK_SIZE)) { + ok = 1; + } +#endif + return ok; +} + +#ifdef DEBUG_UART +static void psoc6_uart_putc(char c) +{ + while (Cy_SCB_UART_Put(PSOC6_UART_SCB, (uint32_t)(uint8_t)c) == 0UL) { + } +} + +#ifdef __WOLFBOOT +static const cy_stc_scb_uart_config_t psoc6_uart_cfg = { + .uartMode = CY_SCB_UART_STANDARD, + .oversample = PSOC6_UART_OVERSAMPLE, + .dataWidth = 8UL, + .parity = CY_SCB_UART_PARITY_NONE, + .stopBits = CY_SCB_UART_STOP_BITS_1, + .breakWidth = 11UL, + .ctsPolarity = CY_SCB_UART_ACTIVE_LOW, + .rtsPolarity = CY_SCB_UART_ACTIVE_LOW, +}; + +void uart_init(void) +{ + Cy_GPIO_Pin_FastInit(PSOC6_UART_PORT, PSOC6_UART_TX_PIN, + CY_GPIO_DM_STRONG_IN_OFF, 1UL, PSOC6_UART_TX_MUX); + Cy_GPIO_Pin_FastInit(PSOC6_UART_PORT, PSOC6_UART_RX_PIN, + CY_GPIO_DM_HIGHZ, 1UL, PSOC6_UART_RX_MUX); + + (void)Cy_SysClk_PeriphAssignDivider(PSOC6_UART_PCLK, + CY_SYSCLK_DIV_16_5_BIT, PSOC6_UART_DIV_NUM); + (void)Cy_SysClk_PeriphSetFracDivider(CY_SYSCLK_DIV_16_5_BIT, + PSOC6_UART_DIV_NUM, PSOC6_UART_DIV_INT, PSOC6_UART_DIV_FRAC); + (void)Cy_SysClk_PeriphEnableDivider(CY_SYSCLK_DIV_16_5_BIT, + PSOC6_UART_DIV_NUM); + + if (Cy_SCB_UART_Init(PSOC6_UART_SCB, &psoc6_uart_cfg, NULL) == + CY_SCB_UART_SUCCESS) { + Cy_SCB_UART_Enable(PSOC6_UART_SCB); + } +} + +static void psoc6_uart_flush(void) +{ + while (!Cy_SCB_UART_IsTxComplete(PSOC6_UART_SCB)) { + } +} +#endif /* __WOLFBOOT */ + +void uart_write(const char* buf, unsigned int sz) +{ + unsigned int i; + + for (i = 0; i < sz; i++) { + if (buf[i] == '\n') { + psoc6_uart_putc('\r'); + } + psoc6_uart_putc(buf[i]); + } +} +#endif /* DEBUG_UART */ + +#if defined(BOOT_BENCHMARK) && defined(__WOLFBOOT) +static const cy_stc_tcpwm_counter_config_t psoc6_timer_cfg = { + .period = 0xFFFFFFFFUL, + .clockPrescaler = CY_TCPWM_COUNTER_PRESCALER_DIVBY_1, + .runMode = CY_TCPWM_COUNTER_CONTINUOUS, + .countDirection = CY_TCPWM_COUNTER_COUNT_UP, + .compareOrCapture = CY_TCPWM_COUNTER_MODE_CAPTURE, + .interruptSources = CY_TCPWM_INT_NONE, + .captureInputMode = CY_TCPWM_INPUT_LEVEL, + .captureInput = CY_TCPWM_INPUT_0, + .reloadInputMode = CY_TCPWM_INPUT_LEVEL, + .reloadInput = CY_TCPWM_INPUT_0, + .startInputMode = CY_TCPWM_INPUT_LEVEL, + .startInput = CY_TCPWM_INPUT_0, + .stopInputMode = CY_TCPWM_INPUT_LEVEL, + .stopInput = CY_TCPWM_INPUT_0, + .countInputMode = CY_TCPWM_INPUT_LEVEL, + .countInput = CY_TCPWM_INPUT_1, +}; + +/* TCPWM0 counter 0 is 32 bits wide; extend it in software across wraps */ +uint64_t hal_get_timer_us(void) +{ + static int started; + static uint32_t last; + static uint32_t high; + uint32_t now; + + if (!started) { + (void)Cy_SysClk_PeriphAssignDivider(PSOC6_TIMER_PCLK, + CY_SYSCLK_DIV_16_BIT, PSOC6_TIMER_DIV_NUM); + (void)Cy_SysClk_PeriphSetDivider(CY_SYSCLK_DIV_16_BIT, + PSOC6_TIMER_DIV_NUM, PSOC6_TIMER_DIV); + (void)Cy_SysClk_PeriphEnableDivider(CY_SYSCLK_DIV_16_BIT, + PSOC6_TIMER_DIV_NUM); + if (Cy_TCPWM_Counter_Init(PSOC6_TIMER_TCPWM, PSOC6_TIMER_CNT, + &psoc6_timer_cfg) == CY_TCPWM_SUCCESS) { + Cy_TCPWM_Counter_Enable(PSOC6_TIMER_TCPWM, PSOC6_TIMER_CNT); + Cy_TCPWM_TriggerStart_Single(PSOC6_TIMER_TCPWM, PSOC6_TIMER_CNT); + } + started = 1; + } + now = Cy_TCPWM_Counter_GetCounter(PSOC6_TIMER_TCPWM, PSOC6_TIMER_CNT); + if (now < last) { + high++; + } + last = now; + return ((uint64_t)high << 32) | now; +} +#endif /* BOOT_BENCHMARK && __WOLFBOOT */ #ifdef __WOLFBOOT static const cy_stc_pll_manual_config_t srss_0_clock_0_pll_0_pllConfig = { @@ -513,19 +683,38 @@ static void hal_set_pll(void) * @note Must be called early in the boot process before any flash operations * or cryptographic functions. */ -void hal_init(void) +/* The ROM runs every flash call through the CM0+ NMI vector, so the table + * must outlive a self-update erasing the bootloader; the PDL also installs + * ISRs only when VTOR points at __ramVectors */ +static void psoc6_ram_vectors(void) { -#define VTOR (*(volatile uint32_t*)(0xE000ED08)) - VTOR = FLASH_BASE_ADDRESS; -#undef VTOR + const cy_israddress *flash_iv = + (const cy_israddress*)PSOC6_FLASH_PTR(FLASH_BASE_ADDRESS); + uint32_t i; -#ifdef DUALBANK_SWAP - bankmode_init(); -#endif + for (i = 0; i < PSOC6_CM0P_SYS_VECTORS; i++) { + __ramVectors[i] = flash_iv[i]; + } + for (; i < PSOC6_CM0P_VECTORS; i++) { + __ramVectors[i] = flash_iv[PSOC6_HARDFAULT_VECTOR]; + } + SCB->VTOR = (uint32_t)(uintptr_t)__ramVectors; + __DSB(); +} + +void hal_init(void) +{ + psoc6_ram_vectors(); Cy_PDL_Init(CY_DEVICE_CFG); Cy_Flash_Init(); hal_set_pll(); +#ifdef DEBUG_UART + uart_init(); +#endif +#ifdef DUALBANK_SWAP + bankmode_init(); +#endif #ifdef EXT_FLASH if (hal_ext_flash_init() != CY_SMIF_SUCCESS) { @@ -545,7 +734,21 @@ void hal_prepare_boot(void) #ifdef EXT_FLASH hal_ext_flash_deinit(); #endif +#ifdef DEBUG_UART + psoc6_uart_flush(); +#endif +} + +#ifdef WOLFBOOT_BOOT_REMOTE_CORE +/* The CM0+ stays parked so it can service the CM4's SROM flash calls (NMI) */ +void hal_boot_remote_core(const uint32_t *app_offset) +{ + Cy_SysEnableCM4((uint32_t)(uintptr_t)app_offset); + while (1) { + __WFI(); + } } +#endif #endif @@ -577,13 +780,15 @@ int RAMFUNCTION hal_flash_write(uint32_t address, const uint8_t* data, int len) /* Validate input parameters (address checked against base, PDL handles bank * mapping) */ - if (!data || (len <= 0) || (address < FLASH_BASE_ADDRESS)) + if ((data == NULL) || (len <= 0) || + !psoc6_flash_range_ok(row_start, + ((uint32_t)len + offset + ROW_SIZE - 1U) & ~(ROW_SIZE - 1U))) return -1; /* Handle unaligned start address with read-modify-write */ if (address != row_start) { /* Read existing row from flash */ - memcpy(psoc6_write_buffer, (const void*)row_start, ROW_SIZE); + memcpy(psoc6_write_buffer, PSOC6_FLASH_PTR(row_start), ROW_SIZE); /* Merge new data at offset, limiting to row boundary */ write_len = ((ROW_SIZE - offset) < (uint32_t)len) ? (ROW_SIZE - offset) @@ -591,12 +796,8 @@ int RAMFUNCTION hal_flash_write(uint32_t address, const uint8_t* data, int len) memcpy(&psoc6_write_buffer[offset], data, write_len); /* Program the updated data into flash row */ - ret = - Cy_Flash_ProgramRow(row_start, (const uint32_t*)psoc6_write_buffer); + ret = psoc6_write_row(row_start); if (ret) { - wolfBoot_printf( - "hal_flash_write: ProgramRow failed at 0x%08X ret=%d\n", - row_start, ret); return ret; } @@ -616,17 +817,13 @@ int RAMFUNCTION hal_flash_write(uint32_t address, const uint8_t* data, int len) } else { /* Read-modify-write for partial row support */ - memcpy(psoc6_write_buffer, (const void*)row_start, ROW_SIZE); + memcpy(psoc6_write_buffer, PSOC6_FLASH_PTR(row_start), ROW_SIZE); memcpy(psoc6_write_buffer, data, write_len); } /* Program the updated data into flash row */ - ret = - Cy_Flash_ProgramRow(row_start, (const uint32_t*)psoc6_write_buffer); + ret = psoc6_write_row(row_start); if (ret) { - wolfBoot_printf( - "hal_flash_write: ProgramRow failed at 0x%08X ret=%d\n", - row_start, ret); return ret; } @@ -669,310 +866,122 @@ void RAMFUNCTION hal_flash_lock(void) int RAMFUNCTION hal_flash_erase(uint32_t address, int len) { cy_rslt_t status; + uint32_t step; + int ret = 0; /* Validate input parameters */ - if ((len <= 0) || (address % ROW_SIZE) || (address < FLASH_BASE_ADDRESS)) + if ((len <= 0) || (address % ROW_SIZE) || + !psoc6_flash_range_ok(address, + ((uint32_t)len + ROW_SIZE - 1U) & ~(ROW_SIZE - 1U))) return -1; /* Select optimal erase size based on alignment and length */ - while (len > 0) { + while ((len > 0) && (ret == 0)) { + status = CY_RSLT_SUCCESS; if ((address % FLASH_SECTOR_SIZE == 0) && ((uint32_t)len >= FLASH_SECTOR_SIZE)) { - /* Erase full sector (most efficient) */ - status = Cy_Flash_EraseSector(address); - if (status != CY_RSLT_SUCCESS) - return -1; - - address += FLASH_SECTOR_SIZE; - len -= FLASH_SECTOR_SIZE; + step = FLASH_SECTOR_SIZE; + if (!psoc6_is_erased(address, step)) + status = Cy_Flash_EraseSector(address); } else if ((address % FLASH_SUBSECTOR_SIZE == 0) && ((uint32_t)len >= FLASH_SUBSECTOR_SIZE)) { - /* Erase subsector (medium efficiency) */ - status = Cy_Flash_EraseSubsector(address); - if (status != CY_RSLT_SUCCESS) - return -1; - - address += FLASH_SUBSECTOR_SIZE; - len -= FLASH_SUBSECTOR_SIZE; + step = FLASH_SUBSECTOR_SIZE; + if (!psoc6_is_erased(address, step)) + status = Cy_Flash_EraseSubsector(address); } else { - /* Erase single row (smallest granularity) */ - status = Cy_Flash_EraseRow(address); - if (status != CY_RSLT_SUCCESS) - return -1; - - address += ROW_SIZE; - len -= ROW_SIZE; + step = ROW_SIZE; + if (!psoc6_is_erased(address, step)) + status = Cy_Flash_EraseRow(address); + } + if (status != CY_RSLT_SUCCESS) { + ret = -1; } + address += step; + len -= (int)step; } + Cy_SysLib_ClearFlashCacheAndBuffer(); - return 0; + return ret; } -/** - * @brief Perform a system reset - * - * Triggers a software reset of the PSoC6 device by calling NVIC_SystemReset(). - * - * @note This function does not return. - */ -void RAMFUNCTION arch_reboot(void) +/* With RAM_CODE, wolfBoot links the RAM copy from src/boot_arm.c */ +#if !defined(RAM_CODE) || !defined(__WOLFBOOT) +void arch_reboot(void) { NVIC_SystemReset(); } +#endif /****************************************************************************** * External Flash Support ******************************************************************************/ #ifdef EXT_FLASH +static int RAMFUNCTION psoc6_qspi_offset(uintptr_t address, uint32_t len, + uint32_t *offset) +{ + const cy_stc_smif_mem_config_t *mem = smif_blk_config->memConfig[0]; + + int ret = -1; + + if (psoc6_in_region((uint32_t)address, len, mem->baseAddress, + mem->deviceCfg->memSize)) { + *offset = (uint32_t)address - mem->baseAddress; + ret = 0; + } + return ret; +} -/** - * @brief Write data to external flash with sector management - * - * Handles writing to external flash which has larger erase sectors - * (e.g. 0x40000 bytes for S25FL512S, determined at runtime via SFDP) than - * the row size used by wolfBoot. Implements a temporary sector backup - * mechanism to preserve existing data during partial sector writes. - * - * Algorithm: - * 1. Check if the target sector differs from the cached temporary sector - * 2. If different: backup entire source sector to temp sector, then erase - * source - * 3. If same: check if target area needs erasing (contains non-0xFF data) - * 4. Write new data to the target location - * - * @param address External flash address (must be row-aligned) - * @param data Pointer to source data buffer - * @param len Number of bytes to write (must be row-aligned) - * - * @return 0 on success, -1 on error - * - * @note Uses internal emulated EEPROM to track temporary sector state for - * power-fail safety. - * @note SWAP_TMP_SECTOR_ADDR is used as temporary storage during sector - * updates. - */ int RAMFUNCTION ext_flash_write(uintptr_t address, const uint8_t* data, int len) { - tmp_sector_info info; - cy_rslt_t status; - uint32_t base_address; - uint32_t erase_size; - uint32_t sector_start; - uint8_t need_update; uint32_t offset; int ret = -1; - int i; - - /* Validate alignment requirements */ - if (!data || (address % ROW_SIZE) || (len % ROW_SIZE)) - return -1; - - base_address = smif_blk_config->memConfig[0]->baseAddress; - erase_size = smif_blk_config->memConfig[0]->deviceCfg->eraseSize; - - memcpy(&info, (void*)TMP_SECTOR_INFO_ADDR, sizeof(tmp_sector_info)); - sector_start = address - (address % erase_size); - - /* Determine if temporary sector needs updating (invalid magic or different - * sector) */ - need_update = ((info.magic != EXT_FLASH_TMP_MAGIC) || - (info.sector_address != sector_start)); - - if (need_update) { - /* Invalidate temp sector info before starting update process */ - if (hal_flash_erase(TMP_SECTOR_INFO_ADDR, ROW_SIZE) != 0) - goto error; - - /* Erase the temporary sector */ - status = Cy_SMIF_MemEraseSector(QSPIPort, smif_blk_config->memConfig[0], - SWAP_TMP_SECTOR_ADDR - base_address, - erase_size, &smif_context); - if (status != CY_SMIF_SUCCESS) - goto error; - - /* Backup source sector to temporary location row by row */ - for (offset = 0; offset < erase_size; offset += ROW_SIZE) { - status = - Cy_SMIF_MemRead(QSPIPort, smif_blk_config->memConfig[0], - sector_start - base_address + offset, - psoc6_write_buffer, ROW_SIZE, &smif_context); - if (status != CY_SMIF_SUCCESS) - goto error; - - status = - Cy_SMIF_MemWrite(QSPIPort, smif_blk_config->memConfig[0], - SWAP_TMP_SECTOR_ADDR - base_address + offset, - psoc6_write_buffer, ROW_SIZE, &smif_context); - if (status != CY_SMIF_SUCCESS) - goto error; - } - /* Mark temporary sector as valid */ - info.magic = EXT_FLASH_TMP_MAGIC; - info.sector_address = sector_start; - if (hal_flash_write(TMP_SECTOR_INFO_ADDR, (const void*)&info, - sizeof(tmp_sector_info)) != 0) - goto error; - - /* Erase the source sector */ - status = Cy_SMIF_MemEraseSector(QSPIPort, smif_blk_config->memConfig[0], - sector_start - base_address, erase_size, - &smif_context); - if (status != CY_SMIF_SUCCESS) - goto error; + if ((data != NULL) && (len > 0) && + (psoc6_qspi_offset(address, (uint32_t)len, &offset) == 0) && + (Cy_SMIF_MemWrite(QSPIPort, smif_blk_config->memConfig[0], offset, + data, (uint32_t)len, &smif_context) == CY_SMIF_SUCCESS)) { + ret = 0; } - else { - /* Check if target write region needs erasing (contains non-erased data) - */ - for (i = 0; i < len; i += ROW_SIZE) { - status = - Cy_SMIF_MemRead(QSPIPort, smif_blk_config->memConfig[0], - address - base_address + i, psoc6_write_buffer, - ROW_SIZE, &smif_context); - if (status != CY_SMIF_SUCCESS) { - wolfBoot_printf( - "ext_flash_write: SMIF read failed at 0x%08lX\n", - (unsigned long)(address + i)); - goto error; - } - - /* Check if all bytes are in erased state. */ - if ((*(uint32_t*)psoc6_write_buffer != - EXT_FLASH_WORD_ERASED_VALUE) || - (memcmp(psoc6_write_buffer + 4, psoc6_write_buffer, - ROW_SIZE - 4) != 0)) { - /* Non-erased data found, erase the containing sector */ - status = Cy_SMIF_MemEraseSector( - QSPIPort, smif_blk_config->memConfig[0], - sector_start - base_address, erase_size, &smif_context); - if (status != CY_SMIF_SUCCESS) { - wolfBoot_printf("ext_flash_write: SMIF erase failed for " - "sector 0x%08lX\n", - (unsigned long)sector_start); - goto error; - } - break; - } - } - } - - /* Write the row data */ - status = Cy_SMIF_MemWrite(QSPIPort, smif_blk_config->memConfig[0], - address - base_address, data, len, &smif_context); - if (status != CY_SMIF_SUCCESS) - goto error; - - ret = 0; - -error: return ret; } -/** - * @brief Read data from external flash with temporary sector redirection - * - * Reads data from external flash, automatically redirecting reads to the - * temporary backup sector if the requested address falls within a sector - * that is currently being updated (indicated by valid tmp_sector_info). - * - * This redirection ensures data consistency during interrupted write - * operations, as the temporary sector contains the original data before - * modification. - * - * @param address External flash address to read from - * @param data Pointer to destination buffer - * @param len Number of bytes to read (must be > 0) - * - * @return Number of bytes read on success, -1 on invalid parameters or - * SMIF failure - * - * @note Redirection is transparent to the caller and ensures read consistency - * even during power-fail recovery scenarios. - */ int RAMFUNCTION ext_flash_read(uintptr_t address, uint8_t* data, int len) { - tmp_sector_info info; - uint32_t base_address; - uint32_t erase_size; - uint32_t sector_start; uint32_t offset; - cy_rslt_t status; - - if (!data || (len <= 0)) - return -1; - - base_address = smif_blk_config->memConfig[0]->baseAddress; - erase_size = smif_blk_config->memConfig[0]->deviceCfg->eraseSize; - - memcpy(&info, (void*)TMP_SECTOR_INFO_ADDR, sizeof(tmp_sector_info)); + int ret = -1; - /* Redirect to temporary sector if active and address falls within that - * sector */ - if (info.magic == EXT_FLASH_TMP_MAGIC) { - sector_start = address - (address % erase_size); - if (sector_start == info.sector_address) { - /* Remap to temporary sector while preserving offset within sector - */ - address = SWAP_TMP_SECTOR_ADDR + (address % erase_size); - } + if ((data != NULL) && (len > 0) && + (psoc6_qspi_offset(address, (uint32_t)len, &offset) == 0) && + (Cy_SMIF_MemRead(QSPIPort, smif_blk_config->memConfig[0], offset, + data, (uint32_t)len, &smif_context) == CY_SMIF_SUCCESS)) { + ret = len; } - - offset = address - base_address; - status = Cy_SMIF_MemRead(QSPIPort, smif_blk_config->memConfig[0], offset, - data, len, &smif_context); - if (status != CY_SMIF_SUCCESS) - return -1; - return len; + return ret; } -/** - * @brief External flash erase operation (no-op) - * - * External flash erase is handled by ext_flash_write() due to sector size - * mismatch. wolfBoot expects row-sized erase granularity, but external - * flash minimum erase size is much larger (eg. 0x40000 bytes for S25FL512S). - * The write function manages sector erasing and preserves existing data - * via temporary sector backup. - * - * @param address External flash address (unused) - * @param len Number of bytes (unused) - * @return Always returns 0 (success) - */ +/* hal_ext_flash_init() rejects parts whose erase sector does not divide + * WOLFBOOT_SECTOR_SIZE, so the PDL never rounds an erase past the request */ int RAMFUNCTION ext_flash_erase(uintptr_t address, int len) { - (void)address; - (void)len; - return 0; -} -#endif /* EXT_FLASH */ + uint32_t offset; + uint32_t erase_size = smif_blk_config->memConfig[0]->deviceCfg->eraseSize; + int ret = -1; -#ifdef EXT_FLASH + if ((len > 0) && ((address % erase_size) == 0U) && + (((uint32_t)len % erase_size) == 0U) && + (psoc6_qspi_offset(address, (uint32_t)len, &offset) == 0) && + (Cy_SMIF_MemEraseSector(QSPIPort, smif_blk_config->memConfig[0], + offset, (uint32_t)len, &smif_context) == CY_SMIF_SUCCESS)) { + ret = 0; + } + return ret; +} -/** - * @brief Lock external flash and finalize sector operations - * - * Invalidates the temporary sector tracking information stored in internal - * emulated EEPROM. This signals that any pending sector update has been - * completed and the temporary sector data is no longer needed. - * - * Should be called after a series of external flash write operations to - * commit the changes and free the temporary sector for future use. - * - * @note Erases the tmp_sector_info structure if it contains valid magic, - * which clears the EXT_FLASH_TMP_MAGIC marker. - */ void ext_flash_lock(void) { - tmp_sector_info info; - memcpy(&info, (void*)TMP_SECTOR_INFO_ADDR, sizeof(tmp_sector_info)); - - /* Clear temporary sector metadata to invalidate any active state */ - if (info.magic == EXT_FLASH_TMP_MAGIC) { - hal_flash_erase(TMP_SECTOR_INFO_ADDR, ROW_SIZE); - } } void ext_flash_unlock(void) @@ -986,10 +995,61 @@ void ext_flash_unlock(void) #if defined(DUALBANK_SWAP) && defined(__WOLFBOOT) -#if (WOLFBOOT_BOOTLOADER_START != FLASH_BANK1_BASE) +#if (WOLFBOOT_ORIGIN != FLASH_BANK1_BASE) #error "The bootloader must point to FLASH_BANK1_BASE" #endif +static uint32_t RAMFUNCTION psoc6_bank_record_check(const active_bank *rec) +{ + return ~(rec->magic ^ rec->mapping ^ rec->seq); +} + +/* Returns the slot of the newest valid record, or -1 */ +static int RAMFUNCTION psoc6_bank_record_read(active_bank *out) +{ + active_bank rec; + uint32_t slot; + int best = -1; + + for (slot = 0; slot < BANK_RECORD_SLOTS; slot++) { + memcpy(&rec, PSOC6_FLASH_PTR(BANK_RECORD_ADDR(slot)), sizeof(rec)); + if ((rec.magic == BANKMODE_MAGIC) && + (rec.check == psoc6_bank_record_check(&rec)) && + ((rec.mapping == CY_FLASH_MAPPING_A) || + (rec.mapping == CY_FLASH_MAPPING_B)) && + ((best < 0) || ((int32_t)(rec.seq - out->seq) > 0))) { + *out = rec; + best = (int)slot; + } + } + return best; +} + +static int RAMFUNCTION psoc6_bank_record_write(uint32_t mapping) +{ + active_bank rec; + uint32_t seq = 1U; + uint32_t slot = 0U; + int cur; + int ret; + + cur = psoc6_bank_record_read(&rec); + if (cur >= 0) { + seq = rec.seq + 1U; + slot = (uint32_t)(1 - cur); + } + rec.magic = BANKMODE_MAGIC; + rec.mapping = mapping; + rec.seq = seq; + rec.check = psoc6_bank_record_check(&rec); + ret = hal_flash_erase(BANK_RECORD_ADDR(slot), ROW_SIZE); + if (ret == 0) { + ret = hal_flash_write(BANK_RECORD_ADDR(slot), (const uint8_t*)&rec, + sizeof(rec)); + } + return ret; +} + /** * @brief Copy bootloader from Bank 1 to Bank 2 for dual-bank operation * @@ -1000,30 +1060,30 @@ void ext_flash_unlock(void) * The function is idempotent - if both banks already contain identical * bootloader code, no flash operations are performed. * - * @note Bootloader size is defined by WOLFBOOT_BOOTLOADER_SIZE. + * @note Bootloader size is defined by BOOTLOADER_PARTITION_SIZE. * @note This function should be called once during initial setup or after * bootloader updates. */ void RAMFUNCTION fork_bootloader(void) { - uint8_t* src = (uint8_t*)FLASH_BANK1_BASE; - uint8_t* dst = (uint8_t*)(FLASH_BANK1_BASE + (CY_FLASH_SIZE / 2)); + const uint8_t* src = (const uint8_t*)PSOC6_FLASH_PTR(FLASH_BANK1_BASE); + const uint8_t* dst = (const uint8_t*)PSOC6_FLASH_PTR(FLASH_BANK2_COPY); uint32_t i; int ret; /* Skip fork if bootloader already identical in both banks (idempotent * operation) */ - if (memcmp(src, dst, WOLFBOOT_BOOTLOADER_SIZE) == 0) + if (memcmp(src, dst, BOOTLOADER_PARTITION_SIZE) == 0) return; hal_flash_unlock(); /* Erase entire bootloader region in Bank 2, then copy from Bank 1 row by * row */ - ret = hal_flash_erase((uintptr_t)dst, WOLFBOOT_BOOTLOADER_SIZE); + ret = hal_flash_erase(FLASH_BANK2_COPY, BOOTLOADER_PARTITION_SIZE); if (ret == 0) { - for (i = 0; i < WOLFBOOT_BOOTLOADER_SIZE; i += ROW_SIZE) { - ret = hal_flash_write((uintptr_t)(dst + i), src + i, ROW_SIZE); + for (i = 0; i < BOOTLOADER_PARTITION_SIZE; i += ROW_SIZE) { + ret = hal_flash_write(FLASH_BANK2_COPY + i, src + i, ROW_SIZE); if (ret != 0) { wolfBoot_printf( "Fork bootloader: write failed at offset 0x%X, ret=%d\n", i, @@ -1039,109 +1099,48 @@ void RAMFUNCTION fork_bootloader(void) hal_flash_lock(); } -/** - * @brief Swap active flash bank mapping and persist the new state - * - * Toggles between Bank A and Bank B flash mapping to switch which physical - * flash bank appears at the base address. This enables A/B firmware update - * schemes where new firmware is written to the inactive bank and activated - * by swapping. - * - * The current bank mapping is persisted to emulated EEPROM so it survives - * reset and can be restored on subsequent boots. - * - * State transitions: - * - Bank A -> Bank B - * - Bank B -> Bank A - * - Invalid/first boot -> Bank A (initialized) - * - * @note Waits for flash controller to complete before returning. - * @note Bank mode data is stored at ACTIVE_BANK_MODE_DATA_ADDRESS in - * emulated EEPROM. - */ +/* Toggle the bank mapping and record it; with no record, select bank A */ void RAMFUNCTION hal_flash_dualbank_swap(void) { - active_bank bankmode_data = *(active_bank*)ACTIVE_BANK_MODE_DATA_ADDRESS; + active_bank rec; + uint32_t mapping = CY_FLASH_MAPPING_A; - /* Read and validate saved bank mode from emulated EEPROM area */ - if (bankmode_data.magic == BANKMODE_MAGIC) { - if (bankmode_data.mapping == CY_FLASH_MAPPING_A) { - bankmode_data.mapping = CY_FLASH_MAPPING_B; - } - else { - bankmode_data.mapping = CY_FLASH_MAPPING_A; - } - } - else { - /* First boot: initialize to Bank A */ - bankmode_data.magic = BANKMODE_MAGIC; - bankmode_data.mapping = CY_FLASH_MAPPING_A; + if ((psoc6_bank_record_read(&rec) >= 0) && + (rec.mapping == CY_FLASH_MAPPING_A)) { + mapping = CY_FLASH_MAPPING_B; } - - /* Set the active bank mapping */ - Cy_Flashc_SetMain_Flash_Mapping(bankmode_data.mapping); - - /* Persist bank mode to emulated EEPROM flash area */ - hal_flash_erase(ACTIVE_BANK_MODE_DATA_ADDRESS, ROW_SIZE); - hal_flash_write(ACTIVE_BANK_MODE_DATA_ADDRESS, (uint8_t*)&bankmode_data, - sizeof(active_bank)); + Cy_Flashc_SetMain_Flash_Mapping((cy_en_maptype_t)mapping); + (void)psoc6_bank_record_write(mapping); /* Wait for flash command completion */ while (FLASHC_FLASH_CMD & FLASHC_V2_FLASH_CMD_INV_Msk) ; } -/** - * @brief Initialize dual-bank flash mode on system startup - * - * Sets up the flash controller for dual-bank operation and restores the - * previously active bank mapping from persistent storage. Called during - * bootloader initialization. - * - * Initialization sequence: - * 1. Fork bootloader to Bank 2 (if not already present) - * 2. Enable dual-bank mode in flash controller - * 3. Read saved bank mapping from emulated EEPROM - * 4. Restore bank mapping or initialize to Bank A on first boot - * 5. Wait for flash controller ready - * - * @return 0 on success - * - * @note Must be called before any firmware verification or boot operations - * that depend on correct flash mapping. - * @note On first boot (no valid saved state), initializes to Bank A and - * persists this configuration. - */ +/* Enter dual-bank mode with the recorded mapping, or bank A on first boot */ int RAMFUNCTION bankmode_init(void) { + active_bank rec; + int ret = 0; + /* Ensure bootloader is present in both banks */ fork_bootloader(); - /* Enable dual bank mode */ Cy_Flashc_SetMainBankMode(CY_FLASH_DUAL_BANK_MODE); - __ASM volatile("isb 0xF" ::: "memory"); + __ISB(); - /* Read and restore bank mode from emulated EEPROM flash area */ - active_bank bankmode_data = *(active_bank*)ACTIVE_BANK_MODE_DATA_ADDRESS; - if (bankmode_data.magic == BANKMODE_MAGIC) { - /* Restore previously saved bank mapping */ - Cy_Flashc_SetMain_Flash_Mapping(bankmode_data.mapping); + if (psoc6_bank_record_read(&rec) >= 0) { + Cy_Flashc_SetMain_Flash_Mapping((cy_en_maptype_t)rec.mapping); } else { - /* First boot: initialize to Bank A */ - bankmode_data.magic = BANKMODE_MAGIC; - bankmode_data.mapping = CY_FLASH_MAPPING_A; - - /* Save initial bank mode to emulated EEPROM flash area */ - hal_flash_erase(ACTIVE_BANK_MODE_DATA_ADDRESS, ROW_SIZE); - hal_flash_write(ACTIVE_BANK_MODE_DATA_ADDRESS, (uint8_t*)&bankmode_data, - sizeof(active_bank)); + Cy_Flashc_SetMain_Flash_Mapping(CY_FLASH_MAPPING_A); + ret = psoc6_bank_record_write(CY_FLASH_MAPPING_A); } /* Wait for flash controller ready */ while (FLASHC_FLASH_CMD & FLASHC_V2_FLASH_CMD_INV_Msk) ; - return 0; + return ret; } #endif /* DUALBANK_SWAP && __WOLFBOOT */ diff --git a/hal/psoc6.h b/hal/psoc6.h new file mode 100644 index 0000000000..bbdbc79e0d --- /dev/null +++ b/hal/psoc6.h @@ -0,0 +1,97 @@ +/* psoc6.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 + */ + +#ifndef WOLFBOOT_HAL_PSOC6_H +#define WOLFBOOT_HAL_PSOC6_H + +#define PSOC6_CPU_FREQ 100000000UL +#define PSOC6_CLK_PERI_HZ PSOC6_CPU_FREQ + +#define PSOC6_FLASH_BASE 0x10000000UL +#define PSOC6_FLASH_SIZE 0x00200000UL +#define PSOC6_WFLASH_BASE 0x14000000UL +#define PSOC6_WFLASH_SIZE 0x00008000UL +#define PSOC6_FLASH_ROW_SIZE 512U +#define PSOC6_FLASH_BANK2_BASE 0x12000000UL +#define PSOC6_FLASH_BANK_SIZE (PSOC6_FLASH_SIZE / 2U) + +/* CM0+ vector table: 16 system exceptions + 32 interrupts */ +#define PSOC6_CM0P_SYS_VECTORS 16U +#define PSOC6_CM0P_VECTORS 48U +#define PSOC6_HARDFAULT_VECTOR 3U + +/* The CM4 VTOR needs 1 KB alignment for its 16 + 168 vectors */ +#define PSOC6_CM4_VTOR_ALIGN 0x400U + +#define PSOC6_UART_BAUD 115200U +#define PSOC6_UART_OVERSAMPLE 16U +/* 16.5-bit fractional divider: (int + 1) + frac / 32 */ +#define PSOC6_UART_DIV_X32 \ + ((((PSOC6_CLK_PERI_HZ) * 32UL) + \ + (((PSOC6_UART_BAUD) * (PSOC6_UART_OVERSAMPLE)) / 2UL)) / \ + ((PSOC6_UART_BAUD) * (PSOC6_UART_OVERSAMPLE))) +#define PSOC6_UART_DIV_INT ((PSOC6_UART_DIV_X32 / 32UL) - 1UL) +#define PSOC6_UART_DIV_FRAC (PSOC6_UART_DIV_X32 % 32UL) +#define PSOC6_UART_DIV_NUM 0U + +#define PSOC6_TIMER_HZ 1000000UL +#define PSOC6_TIMER_DIV ((PSOC6_CLK_PERI_HZ / PSOC6_TIMER_HZ) - 1UL) +#define PSOC6_TIMER_DIV_NUM 0U +#define PSOC6_TIMER_TCPWM TCPWM0 +#define PSOC6_TIMER_CNT 0U +#define PSOC6_TIMER_PCLK PCLK_TCPWM0_CLOCKS0 + +#if defined(PSOC6_BOARD_CY8CEVAL_062S2) + #define PSOC6_UART_SCB SCB0 + #define PSOC6_UART_PCLK PCLK_SCB0_CLOCK + #define PSOC6_UART_PORT GPIO_PRT0 + #define PSOC6_UART_RX_PIN 2U + #define PSOC6_UART_TX_PIN 3U + #define PSOC6_UART_RX_MUX P0_2_SCB0_UART_RX + #define PSOC6_UART_TX_MUX P0_3_SCB0_UART_TX + #define PSOC6_LED_PORT GPIO_PRT11 + #define PSOC6_LED_PIN 1U +#elif defined(PSOC6_BOARD_CY8CKIT_062S2_43012) + #define PSOC6_UART_SCB SCB5 + #define PSOC6_UART_PCLK PCLK_SCB5_CLOCK + #define PSOC6_UART_PORT GPIO_PRT5 + #define PSOC6_UART_RX_PIN 0U + #define PSOC6_UART_TX_PIN 1U + #define PSOC6_UART_RX_MUX P5_0_SCB5_UART_RX + #define PSOC6_UART_TX_MUX P5_1_SCB5_UART_TX + #define PSOC6_LED_PORT GPIO_PRT1 + #define PSOC6_LED_PIN 5U +#else + #error "Set PSOC6_BOARD to CY8CEVAL_062S2 or CY8CKIT_062S2_43012" +#endif + +/* Both kits drive the user LEDs active low */ +#define PSOC6_LED_ON 0U +#define PSOC6_LED_OFF 1U + +/* Host unit tests point flash reads at a model */ +#ifndef PSOC6_FLASH_PTR +#define PSOC6_FLASH_PTR(a) ((const void *)(uintptr_t)(a)) +#endif + +int psoc6_flash_range_ok(uint32_t address, uint32_t len); + +#endif /* WOLFBOOT_HAL_PSOC6_H */ diff --git a/hal/psoc6.ld b/hal/psoc6.ld index 2d349d0361..ca40120f0a 100644 --- a/hal/psoc6.ld +++ b/hal/psoc6.ld @@ -1,7 +1,7 @@ MEMORY { FLASH (rx) : ORIGIN = 0x10000000, LENGTH = @BOOTLOADER_PARTITION_SIZE@ - RAM (rwx) : ORIGIN = 0x08000000, LENGTH = 64K + RAM (rwx) : ORIGIN = 0x08000000, LENGTH = 0x60000 } SECTIONS @@ -14,8 +14,12 @@ SECTIONS KEEP(*(.isr_vector)) KEEP(*(.vectors)) . = ALIGN(0x400); - *(.text*) - *(.rodata*) + *(EXCLUDE_FILE(*cy_flash.o *cy_ipc_*.o *cy_syslib.o *cy_syslib_ext.o + *cy_device.o *system_psoc6_cm0plus.o *cy_sysclk.o *cy_smif*.o + *libgcc.a:*) .text*) + *(EXCLUDE_FILE(*cy_flash.o *cy_ipc_*.o *cy_syslib.o *cy_syslib_ext.o + *cy_device.o *system_psoc6_cm0plus.o *cy_sysclk.o *cy_smif*.o + *libgcc.a:*) .rodata*) *(.init*) *(.fini*) . = ALIGN(4); @@ -64,6 +68,18 @@ SECTIONS KEEP(*(.cy_ramfunc*)) . = ALIGN(4); + /* PDL flash and QSPI paths, which a bootloader self-update runs after + * erasing the bootloader region */ + *cy_flash.o(.text* .rodata*) + *cy_ipc_*.o(.text* .rodata*) + *cy_syslib.o(.text* .rodata*) + *cy_syslib_ext.o(.text* .rodata*) + *cy_device.o(.text* .rodata*) + *system_psoc6_cm0plus.o(.text* .rodata*) + *cy_sysclk.o(.text* .rodata*) + *cy_smif*.o(.text* .rodata*) + *libgcc.a:*(.text* .rodata*) + . = ALIGN(4); _end_data = .; __data_end__ = .; @@ -84,6 +100,7 @@ SECTIONS _end = .; . = ALIGN(0x100); __ramVectors = .; + . = . + 0xC0; } > RAM . = ALIGN(4); } diff --git a/include/hal.h b/include/hal.h index 05b7f33338..813426ef2f 100644 --- a/include/hal.h +++ b/include/hal.h @@ -44,6 +44,10 @@ extern int wolfBoot_fit_check_dest(void *dst, uint32_t len); #else extern void do_boot(const uint32_t *app_offset); #endif +#ifdef WOLFBOOT_BOOT_REMOTE_CORE +/* Starts the verified image on another core; does not return */ +void hal_boot_remote_core(const uint32_t *app_offset); +#endif extern void arch_reboot(void); /* Simulator-only calls */ diff --git a/src/boot_arm.c b/src/boot_arm.c index e22d2ddafd..8acb17d9d0 100644 --- a/src/boot_arm.c +++ b/src/boot_arm.c @@ -25,6 +25,7 @@ #include "loader.h" #include "printf.h" #include "wolfboot/wolfboot.h" +#include "hal.h" extern unsigned int _start_text; extern unsigned int _stored_data; @@ -532,6 +533,9 @@ static uint32_t ns_entry; void RAMFUNCTION do_boot(const uint32_t *app_offset) { +#ifdef WOLFBOOT_BOOT_REMOTE_CORE + hal_boot_remote_core(app_offset); +#endif #if defined(CORTEX_R5) (void)app_entry; (void)app_end_stack; diff --git a/test-app/ARM-psoc6-cm4.ld b/test-app/ARM-psoc6-cm4.ld new file mode 100644 index 0000000000..477b532161 --- /dev/null +++ b/test-app/ARM-psoc6-cm4.ld @@ -0,0 +1,97 @@ +MEMORY +{ + FLASH (rx) : ORIGIN = @WOLFBOOT_TEST_APP_ADDRESS@, LENGTH = @WOLFBOOT_TEST_APP_SIZE@ + RAM (rwx) : ORIGIN = 0x08060000, LENGTH = 0x9F800 +} + +SECTIONS +{ + + .text : + { + _start_text = .; + __Vectors = .; + KEEP(*(.isr_vector)) + KEEP(*(.vectors)) + . = ALIGN(0x400); + *(.text*) + *(.rodata*) + *(.init*) + *(.fini*) + . = ALIGN(4); + _end_text = .; + __etext = .; + } > FLASH + + .edidx : + { + . = ALIGN(4); + *(.ARM.exidx*) + } > FLASH + + _stored_data = .; + + .data : AT (_stored_data) + { + _start_data = .; + __data_start__ = .; + + KEEP(*(.data*)) + . = ALIGN(4); + KEEP(*(.ramcode)) + . = ALIGN(4); + /* preinit data */ + PROVIDE_HIDDEN (__preinit_array_start = .); + KEEP(*(.preinit_array)) + PROVIDE_HIDDEN (__preinit_array_end = .); + + . = ALIGN(4); + /* init data */ + PROVIDE_HIDDEN (__init_array_start = .); + KEEP(*(SORT(.init_array.*))) + KEEP(*(.init_array)) + PROVIDE_HIDDEN (__init_array_end = .); + + . = ALIGN(4); + /* finit data */ + PROVIDE_HIDDEN (__fini_array_start = .); + KEEP(*(SORT(.fini_array.*))) + KEEP(*(.fini_array)) + PROVIDE_HIDDEN (__fini_array_end = .); + + KEEP(*(.jcr*)) + . = ALIGN(4); + + KEEP(*(.cy_ramfunc*)) + . = ALIGN(4); + + _end_data = .; + __data_end__ = .; + + } > RAM + + .bss (NOLOAD) : + { + _start_bss = .; + __bss_start__ = .; + __zero_table_start__ = .; + *(.bss*) + *(COMMON) + . = ALIGN(4); + _end_bss = .; + __bss_end__ = .; + __zero_table_end__ = .; + _end = .; + . = ALIGN(0x100); + __ramVectors = .; + } > RAM + . = ALIGN(4); +} + +END_STACK = ORIGIN(RAM) + LENGTH(RAM); +PROVIDE(_end_stack = END_STACK); +PROVIDE(_start_heap = END_STACK); + +__StackTop = ORIGIN(RAM) + LENGTH(RAM); +__StackLimit = __StackTop - 0x4000; +PROVIDE(__stack = __StackTop); diff --git a/test-app/ARM-psoc6.ld b/test-app/ARM-psoc6.ld index 58a791e195..0514c95687 100644 --- a/test-app/ARM-psoc6.ld +++ b/test-app/ARM-psoc6.ld @@ -1,7 +1,7 @@ MEMORY { FLASH (rx) : ORIGIN = @WOLFBOOT_TEST_APP_ADDRESS@, LENGTH = @WOLFBOOT_TEST_APP_SIZE@ - RAM (rwx) : ORIGIN = 0x08000000, LENGTH = 64K + RAM (rwx) : ORIGIN = 0x08000000, LENGTH = 0x60000 } SECTIONS diff --git a/test-app/Makefile b/test-app/Makefile index 9b96d45d87..72aed70d97 100644 --- a/test-app/Makefile +++ b/test-app/Makefile @@ -1532,20 +1532,30 @@ ifeq ($(TARGET),mpfs250) CFLAGS+=-DRAMFUNCTION='__attribute__((used,section(".ramcode")))' endif -# $(CYPRESS_PDL)/devices/templates/COMPONENT_MTB/COMPONENT_CM0P/system_psoc6_cm0plus.o - ifeq ($(TARGET),psoc6) - LSCRIPT_TEMPLATE:=ARM-psoc6.ld - APP_OBJS+= $(CYPRESS_PDL)/drivers/source/cy_gpio.o - APP_OBJS+= $(CYPRESS_PDL)/drivers/source/cy_device.o - CFLAGS+=-I$(CYPRESS_PDL)/drivers/include/ - CFLAGS+=-I$(CYPRESS_PDL)/devices/include - CFLAGS+=-I$(CYPRESS_PDL)/cmsis/include - CFLAGS+=-I$(CYPRESS_TARGET_LIB) - CFLAGS+=-I$(CYPRESS_CORE_LIB)/include - CFLAGS+=-I$(CYPRESS_PDL)/devices/include/ip - CFLAGS+=-I$(CYPRESS_PDL)/devices/templates/COMPONENT_MTB - CFLAGS+=-DCY8C624ABZI_D44 + PSOC6_APP_SRCS:=$(patsubst %.o,%.c,$(filter-out %.S %cy_syslib_ext.o,$(PSOC6_PDL_OBJS))) + ifeq ($(DEBUG_UART),1) + PSOC6_APP_SRCS+=$(PSOC6_UART_OBJS:.o=.c) + endif + ifeq ($(EXT_FLASH),1) + PSOC6_APP_SRCS+=$(PSOC6_SMIF_OBJS:.o=.c) + endif + ifeq ($(PSOC6_CM4_APP),1) + LSCRIPT_TEMPLATE:=ARM-psoc6-cm4.ld + CFLAGS+=-mcpu=cortex-m4 + LDFLAGS+=-mcpu=cortex-m4 + PSOC6_APP_SRCS+=$(CYPRESS_PDL_TEMPLATE)/COMPONENT_CM4/system_psoc6_cm4.c + else + LSCRIPT_TEMPLATE:=ARM-psoc6.ld + PSOC6_APP_SRCS+=$(CYPRESS_PDL_TEMPLATE)/COMPONENT_CM0P/system_psoc6_cm0plus.c + endif + # wolfBoot's PDL and string objects are CM0+ builds; the app may run on the CM4 + APP_OBJS:=$(patsubst ../src/string.o,psoc6_pdl/string.o,$(APP_OBJS)) + APP_OBJS+=$(addprefix psoc6_pdl/,$(notdir $(PSOC6_APP_SRCS:.c=.o))) + APP_OBJS+=psoc6_pdl/cy_syslib_ext.o + vpath %.c $(sort $(dir $(PSOC6_APP_SRCS))) + vpath string.c ../src + vpath %.S $(CYPRESS_PDL)/drivers/source/TOOLCHAIN_GCC_ARM endif ifeq ($(TARGET),x86_fsp_qemu) @@ -1865,6 +1875,16 @@ endif ../hal/$(TARGET)_ns.o: ../hal/$(TARGET).c FORCE $(Q)$(CC) $(CFLAGS) -c -o $(@) ../hal/$(TARGET).c -DNONSECURE_APP +psoc6_pdl/%.o: %.c + @echo "\t[CC-$(ARCH)] $@" + $(Q)mkdir -p $(dir $@) + $(Q)$(CC) $(CFLAGS) -c $(OUTPUT_FLAG) $@ $< + +psoc6_pdl/%.o: %.S + @echo "\t[AS-$(ARCH)] $@" + $(Q)mkdir -p $(dir $@) + $(Q)$(CC) $(CFLAGS) -c $(OUTPUT_FLAG) $@ $< + mcxw_ns/%.o: %.c @echo "\t[CC-$(ARCH)] $@" $(Q)mkdir -p $(dir $@) @@ -1944,7 +1964,7 @@ clean: $(Q)rm -f *.bin *.elf *.map tags *.o $(LSCRIPT) $(APP_OBJS) wcs/*.o $(Q)rm -rf $(WOLFSSL_LOCAL_OBJDIR) $(WOLFTPM_LOCAL_OBJDIR) \ $(WOLFHSM_LOCAL_OBJDIR) $(WOLFIP_LOCAL_OBJDIR) mcxw_ns lpc55s69_ns \ - ../generated/test-app + psoc6_pdl ../generated/test-app $(LSCRIPT): $(LSCRIPT_TEMPLATE) FORCE $(Q)printf "%d" $(WOLFBOOT_PARTITION_BOOT_ADDRESS) > .wolfboot-offset diff --git a/test-app/app_psoc6.c b/test-app/app_psoc6.c index 9123298ee6..9aec72e1ff 100644 --- a/test-app/app_psoc6.c +++ b/test-app/app_psoc6.c @@ -1,6 +1,6 @@ -/* main.c +/* app_psoc6.c * - * Test bare-metal boot-led-on application + * Test bare-metal application for the Infineon PSoC 6 (CM0+ or CM4) * * Copyright (C) 2026 wolfSSL Inc. * @@ -21,22 +21,45 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA */ -#include #include -#include -#include "wolfboot/wolfboot.h" +#include "cy_device_headers.h" +#include "cy_device.h" +#include "cy_flash.h" #include "cy_gpio.h" -#define BOOT_LED P1_5 -#define BOOT_D7 P5_7 +#include "target.h" +#include "wolfboot/wolfboot.h" +#include "printf.h" +#include "hal/psoc6.h" + +#ifdef PSOC6_CM4_APP +#define APP_CORE "CM4" +#else +#define APP_CORE "CM0+" +#endif +void main(void) +{ + uint32_t boot_ver; -void main(void) { Cy_PDL_Init(CY_DEVICE_CFG); - Cy_GPIO_Pin_FastInit(P5_7_PORT, P5_7_NUM, CY_GPIO_DM_PULLUP, 0UL, P5_7_GPIO); - Cy_GPIO_Pin_FastInit(P1_5_PORT, P1_5_NUM, CY_GPIO_DM_PULLUP, 0UL, P1_5_GPIO); - while(1) - ; -} + Cy_Flash_Init(); + Cy_GPIO_Pin_FastInit(PSOC6_LED_PORT, PSOC6_LED_PIN, + CY_GPIO_DM_STRONG_IN_OFF, PSOC6_LED_OFF, HSIOM_SEL_GPIO); + boot_ver = wolfBoot_current_firmware_version(); + wolfBoot_printf("Hello from PSoC6 " APP_CORE " firmware version %d\n", + boot_ver); + + if (boot_ver != 1U) { + wolfBoot_success(); + wolfBoot_printf("Update successful, firmware version %d confirmed\n", + boot_ver); + Cy_GPIO_Write(PSOC6_LED_PORT, PSOC6_LED_PIN, PSOC6_LED_ON); + } + + while (1) { + __WFI(); + } +} diff --git a/test-app/startup_arm.c b/test-app/startup_arm.c index 5f5a5e12b7..b422927e39 100644 --- a/test-app/startup_arm.c +++ b/test-app/startup_arm.c @@ -131,12 +131,19 @@ void isr_empty(void) } +#if defined(TARGET_psoc6) && !defined(PSOC6_CM4_APP) +/* PSoC6 flash calls run in the boot ROM's CM0+ NMI handler */ +# define isr_NMI ((void (*)(void))(0x0000000D)) +#else +# define isr_NMI isr_fault +#endif + __attribute__ ((section(".isr_vector"))) void (* const IV[])(void) = { (void (*)(void))(&_end_stack), isr_reset, // Reset - isr_fault, // NMI + isr_NMI, // NMI isr_fault, // HardFault isr_memfault, // MemFault isr_busfault, // BusFault diff --git a/tools/ci/fetch-psoc6-pdl.sh b/tools/ci/fetch-psoc6-pdl.sh new file mode 100755 index 0000000000..0206e24c41 --- /dev/null +++ b/tools/ci/fetch-psoc6-pdl.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# fetch-psoc6-pdl.sh -- fetch the Infineon libraries the PSoC 6 port builds +# against (mtb-pdl-cat1, mtb-template-cat1, core-lib, cmsis) at pinned +# releases, into /, where the psoc6 configs expect them in lib/. +# +# usage: tools/ci/fetch-psoc6-pdl.sh [dir] (default: lib) +# make -C tools/unit-tests PSOC6_LIBS= + +set -e + +OUT="${1:-lib}" + +fetch() { + if [ -e "$OUT/$1" ]; then + echo "$OUT/$1 already exists" >&2 + exit 1 + fi + git -c advice.detachedHead=false clone -q --depth 1 --branch "$2" \ + "https://github.com/Infineon/$1" "$OUT/$1" +} + +mkdir -p "$OUT" +fetch mtb-pdl-cat1 release-v3.24.0 +fetch mtb-template-cat1 release-v1.12.0 +fetch core-lib release-v1.8.0 +fetch cmsis release-v6.1.1 diff --git a/tools/config.mk b/tools/config.mk index b51fc07ce2..52889048f1 100644 --- a/tools/config.mk +++ b/tools/config.mk @@ -13,8 +13,9 @@ ifeq ($(ARCH),) NXP_CUSTOM_DCD=0 NXP_CUSTOM_DCD_OBJS="" STM32CUBE?=$(HOME)/STM32Cube/Repository/STM32Cube_FW_WB_V1.3.0 - CYPRESS_PDL?=$(HOME)/src/psoc6pdl - CYPRESS_TARGET_LIB?=$(HOME)/src/TARGET_CY8CKIT-062S2-43012 + CYPRESS_PDL?=$(HOME)/src/mtb-pdl-cat1 + CYPRESS_CMSIS?=$(HOME)/src/cmsis + CYPRESS_TEMPLATE_LIB?=$(HOME)/src/mtb-template-cat1 CYPRESS_CORE_LIB?=$(HOME)/src/cypress-core-lib DEBUG?=0 VTOR?=1 @@ -119,11 +120,11 @@ WATCHDOG?=0 WATCHDOG_HANDOFF?=0 CONFIG_VARS:= ARCH TARGET SIGN HASH MCUXSDK MCUXPRESSO MCUXPRESSO_CPU MCUXPRESSO_DRIVERS \ - MCUXPRESSO_CMSIS FREEDOM_E_SDK STM32CUBE CYPRESS_PDL CYPRESS_CORE_LIB CYPRESS_TARGET_LIB DEBUG VTOR \ + MCUXPRESSO_CMSIS FREEDOM_E_SDK STM32CUBE CYPRESS_PDL CYPRESS_CORE_LIB CYPRESS_CMSIS CYPRESS_TEMPLATE_LIB DEBUG VTOR \ CORTEX_M0 CORTEX_M7 CORTEX_M23 CORTEX_M33 CORTEX_M55 NO_ASM EXT_FLASH SPI_FLASH NO_XIP UART_FLASH ALLOW_DOWNGRADE NVM_FLASH_WRITEONCE CUSTOM_PARTITION_TRAILER \ NVM_FLASH_JOURNAL NVM_FLASH_JOURNAL_DUAL NVM_FLASH_ECC FLASH_WRITE_UNIT \ - DISABLE_BACKUP WOLFBOOT_VERSION V NO_MPU ENCRYPT FLAGS_HOME FLAGS_INVERT \ - SPMATH SPMATHALL RAM_CODE DUALBANK_SWAP IMAGE_HEADER_SIZE PKA TZEN PSOC6_CRYPTO \ + DISABLE_BACKUP WOLFBOOT_VERSION V NO_MPU ENCRYPT FLAGS_HOME FLAGS_INVERT NO_SWAP_EXT \ + SPMATH SPMATHALL RAM_CODE DUALBANK_SWAP IMAGE_HEADER_SIZE PKA TZEN PSOC6_CRYPTO PSOC6_BOARD PSOC6_CM4_APP \ WOLFTPM WOLFBOOT_TPM_VERIFY MEASURED_BOOT WOLFBOOT_TPM_SEAL WOLFBOOT_TPM_KEYSTORE WOLFBOOT_SECURE_APP WOLFBOOT_SECURE_HANDOFF_ADDRESS \ WOLFBOOT_TPM_MFG_AUTH_DERIVE \ WOLFBOOT_ATTESTATION_IAK \ diff --git a/tools/scripts/check-ramcode.py b/tools/scripts/check-ramcode.py new file mode 100755 index 0000000000..77bec2b35b --- /dev/null +++ b/tools/scripts/check-ramcode.py @@ -0,0 +1,391 @@ +#!/usr/bin/env python3 +# check-ramcode.py: fail when code reachable from an entry function, or data +# it reads, is outside RAM, such as a RAM_CODE self-update calling into or +# reading from the flash it erases. +# +# usage: check-ramcode.py +# check-ramcode.py --selftest + +import bisect +import os +import re +import subprocess +import sys +import tempfile + + +def tool(*args): + return subprocess.run(list(args), capture_output=True, text=True, + check=True).stdout + + +def check(elf, entry, ram_lo, ram_hi): + # RAM_CODE puts code and data in one section, so nm cannot tell them apart + objects = set() + marks = [] + for line in tool("arm-none-eabi-readelf", "-sW", elf).splitlines(): + f = line.split() + if len(f) == 8 and f[3] == "OBJECT": + objects.add(f[7]) + # Mapping symbols mark the literal pools inside functions + elif len(f) == 8 and f[7][:2] in ("$d", "$t", "$a"): + marks.append((int(f[1], 16), f[7][1])) + marks.sort() + syms = [] + objs = [] + for line in tool("arm-none-eabi-nm", "-S", "-n", elf).splitlines(): + f = line.split() + if len(f) == 4 and f[3] in objects: + objs.append((int(f[0], 16), int(f[1], 16), "data " + f[3])) + # Assembly functions without .size print no size field + elif len(f) == 4 and f[2] in "TtWw": + syms.append([int(f[0], 16) & ~1, int(f[1], 16), f[3]]) + elif len(f) == 3 and f[1] in "TtWw": + syms.append([int(f[0], 16) & ~1, 0, f[2]]) + syms.sort() + objs.sort() + sections = [] + image = [] + sec_row = re.compile(r"^\s*\[\s*\d+\]\s+\S+\s+(\S+)\s+([0-9a-f]+)\s+" + r"[0-9a-f]+\s+([0-9a-f]+)") + for line in tool("arm-none-eabi-readelf", "-SW", elf).splitlines(): + m = sec_row.match(line) + if m and int(m.group(2), 16) != 0: + sec = (int(m.group(2), 16), int(m.group(2), 16) + + int(m.group(3), 16)) + sections.append(sec) + if m.group(1) != "NOBITS" and not ram_lo <= sec[0] < ram_hi: + image.append(sec) + + def section_end(addr): + return min([e for b, e in sections if b <= addr < e] or [addr + 2]) + + # An unsized function ends at the next symbol, within its own section + for i, f in enumerate(syms): + if f[1] == 0: + end = syms[i + 1][0] if i + 1 < len(syms) else f[0] + 2 + f[1] = max(min(end, section_end(f[0])) - f[0], 2) + funcs = [tuple(f) for f in syms] + starts = [f[0] for f in funcs] + obj_starts = [o[0] for o in objs] + names = {f[2]: f for f in funcs} + places = dict(names) + places.update((o[2], o) for o in objs) + # Section contents, for the words of data objects + mem = {} + row = re.compile(r"^ ([0-9a-f]+) ((?:[0-9a-f]{2,8} ?){1,4})") + alloc = True + for line in tool("arm-none-eabi-objdump", "-s", elf).splitlines(): + if line.startswith("Contents of section "): + alloc = not line.split()[3].startswith((".debug", ".comment", + ".ARM.attributes")) + continue + m = row.match(line) + if m and alloc: + data = bytes.fromhex(m.group(2).replace(" ", "")) + base = int(m.group(1), 16) + for i, byte in enumerate(data): + mem[base + i] = byte + # The self-update writes the new image at the image base + image_base = min([a for a in mem if not ram_lo <= a < ram_hi] or [-1]) + if entry not in names: + print("%s: no function %s" % (elf, entry)) + return 1 + + def owner(addr): + i = bisect.bisect_right(starts, addr) - 1 + if i >= 0 and addr < funcs[i][0] + max(funcs[i][1], 2): + return funcs[i] + return None + + def obj_owner(addr): + i = bisect.bisect_right(obj_starts, addr) - 1 + if i >= 0 and addr < objs[i][0] + objs[i][1]: + return objs[i] + return None + + edges = {} + + # A word is a Thumb function pointer, a pointer to data, or a constant; + # data in the flash image counts even without a symbol + def link(src, value, branch=False): + dst = owner(value & ~1) if value & 1 else None + if dst is None or dst[0] != value & ~1: + dst = None + # The entry's own image base literal is its write target + if not branch and (value != image_base or src[2] != entry): + dst = obj_owner(value) + if dst is None and any(b <= value < e for b, e in image): + dst = (value, 1, "data at 0x%08x" % value) + places[dst[2]] = dst + if dst is not None and dst != src: + edges.setdefault(src[2], set()).add(dst[2]) + + def word(addr): + if all(a in mem for a in range(addr, addr + 4)): + return int.from_bytes(bytes(mem[a] for a in range(addr, addr + 4)), + "little") + return None + + # Literal words, read from the bytes: objdump may decode a pool that + # starts on a halfword boundary at the wrong offset + pools = [] + for i, (addr, kind) in enumerate(marks): + func = owner(addr) + if kind != "d" or func is None: + continue + end = func[0] + func[1] + if i + 1 < len(marks): + end = min(end, marks[i + 1][0]) + pools.append((addr, end)) + for at in range((addr + 3) & ~3, end - 3, 4): + value = word(at) + if value is not None: + link(func, value) + pool_starts = [p[0] for p in pools] + + def in_pool(addr): + i = bisect.bisect_right(pool_starts, addr) - 1 + return i >= 0 and addr < pools[i][1] + + insn = re.compile(r"^\s*([0-9a-f]+):\s+(?:[0-9a-f]{4,8} )+\s*(\S*)\s*(.*)$") + pc_load = re.compile(r"(r\d+), \[pc, #-?\d+\]\s*@ \(([0-9a-f]+)") + imm = r"#(-?(?:0x[0-9a-f]+|\d+))" + # Register constants built in code count like literals. The entry may not + # read through the image base; callees get it as their write destination, + # and a flash driver may read that back to skip unchanged rows + regs = {} + func = None + base_read = False + for line in tool("arm-none-eabi-objdump", "-D", elf).splitlines(): + m = insn.match(line) + if not m: + continue + here = int(m.group(1), 16) + src = owner(here) + if src is None or in_pool(here): + continue + if src is not func: + func, regs = src, {} + op, args = m.group(2), m.group(3) + t = re.match(r"([0-9a-f]+) <", args) + if op.startswith("b") and t: + link(src, int(t.group(1), 16) | 1, branch=True) + if op.startswith(("bl", "blx")): + for r in ("r0", "r1", "r2", "r3", "r12"): + regs.pop(r, None) + continue + if op.startswith(("ldr", "ldm")): + base = re.search(r"\[(r\d+)[,\]]", args) if op.startswith("ldr") \ + else re.match(r"(r\d+)", args) + if (src[2] == entry) and base and \ + (regs.get(base.group(1)) == image_base): + base_read = True + dst = re.match(r"(r\d+)(?:,|$)", args) + if dst is None: + continue + reg = dst.group(1) + value = None + t = pc_load.match(args) if op.startswith("ldr") else None + u = re.match(r"r\d+, " + imm + "$", args) + v = re.match(r"r\d+, (r\d+), " + imm + "$", args) or \ + re.match(r"r\d+, " + imm + "$", args) + w = re.match(r"r\d+, (r\d+)$", args) + if t: + value = word(int(t.group(2), 16)) + elif op.startswith(("mov", "cpy")) and w: + value = regs.get(w.group(1)) + elif op.startswith(("mov", "movw")) and not op.startswith("movt") and u: + value = int(u.group(1), 0) & 0xFFFFFFFF + elif op.startswith("movt") and u and reg in regs: + value = (regs[reg] & 0xFFFF) | ((int(u.group(1), 0) & 0xFFFF) << 16) + elif op.startswith(("lsl", "add", "sub")) and v: + srcreg = v.group(1) if v.re.groups == 2 else reg + amount = int(v.group(v.re.groups), 0) + if srcreg in regs: + if op.startswith("lsl"): + value = (regs[srcreg] << amount) & 0xFFFFFFFF + elif op.startswith("add"): + value = (regs[srcreg] + amount) & 0xFFFFFFFF + else: + value = (regs[srcreg] - amount) & 0xFFFFFFFF + if value is None: + regs.pop(reg, None) + continue + regs[reg] = value + # A pool placed after the function's .size has no owner to scan it + link(src, value) + + # Data objects may hold callbacks or further pointers + for obj in objs: + for addr in range(obj[0], obj[0] + obj[1] - 3, 4): + value = word(addr) + if value is not None: + link(obj, value) + + seen, todo, bad = set(), [entry], [] + if base_read: + bad.append("0x%08x read of the image base by %s" % (image_base, entry)) + while todo: + name = todo.pop() + if name in seen: + continue + seen.add(name) + if not ram_lo <= places[name][0] < ram_hi: + bad.append("0x%08x %s" % (places[name][0], name)) + todo.extend(edges.get(name, ())) + if bad: + print("%s: reachable from %s but outside RAM:" % (elf, entry)) + print("\n".join(sorted(bad))) + return 1 + print("%s: %d functions reachable from %s, all in RAM" % ( + elf, len([n for n in seen if n in names]), entry)) + return 0 + + +SELFTEST_SRC = """ + .syntax unified + .thumb + .section .flashbase, "a" + .type vectors, %%object +vectors: + .word flash_helper + .size vectors, . - vectors + .section .flashdata, "a" + .byte 0 + .type bytes, %%object +bytes: + .byte 1, 2, 3 + .size bytes, . - bytes +.Lflash_string: + .asciz "flash" + .section .ramdata, "aw" +.Lram_string: + .asciz "ram" + .section .ramtext, "ax" + .global entry + .thumb_func +entry: + bl ram_helper + bl %(callee)s + ldr r0, =callbacks + ldr r0, [r0] + blx r0 + ldr r1, =vectors + ldr r2, =%(read)s + ldrb r2, [r2] + movs r3, #1 + lsls r3, r3, #28 + %(extra)s + bx lr + .ltorg + .size entry, . - entry + .thumb_func +ram_helper: + bx lr + .thumb_func +ram_base_reader: + ldr r0, =vectors + bx lr + .ltorg + .thumb_func +ram_built_reader: + movs r0, #1 + lsls r0, r0, #28 + ldr r0, [r0] + bx lr + .thumb_func +late_call: + ldr r3, =%(late)s + blx r3 + bx lr + .size late_call, . - late_call + .ltorg + .thumb_func +late_read: + ldr r3, =.Lflash_string + ldrb r3, [r3] + bx lr + .size late_read, . - late_read + .ltorg + .section .flashtext, "ax" + .thumb_func +flash_helper: + bx lr + .section %(table)s + .type callbacks, %%object +callbacks: + .word %(callback)s + .size callbacks, . - callbacks +""" + + +def selftest(): + # Unsized assembly helpers in RAM or flash, called directly or through a + # callback table, and data read from RAM or flash, with or without a + # symbol; the entry may only write at the image base, not read it + ram_data = '.ramdata, "aw"' + ram_code = '.ramtext, "ax"' + flash_data = '.flashdata, "a"' + ram, flash = "ram_helper", "flash_helper" + cases = ((ram, ram_data, ram, ".Lram_string", 0), + (flash, ram_data, ram, ".Lram_string", 1), + (ram, ram_data, flash, ".Lram_string", 1), + (ram, ram_code, ram, ".Lram_string", 0), + (ram, ram_code, flash, ".Lram_string", 1), + (ram, flash_data, ram, ".Lram_string", 1), + (ram, ram_data, ram, ".Lflash_string", 1), + (ram, ram_data, ram, "bytes", 1), + (ram, ram_data, ram, "bytes + 2", 1), + (ram, ram_data, ram, "vectors", 1), + ("ram_base_reader", ram_data, ram, ".Lram_string", 1), + ("ram_built_reader", ram_data, ram, ".Lram_string", 1), + (ram, ram_data, ram, ".Lram_string", 1, "ldr r3, [r3]"), + (ram, ram_data, ram, ".Lram_string", 1, + "mov r2, r1\n ldr r0, [r2]"), + (ram, ram_data, ram, ".Lram_string", 1, + "movs r2, r3\n ldr r0, [r2]"), + (ram, ram_data, ram, ".Lram_string", 0, "bl late_call"), + (ram, ram_data, ram, ".Lram_string", 1, "bl late_call", flash), + (ram, ram_data, ram, ".Lram_string", 1, "bl late_read")) + with tempfile.TemporaryDirectory() as tmp: + for case in cases: + callee, table, callback, read, expect = case[:5] + extra = case[5] if len(case) > 5 else "nop" + late = case[6] if len(case) > 6 else ram + src = os.path.join(tmp, "t.S") + elf = os.path.join(tmp, "t.elf") + with open(src, "w") as out: + out.write(SELFTEST_SRC % {"callee": callee, "table": table, + "callback": callback, "read": read, + "extra": extra, "late": late}) + subprocess.run(["arm-none-eabi-gcc", "-mcpu=cortex-m0plus", + "-mthumb", "-nostdlib", "-Wl,-e,entry", + "-Wl,--section-start=.ramtext=0x08000000", + "-Wl,--section-start=.ramdata=0x08001000", + "-Wl,--section-start=.flashbase=0x10000000", + "-Wl,--section-start=.flashtext=0x10000100", + "-Wl,--section-start=.flashdata=0x10001000", + "-o", elf, src], check=True) + if check(elf, "entry", 0x08000000, 0x08060000) != expect: + print("selftest: wrong result calling %s and %s from %s, " + "reading %s, then %s (late pool %s)" % (callee, + callback, table, + read, extra, + late)) + return 1 + print("selftest: ok") + return 0 + + +def main(): + if len(sys.argv) == 2 and sys.argv[1] == "--selftest": + sys.exit(selftest()) + if len(sys.argv) != 5: + sys.exit("usage: check-ramcode.py ") + sys.exit(check(sys.argv[1], sys.argv[2], int(sys.argv[3], 0), + int(sys.argv[4], 0))) + + +if __name__ == "__main__": + main() diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index be9cc5b5d6..5631df2ac9 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -284,6 +284,16 @@ TESTS+=unit-lpc55s69-puf else $(info Skipping LPC55S69 unit tests: set MCUXSDK (see tools/ci/fetch-mcuxsdk-headers.sh)) endif +# PSoC 6 HAL tests build against the real Infineon PDL headers, fetched into +# PSOC6_LIBS by tools/ci/fetch-psoc6-pdl.sh, with only CMSIS mocked. +PSOC6_LIBS?= +ifneq ($(wildcard $(PSOC6_LIBS)/mtb-pdl-cat1/drivers/include/cy_flash.h),) +TESTS+=unit-psoc6-flash +TESTS+=unit-psoc6-qspi +TESTS+=unit-psoc6-boot +else +$(info Skipping PSoC 6 unit tests: set PSOC6_LIBS (see tools/ci/fetch-psoc6-pdl.sh)) +endif TESTS+=unit-flash-write-nrf52 TESTS+=unit-nrf5340-uart-crlf TESTS+=unit-flash-write-samr21 @@ -2542,6 +2552,33 @@ unit-rw612-uart: unit-rw612-uart.c ../../hal/rw612.c -isystem $(MCUXSDK)/drivers/flexcomm \ -isystem $(MCUXSDK)/drivers/flexcomm/usart $(CFLAGS) $(LDFLAGS) +PSOC6_PDL=$(PSOC6_LIBS)/mtb-pdl-cat1 +PSOC6_TEST_INC=-U__ARM_ARCH -D__ARM_ARCH=6 -D__ARM_ARCH_6M__=1 \ + -DCY8C624ABZI_S2D44 -DPSOC6_BOARD_CY8CEVAL_062S2 \ + -DCY_CUSTOM_ASSERT_HANDLER -Icmsis_stub -I../.. \ + -isystem $(PSOC6_PDL)/drivers/include \ + -isystem $(PSOC6_PDL)/devices/COMPONENT_CAT1A/include \ + -isystem $(PSOC6_PDL)/devices/COMPONENT_CAT1A/include/ip \ + -isystem $(PSOC6_LIBS)/mtb-template-cat1/files/templates/cat1a/COMPONENT_MTB \ + -isystem $(PSOC6_LIBS)/core-lib/include +PSOC6_TEST_DEPS=../../hal/psoc6.c ../../hal/psoc6.h cmsis_stub/core_cm0plus.h + +unit-psoc6-flash: unit-psoc6-flash.c $(PSOC6_TEST_DEPS) + gcc -o $@ unit-psoc6-flash.c -DDEBUG_UART -DRAM_CODE $(PSOC6_TEST_INC) \ + $(CFLAGS) $(LDFLAGS) + +unit-psoc6-qspi: unit-psoc6-qspi.c $(PSOC6_TEST_DEPS) + gcc -o $@ unit-psoc6-qspi.c -DEXT_FLASH=1 -DPART_UPDATE_EXT=1 \ + $(PSOC6_TEST_INC) $(CFLAGS) $(LDFLAGS) + +unit-psoc6-boot: unit-psoc6-boot.c $(PSOC6_TEST_DEPS) + gcc -o $@ unit-psoc6-boot.c -D__WOLFBOOT -DDEBUG_UART \ + -DWOLFBOOT_HASH_SHA256 -DWOLFBOOT_SIGN_ECC256 -DDUALBANK_SWAP=1 \ + -DBOOT_BENCHMARK \ + -DWOLFBOOT_BOOT_REMOTE_CORE -DWOLFBOOT_ORIGIN=0x10000000 \ + -DBOOTLOADER_PARTITION_SIZE=0x20000 -DARCH_FLASH_OFFSET=0x10000000 \ + $(PSOC6_TEST_INC) $(CFLAGS) $(LDFLAGS) + MCXW_SSS=$(MCUXSDK)/middleware/secure-subsystem MCXW_TEST_INC=-DCPU_MCXW716CMFTA $(MCUXSDK_TEST_INC) -isystem $(MCXW_SDK) \ -isystem $(MCXW_SDK)/../periph2 -isystem $(MCXW_SDK)/drivers \ @@ -2667,7 +2704,8 @@ GENERATED_SRC:=aurix_erased_extract.h elf_store_extract.h \ CONDITIONAL_TESTS:=unit-efi-x86-open-image unit-linux-loader-e820 \ unit-linux-loader-syssize unit-flash-write-mcxa unit-rw612-flash \ unit-rw612-tz unit-rw612-uart unit-mcxw-flash unit-mcxw-tz \ - unit-lpc55s69-flash unit-lpc55s69-tz unit-lpc55s69-uart unit-lpc55s69-puf + unit-lpc55s69-flash unit-lpc55s69-tz unit-lpc55s69-uart unit-lpc55s69-puf \ + unit-psoc6-flash unit-psoc6-qspi unit-psoc6-boot clean: covclean rm -f $(TESTS) $(CONDITIONAL_TESTS) $(GENERATED_SRC) *.o *.gcno *.gcda \ diff --git a/tools/unit-tests/cmsis_stub/core_cm0plus.h b/tools/unit-tests/cmsis_stub/core_cm0plus.h new file mode 100644 index 0000000000..c79a3d7382 --- /dev/null +++ b/tools/unit-tests/cmsis_stub/core_cm0plus.h @@ -0,0 +1,135 @@ +/* core_cm0plus.h + * + * Host stand-in for the Arm CMSIS Cortex-M0+ core header, so the PSoC 6 HAL + * unit tests build against the real Infineon PDL headers: core registers and + * intrinsics are mock objects the tests define and inspect. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#ifndef CORE_CM0PLUS_STUB_H +#define CORE_CM0PLUS_STUB_H + +#include + +#define __I volatile const +#define __O volatile +#define __IO volatile +#define __IM volatile const +#define __OM volatile +#define __IOM volatile +#define __STATIC_INLINE static inline +#define __STATIC_FORCEINLINE static inline +#define __WEAK __attribute__((weak)) +#define __USED __attribute__((used)) +#define __NO_RETURN __attribute__((noreturn)) +#define __ALIGNED(x) __attribute__((aligned(x))) +#define __PACKED __attribute__((packed)) +#define __ASM __asm__ +#define __INLINE inline +#define __CORTEX_M 0U + +#define _VAL2FLD(field, value) \ + (((uint32_t)(value) << field ## _Pos) & field ## _Msk) +#define _FLD2VAL(field, value) \ + (((uint32_t)(value) & field ## _Msk) >> field ## _Pos) + +typedef struct { + __IOM uint32_t ISER[1]; + uint32_t RESERVED0[31]; + __IOM uint32_t ICER[1]; + uint32_t RESERVED1[31]; + __IOM uint32_t ISPR[1]; + uint32_t RESERVED2[31]; + __IOM uint32_t ICPR[1]; + uint32_t RESERVED3[31]; + uint32_t RESERVED4[64]; + __IOM uint32_t IP[8]; +} NVIC_Type; + +typedef struct { + __IM uint32_t CPUID; + __IOM uint32_t ICSR; + __IOM uint32_t VTOR; + __IOM uint32_t AIRCR; + __IOM uint32_t SCR; + __IOM uint32_t CCR; + uint32_t RESERVED1; + __IOM uint32_t SHP[2]; + __IOM uint32_t SHCSR; +} SCB_Type; + +typedef struct { + __IOM uint32_t CTRL; + __IOM uint32_t LOAD; + __IOM uint32_t VAL; + __IM uint32_t CALIB; +} SysTick_Type; +#define SysTick_CTRL_ENABLE_Msk (1UL << 0) +#define SysTick_CTRL_TICKINT_Msk (1UL << 1) +#define SysTick_LOAD_RELOAD_Msk 0xFFFFFFUL + +extern NVIC_Type mock_nvic; +extern SCB_Type mock_scb; +extern SysTick_Type mock_systick; +#define NVIC (&mock_nvic) +#define SCB (&mock_scb) +#define SysTick (&mock_systick) + +extern uint32_t mock_primask; +extern int mock_barriers; +extern int mock_wfi; +static inline uint32_t __get_PRIMASK(void) { return mock_primask; } +static inline void __set_PRIMASK(uint32_t v) { mock_primask = v; } +static inline void __disable_irq(void) { mock_primask = 1; } +static inline void __enable_irq(void) { mock_primask = 0; } +static inline void __DSB(void) { mock_barriers++; } +static inline void __ISB(void) { mock_barriers++; } +static inline void __DMB(void) { mock_barriers++; } +static inline void __NOP(void) { } +void mock_wfi_hook(void); +static inline void __WFI(void) { mock_wfi++; mock_wfi_hook(); } +static inline void __WFE(void) { } +static inline void __SEV(void) { } +static inline uint32_t __get_IPSR(void) { return 0; } + +static inline void NVIC_EnableIRQ(IRQn_Type irq) +{ + NVIC->ISER[0] = 1UL << ((uint32_t)irq & 0x1FUL); +} +static inline void NVIC_DisableIRQ(IRQn_Type irq) +{ + NVIC->ICER[0] = 1UL << ((uint32_t)irq & 0x1FUL); +} +static inline void NVIC_ClearPendingIRQ(IRQn_Type irq) +{ + NVIC->ICPR[0] = 1UL << ((uint32_t)irq & 0x1FUL); +} +static inline void NVIC_SetPriority(IRQn_Type irq, uint32_t priority) +{ + (void)irq; + (void)priority; +} +static inline uint32_t NVIC_GetEnableIRQ(IRQn_Type irq) +{ + return (NVIC->ISER[0] >> ((uint32_t)irq & 0x1FUL)) & 1UL; +} +void NVIC_SystemReset(void); + +#endif /* CORE_CM0PLUS_STUB_H */ diff --git a/tools/unit-tests/unit-psoc6-boot.c b/tools/unit-tests/unit-psoc6-boot.c new file mode 100644 index 0000000000..346244c67b --- /dev/null +++ b/tools/unit-tests/unit-psoc6-boot.c @@ -0,0 +1,696 @@ +/* unit-psoc6-boot.c + * + * Unit tests for the PSoC 6 bootloader paths in hal/psoc6.c: dual-bank + * swap against a model of the bank mapping, the bootloader fork, the + * debug console, the microsecond timer, the SRAM vector table and the CM4 + * handoff. Built against the Infineon mtb-pdl-cat1 headers. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include +#include + +/* PSoC 6 dual-bank layout (cypsoc6.config with DUALBANK_SWAP=1) */ +#define H_TARGETS_TARGET_ +#define WOLFBOOT_FIXED_PARTITIONS +#define WOLFBOOT_SECTOR_SIZE 0x1000 +#define WOLFBOOT_PARTITION_SIZE 0xE0000 +#define WOLFBOOT_PARTITION_BOOT_ADDRESS 0x10020000 +#define WOLFBOOT_PARTITION_UPDATE_ADDRESS 0x12020000 +#define WOLFBOOT_PARTITION_SWAP_ADDRESS 0xFFFFFFFF + +const void *mock_flash_ptr(uint32_t address); +#define PSOC6_FLASH_PTR(a) mock_flash_ptr(a) + +#include "cy_device_headers.h" +#include "cy_device.h" +#include "cy_tcpwm.h" +#include "cy_scb_uart.h" +#include "hal/psoc6.h" + +/* The PDL's inline SCB accessors touch registers; route them to mocks */ +void mock_uart_enable(CySCB_Type *base); +bool mock_uart_tx_complete(CySCB_Type const *base); +#define Cy_SCB_UART_Enable mock_uart_enable +#define Cy_SCB_UART_IsTxComplete mock_uart_tx_complete + +/* Point the register blocks the HAL touches at mocks */ +static TCPWM_V1_Type mock_tcpwm; +static uint32_t mock_flash_cmd; +#undef PSOC6_TIMER_TCPWM +#define PSOC6_TIMER_TCPWM ((TCPWM_Type *)&mock_tcpwm) +#undef FLASHC_FLASH_CMD +#define FLASHC_FLASH_CMD mock_flash_cmd + +#include "../../hal/psoc6.c" + +#define ROW PSOC6_FLASH_ROW_SIZE +#define BANK PSOC6_FLASH_BANK_SIZE +#define BL_SIZE BOOTLOADER_PARTITION_SIZE +#define MOCK_MAX_OPS 1024 +#define MOCK_MAX_CALLS 8 + +enum { CLK_ASSIGN, CLK_SET, CLK_SET_FRAC, CLK_ENABLE }; + +struct clk_call { + int op; + uint32_t type; + uint32_t num; + uint32_t a; + uint32_t b; +}; + +struct gpio_call { + GPIO_PRT_Type *base; + uint32_t pin; + uint32_t mode; + uint32_t out; + en_hsiom_sel_t hsiom; +}; + +uint32_t mock_primask; +int mock_barriers; +int mock_wfi; +NVIC_Type mock_nvic; +SCB_Type mock_scb; +SysTick_Type mock_systick; +cy_israddress __ramVectors[PSOC6_CM0P_VECTORS]; +const cy_stc_device_t *cy_device; +const cy_stc_device_t cy_deviceIpBlockCfgPSoC6_02; + +static uint8_t mock_main[PSOC6_FLASH_SIZE]; +static uint8_t mock_wflash[PSOC6_WFLASH_SIZE]; +static int mock_dual; +static cy_en_maptype_t mock_map; +static int n_flash_ops; +static int n_wflash_writes; +static int set_mapping_calls; +static uint32_t cm4_vector; +static int cm4_started; +static int fail_writes; +static jmp_buf park; +static struct clk_call clk_calls[MOCK_MAX_CALLS]; +static int n_clk_calls; +static struct gpio_call gpio_calls[MOCK_MAX_CALLS]; +static int n_gpio_calls; +static cy_stc_scb_uart_config_t uart_cfg; +static cy_en_scb_uart_status_t uart_init_ret; +static int uart_init_calls; +static int uart_enable_calls; +static int tx_busy_polls; +static int tx_polls; + +void NVIC_SystemReset(void) +{ +} + +void CY_ASSERT_HANDLER(void) +{ + ck_abort_msg("PDL assert"); +} + +/* The parked CM0+ loops on __WFI(); leave the loop once the CM4 started */ +void mock_wfi_hook(void) +{ + if (cm4_started) + longjmp(park, 1); +} + +static uint8_t *mock_phys(uint32_t address) +{ + uint32_t off; + + if ((address >= PSOC6_WFLASH_BASE) && + (address < PSOC6_WFLASH_BASE + PSOC6_WFLASH_SIZE)) { + return &mock_wflash[address - PSOC6_WFLASH_BASE]; + } + if (!mock_dual && (address >= PSOC6_FLASH_BASE) && + (address < PSOC6_FLASH_BASE + PSOC6_FLASH_SIZE)) { + return &mock_main[address - PSOC6_FLASH_BASE]; + } + /* Dual-bank mode: mapping B swaps which bank is seen at each base */ + if (mock_dual && (address >= PSOC6_FLASH_BASE) && + (address < PSOC6_FLASH_BASE + BANK)) { + off = address - PSOC6_FLASH_BASE; + return &mock_main[off + ((mock_map == CY_FLASH_MAPPING_B) ? BANK : 0)]; + } + if (mock_dual && (address >= PSOC6_FLASH_BANK2_BASE) && + (address < PSOC6_FLASH_BANK2_BASE + BANK)) { + off = address - PSOC6_FLASH_BANK2_BASE; + return &mock_main[off + ((mock_map == CY_FLASH_MAPPING_B) ? 0 : BANK)]; + } + ck_abort_msg("flash access outside the model: 0x%08x", address); + return NULL; +} + +const void *mock_flash_ptr(uint32_t address) +{ + return mock_phys(address); +} + +static void count_op(uint32_t addr) +{ + n_flash_ops++; + if ((addr >= PSOC6_WFLASH_BASE) && + (addr < PSOC6_WFLASH_BASE + PSOC6_WFLASH_SIZE)) + n_wflash_writes++; +} + +cy_en_flashdrv_status_t Cy_Flash_WriteRow(uint32_t rowAddr, + const uint32_t* data) +{ + if (fail_writes) { + return CY_FLASH_DRV_ERR_UNC; + } + count_op(rowAddr); + memcpy(mock_phys(rowAddr), data, ROW); + return CY_FLASH_DRV_SUCCESS; +} + +cy_en_flashdrv_status_t Cy_Flash_ProgramRow(uint32_t rowAddr, + const uint32_t* data) +{ + if (fail_writes) { + return CY_FLASH_DRV_ERR_UNC; + } + count_op(rowAddr); + memcpy(mock_phys(rowAddr), data, ROW); + return CY_FLASH_DRV_SUCCESS; +} + +cy_en_flashdrv_status_t Cy_Flash_EraseRow(uint32_t rowAddr) +{ + count_op(rowAddr); + memset(mock_phys(rowAddr), 0, ROW); + return CY_FLASH_DRV_SUCCESS; +} + +cy_en_flashdrv_status_t Cy_Flash_EraseSubsector(uint32_t subSectorAddr) +{ + count_op(subSectorAddr); + memset(mock_phys(subSectorAddr), 0, ROW * 8U); + return CY_FLASH_DRV_SUCCESS; +} + +cy_en_flashdrv_status_t Cy_Flash_EraseSector(uint32_t sectorAddr) +{ + count_op(sectorAddr); + memset(mock_phys(sectorAddr), 0, ROW * 512U); + return CY_FLASH_DRV_SUCCESS; +} + +void Cy_SysLib_ClearFlashCacheAndBuffer(void) +{ +} + +void Cy_Flashc_SetMainBankMode(cy_en_bankmode_t mode) +{ + mock_dual = (mode == CY_FLASH_DUAL_BANK_MODE); +} + +void Cy_Flashc_SetMain_Flash_Mapping(cy_en_maptype_t mapping) +{ + ck_assert_int_eq(mock_dual, 1); + mock_map = mapping; + set_mapping_calls++; +} + +void Cy_SysEnableCM4(uint32_t vectorTableOffset) +{ + cm4_vector = vectorTableOffset; + cm4_started = 1; +} + +static void record_clk(int op, uint32_t type, uint32_t num, uint32_t a, + uint32_t b) +{ + ck_assert_int_lt(n_clk_calls, MOCK_MAX_CALLS); + clk_calls[n_clk_calls].op = op; + clk_calls[n_clk_calls].type = type; + clk_calls[n_clk_calls].num = num; + clk_calls[n_clk_calls].a = a; + clk_calls[n_clk_calls].b = b; + n_clk_calls++; +} + +cy_en_sysclk_status_t Cy_SysClk_PeriphAssignDivider(en_clk_dst_t ipBlock, + cy_en_divider_types_t dividerType, uint32_t dividerNum) +{ + record_clk(CLK_ASSIGN, dividerType, dividerNum, ipBlock, 0); + return CY_SYSCLK_SUCCESS; +} + +cy_en_sysclk_status_t Cy_SysClk_PeriphSetDivider( + cy_en_divider_types_t dividerType, uint32_t dividerNum, + uint32_t dividerValue) +{ + record_clk(CLK_SET, dividerType, dividerNum, dividerValue, 0); + return CY_SYSCLK_SUCCESS; +} + +cy_en_sysclk_status_t Cy_SysClk_PeriphSetFracDivider( + cy_en_divider_types_t dividerType, uint32_t dividerNum, + uint32_t dividerIntValue, uint32_t dividerFracValue) +{ + record_clk(CLK_SET_FRAC, dividerType, dividerNum, dividerIntValue, + dividerFracValue); + return CY_SYSCLK_SUCCESS; +} + +cy_en_sysclk_status_t Cy_SysClk_PeriphEnableDivider( + cy_en_divider_types_t dividerType, uint32_t dividerNum) +{ + record_clk(CLK_ENABLE, dividerType, dividerNum, 0, 0); + return CY_SYSCLK_SUCCESS; +} + +void Cy_GPIO_Pin_FastInit(GPIO_PRT_Type* base, uint32_t pinNum, + uint32_t driveMode, uint32_t outVal, en_hsiom_sel_t hsiom) +{ + ck_assert_int_lt(n_gpio_calls, MOCK_MAX_CALLS); + gpio_calls[n_gpio_calls].base = base; + gpio_calls[n_gpio_calls].pin = pinNum; + gpio_calls[n_gpio_calls].mode = driveMode; + gpio_calls[n_gpio_calls].out = outVal; + gpio_calls[n_gpio_calls].hsiom = hsiom; + n_gpio_calls++; +} + +cy_en_scb_uart_status_t Cy_SCB_UART_Init(CySCB_Type *base, + cy_stc_scb_uart_config_t const *config, + cy_stc_scb_uart_context_t *context) +{ + ck_assert_ptr_eq(base, SCB0); + ck_assert_ptr_null(context); + uart_cfg = *config; + uart_init_calls++; + return uart_init_ret; +} + +void mock_uart_enable(CySCB_Type *base) +{ + ck_assert_ptr_eq(base, SCB0); + ck_assert_int_eq(uart_init_calls, 1); + uart_enable_calls++; +} + +bool mock_uart_tx_complete(CySCB_Type const *base) +{ + ck_assert_ptr_eq(base, SCB0); + tx_polls++; + return tx_polls > tx_busy_polls; +} + +void uart_printf(const char* fmt, ...) +{ + (void)fmt; +} + +cy_en_tcpwm_status_t Cy_TCPWM_Counter_Init(TCPWM_Type *base, uint32_t cntNum, + cy_stc_tcpwm_counter_config_t const *config) +{ + ck_assert_ptr_eq(base, PSOC6_TIMER_TCPWM); + ck_assert_uint_eq(cntNum, PSOC6_TIMER_CNT); + ck_assert_uint_eq(config->period, 0xFFFFFFFFUL); + ck_assert_uint_eq(config->countDirection, CY_TCPWM_COUNTER_COUNT_UP); + return CY_TCPWM_SUCCESS; +} + +/* hal_init() and uart_write() link these; the tests do not run them */ +#define NOT_CALLED() ck_abort_msg("unexpected PDL call in a host test") +uint32_t Cy_SCB_Write(CySCB_Type *base, uint32_t data) +{ (void)base; (void)data; NOT_CALLED(); return 0; } +void Cy_PDL_Init(const cy_stc_device_t * device) +{ (void)device; NOT_CALLED(); } +void Cy_Flash_Init(void) +{ NOT_CALLED(); } +void Cy_SysClk_ClkFastSetDivider(uint8_t divider) +{ (void)divider; NOT_CALLED(); } +void Cy_SysClk_ClkPeriSetDivider(uint8_t divider) +{ (void)divider; NOT_CALLED(); } +void Cy_SysClk_ClkSlowSetDivider(uint8_t divider) +{ (void)divider; NOT_CALLED(); } +void Cy_SysLib_SetWaitStates(bool ulpMode, uint32_t clkHfMHz) +{ (void)ulpMode; (void)clkHfMHz; NOT_CALLED(); } +cy_en_sysclk_status_t Cy_SysClk_ClkHfSetSource(uint32_t clkHf, + cy_en_clkhf_in_sources_t source) +{ (void)clkHf; (void)source; NOT_CALLED(); return 0; } +cy_en_sysclk_status_t Cy_SysClk_ClkHfSetDivider(uint32_t clkHf, + cy_en_clkhf_dividers_t divider) +{ (void)clkHf; (void)divider; NOT_CALLED(); return 0; } +cy_en_sysclk_status_t Cy_SysClk_ClkPathSetSource(uint32_t clkPath, + cy_en_clkpath_in_sources_t source) +{ (void)clkPath; (void)source; NOT_CALLED(); return 0; } +cy_en_sysclk_status_t Cy_SysClk_PllManualConfigure(uint32_t clkPath, + const cy_stc_pll_manual_config_t *config) +{ (void)clkPath; (void)config; NOT_CALLED(); return 0; } +cy_en_sysclk_status_t Cy_SysClk_PllEnable(uint32_t clkPath, + uint32_t timeoutus) +{ (void)clkPath; (void)timeoutus; NOT_CALLED(); return 0; } + +static void put_bootloader(uint32_t phys, uint8_t seed) +{ + uint32_t i; + + for (i = 0; i < BL_SIZE; i++) + mock_main[phys + i] = (uint8_t)(seed + (i >> 2)); +} + +static void put_bank_record(uint32_t slot, uint32_t mapping, uint32_t seq) +{ + active_bank rec; + + rec.magic = BANKMODE_MAGIC; + rec.mapping = mapping; + rec.seq = seq; + rec.check = ~(rec.magic ^ rec.mapping ^ rec.seq); + memcpy(&mock_wflash[slot * ROW], &rec, sizeof(rec)); +} + +static active_bank get_bank_record(uint32_t slot) +{ + active_bank rec; + + memcpy(&rec, &mock_wflash[slot * ROW], sizeof(rec)); + return rec; +} + +static void setup(void) +{ + memset(mock_main, 0, sizeof(mock_main)); + memset(mock_wflash, 0, sizeof(mock_wflash)); + memset(&mock_tcpwm, 0, sizeof(mock_tcpwm)); + memset(&mock_scb, 0, sizeof(mock_scb)); + memset(__ramVectors, 0, sizeof(__ramVectors)); + mock_dual = 0; + mock_map = CY_FLASH_MAPPING_A; + n_flash_ops = 0; + n_wflash_writes = 0; + set_mapping_calls = 0; + cm4_vector = 0; + cm4_started = 0; + mock_wfi = 0; + mock_flash_cmd = 0; + fail_writes = 0; + memset(clk_calls, 0, sizeof(clk_calls)); + n_clk_calls = 0; + memset(gpio_calls, 0, sizeof(gpio_calls)); + n_gpio_calls = 0; + memset(&uart_cfg, 0, sizeof(uart_cfg)); + uart_init_ret = CY_SCB_UART_SUCCESS; + uart_init_calls = 0; + uart_enable_calls = 0; + tx_busy_polls = 0; + tx_polls = 0; +} + +static void check_clk(int i, int op, uint32_t type, uint32_t num, uint32_t a, + uint32_t b) +{ + ck_assert_int_lt(i, n_clk_calls); + ck_assert_int_eq(clk_calls[i].op, op); + ck_assert_uint_eq(clk_calls[i].type, type); + ck_assert_uint_eq(clk_calls[i].num, num); + ck_assert_uint_eq(clk_calls[i].a, a); + ck_assert_uint_eq(clk_calls[i].b, b); +} + +static void check_gpio(int i, GPIO_PRT_Type *base, uint32_t pin, + uint32_t mode, en_hsiom_sel_t hsiom) +{ + ck_assert_int_lt(i, n_gpio_calls); + ck_assert_ptr_eq(gpio_calls[i].base, base); + ck_assert_uint_eq(gpio_calls[i].pin, pin); + ck_assert_uint_eq(gpio_calls[i].mode, mode); + ck_assert_uint_eq(gpio_calls[i].out, 1); + ck_assert_int_eq(gpio_calls[i].hsiom, hsiom); +} + +START_TEST(test_first_boot_forks_and_records_bank_a) +{ + put_bootloader(0, 0x10); + + ck_assert_int_eq(bankmode_init(), 0); + ck_assert_mem_eq(&mock_main[BANK], &mock_main[0], BL_SIZE); + ck_assert_int_eq(mock_dual, 1); + ck_assert_int_eq(mock_map, CY_FLASH_MAPPING_A); + ck_assert_uint_eq(get_bank_record(0).magic, BANKMODE_MAGIC); + ck_assert_uint_eq(get_bank_record(0).mapping, CY_FLASH_MAPPING_A); + ck_assert_uint_eq(get_bank_record(0).seq, 1); +} +END_TEST + +START_TEST(test_boot_restores_bank_b_without_rewrite) +{ + put_bootloader(0, 0x10); + put_bootloader(BANK, 0x10); + put_bank_record(0, CY_FLASH_MAPPING_A, 7); + put_bank_record(1, CY_FLASH_MAPPING_B, 8); + + ck_assert_int_eq(bankmode_init(), 0); + ck_assert_int_eq(mock_map, CY_FLASH_MAPPING_B); + ck_assert_int_eq(n_flash_ops, 0); +} +END_TEST + +START_TEST(test_fork_refreshes_stale_copy) +{ + put_bootloader(0, 0x10); + put_bootloader(BANK, 0x55); + fork_bootloader(); + ck_assert_mem_eq(&mock_main[BANK], &mock_main[0], BL_SIZE); + /* Already identical: no flash operations */ + n_flash_ops = 0; + fork_bootloader(); + ck_assert_int_eq(n_flash_ops, 0); +} +END_TEST + +START_TEST(test_swap_toggles_and_persists) +{ + put_bootloader(0, 0x10); + put_bootloader(BANK, 0x10); + put_bank_record(0, CY_FLASH_MAPPING_A, 1); + ck_assert_int_eq(bankmode_init(), 0); + + hal_flash_dualbank_swap(); + ck_assert_int_eq(mock_map, CY_FLASH_MAPPING_B); + /* The new record goes to the other slot; the old one stays intact */ + ck_assert_uint_eq(get_bank_record(1).mapping, CY_FLASH_MAPPING_B); + ck_assert_uint_eq(get_bank_record(1).seq, 2); + ck_assert_uint_eq(get_bank_record(0).mapping, CY_FLASH_MAPPING_A); + + hal_flash_dualbank_swap(); + ck_assert_int_eq(mock_map, CY_FLASH_MAPPING_A); + ck_assert_uint_eq(get_bank_record(0).mapping, CY_FLASH_MAPPING_A); + ck_assert_uint_eq(get_bank_record(0).seq, 3); +} +END_TEST + +START_TEST(test_swap_without_record_selects_bank_a) +{ + mock_dual = 1; + hal_flash_dualbank_swap(); + ck_assert_int_eq(mock_map, CY_FLASH_MAPPING_A); + ck_assert_uint_eq(get_bank_record(0).magic, BANKMODE_MAGIC); + ck_assert_uint_eq(get_bank_record(0).mapping, CY_FLASH_MAPPING_A); +} +END_TEST + +START_TEST(test_torn_record_keeps_previous_mapping) +{ + active_bank rec; + + put_bank_record(0, CY_FLASH_MAPPING_B, 5); + mock_dual = 1; + /* Power lost after the spare slot was erased, before it was written */ + fail_writes = 1; + hal_flash_dualbank_swap(); + ck_assert_int_eq(psoc6_bank_record_read(&rec), 0); + ck_assert_uint_eq(rec.mapping, CY_FLASH_MAPPING_B); + /* A record with a bad check word is ignored too */ + put_bank_record(1, CY_FLASH_MAPPING_A, 6); + mock_wflash[ROW + 8U] ^= 1U; + ck_assert_int_eq(psoc6_bank_record_read(&rec), 0); + ck_assert_uint_eq(rec.mapping, CY_FLASH_MAPPING_B); +} +END_TEST + +START_TEST(test_record_sequence_wraps) +{ + active_bank rec; + + put_bank_record(0, CY_FLASH_MAPPING_A, 0xFFFFFFFFU); + put_bank_record(1, CY_FLASH_MAPPING_B, 0U); + ck_assert_int_eq(psoc6_bank_record_read(&rec), 1); + ck_assert_uint_eq(rec.mapping, CY_FLASH_MAPPING_B); +} +END_TEST + +START_TEST(test_update_alias_writes_other_bank) +{ + uint8_t data[ROW]; + + memset(data, 0xC3, sizeof(data)); + mock_dual = 1; + mock_map = CY_FLASH_MAPPING_A; + ck_assert_int_eq(hal_flash_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS, data, + ROW), 0); + ck_assert_mem_eq(&mock_main[BANK + 0x20000U], data, ROW); + + mock_map = CY_FLASH_MAPPING_B; + ck_assert_int_eq(hal_flash_write(WOLFBOOT_PARTITION_UPDATE_ADDRESS, data, + ROW), 0); + ck_assert_mem_eq(&mock_main[0x20000U], data, ROW); + /* The alias ends with the bank */ + ck_assert_int_eq(hal_flash_write(PSOC6_FLASH_BANK2_BASE + BANK - 4U, data, + 8), -1); +} +END_TEST + +START_TEST(test_uart_init_routes_pins_and_divider) +{ + uart_init(); + + ck_assert_int_eq(n_gpio_calls, 2); + check_gpio(0, GPIO_PRT0, 3, CY_GPIO_DM_STRONG_IN_OFF, P0_3_SCB0_UART_TX); + check_gpio(1, GPIO_PRT0, 2, CY_GPIO_DM_HIGHZ, P0_2_SCB0_UART_RX); + /* 100 MHz / (16 * 115200) = 54.25: 16.5-bit divider 53 + 8/32 */ + ck_assert_int_eq(n_clk_calls, 3); + check_clk(0, CLK_ASSIGN, CY_SYSCLK_DIV_16_5_BIT, 0, PCLK_SCB0_CLOCK, 0); + check_clk(1, CLK_SET_FRAC, CY_SYSCLK_DIV_16_5_BIT, 0, 53, 8); + check_clk(2, CLK_ENABLE, CY_SYSCLK_DIV_16_5_BIT, 0, 0, 0); + ck_assert_int_eq(uart_init_calls, 1); + ck_assert_uint_eq(uart_cfg.uartMode, CY_SCB_UART_STANDARD); + ck_assert_uint_eq(uart_cfg.oversample, 16); + ck_assert_uint_eq(uart_cfg.dataWidth, 8); + ck_assert_uint_eq(uart_cfg.parity, CY_SCB_UART_PARITY_NONE); + ck_assert_uint_eq(uart_cfg.stopBits, CY_SCB_UART_STOP_BITS_1); + ck_assert_int_eq(uart_enable_calls, 1); +} +END_TEST + +START_TEST(test_uart_init_failure_leaves_scb_disabled) +{ + uart_init_ret = CY_SCB_UART_BAD_PARAM; + uart_init(); + ck_assert_int_eq(uart_init_calls, 1); + ck_assert_int_eq(uart_enable_calls, 0); +} +END_TEST + +START_TEST(test_prepare_boot_drains_uart) +{ + tx_busy_polls = 3; + hal_prepare_boot(); + ck_assert_int_eq(tx_polls, 4); +} +END_TEST + +START_TEST(test_timer_counts_and_extends_wrap) +{ + mock_tcpwm.CNT[PSOC6_TIMER_CNT].COUNTER = 1000U; + ck_assert_uint_eq(hal_get_timer_us(), 1000U); + /* 100 MHz / (99 + 1) = 1 MHz on the TCPWM0 counter 0 clock */ + ck_assert_int_eq(n_clk_calls, 3); + check_clk(0, CLK_ASSIGN, CY_SYSCLK_DIV_16_BIT, 0, PCLK_TCPWM0_CLOCKS0, 0); + check_clk(1, CLK_SET, CY_SYSCLK_DIV_16_BIT, 0, 99, 0); + check_clk(2, CLK_ENABLE, CY_SYSCLK_DIV_16_BIT, 0, 0, 0); + mock_tcpwm.CNT[PSOC6_TIMER_CNT].COUNTER = 0xFFFFFF00U; + ck_assert_uint_eq(hal_get_timer_us(), 0xFFFFFF00U); + mock_tcpwm.CNT[PSOC6_TIMER_CNT].COUNTER = 0x10U; + ck_assert_uint_eq(hal_get_timer_us(), 0x100000010ULL); + mock_tcpwm.CNT[PSOC6_TIMER_CNT].COUNTER = 0x20U; + ck_assert_uint_eq(hal_get_timer_us(), 0x100000020ULL); +} +END_TEST + +START_TEST(test_ram_vectors_keep_rom_nmi) +{ + /* Table entries are cy_israddress wide: 4 bytes on the target */ + cy_israddress iv[PSOC6_CM0P_SYS_VECTORS]; + uint32_t i; + + for (i = 0; i < PSOC6_CM0P_SYS_VECTORS; i++) + iv[i] = (cy_israddress)(uintptr_t)(0x10000100U + i * 4U); + iv[2] = (cy_israddress)(uintptr_t)0x0000000DU; + memcpy(mock_main, iv, sizeof(iv)); + + psoc6_ram_vectors(); + for (i = 0; i < PSOC6_CM0P_SYS_VECTORS; i++) + ck_assert_ptr_eq(__ramVectors[i], iv[i]); + for (; i < PSOC6_CM0P_VECTORS; i++) + ck_assert_ptr_eq(__ramVectors[i], iv[PSOC6_HARDFAULT_VECTOR]); + ck_assert_uint_eq(mock_scb.VTOR, (uint32_t)(uintptr_t)__ramVectors); +} +END_TEST + +START_TEST(test_cm4_handoff_parks_cm0) +{ + const uint32_t *app = + (const uint32_t *)(uintptr_t)(WOLFBOOT_PARTITION_BOOT_ADDRESS + 0x400U); + + if (setjmp(park) == 0) { + hal_boot_remote_core(app); + ck_abort_msg("hal_boot_remote_core returned"); + } + ck_assert_int_eq(cm4_started, 1); + ck_assert_uint_eq(cm4_vector, WOLFBOOT_PARTITION_BOOT_ADDRESS + 0x400U); + ck_assert_int_ge(mock_wfi, 1); +} +END_TEST + +Suite *psoc6_boot_suite(void) +{ + Suite *s = suite_create("psoc6-boot"); + TCase *tc = tcase_create("boot"); + + tcase_add_checked_fixture(tc, setup, NULL); + tcase_add_test(tc, test_first_boot_forks_and_records_bank_a); + tcase_add_test(tc, test_boot_restores_bank_b_without_rewrite); + tcase_add_test(tc, test_fork_refreshes_stale_copy); + tcase_add_test(tc, test_swap_toggles_and_persists); + tcase_add_test(tc, test_swap_without_record_selects_bank_a); + tcase_add_test(tc, test_torn_record_keeps_previous_mapping); + tcase_add_test(tc, test_record_sequence_wraps); + tcase_add_test(tc, test_update_alias_writes_other_bank); + tcase_add_test(tc, test_uart_init_routes_pins_and_divider); + tcase_add_test(tc, test_uart_init_failure_leaves_scb_disabled); + tcase_add_test(tc, test_prepare_boot_drains_uart); + tcase_add_test(tc, test_timer_counts_and_extends_wrap); + tcase_add_test(tc, test_ram_vectors_keep_rom_nmi); + tcase_add_test(tc, test_cm4_handoff_parks_cm0); + suite_add_tcase(s, tc); + return s; +} + +int main(void) +{ + int fails; + Suite *s = psoc6_boot_suite(); + SRunner *sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + return fails; +} diff --git a/tools/unit-tests/unit-psoc6-flash.c b/tools/unit-tests/unit-psoc6-flash.c new file mode 100644 index 0000000000..d872bbb49f --- /dev/null +++ b/tools/unit-tests/unit-psoc6-flash.c @@ -0,0 +1,506 @@ +/* unit-psoc6-flash.c + * + * Unit tests for the PSoC 6 internal flash driver and console in + * hal/psoc6.c, against a model of main and work flash, built against the + * Infineon mtb-pdl-cat1 headers. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include + +/* PSoC 6 layout (config/examples/cypsoc6.config) */ +#define H_TARGETS_TARGET_ +#define WOLFBOOT_FIXED_PARTITIONS +#define WOLFBOOT_SECTOR_SIZE 0x1000 +#define WOLFBOOT_PARTITION_SIZE 0xE0000 +#define WOLFBOOT_PARTITION_BOOT_ADDRESS 0x10020000 +#define WOLFBOOT_PARTITION_UPDATE_ADDRESS 0x10100000 +#define WOLFBOOT_PARTITION_SWAP_ADDRESS 0x101E0000 + +#include "cy_device_headers.h" +#include "cy_scb_uart.h" + +const void *mock_flash_ptr(uint32_t address); +#define PSOC6_FLASH_PTR(a) mock_flash_ptr(a) +/* The PDL's inline Put writes the TX FIFO register; record the bytes */ +uint32_t mock_uart_put(CySCB_Type *base, uint32_t data); +#define Cy_SCB_UART_Put mock_uart_put + +#include "../../hal/psoc6.c" + +#define MOCK_MAX_OPS 64 +#define MOCK_TX_MAX 64 +#define ROW PSOC6_FLASH_ROW_SIZE +#define SUBSECTOR (ROW * 8U) +#define SECTOR (ROW * 512U) +#define TEST_ROW 0x10040000U + +enum { OP_WRITE, OP_PROGRAM, OP_ERASE_ROW, OP_ERASE_SUBSECTOR, + OP_ERASE_SECTOR }; + +uint32_t mock_primask; +int mock_barriers; +int mock_wfi; +NVIC_Type mock_nvic; +SCB_Type mock_scb; +SysTick_Type mock_systick; + +static uint8_t mock_main[PSOC6_FLASH_SIZE]; +static uint8_t mock_wflash[PSOC6_WFLASH_SIZE]; +static struct { + int op; + uint32_t addr; +} ops[MOCK_MAX_OPS]; +static int n_ops; +static int cache_clears; +static int fail_at_op; +static char tx[MOCK_TX_MAX]; +static int n_tx; + +void mock_wfi_hook(void) +{ +} + +void CY_ASSERT_HANDLER(void) +{ + ck_abort_msg("PDL assert"); +} + +static int resets; + +void NVIC_SystemReset(void) +{ + resets++; +} + +static uint8_t *mock_phys(uint32_t address) +{ + if ((address >= PSOC6_FLASH_BASE) && + (address < PSOC6_FLASH_BASE + PSOC6_FLASH_SIZE)) { + return &mock_main[address - PSOC6_FLASH_BASE]; + } + if ((address >= PSOC6_WFLASH_BASE) && + (address < PSOC6_WFLASH_BASE + PSOC6_WFLASH_SIZE)) { + return &mock_wflash[address - PSOC6_WFLASH_BASE]; + } + ck_abort_msg("flash access outside the model: 0x%08x", address); + return NULL; +} + +const void *mock_flash_ptr(uint32_t address) +{ + return mock_phys(address); +} + +static cy_en_flashdrv_status_t mock_op(int op, uint32_t addr, uint32_t size) +{ + ck_assert_int_lt(n_ops, MOCK_MAX_OPS); + ck_assert_uint_eq(addr % size, 0); + ops[n_ops].op = op; + ops[n_ops].addr = addr; + n_ops++; + if (n_ops == fail_at_op) { + return CY_FLASH_DRV_ERR_UNC; + } + if (op != OP_WRITE) { + memset(mock_phys(addr), 0, size); + } + return CY_FLASH_DRV_SUCCESS; +} + +cy_en_flashdrv_status_t Cy_Flash_WriteRow(uint32_t rowAddr, + const uint32_t* data) +{ + cy_en_flashdrv_status_t st = mock_op(OP_WRITE, rowAddr, ROW); + + if (st == CY_FLASH_DRV_SUCCESS) { + memcpy(mock_phys(rowAddr), data, ROW); + } + return st; +} + +cy_en_flashdrv_status_t Cy_Flash_ProgramRow(uint32_t rowAddr, + const uint32_t* data) +{ + const uint8_t *p = mock_phys(rowAddr); + cy_en_flashdrv_status_t st; + uint32_t i; + + /* The PDL requires an erased row */ + for (i = 0; i < ROW; i++) + ck_assert_uint_eq(p[i], 0); + st = mock_op(OP_PROGRAM, rowAddr, ROW); + if (st == CY_FLASH_DRV_SUCCESS) { + memcpy(mock_phys(rowAddr), data, ROW); + } + return st; +} + +cy_en_flashdrv_status_t Cy_Flash_EraseRow(uint32_t rowAddr) +{ + return mock_op(OP_ERASE_ROW, rowAddr, ROW); +} + +cy_en_flashdrv_status_t Cy_Flash_EraseSubsector(uint32_t subSectorAddr) +{ + return mock_op(OP_ERASE_SUBSECTOR, subSectorAddr, SUBSECTOR); +} + +cy_en_flashdrv_status_t Cy_Flash_EraseSector(uint32_t sectorAddr) +{ + return mock_op(OP_ERASE_SECTOR, sectorAddr, SECTOR); +} + +void Cy_SysLib_ClearFlashCacheAndBuffer(void) +{ + cache_clears++; +} + +uint32_t mock_uart_put(CySCB_Type *base, uint32_t data) +{ + (void)base; + ck_assert_int_lt(n_tx, MOCK_TX_MAX); + tx[n_tx++] = (char)data; + return 1UL; +} + +static void fill(uint32_t address, uint8_t seed, uint32_t len) +{ + uint8_t *p = mock_phys(address); + uint32_t i; + + for (i = 0; i < len; i++) { + p[i] = (uint8_t)(seed + i); + } +} + +static void setup(void) +{ + memset(mock_main, 0, sizeof(mock_main)); + memset(mock_wflash, 0, sizeof(mock_wflash)); + memset(ops, 0, sizeof(ops)); + n_ops = 0; + cache_clears = 0; + fail_at_op = 0; + n_tx = 0; +} + +START_TEST(test_range_ok) +{ + ck_assert_int_eq(psoc6_flash_range_ok(PSOC6_FLASH_BASE, ROW), 1); + ck_assert_int_eq(psoc6_flash_range_ok(PSOC6_FLASH_BASE, + PSOC6_FLASH_SIZE), 1); + ck_assert_int_eq(psoc6_flash_range_ok(PSOC6_FLASH_BASE + + PSOC6_FLASH_SIZE - ROW, ROW), 1); + ck_assert_int_eq(psoc6_flash_range_ok(PSOC6_FLASH_BASE + + PSOC6_FLASH_SIZE - ROW, ROW + 1U), 0); + ck_assert_int_eq(psoc6_flash_range_ok(PSOC6_FLASH_BASE - ROW, ROW), 0); + ck_assert_int_eq(psoc6_flash_range_ok(PSOC6_FLASH_BASE, 0), 0); + ck_assert_int_eq(psoc6_flash_range_ok(0xFFFFFE00U, 0x400U), 0); + ck_assert_int_eq(psoc6_flash_range_ok(PSOC6_FLASH_BASE + ROW, + 0xFFFFFFFFU), 0); + ck_assert_int_eq(psoc6_flash_range_ok(PSOC6_WFLASH_BASE, + PSOC6_WFLASH_SIZE), 1); + ck_assert_int_eq(psoc6_flash_range_ok(PSOC6_WFLASH_BASE, + PSOC6_WFLASH_SIZE + 1U), 0); + /* The second-bank alias is only valid with DUALBANK_SWAP */ + ck_assert_int_eq(psoc6_flash_range_ok(PSOC6_FLASH_BANK2_BASE, ROW), 0); +} +END_TEST + +START_TEST(test_write_full_row) +{ + uint8_t data[ROW]; + uint32_t i; + + for (i = 0; i < ROW; i++) + data[i] = (uint8_t)(i * 3U + 1U); + ck_assert_int_eq(hal_flash_write(TEST_ROW, data, ROW), 0); + ck_assert_int_eq(n_ops, 1); + ck_assert_int_eq(ops[0].op, OP_PROGRAM); + ck_assert_uint_eq(ops[0].addr, TEST_ROW); + ck_assert_int_eq(cache_clears, 1); + ck_assert_mem_eq(mock_phys(TEST_ROW), data, ROW); +} +END_TEST + +START_TEST(test_rewrite_programmed_row_erases) +{ + uint8_t data[ROW]; + + fill(TEST_ROW, 0x21, ROW); + memset(data, 0x42, sizeof(data)); + ck_assert_int_eq(hal_flash_write(TEST_ROW, data, ROW), 0); + ck_assert_int_eq(n_ops, 1); + ck_assert_int_eq(ops[0].op, OP_WRITE); + ck_assert_mem_eq(mock_phys(TEST_ROW), data, ROW); +} +END_TEST + +START_TEST(test_write_unaligned_head_preserves_row) +{ + uint8_t before[ROW]; + uint8_t data[10]; + + fill(TEST_ROW, 0x40, ROW); + memcpy(before, mock_phys(TEST_ROW), ROW); + memset(data, 0xA5, sizeof(data)); + + ck_assert_int_eq(hal_flash_write(TEST_ROW + 100U, data, sizeof(data)), 0); + ck_assert_int_eq(n_ops, 1); + ck_assert_uint_eq(ops[0].addr, TEST_ROW); + ck_assert_mem_eq(mock_phys(TEST_ROW), before, 100); + ck_assert_mem_eq(mock_phys(TEST_ROW + 100U), data, sizeof(data)); + ck_assert_mem_eq(mock_phys(TEST_ROW + 110U), before + 110, ROW - 110U); +} +END_TEST + +START_TEST(test_write_partial_tail_preserves_row) +{ + uint8_t before[ROW]; + uint8_t data[ROW + 188U]; + + fill(TEST_ROW + ROW, 0x11, ROW); + memcpy(before, mock_phys(TEST_ROW + ROW), ROW); + memset(data, 0x5A, sizeof(data)); + + ck_assert_int_eq(hal_flash_write(TEST_ROW, data, sizeof(data)), 0); + ck_assert_int_eq(n_ops, 2); + ck_assert_uint_eq(ops[0].addr, TEST_ROW); + ck_assert_uint_eq(ops[1].addr, TEST_ROW + ROW); + ck_assert_mem_eq(mock_phys(TEST_ROW), data, sizeof(data)); + ck_assert_mem_eq(mock_phys(TEST_ROW + sizeof(data)), before + 188, + ROW - 188U); +} +END_TEST + +START_TEST(test_write_across_row_boundary) +{ + uint8_t data[40]; + + memset(data, 0x3C, sizeof(data)); + ck_assert_int_eq(hal_flash_write(TEST_ROW + ROW - 20U, data, + sizeof(data)), 0); + ck_assert_int_eq(n_ops, 2); + ck_assert_uint_eq(ops[0].addr, TEST_ROW); + ck_assert_uint_eq(ops[1].addr, TEST_ROW + ROW); + ck_assert_mem_eq(mock_phys(TEST_ROW + ROW - 20U), data, sizeof(data)); + ck_assert_uint_eq(*(uint8_t*)mock_phys(TEST_ROW + ROW - 21U), 0); + ck_assert_uint_eq(*(uint8_t*)mock_phys(TEST_ROW + ROW + 20U), 0); +} +END_TEST + +START_TEST(test_write_unchanged_row_skipped) +{ + uint8_t data[ROW]; + + fill(TEST_ROW, 0x77, ROW); + memcpy(data, mock_phys(TEST_ROW), ROW); + ck_assert_int_eq(hal_flash_write(TEST_ROW, data, ROW), 0); + ck_assert_int_eq(n_ops, 0); + ck_assert_int_eq(cache_clears, 0); +} +END_TEST + +START_TEST(test_write_rejects_bad_args) +{ + uint8_t data[ROW]; + + memset(data, 1, sizeof(data)); + ck_assert_int_eq(hal_flash_write(TEST_ROW, NULL, ROW), -1); + ck_assert_int_eq(hal_flash_write(TEST_ROW, data, 0), -1); + ck_assert_int_eq(hal_flash_write(TEST_ROW, data, -4), -1); + ck_assert_int_eq(hal_flash_write(PSOC6_FLASH_BASE - ROW, data, ROW), -1); + ck_assert_int_eq(hal_flash_write(PSOC6_FLASH_BASE + PSOC6_FLASH_SIZE - + 10U, data, 20), -1); + ck_assert_int_eq(hal_flash_write(PSOC6_FLASH_BANK2_BASE, data, ROW), -1); + ck_assert_int_eq(n_ops, 0); +} +END_TEST + +/* Built as the application with RAM_CODE: the HAL still provides arch_reboot */ +START_TEST(test_app_arch_reboot_resets) +{ + resets = 0; + arch_reboot(); + ck_assert_int_eq(resets, 1); +} +END_TEST + +START_TEST(test_write_failure_stops) +{ + uint8_t data[3U * ROW]; + + memset(data, 0x99, sizeof(data)); + fail_at_op = 2; + ck_assert_int_eq(hal_flash_write(TEST_ROW, data, sizeof(data)), -1); + ck_assert_int_eq(n_ops, 2); +} +END_TEST + +START_TEST(test_write_work_flash) +{ + uint8_t data[8] = { 1, 2, 3, 4, 5, 6, 7, 8 }; + + ck_assert_int_eq(hal_flash_write(PSOC6_WFLASH_BASE + 4U, data, + sizeof(data)), 0); + ck_assert_int_eq(n_ops, 1); + ck_assert_uint_eq(ops[0].addr, PSOC6_WFLASH_BASE); + ck_assert_mem_eq(mock_phys(PSOC6_WFLASH_BASE + 4U), data, sizeof(data)); +} +END_TEST + +START_TEST(test_erase_picks_largest_unit) +{ + fill(TEST_ROW, 1, SECTOR + SUBSECTOR + ROW); + ck_assert_int_eq(hal_flash_erase(TEST_ROW, SECTOR + SUBSECTOR + ROW), 0); + ck_assert_int_eq(n_ops, 3); + ck_assert_int_eq(ops[0].op, OP_ERASE_SECTOR); + ck_assert_uint_eq(ops[0].addr, TEST_ROW); + ck_assert_int_eq(ops[1].op, OP_ERASE_SUBSECTOR); + ck_assert_uint_eq(ops[1].addr, TEST_ROW + SECTOR); + ck_assert_int_eq(ops[2].op, OP_ERASE_ROW); + ck_assert_uint_eq(ops[2].addr, TEST_ROW + SECTOR + SUBSECTOR); + ck_assert_int_eq(cache_clears, 1); +} +END_TEST + +START_TEST(test_erase_unaligned_start_uses_rows) +{ + fill(TEST_ROW + ROW, 1, SUBSECTOR); + ck_assert_int_eq(hal_flash_erase(TEST_ROW + ROW, SUBSECTOR), 0); + ck_assert_int_eq(n_ops, 8); + ck_assert_int_eq(ops[0].op, OP_ERASE_ROW); + ck_assert_int_eq(ops[6].op, OP_ERASE_ROW); + ck_assert_uint_eq(ops[6].addr, TEST_ROW + 7U * ROW); + ck_assert_int_eq(ops[7].op, OP_ERASE_ROW); + ck_assert_uint_eq(ops[7].addr, TEST_ROW + SUBSECTOR); +} +END_TEST + +START_TEST(test_erase_skips_blank) +{ + fill(TEST_ROW + SUBSECTOR + 8U, 1, 1); + ck_assert_int_eq(hal_flash_erase(TEST_ROW, 2U * SUBSECTOR), 0); + ck_assert_int_eq(n_ops, 1); + ck_assert_int_eq(ops[0].op, OP_ERASE_SUBSECTOR); + ck_assert_uint_eq(ops[0].addr, TEST_ROW + SUBSECTOR); +} +END_TEST + +START_TEST(test_erase_rounds_len_up) +{ + fill(TEST_ROW, 1, ROW); + ck_assert_int_eq(hal_flash_erase(TEST_ROW, 100), 0); + ck_assert_int_eq(n_ops, 1); + ck_assert_int_eq(ops[0].op, OP_ERASE_ROW); + ck_assert_uint_eq(*(uint8_t*)mock_phys(TEST_ROW + ROW - 1U), 0); +} +END_TEST + +START_TEST(test_erase_rejects_bad_args) +{ + ck_assert_int_eq(hal_flash_erase(TEST_ROW + 4U, ROW), -1); + ck_assert_int_eq(hal_flash_erase(TEST_ROW, 0), -1); + ck_assert_int_eq(hal_flash_erase(TEST_ROW, -ROW), -1); + ck_assert_int_eq(hal_flash_erase(PSOC6_FLASH_BASE + PSOC6_FLASH_SIZE - + ROW, ROW + 1), -1); + ck_assert_int_eq(hal_flash_erase(PSOC6_FLASH_BANK2_BASE, ROW), -1); + ck_assert_int_eq(n_ops, 0); +} +END_TEST + +START_TEST(test_erase_failure_stops) +{ + fill(TEST_ROW, 1, 3U * ROW); + fail_at_op = 1; + ck_assert_int_eq(hal_flash_erase(TEST_ROW + ROW, 2U * ROW), -1); + ck_assert_int_eq(n_ops, 1); + ck_assert_int_eq(cache_clears, 1); +} +END_TEST + +START_TEST(test_uart_divider) +{ + uint64_t div_x32 = ((uint64_t)(PSOC6_UART_DIV_INT + 1U) * 32U) + + PSOC6_UART_DIV_FRAC; + uint64_t baud = ((uint64_t)PSOC6_CLK_PERI_HZ * 32U) / + (div_x32 * PSOC6_UART_OVERSAMPLE); + uint64_t err = (baud > PSOC6_UART_BAUD) ? baud - PSOC6_UART_BAUD : + PSOC6_UART_BAUD - baud; + + ck_assert_uint_eq(PSOC6_UART_DIV_INT, 53); + ck_assert_uint_eq(PSOC6_UART_DIV_FRAC, 8); + ck_assert_uint_lt(PSOC6_UART_DIV_FRAC, 32); + /* Under 0.1 % baud error */ + ck_assert_uint_lt(err * 1000U, PSOC6_UART_BAUD); +} +END_TEST + +START_TEST(test_uart_write_crlf) +{ + uart_write("a\nb\n", 4); + ck_assert_int_eq(n_tx, 6); + ck_assert_mem_eq(tx, "a\r\nb\r\n", 6); +} +END_TEST + +Suite *psoc6_flash_suite(void) +{ + Suite *s = suite_create("psoc6-flash"); + TCase *tc = tcase_create("flash"); + + tcase_add_checked_fixture(tc, setup, NULL); + tcase_add_test(tc, test_range_ok); + tcase_add_test(tc, test_write_full_row); + tcase_add_test(tc, test_rewrite_programmed_row_erases); + tcase_add_test(tc, test_write_unaligned_head_preserves_row); + tcase_add_test(tc, test_write_partial_tail_preserves_row); + tcase_add_test(tc, test_write_across_row_boundary); + tcase_add_test(tc, test_write_unchanged_row_skipped); + tcase_add_test(tc, test_write_rejects_bad_args); + tcase_add_test(tc, test_write_failure_stops); + tcase_add_test(tc, test_app_arch_reboot_resets); + tcase_add_test(tc, test_write_work_flash); + tcase_add_test(tc, test_erase_picks_largest_unit); + tcase_add_test(tc, test_erase_unaligned_start_uses_rows); + tcase_add_test(tc, test_erase_skips_blank); + tcase_add_test(tc, test_erase_rounds_len_up); + tcase_add_test(tc, test_erase_rejects_bad_args); + tcase_add_test(tc, test_erase_failure_stops); + tcase_add_test(tc, test_uart_divider); + tcase_add_test(tc, test_uart_write_crlf); + suite_add_tcase(s, tc); + return s; +} + +int main(void) +{ + int fails; + Suite *s = psoc6_flash_suite(); + SRunner *sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + return fails; +} diff --git a/tools/unit-tests/unit-psoc6-qspi.c b/tools/unit-tests/unit-psoc6-qspi.c new file mode 100644 index 0000000000..e28dc157bf --- /dev/null +++ b/tools/unit-tests/unit-psoc6-qspi.c @@ -0,0 +1,319 @@ +/* unit-psoc6-qspi.c + * + * Unit tests for the PSoC 6 QSPI (SMIF) external flash driver in + * hal/psoc6.c, against a model of the SMIF memory slot API, built against + * the Infineon mtb-pdl-cat1 headers. + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ + +#include +#include +#include + +/* PSoC 6 QSPI layout (config/examples/cypsoc6-qspi.config) */ +#define H_TARGETS_TARGET_ +#define WOLFBOOT_FIXED_PARTITIONS +#define WOLFBOOT_SECTOR_SIZE 0x40000 +#define WOLFBOOT_PARTITION_SIZE 0x180000 +#define WOLFBOOT_PARTITION_BOOT_ADDRESS 0x10040000 +#define WOLFBOOT_PARTITION_UPDATE_ADDRESS 0x18000000 +#define WOLFBOOT_PARTITION_SWAP_ADDRESS 0x101C0000 + +#include "cy_device_headers.h" +#include "cy_gpio.h" + +/* The PDL's inline HSIOM accessor writes registers; route it to a mock */ +void mock_set_hsiom(GPIO_PRT_Type *base, uint32_t pinNum, en_hsiom_sel_t value); +#define Cy_GPIO_SetHSIOM mock_set_hsiom + +#include "../../hal/psoc6.c" + +#define QSPI_BASE CY_XIP_BASE +#define QSPI_SIZE 0x04000000U +#define QSPI_ERASE 0x40000U +#define QSPI_PAGE 0x100U +#define MOCK_MAX_OPS 16 + +enum { OP_READ, OP_WRITE, OP_ERASE }; + +uint32_t mock_primask; +int mock_barriers; +int mock_wfi; +NVIC_Type mock_nvic; +SCB_Type mock_scb; +SysTick_Type mock_systick; + +static struct { + int op; + uint32_t offset; + uint32_t len; + const uint8_t *src; + uint8_t *dst; +} ops[MOCK_MAX_OPS]; +static int n_ops; +static cy_en_smif_status_t next_status; + +void mock_wfi_hook(void) +{ +} + +void CY_ASSERT_HANDLER(void) +{ + ck_abort_msg("PDL assert"); +} + +void NVIC_SystemReset(void) +{ +} + +static cy_en_smif_status_t record(int op, uint32_t offset, uint32_t len) +{ + ck_assert_int_lt(n_ops, MOCK_MAX_OPS); + ops[n_ops].op = op; + ops[n_ops].offset = offset; + ops[n_ops].len = len; + n_ops++; + return next_status; +} + +cy_en_smif_status_t Cy_SMIF_MemWrite(SMIF_Type *base, + cy_stc_smif_mem_config_t const *memConfig, uint32_t address, + uint8_t const writeBuff[], uint32_t length, + cy_stc_smif_context_t const *context) +{ + (void)base; + (void)context; + ck_assert_ptr_eq(memConfig, &mem_sfdp_0); + ops[n_ops].src = writeBuff; + return record(OP_WRITE, address, length); +} + +cy_en_smif_status_t Cy_SMIF_MemRead(SMIF_Type *base, + cy_stc_smif_mem_config_t const *memConfig, uint32_t address, + uint8_t readBuff[], uint32_t length, cy_stc_smif_context_t const *context) +{ + (void)base; + (void)context; + ck_assert_ptr_eq(memConfig, &mem_sfdp_0); + ops[n_ops].dst = readBuff; + return record(OP_READ, address, length); +} + +cy_en_smif_status_t Cy_SMIF_MemEraseSector(SMIF_Type *base, + cy_stc_smif_mem_config_t const *memConfig, uint32_t address, + uint32_t length, cy_stc_smif_context_t const *context) +{ + (void)base; + (void)context; + ck_assert_ptr_eq(memConfig, &mem_sfdp_0); + return record(OP_ERASE, address, length); +} + +/* Only the hal_ext_flash_init() test may reach these */ +static int in_init; +#define INIT_ONLY() \ + do { if (!in_init) ck_abort_msg("unexpected PDL call in a host test"); \ + } while (0) +void mock_set_hsiom(GPIO_PRT_Type *base, uint32_t pinNum, en_hsiom_sel_t value) +{ (void)base; (void)pinNum; (void)value; INIT_ONLY(); } +cy_en_gpio_status_t Cy_GPIO_Pin_Init(GPIO_PRT_Type *base, uint32_t pinNum, + const cy_stc_gpio_pin_config_t *config) +{ (void)base; (void)pinNum; (void)config; INIT_ONLY(); return CY_GPIO_SUCCESS; } +cy_en_sysclk_status_t Cy_SysClk_ClkHfSetSource(uint32_t clkHf, + cy_en_clkhf_in_sources_t source) +{ (void)clkHf; (void)source; INIT_ONLY(); return CY_SYSCLK_SUCCESS; } +cy_en_sysclk_status_t Cy_SysClk_ClkHfSetDivider(uint32_t clkHf, + cy_en_clkhf_dividers_t divider) +{ (void)clkHf; (void)divider; INIT_ONLY(); return CY_SYSCLK_SUCCESS; } +cy_en_sysclk_status_t Cy_SysClk_ClkHfEnable(uint32_t clkHf) +{ (void)clkHf; INIT_ONLY(); return CY_SYSCLK_SUCCESS; } +cy_en_smif_status_t Cy_SMIF_Init(SMIF_Type *base, + cy_stc_smif_config_t const *config, uint32_t timeout, + cy_stc_smif_context_t *context) +{ (void)base; (void)config; (void)timeout; (void)context; INIT_ONLY(); + return CY_SMIF_SUCCESS; } +cy_en_smif_status_t Cy_SMIF_MemInit(SMIF_Type *base, + cy_stc_smif_block_config_t const * blockConfig, + cy_stc_smif_context_t *context) +{ (void)base; (void)context; INIT_ONLY(); + ck_assert_ptr_eq(blockConfig, &smifBlockConfig_sfdp); + return CY_SMIF_SUCCESS; } +void Cy_SysLib_Delay(uint32_t milliseconds) +{ (void)milliseconds; ck_abort_msg("SMIF init retried"); } +void Cy_SMIF_Enable(SMIF_Type *base, cy_stc_smif_context_t *context) +{ (void)base; (void)context; INIT_ONLY(); } +#define NOT_CALLED() ck_abort_msg("unexpected PDL call in a host test") +const cy_stc_device_t *cy_device; +cy_en_flashdrv_status_t Cy_Flash_WriteRow(uint32_t rowAddr, + const uint32_t* data) +{ (void)rowAddr; (void)data; NOT_CALLED(); return 0; } +cy_en_flashdrv_status_t Cy_Flash_ProgramRow(uint32_t rowAddr, + const uint32_t* data) +{ (void)rowAddr; (void)data; NOT_CALLED(); return 0; } +cy_en_flashdrv_status_t Cy_Flash_EraseRow(uint32_t rowAddr) +{ (void)rowAddr; NOT_CALLED(); return 0; } +cy_en_flashdrv_status_t Cy_Flash_EraseSubsector(uint32_t subSectorAddr) +{ (void)subSectorAddr; NOT_CALLED(); return 0; } +cy_en_flashdrv_status_t Cy_Flash_EraseSector(uint32_t sectorAddr) +{ (void)sectorAddr; NOT_CALLED(); return 0; } +void Cy_SysLib_ClearFlashCacheAndBuffer(void) +{ NOT_CALLED(); } + +static void setup(void) +{ + memset(ops, 0, sizeof(ops)); + n_ops = 0; + next_status = CY_SMIF_SUCCESS; + dev_sfdp_0.eraseSize = QSPI_ERASE; + dev_sfdp_0.programSize = QSPI_PAGE; + dev_sfdp_0.memSize = QSPI_SIZE; + smif_blk_config = &smifBlockConfig_sfdp; + in_init = 0; +} + +START_TEST(test_write_maps_offset) +{ + uint8_t data[7] = { 1, 2, 3, 4, 5, 6, 7 }; + + ck_assert_int_eq(ext_flash_write(QSPI_BASE + 0x123U, data, 7), 0); + ck_assert_int_eq(n_ops, 1); + ck_assert_int_eq(ops[0].op, OP_WRITE); + ck_assert_uint_eq(ops[0].offset, 0x123U); + ck_assert_uint_eq(ops[0].len, 7); + ck_assert_ptr_eq(ops[0].src, data); +} +END_TEST + +START_TEST(test_write_rejects_out_of_range) +{ + uint8_t data[16]; + + memset(data, 0, sizeof(data)); + ck_assert_int_eq(ext_flash_write(QSPI_BASE - 1U, data, 1), -1); + ck_assert_int_eq(ext_flash_write(QSPI_BASE + QSPI_SIZE - 8U, data, 16), + -1); + ck_assert_int_eq(ext_flash_write(QSPI_BASE + QSPI_SIZE, data, 1), -1); + ck_assert_int_eq(ext_flash_write(QSPI_BASE, data, 0), -1); + ck_assert_int_eq(ext_flash_write(QSPI_BASE, NULL, 4), -1); + ck_assert_int_eq(n_ops, 0); + ck_assert_int_eq(ext_flash_write(QSPI_BASE + QSPI_SIZE - 16U, data, 16), + 0); +} +END_TEST + +START_TEST(test_write_error) +{ + uint8_t data[4] = { 0 }; + + next_status = CY_SMIF_EXCEED_TIMEOUT; + ck_assert_int_eq(ext_flash_write(QSPI_BASE, data, 4), -1); +} +END_TEST + +START_TEST(test_read_returns_len) +{ + uint8_t buf[32]; + + ck_assert_int_eq(ext_flash_read(QSPI_BASE + 0x40000U, buf, 32), 32); + ck_assert_int_eq(ops[0].op, OP_READ); + ck_assert_uint_eq(ops[0].offset, 0x40000U); + ck_assert_ptr_eq(ops[0].dst, buf); + next_status = CY_SMIF_BUSY; + ck_assert_int_eq(ext_flash_read(QSPI_BASE, buf, 32), -1); + ck_assert_int_eq(ext_flash_read(QSPI_BASE + QSPI_SIZE, buf, 1), -1); + ck_assert_int_eq(ext_flash_read(QSPI_BASE, NULL, 1), -1); +} +END_TEST + +START_TEST(test_erase_whole_sectors) +{ + ck_assert_int_eq(ext_flash_erase(QSPI_BASE + QSPI_ERASE, + 2 * QSPI_ERASE), 0); + ck_assert_int_eq(n_ops, 1); + ck_assert_int_eq(ops[0].op, OP_ERASE); + ck_assert_uint_eq(ops[0].offset, QSPI_ERASE); + ck_assert_uint_eq(ops[0].len, 2 * QSPI_ERASE); +} +END_TEST + +START_TEST(test_erase_rejects_partial_sectors) +{ + /* The PDL would round these out and erase data around the request */ + ck_assert_int_eq(ext_flash_erase(QSPI_BASE + 0x1000U, QSPI_ERASE), -1); + ck_assert_int_eq(ext_flash_erase(QSPI_BASE, 0x1000), -1); + ck_assert_int_eq(ext_flash_erase(QSPI_BASE, 0), -1); + ck_assert_int_eq(ext_flash_erase(QSPI_BASE + QSPI_SIZE, QSPI_ERASE), -1); + ck_assert_int_eq(n_ops, 0); +} +END_TEST + +/* hal_ext_flash_init() fails when the SFDP erase size does not fit the + * wolfBoot sector, before any flash operation */ +START_TEST(test_init_checks_geometry) +{ + in_init = 1; + ck_assert_int_eq(hal_ext_flash_init(), CY_SMIF_SUCCESS); + dev_sfdp_0.eraseSize = 0x80000U; + ck_assert_int_eq(hal_ext_flash_init(), CY_SMIF_BAD_PARAM); + dev_sfdp_0.eraseSize = 0x30000U; + ck_assert_int_eq(hal_ext_flash_init(), CY_SMIF_BAD_PARAM); + ck_assert_int_eq(n_ops, 0); +} +END_TEST + +START_TEST(test_geometry) +{ + ck_assert_int_eq(psoc6_qspi_geometry_ok(0x40000U), 1); + ck_assert_int_eq(psoc6_qspi_geometry_ok(0x1000U), 1); + ck_assert_int_eq(psoc6_qspi_geometry_ok(0x80000U), 0); + ck_assert_int_eq(psoc6_qspi_geometry_ok(0x30000U), 0); + ck_assert_int_eq(psoc6_qspi_geometry_ok(0), 0); +} +END_TEST + +Suite *psoc6_qspi_suite(void) +{ + Suite *s = suite_create("psoc6-qspi"); + TCase *tc = tcase_create("qspi"); + + tcase_add_checked_fixture(tc, setup, NULL); + tcase_add_test(tc, test_write_maps_offset); + tcase_add_test(tc, test_write_rejects_out_of_range); + tcase_add_test(tc, test_write_error); + tcase_add_test(tc, test_read_returns_len); + tcase_add_test(tc, test_erase_whole_sectors); + tcase_add_test(tc, test_erase_rejects_partial_sectors); + tcase_add_test(tc, test_init_checks_geometry); + tcase_add_test(tc, test_geometry); + suite_add_tcase(s, tc); + return s; +} + +int main(void) +{ + int fails; + Suite *s = psoc6_qspi_suite(); + SRunner *sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + fails = srunner_ntests_failed(sr); + srunner_free(sr); + return fails; +} From 650a380b1b1b5b85281f033aba67299138355ea8 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 18:50:10 +0100 Subject: [PATCH 4/4] Clock the PSoC 6 QSPI interface from the PLL --- hal/psoc6.c | 9 +++++---- tools/unit-tests/unit-psoc6-qspi.c | 29 +++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/hal/psoc6.c b/hal/psoc6.c index 669abcf703..699114dcbd 100644 --- a/hal/psoc6.c +++ b/hal/psoc6.c @@ -139,7 +139,7 @@ static SMIF_Type* QSPIPort = SMIF0; * * SMIF Block - SMIF0 */ -#define CY_SMIF_SYSCLK_HFCLK_DIVIDER (CY_SYSCLK_CLKHF_DIVIDE_BY_4) +#define CY_SMIF_SYSCLK_HFCLK_DIVIDER (CY_SYSCLK_CLKHF_DIVIDE_BY_8) #define CY_SMIF_INIT_TRY_COUNT (10U) #define CY_SMIF_INIT_RETRY_DELAY_MS (500U) #define CY_SMIF_INIT_TIMEOUT_US (1000U) @@ -290,7 +290,7 @@ static const cy_stc_smif_config_t QSPI_config = { * * Initialization steps: * 1. Configure QSPI data, clock, and slave-select GPIO pins - * 2. Enable SMIF HF clock (CLK_HF2) with divide-by-4 + * 2. Enable SMIF HF clock (CLK_HF2) from the PLL with divide-by-8 * 3. Initialize SMIF block in normal mode * 4. Enable SMIF block * 5. Initialize external memory configuration with retry @@ -330,9 +330,10 @@ cy_en_smif_status_t hal_ext_flash_init(void) (void)Cy_GPIO_Pin_Init(qspi_SS_Port, qspi_SS_Pin, &qspi_ss_config); Cy_GPIO_SetHSIOM(qspi_SS_Port, qspi_SS_Pin, qspi_SS_Mux); - /* Step 2: Enable SMIF HF clock (CLK_HF2) sourced from CLK_PATH0 */ + /* Step 2: Enable SMIF HF clock (CLK_HF2) from the PLL, 100 MHz / 8. The + * FLL on CLK_PATH0 keeps whatever the last loader left, even unlocked. */ (void)Cy_SysClk_ClkHfSetSource(CY_SYSCLK_CLKHF_IN_CLKPATH2, - CY_SYSCLK_CLKHF_IN_CLKPATH0); + CY_SYSCLK_CLKHF_IN_CLKPATH1); (void)Cy_SysClk_ClkHfSetDivider(CY_SYSCLK_CLKHF_IN_CLKPATH2, CY_SMIF_SYSCLK_HFCLK_DIVIDER); (void)Cy_SysClk_ClkHfEnable(CY_SYSCLK_CLKHF_IN_CLKPATH2); diff --git a/tools/unit-tests/unit-psoc6-qspi.c b/tools/unit-tests/unit-psoc6-qspi.c index e28dc157bf..9ee1b7b8a4 100644 --- a/tools/unit-tests/unit-psoc6-qspi.c +++ b/tools/unit-tests/unit-psoc6-qspi.c @@ -136,12 +136,24 @@ void mock_set_hsiom(GPIO_PRT_Type *base, uint32_t pinNum, en_hsiom_sel_t value) cy_en_gpio_status_t Cy_GPIO_Pin_Init(GPIO_PRT_Type *base, uint32_t pinNum, const cy_stc_gpio_pin_config_t *config) { (void)base; (void)pinNum; (void)config; INIT_ONLY(); return CY_GPIO_SUCCESS; } +static uint32_t smif_clk_src; +static uint32_t smif_clk_div; cy_en_sysclk_status_t Cy_SysClk_ClkHfSetSource(uint32_t clkHf, cy_en_clkhf_in_sources_t source) -{ (void)clkHf; (void)source; INIT_ONLY(); return CY_SYSCLK_SUCCESS; } +{ + INIT_ONLY(); + if (clkHf == 2U) + smif_clk_src = (uint32_t)source; + return CY_SYSCLK_SUCCESS; +} cy_en_sysclk_status_t Cy_SysClk_ClkHfSetDivider(uint32_t clkHf, cy_en_clkhf_dividers_t divider) -{ (void)clkHf; (void)divider; INIT_ONLY(); return CY_SYSCLK_SUCCESS; } +{ + INIT_ONLY(); + if (clkHf == 2U) + smif_clk_div = (uint32_t)divider; + return CY_SYSCLK_SUCCESS; +} cy_en_sysclk_status_t Cy_SysClk_ClkHfEnable(uint32_t clkHf) { (void)clkHf; INIT_ONLY(); return CY_SYSCLK_SUCCESS; } cy_en_smif_status_t Cy_SMIF_Init(SMIF_Type *base, @@ -186,6 +198,8 @@ static void setup(void) dev_sfdp_0.memSize = QSPI_SIZE; smif_blk_config = &smifBlockConfig_sfdp; in_init = 0; + smif_clk_src = 0xFFU; + smif_clk_div = 0xFFU; } START_TEST(test_write_maps_offset) @@ -278,6 +292,16 @@ START_TEST(test_init_checks_geometry) } END_TEST +/* The SMIF clock comes from the PLL that wolfBoot sets up, never the FLL */ +START_TEST(test_init_clocks_smif_from_pll) +{ + in_init = 1; + ck_assert_int_eq(hal_ext_flash_init(), CY_SMIF_SUCCESS); + ck_assert_uint_eq(smif_clk_src, (uint32_t)CY_SYSCLK_CLKHF_IN_CLKPATH1); + ck_assert_uint_eq(smif_clk_div, (uint32_t)CY_SYSCLK_CLKHF_DIVIDE_BY_8); +} +END_TEST + START_TEST(test_geometry) { ck_assert_int_eq(psoc6_qspi_geometry_ok(0x40000U), 1); @@ -301,6 +325,7 @@ Suite *psoc6_qspi_suite(void) tcase_add_test(tc, test_erase_whole_sectors); tcase_add_test(tc, test_erase_rejects_partial_sectors); tcase_add_test(tc, test_init_checks_geometry); + tcase_add_test(tc, test_init_clocks_smif_from_pll); tcase_add_test(tc, test_geometry); suite_add_tcase(s, tc); return s;