从大学时期,使用的第一块ARM单片机——飞思卡尔K60起,到后面参加工作 STM32,I.MX RT系列,Infineon 的 TLE984等等。从当初啥外设都不知,只会在main函数里写个 printf,到现在对ARM架构略知一二。接下来笔者来重点说明下,我们常常见到的startup.s文件。也就是常说的启动文件
若连一个单片机是怎么启动都不知道,只知道在while(1)里写代码,这就常说的只会敲代码的码农,还不算一个完整的嵌入式工程师吧。这里笔者就拿最近在用的 i.MX RT1052来讲,cortex M7 一款性价比贼高的跨界处理器。下面的描述都是笔者通过相关书籍或网络整理后的笔记内容:
;--------------------------------------------------------------------------------------------------------------- ; 第一部分: ; ; 1.启动代码最重要的工作是把异常&中断向量表放到正确的Flash地址上(也有可能是RAM上) ; 2.把向量表定义为只读数据段,并导出向量表符号(Symbol),让链接器识别此符号并根据分散加载文件正确的放置向量表(链接部分就不讲了) ; 3.__Vector***标号以及|Image$$ARM_LIB_STACK$$ZI$$Limit|标号需要与分散加载文件合起来看,才会明白其真正的功能 ; ;---------------------------------------------------------------------------------------------------------------
PRESERVE8 ; 8 字节对齐
THUMB ; THUMB指令集
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY ; 声明权限为"READONLY" 名称为"RESET"的数据段
EXPORT __Vectors ; 导出"__Vectors"标号,理解为向量表收地址,分散加载描述文件中会用到
EXPORT __Vectors_End
EXPORT __Vectors_Size
IMPORT |Image$$ARM_LIB_STACK$$ZI$$Limit| ; 导入"|Image$$ARM_LIB_STACK$$ZI$$Limit|"
;--------------------------------------------------------------------------------------------------------------- ; 第二部分: ; ; |Image$$ARM_LIB_STACK$$ZI$$Limit| : ; 1.栈顶指针地址,此语法跟MDK编译器的底层相关,是ARMCC编译器才能识别的语法,GCC与IAR的底层编译器ICCARM编译器 ; 不能识别 ; 2.|Image$$ARM_LIB_STACK$$ZI$$Limit| 是一个链接器 Image Symbol ; 3.此处|Image$$ARM_LIB_STACK$$ZI$$Limit|相当于栈顶地址,或者理解为直接将栈顶地址写到此处也行 ; ; Reset_Handler : ; 1.Reset_Handler函数地址,此处相当于把Reset_Handler函数地址赋值给PC,即调用Reset_Handler函数 ; ; 重要关键节点: ; 1.绝大多数Cortex-M微控制器(M0 M3 M4 M7都是这样)复位后先进入厂商BOOTROM,此时所有用户行为均无法介入处理器 ; 2.厂商BOOTROM(有些厂商会有其他称呼)主要负责处理一些芯片最基本的初始化、加密(RT1052就是这样)以及一些对MCU ; 的差异化设置等工作 ; 3.BOOTROM顺利完成后,MCU控制权会交给用户,即启动代码 ; 4.启动代码(运行汇编语言则不需要此启动代码),最重要的工作在于设置MSP(主堆栈指针)以及PC(程序计数器)的值 ; 5.Cortex-M微控制器会默认把0x00000000地址里面的值设置为MSP的值,0x00000004地址里面的值设置为PC的值 ; ; 关于External(异常)与 Interrupts(中断)的区别说明 ; 1.External(异常)与 Interrupts(中断)是不同的,是两个不同的概念 ; 2.External(异常)是向量表的前16个向量,其优先级为负数,高于所有中断,而且不可调整优先级也不可以关闭,可以 ; 打断正常程序与Interrupts(中断)的运行 ; 3.从第16个向量以后才是Interrupts(中断),可以设置优先级,不用时可以关闭,但优先级一般低于External(异常) ; ; ;---------------------------------------------------------------------------------------------------------------
__Vectors DCD |Image$$ARM_LIB_STACK$$ZI$$Limit| ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ;NMI Handler
DCD HardFault_Handler ;Hard Fault Handler
DCD MemManage_Handler ;MPU Fault Handler
DCD BusFault_Handler ;Bus Fault Handler
DCD UsageFault_Handler ;Usage Fault Handler
DCD 0 ;Reserved
DCD 0 ;Reserved
DCD 0 ;Reserved
DCD 0 ;Reserved
DCD SVC_Handler ;SVCall Handler 操作系统启动时会调用
DCD DebugMon_Handler ;Debug Monitor Handler
DCD 0 ;Reserved
DCD PendSV_Handler ;操作系统会用到的异常向量
DCD SysTick_Handler ;操作系统会用到的滴答定时器异常向量(没有操作系统时可以用作普通定时器中断,这里强烈建议这么用)
;External Interrupts ;--------------------------------------------------------------------------------------------------------------- ; 第三部分: ; ; 1.这里开始是中断向量表 ; 2.各个向量的顺序是芯片设计的时候就定义好的,不能更改 ; ;---------------------------------------------------------------------------------------------------------------
DCD DMA0_DMA16_IRQHandler ;DMA channel 0/16 transfer complete
DCD DMA1_DMA17_IRQHandler ;DMA channel 1/17 transfer complete
DCD DMA2_DMA18_IRQHandler ;DMA channel 2/18 transfer complete
DCD DMA3_DMA19_IRQHandler ;DMA channel 3/19 transfer complete
DCD DMA4_DMA20_IRQHandler ;DMA channel 4/20 transfer complete
DCD DMA5_DMA21_IRQHandler ;DMA channel 5/21 transfer complete
......
DCD DefaultISR ;251
DCD DefaultISR ;252
DCD DefaultISR ;253
DCD DefaultISR ;254
DCD 0xFFFFFFFF ; Reserved for user TRIM value
__Vectors_End
__Vectors_Size EQU __Vectors_End - __Vectors
;--------------------------------------------------------------------------------------------------------------- ; 第四部分: ; ; 这部分开始可以称作Reset Handler实体,芯片上电后,经过BOOTROM后进入的用户可控最开始处的地方 ; 1.如果想让MCU正常使用C语言,务必在此处调用__main函数 ; 2.__main()不是main()两者有着本质的区别 ; 3.__main()是C Library中的函数,keil开发环境中自带的C Library ; 4.main()是被__main()调用的,__main()工作完成最后一步就是调用main() ; 5.__main()被调用之前,可以根据需要插入一个或多个其他功能函数 ; ;---------------------------------------------------------------------------------------------------------------
AREA RES_HANDLER, CODE, READONLY
; Reset Handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
CPSID I ; Mask interrupts
LDR R0, =0xE000ED08 ;
LDR R1, =__Vectors
STR R1, [R0]
LDR R2, [R1]
MSR MSP, R2
;Config OCRAM,DTCM,ITCM
LDR R0,=0x400AC038
LDR R1,=0x00AA0000
STR R1,[R0]
LDR R0,=0x400AC040
LDR R1,=0x00200007
STR R1,[R0]
LDR R0,=0x400AC044
LDR R1,=0xFFFFAAA9 ;OCRAM = 32KB, DTCM = 224KB ,ITCM = 256KB 这里定义RAM分配,不是每个MCU都是这样的
STR R1,[R0]
LDR R0, =SystemInit
BLX R0
CPSIE i ; Unmask interrupts
LDR R0, =__main ;__main 不是我们所理解的 main函数
BX R0
ENDP
AREA OTHER_HANDLER, CODE, READONLY
;--------------------------------------------------------------------------------------------------------------- ; 第五部分: ; ; 1.[WEAK]指定了这个函数为“若函数” (具体怎么用,问问度娘就知道了) ; 2.这些中断服务函数定义成弱函数的意义是,当中断出现时,需要有一个中断服务函数予以响应,但真实的用户程序往往只 ; 会使用一部分中断,甚至不使用中断,所以以下这些函数给出了异常/中断服务函数的默认实现(死循环,汇编中的'B.' ; 语句,相当于while(1)),因为不知道用户是否会用到多少中断,但这些服务函数又很重要,所以就把这些函数都实现并 ; 声明为若函数 ; 3.在C语言中声明弱函数是在函数后加"__attribute((weak))" ; ;---------------------------------------------------------------------------------------------------------------
; Dummy Exception Handlers (infinite loops which can be modified)
NMI_Handler
PROC
EXPORT NMI_Handler [WEAK]
B . ;这里实现死循环 类似 while(1)
ENDP
HardFault_Handler
PROC
EXPORT HardFault_Handler [WEAK]
B .
ENDP
......
ENET_1588_Timer_IRQHandler
PROC
EXPORT ENET_1588_Timer_IRQHandler [WEAK]
LDR R0, =ENET_1588_Timer_DriverIRQHandler
BX R0
ENDP
Default_Handler
PROC
EXPORT DMA0_DMA16_DriverIRQHandler [WEAK]
EXPORT DMA1_DMA17_DriverIRQHandler [WEAK]
EXPORT DMA2_DMA18_DriverIRQHandler [WEAK]
......
PWM4_1_IRQHandler
PWM4_2_IRQHandler
PWM4_3_IRQHandler
PWM4_FAULT_IRQHandler
Reserved168_IRQHandler
Reserved169_IRQHandler
Reserved170_IRQHandler
Reserved171_IRQHandler
Reserved172_IRQHandler
Reserved173_IRQHandler
SJC_ARM_DEBUG_IRQHandler
NMI_WAKEUP_IRQHandler
DefaultISR
LDR R0, =DefaultISR
BX R0
ENDP
ALIGN
END
|