STM32启动文件浅析
目录
- STM32启动文件简介
- 启动文件中的一些指令
- 启动文件代码详解
- 栈空间的开辟
- 堆空间的开辟
- 中断向量表定义(简称:向量表)
- 复位程序
- 对于weak的理解
- 对于_main函数的分析
- 中断服务程序
- 用户堆栈初始化
- 系统启动流程
STM32启动文件简介
STM32启动文件由ST官方提供,在官方的固件包里。启动文件由汇编编写,是系统上电复位后第一个执行的程序。
启动文件主要做了以下工作:
- 初始化堆栈指针
SP = _initial_sp
- 初始化程序计数器指针
PC = Reset_Handler
- 设置堆和栈的大小
- 初始化中断向量表
- 配置外部SRAM作为数据存储器(可选)
- 配置系统时钟,通过调用SystemInit函数(可选)
- 调用C库中的 _main 函数初始化用户堆栈,最终调用 main 函数
ARM指针寄存器 —— 堆栈指针寄存器SP、程序计数器PC、连接寄存器LR
堆栈指针R13(SP):每一种异常模式都有其自己独立的R13,它通常指向异常模式所专用的堆栈,也就是说五种异常模式、非异常模式(用户模式和系统模式),都有各自独立的堆栈,用不同的堆栈指针来索引。这样当ARM进入异常模式的时候,程序就可以把一般通用寄存器压入堆栈,返回时再出栈,保证了各种模式下程序的状态的完整性;
连接寄存器R14(LR):每种模式下R14都有自身版组,它有两个特殊功能;
-
保存子程序返回地址;使用BL或BLX时,跳转指令自动把返回地址放入R14中;子程序通过把R14复制到PC来实现返回,通常用下列指令之一:
MOV PC, LR BX LR
通常子程序这样写,保证了子程序中还可以调用子程序:
stmfd sp!, {lr} ... ldmfd sp!, {pc}
-
当异常发生时,异常模式的R14用来保存异常返回地址,将R14:如栈可以处理嵌套中断
程序计数器R15(PC):PC是有读写限制的,当没有超过读取限制的时候,读取的值是指令的地址加上8个字节,由于ARM指令总是以字对齐的,故bit[1:0]总是00。当用str或stm存储PC的时候,偏移量有可能是8或12等其它值。
ARM处理器使用流水线来增加处理器指令流的速度,这样可使几个操作同时进行,并使处理与存储器系统之间的操作更加流畅,连续,能提供0.9MIPS/MHZ的指令执行速度。
在随机存储器区划出一块区域作为堆栈区,数据可以一个个顺序地存入(压入)到这个区域之中,这个过程称为压栈(push )
。通常用一个指针(堆栈指针 SP—StackPointer)
实现做一次调整,SP总指向最后一个压入堆栈的数据所在的数据单元(栈顶)。从堆栈中读取数据时,按照堆栈 指针指向的堆栈单元读取堆栈数据,这个过程叫做弹出(pop )
,每弹出一个数据,SP 即向相反方向做一次调整,如此就实现了后进先出的原则。
堆栈是计算机中广泛应用的技术,基于堆栈具有的数据进出LIFO特性,常应用于:
- 保存中断断点;
- 保存子程序调用返回点;
- 保存CPU现场数据等;
- 也用于程序间传递参数;
ARM处理器中通常将寄存器R13作为堆栈指针(SP)。ARM处理器针对不同的模式,共有 6 个堆栈指针(SP),其中用户模式和系统模式共用一个SP,每种异常模式都有各自专用的R13寄存器(SP)。它们通常指向各模式所对应的专用堆栈,也就是ARM处理器允许用户程序有六个不同的堆栈空间。这些堆栈指针分别为R13、R13_svc、R13_abt、R13_und、R13_irq、R13_fiq,如下表堆栈指针寄存器所示。
堆栈指针 | 用户 | 系统 | 管理 | 中止 | 未定义 | 中断 | 快速中断 |
---|---|---|---|---|---|---|---|
R13 (SP) | R13 | R13 | R13_svc | R13_abt | R13_und | R13_irq | R13_fiq |
为了更准确地描述堆栈,根据“压栈”操作时堆栈指针的增减方向,将堆栈区分为‘递增堆栈’(SP 向大数值方向变化)和‘递减堆栈’(SP 向小数值方向变化);又根据SP 指针指向的存储单元是否含有堆栈数据,又将堆栈区分为‘满堆栈’(SP 指向单元含有堆栈有效数据)和‘空堆栈’(SP 指向单元不含有堆栈有效数据)。
这样两两组合共有四种堆栈方式——满递增、空递增、满递减和空递减。
ARM处理器的堆栈操作具有非常大的灵活性,对这四种类型的堆栈都支持。
ARM处理器中的R13被用作SP。当不使用堆栈时,R13 也可以用做通用数据寄存器。
栈的整体作用
- 保护现场;
- 传递参数;
- 临时变量保存在栈中;
深入理解ARM三个寄存器
PC 代表程序计数器,流水线使用三个阶段,因此指令分为三个阶段执行:
- 取指:从存储器装载一条指令;
- 译码:识别将要被执行的指令;
- 执行:处理指令并将结果写回寄存器;
R15(PC)总是指向 正在取指 的指令:ARM指令是三级流水线,取指、译指、执行是同时进行的,现在PC指向的是正在取指的地址,那么cpu正在译指的指令地址是PC-4(假设在ARM状态 下,一个指令占4个字节),cpu正在执行的指令地址是PC-8,也就是说PC所指向的地址和现在所执行的指令地址相差8。当突然发生中断的时候,保存的是PC的地址这样你就知道了,如果返回的时候返回PC,那么中间就有一个指令没有执行,所以用 SUB pc lr -lrq #4
启动文件中的一些指令
指令名称 | 作用 |
---|---|
EQU | 给数字常量取一个符号名,相当于C语言中的define |
AREA | 汇编一个新的代码段或者数据段 |
ALIGN | 编译器对指令或者数据的存放地址进行对齐,一般需要跟一个立即数,缺省表示4字节对齐。要注意的是,这个不是ARM的指令,是编译器的,这里放到一起为了方便 |
SPACE | 分配空间 |
PRESERVE8 | 当前文件堆栈需要按照8字节对齐 |
THUMB | 表示后面指令兼容 THUMB 指令。在 ARM 以前的指令集中有 16 位的THUMBM 指令,现在 Cortex-M 系列使用的都是 THUMB-2 指令集,THUMB-2 是 32 位的,兼容 16 位和 32 位的指令,是 THUMB 的超级版 |
EXPORT | 声明一个标号具有全局属性,可被外部的文件使用 |
DCD | 以字节为单位分配内存,要求 4 字节对齐,并要求初始化这些内存 |
PROC | 定义子程序,与 ENDP 成对使用,表示子程序结束 |
WEAK | 弱定义,如果外部文件声明了一个标号,则优先使用外部文件定义的标号,如果外部文件没有定义也不会出错。要注意的是,这个不是 ARM 的指令,是编译器的,这里放到一起为了方便 |
IMPORT | 声明标号来自外部文件,跟 C 语言中的 extern 关键字类似 |
LDR | 从存储器中加载字到一个存储器中 |
BLX | 跳转到由寄存器给出的地址,并根据寄存器的 LSE 确定处理器的状态,还要把跳转前的下条指令地址保存到 LR |
BX | 跳转到由寄存器/标号给出的地址,不用返回 |
B | 跳转到一个标号 |
IF,ELSE,ENDIF | 汇编条件分支语句,跟 C 语言的类似 |
END | 到达文件的末尾,文件结束 |
启动文件代码详解
下面,我们以 STM32F103 的启动代码为例讲解,版本是:STM32Cube_FW_F1_V1.8.0,
启动文件名称是:startup_stm32f103xe.s。把启动代码分成几个功能段进行详细的讲解,详
情如下
栈空间的开辟
Stack_Size EQU 0x00000400AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
**EQU:**宏定义的伪指令,给数字常量取一个符号名,类似与 C 中的 define。定义栈大小为 0x00000400 字节,即 1024B(1KB),常量的符号是 Stack_Size。
AREA 汇编一个新的代码段或者数据段。段名为 STACK,段名可以任意命名;NOINIT 表示不初始化; READWRITE 表示可读可写;ALIGN=3,表示按照 2^3 对齐,即 8 字节对齐。
SPACE 分配内存指令,分配大小为 Stack_Size 字节连续的存储单元给栈空间。
__initial_sp 紧挨着 SPACE 放置,表示栈的结束地址,栈是从高往低生长,所以结束地址就是栈顶地址。
栈主要用于存放局部变量,函数形参等,属于编译器自动分配和释放的内存,栈的大小不能超过内部 SRAM 的大小。如果工程的程序量比较大,定义的局部变量比较多,那么就需要在启动代码中修改栈的大小,即修改 Stack_Size 的值。如果程序出现了莫名其妙的错误,并进入了 HardFault 的时候,你就要考虑下是不是栈空间不够大,溢出了的问题。
堆空间的开辟
Heap_Size EQU 0x00000200AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
堆空间开辟代码跟栈空间开辟代码是类似的了。这部分代码的意思就是:开辟堆的大小为 0x00000200(512 字节),段名为 HEAP,不初始化,可读可写,(2^3 对齐)8 字节对齐。__heap_base表示堆的起始地址,__heap_limit 表示堆的结束地址。堆和栈的生长方向相反的,堆是由低向高生长,而栈是从高往低生长。
堆主要用于动态内存的分配,像 malloc()、calloc()和 realloc()等函数申请的内存就在堆上面。堆中的内存一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。
PRESERVE8
THUMB
PRESERVE8:指示编译器按照 8 字节对齐。
THUMB:指示编译器之后的指令为 THUMB 指令。
中断向量表定义(简称:向量表)
中断向量表定义代码
AREA RESET, DATA, READONLYEXPORT __VectorsEXPORT __Vectors_EndEXPORT __Vectors_Size
定义一个数据段,名字为RESET, READONLY表示只读;EXPORT表示声明一个标号具有全局属性,可被外部的文件使用。这里是声明了__Vectors、__Vectors_End 和 __Vectors_Size 三个标号具有全局性,可被外部的文件使用
当内核响应了一个发生的异常后,对应的异常服务例程(ESR)就会执行。为了决定 ESR的入口地址, 内核使用了向量表查表机制。向量表其实是一个 WORD(32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。向量表在地址空间中的位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 (即 FLASH 地址 0) 处必须包含一张向量表,用于初始时的异常分配。
位置 | 优先级 | 优先级类型 | 名称 | 说明 | 地址 |
---|---|---|---|---|---|
- | - | - | 保留 | 0x0000_0000 | |
-3 | 固定 | Reset | 复位 | 0x0000_0004 | |
-2 | 固定 | NMI | 不可屏蔽中断;RCC时钟安全系统(CSS)联接到NMI向量 | 0x0000_0008 | |
-1 | 固定 | 硬件失效(HardFault) | 所有类型的失效 | 0x0000_000C | |
0 | 可设置 | 存储管理(MemManage) | 存储器管理 | 0x0000_0010 | |
1 | 可设置 | 总线错误(BusFault) | 预取值失败,存储器访问失败 | 0x0000_0014 | |
2 | 可设置 | 错误应用(UsageFault) | 未定义的指令或非法状态 | 0x0000_0018 | |
- | - | - | 保留 | 0x0000_001C ~ 0x0000_002B | |
3 | 可设置 | SVCall | 通过SWI指令的系统服务调用 | 0x0000_002C | |
4 | 可设置 | 调式监控(DebugMonitor) | 调试监控器 | 0x0000_0030 | |
- | - | - | 保留 | 0x0000_0034 | |
5 | 可设置 | PendSV | 可挂起的系统服务 | 0x0000_0038 | |
6 | 可设置 | SysTick | 系统嘀嗒定时器 | 0x0000_003C | |
0 | 7 | 可设置 | WWDG | 窗口定时器中断 | 0x0000_0040 |
1 | 8 | 可设置 | PVD | 连到EXTI的电源电压检测(PVD)中断 | 0x0000_0044 |
2 | 9 | 可设置 | TAMPER | 侵入检测中断 | 0x0000_0048 |
3 | 10 | 可设置 | RTC | 实时时钟(RTC)全局中断 | 0x0000_004C |
4 | 11 | 可设置 | FLASH | 内存全局中断 | 0x0000_0050 |
中间部分省略,详情请参考《STM32中文参考手册》第九章 中断和事件 中断和异常向量
56 | 63 | 可设置 | DMA2通道1 | DMA2通道1全局中断 | 0x0000_0120 |
---|---|---|---|---|---|
57 | 64 | 可设置 | DMA2通道2 | DMA2通道2全局中断 | 0x0000_0124 |
58 | 65 | 可设置 | DMA2通道3 | DMA2通道3全局中断 | 0x0000_0128 |
59 | 66 | 可设置 | DMA2通道4_5 | DMA2通道4和DMA2通道5全局中断 | 0x0000_012C |
举个例子,如果发生了异常 SVCall,则 NVIC 会计算出偏移移量是 11x4=0x2C,然后从那里取出服务例程的入口地址并跳入。要注意的是这里有个另类:地址 0x0000 0000 并不是什么入口地址,而是给出了复位后 MSP 的初值。更详细的向量表,可以参考《STM32中文参考手册》第九章-中断和事件-中断和异常向量
F103 的向量表格中灰色部分是系统内核异常。表格中位置 0 到 59 是外部中断,CM3内核的芯片最大支持 240 个外部中断,具体使用多少个由芯片厂家设计决定。如这个表格中的 103 芯片只是使用了 60 个。这里说的外部中断是相对内核而言
__Vectors DCD __initial_sp ; Top of Stack (栈顶地址)DCD Reset_Handler ; Reset Handler (复位程序地址)DCD NMI_Handler ; NMI HandlerDCD HardFault_Handler ; Hard Fault HandlerDCD MemManage_Handler ; MPU Fault HandlerDCD UsageFault_Handler ; Usage Fault HandlerDCD 0 ; ReservedDCD 0 ; ReservedDCD 0 ; ReservedDCD 0 ; ReservedDCD SVC_Handler ; SVCall HandlerDCD DebugMon_Handler ; Debug Monitor HandlerDCD 0 ; ReservedDCD PendSV_Handler ; SysTick Handler; External Interrupts(外部中断)DCD WWDG_IRQHandler ; Window WatchdogDCD PVD_IRQHandler ; PVD through EXTI Line detectDCD TAMPER_IRQHandler ; TamperDCD RTC_IRQHandler ; RTCDCD FLASH_IRQHandler ; Flash; 中间篇幅太长, 省略掉, 代码向量表与STM32F103的向量表对应DCD DMA2_Channel1_IRQHandler ; DMA2 Channel1DCD DMA2_Channel2_IRQHandler ; DMA2 Channel2DCD DMA2_Channel3_IRQHandler ; DMA2 Channel3DCD DMA2_Channel4_5_IRQHandler ; DMA2 Channel4 & Channel5
__Vectors_End__Vectors_Size EQU __Vectors_End - __Vectors
__Vectors 为向量表起始地址, __Vectors_End 为向量表结束地址,__Vectors_Size 为向量表大小,__Vectors_Size = __Vectors_End - __Vectors。
DCD:分配一个或者多个以字为单位的内存,以四字节对齐,并要求初始化这些内存。
中断向量表被放置在代码段的最前面。例如:当我们的程序在 FLASH 运行时,那么向量表的起始地址是:0x0800 0000。结合图 2.3.2 可以知道,地址 0x0800 0000 存放的是栈顶地址。DCD:以四字节对齐分配内存,也就是下个地址是0x0800 0004,存放的是Reset_Handler中断函数入口地址。
从代码上看,向量表中存放的都是中断服务函数的函数名,所以 C 语言中的函数名对芯片来说实际上就是一个地址。
复位程序
接下来是定义只读代码段
AREA |.text|, CORE, READONLY
定义一个段命为.text,只读的代码段,在CODE区;复位子程序代码
; Reset handler
Reset_Handler PROC // 子程序开始EXPORT Reset_Handler [WEAK] // 声明复位中断向量 Reset_Handler 为全局属性,这样外部文件就可以调用此复位中断服务;WEAK: 表示弱定义,如果外部文件优先定义了该标号则首先引用外部定义的标号,如果外部文件没有声明也不会出错。这里表示复位子程序可以由用户在其他文件重新实现,这里并不是唯一的IMPORT __main // IMPORT 表示该标号来自外部文件IMPORT SystemInit // 这里表示 SystemInit 和__main 这两个函数均来自外部的文件LDR R0 ,= SystemInit // LDR 表示从存储器中加载字到一个存储器中; SystemInit 是一个标准的库函数,在 system_stm32f1xx.c 文件中定义,主要作用是配置系统时钟、还有就是初始化 FSMC/FMC总线上外挂的 SRAM(可选),前面说配置外部 SRAM 作为数据存储器(可选)就是这个BLX R0 // BLX 表示跳转到由寄存器给出的地址,并根据寄存器的 LSE 确定处理器的状态,还要把跳转前的下条指令地址保存到 LRLDR R0 ,= __main // 把__main 的地址给 R0。__main 是一个标准的 C 库函数,主要作用是初始化用户堆栈和变量等,最终调用 main 函数去到 C 的世界。这就是为什么我们写的程序都有一个 main 函数的原因,如果不调用__main,那么程序最终就不会调用我们 C 文件里面的main,也就无法正常运行BX R0 // BX 表示跳转到由寄存器/标号给出的地址,不用返回。这里表示切换到__main地址,最终调用 main 函数,不返回,进入 C 的世界ENDP // 子程序结束
利用 PROC、ENDP 这一对伪指令把程序段分为若干个过程,使程序的结构加清晰;
LDR、BLX、BX 是内核的指令,可在《CM3 权威指南 CnR2》第四章-指令集里面查询到
对于weak的理解
weak 顾名思义是“弱”的意思,在汇编中,在函数名称后面加[WEAK]来表示,而在 C语言中,在函数名称前面加上 __weak 修饰符来表示,这样的函数我们称为“弱函数”;
被 [WEAK] 或 __weak 声明的函数,我们可以在自己的文件中重新定义一个同名函数,最终编译器编译的时候,会选择我们定义的函数,如果我们没有重新定义这个函数,那么编译器就会执行[WEAK]或__weak 声明的函数,并且编译器不会报错;
HardFault_Handler\PROCEXPORT HardFault_Handler [WEAK]B .ENDP
同样我们打开stm32f1xx_it.c文件中也定义了HardFault_Handler中断函数
void HardFault_Handler(void)
{/* Go to infinite loop when Hard Fault exception occurs */while(1){}
}
在 stm32f1xx_it.c 文件定义了 HardFault_Handler 中断函数的情况下,当HardFault_Handler 中断来到的时候,代码会运行到 stm32f1xx_it.c 文件的 HardFault_Handler中断函数,且进入 while(1);
下面,我们注释掉 stm32f1xx_it.c 的 HardFault_Handler 中断函数,然后进行编译,发现不会报错。这时候当 HardFault_Handler 中断来到的时候,代码会运行到启动文件的“弱函数”中,即在启动文件中 164 行代码,进行原地跳转(即无限循环);
对于_main函数的分析
当看到__main 函数时,估计有不少人认为这个是 main 函数的别名或是编译之后的名字,否则在启动代码中再也无法找到和 main 相关的字眼了。可事实是,_main 和 main 是两个完全不同的函数。_main 代码是编译器自动创建的,因此无法找到_main 代码。MDK 文档中有一句说明:it is automatically craated by the linker when it sees a definition of main() 。大体意思可以理解为:当编译器发现定义了 main 函数,那么就会自动创建_main;
程序经过汇编启动代码,执行到__main()后,可以看出有两个大的函数:
- __scatterload():负责把 RW/RO 输出段从装载域地址复制到运行域地址,并完成了 ZI运行域的初始化工作;
- __rt_entry():负责初始化堆栈,完成库函数的初始化,最后自动跳转向 main()函数;
中断服务程序
接下来就是中断服务程序了
; 系统异常中断
NMI_Handler PROCEXPORT NMI_Handler [WEAK]B . ;原地跳转(即无限循环)ENDP
HardFault_Handler\PROCEXPORT HardFault_Handler [WEAK]B .ENDP
;中间代码太长, 已经省略掉
SysTick_Handler PROCEXPORT SysTick_Handler [WEAK]B .ENDP
;外部中断
Default_Handler PROCEXPORT WWDG_IRQHandler [WEAK]EXPORT PVD_IRQHandler [WEAK]EXPORT TAMPER_IRQHandler [WEAK]EXPORT RTC_IRQHandler [WEAK]EXPORT FLASH_IRQHandler [WEAK]
;中间代码太长, 已经省略掉
DMA2_Channel1_IRQHandler
DMA2_Channel2_IRQHandler
DMA2_Channel3_IRQHandler
DMA2_Channel4_5_IRQHandlerB .ENDP
可以看到这些中断服务函数都被[WEAK]声明为弱定义函数,如果外部文件声明了一个标号,则优先使用外部文件定义的标号,如果外部文件没有定义也不会出错;
这些中断函数分为系统异常中断和外部中断,外部中断根据不同芯片有所变化。B 指令是跳转到一个标号,这里跳转到一个‘.’,表示无限循环;
在启动文件代码中,已经把我们所有中断的中断服务函数写好了,但都是声明为弱定义,所以真正的中断服务函数需要我们在外部实现;
如果我们开启了某个中断,但是忘记写对应的中断服务程序函数又或者把中断服务函数名写错,那么中断发生时,程序就会跳转到启动文件预先写好的弱定义的中断服务程序中,并且在 B 指令作用下跳转到一个‘.’中,无限循环;
这里的系统异常中断是内核的,外部中断是外设的;
用户堆栈初始化
ALIGN指令
ALIGN
ALIGN 表示对指令或者数据的存放地址进行对齐,一般需要跟一个立即数,缺省表示4 字节对齐。要注意的是,这个不是 ARM 的指令,是编译器的;
接下就是启动文件最后一部分代码,用户堆栈初始化代码
IF :DEF:__MICROLIB // 判断是否定义了__MICROLIB。关于__MICROLIB 这个宏定义// 如果定义__MICROLIB,声明__initial_sp、__heap_base 和__heap_limit这三个标号具有全局属性,可被外部的文件使用。//__initial_sp 表示栈顶地址,__heap_base表示堆起始地址,__heap_limit 表示堆结束地址EXPORT __initial_spEXPORT __heap_baseEXPORT __heap_limitELSE // 没有定义__MICROLIB,实际的情况就是我们没有定义__MICROLIB,所以使用默认的 C 库运行。堆栈的初始化由 C 库函数__main 来完成IMPORT __use_two_region_memory // IMPORT 声明__use_two_region_memory 标号来自外部文件EXPORT __user_initial_stackheap // EXPORT 声明__user_initial_stackheap 具有全局属性,可被外部的文件使用__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 // 跳转到 LR 标号给出的地址,不用返回ALIGNENDIFEND // END 表示到达文件的末尾,文件结束
IF, ELSE, ENDIF 是汇编的条件分支语句
系统启动流程
在以前 ARM7/ARM9 内核的控制器在复位后,CPU 会从存储空间的绝对地址0x00000000 取出第一条指令执行复位中断服务程序的方式启动,即固定了复位后的起始地址为 0x00000000(PC = 0x00000000),同时中断向量表的位置也是固定的。而 Cortex-M3内核复位后的起始地址和中断向量表的位置可以被重映射。充映射的方法是通过启动模式的选择,有以下 3 种情况:
- 通过 boot 引脚设置可以将中断向量表定位于 SRAM 区,即起始地址为 0x2000000,同时复位后 PC 指针位于
0x2000000 处; - 通过 boot 引脚设置可以将中断向量表定位于 FLASH 区,即起始地址为 0x8000000,同时复位后 PC 指针位于
0x8000000 处; - 通过 boot 引脚设置可以将中断向量表定位于内置 Bootloader 区,本文不对这种情况做论述;
Cortex-M3 内核规定,起始地址必须存放堆顶指针,而第二个地址则必须存放复位中断入口向量地址,这样在 Cortex-M3 内核复位后,会自动从起始地址的下一个 32 位空间取出复位中断入口向量,跳转执行复位中断服务程序。
下面将结合《Cortex-M3 权威指南(中文)》chpt03-复位序列的内容进行讲解。
启动模式不同,启动的起始地址是不一样的,下面我们以代码下载到内部 FLASH 的情况举例,即代码从地址 0x0800 0000 开始被执行。
我们知道的复位方式有三种:上电复位,硬件复位和软件复位。当产生复位,并且离开复位状态后,CM3 内核做的第一件事就是读取下列两个 32 位整数的值:
- 从地址 0x0800 0000 处取出堆栈指针 MSP 的初始值,该值就是栈顶地址;
- 从地址 0x0800 0004 处取出程序计数器指针 PC 的初始值,该值指向复位后执行的第一条指令;
下面用示意图表示
请注意,这与传统的 ARM 架构不同——其实也和绝大多数的其它单片机不同。传统的 ARM 架构总是从 0 地址开始执行第一条指令。它们的 0 地址处总是一条跳转指令。而 在 CM3 内核中,0 地址处提供 MSP 的初始值,然后就是向量表(向量表在以后还可以被移至其它位置)。向量表中的数值是 32 位的地址,而不是跳转指令。向量表的第一个条目指向复位后应执行的第一条指令,就是 Reset_Handler 这个函数。下面继续以战舰开发板 HAL库例程的实验 1 跑马灯实验为例,代码从地址 0x0800 0000 开始被执行,讲解一下系统启动,初始化堆栈、MSP 和 PC 后的内存情况
因为 CM3 使用的是向下生长的满栈,所以 MSP 的初始值必须是堆栈内存的末地址加 1。举例来说,如果你的栈区域在 0x20000388‐0x20000787 之间,那么 MSP 的初始值就必须是 0x20000788。
向量表跟随在 MSP 的初始值之后——也就是第 2 个表目。
R15 是程序计数器,在汇编代码中,可以使用名字“PC”来访问它。ARM 规定:PC最低两位并不表示真实地址,最低位 LSB 用于表示是 ARM 指令(0)还是 Thumb 指令(1),因为 CM3 主要执行 Thumb 指令,所以这些指令的最低位都是 1(都是奇数)。因为 CM3 内
部使用了指令流水线,读 PC 时返回的值是当前指令的地址+4。比如说:
0x1000: MOV R0, PC ; R0 = 0x1004
如果向 PC 写数据,就会引起一次程序的分支(但是不更新 LR 寄存器)。CM3 中的指令至少是半字对齐的,所以 PC 的 LSB 总是读回 0。然而,在分支时,无论是直接写 PC 的值还是使用分支指令,都必须保证加载到 PC 的数值是奇数(即 LSB=1),表明是在Thumb 状态下执行。倘若写了 0,则视为转入 ARM 模式,CM3 将产生一个 fault 异常。
正因为 上 述 原 因 , 图 3.3 中使用 0x080001CD 来表达地址 0x080001CC 。 当0x080001CD 处的指令得到执行后,就正式开始了程序的执行(即去到 C 的世界)。所以在此之前初始化 MSP 是必需的,因为可能第 1 条指令还没执行就会被 NMI 或是其它 fault 打断。MSP 初始化好后就已经为它们的服务例程准备好了堆栈。
相关文章:
STM32启动文件浅析
目录 STM32启动文件简介启动文件中的一些指令 启动文件代码详解栈空间的开辟堆空间的开辟中断向量表定义(简称:向量表)复位程序对于weak的理解对于_main函数的分析 中断服务程序用户堆栈初始化 系统启动流程 STM32启动文件简介 STM32启动文件…...
h5页面与小程序页面互相跳转
小程序跳转h5页面 一个home页 /pages/home/home 一个含有点击事件的元素:<button type"primary" bind:tap"toWebView">点击跳转h5页面</button>toWebView(){ wx.navigateTo({ url: /pages/webview/webview }) } 一个webView页 /pa…...
探索 JavaScript 事件机制(四):React 合成事件系统
前言 在前端开发中,事件处理是不可或缺的一部分。在众多的前端框架中,React 凭借其高效和灵活性受到众多开发者的喜爱。React 的事件处理系统,即“合成事件系统”,是其性能优化的一大亮点。 本文将带你深入浅出地探索 React 的合…...
openlayers 封装加载本地geojson数据 - vue3
Geojson数据是矢量数据,主要是点、线、面数据集合 Geojson数据获取:DataV.GeoAtlas地理小工具系列 实现代码如下: import {ref,toRaw} from vue; import { Vector as VectorLayer } from ol/layer.js; import { Vector as VectorSource } fr…...
手机号码携号转网查询接口-在线手机号码携号转网查询-手机号码携号转网查询API
接口简介:通过手机号精准查询该号码转网前及转网后所归属运营商 可查询号码是否为虚拟手机号 可查询到号码归属地信息 高准确率,实时查询运营商数据库 多用于营销场景,如运营商业务办理、客户信息查询、携号转网、电话营销等 接口地址&#x…...
yolo目标检测和姿态识别和目标追踪
要检测摄像头画面中有多少人,人一排排坐着,像教室那样。由于摄像头高度和角度的原因,有的人会被遮挡。 yolo v5 首先需要下载yolo v5官方代码,可以克隆或下载主分支的代码,或者下载release中发布的。 简单说一下环境…...
Docker搭建开源Web云桌面操作系统Puter和DaedalOS
文章目录 Puter 操作系统说明基于 Docker 启动 Puter 操作系统拉取镜像运行容器基于 Docker-Compose 启动 Puter操作系统创建目录编写docker-compose.yml运行在本地直接运行puter操作系统puter界面截图puter个人使用总结构建自己的Puter镜像daedalos基于web的操作系统说明技术特…...
FAQ-为什么交换机发给服务器的日志显示的时间少8小时
问题描述 配置交换机向日志服务器发送日志,在交换机上面查看日志显示的时间比日志服务器显示的时间快8个小时 解决方案 根据公司全球化整改的要求,syslog默认发送的是UTC时间。 当前设备上配置了时区UTC8,因此,设备上显示的本地…...
[表达式]真假计算
题目描述 有一棵树,不一定是二叉树。 所有叶子节点都是 True 或者 False。 对于从上往下奇数层的非叶子节点是 and,偶数层非叶子节点为 or。 树上每个节点的值是所有孩子节点的值进行该节点的运算操作。 判断一棵树能否砍掉,最快的方法就是从…...
记录一次线上环境svchost.exe antimalware service executable 进程占用CPU过高问题
博主介绍: 大家好,我是想成为Super的Yuperman,互联网宇宙厂经验,17年医疗健康行业的码拉松奔跑者,曾担任技术专家、架构师、研发总监负责和主导多个应用架构。 技术范围: 目前专注java体系,有多…...
Docker 部署 EMQX 一分钟极速部署
部署 EMQX ( Docker ) [Step 1] : 拉取 EMQX 镜像 docker pull emqx/emqx:latest[Step 2] : 创建目录 ➡️ 创建容器 ➡️ 拷贝文件 ➡️ 授权文件 ➡️ 删除容器 # 创建目录 mkdir -p /data/emqx/{etc,data,log}# 创建容器 docker run -d --name emqx -p 1883:1883 -p 1808…...
STL-常用容器-list
1list基本概念 **功能:**将数据进行链式存储 链表(list)是一种物理存储单元上非连续的存储结构,数据元素的逻辑顺序是通过链表中的指针链接实现的 链表的组成:链表由一系列结点组成 结点的组成:一个是存储…...
Lambda 架构
Lambda架构是一种用于构建可扩展、容错和实时数据处理系统的架构模式。 它由三个主要部分组成:批处理层(Batch Layer)、实时层(Speed Layer)和服务层(Serving Layer)。 Lambda架构旨在结合批处…...
Windows电脑设置网络唤醒(Wake-on-LAN)
1. 启用 Windows 电脑的 Wake-on-LAN 功能 首先,你需要确保你的 Windows 电脑支持并启用了 Wake-on-LAN: BIOS/UEFI 设置(具体看自己电脑主板如何设置): 启动 Windows 电脑,进入 BIOS/UEFI 设置。找到网络适配器相关的设置,启用 …...
前端项目构建流程
1. 需求分析 目标:明确项目目标、核心功能和用户需求。 产品需求讨论: 与产品经理、客户、业务部门讨论项目的需求和目标,理解产品的功能、业务流程以及用户需求。定义用户角色(Persona),明确不同用户的功…...
支持国密算法的数字证书-国密SSL证书详解
在互联网中,数字证书作为标志通讯各方身份信息的数字认证而存在,常见的数字证书大都采用国际算法,比如RSA算法、ECC算法、SHA2算法等。随着我国加强网络安全技术自主可控的大趋势,也出现了支持国密算法的数字证书-国密SSL证书。那…...
【EndNote使用教程】创建文献库、导入文献、文献分类
1、创建文献库 打开“EndNote”,点击“文件”,点击“新建”,选择保存文件路径。 2、导入文献 (1)可以选择导入电脑上的PDF文件,如下图所示。 (2) 也可以选择直接在浏览器网页上面直…...
双十一电容笔选哪个好?!西圣、益博思、吉玛仕电容笔实测对比!
当数码测评博主几年年,我也实测过不下10款电容笔了,对电容笔这个品类也算是半个内行人了。提到电容笔,在平替品牌的追逐中,西圣、益博思、吉玛仕这三款作为国货黑马一直备受瞩目,综合各大电商平台的销量榜、好评口碑榜…...
房地产网络安全:主要风险及缓解建议
房地产行业已开始数字化转型,因此极易受到网络犯罪的攻击。潜在风险的清单很长:从客户敏感信息的数据泄露到勒索软件攻击,网络犯罪分子将房地产公司视为其所携带的所有类型敏感信息的高价值目标。 在本文中,我们将探讨房地产领域…...
玩转大模型的第一步——提示词(Prompt)工程【抛砖篇】
前言 AI大模型提示词工程,又名 LLM prompts Project,指的是在使用大型语言模型(如OpenAI的GPT系列)时,用于引导模型生成特定响应的输入,是在使用AI大模型过程中非常重要的一个环节,是模型生成文…...
火山引擎数据飞轮线上研讨会即将开启,助力消费品牌双十一造爆款
随着双十一的临近,各大品牌方的备战工作已进入紧张而有序的倒计时阶段。这场持续十多年的电商大促,对消费者来说是购物狂欢节,对各大品牌方来说,则是更是品牌实力与策略的比拼。面对日益激烈的市场竞争,如何更好地撬动…...
【python实战】利用代理ip爬取Alibaba海外版数据
引言 在跨境电商的业务场景中,数据采集是分析市场、了解竞争对手以及优化经营策略的重要环节。然而,随着越来越多企业依赖数据驱动决策,许多跨境电商平台为了保护自身数据,采取了更严格的防护措施。这些平台通过屏蔽大陆IP地址或部…...
FFMPEG录屏(20)--- 枚举macOS下的窗口和屏幕列表,并获取名称缩略图等信息
在 macOS 下获取可屏幕共享的窗口和屏幕 在 macOS 下,我们可以通过使用 Core Graphics 和 Cocoa 框架来获取当前系统中可屏幕共享的窗口和屏幕信息。本文将详细介绍如何获取窗口和屏幕的 ID、标题、坐标、进程图标和缩略图等信息。 前提条件 在开始之前ÿ…...
Redis 命令集 (超级详细)
目录 Redis 常用命令集 string类型 hash类型 list类型 set类型 zset类型 bitmap 类型 geo 类型 GEOADD (添加地理位置的坐标) GEOPOS (获取地理位置的坐标) GEODIST (计算两个位置之间的距离) GEOHASH (返回一个或多个位置对象的 geohash 值) GEORADIUS (根据用户…...
Spring Cloud --- GateWay和Sentinel集成实现服务限流
pom添加依赖 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency><groupId>com.alibaba.csp</groupId><artifactId>s…...
python excel如何转成json,并且如何解决excel转成json时中文汉字乱码的问题
1.解决excel转成json时中文汉字乱码的问题 真的好久没有打开这个博客也好久没有想起来记录一下问题了,今天将表格测试集转成json格式的时候遇到了汉字都变成了乱码的问题,虽然这不是个大问题,但是编码问题挺烦人的,乱码之后像下图…...
【MySQL】实战篇—数据库设计与实现:根据需求设计数据库架构
在设计数据库架构时,开发者需要遵循一系列步骤,以确保数据库能够高效、可靠地满足系统需求。以下是设计数据库架构的理论知识和步骤说明。 1. 需求分析 需求分析是数据库设计的第一步,旨在理解系统的功能需求和数据需求。通过与利益相关者&…...
[Python学习日记-53] Python 中的正则表达式模块 —— re
[Python学习日记-53] Python 中的正则表达式模块 —— re 简介 re 模块 练习 简介 我们在编程的时候经常会遇到想在一段文字当中找出电话号码、身份证号、身高、年龄之类的信息,就像下面的数据一样 # 文件名:美丽学姐联系方式.txt 姓名 地区 …...
Unity编辑器 连接不到SteamVR问题记录
问题表现:之前正常的工程,某天打开后运行,在SteamVR未打开时,Unity工程运行后无法调用起来Steam VR,无任何反应,但用其他软件则可以调用起来SteamVR,并且运行正常,在重装了XR的一些插…...
nginx 日志配置笔记
Nginx 的日志配置非常重要,它可以帮助你记录服务器的访问情况、错误信息等,便于后续的分析和故障排查。Nginx 的日志配置主要包括访问日志(access log)和错误日志(error log)。 1、访问日志(Ac…...
wordpress系列文章/seo专业培训中心
问题报错: Plugin execution not covered by lifecycle configuration: org.apache.maven.plugins:maven-resources-plugin:2.6:resources 问题原因: m2e在其执行maven的生命周期管理时没有定义该插件 解决办法: 1、eclipse:…...
vmware 下wordpress/网页在线代理翻墙
本科生一定要过计算机一级吗本科生并不一定要过计算机一级,这个具体要看学校的规定。有些学校对计算机不做要求,有的学校要求必须要过计算机二级才能拿毕业。计算机一级书有用吗其实,计算机一级书用处并不是很大,只需要会基本的电…...
如何做互联网网站/网站点击量软件
今天当我浏览完网页后, 突然桌面上多了好几个快捷网址的快捷链接. 一些什么在线博彩之类的, 外国网站.我当时就删除了, 没去理会那么多. 没想到过一会儿后, 从任务栏底部自动冒出来一个类似于广告条之类的东东. 真是太可恨了..再一打开IE, 昏, 首页都让给改了. 查了半天查到注册…...
西安网站建设外包服务/百度seo排名优化公司推荐
Hoisting 是指 js 在执行代码前,默认会将变量的声明和函数的声明,提升到当前作用域顶端的行为。 这里要注意一下,只提升声明,例如: console.log(a); var a 10; //输出undefined,因为只将var a;提升到了作用…...
用python 做网站/seo网站免费优化软件
首先引入类库,Microsoft.Office.Interop.Word,然后进行编程。代码如下:首先引入类库,Microsoft.Office.Interop.Word,然后进行编程。代码如下: using System; using System.Collections.Generic; using System.ComponentModel; us…...
网站建设制作设计公司哪家好/php免费开源crm系统
// 1、引入命名空间Ext.namespace("ExtUD.Ext");//相当于java中包的作用// 2、编写自定义控件类ExtUD.Ext.UDPanel Ext.extend(Ext.Panel, { title : 自定义控件, html:自定义控件面板, layout:fit, getAlert:function(){alert(自定义控件函数!)…...