diff --git a/doc/services/storage/zms/zms.rst b/doc/services/storage/zms/zms.rst index 5adf018c481d..6b5845bf11ab 100644 --- a/doc/services/storage/zms/zms.rst +++ b/doc/services/storage/zms/zms.rst @@ -110,6 +110,18 @@ By default, :c:func:`zms_mount` returns an error if the partition cannot be moun For recovery-oriented use cases, :c:func:`zms_mount_force` can be used to automatically wipe and reinitialize the partition when the first mount attempt fails. +Mount flags +----------- + +ZMS mount behavior can be controlled with optional flags in ``zms_fs.mount_flags``. + +- Default behavior (no optional flags set, ``mount_flags = 0``): if the partition is erased and no valid + ZMS header is found, :c:func:`zms_mount` formats the partition by creating the + initial ZMS header. +- ``ZMS_MOUNT_FLAG_NO_FORMAT``: if the partition is erased and no valid ZMS + header is found, :c:func:`zms_mount` does not format the partition and returns + ``-ENOTSUP``. + To mount the filesystem the following elements in the :c:struct:`zms_fs` structure must be initialized: .. code-block:: c @@ -125,6 +137,9 @@ To mount the filesystem the following elements in the :c:struct:`zms_fs` structu /** Number of sectors in the file system */ uint32_t sector_count; + /** Optional mount behavior flags (enum zms_mount_flags) */ + uint32_t mount_flags; + /** Flash device runtime structure */ const struct device *flash_device; }; diff --git a/include/zephyr/kvss/zms.h b/include/zephyr/kvss/zms.h index 45ab2f3424bb..27a6ed82a407 100644 --- a/include/zephyr/kvss/zms.h +++ b/include/zephyr/kvss/zms.h @@ -31,6 +31,17 @@ extern "C" { * @{ */ +/** Mount options for @ref zms_mount and @ref zms_mount_force. */ +enum zms_mount_flags { + /** + * Do not format erased media during mount. + * + * When this flag is set and the backing flash area is erased (no valid ZMS + * header), mount fails instead of creating a new ZMS header. + */ + ZMS_MOUNT_FLAG_NO_FORMAT = BIT(0), +}; + /** Zephyr Memory Storage file system structure */ struct zms_fs { /** File system offset in flash */ @@ -49,6 +60,8 @@ struct zms_fs { uint32_t sector_size; /** Number of sectors in the file system */ uint32_t sector_count; + /** Mount behavior flags from @ref zms_mount_flags */ + uint32_t mount_flags; /** Current cycle counter of the active sector (pointed to by `ate_wra`) */ uint8_t sector_cycle; /** Flag indicating if the file system is initialized */ @@ -91,6 +104,11 @@ typedef uint32_t zms_id_t; /** * @brief Mount a ZMS file system onto the device specified in `fs`. * + * @details If the flash area is erased and no valid ZMS header is found, + * mount will format the area and create a valid header by default. + * Set @ref ZMS_MOUNT_FLAG_NO_FORMAT in `fs->mount_flags` to disable this + * auto-format behavior and fail the mount instead. + * * @param fs Pointer to the file system. * * @retval 0 on success. @@ -272,6 +290,36 @@ ssize_t zms_active_sector_free_space(struct zms_fs *fs); */ int zms_sector_use_next(struct zms_fs *fs); +/** + * @brief Return the maximum sector recycle count across all sectors. + * + * Iterates all sectors and stores the highest 32-bit cycle counter found in + * each sector's empty ATE in @p cycles. This can be used to estimate + * write-cycle consumption during testing. + * + * @param fs Pointer to the file system. + * @param cycles Pointer to store the maximum 32-bit cycle count across sectors. + * + * @retval 0 on success. + * @retval -EINVAL if @p fs or @p cycles is NULL. + * @retval -EACCES if the file system is not mounted. + */ +int zms_get_num_cycles(struct zms_fs *fs, uint32_t *cycles); + +/** + * @brief Return the recycle count for a specific sector. + * + * @param fs Pointer to the file system. + * @param sector Sector index (0-based, must be less than @c fs->sector_count). + * @param cycles Pointer to store the 32-bit cycle count. + * + * @retval 0 on success. + * @retval -EINVAL if @p fs or @p cycles is NULL, or @p sector is out of range. + * @retval -EACCES if the file system is not mounted. + * @retval -ENOENT if the sector has no valid empty ATE. + */ +int zms_get_sector_num_cycles(struct zms_fs *fs, uint32_t sector, uint32_t *cycles); + /** * @} */ diff --git a/samples/subsys/kvss/kvss.rst b/samples/subsys/kvss/kvss.rst index 2780a5ae36cc..81baeb43f57f 100644 --- a/samples/subsys/kvss/kvss.rst +++ b/samples/subsys/kvss/kvss.rst @@ -1,5 +1,6 @@ .. zephyr:code-sample-category:: kvss :name: Key-Value Storage Systems :show-listing: + :glob: **/* Samples that demonstrate how to interact with Key-Value Storage Systems diff --git a/samples/subsys/kvss/zms/zms_cycle_count/CMakeLists.txt b/samples/subsys/kvss/zms/zms_cycle_count/CMakeLists.txt new file mode 100644 index 000000000000..7cc51fd8a327 --- /dev/null +++ b/samples/subsys/kvss/zms/zms_cycle_count/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(zms_cycle_count) + +target_sources(app PRIVATE src/main.c) diff --git a/samples/subsys/kvss/zms/zms_cycle_count/README.rst b/samples/subsys/kvss/zms/zms_cycle_count/README.rst new file mode 100644 index 000000000000..d556c416f890 --- /dev/null +++ b/samples/subsys/kvss/zms/zms_cycle_count/README.rst @@ -0,0 +1,90 @@ +.. zephyr:code-sample:: zms-cycle-count + :name: ZMS Cycle Count Verification + :relevant-api: zms_high_level_api + + Verify that ZMS sector cycle counters are tracked correctly across garbage + collection cycles, including past the uint8_t wraparound boundary. + +Overview +******** + + This sample exercises and verifies the ZMS sector cycle counting APIs: + + * :c:func:`zms_get_num_cycles` + * :c:func:`zms_get_sector_num_cycles` + * :c:func:`zms_sector_use_next` + + It checks that the recycle count returned by ZMS matches the expected value + after many forced sector advances, and that the count keeps growing correctly + once it exceeds the original ``uint8_t`` cycle counter range (256), which is + where the legacy field used to wrap around. + + The sample runs in three phases: + + #. **Phase 1** -- Mounts the partition, clears it, and performs 30 sector + advances. The cycle count is checked against the expected value + (``base + PHASE1_ITERATIONS / SECTOR_COUNT``). + #. **Phase 2** -- Performs additional sector advances until the relative + cycle count exceeds 256, validating that the counter keeps incrementing + past the historical ``uint8_t`` wraparound point. + #. **Phase 3** -- Reads the per-sector cycle count via + :c:func:`zms_get_sector_num_cycles` for every sector, ensures none of them + are zero, and verifies that an out-of-range sector index returns + ``-EINVAL``. + + On success the sample prints ``PASS: cycle count verification succeeded``. + +Requirements +************ + +* A board with flash support or the ``native_sim`` target + +Building and Running +******************** + +The sample can be built for several platforms. It has been tested on +``native_sim``, ``qemu_x86`` and ``nrf7002dk/nrf5340/cpuapp``. + +.. zephyr-app-commands:: + :zephyr-app: samples/subsys/kvss/zms/zms_cycle_count + :goals: build + :compact: + +Sample Output +============= + +The exact ``base`` value depends on the existing state of the storage +partition (it grows by one full cycle each time the sample is re-run against +persistent flash). The example below shows a fresh-flash run where +``base=1``; subsequent runs simply start from a higher base and end at a +higher final value, but the relative growth and the ``PASS`` line are the +same. + +.. code-block:: console + + *** Booting Zephyr OS build v4.4.0-rc2 *** + [00:00:00.000,000] fs_zms: 3 Sectors of 4096 bytes + [00:00:00.000,000] fs_zms: alloc wra: 0, fc0 + [00:00:00.000,000] fs_zms: data wra: 0, 0 + [00:00:00.000,000] fs_zms: 3 Sectors of 4096 bytes + [00:00:00.000,000] fs_zms: alloc wra: 0, fc0 + [00:00:00.000,000] fs_zms: data wra: 0, 0 + After mount: num_cycles=1 (base=1) + --- Phase 1: 30 sector advances --- + After sector_use_next[1]: num_cycles=1 + After sector_use_next[2]: num_cycles=2 + After sector_use_next[3]: num_cycles=2 + ... + After sector_use_next[30]: num_cycles=11 + Phase 1 expected=11, got=11 + --- Phase 2: 741 sector advances to exceed 256 cycles above base --- + Phase 2 progress [3/741]: num_cycles=12 + Phase 2 progress [6/741]: num_cycles=13 + ... + Phase 2 progress [741/741]: num_cycles=258 + Final expected=258, got=258 (total_advances=771) + --- Phase 3: per-sector cycle count verification --- + Sector 0: cycles=258 + Sector 1: cycles=258 + Sector 2: cycles=257 + PASS: cycle count verification succeeded (num_cycles=258 > 255) diff --git a/samples/subsys/kvss/zms/zms_cycle_count/prj.conf b/samples/subsys/kvss/zms/zms_cycle_count/prj.conf new file mode 100644 index 000000000000..ca11ea7724aa --- /dev/null +++ b/samples/subsys/kvss/zms/zms_cycle_count/prj.conf @@ -0,0 +1,6 @@ +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_ZMS=y +CONFIG_LOG=y +CONFIG_LOG_BLOCK_IN_THREAD=y +CONFIG_MAIN_STACK_SIZE=4096 diff --git a/samples/subsys/kvss/zms/zms_cycle_count/sample.yaml b/samples/subsys/kvss/zms/zms_cycle_count/sample.yaml new file mode 100644 index 000000000000..3d97845297dc --- /dev/null +++ b/samples/subsys/kvss/zms/zms_cycle_count/sample.yaml @@ -0,0 +1,15 @@ +sample: + name: ZMS Cycle Count Verification + +tests: + sample.zms.cycle_count: + tags: zms + platform_allow: + - nrf7002dk/nrf5340/cpuapp + - qemu_x86 + - native_sim + harness: console + harness_config: + type: one_line + regex: + - "PASS: cycle count verification succeeded" diff --git a/samples/subsys/kvss/zms/zms_cycle_count/src/main.c b/samples/subsys/kvss/zms/zms_cycle_count/src/main.c new file mode 100644 index 000000000000..5780db293f92 --- /dev/null +++ b/samples/subsys/kvss/zms/zms_cycle_count/src/main.c @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2026 BayLibre SAS + * + * SPDX-License-Identifier: Apache-2.0 + * + * ZMS cycle count verification sample. + * Verifies that zms_get_num_cycles() correctly tracks sector recycle counts + * across multiple garbage collection cycles, including past the uint8_t + * wraparound boundary at 256. + */ + +#include +#include +#include +#include +#include + +#define ZMS_PARTITION storage_partition +#define ZMS_PARTITION_DEVICE PARTITION_DEVICE(ZMS_PARTITION) +#define ZMS_PARTITION_OFFSET PARTITION_OFFSET(ZMS_PARTITION) + +#define SECTOR_COUNT 3 + +/* Phase 1: basic test with 30 iterations (expect num_cycles = 11) */ +#define PHASE1_ITERATIONS 30 + +/* Phase 2: push past uint8_t wraparound (256). + * After phase 1 we have accumulated PHASE1_ITERATIONS / SECTOR_COUNT cycles + * above the base. We need 256 more cycles to exceed the uint8_t range. + * PHASE2_REL_TARGET is the total relative cycles needed (above base). + */ +#define PHASE2_REL_TARGET 257 +#define PHASE2_ITERATIONS ((PHASE2_REL_TARGET - (PHASE1_ITERATIONS / SECTOR_COUNT)) \ + * SECTOR_COUNT) + +static struct zms_fs fs; + +int main(void) +{ + int rc; + struct flash_pages_info info; + uint32_t num_cycles; + uint32_t expected; + uint32_t base_cycles; + int total_advances = 0; + + fs.flash_device = ZMS_PARTITION_DEVICE; + if (!device_is_ready(fs.flash_device)) { + printk("Flash device not ready\n"); + return 0; + } + + fs.offset = ZMS_PARTITION_OFFSET; + rc = flash_get_page_info_by_offs(fs.flash_device, fs.offset, &info); + if (rc) { + printk("Unable to get page info: %d\n", rc); + return 0; + } + fs.sector_size = info.size; + fs.sector_count = SECTOR_COUNT; + + /* Mount and clear to start from a known state */ + rc = zms_mount(&fs); + if (rc) { + printk("FAIL: initial mount failed: %d\n", rc); + return 0; + } + rc = zms_clear(&fs); + if (rc) { + printk("FAIL: clear failed: %d\n", rc); + return 0; + } + + /* Re-mount after clear */ + rc = zms_mount(&fs); + if (rc) { + printk("FAIL: re-mount failed: %d\n", rc); + return 0; + } + + rc = zms_get_num_cycles(&fs, &num_cycles); + if (rc) { + printk("FAIL: zms_get_num_cycles failed: %d\n", rc); + return 0; + } + base_cycles = num_cycles; + printk("After mount: num_cycles=%u (base=%u)\n", num_cycles, base_cycles); + + /* === Phase 1: basic cycle count test === */ + printk("--- Phase 1: %d sector advances ---\n", PHASE1_ITERATIONS); + for (int i = 1; i <= PHASE1_ITERATIONS; i++) { + rc = zms_sector_use_next(&fs); + if (rc) { + printk("FAIL: sector_use_next returned %d at iteration %d\n", rc, i); + return 0; + } + total_advances++; + rc = zms_get_num_cycles(&fs, &num_cycles); + if (rc) { + printk("FAIL: zms_get_num_cycles failed: %d\n", rc); + return 0; + } + printk("After sector_use_next[%d]: num_cycles=%u\n", i, num_cycles); + } + + expected = base_cycles + (PHASE1_ITERATIONS / SECTOR_COUNT); + printk("Phase 1 expected=%u, got=%u\n", expected, num_cycles); + if (num_cycles != expected) { + printk("FAIL: phase 1 cycle count mismatch\n"); + return 0; + } + + /* === Phase 2: push past uint8_t boundary (256) === */ + printk("--- Phase 2: %d sector advances to exceed 256 cycles above base ---\n", + PHASE2_ITERATIONS); + for (int i = 1; i <= PHASE2_ITERATIONS; i++) { + rc = zms_sector_use_next(&fs); + if (rc) { + printk("FAIL: sector_use_next returned %d at phase2 iteration %d\n", + rc, i); + return 0; + } + total_advances++; + /* Print progress every SECTOR_COUNT advances (each full cycle) */ + if ((i % SECTOR_COUNT) == 0) { + rc = zms_get_num_cycles(&fs, &num_cycles); + if (rc) { + printk("FAIL: zms_get_num_cycles failed: %d\n", rc); + return 0; + } + printk("Phase 2 progress [%d/%d]: num_cycles=%u\n", + i, PHASE2_ITERATIONS, num_cycles); + } + } + + rc = zms_get_num_cycles(&fs, &num_cycles); + if (rc) { + printk("FAIL: zms_get_num_cycles failed: %d\n", rc); + return 0; + } + expected = base_cycles + (total_advances / SECTOR_COUNT); + printk("Final expected=%u, got=%u (total_advances=%d)\n", + expected, num_cycles, total_advances); + + if (num_cycles != expected) { + printk("FAIL: full_cycle_cnt mismatch after wraparound\n"); + return 0; + } + + if (num_cycles - base_cycles < 256) { + printk("FAIL: num_cycles=%u did not exceed uint8_t range above base=%u\n", + num_cycles, base_cycles); + return 0; + } + + /* === Phase 3: verify per-sector cycle counts === */ + printk("--- Phase 3: per-sector cycle count verification ---\n"); + for (uint32_t s = 0; s < SECTOR_COUNT; s++) { + uint32_t sector_cycles; + + rc = zms_get_sector_num_cycles(&fs, s, §or_cycles); + if (rc) { + printk("FAIL: zms_get_sector_num_cycles(sector=%u) returned %d\n", s, rc); + return 0; + } + printk("Sector %u: cycles=%u\n", s, sector_cycles); + if (sector_cycles == 0) { + printk("FAIL: sector %u has 0 cycles\n", s); + return 0; + } + } + + /* Verify invalid sector index returns -EINVAL */ + uint32_t dummy; + + rc = zms_get_sector_num_cycles(&fs, SECTOR_COUNT, &dummy); + if (rc != -EINVAL) { + printk("FAIL: out-of-range sector returned %d instead of -EINVAL\n", rc); + return 0; + } + + printk("PASS: cycle count verification succeeded (num_cycles=%u > 255)\n", num_cycles); + + return 0; +} diff --git a/subsys/kvss/zms/zms.c b/subsys/kvss/zms/zms.c index 04dd014777cb..c11ea28f8e55 100644 --- a/subsys/kvss/zms/zms.c +++ b/subsys/kvss/zms/zms.c @@ -6,6 +6,7 @@ * ZMS: Zephyr Memory Storage */ +#include #include #include #include @@ -25,7 +26,8 @@ LOG_MODULE_REGISTER(fs_zms, CONFIG_ZMS_LOG_LEVEL); static int zms_prev_ate(struct zms_fs *fs, uint64_t *addr, struct zms_ate *ate); static int zms_ate_valid(struct zms_fs *fs, const struct zms_ate *entry); -static int zms_add_empty_ate(struct zms_fs *fs, uint64_t addr); +static int zms_add_empty_ate(struct zms_fs *fs, uint64_t addr, uint32_t prev_cycle_cnt); +static int zms_get_full_sector_cycle(struct zms_fs *fs, uint64_t addr, uint32_t *cycle_cnt); static int zms_get_sector_cycle(struct zms_fs *fs, uint64_t addr, uint8_t *cycle_cnt); static int zms_get_sector_header(struct zms_fs *fs, uint64_t addr, struct zms_ate *empty_ate, struct zms_ate *close_ate); @@ -643,14 +645,20 @@ static int zms_wipe_partition(struct zms_fs *fs) { int rc; uint64_t addr; + uint32_t prev_cycle_cnt; for (uint32_t i = 0; i < fs->sector_count; i++) { addr = (uint64_t)i << ADDR_SECT_SHIFT; + prev_cycle_cnt = 0; + rc = zms_get_full_sector_cycle(fs, addr, &prev_cycle_cnt); + if ((rc < 0) && (rc != -ENOENT)) { + return rc; + } rc = zms_flash_erase_sector(fs, addr); if (rc) { return rc; } - rc = zms_add_empty_ate(fs, addr); + rc = zms_add_empty_ate(fs, addr, prev_cycle_cnt); if (rc) { return rc; } @@ -872,7 +880,7 @@ static inline int zms_verify_and_increment_cycle_cnt(struct zms_fs *fs, uint64_t return 0; } -static int zms_add_empty_ate(struct zms_fs *fs, uint64_t addr) +static int zms_add_empty_ate(struct zms_fs *fs, uint64_t addr, uint32_t prev_cycle_cnt) { struct zms_ate empty_ate; uint8_t cycle_cnt; @@ -891,21 +899,30 @@ static int zms_add_empty_ate(struct zms_fs *fs, uint64_t addr) FIELD_PREP(ZMS_MAGIC_NUMBER_MASK, ZMS_MAGIC_NUMBER) | FIELD_PREP(ZMS_ATE_FORMAT_MASK, ZMS_DEFAULT_ATE_FORMAT); + /* Get cycle_cnt independently for data validity purposes */ rc = zms_get_sector_cycle(fs, addr, &cycle_cnt); if (rc == -ENOENT) { - /* sector never used */ + /* sector erased or never used */ +#if !defined(CONFIG_ZMS_ID_64BIT) cycle_cnt = 0; +#else + cycle_cnt = (uint8_t)prev_cycle_cnt; +#endif } else if (rc) { /* bad flash read */ return rc; } - /* Increase cycle counter */ + /* Increase cycle counter for data validity */ rc = zms_verify_and_increment_cycle_cnt(fs, addr, &cycle_cnt); if (rc < 0) { return rc; } +#if !defined(CONFIG_ZMS_ID_64BIT) + empty_ate.full_cycle_cnt = prev_cycle_cnt + 1; +#endif empty_ate.cycle_cnt = cycle_cnt; + zms_ate_crc8_update(&empty_ate); /* Adding empty ate to this sector changes fs->ate_wra value @@ -946,6 +963,38 @@ static int zms_get_sector_cycle(struct zms_fs *fs, uint64_t addr, uint8_t *cycle return -ENOENT; } +static int zms_get_full_sector_cycle(struct zms_fs *fs, uint64_t addr, uint32_t *cycle_cnt) +{ + int rc; + struct zms_ate empty_ate; + uint64_t empty_addr; + + empty_addr = zms_empty_ate_addr(fs, addr); + + /* read the cycle counter of the current sector */ + rc = zms_flash_ate_rd(fs, empty_addr, &empty_ate); + if (rc < 0) { + /* flash error */ + return rc; + } + + if (zms_empty_ate_valid(fs, &empty_ate)) { +#if !defined(CONFIG_ZMS_ID_64BIT) + if (empty_ate.full_cycle_cnt == 0 && empty_ate.cycle_cnt > 0) { + *cycle_cnt = empty_ate.cycle_cnt; + } else { + *cycle_cnt = empty_ate.full_cycle_cnt; + } +#else + *cycle_cnt = empty_ate.cycle_cnt; +#endif + return 0; + } + + /* there is no empty ATE in this sector */ + return -ENOENT; +} + static int zms_get_sector_header(struct zms_fs *fs, uint64_t addr, struct zms_ate *empty_ate, struct zms_ate *close_ate) { @@ -1040,6 +1089,7 @@ static int zms_gc(struct zms_fs *fs) uint64_t gc_addr; uint64_t gc_prev_addr; uint64_t wlk_addr; + uint32_t saved_full_cycle_cnt = 0; uint64_t wlk_prev_addr; uint64_t data_addr; uint64_t stop_addr; @@ -1053,7 +1103,7 @@ static int zms_gc(struct zms_fs *fs) return rc; } /* sector never used */ - rc = zms_add_empty_ate(fs, fs->ate_wra); + rc = zms_add_empty_ate(fs, fs->ate_wra, 0); if (rc) { return rc; } @@ -1171,6 +1221,12 @@ static int zms_gc(struct zms_fs *fs) return rc; } + /* Read full_cycle_cnt BEFORE erasing so it can be preserved */ + rc = zms_get_full_sector_cycle(fs, sec_addr, &saved_full_cycle_cnt); + if (rc && rc != -ENOENT) { + return rc; + } + /* Erase the GC'ed sector when needed */ rc = zms_flash_erase_sector(fs, sec_addr); if (rc) { @@ -1180,7 +1236,7 @@ static int zms_gc(struct zms_fs *fs) #ifdef CONFIG_ZMS_LOOKUP_CACHE zms_lookup_cache_invalidate(fs, sec_addr >> ADDR_SECT_SHIFT); #endif - rc = zms_add_empty_ate(fs, sec_addr); + rc = zms_add_empty_ate(fs, sec_addr, saved_full_cycle_cnt); return rc; } @@ -1327,15 +1383,20 @@ static int zms_init(struct zms_fs *fs) goto end; } } - } else { + } else if (!(fs->mount_flags & ZMS_MOUNT_FLAG_NO_FORMAT)) { rc = zms_flash_erase_sector(fs, addr); if (rc) { goto end; } - rc = zms_add_empty_ate(fs, addr); + rc = zms_add_empty_ate(fs, addr, 0); if (rc) { goto end; } + } else { + /* No valid empty ATE in the last sector */ + LOG_ERR("No valid empty ATE found in the last sector"); + rc = -ENOTSUP; + goto end; } rc = zms_get_sector_cycle(fs, addr, &fs->sector_cycle); if (rc == -ENOENT) { @@ -1435,6 +1496,7 @@ static int zms_init(struct zms_fs *fs) */ bool gc_done_marker = false; struct zms_ate gc_done_ate; + uint32_t saved_full_cycle_cnt = 0; fs->sector_cycle = empty_ate.cycle_cnt; addr = fs->ate_wra + fs->ate_size; @@ -1456,19 +1518,27 @@ static int zms_init(struct zms_fs *fs) LOG_INF("GC Done marker found"); addr = fs->ate_wra & ADDR_SECT_MASK; zms_sector_advance(fs, &addr); + rc = zms_get_full_sector_cycle(fs, addr, &saved_full_cycle_cnt); + if (rc && rc != -ENOENT) { + goto end; + } rc = zms_flash_erase_sector(fs, addr); if (rc < 0) { goto end; } - rc = zms_add_empty_ate(fs, addr); + rc = zms_add_empty_ate(fs, addr, saved_full_cycle_cnt); goto end; } LOG_INF("No GC Done marker found: restarting gc"); + rc = zms_get_full_sector_cycle(fs, fs->ate_wra, &saved_full_cycle_cnt); + if (rc && rc != -ENOENT) { + goto end; + } rc = zms_flash_erase_sector(fs, fs->ate_wra); if (rc) { goto end; } - rc = zms_add_empty_ate(fs, fs->ate_wra); + rc = zms_add_empty_ate(fs, fs->ate_wra, saved_full_cycle_cnt); if (rc) { goto end; } @@ -1901,12 +1971,13 @@ static ssize_t zms_free_space(struct zms_fs *fs, uint32_t data_wra, uint32_t ate } /* initial value: available space for data at the top of the sector */ - free_space = ate_wra - data_wra - fs->ate_size; - - if (free_space < 0) { + if (ate_wra < (data_wra + fs->ate_size)) { /* not enough room for an ATE */ return 0; + } else { + free_space = ate_wra - data_wra - fs->ate_size; } + if (free_space < ZMS_DATA_IN_ATE_SIZE) { /* more data can be stored inside an ATE */ return ZMS_DATA_IN_ATE_SIZE; @@ -2063,3 +2134,60 @@ int zms_sector_use_next(struct zms_fs *fs) k_mutex_unlock(&fs->zms_lock); return ret; } + +int zms_get_num_cycles(struct zms_fs *fs, uint32_t *cycles) +{ + uint32_t max_cycle_cnt = 0; + uint32_t cycle_cnt; + int rc; + + if (!fs || !cycles) { + return -EINVAL; + } + + if (!fs->ready) { + return -EACCES; + } + + k_mutex_lock(&fs->zms_lock, K_FOREVER); + + for (uint32_t i = 0; i < fs->sector_count; i++) { + rc = zms_get_full_sector_cycle(fs, (uint64_t)i << ADDR_SECT_SHIFT, &cycle_cnt); + + if (rc) { + continue; + } + + max_cycle_cnt = MAX(max_cycle_cnt, cycle_cnt); + } + + k_mutex_unlock(&fs->zms_lock); + + *cycles = max_cycle_cnt; + return 0; +} + +int zms_get_sector_num_cycles(struct zms_fs *fs, uint32_t sector, uint32_t *cycles) +{ + int rc; + + if (!fs || !cycles) { + return -EINVAL; + } + + if (!fs->ready) { + return -EACCES; + } + + if (sector >= fs->sector_count) { + return -EINVAL; + } + + k_mutex_lock(&fs->zms_lock, K_FOREVER); + + rc = zms_get_full_sector_cycle(fs, (uint64_t)sector << ADDR_SECT_SHIFT, cycles); + + k_mutex_unlock(&fs->zms_lock); + + return rc; +} diff --git a/subsys/kvss/zms/zms_priv.h b/subsys/kvss/zms/zms_priv.h index 44cfb2f3d280..581e28f7b12d 100644 --- a/subsys/kvss/zms/zms_priv.h +++ b/subsys/kvss/zms/zms_priv.h @@ -73,8 +73,12 @@ struct zms_ate { /** data field used to store small sized data */ uint8_t data[8]; struct { - /** data offset within sector */ - uint32_t offset; + union { + /** data offset within sector */ + uint32_t offset; + /** full cycle count (for empty ATE) */ + uint32_t full_cycle_cnt; + }; union { /** * crc for data: The data CRC is checked only when the whole data diff --git a/tests/subsys/kvss/zms/src/main.c b/tests/subsys/kvss/zms/src/main.c index cb861cca38c9..9ac05a7c7a5c 100644 --- a/tests/subsys/kvss/zms/src/main.c +++ b/tests/subsys/kvss/zms/src/main.c @@ -88,6 +88,7 @@ static void after(void *data) } fixture->fs.sector_count = TEST_SECTOR_COUNT; + fixture->fs.mount_flags = 0; } ZTEST_SUITE(zms, NULL, setup, before, after, NULL); @@ -123,6 +124,42 @@ static void execute_long_pattern_write(uint32_t id, struct zms_fs *fs) zassert_mem_equal(wr_buf, rd_buf, sizeof(rd_buf), "RD buff should be equal to the WR buff"); } +static void erase_test_partition(void) +{ + const struct flash_area *fa; + int err; + + err = flash_area_open(TEST_ZMS_AREA_ID, &fa); + zassert_true(err == 0, "flash_area_open() fail: %d", err); + + err = flash_area_erase(fa, 0, fa->fa_size); + zassert_true(err == 0, "flash_area_erase() fail: %d", err); + + flash_area_close(fa); +} + +ZTEST_F(zms, test_zms_mount_no_format_on_erased_flash) +{ + int err; + + erase_test_partition(); + + fixture->fs.mount_flags = ZMS_MOUNT_FLAG_NO_FORMAT; + err = zms_mount(&fixture->fs); + zassert_equal(err, -ENOTSUP, "zms_mount should fail without formatting: %d", err); +} + +ZTEST_F(zms, test_zms_mount_auto_format_on_erased_flash) +{ + int err; + + erase_test_partition(); + + fixture->fs.mount_flags = 0U; + err = zms_mount(&fixture->fs); + zassert_true(err == 0, "zms_mount call failure: %d", err); +} + ZTEST_F(zms, test_zms_write) { int err; @@ -171,6 +208,13 @@ ZTEST_F(zms, test_zms_corrupted_write) err = zms_mount(&fixture->fs); zassert_true(err == 0, "zms_mount call failure: %d", err); + /* Get the address of current write calls and the maximum number of writes and reinitialize + * the current number of write calls to 0. + */ + stats_walk(fixture->sim_thresholds, flash_sim_max_write_calls_find, &flash_max_write_calls); + stats_walk(fixture->sim_stats, flash_sim_write_calls_find, &flash_write_stat); + *flash_write_stat = 0; + err = zms_read(&fixture->fs, TEST_DATA_ID, rd_buf, sizeof(rd_buf)); zassert_true(err == -ENOENT, "zms_read unexpected failure: %d", err); @@ -195,9 +239,6 @@ ZTEST_F(zms, test_zms_corrupted_write) /* Set the maximum number of writes that the flash simulator can * execute. */ - stats_walk(fixture->sim_thresholds, flash_sim_max_write_calls_find, &flash_max_write_calls); - stats_walk(fixture->sim_stats, flash_sim_write_calls_find, &flash_write_stat); - *flash_max_write_calls = *flash_write_stat - 1; *flash_write_stat = 0; @@ -224,6 +265,13 @@ ZTEST_F(zms, test_zms_corrupted_write) "write operation has failed"); } +static uint16_t get_max_writes_to_trigger_gc(struct zms_fs *fs, size_t data_size) +{ + const size_t aligned_size_headers = 3 * fs->ate_size; + + return (fs->sector_size - aligned_size_headers) / (data_size + fs->ate_size) + 1; +} + ZTEST_F(zms, test_zms_gc) { int err; @@ -231,14 +279,16 @@ ZTEST_F(zms, test_zms_gc) uint8_t buf[32]; uint8_t rd_buf[32]; const uint8_t max_id = 10; - /* 21st write will trigger GC. */ - const uint16_t max_writes = 21; + uint16_t max_writes; fixture->fs.sector_count = 2; err = zms_mount(&fixture->fs); zassert_true(err == 0, "zms_mount call failure: %d", err); + /* Calculate the number of writes to fill a sector and trigger GC. + */ + max_writes = get_max_writes_to_trigger_gc(&fixture->fs, sizeof(buf)); for (int i = 0; i < max_writes; i++) { uint8_t id = (i % max_id); uint8_t id_data = id + max_id * (i / max_id); @@ -277,6 +327,137 @@ ZTEST_F(zms, test_zms_gc) } } +ZTEST_F(zms, test_zms_cycle_count_input_validation) +{ + int err; + uint32_t cycles; + + /* NULL fs / NULL out pointer must be rejected. */ + err = zms_get_num_cycles(NULL, &cycles); + zassert_equal(err, -EINVAL, "expected -EINVAL for NULL fs, got %d", err); + + err = zms_get_num_cycles(&fixture->fs, NULL); + zassert_equal(err, -EINVAL, "expected -EINVAL for NULL cycles, got %d", err); + + /* Unmounted filesystem must be rejected. */ + err = zms_get_num_cycles(&fixture->fs, &cycles); + zassert_equal(err, -EACCES, "expected -EACCES on unmounted fs, got %d", err); + + err = zms_get_sector_num_cycles(&fixture->fs, 0, &cycles); + zassert_equal(err, -EACCES, "expected -EACCES on unmounted fs, got %d", err); + + err = zms_mount(&fixture->fs); + zassert_true(err == 0, "zms_mount call failure: %d", err); + + /* NULL fs / NULL out pointer. */ + err = zms_get_sector_num_cycles(NULL, 0, &cycles); + zassert_equal(err, -EINVAL, "expected -EINVAL for NULL fs, got %d", err); + + err = zms_get_sector_num_cycles(&fixture->fs, 0, NULL); + zassert_equal(err, -EINVAL, "expected -EINVAL for NULL cycles, got %d", err); + + /* Sector index out of range. */ + err = zms_get_sector_num_cycles(&fixture->fs, fixture->fs.sector_count, &cycles); + zassert_equal(err, -EINVAL, "expected -EINVAL for out-of-range sector, got %d", err); +} + +ZTEST_F(zms, test_zms_cycle_count_increments) +{ + int err; + uint32_t base_cycles; + uint32_t num_cycles; + uint32_t sector_cycles; + const uint32_t advances = 9; + + fixture->fs.sector_count = 3; + + err = zms_mount(&fixture->fs); + zassert_true(err == 0, "zms_mount call failure: %d", err); + + err = zms_get_num_cycles(&fixture->fs, &base_cycles); + zassert_true(err == 0, "zms_get_num_cycles failed: %d", err); + + /* Each call to zms_sector_use_next advances the active sector by one; + * after sector_count advances every sector has been recycled once and + * the maximum cycle count must have grown by exactly 1. + */ + for (uint32_t i = 0; i < advances; i++) { + err = zms_sector_use_next(&fixture->fs); + zassert_true(err == 0, "zms_sector_use_next failed at %u: %d", i, err); + } + + err = zms_get_num_cycles(&fixture->fs, &num_cycles); + zassert_true(err == 0, "zms_get_num_cycles failed: %d", err); + zassert_equal(num_cycles, base_cycles + (advances / fixture->fs.sector_count), + "num_cycles=%u expected=%u (base=%u)", num_cycles, + base_cycles + (advances / fixture->fs.sector_count), base_cycles); + + /* Each individual sector must report a non-zero, in-range cycle count. */ + for (uint32_t s = 0; s < fixture->fs.sector_count; s++) { + err = zms_get_sector_num_cycles(&fixture->fs, s, §or_cycles); + zassert_true(err == 0, "zms_get_sector_num_cycles(%u) failed: %d", s, err); + zassert_true(sector_cycles <= num_cycles, + "sector %u cycles=%u exceeds max=%u", s, sector_cycles, + num_cycles); + } +} + +ZTEST_F(zms, test_zms_cycle_count_persistence) +{ + int err; + uint32_t base_cycles; + uint32_t num_cycles_before; + uint32_t num_cycles_after_clear; + uint32_t num_cycles_after_remount; + const uint32_t advances = 6; + + fixture->fs.sector_count = 3; + + err = zms_mount(&fixture->fs); + zassert_true(err == 0, "zms_mount call failure: %d", err); + + err = zms_get_num_cycles(&fixture->fs, &base_cycles); + zassert_true(err == 0, "zms_get_num_cycles failed: %d", err); + + for (uint32_t i = 0; i < advances; i++) { + err = zms_sector_use_next(&fixture->fs); + zassert_true(err == 0, "zms_sector_use_next failed at %u: %d", i, err); + } + + err = zms_get_num_cycles(&fixture->fs, &num_cycles_before); + zassert_true(err == 0, "zms_get_num_cycles failed: %d", err); + zassert_true(num_cycles_before > base_cycles, + "cycle count did not advance: before=%u base=%u", num_cycles_before, + base_cycles); + + /* zms_clear must not roll the cycle counter back: the per-sector + * full_cycle_cnt is preserved (and bumped) by zms_wipe_partition. + */ + err = zms_clear(&fixture->fs); + zassert_true(err == 0, "zms_clear failed: %d", err); + + err = zms_mount(&fixture->fs); + zassert_true(err == 0, "zms_mount (after clear) call failure: %d", err); + + err = zms_get_num_cycles(&fixture->fs, &num_cycles_after_clear); + zassert_true(err == 0, "zms_get_num_cycles failed: %d", err); + zassert_true(num_cycles_after_clear >= num_cycles_before, + "cycle count regressed across zms_clear: before=%u after=%u", + num_cycles_before, num_cycles_after_clear); + + /* Re-mount and ensure the cycle count survives, exercising the + * full_cycle_cnt persistence path. + */ + err = zms_mount(&fixture->fs); + zassert_true(err == 0, "zms_mount (remount) call failure: %d", err); + + err = zms_get_num_cycles(&fixture->fs, &num_cycles_after_remount); + zassert_true(err == 0, "zms_get_num_cycles failed: %d", err); + zassert_equal(num_cycles_after_remount, num_cycles_after_clear, + "cycle count not persisted across remount: after_clear=%u after_remount=%u", + num_cycles_after_clear, num_cycles_after_remount); +} + static void write_content(uint32_t max_id, uint32_t begin, uint32_t end, struct zms_fs *fs) { uint8_t buf[32]; @@ -328,6 +509,11 @@ ZTEST_F(zms, test_zms_gc_3sectors) /* 101st write will trigger 4th GC. */ const uint16_t max_writes_4 = 41 + 20 + 20 + 20; + if (fixture->fs.sector_size != 1024) { + /* this test is designed for 1KB sectors */ + ztest_test_skip(); + } + fixture->fs.sector_count = 3; err = zms_mount(&fixture->fs); @@ -406,8 +592,7 @@ ZTEST_F(zms, test_zms_corrupted_sector_close_operation) uint32_t *flash_max_write_calls; uint32_t *flash_max_len; const uint16_t max_id = 10; - /* 21st write will trigger GC. */ - const uint16_t max_writes = 21; + uint16_t max_writes; /* Get the address of simulator parameters. */ stats_walk(fixture->sim_thresholds, flash_sim_max_write_calls_find, &flash_max_write_calls); @@ -417,6 +602,9 @@ ZTEST_F(zms, test_zms_corrupted_sector_close_operation) err = zms_mount(&fixture->fs); zassert_true(err == 0, "zms_mount call failure: %d", err); + /* Calculate the number of writes to fill a sector and trigger GC. + */ + max_writes = get_max_writes_to_trigger_gc(&fixture->fs, sizeof(buf)); for (int i = 0; i < max_writes; i++) { uint8_t id = (i % max_id); uint8_t id_data = id + max_id * (i / max_id); @@ -605,6 +793,14 @@ ZTEST_F(zms, test_zms_gc_corrupt_close_ate) ate.crc8 = crc8_ccitt(0xff, (uint8_t *)&ate + SIZEOF_FIELD(struct zms_ate, crc8), sizeof(struct zms_ate) - SIZEOF_FIELD(struct zms_ate, crc8)); + /* Ensure sectors are erased before injecting handcrafted ATEs. + * Flash simulator writes are one-way bit transitions and cannot set bits back to 1. + */ + fixture->fs.sector_count = 3; + err = flash_erase(fixture->fs.flash_device, fixture->fs.offset, + fixture->fs.sector_count * fixture->fs.sector_size); + zassert_true(err == 0, "flash_erase sector 1 failed: %d", err); + /* Add empty ATE */ err = flash_write(fixture->fs.flash_device, fixture->fs.offset + fixture->fs.sector_size - sizeof(struct zms_ate), @@ -630,8 +826,6 @@ ZTEST_F(zms, test_zms_gc_corrupt_close_ate) &close_ate, sizeof(close_ate)); zassert_true(err == 0, "flash_write failed: %d", err); - fixture->fs.sector_count = 3; - err = zms_mount(&fixture->fs); zassert_true(err == 0, "zms_mount call failure: %d", err); @@ -841,7 +1035,7 @@ ZTEST_F(zms, test_zms_cache_gc) */ num = num_matching_cache_entries(0ULL << ADDR_SECT_SHIFT, true, &fixture->fs); - zassert_equal(num, 0, "not invalidated cache entries aftetr gc"); + zassert_equal(num, 0, "not invalidated cache entries after gc"); num = num_matching_cache_entries(2ULL << ADDR_SECT_SHIFT, true, &fixture->fs); zassert_equal(num, 2, "invalid cache content after gc");