可信网站验证服务证书,创联互动建设网站,住房建筑信息平台,企业网络设计方案预算目录 STM32启动文件简介启动文件中的一些指令 启动文件代码详解栈空间的开辟堆空间的开辟中断向量表定义#xff08;简称#xff1a;向量表#xff09;复位程序对于weak的理解对于_main函数的分析 中断服务程序用户堆栈初始化 系统启动流程 STM32启动文件简介
STM32启动文件… 目录 STM32启动文件简介启动文件中的一些指令 启动文件代码详解栈空间的开辟堆空间的开辟中断向量表定义简称向量表复位程序对于weak的理解对于_main函数的分析 中断服务程序用户堆栈初始化 系统启动流程 STM32启动文件简介
STM32启动文件由ST官方提供在官方的固件包里。启动文件由汇编编写是系统上电复位后第一个执行的程序。
启动文件主要做了以下工作
初始化堆栈指针 SP _initial_sp初始化程序计数器指针 PC Reset_Handler设置堆和栈的大小初始化中断向量表配置外部SRAM作为数据存储器可选配置系统时钟通过调用SystemInit函数可选调用C库中的 _main 函数初始化用户堆栈最终调用 main 函数 ARM指针寄存器 —— 堆栈指针寄存器SP、程序计数器PC、连接寄存器LR 堆栈指针R13SP每一种异常模式都有其自己独立的R13它通常指向异常模式所专用的堆栈也就是说五种异常模式、非异常模式用户模式和系统模式都有各自独立的堆栈用不同的堆栈指针来索引。这样当ARM进入异常模式的时候程序就可以把一般通用寄存器压入堆栈返回时再出栈保证了各种模式下程序的状态的完整性
连接寄存器R14LR每种模式下R14都有自身版组它有两个特殊功能 保存子程序返回地址使用BL或BLX时跳转指令自动把返回地址放入R14中子程序通过把R14复制到PC来实现返回通常用下列指令之一 MOV PC, LR
BX LR通常子程序这样写保证了子程序中还可以调用子程序 stmfd sp!, {lr}
...
ldmfd sp!, {pc}当异常发生时异常模式的R14用来保存异常返回地址将R14如栈可以处理嵌套中断
程序计数器R15PCPC是有读写限制的当没有超过读取限制的时候读取的值是指令的地址加上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)R13R13R13_svcR13_abtR13_undR13_irqR13_fiq
为了更准确地描述堆栈根据“压栈”操作时堆栈指针的增减方向将堆栈区分为‘递增堆栈’SP 向大数值方向变化和‘递减堆栈’SP 向小数值方向变化又根据SP 指针指向的存储单元是否含有堆栈数据又将堆栈区分为‘满堆栈’SP 指向单元含有堆栈有效数据和‘空堆栈’SP 指向单元不含有堆栈有效数据。
这样两两组合共有四种堆栈方式——满递增、空递增、满递减和空递减。 ARM处理器的堆栈操作具有非常大的灵活性对这四种类型的堆栈都支持。 ARM处理器中的R13被用作SP。当不使用堆栈时R13 也可以用做通用数据寄存器。 栈的整体作用 保护现场传递参数临时变量保存在栈中 深入理解ARM三个寄存器 PC 代表程序计数器流水线使用三个阶段因此指令分为三个阶段执行
取指从存储器装载一条指令译码识别将要被执行的指令执行处理指令并将结果写回寄存器
R15PC总是指向 正在取指 的指令ARM指令是三级流水线取指、译指、执行是同时进行的现在PC指向的是正在取指的地址那么cpu正在译指的指令地址是PC-4假设在ARM状态 下一个指令占4个字节cpu正在执行的指令地址是PC-8也就是说PC所指向的地址和现在所执行的指令地址相差8。当突然发生中断的时候保存的是PC的地址这样你就知道了如果返回的时候返回PC那么中间就有一个指令没有执行所以用 SUB pc lr -lrq #4
启动文件中的一些指令
指令名称作用EQU给数字常量取一个符号名相当于C语言中的defineAREA汇编一个新的代码段或者数据段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 确定处理器的状态还要把跳转前的下条指令地址保存到 LRBX跳转到由寄存器/标号给出的地址不用返回B跳转到一个标号IF,ELSE,ENDIF汇编条件分支语句跟 C 语言的类似END到达文件的末尾文件结束
启动文件代码详解
下面我们以 STM32F103 的启动代码为例讲解版本是STM32Cube_FW_F1_V1.8.0 启动文件名称是startup_stm32f103xe.s。把启动代码分成几个功能段进行详细的讲解详 情如下
栈空间的开辟
Stack_Size EQU 0x00000400AREA STACK, NOINIT, READWRITE, ALIGN3
Stack_Mem SPACE Stack_Size
__initial_sp**EQU**宏定义的伪指令给数字常量取一个符号名类似与 C 中的 define。定义栈大小为 0x00000400 字节即 1024B1KB常量的符号是 Stack_Size。
AREA 汇编一个新的代码段或者数据段。段名为 STACK段名可以任意命名NOINIT 表示不初始化 READWRITE 表示可读可写ALIGN3表示按照 2^3 对齐即 8 字节对齐。
SPACE 分配内存指令分配大小为 Stack_Size 字节连续的存储单元给栈空间。
__initial_sp 紧挨着 SPACE 放置表示栈的结束地址栈是从高往低生长所以结束地址就是栈顶地址。
栈主要用于存放局部变量函数形参等属于编译器自动分配和释放的内存栈的大小不能超过内部 SRAM 的大小。如果工程的程序量比较大定义的局部变量比较多那么就需要在启动代码中修改栈的大小即修改 Stack_Size 的值。如果程序出现了莫名其妙的错误并进入了 HardFault 的时候你就要考虑下是不是栈空间不够大溢出了的问题。
堆空间的开辟
Heap_Size EQU 0x00000200AREA HEAP, NOINIT, READWRITE, ALIGN3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit堆空间开辟代码跟栈空间开辟代码是类似的了。这部分代码的意思就是开辟堆的大小为 0x00000200512 字节段名为 HEAP不初始化可读可写2^3 对齐8 字节对齐。__heap_base表示堆的起始地址__heap_limit 表示堆的结束地址。堆和栈的生长方向相反的堆是由低向高生长而栈是从高往低生长。
堆主要用于动态内存的分配像 malloc()、calloc()和 realloc()等函数申请的内存就在堆上面。堆中的内存一般由程序员分配和释放若程序员不释放程序结束时可能由操作系统回收。
PRESERVE8
THUMBPRESERVE8指示编译器按照 8 字节对齐。 THUMB指示编译器之后的指令为 THUMB 指令。
中断向量表定义简称向量表
中断向量表定义代码 AREA RESET, DATA, READONLYEXPORT __VectorsEXPORT __Vectors_EndEXPORT __Vectors_Size定义一个数据段名字为RESET, READONLY表示只读EXPORT表示声明一个标号具有全局属性可被外部的文件使用。这里是声明了__Vectors、__Vectors_End 和 __Vectors_Size 三个标号具有全局性可被外部的文件使用
当内核响应了一个发生的异常后对应的异常服务例程(ESR)就会执行。为了决定 ESR的入口地址 内核使用了向量表查表机制。向量表其实是一个 WORD32 位整数数组每个下标对应一种异常该下标元素的值则是该 ESR 的入口地址。向量表在地址空间中的位置是可以设置的通过 NVIC 中的一个重定位寄存器来指出向量表的地址。在复位后该寄存器的值为 0。因此在地址 0 即 FLASH 地址 0 处必须包含一张向量表用于初始时的异常分配。
位置优先级优先级类型名称说明地址---保留0x0000_0000-3固定Reset复位0x0000_0004-2固定NMI不可屏蔽中断RCC时钟安全系统CSS联接到NMI向量0x0000_0008-1固定硬件失效HardFault所有类型的失效0x0000_000C0可设置存储管理MemManage存储器管理0x0000_00101可设置总线错误BusFault预取值失败存储器访问失败0x0000_00142可设置错误应用UsageFault未定义的指令或非法状态0x0000_0018---保留0x0000_001C ~ 0x0000_002B3可设置SVCall通过SWI指令的系统服务调用0x0000_002C4可设置调式监控DebugMonitor调试监控器0x0000_0030---保留0x0000_00345可设置PendSV可挂起的系统服务0x0000_00386可设置SysTick系统嘀嗒定时器0x0000_003C07可设置WWDG窗口定时器中断0x0000_004018可设置PVD连到EXTI的电源电压检测(PVD)中断0x0000_004429可设置TAMPER侵入检测中断0x0000_0048310可设置RTC实时时钟RTC全局中断0x0000_004C411可设置FLASH内存全局中断0x0000_0050
中间部分省略详情请参考《STM32中文参考手册》第九章 中断和事件 中断和异常向量
5663可设置DMA2通道1DMA2通道1全局中断0x0000_01205764可设置DMA2通道2DMA2通道2全局中断0x0000_01245865可设置DMA2通道3DMA2通道3全局中断0x0000_01285966可设置DMA2通道4_5DMA2通道4和DMA2通道5全局中断0x0000_012C
举个例子如果发生了异常 SVCall则 NVIC 会计算出偏移移量是 11x40x2C然后从那里取出服务例程的入口地址并跳入。要注意的是这里有个另类地址 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指令 ALIGNALIGN 表示对指令或者数据的存放地址进行对齐一般需要跟一个立即数缺省表示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 取出第一条指令执行复位中断服务程序的方式启动即固定了复位后的起始地址为 0x00000000PC 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 的数值是奇数即 LSB1表明是在Thumb 状态下执行。倘若写了 0则视为转入 ARM 模式CM3 将产生一个 fault 异常。
正因为 上 述 原 因 图 3.3 中使用 0x080001CD 来表达地址 0x080001CC 。 当0x080001CD 处的指令得到执行后就正式开始了程序的执行即去到 C 的世界。所以在此之前初始化 MSP 是必需的因为可能第 1 条指令还没执行就会被 NMI 或是其它 fault 打断。MSP 初始化好后就已经为它们的服务例程准备好了堆栈。