ESP32 Advanced

ESP32 Multi-Application Bootloader: Run Multiple Apps on a Single Device

ESP32 BOOTLOADER (factory) APP 1 (ota_0) APP 2 (ota_1) APP 3 (ota_2) ESP32 Multi-Application Bootloader

Introduction

The ESP32 microcontroller is a versatile and powerful device, widely used in IoT and embedded applications. One of its advanced features is the ability to store multiple firmware images in its flash memory and switch between them. This capability can be leveraged for various purposes, such as testing different firmware versions, running multiple applications, or maintaining a backup firmware.

In this tutorial, we'll explore how to create a serial-based bootloader that allows you to easily switch between multiple applications stored in the ESP32's flash memory. This technique is perfect for projects where you need different functionalities but want to conserve hardware resources by using a single device.

How It Works

The multi-application bootloader system works using ESP32's built-in OTA (Over-The-Air) partition functionality. Here's the basic flow:

  1. The bootloader application runs first when the device powers on
  2. It presents a menu of available applications via the serial interface
  3. When a user selects an application, the bootloader sets the appropriate boot partition and restarts
  4. The selected application runs, with code that ensures it will return to the bootloader on the next restart

The bootloader uses the ESP32's partition table to manage different firmware images. This approach leverages the existing OTA mechanism built into the ESP-IDF framework, but with applications pre-loaded into the flash memory rather than being downloaded over a network.

Partition Table

To enable multiple firmware images, we need a custom partition table. Below is a partition table that accommodates a bootloader and three OTA partitions, suitable for ESP32-S3 with 16MB flash memory:

# Name,      Type, SubType,   Offset,  Size, Flags
nvs,         data, nvs,       0x9000,   24K,
otadata,     data, ota,       ,         8K,
phy_init,    data, phy,       ,         4K,
factory,     app,  factory,   ,         2M,
ota_0,       app,  ota_0,     ,         2M,
ota_1,       app,  ota_1,     ,         2M,
ota_2,       app,  ota_2,     ,         2M,

Save this as a file named partitions.csv. In Arduino IDE, you'll need to select this custom partition scheme when configuring your board.

Bootloader Implementation

Let's create a serial-based bootloader application that presents a menu to the user and allows them to select which application to run. This code should be flashed to the factory partition:

#include 
#include 
#include 

// Define the menu options
typedef struct {
  const char* name;
  const char* description;
  const esp_partition_subtype_t subtype;
} app_info_t;

// Our three apps plus the bootloader itself
app_info_t apps[] = {
  {"Bootloader", "Main Menu (current)", ESP_PARTITION_SUBTYPE_APP_FACTORY},
  {"LED Patterns", "Different LED blinking patterns", ESP_PARTITION_SUBTYPE_APP_OTA_0},
  {"Temp Sensor", "Temperature sensor simulator", ESP_PARTITION_SUBTYPE_APP_OTA_1},
  {"Echo Test", "Serial echo and character counter", ESP_PARTITION_SUBTYPE_APP_OTA_2}
};

// ASCII art header
void printHeader() {
  Serial.println();
  Serial.println("  ______  _____ _____ ____ ___    _____ ____  ");
  Serial.println(" | ____/ |  ___/ ____|  _ \\_ _|  | ____|___ \\ ");
  Serial.println(" |  _|   | |_  | |  _ | |_) | |   |  _|   __) |");
  Serial.println(" | |___  |  _| | |_| ||  __/| |   | |___ / __/ ");
  Serial.println(" |_____| |_|    \\____|_|   |___|  |_____|_____|");
  Serial.println();
  Serial.println("       MULTI-APPLICATION BOOTLOADER DEMO       ");
  Serial.println("============================================");
}

// Display the menu of available applications
void displayMenu() {
  printHeader();
  Serial.println("Available Applications:");
  Serial.println();
  
  for(int i = 1; i < sizeof(apps)/sizeof(app_info_t); i++) {
    Serial.print("  ");
    Serial.print(i);
    Serial.print(". ");
    Serial.print(apps[i].name);
    Serial.print(" - ");
    Serial.println(apps[i].description);
  }
  
  Serial.println();
  Serial.println("Enter application number (1-3):");
}

// Switch to the selected application
void switchToApp(int appIndex) {
  if(appIndex < 1 || appIndex >= sizeof(apps)/sizeof(app_info_t)) {
    Serial.println("Invalid selection!");
    return;
  }
  
  // Find the partition for the selected app
  const esp_partition_t* next_partition = esp_partition_find_first(
    ESP_PARTITION_TYPE_APP, 
    apps[appIndex].subtype, 
    NULL);
    
  if(next_partition && esp_ota_set_boot_partition(next_partition) == ESP_OK) {
    Serial.print("Switching to ");
    Serial.println(apps[appIndex].name);
    Serial.println("Restarting in 3 seconds...");
    delay(3000);
    ESP.restart();  // Restart to boot from the new partition
  } else {
    Serial.println("ERROR: Failed to set boot partition!");
    Serial.println("Application may not be installed or partition table is incorrect.");
  }
}

void setup() {
  // Initialize serial and wait for it to connect
  Serial.begin(115200);
  delay(1000);
  
  // Show device information
  const esp_partition_t* running = esp_ota_get_running_partition();
  
  printHeader();
  Serial.println("System Information:");
  Serial.print("  ESP32 Chip Model: ");
  Serial.println(ESP.getChipModel());
  Serial.print("  Flash Size: ");
  Serial.print(ESP.getFlashChipSize() / (1024 * 1024));
  Serial.println(" MB");
  Serial.print("  Free Heap: ");
  Serial.print(ESP.getFreeHeap() / 1024);
  Serial.println(" KB");
  Serial.print("  Current Partition: ");
  Serial.println(running->label);
  Serial.println();
  
  Serial.println("Press any key to continue...");
}

void loop() {
  // Wait for user input
  if(Serial.available() > 0) {
    // Clear any pending input
    while(Serial.available()) {
      Serial.read();
    }
    
    // Show the menu
    displayMenu();
    
    // Wait for application selection
    while(!Serial.available()) {
      delay(100); // Wait for input
    }
    
    int choice = Serial.parseInt();
    Serial.print("Selected: ");
    Serial.println(choice);
    
    if(choice >= 1 && choice <= 3) {
      switchToApp(choice);
    } else {
      Serial.println("Invalid selection! Please try again.");
      delay(2000);
    }
  }
  
  delay(100);
}

Application Structure

Each application needs to include code that will set the boot partition back to the factory partition (bootloader) when it restarts. This ensures that after running the application, the system will always return to the bootloader menu. Here's a template for how each application should implement this functionality:

#include 
#include 
#include 

// Function to set the next boot to the factory partition (bootloader)
void reset_to_factory_app() {
  const esp_partition_t *factory_partition = esp_partition_find_first(
    ESP_PARTITION_TYPE_APP, 
    ESP_PARTITION_SUBTYPE_APP_FACTORY, 
    NULL);
    
  if (factory_partition != NULL) {
    if (esp_ota_set_boot_partition(factory_partition) == ESP_OK) {
      Serial.println("Set boot partition to bootloader");
    } else {
      Serial.println("Failed to set boot partition!");
    }
  } else {
    Serial.println("Factory partition not found!");
  }
}

void setup() {
  Serial.begin(115200);
  
  // IMPORTANT: Set the next boot to return to the bootloader
  reset_to_factory_app();
  
  // Your application initialization code goes here
  Serial.println("\n\n=== YOUR APPLICATION NAME ===");
  // ...
}

void loop() {
  // Your application main code goes here
  // ...
  
  // Optional: Add a way to manually return to bootloader
  if (Serial.available() > 0) {
    char cmd = Serial.read();
    if (cmd == 'b') {
      Serial.println("Returning to bootloader...");
      delay(1000);
      ESP.restart();
    }
  }
}

Installation Steps

Follow these steps to set up the multi-application bootloader on your ESP32-S3:

1. Configure Arduino IDE

  • Select Tools → Board → ESP32S3 Dev Module (or your specific board)
  • Select Tools → Flash Mode → QIO 80MHz
  • Select Tools → Flash Size → 16MB
  • Select Tools → Partition Scheme → Custom Partition Table CSV
  • Select Tools → USB CDC On Boot → Enabled

2. Flash the Bootloader

  • Upload the bootloader code to your ESP32-S3
  • This will be flashed to the factory partition automatically

3. Compile and Flash Applications

  • Create each application using the template above
  • Compile each application and export the binary file
  • Flash each application to its corresponding OTA partition using esptool.py
esptool.py --chip esp32s3 --port [YOUR_PORT] write_flash 0x220000 app1.bin
esptool.py --chip esp32s3 --port [YOUR_PORT] write_flash 0x420000 app2.bin
esptool.py --chip esp32s3 --port [YOUR_PORT] write_flash 0x620000 app3.bin

Practical Uses

The ESP32 multi-application bootloader has several practical applications:

Testing & Development

Quickly test different firmware versions without reflashing the entire device. Perfect for development and debugging.

Multi-purpose Devices

Create versatile IoT devices that can serve different purposes based on user selection.

Backup Firmware

Maintain a known-good firmware version for recovery in case of issues with experimental updates.

Educational Platforms

Create learning devices that can demonstrate different concepts or algorithms without needing multiple hardware setups.

Conclusion

The ESP32 multi-application bootloader provides a powerful way to leverage the full potential of your ESP32 microcontroller. By storing multiple applications in the flash memory and being able to easily switch between them, you can create versatile devices that serve multiple purposes or maintain backup firmware for critical systems.

This approach is particularly valuable for development and testing, as it allows you to quickly switch between different versions of firmware without going through the complete flashing process each time. It's also perfect for creating multipurpose IoT devices that can adapt to different needs.

Next Steps

Now that you have a basic multi-application bootloader working, consider these enhancements:

  • Add persistent settings using the NVS (Non-Volatile Storage)
  • Create a graphical version using a display if your ESP32 has one
  • Implement authentication to prevent unauthorized app switching
  • Add WiFi-based remote application selection