STM32 Part 2: Initialization

Programming is breaking up something big and impossible into something small and very real.


Hello everyone, to begin with, I would like to thank the moderators for missing my first (disgusting) post, and to say hello to mom! And also I would like to thank all the readers and people who pointed out my mistakes and helped to fix them. I immediately make a reservation that in Russian I did not write from grade 6, in general, do not be angry.

STM32 Part 1: The Basics

So let's get started. In a previous article, I quickly ran through the very first points. This was our startup.c file, which was responsible for 2 vectors (stack, Reset_Handler) and a little bit about the linker script. Today we will supplement our initialization code, analyze the linker for spare parts and find out how everything works.


extern void *_estack;

void Reset_Handler();

void *vectors[] __attribute__((section(".isr_vector"), used)) = {
    &_estack,
    &Reset_Handler
};

void __attribute__((naked, noreturn)) Reset_Handler()
{
    while(1);
}


If this code is not clear to anyone, then you can read about it here (the article is not a fountain, but tried to explain).

Now let's figure out what is missing here and how to add it. Obviously, microns have other vectors. For example, the Hard_Fault vector is a vector that is called upon improper actions with ΞΌ, for example, if he tries to execute an instruction that the processor does not understand, he will jump to the address of the Hard_Fault vector. There are a lot of such vectors and it’s not a fact that in our program we will use everything. Also, interrupt vectors are in the same table.

(For those who still don’t know what a vector is, this is a pointer to an address, aka β€œpointer”).

We supplement our code, and then I will explain what I did.

extern void *_estack;

void Reset_Handler();
void Default_Handler();

void NMI_Handler()   __attribute__((weak, alias("Default_Handler")));
void HardFault_Handler()   __attribute__((weak, alias("Default_Handler")));
//     

void *vectors[] __attribute__((section(".isr_vector"), used)) = {
    &_estack,
    &Reset_Handler
    &NMI_Handler,
    &HardFault_Handler
};

void __attribute__((naked, noreturn)) Reset_Handler()
{
    while(1);
}

void __attribute__((naked, noreturn)) Default_Handler(){
    while(1);
}


Here I added a new function Default_Handler (); which will handle all interrupts, absolutely. But also with the help, __attribute__((weak, alias("Default_Handler")));I noted that if a function with the same name is declared, then it will be the handler of this interrupt. In other words, right now it's just a nickname for Default_Handler. So we can add all the vectors in the list from your Reference manual. This is done so that if you suddenly decided to use an interrupt, but forgot to create a function handler, then the usual Default_Handler is called.

I think at this point with vectors you can finish and go to the initialization of sectors and to the linker. To begin, let's update our startup.c again by adding the initialization of the data and bss sectors to the Reset_Handler body as well as several external variables.

extern void *_sidata, *_sdata, *_edata, *_sbss, *_ebss;

void __attribute__((naked, noreturn)) Reset_Handler()
{


	void **pSource, **pDest;
	for (pSource = &_sidata, pDest = &_sdata; pDest != &_edata; pSource++, pDest++)
		*pDest = *pSource;

	for (pDest = &_sbss; pDest != &_ebss; pDest++)
		*pDest = 0;

	while(1);
}


So, new variables, and again they were declared in our linker script. Recall the previous script . And also compare it with a new one.

MEMORY{
	ROM(rx) : ORIGIN = 0x08000000, LENGTH = 1M
	SRAM (rwx):     ORIGIN = 0x20010000, LENGTH = 240K
}

_estack = LENGTH(SRAM) + ORIGIN(SRAM);

SECTIONS{
	.isr_vector : {
	KEEP(*(.isr_vector))
	} >ROM
	
	.text : {
        . = ALIGN(4);
        *(.text)
    } >ROM
	
	_sidata = LOADADDR(.data);
        .data : {
		. = ALIGN(4);
		_sdata = .;
		*(.data)
		. = ALIGN(4);
		_edata = .;
	} >SRAM AT>ROM
	
	.bss : {
		. = ALIGN(4);
		_sbss = .;
		*(.bss)
		. = ALIGN(4);
		_ebss = .;
	} >SRAM
}


Two new sections have been added, these are .data and .bss. Why do we need them? well .data will leave global variables and static will go to bss. Both of these sections are in RAM, and if everything is simple with the bss section (these are just zeros), then there are some difficulties with the data section. First, data is already initialized data that is known at compile time. Thus, initially the data section will lie somewhere in the flash memory. Secondly, we need to somehow indicate to the linker the script that it is in flash memory, but it will be transferred to RAM during program execution. Let's take a look at a piece of the script where we describe the data section.


        _sidata = LOADADDR(.data);
        .data : {
		. = ALIGN(4);
		_sdata = .;
		*(.data)
		. = ALIGN(4);
		_edata = .;
	} >SRAM AT>ROM


It begins with the declaration of _sidata and the assignment, at the moment, of a meaning that is not clear to us. Next, a description of the section itself begins. I advise you to read about the ALIGN function here , but in a nutshell, we assign to our point (current address) a value that is more easily accepted by our processor when it tries to load data or instructions from there.

Also included are _sdata, _edata, and the end of the section. But at the end of the section there is just what we need.SRAM AT>ROM. This line tells our linker that the section is in SRAM but this section is loaded into ROM or flash memory. Now back to our _sidata variable and the LOADADDR function. This function will return us a value that will be equal to the address of the loading section. That is, we indicated that the section was loaded in flash memory, but we use it in RAM, and in order to transfer this section, we need to know its address in flash memory, which is what the LOADADDR function returns. Meanwhile, the variables _sdata and _edata indicate the location of the section in RAM, thereby giving us the opportunity to load the data section into the correct piece of memory. Let me remind you that this is what the initialization code that we supplemented above deals with.

Why do we need this linker and such difficulties with the distribution of sections of our application? The Data section is just a small example of why we need to be able to place and shift sections in memory. Below is an example of the text section that I use.

.text (ORIGIN(ROM_ITCM) + SIZEOF(.isr_vector)): {
        . = ALIGN(4);
        *(.text)
    } AT>ROM_AXIM


The essence of this block is that I load the text section into flash memory as well, but use a different bus to access instructions and an ART accelerator (sort of like a regular cache). And in order for everything to work, I indicated the load address and address during code execution, but it would not work if I did not indicate that the section should be offset. There are many more such inventions, I will not show them all.

At this stage, sections are loaded where necessary and initialized. we have a working base for further code writing. In the next article, we will write our first driver and blink an LED.

Thank you all for your attention, and success in your endeavors.

All Articles