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_t

Input arguments for erase-related block-device ioctl commands.

Pass a pointer to this structure as the args argument to esp_blockdev_ops_t::ioctl when using ESP_BLOCKDEV_CMD_MARK_DELETED or ESP_BLOCKDEV_CMD_ERASE_CONTENTS. Both start_addr and erase_len must be aligned to esp_blockdev_geometry_t::erase_size for the target device.

Public Members

uint64_t start_addr

Byte offset of the range from the start of the device.

Must be a multiple of esp_blockdev_geometry_t::erase_size.

size_t erase_len

Length of the range in bytes.

Must be a multiple of esp_blockdev_geometry_t::erase_size.

struct esp_blockdev_flags_t

Block device property flags (esp_blockdev_flags_t)

Various properties and characteristics of a BDL device. Represented as a struct when __DOXYGEN__ is defined (for documentation only) and as a union in normal builds.

Note

The macros ESP_BLOCKDEV_FLAGS_CONFIG_DEFAULT and ESP_BLOCKDEV_FLAGS_INST_CONFIG_DEFAULT provide a typical configuration for ESP-IDF components that expose a BDL interface. Use them as a starting point for custom initializers.

Public Members

bool read_only

no erase/write operations allowed

bool encrypted

the device data is encrypted

bool erase_before_write

erasing required before any write operation

bool and_type_write

0-bits cannot be changed to 1-bits (NAND/NOR flash behavior)

bool default_val_after_erase

default bit value after erasing (0 or 1)

bool reserved[27]

Reserved for future blockdev flags

uint32_t val

Raw bitfield view for bulk initialization

struct esp_blockdev_geometry_t

Block device geometry.

Granularity and capacity parameters for read, write, and erase operations on the device.

Public Members

uint64_t disk_size

Total addressable size of the device in bytes.

Mandatory for every block device.

size_t read_size

Minimum read transaction size in bytes.

Mandatory. All read lengths and offsets must respect this granularity where required by the driver.

size_t write_size

Minimum write transaction size in bytes.

Mandatory for read/write devices; set to 0 for read-only devices.

size_t erase_size

Minimum erase granularity in bytes.

Mandatory for read/write devices; set to 0 for read-only devices.

Note

Often corresponds to the logical sector size used by file systems.

size_t recommended_write_size

Suggested write chunk size in bytes for best performance.

Set to 0 if not used. Optional hint for upper layers.

size_t recommended_read_size

Suggested read chunk size in bytes for best performance.

Set to 0 if not used. Optional hint for upper layers.

size_t recommended_erase_size

Suggested erase chunk size in bytes for best performance.

Set to 0 if not used. Optional hint for upper layers.

Note

Often matches the physical erase block of the underlying flash or similar media.

struct esp_blockdev_ops_t

Block device operations.

Function table for read, write, erase, sync, ioctl, and release on a block device instance.

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 data from the device.

Reads the requested number of bytes from the device at the given offset into the output buffer.

Parameters

  • dev_handle Target device handle

  • dst_buf Output buffer to receive the data read

  • dst_buf_size Size of the destination buffer (in bytes)

  • src_addr Source address for the read (offset in the device's address space)

  • data_read_len Data read length (in bytes)

Return value

  • ESP_OK on success

  • Another esp_err_t error code on failure

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 data to the device.

Writes the requested number of bytes from the input buffer to the device at the given offset.

Parameters

  • dev_handle Target device handle

  • src_buf Input buffer providing the data to write

  • dst_addr Destination address for the write (offset in the device's address space)

  • data_write_len Length of the data to be written (in bytes)

Return value

  • ESP_OK on success

  • Another esp_err_t error code on failure

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

Erase an address range on the device.

Erases the 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 (erase_size in esp_blockdev_geometry_t). The implementation may use one or more internal operations, depending on the device. The result must be that data in the range is no longer readable as previously written content.

Parameters

  • dev_handle Target device handle

  • start_addr Start address for the erase operation

  • erase_len Length of the address range to erase (in bytes)

Return value

  • ESP_OK on success

  • Another esp_err_t error code on failure

esp_err_t (*sync)(esp_blockdev_handle_t dev_handle)

Commit pending writes to the device.

Commits all pending writes and blocks until they complete. Does nothing if the device does not cache writes.

Parameters

  • dev_handle Target device handle

Return value

  • ESP_OK on success

  • Another esp_err_t error code on failure

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

Device-specific I/O control.

Each command has parameters that the handler must validate. Implementations may extend commands within these ranges:

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

  • 0x80-0xFF: user-defined commands

Refer to the ESP_BLOCKDEV_CMD_ macros in this header.

Parameters

  • dev_handle Target device handle

  • cmd Command ID

  • args Command-specific arguments

Return value

  • ESP_OK on success

  • Another esp_err_t error code on failure

esp_err_t (*release)(esp_blockdev_handle_t dev_handle)

Release resources for this device handle.

Parameters

  • dev_handle Target device handle

Return value

  • ESP_OK on success

  • Another esp_err_t error code on failure

struct esp_blockdev

Generic IDF block device interface.

Describes a block device that supports common disk operations and exposes parameters needed to integrate it into a component stack.

Note

The context pointer ctx holds driver-private data; the BDL layer does not interpret it.

Note

The ops table may be shared by multiple instances of the same driver type.

Public Members

void *ctx

Owner-provided context; not interpreted by the BDL layer

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_FLAGS_CONFIG_DEFAULT()

Brace-enclosed initializer for esp_blockdev_flags_t with typical ESP-IDF defaults.

ESP_BLOCKDEV_FLAGS_INST_CONFIG_DEFAULT(flags)

Assign typical ESP-IDF default values to an existing esp_blockdev_flags_t.

Parameters:
  • flags -- Flags instance to initialize (statement form, not an expression)

ESP_BLOCKDEV_HANDLE_INVALID

Sentinel for an invalid block device handle.

Compare handles to NULL; this macro expands to NULL for readability.

Type Definitions

typedef struct esp_blockdev esp_blockdev_t

Forward declaration; see esp_blockdev

typedef esp_blockdev_t *esp_blockdev_handle_t

Opaque handle to a block device instance.

Pointer to esp_blockdev. This is the only public identifier for a BDL instance: it is returned from the device factory and must be released through esp_blockdev_ops_t::release when no longer needed.


Was this page helpful?