In the third and final part of this series, I'd like to discuss another implementation option for a bootloader. The difference with this variant is that the bootloader isn't a standalone firmware project, but is integrated into the main application. This gives the application a way to perform an update itself without (completely) interrupting other firmware functions. If you'd like to reread the first two parts first, you can find them here:
Problem description
Let's imagine a microcontroller firmware that can interact with a user via a user interface. The firmware is also responsible for reading sensor data and controlling actuators. The firmware can independently download and install software updates from a server. The problem: With a traditional update, the user would first have to put the firmware into update mode, where the new firmware is then installed using the bootloader. This has the following disadvantages, among others:
- The user cannot access the remaining functions of the firmware during the update.
- The bootloader needs extensive knowledge of the hardware so that it can keep all actuators in a secure state.
- The bootloader may need to handle a complex communication stack (in this case, Wi-Fi). This not only requires a lot of memory but also makes it prone to errors. This may also require bootloader updates.
- Since the application software usually also requires this communication stack, it is always duplicated. If the stack is adapted for the application, this must either be done for the bootloader, or the host software must support two different variants of the protocol.
- If something goes wrong during the update, the firmware will no longer be usable until a new update is installed.
The first point is usually the most problematic. Long update times can be frustrating for users. Therefore, it can be advantageous to run the firmware update in the background, without the user noticing.
Solution
As described, it would be desirable for the firmware to update itself without affecting its other functions. However, there are the following problems:
- Firmware running on flash memory cannot overwrite itself.
- As long as the flash is being written to, CPU stalling typically occurs. The CPU waits until the write operation is complete before loading the next instruction from the flash. This causes the application to run significantly slower.
To solve this problem, some microcontroller manufacturers (examples include Microchip and ST) have released a wide range of microcontrollers whose flash memory consists of two symmetrical flash banks. This has the advantage that the CPU can simultaneously execute instructions from one bank while writing to the other bank.
Physical and virtual addresses
To understand how the update works, you first need to know that there is a difference between physical and virtual addresses. Basically, both flash banks, as well as RAM and the controller's registers, have their own physical address range. However, these individual physical addresses are virtual mapped to a larger overall area. This means that the software usually doesn't know the physical address of the flash, but only accesses the virtual addresses. Using the virtual address, the software can then directly access registers, RAM, or one of the two flash banks. Typically, the flash banks are mapped according to the following scheme:
To fully utilize the virtual address range, we can either write a firmware so large that it spans the entire virtual program memory. The linker only knows the virtual addresses when creating the firmware, so in this case, the developer doesn't need to know how the flash banks are allocated. Alternatively, we can limit our firmware to half the virtual program memory. This ensures that bank 2 always remains free and this area is available for updates. After an update, the mapping can then simply be changed to the following scheme:
This means that after a reset, the firmware on bank 2 is always started. The old firmware remains on bank 1, even if it is no longer accessed.
The update process
But what does the update process look like? Basically, like any other update software, the update software, which is part of the main application, must ensure that it does not delete itself and only writes to the flash bank from which it is not executed. This can be achieved quite easily by always incrementing the address by half the virtual address when writing to the flash. Program data that actually belongs at address zero is thus written to address 0 + half program memory (and thus to the beginning of the second flash bank). If the program memory is mapped so that Bank 1 is at virtual address 0, then writing is always done to Bank 2. If Bank 2 is at virtual address 0, writing is always done to Bank 1.
By splitting the memory into two flash banks, firmware running on one flash bank can perform an update on the other flash bank without causing CPU stalls. This allows the update to occur in the background, while the firmware can continue processing its other tasks in parallel. Especially when an operating system is being used, and the update process runs in a low-priority thread, other functions are barely affected.
Once the firmware update is complete, the application can remap the two flash banks and start the new firmware by jumping to the beginning of the virtual address range.
Do I still need a classic bootloader?
Essentially, the procedure described above doesn't completely replace the classic bootloader. For example, it lacks a check of the application memory. Furthermore, depending on the controller, the mapping of the flash banks is reset after a reset. Therefore, a piece of code may be required to decide which flash bank to access. Accordingly, the mapping of the address ranges must be performed by the bootloader:
Let's first look at the PIC32MZ. In addition to program memory, it has a so-called bootflash. This is reserved, among other things, for the bootloader. Upon reset, the PIC32MZ always boots from the bootflash. Although the PIC32MZ has a bootloader pre-installed in the bootflash, it always jumps to bank 1. Therefore, for this controller family, the standard bootloader (provided by Microchip as a software project) must be adapted accordingly.
With "dual-flash" variants of the STM32, things are a little different. Here, a pre-installed bootloader is located in the so-called "system memory." This bootloader decides whether to jump to Bank 1 or Bank 2 based on the BFB2 ("Boot from Bank 2") bit in the "option bytes." The option bytes are also located in the flash, so they are not reset upon reset. If the BFB2 bit is set, the microcontroller first boots into the pre-installed bootloader after a reset, which then starts the application in Bank 2. If the bit is cleared, the bootloader is not run during startup, and instead the application on Bank 1 is executed.
This means that dual-flash versions of the STM32 do not require a bootloader. However, this requires that the memory area is verified after an update—i.e., you check whether the update was successful—before changing the BFB2 bit. According to Application Note AN2606, the pre-installed bootloader on the ST also checks whether Bank 2 contains "valid code." However, this check is not a checksum check or anything similar. Instead, the Reference Manual for the STM32L0X2 (RM0376) states: "The code is considered as valid when the first data located at the bank start address (which should be the stack pointer) points to a valid address (stack top address)." If the update was performed incorrectly, or if the memory becomes corrupted after a successful update, the bootloader won't detect it. In addition, if an update is performed from bank 2 to bank 1, so that the current code is subsequently located in bank 1, a check is never performed at startup.
So, as I said, I have to check whether I've performed the update correctly before changing the BFB2 bit. This at least prevents a faulty firmware from booting immediately after the update.
Conclusion
Microcontrollers with two symmetrical flash banks enable intelligent update methods. The additional flash bank allows a firmware update to be performed automatically without having to configure its other functions.
Another interesting advantage of this technology: In case the firmware on one of the two flash banks becomes corrupted in the field, I still have an older, working firmware version stored as a backup. This means that even if parts of the flash become corrupted, the system won't completely stop working, provided I'm able to detect the defect.
It should be mentioned, however, that this whole thing also has a downside. I always have to configure my system so that my microcontroller has twice the flash memory than my firmware would require with a traditional update process. Of course, I have to pay for this memory.






