这本权威指南只是对应某一种具体的处理器内核,深入一种处理器内核对于我们理解整个ARM架构大有帮助
书籍资源在下面的链接
Arm_Cortex-M3权威指南
第2章 Cortex-M3概览
简介
Cortex-M3是一个32位处理器内核。内部的数据路径是32位,寄存器是32位,存储器接口也是32位。并且CM3拥有独立的指令总线和数据总线,也就是取指与数据访问是独立开来的


R0-R12都是32位通用寄存器,用于数据操作,但是注意:绝大多数 16 位 Thumb 指令只能访问 R0‐R7,而 32 位 Thumb‐2 指令可以访问所有寄存器
R13是两个堆栈指针,MSP是主堆栈指针,用于操作系统内核以及异常处理例程(包括中断服务例程) ;PSP是进程堆栈指针,由用户的应用程序代码使用。这里我们要注意这两个堆栈指针任一时刻只能使用其中一个,并且堆栈指针的最低两位永远是0,这意味着堆栈总是4字节对齐的
R14是连接寄存器,作用是当呼叫一个子程序时,由R14存储返回地址。为什么要这样设计呢,因为ARM访问内存的次数往往要3个以上指令周期。把返回地址直接存储在寄存器中,这样已经足够很多只有1级子程序调用的代码无需访问内存,从而提高了程序调用的效率
R15是程序计数寄存器,它指向当前的程序地址,如果修改它的值,就能改变程序的执行流
还有一些特殊功能的寄存器


操作模式和特权级别
Cortex-M3处理器支持两种处理器的操作模式,还支持两级特权操作。
两种处理器操作模式分为处理者模式和线程模式,两种特权级别分别为特权级和用户级

在特权级下,程序可以访问所有范围的存储器(如果有MPU,还是不能访问MPU禁止的区域),并且可以执行所有指令
事实上,从用户级到特权级别的唯一途径就是异常:如果在重新执行过程中触发了一个异常,处理器总是先切换入特权级,并且在异常服务例程执行完毕退出时,返回先前的状态(也可以手工指定返回的状态)

通过引入特权级和用户级,可以提高系统安全性,例如在操作系统开启了一个用户程序后,通常会让它在用户级下执行,从而使系统不会因某个程序的崩溃或恶意破坏而受损
内建的嵌套向量中断控制器
嵌套向量中断控制器NVIC提供如下的功能:
- 可嵌套中断支持。系统的异常可以被赋予不同的优先级。当前优先级被存储在xPSR的专用字段中。当一个异常发生时,硬件会自动比较该异常的优先级是否比当前的异常优先级更高,如果高的话立刻抢占
- 向量中断支持。当开始响应一个中断后,CM3会自动定位一张向量表,并且根据中断号从表中找出ISR的入口地址,然后跳过去执行
- 动态优先级调整支持 。软件可以在运行时期更改中断的优先级,如果在某 ISR 中修改了自己所对应中断的优先级,而且这个中断又有新的实例处于悬起中(pending),也不会自己打断自己,从而没有重入(reentry)。因为如果能打断自己的话可能会有一种极端情况让栈爆掉,如果在ISR中提高了A中断的优先级,然后其它任务触发了A中断那么这时A中断会被打断执行一次更高优先级的A中断,然后中断里再改优先级,改的更高,如果又有任务触发了这个A中断,那么又会抢占,这样下去栈里全是A中断的现场,直接爆了,所以我们不允许自己打断自己从而没有重入
- 中断延迟大大缩短。因为Cortex-M3为了缩短中断延迟,引入了好几个新特性,这个后续再讲
- 中断可屏蔽。既可以屏蔽优先级低于某个阈值的中断/异常,也可以全体封杀。这是为了让时间关键的任务能在死线到来前完成,而不被干扰
存储器映射

这里我就详细讲一下中断的流程,可能很多人不懂中断向量表存在哪,里面是什么,具体的中断处理函数存在哪,以及怎么触发中断的,中断相关寄存器的干啥的
中断向量表里面存的是中断处理函数的地址,也就是说中断向量表只存了地址,然后找到对应的中断跳到具体的处理函数去执行,中断向量表存在代码区,一般是0x00000000地址开始
具体的中断处理函数也是存在代码区,因为中断向量表里面存了处理函数的地址,于是找到具体中断就可以通过地址跳到中断处理函数去执行
中断的触发一般是由硬件触发,硬件检测到某种情况然后可能就会触发中断,比如在stm32中的gpio中断,我们可以配置下降沿触发,当电平为下降沿时GPIO会自动触发中断,然后CPU这个硬件会自动去查这个中断是第几号,并且计算地址,然后去这个地址读出函数地址,然后PC指针直接跳过去
中断相关寄存器是配置某种中断是否打开,因为一开始所有外设的中断都是关着的,屏蔽的,我们配置寄存器本质就是告诉硬件允许这个中断触发
中断处理流程是这样的:代码跑着 → 中断硬件触发 → CPU 自动暂停 → 自动去 0x00000000 查表 → 自动跳到中断函数 → 执行完自动回来。
哪如何访问外设控制外设,比如ADC,GPIO这种外设呢。通过把片上外设的寄存器映射到外设区,就可以简单地以访问内存的方式来访问这些外设的寄存器,从而控制外设的工作
总线接口
Cortex-M3内部有若干个总线接口,以使CM3能同时取址和访存,分别是:
- 指令存储区总线(两条)
- 系统总线
- 私有外设总线
指令存储区的两条总线分别是 I‐Code 总线和 D‐Code 总线,前者用于取指,后者用于查表等操作,它们按最佳执行速度进行优化
系统总线用于访问内存和外设,覆盖的区域包括 SRAM,片上外设,片外 RAM,片外扩展设备,
以及系统级存储区的部分空间。
私有外设总线负责一部分私有外设的访问,主要就是访问调试组件。它们也在系统级存储区。
存储器保护单元(MPU)
使用MPU就是进一步增加系统的安全性。Cortex‐M3 有一个可选的存储器保护单元。配上它之后,就可以对特权级访问和用户级访问分别施加不同的访问限制。当检测到犯规(violated)时,MPU 就会产生一个 fault 异常,可以由 fault异常的服务例程来分析该错误,并且在可能时改正它
最常见的用法的就是由操作系统使用MPU,以使特权级代码的数据,包括操作系统本身的数据不被其它用户程序弄坏。它可以把某些内存区域设置成只读,从而避免了那里的内容意外被更高,还可以在多任务系统中把不同任务之间的数据区隔离
指令集
在过去,做ARM开发必须处理好两个状态:32位的ARM状态和16位的Thumb状态。先讲一下为什么使用这两种不同的指令集:因为当年的硬件不像现在性能很强,而是有限制的,Flash又小又贵,如果全用arm指令,代码体积大,Flash不够用,芯片成本会很高。如果全用Thumb指令,性能又不够,跑不动复杂算法,中断响应慢。所以才有了混合使用这种方法,主程序用Thumb省空间,关键代码用ARM提性能
但是使用两种指令集的缺点也很明显,会有额外开销,并且ARM代码和Thumb代码需要以不同的方式编译,这也增加了软件开发管理的复杂度
事实上,Cortex‐M3 内核干脆都不支持 ARM 指令,中断也在 Thumb 态下处
理(以前的 ARM 总是在 ARM 状态下处理所有的中断和异常)。这里我要说明的是Thumb-2指令集也有32位指令,但是这个32位指令不是原原本本的AMR指令,我们可以将其对Thumb16位指令的扩展,所以这个芯片从硬件上就砍掉了ARM指令集,所以它才不需要切换状态,因为本身就是Thumb指令集
中断和异常
ARMv7‐M 开创了一个全新的异常模型,CM3 采用了它。这两种模式的机制是下面这样
传统 ARM(ARM7/ARM9):用 7 种模式 + IRQ/FIQ 双中断 + 软件手动压栈 + 强制 ARM 状态的复杂模型,适合高性能处理器
Cortex-M3(ARMv7-M):彻底推翻传统,用 2 种模式 + 统一 NVIC + 硬件自动压栈 + 全程 Thumb 状态的极简模型,专门为 MCU 设计,中断响应更快、更稳定、更安全,彻底解决了传统模型的所有痛点

虽然 CM3 是支持 240 个外中断的,但具体使用了多少个是由芯片生产商决定。CM3 还有一个NMI(不可屏蔽中断)输入脚。当它被置为有效(assert)时,NMI 服务例程会无条件地执行。
调试支持
这个地方其实我没怎么懂,对这方面几乎没有了解,所以先不说了,等到以后我自己学东西拓展到这里时再补上
Cortex-M3 的品性简评
这里我直接粘贴原文了
高性能
- 许多指令都是单周期的------包括乘法相关指令。并且从整体性能上,Cortex‐M3 比得过绝大多数其它的架构。
- 指令总线和数据总线被分开,取值和访内可以并行不悖
- Thumb‐2 的到来告别了状态切换的旧世代,再也不需要花时间来切换于 32 位 ARM 状态和16 位 Thumb 状态之间了。这简化了软件开发和代码维护,使产品面市更快。
- Thumb‐2 指令集为编程带来了更多的灵活性。许多数据操作现在能用更短的代码搞定,这意味着 Cortex‐M3 的代码密度更高,也就对存储器的需求更少。
- 取指都按 32 位处理。同一周期最多可以取出两条指令,留下了更多的带宽给数据传输。 Cortex‐M3 的设计允许单片机高频运行(现代半导体制造技术能保证 100MHz 以上的速度)。即使在相同的速度下运行,CM3 的每指令周期数(CPI)也更低,于是同样的 MHz 下可以做更多的工作;另一方面,也使同一个应用在 CM3 上需要更低的主频。
先进的中断处理功能
- 内建的嵌套向量中断控制器支持多达 240 条外部中断输入。向量化的中断功能剧烈地缩短了中断延迟,因为不再需要软件去判断中断源。中断的嵌套也是在硬件水平上实现的,不需要软件代码来实现。
- Cortex‐M3 在进入异常服务例程时,自动压栈了 R0‐R3, R12, LR, PSR 和 PC,并且在返回时自动弹出它们,这多清爽!既加速了中断的响应,也再不需要汇编语言代码了(第 8 章有详述)。
- NVIC 支持对每一路中断设置不同的优先级,使得中断管理极富弹性。最粗线条的实现也至少要支持 8 级优先级,而且还能动态地被修改。
- 优化中断响应还有两招,它们分别是"咬尾中断机制"和"晚到中断机制"。
- 有些需要较多周期才能执行完的指令,是可以被中断-继续的------就好比它们是一串指令一样。这些指令包括加载多个寄存器(LDM),存储多个寄存器(STM),多个寄存器参与的 PUSH,以及多个寄存器参与的 POP。
- 除非系统被彻底地锁定,NMI(不可屏蔽中断)会在收到请求的第一时间予以响应。对于很多安全‐关键(safety‐critical)的应用,NMI 都是必不可少的(如化学反应即将失控时的紧急停机)。
低功耗
- Cortex‐M3 需要的逻辑门数少,所以先天就适合低功耗要求的应用(功率低于 0.19mW/MHz)
- 在内核水平上支持节能模式(SLEEPING 和 SLEEPDEEP 位)。通过使用"等待中断指令(WFI)"
和"等待事件指令(WFE)",内核可以进入睡眠模式,并且以不同的方式唤醒。另外,模块的时钟是尽可能地分开供应的,所以在睡眠时可以把 CM3 的大多数"官能团"给停掉。 - CM3 的设计是全静态的、同步的、可综合的。任何低功耗的或是标准的半导体工艺均可放心饮用。
系统特性
- 系统支持"位寻址带"操作(8051 位寻址机制的"威力大幅加强版"),字节不变的大端模式,并且支持非对齐的数据访问。
- 拥有先进的 fault 处理机制,支持多种类型的异常和 faults,使故障诊断更容易。
- 通过引入 banked 堆栈指针机制,把系统程序使用的堆栈和用户程序使用的堆栈划清界线。如果再配上可选的 MPU,处理器就能彻底满足对软件健壮性和可靠性有严格要求的应用。
调试支持
- 在支持传统的 JTAG 基础上,还支持更新更好的串行线调试接口。
- 基于 CoreSight 调试解决方案,使得处理器哪怕是在运行时,也能访问处理器状态和存储器内容。
- 内建了对多达 6 个断点和 4 个数据观察点的支持。
- 可以选配一个 ETM,用于指令跟踪。数据的跟踪可以使用 DWT
- 在调试方面还加入了以下的新特性,包括 fault 状态寄存器,新的 fault 异常,以及闪存修补 (patch)
操作,使得调试大幅简化。 - 可选 ITM 模块,测试代码可以通过它输出调试信息,而且"拎包即可入住"般地方便使用。
第3章 Cortex-M3基础
寄存器组
R0-R7也被称为低组寄存器。所有指令都能访问它们。它们的字长全是32位,复位后的初始值是不可预料的
R8-R12 也被称为高组寄存器,这是因为只有很少的16位Thumb指令能访问它们,32位的指令则不受限制
R13是堆栈指针。当引用R13是,我们引用到的是当前正在使用的那一个,另一个必须用特殊的指令来访问。这两个堆栈指针分别是主堆栈指针,进程堆栈指针。需要注意的是,并不是没有应用都必须用齐两个堆栈指针。简单的应用程序只使用MSP就够了。堆栈指针用于访问堆栈,并且PUSH指令和POP指令默认使用SP

寄存器的PUSH和POP操作永远都是4字对齐的-也就是说他们的地址必须是0x4,0x8,0xc。这样一来,R13的最低两位被硬件连接到0,并且总是读出0
R14是连接寄存器(LR)。LR用于在调用子程序时存储返回地址。例如我们在使用BL指令时,就自动填充LR的值。尽管PC的LSB总是0,LR的LSB却是可读可写的。这是历史遗留的产物
R15是程序计数器,在汇编代码中我们也可以使用名字PC来访问它。如果向PC写数据,那么CPU会立刻跳到这个地址去执行,但不会吧当前返回地址存到LR寄存器。普通函数调用BL指令会自动把下一条指令地址存到LR,方便返回。直接写PC就是只跳转,不存LR,跳过去就回不来了
因为CM3指令半字对齐,PC的LSB读回总是0。硬件保证指令不会存在奇数地址,所以读PC时LSB固定为0
当我们要跳到某个地址执行,给PC写的地址必须是奇数,用来告诉CPU,我要在Thumb指令集下执行,因为CM3不支持ARM指令集。实际执行时,CPU会自动把LSB清0,拿到真实的偶数地址(比如我们写0x08000005,实际跳去0x08000004),既满足了标识要求,由保证了指令对齐。如果我们给CP写了一个偶数地址也就是LSB=0,CPU会认为我们要切换到ARM模式,但CM3根本不支持ARM模式,直接触发硬件fault异常,系统直接死机
特殊功能寄存器组
Cortex‐M3 中的特殊功能寄存器包括:
- 程序状态寄存器组(PSRs 或曰 xPSR)
- 中断屏蔽寄存器组(PRIMASK, FAULTMASK,以及 BASEPRI)
- 控制寄存器(CONTROL)
这几个寄存器只能被专用的MSR和MRS指令访问,而且它们也没有存储地址。
书中介绍这几个寄存器的内容就是一些定义的东西,就是告诉我们每个位的作用是啥,我就直接截图书上的内容了



操作模式
当处理器处在线程状态下时,既可以使用特权级,也可以使用用户级;另一方面,handler模式总是特权级的。在复位后,处理器进入线程模式+特权级
在线程模式+用户级下,对系统控制空间(SCS)的访问将被阻止,并且还禁止使用MSR访问刚才讲到的特殊功能寄存器。这里我提一下,系统控制空间其实包括了一堆的寄存器,这些寄存器的地址是映射在4GB地址空间里的,这些寄存器只包括了系统最关键最基本的东西,比如NVIC中断,SysTick系统时钟,至于为什么有些寄存器(比如我们的通用寄存器)则不是映射的而是直接引用的,这是因为SCS里的寄存器数量上百个,功能复杂,还需要和CPU,其它外设协同工作,根本不可能用CPU的方式实现
在特权级下的代码可以通过置位 CONTROL[0]来进入用户级。而不管是任何原因产生了任何异常,处理器都将以特权级来运行其服务例程,异常返回后将回到产生异常之前的特权级。用户级下的代码不能再试图修改 CONTROL[0]来回到特权级。它必须通过一个异常 handler,由那个异常 handler 来修改 CONTROL[0],才能在返回到线程模式后拿到特权级

异常和中断
CM3支持大量异常,包括11个系统异常和最多240个外部中断--简称IRQ。具体使用了这240个中断源中的多个个,则由芯片制造商决定。由外设产生的中断信号,除了SysTick的之外,全都连接到NVIC的中断输入信号线
作为中断功能的强化,NVIC 还有一条 NMI 输入信号线。NMI 究竟被拿去做什么,还要视处理器的设计而定。

向量表s
当一个发生的异常被 CM3 内核接受,对应的异常 handler 就会执行。为了决定 handler 的入口地址,CM3 使用了"向量表查表机制"。这里使用一张向量表。向量表其实是一个 WORD(32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该异常 handler 的入口地址。向量表的存储位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 处必须包含一张向量表,用于初始时的异常分配。

栈内存操作
这里我就直接复制书上的内容了,因为这个没有特别复杂,我们在学算法和数据结构时也接触过
笼统地讲,堆栈操作就是对内存的读写操作,但是其地址由 SP 给出。寄存器的数据通过 PUSH 操作存入堆栈,以后用 POP 操作从堆栈中取回。在 PUSH 与 POP 的操作中,SP 的值会按堆栈的使用法则自动调整,以保证后续的 PUSH 不会破坏先前 PUSH 进的内容。
堆栈的功能就是把寄存器的数据放入内存,以便将来能恢复之------当一个任务或一段子程序执行完毕后恢复。正常情况下,PUSH 与 POP 必须成对使用,而且参与的寄存器,不论是身份还是先后顺序都必须完全一致。当 PUSH/POP 指令执行时,SP 指针的值也根着自减自增。

Cortex-M3 堆栈的实现
Cortex‐M3 使用的是"向下生长的满栈"模型。堆栈指针 SP 指向最后一个被压入堆栈的 32位数值。在下一次压栈时,SP 先自减 4,再存入新的数值
在进入 ISR 时,CM3 会自动把一些寄存器压栈,这里使用的是进入 ISR 之前使用的 SP指针(MSP 或者是 PSP)。离开 ISR 后,只要 ISR 没有更改过 CONTROL[1],就依然使用先前的 SP 指针来执行出栈操作。
复位序列
在离开复位状态后,CM3 做的第一件事就是读取下列两个 32 位整数的值:
从地址 0x0000,0000 处取出 MSP 的初始值。
从地址 0x0000,0004 处取出 PC 的初始值------这个值是复位向量,LSB 必须是 1。然后从这个值所对应的地址处取指。

请注意,这与传统的 ARM 架构不同------其实也和绝大多数的其它单片机不同。传统的ARM 架构总是从 0 地址开始执行第一条指令。它们的 0 地址处总是一条跳转指令。在 CM3中,0 地址处提供 MSP 的初始值,然后就是向量表(向量表在以后还可以被移至其它位置)。
向量表中的数值是 32 位的地址,而不是跳转指令。向量表的第一个条目指向复位后应执行的第一条指令。

因为 CM3 使用的是向下生长的满栈,所以 MSP 的初始值必须是堆栈内存的末地址加 1。举例来说,如果你的堆栈区域在 0x20007C00‐0x20007FFF 之间,那么 MSP 的初始值就必须是0x20008000。
向量表跟随在 MSP 的初始值之后------也就是第 2 个表目。要注意因为 CM3 是在 Thumb态下执行,所以向量表中的每个数值都必须把 LSB 置 1(也就是奇数)。正是因为这个原因,图 3.18 中使用 0x101 来表达地址 0x100。当 0x100 处的指令得到执行后,就正式开始了程序的执行。在此之前初始化 MSP 是必需的,因为可能第 1 条指令还没执行就会被 NMI 或是其它 fault 打断。MSP 初始化好后就已经为它们的服务例程准备好了堆栈。
第4章 指令集
其实这一张对于我们了解架构不是特别重要,因为这部分更像是语法部分,但是这部分语法也不是通用的(就像C语言那样),因为不同的汇编器会有一些不同的地方。这里我就讲一下我认为重要的

EQU 指示字,这是方便我们定义常数,然后再代码中使用它们,例如
NVIC_IRQ_SETEN0 EQU 0xE000E100
NVIC_IRQ0_ENABLE EQU 0x1
有的情况需要我们手工汇编,查出该指令的确切二进制机器码,然后使用DCI编译器指示字,比如这样DCI 0xBE00。
我举个例子
NOP ; 执行空操作
NOP 的机器码是 0x46C0
所以你可以写成
DCI 0x46C0 ; 效果 = 执行 NOP
类似地,我们还可以使用DCB来定义一串字节常数--允许以字符串的形式的表达,在stm32的启动文件中我们可以看到向量表里面就用了DCB
__Vectors
DCD __initial_sp ; 栈顶地址
DCD Reset_Handler ; 复位函数地址
DCD NMI_Handler ; NMI 地址
DCD HardFault_Handler
...
在这里__Vectors就相当于一个数组,这些DCD定义的字符串相当于数组的元素
__Vectors
DCD __initial_sp ; 地址 = __Vectors + 0
DCD Reset_Handler ; 地址 = __Vectors + 4
DCD NMI_Handler ; 地址 = __Vectors + 8
当然我们还可以这样写

那么这里是MY_NUMBER对应一个元素,就相当于我们让a=1一样,而不是前面那样像数组一样
这一章大部分都是讲了一些语法和使用注意事项,对于我们了解整个架构帮助不是很大,所以先跳过
存储系统功能概览
CM3 的存储器系统与从传统 ARM 架构的相比,已经脱胎换骨了:
第一, 它的存储器映射是预定义的,并且还规定好了哪个位置使用哪条总线。
第二, CM3 的存储器系统支持所谓的"位带"(bit‐band)操作。通过它,实现了对单一比特的原子操作。位带操作仅适用于一些特殊的存储器区域中,见本章论述。
第三, CM3 的存储器系统支持非对齐访问和互斥访问。这两个特性是直到了 v7M 时才出来的。
最后,CM3 的存储器系统支持 both 小端配置和大端配置。