STM32 Baremetal Programming

STM32 Startup script in C++

Rohit Nimkar
5 min readNov 27, 2022

A descriptive guide on how to write the startup script for the STM32F103 microcontroller in C/C++.

The startup script initializes the essential peripherals of the CPU and prepares the SRAM for program execution.

The Definitive Guide to Cortex M3 and M4 by Joseph Yiu states that:

It is possible to have startup code written in C. However, this requires importing compiler-specific symbols and in same case compiler-specific directices. So the C/C++ startup code is still toolchain dependent.

I started my embedded development from Keil and STM32CubeIDE, then I slowly moved towards using only a text editor without any IDE. I kept using the startup files provided by those IDEs.

Image credits: Shawn Hymel

In this article, I am going to explain writing the startup routine for the ARM Cortex M micro-controllers in C++. STM32F103 is the example board taken for this project, however, this can be made to work on other controllers by tweaking the address and sizes of Flash and SRAM.

Startup Sequence of Cortex M

After reset and before the processor starts executing the program, the Cortex-M processors read the first two words from the memory.

Reset sequence for cortex M
Fig. Reset sequence

The beginning of the memory space contains the vector table, and the first two words in the vector table are the initial value for the Main Stack Pointer (MSP), and the reset vector, which is the starting address of the reset handler. After these two words are read by the processor, the processor then sets up the MSP and the Program Counter (PC) with these values.

The stack pointer usually points to the end of the SRAM. This is because the stack operations in the Cortex-M3 or Cortex-M4 processors are based on a full descending stack (SP decrement before store), the initial SP value should be set to the first memory after the top of the stack region.
For the STM32F103 Blue Pill board, the main stack pointer is configured as follows:

#define SRAM_START 0x20000000U
#define SRAM_SIZE (20U * 1024U) // 20KB
#define SRAM_END ((SRAM_START) + (SRAM_SIZE))
#define STACK_START SRAM_END

Vector table definition

As mentioned earlier, the first task for the startup code is to initialize the interrupt vector table at the address 0x00000000 .

Interrupt vector table for Cortex M3
Fig. Vector table as specified by the datasheet

The following code snippet initializes the vector table in the order required by the datasheet.

uint32_t vectors[] __attribute__((section(".isr_vector"))) = {
STACK_START,
(uint32_t)Reset_Handler,
(uint32_t)NMI_Handler,
(uint32_t)HardFault_Handler,
(uint32_t)MemManage_Handler,
(uint32_t)BusFault_Handler,
(uint32_t)UsageFault_Handler,
0, // reserved
.
.
.
};

The above array would be put in the .data section by the compiler but by using the section compiler attribute, we instruct the compiler to store this array in its own separate section.

ENTRY(Reset_Handler)

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

SECTIONS
{

.isr_vector :
{
*(.isr_vector)
. = ALIGN(4);
}> FLASH

.text :
{
*(.text)
*(.text.*)
*(.init)
*(.fini)......

The above code snippet instructs the linker to put the .usr_vector section at the start of the flash memory i.e. at 0x00000000 the address relative to flash.

Reset Handler

After initializing the stack, the CPU starts execution from 0x00000004 which is the address of Reset_Handler() .

void Reset_Handler(void)
{
// copy .data section to SRAM
uint8_t *pSramData = (uint8_t *)&_sdata; // sram
uint8_t *pFlashData = (uint8_t *)&_la_data; // flash
uint32_t data_size = (uint32_t)&_edata - (uint32_t)&_sdata;
for (uint32_t i = 0; i < data_size; i++)
{
*pSramData++ = *pFlashData++;
}

// init. the .bss section to zero in SRAM
uint32_t bss_size = (uint32_t)&_ebss - (uint32_t)&_sbss;
uint8_t *pBssData = (uint8_t *)&_sbss;
for (uint32_t i = 0; i < bss_size; i++)
{
*pBssData++ = 0;
}

// now invoke main
main();
}

The reset handler copies the content of the .data section from flash to SRAM. The address of the .data section in flash is given by the linker script by the following statement. The variable/symbol _la_data marks the start of the .data section in flash memory, on the other hand, the symbol _s_data stores the start of the .data section in SRAM.

..............
*(.rodata.*)
. = ALIGN(4);
_etext = .;
}> FLASH

_la_data = LOADADDR(.data);

.data :
{
_sdata = .;
*(.data)
.............

In a similar way, the Reset_Handler() initializes the .bss section with zeroes. It then invokes the main() function.

main() function

The main function initializes the built-in LED connected to pin C13 on the Blue Pill board and blinks it in an infinite loop.

int main(void)
{
GPIO::enable_PortC();
GPIO::setMode(GPIOC, GPIO::PIN_13,GPIO::OUTPUT_2MHZ);
GPIO::setConfig(GPIOC, GPIO::PIN_13,GPIO::OUTPUT_PUSH_PULL);
while (1)
{
GPIO::toggle(GPIOC, GPIO::PIN_13);
ms_delay(1000U);
}
}

Getting the source code

The complete source code for this project is available in the following GitHub repository. Follow the instructions given in the README for successfully building and running the program on your development board.

Check out this blog on how to get started with bare metal programming on stm32 using VsCode and Makefile-based projects.

About the author

I am an embedded systems engineer working with ARM Cortex-M microcontrollers. I work on freelance projects in embedded systems as well as UI/UX design and development.

I love to read and write about Linux, Embedded C++/C, RTOS, and Operating Systems internals. Do follow me on LinkedIn for more such interesting content.

--

--

Rohit Nimkar

Know a little about coding but aim to self employ myself from it.