diff --git a/boards/stm32f4/board_stm32f4.c b/boards/stm32f4/board_stm32f4.c index 6e68af2..db59a92 100644 --- a/boards/stm32f4/board_stm32f4.c +++ b/boards/stm32f4/board_stm32f4.c @@ -124,41 +124,32 @@ static int stm32f4_flash_write(uint32_t addr, const void *buf, size_t len) static int stm32f4_flash_erase(uint32_t addr, size_t len) { - (void)len; - - /* Determine sector number from address */ - int sector = -1; - if (addr >= 0x08000000 && addr < 0x08004000) sector = 0; - else if (addr >= 0x08004000 && addr < 0x08008000) sector = 1; - else if (addr >= 0x08008000 && addr < 0x0800C000) sector = 2; - else if (addr >= 0x0800C000 && addr < 0x08010000) sector = 3; - else if (addr >= 0x08010000 && addr < 0x08020000) sector = 4; - else if (addr >= 0x08020000 && addr < 0x08040000) sector = 5; - else if (addr >= 0x08040000 && addr < 0x08060000) sector = 6; - else if (addr >= 0x08060000 && addr < 0x08080000) sector = 7; - else if (addr >= 0x08080000 && addr < 0x080A0000) sector = 8; - else if (addr >= 0x080A0000 && addr < 0x080C0000) sector = 9; - else if (addr >= 0x080C0000 && addr < 0x080E0000) sector = 10; - else if (addr >= 0x080E0000 && addr < 0x08100000) sector = 11; - - if (sector < 0) + /* Erase every sector [addr, addr + len) touches. Only the sector + * holding addr used to be erased and len was ignored, so erasing a + * 256K slot cleared its first sector and left stale image bytes in + * the rest -- which the following program step cannot overwrite. */ + int first, last; + if (stm32f4_flash_sector_range(addr, len, &first, &last) != 0) return EOS_ERR_INVALID; flash_unlock(); - flash_wait_busy(); - FLASH_CR &= ~(0xF << 3); /* Clear SNB bits */ - FLASH_CR |= (sector << 3); /* Set sector number */ - FLASH_CR |= (1 << 1); /* SER — sector erase */ - FLASH_CR |= (1 << 16); /* STRT — start erase */ + for (int sector = first; sector <= last; sector++) { + flash_wait_busy(); + + FLASH_CR &= ~(0xF << 3); /* Clear SNB bits */ + FLASH_CR |= (sector << 3); /* Set sector number */ + FLASH_CR |= (1 << 1); /* SER — sector erase */ + FLASH_CR |= (1 << 16); /* STRT — start erase */ - flash_wait_busy(); + flash_wait_busy(); - if (FLASH_SR & 0xF0) { - FLASH_SR = 0xF0; - FLASH_CR &= ~(1 << 1); - flash_lock(); - return EOS_ERR_FLASH; + if (FLASH_SR & 0xF0) { + FLASH_SR = 0xF0; + FLASH_CR &= ~(1 << 1); + flash_lock(); + return EOS_ERR_FLASH; + } } FLASH_CR &= ~(1 << 1); /* Clear SER */ diff --git a/boards/stm32f4/board_stm32f4.h b/boards/stm32f4/board_stm32f4.h index 23e1e5e..525a04b 100644 --- a/boards/stm32f4/board_stm32f4.h +++ b/boards/stm32f4/board_stm32f4.h @@ -15,15 +15,25 @@ /* * STM32F407 Flash Memory Map (1MB total) * - * 0x08000000 - 0x08003FFF (16K) Stage-0 (eBootloader) - * 0x08004000 - 0x0800FFFF (48K) Stage-1 (E-Boot) - * 0x08010000 - 0x0807FFFF (448K) Slot A (application firmware) - * 0x08080000 - 0x080EFFFF (448K) Slot B (candidate firmware) - * 0x080F0000 - 0x080F3FFF (16K) Recovery image (golden) - * 0x080F4000 - 0x080F5FFF (8K) Boot control (primary) - * 0x080F6000 - 0x080F7FFF (8K) Boot control (backup) - * 0x080F8000 - 0x080F9FFF (8K) Boot log - * 0x080FA000 - 0x080FFFFF (24K) Device config / reserved + * The F407 erases whole sectors only, and they are not uniform: + * sectors 0-3 are 16K, sector 4 is 64K, sectors 5-11 are 128K. Every + * region therefore owns whole sectors -- erasing one region (a slot + * update, a boot control save, a log clear) must never erase another. + * Slot B, the recovery image, both boot control copies and the boot log + * used to share sector 11, so saving the boot control block wiped the + * other four. + * + * Sector Address range Size Region + * 0 0x08000000 - 0x08003FFF 16K Stage-0 (eBootloader) + * 1-3 0x08004000 - 0x0800FFFF 48K Stage-1 (E-Boot) + * 4 0x08010000 - 0x0801FFFF 64K Boot log + * 5-6 0x08020000 - 0x0805FFFF 256K Slot A (application firmware) + * 7-8 0x08060000 - 0x0809FFFF 256K Slot B (candidate firmware) + * 9 0x080A0000 - 0x080BFFFF 128K Recovery image (golden) + * 10 0x080C0000 - 0x080DFFFF 128K Boot control (primary) + * 11 0x080E0000 - 0x080FFFFF 128K Boot control (backup) + * + * Stage-0 and stage-1 match stm32f4_stage0.ld and stm32f4_stage1.ld. */ #define STM32F4_FLASH_BASE 0x08000000 @@ -35,22 +45,73 @@ #define STM32F4_STAGE1_ADDR 0x08004000 #define STM32F4_STAGE1_SIZE (48 * 1024) -#define STM32F4_SLOT_A_ADDR 0x08010000 -#define STM32F4_SLOT_A_SIZE (448 * 1024) +#define STM32F4_LOG_ADDR 0x08010000 +#define STM32F4_LOG_SIZE (64 * 1024) -#define STM32F4_SLOT_B_ADDR 0x08080000 -#define STM32F4_SLOT_B_SIZE (448 * 1024) +#define STM32F4_SLOT_A_ADDR 0x08020000 +#define STM32F4_SLOT_A_SIZE (256 * 1024) -#define STM32F4_RECOVERY_ADDR 0x080F0000 -#define STM32F4_RECOVERY_SIZE (16 * 1024) +#define STM32F4_SLOT_B_ADDR 0x08060000 +#define STM32F4_SLOT_B_SIZE (256 * 1024) -#define STM32F4_BOOTCTL_ADDR 0x080F4000 -#define STM32F4_BOOTCTL_BACKUP_ADDR 0x080F6000 +#define STM32F4_RECOVERY_ADDR 0x080A0000 +#define STM32F4_RECOVERY_SIZE (128 * 1024) -#define STM32F4_LOG_ADDR 0x080F8000 +#define STM32F4_BOOTCTL_ADDR 0x080C0000 +#define STM32F4_BOOTCTL_SIZE (128 * 1024) + +#define STM32F4_BOOTCTL_BACKUP_ADDR 0x080E0000 +#define STM32F4_BOOTCTL_BACKUP_SIZE (128 * 1024) #define STM32F4_VECTOR_OFFSET (STM32F4_STAGE1_ADDR - STM32F4_FLASH_BASE) +/* ---- Flash sector geometry ---- */ + +#define STM32F4_FLASH_SECTORS 12 + +/** + * @brief Sector number holding @p addr, or -1 outside the 1MB array. + */ +static inline int stm32f4_flash_sector_of(uint32_t addr) +{ + if (addr < STM32F4_FLASH_BASE) + return -1; + uint32_t off = addr - STM32F4_FLASH_BASE; + if (off < 0x10000) /* sectors 0-3: 16K each */ + return (int)(off / 0x4000); + if (off < 0x20000) /* sector 4: 64K */ + return 4; + if (off < STM32F4_FLASH_SIZE) /* sectors 5-11: 128K each */ + return 4 + (int)(off / 0x20000); + return -1; +} + +/** + * @brief First and last sector an erase of [addr, addr + len) must clear. + * + * Every sector the range touches is included. A sector is the smallest + * unit the F407 can erase, so the erase may clear bytes past either end of + * the range; the memory map above keeps each region sector-aligned so + * those bytes never belong to another region. + * + * @return 0 on success; -1 for len == 0, or a range that wraps or leaves + * the flash array (nothing is to be erased then). + */ +static inline int stm32f4_flash_sector_range(uint32_t addr, size_t len, + int *first, int *last) +{ + if (len == 0 || (uint64_t)addr + len > (uint64_t)STM32F4_FLASH_BASE + + STM32F4_FLASH_SIZE) + return -1; + int f = stm32f4_flash_sector_of(addr); + int l = stm32f4_flash_sector_of((uint32_t)(addr + len - 1)); + if (f < 0 || l < 0) + return -1; + *first = f; + *last = l; + return 0; +} + /* UART for recovery */ #define STM32F4_UART_INSTANCE 2 /* USART2 on PA2/PA3 */ diff --git a/docs/architecture.md b/docs/architecture.md index e67396f..dda1164 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -34,15 +34,17 @@ Address Range Size Region ───────────────────────────────────────────────────── 0x08000000 - 0x08003FFF 16K Stage-0 (eBootloader) 0x08004000 - 0x0800FFFF 48K Stage-1 (E-Boot) -0x08010000 - 0x0807FFFF 448K Slot A (active firmware) -0x08080000 - 0x080EFFFF 448K Slot B (candidate firmware) -0x080F0000 - 0x080F3FFF 16K Recovery image -0x080F4000 - 0x080F5FFF 8K Boot control (primary) -0x080F6000 - 0x080F7FFF 8K Boot control (backup) -0x080F8000 - 0x080F9FFF 8K Boot log -0x080FA000 - 0x080FFFFF 24K Device config / reserved +0x08010000 - 0x0801FFFF 64K Boot log +0x08020000 - 0x0805FFFF 256K Slot A (active firmware) +0x08060000 - 0x0809FFFF 256K Slot B (candidate firmware) +0x080A0000 - 0x080BFFFF 128K Recovery image +0x080C0000 - 0x080DFFFF 128K Boot control (primary) +0x080E0000 - 0x080FFFFF 128K Boot control (backup) ``` +The F407 erases whole sectors (4 x 16K, 1 x 64K, 7 x 128K), so every +region owns whole sectors and erasing one never touches another. + ## Boot Flow ``` diff --git a/docs/installation_usage.md b/docs/installation_usage.md index eafef96..d805782 100644 --- a/docs/installation_usage.md +++ b/docs/installation_usage.md @@ -161,7 +161,7 @@ python3 -m pip install -r requirements.txt # Pack firmware with eboot image header. The input limits (size, load and # entry addresses) are listed in quickstart.md and in tools/imgpack.py. python tools/imgpack.py --input firmware.bin --output firmware.packed.bin \ - --version 1.0.0 --load-addr 0x08010000 --entry-addr 0x08010100 + --version 1.0.0 --load-addr 0x08020000 --entry-addr 0x08020100 # Sign the image python tools/sign_image.py firmware.packed.bin --key signing_key.pem diff --git a/docs/memory-map.md b/docs/memory-map.md index f448441..f0f2860 100644 --- a/docs/memory-map.md +++ b/docs/memory-map.md @@ -8,19 +8,17 @@ ├──────────────────────────────────────────────────────────┤ │ 0x08004000 Stage-1 (E-Boot) 48 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x08010000 Slot A (Active Firmware) 448 KB │ +│ 0x08010000 Boot Log 64 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x08080000 Slot B (Candidate Firmware) 448 KB │ +│ 0x08020000 Slot A (Active Firmware) 256 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x080F0000 Recovery Image (Golden) 16 KB │ +│ 0x08060000 Slot B (Candidate Firmware) 256 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x080F4000 Boot Control (Primary) 8 KB │ +│ 0x080A0000 Recovery Image (Golden) 128 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x080F6000 Boot Control (Backup) 8 KB │ +│ 0x080C0000 Boot Control (Primary) 128 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x080F8000 Boot Log 8 KB │ -├──────────────────────────────────────────────────────────┤ -│ 0x080FA000 Device Config / Reserved 24 KB │ +│ 0x080E0000 Boot Control (Backup) 128 KB │ └──────────────────────────────────────────────────────────┘ ``` @@ -29,8 +27,8 @@ 1. **Stage-0 is never updated in the field** — it must remain immutable after manufacturing to guarantee recovery is always possible. -2. **Boot control is dual-copied** — primary at 0x080F4000, backup at - 0x080F6000. Both carry CRC32 protection. On load, if primary is +2. **Boot control is dual-copied** — primary at 0x080C0000, backup at + 0x080E0000. Both carry CRC32 protection. On load, if primary is corrupt the backup is used. 3. **Slot A and Slot B are identical in size** — this simplifies the @@ -42,6 +40,11 @@ 5. **Boot log is a circular buffer** — the `log_head` field in the boot control block tracks the write position. +6. **Every region owns whole erase sectors** — flash is erased a sector at + a time, so a region that shares a sector is wiped whenever its neighbour + is erased. On the F407 (4 x 16K, 1 x 64K, 7 x 128K sectors) that is why + the small regions take a full sector each. + ## Adapting to Other MCUs To port to a different MCU, create a new `boards//` directory and: diff --git a/docs/quickstart.md b/docs/quickstart.md index 19e1eaf..700d386 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -102,7 +102,7 @@ cmake -B build -DEBLDR_BOARD= -DCMAKE_BUILD_TYPE=Release \ # [load-addr, load-addr + size). --load-addr 0 means "unset" and skips the # entry check, as the bootloader does. python3 imgpack.py --input firmware.bin --output firmware.eimg \ - --load-addr 0x08010000 --entry-addr 0x08010100 --version 1.0.0 + --load-addr 0x08020000 --entry-addr 0x08020100 --version 1.0.0 # Sign it. This also sets the SHA-256 flag and marks the header as v2. python3 sign_image.py --image firmware.eimg --method ed25519 \ diff --git a/docs/secure_boot_chain.md b/docs/secure_boot_chain.md index 4b6c512..110c740 100644 --- a/docs/secure_boot_chain.md +++ b/docs/secure_boot_chain.md @@ -56,7 +56,7 @@ The eBootloader implements a staged chain of trust where each boot stage verifie ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ APPLICATION FIRMWARE │ -│ 448 KB × 2 slots (A/B) │ +│ 256 KB × 2 slots (A/B) │ │ │ │ • Application entry point (vector table) │ │ • Self-test → eos_fw_confirm_running_image() │ diff --git a/docs/threat_model.md b/docs/threat_model.md index 7fff178..12a8773 100644 --- a/docs/threat_model.md +++ b/docs/threat_model.md @@ -45,7 +45,7 @@ │ 48 KB — image scan, boot policy, boot log │ ├─ ─ ─ ─ ─ ─ TB-3: Stage-1 → Application ─ ─ ─ ─ ┤ │ Application Firmware │ -│ 448 KB × 2 slots (A/B) │ +│ 256 KB × 2 slots (A/B) │ └──────────────────────────────────────────────────┘ External boundaries: @@ -80,7 +80,7 @@ External boundaries: ### 3.2 Flash Contents -- **Regions:** Slot A (448 KB), Slot B (448 KB), Recovery (16 KB), Boot control primary (8 KB), Boot control backup (8 KB), Boot log (8 KB), Device config (24 KB) +- **Regions:** Slot A (256 KB), Slot B (256 KB), Recovery (128 KB), Boot control primary (128 KB), Boot control backup (128 KB), Boot log (64 KB) -- one or more whole flash sectors each - **Attack vector:** Direct flash modification (physical), corrupted OTA writes, bit-flip attacks ### 3.3 JTAG/SWD Debug Interface diff --git a/llms-full.txt b/llms-full.txt index ddd4f9d..c18fe9e 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -57,15 +57,17 @@ Address Range Size Region ───────────────────────────────────────────────────── 0x08000000 - 0x08003FFF 16K Stage-0 (eBootloader) 0x08004000 - 0x0800FFFF 48K Stage-1 (E-Boot) -0x08010000 - 0x0807FFFF 448K Slot A (active firmware) -0x08080000 - 0x080EFFFF 448K Slot B (candidate firmware) -0x080F0000 - 0x080F3FFF 16K Recovery image -0x080F4000 - 0x080F5FFF 8K Boot control (primary) -0x080F6000 - 0x080F7FFF 8K Boot control (backup) -0x080F8000 - 0x080F9FFF 8K Boot log -0x080FA000 - 0x080FFFFF 24K Device config / reserved +0x08010000 - 0x0801FFFF 64K Boot log +0x08020000 - 0x0805FFFF 256K Slot A (active firmware) +0x08060000 - 0x0809FFFF 256K Slot B (candidate firmware) +0x080A0000 - 0x080BFFFF 128K Recovery image +0x080C0000 - 0x080DFFFF 128K Boot control (primary) +0x080E0000 - 0x080FFFFF 128K Boot control (backup) ``` +The F407 erases whole sectors (4 x 16K, 1 x 64K, 7 x 128K), so every +region owns whole sectors and erasing one never touches another. + ## Boot Flow ``` @@ -190,7 +192,7 @@ The eBootloader implements a staged chain of trust where each boot stage verifie ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ APPLICATION FIRMWARE │ -│ 448 KB × 2 slots (A/B) │ +│ 256 KB × 2 slots (A/B) │ │ │ │ • Application entry point (vector table) │ │ • Self-test → eos_fw_confirm_running_image() │ @@ -657,7 +659,7 @@ Planned for future releases. │ 48 KB — image scan, boot policy, boot log │ ├─ ─ ─ ─ ─ ─ TB-3: Stage-1 → Application ─ ─ ─ ─ ┤ │ Application Firmware │ -│ 448 KB × 2 slots (A/B) │ +│ 256 KB × 2 slots (A/B) │ └──────────────────────────────────────────────────┘ External boundaries: @@ -692,7 +694,7 @@ External boundaries: ### 3.2 Flash Contents -- **Regions:** Slot A (448 KB), Slot B (448 KB), Recovery (16 KB), Boot control primary (8 KB), Boot control backup (8 KB), Boot log (8 KB), Device config (24 KB) +- **Regions:** Slot A (256 KB), Slot B (256 KB), Recovery (128 KB), Boot control primary (128 KB), Boot control backup (128 KB), Boot log (64 KB) -- one or more whole flash sectors each - **Attack vector:** Direct flash modification (physical), corrupted OTA writes, bit-flip attacks ### 3.3 JTAG/SWD Debug Interface @@ -1477,19 +1479,17 @@ value for `EBLDR_PRODUCTION_KEY`. ├──────────────────────────────────────────────────────────┤ │ 0x08004000 Stage-1 (E-Boot) 48 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x08010000 Slot A (Active Firmware) 448 KB │ -├──────────────────────────────────────────────────────────┤ -│ 0x08080000 Slot B (Candidate Firmware) 448 KB │ +│ 0x08010000 Boot Log 64 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x080F0000 Recovery Image (Golden) 16 KB │ +│ 0x08020000 Slot A (Active Firmware) 256 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x080F4000 Boot Control (Primary) 8 KB │ +│ 0x08060000 Slot B (Candidate Firmware) 256 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x080F6000 Boot Control (Backup) 8 KB │ +│ 0x080A0000 Recovery Image (Golden) 128 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x080F8000 Boot Log 8 KB │ +│ 0x080C0000 Boot Control (Primary) 128 KB │ ├──────────────────────────────────────────────────────────┤ -│ 0x080FA000 Device Config / Reserved 24 KB │ +│ 0x080E0000 Boot Control (Backup) 128 KB │ └──────────────────────────────────────────────────────────┘ ``` @@ -1498,8 +1498,8 @@ value for `EBLDR_PRODUCTION_KEY`. 1. **Stage-0 is never updated in the field** — it must remain immutable after manufacturing to guarantee recovery is always possible. -2. **Boot control is dual-copied** — primary at 0x080F4000, backup at - 0x080F6000. Both carry CRC32 protection. On load, if primary is +2. **Boot control is dual-copied** — primary at 0x080C0000, backup at + 0x080E0000. Both carry CRC32 protection. On load, if primary is corrupt the backup is used. 3. **Slot A and Slot B are identical in size** — this simplifies the @@ -1511,6 +1511,11 @@ value for `EBLDR_PRODUCTION_KEY`. 5. **Boot log is a circular buffer** — the `log_head` field in the boot control block tracks the write position. +6. **Every region owns whole erase sectors** — flash is erased a sector at + a time, so a region that shares a sector is wiped whenever its neighbour + is erased. On the F407 (4 x 16K, 1 x 64K, 7 x 128K sectors) that is why + the small regions take a full sector each. + ## Adapting to Other MCUs To port to a different MCU, create a new `boards//` directory and: @@ -1698,7 +1703,7 @@ python3 -m pip install -r requirements.txt # Pack firmware with eboot image header. The input limits (size, load and # entry addresses) are listed in quickstart.md and in tools/imgpack.py. python tools/imgpack.py --input firmware.bin --output firmware.packed.bin \ - --version 1.0.0 --load-addr 0x08010000 --entry-addr 0x08010100 + --version 1.0.0 --load-addr 0x08020000 --entry-addr 0x08020100 # Sign the image python tools/sign_image.py firmware.packed.bin --key signing_key.pem @@ -1874,7 +1879,7 @@ cmake -B build -DEBLDR_BOARD= -DCMAKE_BUILD_TYPE=Release \ # [load-addr, load-addr + size). --load-addr 0 means "unset" and skips the # entry check, as the bootloader does. python3 imgpack.py --input firmware.bin --output firmware.eimg \ - --load-addr 0x08010000 --entry-addr 0x08010100 --version 1.0.0 + --load-addr 0x08020000 --entry-addr 0x08020100 --version 1.0.0 # Sign it. This also sets the SHA-256 flag and marks the header as v2. python3 sign_image.py --image firmware.eimg --method ed25519 \ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d0ff509..66e69be 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -242,6 +242,17 @@ target_link_libraries(eboot_test_keystore_production PRIVATE eboot_core) add_test(NAME test_keystore_production COMMAND eboot_test_keystore_production) list(APPEND EBLDR_UNIT_TESTS test_keystore_production) +# --- test_stm32f4_flash_layout: erase covers its range, regions own their sectors --- +# board_stm32f4.c is cross-only, but its sector range helper and memory map +# live in board_stm32f4.h as plain C. +add_executable(eboot_test_stm32f4_flash_layout unit/test_stm32f4_flash_layout.c) +target_include_directories(eboot_test_stm32f4_flash_layout PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../boards/stm32f4 +) +target_link_libraries(eboot_test_stm32f4_flash_layout PRIVATE eboot_core) +add_test(NAME test_stm32f4_flash_layout COMMAND eboot_test_stm32f4_flash_layout) +list(APPEND EBLDR_UNIT_TESTS test_stm32f4_flash_layout) + # --- Valgrind test targets --- # # The list is derived from the suites registered above, not written out again. diff --git a/tests/unit/test_stm32f4_flash_layout.c b/tests/unit/test_stm32f4_flash_layout.c new file mode 100644 index 0000000..01d04f1 --- /dev/null +++ b/tests/unit/test_stm32f4_flash_layout.c @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 EoS Project +// ISO/IEC 25000 | ISO/IEC/IEEE 15288:2023 + +/** + * @file test_stm32f4_flash_layout.c + * @brief STM32F4 erase covers the whole range; no two regions share a sector. + * + * Two defects, one consequence. stm32f4_flash_erase() erased only the + * sector holding addr and ignored len, so a 448K slot erase cleared one + * sector of it. And the memory map put Slot B's tail, the recovery image, + * both boot control copies and the boot log in sector 11, so the erase + * that eos_bootctl_save() issues before every write wiped the other four. + * + * board_stm32f4.c drives flash registers and is built only by a cross + * build; its erase takes the sector range from stm32f4_flash_sector_range() + * in board_stm32f4.h, which is plain C and is what this suite exercises, + * together with the memory map macros in the same header. + */ + +#include "board_stm32f4.h" +#include "eos_bootctl.h" +#include +#include +#include + +static int tests_run = 0; +static int tests_passed = 0; + +#define ASSERT(cond) do { \ + if (!(cond)) { \ + printf("[FAIL] %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + exit(1); \ + } \ +} while (0) + +#define TEST(name) \ + static void name(void); \ + static void run_##name(void) { \ + printf(" %-58s ", #name); \ + tests_run++; \ + name(); \ + tests_passed++; \ + printf("[PASS]\n"); \ + } \ + static void name(void) + +#define FLASH_END ((uint32_t)STM32F4_FLASH_BASE + STM32F4_FLASH_SIZE) + +/* RM0090 table 5: STM32F405/407 main memory sectors. */ +static const uint32_t sector_start[STM32F4_FLASH_SECTORS + 1] = { + 0x08000000, 0x08004000, 0x08008000, 0x0800C000, 0x08010000, 0x08020000, + 0x08040000, 0x08060000, 0x08080000, 0x080A0000, 0x080C0000, 0x080E0000, + 0x08100000, +}; + +static int is_sector_boundary(uint32_t addr) +{ + for (int i = 0; i <= STM32F4_FLASH_SECTORS; i++) + if (sector_start[i] == addr) return 1; + return 0; +} + +TEST(test_sector_of_matches_the_reference_manual) +{ + for (int i = 0; i < STM32F4_FLASH_SECTORS; i++) { + ASSERT(stm32f4_flash_sector_of(sector_start[i]) == i); + ASSERT(stm32f4_flash_sector_of(sector_start[i + 1] - 1) == i); + } + ASSERT(stm32f4_flash_sector_of(STM32F4_FLASH_BASE - 1) == -1); + ASSERT(stm32f4_flash_sector_of(FLASH_END) == -1); +} + +TEST(test_erase_range_covers_every_sector_it_touches) +{ + int first = -1, last = -1; + + /* A whole slot: both of its sectors, not just the first. */ + ASSERT(stm32f4_flash_sector_range(STM32F4_SLOT_A_ADDR, STM32F4_SLOT_A_SIZE, + &first, &last) == 0); + ASSERT(first == 5 && last == 6); + ASSERT(stm32f4_flash_sector_range(STM32F4_SLOT_B_ADDR, STM32F4_SLOT_B_SIZE, + &first, &last) == 0); + ASSERT(first == 7 && last == 8); + + /* Spanning the 16K / 64K / 128K geometry changes. */ + ASSERT(stm32f4_flash_sector_range(0x0800C000, 0x14001, &first, &last) == 0); + ASSERT(first == 3 && last == 5); + + /* The whole array. */ + ASSERT(stm32f4_flash_sector_range(STM32F4_FLASH_BASE, STM32F4_FLASH_SIZE, + &first, &last) == 0); + ASSERT(first == 0 && last == STM32F4_FLASH_SECTORS - 1); + + /* A small erase stays in its sector. */ + ASSERT(stm32f4_flash_sector_range(STM32F4_BOOTCTL_ADDR, sizeof(eos_bootctl_t), + &first, &last) == 0); + ASSERT(first == 10 && last == 10); +} + +TEST(test_erase_range_rejects_what_it_cannot_erase) +{ + int first = 99, last = 99; + + ASSERT(stm32f4_flash_sector_range(STM32F4_SLOT_A_ADDR, 0, &first, &last) != 0); + ASSERT(stm32f4_flash_sector_range(FLASH_END - 4, 8, &first, &last) != 0); + ASSERT(stm32f4_flash_sector_range(STM32F4_FLASH_BASE - 4, 8, &first, &last) != 0); + ASSERT(stm32f4_flash_sector_range(0xFFFFFF00u, 0x200, &first, &last) != 0); + ASSERT(first == 99 && last == 99); +} + +struct region { const char *name; uint32_t addr; uint32_t size; }; + +static const struct region regions[] = { + { "stage0", STM32F4_STAGE0_ADDR, STM32F4_STAGE0_SIZE }, + { "stage1", STM32F4_STAGE1_ADDR, STM32F4_STAGE1_SIZE }, + { "log", STM32F4_LOG_ADDR, STM32F4_LOG_SIZE }, + { "slot_a", STM32F4_SLOT_A_ADDR, STM32F4_SLOT_A_SIZE }, + { "slot_b", STM32F4_SLOT_B_ADDR, STM32F4_SLOT_B_SIZE }, + { "recovery", STM32F4_RECOVERY_ADDR, STM32F4_RECOVERY_SIZE }, + { "bootctl", STM32F4_BOOTCTL_ADDR, STM32F4_BOOTCTL_SIZE }, + { "bootctl_backup", STM32F4_BOOTCTL_BACKUP_ADDR, STM32F4_BOOTCTL_BACKUP_SIZE }, +}; +#define N_REGIONS (sizeof(regions) / sizeof(regions[0])) + +TEST(test_every_region_owns_whole_sectors) +{ + for (size_t i = 0; i < N_REGIONS; i++) { + const struct region *r = ®ions[i]; + if (!is_sector_boundary(r->addr) || !is_sector_boundary(r->addr + r->size)) { + printf("\n %s [0x%08X, +0x%X) is not sector-aligned ", + r->name, (unsigned)r->addr, (unsigned)r->size); + ASSERT(0); + } + } +} + +TEST(test_no_two_regions_share_a_sector) +{ + int owner[STM32F4_FLASH_SECTORS]; + for (int s = 0; s < STM32F4_FLASH_SECTORS; s++) owner[s] = -1; + + for (size_t i = 0; i < N_REGIONS; i++) { + int first, last; + ASSERT(stm32f4_flash_sector_range(regions[i].addr, regions[i].size, + &first, &last) == 0); + for (int s = first; s <= last; s++) { + if (owner[s] != -1) { + printf("\n sector %d holds both %s and %s ", s, + regions[owner[s]].name, regions[i].name); + ASSERT(0); + } + owner[s] = (int)i; + } + } +} + +TEST(test_layout_fits_what_it_holds) +{ + /* A/B slots are interchangeable, so they are the same size. */ + ASSERT(STM32F4_SLOT_A_SIZE == STM32F4_SLOT_B_SIZE); + ASSERT(sizeof(eos_bootctl_t) <= STM32F4_BOOTCTL_SIZE); + ASSERT(sizeof(eos_bootctl_t) <= STM32F4_BOOTCTL_BACKUP_SIZE); + ASSERT(EOS_BOOT_LOG_MAX * sizeof(eos_boot_log_entry_t) <= STM32F4_LOG_SIZE); + /* Stage-1's vector table is where stage-0 jumps; the linker scripts + * place stage-0 at 0x08000000/16K and stage-1 at 0x08004000/48K. */ + ASSERT(STM32F4_STAGE0_ADDR == 0x08000000 && STM32F4_STAGE0_SIZE == 16 * 1024); + ASSERT(STM32F4_STAGE1_ADDR == 0x08004000 && STM32F4_STAGE1_SIZE == 48 * 1024); +} + +int main(void) +{ + printf("STM32F4 flash erase and layout\n\n"); + + run_test_sector_of_matches_the_reference_manual(); + run_test_erase_range_covers_every_sector_it_touches(); + run_test_erase_range_rejects_what_it_cannot_erase(); + run_test_every_region_owns_whole_sectors(); + run_test_no_two_regions_share_a_sector(); + run_test_layout_fits_what_it_holds(); + + printf("\n%d/%d passed\n", tests_passed, tests_run); + return tests_passed == tests_run ? 0 : 1; +}