STM32启动文件

startup_stm32f10x_ld_vl.s : for STM32 Low density Value line devices
startup_stm32f10x_ld.s : for STM32 Low density devices
startup_stm32f10x_md_vl.s : for STM32 Medium density Value line devices
startup_stm32f10x_md.s : for STM32 Medium density devices
startup_stm32f10x_hd.s : for STM32 High density devices
startup_stm32f10x_xl.s : for STM32 XL density devices
startup_stm32f10x_cl.s : for STM32 Connectivity line devices

cl:互联型产品,stm32f105/107系列
vl:超值型产品,stm32f100系列
xl:超高密度产品,stm32f101/103系列
ld:低密度产品,FLASH小于64K
md:中等密度产品,FLASH=64 or 128
hd:高密度产品,FLASH大于128

1 startup_stm32f10x_hd.s解析:

1.1 启动文件简介

Set the initial SP                      //初始化堆栈指针 SP
Set the initial PC == Reset_Handler     //初始化 PC 指针
Set the vector table entries with the exceptions ISR address      //初始化中断向量表
Configure the system clock and the external SRAM mounted on STM324xG-EVAL board to be used as data memory (optional,to be enabled by user)   //配置系统时钟
Branches to __main in the C library (which eventually calls main()).    //调用 C 库函数_main 初始化用户堆栈,从而最终调用 main 函数去到 C 的世界

        ARM汇编指令:

指令名称作用
EQU给数字常量取一个符号名,相当于 C 语言中的 define
AREA汇编一个新的代码段或者数据段
SPACE分配内存地址
PRESERVE8当前文件堆栈需按照 8 字节对齐
EXPORT声明一个标号具有全局属性,可被外部的文件使用
DCD以字为单位分配内存,要求 4 字节对齐,并要求初始化这些内存
PORC定义子程序,与 ENDP 成对使用,表示子程序结束
WEAK弱定义,如果外部文件声明了一个标号,则优先使用外部文件定义的标号,如果外部文件没有定义也不出错。要注意的是:这个不是 ARM的指令,是编译器的,这里放在一起只是为了方便。
IMPORT声明标号来自外部文件,跟 C 语言中的 EXTERN 关键字类似
B跳转到一个标号
ALIGN编译器对指令或者数据的存放地址进行对齐,一般需要跟一个立即数,缺省表示 4 字节对齐。要注意的是:这个不是 ARM 的指令,是编译器的,这里放在一起只是为了方便。
END到达文件的末尾,文件结束
IF,ELSE,ENDIF汇编条件分支语句,跟 C 语言的 if else 类似

1.2 开辟栈空间

/* Amount of memory (in bytes) allocated for Stack  
 Tailor this value to your application needs  
 <h> Stack Configuration                     ;栈定义  
 <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>  
 </h> */
Stack_Size  EQU     0x00000400  ;    /*EQU伪指令,作用是左边的符号名代表右边的表达式,定义栈大小1024B*/  

            AREA    STACK, NOINIT, READWRITE, ALIGN=3    /*定义栈段:名称为STACK,未初始化,可读写,ELF 的栈段按2^3=8对齐*/  
Stack_Mem   SPACE   Stack_Size    /*分配一片连续的存储区域并初始化为 0,栈空间:0x400个字节*/
__initial_sp    /*栈空间顶地址*/
  • 开辟栈的大小为 0X00000400(1KB)(4*162),名字为 STACK,NOINIT 即不初始化,可读可写, 8(2^3)字节对齐。
  • 栈的作用是用于局部变量,函数调用,函数形参等的开销,栈的大小不能超过内部SRAM 的大小。如果编写的程序比较大,定义的局部变量很多,那么就需要修改栈的大小。如果某一天,你写的程序出现了莫名奇怪的错误,并进入了硬 fault 的时候,这时你就要考虑下是不是栈不够大,溢出了。
  • EQU:宏定义的伪指令,相当于等于,类似与 C 中的 define。
  • AREA:告诉汇编器汇编一个新的代码段或者数据段。 STACK 表示段名,这个可以任意命名; NOINIT 表示不初始化; READWRITE 表示可读可写, ALIGN=3,表示按照 2^3对齐,即 8 字节对齐。
  • SPACE:用于分配一定大小的内存空间,单位为字节。这里指定大小等于 Stack_Size。标号__initial_sp 紧挨着 SPACE 语句放置,表示栈的结束地址,即栈顶地址,栈是由高向低生长的,而地址是由低向高分配的。

1.3 开辟堆空间

/*  <h> Heap Configuration                  堆定义  
    <o>  Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>  
    </h>  */
Heap_Size   EQU     0x00000200          //定义堆的大小

            AREA    HEAP, NOINIT, READWRITE, ALIGN=3    //堆段,malloc用的地方,不一定连续空间,未初始化,允许读写,堆数据段8字节边界对齐
__heap_base    /*堆空间起始地址*/
Heap_Mem    SPACE   Heap_Size    /*堆空间:0x200个字节*/
__heap_limit    /*堆空间结束地址*/

    PRESERVE8    /*PRESERVE8 指令指定当前文件保持堆栈八字节对齐*/
    THUMB    /*告诉汇编器下面是32位的Thumb指令,如果需要汇编器将插入位以保证对齐*/
  • 开辟堆的大小为 0X00000200(512 字节),名字为 HEAP, NOINIT 即不初始化,可读可写, 8(2^3)字节对齐。
  • __heap_base表示堆的起始地址,__heap_limit表示堆的结束址。堆是由低向高生长的,跟栈的生长方向相反。
  • 堆主要用来动态内存的分配,像 malloc()函数申请的内存就在堆上面。这个在 STM32里面用的比较少。、
  •  PRESERVE8表指定当前文件的堆栈按照8字节对齐,THUMB表示接下来的指令兼容THUMB指令。
  • THUMB是ARM老的16bit的指令集,现在Cortex-M系列的ARM都使用32bit的THUMB-2指令集,它兼容16bit和32bit的指令。

1.4 定义向量表

/*EXPORT 命令声明一个符号,可由链接器用于解释各个目标和库文件中的符号引用,相当于声明了一个全局变量。 GLOBAL 于 EXPORT相同。
 以下为向量表,在复位时被映射到FLASH的0地址
 Vector Table Mapped to Address 0 at Reset;实际上是在CODE区(假设STM32从FLASH启动,则此中断向量表起始地址即为0x8000000)*/
AREA    RESET, DATA, READONLY    /*定义一块数据段<DATA>,只可读<READONLY,默认READWRITE>,段名字是RESET*/
EXPORT  __Vectors    /*标号输出,中断向量表开始;EXPORT在程序中声明一个全局的标号__Vectors,该标号可在其他的文件中引用*/
EXPORT  __Vectors_End    /*在程序中声明一个全局标号__Vectors_End*/
EXPORT  __Vectors_Size    /*在程序中声明一个全局号__Vectors_Size,中断向量表大小*/
  • 定义一个数据段,名字为 RESET,可读。并声明 __Vectors__Vectors_End和 __Vectors_Size这三个标号具有全局属性,可供外部的文件调用。
  • EXPORT: 声明一个标号可被外部的文件使用,使标号具有全局属性。如果是 IAR 编译器,则使用的是 GLOBAL 这个指令。
  • 当内核响应了一个发生的异常后,对应的异常服务例程(ESR)就会执行。为了决定 ESR的入口地址, 内核使用了“向量表查表机制”。这里使用一张向量表。向量表其实是一个WORD(32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。向量表在地址空间中的位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 (即 FLASH 地址 0) 处必须包含一张向量表,用于初始时的异常分配。要注意的是这里有个另类: 0 号类型并不是什么入口地址,而是给出了复位后 MSP 的初值。

        由手册存储器映像图获知,STM32的0地址处是用来映射的。

         STM32启动的方式如下:

        (1)用户闪存存储器: 用户代码烧录在这里,STM32正常启动时就是从这里启动
        (2)系统存储器: 实现ISP下载功能。ISP(in-system programming)意为在系统编程。烧录程序时不需要烧录器,PC机通过串口把BIN/HEX文件直接烧录到单片机内部FLASH中
        (3)内嵌SRAM: 实现调试器调试功能用
        当选择从用户闪存存储器(flash)启动时候,0x0地址就会把flash的起始地址映射于此,flash的起始地址是0x08000000,中断向量表就放在这个地址。当用户选择从内嵌SRAM启动时候,0x0地址就会把SRAM的起始地址映射于此,SRAM的起始地址是0x20000000。注意,一般我们都是选择从flash启动的

         中断向量表我的内容如下:

//DCD 命令分配一个或多个字的存储器,在四个字节的边界上对齐,并定义存储器的运行时初值。
__Vectors   DCD     __initial_sp               // Top of Stack     栈顶指针,被放在向量表的开始,FLASH的0地址,复位后首先装载栈顶指针 
            DCD     Reset_Handler              // Reset Handler   复位异常,装载完栈顶后,第一个执行的,并且不返回。
            DCD     NMI_Handler                // NMI Handler   不可屏蔽中断
            DCD     HardFault_Handler          // Hard Fault Handler  硬件错误中断
            DCD     MemManage_Handler          // MPU Fault Handler   内存管理错误中断
            DCD     BusFault_Handler           // Bus Fault Handler  总线错误中断,一般发生在数据访问异常,比如fsmc访问不当
            DCD     UsageFault_Handler         // Usage Fault Handler 用法错误中断,一般是预取值,或者位置指令,数据处理等错误
            DCD     0                          // Reserved
            DCD     0                          // Reserved
            DCD     0                          // Reserved
            DCD     0                          // Reserved
            DCD     SVC_Handler                // SVCall Handler   系统调用异常,主要是为了调用操作系统内核服务
            DCD     DebugMon_Handler           // Debug Monitor Handler 调试监视异常
            DCD     0                          // Reserved
            DCD     PendSV_Handler             /*PendSV Handler   挂起异常,此处可以看见用作了uCOS-II的上下文切换异常,这是被推荐使用的,因为Cortex-M3会在异常发生时自动保存R0-R3,R12,R13(堆栈指针SP),R14(链接地址,也叫返回地址LR,在异常返回时使用),R15(程序计数器PC,为当前应用程序+4)和中断完成时自动回复我们只需保存R4-R11,大大减少了中断响应和上下文切换的时间。说明:此处涉及到一个中断保存寄存器问题:因为在所有的运行模式下,未分组寄存器都指向同一个物理寄存器,他们未被系统用作特殊的用途,因此,在中断或者异常处理进行模式转换时,由于不同模式(此处为"线程"和"特权")均使用相同的物理寄存器,可能会造成寄存器中数据的破坏,这也是常说的"关键代码段"和"l临界区"保护的原因。*/
            DCD     SysTick_Handler            // SysTick Handler  滴答定时器,为操作系统内核时钟

            // External Interrupts        以上都是Coretex M3内核自带的;以下为外部中断向量表
            DCD     WWDG_IRQHandler            // Window Watchdog
            DCD     PVD_IRQHandler             // PVD through EXTI Line detect
            DCD     TAMPER_IRQHandler          // Tamper
            DCD     RTC_IRQHandler             // RTC
            DCD     FLASH_IRQHandler           // Flash
            DCD     RCC_IRQHandler             // RCC
            DCD     EXTI0_IRQHandler           // EXTI Line 0
            DCD     EXTI1_IRQHandler           // EXTI Line 1
            DCD     EXTI2_IRQHandler           // EXTI Line 2
            DCD     EXTI3_IRQHandler           // EXTI Line 3
            DCD     EXTI4_IRQHandler           // EXTI Line 4
            DCD     DMA1_Channel1_IRQHandler   // DMA1 Channel 1
            DCD     DMA1_Channel2_IRQHandler   // DMA1 Channel 2
            DCD     DMA1_Channel3_IRQHandler   // DMA1 Channel 3
            DCD     DMA1_Channel4_IRQHandler   // DMA1 Channel 4
            DCD     DMA1_Channel5_IRQHandler   // DMA1 Channel 5
            DCD     DMA1_Channel6_IRQHandler   // DMA1 Channel 6
            DCD     DMA1_Channel7_IRQHandler   // DMA1 Channel 7
            DCD     ADC1_2_IRQHandler          // ADC1 & ADC2
            DCD     USB_HP_CAN1_TX_IRQHandler  // USB High Priority or CAN1 TX
            DCD     USB_LP_CAN1_RX0_IRQHandler // USB Low  Priority or CAN1 RX0
            DCD     CAN1_RX1_IRQHandler        // CAN1 RX1
            DCD     CAN1_SCE_IRQHandler        // CAN1 SCE
            DCD     EXTI9_5_IRQHandler         // EXTI Line 9..5
            DCD     TIM1_BRK_IRQHandler        // TIM1 Break
            DCD     TIM1_UP_IRQHandler         // TIM1 Update
            DCD     TIM1_TRG_COM_IRQHandler    // TIM1 Trigger and Commutation
            DCD     TIM1_CC_IRQHandler         // TIM1 Capture Compare
            DCD     TIM2_IRQHandler            // TIM2
            DCD     TIM3_IRQHandler            // TIM3
            DCD     TIM4_IRQHandler            // TIM4
            DCD     I2C1_EV_IRQHandler         // I2C1 Event
            DCD     I2C1_ER_IRQHandler         // I2C1 Error
            DCD     I2C2_EV_IRQHandler         // I2C2 Event
            DCD     I2C2_ER_IRQHandler         // I2C2 Error
            DCD     SPI1_IRQHandler            // SPI1
            DCD     SPI2_IRQHandler            // SPI2
            DCD     USART1_IRQHandler          // USART1
            DCD     USART2_IRQHandler          // USART2
            DCD     USART3_IRQHandler          // USART3
            DCD     EXTI15_10_IRQHandler       // EXTI Line 15..10
            DCD     RTCAlarm_IRQHandler        // RTC Alarm through EXTI Line
            DCD     USBWakeUp_IRQHandler       // USB Wakeup from suspend
            DCD     TIM8_BRK_IRQHandler        // TIM8 Break
            DCD     TIM8_UP_IRQHandler         // TIM8 Update
            DCD     TIM8_TRG_COM_IRQHandler    // TIM8 Trigger and Commutation
            DCD     TIM8_CC_IRQHandler         // TIM8 Capture Compare
            DCD     ADC3_IRQHandler            // ADC3
            DCD     FSMC_IRQHandler            // FSMC
            DCD     SDIO_IRQHandler            // SDIO
            DCD     TIM5_IRQHandler            // TIM5
            DCD     SPI3_IRQHandler            // SPI3
            DCD     UART4_IRQHandler           // UART4
            DCD     UART5_IRQHandler           // UART5
            DCD     TIM6_IRQHandler            // TIM6
            DCD     TIM7_IRQHandler            // TIM7
            DCD     DMA2_Channel1_IRQHandler   // DMA2 Channel1
            DCD     DMA2_Channel2_IRQHandler   // DMA2 Channel2
            DCD     DMA2_Channel3_IRQHandler   // DMA2 Channel3
            DCD     DMA2_Channel4_5_IRQHandler // DMA2 Channel4 & Channel5
__Vectors_End                                  //向量表结束标志
__Vectors_Size  EQU  __Vectors_End - __Vectors //计算向量表地址空间大小,得到向量表的大小,304个字节也就是0x130个字节

        在《STM32中文参考手册_V10.pdf》的第9章,有对这些中断的描述的表格:(部分截图)

         向量表(一般)从flash的0地址处开始放置,以4字节为一单位,地址0存放的是栈顶地址,0x04放的是复位处理函数的地址,以此类推。代码中的”*_Handler”是对应中断的中断服务函数,也就是中断处理程序的地址。

 1.5 复位处理程序

        复位处理程序是系统上电/复位后第一个执行的:

AREA    |.text|, CODE, READONLY     /*定义一个代码段,可读,段名字是.text  
                                     |.text|  用于表示由 C 编译程序产生的代码段,或用于以某种方式与 C 库关联的代码段。定义C编译器源代码的代码段,只读.定义一个代码段,可读,段名字是.text。 */  
                  
Reset_Handler   PROC                //利用PROC、ENDP这一对伪指令把程序段分为若干个过程,使程序的结构加清晰
EXPORT  Reset_Handler             [WEAK]    /*[WEAK];WEAK声明其他的同名标号优先于该标号被引用,就是说如果外面声明了的话,调用外面的对应函数*/
IMPORT  __main             /*;IMPORT:伪指令用于通知编译器要使用的标号在其他的源文件中定义/但要在当前源文件中引用,而且无论当前源文件是否引用该标号/该标号均会被加入到当前源文件的符号表中*/
IMPORT  SystemInit
LDR     R0, =SystemInit    /*系统初始化*/
BLX     R0                 /*带链接的跳转,切换指令集,跳到SystemInit*/      
LDR     R0, =__main        /*__main为运行时库提供的函数;完成堆栈,堆的初始化等工作,会调用下面定义的__user_initial_stackheap*/
BX      R0                 /*切换指令集,main函数不返回跳到__main,进入C的世界*/
ENDP
  • 定义一个名称为.text 的代码段,可读。
  • 复位子程序是系统上电后第一个执行的程序,调用 SystemInit 函数初始化系统时钟,然后调用 C 库函数_mian,最终调用 main 函数去到 C 的世界。
  • WEAK:表示弱定义,如果外部文件优先定义了该标号则首先引用该标号,如果外部文件没有声明也不会出错。这里表示复位子程序可以由用户在其他文件重新实现,这里并不是唯一的。特别注意[WEAK]声明,它表示弱定义:如果外部文件优先定义了该标号则首先引用外部文件定义的标号,反之就引用此处用[WEAK]声明的标号。
  • IMPORT:表示该标号来自外部文件,跟 C 语言中的 EXTERN 关键字类似。这里表示SystemInit 和__main 这两个函数均来自外部的文件。
  • SystemInit() 是一个标准的库函数,在 system_stm32f4xx.c 这个库文件总定义。主要作用是配置系统时钟,这里调用这个函数之后,F429 的系统时钟配被配置为 180M。
  • __main 是一个标准的 C 库函数,主要作用是初始化用户堆栈,最终调用 main 函数去到 C 的世界。这就是为什么我们写的程序都有一个 main 函数的原因。如果我们在这里不调用__main,那么程序最终就不会调用我们 C 文件里面的 main,如果是调皮的用户就可以修改主函数的名称,然后在这里面 IMPORT 你写的主函数名称即可。
  • LDR:作为伪指令加载一个立即数或一个地址到一个寄存器

  • 这个时候你在 C 文件里面写的主函数名称就不是 main 了,而是 user_main 了。

        LDR、 BLX、 BX 是 CM4 内核的指令,可在《CM3 权威指南 CnR2》第四章-指令集里面查询到,具体作用见下表:

指令名称做用
LDR从存储器中加载字到一个寄存器中
BL跳转到由寄存器/标号给出的地址,并把跳转前的下条指令地址保存到 LR
BLX跳转到由寄存器给出的地址,并根据寄存器的 LSE 确定处理器的状态,还要把跳转前的下条指令地址保存到 LR
BX跳转到由寄存器/标号给出的地址,不用返回

1.6 中断服务函数

  • 在启动文件里面已经帮我们写好所有中断的中断服务函数,跟我们平时写的中断服务函数不一样的就是这些函数都是空的,真正的中断复服务程序需要我们在外部的 C 文件里面重新实现,这里只是提前占了一个位置而已。
  • 如果我们在使用某个外设的时候,开启了某个中断,但是又忘记编写配套的中断服务程序或者函数名写错,那当中断来临的时,程序就会跳转到启动文件预先写好的空的中断服务程序中,并且在这个空函数中无线循环,即程序就死在这里。
// Dummy Exception Handlers (infinite loops which can be modified) 
// WEAK声明其他的同名标号优先于该标号被引用,就是说如果外面声明了的话,;会调用外面的
NMI_Handler   PROC                  //系统异常
EXPORT  NMI_Handler                [WEAK]    //不可屏蔽中断处理函数
B       .                           //B表跳转到一个标号,"."表无限循环
ENDP
HardFault_Handler\                   //意为换行  
                PROC  
                EXPORT  HardFault_Handler          [WEAK]    //硬件错误处理函数  
                B       .  
                ENDP  
MemManage_Handler\  
                PROC  
                EXPORT  MemManage_Handler          [WEAK]  
                B       .  
                ENDP  
BusFault_Handler\  
                PROC  
                EXPORT  BusFault_Handler           [WEAK]  
                B       .  
                ENDP  
UsageFault_Handler\  
                PROC  
                EXPORT  UsageFault_Handler         [WEAK]  
                B       .  
                ENDP  
SVC_Handler     PROC  
                EXPORT  SVC_Handler                [WEAK]  
                B       .  
                ENDP  
DebugMon_Handler\  
                PROC  
                EXPORT  DebugMon_Handler           [WEAK]  
                B       .  
                ENDP  
PendSV_Handler  PROC  
                EXPORT  PendSV_Handler             [WEAK]  
                B       .  
                ENDP  
SysTick_Handler PROC  
                EXPORT  SysTick_Handler            [WEAK]  
                B       .  
                ENDP  
  
Default_Handler PROC  
/*输出异常向量表标号,方便外部实现异常的具体功能,[WEAK]是弱定义的意思,如果外部定义了,优先执行外部定义,否则下面的函数定义*/  
                EXPORT  WWDG_IRQHandler            [WEAK]  
                EXPORT  PVD_IRQHandler             [WEAK]  
                EXPORT  TAMPER_IRQHandler          [WEAK]  
                EXPORT  RTC_IRQHandler             [WEAK]  
                EXPORT  FLASH_IRQHandler           [WEAK]  
                EXPORT  RCC_IRQHandler             [WEAK]  
                EXPORT  EXTI0_IRQHandler           [WEAK]  
                EXPORT  EXTI1_IRQHandler           [WEAK]  
                EXPORT  EXTI2_IRQHandler           [WEAK]  
                EXPORT  EXTI3_IRQHandler           [WEAK]  
                EXPORT  EXTI4_IRQHandler           [WEAK]  
                EXPORT  DMA1_Channel1_IRQHandler   [WEAK]  
                EXPORT  DMA1_Channel2_IRQHandler   [WEAK]  
                EXPORT  DMA1_Channel3_IRQHandler   [WEAK]  
                EXPORT  DMA1_Channel4_IRQHandler   [WEAK]  
                EXPORT  DMA1_Channel5_IRQHandler   [WEAK]  
                EXPORT  DMA1_Channel6_IRQHandler   [WEAK]  
                EXPORT  DMA1_Channel7_IRQHandler   [WEAK]  
                EXPORT  ADC1_2_IRQHandler          [WEAK]  
                EXPORT  USB_HP_CAN1_TX_IRQHandler  [WEAK]  
                EXPORT  USB_LP_CAN1_RX0_IRQHandler [WEAK]  
                EXPORT  CAN1_RX1_IRQHandler        [WEAK]  
                EXPORT  CAN1_SCE_IRQHandler        [WEAK]  
                EXPORT  EXTI9_5_IRQHandler         [WEAK]  
                EXPORT  TIM1_BRK_IRQHandler        [WEAK]  
                EXPORT  TIM1_UP_IRQHandler         [WEAK]  
                EXPORT  TIM1_TRG_COM_IRQHandler    [WEAK]  
                EXPORT  TIM1_CC_IRQHandler         [WEAK]  
                EXPORT  TIM2_IRQHandler            [WEAK]  
                EXPORT  TIM3_IRQHandler            [WEAK]  
                EXPORT  TIM4_IRQHandler            [WEAK]  
                EXPORT  I2C1_EV_IRQHandler         [WEAK]  
                EXPORT  I2C1_ER_IRQHandler         [WEAK]  
                EXPORT  I2C2_EV_IRQHandler         [WEAK]  
                EXPORT  I2C2_ER_IRQHandler         [WEAK]  
                EXPORT  SPI1_IRQHandler            [WEAK]  
                EXPORT  SPI2_IRQHandler            [WEAK]  
                EXPORT  USART1_IRQHandler          [WEAK]  
                EXPORT  USART2_IRQHandler          [WEAK]  
                EXPORT  USART3_IRQHandler          [WEAK]  
                EXPORT  EXTI15_10_IRQHandler       [WEAK]  
                EXPORT  RTCAlarm_IRQHandler        [WEAK]  
                EXPORT  USBWakeUp_IRQHandler       [WEAK]  
                EXPORT  TIM8_BRK_IRQHandler        [WEAK]  
                EXPORT  TIM8_UP_IRQHandler         [WEAK]  
                EXPORT  TIM8_TRG_COM_IRQHandler    [WEAK]  
                EXPORT  TIM8_CC_IRQHandler         [WEAK]  
                EXPORT  ADC3_IRQHandler            [WEAK]  
                EXPORT  FSMC_IRQHandler            [WEAK]  
                EXPORT  SDIO_IRQHandler            [WEAK]  
                EXPORT  TIM5_IRQHandler            [WEAK]  
                EXPORT  SPI3_IRQHandler            [WEAK]  
                EXPORT  UART4_IRQHandler           [WEAK]  
                EXPORT  UART5_IRQHandler           [WEAK]  
                EXPORT  TIM6_IRQHandler            [WEAK]  
                EXPORT  TIM7_IRQHandler            [WEAK]  
                EXPORT  DMA2_Channel1_IRQHandler   [WEAK]  
                EXPORT  DMA2_Channel2_IRQHandler   [WEAK]  
                EXPORT  DMA2_Channel3_IRQHandler   [WEAK]  
                EXPORT  DMA2_Channel4_5_IRQHandler [WEAK]  
/*如下只是定义一些空函数 */ 
WWDG_IRQHandler  
PVD_IRQHandler  
TAMPER_IRQHandler  
RTC_IRQHandler  
FLASH_IRQHandler  
RCC_IRQHandler  
EXTI0_IRQHandler  
EXTI1_IRQHandler  
EXTI2_IRQHandler  
EXTI3_IRQHandler  
EXTI4_IRQHandler  
DMA1_Channel1_IRQHandler  
DMA1_Channel2_IRQHandler  
DMA1_Channel3_IRQHandler  
DMA1_Channel4_IRQHandler  
DMA1_Channel5_IRQHandler  
DMA1_Channel6_IRQHandler  
DMA1_Channel7_IRQHandler  
ADC1_2_IRQHandler  
USB_HP_CAN1_TX_IRQHandler  
USB_LP_CAN1_RX0_IRQHandler  
CAN1_RX1_IRQHandler  
CAN1_SCE_IRQHandler  
EXTI9_5_IRQHandler  
TIM1_BRK_IRQHandler  
TIM1_UP_IRQHandler  
TIM1_TRG_COM_IRQHandler  
TIM1_CC_IRQHandler  
TIM2_IRQHandler  
TIM3_IRQHandler  
TIM4_IRQHandler  
I2C1_EV_IRQHandler  
I2C1_ER_IRQHandler  
I2C2_EV_IRQHandler  
I2C2_ER_IRQHandler  
SPI1_IRQHandler  
SPI2_IRQHandler  
USART1_IRQHandler  
USART2_IRQHandler  
USART3_IRQHandler  
EXTI15_10_IRQHandler  
RTCAlarm_IRQHandler  
USBWakeUp_IRQHandler  
TIM8_BRK_IRQHandler  
TIM8_UP_IRQHandler  
TIM8_TRG_COM_IRQHandler  
TIM8_CC_IRQHandler  
ADC3_IRQHandler  
FSMC_IRQHandler  
SDIO_IRQHandler  
TIM5_IRQHandler  
SPI3_IRQHandler  
UART4_IRQHandler  
UART5_IRQHandler  
TIM6_IRQHandler  
TIM7_IRQHandler  
DMA2_Channel1_IRQHandler  
DMA2_Channel2_IRQHandler  
DMA2_Channel3_IRQHandler  
DMA2_Channel4_5_IRQHandler  
                B       .  
  
                ENDP  
  
                ALIGN                   //默认是字对齐方式,也说明了代码是4字节对齐的 

       

  • B:跳转到一个标号。这里跳转到一个‘.’,即表示无线循环。
  •  真正的中断服务函数需要我们在外部的.c文件中实现,这里只是占位,是把中断和与之对应中断服务函数绑定了。当我们开启某个中断后,没有写对应的中断服务函数或者函数名有误,当中断来临时程序还是跳转到启动文件预先写好的中断服务函数中,在这个函数中无限循环。

1.7 堆栈初始化

/*******************************************************************************
 User Stack and Heap initialization
*******************************************************************************/
IF      :DEF:__MICROLIB         //判断是否使用DEF:__MICROLIB(micro lib),如果勾选了micro lib

EXPORT  __initial_sp            //将栈顶地址、堆起始地址、堆结束地址赋予全局属性,使外部程序可用
EXPORT  __heap_base
EXPORT  __heap_limit

ELSE                            //如果没有勾选micro lib

IMPORT  __use_two_region_memory    //两区堆栈空间,堆和栈有各自的空间地址
EXPORT  __user_initial_stackheap   //这个函数由用户自己实现

__user_initial_stackheap           //标号__user_initial_stackheap,表示用户堆栈初始化程序入口
/*此处是初始化两区的堆栈空间,堆是从由低到高的增长,栈是由高向低生长的,两个是互相独立的数据段,并不能交叉使用*/
LDR     R0, =  Heap_Mem                  //保存堆起始地址
LDR     R1, =(Stack_Mem + Stack_Size)    //保存栈结束地址
LDR     R2, = (Heap_Mem +  Heap_Size)    //保存堆结束地址
LDR     R3, = Stack_Mem                  //保存栈起始地址
BX      LR

ALIGN

ENDIF

END                                      //END命令指示汇编器,已到达一个源文件的末尾

ALIGH
  • ALIGN:对指令或者数据存放的地址进行对齐,后面会跟一个立即数。缺省表示 4 字节对齐。
  • 首先判断是否定义了__MICROLIB ,如果定义了这个宏则赋予标号__initial_sp(栈顶地址)、 __heap_base(堆起始地址)、 __heap_limit(堆结束地址)全局属性,可供外部文件调用。有关这个宏我们在 KEIL 里面配置,具体见下图。然后堆栈的初始化就由 C 库函数_main 来完成。

  • 选中它表示使用c库的备选库,里面有一个__mian函数。若定义了则赋予标号__initial_sp(栈顶地址)、__heap_base(堆的起始地址)、__heap_limit(堆的结束地址)为外部文件可调用的变量,即可供外部c库中的__main调用,由__main初始化堆栈,否则需要用户自己实现__user_initial_stackheap函数初始化堆栈。
  • 如果没有定义__MICROLIB,则插入标号__use_two_region_memory,这个函数需要用户自己实现,具体要实现成什么样,可在 KEIL 的帮助文档里面查询到,具体见下图。

  • 然后声明标号__user_initial_stackheap 具有全局属性,可供外部文件调用,并实现这个标号的内容。
  • IF,ELSE,ENDIF:汇编的条件分支语句,跟 C 语言的 if ,else 类似
  • END:文件结束。

        启动文件相当于嵌入式Linux中的BootLoader,只不过这里简单多了。感觉大概了解就可以,需要深究时再深究。

2 STM32 中断向量表的位置 、重定向

        知道怎么跳到main函数了,那么,中断发生后,又是怎么跑到中断入口地址的呢?
        从stm32f10x.s可以看到,已经定义好了一大堆的中断响应函数,这就是中断向量表,标号__Vectors,表示中断向量表入口地址,例如:AREA    RESET, DATA, READONLY ; 定义只读数据段,实际上是在CODE区(假设STM32从FLASH启动,则此中断向量表起始地址即为0x8000000)

         EXPORT  __Vectors  
 OS_CPU_SysTickHandler  
IMPORT  OS_CPU_PendSVHandler  
rs       DCD     __initial_sp              // Top of Stack  
         DCD     Reset_Handler             // Reset Handler  
         DCD     NMI_Handler               // NMI Handler  
         DCD     HardFault_Handler         // Hard Fault Handler  
         DCD     MemManage_Handler         // MPU Fault Handler  
         DCD     BusFault_Handler          // Bus Fault Handler  
         DCD     UsageFault_Handler        // Usage Fault Handler 

        这个向量表的编写是有讲究的,跟硬件一一对应不能乱写的,CPU找入口地址就靠它了,bin文件开头就是他们的地址,参考手册RM0008的10.1.2节可以看到排列。
        我们再结合CORTEX-M3的特性,他上电后根据boot引脚来决定PC位置,比如boot设置为flash启动,则启动后PC跳到0x08000000。此时CPU会先取2个地址,第一个是栈顶地址,第二个是复位异常地址,故有了上面的写法,这样就跳到reset_handler。那么这个reset_handler的实际地址是多少.?下面的一堆例如Nmi_handler地址又是多少呢?发生中断是怎么跑到这个地址的呢?下面挨个讲解。
        1:我们可以通过反向来得知这些入口地址,查看工程下的map文件就可以看到了,这个地址跟keil里面设置的target->flash起始地址息息相关,实际上我们不太需要关心,让编译器分配,中断向量表放的就是他们的地址。
        2:对比ARM7/ARM9内核,Cortex-M3内核则是固定了中断向量表的位置而起始地址是可变化的。
        3:进到C语言后会先配置NVIC,NVIC_SetVectorTable()里面可以配置中断向量表的起始地址和偏移,主要是告诉CPU该向量表是位于Flash还是Ram,偏移是多少。例如设置为位于Flash内,偏移就是烧入的程序地址,可在Keil target中设置。这样CPU就知道入口地址了。
        4:发生中断后,CPU找到中断向量表地址,然后根据偏移(对号入座)再找到中断地址,这样就跳过去了。

        我们截一个图说明一下,map文件:

        对应的bin文件,看是不是放的上面地址:

        显然,200039c0就是栈顶地址,而08006F21就是reset_handler地址!
        如何定位?以放到0x20000000为例
        1:keil设置ram起始为0x20000100,我们在0x20000000~0x20000100放中断向量表,其他给程序用
        2:设置NVIC_SetVectorTable(NVIC_VectTab_FLASH,0);
        3:跳到C时把中断向量表拷贝到0x20000000

3 个人思考

3.1 在拿到ST公司官方的IAP 程序后 我们要思考几点:

        1.ST 官方IAP是什么针对什么芯片型号的?我们要用的又是什么芯片型号?
        2.我们要用官方IAP适合我们芯片的程序升级使用,要在原有的基础上做那些改变?

        初略看了一下IAP源码后,现在我们可以回答一下上面的2个问题了:
        1.官网刚下载的IAP针对的是stm32f103c8芯片的,所以他的启动代码文件选择的是 startup_stm32f10x_md.s,而我的芯片是stm32f100cb,所以我的启动代码文件选择的是 startup_stm32f10x_md_lv.s 

         2 .第二个问题就是今天我们要做详细分析才能回答的问题了。

                 (1).知道了IAP官方源码的芯片和我们要用芯片的差异,首先我们要在源码的基础上做芯片级的改动;
                        A.首先改变编译器keil的芯片型号上我们要改成我们的芯片类型---STM32F100CB;
                        B.在keil的options for  targer 选项C/C++/PREPROMCESSOR symbols的Define栏里定义,把有关STM32F10X_MD的宏定义改成:STM32F10X_MD_VL
        也可以在STM32F10X.H里用宏定义:

/* Uncomment the line below according to the target STM32 device used in your application */
#if !defined (STM32F10X_LD) && !defined (STM32F10X_LD_VL) && !defined (STM32F10X_MD) && !defined (STM32F10X_MD_VL) && !defined (STM32F10X_HD) && !defined (STM32F10X_HD_VL) && !defined (STM32F10X_XL) && !defined (STM32F10X_CL) 
  /* #define STM32F10X_LD */     /*!< STM32F10X_LD: STM32 Low density devices */
  /* #define STM32F10X_LD_VL */  /*!< STM32F10X_LD_VL: STM32 Low density Value Line devices */  
  /* #define STM32F10X_MD  */    /*!< STM32F10X_MD: STM32 Medium density devices */
   #define STM32F10X_MD_VL       /*!< STM32F10X_MD_VL: STM32 Medium density Value Line devices */  
  /* #define STM32F10X_HD */     /*!< STM32F10X_HD: STM32 High density devices */
  /* #define STM32F10X_HD_VL */  /*!< STM32F10X_HD_VL: STM32 High density value line devices */  
  /* #define STM32F10X_XL */     /*!< STM32F10X_XL: STM32 XL-density devices */
  /* #define STM32F10X_CL */     /*!< STM32F10X_CL: STM32 Connectivity line devices */
#endif

        上面代码说的是如果没有定义 STM32F10X_MD_VL, 则宏定义 STM32F10X_MD_VL
                C.外部时钟在stm32f10x.h  依据实际修改,原文是说如果没有宏定义外部时钟HES_VALUE的值,但是宏定义了stm32f10x_cl 则外部时钟设置为25MHZ, 否则外部时钟都设置为8MHZ;  我用的外部晶振是8MHZ的所以不必修改这部分代码;

#if !defined  HSE_VALUE
#ifdef STM32F10X_CL   
#define HSE_VALUE    ((uint32_t)25000000) /*!< Value of the External oscillator in Hz */
#else 
#define HSE_VALUE    ((uint32_t)8000000) /*!< Value of the External oscillator in Hz */
#endif /* STM32F10X_CL */
#endif /* HSE_VALUE */

                D.做系统主频时钟的更改

        system_stm32f10x.c的系统主频率,依实际情况修改 ;我用的芯片主频时钟是24MHZ;

#if defined (STM32F10X_LD_VL) || (defined STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
/* #define SYSCLK_FREQ_HSE    HSE_VALUE */
#define SYSCLK_FREQ_24MHz  24000000
#else
/* #define SYSCLK_FREQ_HSE    HSE_VALUE */
#define SYSCLK_FREQ_24MHz  24000000 
/* #define SYSCLK_FREQ_36MHz  36000000 */
/* #define SYSCLK_FREQ_48MHz  48000000 */
/* #define SYSCLK_FREQ_56MHz  56000000 */
/*#define SYSCLK_FREQ_72MHz  72000000*/ 
#endif

                E.下面是关键部分操作了,在说这部分操作前我们先来说一下内存映射:

        下图在stm32f103ZEXX芯片手册的38页,我们只截取关键部分

         从上图我们看出几个关键部分:
                1.内部flash 是从0x0800 0000开始 到0x0801 FFFF结束,0x0801FFFF-0x0800 0000= 0x20000 =128k,128k也就是flash的大小;
                2.SRAM的开始地址是0x2000 0000 ;

        我们要把我们的在线升级程序IAP放到FLASH里以0x0800 0000 开始的位置,应用程序放APP放到以0x08003000开始的位置,中断向量表也放在0x0800 3000开始的位置;如图

         所以我们需要先查看一下misc.h 文件中的中断项量表的初始位置宏定义 为NVIC_VectTab_Flash 0x0800000
        那么要就要设置编译器keil 中的  options  for target 的target选项中的 IROM1地址 为0x0800 0000 大小为 0x20000即128K;IRAM1地址为0x2000 0000  大小为0x2000;(提示:这一项IROM1 地址 即为当前程序下载到flash的地址的起始位置)
        下面我们来分析一下修改后的IAP代码:

/*******************************************************************************
  * @函数名称 main
  * @函数说明 主函数
  * @输入参数
  * @输出参数  
  * @返回参数
*******************************************************************************/
int main(void){    //Flash 解锁    FLASH_Unlock();
    KEY_Configuration() ;
    //配置串口1
    IAP_Init();    
    //PA15管脚是否为低电平
    if (GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_15)  == 0x00)    {       
       //执行IAP驱动程序更新Flash程序
        SerialPutString("\r\n======================================================================");      
        SerialPutString("\r\n=              (C) COPYRIGHT 2011 Lierda                             =");       
        SerialPutString("\r\n=                                                                    =");        
        SerialPutString("\r\n=     In-Application Programming Application  (Version 1.0.0)        =");        
        SerialPutString("\r\n=                                                                    =");        
        SerialPutString("\r\n=                                   By wuguoyan                      ="); 
        SerialPutString("\r\n=========================================================");   
        SerialPutString("\r\n\r\n");        Main_Menu ();    }    
//执行用户程序
    else    {        
//判断用户已经下载程序,因为正常情况下此地址是栈地址
        //如没有这一句话,即使没有下载程序也会进入而导致跑飞
        if (((*(__IO uint32_t*)ApplicationAddress) & 0x2FFE0000 ) == 0x20000000)        
{           
         SerialPutString("Execute user Program\r\n\n");
         //跳转至用户代码
         JumpAddress = *(__IO uint32_t*) (ApplicationAddress + 4);            
         Jump_To_Application = (pFunction) JumpAddress;
         //初始化用户程序指针的堆栈指针
         __set_MSP(*(__IO uint32_t*) ApplicationAddress);            
         Jump_To_Application();        }        
         else 
{        SerialPutString("no user Program\r\n\n");        }    }    
         while (1)    
            {    }}

        这里重点说一下几句经典且非常重要的代码:
        第一句:if (((*(__IO uint32_t*)ApplicationAddress) & 0x2FFE0000 ) == 0x20000000)   //判断栈定地址值是否在0x2000 0000 - 0x 2000 2000之间
        怎么理解呢?在程序里#define ApplicationAddress    0x8003000 ,*(__IO uint32_t*)ApplicationAddress)  即取0x8003000开始到0x8003003 的4个字节的值, 因为我们的应用程序APP中设置把 中断向量表 放置在0x08003000 开始的位置;而中断向量表里第一个放的就是栈顶地址的值。也就是说,这句话即通过判断栈顶地址值是否正确(是否在0x2000 0000 - 0x 2000 2000之间) 来判断是否应用程序已经下载了。

        第二句:    JumpAddress = *(__IO uint32_t*) (ApplicationAddress + 4);   [  common.c文件第18行定义了:  pFunction   Jump_To_Application;]
        ApplicationAddress + 4  即为0x0800 3004 ,里面放的是中断向量表的第二项“复位地址”  JumpAddress = *(__IO uint32_t*) (ApplicationAddress + 4); 之后此时JumpAddress
        第三句:    Jump_To_Application = (pFunction) JumpAddress;
        startup_stm32f10x_md_lv. 文件中别名  typedef  void (*pFunction)(void);     这个看上去有点奇怪;正常第一个整型变量   typedef  int  a;  就是给整型定义一个别名 a void (*pFunction)(void);   是声明一个函数指针,加上一个typedef 之后  pFunction只不过是类型 void (*)(void) 的一个别名;例如:pFunction   a1,a2,a3;
                void  fun(void)
                {
                    ......
                }
                a1 = fun;
        所以,Jump_To_Application = (pFunction) JumpAddress;  此时Jump_To_Application指向了复位函数所在的地址;
        第四 、五句: __set_MSP(*(__IO uint32_t*) ApplicationAddress);      \\设置主函数栈指针
                               Jump_To_Application();                         \\执行复位函数
        我们看一下启动文件startup_stm32f10x_md_vl。s 中的启动代码,更容易理解

 3.2 解析STM32的启动过程

        当前的嵌入式应用程序开发过程里,并且C语言成为了绝大部分场合的最佳选择。如此一来main函数似乎成为了理所当然的起点——因为C程序往往从main函数开始执行。但一个经常会被忽略的问题是:微控制器(单片机)上电后,是如何寻找到并执行main函数的呢?很显然微控制器无法从硬件上定位main函数的入口地址,因为使用C语言作为开发语言后,变量/函数的地址便由编译器在编译时自行分配,这样一来main函数的入口地址在微控制器的内部存储空间中不再是绝对不变的。相信读者都可以回答这个问题,答案也许大同小异,但肯定都有个关键词,叫“启动文件”,用英文单词来描述是“Bootloader”。
        无论性能高下,结构简繁,价格贵贱,每一种微控制器(处理器)都必须有启动文件,启动文件的作用便是负责执行微控制器从“复位”到“开始执行main函数”中间这段时间(称为启动过程)所必须进行的工作。最为常见的51,AVR或MSP430等微控制器当然也有对应启动文件,但开发环境往往自动完整地提供了这个启动文件,不需要开发人员再行干预启动过程,只需要从 main函数开始进行应用程序的设计即可。
话题转到STM32微控制器,无论是keil uvision4还是IAR EWARM开发环境,ST公司都提供了现成的直接可用的启动文件,程序开发人员可以直接引用启动文件后直接进行C应用程序的开发。这样能大大减小开发人员从其它微控制器平台跳转至STM32平台,也降低了适应STM32微控制器的难度(对于上一代ARM的当家花旦ARM9,启动文件往往是第一道难啃却又无法逾越的坎)。
        相对于ARM上一代的主流ARM7/ARM9内核架构,新一代Cortex内核架构的启动方式有了比较大的变化。ARM7/ARM9内核的控制器在复位后,CPU会从存储空间的绝对地址0x000000取出第一条指令执行复位中断服务程序的方式启动,即固定了复位后的起始地址为0x000000(PC = 0x000000)同时中断向量表的位置并不是固定的。而Cortex-M3内核则正好相反,有3种情况:
        1:通过boot引脚设置可以将中断向量表定位于SRAM区,即起始地址为0x2000000,同时复位后PC指针位于0x2000000处;
        2:通过boot引脚设置可以将中断向量表定位于FLASH区,即起始地址为0x8000000,同时复位后PC指针位于0x8000000处;
        3:通过boot引脚设置可以将中断向量表定位于内置Bootloader区,本文不对这种情况做论述;

        而Cortex-M3内核规定,起始地址必须存放堆顶指针,而第二个地址则必须存放复位中断入口向量地址,这样在Cortex-M3内核复位后,会自动从起始地址的下一个32位空间取出复位中断入口向量,跳转执行复位中断服务程序。对比ARM7/ARM9内核,Cortex-M3内核则是固定了中断向量表的位置而起始地址是可变化的。

        stm32的启动过程
        (1)没有IAP,只有APP时的正常启动流程:
                STM32的FLASH地址起始于0x08000000,程序文件就从此地址开始写入。此外STM32内部通过“中断向量表”来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动,而“中断向量表”的起始地址是0x08000004,当中断来临,STM32的内部硬件机制亦会自动将PC指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。

        根据上图分析启动和运行过程,
                ① STM32在复位后,先从0X08000004地址取出复位中断向量的地址,并跳转到复位中断服务程序,
                ② 在复位中断服务程序执行完之后,会跳转到的main函数(如使用KEIL MDK调试时一下载进程序,会发现需要运行几次下一步才会跳转到main函数的位置)
                ③ main函数一般都是超循环体(while(1)死循环),在main函数执行过程中,如果收到中断请求(发生重中断),此时STM32强制将PC指针指回中断向量表处
                ④ 根据中断源进入相应的中断服务程序,
                ⑤ 在执行完中断服务程序以后,程序再次返回main函数执行。

         (2)加入IAP后的启动流程

         根据上图分析加入IAP后的起动和运行过程
                ① STM32复位后,还是从0X08000004地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到IAP的main函数,如将IAP看作是一个APP的话,那么此部分和正常起动是一样的。(此步=执行复位中断服务程序+跳转main,即将正常运行的①和②合并了)。
                ② 在执行完IAP以后(固件升级或直接跳转),跳转至APP的复位向量表(APP的复位中断向量起始地址为0X08000004+N+M)。
                ③ 取出APP的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至APP的main函数(此步=执行复位中断服务程序+跳转main)
                ④ 同样main函数为一个超循环,并且注意到此时STM32的FLASH,在不同位置上,共有两个中断向量表。在main函数执行过程中,如果CPU得到一个中断请求,PC指针仍强制跳转到地址0X08000004中断向量表处,而不是APP程序的中断向量表。
                ⑤ 程序再根据我们设置的中断向量表偏移量,跳转到对应中断源的APP的中断服务程序中,
                ⑥ 在执行完中断服务程序后,程序返回main函数继续运行。

  • 1
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值