SDK中有很多内容,前面没有讲,一笔带过了,我们来逐个看看它们的作用。
以下部分内容整理自《野火STM32库开发指南》。
程序的启动,加载,存储像一团迷雾,挡在了我们和底层配置之间。不了解起始也不妨碍我们使用固件库编程,但是这部分内容太精彩了,越往下看,坑越多,这里推荐一本好书俞甲子老师的《程序员的自我修养--链接、装载与库》,揭开了C程序底层的奥秘,尤其是关于链接的部分,会带到一个新的高度。
同时王利涛老师的《嵌入式C语言自我修养》也是按照这部分讲的。还是建议俞甲子的那本。
先来启动文件startup_air32f10x.s
启动文件是必不可少的,这个文件用汇编编写,规定了程序上电后,MCU执行的第一个操作。
第一个操作是啥呢?
OK,我们需要看下《CM3权威指南》:
3.8 复位序列
在离开复位状态后,CM3做的第一件就是读取下列32个整数的值:
- 从地址 0x0000 0000处取出MSP的初始值 - 即这个地址上放的数据是多少,栈指针指向的地址;我们这里放的0x2000 0400。因为栈在SRAM里,SRAM的基地址是0x2000 0000, 栈大小是0x0400,启动文件里设置好的。栈又是满减栈,所以初始地址就是0x2000 0400。data段和bss段大小为0。
- 从地址 0x0000 0004处取出PC 的初始值(Program Counter程序计数器,其实就是当前程序的位置 )。这里很有讲究,PC的初始值一定是指向复位向量的(因为复位向量指向了1个子程序,帮助系统初始化时钟,堆栈,搬运代码等基本操作,后续我们才能使用我们自己写的main函数)
当我们将Air32F103设置为从内部FLASH启动后,FLASH存储器将被映射到启动空间(0x0000 0000)
从0x0800_0000处取出栈顶地址存放于MSP寄存器, 栈顶地址为0x0000400
从0x0800_0004处取出复位中断服务入口地址放入PC寄存器,复位中断函数Reset_Handler地址为0x800023D
在这之前先看看用到的汇编指令的含义:
指令 | 作用 |
EQU | 给数字常量起个符号名字,相当于define |
AREA | 汇编1个新的代码段或者数据段 |
SPACE | 分配内存空间 |
EXPORT | 全局声明,可被外部文件调用,类似extern |
DCD | 以字节为单位分配内存,要求4字节对齐,并初始化这些内存 |
PROC | 定义子程序,与ENDP成对使用 |
ENDP | 子程序结束 |
IMPORT | 声明标号来自外部 |
B | 跳转到标号 |
BL | 跳转到寄存器/标号给出的地址,并把跳转前下一条指令的地址存入LR(链接寄存器)中。 |
BLX | 跳转到寄存器给出的地址,并把跳转前下一条指令的地址存入LR(链接寄存器)中。 |
BX | 跳转到寄存器/标号给出的地址,不返回 |
END | 到达文件结尾,文件结束 |
WEAK | 编译器指令,弱定义 |
Cortex M3存储映射表
这个表比较泛泛,具体怎么存储的,还得看map文件分析
1.初始化栈,栈的作用是用于局部变量,函数调用,函数形参等
一般CM3内核优先使用R0,R1,R2,R3这4个内核通用寄存器,当参数超过4时才会使用栈。- 《CM3权威指南CnR2 - 宋岩》
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
这段代码定义了一个段,名字叫STACK,大小是0x0000 0400,1024 = 1KByte。注意:此时也没有给其划分地址,只是给这个段起了个名字,他的地址要在分散加载时才知道。
(后续我们看分散加载时就明白了,因为栈放在SRAM中,并且没有全局变量和局部变量,所以栈(内部SRAM)的起始地址是0x2000 0000,那么结束地址就是0x2000 0400)
正常来说 栈顶地址=SRAM起始地址+data段大小+bss段大小+栈大小。因为我们的例程中没有全局变量,所以data段和和bss段大小为0,栈顶地址就等于SRAM起始地址+栈大小
EQU:宏定义的伪指令,相当于等于,类似与C中的define。Stack_Siz=0x00000400
AREA:告诉汇编器汇编一个新的代码段或者数据段。STACK表示段名,这个可以任意命名;NOINIT表示不初始化; READWRITE表示可读可写,ALIGN=3,表示按照2^3对齐,即8字节对齐。
SPACE:用于分配一定大小的内存空间,单位为字节。这里指定大小等于Stack_Size,即分配1个大小为0x00000400的空间。
标号__initial_sp紧挨着SPACE语句放置,表示栈的结束地址,即栈顶地址,栈是由高向低生长的。地址是多少要看栈放在哪里,放在第几位。
__initial_sp 的地址是0x2000 0400,栈的空间是0x2000 0000 ~ 0x2000 03FF。
因为MSP主堆栈指针的地址=堆栈内存末地址+1
方法1:进入Keil 调试模式,复位后,查看SP和PC寄存器的值
方法2:把Listings文件夹下的1.map文件用keil打开(直接拖进去),就能看到了
2. 初始化堆
堆主要用来动态内存的分配,像malloc()函数申请的内存就在堆上面。这个在STM32里面用的比较少。
Heap_Size EQU 0x00001000
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
EQU:宏定义的伪指令,相当于等于,类似与C中的define。Heap_Size =0x00001000
堆的使用要结合malloc函数,是动态分配的,所以这里并没有划分出对应的空间。
3.向量表
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
定义一个段,名字叫RESET, 类型是DATA,只读。注意:此时也没有给其划分地址,只是给这个段起了个名字,他的地址要在分散加载时才知道。(只读数据即ROM,放在FLASH里,起始地址0x0800 0000)
AREA:告诉汇编器汇编一个新的数据段。RESET表示段名; 可读
EXPORT:声明3个可被外部使用的标号,使标号具有全局属性。如果是IAR编译器,则使用的是GLOBAL这个指令。
__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
忽略以下代码。。。。。
__Vectors 从向量表起始开始
DCD 为 __initial_sp(即栈顶地址)以及后续的符号分配4字节大小的地址。这块区域有多大,要看实际使用了多少。
DCD 0
DCD 0
;DCD 0X20005000
;DCD BOOT_RAM
__Vectors_End
__Vectors_End 标号,向量表结束
__Vectors_Size EQU __Vectors_End - __Vectors
向量表大小用 结束地址-起始地址即可。
AREA |.text|, CODE, READONLY
定义代码段(.text),类型是CODE,只读
BOOT_RAM PROC
EXPORT BOOT_RAM [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
子程序 BOOT RAM,
复位中断子程序
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
;unlock
LDR R0,=0x400210F0
MOV R1,#0x00000001
STR R1,[R0]
LDR R2,=0x40016C00
LDR R3,=0xa7d93a86
STR R3,[R2]
LDR R3,=0xab12dfcd
STR R3,[R2]
LDR R3,=0xcded3526
STR R3,[R2]
LDR R3,=0x200183FF
STR R3,[R2,#0x18]
LDR R4,=0x4002228c
LDR R5,=0xa5a5a5a5
STR R5,[R4]
;lock
LDR R2,=0x400210F0
LDR R3,=0x00000000
STR R3,[R2]
LDR R2,=0x40016C00
LDR R3,=0x5826c579
STR R3,[R2]
LDR R3,=0x54ed2032
STR R3,[R2]
LDR R3,=0x3212cad9
STR R3,[R2]
LDR R2,=0x4002228c
LDR R3,=0x5a5a5a5a
STR R3,[R2]
MOV R1,#0x00000000
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
EXPORT,定义一个全局标号 Reset_Handler , 弱定义
LDR R0,=0x400210F0
MOV R1,#0x00000001
STR R1,[R0]
4.分散加载
分散加载文件位于Objects文件夹下,文件以sct结尾,例如1.sct
分散加载的作用:将Code与Data放在指定的区域。
LR_IROM1 0x08000000 0x00040000 { ; Code链接区域 Code链接起始地址 Code链接区域大小
ER_IROM1 0x08000000 0x00040000 { ; Code = execution address
*.o (RESET, +First) //放置所有.o文件,但是RESET放在最前面
*(InRoot$$Sections)
.ANY (+RO) //最后放置只读数据,例如 const修饰的变量
}
RW_IRAM1 0x20000000 0x00018000 { ; RW data //可读写的Data段起始地址,以及大小
.ANY (+RW +ZI) //先放RW段,再放ZI段
}
}
其实在分散加载之前,所有的段都没有地址。分散加载给他们分配对应的地址。
这里有几个段名解释下:
RO段:只读数据段,一般放在ROM里,即FLASH里
RW段:可读可写的数据段,例如有初始值的全局变量
ZI段:可读可写的数据段,但没有初始化,或者初始化值为0。即BSS段,因为这部分变量没有初值或初值为0,所以编译后,不占用ImageSIze(bin文件大小)。可能是很久以前,资源宝贵吧,所以早期的程序员将变量分的很细,减少浪费
关于分散加载的详细内容请看: