赞
踩
MCU上电后默认取0x00000000地址的内容作为SP栈顶指针,取0x00000004地址内容作为上电复位第一个执行的函数地址,在startup_gd32a50x.s中就是Reset_Handler函数,
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit ;声明文件外定义的函数,MCU时钟配置
IMPORT __main ;这个是keil库自带的函数
LDR R0, =SystemInit ;将systemnit函数地址赋值给内部寄存器R0
BLX R0 ;跳转到该函数执行。BL表示执行完后返回LR链接地址
LDR R0, =__main
BX R0 ; BX表示不返回
ENDP
MCU默认上电是从0x00000000地址开始执行,我们MCU内部flash地址为0x08000000,可以通过将MCU的boot0和boot1两个引 脚置为不同的电平组合进行启动地址映射。
SystemInit函数配置MCU运行时钟及中断向量表重定位。
_main函数是keil自带的封装函数,我们看不到具体的实现代码,主要功能就是将RW段(初始化不为0的全局变量)从它的加载地址(下载flash地址)拷贝到对应的ram地址去,同时在RAM地址RW段之后将ZI段(定义未初始化的全局变量)大小地址清0,最后跳到main函数入口地址。
keil工程的配置如下:
一般在界面上配置rom和ram起始地址和大小,keil会根据配置生成一个分散加载脚本,起始就是链接脚本,编译器将各模块.c编译成目标.o文件,链接脚本就是决定它们在最终的image镜像文件中的放置顺序。
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************
LR_IROM1 0x08000000 0x00060000 { ; load region size_region
ER_IROM1 0x08000000 0x00060000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
RW_IRAM1 0x20000000 0x0000C000{ ; RW data
.ANY (+RW +ZI)
}
}
红色为map文件加载地址,绿色为它的运行地址,因为它属于RO段,不需要从flash 拷贝到ram去执行,故它的运行地址就是它的加载地址。
对于rw段,它的运行地址是ram,加载地址是flash,这部分工作就需要_main去完成。
我们可以修改启动文件startup_gd32a50x.s,不用keil自带的__main函数,然后自己实现RW段从flash到ram地址的拷贝,RAM中ZI段的清0,自定义一个最终的入口函数my_main
;/* reset Handler */ Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT RW_Copy_Load IMPORT ZI_Clear IMPORT my_main IMPORT |Image$$RW_IRAM1$$RW$$Base| ;keil自带,RW段ram起始地址 IMPORT |Load$$ER_IROM1$$RW$$Base| ;RW段在flash中的地址 IMPORT |Image$$RW_IRAM1$$RW$$Length|;RW段占的字节数 IMPORT |Image$$RW_IRAM1$$ZI$$Base| ;ZI段RAM起始地址 IMPORT |Image$$RW_IRAM1$$ZI$$Length|;ZI段字节数 IMPORT SystemInit ; IMPORT __main LDR R0, =SystemInit BLX R0 ; BL SystemInit LDR R0, =|Load$$ER_IROM1$$RW$$Base|;源地址 调C语言传参 LDR R1, =|Image$$RW_IRAM1$$RW$$Base|;目的地址 LDR R2, =|Image$$RW_IRAM1$$RW$$Length|;拷贝数据长度 BL RW_Copy_Load;跳到拷贝C函数执行 LDR R0, =|Image$$RW_IRAM1$$ZI$$Base|;ZI段RAM的起始地址 LDR R1, =|Image$$RW_IRAM1$$ZI$$Length|; ZI段RAM的数据长度 BL ZI_Clear;跳到ZI段清0函数执行 LDR R0, = my_main ;跳到我们自定义的my_main函数执行 BX R0 ENDP
//RW段拷贝C函数 void RW_Copy_Load(uint8_t* source,uint8_t* dest, uint16_t length) { uint16_t num; for(num = 0;num < length;num++){ *dest = *source; dest+=1; source+=1; } } //ZI段清0函数 void ZI_Clear(uint8_t* ZI_base,uint16_t length) { uint16_t num; for(num = 0;num < length;num++){ *ZI_base = 0; ZI_base+=1; } }
修改中断向量表将栈顶指针地址设为0x2000C000,通过编译map文件可知Reset_Handler第一个复位执行函数的地址为0x0800015d
进入debug仿真后,通过memory窗口读flash起始地址值如下:
可以看到flash起始地址0x08000000地址内容是栈顶指针0x2000C000,0x08000004地址内容是0x0800015D,正是Reset_Handler函数的地址,也就是上电后PC指针读取第一个要执行的函数地址。
内核寄存器值如下:
通过寄存器窗口也可以看到SP指针为0x2000C000,PC指针为0x0800015C,这不是0x0800015D的原因是arm架构MCU是采用Thumb32和Thumb16位指令集混用,0x0800015D是16bit指令,0x0800015C是32位指令,区别就是最低bit位为1是16bit,为0是32bit。
Debug仿真在my_main函数设置断点,全速运行,可以看到程序跳转到my_main函数
我们代码中定义的全局变量uint8_t num[5]= {0,1,2,3,4};在debug模式下,寄存器窗口sram地址可以看到0x00~0x04被从flash加载地址拷贝到sram运行地址如下:
因为keil中编译器会将所有模块的库.c都会编译成.o,包括我们main函数中并没有用到的,如果在链接时把main中没有调用的模块.o也链接的,会导致最终的image镜像文件非常大,所以keil中自带的分散加载文件(就是链接脚本),它会在链接时把main中没用的模块.o从镜像文件去除,只保存调用的。
在linux驱动开发中我们要自己编写makefile来决定编译哪个文件及不编译哪个文件,包括各个目标文件所依赖的.c文件,同时也要自己实现.lds链接脚本指定image运行的入口enrty地址,包括text、RW、RO及ZI段的链接顺序及堆栈的初始化,代码从加载地址到运行地址的拷贝等等,就是keil软件隐藏的很多细节都要自己实现。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。