这几天看了一下M3芯片从Norflash启动流程,记录一下。
1. 上电的第一条指令
CPU刚上电,需要从某一地址(下面以0x0为例说明)读取指令,但是内存肯定不行(由于刚上电,内存是没有数据的)。对于norflash(有地址总线),可以直接将norflash map到地址0x0位置,这样就可以从Norflash直接启动。相比于内存的读速度,norflash 还是太慢了,并且无法直接写。 通常情况,比较靠前的几条指令就会将flash中的代码段和数据段搬到memory中,然后跳转到memory中去执行。
2. 以boot loader为例
bootloader 是一段引导程序,上电开始,先将boot loader加载到0x0,bootloader首先将自己搬运到memory,然后将真正的应用(且称为APP吧)搬到memory,搬运完成之后,再跳转到APP位置去执行。所以必须要求bootloader 在memory中的空间不能和APP有overlap,否则在搬运过程自己把自己给写乱了。通常情况相对于APP来说,bootloader代码都比较少,并且在APP启动之后,bootloader就没有必要存在于内存中(可以被APP给擦写),于是可以设计如下内存模型, 以512K 内存为例,高64K 运行bootloader,低448k用于运行APP。
由于运行APP时候,不再需要bootloader,可以将APP的栈位置和bootloader的栈位置都设为0x80000(栈是从内存顶部往下分配的)
问题1. 如何将bootloader 的起始位置设置为0x70000,APP的起始位设置位0?
在bootloader链接脚本中指定:
在链接脚本中指定,在连接时候,将起始地址设置位0x70000
MEMORY
{
FLASH (rx) : ORIGIN = 0x10000000, LENGTH = 8M
RAM (rwx) : ORIGIN = 0x00070000, LENGTH = 64K
}
ENTRY(Reset_Handler)
SECTIONS
{
.text :
{
KEEP(*(.vectors))
__Vectors_End = .;
__Vectors_Size = __Vectors_End - __Vectors;
__end__ = .;
......
}
......
}
告诉链接器,生成的可执行文件基地址是0x70000,内存长度是64K, 第一条执行指令位置在:Reset_Handler。.text起始地址没有再特别指定,默认是从RAM ORIGIN :0x70000开始,其实就是告诉链接器代码段起始地址是0x70000.
而APP的链接脚本:
MEMORY
{
FLASH (rx) : ORIGIN = 0x10000000, LENGTH = 8M
RAM (rwx) : ORIGIN = 0x00000000, LENGTH = 512K
}
ENTRY(Reset_Handler)
SECTIONS
{
.text :
{
KEEP(*(.vectors))
__Vectors_End = .;
__Vectors_Size = __Vectors_End - __Vectors;
__end__ = .;
......
}
......
}
告诉链接器,生成的可执行文件基地址是0x0,内存长度是512K, 第一条执行指令位置在:Reset_Handler(可以和bootloader用同一个函数,但是执行逻辑肯定不一样,可以用宏隔开,也可以是不同的启动汇编)。.告诉链接器代码段起始地址是0x0.
问题2. 刚刚不是说系统一启动,就从0x0位置开始执行,那现在bootloader的代码还是flash中,怎么办?
使用map方法,刚上电,将整个norflash地址空间(0~8M)map到0~0x800000,将RAM地址空间(0~512K) map到0x10000000 ~ 0x100080000,等bootloader起来之后,把自己搬运到0x10000000,然后再重新map(通过设置指定地址的值,而改变总线地址空间),重新map之后,RAM地址空间(0~512K) 被map到0~0x80000, Flash地址被map到0x10000000~0x10800000。
问题3. 之前bootloader的链接器将base地址设置位了0x70000,那上电之后,从0地址开始,找入口函数Reset_Handler,这个地址被map到0x75b80(相对于起始地址,偏移0x5b80),而实际上当PC跳到0x75b80 是个未知的地方,可能都不是代码段了. 那要如何处理?
可以在生成bootloader之后,将这个值手动减小0x70000. 这样CPU 去找Reset_Handler,就会找到0x5b80,就能正确执行指令。
问题4. 如何精确找到Reset_Handler位置并替换?
.section .vectors
.align 2
.globl __Vectors
__Vectors:
.long __StackTop /* Top of Stack */
.long Reset_Handler /* Reset Handler */
.long NMI_Handler /* NMI Handler */
.long HardFault_Handler /* Hard Fault Handler */
.long MemManage_Handler /* MPU Fault Handler */
.long BusFault_Handler /* Bus Fault Handler */
.long UsageFault_Handler /* Usage Fault Handler */
......
这个就是我们的中断向量表,结合前面的链接脚本就知道,_Vector的代码段地址就是0x70000 对应flash 的0x0地址,所以0x4地址,就是存放Reset_Handler地址。只需要将0x4位置的内容减去0x70000就可以了
build出来bootloader之后,执行这条语句,就可以将前面的80 5b 07 00 变成 80 5b 00 00
echo -e -n "\x00" | dd of=bootloader_fw__.bin bs=1 count=1 conv=notrunc seek=6
问题5. 为什么只需要替换Reset_Handler?
在Reset_Handler中不要跳转到其他函数,并且在Reset_Handler中将代码段和数据段(我们在bootloader代码不用alloc,避免堆的使用,而bss段根本就不需要拷贝)搬运到0x70000位置就可以了.
问题6.如何copy 代码?
先从Reset_Handler开始:
Reset_Handler:
ldr r0, =0x36303131 //随便写一个数,判断是否是第一次启动
cmp r0, r8 //R8是默认值,第一次启动,肯定和0x36303131不相等
beq .REMPAP_DONE //如果相等,说明第二次map 已经做完了,可以直接跳到REMAP_DONE
/* 将bootloader的代码段 copy到内存中,
将falsh的0x0(代码段起始位置)到etext,拷贝到内存中,
从0x0开始,前面我们有说过,第一次map,
内存会map为 0x10000000~ 0x10080000*/
ldr r0, =0
ldr r1, =__Vectors //_Vectors = 0x70000
ldr r2, =__etext
sub r2, r1 //计算代码段长度
ldr r1, =0x10000000 //内存0x0即内存的起始位置
.L_cp_text_loop:
ldr r3, [r0]
str r3, [r1]
adds r0, #4
adds r1, #4
cmp r0, r2
bne .L_cp_text_loop
/* 将bootloader的代码段和数据段,copy到内存中,
将falsh的0x0(代码段起始位置)到__data_end__,
拷贝到内存中,从0x70000开始 */
ldr r0, =0
ldr r1, =__Vectors //_Vectors = 0x70000
ldr r2, =__data_end__
ldr r4, =0x10000000 //内存0x0即内存的起始位置
adds r1, r4 //r1 = 0x10070000
adds r2, r4
.L_cp_bootloader_loop:
ldr r3, [r0]
str r3, [r1]
adds r0, #4
adds r1, #4
cmp r1, r2
bne .L_cp_bootloader_loop
这段代码执行完的效果:
问题7. 为什么要做copy两次呢?
/* 将0x50b0003c 的值设为1,就完成了第二次map,具体要看硬件设计了*/
ldr r0, =0x50b0003c
ldr r1, =0x00000001
str r1, [r0]
ldr r1, [r0]
nop
nop
/* 还记得前面有将 75b80 改为 05b80吧,现在因为把代码搬到了内存的0x70000位置了,所以要把Reset_Handler的地址还原*/
ldr r0, =__Vectors /*__Vectors = 0x70000,
此时的_Vector已经是内存中的Vector了,
(但是还是0x0地址的_Vector),不再是flash中的Vector,
因为已经完成了Remap ,这也是回答前面为什么要有两次copy了 */
adds r0, #4 /*r0值为0x70004,前面知道0x7004是从flash的0x4 copy过来的,所以[r0]为0x5b80*/
ldr r1, [r0] //r1值为5b80
ldr r2, =__Vectors
adds r1, r2 //r1为75b80
str r1, [r0] //向0x70004地址写入75b80
/* 跳转到bootloader的高地址执行(0x70000) ,
注意remap之后,为了保证正常运行,必须要有一份
和remap之前完全一样的代码段,否则程序就会拿到随机的内容,
这就是为什么需要有前面的第一次拷贝动作(将代码段拷贝到内存地址为0x0位置)*/
ldr r0, =__Vectors
ldr r1, [r0]
msr msp, r1 /*程序需要从内存的0x0跳转到内存0x70000处,
两个原因:1. 跳转到0x70000之后,代码段,数据段才和链接器指定的地址匹配,
如果不跳,除了当前执行的Reset_Handler(下一次连Reset_Handler也无法执行了,
因为Reset_Handler地址也被还原了),
程序无法跳转;2. 0x0将来要用来存放APP的代码段和数据段*/
ldr r8, =0x36303131 //将R8设置为值,为了第二次直接进入.REMAP_DONE
ldr r1, [r0, #4] //r1为0x70004,即Reset_Handler
bx r1 //跳转到Reset_Handler执行
问题8. 第二次进入Reset_Handler,直接进入.REMAP_DONE,做了些什么呢?
.REMPAP_DONE:
movs r8, 0
#ifndef __NO_SYSTEM_INIT
bl SystemInit
#endif
#ifndef __START
#define __START _start
#endif
bl __START
首先将r8设为0,r8之前存放0x36303131 (为了不再做一次remap, 用一个不常用的寄存器保存着这个值).
进入SystemInit函数,然后进入_start 函数,可以看出_start就是_mainCRTStartup
进入_mainCRTStartup,这个函数不细看了,令我们欣喜的是,看到了我们熟悉的main函数,
后面就是C语言了,从Flash中将APP load进memory 0x0位置,load完成之后,跳转到APP的Reset_Handler执行.自此,整个启动流程就分析完了