Writing custom bootloader for STMF303RE ARM Cortex M4 microcontroller

In this post we will go through the process of writing a custom STM32303RE bootloader. We will use the reference manual to find out all relevant details and use STM32CubeIDE for development. The actual board used during the development is STM32 Nucleo-64 board. Reference manual for the MCU can be found here.

Flash memory organization

The reference manual states the following:

The memory organization is based on a main memory block containing 128 pages of

2 Kbytes in STM32F303xB/C and STM32F358xC devices, 256 pages of 2 Kbytes in the

STM32F303xD/E and an information block as shown in Table 7. In STM32F303x6/8 and

STM32F328x8 devices the memory block contains 32 pages of 2 Kbytes.

What does this means for us. It means that when we start writing our custom bootloader, we will need to occupy multiple of 2KiB for our firmware. For our need, we will occupy only the first 12Kib in range (0x0800 0000 – 0x0800 2FFF), and leave the remaining flash to application firmware.

Startup procedure for ARM Cortex-M4 MCU

Before we start writing the bootloader, we need to understand how the MCU actually gets to our main(void) function. To understand this we need to refer to ARMv7-M Architecture Reference Manual.

B1.5.3 The vector table

On reset, the processor initializes the vector table base address to an IMPLEMENTATION DEFINED address. Software
can find the current location of the table, or relocate the table, using the VTOR, see Vector Table Offset Register,
VTOR on page B3-657.

Address of the VTOR register is 0xE000ED08. The proof is show below.

Strange thing here is that we have somewhat confusing documentation regarding the default value. B3.2.2 states that it’s implementation defined, and SCB register table says it is 0.Let’s look at the actual value of the register in the debugger.

VTOR register value

We can see that the actual VTOR register contains value 0. But how can this be since our flash is starting from address 0x08000000. To answer this section we need to open reference manual of the actual MCU.

Boot from main Flash memory: the main Flash memory is aliased in the boot memory

space (0x0000 0000), but still accessible from its original memory space (0x0800

0000). In other words, the Flash memory contents can be accessed starting from

address 0x0000 0000 or 0x0800 0000

So it seems that the address 0x0000 0000- is alias of flash starting at address 0x0800 0000.
Let’s actually check this by comparing addresses 0x0000 0000 – 0x0000 0000F and 0x8000 0000 – 0x8000 000F

It looks like it is the same, and the address 0x0000 0000 is addressable from the MCU as stated in the reference manual.

The execution plan

To realize this project we need to create a list of tasks that we need to perform in order to realize our bootloader. Here are the bullets.

  • Update linker script to force place all our code in first flash page
  • Update main function to do:
    • Adjust VTOR register to point to second flash page
    • Load our reset function pointer address from new VTOR and jump to it

Update linker script to force place all our code in first flash page

Linker script reflects the memory map of the MCU. Since the IDE already created the full linker script we do not have to do much except to change MEMORY segment.

Original map:

MEMORY
{
  CCMRAM    (xrw)    : ORIGIN = 0x10000000,   LENGTH = 16K
  RAM    (xrw)    : ORIGIN = 0x20000000,   LENGTH = 64K
  FLASH    (rx)    : ORIGIN = 0x8000000,   LENGTH = 512K
}

Modified map:

MEMORY
{
  CCMRAM    (xrw)    : ORIGIN = 0x10000000,   LENGTH = 16K
  RAM    (xrw)    : ORIGIN = 0x20000000,   LENGTH = 64K
  FLASH    (rx)    : ORIGIN = 0x8000000,   LENGTH = 12K /* now 12K */
}

Update main function

Main function should now adjust VTOR register to reflect the new vector table location.
We need to to this because our application firmware has it’s own list of interrupt handlers shipped. If we left the bootloader vector table as default vector table, all interrupts configured by our application would not be triggered.

Original main function:

int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {

    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

Modified main function:

int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  // Adjust VTOR
  SCB->VTOR = 0x8003000;
  // Define reset address as double pointer and finally resolve it to actual new reset handler.
  void**reset_handler_address = (void**)0x8003004;
  void (*new_reset_handler)(void) = (void(*)(void))*reset_handler_address;
  // jump to new reset handler
  new_reset_handler();

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {

    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

Now we simply need to compile and download the code to board. The code itself wont do anything since we do not have our application starting at address 0x0800 0300.

Creating new project for our firmware that will be kick-started by bootloader

Let’s create new project with out STM32CubeIDE and do some modifications before uploading the actual code to MCU.

Let’s update our link script memory block from

MEMORY
{
  CCMRAM    (xrw)    : ORIGIN = 0x10000000,   LENGTH = 16K
  RAM    (xrw)    : ORIGIN = 0x20000000,   LENGTH = 64K
  FLASH    (rx)    : ORIGIN = 0x8000000,   LENGTH = 512K
}

to following values:

MEMORY
{
  CCMRAM    (xrw)    : ORIGIN = 0x10000000,   LENGTH = 16K
  RAM    (xrw)    : ORIGIN = 0x20000000,   LENGTH = 64K
  /* before was   FLASH    (rx)    : ORIGIN = 0x8000000,   LENGTH = 512K */
  FLASH    (rx)    : ORIGIN = 0x8003000,   LENGTH = 500K
}

Let’s also modify the main function so that we get a visual indication form it (green led flashing).

int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART2_UART_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	  HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
	  for(int i = 0; i < 1000000; i++);

    /* USER CODE END WHILE */


    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

Everything else should be the same. We now just need to compile and download the firmware to our MCU.

How do we know that bootloader is working?

Let’s get back to bootloader project and debug the firmware. Since the bootloader does not have led code (no blinking), if the board starts blinking, we know that application flash is unchanged and that our bootloader jumped to application firmware main function.

After we resumed the code, board started blinking – our bootloader is working!

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *