STM32F103 – Custom Bootloader Stack Pointer Issues

bootloadercmicrocontrollerstack-pointerstm32

So, I am trying to use this USB mass storage bootloader for the STM32F103C8 (64kB flash):
https://github.com/sfyip/STM32F103_MSD_BOOTLOADER

To compile .hex files for it, I've already set the flash origin to 0x8004000 and the vector table offset to the same value. I've verified that, indeed, the generated hex file starts at 0x8004000 and through debug I've verified that SCB->VTOR is the right value. However, I still could run a simple blink app only from the debug from STM32CubeIDE – when I uploaded the firmware to the 0x8004000 address either via ST-link or via the bootloader, the blink did not work.

However, I then took a look at the example hex file of a blink project the bootloader creator provides. Here I saw that the first value of the hex file is 0x20000408 – the initial stack pointer value (in RAM space), if I understand everything correctly. In the bootloader hex itself, the initial stack pointer value is 0x20001250 (compiled with keil uVision 5). However, in all applications I compile from Stm32CubeIDE that I use, the value is 0x20005000. That is when I started to edit my hex files and play with it.

What I discovered is that any values lower than 0x20005000 – I've tried 0x20000408, 0x20002000, 0x20003000, 0x20004000 and even 0x20004999 – do work, they successfully upload to the microcontroller both with ST-link and the bootloader. The bootloader flawlessly jumps to the main application and the blinking starts. But with the value 0x20005000, the application does not work (larger values obviously don't work since they are beneath the RAM space). During debug I discovered that with 0x20005000 value the microcontroller jumps into the hard fault handler, with 0x20004999 it does not.

The jump to main application in the bootloader is performed with this code, where APP_ADDR is 0x8004000:

  uint32_t jump_addr = *((__IO uint32_t*)(APP_ADDR+4u));
  HAL_DeInit();
  /* Change the main stack pointer. */
  __set_MSP(*(__IO uint32_t*)APP_ADDR);
  SCB->VTOR = APP_ADDR;
  
  ((void (*) (void)) (jump_addr)) (); 

Everything seems fine, the MSP is set according to the hex file (0x20005000), Vector offset is set to 0x8004000 (which is set again in my app in SystemInit()) and the jump is performed.

  1. What is going on here, why can't I set initial stack pointer to 0x20005000?
  2. Am I breaking something by changing it? Will apps more complex than blink() work?
  3. Could the problem be related to the fact that the bootloader is compiled with Keil uVision, while I use Stm32CubeIDE? Perhaps, different linker settings? If so, what do I need to change in the keil uVision linker?
  4. If I do need to set the initial stack pointer to another value, how do I do it in Stm32CubeIDE, not by modifying the .hex file?
  5. Could this issue be common for all bootloaders?
  6. Am I even on the right track, or the issue is deeper than I think?

Best Answer

Normally in ARM systems, setting the stack pointer is done by hardware, but apparently not in this bootloader scenario(?).

So we have to treat it just like any old-fashioned microcontroller program where we set the SP manually. Generally speaking:

  • C programming is enabled at the point where you leave the function setting the stack pointer. You shouldn't normally write C code in that same function, or in case you do, don't declare any variables.
  • The C compiler doesn't understand what inline asm and similar code used for setting the SP does.
  • Therefore, in the function where you set the stack pointer, you cannot declare any local variables because those risk getting allocated by the stack. And if you allocate a variable on the stack before you have told the program where the stack is... well, that's obviously going to to end up badly.

In your code you have uint32_t jump_addr and this is a local variable with automatic storage. If you are lucky, the compiler places it in a CPU register but it is by no means required to do so. We have to ensure that it doesn't end up on the stack. Ideally we should be able to declare it as register but this is just a recommendation to the compiler, not a direct order.

A more reliable way might be to declare the variable as static const jump_addr = .... This makes the assumption that such a variable ends up in flash. Otherwise, if it ends up in .data, that might not improve the situation since .data is probably not initialized this early on in the CRT code. If you use this declaration you must verify in the .map file where the variable ended up.

The best solution ends up as not declaring the variable at all:

( (void(*)(void))(APP_ADDR+4u) ) (); 

This should result in the function code getting baked into the machine code and not placed in a memory area which is unreliable at this point.

Alternatively, you could write this whole function in inline assembler and ensure to use a register for the address.

A brief guide for how to implement the whole CRT startup code yourself can be found here: https://stackoverflow.com/a/47940277/584518