Skip to content

Commit 17e7391

Browse files
authored
blog: article: simple boot explained (#503)
The article about the concept called Simple Boot that allows to make single image builds for the ESP32 family of microcontrollers. Signed-off-by: Marek Matej <[email protected]> Co-authored-by: Marek Matej <[email protected]>
1 parent 8cad2b7 commit 17e7391

File tree

2 files changed

+160
-0
lines changed

2 files changed

+160
-0
lines changed
74.7 KB
Loading
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
---
2+
title: "Simple Boot explained"
3+
date: 2025-06-30
4+
showAuthor: false
5+
authors:
6+
- "marek-matej"
7+
tags:
8+
- ESP32
9+
- ESP32-S2
10+
- ESP32-S3
11+
- ESP32-C3
12+
- ESP32-C6
13+
- ESP32-C2
14+
- ESP-IDF
15+
- Zephyr
16+
- NuttX
17+
18+
summary: "In this article, we explore a simplified ESP32 boot process using single-image binaries to speed up build and flash times — ideal for development workflows. This approach sacrifices features like OTA updates but enables faster iteration."
19+
---
20+
21+
## Motivation
22+
23+
Building executables for embedded systems is often a time-consuming process. As the number of builds increases, the cumulative wait time can become significant. This applies not only to local desktop builds but also to CI environments, where high resource utilization may lead to increased costs.
24+
25+
The ESP32 series of microcontrollers traditionally relies on a 2nd stage bootloader. Due to the design of the ROM loader, using this bootloader is typically the only viable method to boot the SoC.
26+
27+
The ROM loader imposes the following requirements:
28+
- Only ESP image format is supported
29+
- SRAM only target addresses
30+
- Limited size of the image
31+
32+
To satisfy these constraints, the 2nd stage bootloader is essential. Among other functions, it serves as a bridge to overcome the limitations imposed by the ROM loader.
33+
34+
However, the bootloader image often needs to be rebuilt and re-flashed, further increasing the overall build time.
35+
36+
To mitigate this and reduce the time required for both building and flashing, we can integrate essential features of the ESP32 bootloader directly into the application image. This integration must be carried out with careful consideration of the ROM loader’s requirements and limitations.
37+
38+
## Technical background
39+
40+
In our journey, the main obstacle we need to overcome is the ROM loader’s limitation to accept only RAM-loadable images.
41+
42+
Since the ESP image format consists of a list of memory segments interpreted by the ROM loader, we can construct a binary image that includes only the segments the ROM loader is capable of processing.
43+
44+
By adhering to these constraints, we can craft a simplified bootable image that embeds the minimal required bootloader functionality directly into the application itself, eliminating the need to recompile and reflash a separate bootloader component.
45+
46+
In the next section, we’ll walk through how to prepare such an image and modify the application accordingly.
47+
48+
Now its a good time to read [the ROM-loader chapter at related article](https://developer.espressif.com/blog/esp32-bootstrapping/#rom-loader).
49+
50+
## Image build
51+
52+
The image loadable by the ROM code is generated using the `esptool elf2image` command. To hide the non-RAM segments from the ROM loader view, we use the `--ram-only-header` argument.
53+
54+
```mermaid
55+
flowchart LR
56+
A -->|linker| B
57+
B -->|post build| C
58+
C -->|post process| D
59+
A@{ shape: rounded, label: "**Build**</br>*(west build)*" }
60+
B@{ shape: rounded, label: "**.elf**</br>*(objcpy .bin discarded)*" }
61+
C@{ shape: rounded, label: "**esptool.py**</br>*elf2image*</br>*--ram-only-header*" }
62+
D@{ shape: rounded, label: "**.bin**</br>*(ESP image format)*" }
63+
```
64+
65+
To produce a compliant binary image named `zephyr.bin` from the input ELF file `zephyr.elf`, the following command can be used:
66+
67+
```shell
68+
esptool.py elf2image --ram-only-header -o zephyr.bin zephyr.elf
69+
```
70+
71+
This command creates an image where the exposed initial segments are SRAM-loadable and thus acceptable to the ROM loader. These are followed by additional segments intended for cache mapping, which cannot be processed during the initial boot phase.
72+
73+
Importantly, the image header only declares the number of RAM-loadable segments. As a result, the flash segments are effectively hidden from the ROM loader and must be handled later during runtime.
74+
75+
This means that a minimal bootstrap routine must be executed as part of the loaded application. Its responsibility is to remap or reinitialize the remaining segments—typically code or data located in flash—so that the system can transition from minimal execution to full application runtime.
76+
77+
As a rule of thumb, the image header will account for all SRAM segments. Any segments that cannot be loaded by the ROM loader will be placed after the declared number of segments, making them invisible to the ROM loader.
78+
79+
{{< alert icon="eye" >}}
80+
- The 1-byte checksum is placed after the last SRAM loadable segment, following by the rest of the segments.
81+
- SHA256 digest is not implemented and it is disabled in the image header.
82+
{{< /alert >}}
83+
84+
In the next section, we’ll explore how to implement this runtime remapping and integrate it into the startup sequence.
85+
86+
## Image runtime
87+
88+
First, the ROM loader identifies the image header and calculates a 1-byte checksum over the number of segments specified in the header. If the checksum matches, the ROM loader transfers control to the entry point address.
89+
90+
Once the clock and flash subsystems are initialized, it is time to handle the memory-mapped segments that were hidden from the ROM loader. This is done by parsing the image's segment headers and identifying their target virtual memory addresses (VMAs). As a result, the real number of image segments is calculated.
91+
92+
After collecting all the "invisible" segments, their load memory addresses (LMAs) must be corrected, as the `elf2image` tool may have reordered the segments in the binary. At this point, these segments are mapped into their correct memory locations, and the system can proceed with the standard initialization process.
93+
94+
{{< alert icon="eye" >}}
95+
- Image parsing identifies segments whose VMAs fall within the cache's address range
96+
- The end of the segment is indicated by the `load_addr` field in the header having the value `0xFFFFFFFF`
97+
{{< /alert >}}
98+
99+
Typically, there are exactly two such mapped segments: one in the IROM (instruction ROM) region and one in the DROM (data ROM) region, which are located in the CACHE address space.
100+
101+
102+
```mermaid
103+
flowchart LR
104+
ROM ==> SRAM
105+
FLASH .->|1.Load| ROM
106+
SRAM .->|2.Scan| FLASH
107+
SRAM .->|3.Map| B
108+
FLASH .->|3.a| CACHE
109+
SRAM ==>|Platform init| CACHE
110+
PSRAM .->|3.b| CACHE
111+
CACHE ==>|System init| INIT
112+
113+
subgraph A ["1st stage"];
114+
ROM;
115+
SRAM;
116+
end;
117+
118+
subgraph C ["Application start"];
119+
INIT;
120+
end;
121+
122+
subgraph B ["2nd stage"];
123+
CACHE;
124+
FLASH;
125+
PSRAM;
126+
end;
127+
128+
ROM@{ shape: rounded, label: "**ROM loader**</br>(ROM code)" };
129+
SRAM@{ shape: rounded, label: "**Simpleboot**</br>(RAM code)" };
130+
CACHE@{ shape: rounded, label: "**CACHE**</br>(.code)</br>(.rodata)" };
131+
FLASH@{ shape: rounded, label: "**FLASH**</br>(0x0)</br>(0x1000)" };
132+
PSRAM@{ shape: rounded, label: "**PSRAM**</br>" };
133+
INIT@{ shape: rounded, label: "**INIT**</br>(z_prep_c)</br>(z_cstart))" };
134+
135+
```
136+
137+
## Using the Simple Boot in Zephyr
138+
139+
To create single image ESP32 builds in Zephyr, we simply build the code for the target board. The configuration option `CONFIG_ESP_SIMPLE_BOOT` is enabled by default.
140+
141+
```shell
142+
west build -b <full/board/qualifier> <sample/code/path>
143+
```
144+
145+
The configuration option that separates the single image (or simple boot image) from the bootloader supported image is selecting the bootloader. Adding the `CONFIG_BOOTLOADER_MCUBOOT=y` tells the build system to create the image that should be loaded by the MCUboot, to disable `CONFIG_ESP_SIMPLE_BOOT`.
146+
147+
## Conclusion
148+
149+
Single-image binaries reduce build and flash times, making them well-suited for testing and lightweight development workflows. However, this simplicity comes with certain limitations:
150+
151+
- Fixed boot address
152+
- Supported for only one image format
153+
- No support for OTA updates
154+
- No flash encryption now (can be implemented in the future)
155+
156+
While this simplified boot approach is not suitable for all applications, it can effectively serve specific use cases—particularly during development, prototyping, or continuous integration scenarios where speed and simplicity are prioritized over advanced features.
157+
158+
## Additional readings
159+
160+
- [ESP32 bootstrapping article](https://developer.espressif.com/blog/esp32-bootstrapping/).

0 commit comments

Comments
 (0)