当前位置:   article > 正文

ARM MCU启动流程分析_arm单片机启动流程

arm单片机启动流程

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

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)
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

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

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
//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;
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

修改中断向量表将栈顶指针地址设为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软件隐藏的很多细节都要自己实现。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/625379
推荐阅读
相关标签
  

闽ICP备14008679号