解密 ESP32 Bootloader|自訂流程和跳轉邏輯全實作!
ESP32 Bootloader(引導程序)是芯片啟動時運行的第一段代碼,負責初始化硬件、驗證韌體,並決定如何載入應用程序。
這篇文章將帶你從 上電開始,一步步了解 Bootloader 的角色,並用實際專案示範如何撰寫自己的 ESP32 Bootloader,觀察從 ROM → Bootloader → App 的整個流程。
無論你是開發 OTA、開機選單、或想玩點進階功能,這篇教學都將是你理解「ESP32 啟動機制」的第一步。只需幾分鐘,你將不再只會用 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()
。
開發環境
在開始編程之前,請確保已完成以下準備工作:
- 安裝 ESP-IDF 開發環境 (至少版本 v4.4 或更高)。
- ESP32 開發板。
專案結構
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 開發者都該具備的底層知識。