Advanced Development with STM32

For most beginners, understanding how a compiler compiles a .c file into an .o file is not very difficult, but it is challenging to grasp the purpose of the final linking process and why it is done this way.Additionally, why is the startup file in our sample project one that we wrote ourselves, and how does it manage to direct the program entry to the main function? In this article, we will delve into these two topics.

Linker

The Linking Process

First, to understand how the linker works, we need to take a closer look at the specific methods and principles involved in the entire compilation process.I believe everyone knows that before high-level languages emerged, the assembly language we used was the closest to hardware, aside from machine code. Code written in assembly can even be easily converted to machine code manually. Therefore, the following introduction requires some understanding of assembly programming (such as 8051 assembly).During the execution of a microcontroller, commands are executed in only two ways: sequential execution and execution based on instruction jumps. In well-written assembly code, functions are typically organized into different storage locations, with program labels written at the beginning (e.g., “START:”). Finally, the compiler loads the address of the START program into the location where the jump instruction with the START label is written.From this, we can understand the process of compiling C language into a binary executable file. First, each C file is compiled into an .o file, which is an intermediate file with unresolved addresses. Then, the linker in the toolchain links all the .o files from the C files, arranging them in order in storage and resolving the addresses of each function so that functions from different locations can jump to the entry address of that function. Thus, an orderly executable file for the microcontroller is generated.As for the arrangement order and address locations of the functionalities produced by each .c file in the microcontroller’s storage, this is displayed in the .map file generated by the linker, as shown in the following snippet copied from the .map file of the sample project:

.isr_vector     0x08000000      0x134
                0x08000000                . = ALIGN (0x4)
 *(.isr_vector)
 .isr_vector    0x08000000      0x134 ./USER/CoIDE_startup.o
                0x08000000                g_pfnVectors
                0x08000134                . = ALIGN (0x4)

.text           0x08000134     0x1464
                0x08000134                . = ALIGN (0x4)
 *(.text)
 .text          0x08000134       0x5c /home/yangliu/Library/gcc-arm-none-eabi-5_4-2016q3/bin/../lib/gcc/arm-none-eabi/5.4.1/armv7-m/crtbegin.o
 .text          0x08000190       0x80 ./USER/main.o
                0x08000190                main
 .text          0x08000210       0x68 ./USER/CoIDE_startup.o
                0x08000210                Reset_Handler
                0x08000210                Default_Reset_Handler
                0x08000268                EXTI2_IRQHandler
                0x08000268                TIM8_TRG_COM_IRQHandler
                0x08000268                TIM8_CC_IRQHandler
                0x08000268                TIM1_CC_IRQHandler
                0x08000268                TIM6_IRQHandler
                0x08000268                PVD_IRQHandler
                0x08000268                SDIO_IRQHandler
                0x08000268                EXTI3_IRQHandler
                0x08000268                EXTI0_IRQHandler
                0x08000268                I2C2_EV_IRQHandler
                0x08000268                ADC1_2_IRQHandler123456789101112131415161718192021222324252627

Thus, our gcc linker is responsible for this task. Of course, it is not just the gcc linker; all C program compilation toolchains in the world should be designed with this concept in mind. However, I do not rule out that I have limited experience and have not seen any exceptions.

Usage of the Linker in the Toolchain

In practice, the executable program of the linker is actually the arm-none-eabi-ld file. However, in actual coding, when encountering projects that mix .c and .cpp files, ld will report errors during the linking process. The official recommendation is to use the arm-none-eabi-gcc command to link the project, as it will automatically call the ld program and avoid the aforementioned issues. Therefore, we will introduce the workings of the linker using the arm-none-eabi-gcc command.

$(CC) $(C_OBJ) -T stm32_f103ze_gcc.ld -o $(TARGET).elf   -mthumb -mcpu=cortex-m3 -Wl,--start-group -lc -lm -Wl,--end-group -specs=nano.specs -specs=nosys.specs -static -Wl,-cref,-u,Reset_Handler -Wl,-Map=Project.map -Wl,--gc-sections -Wl,--defsym=malloc_getpagesize_P=0x80 
1

In the above snippet from the sample project’s makefile, we can see the command for generating the .elf file at the end. The variable CC is set to arm-none-eabi-gcc, and the variable OBJ contains all the .o files. **-o xx.elf** indicates that the .elf file is generated from the linked .o files.

ld File

In the linking process, a significant difference from the compilation command is the -T xx.ld.Here, -T xx.ld actually calls a .ld file. So, what is the purpose of the .ld file?This is quite advanced. In the 51 microcontroller, we know that after generating code, there will be sections in the microcontroller’s memory such as code, xdata, and data, which partition the execution part of the code, variable parts, etc. The .ld file is a rule file used by the linker that tells the linker the addresses and sizes of the ROM and RAM in the microcontroller system, and instructs the linker where to place which code.The .ld file has its own syntax and parameter setting rules, which you do not need to understand in detail, but it is important to grasp some of the information within it.

/* Entry Point */
ENTRY(Reset_Handler)

/* Highest address of the user mode stack */
_estack = 0x20010000;    /* end of 64K RAM */

/* Generate a link error if heap and stack don't fit into RAM */
_Min_Heap_Size = 0;      /* required amount of heap  */
_Min_Stack_Size = 0x200; /* required amount of stack */

/* Specify the memory areas */
MEMORY
{
  FLASH (rx)      : ORIGIN = 0x08000000, LENGTH = 512K
  RAM (xrw)       : ORIGIN = 0x20000000, LENGTH = 64K
  MEMORY_B1 (rx)  : ORIGIN = 0x60000000, LENGTH = 0K
}

SECTIONS
{
  /* The startup code goes first into FLASH */
  .isr_vector :
  {
    . = ALIGN(4);
    KEEP(*(.isr_vector)) /* Startup code */
    . = ALIGN(4);
  } >FLASH

  /* The program code and other data goes into FLASH */
  .text :
  {
    . = ALIGN(4);
    *(.text)           /* .text sections (code) */
    *(.text*)          /* .text* sections (code) */
    *(.glue_7)         /* glue arm to thumb code */
    *(.glue_7t)        /* glue thumb to arm code */
    *(.eh_frame)

    KEEP (*(.init))
    KEEP (*(.fini))

    . = ALIGN(4);
    _etext = .;        /* define a global symbols at end of code */
  } >FLASH

  /* Constant data goes into FLASH */
  .rodata :
  {
    . = ALIGN(4);
    *(.rodata)         /* .rodata sections (constants, strings, etc.) */
    *(.rodata*)        /* .rodata* sections (constants, strings, etc.) */
    . = ALIGN(4);
  } >FLASH

  .ARM.extab   : { *(.ARM.extab* .gnu.linkonce.armextab.*) } >FLASH
  .ARM : {
    __exidx_start = .;
    *(.ARM.exidx*)
    __exidx_end = .;
  } >FLASH

  .preinit_array     :
  {
    PROVIDE_HIDDEN (__preinit_array_start = .);
    KEEP (*(.preinit_array*))
    PROVIDE_HIDDEN (__preinit_array_end = .);
  } >FLASH
  .init_array :
  {
    PROVIDE_HIDDEN (__init_array_start = .);
    KEEP (*(SORT(.init_array.*)))
    KEEP (*(.init_array*))
    PROVIDE_HIDDEN (__init_array_end = .);
  } >FLASH
  .fini_array :
  {
    PROVIDE_HIDDEN (__fini_array_start = .);
    KEEP (*(SORT(.fini_array.*)))
    KEEP (*(.fini_array*))
    PROVIDE_HIDDEN (__fini_array_end = .);
  } >FLASH

  /* used by the startup to initialize data */
  _sidata = LOADADDR(.data);

  /* Initialized data sections goes into RAM, load LMA copy after code */
  .data : 
  {
    . = ALIGN(4);
    _sdata = .;        /* create a global symbol at data start */
    *(.data)           /* .data sections */
    *(.data*)          /* .data* sections */

    . = ALIGN(4);
    _edata = .;        /* define a global symbol at data end */
  } >RAM AT> FLASH

  /* Uninitialized data section */
  . = ALIGN(4);
  .bss :
  {
    /* This is used by the startup in order to initialize the .bss section */
    _sbss = .;         /* define a global symbol at bss start */
    __bss_start__ = _sbss;
    *(.bss)
    *(.bss*)
    *(COMMON)

    . = ALIGN(4);
    _ebss = .;         /* define a global symbol at bss end */
    __bss_end__ = .;
  } >RAM

  /* User_heap_stack section, used to check that there is enough RAM left */
  ._user_heap_stack :
  {
    . = ALIGN(4);
    PROVIDE ( end = . );
    PROVIDE ( _end = . );
    . = . + _Min_Heap_Size;
    . = . + _Min_Stack_Size;
    . = ALIGN(4);
  } >RAM

  /* MEMORY_bank1 section, code must be located here explicitly            */
  /* Example: extern int foo(void) __attribute__ ((section (".mb1text"))); */
  .memory_b1_text :
  {
    *(.mb1text)        /* .mb1text sections (code) */
    *(.mb1text*)       /* .mb1text* sections (code)  */
    *(.mb1rodata)      /* read-only data (constants) */
    *(.mb1rodata*)
  } >MEMORY_B1

  /* Remove information from the standard libraries */
  /DISCARD/ :
  {
    libc.a ( * )
    libm.a ( * )
    libgcc.a ( * )
  }

  .ARM.attributes 0 : { *(.ARM.attributes) }
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145

As for the other linking parameters, most are the same as the compilation parameters, with the differences being:

--start-group -lc -lm -Wl,--end-group -specs=nano.specs -specs=nosys.specs -static -Wl,-cref,-u,Reset_Handler -Wl,-Map=Project.map -Wl,--gc-sections -Wl,--defsym=malloc_getpagesize_P=0x80 
1

I only have a general understanding of these commands, but I do not fully comprehend some specific parameters. If anyone is interested, you can search for more information, or the best way is to refer to the documentation in the toolchain for explanations.In our actual project establishment and coding, we use ld files obtained from elsewhere. The .ld file in the sample project can be used with slight modifications to the memory size, stack, and other locations based on the specific model of STM32.In the future, when we introduce the libopencm3 driver library, the author has written ld files for all chip models, which we can also copy and modify for our own projects. Some variables in the ld file, such as stack size, will be explained during the discussion of the startup file, as the startup file and the ld file are closely related.

Startup File

Many beginners who have just started with STM32 often have the impression that the startup file is simply a statement in tutorials: the startup file initializes the system before executing the main function and sets the PC (program counter, which is the pointer to the current executing code location) to the main function. Indeed, in integrated development environments like KEIL or IAR, we do not need to worry about the existence of the startup file, but in our use of gcc, we need to understand this file.In the sample project, I placed a startup file that I copied and modified from the CooCox open-source integrated development environment, located in the USER directory as CoIDE_startup.c. I will not include the content of the file here; we will only discuss a portion of it.To understand the startup code, we first need to look at one of the new features of the GNU compiler that differs from other compilers: *_attribute*((xxx)). In gcc, the attribute keyword is used to assign characteristics to functions or variables, similar to the weak specifier in MDK, but the use of attributes is more diverse and flexible.Secondly, we need to know that in the Cortex-M3 core we are using, the program execution starts by fetching the value of the MSP (stack pointer register) from the first address of ROM, and then fetching the address of the reset interrupt function from the second address and jumping to it. Generally, the interrupt vector table of a microcontroller system is initially placed at the very beginning of ROM, so we define an array of function pointers behind the initial stack value, forming a data structure loaded at the beginning address of ROM:

__attribute__ ((used,section(".isr_vector")))

void (* const g_pfnVectors[])(void) =
{
  /*----------Core Exceptions-------------------------------------------------*/
  (void *)&pulStack[STACK_SIZE],     /*!< The initial stack pointer         */
  Reset_Handler,                /*!< Reset Handler                            */
  NMI_Handler,                  /*!< NMI Handler                              */
  HardFault_Handler,            /*!< Hard Fault Handler                       */
  MemManage_Handler,            /*!< MPU Fault Handler                        */
  BusFault_Handler,             /*!< Bus Fault Handler                        */
  UsageFault_Handler,           /*!< Usage Fault Handler                      */
  0,0,0,0,                      /*!< Reserved                                 */
  SVC_Handler,                  /*!< SVCall Handler                           */
  DebugMon_Handler,             /*!< Debug Monitor Handler                    */
  0,                            /*!< Reserved                                 */
  PendSV_Handler,               /*!< PendSV Handler                           */
  SysTick_Handler,              /*!< SysTick Handler                          */

  /*----------External Exceptions---------------------------------------------*/
  WWDG_IRQHandler,              /*!<  0: Window Watchdog                      */
  PVD_IRQHandler,               /*!<  1: PVD through EXTI Line detect         */
  TAMPER_IRQHandler,            /*!<  2: Tamper                               */
  RTC_IRQHandler,               /*!<  3: RTC                                  */
  FLASH_IRQHandler,             /*!<  4: Flash                                */
  RCC_IRQHandler,               /*!<  5: RCC                                  */
  EXTI0_IRQHandler,             /*!<  6: EXTI Line 0                          */
  EXTI1_IRQHandler,             /*!<  7: EXTI Line 1                          */
  EXTI2_IRQHandler,             /*!<  8: EXTI Line 2                          */
  EXTI3_IRQHandler,             /*!<  9: EXTI Line 3                          */
  EXTI4_IRQHandler,             /*!< 10: EXTI Line 4                          */
  DMA1_Channel1_IRQHandler,     /*!< 11: DMA1 Channel 1                       */
  DMA1_Channel2_IRQHandler,     /*!< 12: DMA1 Channel 2                       */
  DMA1_Channel3_IRQHandler,     /*!< 13: DMA1 Channel 3                       */
  DMA1_Channel4_IRQHandler,     /*!< 14: DMA1 Channel 4                       */
  DMA1_Channel5_IRQHandler,     /*!< 15: DMA1 Channel 5                       */
  DMA1_Channel6_IRQHandler,     /*!< 16: DMA1 Channel 6                       */
  DMA1_Channel7_IRQHandler,     /*!< 17: DMA1 Channel 7                       */
  ADC1_2_IRQHandler,            /*!< 18: ADC1 & ADC2                          */
  USB_HP_CAN1_TX_IRQHandler,    /*!< 19: USB High Priority or CAN1 TX         */
  USB_LP_CAN1_RX0_IRQHandler,   /*!< 20: USB Low  Priority or CAN1 RX0        */
  CAN1_RX1_IRQHandler,          /*!< 21: CAN1 RX1                             */
  CAN1_SCE_IRQHandler,          /*!< 22: CAN1 SCE                             */
  EXTI9_5_IRQHandler,           /*!< 23: EXTI Line 9..5                       */
  TIM1_BRK_IRQHandler,          /*!< 24: TIM1 Break                           */
  TIM1_UP_IRQHandler,           /*!< 25: TIM1 Update                          */
  TIM1_TRG_COM_IRQHandler,      /*!< 26: TIM1 Trigger and Commutation         */
  TIM1_CC_IRQHandler,           /*!< 27: TIM1 Capture Compare                 */
  TIM2_IRQHandler,              /*!< 28: TIM2                                 */
  TIM3_IRQHandler,              /*!< 29: TIM3                                 */
  TIM4_IRQHandler,              /*!< 30: TIM4                                 */
  I2C1_EV_IRQHandler,           /*!< 31: I2C1 Event                           */
  I2C1_ER_IRQHandler,           /*!< 32: I2C1 Error                           */
  I2C2_EV_IRQHandler,           /*!< 33: I2C2 Event                           */
  I2C2_ER_IRQHandler,           /*!< 34: I2C2 Error                           */
  SPI1_IRQHandler,              /*!< 35: SPI1                                 */
  SPI2_IRQHandler,              /*!< 36: SPI2                                 */
  USART1_IRQHandler,            /*!< 37: USART1                               */
  USART2_IRQHandler,            /*!< 38: USART2                               */
  USART3_IRQHandler,            /*!< 39: USART3                               */
  EXTI15_10_IRQHandler,         /*!< 40: EXTI Line 15..10                     */
  RTCAlarm_IRQHandler,          /*!< 41: RTC Alarm through EXTI Line          */
  USBWakeUp_IRQHandler,         /*!< 42: USB Wakeup from suspend              */  
  TIM8_BRK_IRQHandler,          /*!< 43: TIM8 Break                           */        
  TIM8_UP_IRQHandler,           /*!< 44: TIM8 Update                          */ 
  TIM8_TRG_COM_IRQHandler,      /*!< 45: TIM8 Trigger and Commutation         */
  TIM8_CC_IRQHandler,           /*!< 46: TIM8 Capture Compare                 */
  ADC3_IRQHandler,              /*!< 47: ADC3                                 */
  FSMC_IRQHandler,              /*!< 48: FSMC                                 */
  SDIO_IRQHandler,              /*!< 49: SDIO                                 */
  TIM5_IRQHandler,              /*!< 50: TIM5                                 */
  SPI3_IRQHandler,              /*!< 51: SPI3                                 */
  UART4_IRQHandler,             /*!< 52: UART4                                */     
  UART5_IRQHandler,             /*!< 52: UART5                                */           
  TIM6_IRQHandler,              /*!< 53: TIM6                                 */           
  TIM7_IRQHandler,              /*!< 54: TIM7                                 */           
  DMA2_Channel1_IRQHandler,     /*!< 55: DMA2 Channel1                        */  
  DMA2_Channel2_IRQHandler,     /*!< 56: DMA2 Channel2                        */  
  DMA2_Channel3_IRQHandler,     /*!< 57: DMA2 Channel3                        */    
  DMA2_Channel4_5_IRQHandler,   /*!< 58: DMA2 Channel4 & Channel5             */   
  (void *)0xF108F85F            /*!< Boot in RAM mode                         */
};

Note that in the attribute of the array, it specifies the location of the function in [section(“.isr_vector”)], and [.isr_vector] is defined at the beginning of FLASH in the ld file:

/* The startup code goes first into FLASH */
  .isr_vector :
  {
    . = ALIGN(4);
    KEEP(*(.isr_vector)) /* Startup code */
    . = ALIGN(4);
  } >FLASH

It is evident that in the second cycle after startup, the core reads the address of the reset vector table and jumps to it. Therefore, the startup code of the microcontroller must be stored in the reset vector, and we find the reset function in the startup file:

#pragma weak Reset_Handler = Default_Reset_Handler  

void Default_Reset_Handler(void)
{
  /* Initialize data and bss */
  unsigned long *pulSrc, *pulDest;

  /* Copy the data segment initializers from flash to SRAM */
  pulSrc = &_sidata;
  for(pulDest = &_sdata; pulDest < &_edata; )
  {
    *(pulDest++) = *(pulSrc++);
  }

  /* Zero fill the bss segment.  This is done with inline assembly since this
     will clear the value of pulDest if it is not kept in a register. */
  __asm("  ldr     r0, =_sbss\n"
        "  ldr     r1, =_ebss\n"
        "  mov     r2, #0\n"
        "  .thumb_func\n"
        "zero_loop:\n"
        "    cmp     r0, r1\n"
        "    it      lt\n"
        "    strlt   r2, [r0], #4\n"
        "    blt     zero_loop");

  /* Setup the microcontroller system. */
  SystemInit();

  /* Call the application's entry point.*/
  main();
}

In the startup function, we can clearly see that in the last step, the microcontroller’s program is transferred to the entry of the main function. So, what did C language and inline assembly do before executing the main function? First, the C language at the head position copies the terminal vector table from the head position of ROM to the head position of RAM (i.e., 0x20000000). This operation is not used in RAM, of course, because in the M3 core, it allows users to redefine the location of the terminal vector table in the NVIC register. We can use

NVIC_SetVectorTable(NVIC_VectTab_FLASH,0);

to set the terminal vector table to the 0x20000000 position. This function is actually used for convenience in environments with systems, allowing for faster terminal response and quick dynamic changes to the terminal processing program.Of course, in our application, we did not use this feature, so the operation of copying the interrupt vector table can be deleted. Its role here is merely to prevent users from using the redirection vector table statement in the program, which could cause the program to malfunction. This is because the interrupt vector is the guarantee of the system’s basic stability. If the microcontroller cannot correctly jump in the event of hardware errors or interrupts, it will severely impact code debugging and the stable operation of the system.Following this, the inline assembly code implements the initialization of global and static variables and transfers them from flash to memory, which corresponds to the initialization operation of global variables and static variables in C language. After this, the SystemInit(); function is called to configure clock parameters, etc. Finally, our main function can be executed!This is the startup file we used in this example. In a Keil project, this file is written in assembly code, but the functionality of these files is the same: setting the terminal vector table, initializing global and static variables, and entering the main function, all follow this process.In the gcc environment, we can also write such files in assembly. We have many options available, and of course, we do not need to write these linker files and startup codes ourselves. In the actual project establishment later, I will tell you the practical methods. However, before that, we should first learn the foundational content well.

Other Notes

In the file, we see variables like **_sidata, _sdata**, etc. These variables are defined as extern at the beginning of the file:

extern unsigned long _sidata;    /*!< Start address for the initialization 
                                      values of the .data section.            */
extern unsigned long _sdata;     /*!< Start address for the .data section     */    
extern unsigned long _edata;     /*!< End address for the .data section       */    
extern unsigned long _sbss;      /*!< Start address for the .bss section      */
extern unsigned long _ebss;      /*!< End address for the .bss section        */

However, this file does not include any .h files. So where do they come from? Observant students may have noticed that we previously mentioned that these variable definitions actually come from the ld file. They are defined in the ld file, and the linker will convert them into actual addresses for our program to use.Finally, let’s talk about the attribute ((weak)) attribute. This attribute indicates that the variable or function following it is weakly declared, meaning that if this function is called without other declarations, it will be used, but if there are declarations elsewhere, it will be replaced by the user-defined function. Therefore, in the startup file, they are used to modify the interrupt handling functions to provide a default address for the interrupt vector table, and when the user defines it, the address is changed to the user-defined location.

Conclusion

Having discussed so much, this is one of the more challenging parts to understand in this series, as it involves the features of GNU C and the fundamental aspects of computer compilation and linking, as well as how the Cortex-M3 core operates. However, I encourage everyone to study and understand it carefully. If after reading this article you still do not understand, please look up related materials. When you grasp and connect these concepts, you will discover how C language operates on microcontrollers, how crucial the interrupt system is, and you will see the microcontroller in a new light.Finally, I want to mention that many students currently only master a very simple application method for microcontrollers, which is a result of being spoiled by tools like Keil and IAR. In reality, the complexity and exquisite design behind microcontrollers are substantial, and we will see this in the upcoming use of the Nuttx system.At that time, you will realize that the M3 microcontroller has many interrupts that you have not previously used, and the M3 core is incredibly powerful. Therefore, I recommend everyone to study the assembly tutorial for the 51 microcontroller. Once you understand and use assembly, you will find it easier to comprehend future explanations and this article’s content. Of course, if you are interested, you can first check out the authoritative guide on Cortex-M3 translated by Song Yan to get a preview of the charm of the Cortex-M3 core.

Disclaimer:This article is reproduced from “Technology Makes Dreams Greater”,if there are any issues regarding the content, copyright, or other matters, please contact the staff via WeChat (prrox66), and we will promptly address the deletion!Submissions/Recruitment/Advertising/Course Cooperation/Resource Exchange, please add WeChat: 13237418207
Advanced Development with STM32

A high-paying path for embedded engineers

Advanced Development with STM32

Ten filtering algorithms you must know for AD sampling in microcontrollers

Advanced Development with STM32

Scan to add customer service WeChat, note “Join Group” to pull you into the official exclusive technical WeChat group of Fanyi Education, to discuss technical issues and insights with many electronic technology experts~

Share 💬 Like 👍 Read ❤️ Support with a “triple click”!

Leave a Comment