[分享] 简析stm32启动过程【转载&重写】
2078 查看
5 回复
 楼主 | 发布于 2018-05-04 | 只看楼主
分享到:
说明:本文是杰杰以前保存下来的,出处已经不知道在哪了,应该是各大论坛中,当然转这种文章,到处都有。今天我就把它重写一遍。基于原作者的内容添加一些内容(源码)讲解。杰杰水平有限,出错在所难免,还望各位大神指点一二。
startup_stm32f10x_cl.s
互联型的STM32F105xx,STM32F107xx
startup_stm32f10x_hd.s 大容量的STM32F101xx,STM32F102xx,STM32F103xx
startup_stm32f10x_hd_vl.s 大容量的STM32F100xx
startup_stm32f10x_ld.s 小容量的STM32F101xx,STM32F102xx,STM32F103xx
startup_stm32f10x_ld_vl.s 小容量的STM32F100xx
startup_stm32f10x_md.s 中容量的STM32F101xx,STM32F102xx,STM32F103xx
startup_stm32f10x_md_vl.s 中容量的STM32F100xx
startup_stm32f10x_xl.s 超大容量FLASH在512K到1024K字节的STM32F101xx,STM32F102xx,STM32F103xx
选择启动文件
建立中断服务入口地址,即把中断向量与中断服务函数链接起来。
我们知道在串口NVIC配置中我们只定义了个    NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
也就是中断服务向量,再然后我们在stm32f10x_it.c文件的void USART2_IRQHandler(void){} 函数里添加串口的服务程序。
但是mcu怎么知道中断向量USART2_IRQn对应的是USART2_IRQHandler(){}呢,这个就是启动文件所起的作用。
启动文件定义了Stack_Size  &  Heap_Size
__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
  • Stack_Size      EQU     0x00000400
  •                 AREA    STACK, NOINIT, READWRITE, ALIGN=3
  • Stack_Mem       SPACE   Stack_Size
  • __initial_sp
  • ; <h> Heap Configuration
  • ;   <o>  Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
  • ; </h>
  • Heap_Size       EQU     0x00000200
  •                 AREA    HEAP, NOINIT, READWRITE, ALIGN=3
  • __heap_base
  • Heap_Mem        SPACE   Heap_Size
  • __heap_limit
当前的嵌入式应用程序开发过程里,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的启动过程

下面就从ST的启动文件说起,由于库中的startup_stm32f10x_hd.s与编译环境有关,所以针对的是库中的

STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\TrueSTUDIO路径下的文件进行分析。
startup_stm32f10x_hd.s:    第147到155行
  • Reset_Handler   PROC
  •                 EXPORT  Reset_Handler             [WEAK]
  •                 IMPORT  __main
  •                 IMPORT  SystemInit
  •                 LDR     R0, =SystemInit
  •                 BLX     R0
  •                 LDR     R0, =__main
  •                 BX      R0
  •                 ENDP
这个文件里面首先定义了复位中断(复位入口矢量被硬件固定在地址0x0000_0004)的处理函数:Reset_Handler,它的作用就是将保存于flash中的初始化数据复制到sram中,调用上面说到的SystemInit来初始化时钟,接着跳转到main执行。
在进入main函数之前,我们要进行系统初始化:SystemInit
system_stm32f10x.c:
SystemInit():在"startup_stm32f10x_xx.s"文件中被调用,功能是设置系统时钟(包括时钟源,PLL系数,AHB/APBx的预分频系数还有 flash的设定),这个函数会在系统复位之后首先被执行。文件中默认的一些设置:允许HSE(外部时钟),FLASH(允许预取缓冲区,设置2个等待周 期),PLL系数为9,开启PLL并选择PLL输出作为时钟源(SYSCLK),后续设置SYSCLK = HCLK = APB2 = 72MHz,APB1 = HCLK/2 = 36MHz,HCLK即AHB的时钟。

SystemCoreClockUpdate():在系统时钟HCLK变化的时候调用,以更新一个全局变量SystemCoreClock,这个变量可以为应用程序使用,必须保证正确。默认不调用这个函数,因为SystemCoreClock默认被设置为设定的频率了(72MHz)

另外,下面这种设置寄存器的方法值得借鉴,先用位名清除相应的位,再进行设置,例如:

#define  RCC_CFGR_PLLSRC                     ((uint32_t)0x00010000)        /*!< PLL entry clock source */

#define  RCC_CFGR_PLLXTPRE                   ((uint32_t)0x00020000)        /*!< HSE divider for PLL entry */

#define  RCC_CFGR_PLLMULL                    ((uint32_t)0x003C0000)        /*!< PLLMUL[3:0] bits (PLL multiplication factor) */

#define  RCC_CFGR_PLLSRC_HSE                ((uint32_t)0x00010000)        /*!< HSE clock selected as PLL entry clock source */

#define  RCC_CFGR_PLLMULL9                  ((uint32_t)0x001C0000)        /*!< PLL input clock*9 */

/*  PLL configuration: PLLCLK = HSE * 9 = 72 MHz */

RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE | RCC_CFGR_PLLMULL));

RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9);

接着定义了Default_Handler 很多中断服务函数
一些是系统的,
还有一些我们用到的
  • __Vectors       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
  •                 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
  •                 DCD     SysTick_Handler            ; SysTick Handler
  •                 ; External Interrupts
  •                 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
  •                /***    很多    不一一列举   ***/
这个是作为其他所有中断的默认处理函数,作用就是死循环,所以你假如开启了某个中断,请按照这里面的中断函数名给它写中断处理函数,例如串口中断处理函数名是 USART1_IRQHandler,你开了串口中断,如果不重写USART1_IRQHandler,就默认执行Default_Handler,死循环了。而如果你有重写,那么中断向量表中的处理函数的地址就会更新为你自己写的那个函数的地址了。为什么会这样呢?因为此文件的末尾用了类似这样的语句:









weak    USART1_IRQHandler

.thumb_set USART1_IRQHandler,Default_Handler

它给中断处理函数提供了弱(weak)别名(Default_Handler),如果不重写,中断了默认执行Default_Handler,如果重写了,因为是弱别名,所以会被你写的同名函数覆盖。WEAK声明其他的同名标号优先于该标号被引用,就是说如果外面声明了的话,;会调用外面的

最后定义了一个中断向量表的段(.section    .isr_vector,"a",%progbits),这个表将会放置在0x0000 0000那里,第二个字(0x0000 0004)是复位向量,复位之后从这地址所指的函数执行。

那么,如何保证.isr_vector这个段将放在flash的最开始(0x08000000)呢?这就需要链接脚本了,即我们用的那个stm32_flash.ld文件了,查看一下就知道了,里面先定义了堆栈的地址:

_estack

/* Highest address of the user mode stack */

_estack = 0x20005000;    /* end of 20K 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 = 0x100; /* required amount of stack */

接着声明了各个内存的区域(定义了几个代表某个线性空间的名字,如下面的FLASH):

/* Specify the memory areas */

MEMORY

{

FLASH (rx)      : ORIGIN = 0x08000000, LENGTH = 128K

RAM (xrw)       : ORIGIN = 0x20000000, LENGTH = 20K

MEMORY_B1 (rx)  : ORIGIN = 0x60000000, LENGTH = 0K }

接着下面再介绍上面这三个名字里面都放了什么东西,首先是FLASH的:

/* Define output sections */

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) */

*(.rodata)         /* .rodata sections (constants, strings, etc.) */

*(.rodata*)        /* .rodata* sections (constants, strings, etc.) */

*(.glue_7)         /* glue arm to thumb code */

*(.glue_7t)        /* glue thumb to arm code */

KEEP (*(.init))

KEEP (*(.fini))

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

} >FLASH

在 startup_stm32f10x_hd.s:  中
  •               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]
一样的道理









可以看到startup_stm32f10x_hd.s:  中定义的这个段_vector确实放在了最开头的位置。

但是我们前面说复位向量在0x0000 0004,现在其地址是在flash中,所以地址是0x0800 0004,这个怎么回事呢?原来,stm32可以通过boot0、boot1引脚的配置将flash映射到0x0000 0000处。具体可参考stm32的用户手册2.4节。

从主闪存存储器启动(BOOT0 = 0,BOOT1 = X):主闪存存储器被映射到启动空间(0x0000 0000),但仍然能够在它原有的地址(0x0800 0000)访问它,即闪存存储器的内容可以在两个地址区域访问,0x0000 0000或0x0800 0000。

至此,整个STM32的启动过程以及程序结构都可以比较清晰了。

欢迎关注“创客飞梦空间”
干货不间断
在未来的日子里
创客飞梦空间与你们同在,放飞我们的梦想
(0 ) (1 )
回复 举报

回复于 2018-05-04 沙发

(0 )
评论 (0) 举报

回复于 2018-05-04 2#

厉害,感谢分享;
(0 )
评论 (0) 举报

回复于 2018-05-05 3#

厉害,支持原创。
(0 )
评论 (0) 举报

回复于 2018-05-07 4#

这个很详细,楼主辛苦
(0 )
评论 (0) 举报

回复于 2018-05-08 5#

讲的很好,受教了!
(0 )
评论 (0) 举报
  • 发表回复
    0/3000





    举报

    请选择举报类别

    • 广告垃圾
    • 违规内容
    • 恶意灌水
    • 重复发帖

    全部板块

    返回顶部