Bootloaders are included in nearly every embedded system and provide a great way to update application code in the field without having to access a programming port. As important as bootloaders are, developers often get tripped up trying to jump from the bootloader into their application code. The jump needs to be clean but there are several factors that cause issues such as:
- Write once registers (ex. The watchdog register)
- Clock settings
- Stack and program pointers
- Peripheral settings
There are two different ways that a developer can cleanly transition from the bootloader to the application code. The first method involves a developer carefully matching their application code and bootloader settings. For example, a developer would match watchdog registers, clock settings and maybe even peripherals such as UART. The bootloader initializes all these components to a known system state and simply hands off the program execution to the application code. The problem with this is that any changes in the application code also need to be changed in the bootloader.
The second and cleaner way to handle the jump between the bootloader and the application code is to follow a simple process that reverts any settings touched by the bootloader back to their original reset states. This would include peripherals such as GPIO, clocks and even making modifications to the stack and program counters. When a developer does this, the application code is completely unaware that there was another application executing in memory. The only settings that would need to be matched would be registers that can only be written once!
The process for preparing to make the jump from bootloader to application code is straightforward and process can be found below:
- Verify that the application reset vector has been programmed
- Verify application checksum and security credentials
- De-initialize peripherals and place them in their reset state
- Set the vector table register to the application reset vectors (ARM)
- Set the stack pointer register to the application start address
- Set the program counter to the reset vector (Jump to the application)
- Cross your fingers!
Let’s briefly discuss each step. Before the bootloader does anything, it needs to verify the application integrity. The first step is to verify that the reset vector has been programmed and is not 0xFFFFFFFF or 0x00000000. Typically, erased flash will have all its bits set so if we see that state then bootloader knows something is wrong and should remain in the bootloader which is a known and safe state. Keep in mind that we are assuming that any programmed value in between is correct, which might be a bad assumption but for today good enough.
Next, the bootloader should perform a checksum calculation over the application space to make sure that the application is valid. Once again, if something went wrong during the update process, there could be a partial program with a reset vector programmed and there would be no way to know this without a checksum. On top of this, the bootloader should also check any digital signing or security measures to make sure that not only the application is intact but that its source was also correct and it isn’t malware or a modified program.
Once the bootloader has verified that the application is intact and from the correct source, it’s time to get back to an original state. Any peripheral that was touched that is NOT write once should be put back to their reset state. The best way to do this is to review the drivers that are used, the registers they initialize and access and then look those registers up in the datasheet. Every datasheet shows what the power-on reset register values are. For every register that was modified, they can be reset to these states. Typically, I will avoid changing clock registers, watchdog and write once registers. I simply match these states between the application and the bootloader.
At this point, the microcontroller is back at the reset state and ready to run the application code. Before doing so, the vector table register needs to be updated to point to the vector table location for the application rather than the bootloader. The interrupt vector table could be located anywhere so there does need to be coordination between the bootloader and application linker files. For example, a developer would write a single line of code as follows where PROGRAM_FLASH_BASE is the first vector table location of the application:
SCB_VTOR = (uint32_t) PROGRAM_FLASH_BASE;
Once this has been done, it is time to jump to the application. There are several different ways that a developer can make the jump from the bootloader to the application. One way would be to simply dereference the reset vector location of the application. The problem with that is that the stack pointer may not be in the correct place and strange behavior could result. Ideally, a developer will set the stack pointer and then the program counter. How this is done will vary from one microcontroller to the next. Almost always, this needs to be done using inline assembly code (the one time I advocate that it is okay to write assembly code). For an ARM microcontroller, an example code snippet can be seen below:
void Flash_StartApplication(uint32_t startAddress)
asm(” ldr SP,[r0,#0]”);
The exact code is going to vary slightly based on the compiler that is used. Inline assembly is not in the C standard so each compiler vendor has implemented it in a different way or in some instances not at all! Let’s examine what this is doing.
To minimize the assembly language code, it is critical that the assembly language code be wrapped in a C function. The reason for this is that when startAddress is passed into the Flash_StartApplication function, it is stored automatically in register r0. With that knowledge, there is no reason to add extra assembly language instructions to load the desired start address into the register. (Yes, it saves us one assembly instruction but doing it this way is also more maintainable and flexible). The first assembly instruction is then to take the value stored in register r0 with 0 offset (#0) and copy it into the stack pointer (SP) register. The second instruction then tells the processor to take the value stored in r0 with an additional offset of 4 (#4) and copy it into the program counter (PC) register. The offset 4 is essentially adding 4 to the value stored in r0. The next instruction executed will then be the reset vector of the application code. We’ve just successfully jumped into the application!
That’s all there is to it! Following this process, you can now easily make the jump from a bootloader to your application code and ensure that it will behave the way that you expect it to.
If you are interested in learning more about bootloaders, please read Jacob’s Bootloader Design Techniques White Paper. If you are interested in learning about bootloaders hands-on, contact firstname.lastname@example.org to learn more about his Bootloader Design Techniques course.