Bootloader Execution from RAM on STM32F103RB: Key Challenges

Running a bootloader from RAM on an STM32F103RB microcontroller presents several technical challenges, particularly when it comes to managing the reset vector and configuring the linker script to ensure proper execution. The STM32F103RB, based on the ARM Cortex-M3 architecture, typically executes code from Flash memory. However, there are scenarios where running the bootloader from RAM is necessary, such as when the bootloader needs to update itself or when Flash memory is unavailable during certain operations.

The primary challenge lies in the fact that the reset vector, which is the first piece of code executed after a reset, must reside in Flash memory. This is because the microcontroller always starts execution from the Flash memory address 0x08000000. Once the reset vector is executed, the bootloader code needs to be copied to RAM and executed from there. This requires careful configuration of the linker script and the bootloader code itself to ensure that the reset vector is placed in Flash while the rest of the bootloader code is placed in RAM.

Additionally, the interrupt vector table, which is typically located at the beginning of Flash memory, must also be managed carefully. If the bootloader is running from RAM, the interrupt vectors must be redirected to the corresponding handlers in RAM. This adds another layer of complexity to the bootloader implementation.

Reset Vector Placement and Linker Script Configuration

The reset vector is a critical component of any embedded system, as it defines the initial entry point of the program after a reset. On the STM32F103RB, the reset vector must be located in Flash memory at address 0x08000000. This is because the microcontroller’s hardware is designed to fetch the initial stack pointer value and the reset handler address from this location upon reset.

To run the bootloader from RAM, the reset vector in Flash must be configured to jump to a small startup routine that copies the bootloader code from Flash to RAM and then branches to the RAM-based bootloader. This requires modifying the linker script to ensure that the reset vector is placed at the correct Flash address while the rest of the bootloader code is placed in RAM.

The linker script must define two memory regions: one for Flash and one for RAM. The reset vector and the startup code must be placed in the Flash region, while the main bootloader code and the interrupt vector table must be placed in the RAM region. The linker script must also ensure that the addresses of the RAM-based code are correctly calculated and that the startup code can correctly copy the bootloader code to RAM.

For example, the linker script might look something like this:

MEMORY
{
  FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 16K
  RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}

SECTIONS
{
  .isr_vector : {
    *(.isr_vector)
  } > FLASH

  .text : {
    *(.text)
    *(.rodata)
    *(.rodata.*)
    *(.glue_7)
    *(.glue_7t)
    *(.eh_frame)
    KEEP(*(.init))
    KEEP(*(.fini))
  } > RAM

  .data : {
    *(.data)
  } > RAM AT > FLASH

  .bss : {
    *(.bss)
    *(COMMON)
  } > RAM
}

In this example, the .isr_vector section, which contains the reset vector and the interrupt vector table, is placed in Flash memory. The .text section, which contains the main bootloader code, is placed in RAM. The .data section, which contains initialized data, is also placed in RAM but is loaded from Flash memory at startup. The .bss section, which contains uninitialized data, is placed in RAM and is initialized to zero at startup.

The startup code, which is placed in Flash, must copy the .text and .data sections from Flash to RAM and then branch to the RAM-based bootloader. This can be done using a simple memory copy routine:

void startup(void) {
  extern uint32_t _etext;
  extern uint32_t _sdata;
  extern uint32_t _edata;
  extern uint32_t _sbss;
  extern uint32_t _ebss;

  // Copy data section from Flash to RAM
  uint32_t *src = &_etext;
  uint32_t *dst = &_sdata;
  while (dst < &_edata) {
    *dst++ = *src++;
  }

  // Zero initialize the BSS section
  dst = &_sbss;
  while (dst < &_ebss) {
    *dst++ = 0;
  }

  // Jump to the main bootloader code in RAM
  void (*bootloader)(void) = (void (*)(void))0x20000000;
  bootloader();
}

This startup code copies the .text and .data sections from Flash to RAM and then jumps to the RAM-based bootloader. The bootloader code must be compiled and linked to run from RAM, and the linker script must ensure that the addresses are correctly calculated.

Implementing a Two-Stage Bootloader for RAM Execution

Given the complexity of managing the reset vector and linker script configuration, a two-stage bootloader approach is often the most straightforward solution. In this approach, a small first-stage bootloader is placed in Flash memory. This first-stage bootloader is responsible for copying the main bootloader code from Flash to RAM and then jumping to the RAM-based bootloader.

The first-stage bootloader is typically very small and simple, consisting of just enough code to perform the memory copy and branch to the RAM-based bootloader. The main bootloader, which contains the actual bootloader logic, is compiled and linked to run from RAM. This approach simplifies the linker script configuration and ensures that the reset vector and interrupt vector table are correctly managed.

The first-stage bootloader might look something like this:

void first_stage_bootloader(void) {
  extern uint32_t _etext;
  extern uint32_t _sdata;
  extern uint32_t _edata;
  extern uint32_t _sbss;
  extern uint32_t _ebss;

  // Copy data section from Flash to RAM
  uint32_t *src = &_etext;
  uint32_t *dst = &_sdata;
  while (dst < &_edata) {
    *dst++ = *src++;
  }

  // Zero initialize the BSS section
  dst = &_sbss;
  while (dst < &_ebss) {
    *dst++ = 0;
  }

  // Jump to the main bootloader code in RAM
  void (*bootloader)(void) = (void (*)(void))0x20000000;
  bootloader();
}

The main bootloader, which is compiled and linked to run from RAM, might look something like this:

void main_bootloader(void) {
  // Bootloader logic goes here
}

The linker script for the first-stage bootloader would be similar to the one shown earlier, with the .isr_vector section placed in Flash and the .text section placed in RAM. The linker script for the main bootloader would be simpler, as it only needs to define the RAM memory region:

MEMORY
{
  RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}

SECTIONS
{
  .text : {
    *(.text)
    *(.rodata)
    *(.rodata.*)
    *(.glue_7)
    *(.glue_7t)
    *(.eh_frame)
    KEEP(*(.init))
    KEEP(*(.fini))
  } > RAM

  .data : {
    *(.data)
  } > RAM

  .bss : {
    *(.bss)
    *(COMMON)
  } > RAM
}

This two-stage bootloader approach simplifies the linker script configuration and ensures that the reset vector and interrupt vector table are correctly managed. It also allows the main bootloader to be updated or replaced without modifying the first-stage bootloader, which can be useful in certain scenarios.

Conclusion

Running a bootloader from RAM on an STM32F103RB microcontroller presents several technical challenges, particularly when it comes to managing the reset vector and configuring the linker script. A two-stage bootloader approach, where a small first-stage bootloader in Flash copies the main bootloader code to RAM and then jumps to it, is often the most straightforward solution. This approach simplifies the linker script configuration and ensures that the reset vector and interrupt vector table are correctly managed. By carefully configuring the linker script and implementing a two-stage bootloader, it is possible to run a bootloader from RAM on the STM32F103RB microcontroller.

Similar Posts

Leave a Reply

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