Advanced Development with STM32

Follow and star our public account to access exciting content

Advanced Development with STM32

Source: https://blog.csdn.net/zhengyangliu123/article/details/79090601

Compiled by: Technology Makes Dreams Greater | Li Xiaoyao

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

Linker

Linking Process

First, to understand how the linker works, we need to take a closer look at the specific methods and principles of the entire compilation process. As we all know, before high-level languages emerged, the assembly language we used was the closest to hardware apart 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 divided into blocks stored in different locations, with program labels written at the front (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. Thus, we can understand the process of compiling C language into a binary executable file. Each C file is first compiled into an .o file, which is an intermediate file with unresolved addresses. The toolchain’s linker then links all the .o files of the C files, arranging them in order in memory and resolving the addresses of each function so that functions in different locations can jump to the entry address of that function, thus generating an ordered executable file for the microcontroller. As for the order and address of the functionalities produced by each .c file in the microcontroller’s memory, it is displayed in the .map file generated by the linker, as shown in the following snippet copied from the sample project’s .map file:

.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 work. Of course, it is not just the gcc linker; all C program toolchains in the world should be designed with this concept. However, I do not exclude the possibility that I have limited experience and have not seen anything special.

Usage of Linker in Toolchain

In practice, the executable program of the linker is actually the arm-none-eabi-ld file. However, in my actual coding process, when encountering projects that mix .c and .cpp files, ld will report errors during the linking process. The official recommendation for this is to use the arm-none-eabi-gcc command to link the project, which will automatically call the ld program and avoid the aforementioned situation. 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 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, the significant difference from the compilation command is -T xx.ld. Here, -T xx.ld actually calls a .ld file. So what is the .ld file for? This is quite advanced. In the 51 microcontroller, we know that after generating the code, there will be sections like code, xdata, and data in the 51 microcontroller’s memory, which divide the execution part of the code, variable parts, etc. The .ld file is a rule file used by the linker; it tells the linker the addresses and sizes of the ROM and RAM of the microcontroller system and instructs the linker where to store which code. The .ld file has its own syntax and parameter setting rules, which you do not need to understand in detail, but you should be able to comprehend 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__ = _ebss;
  } >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

For these commands, I only have a general understanding of what they are, but I do not fully understand some specific parameters. If anyone is interested, you can search for them yourself, or the best way is to look for explanations in the documentation of the toolchain. 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. Alternatively, in the future, when we introduce the libopencm3 driver library, the author has written ld files for all chip models, which we can copy and modify for our own projects. Some variables in the ld file, such as stack size, will be explained in the process of discussing the startup file, as the startup file is closely related to the contents of the ld file.

Startup File

Many students 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 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, but we will discuss a part of it. To understand the startup code, we first need to look at one of the new features of the GNU compiler that distinguishes it 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 (Main Stack Pointer) 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 front of ROM, so we define an array of function pointers behind the initial value of the stack, forming a data structure loaded at the ROM’s starting address:

__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 array’s attribute modification, 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

Thus, 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. 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 the C language and inline assembly do before executing the main function? First, the C code at the head copies the terminal vector table from the head of ROM to the head of RAM (i.e., 0x20000000). This is not used in the RAM’s terminal vector table 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 location. This function is actually used for environments with systems installed, allowing for faster terminal response and quick dynamic changes to the terminal processing program. However, in our application, we did not use this feature, so the operation of copying the interrupt vector table can be omitted. Its purpose here is merely to prevent users from using the redirection vector table statement in the program, which could cause the program to malfunction. 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 system stability. The following assembly code implements the initialization of global and static variables and transfers them from flash to memory, which is the operation of initializing global variables and static variables in C language. After this, the SystemInit(); function is called to configure clock parameters, and finally, our main function can execute! 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 before us, and of course, we do not need to write these linker files and startup codes ourselves. In the actual project establishment, I will tell you the practical methods. However, before that, we should first learn the basic 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 use this function, but if there is a declaration elsewhere, it will replace this function. Therefore, in the startup file, they are used to modify 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 carefully. If after reading this article you still do not understand, please look up related materials. When you understand and connect these concepts, you will find that this is how C language works on microcontrollers, and how important the interrupt system is. You will see the microcontroller in a new light. Finally, I want to mention that many students currently only grasp a simple application method for microcontrollers, which is a result of being spoiled by KEIL, IAR, and similar tools. In reality, the actual work behind microcontrollers is complex and filled with exquisite designs, which we will see in the future when using the nuttx system. At that time, you will discover that the M3 microcontroller has many interrupts we have not used before, and the M3 core is so 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 grasp future explanations and also to understand the content of this article. Of course, if anyone is interested, you can first take a look at the authoritative guide on Cortex-M3 translated by Song Yan to experience the charm of the Cortex-M3 core in advance.

Copyright Statement:This article is sourced from the internet, freely conveying knowledge, and the copyright belongs to the original author. If there are any copyright issues, please contact me for deletion.

‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧ END ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧

Follow my public WeChat account, reply "Join Group" to join the technical exchange group according to the rules.

Click "Read the original text" for more sharing. Feel free to share, bookmark, like, and view.

Leave a Comment