解密 ESP32 Bootloader|自訂流程和跳轉邏輯全實作!


ESP32 Bootloader(引導程序)是芯片啟動時運行的第一段代碼,負責初始化硬件、驗證韌體,並決定如何載入應用程序。
這篇文章將帶你從 上電開始,一步步了解 Bootloader 的角色,並用實際專案示範如何撰寫自己的 ESP32 Bootloader,觀察從 ROM → Bootloader → App 的整個流程。
無論你是開發 OTA、開機選單、或想玩點進階功能,這篇教學都將是你理解「ESP32 啟動機制」的第一步。只需幾分鐘,你將不再只會用 ESP32 Bootloader,而是真的理解並能駕馭它。

ESP32 Bootloader

什麼是 ESP32 Bootloader?

Bootloader 是嵌入式系統啟動時執行的 低階程式,主要功能包括:

  • 初始化 CPU、記憶體、時鐘等硬件
  • 從 Flash 載入應用程式(App)
  • 驗證韌體完整性(可選)
  • 提供 OTA(空中升級) 支援

在 ESP32 中,Bootloader 通常存放在 Flash 0x1000 位置,並由 ROM Code 直接執行。

ESP32 開機流程圖

    A[Power On / Reset] --> B[Boot ROM (Factory Burned)]
    B --> C[Bootloader (Customizable)]
    C --> D[Load Application]
    D --> E[Run app_main()]
  • Boot ROM 是晶片內建的,負責載入 bootloader。
  • Bootloader 是你可以編寫和自訂的階段。
  • 最後才會跳到 main.c 中的 app_main()

開發環境

在開始編程之前,請確保已完成以下準備工作:

專案結構

esp32_boot_demo/
├── CMakeLists.txt
├── main
│   ├── CMakeLists.txt
│   └── main.c                 // User application
├── bootloader_components
│   └── main         
│       ├── CMakeLists.txt   
│       └── bootloader_main.c  // Custom bootloader
└── README.md                  

程式碼

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

# 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}")

程式碼解說

  • bootloader_init()
    初始化硬體基礎設施。
  • bootloader_utility_load_boot_image_from_deep_sleep()
    用於深度睡眠喚醒時的快速啟動,跳過驗證步驟以加速啟動。
  • select_partition_number()
    解析分區表並決定要啟動哪個分區(例如 factory 或 ota_0)。
  • bootloader_utility_load_boot_image()
    最終載入應用程式映像檔,並跳轉到其入口地址(通常是 0x10000)。

編譯和燒錄

完成程式碼後,您可以使用 ESP-IDF 提供的命令進行編譯、燒錄和監控。

在 VS Code 的左下角 ESP-IDF 工具列:

  • 點選 Build project
  • 點選 Flash device
  • 點選 Monitor device

你就會看到:

[boot] Custom Bootloader Welcome Message=== Hello from app_main ===

結論

透過這篇實作,我們完整走了一遍 ESP32 Bootloader 的啟動流程。從上電、進入 Bootloader、初始化,再跳轉到真正的應用程式。
你不只是看懂 ESP32 bootloader 在做什麼,更親手實作了自己的流程與跳轉邏輯。這樣的能力,在 OTA 更新、安全驗證、甚至多系統開機選單設計中,都非常實用。
了解 ESP32 bootloader,不只是進階開發者的專利,而是每個 ESP32 開發者都該具備的底層知識。