STM32 built-in bootloader

Once we have finished our main working horse algorithm (well, I know it never finished), the next thing we facing is future support. All software has bugs, and importantly we want to be able to update our device every time when some of the bugs has been fixed. 




STM did arguably good job providing built-in bootloader that can use multiple interfaces. Here I describe how to use the USB-based update mechanism dubbed DFU. DFU is an open prototcol that is well supported by many IDEs including STM32CubeProgrammer. Furthermore literally every programming language has DFU libraries, so it is fairly easy to write your own uploader if needed.

I start with STM32H7 series and I use the NUCLEO-H743ZI2 board for concreteness. 

To run the built-in bootloader, we need to do two things. First, we need to set the stack pointer to the area of RAM that bootloader expected to see. Secong, we need to change the PC register value to the entry address of the bootloader. Sounds easy, right?

It worth noticing that STM recommends also to do the following steps before jumpint to the bootloader:

  • Disable all peripheral clocks
  • Disable used PLL
  • Disable interrupts
  • Clear pending interrupts

Here is the code that effectively does it.


typedef  void (*pFunction)(void);

pFunction SysMemBootJump;

uint32_t JumpAddress;


void jump_to_dfu(void)

{

    /* STM32H7 system BootLoader address. Other MCUs might have a different one! */

    __IO uint32_t BootAddr = 0x1FF09800; 


    /* Disable all interrupts */

    __disable_irq();


    /* Jump to system BootLoader -- prepare pointers */

    JumpAddress = *(__IO uint32_t *)  (BootAddr + 4);

    SysMemBootJump = (pFunction) JumpAddress;


    /* Clear Interrupt Enable Register & Interrupt Pending Register */

    for (int i=0;i<8;i++)

    {

  NVIC->ICER[i]=0xFFFFFFFF;

  NVIC->ICPR[i]=0xFFFFFFFF;

    }


    /* De-init clocks/PLLs. Extrimely important on some MCUs. Without this line the MCU instantly reboots once jumped to the bootloader */

    HAL_RCC_DeInit();


    /* Initialize system BootLoader's Stack Pointer */

    __set_MSP(*(__IO uint32_t *) BootAddr);


    /* Jump to the system BootLoader */

    SysMemBootJump();

}



Good practice is to start the bootloader right after reboot. Otherwise few things may happen.
  • Your watchdog timer can reboot the MCU during update. Once you enabled it, there is no way to stop it (other than reboot)
  • The firmware (for example your RTOS) can enable some protection mechanisms that won't let you to write certain memeory locations or registeres. 
An easy way to implement it is by using a "magic number" that is written to RAM location that is not automatically cleared during restart. When the firmware needs to run the bootloader, it assigns a value, say 12345, and reboots. Right after start, you check the number and if it is valid, you run the bootloader. Here is the code:

/* place to the memory region that does not get initialised automatically */

__attribute__((section(".ccram_buffer")))  uint32_t magic_code;  


....

/* Need to run bootloader now! */

magic_code = 1234567;

NVIC_SystemReset();

....


int main(void)

{

....

  /* USER CODE BEGIN SysInit */


  if (magic_code == 1234567) {

    MX_GPIO_Init();

    magic_code = 0;


    jump_to_dfu();

  }

....

}

You probably noticed the attribute before our magic code definition. The section must be specified in your linker file. This would vary with different MCUs. For my board, the file name is STM32H743ZITX_FLASH.ld and the section is declared as following:

  .dma_buffer : /* Space before ':' is critical */

  {

    *(.ccram_buffer)

  } >RAM_D3


Don't change spaces, the linker file is very picky to its format. The memory area in STM32H7 called RAM_D3 is not initialised with zeros by default, so our magic number is in a safe place between reboots.

Now let me show a couple pitfalls. First, the bootloader address is different for different MCU series. You need to check the app notes AN2606 [1] to make sure you jumping the correct address. You can even add the #ifdefs to your jump_to_dfu() like this:

#ifdef STM32G474xx

  __IO uint32_t BootAddr = 0x1FFF0000; // STM32G47

#elif  STM32H743xx

  __IO uint32_t BootAddr = 0x1FF09800; // STM32H74 

#elif  STM32H7A3xx

  __IO uint32_t BootAddr = 0x1FF09800; // STM32H7A

#else

#error Define a correct bootloader address

#endif



Second, the RAM locations that are not initialised by default are not the same for different MCUs. So,  you need to figure out what sections should be used for your MCU.

Finaly, it seems that it does not work on some MCUs. Specifically, I did not manage to make it working on STM32G474QET6,  weather it is a silicon bug or I misunderstand something. If you found a way to fix it, please let me know.



 

Comments

Popular posts from this blog

Why this blog?

As simple as the timer TIM1 (not really)

Watch out! Scammers in the town.