STM32/ARM Cortex-M3 HOWTO: Development under Ubuntu (Debian)

The linker

At this point he have played with the different parts, but how do code fit together? How does the system know where to put the different things?

If we look back at the first mini example, there was only 3 files in that project.

  1. main.c
  2. stm32.ld
  3. Makefile

And here we will focus on the interaction between the code and the linker, in this case main.c and stm32.ld.

NVIC

Before I mentioned that there needs to be a vector with data at address 0x0, and the rest just ended up in some good place and worked good enough.

If we look at the c style version of this vector (from the first mini example), it looked like this.

#define STACK_TOP 0x20000800

// Define the vector table
unsigned int * myvectors[4] 
__attribute__ ((section("vectors")))= {
    (unsigned int *)   STACK_TOP,         // stack pointer
    (unsigned int *)   main,              // code entry point
    (unsigned int *)   nmi_handler,       // NMI handler (not really)
    (unsigned int *)   hardfault_handler  // hard fault handler (let's hope not)
};

Now there is two small problems with this code. The first is that the name does not tell us anything about what is does, and the second is that we have a define that tells us where the top of the stack is. Therefore let's rename the vector to "the_nvic_vector" and call the section .nvic_vector.

unsigned int * the_nvic_vector[4]
__attribute__ ((section(".nvic_vector")))= {

In the linker file we then need to add this new section called .nvic_vector and make sure that this is located at address 0x0.

MEMORY
{
  rom (rx)  : ORIGIN = 0x00000000, LENGTH = 128K
}
SECTIONS
{
    .nvic_vector : 
    {
        *(vectors)    /* Vector table */   
    } >rom
}

But how did I know what to put into the linker file? (I will return to this important question shortly).

Then we can replace the define with a variable that we get from the linker. Since the linker file has the rest of this information it is a more logical place and will give us a better overview of the memory map.

extern unsigned int _STACKTOP;

And then we put the address of that variable right into the vector and we have something that looks like this. Please note that we don't care what is in this variable, only where it is located.

extern unsigned int _STACKTOP;

unsigned int * the_nvic_vector[4]
__attribute__ ((section(".nvic_vector")))= {
    (unsigned int *)    &_STACKTOP,        // stack pointer
    (unsigned int *)    main,              // code entry point
    (unsigned int *)    nmi_handler,       // NMI handler (not really)
    (unsigned int *)    hardfault_handler  // hard fault handler (let's hope not)
};

So we start to tell what is the maximum address on this device, and we call this address stack.

MEMORY
{
  stack(rwx): ORIGIN = 0x20004FFC, LENGTH = 0K
}

Then we add section that just adds the variable _STACKTOP at this address.

SECTIONS
{
    .stack :
    {
        _STACKTOP = .;
    } >stack
}

And together with the nvic part we have something that looks like this.

MEMORY
{
  stack(rwx): ORIGIN = 0x20004FFC, LENGTH = 0K
  rom (rx)  : ORIGIN = 0x00000000, LENGTH = 128K
}
SECTIONS
{
    .nvic_vector : 
    {
        *(vectors)    /* Vector table */   
    } >rom

    .stack :
    {
        _STACKTOP = .;
    } >stack
}

binutils

But how did I know this?

There is a couple of nice command that comes with binutils that can help us here.

  1. objdump
  2. nm
  3. readelf

But since we must use the platform specific version the commands, their correct names is actually arm-none-eabi-objdump and arm-none-eabi-nm. However since I will simply call them objdump and nm.

The basic idea is that the compiler will output a object file per c file, and the linker will put those object files together into 1 file that we can put on our device (kind off). objdump can be used to get information out from those objects/elf files.

If we look into the object file where we should find the nvic vector, main.o, we can use objdump to get the information that is related to the nvic vector.

$> arm-none-eabi-objdump --syms main.o | grep .nvic_vector
00000000 l    d  .nvic_vector   00000000 .nvic_vector
00000000 g     O .nvic_vector   00000010 the_nvic_vector

On the second line we can see that our vector "the_nvic_vector", will be put into section .nvic_vector. This is actually the input data that tells the linker how to handle the code.

And if we then look at the same but after the linker, in the main.elf. This is almost the same in this case.

$> arm-none-eabi-objdump --syms  main.elf | grep .nvic_vector
00000000 l    d  .nvic_vector   00000000 .nvic_vector
00000000 g     O .nvic_vector   00000010 the_nvic_vector

But we also need to look with nm on main.elf so we can see what is at address 0, and lucky us we find our vector.

$> arm-none-eabi-nm -n main.elf | grep 00000000
00000000 D the_nvic_vector

If we do the same but with the _STACKTOP we see a different story.

$> arm-none-eabi-objdump --syms main.o | grep STACK
00000000         *UND*  00000000 _STACKTOP

Here you can see that he has no idea what to do with this strange variable, but if we then look after the linker.

$> arm-none-eabi-objdump --syms main.elf | grep STACK
20004ffc g       .debug_abbrev  00000000 _STACKTOP

We notice that _STACKTOP goes into section .debug_abbrev and we notice the number 20004ffc. In stm32.ld we told him to put this variable at address 20004ffc, so that number should be a address, and we can double check that with nm.

$> arm-none-eabi-nm  main.elf | grep STACK
20004ffc N _STACKTOP

main

But what about main, where does the actual code go?

$> arm-none-eabi-objdump --syms main.o | grep main
00000000 l    df *ABS*  00000000 main.c
0000001c g     F .text  0000006c main

Here we find that he will put main i a section called .text, and since we know that we need to put the code after the nvic vector we add that section to the linker file.

MEMORY
{
  stack(rwx): ORIGIN = 0x20004FFC, LENGTH = 0K
  rom (rx)  : ORIGIN = 0x00000000, LENGTH = 128K
}
SECTIONS
{
    .nvic_vector :
    {
        *(vectors)    /* Vector table */
    } >rom

    .text :
    {
        *(.text)      /* Program code */
    } >rom

    .stack :
    {
        _STACKTOP = .;
    } >stack
}

And the result is that main will be put at address 0x2c.

$> arm-none-eabi-objdump --syms main.elf | grep main
main.elf:     file format elf32-littlearm
00000000 l    df *ABS*  00000000 main.c
0000002c g     F .text  0000006c main

$> arm-none-eabi-nm main.elf | grep main
0000002c T main

And if we look at the relevant output from nm and sort by number, we can see that the system looks like we like it to.

$> arm-none-eabi-nm -n main.elf 
00000000 D the_nvic_vector
0000002c T main
00000098 T nmi_handler
000000a4 T hardfault_handler
20004ffc N _STACKTOP

One thing that is important is the order you put things into the linker file, let's say that we put the .text section before the .nvic_vector.

SECTIONS
{
    .text :
    {
        *(.text)      /* Program code */
    } >rom

    .nvic_vector :
    {
        *(vectors)    /* Vector table */
    } >rom
}

Then when we look at the resulting memory map we will see thing that makes no sense.

$> arm-none-eabi-nm -n main.elf 
00000000 T main
00000088 T nmi_handler
00000094 T hardfault_handler
000000e8 D the_nvic_vector

Notice how main ends up at address 0x0, and the_nvic_vector at 0xe8. With this setup the system would not even start.

Best to put them in the "correct" order.

Local variables

Now we have the start vector and code in place, but what happens when I start to code? Where does my local variables go?

Short answer is that they go on the stack, and we told the system that the stack begins at the end of the ram. Then we know that on this platform the stack grows down in the address space.

Let's add a local variable and check with gdb where it is, and let's use the classic i.

int main(void)
{
...
int i=0;
....
}

Then we flash and start the system and have a look with gdb at the address of i.

(gdb) print &i
$1 = (int *) 0x20004ff0

And that is just a little bit lower than _STACKTOP that was 0x20004FFC, so I would say that this must be on the stack.

Global variables

Let's add a global variable and have it init to zero, and see what happens.

unsigned int my_global_variable_zero = 0;

int main(void)
{
...
}

Then we build and have a look at main.o.

$> arm-none-eabi-objdump --syms  main.o | grep my_global_variable
00000000 g     O .bss   00000004 my_global_variable_zero

So this variable goes into a section called .bss, and if you read the gcc manual you will find out that .bss is a section that variables that has a default init to zero will be put into.

So let's add a .bss section to the linker file. And have him fill from the lower address that is 0x20000000 on this chip.

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

SECTIONS
{
    .bss :
    {
        *(.bss)       /* Zero-filled run time allocate data memory */
    } >ram
}

Then we build again and check in the main.elf.

$ arm-none-eabi-objdump --syms  main.elf | grep my_global_variable
20000000 g     O .bss   00000004 my_global_variable_zero

This looks ok, but when you run this code you will notice that there is a little problem. The variable exists, but it does not default to 0! There is always something else there.

Back to school again, the point with .bss is that we can save space by not putting a lot of zeros into the flashed image. And on a bare metal system that we are building we are responsible to do that our self...!

That was not nice, let's try to set it to 1 instead. And see if that works any better.

unsigned int my_global_variable_one = 1;

int main(void)
{
...
}
$> arm-none-eabi-objdump --syms  main.o | grep my_global_variable
00000000 g     O .data  00000004 my_global_variable_one

Then the global variable is put into a section called .data! so let's add a .data section.

    .data :
    {
        *(.data)      /* Data memory */
    } >ram

But it still does not work!

Back to school again, since you can't store variables in ram over a power cycle the variables init values need to be store in a rom/flash and copied in place when the system starts.

So we need to look a a magic syntax for load address, AT. And if we add >ram AT >rom, the linker will reserve space in ram for the variable but put the init value in rom.

    .data :
    {
        *(.data)      /* Data memory */
    } >ram AT >rom

And now the system starts, and if we look at the .data section in ram it looks ok.

$> arm-none-eabi-objdump --syms  main.elf | grep my_global_variable
20000000 g     O .data  00000004 my_global_variable_one

BUT how do we solve that init problem for .bss and .data?

RTFM

What does the gcc ld manual tell us?

http://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_node/ld_21.html

And that tells us to find the start and stop address for .bss, .data and where the init data goes. And I will try to do this a little bit more clear than the manual.

For .bss it is easy.

    .bss :
    {
        _BSS_BEGIN = .;
        *(.bss)       /* Zero-filled run time allocate data memory */
        _BSS_END = .;
    } >ram

Then in code we put some externs and then play with the address from those. With .bss we go over the addresses and init those to 0.

extern unsigned int _BSS_BEGIN;
extern unsigned int _BSS_END;

int main(void)
{
    uint32_t* bss_begin = &_BSS_BEGIN;
    uint32_t* bss_end   = &_BSS_END;

    while(bss_begin < bss_end)
    {
        *bss_begin = 0;
        bss_begin++;
    }
}

The .data case is a little bit trickier, since we have two areas. The address in ram is the same style as .bss.

    .data :
    {
        _DATA_BEGIN = .;
        *(.data)      /* Data memory */
        _DATA_END = .;
    } >ram AT >rom

But where did the init data go? The manual tricks us into think it has something to do with .text, but it has nothing to do with the .text section.

We know that AT >rom will add it to rom, so that would be after the .text section, just as .text comes right after .nvic_vector.

    .text : 
    {
        *(.text)      /* Program code */
        _DATAI_BEGIN = .;
    } >rom
    
    .data : 
    {
        _DATA_BEGIN = .;  
        *(.data)      /* Data memory */
        *(.data.*)        
        _DATA_END = .;
    } >ram AT >rom
    
    .data_init : 
    {   
        _DATAI_END = .;
    } >rom

This more or less tells the linker to first save the address where .text ends in a var called _DATAI_BEGIN. Then add the init data from .data to rom, and then we have a last section that saves the address after into a var called _DATAI_END.

So we do end up with more code for this case, and then we copy from rom/flash onto the ram. And it looks like this.

extern unsigned int _DATA_BEGIN;
extern unsigned int _DATA_END;
extern unsigned int _DATAI_BEGIN;
extern unsigned int _DATAI_END;

int main(void)
{
    uint32_t* data_begin  = &_DATA_BEGIN;
    uint32_t* data_end    = &_DATA_END;
    uint32_t* datai_begin = &_DATAI_BEGIN;
    uint32_t* datai_end   = &_DATAI_END;

    uint32_t data_size  = data_end  - data_begin;
    uint32_t datai_size = datai_end - datai_begin;

    if(data_size != datai_size) {
        //Linker script is not correct.
        while(1);
    }

    while(data_begin < data_end)
    {
        *data_begin = *datai_begin;
        data_begin++;
        datai_begin++;
    }
}

To play it safe and to check that we don't have a size mismatch between the data and the init data, I added a little size check there in the start.

Why is main filled with init stuff?

What happened to my clean main function? Why is it filled with this init stuff?

Well we know why it is there, but to be honest it should not be there. This kind of stuff should be in a place called low_level_init.

So let's create it and move all of this stuff out from main.c and into a file called low_level_init.c. Then the files will look like this:

Filename: stm32.ld

MEMORY
{
  ram (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
  stack(rwx): ORIGIN = 0x20004FFC, LENGTH = 0K

  rom (rx)  : ORIGIN = 0x00000000, LENGTH = 128K
}

SECTIONS
{
    .nvic_vector : 
    {
        *(vectors)    /* Vector table */
    } >rom

    .text : 
    {
        *(.text)      /* Program code */
        *(.text.*)
        *(.rodata)    /* Read only data */
        *(.rodata.*)        
        _DATAI_BEGIN = .;
    } >rom

    .data : 
    {
        _DATA_BEGIN = .;
        *(.data)      /* Data memory */
        *(.data.*)        
        _DATA_END = .;
    } >ram AT >rom

    .data_init : 
    {
        _DATAI_END = .;
    } >rom

    .bss :
    {
        _BSS_BEGIN = .;
        *(.bss)       /* Zero-filled run time allocate data memory */
        *(COMMON)        
        _BSS_END = .;
    } >ram 

    .heap :
    {
        _HEAP = .;
    } >ram

    .stack :
    {
        _STACKTOP = .;
    } >stack

}  

Filename: low_level_init.c

#include <stdint.h>

#include "main.h"

void low_level_init(void);
void nmi_handler(void);
void hardfault_handler(void);

extern unsigned int _STACKTOP;

// Define the vector table
unsigned int * the_nvic_vector[4] 
__attribute__ ((section(".nvic_vector")))= {
    (unsigned int *)	&_STACKTOP,        // stack pointer
    (unsigned int *) 	low_level_init,    // code entry point
    (unsigned int *)	nmi_handler,       // NMI handler (not really)
    (unsigned int *)	hardfault_handler  // hard fault handler (let's hope not)
};


extern unsigned int _BSS_BEGIN;
extern unsigned int _BSS_END;

extern unsigned int _DATA_BEGIN;
extern unsigned int _DATA_END;
extern unsigned int _DATAI_BEGIN;
extern unsigned int _DATAI_END;

void low_level_init(void)
{
    uint32_t* bss_begin = &_BSS_BEGIN;
    uint32_t* bss_end   = &_BSS_END;
    while(bss_begin < bss_end)
    {
        *bss_begin = 0;
        bss_begin++;
    }

    uint32_t* data_begin  = &_DATA_BEGIN;
    uint32_t* data_end    = &_DATA_END;
    uint32_t* datai_begin = &_DATAI_BEGIN;
    uint32_t* datai_end   = &_DATAI_END;

    uint32_t data_size  = data_end  - data_begin;
    uint32_t datai_size = datai_end - datai_begin;

    if(data_size != datai_size) {
        //Linker script is not correct.
        while(1);
    }

    while(data_begin < data_end)
    {
        *data_begin = *datai_begin;
        data_begin++;
        datai_begin++;
    }


    main();
}


void nmi_handler(void)
{
    return ;
}

void hardfault_handler(void)
{
    return ;
}

Filename: main.c

#include <stdint.h>

#include "main.h"

unsigned int my_global_variable_zero = 0;
unsigned int my_global_variable_one  = 1;
unsigned int my_global_variable_two  = 2;

int main(void)
{
    int i=0;

    while(1)
    {
        i++;
        my_global_variable_zero++;
        my_global_variable_one++;
        my_global_variable_two++;
    }
}

Filename: main.h

#ifndef  __MAIN_H
#define  __MAIN_H

int main(void);

#endif  // __MAIN_H 

/Have fun.

This page is licensed with Creative Commons ShareAlike 3.0