# 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.

## Ioctl Command Management

The ioctl command space (`0x00`–`0xFF`) is shared across all components in an ESP-IDF project. To prevent silent collisions at integration time, the Block Device Layer provides a build-time overlap checker that validates command reservations across the entire component tree.

### Command Ranges

Range | Base macro | Purpose
--- | --- | ---
`0x00–0x7F` | `ESP_BLOCKDEV_CMD_SYSTEM_BASE` | ESP-IDF system commands
`0x80–0xFF` | `ESP_BLOCKDEV_CMD_USER_BASE` | User / component extensions

### Reservation Macros

Components declare their ioctl command allocations using the following macros (defined in `esp_blockdev.h`):

    // Reserve an inclusive range of command values
    ESP_BLOCKDEV_RESERVE_CMD_RANGE(component, start, end);

    // Reserve exactly one command value (shorthand for start == end)
    ESP_BLOCKDEV_RESERVE_CMD(component, start);

Both `start` and `end` are inclusive and must satisfy:

- `0 <= start <= end <= 255`

#### Example

    #include "esp_blockdev.h"

    /* Reserve system commands 0x0A..0x14 for nand_flash */
    ESP_BLOCKDEV_RESERVE_CMD_RANGE(nand_flash,
        ESP_BLOCKDEV_CMD_SYSTEM_BASE + 10,
        ESP_BLOCKDEV_CMD_SYSTEM_BASE + 20);

    /* Reserve a single user command for sdcard */
    ESP_BLOCKDEV_RESERVE_CMD(sdcard, ESP_BLOCKDEV_CMD_USER_BASE + 1);

### Registering Definition Files

Each component that owns ioctl definitions must register the file(s) containing reservation macros with the build system. This is done in the component's `CMakeLists.txt`:

    idf_build_set_property(
        ESP_BLOCKDEV_IOCTL_DEF_FILES
        "${CMAKE_CURRENT_LIST_DIR}/include/my_ioctl_defs.h"
        APPEND
    )

The `esp_blockdev` component itself registers its own header, so core commands (`ESP_BLOCKDEV_CMD_MARK_DELETED`, `ESP_BLOCKDEV_CMD_ERASE_CONTENTS`) are always checked.

When an overlap or invalid range is detected, the build fails with a diagnostic showing the conflicting component names, hex ranges, and source file paths.

Note

Adjacent ranges are valid. For example `0x10..0x1F` and `0x20..0x2F` do not overlap.

## 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_RESERVE_CMD_RANGE(component, start, end)

  Register a reserved ioctl command value or inclusive range for overlap checking.

  These macros are machine-readable markers for the build-time checker. They do not affect normal builds unless `ESP_BLOCKDEV_CHECK_CMD_OVERLAP` is defined.

  Register reservation declarations in headers or other files that are appended to `ESP_BLOCKDEV_IOCTL_DEF_FILES`.

      ESP_BLOCKDEV_RESERVE_CMD_RANGE(nand_flash, ESP_BLOCKDEV_CMD_SYSTEM_BASE + 10, ESP_BLOCKDEV_CMD_SYSTEM_BASE + 20);
      ESP_BLOCKDEV_RESERVE_CMD(sdcard, ESP_BLOCKDEV_CMD_USER_BASE + 1);

  Reserve an inclusive range of ioctl command values for build-time overlap checking

  Parameters:

    - component -- Unquoted identifier naming the owning component

    - start -- First command value in the range (0x00–0xFF)

    - end -- Last command value in the range (0x00–0xFF, >= start)

ESP_BLOCKDEV_RESERVE_CMD(component, start)

  Reserve a single ioctl command value for build-time overlap checking.

  Equivalent to `ESP_BLOCKDEV_RESERVE_CMD_RANGE(component, start, start)`.

  Parameters:

    - component -- Unquoted identifier naming the owning component

    - start -- The command value to reserve (0x00–0xFF)

ESP_BLOCKDEV_FLAGS_CONFIG_DEFAULT()

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

  See also

  ESP_BLOCKDEV_FLAGS_INST_CONFIG_DEFAULT

ESP_BLOCKDEV_FLAGS_INST_CONFIG_DEFAULT(flags)

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

  See also

  ESP_BLOCKDEV_FLAGS_CONFIG_DEFAULT

  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?

- Thank you! We received your feedback.
- If you have any comments, fill in Espressif Documentation Feedback Form.

- We value your feedback.
- Let us know how we can improve this page by filling in Espressif Documentation Feedback Form.