ARM MCU启动流程分析

文章讲述了MCU上电后的初始化过程,涉及默认地址设置、SystemInit函数的作用、如何通过bootpins映射启动地址、以及如何自定义main函数以实现数据拷贝和ZI段清零。还讨论了keil工具链的分散加载和链接脚本的重要性。
摘要由CSDN通过智能技术生成

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两个引 脚置为不同的电平组合进行启动地址映射。
MCU启动方式选择说明
SystemInit函数配置MCU运行时钟及中断向量表重定位。
中断向量表说明
_main函数是keil自带的封装函数,我们看不到具体的实现代码,主要功能就是将RW段(初始化不为0的全局变量)从它的加载地址(下载flash地址)拷贝到对应的ram地址去,同时在RAM地址RW段之后将ZI段(定义未初始化的全局变量)大小地址清0,最后跳到main函数入口地址。
keil工程的配置如下:
kei工程配置
一般在界面上配置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段
红色为map文件加载地址,绿色为它的运行地址,因为它属于RO段,不需要从flash 拷贝到ram去执行,故它的运行地址就是它的加载地址。
map文件中RW段
对于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
中断向量表栈顶指针
map文件中Reset_Handler函数在flash中loader地址
进入debug仿真后,通过memory窗口读flash起始地址值如下:
debug  查看flash首地址中的值
可以看到flash起始地址0x08000000地址内容是栈顶指针0x2000C000,0x08000004地址内容是0x0800015D,正是Reset_Handler函数的地址,也就是上电后PC指针读取第一个要执行的函数地址。
内核寄存器值如下:
debug 内核寄存器值
通过寄存器窗口也可以看到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运行地址如下:
debug 查看RW段从loader地址到execute地址的拷贝
因为keil中编译器会将所有模块的库.c都会编译成.o,包括我们main函数中并没有用到的,如果在链接时把main中没有调用的模块.o也链接的,会导致最终的image镜像文件非常大,所以keil中自带的分散加载文件(就是链接脚本),它会在链接时把main中没用的模块.o从镜像文件去除,只保存调用的。
在linux驱动开发中我们要自己编写makefile来决定编译哪个文件及不编译哪个文件,包括各个目标文件所依赖的.c文件,同时也要自己实现.lds链接脚本指定image运行的入口enrty地址,包括text、RW、RO及ZI段的链接顺序及堆栈的初始化,代码从加载地址到运行地址的拷贝等等,就是keil软件隐藏的很多细节都要自己实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值