From 0a2fc7f34a70afd29b75c0bbf529a9f85a2acd8b Mon Sep 17 00:00:00 2001 From: Martin Vychodil Date: Tue, 16 Dec 2025 21:52:13 +0100 Subject: [PATCH] feat(wear_levelling): Added BDL support --- components/wear_levelling/BDL_Access.cpp | 63 +++++ components/wear_levelling/CMakeLists.txt | 4 +- components/wear_levelling/Partition.cpp | 28 +- components/wear_levelling/WL_Ext_Perf.cpp | 3 +- components/wear_levelling/WL_Ext_Safe.cpp | 2 +- components/wear_levelling/WL_Flash.cpp | 7 +- .../wear_levelling/host_test/main/test_wl.cpp | 234 +++++++++++++++- .../host_test/pytest_wear_levelling_linux.py | 2 +- .../wear_levelling/include/wear_levelling.h | 36 ++- .../private_include/BDL_Access.h | 42 +++ .../private_include/Flash_Access.h | 7 +- .../private_include/Partition.h | 20 +- .../private_include/WL_Ext_Perf.h | 3 +- .../private_include/WL_Ext_Safe.h | 3 +- .../wear_levelling/private_include/WL_Flash.h | 9 +- .../test_apps/main/CMakeLists.txt | 3 +- .../wear_levelling/test_apps/main/test_wl.c | 204 +++++++++++++- components/wear_levelling/wear_levelling.cpp | 127 +++++---- components/wear_levelling/wl_blockdev.cpp | 249 ++++++++++++++++++ 19 files changed, 940 insertions(+), 106 deletions(-) create mode 100644 components/wear_levelling/BDL_Access.cpp create mode 100644 components/wear_levelling/private_include/BDL_Access.h create mode 100644 components/wear_levelling/wl_blockdev.cpp diff --git a/components/wear_levelling/BDL_Access.cpp b/components/wear_levelling/BDL_Access.cpp new file mode 100644 index 0000000000..3a11844ae9 --- /dev/null +++ b/components/wear_levelling/BDL_Access.cpp @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "BDL_Access.h" +#include +#include +#include +#include "esp_log.h" + +static const char *TAG = "wl_bdl_access"; + +BDL_Access::BDL_Access(esp_blockdev_handle_t bdl_device) + : bdl_device(bdl_device) +{ + assert(bdl_device != ESP_BLOCKDEV_HANDLE_INVALID); +} + +size_t BDL_Access::get_flash_size() +{ + assert(this->bdl_device->geometry.disk_size <= SIZE_MAX); + return (size_t)this->bdl_device->geometry.disk_size; +} + +esp_err_t BDL_Access::erase_sector(size_t sector) +{ + size_t erase_size = this->bdl_device->geometry.erase_size; + return this->bdl_device->ops->erase(this->bdl_device, sector * erase_size, erase_size); +} + +esp_err_t BDL_Access::erase_range(size_t start_address, size_t size) +{ + ESP_LOGV(TAG, "%s - start_address=0x%08" PRIx32 ", size=0x%08" PRIx32, __func__, (uint32_t)start_address, (uint32_t)size); + return this->bdl_device->ops->erase(this->bdl_device, start_address, size); +} + +esp_err_t BDL_Access::write(size_t dest_addr, const void *src, size_t size) +{ + ESP_LOGV(TAG, "%s - dest_addr=0x%08" PRIx32 ", size=0x%08" PRIx32, __func__, (uint32_t)dest_addr, (uint32_t)size); + return this->bdl_device->ops->write(this->bdl_device, (const uint8_t *)src, dest_addr, size); +} + +esp_err_t BDL_Access::read(size_t src_addr, void *dest, size_t size) +{ + ESP_LOGV(TAG, "%s - src_addr=0x%08" PRIx32 ", size=0x%08" PRIx32, __func__, (uint32_t)src_addr, (uint32_t)size); + return this->bdl_device->ops->read(this->bdl_device, (uint8_t *)dest, size, src_addr, size); +} + +size_t BDL_Access::get_sector_size() +{ + return this->bdl_device->geometry.erase_size; +} + +bool BDL_Access::is_readonly() +{ + return this->bdl_device->device_flags.read_only; +} + +BDL_Access::~BDL_Access() +{ +} diff --git a/components/wear_levelling/CMakeLists.txt b/components/wear_levelling/CMakeLists.txt index 28e98bbb2f..e8b289dc11 100644 --- a/components/wear_levelling/CMakeLists.txt +++ b/components/wear_levelling/CMakeLists.txt @@ -1,13 +1,15 @@ idf_component_register(SRCS "Partition.cpp" + "BDL_Access.cpp" "SPI_Flash.cpp" "WL_Ext_Perf.cpp" "WL_Ext_Safe.cpp" "WL_Flash.cpp" "crc32.cpp" "wear_levelling.cpp" + "wl_blockdev.cpp" INCLUDE_DIRS include PRIV_INCLUDE_DIRS private_include - REQUIRES esp_partition + REQUIRES esp_partition esp_blockdev PRIV_REQUIRES spi_flash) if(CONFIG_COMPILER_STATIC_ANALYZER AND CMAKE_C_COMPILER_ID STREQUAL "GNU") # TODO IDF-10089 diff --git a/components/wear_levelling/Partition.cpp b/components/wear_levelling/Partition.cpp index f99dee43be..6bab8ae860 100644 --- a/components/wear_levelling/Partition.cpp +++ b/components/wear_levelling/Partition.cpp @@ -1,12 +1,12 @@ /* - * SPDX-FileCopyrightText: 2015-2023 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ +#include #include "esp_log.h" #include "Partition.h" -#include static const char *TAG = "wl_partition"; @@ -22,34 +22,25 @@ size_t Partition::get_flash_size() esp_err_t Partition::erase_sector(size_t sector) { - esp_err_t result = ESP_OK; - result = erase_range(sector * this->partition->erase_size, this->partition->erase_size); - return result; + return erase_range(sector * this->partition->erase_size, this->partition->erase_size); } esp_err_t Partition::erase_range(size_t start_address, size_t size) { - esp_err_t result = esp_partition_erase_range(this->partition, start_address, size); - if (result == ESP_OK) { - ESP_LOGV(TAG, "erase_range - start_address=0x%08" PRIx32 ", size=0x%08" PRIx32 ", result=0x%08x", (uint32_t) start_address, (uint32_t) size, result); - } else { - ESP_LOGE(TAG, "erase_range - start_address=0x%08" PRIx32 ", size=0x%08" PRIx32 ", result=0x%08x", (uint32_t) start_address, (uint32_t) size, result); - } - return result; + ESP_LOGV(TAG, "%s - start_address=0x%08" PRIx32 ", size=0x%08" PRIx32, __func__, (uint32_t)start_address, (uint32_t)size); + return esp_partition_erase_range(this->partition, start_address, size); } esp_err_t Partition::write(size_t dest_addr, const void *src, size_t size) { - esp_err_t result = ESP_OK; - result = esp_partition_write(this->partition, dest_addr, src, size); - return result; + ESP_LOGV(TAG, "%s - dest_addr=0x%08" PRIx32 ", size=0x%08" PRIx32, __func__, (uint32_t)dest_addr, (uint32_t)size); + return esp_partition_write(this->partition, dest_addr, src, size); } esp_err_t Partition::read(size_t src_addr, void *dest, size_t size) { - esp_err_t result = ESP_OK; - result = esp_partition_read(this->partition, src_addr, dest, size); - return result; + ESP_LOGV(TAG, "%s - src_addr=0x%08" PRIx32 ", size=0x%08" PRIx32, __func__, (uint32_t)src_addr, (uint32_t)size); + return esp_partition_read(this->partition, src_addr, dest, size); } size_t Partition::get_sector_size() @@ -64,5 +55,4 @@ bool Partition::is_readonly() Partition::~Partition() { - } diff --git a/components/wear_levelling/WL_Ext_Perf.cpp b/components/wear_levelling/WL_Ext_Perf.cpp index 7bf51e3616..5858e068ed 100644 --- a/components/wear_levelling/WL_Ext_Perf.cpp +++ b/components/wear_levelling/WL_Ext_Perf.cpp @@ -4,7 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ #include "WL_Ext_Perf.h" -#include "Partition.h" #include #include #include "esp_log.h" @@ -27,7 +26,7 @@ WL_Ext_Perf::~WL_Ext_Perf() free(this->sector_buffer); } -esp_err_t WL_Ext_Perf::config(WL_Config_s *cfg, Partition *partition) +esp_err_t WL_Ext_Perf::config(WL_Config_s *cfg, Flash_Access *partition) { wl_ext_cfg_t *ext_cfg = (wl_ext_cfg_t *)cfg; diff --git a/components/wear_levelling/WL_Ext_Safe.cpp b/components/wear_levelling/WL_Ext_Safe.cpp index 73edc79c3c..939f144512 100644 --- a/components/wear_levelling/WL_Ext_Safe.cpp +++ b/components/wear_levelling/WL_Ext_Safe.cpp @@ -50,7 +50,7 @@ WL_Ext_Safe::~WL_Ext_Safe() { } -esp_err_t WL_Ext_Safe::config(WL_Config_s *cfg, Partition *partition) +esp_err_t WL_Ext_Safe::config(WL_Config_s *cfg, Flash_Access *partition) { esp_err_t result = ESP_OK; diff --git a/components/wear_levelling/WL_Flash.cpp b/components/wear_levelling/WL_Flash.cpp index f7d7a336c3..92050daef9 100644 --- a/components/wear_levelling/WL_Flash.cpp +++ b/components/wear_levelling/WL_Flash.cpp @@ -1,12 +1,11 @@ /* - * SPDX-FileCopyrightText: 2015-2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ #include #include "esp_random.h" #include "esp_log.h" -#include "Partition.h" #include "WL_Flash.h" #include #include "crc32.h" @@ -39,7 +38,7 @@ WL_Flash::~WL_Flash() free(this->temp_buff); } -esp_err_t WL_Flash::config(wl_config_t *cfg, Partition *partition) +esp_err_t WL_Flash::config(wl_config_t *cfg, Flash_Access *partition) { ESP_LOGV(TAG, "%s partition_start_addr=0x%08" PRIx32 ", wl_partition_size=0x%08" PRIx32 ", wl_page_size=0x%08" PRIx32 ", flash_sector_size=0x%08" PRIx32 ", wl_update_rate=0x%08" PRIx32 ", wl_pos_update_record_size=0x%08" PRIx32 ", version=0x%08" PRIx32 ", wl_temp_buff_size=0x%08" PRIx32 , __func__, (uint32_t) cfg->wl_partition_start_addr, @@ -611,7 +610,7 @@ esp_err_t WL_Flash::read(size_t src_addr, void *dest, size_t size) return result; } -Partition *WL_Flash::get_part() +Flash_Access *WL_Flash::get_part() { return this->partition; } diff --git a/components/wear_levelling/host_test/main/test_wl.cpp b/components/wear_levelling/host_test/main/test_wl.cpp index ebf23830a0..69ac2634f3 100644 --- a/components/wear_levelling/host_test/main/test_wl.cpp +++ b/components/wear_levelling/host_test/main/test_wl.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2016-2023 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2016-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -279,7 +279,7 @@ void calculate_wl_state_address_info(const esp_partition_t *partition, size_t *o void calculate_wl_state_crc(WL_State_s *state_ptr) { int check_size = WL_STATE_CRC_LEN_V2; - // Chech CRC and recover state + // Check CRC and recover state state_ptr->crc32 = crc32::crc32_le(WL_CFG_CRC_CONST, (uint8_t *)state_ptr, check_size); } @@ -421,3 +421,233 @@ TEST_CASE("power down between WL status 1 and WL status 2 update", "[wear_levell free(tmp_state); } + +/* ======================================================================== */ +/* BDL (Block Device Layer) interface tests */ +/* ======================================================================== */ + +TEST_CASE("BDL read/write/erase operations", "[wear_levelling][bdl]") +{ + const esp_partition_t *partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "storage"); + REQUIRE(partition != NULL); + + esp_blockdev_handle_t part_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + REQUIRE(esp_partition_ptr_get_blockdev(partition, &part_blockdev) == ESP_OK); + + esp_blockdev_handle_t wl_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + REQUIRE(wl_get_blockdev(part_blockdev, &wl_blockdev) == ESP_OK); + + REQUIRE(wl_blockdev->geometry.disk_size > 0); + REQUIRE(wl_blockdev->geometry.erase_size > 0); + + const size_t data_size = 256; + uint8_t test_data[data_size]; + uint8_t data_buffer[data_size]; + size_t target_addr = 3 * 4 * 1024; //"random" address - 3rd sector of the partition (ie anything but 0 sector) + + REQUIRE(wl_blockdev->ops->erase(wl_blockdev, target_addr, wl_blockdev->geometry.erase_size) == ESP_OK); + memset(test_data, 0xFF, data_size); + REQUIRE(wl_blockdev->ops->read(wl_blockdev, data_buffer, data_size, target_addr, data_size) == ESP_OK); + REQUIRE(memcmp(test_data, data_buffer, data_size) == 0); + + memset(test_data, 'P', data_size); + REQUIRE(wl_blockdev->ops->write(wl_blockdev, test_data, target_addr, data_size) == ESP_OK); + REQUIRE(wl_blockdev->ops->read(wl_blockdev, data_buffer, data_size, target_addr, data_size) == ESP_OK); + REQUIRE(memcmp(test_data, data_buffer, data_size) == 0); + + REQUIRE(wl_blockdev->ops->sync(wl_blockdev) == ESP_OK); + + REQUIRE(wl_blockdev->ops->release(wl_blockdev) == ESP_OK); + REQUIRE(part_blockdev->ops->release(part_blockdev) == ESP_OK); +} + +TEST_CASE("BDL error paths", "[wear_levelling][bdl]") +{ + esp_blockdev_handle_t out = ESP_BLOCKDEV_HANDLE_INVALID; + CHECK(wl_get_blockdev(ESP_BLOCKDEV_HANDLE_INVALID, &out) == ESP_ERR_INVALID_ARG); + CHECK(wl_get_blockdev(ESP_BLOCKDEV_HANDLE_INVALID, NULL) == ESP_ERR_INVALID_ARG); + + const esp_partition_t *partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "storage"); + REQUIRE(partition != NULL); + + esp_blockdev_handle_t part_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + REQUIRE(esp_partition_ptr_get_blockdev(partition, &part_blockdev) == ESP_OK); + + CHECK(wl_get_blockdev(part_blockdev, NULL) == ESP_ERR_INVALID_ARG); + + REQUIRE(part_blockdev->ops->release(part_blockdev) == ESP_OK); +} + +TEST_CASE("BDL disk_size truncation guard", "[wear_levelling][bdl]") +{ + const esp_partition_t *partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "storage"); + REQUIRE(partition != NULL); + + esp_blockdev_handle_t part_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + REQUIRE(esp_partition_ptr_get_blockdev(partition, &part_blockdev) == ESP_OK); + + uint64_t orig_size = part_blockdev->geometry.disk_size; + esp_blockdev_handle_t wl_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + + // Just above the uint32_t boundary + part_blockdev->geometry.disk_size = (uint64_t)UINT32_MAX + 1; + CHECK(wl_get_blockdev(part_blockdev, &wl_blockdev) == ESP_ERR_INVALID_SIZE); + CHECK(wl_blockdev == ESP_BLOCKDEV_HANDLE_INVALID); + + // Far above the uint32_t boundary + wl_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + part_blockdev->geometry.disk_size = UINT64_MAX; + CHECK(wl_get_blockdev(part_blockdev, &wl_blockdev) == ESP_ERR_INVALID_SIZE); + CHECK(wl_blockdev == ESP_BLOCKDEV_HANDLE_INVALID); + + part_blockdev->geometry.disk_size = orig_size; + REQUIRE(part_blockdev->ops->release(part_blockdev) == ESP_OK); +} + +TEST_CASE("BDL geometry and flags verification", "[wear_levelling][bdl]") +{ + const esp_partition_t *partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "storage"); + REQUIRE(partition != NULL); + + esp_blockdev_handle_t part_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + REQUIRE(esp_partition_ptr_get_blockdev(partition, &part_blockdev) == ESP_OK); + + esp_blockdev_handle_t wl_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + REQUIRE(wl_get_blockdev(part_blockdev, &wl_blockdev) == ESP_OK); + + CHECK(wl_blockdev->geometry.disk_size > 0); + CHECK((uint64_t)wl_blockdev->geometry.disk_size < part_blockdev->geometry.disk_size); + CHECK(wl_blockdev->geometry.erase_size > 0); + CHECK(wl_blockdev->geometry.read_size == part_blockdev->geometry.read_size); + CHECK(wl_blockdev->geometry.write_size == part_blockdev->geometry.write_size); + CHECK(wl_blockdev->geometry.recommended_read_size == part_blockdev->geometry.recommended_read_size); + CHECK(wl_blockdev->geometry.recommended_write_size == part_blockdev->geometry.recommended_write_size); + + CHECK(wl_blockdev->device_flags.val == part_blockdev->device_flags.val); + + REQUIRE(wl_blockdev->ops != NULL); + CHECK(wl_blockdev->ops->read != NULL); + CHECK(wl_blockdev->ops->write != NULL); + CHECK(wl_blockdev->ops->erase != NULL); + CHECK(wl_blockdev->ops->sync != NULL); + CHECK(wl_blockdev->ops->ioctl != NULL); + CHECK(wl_blockdev->ops->release != NULL); + + REQUIRE(wl_blockdev->ops->release(wl_blockdev) == ESP_OK); + REQUIRE(part_blockdev->ops->release(part_blockdev) == ESP_OK); +} + +TEST_CASE("BDL sync operation", "[wear_levelling][bdl]") +{ + const esp_partition_t *partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "storage"); + REQUIRE(partition != NULL); + + esp_blockdev_handle_t part_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + REQUIRE(esp_partition_ptr_get_blockdev(partition, &part_blockdev) == ESP_OK); + + esp_blockdev_handle_t wl_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + REQUIRE(wl_get_blockdev(part_blockdev, &wl_blockdev) == ESP_OK); + + size_t erase_size = wl_blockdev->geometry.erase_size; + REQUIRE(wl_blockdev->ops->erase(wl_blockdev, 0, erase_size) == ESP_OK); + + uint8_t test_data[64]; + memset(test_data, 'S', sizeof(test_data)); + REQUIRE(wl_blockdev->ops->write(wl_blockdev, test_data, 0, sizeof(test_data)) == ESP_OK); + REQUIRE(wl_blockdev->ops->sync(wl_blockdev) == ESP_OK); + + uint8_t read_data[64]; + REQUIRE(wl_blockdev->ops->read(wl_blockdev, read_data, sizeof(read_data), 0, sizeof(read_data)) == ESP_OK); + REQUIRE(memcmp(test_data, read_data, sizeof(test_data)) == 0); + + REQUIRE(wl_blockdev->ops->release(wl_blockdev) == ESP_OK); + REQUIRE(part_blockdev->ops->release(part_blockdev) == ESP_OK); +} + +TEST_CASE("BDL ioctl relay to bottom device", "[wear_levelling][bdl]") +{ + const esp_partition_t *partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "storage"); + REQUIRE(partition != NULL); + + esp_blockdev_handle_t part_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + REQUIRE(esp_partition_ptr_get_blockdev(partition, &part_blockdev) == ESP_OK); + + esp_blockdev_handle_t wl_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + REQUIRE(wl_get_blockdev(part_blockdev, &wl_blockdev) == ESP_OK); + + CHECK(wl_blockdev->ops->ioctl(wl_blockdev, ESP_BLOCKDEV_CMD_MARK_DELETED, NULL) == ESP_ERR_NOT_SUPPORTED); + + REQUIRE(wl_blockdev->ops->release(wl_blockdev) == ESP_OK); + REQUIRE(part_blockdev->ops->release(part_blockdev) == ESP_OK); +} + +TEST_CASE("BDL erase alignment validation", "[wear_levelling][bdl]") +{ + const esp_partition_t *partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "storage"); + REQUIRE(partition != NULL); + + esp_blockdev_handle_t part_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + REQUIRE(esp_partition_ptr_get_blockdev(partition, &part_blockdev) == ESP_OK); + + esp_blockdev_handle_t wl_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + REQUIRE(wl_get_blockdev(part_blockdev, &wl_blockdev) == ESP_OK); + + size_t erase_size = wl_blockdev->geometry.erase_size; + + CHECK(wl_blockdev->ops->erase(wl_blockdev, 1, erase_size) == ESP_ERR_INVALID_SIZE); + CHECK(wl_blockdev->ops->erase(wl_blockdev, 0, erase_size - 1) == ESP_ERR_INVALID_SIZE); + CHECK(wl_blockdev->ops->erase(wl_blockdev, 1, erase_size + 1) == ESP_ERR_INVALID_SIZE); + REQUIRE(wl_blockdev->ops->erase(wl_blockdev, 0, erase_size) == ESP_OK); + + REQUIRE(wl_blockdev->ops->release(wl_blockdev) == ESP_OK); + REQUIRE(part_blockdev->ops->release(part_blockdev) == ESP_OK); +} + +TEST_CASE("BDL read buffer size validation", "[wear_levelling][bdl]") +{ + const esp_partition_t *partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "storage"); + REQUIRE(partition != NULL); + + esp_blockdev_handle_t part_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + REQUIRE(esp_partition_ptr_get_blockdev(partition, &part_blockdev) == ESP_OK); + + esp_blockdev_handle_t wl_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + REQUIRE(wl_get_blockdev(part_blockdev, &wl_blockdev) == ESP_OK); + + uint8_t buf[64]; + CHECK(wl_blockdev->ops->read(wl_blockdev, buf, 32, 0, 64) == ESP_ERR_INVALID_ARG); + REQUIRE(wl_blockdev->ops->read(wl_blockdev, buf, sizeof(buf), 0, sizeof(buf)) == ESP_OK); + + REQUIRE(wl_blockdev->ops->release(wl_blockdev) == ESP_OK); + REQUIRE(part_blockdev->ops->release(part_blockdev) == ESP_OK); +} + +TEST_CASE("BDL release and reconnect", "[wear_levelling][bdl]") +{ + const esp_partition_t *partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "storage"); + REQUIRE(partition != NULL); + + esp_blockdev_handle_t part_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + REQUIRE(esp_partition_ptr_get_blockdev(partition, &part_blockdev) == ESP_OK); + + esp_blockdev_handle_t wl_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + REQUIRE(wl_get_blockdev(part_blockdev, &wl_blockdev) == ESP_OK); + + size_t erase_size = wl_blockdev->geometry.erase_size; + REQUIRE(wl_blockdev->ops->erase(wl_blockdev, 0, erase_size) == ESP_OK); + uint8_t test_data[64]; + memset(test_data, 'R', sizeof(test_data)); + REQUIRE(wl_blockdev->ops->write(wl_blockdev, test_data, 0, sizeof(test_data)) == ESP_OK); + + REQUIRE(wl_blockdev->ops->release(wl_blockdev) == ESP_OK); + + wl_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + REQUIRE(wl_get_blockdev(part_blockdev, &wl_blockdev) == ESP_OK); + + uint8_t read_data[64]; + REQUIRE(wl_blockdev->ops->read(wl_blockdev, read_data, sizeof(read_data), 0, sizeof(read_data)) == ESP_OK); + REQUIRE(memcmp(test_data, read_data, sizeof(test_data)) == 0); + + REQUIRE(wl_blockdev->ops->release(wl_blockdev) == ESP_OK); + REQUIRE(part_blockdev->ops->release(part_blockdev) == ESP_OK); +} diff --git a/components/wear_levelling/host_test/pytest_wear_levelling_linux.py b/components/wear_levelling/host_test/pytest_wear_levelling_linux.py index 6c952b16f1..6310ba3b65 100644 --- a/components/wear_levelling/host_test/pytest_wear_levelling_linux.py +++ b/components/wear_levelling/host_test/pytest_wear_levelling_linux.py @@ -8,4 +8,4 @@ from pytest_embedded_idf.utils import idf_parametrize @pytest.mark.host_test @idf_parametrize('target', ['linux'], indirect=['target']) def test_wear_levelling_linux(dut: Dut) -> None: - dut.expect_exact('All tests passed', timeout=120) + dut.expect_exact('All tests passed', timeout=180) diff --git a/components/wear_levelling/include/wear_levelling.h b/components/wear_levelling/include/wear_levelling.h index ed501f6e76..fc3c7f03fb 100644 --- a/components/wear_levelling/include/wear_levelling.h +++ b/components/wear_levelling/include/wear_levelling.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -9,6 +9,7 @@ #include "esp_log.h" #include "esp_partition.h" +#include "esp_blockdev.h" #ifdef __cplusplus extern "C" { @@ -120,6 +121,39 @@ size_t wl_size(wl_handle_t handle); */ size_t wl_sector_size(wl_handle_t handle); +/* -------------------------------------------------------------- */ +/* BDL support */ +/* -------------------------------------------------------------- */ + + /** + * @brief Creates a wear-levelling BDL instance on top of the given bottom block device. + * + * All I/O is routed exclusively through @p bdl_bottom_device, keeping the BDL stack + * fully independent of the legacy wl_handle_t driver. The underlying device can be + * an esp_partition BDL, a memory-backed disk, or any other block device implementation. + * + * Internally allocates a WL_Flash engine (deriving its configuration from the bottom + * device geometry) and a BDL_Access adapter. Releasing the returned BDL handle + * flushes and destroys the wear-levelling instance. + * + * The output device inherits geometry constraints (read_size, write_size) and device + * flags (read_only, encrypted, etc.) from @p bdl_bottom_device. The disk_size and + * erase_size are determined by the WL engine. + * + * @note The WL configuration stores partition size as uint32_t. If the bottom device + * reports a disk_size exceeding UINT32_MAX the function fails immediately with + * ESP_ERR_INVALID_SIZE to prevent silent truncation (fail-fast). + * + * @param[in] bdl_bottom_device Handle of the bottom block device layer instance (e.g. partition BDL) + * @param[out] out_bdl_handle_ptr Handle of the created wear-levelling block device instance + * + * @return + * - ESP_OK on success + * - ESP_ERR_NO_MEM if memory allocation has failed + * - ESP_ERR_INVALID_ARG if any of the arguments are invalid + * - ESP_ERR_INVALID_SIZE if @p bdl_bottom_device disk_size exceeds UINT32_MAX + */ + esp_err_t wl_get_blockdev(const esp_blockdev_handle_t bdl_bottom_device, esp_blockdev_handle_t *out_bdl_handle_ptr); #ifdef __cplusplus } // extern "C" diff --git a/components/wear_levelling/private_include/BDL_Access.h b/components/wear_levelling/private_include/BDL_Access.h new file mode 100644 index 0000000000..dbedf7ca10 --- /dev/null +++ b/components/wear_levelling/private_include/BDL_Access.h @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef _BDL_Access_H_ +#define _BDL_Access_H_ + +#include "esp_err.h" +#include "Flash_Access.h" +#include "esp_blockdev.h" + +/** + * @brief Flash_Access adapter that delegates all I/O to an esp_blockdev_handle_t. + * + * Used exclusively by the BDL wear-levelling path so that the core WL_Flash + * engine can operate on any block device without knowing about esp_partition. + */ +class BDL_Access : public Flash_Access +{ +public: + explicit BDL_Access(esp_blockdev_handle_t bdl_device); + + size_t get_flash_size() override; + + esp_err_t erase_sector(size_t sector) override; + esp_err_t erase_range(size_t start_address, size_t size) override; + + esp_err_t write(size_t dest_addr, const void *src, size_t size) override; + esp_err_t read(size_t src_addr, void *dest, size_t size) override; + + size_t get_sector_size() override; + bool is_readonly() override; + + ~BDL_Access() override; + +private: + esp_blockdev_handle_t bdl_device; +}; + +#endif // _BDL_Access_H_ diff --git a/components/wear_levelling/private_include/Flash_Access.h b/components/wear_levelling/private_include/Flash_Access.h index 30a4c621b3..d9856fa284 100644 --- a/components/wear_levelling/private_include/Flash_Access.h +++ b/components/wear_levelling/private_include/Flash_Access.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -30,6 +30,11 @@ public: return ESP_OK; }; + virtual bool is_readonly() + { + return false; + }; + virtual ~Flash_Access() {}; }; diff --git a/components/wear_levelling/private_include/Partition.h b/components/wear_levelling/private_include/Partition.h index 48cf2f5cc4..60c3355db1 100644 --- a/components/wear_levelling/private_include/Partition.h +++ b/components/wear_levelling/private_include/Partition.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -22,21 +22,21 @@ class Partition : public Flash_Access public: Partition(const esp_partition_t *partition); - virtual size_t get_flash_size(); + size_t get_flash_size() override; - virtual esp_err_t erase_sector(size_t sector); - virtual esp_err_t erase_range(size_t start_address, size_t size); + esp_err_t erase_sector(size_t sector) override; + esp_err_t erase_range(size_t start_address, size_t size) override; - virtual esp_err_t write(size_t dest_addr, const void *src, size_t size); - virtual esp_err_t read(size_t src_addr, void *dest, size_t size); + esp_err_t write(size_t dest_addr, const void *src, size_t size) override; + esp_err_t read(size_t src_addr, void *dest, size_t size) override; - virtual size_t get_sector_size(); - virtual bool is_readonly(); + size_t get_sector_size() override; + bool is_readonly() override; + + ~Partition() override; - virtual ~Partition(); protected: const esp_partition_t *partition; - }; #endif // _Partition_H_ diff --git a/components/wear_levelling/private_include/WL_Ext_Perf.h b/components/wear_levelling/private_include/WL_Ext_Perf.h index 809e5176ec..266a3eed1e 100644 --- a/components/wear_levelling/private_include/WL_Ext_Perf.h +++ b/components/wear_levelling/private_include/WL_Ext_Perf.h @@ -6,7 +6,6 @@ #ifndef _WL_Ext_Perf_H_ #define _WL_Ext_Perf_H_ -#include "Partition.h" #include "WL_Flash.h" #include "WL_Ext_Cfg.h" @@ -16,7 +15,7 @@ public: WL_Ext_Perf(); ~WL_Ext_Perf() override; - esp_err_t config(WL_Config_s *cfg, Partition *partition) override; + esp_err_t config(WL_Config_s *cfg, Flash_Access *partition) override; esp_err_t init() override; size_t get_flash_size() override; diff --git a/components/wear_levelling/private_include/WL_Ext_Safe.h b/components/wear_levelling/private_include/WL_Ext_Safe.h index 45e1e63c37..b137add3ae 100644 --- a/components/wear_levelling/private_include/WL_Ext_Safe.h +++ b/components/wear_levelling/private_include/WL_Ext_Safe.h @@ -6,7 +6,6 @@ #ifndef _WL_Ext_Safe_H_ #define _WL_Ext_Safe_H_ -#include "Partition.h" #include "WL_Flash.h" #include "WL_Ext_Cfg.h" #include "WL_Ext_Perf.h" @@ -17,7 +16,7 @@ public: WL_Ext_Safe(); ~WL_Ext_Safe() override; - esp_err_t config(WL_Config_s *cfg, Partition *partition) override; + esp_err_t config(WL_Config_s *cfg, Flash_Access *partition) override; esp_err_t init() override; size_t get_flash_size() override; diff --git a/components/wear_levelling/private_include/WL_Flash.h b/components/wear_levelling/private_include/WL_Flash.h index 5f24467e74..6d3d9b98b2 100644 --- a/components/wear_levelling/private_include/WL_Flash.h +++ b/components/wear_levelling/private_include/WL_Flash.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2015-2023 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -8,7 +8,6 @@ #include "esp_err.h" #include "Flash_Access.h" -#include "Partition.h" #include "WL_Config.h" #include "WL_State.h" @@ -22,7 +21,7 @@ public : WL_Flash(); ~WL_Flash() override; - virtual esp_err_t config(wl_config_t *cfg, Partition *partition); + virtual esp_err_t config(wl_config_t *cfg, Flash_Access *partition); virtual esp_err_t init(); size_t get_flash_size() override; @@ -36,7 +35,7 @@ public : esp_err_t flush() override; - Partition *get_part(); + Flash_Access *get_part(); wl_config_t *get_cfg(); protected: @@ -44,7 +43,7 @@ protected: bool initialized = false; wl_state_t state; wl_config_t cfg; - Partition *partition = NULL; + Flash_Access *partition = NULL; size_t addr_cfg; size_t addr_state1; diff --git a/components/wear_levelling/test_apps/main/CMakeLists.txt b/components/wear_levelling/test_apps/main/CMakeLists.txt index e82672a921..9dae560093 100644 --- a/components/wear_levelling/test_apps/main/CMakeLists.txt +++ b/components/wear_levelling/test_apps/main/CMakeLists.txt @@ -1,5 +1,6 @@ idf_component_register(SRCS test_wl.c PRIV_INCLUDE_DIRS . - PRIV_REQUIRES wear_levelling unity + PRIV_REQUIRES unity + REQUIRES wear_levelling EMBED_FILES test_partition_v1.bin ) diff --git a/components/wear_levelling/test_apps/main/test_wl.c b/components/wear_levelling/test_apps/main/test_wl.c index fa402f01c0..964f2571c9 100644 --- a/components/wear_levelling/test_apps/main/test_wl.c +++ b/components/wear_levelling/test_apps/main/test_wl.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -319,12 +319,214 @@ TEST(wear_levelling, version_update) } #endif // CONFIG_WL_SECTOR_SIZE_4096 +TEST(wear_levelling, test_bdl_read_write_erase) +{ + const esp_partition_t *partition = get_test_data_partition(); + + esp_blockdev_handle_t part_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + TEST_ESP_OK(esp_partition_ptr_get_blockdev(partition, &part_blockdev)); + + esp_blockdev_handle_t wl_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + TEST_ESP_OK(wl_get_blockdev(part_blockdev, &wl_blockdev)); + + TEST_ASSERT(wl_blockdev->geometry.disk_size > 0); + TEST_ASSERT(wl_blockdev->geometry.erase_size > 0); + + const size_t data_size = 256; + uint8_t test_data[data_size]; + uint8_t data_buffer[data_size]; + const off_t target_addr = 3 * 4 * 1024; + + TEST_ESP_OK(wl_blockdev->ops->erase(wl_blockdev, target_addr, wl_blockdev->geometry.erase_size)); + memset((void*)test_data, 0xFF, data_size); + TEST_ESP_OK(wl_blockdev->ops->read(wl_blockdev, data_buffer, data_size, target_addr, data_size)); + TEST_ASSERT_EQUAL(0, memcmp(test_data, data_buffer, data_size)); + + memset((void*)test_data, 'P', data_size); + TEST_ESP_OK(wl_blockdev->ops->write(wl_blockdev, test_data, target_addr, data_size)); + TEST_ESP_OK(wl_blockdev->ops->read(wl_blockdev, data_buffer, data_size, target_addr, data_size)); + TEST_ASSERT_EQUAL(0, memcmp(test_data, data_buffer, data_size)); + + TEST_ESP_OK(wl_blockdev->ops->sync(wl_blockdev)); + + // Release WL BDL (internally unmounts WL) + TEST_ESP_OK(wl_blockdev->ops->release(wl_blockdev)); + + TEST_ESP_OK(part_blockdev->ops->release(part_blockdev)); +} + +TEST(wear_levelling, test_bdl_error_paths) +{ + esp_blockdev_handle_t out = ESP_BLOCKDEV_HANDLE_INVALID; + TEST_ESP_ERR(ESP_ERR_INVALID_ARG, wl_get_blockdev(ESP_BLOCKDEV_HANDLE_INVALID, &out)); + TEST_ESP_ERR(ESP_ERR_INVALID_ARG, wl_get_blockdev(ESP_BLOCKDEV_HANDLE_INVALID, NULL)); + + const esp_partition_t *partition = get_test_data_partition(); + esp_blockdev_handle_t part_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + TEST_ESP_OK(esp_partition_ptr_get_blockdev(partition, &part_blockdev)); + + TEST_ESP_ERR(ESP_ERR_INVALID_ARG, wl_get_blockdev(part_blockdev, NULL)); + + TEST_ESP_OK(part_blockdev->ops->release(part_blockdev)); +} + +TEST(wear_levelling, test_bdl_geometry_and_flags) +{ + const esp_partition_t *partition = get_test_data_partition(); + + esp_blockdev_handle_t part_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + TEST_ESP_OK(esp_partition_ptr_get_blockdev(partition, &part_blockdev)); + + esp_blockdev_handle_t wl_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + TEST_ESP_OK(wl_get_blockdev(part_blockdev, &wl_blockdev)); + + TEST_ASSERT(wl_blockdev->geometry.disk_size > 0); + TEST_ASSERT(wl_blockdev->geometry.disk_size < part_blockdev->geometry.disk_size); + TEST_ASSERT(wl_blockdev->geometry.erase_size > 0); + TEST_ASSERT_EQUAL(part_blockdev->geometry.read_size, wl_blockdev->geometry.read_size); + TEST_ASSERT_EQUAL(part_blockdev->geometry.write_size, wl_blockdev->geometry.write_size); + TEST_ASSERT_EQUAL(part_blockdev->geometry.recommended_read_size, wl_blockdev->geometry.recommended_read_size); + TEST_ASSERT_EQUAL(part_blockdev->geometry.recommended_write_size, wl_blockdev->geometry.recommended_write_size); + + TEST_ASSERT_EQUAL(part_blockdev->device_flags.val, wl_blockdev->device_flags.val); + + TEST_ASSERT_NOT_NULL(wl_blockdev->ops); + TEST_ASSERT_NOT_NULL(wl_blockdev->ops->read); + TEST_ASSERT_NOT_NULL(wl_blockdev->ops->write); + TEST_ASSERT_NOT_NULL(wl_blockdev->ops->erase); + TEST_ASSERT_NOT_NULL(wl_blockdev->ops->sync); + TEST_ASSERT_NOT_NULL(wl_blockdev->ops->ioctl); + TEST_ASSERT_NOT_NULL(wl_blockdev->ops->release); + + TEST_ESP_OK(wl_blockdev->ops->release(wl_blockdev)); + TEST_ESP_OK(part_blockdev->ops->release(part_blockdev)); +} + +TEST(wear_levelling, test_bdl_sync) +{ + const esp_partition_t *partition = get_test_data_partition(); + + esp_blockdev_handle_t part_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + TEST_ESP_OK(esp_partition_ptr_get_blockdev(partition, &part_blockdev)); + + esp_blockdev_handle_t wl_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + TEST_ESP_OK(wl_get_blockdev(part_blockdev, &wl_blockdev)); + + size_t erase_size = wl_blockdev->geometry.erase_size; + TEST_ESP_OK(wl_blockdev->ops->erase(wl_blockdev, 0, erase_size)); + + uint8_t test_data[64]; + memset(test_data, 'S', sizeof(test_data)); + TEST_ESP_OK(wl_blockdev->ops->write(wl_blockdev, test_data, 0, sizeof(test_data))); + TEST_ESP_OK(wl_blockdev->ops->sync(wl_blockdev)); + + uint8_t read_data[64]; + TEST_ESP_OK(wl_blockdev->ops->read(wl_blockdev, read_data, sizeof(read_data), 0, sizeof(read_data))); + TEST_ASSERT_EQUAL(0, memcmp(test_data, read_data, sizeof(test_data))); + + TEST_ESP_OK(wl_blockdev->ops->release(wl_blockdev)); + TEST_ESP_OK(part_blockdev->ops->release(part_blockdev)); +} + +TEST(wear_levelling, test_bdl_ioctl) +{ + const esp_partition_t *partition = get_test_data_partition(); + + esp_blockdev_handle_t part_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + TEST_ESP_OK(esp_partition_ptr_get_blockdev(partition, &part_blockdev)); + + esp_blockdev_handle_t wl_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + TEST_ESP_OK(wl_get_blockdev(part_blockdev, &wl_blockdev)); + + TEST_ASSERT_EQUAL(ESP_ERR_NOT_SUPPORTED, + wl_blockdev->ops->ioctl(wl_blockdev, ESP_BLOCKDEV_CMD_MARK_DELETED, NULL)); + + TEST_ESP_OK(wl_blockdev->ops->release(wl_blockdev)); + TEST_ESP_OK(part_blockdev->ops->release(part_blockdev)); +} + +TEST(wear_levelling, test_bdl_erase_alignment) +{ + const esp_partition_t *partition = get_test_data_partition(); + + esp_blockdev_handle_t part_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + TEST_ESP_OK(esp_partition_ptr_get_blockdev(partition, &part_blockdev)); + + esp_blockdev_handle_t wl_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + TEST_ESP_OK(wl_get_blockdev(part_blockdev, &wl_blockdev)); + + size_t erase_size = wl_blockdev->geometry.erase_size; + + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_SIZE, wl_blockdev->ops->erase(wl_blockdev, 1, erase_size)); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_SIZE, wl_blockdev->ops->erase(wl_blockdev, 0, erase_size - 1)); + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_SIZE, wl_blockdev->ops->erase(wl_blockdev, 1, erase_size + 1)); + TEST_ESP_OK(wl_blockdev->ops->erase(wl_blockdev, 0, erase_size)); + + TEST_ESP_OK(wl_blockdev->ops->release(wl_blockdev)); + TEST_ESP_OK(part_blockdev->ops->release(part_blockdev)); +} + +TEST(wear_levelling, test_bdl_read_buffer_check) +{ + const esp_partition_t *partition = get_test_data_partition(); + + esp_blockdev_handle_t part_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + TEST_ESP_OK(esp_partition_ptr_get_blockdev(partition, &part_blockdev)); + + esp_blockdev_handle_t wl_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + TEST_ESP_OK(wl_get_blockdev(part_blockdev, &wl_blockdev)); + + uint8_t buf[64]; + TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, wl_blockdev->ops->read(wl_blockdev, buf, 32, 0, 64)); + TEST_ESP_OK(wl_blockdev->ops->read(wl_blockdev, buf, sizeof(buf), 0, sizeof(buf))); + + TEST_ESP_OK(wl_blockdev->ops->release(wl_blockdev)); + TEST_ESP_OK(part_blockdev->ops->release(part_blockdev)); +} + +TEST(wear_levelling, test_bdl_release_and_reconnect) +{ + const esp_partition_t *partition = get_test_data_partition(); + + esp_blockdev_handle_t part_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + TEST_ESP_OK(esp_partition_ptr_get_blockdev(partition, &part_blockdev)); + + esp_blockdev_handle_t wl_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + TEST_ESP_OK(wl_get_blockdev(part_blockdev, &wl_blockdev)); + + size_t erase_size = wl_blockdev->geometry.erase_size; + TEST_ESP_OK(wl_blockdev->ops->erase(wl_blockdev, 0, erase_size)); + uint8_t test_data[64]; + memset(test_data, 'R', sizeof(test_data)); + TEST_ESP_OK(wl_blockdev->ops->write(wl_blockdev, test_data, 0, sizeof(test_data))); + + TEST_ESP_OK(wl_blockdev->ops->release(wl_blockdev)); + + wl_blockdev = ESP_BLOCKDEV_HANDLE_INVALID; + TEST_ESP_OK(wl_get_blockdev(part_blockdev, &wl_blockdev)); + + uint8_t read_data[64]; + TEST_ESP_OK(wl_blockdev->ops->read(wl_blockdev, read_data, sizeof(read_data), 0, sizeof(read_data))); + TEST_ASSERT_EQUAL(0, memcmp(test_data, read_data, sizeof(test_data))); + + TEST_ESP_OK(wl_blockdev->ops->release(wl_blockdev)); + TEST_ESP_OK(part_blockdev->ops->release(part_blockdev)); +} + TEST_GROUP_RUNNER(wear_levelling) { RUN_TEST_CASE(wear_levelling, wl_unmount_doesnt_leak_memory) RUN_TEST_CASE(wear_levelling, wl_mount_checks_partition_params) RUN_TEST_CASE(wear_levelling, multiple_tasks_single_handle) RUN_TEST_CASE(wear_levelling, write_doesnt_touch_other_sectors) + RUN_TEST_CASE(wear_levelling, test_bdl_read_write_erase) + RUN_TEST_CASE(wear_levelling, test_bdl_error_paths) + RUN_TEST_CASE(wear_levelling, test_bdl_geometry_and_flags) + RUN_TEST_CASE(wear_levelling, test_bdl_sync) + RUN_TEST_CASE(wear_levelling, test_bdl_ioctl) + RUN_TEST_CASE(wear_levelling, test_bdl_erase_alignment) + RUN_TEST_CASE(wear_levelling, test_bdl_read_buffer_check) + RUN_TEST_CASE(wear_levelling, test_bdl_release_and_reconnect) #if CONFIG_WL_SECTOR_SIZE_4096 RUN_TEST_CASE(wear_levelling, version_update) diff --git a/components/wear_levelling/wear_levelling.cpp b/components/wear_levelling/wear_levelling.cpp index c3500d4875..9be7f6a62f 100644 --- a/components/wear_levelling/wear_levelling.cpp +++ b/components/wear_levelling/wear_levelling.cpp @@ -1,10 +1,11 @@ /* - * SPDX-FileCopyrightText: 2015-2023 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ #include +#include #include #include #include "wear_levelling.h" @@ -13,7 +14,6 @@ #include "WL_Flash.h" #include "WL_Ext_Perf.h" #include "WL_Ext_Safe.h" -#include "SPI_Flash.h" #include "Partition.h" #ifndef MAX_WL_HANDLES @@ -49,7 +49,22 @@ static wl_instance_t s_instances[MAX_WL_HANDLES]; static _lock_t s_instances_lock; static const char *TAG = "wear_levelling"; -static esp_err_t check_handle(wl_handle_t handle, const char *func); +static esp_err_t check_handle(wl_handle_t handle, const char *func) +{ + if (handle == WL_INVALID_HANDLE) { + ESP_LOGE(TAG, "%s: invalid handle", func); + return ESP_ERR_NOT_FOUND; + } + if (handle >= MAX_WL_HANDLES) { + ESP_LOGE(TAG, "%s: instance[0x%08" PRIx32 "] out of range", func, handle); + return ESP_ERR_INVALID_ARG; + } + if (s_instances[handle].instance == NULL) { + ESP_LOGE(TAG, "%s: instance[0x%08" PRIx32 "] not initialized", func, handle); + return ESP_ERR_NOT_FOUND; + } + return ESP_OK; +} esp_err_t wl_mount(const esp_partition_t *partition, wl_handle_t *out_handle) { @@ -170,100 +185,106 @@ out: esp_err_t wl_unmount(wl_handle_t handle) { - esp_err_t result = ESP_OK; _lock_acquire(&s_instances_lock); - result = check_handle(handle, __func__); + + esp_err_t result = check_handle(handle, __func__); if (result == ESP_OK) { - // We use placement new in wl_mount, so call destructor directly - Partition *part = s_instances[handle].instance->get_part(); - // We have to flush state of the component + // Acquire per-instance lock to drain any in-flight I/O that is + // running outside the global lock. + _lock_acquire(&s_instances[handle].lock); + Flash_Access *part = s_instances[handle].instance->get_part(); if (!part->is_readonly()) { result = s_instances[handle].instance->flush(); } - part->~Partition(); + part->~Flash_Access(); free(part); s_instances[handle].instance->~WL_Flash(); free(s_instances[handle].instance); s_instances[handle].instance = NULL; + _lock_release(&s_instances[handle].lock); _lock_close(&s_instances[handle].lock); // also zeroes the lock variable } + _lock_release(&s_instances_lock); return result; } esp_err_t wl_erase_range(wl_handle_t handle, size_t start_addr, size_t size) { + _lock_acquire(&s_instances_lock); esp_err_t result = check_handle(handle, __func__); - if (result != ESP_OK) { - return result; + if (result == ESP_OK) { + _lock_acquire(&s_instances[handle].lock); + _lock_release(&s_instances_lock); + result = s_instances[handle].instance->erase_range(start_addr, size); + _lock_release(&s_instances[handle].lock); + } else { + _lock_release(&s_instances_lock); } - _lock_acquire(&s_instances[handle].lock); - result = s_instances[handle].instance->erase_range(start_addr, size); - _lock_release(&s_instances[handle].lock); + return result; } esp_err_t wl_write(wl_handle_t handle, size_t dest_addr, const void *src, size_t size) { + _lock_acquire(&s_instances_lock); esp_err_t result = check_handle(handle, __func__); - if (result != ESP_OK) { - return result; + if (result == ESP_OK) { + _lock_acquire(&s_instances[handle].lock); + _lock_release(&s_instances_lock); + result = s_instances[handle].instance->write(dest_addr, src, size); + _lock_release(&s_instances[handle].lock); + } else { + _lock_release(&s_instances_lock); } - _lock_acquire(&s_instances[handle].lock); - result = s_instances[handle].instance->write(dest_addr, src, size); - _lock_release(&s_instances[handle].lock); + return result; } esp_err_t wl_read(wl_handle_t handle, size_t src_addr, void *dest, size_t size) { + _lock_acquire(&s_instances_lock); esp_err_t result = check_handle(handle, __func__); - if (result != ESP_OK) { - return result; + if (result == ESP_OK) { + _lock_acquire(&s_instances[handle].lock); + _lock_release(&s_instances_lock); + result = s_instances[handle].instance->read(src_addr, dest, size); + _lock_release(&s_instances[handle].lock); + } else { + _lock_release(&s_instances_lock); } - _lock_acquire(&s_instances[handle].lock); - result = s_instances[handle].instance->read(src_addr, dest, size); - _lock_release(&s_instances[handle].lock); + return result; } size_t wl_size(wl_handle_t handle) { - esp_err_t err = check_handle(handle, __func__); - if (err != ESP_OK) { - return 0; + _lock_acquire(&s_instances_lock); + esp_err_t result = check_handle(handle, __func__); + if (result == ESP_OK) { + _lock_acquire(&s_instances[handle].lock); + _lock_release(&s_instances_lock); + result = s_instances[handle].instance->get_flash_size(); + _lock_release(&s_instances[handle].lock); + } else { + _lock_release(&s_instances_lock); } - _lock_acquire(&s_instances[handle].lock); - size_t result = s_instances[handle].instance->get_flash_size(); - _lock_release(&s_instances[handle].lock); + return result; } size_t wl_sector_size(wl_handle_t handle) { - esp_err_t err = check_handle(handle, __func__); - if (err != ESP_OK) { - return 0; + _lock_acquire(&s_instances_lock); + esp_err_t result = check_handle(handle, __func__); + if (result == ESP_OK) { + _lock_acquire(&s_instances[handle].lock); + _lock_release(&s_instances_lock); + result = s_instances[handle].instance->get_sector_size(); + _lock_release(&s_instances[handle].lock); + } else { + _lock_release(&s_instances_lock); } - _lock_acquire(&s_instances[handle].lock); - size_t result = s_instances[handle].instance->get_sector_size(); - _lock_release(&s_instances[handle].lock); + return result; } - -static esp_err_t check_handle(wl_handle_t handle, const char *func) -{ - if (handle == WL_INVALID_HANDLE) { - ESP_LOGE(TAG, "%s: invalid handle", func); - return ESP_ERR_NOT_FOUND; - } - if (handle >= MAX_WL_HANDLES) { - ESP_LOGE(TAG, "%s: instance[0x%08" PRIx32 "] out of range", func, handle); - return ESP_ERR_INVALID_ARG; - } - if (s_instances[handle].instance == NULL) { - ESP_LOGE(TAG, "%s: instance[0x%08" PRIx32 "] not initialized", func, handle); - return ESP_ERR_NOT_FOUND; - } - return ESP_OK; -} diff --git a/components/wear_levelling/wl_blockdev.cpp b/components/wear_levelling/wl_blockdev.cpp new file mode 100644 index 0000000000..e58d3a02e4 --- /dev/null +++ b/components/wear_levelling/wl_blockdev.cpp @@ -0,0 +1,249 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include "wear_levelling.h" +#include "WL_Config.h" +#include "WL_Ext_Cfg.h" +#include "WL_Flash.h" +#include "WL_Ext_Perf.h" +#include "WL_Ext_Safe.h" +#include "BDL_Access.h" + +#ifndef WL_DEFAULT_UPDATERATE +#define WL_DEFAULT_UPDATERATE 16 +#endif + +#ifndef WL_DEFAULT_TEMP_BUFF_SIZE +#define WL_DEFAULT_TEMP_BUFF_SIZE 32 +#endif + +#ifndef WL_DEFAULT_WRITE_SIZE +#define WL_DEFAULT_WRITE_SIZE 16 +#endif + +#ifndef WL_DEFAULT_START_ADDR +#define WL_DEFAULT_START_ADDR 0 +#endif + +#ifndef WL_CURRENT_VERSION +#define WL_CURRENT_VERSION 2 +#endif + +static const char *TAG = "wl_blockdev"; + +/* ========================================================================= */ +/* BDL path – standalone WL instance, no wl_handle_t */ +/* ========================================================================= */ + +typedef struct { + WL_Flash *wl_instance; + BDL_Access *bdl_access; + esp_blockdev_handle_t bottom_bdl; + _lock_t lock; +} wl_bdl_ctx_t; + +static esp_err_t wl_bdl_read(esp_blockdev_handle_t dev, uint8_t *dst, size_t dst_size, + uint64_t src_addr, size_t len) +{ + if (dst_size < len) { + return ESP_ERR_INVALID_ARG; + } + if (src_addr % dev->geometry.read_size != 0 || len % dev->geometry.read_size) { + return ESP_ERR_INVALID_SIZE; + } + + wl_bdl_ctx_t *ctx = (wl_bdl_ctx_t *)dev->ctx; + _lock_acquire(&ctx->lock); + esp_err_t ret = ctx->wl_instance->read((size_t)src_addr, dst, len); + _lock_release(&ctx->lock); + return ret; +} + +static esp_err_t wl_bdl_write(esp_blockdev_handle_t dev, const uint8_t *src, + uint64_t dst_addr, size_t len) +{ + if (dst_addr % dev->geometry.write_size != 0 || len % dev->geometry.write_size) { + return ESP_ERR_INVALID_SIZE; + } + + wl_bdl_ctx_t *ctx = (wl_bdl_ctx_t *)dev->ctx; + _lock_acquire(&ctx->lock); + esp_err_t ret = ctx->wl_instance->write((size_t)dst_addr, src, len); + _lock_release(&ctx->lock); + return ret; +} + +static esp_err_t wl_bdl_erase(esp_blockdev_handle_t dev, uint64_t start, size_t len) +{ + if (start % dev->geometry.erase_size != 0 || len % dev->geometry.erase_size) { + return ESP_ERR_INVALID_SIZE; + } + + wl_bdl_ctx_t *ctx = (wl_bdl_ctx_t *)dev->ctx; + _lock_acquire(&ctx->lock); + esp_err_t ret = ctx->wl_instance->erase_range((size_t)start, len); + _lock_release(&ctx->lock); + return ret; +} + +static esp_err_t wl_bdl_sync(esp_blockdev_handle_t dev) +{ + wl_bdl_ctx_t *ctx = (wl_bdl_ctx_t *)dev->ctx; + _lock_acquire(&ctx->lock); + esp_err_t ret = ctx->wl_instance->flush(); + _lock_release(&ctx->lock); + return ret; +} + +static esp_err_t wl_bdl_ioctl(esp_blockdev_handle_t dev, const uint8_t cmd, void *args) +{ + wl_bdl_ctx_t *ctx = (wl_bdl_ctx_t *)dev->ctx; + if (ctx->bottom_bdl == ESP_BLOCKDEV_HANDLE_INVALID) { + return ESP_ERR_NOT_ALLOWED; + } + if (ctx->bottom_bdl->ops->ioctl == NULL) { + return ESP_ERR_NOT_SUPPORTED; + } + return ctx->bottom_bdl->ops->ioctl(ctx->bottom_bdl, cmd, args); +} + +static esp_err_t wl_bdl_release(esp_blockdev_handle_t dev) +{ + wl_bdl_ctx_t *ctx = (wl_bdl_ctx_t *)dev->ctx; + + if (ctx->wl_instance) { + if (!ctx->bdl_access->is_readonly()) { + ctx->wl_instance->flush(); + } + ctx->wl_instance->~WL_Flash(); + free(ctx->wl_instance); + } + if (ctx->bdl_access) { + ctx->bdl_access->~BDL_Access(); + free(ctx->bdl_access); + } + + _lock_close(&ctx->lock); + free(ctx); + free(dev); + return ESP_OK; +} + +static const esp_blockdev_ops_t s_wl_bdl_ops = { + .read = wl_bdl_read, + .write = wl_bdl_write, + .erase = wl_bdl_erase, + .sync = wl_bdl_sync, + .ioctl = wl_bdl_ioctl, + .release = wl_bdl_release, +}; + +esp_err_t wl_get_blockdev(const esp_blockdev_handle_t bdl_bottom_device, + esp_blockdev_handle_t *out_bdl_handle_ptr) +{ + if (bdl_bottom_device == ESP_BLOCKDEV_HANDLE_INVALID || out_bdl_handle_ptr == NULL) { + return ESP_ERR_INVALID_ARG; + } + + *out_bdl_handle_ptr = ESP_BLOCKDEV_HANDLE_INVALID; + + if (bdl_bottom_device->geometry.disk_size > UINT32_MAX) { + ESP_LOGE(TAG, "%s: disk_size 0x%016" PRIx64 " exceeds uint32_t range, " + "WL configuration would be silently truncated", + __func__, bdl_bottom_device->geometry.disk_size); + return ESP_ERR_INVALID_SIZE; + } + + esp_err_t result = ESP_OK; + + wl_bdl_ctx_t *ctx = (wl_bdl_ctx_t *)calloc(1, sizeof(wl_bdl_ctx_t)); + if (!ctx) { + return ESP_ERR_NO_MEM; + } + ctx->bottom_bdl = bdl_bottom_device; + + void *access_mem = malloc(sizeof(BDL_Access)); + if (!access_mem) { + free(ctx); + return ESP_ERR_NO_MEM; + } + ctx->bdl_access = new (access_mem) BDL_Access(bdl_bottom_device); + + void *wl_mem = NULL; + +#if CONFIG_WL_SECTOR_SIZE == 512 +#if CONFIG_WL_SECTOR_MODE == 1 + wl_mem = malloc(sizeof(WL_Ext_Safe)); + if (!wl_mem) { result = ESP_ERR_NO_MEM; goto fail; } + ctx->wl_instance = new (wl_mem) WL_Ext_Safe(); +#else + wl_mem = malloc(sizeof(WL_Ext_Perf)); + if (!wl_mem) { result = ESP_ERR_NO_MEM; goto fail; } + ctx->wl_instance = new (wl_mem) WL_Ext_Perf(); +#endif +#endif +#if CONFIG_WL_SECTOR_SIZE == 4096 + wl_mem = malloc(sizeof(WL_Flash)); + if (!wl_mem) { result = ESP_ERR_NO_MEM; goto fail; } + ctx->wl_instance = new (wl_mem) WL_Flash(); +#endif + + { + wl_ext_cfg_t cfg; + cfg.wl_partition_start_addr = WL_DEFAULT_START_ADDR; + cfg.wl_partition_size = (uint32_t)bdl_bottom_device->geometry.disk_size; + cfg.wl_page_size = bdl_bottom_device->geometry.erase_size; + cfg.flash_sector_size = bdl_bottom_device->geometry.erase_size; + cfg.wl_update_rate = WL_DEFAULT_UPDATERATE; + cfg.wl_pos_update_record_size = WL_DEFAULT_WRITE_SIZE; + cfg.version = WL_CURRENT_VERSION; + cfg.wl_temp_buff_size = WL_DEFAULT_TEMP_BUFF_SIZE; + cfg.fat_sector_size = CONFIG_WL_SECTOR_SIZE; + + result = ctx->wl_instance->config(&cfg, ctx->bdl_access); + if (result != ESP_OK) { + ESP_LOGE(TAG, "%s: config failed, result=0x%x", __func__, result); + goto fail; + } + + result = ctx->wl_instance->init(); + if (result != ESP_OK) { + ESP_LOGE(TAG, "%s: init failed, result=0x%x", __func__, result); + goto fail; + } + } + + { + esp_blockdev_t *dev = (esp_blockdev_t *)calloc(1, sizeof(esp_blockdev_t)); + if (!dev) { result = ESP_ERR_NO_MEM; goto fail; } + + dev->ctx = ctx; + dev->ops = &s_wl_bdl_ops; + dev->device_flags = bdl_bottom_device->device_flags; + dev->geometry.disk_size = ctx->wl_instance->get_flash_size(); + dev->geometry.read_size = bdl_bottom_device->geometry.read_size; + dev->geometry.write_size = bdl_bottom_device->geometry.write_size; + dev->geometry.erase_size = ctx->wl_instance->get_sector_size(); + dev->geometry.recommended_read_size = bdl_bottom_device->geometry.recommended_read_size; + dev->geometry.recommended_write_size = bdl_bottom_device->geometry.recommended_write_size; + dev->geometry.recommended_erase_size = ctx->wl_instance->get_sector_size(); + + _lock_init(&ctx->lock); + *out_bdl_handle_ptr = dev; + } + return ESP_OK; + +fail: + if (ctx->wl_instance) { ctx->wl_instance->~WL_Flash(); free(ctx->wl_instance); } + if (ctx->bdl_access) { ctx->bdl_access->~BDL_Access(); free(ctx->bdl_access); } + free(ctx); + return result; +}