赞
踩
作为一个单片机相关的开发人员,大家应该都很清楚单片机是从main函数开始执行代码的,但是很显然微控制器无法从硬件上定位main函数的入口地址,因为使用C语言作为开发语言后,变量/函数的地址便由编译器在编译时自行分配,这样一来main函数的入口地址在微控制器的内部存储空间中不再是绝对不变的。那么单片机又是如何找到咱们所写的main函数的呢,大家应该在各种单片机开发过程中都会了解到一个启动文件的东西。没错,单片机正式通过启动文件来找到咱们的main函数的。接下来咱们就来看一下STM32单片机是如何启动的。
上述开机启动流程比较详细,内容较为全面,但部分步骤可以省略(红字可省略标出),因为对于某些初始化,我们可能会在main函数中重新配置。
其中提到了两个指针一个是SP指针一个是PC指针,这两个指针的作用如下:
PC:Program Counter,是通用寄存器,但是有特殊用途,用来指向当前运行指令的下一条指令。
SP:Stack Pointer,堆栈指针,也是通用寄存器,用于入栈和出栈操作。
对于ARM7而言,是三级流水线结构,一条指令为4个字节大小,一条指令包含三个过程:Fetch(取指)、Decode(译指)、Execute(执行)。
CPU运行地址 = 当前PC值 = 当前程序执行位置 + 8;
对于ARM7而言,PC为R15,SP为R13。
PC不是指向正在执行的指令,而是始终指向下一个取指的指令。对于ARM7三级流水先结构和指令的三个过程,所以PC = 当前程序执行位置 + 8。
单片机上电之后会默认从地址0x00000000取出栈指针的初始值,作为MSP的值,从地址0x00000004取出程序指针PC的初始值,对应第一条执行的程序。我以我们需要看一下STM32官方对内存地址的管理,STM32将4GB的地址空间进行了如下划分:
其中Block 0 的内容如下,我们从哪里开始执行代码取决于BOOT引脚:
BOOT引脚的电平和启动位置的具体关系如下,一般我们在KEIL软件中设置的下载地址是从0x0800 0000开始,也就是说咱们的代码一般是保存到Flash里的,所以一般也是从主闪存存储器开始执行的:
无论性能高下,结构简繁,价格贵贱,每一种微控制器(处理器)都必须有启动文件,启动文件的作用便是负责执行微控制器从“复位”到“开始执行main函数”中间这段时间(称为启动过程)所必须进行的工作。最为常见的51,AVR或MSP430等微控制器当然也有对应启动文件,但开发环境往往自动完整地提供了这个启动文件,不需要开发人员再行干预启动过程,只需要从main函数开始进行应用程序的设计即可。
启动文件是使用汇编指令写的,所以在看启动文件之前大家需要先了解一下汇编指令,常用的汇编指令如下:
有了以上汇编指令的基础,那么我们去读启动文件就会方便很多:
1,Stack栈
栈的作用是用于局部变量,函数调用,函数形参等的开销,栈的大小不能超过内部SRAM 的大小。当程序较大时,需要修改栈的大小,不然可能会出现的HardFault的错误。
第35行:表示开辟栈的大小为 0X00002000,EQU是伪指令,相当于C 中的 define。
第37行:开辟一段可读可写数据空间,ARER 伪指令表示下面将开始定义一个代码段或者数据段。此处是定义数据段。ARER 后面的关键字表示这个段的属性。段名为STACK,可以任意命名;NOINIT 表示不初始化;READWRITE 表示可读可写,ALIGN=3,表示按照 8 字节对齐。
第38行:SPACE 用于分配大小等于 Stack_Size连续内存空间,单位为字节。
第39行: __initial_sp表示栈顶地址。栈是由高向低生长的。
2,Heap堆
堆主要用来动态内存的分配,像 malloc()函数申请的内存就在堆中。
开辟堆的大小为 0X00000200(512 字节),名字为 HEAP,NOINIT 即不初始化,可读可写,8字节对齐。__heap_base 表示对的起始地址,__heap_limit 表示堆的结束地址。
向量表是一个WORD( 32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。向量表在地址空间中的位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 (即 FLASH 地址 0)处必须包含一张向量表,用于初始时的异常分配。
值得注意的是这里有个另类: 0 号类型并不是什么入口地址,而是给出了复位后 MSP 的初值,后面会具体讲解。
第57行:定义一块代码段,段名字是RESET,READONLY 表示只读。
第58-60行:使用EXPORT将3个标识符申明为可被外部引用,声明 __Vectors、__Vectors_End 和__Vectors_Size 具有全局属性。
第62行:__Vectors 表示向量表起始地址,DCD 表示分配 1 个 4 字节的空间。每行 DCD 都会生成一个 4 字节的二进制代码,中断向 量表 存放的实际上是中断服务程序的入口地址。当异常(也即是中断事件)发生时,CPU 的中断系统会将相应的入口地址赋值给 PC 程序计数器,之后就开始执行中断服务程序。在60行之后,依次定义了中断服务程序的入口地址。
第140行:__Vectors_End 为向量表结束地址。
第141行:__Vectors_Size则是向量表的大小,向量表的大小是通过__Vectors 和__Vectors_End 相减得到的。
复位程序是系统上电后执行的第一个程序,复位程序也是中断程序,只是这个程序比较特殊,因此单独提出来讲解。
第147行:定义了一个服务程序,PROC表示程序的开始。
第148行:使用EXPORT将Reset_Handler申明为可被外部引用,后面WEAK表示弱定义,如果外部文件定义了该标号则首先引用该标 号,如 果外部文件没有声明也不会出错。这里表示复位程序可以由用户在其他文件重新实现,这种写法在HAL库中是很常见的。
第149-150行:表示该标号来自外部文件,SystemInit()是一个库函数,在system_stm32f1xx.c中定义的,__main 是一个标准的 C 库函数,主要作用是初始化用户堆栈,这个是由编译器完成的,该函数最终会调用我们自己写的main函数,从而进入C世界中。
第151行:这是一条汇编指令,表示从存储器中加载SystemInit到一个寄存器R0的地址中。
第152行:汇编指令,表示跳转到寄存器R0的地址,并根据寄存器的 LSE 确定处理器的状态,还要把跳转前的下条指令地址保存到 LR。
第153行:和149行是一个意思,表示从存储器中加载__main到一个寄存器R0的地址中。
第154行:和150稍微不同,这里跳转到至指定寄存器的地址后,不会返回。
第155行:和PROC是对应的,表示程序的结束。
我们平时要使用哪个中断,就需要编写相应的中断服务程序,只是启动文件把这些函数留出来了,但是内容都是空的,真正的中断复服务程序需要我们在外部的 C 文件里面重新实现,这里只是提前占了一个位置罢了。
这部分没啥好说的,和服务程序类似的,只需要注意‘B .’语句,B表示跳转,这里跳转到一个‘.’,即表示无限循环,如果在调试代码的过程中发现代码卡死到这个B的位置上,那就说名你的代码开启了某个中断,但是你没有对中断进行处理,就会出现这中情况。
堆栈初始化是由一个IF条件来实现的,MICROLIB的定义与否决定了堆栈的初始化方式。这个定义是在Options->Target中设置的。
如果没有定义__MICROLIB , 则会使用双段存储器模式,且声明了__user_initial_stackheap 具有全局属性,这需要开发者自己来初始化堆栈。
1,上电之后先去0x0000 0000地址加载SP指针从地址0x0000 0004加载PC指针,如果是BOOT0引脚为低电平,则指向Flash处执行程序
2,从Flash处也就是0x0800 0000加载栈顶指针SP,从0x0800 0004处加载中断向量表的起始地址PC,也就是复位程序的地址
3,执行复位程序,先执行SystemInit()函数初始化系统时钟,然后执行main()函数
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。