rBoot - An open source boot loader for the ESP8266
by Richard A Burton, richardaburton@gmail.com http://richard.burtons.org/
March 2021: Forked for use with Sming and partition tables.
rBoot is designed to be a flexible open source boot loader, a replacement for the binary blob supplied with the SDK. It has the following advantages over the Espressif loader:
Open source (written in C).
Supports up to 256 roms.
Roms can be variable size.
Able to test multiple roms to find a valid backup (without resetting).
Flash layout can be changed on the fly (with care and appropriately linked rom images).
GPIO support for rom selection.
Wastes no stack space (SDK boot loader uses 144 bytes).
Documented config structure to allow easy editing from user code.
Can validate .irom0.text section with checksum.
Temporary next-boot rom selection.
Limitations
The ESP8266 can only map 8Mbits (1MB) of flash to memory, but which 8Mbits to map is selectable. This allows individual roms to be up to 1MB in size, so long as they do not straddle an 8Mbit boundary on the flash. This means you could have four 1MB roms or 8 512KB roms on a 32Mbit flash (such as on the ESP-12), or a combination. Note, however, that you could not have, for example, a 512KB rom followed immediately by a 1MB rom because the 2nd rom would then straddle an 8MBit boundary. By default support for using more than the first 8Mbit of the flash is disabled, because it requires several steps to get it working. See below for instructions.
Building
A Makefile is included, which should work with the gcc xtensa cross compiler. There are two source files, the first is compiled and included as data in the second. When run this code is copied to memory and executed (there is a good reason for this, see my blog for an explanation). The make file will handle this for you, but you’ll need my esptool2 (see github).
To use the Makefile set SDK_BASE
to point to the root of the
Espressif SDK and either set XTENSA_BINDIR
to the gcc xtensa bin
directory or include it in your PATH
. These can be set as
environment variables or by editing the Makefile.
Two small assembler stub functions allow the bootloader to launch the
user code without reserving any space on the stack (while the SDK boot
loader uses 144 bytes). This compiles fine with GCC, but if you use
another compiler and it will not compile/work for you then uncomment the
#define BOOT_NO_ASM
in rboot.h
to use a C version of these
functions (this uses 32 bytes).
Tested with SDK v2.2 and GCC v4.8.5.
Installation
Simply write rboot.bin to the first sector of the flash. Remember to set
your flash size correctly with your chosen flash tool (e.g. for
esptool.py use the -fs
option). When run rBoot will create it’s own
config at the start of sector two for a simple two rom system. You can
can then write your two roms to flash addresses 0x2000
and (half
chip size + 0x2000
). E.g. for 8Mbit flash:
esptool.py write_flash -fs 8m 0x0000 rboot.bin 0x2000 user1.bin 0x82000 user2.bin
Note: your device may need other options specified. E.g. The nodemcu
devkit v1.0 (commonly, but incorrectly, sold as v2) also needs the
-fm dio
option.
For more interesting rom layouts you’ll need to write an rBoot config sector manually, see next step.
The two testload bin files can be flashed in place of normal user roms for testing rBoot. You do not need these for normal use.
rBoot Config
typedef struct {
uint8_t magic; // our magic
uint8_t version; // config struct version
uint8_t mode; // boot loader mode
uint8_t current_rom; // currently selected rom
uint8_t gpio_rom; // rom to use for gpio boot
uint8_t count; // number of roms in use
uint8_t unused[2]; // padding
uint32_t roms[MAX_ROMS]; // flash addresses of the roms
#ifdef BOOT_CONFIG_CHKSUM
uint8_t chksum; // boot config chksum
#endif
} rboot_config;
Write a config structure as above to address 0x1000
on the flash. If
you want more than 4 roms (default) just increase MAX_ROMS when you
compile rBoot. Think about how you intend to layout your flash before
you start! Rom addresses must be sector aligned i.e start on a multiple
of 4096.
magic
should have value0xe1
(defined asBOOT_CONFIG_MAGIC
).version
is used in case the config structure changes after deployment. It is defined as0x01
(BOOT_CONFIG_VERSION
). I don’t intend to increase this, but you should if you choose to reflash the bootloader after deployment and the config structure has changed.mode
can be0x00
(MODE_STANDARD
) or0x01
(MODE_GPIO_ROM
). See below for an explanation ofMODE_GPIO_ROM
. There is also an optional extra mode flag0x04
(MODE_GPIO_ERASES_SDKCONFIG
), see below for details.current_rom
is the rom to boot, numbered0
tocount-1
.gpio_rom
is the rom to boot when the GPIO is triggered at boot.count
is the number of roms available (may be less thanMAX_ROMS
, but not more).unused[2]
is padding so theuint32_t
rom addresses are 4 bytes aligned.roms
is the array of flash address for the roms. The default generated config will contain two entries:0x00002000
and0x00082000
.chksum
(if enabled, not by default) should be the xor of0xef
followed by each of the bytes of the config structure up to (but obviously not including) the chksum byte itself.
Default config
A default config sector will be created on boot if one does not exists,
or if an existing config is corrupted, and the default rom will be set
to rom 0. If you want to have a very customised config for which the
default would not be suitable, you can override the implementation in
the rboot.h
header file. See the comments and example code in
rboot.h
for more information.
GPIO boot mode
- RBOOT_GPIO_ENABLED
If rBoot is compiled with BOOT_GPIO_ENABLED
set in rboot.h
(or
RBOOT_GPIO_ENABLED
set in the Makefile), then GPIO boot
functionality will be included in the rBoot binary. The feature can then
be enabled by setting the rboot_config mode
field to
MODE_GPIO_ROM
. You must also set gpio_rom
in the config to
indicate which rom to boot when the GPIO is activated at boot.
If the GPIO input pin reads high at boot then rBoot will start the
currently selected normal or temp rom (as appropriate). However if the
GPIO is pulled low then the rom indicated in config option gpio_rom
is started instead.
The default GPIO is 16, but this can be overridden in the Makefile
(RBOOT_GPIO_NUMBER
) or rboot.h
(BOOT_GPIO_NUM
). If GPIOs
other than 16 are used, the internal pullup resistor is enabled before
the pin is read and disabled immediately afterwards. For pins that
default on reset to configuration other than GPIO input, the pin mode is
changed to input when reading but changed back before rboot continues.
After a GPIO boot the current_rom
field will be updated in the
config, so the GPIO booted rom should change this again if required.
GPIO boot skip mode
- RBOOT_GPIO_SKIP_ENABLED
If rBoot is compiled with BOOT_GPIO_SKIP_ENABLED
set in rboot.h
(or RBOOT_GPIO_SKIP_ENABLED
set in the Makefile), then a GPIO can be
used to skip to the next rom at boot. The feature must then be enabled
by setting the rboot_config ‘mode’ field to MODE_GPIO_SKIP
. This
means you do not need to have a dedicated GPIO boot rom. If you have a
rom that is technically good (valid checksum, etc.) but has operational
problems, e.g. wifi doesn’t work or it crashes on boot, rBoot will not
be able to detect that and switch rom automatically. In this scenario
rebooting the device while pulling the GPIO low will force rBoot to skip
this rom and try the next one instead. In a simple two rom setup this
simply toggles booting of the other rom.
RBOOT_GPIO_SKIP_ENABLED
and RBOOT_GPIO_ENABLED
cannot be used at
the same time. BOOT_GPIO_NUM
is used to select the GPIO pin, as with
RBOOT_GPIO_ENABLED
.
Erasing SDK configuration on GPIO boot (rom or skip mode)
If you set the MODE_GPIO_ERASES_SDKCONFIG
flag in the configuration
like this: conf.mode = MODE_GPIO_ROM|MODE_GPIO_ERASES_SDKCONFIG
;
then a GPIO boot will also the erase the Espressif SDK persistent
settings store in the final 16KB of flash. This includes removing
calibration constants, saved SSIDs, etc.
Note that MODE_GPIO_ERASES_SDKCONFIG
is a flag, so it has to be set
as well as MODE_GPIO_ROM
to take effect.
Linking user code
Each rom will need to be linked with an appropriate linker file, specifying where it will reside on the flash. If you are only flashing one rom to multiple places on the flash it must be linked multiple times to produce the set of rom images. This is the same as with the SDK loader.
Because there are endless possibilities for layout with this loader I don’t supply sample linker files. Instead I’ll tell you how to make them.
For each rom slot on the flash take a copy of the eagle.app.v6.ld
linker script from the sdk. You then need to modify just one line in it
for each rom:
irom0_0_seg : org = 0x40240000, len = 0x3C000
Change the org address to be 0x40200000
(base memory mapped location
of the flash) + flash address + 0x10
(offset of data after the
header). The logical place for your first rom is the third sector,
address 0x2000
. 0x40200000 + 0x2000 + 0x10 = 0x40202010
If you
use the default generated config the loader will expect to find the
second rom at flash address half-chip-size + 0x2000
(e.g. 0x82000
on an 8MBit flash) so the irom0_0_seg
should be:
0x40200000 + 0x82000 + 0x10 = 0x40282010
Due to the limitation of
mapped flash (max 8MBit) if you use a larger chip and do not have big
flash support enabled the second rom in the default config will still be
placed at 0x082000
, not truly half-chip-size + 0x2000
. Ideally
you should also adjust the len to help detect over sized sections at
link time, but more important is the overall size of the rom which you
need to ensure fits in the space you have allocated for it in your flash
layout plan.
Then simply compile and link as you would normally for OTA updates with
the SDK boot loader, except using the linker scripts you’ve just
prepared rather than the ones supplied with the SDK. Remember when
building roms to create them as ‘new’ type roms (for use with SDK boot
loader v1.2+). Or if using my esptool2 use the -boot2
option. Note:
the test loads included with rBoot are built with -boot0
because
they do not contain a .irom0.text
section (and so the value of
irom0_0_seg
in the linker file is irrelevant to them) but ‘normal’
user apps always do.
irom checksum
The SDK boot loader checksum only covers sections loaded into ram (data
and some code). Most of the SDK and user code remains on the flash and
that is not included in the checksum. This means you could attempt to
boot a corrupt rom and, because it looks ok to the boot loader, there
will be no attempt to switch to a backup rom. rBoot improves on this by
allowing the .irom0.text
section to be included in the checksum. To
enable this uncomment #define BOOT_IROM_CHKSUM
in rboot.h
and
build your roms with esptool2 using the -iromchksum
option.
Big flash support
This only needs to be enabled if you wish to be able to memory map more than the first 8MBit of the flash. Note you can still only map 8Mbit at a time. Use this if you want to have multiple 1MB roms, or more smaller roms than will fit in 8Mbits. If you have a large flash but only need, for example, two 512KB roms you do not need to enable this mode.
Support in rBoot is enabled by uncommenting the
#define BOOT_BIG_FLASH
in rboot.h
.
Thinking about your linker files is either simpler or more complicated,
depending on your usage of the flash. If you intend to use multiple 1MB
roms you will only need one linker file and you only need to link once
for OTA updates. Although when you perform an OTA update the rom will be
written to a different position on the flash, each 8Mbit of flash is
mapped (separately) to 0x40200000
. So when any given rom is run the
code will appear at the same place in memory regardless of where it is
on the flash. Your base address for the linker would be 0x40202010
.
(Actually all but the first rom could base at 0x40200010
(because
they don’t need to leave space for rBoot and config) but then you’re
just making it more complicated again!)
If you wanted eight 512KB roms you would need two linker files - one for the first half of any given 8Mbits of flash and another for the second half. Just remember you are really laying out within a single 8MBit area, which can then be replicated multiple times on the flash.
Now the clever bit - rBoot needs to hijack the memory mapping code to
select which 8Mbits gets mapped. There is no API for this, but we can
override the SDK function. First we need to slightly modify the SDK
library libmain.a
, like so:
xtensa-lx106-elf-objcopy -W Cache_Read_Enable_New libmain.a libmain2.a
This produces a version of libmain with a ‘weakened’
Cache_Read_Enable_New
function, which we can then override with our
own. Modify your Makefile to link against the library main2
instead
of main
.
Next add rboot-bigflash.c
(from the appcode
directory) &
rboot.h
to your project - this adds the replacement
Cache_Read_Enable_New
to your code.
Getting gcc to apply the override correctly can be slightly tricky (I’m
not sure why, it shouldn’t be). One option is to add
-u Cache_Read_Enable_New
to your LD_FLAGS
and change the order
of objects on the LD command so your objects/.a
file is before the
libraries. Another way that seems easier was to
#include rboot-bigflash.c
into the main .c file, rather than
compiling it to a separate object file. I can’t make any sense of that,
but I suggest you uncomment the message in the Cache_Read_Enable_New
function when you first build with it, to make sure you are getting your
version into the rom.
Now when rBoot starts your rom, the SDK code linked in it that normally performs the memory mapping will delegate part of that task to rBoot code (linked in your rom, not in rBoot itself) to choose which part of the flash to map.
Temporary boot option and rBoot<–>app communication
- RBOOT_RTC_ENABLED
To enable communication between rBoot and your app you should enable the
BOOT_RTC_ENABLED
option in rboot.h
. rBoot will then use the RTC
data area to pass a structure with boot information which can be read by
the app. This will allow the app to determine the boot mode (normal,
temporary or GPIO) and the booted rom (even if it is a tempoary boot).
Your app can also update this structure to communicate with rBoot when
the device is next rebooted, e.g. to instruct it to temporarily boot a
different rom to the one saved in the config. See the api documentation
and/or the rBoot sample project for more details. Note: the message
“don’t use rtc mem data”, commonly seen on startup, comes from the sdk
and is not related to this rBoot feature.