Effortlessly Master the ESP32 Bootloader | From Power-On to App Execution
The ESP32 Bootloader is the first piece of code that runs when the chip powers up. It is responsible for initializing the hardware, verifying the firmware, and deciding how to load the application.
This article will guide you through what happens from the moment the ESP32 is powered on. We’ll explore the role of the bootloader, and walk through a hands-on project to write your own ESP32 bootloader. You’ll gain insight into the full flow from ROM → Bootloader → App.
Whether you’re developing OTA functionality, a boot menu, or want to dive into more advanced features, this tutorial is your first step toward mastering the ESP32 startup mechanism. In just a few minutes, you’ll move beyond simply usingthe ESP32 bootloader — to understanding and controlling it.

Contents
What Is the ESP32 Bootloader?
The bootloader is a low-level program that runs at the startup of an embedded system. Its key responsibilities include:
- Initializing the CPU, memory, clocks, and other hardware
- Loading the application (App) from flash
- Verifying firmware integrity (optional)
- Supporting OTA (Over-The-Air updates)
On the ESP32, the bootloader is typically stored at Flash address 0x1000 and is directly executed by the on-chip ROM code.
ESP32 Bootloader Flow Diagram
A[Power On / Reset] --> B[Boot ROM (Factory Burned)]
B --> C[Bootloader (Customizable)]
C --> D[Load Application]
D --> E[Run app_main()]
- Boot ROM is factory-programmed into the chip and is responsible for loading the bootloader.
- Bootloader is the customizable stage where you can implement your own logic.
- The final step jumps to
app_main()
in your application code.
Development Environment
Before starting your programming, make sure to complete the following preparations:
- Install ESP-IDF (version 4.4 or higher): ESP-IDF is the official development framework for programming the ESP32, and it supports multiple operating systems such as Windows, macOS, and Linux.
- ESP32 Development Board: An ESP32 board is required.
Project Structure
esp32_boot_demo/
├── CMakeLists.txt
├── main
│ ├── CMakeLists.txt
│ └── main.c // User application
├── bootloader_components
│ └── main
│ ├── CMakeLists.txt
│ └── bootloader_main.c // Custom bootloader
└── README.md
Code
bootloader/bootloader_main.c
#include <stdbool.h>
#include "sdkconfig.h"
#include "esp_log.h"
#include "bootloader_init.h"
#include "bootloader_utility.h"
#include "bootloader_common.h"
static const char* TAG = "boot";
static int select_partition_number(bootloader_state_t *bs);
/*
* We arrive here after the ROM bootloader finished loading this second stage bootloader from flash.
* The hardware is mostly uninitialized, flash cache is down and the app CPU is in reset.
* We do have a stack, so we can do the initialization in C.
*/
void __attribute__((noreturn)) call_start_cpu0(void)
{
// 1. Hardware initialization
if (bootloader_init() != ESP_OK) {
bootloader_reset();
}
#ifdef CONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP
// If this boot is a wake up from the deep sleep then go to the short way,
// try to load the application which worked before deep sleep.
// It skips a lot of checks due to it was done before (while first boot).
bootloader_utility_load_boot_image_from_deep_sleep();
// If it is not successful try to load an application as usual.
#endif
// 2. Select the number of boot partition
bootloader_state_t bs = {0};
int boot_index = select_partition_number(&bs);
if (boot_index == INVALID_INDEX) {
bootloader_reset();
}
// 2.1 Print a custom message!
esp_rom_printf("[%s] %s\n", TAG, "Custom Bootloader Welcome Message");
// 3. Load the app image for booting
bootloader_utility_load_boot_image(&bs, boot_index);
}
// Select the number of boot partition
static int select_partition_number(bootloader_state_t *bs)
{
// 1. Load partition table
if (!bootloader_utility_load_partition_table(bs)) {
ESP_LOGE(TAG, "load partition table error!");
return INVALID_INDEX;
}
// 2. Select the number of boot partition
return bootloader_utility_get_selected_boot_partition(bs);
}
// Return global reent struct if any newlib functions are linked to bootloader
struct _reent *__getreent(void)
{
return _GLOBAL_REENT;
}
main/main.c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
// Entry point of the main application.
// This is the function that runs after the bootloader hands over control.
void app_main() {
// Print a message to indicate the app has started.
printf("=== Hello from app_main ===\n");
// Run an infinite loop with 1-second delay.
// This is a common pattern in embedded systems to keep the task alive.
while (1) {
vTaskDelay(1000 / portTICK_PERIOD_MS); // Delay for 1000 ms (1 second)
}
}
CMakeLists.txt
(root)
# esp32_boot_demo/CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(esp32_boot_demo)
main/CMakeLists.txt
idf_component_register(
SRCS "main.c"
INCLUDE_DIRS "."
)
bootloader/CMakeLists.txt
idf_component_register(SRCS "bootloader_main.c"
REQUIRES bootloader bootloader_support)
idf_build_get_property(target IDF_TARGET)
# Use the linker script files from the actual bootloader
set(scripts "${IDF_PATH}/components/bootloader/subproject/main/ld/${target}/bootloader.ld"
"${IDF_PATH}/components/bootloader/subproject/main/ld/${target}/bootloader.rom.ld")
target_linker_script(${COMPONENT_LIB} INTERFACE "${scripts}")
Code Explanation
bootloader_init()
Initializes fundamental hardware components.bootloader_utility_load_boot_image_from_deep_sleep()
Used for quick startup after deep sleep by skipping verification steps.select_partition_number()
Parses the partition table and selects the correct boot partition (e.g.,factory
,ota_0
).bootloader_utility_load_boot_image()
Loads the application image and jumps to its entry point (usually0x10000
).
Compile and Flash
After writing the code, you can use the ESP-IDF tools to build, flash, and monitor:
In the VS Code lower-left ESP-IDF toolbar:
- Click Build project
- Click Flash device
- Click Monitor device
You should then see:
[boot] Custom Bootloader Welcome Message=== Hello from app_main ===
Conclusion
Through this hands-on example, we’ve walked through the complete ESP32 boot process — from power-up, to bootloader initialization, and finally transferring control to the application.
You now understand what the ESP32 bootloader does and how to implement your own startup logic. This skill is extremely useful for OTA updates, secure boot verification, or designing a multi-boot system.
Understanding the ESP32 bootloader is not just for advanced developers — it’s essential knowledge for every ESP32 developer looking to work at the system level.