ESP32 Multi-Application Bootloader: Run Multiple Apps on a Single Device
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:
- The bootloader application runs first when the device powers on
- It presents a menu of available applications via the serial interface
- When a user selects an application, the bootloader sets the appropriate boot partition and restarts
- 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