Keil Logo

ARM: How to Write a Bootloader


Information in this knowledgebase article applies to:

  • MDK-ARM
  • armcc or armclang

QUESTION

How can I write a bootloader properly?

ANSWER

A bootloader is a piece of code which allows user application code to be updated. The new code can be obtained using alternative download channels, such as a USB stick or a network port. After the boot ROM’s execution, the bootloader is executed and will do the update when required and then execute the end-user application.

The bootloader and the user application should be written and built as two separate µVision projects or targets, resulting in two separate and executable images/applications. The main tasks of the bootloader are to reprogram/replace the user application, if necessary, and to jump to the user application to execute it. The user application doesn't necessarily need to know the existence of the bootloader.

The bootloader is usually placed at the chips flash base address, so that it will be executed by the CPU after reset. The following figure demonstrates typical code placement of the user application and the bootloader.

There are many ways to direct the bootloader to enter programming mode to reprogram the user application into flash, or to simply jump to the existing user application to execute. The easiest way to do this is to check a GPIO pin to determine if it should enter into programming mode or not.

Most chip vendors provide users with a convenient way, such as In-System Programming (ISP) and In-Application Programming (IAP) interfaces, to be used by the bootloader to update the flash contents.

When the flash content was updated or is already up-to-date, the bootloader jumps to the user application. This requires a number of steps before the user application can be executed. This is usually done by calling a function such as the example below, BootJump(), which has the aim to basically restore reset conditions for the user application:

static void BootJump( uint32_t *Address )
{
  1. Make sure, the CPU is in privileged mode.
      if( CONTROL_nPRIV_Msk & __get_CONTROL( ) )
      {  /* not in privileged mode */
        EnablePrivilegedMode( ) ;
      }
    

    The function EnablePrivilegedMode( ) triggers a SVC, and enters handler mode (which can only run in privileged mode). The nPRIV bit in the CONTROL register is cleared which can only be done in privileged mode. See ARM: How to write an SVC function about implementing SVC functions.

  2. Disable all enabled interrupts in NVIC.
    NVIC->ICER[ 0 ] = 0xFFFFFFFF ;
    NVIC->ICER[ 1 ] = 0xFFFFFFFF ;
    NVIC->ICER[ 2 ] = 0xFFFFFFFF ;
    NVIC->ICER[ 3 ] = 0xFFFFFFFF ;
    NVIC->ICER[ 4 ] = 0xFFFFFFFF ;
    NVIC->ICER[ 5 ] = 0xFFFFFFFF ;
    NVIC->ICER[ 6 ] = 0xFFFFFFFF ;
    NVIC->ICER[ 7 ] = 0xFFFFFFFF ;
    
  3. Disable all enabled peripherals which might generate interrupt requests, and clear all pending interrupt flags in those peripherals. Because this is device-specific, refer to the device datasheet for the proper way to clear these peripheral interrupts.
  4. Clear all pending interrupt requests in NVIC.
    NVIC->ICPR[ 0 ] = 0xFFFFFFFF ;
    NVIC->ICPR[ 1 ] = 0xFFFFFFFF ;
    NVIC->ICPR[ 2 ] = 0xFFFFFFFF ;
    NVIC->ICPR[ 3 ] = 0xFFFFFFFF ;
    NVIC->ICPR[ 4 ] = 0xFFFFFFFF ;
    NVIC->ICPR[ 5 ] = 0xFFFFFFFF ;
    NVIC->ICPR[ 6 ] = 0xFFFFFFFF ;
    NVIC->ICPR[ 7 ] = 0xFFFFFFFF ;
    
  5. Disable SysTick and clear its exception pending bit, if it is used in the bootloader, e. g. by the RTX.
    SysTick->CTRL = 0 ;
    SCB->ICSR |= SCB_ICSR_PENDSTCLR_Msk ;
    
  6. Disable individual fault handlers if the bootloader used them.
    SCB->SHCSR &= ~( SCB_SHCSR_USGFAULTENA_Msk | \ 
                     SCB_SHCSR_BUSFAULTENA_Msk | \ 
                     SCB_SHCSR_MEMFAULTENA_Msk ) ;
    
  7. Activate the MSP, if the core is found to currently run with the PSP.
    if( CONTROL_SPSEL_Msk & __get_CONTROL( ) )
    {  /* MSP is not active */
      __set_CONTROL( __get_CONTROL( ) & ~CONTROL_SPSEL_Msk ) ;
    }
    
  8. Load the vector table address of the user application into SCB->VTOR register. Make sure the address meets the alignment requirements.
    SCB->VTOR = ( uint32_t )Address ;
    
    A few device families, like the NXP 4300 series, will also have a "shadow pointer" to the VTOR, which also needs to be updated with the new address. Review the device datasheet to see if one exists.
  9. Set the MSP to the value found in the user application vector table.
    __set_MSP( Address[ 0 ] ) ;
    
  10. Set the PC to the reset vector value of the user application via a function call.
    ( ( void ( * )( void ) )Address[ 1 ] )( ) ;
    
    The program flow will never return to this point.

This is the end of the function BootJump( ).

}

Define the starting address for the main application, and call the jump function with the address as a parameter:

#define USER_APPLICATION_BASE_ADDRESS 0x00008000  /* as example */

BootJump( ( uint32_t * )USER_APPLICATION_BASE_ADDRESS ) ;

Now, flash the device with both applications, to debug the jump. Use the Disassembly dialog to confirm the memory address of the instructions.

MORE INFORMATION

SEE ALSO

FORUM THREADS

The following Discussion Forum threads may provide information related to this topic.

Last Reviewed: Tuesday, February 5, 2019


Did this article provide the answer you needed?
 
Yes
No
Not Sure
 
  Arm logo
Important information

This site uses cookies to store information on your computer. By continuing to use our site, you consent to our cookies.

Change Settings

Privacy Policy Update

Arm’s Privacy Policy has been updated. By continuing to use our site, you consent to Arm’s Privacy Policy. Please review our Privacy Policy to learn more about our collection, use and transfers
of your data.