Merge branch 'feat/bdl_generic_partition' into 'master'

feat(storage/blockdev): Add generic partition and memory mapping blockdev drivers

Closes IDF-12750

See merge request espressif/esp-idf!39989
This commit is contained in:
Tomas Rohlinek
2026-03-19 10:53:44 +01:00
20 changed files with 1146 additions and 3 deletions
+1
View File
@@ -87,6 +87,7 @@
/components/esp_app_format/ @esp-idf-codeowners/system @esp-idf-codeowners/app-utilities
/components/esp_asrc_adapter/ @esp-idf-codeowners/peripherals
/components/esp_blockdev/ @esp-idf-codeowners/storage
/components/esp_blockdev_util/ @esp-idf-codeowners/storage
/components/esp_bootloader_format/ @esp-idf-codeowners/system @esp-idf-codeowners/app-utilities
/components/esp_coex/ @esp-idf-codeowners/wifi @esp-idf-codeowners/bluetooth @esp-idf-codeowners/ieee802154
/components/esp_common/ @esp-idf-codeowners/system
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -36,8 +36,9 @@ extern "C" {
#define ESP_BLOCKDEV_CMD_SYSTEM_BASE 0x00 /*!< System commands base value */
#define ESP_BLOCKDEV_CMD_USER_BASE 0x80 /*!< User commands base value */
#define ESP_BLOCKDEV_CMD_MARK_DELETED ESP_BLOCKDEV_CMD_SYSTEM_BASE /*!< mark given range as invalid, data to be deleted/overwritten eventually */
#define ESP_BLOCKDEV_CMD_ERASE_CONTENTS ESP_BLOCKDEV_CMD_SYSTEM_BASE + 1 /*!< erase required range and set the contents to the device's default bit values */
// Command name Code Description Argument type
#define ESP_BLOCKDEV_CMD_MARK_DELETED ESP_BLOCKDEV_CMD_SYSTEM_BASE /*!< mark given range as invalid, data to be deleted/overwritten eventually [ esp_blockdev_cmd_arg_erase_t* ]*/
#define ESP_BLOCKDEV_CMD_ERASE_CONTENTS (ESP_BLOCKDEV_CMD_SYSTEM_BASE + 1) /*!< erase required range and set the contents to the device's default bit values [ esp_blockdev_cmd_arg_erase_t* ]*/
typedef struct esp_blockdev_cmd_arg_erase_t {
uint64_t start_addr; /*!< IN - starting address of the disk space to erase/trim/discard/sanitize (in bytes), must be a multiple of erase block size */
@@ -0,0 +1,3 @@
idf_component_register(SRC_DIRS "."
INCLUDE_DIRS "include"
PRIV_REQUIRES esp_blockdev)
@@ -0,0 +1,226 @@
/*
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <assert.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/types.h>
#include "esp_blockdev.h"
#include "esp_err.h"
#include "esp_check.h"
#include "esp_blockdev/generic_partition.h"
static const char *TAG = "esp_blockdev/generic_partition";
typedef struct {
esp_blockdev_t dev;
esp_blockdev_handle_t parent;
size_t start_offset;
} esp_blockdev_generic_partition_t;
ssize_t esp_blockdev_generic_partition_translate_address_to_parent(esp_blockdev_handle_t dev_handle, size_t address)
{
ESP_RETURN_ON_FALSE(dev_handle != NULL, -1, TAG, "The dev_handle cannot be NULL");
esp_blockdev_generic_partition_t *dev = (esp_blockdev_generic_partition_t *)dev_handle;
esp_blockdev_handle_t parent = dev->parent;
ESP_RETURN_ON_FALSE(address <= dev->dev.geometry.disk_size, -1, TAG, "The address falls outside of the partition");
uint64_t translated = (uint64_t)dev->start_offset + (uint64_t)address;
ESP_RETURN_ON_FALSE(translated >= (uint64_t)address, -1, TAG, "Address translation overflowed");
ESP_RETURN_ON_FALSE(translated <= parent->geometry.disk_size, -1, TAG, "The address range falls outside of the parent device");
return (ssize_t)translated;
}
ssize_t esp_blockdev_generic_partition_translate_address_to_child(esp_blockdev_handle_t dev_handle, size_t address)
{
ESP_RETURN_ON_FALSE(dev_handle != NULL, -1, TAG, "The dev_handle cannot be NULL");
esp_blockdev_generic_partition_t *dev = (esp_blockdev_generic_partition_t *)dev_handle;
uint64_t start_offset = dev->start_offset;
ESP_RETURN_ON_FALSE((uint64_t)address >= start_offset, -1, TAG, "The parent address is below the partition base");
uint64_t translated = (uint64_t)address - start_offset;
ESP_RETURN_ON_FALSE(translated <= dev->dev.geometry.disk_size, -1, TAG, "The address falls outside of the partition");
return (ssize_t)translated;
}
static esp_err_t bd_gp_read(esp_blockdev_handle_t dev_handle, uint8_t *dst_buf, size_t dst_buf_size, uint64_t src_addr, size_t data_read_len)
{
ESP_RETURN_ON_FALSE(dev_handle != NULL, ESP_ERR_INVALID_ARG, TAG, "The dev_handle cannot be NULL");
ESP_RETURN_ON_FALSE(dst_buf != NULL, ESP_ERR_INVALID_ARG, TAG, "The destination buffer cannot be NULL");
ESP_RETURN_ON_FALSE(data_read_len <= dst_buf_size, ESP_ERR_INVALID_SIZE, TAG, "Destination buffer too small");
ESP_RETURN_ON_FALSE(src_addr + data_read_len <= dev_handle->geometry.disk_size, ESP_ERR_INVALID_ARG, TAG, "The address range falls outside of the disk");
esp_blockdev_generic_partition_t *dev = (esp_blockdev_generic_partition_t *)dev_handle;
esp_blockdev_handle_t parent = dev->parent;
assert(dev_handle->geometry.read_size > 0);
assert(src_addr % dev_handle->geometry.read_size == 0);
assert(data_read_len % dev_handle->geometry.read_size == 0);
ssize_t addr_parent = esp_blockdev_generic_partition_translate_address_to_parent(dev_handle, src_addr);
ESP_RETURN_ON_FALSE(addr_parent >= 0, ESP_ERR_INVALID_ARG, TAG, "Failed to translate address");
return parent->ops->read(parent, dst_buf, dst_buf_size, (uint64_t)addr_parent, data_read_len);
}
static esp_err_t bd_gp_write(esp_blockdev_handle_t dev_handle, const uint8_t* src_buf, uint64_t dst_addr, size_t data_write_len)
{
ESP_RETURN_ON_FALSE(dev_handle != NULL, ESP_ERR_INVALID_ARG, TAG, "The dev_handle cannot be NULL");
ESP_RETURN_ON_FALSE(src_buf != NULL, ESP_ERR_INVALID_ARG, TAG, "The source buffer cannot be NULL");
ESP_RETURN_ON_FALSE(dst_addr + data_write_len <= dev_handle->geometry.disk_size, ESP_ERR_INVALID_ARG, TAG, "The address range falls outside of the disk");
esp_blockdev_generic_partition_t *dev = (esp_blockdev_generic_partition_t *)dev_handle;
esp_blockdev_handle_t parent = dev->parent;
/*
* These asserts are intentional: the contract makes violating these preconditions undefined,
* so returning dedicated error codes here is not strictly necessary.
* The underlying parent blockdev will most likely perform the same checks itself.
*/
assert(!dev_handle->device_flags.read_only);
assert(dev_handle->geometry.write_size > 0);
assert(dst_addr % dev_handle->geometry.write_size == 0);
assert(data_write_len % dev_handle->geometry.write_size == 0);
ssize_t addr_parent = esp_blockdev_generic_partition_translate_address_to_parent(dev_handle, dst_addr);
ESP_RETURN_ON_FALSE(addr_parent >= 0, ESP_ERR_INVALID_ARG, TAG, "Failed to translate address");
return parent->ops->write(parent, src_buf, (uint64_t)addr_parent, data_write_len);
}
static esp_err_t bd_gp_erase(esp_blockdev_handle_t dev_handle, uint64_t start_addr, size_t erase_len)
{
ESP_RETURN_ON_FALSE(dev_handle != NULL, ESP_ERR_INVALID_ARG, TAG, "The dev_handle cannot be NULL");
esp_blockdev_generic_partition_t *dev = (esp_blockdev_generic_partition_t *)dev_handle;
esp_blockdev_handle_t parent = dev->parent;
assert(!dev_handle->device_flags.read_only);
assert(dev_handle->geometry.erase_size > 0);
assert(start_addr % dev_handle->geometry.erase_size == 0);
assert(erase_len % dev_handle->geometry.erase_size == 0);
if (start_addr + erase_len > dev->dev.geometry.disk_size) {
return ESP_ERR_INVALID_ARG;
}
ssize_t addr_parent = esp_blockdev_generic_partition_translate_address_to_parent(dev_handle, start_addr);
ESP_RETURN_ON_FALSE(addr_parent >= 0, ESP_ERR_INVALID_ARG, TAG, "Failed to translate address");
return parent->ops->erase(parent, (uint64_t)addr_parent, erase_len);
}
static esp_err_t bd_gp_sync(esp_blockdev_handle_t dev_handle)
{
ESP_RETURN_ON_FALSE(dev_handle != NULL, ESP_ERR_INVALID_ARG, TAG, "The dev_handle cannot be NULL");
esp_blockdev_generic_partition_t *dev = (esp_blockdev_generic_partition_t *)dev_handle;
esp_blockdev_handle_t parent = dev->parent;
if (parent->ops->sync == NULL) {
return ESP_OK;
}
return parent->ops->sync(parent);
}
static esp_err_t bd_gp_ioctl(esp_blockdev_handle_t dev_handle, const uint8_t cmd, void *args)
{
ESP_RETURN_ON_FALSE(dev_handle != NULL, ESP_ERR_INVALID_ARG, TAG, "The dev_handle cannot be NULL");
esp_blockdev_generic_partition_t *dev = (esp_blockdev_generic_partition_t *)dev_handle;
esp_blockdev_handle_t parent = dev->parent;
if (cmd == ESP_BLOCKDEV_CMD_MARK_DELETED || cmd == ESP_BLOCKDEV_CMD_ERASE_CONTENTS) {
ESP_RETURN_ON_FALSE(args != NULL, ESP_ERR_INVALID_ARG, TAG, "The ioctl arguments cannot be NULL");
esp_blockdev_cmd_arg_erase_t *erase_args = (esp_blockdev_cmd_arg_erase_t *)args;
ESP_RETURN_ON_FALSE(erase_args->start_addr <= dev->dev.geometry.disk_size,
ESP_ERR_INVALID_ARG, TAG, "The address range falls outside of the disk");
ESP_RETURN_ON_FALSE(erase_args->erase_len <= dev->dev.geometry.disk_size - erase_args->start_addr,
ESP_ERR_INVALID_ARG, TAG, "The address range falls outside of the disk");
assert(dev->dev.geometry.erase_size > 0);
assert(erase_args->start_addr % dev->dev.geometry.erase_size == 0);
assert(erase_args->erase_len % dev->dev.geometry.erase_size == 0);
ssize_t addr_parent = esp_blockdev_generic_partition_translate_address_to_parent(dev_handle, erase_args->start_addr);
ESP_RETURN_ON_FALSE(addr_parent >= 0, ESP_ERR_INVALID_ARG, TAG, "Failed to translate address");
esp_blockdev_cmd_arg_erase_t translated_args = *erase_args;
translated_args.start_addr = (uint64_t)addr_parent;
ESP_RETURN_ON_FALSE(parent->ops->ioctl != NULL, ESP_ERR_NOT_SUPPORTED, TAG, "Parent device does not implement ioctl");
return parent->ops->ioctl(parent, cmd, &translated_args);
}
ESP_RETURN_ON_FALSE(parent->ops->ioctl != NULL, ESP_ERR_NOT_SUPPORTED, TAG, "Parent device does not implement ioctl");
return parent->ops->ioctl(parent, cmd, args);
}
static esp_err_t bd_gp_release(esp_blockdev_handle_t dev_handle)
{
ESP_RETURN_ON_FALSE(dev_handle != NULL, ESP_ERR_INVALID_ARG, TAG, "The dev_handle cannot be NULL");
free(dev_handle);
return ESP_OK;
}
static const esp_blockdev_ops_t g_generic_partition_ops = {
.read = bd_gp_read,
.write = bd_gp_write,
.erase = bd_gp_erase,
.sync = bd_gp_sync,
.ioctl = bd_gp_ioctl,
.release = bd_gp_release,
};
esp_err_t esp_blockdev_generic_partition_get(esp_blockdev_handle_t parent, size_t start_offset, size_t size, esp_blockdev_handle_t *out)
{
ESP_RETURN_ON_FALSE(parent != NULL, ESP_ERR_INVALID_ARG, TAG, "The parent device handle cannot be NULL");
ESP_RETURN_ON_FALSE(out != NULL, ESP_ERR_INVALID_ARG, TAG, "The out pointer cannot be NULL");
*out = ESP_BLOCKDEV_HANDLE_INVALID;
uint64_t partition_end = (uint64_t)start_offset + (uint64_t)size;
ESP_RETURN_ON_FALSE(partition_end >= (uint64_t)start_offset, ESP_ERR_INVALID_ARG, TAG, "The requested range overflows");
ESP_RETURN_ON_FALSE(partition_end <= parent->geometry.disk_size, ESP_ERR_INVALID_ARG, TAG, "The address range falls outside of the disk");
esp_blockdev_generic_partition_t *part = calloc(1, sizeof(esp_blockdev_generic_partition_t));
if (part == NULL) {
return ESP_ERR_NO_MEM;
}
*part = (esp_blockdev_generic_partition_t) {
.dev = {
.device_flags = parent->device_flags,
.geometry = {
.disk_size = size,
.read_size = parent->geometry.read_size,
.write_size = parent->geometry.write_size,
.erase_size = parent->geometry.erase_size,
.recommended_write_size = parent->geometry.recommended_write_size,
.recommended_read_size = parent->geometry.recommended_read_size,
.recommended_erase_size = parent->geometry.recommended_erase_size,
},
.ops = &g_generic_partition_ops,
},
.parent = parent,
.start_offset = start_offset,
};
part->dev.ctx = part;
*out = (esp_blockdev_handle_t) part;
return ESP_OK;
}
@@ -0,0 +1,55 @@
/*
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <sys/types.h>
#include <stddef.h>
#include "esp_blockdev.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Create a new partition over a given blockdev
*
* @param parent The underlying device
* @param start Address of the start of the new partition. (Behaviour is undefined when this is not a multiple of block size for underlying device)
* @param size Size of the new partition. (Behaviour is undefined when this is not a multiple of block size for underlying device)
* @param out Where to store handle to the newly created block device. Will be unchanged upon failure.
*
* @return ESP_ERR_INVALID_ARG - Invalid argument was passed
* ESP_ERR_NO_MEM - Failed to allocate the struct
* ESP_OK
*/
esp_err_t esp_blockdev_generic_partition_get(esp_blockdev_handle_t parent, size_t start, size_t size, esp_blockdev_handle_t *out);
/**
* @brief Translate virtual partition address to parents address space
*
* @param device The device for which to do the translation
* @param address Address to be translated, relative to the partition start
*
* @return Parent-space address on success
* -1 on error (invalid argument or overflow)
*/
ssize_t esp_blockdev_generic_partition_translate_address_to_parent(esp_blockdev_handle_t device, size_t address);
/**
* @brief Translate virtual partition address from parents address space
*
* @param device The device for which to do the translation
* @param address Address to be translated, in the parent address space
*
* @return Partition-relative address on success
* -1 on error (invalid argument or underflow)
*/
ssize_t esp_blockdev_generic_partition_translate_address_to_child(esp_blockdev_handle_t device, size_t address);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,51 @@
/*
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "esp_blockdev.h"
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Create a memory-backed block device that wraps an existing buffer
*
* @param buffer Pointer to the storage backing the device
* @param buffer_size Size of the storage buffer in bytes
* @param geometry Desired geometry for the device; geometry->disk_size must not exceed buffer_size
* @param read_only Set true to disallow write/erase operations on the mapped memory
* @param out Output pointer that receives the created block device handle; unchanged on failure
*
* @return ESP_ERR_INVALID_ARG Invalid argument provided
* @return ESP_ERR_INVALID_SIZE Provided buffer or geometry is incompatible
* @return ESP_ERR_NO_MEM Memory allocation for device descriptor failed
* @return ESP_OK on success
*/
esp_err_t esp_blockdev_memory_get_from_buffer(uint8_t *buffer, size_t buffer_size, const esp_blockdev_geometry_t *geometry, bool read_only, esp_blockdev_handle_t *out);
/**
* @brief Create a memory-backed block device with internally allocated storage
*
* @param geometry Desired geometry for the device; geometry->disk_size determines the allocation size
* @param caps Heap capability flags passed to heap_caps_malloc
* @param out Output pointer that receives the created block device handle; unchanged on failure
*
* @return ESP_ERR_INVALID_ARG Invalid argument provided
* @return ESP_ERR_INVALID_SIZE Allocation size exceeds supported limits
* @return ESP_ERR_NO_MEM Memory allocation failed
* @return ESP_OK on success
*/
esp_err_t esp_blockdev_memory_create_with_heap_caps(const esp_blockdev_geometry_t *geometry, uint32_t caps, esp_blockdev_handle_t *out);
#ifdef __cplusplus
}
#endif
+174
View File
@@ -0,0 +1,174 @@
/*
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include "esp_check.h"
#include "esp_err.h"
#include "esp_heap_caps.h"
#include "esp_blockdev.h"
#include "esp_blockdev/memory.h"
static const char *TAG = "esp_blockdev/memory";
typedef struct {
esp_blockdev_t dev;
uint8_t *buffer;
size_t capacity;
bool owns_buffer;
} esp_blockdev_memory_t;
static esp_err_t bd_memory_read(esp_blockdev_handle_t dev_handle, uint8_t *dst_buf, size_t dst_buf_size, uint64_t src_addr, size_t data_read_len)
{
ESP_RETURN_ON_FALSE(dev_handle != NULL, ESP_ERR_INVALID_ARG, TAG, "The dev_handle cannot be NULL");
ESP_RETURN_ON_FALSE(dst_buf != NULL, ESP_ERR_INVALID_ARG, TAG, "The destination buffer cannot be NULL");
ESP_RETURN_ON_FALSE(data_read_len <= dst_buf_size, ESP_ERR_INVALID_SIZE, TAG, "Destination buffer too small");
ESP_RETURN_ON_FALSE(src_addr + data_read_len <= dev_handle->geometry.disk_size, ESP_ERR_INVALID_ARG, TAG, "The address range falls outside of the disk");
esp_blockdev_memory_t *dev = (esp_blockdev_memory_t *)dev_handle;
size_t offset = (size_t)src_addr;
memcpy(dst_buf, dev->buffer + offset, data_read_len);
return ESP_OK;
}
static esp_err_t bd_memory_write(esp_blockdev_handle_t dev_handle, const uint8_t* src_buf, uint64_t dst_addr, size_t data_write_len)
{
ESP_RETURN_ON_FALSE(dev_handle != NULL, ESP_ERR_INVALID_ARG, TAG, "The dev_handle cannot be NULL");
ESP_RETURN_ON_FALSE(src_buf != NULL, ESP_ERR_INVALID_ARG, TAG, "The source buffer cannot be NULL");
ESP_RETURN_ON_FALSE(!dev_handle->device_flags.read_only, ESP_ERR_INVALID_STATE, TAG, "The device is read-only");
ESP_RETURN_ON_FALSE(dst_addr + data_write_len <= dev_handle->geometry.disk_size, ESP_ERR_INVALID_ARG, TAG, "The address range falls outside of the disk");
esp_blockdev_memory_t *dev = (esp_blockdev_memory_t *)dev_handle;
size_t offset = (size_t)dst_addr;
memcpy(dev->buffer + offset, src_buf, data_write_len);
return ESP_OK;
}
static esp_err_t bd_memory_erase(esp_blockdev_handle_t dev_handle, uint64_t start_addr, size_t erase_len)
{
ESP_RETURN_ON_FALSE(dev_handle != NULL, ESP_ERR_INVALID_ARG, TAG, "The dev_handle cannot be NULL");
ESP_RETURN_ON_FALSE(!dev_handle->device_flags.read_only, ESP_ERR_INVALID_STATE, TAG, "The device is read-only");
ESP_RETURN_ON_FALSE(start_addr + erase_len <= dev_handle->geometry.disk_size, ESP_ERR_INVALID_ARG, TAG, "The address range falls outside of the disk");
esp_blockdev_memory_t *dev = (esp_blockdev_memory_t *)dev_handle;
size_t offset = (size_t)start_addr;
uint8_t erase_value = dev_handle->device_flags.default_val_after_erase ? 0xFF : 0;
memset(dev->buffer + offset, erase_value, erase_len);
return ESP_OK;
}
static esp_err_t bd_memory_sync(esp_blockdev_handle_t dev_handle)
{
ESP_RETURN_ON_FALSE(dev_handle != NULL, ESP_ERR_INVALID_ARG, TAG, "The dev_handle cannot be NULL");
return ESP_OK;
}
static esp_err_t bd_memory_ioctl(esp_blockdev_handle_t dev_handle, const uint8_t cmd, void *args)
{
ESP_RETURN_ON_FALSE(dev_handle != NULL, ESP_ERR_INVALID_ARG, TAG, "The dev_handle cannot be NULL");
(void)cmd;
(void)args;
return ESP_ERR_NOT_SUPPORTED;
}
static esp_err_t bd_memory_release(esp_blockdev_handle_t dev_handle)
{
ESP_RETURN_ON_FALSE(dev_handle != NULL, ESP_ERR_INVALID_ARG, TAG, "The dev_handle cannot be NULL");
esp_blockdev_memory_t *dev = (esp_blockdev_memory_t *)dev_handle;
if (dev->owns_buffer && dev->buffer != NULL) {
heap_caps_free(dev->buffer);
}
free(dev);
return ESP_OK;
}
static const esp_blockdev_ops_t g_memory_blockdev_ops = {
.read = bd_memory_read,
.write = bd_memory_write,
.erase = bd_memory_erase,
.sync = bd_memory_sync,
.ioctl = bd_memory_ioctl,
.release = bd_memory_release,
};
static esp_err_t esp_blockdev_memory_create_internal(uint8_t *buffer, size_t buffer_size, const esp_blockdev_geometry_t *geometry, bool owns_buffer, bool read_only, esp_blockdev_handle_t *out)
{
ESP_RETURN_ON_FALSE(out != NULL, ESP_ERR_INVALID_ARG, TAG, "The out pointer cannot be NULL");
*out = ESP_BLOCKDEV_HANDLE_INVALID;
ESP_RETURN_ON_FALSE(buffer != NULL, ESP_ERR_INVALID_ARG, TAG, "The buffer cannot be NULL");
ESP_RETURN_ON_FALSE(geometry != NULL, ESP_ERR_INVALID_ARG, TAG, "Geometry pointer cannot be NULL");
ESP_RETURN_ON_FALSE(geometry->disk_size > 0, ESP_ERR_INVALID_ARG, TAG, "Disk size must be greater than zero");
ESP_RETURN_ON_FALSE(geometry->disk_size <= SIZE_MAX, ESP_ERR_INVALID_SIZE, TAG, "Disk size exceeds addressable memory span");
ESP_RETURN_ON_FALSE(geometry->disk_size <= buffer_size, ESP_ERR_INVALID_SIZE, TAG, "Buffer smaller than requested disk size");
esp_blockdev_memory_t *dev = calloc(1, sizeof(esp_blockdev_memory_t));
ESP_RETURN_ON_FALSE(dev != NULL, ESP_ERR_NO_MEM, TAG, "Failed to allocate device structure");
*dev = (esp_blockdev_memory_t) {
.dev = {
.ctx = NULL,
.device_flags = {
.read_only = read_only,
.encrypted = false,
.erase_before_write = 0,
.and_type_write = 0,
.default_val_after_erase = false,
},
.geometry = *geometry,
.ops = &g_memory_blockdev_ops,
},
.buffer = buffer,
.capacity = buffer_size,
.owns_buffer = owns_buffer,
};
*out = (esp_blockdev_handle_t)dev;
return ESP_OK;
}
esp_err_t esp_blockdev_memory_get_from_buffer(uint8_t *buffer, size_t buffer_size, const esp_blockdev_geometry_t *geometry, bool read_only, esp_blockdev_handle_t *out)
{
return esp_blockdev_memory_create_internal(buffer, buffer_size, geometry, false, read_only, out);
}
esp_err_t esp_blockdev_memory_create_with_heap_caps(const esp_blockdev_geometry_t *geometry, uint32_t caps, esp_blockdev_handle_t *out)
{
ESP_RETURN_ON_FALSE(geometry != NULL, ESP_ERR_INVALID_ARG, TAG, "Geometry pointer cannot be NULL");
size_t alloc_size = (size_t)geometry->disk_size;
uint8_t *buffer = heap_caps_malloc(alloc_size, caps);
ESP_RETURN_ON_FALSE(buffer != NULL, ESP_ERR_NO_MEM, TAG, "Failed to allocate device buffer");
memset(buffer, 0x00, alloc_size);
esp_err_t err = esp_blockdev_memory_create_internal(buffer, alloc_size, geometry, true, false, out);
if (err != ESP_OK) {
heap_caps_free(buffer);
return err;
}
return ESP_OK;
}
@@ -0,0 +1,16 @@
# Documentation: .gitlab/ci/README.md#manifest-file-to-control-the-buildtest-apps
components/esp_blockdev_util/test_apps/generic_partition:
enable:
- if: INCLUDE_DEFAULT == 1 or IDF_TARGET == "linux"
disable_test:
- if: IDF_TARGET not in ["esp32", "esp32c3", "linux"]
temporary: true
reason: cover Xtensa and RISC-V targets
components/esp_blockdev_util/test_apps/memory_blockdev:
enable:
- if: INCLUDE_DEFAULT == 1 or IDF_TARGET == "linux"
disable_test:
- if: IDF_TARGET not in ["esp32", "esp32c3", "linux"]
temporary: true
reason: cover Xtensa and RISC-V targets
@@ -0,0 +1,11 @@
# This is the project CMakeLists.txt file for the generic partition test application
cmake_minimum_required(VERSION 3.22)
set(EXTRA_COMPONENT_DIRS
"$ENV{IDF_PATH}/tools/test_apps/components"
"${CMAKE_CURRENT_LIST_DIR}/../../"
"${CMAKE_CURRENT_LIST_DIR}/../../../esp_blockdev")
set(COMPONENTS main)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(generic_partition_test)
@@ -0,0 +1,2 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 | Linux |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | --------- | ----- |
@@ -0,0 +1,2 @@
idf_component_register(SRCS "test_generic_partition.c"
PRIV_REQUIRES unity esp_blockdev esp_blockdev_util)
@@ -0,0 +1,371 @@
/*
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include "unity.h"
#include "unity_test_utils.h"
#include "esp_blockdev.h"
#include "esp_blockdev/generic_partition.h"
#include "esp_blockdev/memory.h"
static esp_blockdev_handle_t create_memory_parent(uint8_t *backing, size_t backing_size)
{
const esp_blockdev_geometry_t geometry = {
.disk_size = backing_size,
.read_size = 1,
.write_size = 1,
.erase_size = 1,
.recommended_write_size = 8,
.recommended_read_size = 8,
.recommended_erase_size = 16,
};
esp_blockdev_handle_t parent = NULL;
TEST_ESP_OK(esp_blockdev_memory_get_from_buffer(backing, backing_size, &geometry, false, &parent));
TEST_ASSERT_NOT_NULL(parent);
return parent;
}
TEST_CASE("generic partition basic read/write/erase", "[generic_partition]")
{
uint8_t backing[256];
memset(backing, 0xEE, sizeof(backing));
esp_blockdev_handle_t parent = create_memory_parent(backing, sizeof(backing));
const size_t partition_offset = 64;
const size_t partition_size = 128;
esp_blockdev_handle_t part = NULL;
TEST_ESP_OK(esp_blockdev_generic_partition_get(parent, partition_offset, partition_size, &part));
TEST_ASSERT_NOT_NULL(part);
TEST_ASSERT_EQUAL_UINT64(partition_size, part->geometry.disk_size);
TEST_ASSERT_EQUAL_UINT32(parent->geometry.read_size, part->geometry.read_size);
TEST_ASSERT_EQUAL_UINT32(parent->geometry.write_size, part->geometry.write_size);
TEST_ASSERT_EQUAL_UINT32(parent->geometry.erase_size, part->geometry.erase_size);
uint8_t pattern[32];
for (size_t i = 0; i < sizeof(pattern); ++i) {
pattern[i] = (uint8_t)(i ^ 0xA5);
}
const uint64_t write_addr = 48;
TEST_ESP_OK(part->ops->write(part, pattern, write_addr, sizeof(pattern)));
TEST_ASSERT_EQUAL_UINT8_ARRAY(pattern, &backing[partition_offset + write_addr], sizeof(pattern));
uint8_t read_buf[sizeof(pattern)];
memset(read_buf, 0, sizeof(read_buf));
TEST_ESP_OK(part->ops->read(part, read_buf, sizeof(read_buf), write_addr, sizeof(read_buf)));
TEST_ASSERT_EQUAL_UINT8_ARRAY(pattern, read_buf, sizeof(pattern));
TEST_ESP_OK(part->ops->erase(part, write_addr, sizeof(pattern)));
TEST_ASSERT_EACH_EQUAL_UINT8(0x00, &backing[partition_offset + write_addr], sizeof(pattern));
TEST_ESP_OK(part->ops->release(part));
TEST_ESP_OK(parent->ops->release(parent));
}
TEST_CASE("generic partition translate helpers", "[generic_partition]")
{
uint8_t backing[32];
memset(backing, 0x00, sizeof(backing));
esp_blockdev_handle_t parent = create_memory_parent(backing, sizeof(backing));
esp_blockdev_handle_t part = NULL;
TEST_ESP_OK(esp_blockdev_generic_partition_get(parent, 4, 16, &part));
ssize_t parent_addr = esp_blockdev_generic_partition_translate_address_to_parent(part, 3);
TEST_ASSERT_GREATER_OR_EQUAL(0, parent_addr);
TEST_ASSERT_EQUAL_INT(7, parent_addr);
ssize_t child_addr = esp_blockdev_generic_partition_translate_address_to_child(part, 10);
TEST_ASSERT_GREATER_OR_EQUAL(0, child_addr);
TEST_ASSERT_EQUAL_INT(6, child_addr);
TEST_ASSERT_EQUAL_INT(-1, esp_blockdev_generic_partition_translate_address_to_parent(part, 20));
TEST_ASSERT_EQUAL_INT(-1, esp_blockdev_generic_partition_translate_address_to_child(part, 2));
TEST_ESP_OK(part->ops->release(part));
TEST_ESP_OK(parent->ops->release(parent));
}
typedef struct {
esp_blockdev_t dev;
struct {
uint8_t *dst_buf;
size_t dst_buf_size;
uint64_t addr;
size_t len;
bool called;
} read;
struct {
const uint8_t *src_buf;
uint64_t addr;
size_t len;
bool called;
} write;
struct {
uint64_t addr;
size_t len;
bool called;
} erase;
bool sync_called;
struct {
uint8_t cmd;
void *args;
esp_blockdev_cmd_arg_erase_t erase_arg;
bool erase_arg_copied;
bool called;
} ioctl;
} mock_blockdev_t;
static esp_err_t mock_read(esp_blockdev_handle_t dev, uint8_t *dst_buf, size_t dst_buf_size, uint64_t src_addr, size_t data_len)
{
mock_blockdev_t *mock = (mock_blockdev_t *)dev->ctx;
mock->read.called = true;
mock->read.dst_buf = dst_buf;
mock->read.dst_buf_size = dst_buf_size;
mock->read.addr = src_addr;
mock->read.len = data_len;
return ESP_OK;
}
static esp_err_t mock_write(esp_blockdev_handle_t dev, const uint8_t *src_buf, uint64_t dst_addr, size_t data_len)
{
mock_blockdev_t *mock = (mock_blockdev_t *)dev->ctx;
mock->write.called = true;
mock->write.src_buf = src_buf;
mock->write.addr = dst_addr;
mock->write.len = data_len;
return ESP_OK;
}
static esp_err_t mock_erase(esp_blockdev_handle_t dev, uint64_t start_addr, size_t erase_len)
{
mock_blockdev_t *mock = (mock_blockdev_t *)dev->ctx;
mock->erase.called = true;
mock->erase.addr = start_addr;
mock->erase.len = erase_len;
return ESP_OK;
}
static esp_err_t mock_sync(esp_blockdev_handle_t dev)
{
mock_blockdev_t *mock = (mock_blockdev_t *)dev->ctx;
mock->sync_called = true;
return ESP_OK;
}
static esp_err_t mock_ioctl(esp_blockdev_handle_t dev, const uint8_t cmd, void *args)
{
mock_blockdev_t *mock = (mock_blockdev_t *)dev->ctx;
mock->ioctl.called = true;
mock->ioctl.cmd = cmd;
mock->ioctl.args = args;
if ((cmd == ESP_BLOCKDEV_CMD_MARK_DELETED || cmd == ESP_BLOCKDEV_CMD_ERASE_CONTENTS) && args != NULL) {
mock->ioctl.erase_arg = *(esp_blockdev_cmd_arg_erase_t *)args;
mock->ioctl.erase_arg_copied = true;
}
return ESP_OK;
}
static esp_err_t mock_release(esp_blockdev_handle_t dev)
{
mock_blockdev_t *mock = (mock_blockdev_t *)dev->ctx;
memset(mock, 0, sizeof(*mock));
return ESP_OK;
}
static const esp_blockdev_ops_t MOCK_OPS = {
.read = mock_read,
.write = mock_write,
.erase = mock_erase,
.sync = mock_sync,
.ioctl = mock_ioctl,
.release = mock_release,
};
static const esp_blockdev_ops_t MOCK_OPS_NO_SYNC_IOCTL = {
.read = mock_read,
.write = mock_write,
.erase = mock_erase,
.sync = NULL,
.ioctl = NULL,
.release = mock_release,
};
static void init_mock_with_ops(mock_blockdev_t *mock, uint64_t disk_size, const esp_blockdev_ops_t *ops);
static void init_mock(mock_blockdev_t *mock, uint64_t disk_size)
{
init_mock_with_ops(mock, disk_size, &MOCK_OPS);
}
static void init_mock_with_ops(mock_blockdev_t *mock, uint64_t disk_size, const esp_blockdev_ops_t *ops)
{
memset(mock, 0, sizeof(*mock));
mock->dev.ctx = mock;
mock->dev.device_flags.val = 0;
mock->dev.geometry = (esp_blockdev_geometry_t) {
.disk_size = disk_size,
.read_size = 4,
.write_size = 4,
.erase_size = 16,
.recommended_read_size = 8,
.recommended_write_size = 8,
.recommended_erase_size = 64,
};
mock->dev.ops = ops;
}
TEST_CASE("generic partition forwards operations to parent", "[generic_partition]")
{
mock_blockdev_t mock_parent;
init_mock(&mock_parent, 1024);
esp_blockdev_handle_t part = NULL;
const size_t start = 128;
const size_t size = 512;
TEST_ESP_OK(esp_blockdev_generic_partition_get((esp_blockdev_handle_t)&mock_parent, start, size, &part));
uint8_t buffer[64];
uint8_t src[32];
memset(src, 0xCC, sizeof(src));
TEST_ESP_OK(part->ops->read(part, buffer, sizeof(buffer), 12, sizeof(buffer)));
TEST_ASSERT_TRUE(mock_parent.read.called);
TEST_ASSERT_EQUAL_PTR(buffer, mock_parent.read.dst_buf);
TEST_ASSERT_EQUAL_UINT32(sizeof(buffer), mock_parent.read.dst_buf_size);
TEST_ASSERT_EQUAL_UINT64(start + 12, mock_parent.read.addr);
TEST_ASSERT_EQUAL_UINT32(sizeof(buffer), mock_parent.read.len);
TEST_ESP_OK(part->ops->write(part, src, 24, sizeof(src)));
TEST_ASSERT_TRUE(mock_parent.write.called);
TEST_ASSERT_EQUAL_PTR(src, mock_parent.write.src_buf);
TEST_ASSERT_EQUAL_UINT64(start + 24, mock_parent.write.addr);
TEST_ASSERT_EQUAL_UINT32(sizeof(src), mock_parent.write.len);
TEST_ESP_OK(part->ops->erase(part, 48, 96));
TEST_ASSERT_TRUE(mock_parent.erase.called);
TEST_ASSERT_EQUAL_UINT64(start + 48, mock_parent.erase.addr);
TEST_ASSERT_EQUAL_UINT32(96, mock_parent.erase.len);
TEST_ESP_OK(part->ops->sync(part));
TEST_ASSERT_TRUE(mock_parent.sync_called);
uint32_t ioctl_arg = 0xDEADBEEF;
TEST_ESP_OK(part->ops->ioctl(part, 0x42, &ioctl_arg));
TEST_ASSERT_TRUE(mock_parent.ioctl.called);
TEST_ASSERT_EQUAL_UINT8(0x42, mock_parent.ioctl.cmd);
TEST_ASSERT_EQUAL_PTR(&ioctl_arg, mock_parent.ioctl.args);
TEST_ESP_OK(part->ops->release(part));
TEST_ESP_OK(mock_parent.dev.ops->release((esp_blockdev_handle_t)&mock_parent));
}
TEST_CASE("generic partition remaps mark deleted ioctl addresses", "[generic_partition]")
{
mock_blockdev_t mock_parent;
init_mock(&mock_parent, 1024);
esp_blockdev_handle_t part = NULL;
const size_t start = 128;
const size_t size = 512;
TEST_ESP_OK(esp_blockdev_generic_partition_get((esp_blockdev_handle_t)&mock_parent, start, size, &part));
esp_blockdev_cmd_arg_erase_t mark_deleted = {
.start_addr = 48,
.erase_len = 96,
};
TEST_ESP_OK(part->ops->ioctl(part, ESP_BLOCKDEV_CMD_MARK_DELETED, &mark_deleted));
TEST_ASSERT_TRUE(mock_parent.ioctl.called);
TEST_ASSERT_EQUAL_UINT8(ESP_BLOCKDEV_CMD_MARK_DELETED, mock_parent.ioctl.cmd);
TEST_ASSERT_TRUE(mock_parent.ioctl.erase_arg_copied);
TEST_ASSERT_EQUAL_UINT64(start + 48, mock_parent.ioctl.erase_arg.start_addr);
TEST_ASSERT_EQUAL_UINT32(96, mock_parent.ioctl.erase_arg.erase_len);
TEST_ASSERT_EQUAL_UINT64(48, mark_deleted.start_addr);
TEST_ASSERT_EQUAL_UINT32(96, mark_deleted.erase_len);
TEST_ESP_OK(part->ops->release(part));
TEST_ESP_OK(mock_parent.dev.ops->release((esp_blockdev_handle_t)&mock_parent));
}
TEST_CASE("generic partition remaps erase contents ioctl addresses", "[generic_partition]")
{
mock_blockdev_t mock_parent;
init_mock(&mock_parent, 1024);
esp_blockdev_handle_t part = NULL;
const size_t start = 128;
const size_t size = 512;
TEST_ESP_OK(esp_blockdev_generic_partition_get((esp_blockdev_handle_t)&mock_parent, start, size, &part));
esp_blockdev_cmd_arg_erase_t erase_contents = {
.start_addr = 64,
.erase_len = 96,
};
TEST_ESP_OK(part->ops->ioctl(part, ESP_BLOCKDEV_CMD_ERASE_CONTENTS, &erase_contents));
TEST_ASSERT_TRUE(mock_parent.ioctl.called);
TEST_ASSERT_EQUAL_UINT8(ESP_BLOCKDEV_CMD_ERASE_CONTENTS, mock_parent.ioctl.cmd);
TEST_ASSERT_TRUE(mock_parent.ioctl.erase_arg_copied);
TEST_ASSERT_EQUAL_UINT64(start + 64, mock_parent.ioctl.erase_arg.start_addr);
TEST_ASSERT_EQUAL_UINT32(96, mock_parent.ioctl.erase_arg.erase_len);
TEST_ASSERT_EQUAL_UINT64(64, erase_contents.start_addr);
TEST_ASSERT_EQUAL_UINT32(96, erase_contents.erase_len);
TEST_ESP_OK(part->ops->release(part));
TEST_ESP_OK(mock_parent.dev.ops->release((esp_blockdev_handle_t)&mock_parent));
}
TEST_CASE("generic partition tolerates missing optional parent ops", "[generic_partition]")
{
mock_blockdev_t mock_parent;
init_mock_with_ops(&mock_parent, 1024, &MOCK_OPS_NO_SYNC_IOCTL);
esp_blockdev_handle_t part = NULL;
TEST_ESP_OK(esp_blockdev_generic_partition_get((esp_blockdev_handle_t)&mock_parent, 128, 512, &part));
TEST_ESP_OK(part->ops->sync(part));
TEST_ASSERT_EQUAL(ESP_ERR_NOT_SUPPORTED, part->ops->ioctl(part, 0x44, NULL));
TEST_ESP_OK(part->ops->release(part));
TEST_ESP_OK(mock_parent.dev.ops->release((esp_blockdev_handle_t)&mock_parent));
}
TEST_CASE("generic partition creation validation", "[generic_partition]")
{
uint8_t backing[128];
memset(backing, 0, sizeof(backing));
esp_blockdev_handle_t parent = create_memory_parent(backing, sizeof(backing));
esp_blockdev_handle_t part = (esp_blockdev_handle_t)0xDEADBEEF;
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_blockdev_generic_partition_get(parent, 64, 256, &part));
TEST_ASSERT_EQUAL_PTR(NULL, part);
part = (esp_blockdev_handle_t)0xDEADBEEF;
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_blockdev_generic_partition_get(parent, SIZE_MAX, 64, &part));
TEST_ASSERT_EQUAL_PTR(NULL, part);
TEST_ESP_OK(parent->ops->release(parent));
}
void app_main(void)
{
unity_run_menu();
}
@@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.generic
@idf_parametrize('target', ['esp32', 'esp32c3', 'linux'], indirect=['target'])
def test_generic_partition(dut: Dut) -> None:
dut.run_all_single_board_cases()
@@ -0,0 +1 @@
CONFIG_UNITY_ENABLE_64BIT=y
@@ -0,0 +1,11 @@
# This is the project CMakeLists.txt file for the memory blockdev test application
cmake_minimum_required(VERSION 3.22)
set(EXTRA_COMPONENT_DIRS
"$ENV{IDF_PATH}/tools/test_apps/components"
"${CMAKE_CURRENT_LIST_DIR}/../../"
"${CMAKE_CURRENT_LIST_DIR}/../../../esp_blockdev")
set(COMPONENTS main)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(memory_blockdev_test)
@@ -0,0 +1,2 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 | Linux |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | --------- | ----- |
@@ -0,0 +1,2 @@
idf_component_register(SRCS "test_memory_blockdev.c"
PRIV_REQUIRES unity esp_blockdev esp_blockdev_util)
@@ -0,0 +1,191 @@
/*
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include "unity.h"
#include "unity_test_utils.h"
#include "esp_blockdev.h"
#include "esp_blockdev/memory.h"
#include "esp_heap_caps.h"
TEST_CASE("memory blockdev basic operations", "[memory_blockdev]")
{
uint8_t backing[128];
memset(backing, 0x5A, sizeof(backing));
const esp_blockdev_geometry_t geometry = {
.disk_size = sizeof(backing),
.read_size = 1,
.write_size = 1,
.erase_size = 1,
.recommended_write_size = 16,
.recommended_read_size = 16,
.recommended_erase_size = 32,
};
esp_blockdev_handle_t dev = NULL;
TEST_ESP_OK(esp_blockdev_memory_get_from_buffer(backing, sizeof(backing), &geometry, false, &dev));
TEST_ASSERT_NOT_NULL(dev);
TEST_ASSERT_EQUAL_UINT64(geometry.disk_size, dev->geometry.disk_size);
TEST_ASSERT_EQUAL_UINT32(geometry.read_size, dev->geometry.read_size);
TEST_ASSERT_EQUAL_UINT32(geometry.write_size, dev->geometry.write_size);
TEST_ASSERT_EQUAL_UINT32(geometry.erase_size, dev->geometry.erase_size);
TEST_ASSERT_FALSE(dev->device_flags.read_only);
TEST_ASSERT_FALSE(dev->device_flags.default_val_after_erase);
uint8_t write_data[32];
for (size_t i = 0; i < sizeof(write_data); ++i) {
write_data[i] = (uint8_t)(i + 1);
}
TEST_ESP_OK(dev->ops->write(dev, write_data, 48, sizeof(write_data)));
TEST_ASSERT_EQUAL_UINT8_ARRAY(write_data, backing + 48, sizeof(write_data));
uint8_t read_buf[sizeof(write_data)];
memset(read_buf, 0, sizeof(read_buf));
TEST_ESP_OK(dev->ops->read(dev, read_buf, sizeof(read_buf), 48, sizeof(read_buf)));
TEST_ASSERT_EQUAL_UINT8_ARRAY(write_data, read_buf, sizeof(write_data));
TEST_ESP_OK(dev->ops->erase(dev, 48, sizeof(write_data)));
TEST_ASSERT_EACH_EQUAL_UINT8(0x00, backing + 48, sizeof(write_data));
TEST_ESP_OK(dev->ops->sync(dev));
TEST_ESP_ERR(ESP_ERR_NOT_SUPPORTED, dev->ops->ioctl(dev, ESP_BLOCKDEV_CMD_USER_BASE, NULL));
TEST_ESP_OK(dev->ops->release(dev));
}
TEST_CASE("memory blockdev enforces read-only mappings", "[memory_blockdev]")
{
uint8_t backing[64];
for (size_t i = 0; i < sizeof(backing); ++i) {
backing[i] = (uint8_t)(i ^ 0xAA);
}
const esp_blockdev_geometry_t ro_geometry = {
.disk_size = sizeof(backing),
.read_size = 1,
.write_size = 0,
.erase_size = 0,
.recommended_write_size = 0,
.recommended_read_size = 8,
.recommended_erase_size = 0,
};
esp_blockdev_handle_t dev = NULL;
TEST_ESP_OK(esp_blockdev_memory_get_from_buffer(backing, sizeof(backing), &ro_geometry, true, &dev));
TEST_ASSERT_NOT_NULL(dev);
TEST_ASSERT_TRUE(dev->device_flags.read_only);
uint8_t read_buf[16];
TEST_ESP_OK(dev->ops->read(dev, read_buf, sizeof(read_buf), 0, sizeof(read_buf)));
TEST_ASSERT_EQUAL_UINT8_ARRAY(backing, read_buf, sizeof(read_buf));
TEST_ESP_ERR(ESP_ERR_INVALID_STATE, dev->ops->write(dev, read_buf, 0, sizeof(read_buf)));
TEST_ESP_ERR(ESP_ERR_INVALID_STATE, dev->ops->erase(dev, 0, sizeof(read_buf)));
TEST_ESP_OK(dev->ops->release(dev));
/* Release must not take ownership of external buffers */
const uint8_t expected_prefix[] = {0xAA, 0xAB, 0xA8, 0xA9};
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected_prefix, backing, sizeof(expected_prefix));
}
TEST_CASE("memory blockdev validates arguments", "[memory_blockdev]")
{
uint8_t backing[32];
memset(backing, 0x11, sizeof(backing));
esp_blockdev_geometry_t geometry = {
.disk_size = sizeof(backing),
.read_size = 1,
.write_size = 1,
.erase_size = 1,
.recommended_write_size = 0,
.recommended_read_size = 0,
.recommended_erase_size = 0,
};
esp_blockdev_handle_t dev = (esp_blockdev_handle_t)0xDEADBEEF;
TEST_ESP_ERR(ESP_ERR_INVALID_ARG, esp_blockdev_memory_get_from_buffer(NULL, sizeof(backing), &geometry, false, &dev));
TEST_ASSERT_EQUAL_PTR(NULL, dev);
dev = (esp_blockdev_handle_t)0xDEADBEEF;
TEST_ESP_ERR(ESP_ERR_INVALID_ARG, esp_blockdev_memory_get_from_buffer(backing, sizeof(backing), NULL, false, &dev));
TEST_ASSERT_EQUAL_PTR(NULL, dev);
geometry.disk_size = sizeof(backing) + 1;
dev = (esp_blockdev_handle_t)0xDEADBEEF;
TEST_ESP_ERR(ESP_ERR_INVALID_SIZE, esp_blockdev_memory_get_from_buffer(backing, sizeof(backing), &geometry, false, &dev));
TEST_ASSERT_EQUAL_PTR(NULL, dev);
geometry.disk_size = 0;
dev = (esp_blockdev_handle_t)0xDEADBEEF;
TEST_ESP_ERR(ESP_ERR_INVALID_ARG, esp_blockdev_memory_get_from_buffer(backing, sizeof(backing), &geometry, false, &dev));
TEST_ASSERT_EQUAL_PTR(NULL, dev);
geometry.disk_size = sizeof(backing);
esp_blockdev_handle_t heap_dev = NULL;
TEST_ESP_ERR(ESP_ERR_INVALID_ARG, esp_blockdev_memory_create_with_heap_caps(NULL, MALLOC_CAP_8BIT, &heap_dev));
TEST_ESP_ERR(ESP_ERR_INVALID_ARG, esp_blockdev_memory_create_with_heap_caps(&geometry, MALLOC_CAP_8BIT, NULL));
/* Successful create for subsequent bounds checks */
TEST_ESP_OK(esp_blockdev_memory_get_from_buffer(backing, sizeof(backing), &geometry, false, &dev));
TEST_ASSERT_NOT_NULL(dev);
uint8_t read_buf[16];
TEST_ESP_ERR(ESP_ERR_INVALID_SIZE, dev->ops->read(dev, read_buf, sizeof(read_buf) - 1, 0, sizeof(read_buf)));
TEST_ESP_ERR(ESP_ERR_INVALID_ARG, dev->ops->read(dev, read_buf, sizeof(read_buf), sizeof(backing) - 4, sizeof(read_buf)));
TEST_ESP_ERR(ESP_ERR_INVALID_ARG, dev->ops->write(dev, read_buf, sizeof(backing) - 8, sizeof(read_buf) + 1));
TEST_ESP_ERR(ESP_ERR_INVALID_ARG, dev->ops->erase(dev, sizeof(backing) - 4, sizeof(read_buf)));
TEST_ESP_OK(dev->ops->release(dev));
}
TEST_CASE("memory blockdev heap allocation creates zeroed storage", "[memory_blockdev]")
{
const esp_blockdev_geometry_t geometry = {
.disk_size = 64,
.read_size = 1,
.write_size = 1,
.erase_size = 1,
.recommended_write_size = 0,
.recommended_read_size = 0,
.recommended_erase_size = 0,
};
esp_blockdev_handle_t dev = NULL;
TEST_ESP_OK(esp_blockdev_memory_create_with_heap_caps(&geometry, MALLOC_CAP_8BIT, &dev));
TEST_ASSERT_NOT_NULL(dev);
TEST_ASSERT_FALSE(dev->device_flags.read_only);
TEST_ASSERT_FALSE(dev->device_flags.default_val_after_erase);
uint8_t read_buf[64];
TEST_ESP_OK(dev->ops->read(dev, read_buf, sizeof(read_buf), 0, sizeof(read_buf)));
TEST_ASSERT_EACH_EQUAL_UINT8(0x00, read_buf, sizeof(read_buf));
uint8_t pattern[8];
memset(pattern, 0xCD, sizeof(pattern));
TEST_ESP_OK(dev->ops->write(dev, pattern, 4, sizeof(pattern)));
TEST_ESP_OK(dev->ops->read(dev, read_buf, sizeof(pattern), 4, sizeof(pattern)));
TEST_ASSERT_EQUAL_UINT8_ARRAY(pattern, read_buf, sizeof(pattern));
TEST_ESP_OK(dev->ops->release(dev));
}
void app_main(void)
{
unity_run_menu();
}
@@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.generic
@idf_parametrize('target', ['esp32', 'esp32c3', 'linux'], indirect=['target'])
def test_blockdev_memory_device(dut: Dut) -> None:
dut.run_all_single_board_cases()
@@ -0,0 +1 @@
CONFIG_UNITY_ENABLE_64BIT=y