Chapter 173: eFuse Programming and Management
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the concept of eFuses and their one-time programmable (OTP) nature.
- Describe the general structure and organization of eFuses in ESP32 variants.
- Distinguish between system-controlled eFuses and user-programmable eFuse blocks.
- Read various eFuse values, such as MAC address and chip information, using ESP-IDF APIs.
- Understand the process and extreme risks associated with programming (burning) eFuses.
- Utilize the
espefuse.py
command-line tool for inspecting and managing eFuses. - Identify common eFuse fields and their significance for device configuration and security.
- Appreciate the differences in eFuse layouts and capabilities across various ESP32 variants.
- Recognize common pitfalls and troubleshoot issues related to eFuse management.
Introduction
In the world of microcontrollers, there’s often a need to store small amounts of critical data permanently, in a way that cannot be easily altered after the device leaves the factory or is deployed in the field. This data might include unique device identifiers, hardware configuration settings, calibration data, or crucial security parameters like encryption keys. For this purpose, ESP32 microcontrollers incorporate eFuses (electronic fuses).
eFuses are a form of non-volatile, One-Time Programmable (OTP) memory. Once an eFuse bit is programmed (often called “burned”), it permanently changes its state and typically cannot be reverted. This makes eFuses an ideal choice for storing data that should be immutable throughout the device’s lifecycle.
This chapter delves into the eFuse system of ESP32 variants. We will explore their architecture, how to read them, and, with extreme caution, how they can be programmed. Understanding eFuses is vital for customizing device behavior at a fundamental level, implementing robust security features, and managing device identity. We will primarily use the esp_efuse.h
API and the espefuse.py
utility provided by ESP-IDF.
Theory
What are eFuses?
Think of an eFuse as a microscopic electronic equivalent of a physical fuse. A physical fuse, once blown by an overcurrent, breaks a circuit permanently. Similarly, an eFuse bit, once programmed, changes its electrical characteristics to represent a specific state (typically changing from a ‘0’ to a ‘1’). This change is irreversible.
eFuses provide a highly secure and tamper-resistant way to store data because they don’t rely on charge storage like Flash memory (which can degrade or be erased) but on a physical alteration of the silicon.
How eFuses Work (Simplified Conceptual Model)
Internally, an eFuse bit might consist of a narrow polysilicon or metal link. To program it, a higher-than-normal voltage or current is precisely applied across this link. This causes electromigration or other physical phenomena that effectively “blow” or open the link (or create a short, depending on the specific technology), changing its resistance dramatically. The microcontroller’s eFuse controller can then read this resistance to determine if the bit is in its default state (e.g., ‘0’) or programmed state (e.g., ‘1’).
eFuse Structure in ESP32 Variants
ESP32 eFuses are organized into several blocks, with each block containing a number of bits. The total number of eFuse bits and the layout of these blocks vary between ESP32 chip variants (see Variant Notes).
%%{init: {'theme':'base', 'themeVariables': {'primaryColor': '#DBEAFE', 'primaryTextColor': '#1E40AF', 'primaryBorderColor': '#2563EB'}}}%% graph TD subgraph "eFuse Memory Map (Typical)" A[EFUSE_BLK0] --> B(System Data); C[EFUSE_BLK1] --> D(Key Block); E[EFUSE_BLK2] --> F(Key / User Block); G[EFUSE_BLK3] --> H(User Data Block); end subgraph "Block Details" B -- "MAC Address, Chip Version,<br><b>Security Config (!!!)</b>" --> B_Detail["<b>System Controlled</b><br><i>Generally do not write manually!</i>"]; D -- "Flash Encryption Key,<br>Secure Boot Key" --> D_Detail["<b>Purpose-Specific</b><br>Often has write/read protection.<br>Managed by security features."]; F -- "Varies by chip.<br>Can hold keys or user data." --> F_Detail["<b>Purpose-Specific or User</b><br>Check TRM."]; H -- "Custom Serial Numbers,<br>Config Flags, Calibration Data" --> H_Detail["<b>User Programmable</b><br>Safest block for application data."]; end %% Styling classDef sysBlock fill:#FEE2E2,stroke:#DC2626,stroke-width:2px,color:#991B1B; classDef keyBlock fill:#FEF3C7,stroke:#D97706,stroke-width:2px,color:#92400E; classDef userBlock fill:#DBEAFE,stroke:#2563EB,stroke-width:2px,color:#1E40AF; classDef detailNode fill:#F3F4F6,stroke:#6B7280,stroke-width:1px,color:#374151; class A,B,B_Detail sysBlock; class C,D,D_Detail,E,F,F_Detail keyBlock; class G,H,H_Detail userBlock;
These blocks are typically categorized:
- System-Controlled eFuses (Often
EFUSE_BLK0
and others):- These eFuses store critical system parameters, many of which are programmed during chip manufacturing or by specific ESP-IDF operations related to security features.
- Examples include:
- MAC Address (Ethernet and Wi-Fi)
- Chip Version / Package Type
- Flash Encryption Keys (
FE_KEY
) - Secure Boot Keys (
SB_KEY
) - Security control bits (e.g.,
FLASH_CRYPT_CNT
,SECURE_BOOT_EN
,DISABLE_DL_CACHE
) - Voltage and timing calibration data.
- Warning: Manually programming these eFuses incorrectly can permanently damage the chip, disable core functionalities, or lock you out of your device. This is generally handled by
espefuse.py
or specific ESP-IDF functions for features like secure boot enablement.
- User-Programmable eFuses (Often
EFUSE_BLK1
,EFUSE_BLK2
,EFUSE_BLK3
):- ESP32 variants usually provide one or more blocks of eFuses specifically for application use.
EFUSE_BLK3
is commonly available for user data on many variants, offering a certain number of bits (e.g., 256 bits). - These can be used to store application-specific serial numbers, configuration flags, calibration constants, or small amounts of unique device data.
- ESP32 variants usually provide one or more blocks of eFuses specifically for application use.
- Key Blocks: Some eFuse blocks are dedicated to storing cryptographic keys. These often have special read/write protection mechanisms. For instance, a key might be writable but become unreadable by software after a specific eFuse bit is burned, making it usable only by the hardware crypto accelerators.
Read and Write Protection:
Many eFuse fields or blocks have associated write-protection bits. Once a write-protection bit for a field is burned, that field can no longer be modified. Some sensitive fields (especially key storage) might also have read-protection bits that, once burned, prevent software from reading the key, though hardware can still use it.
Key eFuse Fields and Their Purpose (General Examples)
The exact names and availability of eFuse fields are defined in CSV files within ESP-IDF (e.g., esp_efuse_table.csv
) and corresponding generated header files (esp_efuse_fields.h
). Common fields include:
WR_DIS
(Write Disable): A prefix for bits that, when burned, write-protect other eFuse fields or entire blocks. E.g.,WR_DIS.FIELD_NAME
.RD_DIS
(Read Disable): A prefix for bits that, when burned, read-protect other eFuse fields.- MAC Addresses:
MAC
orMAC_FACTORY
: The primary factory-programmed MAC address.- Some chips might have fields for custom MAC addresses.
- Chip Identity & Versioning:
CHIP_VER_PKG
orPKG_VERSION
: Indicates chip package type.CHIP_CPU_FREQ_RATED
: Rated CPU frequency.WAFER_VERSION_MINOR
,WAFER_VERSION_MAJOR
.
- Security Configuration:
FLASH_CRYPT_CNT
: Controls flash encryption. Burning bits here enables encryption and prevents decryption with old keys.SECURE_BOOT_EN
: Enables secure boot, ensuring only signed firmware can run.DISABLE_DL_ENCRYPT
,DISABLE_DL_DECRYPT
,DISABLE_DL_CACHE
: Disable JTAG, flash encryption during download mode, etc.JTAG_DISABLE
: Permanently disables JTAG.
- User Data Blocks:
- Often named
USER_DATA
,BLK1_DATA
,BLK2_DATA
,BLK3_DATA
or accessed via fields likeUSR_DATA_BLK3
. These are typically general-purpose for application use.
- Often named
CODING_SCHEME
:- This eFuse determines the error correction scheme used for other eFuses.
0 (None)
: No error correction. 1 logical bit = 1 physical bit.1 (3/4)
: Reed-Solomon 3/4 coding. More robust but uses more physical bits per logical bit.2 (Repeat)
: Simple repetition coding.
- It’s crucial because it affects how many physical bits are burned when you write logical data and how data is read. The
esp_efuse
API generally handles this abstraction.
- This eFuse determines the error correction scheme used for other eFuses.
Field Name / Prefix | Purpose | Typical Location | Risk Level |
---|---|---|---|
MAC_FACTORY |
Stores the factory-programmed Wi-Fi/Ethernet MAC address. | System (BLK0) | Read-Only |
CHIP_VER_PKG |
Indicates chip revision and package type (e.g., QFN, BGA). | System (BLK0) | Read-Only |
USER_DATA_BLK3 |
A general-purpose block (e.g., 256 bits) for application data. | User (BLK3) | Low (if used correctly) |
FLASH_CRYPT_CNT |
Enables Flash Encryption. Burning bits is irreversible and part of a critical security workflow. | System (BLK0) | EXTREME |
SECURE_BOOT_EN |
Enables Secure Boot, ensuring only signed firmware can execute. | System (BLK0) | EXTREME |
JTAG_DISABLE |
Permanently disables the JTAG debugging interface. | System (BLK0) | High |
WR_DIS.* |
Write-protects another field or block. E.g., WR_DIS.USER_DATA_BLK3 . |
System/User Blocks | High (action is permanent) |
RD_DIS.* |
Read-protects a key block, making it unreadable by software but usable by hardware crypto accelerators. | Key Blocks | EXTREME |
ESP-IDF eFuse API (esp_efuse.h
)
ESP-IDF provides the esp_efuse.h
component to interact with eFuses. Key functions include:
- Reading:
esp_efuse_read_field_blob(const esp_efuse_desc_t* field[], void* dst, size_t dst_size_bits)
: Reads an arbitrary number of bits (a “blob”) from one or more eFuse field descriptors.esp_efuse_read_field_cnt(const esp_efuse_desc_t* field[], int* dst)
: Reads the count of set bits in a field.esp_efuse_get_field_size(const esp_efuse_desc_t* field[])
: Gets the total bit size of a field.- Convenience functions like
esp_efuse_mac_get_default(uint8_t* mac)
oresp_efuse_get_chip_info(...)
(these might be in other headers but use eFuses).
- Writing (Use with EXTREME CAUTION):
esp_efuse_write_field_blob(const esp_efuse_desc_t* field[], const void* src, size_t src_size_bits)
: Writes a blob of data to an eFuse field.esp_efuse_write_field_cnt(const esp_efuse_desc_t* field[], int cnt)
: Writes a specific count of bits to a field (burnscnt
bits from LSB upwards).esp_efuse_batch_write_begin()
,esp_efuse_batch_write_commit()
,esp_efuse_batch_write_cancel()
: For batching multiple eFuse writes into a single physical programming operation. This is the recommended way to write eFuses to minimize stress on the eFuse programming circuitry.
- eFuse Descriptors (
esp_efuse_desc_t
):- The API uses an array of
esp_efuse_desc_t
structures to define the eFuse field(s) you want to access. These descriptors specify the eFuse block, bit offset within the block, and bit count for the field. - Predefined descriptors for common fields are available in generated headers (e.g.,
esp_efuse_fields.h
, which is generated fromesp_efuse_table.csv
).
- The API uses an array of
Warning: Programming eFuses is an irreversible operation. Incorrectly burning eFuses, especially system eFuses, can lead to:
- Bricking the device: Making it permanently unusable.
- Permanent feature disablement: Such as disabling JTAG or Wi-Fi.
- Security vulnerabilities or lockouts: If security-related eFuses are misconfigured.
Always test eFuse programming on development hardware you can afford to lose. Never experiment with eFuse programming on production devices unless you are absolutely certain of the operation and its consequences.
espefuse.py
Command-Line Tool
ESP-IDF includes a powerful Python utility, espefuse.py
, for offline eFuse management. It’s generally safer for initial setup or bulk programming than in-application programming.
Common espefuse.py
commands:
espefuse.py --port /dev/ttyUSB0 summary
: Displays a summary of all eFuse values, their names, and descriptions. This is the safest first command to run.espefuse.py --port /dev/ttyUSB0 dump
: Dumps raw eFuse block values.espefuse.py --port /dev/ttyUSB0 burn_efuse <FIELD_NAME> <value>
: Burns a specific value to a named eFuse field. EXTREMELY DANGEROUS IF MISUSED.espefuse.py --port /dev/ttyUSB0 burn_key <BLOCK> <KEYFILE.bin> <KEY_PURPOSE>
: Burns a cryptographic key into a key block.espefuse.py --port /dev/ttyUSB0 read_protect_efuse <FIELD_NAME>
: Burns the read-protection bit for a field.espefuse.py --port /dev/ttyUSB0 write_protect_efuse <FIELD_NAME>
: Burns the write-protection bit for a field.
Command | Purpose | Danger Level |
---|---|---|
summary |
Displays a full summary of all eFuse fields, values, and protection status. Always run this first. | Safe (Read-Only) |
dump |
Dumps the raw values of eFuse blocks in hexadecimal format. | Safe (Read-Only) |
read_efuse <FIELD> |
Reads the value of a single, specific eFuse field. | Safe (Read-Only) |
burn_efuse <FIELD> <VALUE> |
Burns a value to a specific eFuse field. This is irreversible. | Dangerous |
burn_key <BLOCK> <FILE> |
Burns a cryptographic key from a binary file into a key block. Part of a security workflow. | EXTREME |
write_protect_efuse <FIELD> |
Burns the write-protection bit for a field, making it permanently read-only. | Dangerous |
read_protect_efuse <FIELD> |
Burns the read-protection bit for a key field, making it unreadable by software. | EXTREME |
The tool will usually ask for confirmation before burning anything unless --do-not-confirm
is used (which is highly discouraged for manual operations).
Practical Examples
VERY IMPORTANT WARNING: The following examples include code that can program eFuses. Programming eFuses is PERMANENT AND IRREVERSIBLE. Only run these examples on an ESP32 development board that you are willing to risk potentially damaging or permanently altering. Understand every line of code before running it. We strongly recommend using
espefuse.py summary
first to understand your chip’s current eFuse state.
%%{init: {'theme':'base', 'themeVariables': {'primaryColor': '#DBEAFE', 'primaryTextColor': '#1E40AF', 'primaryBorderColor': '#2563EB'}}}%% graph TD A(Start: I want to burn an eFuse); A --> B{Is this a production device?}; B -- Yes --> C(<b>STOP!</b><br>Test on a sacrificial<br>dev board first.); B -- No --> D{"Is this a system eFuse?<br>(e.g., in BLK0)"}; D -- Yes --> E{"Are you following an<br>official Espressif guide?<br>(e.g., for Secure Boot)"}; E -- No --> F(<b>STOP!</b><br>Incorrectly writing system<br>eFuses can brick the device.); E -- Yes --> G(Proceed with EXTREME caution.<br>Double-check every step.); D -- No (User Block) --> H{Have you read the<br>current value first?}; H -- No --> I(<b>STOP!</b><br>Read first to ensure you<br>are not changing a 1 to a 0.); H -- Yes --> J{"Is the write operation valid?<br>(Only changing 0s to 1s)"}; J -- No --> K(<b>STOP!</b><br>Invalid operation.<br>eFuses are OTP.); J -- Yes --> L{Have you double-checked the<br>field name, value, and bit-length?}; L -- No --> M(Go back and verify in the TRM<br>or esp_efuse_table.csv); L -- Yes --> N(Ready to Burn); N --> O[Use <b>espefuse.py</b> for manual burns<br>or <b>batch-mode API</b> in code.]; O --> P(Verify by reading the<br>value back after burning.); %% Styling classDef startNode fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; classDef endNode fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46; classDef decisionNode fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; classDef processNode fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef stopNode fill:#FEE2E2,stroke:#DC2626,stroke-width:2px,color:#991B1B; class A startNode; class B,D,E,H,J,L decisionNode; class C,F,I,K stopNode; class G,M,N,O,P processNode; class P endNode;
Example 1: Reading Factory-Programmed eFuses
This example demonstrates reading the factory MAC address and some chip information from eFuses. This is a safe, read-only operation.
1. Project Setup:
Create a new ESP-IDF project.
2. Code (main/main.c
):
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h" // For esp_read_mac
#include "esp_efuse.h"
#include "esp_efuse_table.h" // For predefined field descriptors
#include "esp_log.h"
static const char *TAG = "efuse_read_example";
void app_main(void)
{
ESP_LOGI(TAG, "Reading eFuse information...");
// 1. Read MAC Address (Default method)
uint8_t mac_addr[6];
esp_err_t ret = esp_read_mac(mac_addr, ESP_MAC_WIFI_STA);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "MAC Address (WiFi STA): %02x:%02x:%02x:%02x:%02x:%02x",
mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
} else {
ESP_LOGE(TAG, "Failed to read MAC address: %s", esp_err_to_name(ret));
}
// 2. Read Chip Package Version using esp_efuse_read_field_blob
// The ESP_EFUSE_PKG_VERSION descriptor comes from esp_efuse_table.h
uint8_t pkg_version = 0; // Assuming PKG_VERSION is a few bits, uint8_t is enough
// Get the size of the PKG_VERSION field
size_t pkg_version_size_bits = esp_efuse_get_field_size(ESP_EFUSE_PKG_VERSION);
if (pkg_version_size_bits > 0 && pkg_version_size_bits <= sizeof(pkg_version) * 8) {
ret = esp_efuse_read_field_blob(ESP_EFUSE_PKG_VERSION, &pkg_version, pkg_version_size_bits);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Chip Package Version (raw value from eFuse): %d (size: %d bits)", pkg_version, pkg_version_size_bits);
// Interpretation of this value depends on the specific chip's TRM
} else {
ESP_LOGE(TAG, "Failed to read PKG_VERSION: %s", esp_err_to_name(ret));
}
} else {
ESP_LOGW(TAG, "PKG_VERSION field size is 0 or too large for uint8_t (%d bits)", pkg_version_size_bits);
}
// 3. Read a different field, e.g., SPI_PAD_CONFIG_CLK (if available and readable)
// This is just an example; choose a field relevant to your chip from esp_efuse_table.h
// Ensure the field is readable and its size is known.
#ifdef ESP_EFUSE_SPI_PAD_CONFIG_CLK
uint32_t spi_clk_pad_cfg = 0;
size_t spi_clk_pad_cfg_size = esp_efuse_get_field_size(ESP_EFUSE_SPI_PAD_CONFIG_CLK);
if (spi_clk_pad_cfg_size > 0 && spi_clk_pad_cfg_size <= sizeof(spi_clk_pad_cfg) * 8) {
ret = esp_efuse_read_field_blob(ESP_EFUSE_SPI_PAD_CONFIG_CLK, &spi_clk_pad_cfg, spi_clk_pad_cfg_size);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "SPI_PAD_CONFIG_CLK: 0x%x (size: %d bits)", (unsigned int)spi_clk_pad_cfg, spi_clk_pad_cfg_size);
} else {
ESP_LOGE(TAG, "Failed to read SPI_PAD_CONFIG_CLK: %s", esp_err_to_name(ret));
}
}
#else
ESP_LOGW(TAG, "ESP_EFUSE_SPI_PAD_CONFIG_CLK is not defined for this target.");
#endif
ESP_LOGI(TAG, "eFuse reading example finished.");
}
3. CMakeLists.txt (main/CMakeLists.txt
):
idf_component_register(SRCS "main.c"
INCLUDE_DIRS "."
REQUIRES esp_efuse esp_system esp_log freertos)
4. Build, Flash, and Observe:
Build and flash the project. Open the ESP-IDF Monitor. You should see the MAC address and other eFuse values printed. The exact fields and their values will depend on your specific ESP32 variant and its factory programming.
Example 2: Reading and Writing User eFuse Block (EFUSE_BLK3)
This example demonstrates reading and potentially writing to EFUSE_BLK3
, which is often available for user data.
EXTREME CAUTION REITERATED: This example involves WRITING to eFuses. This is PERMANENT.
- Only run this on a test/development board you can afford to alter permanently.
EFUSE_BLK3
is generally safe for user data, but mistakes can still consume valuable OTP bits.- This example attempts to write a small 4-bit pattern.
%%{init: {'theme':'base', 'themeVariables': {'primaryColor': '#DBEAFE', 'primaryTextColor': '#1E40AF', 'primaryBorderColor': '#2563EB'}}}%% sequenceDiagram participant App as Application participant API as esp_efuse API App->>API: 1. esp_efuse_batch_write_begin() activate API Note over App,API: Enters batch mode. Locks eFuse controller. API-->>App: ESP_OK deactivate API App->>API: 2. esp_efuse_write_field_blob(...) activate API Note right of API: Queues the write operation.<br>No physical burning yet. API-->>App: ESP_OK deactivate API App->>API: 3. esp_efuse_write_field_cnt(...) activate API Note right of API: Queues another operation. API-->>App: ESP_OK deactivate API App->>API: 4. esp_efuse_batch_write_commit() activate API Note over App,API: COMMITS ALL QUEUED WRITES!<br>This is the irreversible step. API-->>App: ESP_OK deactivate API
1. Code (main/main.c
):
#include <stdio.h>
#include <string.h> // For memcpy
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_efuse.h"
#include "esp_efuse_table.h" // For ESP_EFUSE_USER_DATA_BLK3 or similar
#include "esp_log.h"
#include "esp_idf_version.h" // For version checks if needed for field names
static const char *TAG = "efuse_user_rw_example";
// Define which user data block/field to use.
// For many chips, ESP_EFUSE_USER_DATA_BLK3 is a 256-bit field in EFUSE_BLK3.
// Or you might use a custom field descriptor if you partition BLK3.
// This example will try to use the first few bits of BLK3.
// For simplicity, we'll use a custom descriptor if the generic one isn't ideal for small writes.
// Custom descriptor for the first 4 bits of BLK3
// IMPORTANT: The block (EFUSE_BLK3) and bit start (0) are examples.
// Verify these against your chip's TRM and esp_efuse_table.csv.
const esp_efuse_desc_t* user_field_to_write[2]; // Array of pointers, NULL terminated
void app_main(void)
{
ESP_LOGI(TAG, "User eFuse Read/Write Example (USE WITH EXTREME CAUTION!)");
// Initialize the descriptor for the first 4 bits of EFUSE_BLK3
// This assumes EFUSE_BLK3 is the correct block for user data.
// Check your chip's TRM and the esp_efuse_table.csv for correct block and field names.
// ESP_EFUSE_USER_DATA_BLK3 is often a predefined descriptor for the whole block.
// For this example, we create a custom descriptor for a small part.
static const esp_efuse_desc_t my_custom_user_field_desc = {
.efuse_block = EFUSE_BLK3, // Or EFUSE_BLK_USER_DATA if defined
.bit_start = 0, // Starting bit within the block
.bit_count = 4 // Number of bits for this field
};
user_field_to_write[0] = &my_custom_user_field_desc;
user_field_to_write[1] = NULL; // Null-terminate the descriptor array
// --- 1. READ current value of the first 4 bits of BLK3 ---
uint8_t current_user_data = 0; // To store 4 bits
esp_err_t ret = esp_efuse_read_field_blob(user_field_to_write, ¤t_user_data, 4);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Current value of first 4 bits in USER_DATA_BLK3: 0x%X", current_user_data & 0x0F);
} else {
ESP_LOGE(TAG, "Failed to read user eFuse field: %s. THIS IS UNEXPECTED.", esp_err_to_name(ret));
ESP_LOGE(TAG, "Aborting write attempt.");
return;
}
// --- 2. WRITE a new value (e.g., 0b0101 = 0x5) IF IT'S DIFFERENT AND INCREASES BITS ---
// eFuses can only change bits from 0 to 1.
// We can only write if the new value has bits set that were previously 0.
// We cannot change a '1' back to a '0'.
uint8_t data_to_write = 0x5; // 0b0101
bool can_write = false;
// Check if the write is valid (only changing 0s to 1s)
if ((current_user_data & data_to_write) == current_user_data) { // All bits currently set in current_user_data must also be set in data_to_write
if (current_user_data != data_to_write) { // And there must be at least one new bit to set
can_write = true;
} else {
ESP_LOGI(TAG, "Desired value 0x%X is already programmed or same. No write needed.", data_to_write & 0x0F);
}
} else {
ESP_LOGW(TAG, "Cannot write 0x%X over 0x%X. This would require changing a 1 to a 0.", data_to_write & 0x0F, current_user_data & 0x0F);
}
if (can_write) {
ESP_LOGW(TAG, "ATTEMPTING TO WRITE 0x%X to first 4 bits of USER_DATA_BLK3. THIS IS PERMANENT.", data_to_write & 0x0F);
ESP_LOGW(TAG, "You have 5 seconds to cancel (Ctrl+C in monitor if interactive, or reset board)...");
vTaskDelay(pdMS_TO_TICKS(5000)); // Give user time to reconsider
// Use batch mode for safety, even for one field
ret = esp_efuse_batch_write_begin();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to begin eFuse batch write: %s", esp_err_to_name(ret));
return;
}
ret = esp_efuse_write_field_blob(user_field_to_write, &data_to_write, 4);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Successfully queued write of 0x%X to user eFuse field.", data_to_write & 0x0F);
} else {
ESP_LOGE(TAG, "Failed to queue write to user eFuse field: %s", esp_err_to_name(ret));
esp_efuse_batch_write_cancel();
return;
}
ret = esp_efuse_batch_write_commit();
if (ret == ESP_OK) {
ESP_LOGI(TAG, "eFuse batch write COMMITTED successfully!");
} else {
ESP_LOGE(TAG, "Failed to COMMIT eFuse batch write: %s", esp_err_to_name(ret));
ESP_LOGE(TAG, "The eFuse may or may not have been written. Check with espefuse.py summary.");
return;
}
// --- 3. READ BACK and verify ---
uint8_t new_user_data = 0;
ret = esp_efuse_read_field_blob(user_field_to_write, &new_user_data, 4);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "NEW value of first 4 bits in USER_DATA_BLK3: 0x%X", new_user_data & 0x0F);
if ((new_user_data & 0x0F) == (data_to_write & 0x0F)) {
ESP_LOGI(TAG, "Verification successful!");
} else {
ESP_LOGE(TAG, "VERIFICATION FAILED! Expected 0x%X, got 0x%X", data_to_write & 0x0F, new_user_data & 0x0F);
}
} else {
ESP_LOGE(TAG, "Failed to read back user eFuse field after write: %s", esp_err_to_name(ret));
}
}
ESP_LOGI(TAG, "User eFuse example finished.");
}
Important Considerations for Example 2:
EFUSE_BLK3
and Field Definition: The example uses a custom descriptor for the first 4 bits ofEFUSE_BLK3
. The actual block available for users (BLK1, BLK2, or BLK3) and its size can vary.EFUSE_BLK_USER_DATA
orEFUSE_BLK_KEY_PURPOSE_USER
might be more generic block enums. Always verify with your chip’s TRM and theesp_efuse_table.csv
for your ESP-IDF version and target.- Predefined Descriptors: ESP-IDF provides predefined descriptors like
ESP_EFUSE_USER_DATA_BLK3
which typically span the entire 256 bits of BLK3. If you use such a descriptor, you’d read/write the whole block. For partial writes, custom descriptors are needed as shown. - Error Checking: The example includes basic checks to prevent writing if it means changing a ‘1’ to a ‘0’. More sophisticated logic might be needed for complex fields.
- Batch Mode:
esp_efuse_batch_write_begin()
andesp_efuse_batch_write_commit()
are crucial. All eFuse writes should ideally be done in a single batch operation to minimize stress on the programming hardware and ensure atomicity if multiple fields are written.
2. Build, Flash, and Observe:
Build and flash. PAY EXTREME ATTENTION TO THE LOGS. The program will pause before attempting to write. If you proceed, it will try to burn the eFuse bits. After an attempted write, always verify the actual eFuse state using espefuse.py summary –port /dev/ttyUSB0.
Example 3: Using espefuse.py
(Conceptual)
This section shows command-line examples. You’d run these from your host computer’s terminal with ESP-IDF environment activated. Replace /dev/ttyUSB0
with your ESP32’s serial port.
- Summarize eFuse State (SAFE):
espefuse.py --port /dev/ttyUSB0 summary
Output: A detailed list of all eFuse fields, their current values (hex and binary), and protection status. Study this output carefully. - Read a Specific Field (SAFE):To read the USER_DATA_BLK3 field (if it exists on your chip and is defined in your eFuse table):
espefuse.py --port /dev/ttyUSB0 read_efuse USER_DATA_BLK3
- Burn a User eFuse Field (DANGEROUS – EXAMPLE ONLY):Suppose you want to burn the value 0x1234 into the first 16 bits of USER_DATA_BLK3. You might define a custom field in a CSV and use espefuse.py to burn it, or burn raw bits into BLK3_WDATA0 (very low level).A more abstract command for a named field (if MY_CUSTOM_FIELD is defined in your tables and maps to user block bits):
# Hypothetical command - FIELD_NAME must be valid # espefuse.py --port /dev/ttyUSB0 burn_efuse MY_CUSTOM_FIELD 0x1234 ```espefuse.py` will ask for confirmation.
- Write-Protect a User Block (PERMANENTLY DISABLES WRITES):If USER_DATA_BLK3 has a write-protect bit (e.g., WR_DIS.USER_DATA_BLK3):
# Hypothetical command - FIELD_NAME must be valid # espefuse.py --port /dev/ttyUSB0 write_protect_efuse WR_DIS.USER_DATA_BLK3
This would burn the write-disable bit forUSER_DATA_BLK3
, making it read-only forever.
Tip: For any
espefuse.py
burn operation, first run withsummary
to understand the current state. Then, if you must burn, double-check the field name and value. Consider what other fields might be affected by coding schemes or shared blocks.
Variant Notes
The eFuse system shows significant variation across the ESP32 family. Key differences include:
- Total eFuse Bits and Block Count:
- ESP32: Has 1024 bits (BLK0-BLK3). BLK0 for system, BLK1-BLK3 for keys/user data.
EFUSE_BLK3
(256 bits) is commonly user-available. - ESP32-S2: Has 1024 bits. Similar structure, but details of key purposes and user blocks differ.
EFUSE_BLK3
is typically for user data. - ESP32-S3: Has more eFuse bits, often 1792 or more, with more dedicated blocks for integrated security features (e.g., Digital Signature, HMAC). Still provides user blocks (e.g.,
EFUSE_BLK_USER_DATA
which might be BLK3 or a different one). - ESP32-C3 (RISC-V): Has 768 bits. Different block structure.
EFUSE_BLK_USR_DATA
(often BLK2, 256 bits) is for user data. - ESP32-C6 (RISC-V): Typically has more eFuses than C3, supporting Wi-Fi 6 and 802.15.4. User data blocks are available.
- ESP32-H2 (RISC-V): Designed for 802.15.4 (Thread/Zigbee). eFuse structure will be tailored for its features, including user blocks.
- ESP32: Has 1024 bits (BLK0-BLK3). BLK0 for system, BLK1-BLK3 for keys/user data.
- Specific Fields: The names, sizes, and purposes of eFuse fields (especially for security and system configuration) are highly variant-specific. What’s
FLASH_CRYPT_CNT
on one might have a slightly different name or bit layout on another. - User Data Blocks: While the concept of a “user block” exists on most, its block number (
BLK1
,BLK2
,BLK3
, etc.) and total size (e.g., 256 bits, 512 bits) can change. Always refer toesp_efuse_table.csv
for your specific target and ESP-IDF version. - Security Features: Newer chips (S3, C6, H2) often have more sophisticated security-related eFuses for features like secure boot v2, digital signature peripheral, HMAC, flash/PSRAM encryption enhancements, and world controller settings.
- Coding Scheme: The default or available coding schemes might differ.
Crucial Advice: The Technical Reference Manual (TRM) for your specific ESP32 variant is the ultimate source of truth for its eFuse map and capabilities. The
components/efuse/espXXXX/esp_efuse_table.csv
(where XXXX is your chip variant) in your ESP-IDF installation also provides the definitions used by the software.
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
Bricked Device | Device fails to boot, bootloader loop, no serial output. | Solution: This is often unrecoverable. Caused by burning critical system eFuses like FLASH_CRYPT_CNT incorrectly. Always test on sacrificial hardware. |
Writing ‘0’ over ‘1’ | espefuse.py reports an error. Programmatic write returns ESP_ERR_EFUSE_INVALID_ENCODING . |
Solution: Remember eFuses are OTP (0->1 only). Read the value first. Your logic must only set new bits to 1. |
Feature Disabled Permanently | JTAG no longer works. Flash encryption cannot be enabled/disabled. | Solution: Caused by burning a disable bit like JTAG_DISABLE or a WR_DIS.* bit. This is permanent. Plan your eFuse usage carefully. |
Verification Fails After Write | You write a value, but reading it back gives a different result. | Solution: Check the CODING_SCHEME . If it’s not ‘None’, the physical bits burned don’t map 1:1 with logical bits. The API handles this, but raw reads may be confusing. Trust the API’s read/write functions. |
espefuse.py fails or shows wrong fields | Command fails with an error, or the `summary` lists fields that don’t match your chip. | Solution: Ensure your ESP-IDF environment is set to the correct target (e.g., idf.py set-target esp32s3 ) before running espefuse.py . |
Forgetting Batch Mode | Writing multiple eFuses in a loop works but seems slow or unreliable. | Solution: Always use the batch API (esp_efuse_batch_write_... ) for programmatic writes to minimize stress on the hardware and ensure atomicity. |
Exercises
- Storing and Retrieving a Custom Device Serial Number:
- Objective: Learn to write a unique serial number to a user eFuse block and read it back.
- Task:
- Identify a suitable user eFuse field (e.g., the first 64 bits of
USER_DATA_BLK3
or a similar user block on your ESP32 variant). - Write an application that first reads this eFuse field.
- If the field reads as all zeros (or your defined “unprogrammed” state), the application should then (with EXTREME CAUTION and user confirmation via serial input if possible) program a predefined 64-bit serial number into this field using
esp_efuse_write_field_blob()
within a batch operation. - On every subsequent boot, the application should read this eFuse field and log the custom serial number.
- Additionally, write-protect this specific 64-bit field by burning its corresponding
WR_DIS
bit usingespefuse.py
after confirming the serial number is correctly programmed. Document theespefuse.py
command used.
- Identify a suitable user eFuse field (e.g., the first 64 bits of
- Warning: This involves permanent eFuse modification. Test thoroughly.
- Firmware Version Tracking in eFuses:
- Objective: Use eFuse bits as a simple, monotonic counter for firmware updates (not a full anti-rollback, but a basic version counter).
- Task:
- Designate 4 bits in a user eFuse block to represent a firmware version (0-15).
- On boot, your application reads these 4 bits. Let this be
efuse_version
. - Define a
current_firmware_version
in your code (e.g.,#define CURRENT_FIRMWARE_VERSION 3
). - Log both versions.
- Simulation (Do not actually burn in a loop): If
current_firmware_version > efuse_version
, the application should log a message indicating that it would burn the eFuse bits to representcurrent_firmware_version
. Explain how you would burn bits to increment the version (e.g., version 1 =0001
, version 2 =0011
, version 3 =0111
– always only changing 0s to 1s). - Describe how you would use
espefuse.py
to manually set the eFuse version bits to a specific value (e.g., to represent version 2).
%%{init: {'theme':'base', 'themeVariables': {'primaryColor': '#DBEAFE', 'primaryTextColor': '#1E40AF', 'primaryBorderColor': '#2563EB'}}}%% graph TD subgraph "Firmware Version Tracking in 4 eFuse Bits" direction TB _ v0("<b>Unprogrammed</b><br>0000"); v1("<b>Version 1</b><br>0001"); v2("<b>Version 2</b><br>0011"); v3("<b>Version 3</b><br>0111"); v4("<b>Version 4</b><br>1111"); end _ --> v0 v0 -- "Burn bit 0" --> v1; v1 -- "Burn bit 1" --> v2; v2 -- "Burn bit 2" --> v3; v3 -- "Burn bit 3" --> v4; note["Note: This scheme only allows 4 updates.<br>You can never go from v2 (0011) back to v1 (0001).<br>The number of set bits always increases."] %% Styling classDef unprogrammed fill:#F3F4F6,stroke:#6B7280,stroke-width:1px,color:#374151; classDef programmed fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; classDef note fill:#FEF9C3,color:#92400E,stroke:#FBBF24; class v0 unprogrammed; class v1,v2,v3,v4 programmed; class note note;
Summary
- eFuses are One-Time Programmable (OTP) non-volatile memory bits used for permanent storage of critical data.
- ESP32 variants have eFuses organized into blocks, some for system use (factory-programmed or for security features) and some for user applications.
- Programming eFuses is irreversible and must be done with extreme caution. Incorrect programming can permanently damage or alter device behavior.
- The
esp_efuse.h
API (with functions likeesp_efuse_read_field_blob
,esp_efuse_write_field_blob
) and eFuse descriptors (esp_efuse_desc_t
) are used for programmatic access. - Batch write mode (
esp_efuse_batch_write_begin/commit
) is essential for safe in-application eFuse programming. espefuse.py
is a command-line tool for offline eFuse inspection and programming, generally safer for initial setup.- Common eFuse uses include storing MAC addresses, chip versions, security keys, and custom device configurations.
- eFuse layouts, total bit counts, and field definitions vary significantly across ESP32, S2, S3, C3, C6, and H2 variants. Always consult the TRM and ESP-IDF’s
esp_efuse_table.csv
. - Write-protection and read-protection eFuses can further secure eFuse contents.
Further Reading
- ESP-IDF Programming Guide – eFuse Manager:
- https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32/api-reference/system/efuse.html (Select your target chip in the documentation for specific details).
espefuse.py
tool documentation:- Can be found in the ESP-IDF documentation or by running
espefuse.py --help
. - https://docs.espressif.com/projects/esptool/en/latest/esp32/espefuse/index.html (esptool.py documentation often covers espefuse.py as well).
- Can be found in the ESP-IDF documentation or by running
- Technical Reference Manual (TRM) for your specific ESP32 variant:
- Available on the Espressif website. The “eFuse Controller” or “eFuse” chapter is essential.
- eFuse Tables in ESP-IDF:
- Located in
components/efuse/espXXXX/esp_efuse_table.csv
within your ESP-IDF installation directory (whereXXXX
is your chip variant likeesp32
,esp32s3
, etc.).
- Located in