ARM Cortex-M底层技术(2)—单片机的启动代码详解

提示:若转载请备注来源,谢谢

启动文件

1. 什么是启动代码

启动代码是系统上电或者复位后运行的第一段代码,是进入C 语言的main 函数之前需要执行的那段汇编代码。或者说用户程序运行之前对系统硬件及软件环境进行必要的初始化并在最后使程序跳转到用户程序
将启动文件理解为一种描述性的代码,不要拘泥于它的实现机制,我之前一直在想启动代码中汇编代码的执行顺序,后来没明白,估计和keil工具有关系,只需要记住一个道理,程序上电执行的第一条代码(我们可以控制的代码),就是启动文件中Reset Handler函数,Reset Handler函数 的地址存放在flash的0x00000004处。

2. 启动代码主要干了什么

对于 Cortex-M系列的芯片而言,启动代码大同小,明白一个几乎所有的也就都明白了。

  1. 设置堆栈
  2. 定义中断向量表
  3. 初始化系统时钟(Reset_Handler中做
  4. 初始化堆栈,有的启动代码不一定做Reset_Handler中做
  5. 执行_main,准备c语言的运行环境,初始化程序计数器指针PC指向main,进而来到c语言的世界

启动文件分析

一、设置堆栈

; <h> Stack Configuration
;   <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>

Stack_Size      EQU     0x00000800

                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size
__initial_sp


; <h> Heap Configuration
;   <o>  Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>

Heap_Size       EQU     0x00000100

                AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit


                PRESERVE8
                THUMB

以上仅仅定义了堆栈的大小,堆栈的起始地址在keil中设置方式有两种,
第一种:
在这里插入图片描述
第二种,分散加载文件(将keil的Use Memory Layout from Target Dialog去掉勾选就会有该文件),在实际项目中,我通常使用该文件,可以理解问链接文件
在这里插入图片描述
在上图(第一幅图片中),启动文件的最后几行,有关于Use MicoLIB的描述,还有关于堆栈的设置,代码为,主要是初始化堆栈的地址范围:

;*******************************************************************************
; User Stack and Heap initialization
;*******************************************************************************
                 IF      :DEF:__MICROLIB
                
                 EXPORT  __initial_sp
                 EXPORT  __heap_base
                 EXPORT  __heap_limit
                
                 ELSE
                
                 IMPORT  __use_two_region_memory
                 EXPORT  __user_initial_stackheap
                 
__user_initial_stackheap

                 LDR     R0, =  Heap_Mem
                 LDR     R1, =(Stack_Mem + Stack_Size)
                 LDR     R2, = (Heap_Mem +  Heap_Size)
                 LDR     R3, = Stack_Mem
                 BX      LR

                 ALIGN

                 ENDIF

二、定义中断向量表

1. 当内核响应了一个发生的异常后,对应的异常服务例程(ESR)就会执行。为了决定 ESR的入口地址, 内核使用了―向量表查表机制‖(是ARM的实现机制)。这里使用一张向量表。向量表其实是一个WORD(32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。
2. 向量表在地址空间中的位置默认存放在0x00000000处,但是还可以设置,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。

代码为:

; Vector Table Mapped to Address 0 at Reset

                AREA    RESET, DATA, READONLY
                EXPORT  __Vectors

__Vectors       DCD     __initial_sp              ; Top of Stack
                DCD     Reset_Handler             ; Reset Handler
                DCD     NMI_IRQHandler            ; NMI Handler
                DCD     HardFault_IRQHandler      ; Hard Fault Handler
                DCD     0                         ; Reserved
                DCD     0                         ; Reserved
				......
				......
                AREA    |.text|, CODE, READONLY

附加:常常在涉及到升级loader中使用

  1. 向量表重定位 + 跳转代码如下:
__asm void SoftReset(u32 address)
{
    
    
	LDR		R1, =0xE000ED08				// 0xE000ED08 重定位地址
	STR		R0,	[R1,	#0]					// R0为形参address
  /* 重定位完后,向量表地址为address */  
	LDR r1, [r0]        // address内前4个直接存放的 Top of Stack          
	msr MSP,r1  		// 给MSP赋值

	ADDS r0, r0, #4    	// Reset_Handler 的地址          	
	LDR r1, [r0]            
	NOP
	BX R1			// 执行 Reset_Handler
	NOP
	NOP
	NOP 
}
  1. 若仅仅想实现向量表重定位,例如将向量表定位到0x00000200,代码如下:
// SCB->VTOR就等于 0xE000ED08
//SCB->VTOR = 0x00000000; // 等价于0xE000ED08 = 0x00000000;
SCB->VTOR = 0x00000200;

三、初始化系统时钟

还是要在重申一下,复位子程序Reset_Handler是系统上电后第一个执行的程序。这里分析两个Reset handler,一个是STM32 demo上的,一个是我们使用的thk88芯片的
stm32代码为:

; Reset handler
Reset_Handler   PROC
                EXPORT  Reset_Handler             [WEAK]
                IMPORT  __main
                IMPORT  SystemInit
                LDR     R0, =SystemInit				// 系统初始化,里面包含了初始化时钟
                BLX     R0               
                LDR     R0, =__main		// 最后跳转到main()
                BX      R0
                ENDP

thk88代码为

Reset_Handler   PROC			

                EXPORT  Reset_Handler             [WEAK]
				IMPORT	main
				
				; 开启内部时钟30M
  				LDR		R3, =SCUBASEADDR
				LDR		R0, =0x00000000
				STR		R0, [R3,#SCUSCK]
				LDR		R0, =0x00000000
				STR		R0, [R3,#SCUINCKD]

				; 关闭USB PHY,禁止DP上拉电阻                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   
				LDR		R0, =0x00000100
				STR     R0, [R3, #SCUCM3]       ;OPEN USB CLK
				LDR		R2, =USBBASEADDR
				LDR		R0, =0x00000001
				STR		R0, [R2,#USBPCON]		; 关闭USB PHY
				LDR		R0, =0x00000000
				LDR 	R2, =USBPHYCON 
				STR		R0, [R2]  				;禁止DP上拉
				;STR		R0, [R2,#USBPHYCON] ;禁止DP上拉

				; 屏蔽 NMI 中断
				LDR 	R0, =0XFFFFFFFF
				LDR		R2,	=SCUNMI
				STR		R0, [R3,R2]	

				; DMA使用event方式
				LDR		R2, = 0x40000100	;SCUEVTSET1 = 0x02;DMA event mode
				LDR		R0, = 0x00000002
				STR		R0, [R2]
				
				;初始化用到的RAM区,4字节对齐,包括RW和ZI区域
				BL 		CLEAR_ZI
				
				IMPORT __main
				LDR R0,= __main
				BX R0
                ENDP

每款芯片的启动代码大同小异,熟悉一个其他的都可以看懂。

总结

具体启动流程如图,两种方式

  1. 启动流程1(使用标准库,不使用Microlib)如下图:
    在这里插入图片描述
  2. 启动流程2(使用Microlib)如下图:
    在这里插入图片描述
    假设STM32被设置为从内部FLASH启动(这也是最常见的一种情况),中断向量表起始地位为0x00000000(映射到0x8000000,两个是一个东西),则栈顶地址存放于0x8000000处,而复位中断服务入口地址存放于0x8000004处(复位地址在栈顶地址4字节后)。当STM32上电后,遇到复位信号,则从0x80000004处取出复位中断服务入口地址,继而执行复位中断服务程序,然后跳转__main函数,最后进入mian函数,来到C的世界。

重申:

  1. 绝大部分ARM-M协议的芯片,复位之后先进入厂商boot,此时所有的用户均无法接入处理器;厂商boot主要负责芯片最初级的初始化,对MCU进行一些差异性设置等,BOOT完成后,会把主动权交给用户,也就是启动代码;
  2. 启动代码(执行汇编语言不需要此启动代码),在启动文件中,会设置MSP(主堆栈指针)和PC(程序计数器)的值,MSP的地址默认是0x00000000,PC的地址默认是0x00000004,这两个地址可以通过CORTEX-M中的VTOR寄存器来进行重映射,修改。

猜你喜欢

转载自blog.csdn.net/zcc_123/article/details/112325395