STM32:启动文件

前言

  复位之后,CPU首先根据boot引脚选择存储器重映射区域,该区域的地址重映射为地址偏移量为0;

  pc指针从地址偏移量为0的地址处开始执行;该地址烧录的代码是xx.s启动文件,也称bootloader;

  启动文件主要分配了堆栈和向量表,然后跳转到SystemInit函数配置系统时钟,然后通过__main最终进入到main函数;

  MDK并没有将启动文件的所有配置开源,比如一部分的配置由__main闭源执行;我们只能配置开源的一部分启动文件的参数;

  本文我们了解一下启动文件及其原理;

1 boot的启动方式

   1.1 STM32F1xx

    存储空间的单位都是byte是因为byte更符合人们的使用习惯;寄存器最小单位是8位寄存器,8bits可存储1byte;每个32位地址寻址1个寄存器;

boot[1:0] 启动地址 存储空间 启动方式 事项
[x:0]

0x0800 0000-

0x0807 FFFF

512K

bytes

Flash启动

使用JTAG或SWD下载

较为常用;

[0:1]

0x1FFF F000-

0x1FFF F7FF

2K

bytes

系统存储器启动

(ISP下载)

使用串口下载,代码通过bootloader搬运到flash中;

该区域内存储了厂家烧录的bootloader程序(即ISP程序),需要配合st提供的下载软件;

[1:1]

0x2000 0000-

0x2001 0000

64K

bytes

内嵌SRAM启动

(MDK在线调试)

使用JTAG或SWD下载,这个模式只能调试使用;

没有掉电存储程序的功能,(需要添加宏定义VECT_TAB_SRAM,配合脚本文件使用;)

  1.2 STM32H7xx

    对于 STM32H743 而言,只有一个boot引脚,配合BOOT_ADD0/BOOT_ADD1 的组合配置,可以让系统从两个不同的区域启动;

boot引脚 启动地址 默认值 默认值启动方式 事项
0 BOOT_ADD0[15:0]  0x0800 Flash启动

启动地址高16位由BOOT_ADD0寄存器决定,低16位为0x0000;

BOOT_ADD0寄存器可以修改,修改后掉电不丢失;

0x0000 0000 到 0x3FFF 0000 的存储器地址都可以设置;

1 BOOT_ADD1[15:0]  0x1FF0 系统存储器启动

2 汇编指令

  2.1 伪指令 pseudo-instruction

    编译器指令,编译器在编译时会进行替换,不会生成机器代码,不占用内存;

      LDR:    load register;后接“=标识符”,则将标识符的地址赋值给目标寄存器,注意不是标识符内的数据;

      AREA:    表示即将分配一个数据段或代码段; 需要跟分配内存的SPACE指令一起使用;

      EQU:    声明常数,汇编的常数单位不是bit,而是byte;

      IMPORT:  引入外部文件的声明变量;引入全局变量;

      EXPORT:  声明当前文件的变量可被外部文件调用,主要提供给链接器用于连接库文件;

      PROC:   与ENDP一同使用,声明一段程序代码;

      ENDP:   与PROC一同使用,声明一段程序代码;

      WEAK:     弱声明,如果外部文件有相同的声明,则编译外部文件的声明,不编译弱声明;

      THUMB:  表示接下来的指令都是thumb指令集;thumb指令大部分只能访问R0-R7,thumb-2指令可以访问R0-R16;

      ALIGN:   声明编译器需要对指令或数据的存放地址进行对齐;默认缺省值为32bit对齐(ALIGN x = 2^x );

      PRESERVE8:  当前文件的堆栈需按照8字节对齐,也就是64bit数据线对齐;

      END:    每个汇编文件以END作为文件结尾;

  2.2 汇编指令

    编译后生成机器码,占用存储内存;

      LDR:    load register;后接“=常数”加载32bit常数存入目的寄存器中;

      LDR.W:     同LDR指令功能相同,.W为指令宽度说明符,表示将LDR变成32位指令;

      LDRH:

      LDRB:

      DCD:    define constant double-words;分配4bytes内存空间用来存储32bits数据,并初始化

      SPACE:   分配一段内存空间,并初始化这些内存空间为0; 需要跟声明内存属性的AREA伪指令一起使用;

      STR:    store register;"STR     r0, [r1]" 表示将r0的数据存储到[r1]中,[r1]表示将r1寄存器内的数据作为地址1,对地址1的数据操作;

                    "STR     r0, [r1]" 目的寄存器为[r1];其他指令的目的地址都是放中间r0位置上的;

      STRH:

      STRB:

      B :       branch,跳转到目标地址执行,执行指令集为ARM指令集;

      BX:      branch with exchange;跳转到目标地址执行,并且指令集从ARM指令集切换到thumb指令集;

      BEQ:    branch if equal;前一条搭配CMP指令使用,如果CMP相等,那么跳转到BEQ的标签处执行;

      MRS:     move to register from state register;读取 特殊功能寄存器的值 至 通用功能寄存器;

      MSR:     move to state register from register;读取 通用功能寄存器的值 至 特殊功能寄存器;

      CMP:     compare;"CMP     r3, #1"  将r3的数据减1,结果为0,那么后面的BEQ跳转执行;CMP指令不改变r3内的数据;

      ORR:   or 按位或;" ORR     lr, lr, #0x04 " 将后面的立即数与后面的lr寄存器数据按位或之后,再把数据存入前面的lr寄存器中;

      CBZ:    compare branch if zero;

      CBNZ:    compare branch if not zero;

    2.2.1 栈操作指令

      cm3不支持硬件堆栈操作,对于通用寄存器的保存是通过STM、LDM系列指令来实现的;以下为两个汇编会用到的指令;

      //cm3内核STM和LDM指令默认高地址寄存器优先处理,所有后面的寄存器组排序顺序没有关系;这个原则不对,和代码冲突,保留;

        STMFD:  store multi full descending;等价于STMDB;

        STMDB:  store multi descending before;先递减地址,后push推入数据到指针所在地址;指针寄存器搭配"!"使用,则指针寄存器地址会随之改变;

        LDMFD:  load  multi full descending;等价于LDMIA;

        LDMIA:   load multi increment after;先pop弹出数据到通用寄存器组,后递增地址;指针寄存器搭配"!"使用,则指针寄存器地址会随之改变;

LDMFD   r1!, {r4 - r11}         ;LDMIA将线程栈指针r1(操作之前先递减)指向的内容加载到CPU寄存器r4~r11
STMFD   r1!, {r4 - r11}         ;STMDB将CPU寄存器r4~r11的值存储到r1指向的地址(每操作一次地址将递减一次)

  2.3 tips

    2.3.1  [register]:间接取址,作用类似指针;将register内的数据作为地址1,对地址1内的数据进行操作;

    2.3.2 #常数:    汇编指令中的常数都是地址,自然数的格式为#常数;

    2.3.3 ARM汇编指令-STMFD/LDMFD - Little_Village - 博客园 (cnblogs.com)

3 堆栈汇

  3.1 堆栈

    3.1.1 STACK 栈

      主要用来存储局部变量,对象的内存块;栈空间是从高地址开始向低地址生长的;

      对于多级调用的函数入口地址,使用栈空间配合连接寄存器来存储主调函数的地址;

    3.1.2 HEAP 堆

      动态分配时使用的内存块;堆空间是从低地址向高地址生长的;

      如果程序中没有使用到动态内存分配的话,编译器是不会编译出"堆"空间的;

     3.1.3 tips

      关于栈空间:程序返回,栈指针大约会自动弹回,该程序使用后的地址可以重复存储使用;

      关于堆空间:而动态分配的内存如果不主动释放,下次动态分配不会重复使用该内存地址,可能导致内存泄漏;

  3.2 堆栈的汇编程序

;***********************分配数据段用来做"栈"**********************
; 表示即将声明数据段STACK用来做"栈",不用填入初始数据,可读写,首地址按照2^3对齐(8字节对齐);  
; 0x1000可表示2^13,地址2^3对齐, 则可寻址2^10;即1k bytes; ×××不对; 
; 0x1000可表示2^12,             即可寻址4K bytes;最高位是限制范围的,后面的0才是可表示范围;√√√ 
Stack_Size      EQU     0x00001000                              ;Stack_Size为栈大小;
                AREA    STACK, NOINIT, READWRITE, ALIGN=3       ;声明一个数据段STACK,数据段属性...;需要搭配SPACE一起声明数据段STACK;
Stack_Mem       SPACE   Stack_Size                              ;Stack_Mem为栈的首地址,应该是给microLIB库使用的;
__initil_sp                                                    ;栈标号,表示栈顶地址;也就是栈的内存最大地址;

; **********************分配数据段用来做"堆"**********************
Heap_Size       EQU     0x00000800                               ;2K bytes
                AREA    HEAP, NOINIT, READWRITE, ALIGN=3         ;声明一个数据段HEAP,属性...;需要搭配SPACE使用;
__heap_base                                                      ;__heap_base 堆标号,表示"堆"的起始地址;
Heap_Mem        SPACE   Heap_Size                                ;Heap_Mem 堆的首地址,应该是给microLIB库使用的;
__heap_limit                                                     ;堆标号,表示"堆"的结束地址;

    堆栈属性ALIGN=3,2^3字节对齐,地址8字节那就是64位对齐了;大约是为了兼容浮点运算;

    3.2.1 Stack_Mem和Stack_Size

 1 ;MDK针对嵌入式推出了microLIB小型库,在功能上是用来替代C标准库的;
 2 ;下面代码的功能主要是决定要不要启用microLIB库;以下代码比较不重要;
 3                  IF      :DEF:__MICROLIB                
 4                  EXPORT  __initial_sp
 5                  EXPORT  __heap_base
 6                  EXPORT  __heap_limit                
 7                  ELSE             
 8                  IMPORT  __use_two_region_memory
 9                  EXPORT  __user_initial_stackheap               
10 __user_initial_stackheap
11                  LDR     R0, =  Heap_Mem
12                  LDR     R1, =(Stack_Mem + Stack_Size)
13                  LDR     R2, = (Heap_Mem +  Heap_Size)
14                  LDR     R3, = Stack_Mem
15                  BX      LR
16                  ALIGN
17                  ENDIF
microLIB

   3.2 中断向量表

;**************中断向量表*****************************************************************
                AREA    RESET, DATA, READONLY   ; 即将声明一个内存区域RESET,是数据段,只读;
                EXPORT  __Vectors               ; 声明"标识__Vectors"可以被外部文件调用
                EXPORT  __Vectors_End           ; 声明"标识__Vectors_End"可以被外部文件调用
                EXPORT  __Vectors_Size          ; 声明"标识__Vectors_Size"可以被外部文件调用

__Vectors       DCD     __initial_sp            ; 分配4字节内存,初始化为栈顶地址,该内存开始的地址标识为"__Vectors"
                DCD     Reset_Handler           ; 分配4字节内存,放入复位中断服务函数的"地址标识Reset Handler"
                DCD     NMI_Handler             ; 
                ;.....
                DCD     0                       ;                                                             
                DCD     WAKEUP_PIN_IRQHandler   ; 
__Vectors_End                                   ; 内存结束的地址标识为"__Vectors_End"

__Vectors_Size  EQU  __Vectors_End - __Vectors  ; 声明标识"__Vectors_Size"用来表示中断向量表的大小

;**************汇编代码段:上电复位中断服务程序举例***************************************************************
                AREA    |.text|, CODE, READONLY ; 即将声明一个内存区域.text,是代码段,只读;

Reset_Handler    PROC                           ;伪指令PROC和ENDP用来声明一个程序,当前程序段标识为"Reset_Handler"
                 EXPORT  Reset_Handler   [WEAK] ;声明当前程序可被外部函数调用,为弱函数
        IMPORT  SystemInit                      ;引入文件外部声明的函数SystemInit()
        IMPORT  __main                          ;引入文件外部声明的函数__main()
                 LDR     R0, =SystemInit        ;将32bit函数入口地址放入R0寄存器中?
                 BLX     R0                     ;跳转到R0寄存器内的地址去执行?
                 LDR     R0, =__main            ;__main为编译器自动生成的函数,主要是解析代码段table的数据,然后跳转到main函数执行,
                 BX      R0                     ;给需要初始化的变量和不需要初始化的变量分配堆栈运行空间;
                 ENDP                           ;

;**************汇编代码段:NMI中断服务程序举例***************************************************************
NMI_Handler     PROC                            ;
                EXPORT  NMI_Handler     [WEAK]  ;这里导入了中断服务程序,如果外部文件写了,那此处的弱函数就不使用了
                B       .                       ;B. 表示在这里跳转当前程序? 就是跳转到前面导入的程序
                ENDP                            ;

4 Systeminit( )

  这个函数还是蛮重要的,先放着,改天得完整仔细过一遍;可能应该放rcc里更适合;

SystemInit( )
/** @addtogroup STM32F10x_System_Private_Functions
  * @{
  */

/**
  * @brief  Setup the microcontroller system
  *         Initialize the Embedded Flash Interface, the PLL and update the 
  *         SystemCoreClock variable.
  * @note   This function should be used only after reset.
  * @param  None
  * @retval None
  */
void SystemInit (void)
{
  /* Reset the RCC clock configuration to the default reset state(for debug purpose) */
  /* Set HSION bit */
  RCC->CR |= (uint32_t)0x00000001;

  /* Reset SW, HPRE, PPRE1, PPRE2, ADCPRE and MCO bits */
#ifndef STM32F10X_CL
  RCC->CFGR &= (uint32_t)0xF8FF0000;
#else
  RCC->CFGR &= (uint32_t)0xF0FF0000;
#endif /* STM32F10X_CL */   
  
  /* Reset HSEON, CSSON and PLLON bits */
  RCC->CR &= (uint32_t)0xFEF6FFFF;

  /* Reset HSEBYP bit */
  RCC->CR &= (uint32_t)0xFFFBFFFF;

  /* Reset PLLSRC, PLLXTPRE, PLLMUL and USBPRE/OTGFSPRE bits */
  RCC->CFGR &= (uint32_t)0xFF80FFFF;

#ifdef STM32F10X_CL
  /* Reset PLL2ON and PLL3ON bits */
  RCC->CR &= (uint32_t)0xEBFFFFFF;

  /* Disable all interrupts and clear pending bits  */
  RCC->CIR = 0x00FF0000;

  /* Reset CFGR2 register */
  RCC->CFGR2 = 0x00000000;
#elif defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
  /* Disable all interrupts and clear pending bits  */
  RCC->CIR = 0x009F0000;

  /* Reset CFGR2 register */
  RCC->CFGR2 = 0x00000000;      
#else
  /* Disable all interrupts and clear pending bits  */
  RCC->CIR = 0x009F0000;
#endif /* STM32F10X_CL */
    
#if defined (STM32F10X_HD) || (defined STM32F10X_XL) || (defined STM32F10X_HD_VL)
  #ifdef DATA_IN_ExtSRAM
    SystemInit_ExtMemCtl(); 
  #endif /* DATA_IN_ExtSRAM */
#endif 

  /* Configure the System clock frequency, HCLK, PCLK2 and PCLK1 prescalers */
  /* Configure the Flash Latency cycles and enable prefetch buffer */
  SetSysClock();

#ifdef VECT_TAB_SRAM
  SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */
#else
  SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH. */
#endif 
}

5 小结

  对于堆栈,平常使用的局部数组什么的不要超出栈的内存大小,超出的话内存泄漏,程序卡死;

  平常说的push和pop应该只针对栈而言;如果堆也能push和pop,针对一整块动态内存,怎么搞呢?从低地址到高地址搞;

  启动文件除了分配中断向量表空间之外,只主动执行了一个Reset_Handler一个函数,

  Reset_Handler函数跳转到system_init函数执行,然后跳转到MDK的__main函数执行;

posted @ 2020-09-19 14:06  caesura_k  阅读(1377)  评论(0编辑  收藏  举报