加载中…
个人资料
  • 博客等级:
  • 博客积分:
  • 博客访问:
  • 关注人气:
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

STM32单片机启动代码分析

(2023-02-04 14:28:49)
标签:

stm32

分类: 资料收藏
想要深入学习STM32单片机,就必须要去研究STM32单片机的启动代码,否则你就无法从整体框架上去了解它,所以STM32启动代码早晚都是要研究学习的,避不掉的坑。启动代码里主要是由汇编和伪指令构成的,下面我们从头到尾来理一遍这些神秘的代码究竟是什么含义。
 
在嵌入式系统中,启动文件是整个系统非常关键的部分,它会进行一些底层的初始化,构建程序运行必要的环境,比如堆栈初始化,变量初始化等。如果启动文件出现错误,则整个系统就跑不起来,因此研究启动文件非常必要。

  在keil中,启动文件由汇编代码编写,一般命名为startup_xxx.s,xxx为支持的某种芯片,比如可以是lpc15xx(NXP的LPC15xx系列)、MK60D10(飞思卡尔)、stm32f10x(意法半导体stm32f10x系列)等Cortext-M0/M3/M4内核芯片。它们的代码格式非常相近,根据启动文件代码由上到下的编写顺序.

可以将其分为以下5个典型部分:

  1.堆栈空间定义;

  2.存放中断向量表;

  3.复位中断函数(Reset_Handler);

  4.其它中断异常服务函数,以及弱[WEAK]声明;

  5.将堆栈地址传递给库函数,利用库函数初始化堆栈,和库函数自身初始化。

1.堆栈空间定义

  如下图所示,定义了栈大小Stack_Size = 0X200,即512字节;堆大小Heap_Size = 0X100,256字节。还定义了三个标号:__initial_sp(栈顶)、__heap_base(堆起始地址)和__heap_limit(堆终止地址),它们的空间由SPACE关键字来申请,并记作Stack_Mem和Heap_Mem。

2.存放中断向量表

 

  在启动代码中,会见到许多由DCD申请空间存放的一个个函数入口,即中断向量表,如下图所示,只列出了部分。

      关键字DCD代表申请一个字的空间,后面的函数名即为中断服务函数入口地址。另外中断向量表一般存放在Flash 0地址。

3. 复位中断函数(Reset_Handler)

 

  程序上电后,首先加载SP和PC,ARM规定从0地址处加载SP,从偏移为4的地址(0x00000004)处加载PC。然后将程序控制权交给程序。我们知道0地址处存放__initial_sp,0x00000004地址处存放Reset_Handler,加载PC后,程序跳转到Reset_Handler开始运行。

 

4.其它中断异常服务函数,以及弱[WEAK]声明

 

5.将堆栈地址传递给库函数

  第3步骤中,调用__main函数,然后__main调用库函数初始化堆栈,但库函数并不知道堆栈的大小,因此我们需要告诉它,具体做法就是传递参数或声明标号。

   下图为具体做法,可以看到第一行为:

IF      :DEF:__MICROLIB

 

  是条件编译选项

                            STM32单片机启动代码分析                                                                                                        图一 申请栈
图一这段代码开辟了一个大小是0x00001000的栈,大小可以根据实际情况去调整,栈主要用于保存函数内局域变量和内核寄存器,在调用函数时就会将一些数据保存在栈内,占用一定的栈空间,在调用函数结束返回到上一层函数时就会归还调用函数所占用的栈空间,所以栈的大小决定了你能申请的局域变量的大小,如果函数内定义了较多局域变量、大数组或多层函数嵌套的情况时建议将栈调大,否则运行时可能会由于栈溢出而导致的HardFault_Handler异常。__initial_sp就是这个栈指向地址的一个标号,后面会用到。
STM32单片机启动代码分析
                                                                      图二 申请堆

图二这段代码开辟了一个大小是0x00000000的堆,即没有分配堆大小,一般未涉及操作系统时,不需要使用堆。如果用到堆也可以直接申请全局变量效果等效于此,所以为了灵活使用,如果程序里用到动态内存分配,直接在使用全局变量定义一个数组效果一样。

需要强调的是ALIGN=3表示8字节对齐,所以自己用全局变量创建数组用作堆时也要保证8字节对齐,可以使用__align(8)来前缀修饰,8字节对齐是为了保证必须使用8字节对齐的函数能正常运行,例如像printf("%.3f",test)这样的函数输出浮点型变量值时,就要保证定义test是8字对齐的。编程时只要保证堆的起始地址是8字节对齐,编译器会自动保证后面在堆里申请的变量也是8字节对齐的。

STM32单片机启动代码分析

 

省略中间部分……,可自行参考启动代码

STM32单片机启动代码分析

 

                                                                     图三 中断向量表

图三这段代码是定义中断向量表,针对STM32单片机中断向量一般表默认是从单片机保存代码的开始位置即0x8000000地址(也可以从映射启动地址,这里不做拓展)。不要纠结这个先后顺序为什么要这样,这是芯片生产厂规定好的向量表第一位就是__initial_sp即栈指针放在0x8000000地址处占4个字节。第二个就是复位中断函数的入口地址放在0x8000004地址处占4个字节。说白了DCD就是申请一个占4字节的坑,每个坑放一个指针,__initial_sp用于放栈指针,其他坑分别放指向一个中断服务函数的入口指针。这样一旦触发中断,硬件上就会来到对应中断的坑,然后从坑里找到对应的中断服务函数的入口地址,跳转到对应中断服务函数运行。

STM32单片机启动代码分析

 

                                                            图四 复位中断服务函数

图四这段代码比较重要,这就是复位中断的服务函数,Reset_Handler标号正好对应上面说的中断向量表里的那个标号一致,编译器就会把这段汇编程序的入口地址编译放到中断向量表里复位中断那个坑里。一旦触发复位,硬件就会立马在中断向量表里从找到复位中断这个坑,从坑里放的指针地址就能跳转到这里来运行。可以看出单片机在复位后,并不是直接从main函数开始运行,而是最先进入复位中断。从复位中断里跳转到__main函数运行,注意__main函数不等于main函数。__main函数是由编译器提供的一个库函数,进入这个函数首先会初始化一些全局变量,堆栈里的数据,比如你定义一个全局变量int x=10;在函数里用到x这个变量时,程序怎么知道他初始值是多少,这个初始10就是在__main函数里进行赋值的,当一切准备工作都做好了__main函数会调用main函数,至此进入到你熟悉的世界里来了,多么美好!

STM32单片机启动代码分析                                                                       图五 堆栈初始化

图五为用户堆和栈的初始化

IF :DEF:__MICROLIB ;类似if语句,:DEF:X 就是说X定义了则为真,否则为假。

EXPORT __initial_sp ;栈顶地址。

EXPORT __heap_base ;堆起始地址。

EXPORT __heap_limit ;堆末端地址。

ELSE ;如果没定义__MICROLIB,则使用默认的C运行时库

如果勾选【Options for Target】->【Target】->【Use MicroLIB】,如下图所示。即使用微库,则__MICROLIB会被定义,编译器编译红线以上代码。用EXPORT声明 __initial_sp、__heap_base和__heap_limit。

STM32单片机启动代码分析
 如果不勾选【Use MicroLIB】,则缺省使用KEIL C库,上图红线以下会参与编译,KEIL C库函数会调用__user_initial_stackheap,通过R0~R3将堆栈以参数形式传递给KEIL C库。

IMPORT __use_two_region_memory ;通知编译器要使用的标号在其他源文件定义了__use_two_region_memory。

EXPORT __user_initial_stackheap ;声明全局标号__user_initial_stackheap,这样外程序也可调用此标号 ;则进行堆栈和堆的赋值,在__main函数执行过程中调用;如果使用默认的C库,程序启动过程中就不会执行该标号下的代码。

__user_initial_stackheap;表示用户堆栈初始化程序入口,在__main函数执行过程中调用。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


下面对其他一些关键词定义做一个大概介绍:

1.[WEAK]:此修饰符修饰的函数为若函数,意为可以其他源文件中重新定义一个同名函数,最终编译器编译的时候,会选择用户定义的函数,如果用户没有重新定义这个函数,那么编译器就会执行WEAK声明的函数。所以我们可以在别的地方定义一个相同名字的函数,而不必也尽量不要修改之前的函数,比如这里就可以看出来启动代码里已经有了USART1_IRQHandler函数,你在自己写的代码里再定义一个USART1_IRQHandler函数,编译也不会报错,而是自动使用你自己编写的函数。

这里的中断服务函数是弱声明的(由[WEAK]关键字标注)。所谓弱声明,即:如果用户定义了相同的函数则此处函数失效而使用用户定义的中断服务函数。这样是为了防止用户使能了中断而没有中断服务函数,从而造成程序崩溃。假设使能了中断,而用户又没有定义中断服务函数则会进入默认中断,如下图所示,默认中断为死循环(死循环与程序崩溃不是一个概念)。

2.IMPORT:表明要调用的函数、变量或其他标号为其他源文件定义,类似C里的函数声明。

3.EXPORT:表明该函数、变量或其他标号可以被其他源文件使用,类似于C中的extern功能。

 

4.AREA:伪指令,用于定义代码段或数据段,后跟属性标号。其中常用的有“ READONLY”表示该段为只读属性,联系到STM32的内部存储介质,可知具有该属性的段一般保存于FLASH区,而“READWRITE”表示该段为可读写属性,可知可读写段一般保存于SRAM区。

 



0

阅读 收藏 喜欢 打印举报/Report
后一篇:倒角和圆角
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有