ARM Cortex-M4启动流程分析

打印 上一主题 下一主题

主题 539|帖子 539|积分 1617

概要

After power-on sequence or a system reset, the ARM® Cortex™-M4 processor fetches the top-of-stack value from address 0x0000 0000 and the base address of boot code from 0x0000 0004 in sequence. Then, it starts executing code from the base address of boot code.
Due to the selected boot source, either the main flash memory (original memory space beginning at 0x0800 0000) or the system memory (original memory space beginning at 0x1FFF F000) is aliased in the boot memory space which begins at the address 0x0000 0000.
When the on-chip SRAM, whose memory space is beginning at 0x2000 0000, is selected as the boot source, in the application initialization code, you have to relocate the vector table in SRAM using the NVIC exception table and offset register.
从以上摘取的文本可以看出,Cortex-M4内核在上电由硬件初始化后,将把0x0000 0000地址处的值设置为Stack Pointer (SP),然后跳转到0x0000 0004存储的地址,进而开始执行代码。(这一点和ARM9不一样,ARM9是直接执行0x0000 0000地址处的代码,因此第一条通常是跳转指令。)
以上启动步骤翻译成汇编代码如下:
  1. ldr r0, =0x0
  2. mov sp, r0  // 也可以是 msr msp, r0
  3. ldr r0, =0x4
  4. bx r0
复制代码
需要注意的两点:

  • 0x00000000地址处存放的SP值BIT[1:0]是无效的,将被自动4字节对齐(参考CortexM4_TRM_r0p1.pdf),Cortex-M4启动后,默认是特权模式下的Thread Mode且使用SP_main(MSP)。在裸机阶段,CONTROL的bit1始终是0,因此主循环(main)中断服务程序(xxx_Handler)都使用MSP,PSP不起作用。


  • Cortex-M4处理器指令是16位对齐的,因此PC寄存器的BIT[0]也是无效的,自动2字节对齐。

    但Cortex-M4仅支持thumb/thumb2指令集,使用跳转指令时,目标地址的BIT[0]需要置1,表明目标分支处于thumb程序区域,因此0x00000004地址处存放的跳转地址BIT[0]必须是1(在反汇编时候,需要忽略跳转指令目标地址的最低位)。
    ARM跳转指令格式:B{条件} 目标地址,结合不同的条件位,可以变成BX: 带状态切换的跳转,BLX: 带链接和状态切换的跳转。由于BX和BLX指令使用目标地址的BIT[0]推算出目标状态,可将处理器的状态从ARM更改为thumb,或从thumb更改为ARM,如果目标地址的BIT[0]为0,则处理器的状态会切换到ARM状态;如果目标地址的BIT[0]为1,则处理器的状态会切换到thumb状态。
  • 参考文档ARM-AAPCS-EABI-v2.08.pdf CortexM4_TRM_r0p1.pdf
由于各类MCU支持不同的启动方式,因此内核启动的0地址将会被映射到不同的其它地址,这种启动方式的切换通常使用一些GPIO进行配置。
以某款国产Cortex-M4 MCU为例,下面将列出不同启动方式下,SP和Reset Code的取值地址:

启动方式SP取值地址PC取值地址Flash0x080000000x08000004SRAM0x200000000x20000004BOOTROM0x1FFFF0000x1FFFF004不过由于这种由启动方式不同导致的地址映射关系改变是由芯片内部处理的,因此在最终生成的bin文件中,SP和PC依旧被放在相对文件的0地址和4字节地址偏移处;这种0地址的映射只是便于在上电时可以使用不同的启动介质,当内核通过某一介质启动后,完全可以切换到其它地址空间运行;例如通过GPIO配置为Flash启动一个stubloader,启动完成后将bootloader从某个其它的介质加载到SRAM,然后修改SP和PC地址,实现切换到SRAM运行。
示例分析

这里给出的用于示例分析的连接脚本和启动文件,只需这两个文件即可构建出用于启动的bin文件。
  1. /* 因为下面.isr_vector节中配置为KEEP(*(.isr_vector)),表明保持isr_vector节信息不变原样输出 */
  2. /* 这里指定程序入口点(ENTRY)对于裸机环境是无意义的(除非使用了标准库的启动代码,例如crt0),放在这里主要起辅助阅读作用 */
  3. ENTRY(Reset_Handler)
  4. /* c栈大小 */
  5. _system_stack_size = 0x200;
  6. /* Specify the memory areas */
  7. MEMORY
  8. {
  9.   FLASH (rx)      : ORIGIN = 0x08000000, LENGTH = 4K
  10.   RAM (xrw)       : ORIGIN = 0x20000000, LENGTH = 4K
  11. }
  12. /* Define output sections */
  13. SECTIONS
  14. {
  15.   /* 这里首先存放启动代码到Flash,KEEP(*(.isr_vector))表示保持isr_vector段的数据不变直接输出,isr_vector段信息将在startup.S启动文件中定义 */
  16.   .isr_vector :
  17.   {
  18.     . = ALIGN(4);
  19.     KEEP(*(.isr_vector)) /* Startup code */
  20.     . = ALIGN(4);
  21.   } >FLASH
  22.   /* 代码和一些常量与常量字符串 */
  23.   .text :
  24.   {
  25.     . = ALIGN(4);
  26.     *(.text)           /* .text sections (code) */
  27.     *(.text*)          /* .text* sections (code) */
  28.     *(.rodata)         /* .rodata sections (constants, strings, etc.) */
  29.     *(.rodata*)        /* .rodata* sections (constants, strings, etc.) */
  30.     *(.glue_7)         /* glue arm to thumb code */
  31.     *(.glue_7t)        /* glue thumb to arm code */
  32.     *(.gnu.linkonce.t*)
  33.    
  34.     KEEP (*(.init))
  35.     KEEP (*(.fini))
  36.     . = ALIGN(4);
  37.     _etext = .;        /* define a global symbols at end of code */
  38.   } >FLASH
  39.   /* used by the startup to initialize data */
  40.   _sidata = .;
  41.   /* Initialized data sections goes into RAM, load LMA copy after code */
  42.   /* 这里data段会放入Flash地址空间并在RAM地址空间预留相同空间,上电后由启动代码复制到到RAM。AT ( _sidata )另一种写法是在段结束处 " >RAM" 改成 " >RAM AT> FLASH"  */
  43.   /* 如果bin文件是加载到RAM运行的,data段只会在RAM地址空间有一份,启动代码就无需"copy data section" */
  44.   .data : AT ( _sidata )
  45.   {
  46.     . = ALIGN(4);
  47.     _sdata = .;        /* create a global symbol at data start */
  48.     *(.data)           /* .data sections */
  49.     *(.data*)          /* .data* sections */
  50.     *(.gnu.linkonce.d*)
  51.    
  52.     . = ALIGN(4);
  53.     _edata = .;        /* define a global symbol at data end */
  54.   } >RAM
  55.   
  56. /* 栈可以放在bss前面或后面,ARM-Thumb过程调用标准和ARM、Thumb C/C++编译器总是使用满递减栈,只需要保证向低地址方向有足够空间即可*/
  57.   .stack :
  58.   {
  59.     . = ALIGN(4);
  60.     _sstack = .;
  61.     . = . + _system_stack_size;
  62.     . = ALIGN(4);
  63.     _estack = .;
  64.   } >RAM
  65.   /* 未初始化或者初始化为0值的全局变量,只需要在RAM中留出空间,启动代码中清零即可 */
  66.   __bss_start = .;
  67.   .bss :
  68.   {
  69.     . = ALIGN(4);
  70.     /* This is used by the startup in order to initialize the .bss secion */
  71.     _sbss = .;
  72.    
  73.     *(.bss)
  74.     *(.bss.*)
  75.     *(COMMON)
  76.    
  77.     . = ALIGN(4);
  78.     /* This is used by the startup in order to initialize the .bss secion */
  79.     _ebss = . ;
  80.    
  81.     *(.bss.init)
  82.   } > RAM
  83.   __bss_end = .;
  84.   _end = .;
  85.   /* 后面省略了移除标准库中调试信息节的声明 */
  86. }
复制代码
通过以上连接脚本,可以得到最终生成的可执行文件布局和内存布局。STM32外设库提供的连接文件将栈(.stack)放在了RAM最高地址处,实际上只需要保证栈向下递减有足够的空间,栈的存放位置可以任意。

启动文件
复位后关闭中断和异常(内核中断),复制data section,清零bss section,最后后进入死循环。
  1.   .syntax unified
  2.   .cpu cortex-m4
  3.   .thumb
  4. .global  g_pfnVectors
  5. .global  Default_Handler
  6. /* start address for the initialization values of the .data section.
  7. defined in linker script */
  8. .word  _sidata
  9. /* start address for the .data section. defined in linker script */  
  10. .word  _sdata
  11. /* end address for the .data section. defined in linker script */
  12. .word  _edata
  13. /* start address for the .bss section. defined in linker script */
  14. .word  _sbss
  15. /* end address for the .bss section. defined in linker script */
  16. .word  _ebss
  17. /* stack top(Full descending). defined in linker script */
  18. // .word _estack
  19.   .section  .text.Reset_Handler
  20.   .weak  Reset_Handler
  21.   .type  Reset_Handler, %function
  22. Reset_Handler:
  23. /* set stack pointer */
  24. // no need for cortex-m4
  25. //  ldr   r0, =_estack
  26. //  mov   sp, r0
  27. /* Disable IRQ and FIQ*/
  28.   cpsid i
  29.   cpsid f
  30. /* Copy the data segment initializers from flash to SRAM */
  31.   movs  r1, #0
  32.   b  LoopCopyDataInit
  33. CopyDataInit:
  34.   ldr  r3, =_sidata
  35.   ldr  r3, [r3, r1]
  36.   str  r3, [r0, r1]
  37.   adds  r1, r1, #4
  38.    
  39. LoopCopyDataInit:
  40.   ldr  r0, =_sdata
  41.   ldr  r3, =_edata
  42.   adds  r2, r0, r1
  43.   cmp  r2, r3
  44.   bcc  CopyDataInit
  45.   ldr  r2, =_sbss
  46.   b  LoopFillZerobss
  47. /* Zero fill the bss segment. */  
  48. FillZerobss:
  49.   movs  r3, #0
  50.   str  r3, [r2], #4
  51.    
  52. LoopFillZerobss:
  53.   ldr  r3, = _ebss
  54.   cmp  r2, r3
  55.   bcc  FillZerobss
  56. MainLoop:
  57.   b  MainLoop
  58. .size  Reset_Handler, .-Reset_Handler
  59.     .section  .text.Default_Handler,"ax",%progbits
  60. Default_Handler:
  61. Infinite_Loop:
  62.   b  Infinite_Loop
  63.   .size  Default_Handler, .-Default_Handler
  64.    .section  .isr_vector,"a",%progbits
  65.   .type  g_pfnVectors, %object
  66.   .size  g_pfnVectors, .-g_pfnVectors
  67.    
  68. g_pfnVectors:
  69.     .word     _estack                           // Top of Stack
  70.     .word     Reset_Handler                     // Reset Handler
  71.     .word     NMI_Handler                       // NMI Handler
  72.     .word     HardFault_Handler                 // Hard Fault Handler
  73.     .word     MemManage_Handler                 // MPU Fault Handler
  74.     .word     BusFault_Handler                  // Bus Fault Handler
  75.     .word     UsageFault_Handler                // Usage Fault Handler
  76.     .word     0                                 // Reserved
  77.     .word     0                                 // Reserved
  78.     .word     0                                 // Reserved
  79.     .word     0                                 // Reserved
  80.     .word     SVC_Handler                       // SVCall Handler
  81.     .word     DebugMon_Handler                  // Debug Monitor Handler
  82.     .word     0                                 // Reserved
  83.     .word     PendSV_Handler                    // PendSV Handler
  84.     .word     SysTick_Handler                   // SysTick Handler
  85.     // external interrupts handler
  86.     .word     WWDGT_IRQHandler                  // 16:Window Watchdog Timer
  87.     .word     LVD_IRQHandler                    // 17:LVD through EXTI Line detect
  88.     .word     TAMPER_IRQHandler                 // 18:Tamper through EXTI Line detect
  89.     .word     RTC_IRQHandler                    // 19:RTC through EXTI Line
  90. /* 省略其他中断入口地址...*/
复制代码
编译和链接选项使用如下(使用-O0禁止编译器优化,避免其他知识的引入)
  1. -mcpu=cortex-m4 -march=armv7e-m -mthumb -mlittle-endian -mfloat-abi=soft -mno-unaligned-access -O0 -fmessage-length=0 -fsigned-char -ffunction-sections -fdata-sections -Wuninitialized -Wall -Wpointer-arith -Wshadow -Wlogical-op -Waggregate-return -Wfloat-equal
复制代码
  1. -mcpu=cortex-m4 -march=armv7e-m -mthumb -mlittle-endian -mfloat-abi=soft -nostartfiles -Xlinker --gc-sections
复制代码
编译完成输出
  1. Invoking: GNU Arm Cross Print Size
  2. arm-none-eabi-size --format=berkeley "app.elf"
  3.    text           data            bss            dec            hex        filename
  4.     372              0            512            884            374        app.elf
复制代码
从下图可以看到bin文件的0地址和4字节地址分别存放了MSP值和执行代码的地址,剩余内容是中断向量地址,按照内核中断在前外设中断在后,依据中断号顺序依次排列。本次使用的MCU共有14个内核中断和60个外设中断。Cortex-M使用的是中断向量模式,且在启动文件中,默认指向了Default_Handler因此可以看到大量相同的跳转地址。(ARM9和Cortex-A使用独立的中断控制器,外设中断可路由到FIQ或者IRQ内核中断,中断产生时,跳转到FIQ或者IRQ中断服务程序,访问中断控制器得到中断号,再查表得对应中断号(与外设中断类型对应)的中断服务程序,因此Cortex-M的中断响应上要比ARM9和Cortex-A要快速一些)


MSP = 0x20000200:
从上文的编译输出可以看到,data区占用0字节,因此只有c栈使用RAM空间,那么它的地址范围就是0x20000200~0x00000000;Cortex-M4使用的是满减栈模型。堆栈指针SP指向一个被压入堆栈的32位数值。在下一次压栈时,SP先自减4,再存入新的数值,因此虽然在连接脚本设置的栈大小是512字节,实际上只能用到508字节,栈顶最高4字节会空着。

在线调试看出SP寄存器值与bin文件一致:

PC = 0x08000130
bin文件的4字节偏移处存放跳转地址,由于是thumb指令,实际的PC值需要将最低字节清零0x08000131 & 0xFFFFFFFE = 0x08000130。


这里也可以将指令复制下来,填入一个在线ARM Code转汇编的网站:https://armconverter.com/
thumb指令只需要复制2字节就可以,可以看到反汇编结果与编写的代码一致。

总结

ARM的裸机启动过程还是比较简单的,设置好PC就可以运行在汇编环境,再设置SP值就可以有寄存器的入栈和出栈(c语言的函数调用时保存当前的寄存器值),只有这两步是必须在汇编环境下操作,后续的copy data 和 clear bss也可以使用c语言进行,例如newlib中的startup.c。


附录:

Arm® Cortex®-M4 Processor Technical Reference Manual Revision r0p1:
https://developer.arm.com/documentation/100166/0001/
Arm Cortex-M4 Processor Datasheet:
https://developer.arm.com/documentation/102832/latest/
ARM-software/abi-aa:
https://github.com/ARM-software/abi-aa/releases
在线ARM机器码转汇编或ARM汇编转ARM机器码:
https://armconverter.com/

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

石小疯

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表