There are multiple strategies of firmware updates, each has their own pros and cons:
- single stable bootloader that selects firmware to boot from and jumps to the starting address. Purely software solution, but requires at least two binaries - bootloader and actual application. On the positive side, you can have as many application versions as fit in the flash. Bootloader resides in the firmware entry point and handles updates
- RAM bootloader gets loaded and is run from memory. Handles obtaining and overwriting actual application firmware on the flash. This is popular for large firmware images, where only one can be fit on the flash at the same time
- Physically multiple flash memories, each with their own application, orchestrated by the bootloader on the MCU
- loads of variations on above
Apart from just selecting bank to boot, you also have an option to switch banks on the fly. Meaning, that you don't even have to reset your device to run the new firmware. You just have to be very very careful with all the remaps. Application note AN4767 deals with that.
Since I don't have to have 100% real uptime, I decided that occasional resets are fine. So, the idea is to run firmware from flash bank 1. Once update is initiated, we save firmware to the bank2 and tell the system bootloader, that on next reset we want to boot into that.
My trusty Discovery board has STM32F429ZIT6 on it. Which means, that it has 2MB of flash memory divided into 24 sectors from 0th to 23rd. First 4 sectors are 16KB each, 5th is 64k, then 7 sectors x 128kB. That's bank 1, exactly half, from address 0x0800 0000 to 0x080F FFFF. Bank 2 is exactly the same, starting from address 0x0810 0000. Table 6 in RM0090 shows this layout.
FLASH_OPTCR control register has bit 4 "Dual-bank Boot option byte" or BFB2. From reference manual and appnotes it is not exactly clear, how this magic works, so I took my time to figure it out. Setting BFB2 bit forces booting into system memory, which checks, whether there is valid data (reset vector) at the start of the Bank2. If it is present, it proceeds with booting from Bank2. If not, it checks Bank1 for the same. This register is stored on flash, so it's not volatile.
Aliasing vs remapIf booting happens from address 0x0000 0000, how does it get to flash beginning address 0x0800 0000? Well, actual flash address gets aliased to the beginning of address space. Apparently, this is done by the hardware. If BFB2 bit is set, address 0x0810 0000 gets aliased.
Remap is the feature, that defines which flash bank is reachable by the addresses 0x0800 0000 and 0x0810 0000. By default first is Bank1 and second is Bank2. If remap is enabled, it's vice versa - 0x0800 0000 is Bank2 address and 0x0810 0000 is Bank1.
Write protectionEach bank and its option bytes are write-protected. To reprogram them, you need to unlock them by writing correct keys to KEY registers. This is done to prevent accidental overwrites and bricking.
OTA procedureOk, now my imagined procedure. We start in the normal operation mode, where device does what it is intended to do. Then it receives request for firmware update:
- Disable all running processes and unnecessary interrupts
- free enough RAM for firmware
- Get firmware, put it in RAM
- Check firmware image in RAM against CRC provided with it
- Calculate how many sectors image takes
- Unlock flash
- Erase sectors for firmware
- Write firmware to flash
- Verify flash contents against CRC
- Lock flash
- Unlock option bytes
- Toggle BFB2
- Lock option bytes
Gocha!When booted from Bank2 (BFB2 set), Bank2 is aliased to address start, so we boot from it. HAL_FLASHEx_Erase() function takes absolute sectors as a parameter. Meaning, that first sector of Bank1 is FLASH_SECTOR_0, but first sector of Bank2 is FLASH_SECTOR_12
What is not clearly stated anywhere, is the fact that banks are remapped, when booting with BFB2. Which means, that whichever bank you are booting from, it's always on address 0x0800 0000 and the other one is on 0x0810 0000. So, when performing an update, writing always should be done to 0x0810 0000.
Now that I think about it, it kind of makes sense - compilation of the code is always done against Bank1 address space, and it should be reachable from any bank. Seems obvious, once you figure it out, but might not be so for the first timers.
Now should figure out a few safeguards:
- firmware stability tracking - we don't want to have frequent crashes of the new flashy firmware. System should gracefully revert to older but stabler version. Probably need a watchdog with some sort of uptime or crash-count tracking;
- ensure, that we don't attempt to boot into broken firmware slot (vector table present, but no actual firmware)
- it would be nice to have one of the first small sectors for non-volatile data storage. Seems a bit of waste to store couple of bytes in 128k sector, but that's ok for now.