Block Device Layer

[中文]

Overview

The Block Device Layer (BDL) defines a C interface that lets storage-oriented components exchange data without custom-made adapters. Each block device exposes this interface in the form of an esp_blockdev_handle_t handle giving access to device flags, geometry information, and a set of supported operations as defined in components/esp_blockdev/include/esp_blockdev.h. Higher-level code inspects that metadata and may invoke the available callbacks to perform I/O operations.

The unified interface makes it possible to compose BDL stacks supporting storage use-cases by chaining multiple universal components. A driver provides a handle representing access to the physical device; a middleware component consumes this handle, augments behaviour (for example splits the space on the device or adds wear levelling capability), and exposes a new handle to the next layer. The topmost components of the chain like file-systems are pure device consumers. This model allows filesystems, middleware, and physical drivers to be mixed and matched as long as every layer honours the interface contracts described below.

Example Block Device Layer Stack

Using Block Devices

Handles

Block devices are accessed through esp_blockdev_handle_t. Handles are obtained from the owning component via the <component>_get_blockdev() convention and must be released with the matching <component>_release_blockdev() helper once the device is no longer needed. Treat handles as opaque: only use the public API in components/esp_blockdev/include/esp_blockdev.h, and do not move or modify the memory they reference.

Geometry and Flags

Each device publishes an esp_blockdev_geometry_t structure that reports capacity together with minimum read, write, and erase granularities. Optional recommended sizes act as performance hints but must not replace alignment checks against the mandatory values. The accompanying esp_blockdev_flags_t structure advertises properties such as read-only media, encryption, or erase-before-write requirements. Middleware can change apparent geometry size, but must verify upon creation that the underlying layer fits its requirements, and ensure that the underlying device will only be accessed correctly.

Operations

The esp_blockdev_ops_t structure defines callbacks for read, write, erase, sync, ioctl, and release. Before invoking a callback, callers must ensure that the given pointer is not NULL; a NULL pointer indicates that the operation is not supported. Callers are responsible for validating alignment and bounds using the geometry data and for respecting flag-driven requirements such as issuing an erase before writing to NAND-like media.

Typical Flow

  1. Acquire a handle from a driver or middleware provider.

  2. Inspect geometry and flags to determine required alignment, available capacity, and special handling.

  3. Issue read, write, erase, and sync requests through the operation table that the provider exposes.

  4. Forward the handle to higher components or release it once all operations complete.

Example

esp_blockdev_handle_t dev = my_component_get_blockdev();
const esp_blockdev_geometry_t *geometry = dev->geometry;
if (dev->ops->read && (sizeof(buffer) % geometry->read_size) == 0) {
    ESP_ERROR_CHECK(dev->ops->read(dev, buffer, sizeof(buffer), 0, sizeof(buffer)));
}
if (dev->ops->release) {
    ESP_ERROR_CHECK(dev->ops->release(dev));
}

Contracts

Flags (esp_blockdev_flags_t)

  • Flags are initialised once during device creation and must remain immutable for the lifetime of the handle.

  • read_only requires write, erase, and mutating ioctl commands to fail with an error such as ESP_ERR_INVALID_STATE.

  • encrypted signals that on-media data is encrypted; higher layers must not assume plaintext visibility or transparent mapping.

  • erase_before_write tells callers that a successful write requires an erase of the target range beforehand. If multiple write operations are issued to the same range without an erase operation in-between, the behavior is undefined, but will likely result in data corruption.

  • and_type_write signals NAND/NOR-style behavior: programming only clears bits (1→0) and effectively stores existing_bits & new_bits. Bits that are already zero remain zero even if the write request supplies ones; erasing first is the only way to restore them.

  • default_val_after_erase identifies whether erased regions read as 0x00 or 0xFF so middleware can keep sentinel values consistent.

Geometry (esp_blockdev_geometry_t)

  • disk_size is the total accessible capacity in bytes; operations must reject any request whose end offset exceeds this value.

  • read_size, write_size, and erase_size are mandatory alignment units, in bytes; both offsets and lengths must align to the corresponding size before the operation runs.

  • Recommended sizes improve throughput when callers honour them but cannot replace the minimum alignment checks; implementations must accept any request that respects the required granularity.

  • When a user sees read-write and read-only variants of the same underlying device, the geometry must be identical aside from the read_only flag. In particular, read_size, write_size, and erase_size should match between the two variants; recommended_* values are expected to match and should differ only when there is a clear benefit to doing so (and should be documented in such cases).

Operations (esp_blockdev_ops_t)

read(dev_handle, dst_buf, dst_buf_size, src_addr, data_read_len)
  • Behaviour: Upon success copies exactly data_read_len bytes into dst_buf.

  • Preconditions: dst_buf is valid, src_addr and data_read_len are aligned to read_size, src_addr + data_read_len <= disk_size, and data_read_len <= dst_buf_size.

  • Postcondition: Returns ESP_OK when the copy succeeds or propagates a relevant ESP_ERR_* code on failure.

write(dev_handle, src_buf, dst_addr, data_write_len)
  • Preconditions: Device is not read_only; src_buf spans at least data_write_len bytes; offset and length align to write_size and stay within disk_size.

  • Behaviour: When erase_before_write is set, callers must issue an erase beforehand. When and_type_write is set, the hardware applies a bitwise AND with the existing contents, so the stored result becomes old_value & new_value; bits cleared by earlier writes stay cleared unless the range is erased first.

  • Postcondition: Returns ESP_OK once the requested range has been accepted by the device (data may still reside in intermediate buffers until sync runs). Misaligned, out-of-range, or read-only attempts should surface ESP_ERR_INVALID_ARG or ESP_ERR_INVALID_STATE, and implementations must avoid leaving the range partially updated.

  • Note: On devices with and_type_write writes depend on the existing contents, so reading the same (or overlapping) range immediately after a write may require a preceding sync to ensure that cached data reflects the fully merged value.

erase(dev_handle, start_addr, erase_len)
  • Preconditions: Device permits erases; start_addr and erase_len align to erase_size; the range remains inside disk_size.

  • Postcondition: The range reads back as default_val_after_erase on success. Misaligned or out-of-range requests should return ESP_ERR_INVALID_ARG; hardware failures are expected to bubble up using driver-specific ESP_ERR_* codes.

sync(dev_handle)
  • Flushes pending writes. Devices that omit this callback operate with write-through semantics.

  • Postcondition: All previously reported writes reach stable storage before ESP_OK is returned (this includes all underlying devices). Timeouts or transport issues should surface as ESP_ERR_TIMEOUT or another relevant ESP_ERR_*.

ioctl(dev_handle, cmd, args)
  • Command identifiers 0x00–0x7F are reserved for ESP-IDF system use; 0x80–0xFF are available for user-defined extensions.

  • Each command defines its own payload layout; because args is a void *, wrappers can only validate or reinterpret the buffer for commands they understand, and must otherwise treat the payload as opaque.

  • Wrappers that cannot service a command should forward it unchanged to the next device in the stack when available; only the bottom device is expected to return ESP_ERR_NOT_SUPPORTED for unrecognised commands.

  • When the stack contains non-transparent address mapping, forwarding commands that embed raw addresses is inherently unsafe: intermediate layers cannot translate opaque payloads, so behaviour is undefined and will typically fail. Such commands should either be blocked explicitly or documented as unsupported in stacked configurations.

release(dev_handle)
  • Optional destructor that frees device resources. The function must be idempotent so that repeated calls either succeed or return a benign error like ESP_ERR_INVALID_STATE.

Error Handling

Callbacks return ESP_OK on success and should propagate ESP_ERR_* codes unchanged to help callers diagnose failures. Middleware and applications are expected to propagate errors from underlying devices rather than masking them inside the stack. NULL function pointers should be treated as "operation not supported".

Validation

Implementations should include tests that cover alignment checks, flag-driven behaviour (read-only, erase-before-write, NAND-style writes), and correct propagation of errors through stacked devices. Middleware that wraps lower handles must also verify that handle lifetime management remains consistent across the stack.

API Reference

Header File

  • components/esp_blockdev/include/esp_blockdev.h

  • This header file can be included with:

    #include "esp_blockdev.h"
    
  • This header file is a part of the API provided by the esp_blockdev component. To declare that your component depends on esp_blockdev, add the following to your CMakeLists.txt:

    REQUIRES esp_blockdev
    

    or

    PRIV_REQUIRES esp_blockdev
    

Structures

struct esp_blockdev_cmd_arg_erase

Arguments for block device erase control commands.

Supplied to ESP_BLOCKDEV_CMD_ERASE_CONTENTS and similar ioctls to describe the erase window.

Public Members

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

size_t erase_len

IN - size of the area to erase/trim/discard/sanitize (in bytes), must be a multiple of erase block size

struct esp_blockdev_flags

Block device property flags.

Various properties and characteristics of given BDL device.

Note

Convenience macros ESP_BLOCKDEV_FLAGS_<INST>_CONFIG_DEFAULT() provide the most common setup of usual ESP-IDF component equipped with BDL interface. They can be used as a starting point for own initializers.

Public Members

uint32_t read_only

no erase/write operations allowed

uint32_t encrypted

the device data is encrypted

uint32_t erase_before_write

erasing required before any write operation

uint32_t and_type_write

0-bits can't be changed to 1-bits - NAND/NOR flash behavior

uint32_t default_val_after_erase

default bit value after erasing (0 or 1)

uint32_t reserved

Reserved for future blockdev flags

uint32_t val

Raw bitfield view for bulk initialization

struct esp_blockdev_geometry

Block device geometry.

Various block size parameters needed for proper R/W/E processing on given device.

Public Members

uint64_t disk_size

Size of the device disk space (in bytes). Mandatory parameter.

size_t read_size

Minimum block size (in bytes) for disk-read operations on given device. Mandatory parameter.

size_t write_size

Minimum block size (in bytes) for disk-write operations on given device. Mandatory parameter for all R/W devices, 0 for R/O.

size_t erase_size

Minimum block size (in bytes) for erase operations on given device. Mandatory parameter for all R/W devices, 0 for R/O.

Note

Typically used as a sector size in file system meaning.

size_t recommended_write_size

Default write block size (in bytes) of given device. Recommended for optimal performance. 0 means not used.

size_t recommended_read_size

Default read block size (in bytes) of given device. Recommended for optimal performance. 0 means not used.

size_t recommended_erase_size

Default erase block size (in bytes) of given device. Recommended for optimal performance. 0 means not used.

Note

Typically used as target hardware erase block size.

struct esp_blockdev_ops

Block device operations.

Various block operations needed for proper R/W/E processing on given device.

Public Members

esp_err_t (*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)

READ operation: Read required number of bytes from the device at given offset, store the data into the output buffer.

Param dev_handle:

Target device handle

Param dst_buf:

Output buffer to receive the data read

Param dst_buf_size:

Size of the destination buffer (in bytes)

Param src_addr:

Data read source address (offset in given device address space)

Param data_read_len:

Data read length (in bytes)

esp_err_t (*write)(esp_blockdev_handle_t dev_handle, const uint8_t *src_buf, uint64_t dst_addr, size_t data_write_len)

WRITE operation: Write required number of bytes taken from the input memory buffer to the device at given offset.

Param dev_handle:

Target device handle

Param src_buf:

Input buffer providing the data to write

Param dst_addr:

Data destination address (offset in given device address space)

Param data_write_len:

Length of the data to be written (in bytes)

esp_err_t (*erase)(esp_blockdev_handle_t dev_handle, uint64_t start_addr, size_t erase_len)

ERASE operation: Erase given address range <start_addr, start_addr + erase_len>. Both erase_len and start_addr must be aligned to multiples of the erase block size (in bytes) given by 'erase_size' member. The erasing process can be secured by one or more operations, depending on given BDL device nature. The only expected result is safely wiped data within the required range.

Param dev_handle:

Target device handle

Param start_addr:

Start address for the erase operation

Param erase_len:

Length of the address space chunk to be erased (in bytes)

esp_err_t (*sync)(esp_blockdev_handle_t dev_handle)

SYNC operation: Commit all the pending write operations and block until all are finished. Does nothing if the device doesn't support write operations caching

Param dev_handle:

Target device handle

esp_err_t (*ioctl)(esp_blockdev_handle_t dev_handle, const uint8_t cmd, void *args)

IOCTL operation: I/O control commands. Each command has corresponding in/out parameters which are to be verified within given ioctl handler. The commands can be extended by the device implementation within the following ranges:

  • 0x00-0x7F: IDF device commands (system)

  • 0x80-0xFF: user-defined commands See ESP_BLOCKDEV_CMD* macros.

Param dev_handle:

Target device handle

Param cmd:

Command ID

Param args:

Command specific arguments

esp_err_t (*release)(esp_blockdev_handle_t dev_handle)

Instance destructor: Cleanup code for the specific device handle

Param dev_handle:

Target device handle

struct esp_blockdev

IDF generic Block device interface structure This structure defines an interface for a generic block-device capable of common disk operations and providing properties important for the device's deployment into the target component stack.

Note

The device context pointer is used to store the device-specific context. It is not used by the BDL layer and is intended to be used by the device implementation.

Note

The ops pointer holds the address of the device operations structure which can be shared by multiple device instances of the same type (driver).

Public Members

void *ctx

Owner-provided private data

esp_blockdev_flags_t device_flags

Capabilities and requirements

esp_blockdev_geometry_t geometry

Operation granularity and capacity

const esp_blockdev_ops_t *ops

Function table implementing the device

Macros

ESP_BLOCKDEV_CMD_SYSTEM_BASE

Block device I/O control commands.

The commands are grouped into two categories: system and user. The system commands are used for internal device management and are not expected to be used by the user (values 0-127). The user commands are used for user-specific operations and are expected to be defined by the user (values 128-255). System commands base value

ESP_BLOCKDEV_CMD_USER_BASE

User commands base value

ESP_BLOCKDEV_CMD_MARK_DELETED

mark given range as invalid, data to be deleted/overwritten eventually [ esp_blockdev_cmd_arg_erase_t* ]

ESP_BLOCKDEV_CMD_ERASE_CONTENTS

erase required range and set the contents to the device's default bit values [ esp_blockdev_cmd_arg_erase_t* ]

ESP_BLOCKDEV_FLAGS_CONFIG_DEFAULT()
ESP_BLOCKDEV_FLAGS_INST_CONFIG_DEFAULT(flags)
ESP_BLOCKDEV_HANDLE_INVALID

Type Definitions

typedef struct esp_blockdev_cmd_arg_erase esp_blockdev_cmd_arg_erase_t

Arguments for block device erase control commands.

Supplied to ESP_BLOCKDEV_CMD_ERASE_CONTENTS and similar ioctls to describe the erase window.

typedef struct esp_blockdev_flags esp_blockdev_flags_t

Block device property flags.

Various properties and characteristics of given BDL device.

Note

Convenience macros ESP_BLOCKDEV_FLAGS_<INST>_CONFIG_DEFAULT() provide the most common setup of usual ESP-IDF component equipped with BDL interface. They can be used as a starting point for own initializers.

typedef struct esp_blockdev_geometry esp_blockdev_geometry_t

Block device geometry.

Various block size parameters needed for proper R/W/E processing on given device.

typedef struct esp_blockdev esp_blockdev_t
typedef esp_blockdev_t *esp_blockdev_handle_t

Standard BDL access handle, the only public BDL instance identifier. Allocated and initialized by the device's class factory, optionally closed by the device release handler.

typedef struct esp_blockdev_ops esp_blockdev_ops_t

Block device operations.

Various block operations needed for proper R/W/E processing on given device.


Was this page helpful?