缩写含义
缩略代号 | 含义 |
ADK | AMBA设计套件 |
AHB AHB-AP AMBA | 先进高性能总线AHB访问端口先进单片机总线架构 |
APB | 先进外设总线 |
ARM ARM | ARM架构参考手册 |
ASIC | 行业领域专用集成电路 |
ATB | 先进跟踪总线 |
BE8 | 字节不变式大端模式 |
CPI | 每条指令的周期数 |
CPU | 中央处理单元 |
DAP | 调试访问端口 |
DSP | 数字信号处理器/数字信号处理 |
DWT | 数据观察点及跟踪 |
ETM | 嵌入式跟踪宏单元 |
FPB | 闪存地址重载及断点 |
FSR | Fault状态寄存器 |
HTM | CoreSight AHB跟踪宏单元 |
ICE | 在线仿真器 |
IDE | 集成开发环境 |
IRQ | 中断请求(通常是指外部中断的请求) |
ISA | 指令系统架构 |
ISR | 中断服务例程 |
ITM | 仪器化跟踪宏单元 |
JTAG JTAG-DP LR | 连结点测试行动组(一个关于测试和调试接口的标准)JTAG调试端口连接寄存器 |
LSB | 最低有效位 |
LSU | 加载/存储单元 |
MCU | 微控制器单元(俗称单片机) |
MMU | 存储器管理单元 |
MPU | 存储器保护单元 |
MSB | 最高有效位 |
MSP | 主堆栈指针 |
NMI | 不可屏蔽中断 |
NVIC | 嵌套向量中断控制器 |
OS | 操作系统 |
PC | 程序计数器 |
PSP | 进程堆栈指针 |
PPB | 私有外设总线 |
第一章 初识CM3
1.1 初识CM3
CM3的招牌功夫包括:
• 性能强劲:在相同的主频下能做处理更多的任务。(低CPI)
• 功耗低: 延长了电池的寿命——这简直就是便携式设备的命门(如无线网络应用)。
• 实时性好:采用了很前卫甚至革命性的设计理念,使它能极速地响应中断,而且响应中断所
需的周期数是确定的。
• 代码密度得到很大改善:一方面力挺大型应用程序,另一方面为低成本设计而省吃俭用。
• 使用更方便:现在从8位/16位处理器转到32位处理器之风刮得越来越猛,更简单的编程模型
和更透彻的调试系统,为与时俱进的人们大大减负。
• 低成本的整体解决方案: 让32位系统比和8位/16位的还便宜,低端的Cortex-M3单片机甚至还
卖不到1美元。
• 遍地开花的优秀开发工具:免费的,便宜的,全能的,要什么有什么。
1.1.1从 Cortex-M3 处理器内核到基于 Cortex-M3 的 MCU
1.1.2 ARM 及 ARM 架构的背景
ARM在1990年成立,当初的名字是“Advanced RISC Machines Ltd.,”,当时它是三家公司的合资——它们分别是苹果电脑, Acorn电脑公司,以及VLSI技术(公司)。
除了设计处理器, ARM也设计系统级IP和软件IP。为了挺它们, ARM开发了许多配套的基础开发工具、硬件以及软件产品。使用这些工具, 合作伙伴可以更加舒心地开发他们自己的产品。
1.2 ARM 的各种架构版本:
款式A(ARMv7-A):需要运行复杂应用程序的“应用处理器”(这里的“应用”尤指大型应用程序,像办公软件,导航软件,网页浏览器等。这些软件的使用习惯和开发模式都很像PC上的软件, 但是基本上没有实时要求)。 支持大型嵌入式操作系统,比如Symbian(诺基亚智能手机用), Linux, 以及微软的Windows CE和智能手机操作系统Windows Mobile。这些应用需要劲爆的处理性能, 并且需要硬件MMU实现的完整而强大的虚拟内存机制, 还基本上会配有Java支持, 有时还要求一个安全程序执行环境。典型的产品包括高端手机和手持仪器,电子钱包以及金融事务处理机。
款式R(ARMv7-R):硬实时且高性能的处理器。标的是高端实时市场(通用处理器能否胜任实时系统的控制, 常遭受质疑,并且在这方面的争论从没停止过。从定义的角度讲, “实时”就是指系统必须在给定的死线(deadline,亦称作“最后期限” )内做出响应。在一个以ARM处理器为核心的系统中,决定能否达到“实时”这个目标的,有很多因素,包括是否使用“实时操作系统”,中断延迟,存储器延时,以及当时处理器是否在运行更高优先级的中断服务例程。
)。那些高级的玩意,像高档轿车的组件,大型发电机控制器, 机器手臂控制器等, 它们使用的处理器不但要很好很强大,还要极其可靠,对事件的反应也要极其敏捷。
款式M(ARMv7-M):认准了旧世代单片机的应用而量身定制。在这些应用中,尤其是对于实时控制系统,低成本、低功耗、极速中断反应以及高处理效率,都是至关重要的。Cortex系列是v7架构的第一次亮相,其中Cortex-M3就是按款式M设计的。
1.2.1处理器命名法
处理器名字 | 架构版本号 | 存储器管理特性 | 其它特性 |
ARM7TDMI | v4T | ||
ARM7TDMI-S | v4T | ||
ARM7EJ-S | v5E | DSP,Jazelle[译注3] | |
ARM920T | v4T | MMU | |
ARM922T | v4T | MMU | |
ARM926EJ-S | v5E | MMU | DSP,Jazelle |
ARM946E-S | v5E | MPU | DSP |
ARM966E-S | v5E | DSP | |
ARM968E-S | v5E | DMA,DSP | |
ARM966HS | v5E | MPU(可选) | DSP |
ARM1020E | v5E | MMU | DSP |
ARM1022E | v5E | MMU | DSP |
ARM1026EJ-S | v5E | MMU 或 MPU[译注2] | DSP, Jazelle |
ARM1136J(F)-S | v6 | MMU | DSP, Jazelle |
ARM1176JZ(F)-S | v6 | MMU+TrustZone | DSP, Jazelle |
ARM11 MPCore | v6 | MMU+多处理器缓存支持 | DSP |
ARM1156T2(F)-S | v6 | MPU | DSP |
Cortex-M3 | v7-M | MPU(可选) | NVIC |
Cortex-R4 | v7-R | MPU | DSP |
Cortex-R4F | v7-R | MPU | DSP+浮点运算 |
Cortex-A8 | v7-A | MMU+TrustZone | DSP, Jazelle |
1.3 指令集的发展
由于历史原因(从ARM7TDMI开始), ARM处理器一直支持两种形式上相对独立的指令集, 它们分别是:
- 32位的ARM指令集:对应处理器状态: ARM状态
- 16位的Thumb指令集:对应处理器状态: Thumb状态
可见,这两种指令集也对应了两种处理器执行状态。在程序的执行过程中,处理器可以动态地在两种执行状态之中切换。 实际上, Thumb指令集在功能上是ARM指令集的一个子集,但它能带来更高的代码密度,给目标代码减肥。这对于要勒紧裤腰带的应用还是很经济的。
1.4 Thumb-2 指令集体系体系结构( ISA)
Thumb-2强大,易用,轻佻,高效。 Thumb-2是16位Thumb指令集的一个超集,在Thumb-2中, 16位指令首次与32位指令并存,结果在Thumb状态下可以做的事情一下子丰富了许多,同样工作需要的指令周期数也明显下降。
Thumb-2指令集与Thumb指令集的关系
Cortex-M3拒绝了32位ARM指令集,把自己的处理能力以身相许般地全托给Thumb-2指令集。 这可能有些令人意外,但事实上这却见证了Cortex-M3的用情专一: 在内核水平上,就已经为适应单片机和小内存器件而抉择、取舍过了。但她没有嫁错郎, 因为Thumb-2完全胜任在这个领域挑大梁。不过, 这也意味着Cortex-M3作为新生代处理器,不是向后兼容的。因此,为ARM7写的ARM汇编语言程序不能直接移植到CM3上来。不过, CM3支持绝大多数传统的Thumb指令,因此用Thumb指令写的汇编程序就从善如流了。
1.5 Cortex-M3 应用场景
高性能+高代码密度+小硅片面积, 3璧合一,使得CM3大面积地成为理想的处理平台:低成本单片机: CM3与生俱来就适合做单片机,甚至简单到用于做玩具和小电器的单片机,都能使用CM3作为内核。这里本是8位机和16位机统治最牢固的腹地, 但是CM3更便宜,更高性能,更易使用,所以值得开发者们转到这个新生的ARM32位系统中来,哪怕花点时间重新学习
- 汽车电子: CM3也是汽车电子的好俅。 CM3同时拥有非常高的性能和极低的中断延迟,打入实时领域的大门。 CM3处理器能支持多达240个外部中断, 内建了嵌套向量中断控制器,还可以选择配上一个存储器保护单元(MPU)。所有这些,使它用于高集成度低成本的汽车应用最合适不过了。
- 数据通信: CM3的低成本+高效率,再加上Thumb-2的强大位操作指令s,使CM3非常理想地适合于很多数据通信应用,尤其是无线数传和Ad-Hoc网络, 如ZigBee和蓝牙等。
- 工业控制:在工控场合,关键的要素在于简洁、快速响应以及可靠。再一次地, CM3处理器的中断处理能力,低中断延迟, 强化的故障处理能力(fault-handing,以后fault就不再译成中文了——译注) ,足以让它能昂首挺胸地踏入这片热土。
- 消费类产品:以往,在许多消费产品中,都必须使用一块甚至好几块高性能的微处理器。你别看CM3只是个小处理器,它的高性能和MPU机制可是足以让复杂的软件跑起来的,同时提供健壮的存储器保护。
目前在市场上已经有了好多基于Cortex-M3内核的处理器产品,最便宜的还不到1美元,让ARM终于比很多8位机还便宜了。
第二章 CM3简介
2.1 简介
Cortex-M3 是一个 32 位处理器内核。内部的数据路径是 32 位的,寄存器是 32 位的,存储器接口也是 32 位的。 CM3 采用了哈佛结构,拥有独立的指令总线和数据总线,可以让取指与数据访问并行不悖。这样一来数据访问不再占用指令总线,从而提升了性能。为实现这个特性, CM3 内部含有好几条总线接口,每条都为自己的应用场合优化过,并且它们可以并行工作。但是另一方面,指令总线和数据总线共享同一个存储器空间(一个统一的存储器系统)。换句话说,不是因为有两条总线,可寻址空间就变成 8GB 了。
比较复杂的应用可能需要更多的存储系统功能,为此 CM3 提供一个可选的 MPU,而且在需要的情况下也可以使用外部的 cache。另外在 CM3 中, Both 小端模式和大端模式都是支持的。
CM3 内部还附赠了好多调试组件,用于在硬件水平上支持调试操作,如指令断点,数据观察点等。另外,为支持更高级的调试,还有其它可选组件,包括指令跟踪和多种类型的调试接口。
2.2 寄存器组
Cortex-M3 处理器拥有 R0-R15 的寄存器组。其中 R13 作为堆栈指针 SP。 SP 有两个,但在同一时刻只能有一个可以看到,这也就是所谓的“banked”寄存器。
2.2.1 R0-R12:通用寄存器
R0-R12 都是 32 位通用寄存器,用于数据操作。但是注意:绝大多数 16 位 Thumb 指令只能访问 R0-R7,而 32 位 Thumb-2 指令可以访问所有寄存器。
2.2.2 Banked R13: 两个堆栈指针
Cortex-M3 拥有两个堆栈指针,然而它们是 banked,因此任一时刻只能使用其中的一个。
- 主堆栈指针(MSP):复位后缺省使用的堆栈指针,用于操作系统内核以及异常处理例程(包括中断服务例程)
- 进程堆栈指针(PSP):由用户的应用程序代码使用。
堆栈指针的最低两位永远是 0,这意味着堆栈总是 4 字节对齐的。
在 ARM 编程领域中,凡是打断程序顺序执行的事件,都被称为异常(exception)。 除了外部中断外, 当有指令执行了“非法操作”, 或者访问被禁的内存区间, 因各种错误产生的 fault, 以及不可屏蔽中断发生时,都会打断程序的执行,这些情况统称为异常。在不严格的上下文中,异常与中断也可以混用。另外, 程序代码也可以主动请求进入异常状态的(常用于系统调用)。
2.2.3 R14:连接寄存器
当呼叫一个子程序时,由 R14 存储返回地址
ARM 为了减少访问内存的次数(访问内存的操作往往要 3 个以上指令周期,带 MMU 和cache 的就更加不确定了),把返回地址直接存储在寄存器中。这样足以使很多只有 1 级子程序调用的代码无需访问内存(堆栈内存),从而提高了子程序调用的效率。如果多于 1 级,则需要把前一级的 R14 值压到堆栈里。在 ARM上编程时,应尽量只使用寄存器保存中间结果,迫不得以时才访问内存。在 RISC 处理器中,为了强调访内操作越过了处理器的界线,并且带来了对性能的不利影响,给它取了一个专业的术语:溅出。
2.2.4 R15:程序计数寄存器
指向当前的程序地址。如果修改它的值,就能改变程序的执行流。
2.2.5 特殊功能寄存器
Cortex-M3 还在内核水平上搭载了若干特殊功能寄存器,包括程序状态字寄存器组(PSRs)
中断屏蔽寄存器组(PRIMASK, FAULTMASK, BASEPRI)控制寄存器(CONTROL)
Cortex-M3 中的特殊功能寄存器集合
寄存器 | 功能 |
xPSR | 记录 ALU 标志(0 标志,进位标志,负数标志,溢出标志),执行状态,以及当前正服务的中断号 |
PRIMASK | 除能所有的中断——当然了,不可屏蔽中断(NMI)才不甩它呢。 |
FAULTMASK | 除能所有的 fault——NMI 依然不受影响,而且被除能的 faults 会“上访”,见后续章节的叙述。 |
BASEPRI | 除能所有优先级不高于某个具体数值的中断。 |
CONTROL | 定义特权状态(见后续章节对特权的叙述),并且决定使用哪一个堆栈指针 |
2.3 操作模式和特权极别
Cortex-M3 处理器支持两种处理器的操作模式,还支持两级特权操作。两种操作模式分别为: 处理者模式(handler mode,以后不再把 handler 中译——译注)和线程模式(thread mode)。引入两个模式的本意,是用于区别普通应用程序的代码和异常服务例程的代码——包括中断服务例程的代码。Cortex-M3 的另一个侧面则是特权的分级——特权级和用户级。这可以提供一种存储器访问。引入两个模式的本意,是用于区别普通应用程序的代码和异常服务例程的代码——包括中断服务例程的代码。
Cortex-M3 的另一个侧面则是特权的分级——特权级和用户级。这可以提供一种存储器访问的保护机制,使得普通的用户程序代码不能意外地,甚至是恶意地执行涉及到要害的操作。处理器支持两种特权级,这也是一个基本的安全模型。
Cortex-M3 下的操作模式和特权级别
在 CM3 运行主应用程序时(线程模式),既可以使用特权级,也可以使用用户级;但是异常服务例程必须在特权级下执行。复位后,处理器默认进入线程模式,特权极访问。在特权级下,程序可以访问所有范围的存储器(如果有 MPU,还要在 MPU 规定的禁地之外),并且可以执行所有指令。
在特权级下的程序可以为所欲为,但也可能会把自己给玩进去——切换到用户级。一旦进入用户级,再想回来就得走“法律程序”了——用户级的程序不能简简单单地试图改写 CONTROL 寄存器就回到特权级,它必须先“申诉”:执行一条系统调用指令(SVC)。这会触发 SVC 异常,然后由异常服务例程(通常是操作系统的一部分)接管,如果批准了进入,则异常服务例程修改 CONTROL 寄存器,才能在用户级的线程模式下重新进入特权级。
事实上,从用户级到特权级的唯一途径就是异常:如果在程序执行过程中触发了一个异常,处理器总是先切换入特权级, 并且在异常服务例程执行完毕退出时,返回先前的状态(也可以手工指定返回的状态——译注)。
合法的操作模式转换图
通过引入特权级和用户级,就能够在硬件水平上限制某些不受信任的或者还没有调试好的程序,不让它们随便地配置涉及要害的寄存器,因而系统的可靠性得到了提高。进一步地,如果配了 MPU,它还可以作为特权机制的补充——保护关键的存储区域不被破坏,这些区域通常是操作系统的区域。
举例来说,操作系统的内核通常都在特权级下执行,所有没有被MPU禁掉的存储器都可以访问。在操作系统开启了一个用户程序后, 通常都会让它在用户级下执行,从而使系统不会因某个程序的崩溃或恶意破坏而受损。
2.4 内建的嵌套向量中断控制器
Cortex-M3 在内核水平上搭载了一颗中断控制器——嵌套向量中断控制器 NVIC(NestedVectored Interrupt Controller)。它与内核有很深的“亲密接触” ——与内核是紧耦合的。NVIC 提供如下的功能:
- 可嵌套中断支持
- 向量中断支持
- 动态优先级调整支持
- 中断延迟大大缩短
- 中断可屏蔽
2.4.1 可嵌套中断支持
可嵌套中断支持的作用范围很广,覆盖了所有的外部中断和绝大多数系统异常。外在表现是,这些异常都可以被赋予不同的优先级。当前优先级被存储在 xPSR 的专用字段中。当一个异常发生时,硬件会自动比较该异常的优先级是否比当前的异常优先级更高。如果发现来了更高优先级的异常,处理器就会中断当前的中断服务例程(或者是普通程序),而服务新来的异常——即立即抢占。
2.4.2 向量中断支持
当开始响应一个中断后, CM3 会自动定位一张向量表,并且根据中断号从表中找出 ISR 的入口地址,然后跳转过去执行。不需要像以前的 ARM 那样, 由软件来分辨到底是哪个中断发生了,也无需半导体厂商提供私有的中断控制器来完成这种工作。这么一来,中断延迟时间大为缩短。
2.4.3 动态优先级调整支持
软件可以在运行时期更改中断的优先级。如果在某ISR中修改了自己所对应中断的优先级,而且这个中断又有新的实例处于悬起中(pending),也不会自己打断自己,从而没有重入风险。(reentry)。(所谓的重入,就是指某段子程序还没有执行完,就因为中断或者是多任务操作系统的调度原因,导致该子程序在一个新的寄存器上下文中被执行(请不要把重入与递归混淆,它们有本质的区别)。这种情况常常会闹出乱子,因此有“可重入性”的研究。)、
2.4.4 中断延迟大大缩短
Cortex-M3 为了缩短中断延迟,引入了好几个新特性。包括自动的现场保护和恢复,以及其它的措施,用于缩短中断嵌套时的 ISR 间延迟。详情请见后面关于“咬尾中断”和“晚到中断”的讲述。
2.4.5 中断可屏蔽
既可以屏蔽优先级低于某个阈值的中断/异常(“异常”,均指除了外部中断之外的异常,而使用“中断”来表示所有外部中断——也就是对于处理器来说是异步的中断)(设置BASEPRI寄存器),也可以全体封杀(设置PRIMASK和FAULTMASK寄存器)。这是为了让时间关键( time-critical)的任务能在死线(deadline,或曰最后期限)到来前完成,而不被干扰。
2.5 存储器映射
总体来说, Cortex-M3 支持 4GB 存储空间,如图所示地被划分成若干区域。
从图中可见,不像其它的 ARM 架构,它们的存储器映射由半导体厂家说了算, Cortex-M3 预先定义好了“粗线条的”存储器映射。通过把片上外设的寄存器映射到外设区,就可以简单地以访问内存的方式来访问这些外设的寄存器,从而控制外设的工作。结果,片上外设可以使用 C 语言来操作。这种预定义的映射关系,也使得对访问速度可以做高度的优化,而且对于片上系统的设计而言更易集成(还有一个重要的,不用每学一种不同的单片机就要熟悉一种新的存储器映射——译注)。
Cortex-M3 的内部拥有一个总线基础设施,专用于优化对这种存储器结构的使用。在此之上, CM3 甚至还允许这些区域之间“越权使用”。比如说,数据存储器也可以被放到代码区,而且代码也能够在外部 RAM 区中执行(但是会变慢不少——译注 )。
处于最高地址的系统级存储区,是 CM3 用于藏“私房钱”的——包括中断控制器、 MPU 以及各种调试组件。所有这些设备均使用固定的地址(本书第 5 章讨论存储器系统)。通过把基础设施的地址定死,就至少在内核水平上,为应用程序的移植扫清了障碍。
2.6 总线接口
Cortex-M3 内部有若干个总线接口,以使 CM3 能同时取址和访内(访问内存),它们是:
- 指令存储区总线(两条)
- 系统总线
- 私有外设总线
有两条代码存储区总线负责对代码存储区的访问,分别是 I-Code 总线和 D-Code 总线。前者用于取指,后者用于查表等操作,它们按最佳执行速度进行优化。
系统总线用于访问内存和外设,覆盖的区域包括 SRAM,片上外设,片外 RAM,片外扩展设备,以及系统级存储区的部分空间。
私有外设总线负责一部分私有外设的访问,主要就是访问调试组件。它们也在系统级存储区。
2.7 存储器保护单元( MPU)
Cortex-M3 有一个可选的存储器保护单元。配上它之后,就可以对特权级访问和用户级访问分别施加不同的访问限制。当检测到犯规(violated)时, MPU 就会产生一个 fault 异常,可以由fault 异常的服务例程来分析该错误,并且在可能时改正它。
MPU 有很多玩法。最常见的就是由操作系统使用 MPU,以使特权级代码的数据,包括操作系统本身的数据不被其它用户程序弄坏。 MPU 在保护内存时是按区管理的(“区”的原文是 region,以后不再中译此名词——译注)。它可以把某些内存 region 设置成只读,从而避免了那里的内容意外被更改;还可以在多任务系统中把不同任务之间的数据区隔离。一句话,它会使嵌入式系统变得更加健壮,更加可靠(很多行业标准,尤其是航空的,就规定了必须使用 MPU 来行使保护职能——译注)。
2.8 指令集
Cortex-M3 只使用 Thumb-2 指令集。这是个了不起的突破,因为它允许 32 位指令和 16 位指令水乳交融,代码密度与处理性能两手抓,两手都硬。而且虽然它很强大,却依然易于使用。
在过去,做 ARM 开发必须处理好两个状态。这两个状态是井水不犯河水的,它们是: 32 位的ARM 状态和 16 位的 Thumb 状态。当处理器在 ARM 状态下时,所有的指令均是 32 位的(哪怕只是个”NOP”指令),此时性能相当高。而在 Thumb 状态下,所有的指令均是 16 位的,代码密度提高了一倍。不过, thumb 状态下的指令功能只是 ARM 下的一个子集,结果可能需要更多条的指令去完成相同的工作,导致处理性能下降。
为了取长补短,很多应用程序都混合使用 ARM 和 Thumb 代码段。然而,这种混合使用是有额外开销(overhead)的,时间上的和空间上的都有,主要发生在状态切换之时。另一方面, ARM 代码和 Thumb 代码需要以不同的方式编译,这也增加了软件开发管理的复杂度。
在诸如 ARM7 处理器上的状态切换模式图
伴随着 Thumb-2 指令集的横空出世,终于可以在单一的操作模式下搞定所有处理了,再也没有来回切换的事来烦你了。 事实上, Cortex-M3 内核干脆都不支持 ARM 指令,中断也在 Thumb 态下处理(以前的 ARM 总是在 ARM 状态下处理所有的中断和异常)。这可不是小便宜,它使 CM3 在好几个方面都比传统的 ARM 处理器更先进:
- 消灭了状态切换的额外开销,节省了 both 执行时间和指令空间。
- 不再需要把源代码文件分成按 ARM 编译的和按 Thumb 编译的,软件开发的管理大大减负。
- 无需再反复地求证和测试:究竟该在何时何地切换到何种状态下,我的程序才最有效率。开发软件容易多了。
不少有趣和强大的指令为 Cortex-M3 注入了新鲜的青春血液,下面给出几个例子:
- UBFX, BFI, BFC: 位段提取,位段插入,位段清零。支持 C 位段,也简化了外设寄存器操作。
- CLZ, RBIT: 计算前导零指令和位反转指令。二者组合使用能实现一些特技。
- UDIV, SDIV: 无符号除法和带符号除法指令。
- SEV, WFE, WFI: 发送事件,等待事件以及等待中断指令。用于实现多处理器之间的任务同步,还可以进入不同的休眠模式。
- MSR,MRS: 通向禁地——访问特殊功能寄存器。
因为 CM3 专情于最新的 Thumb-2,旧的应用程序需要移植和重建。对于大多数 C 源程序,只需简单地重新编译就能重建,汇编代码则可能需要大面积地修改和重写,才能使用 CM3 的新功能,并且融入 CM3 新引入的统一汇编器框架(unified assembler framework)中。
请注意: CM3 并不支持所有的 Thumb-2 指令, ARMv7-M 的规格书只要求实现 Thumb-2 的一个子集。举例来说,协处理器指令就被裁掉了(可以使用外部的数据处理引擎来替代)。 CM3 也没有实现 SIMD 指令集。旧世代的一些 Thumb 指令不再需要,因此也被排除。不支持指令还包括 v6 中引入的 SETEND 指令。如欲查出一个完整的指令列表,可以去看附录 A。
2.9 中断和异常
ARMv7-M 开创了一个全新的异常模型, CM3 采用了它。请你一定要划清界线:这种异常模型跟传统 ARM 处理器使用的完全是两码事。新的异常模型“使能”了非常高效的异常处理。它支持16-4-1=11 种系统异常(保留了 4+1 个档位),外加 240 个外部中断输入。在 CM3 中取消了 FIQ 的概念(v7 前的 ARM 都有这个 FIQ,快中断请求),这是因为有了更新更好的机制——中断优先级管理以及嵌套中断支持,它们被纳入 CM3 的中断管理逻辑中。因此,支持嵌套中断的系统就更容易实现 FIQ。
CM3 的所有中断机制都由 NVIC 实现。除了支持 240 条中断之外, NVIC 还支持 16-4-1=11 个内部异常源,可以实现 fault 管理机制。结果, CM3 就有了 256 个预定义的异常类型,如表 2.2所示。
编号 | 类型 | 优先级 | 简介 |
0 | N/A | N/A | 没有异常在运行 |
1 | 复位 | -3(最高) | 复位 |
2 | NMI | -2 | 不可屏蔽中断(来自外部 NMI 输入脚) |
3 | 硬(hard) fault | -1 | 所有被除能的 fault,都将“上访”成硬 fault |
4 | MemManage fault | 可编程 | 存储器管理 fault, MPU 访问犯规以及访问非法位置 |
5 | 总线 fault | 可编程 | 总线错误(预取流产( Abort)或数据流产) |
6 | 用法(usage)Fault | 可编程 | 由于程序错误导致的异常 |
7-10 | 保留 | N/A | N/A |
11 | SVCall | 可编程 | 系统服务调用 |
12 | 调试监视器 | 可编程 | 调试监视器(断点,数据观察点,或者是外部调试请求 |
13 | 保留 | N/A | N/A |
14 | PendSV | 可编程 | 为系统设备而设的“可悬挂请求”( pendable request) |
15 | SysTick | 可编程 | 系统滴答定时器(也就是周期性溢出的时基定时器——译注) |
16 | IRQ #0 | 可编程 | 外中断#0 |
17 | IRQ #1 | 可编程 | 外中断#1 |
… | … | … | … |
255 | IRQ #239 | 可编程 | 外中断#239 |
虽然 CM3 是支持 240 个外中断的,但具体使用了多少个是由芯片生产商决定。 CM3 还有一个NMI(不可屏蔽中断)输入脚。当它被 置为有效(assert)时, NMI 服务例程会无条件地执行。
2.10 低功耗与高能效
为了使我们的产品功耗更低,以及能源利用效率更高, Cortex-M3 在设计时加入了很多针对性的功能。
首先,在节能模式上,它提供了睡眠模式和深度睡眠模式。芯片以及整个系统在设计时通过与内核的节能模式相呼应,就可以根据应用的要求,在空闲时降低功耗。第二,它精练的设计使得门数很低,并且在工作状态下电路的活动更少,所以 CM3 自己也是“身先士卒”地以身作则了。而且,由于 CM3 的程序代码密度高,程序容量也可以变得更少;同时,再加上它强大的性能减少了程序执行时间,使得系统能以最快的速度回到睡眠中,以削低对能源的用量。综上所述, Cortex-M3 的能效要高于大多的 8 位或 16 位单片机。
2.11 调试支持
Cortex-M3 在内核水平上搭载了若干种调试相关的特性。最主要的就是程序执行控制,包括停机(halting)、单步执行(stepping)、指令断点、数据观察点、寄存器和存储器访问、性能速写(profiling)以及各种跟踪机制。
Cortex-M3 的调试系统基于 ARM 最新的 CoreSight 架构。不同于以往的 ARM 处理器,内核本身不再含有 JTAG 接口。取而代之的,是 CPU 提供称为“调试访问接口(DAP)”的总线接口。通过这个总线接口,可以访问芯片的寄存器,也可以访问系统存储器,甚至是在内核运行的时候访问!对此总线接口的使用,是由一个调试端口(DP)设备完成的。 DPs 不属于 CM3 内核,但它们是在芯片的内部实现的。目前可用的 DPs 包括 SWJ-DP(既支持传统的 JTAG 调试,也支持新的串行线调试协议),另一个 SW-DP 则去掉了对 JTAG 的支持。另外,也可以使用 ARM CoreSignt 产品家族的 JTAG-DP模块。这下就有 3 个 DPs 可以选了,芯片制造商可以从中选择一个,以提供具体的调试接口(通常都是选 SWJ-DP)。
此外, CM3 还能挂载一个所谓的“嵌入式跟踪宏单元(ETM)”。 ETM 可以不断地发出跟踪信息,这些信息通过一个被称为“跟踪端口接口单元(TPIU)”的模块而送到内核的外部,再在芯片外面使用一个“跟踪信息分析仪”,就可以把 TIPU 输出的“已执行指令信息”捕捉到,并且送给调试主机——也就是 PC。
在 Cortex-M3 中,调试动作能由一系列的事件触发,包括断点,数据观察点, fault 条件,或者是外部调试请求输入的信号。当调试事件发生时, Cortex-M3 可能会停机,也可能进入调试监视器异常 handler。具体如何反应,则根据与调试相关寄存器的配置。
与调试相关的还有其它的绝活。现在要介绍的是“仪器化跟踪宏单元(ITM)”,它也有自己的办法把数据送往调试器。通过把数据写到 ITM 的寄存器中,调试器能够通过跟踪接口来收集这些数据,并且显示或者处理它。此法不但容易使用,而且比 JTAG 的输出速度更快。
所有这些调试组件都可以由 DAP 总线接口来控制, CM3 内核提供 DAP 接口。此外,运行中的程序也能控制它们。所有的跟踪信息都能通过 TPIU 来访问到。
2.12 Cortex-M3 的品性简评
2.12.1 高性能
许多指令都是单周期的——包括乘法相关指令。并且从整体性能上, Cortex-M3 比得过绝大多数其它的架构。
- 指令总线和数据总线被分开,取值和访内可以并行不悖
- Thumb-2 的到来告别了状态切换的旧世代,再也不需要花时间来切换于 32 位 ARM 状态和16 位 Thumb 状态之间了。这简化了软件开发和代码维护,使产品面市更快。
- Thumb-2 指令集为编程带来了更多的灵活性。许多数据操作现在能用更短的代码搞定,这意味着 Cortex-M3 的代码密度更高,也就对存储器的需求更少。
- 取指都按 32 位处理。同一周期最多可以取出两条指令,留下了更多的带宽给数据传输。
- Cortex-M3 的设计允许单片机高频运行(现代半导体制造技术能保证 100MHz 以上的速度)。
- 即使在相同的速度下运行, CM3 的每指令周期数(CPI)也更低,于是同样的 MHz 下可以做
- 更多的工作;另一方面,也使同一个应用在 CM3 上需要更低的主频。
2.12.2 先进的中断处理功能
- 内建的嵌套向量中断控制器支持多达 240 条外部中断输入。向量化的中断功能剧烈地缩短了中断延迟,因为不再需要软件去判断中断源。中断的嵌套也是在硬件水平上实现的,不需要软件代码来实现。
- Cortex-M3 在进入异常服务例程时,自动压栈了 R0-R3, R12, LR, PSR 和 PC,并且在返回时自动弹出它们,这多清爽!既加速了中断的响应,也再不需要汇编语言代码了(第8 章有详述)。
- NVIC 支持对每一路中断设置不同的优先级,使得中断管理极富弹性。最粗线条的实现也至少要支持 8 级优先级,而且还能动态地被修改。
- 优化中断响应还有两招,它们分别是“咬尾中断机制”和“晚到中断机制”。
- 有些需要较多周期才能执行完的指令,是可以被中断-继续的——就好比它们是一串指令一样。这些指令包括加载多个寄存器(LDM),存储多个寄存器(STM),多个寄存器参与的PUSH,以及多个寄存器参与的 POP。
- 除非系统被彻底地锁定, NMI(不可屏蔽中断)会在收到请求的第一时间予以响应。对很多安全-关键(safety-critical)的应用, NMI 都是必不可少的(如化学反应即将失控时的紧急停机)。
2.11.3 低功耗
- Cortex-M3 需要的逻辑门数少, 所以先天就适合低功耗要求的应用(功率低于 0.19mW/MHz)
- 在内核水平上支持节能模式(SLEEPING 和 SLEEPDEEP 位)。通过使用“等待中断指令(WFI)”和“等待事件指令(WFE)”,内核可以进入睡眠模式,并且以不同的方式唤醒。另外,模块的时钟是尽可能地分开供应的,所以在睡眠时可以把 CM3 的大多数“官能团”给停掉。
- CM3 的设计是全静态的、同步的、可综合的。任何低功耗的或是标准的半导体工艺均可放心饮用。
2.11.4 系统特性
- 系统支持“位寻址带”操作(8051 位寻址机制的“威力大幅加强版”), 字节不变的大端模式,并且支持非对齐的数据访问。
- 拥有先进的 fault 处理机制,支持多种类型的异常和 faults,使故障诊断更容易。
-
通过引入 banked 堆栈指针机制,把系统程序使用的堆栈和用户程序使用的堆栈划清界线。如果再配上可选的 MPU,处理器就能彻底满足对软件健壮性和可靠性有严格要求的应用。
2.11.5 调试支持
- 在支持传统的 JTAG 基础上,还支持更新更好的串行线调试接口。
- 基于 CoreSight 调试解决方案,使得处理器哪怕是在运行时,也能访问处理器状态和存储器内容。
- 内建了对多达 6 个断点和 4 个数据观察点的支持。
- 可以选配一个 ETM,用于指令跟踪。数据的跟踪可以使用 DWT
- 在调试方面还加入了以下的新特性,包括 fault 状态寄存器,新的 fault 异常,以及闪存修补 (patch)操作,使得调试大幅简化。
- 可选 ITM 模块(软件应用程序驱动的跟踪源。它支持生成软件测量跟踪(SWIT),并且还提供粗略的时间戳功能。ITM模块的主要用途包括:支持printf风格调试,跟踪操作系统和应用程序事件,以及发出诊断系统信息。此外,ITM模块可以传输其他模块(如DWT模块)产生的Trace数据,同时还可以将软件实时测试的一些信息进行输出,且对系统实时性没有任何干扰。),测试代码可以通过它输出调试信息,而且“拎包即可入住”般地方便使用。
3.1 寄存器组
CM3 拥有通用寄存器 R0-R15 以及一些特殊功能寄存器。 R0-R12 是最“通用目的”的,但是绝大多数的 16 位指令只能使用 R0-R7(低组寄存器),而 32 位的 Thumb-2 指令则可以访问所有通用寄存器。特殊功能寄存器有预定义的功能,而且必须通过专用的指令来访问。
3.1.1 通用目的寄存器 R0-R7
R0-R7 也被称为低组寄存器。所有指令都能访问它们。它们的字长全是 32 位,复位后的初始值是不可预料的。
3.1.2 通用目的寄存器 R8-R12
R8-R12 也被称为高组寄存器。这是因为只有很少的 16 位 Thumb 指令能访问它们, 32 位的thumb-2 指令则不受限制。它们也是 32 位字长,且复位后的初始值是不可预料的。
3.1.3 特殊功能寄存器
Cortex-M3 的寄存器组
3.1.4 堆栈指针 R13
R13 是堆栈指针。在 CM3 处理器内核中共有两个堆栈指针,于是也就支持两个堆栈。当引用 R13 (或写作 SP)时,引用到的是当前正在使用的那一个,另一个必须用特殊的指令来访问(MRS,MSR指令)。这两个堆栈指针分别是:
- 主堆栈指针( MSP), 或写作 SP_main。这是缺省的堆栈指针,它由 OS 内核、异常服务例程以及所有需要特权访问的应用程序代码来使用。
- 进程堆栈指针( PSP), 或写作 SP_process。用于常规的应用程序代码(不处于异常服用例程中时)
如无特殊说明,“异常”与“中断”都是指当发生“事件”时,处理器改变正常执行流,去响应该事件的情况。只不过异常之于处理器是同步的,中断之于处理器是异步的。因此常混合使用二术语, ISR 和ESR 也混合使用,请读者不必工于辨析这两个术语的不同,在这里这不是重点
要注意的是,并不是每个程序都要用齐两个堆栈指针才算圆满。简单的应用程序只使用 MSP 就够了。堆栈指针用于访问堆栈,并且 PUSH 指令和 POP 指令默认使用 SP。
堆栈的 PUSH 与 POP
堆栈是一种存储器的使用模型。它由一块连续的内存和一个栈顶指针组成,用于实现“后进先出”的缓冲区。其最典型的应用,就是在数据处理前先保存寄存器的值,再在处理任务完成后从中恢复先前保护的这些值。
堆栈内存的基本概念
在执行 PUSH 和 POP 操作时,那个通常被称为 SP 的地址寄存器,会由硬件自动调整它的值,以避免后续操作破坏先前的数据。本书的后续章节还要围绕着堆栈展开更详细的论述。
在 Cortex-M3 中,有专门的指令负责堆栈操作——PUSH 和 POP。 它俩的汇编语言语法如下例所演示
PUSH {R0} ; *(--R13)=R0。 R13 是 long*的指针
POP {R0} ; R0= *R13++
请注意后面 C 程序风格的注释,它诠释了所谓的“向下生长的满栈”(本章后面在讲到堆栈内存操作时还要展开论述), Cortex-M3 就是以这种方式使用堆栈的。因此,在 PUSH 新数据时,堆栈指针先减一个单元。通常在进入一个子程序后,第一件事就是把寄存器的值先 PUSH 入堆栈中,在子程序退出前再 POP 曾经 PUSH 的那些寄存器。另外, PUSH 和 POP 还能一次操作多个寄存器,如下所示:
subroutine_1
PUSH {R0-R7, R12, R14} ; 保存寄存器列表
… ; 执行处理
POP {R0-R7, R12, R14} ; 恢复寄存器列表
BX R14 ; 返回到主调函数
在程序中为了突出重点,可以一直把 R13 写作 SP。在程序代码中, both MSP 和 PSP 都被称为R13/SP。不过,我们可以通过 MRS/MSR 指令来指名道姓地访问具体的堆栈指针。
MSP,亦写作 SP_main,这是复位后缺省使用堆栈指针,服务于操作系统内核和异常服务例程;而 PSP,亦写作 SP_process,典型地用于普通的用户线程中。
寄存器的 PUSH 和 POP 操作永远都是 4 字节对齐的——也就是说他们的地址必须是0x4,0x8,0xc,……。事实上, R13 的最低两位被硬线连接到 0,并且总是读出 0(Read As Zero)。
3.1.5 连接寄存器 R14
R14 是连接寄存器(LR)。在一个汇编程序中,你可以把它写作 both LR 和 R14。 LR 用于在调用子程序时存储返回地址。例如,当你在使用 BL(分支并连接, Branch and Link)指令时,就自动填充 LR的值。
main ;主程序
…
BL function1 ; 使用“分支并连接”指令呼叫 function1
; PC= function1,并且 LR=main 的下一条指令地址
…
Function1
… ; function1 的代码
BX LR ; 函数返回(如果 function1 要使用 LR,必须在使用前 PUSH,
; 否则返回时程序就可能跑飞了——译注)
尽管 PC 的 LSB 总是 0(因为代码至少是字对齐的), LR 的 LSB 却是可读可写的。这是历史遗留的产物。在以前,由位 0 来指示 ARM/Thumb 状态。因为其它有些 ARM 处理器支持 ARM 和 Thumb状态并存,为了方便汇编程序移植, CM3 需要允许 LSB 可读可写。
3.1.6 程序计数器 R15
R15 是程序计数器,在汇编代码中一般我们都都叫它的外号“PC”。因为 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.2 特殊功能寄存器组
Cortex-M3 中的特殊功能寄存器包括:
- 程序状态寄存器组(PSRs 或曰 xPSR)
- 中断屏蔽寄存器组(PRIMASK, FAULTMASK,以及 BASEPRI)
- 控制寄存器(CONTROL)
它们只能被专用的 MSR/MRS 指令访问,而且它们也没有与之相关联的访问地址。
MRS <gp_reg>, <special_reg> ;读特殊功能寄存器的值到通用寄存器
3.2.1 程序状态寄存器(PSRs 或曰 PSR)
程序状态寄存器在其内部又被分为三个子状态寄存器:
- 应用程序 PSR(APSR)
- 中断号 PSR(IPSR)
- 执行 PSR(EPSR)
通过 MRS/MSR 指令,这 3 个 PSRs 即可以单独访问,也可以组合访问(2 个组合, 3 个组合都可以)。当使用三合一的方式访问时,应使用名字“xPSR”或者“PSR”。
Cortex-M3 中的程序状态寄存器(xPSR)
合体后的程序状态寄存器(xPSR)
3.2.2 PRIMASK, FAULTMASK 和 BASEPRI
这三个寄存器用于控制异常的使能和除能。
名字 | 功能描述 |
PRIMASK | 这是个只有单一比特的寄存器。 在它被置 1 后,就关掉所有可屏蔽的异常,只剩下 NMI 和硬 fault 可以响应。它的缺省值是 0,表示没有关中断。 |
FAULTMASK | 这是个只有 1 个位的寄存器。当它置 1 时,只有 NMI 才能响应,所有其它的异常,甚至是硬 fault,也通通闭嘴。它的缺省值也是 0,表示没有关异常。 |
BASEPRI | 这个寄存器最多有 9 位(由表达优先级的位数决定)。它定义了被屏蔽优先级的阈值。当它被设成某个值后,所有优先级号大于等于此值的中断都被关(优先级号越大,优先级越低)。但若被设成 0,则不关闭任何中断, 0 也是缺省值。 |
Cortex-M3 的屏蔽寄存器组
对于时间-关键任务而言,恰如其分地使用 PRIMASK 和 BASEPRI 来暂时关闭一些中断是非常重要的。而 FAULTMASK 则可以被 OS 用于暂时关闭 fault 处理机能,这种处理在某个任务崩溃时可能需要。因为在任务崩溃时,常常伴随着一大堆 faults。在系统料理“后事”时,通常不再需要响应这些 fault ——人死帐清。总之 FAULTMASK 就是专门留给 OS 用的。
要访问 PRIMASK, FAULTMASK 以及 BASEPRI,同样要使用 MRS/MSR 指令,如:
MRS R0, BASEPRI ;读取 BASEPRI 到 R0 中
MRS R0, FAULTMASK ;似上
MRS R0, PRIMASK ;似上
MSR BASEPRI, R0 ;写入 R0 到 BASEPRI 中
MSR FAULTMASK, R0 ;似上
MSR PRIMASK, R0 ;似上
只有在特权级下,才允许访问这 3 个寄存器。
为了快速地开关中断, CM3 还专门设置了一条 CPS 指令,有 4 种用法
CPSID I ;PRIMASK=1, ;关中断
CPSIE I ;PRIMASK=0, ;开中断
CPSID F ;FAULTMASK=1, ;关异常
CPSIE F ;FAULTMASK=0 ;开异常
3.2.3 控制寄存器(CONTROL)
控制寄存器有两个用途,其一用于定义特权级别,其二用于选择当前使用哪个堆栈指针。由两个比特来行使这两个职能。
位 | 功能 |
CONTROL[1] | 堆栈指针选择 0=选择主堆栈指针 MSP(复位后的缺省值) 1=选择进程堆栈指针 PSP 在线程或基础级(没有在响应异常——译注),可以使用 PSP。 在 handler 模式下,只允许使用 MSP,所以此时不得往该位写 1。 |
CONTROL[0] | 0=特权级的线程模式 1=用户级的线程模式 Handler 模式永远都是特权级的。 |
CONTROL[1]
在 Cortex-M3 的 handler 模式中, CONTROL[1]总是 0。在线程模式中则可以为 0 或 1。
因此,仅当处于特权级的线程模式下,此位才可写,其它场合下禁止写此位。改变处理器的模式也有其它的方式:在异常返回时,通过修改 LR 的位 2,也能实现模式切换。这是 LR 在异常返回时的特殊用法,颠覆了对 LR 的传统使用方式,将在第 5 章中展开论述。
CONTROL[0]
仅当在特权级下操作时才允许写该位。一旦进入了用户级,唯一返回特权级的途径,就是触发一个(软)中断,再由服务例程改写该位。
CONTROL 寄存器也是通过 MRS 和 MSR 指令来操作的:
MRS R0, CONTROL
MSR CONTROL, R0
3.3 操作模式
Cortex-M3 支持 2 个模式和两个特权等级。
操作模式和特权等级
当处理器处在线程状态下时,既可以使用特权级,也可以使用用户级;另一方面, handler 模式总是特权级的。在复位后,处理器进入线程模式+特权级。
在线程模式+用户级下,对系统控制空间(SCS)的访问将被阻止——该空间包含了配置寄存器组以及调试组件的寄存器组。除此之外,还禁止使用 MRS/MSR 访问刚才讲到的,除了 APSR 之外的特殊功能寄存器。如果以身试法,则对于访问特殊功能寄存器的,访问操作被忽略;而对于访问 SCS空间的,将 fault 伺候。
在特权级下的代码可以通过置位 CONTROL[0]来进入用户级。而不管是任何原因产生了任何异常,处理器都将以特权级来运行其服务例程,异常返回后,系统将回到产生异常时所处的级别。用户级下的代码不能再试图修改 CONTROL[0]来回到特权级。它必须通过一个异常 handler,由那个异常handler 来修改 CONTROL[0],才能在返回到线程模式后拿到特权级。
特权级和处理器模式转换图
把代码按特权级和用户极分开对待,有利于使 CM3 的架构更加安全和健壮。例如,当某个用户程序代码出问题时,不会让它成为害群之马,因为用户级的代码是禁止写特殊功能寄存器和 NVIC中寄存器的。另外,如果还配有 MPU,保护力度就更大,甚至可以阻止用户代码访问不属于它的内存区域。
为了避免系统堆栈因应用程序的错误使用而毁坏,我们可以给应用程序专门配一个堆栈,不让它共享操作系统内核的堆栈。在这个管理制度下,运行在线程模式的用户代码使用 PSP,而异常服务例程则使用 MSP。这两个堆栈指针的切换是智能全自动的,就在异常服务的始末由 CM3 硬件处理。第 8 章将详细讨论此主题。
如前所述,特权等级和堆栈指针的选择均由 CONTROL 负责。当 CONTROL[0]=0 时,在异常处理的始末,只发生了处理器模式的转换,如下图所示。
中断前后的状态转换
但若 CONTROL[0]=1(线程模式+用户级),则在中断响应的始末, both 处理器模式和特权等极都要发生变化,如下图所示
中断前后的状态转换+特权等级切换
CONTROL[0]只有在特权级下才能访问。用户级的程序如想进入特权级,通常都是使用一条“系统服务呼叫指令(SVC)”来触发“SVC 异常”,该异常的服务例程可以视具体情况而修改 CONTROL[0]。
3.4 异常与中断
Cortex-M3 支持大量异常,包括 16-4-1=11 个系统异常,和最多 240 个外部中断——简称 IRQ。具体使用了这 240 个中断源中的多少个,则由芯片制造商决定。由外设产生的中断信号,除了 SysTick的之外,全都连接到 NVIC 的中断输入信号线。典型情况下,处理器一般支持 16 到 32 个中断,当然也有在此之外的。
作为中断功能的强化, NVIC 还有一条 NMI 输入信号线。 NMI 究竟被拿去做什么,还要视处理器的设计而定。在多数情况下, NMI 会被连接到一个看门狗定时器,有时也会是电压监视功能块,以便在电压掉至危险级别后警告处理器。 NMI 可以在任何时间被激活,甚至是在处理器刚刚复位之后。
下表列出了 Cortex-M3 可以支持的所有异常。有一定数量的系统异常是用于 fault 处理的,它们可以由多种错误条件引发。 NVIC 还提供了一些 fault 状态寄存器,以便于 fault 服务例程找出导致异常的具体原因。
Cortex-M3 中的异常类型:
编号 | 类型 | 优先级 | 简介 |
0 | N/A | N/A | 没有异常在运行 |
1 | 复位 | -3(最高) | 复位 |
2 | NMI | -2 | 不可屏蔽中断(来自外部 NMI 输入脚) |
3 | 硬(hard) fault | -1 | 所有被除能的 fault, 都将“上访”成硬 fault。除能的原因包括当前被禁用,或者被 PRIMASK 或 BASPRI 掩蔽。 |
4 | MemManage fault | 可编程 | 存储器管理 fault, MPU 访问犯规以及访问非法位置均可引发。企图在“非执行区”取指也会引发此 fault |
5 | 总线 fault | 可编程 | 从总线系统收到了错误响应,原因可以是预取流产( Abort)或数据流产,或者企图访问协处理器 |
6 | 用法(usage) Fault | 可编程 | 由于程序错误导致的异常。通常是使用了一条无效指令,或者是非法的状态转换,例如尝试切换到 ARM 状态 |
7-10 | 保留 | N/A | N/A |
11 | SVCall | 可编程 | 执行系统服务调用指令( SVC)引发的异常 |
12 | 调试监视器 | 可编程 | 调试监视器(断点,数据观察点,或者是外部调试请求 |
13 | 保留 | N/A | N/A |
14 | PendSV | 可编程 | 为系统设备而设的“可悬挂请求”( pendable request) |
15 | SysTick | 可编程 | 系统滴答定时器(也就是周期性溢出的时基定时器——译注) |
16 | IRQ #0 | 可编程 | 外中断#0 |
17 | IRQ #1 | 可编程 | 外中断#1 |
… | … | … | … |
255 | IRQ #239 | 可编程 | 外中断#239 |
3.5 向量表
当 CM3 内核响应了一个发生的异常后,对应的异常服务例程(ESR)就会执行。为了决定 ESR 的入口地址, CM3 使用了“向量表查表机制”。这里使用一张向量表。向量表其实是一个 WORD(32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。向量表在地址空间中的位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 处必须包含一张向量表,用于初始时的异常分配。
向量表结构:
异常类型 | 表项地址偏移量 | 异常向量 |
0 | 0x00 | MSP 的初始值 |
1 | 0x04 | 复位 |
2 | 0x08 | NMI |
3 | 0x0C | 硬 fault |
4 | 0x10 | MemManage fault |
5 | 0x14 | 总线 fault |
6 | 0x18 | 用法 fault |
7-10 | 0x1c-0x28 | 保留 |
11 | 0x2c | SVC |
12 | 0x30 | 调试监视器 |
13 | 0x34 | 保留 |
14 | 0x38 | PendSV |
15 | 0x3c | SysTick |
16 | 0x40 | IRQ #0 |
17 | 0x44 | IRQ #1 |
18-255 | 0x48-0x3FF | IRQ #2 - #239 |
举个例子,如果发生了异常 11(SVC),则 NVIC 会计算出偏移移量是 11x4=0x2C,然后从那里取出服务例程的入口地址并跳入。要注意的是这里有个另类: 0 号类型并不是什么入口地址,而是给出了复位后 MSP 的初值
3.6 栈内存操作
在 Cortex-M3 中,除了可以使用 PUSH 和 POP 指令来处理堆栈外,内核还会在异常处理的始末自动地执行 PUSH 与 POP 操作。本节让我们来检视一下具体的动作,第 9 章则讨论异常处理时的自动栈操作。
3.6.1堆栈的基本操作
笼统地讲,堆栈操作就是对内存的读写操作,但是访问地址由 SP 给出。寄存器的数据通过 PUSH操作存入堆栈,以后用 POP 操作从堆栈中取回。在 PUSH 与 POP 的操作中, SP 的值会按堆栈的使用法则自动调整,以保证后续的 PUSH 不会破坏先前 PUSH 进去的内容。
堆栈的功能就是把寄存器的数据临时备份在内存中,以便将来能恢复之——在一个任务或一段子程序执行完毕后恢复。正常情况下, PUSH 与 POP 必须成对使用,而且参与的寄存器,不论是身份还是先后顺序都必须完全一致。当 PUSH/POP 指令执行时, SP 指针的值也根着自减/自增。
基本的堆栈操作:每次处理单个寄存器
堆栈操作的进一步探讨
如果参与的寄存器比较多,这种 PUSH 和 POP 岂不是又臭又长? 放心, PUSH/POP 指令足够体贴,支持一次操作多个寄存器。像这样:
PUSH {R0-R2} ;压入 R0-R2 PUSH {R3-R5,R8, R12} ;压入 R3-R5,R8,以及 R12
在 POP 时,可以如下操作:
POP {R3-R5,R8, R12} ;弹出 R3-R5, R8,以及 R12 POP {R0-R2} ;弹出 R0-R2
注意:在寄存器列表中,不管寄存器的序号是以什么顺序给出的,汇编器都将把它们升序排序。然后先 push 序号大的寄存器,所以也就先 pop 序号小的寄存器。(这是译者在实验中发现的)。如果不按升序写寄存器,也许有些汇编器会给出语法错误。
PUSH/POP 对子还有这样一种特殊形式,形如:
PUSH {R0-R3, LR} POP {R0-R3, PC}
请注意: POP 的最后一个寄存器是 PC,并不是先前 PUSH 的 LR。这其实是一个返回的小技巧。与其按部就班地把先前 LR 的值弹回 LR,再复制给 PC 来返回;不如干脆绕过 LR,直接传给 PC!那不怕 LR 的值没有被恢复吗?不怕,因为 LR 在子程序调用中的唯一用处,就是在返回时提供返回地址。因此,在返回后,先前保存的返回地址就没有利用价值了,所以只要PC 得到了正确的值,不恢复也没关系。
PUSH 指令等效于与使用 R13 作为地址指针的 STMDB 指令,而 POP 指令则等效于使用R13 作为地址指针的 LDMIA 指令——STMDB/LDMIA 还可以使用其它寄存器作为地址指针。至于这两个指令的细节,第 4 章讲到指令系统时再介绍。
3.7 Cortex-M3 的堆栈实现
Cortex-M3 使用的是“向下生长的满栈”模型。堆栈指针 SP 指向最后一个被压入堆栈的 32 位数值。在下一次压栈时, SP 先自减 4,再存入新的数值。
Cortex-M3 堆栈的 PUSH 实现方式
POP 操作刚好相反:先从 SP 指针处读出上一次被压入的值,再把 SP 指针自增 4。
Cortex-M3 堆栈的 POP 实现方式
在进入 ESR 时, CM3 会自动把一些寄存器压栈,这里使用的是发生本异常的瞬间正在使用的 SP指针(MSP 或者是 PSP)。离开 ESR 后,只要 ESR 没有更改过 CONTROL[1],就依然使用发生本次异常的瞬间正在使用的 SP 指针来执行出栈操作。
3.7.1 再论 Cortex-M3 的双堆栈机制
CONTROL[1]=0 时的堆栈使用情况
当 CONTROL[1]=1 时,线程模式将不再使用 MSP,而改用 PSP(handler 模式永远使用 MSP)。这样做的好处在哪里?原来,在使用 OS 的环境下,只要 OS 内核仅在 handler 模式下执行,用户应用程序仅在用户模式下执行,这种双堆栈机制派上了用场——防止用户程序的堆栈错误破坏 OS 使用的堆栈。
CONTROL[1]=1 时的堆栈切换情况
在特权级下,可以指定具体的堆栈指针,而不受当前使用堆栈的限制,示例代码如下:
MRS R0, MSP ; 读取主堆栈指针到 R0
MSR MSP, R0 ; 写 R0 的值到主堆栈中
MRS R0, PSP ; 读取进程堆栈指针到 R0
MSR PSP, R0 ; 写 R0 的值到进程堆栈中
通过读取 PSP 的值, OS 就能够获取用户应用程序使用的堆栈,进一步地就知道了在发生异常时,被压入寄存器的内容,而且还可以把其它寄存器进一步压栈(使用 STMDB 和 LDMIA 的书写形式)。OS 还可以修改 PSP,用于实现多任务中的任务上下文切换。
3.8 复位序列
在离开复位状态后, CM3 做的第一件事就是读取下列两个 32 位整数的值:
- 从地址 0x0000,0000 处取出 MSP 的初始值。
- 从地址 0x0000,0004 处取出 PC 的初始值——这个值是复位向量, LSB 必须是 1。 然后从这个值所对应的地址处取指。
复位序列
请注意,这与传统的 ARM 架构不同——其实也和绝大多数的其它单片机不同。传统的 ARM 架构总是从 0 地址开始执行第一条指令。它们的 0 地址处总是一条跳转指令。 在 CM3 中,在 0 地址处提供 MSP 的初始值,然后紧跟着就是向量表(向量表在以后还可以被移至其它位置——译注)。 向量表中的数值是 32 位的地址,而不是跳转指令。向量表的第一个条目指向复位后应执行的第一条指令。
初始 MSP 及 PC 初始化的一个范例
因为 CM3 使用的是向下生长的满栈,所以 MSP 的初始值必须是堆栈内存的末地址加 1。举例 来说,如果你的堆栈区域在 0x20007C00-0x20007FFF 之间,那么 MSP 的初始值就必须是 0x20008000。
向量表跟随在 MSP 的初始值之后——也就是第 2 个表目。要注意因为 CM3 是在 Thumb 态下执行,所以向量表中的每个数值都必须把 LSB 置 1(也就是奇数)。正是因为这个原因,图 3.18 中使用0x101 来表达地址 0x100。当 0x100 处的指令得到执行后,就正式开始了程序的执行。在此之前初始化 MSP 是必需的,因为可能第 1 条指令还没来得及执行,就发生了 NMI 或是其它 fault。 MSP 初始化好后就已经为它们的服务例程准备好了堆栈。
对于不同的开发工具,需要使用不同的格式来设置 MSP 初值和复位向量——有些则由开发工具自行计算并生成。如果想要获知细节,最快的办法就是参考开发工具提供的一个示例工程。本书的第 10 章和第 20 章介绍 ARM 提供的开发工具,第 19 章则介绍 GCC 工具链。
终于“开荤”了,本章开始把 Cortex-M3 的指令系统展现出来,并且给出了一些简单却意味深长的例子。在本书的附录 A 中还有一个快速查阅参考。指令集的详细信息由《 ARMv7-M Architecture Application Level Reference Manual》 (Ref2)
4.1 汇编语言基础
4.1.1 汇编语言:基本语法
汇编指令的最典型书写模式如下所示:
标号
操作码 操作数 1, 操作数 2, … ;注释
其中,标号是可选的,如果有,它必须顶格写。标号的作用是让汇编器来计算程序转移的地址。
操作码是指令的助记符,它的前面必须有至少一个空白符, 通常使用一至二个“Tab”键来产生。操作码后面往往跟随若干个操作数,而第 1 个操作数,通常都给出本指令的执行结果存储处。不同指令需要不同数目的操作数,并且对操作数的语法要求也可以不同。举例来说,立即数必须以“#”开头,如
MOV R0, #0x12 ; R0 ← 0x12
MOV R1, #’A’ ; R1 ← 字母 A 的 ASCII 码
注释均以”;”开头,它的有无不影响汇编器工作,只是给程序员看的,能让程序更易理解。还可以使用 EQU 指示字来定义常数,然后在代码中使用它们,例如:
NVIC_IRQ_SETEN0 EQU 0xE000E100 ; 注意: 常数定义必须顶格写
NVIC_IRQ0_ENABLE EQU 0x1
…
LDR R0, =NVIC_IRQ_SETEN0 ;在这里的 LDR 是个伪指令,它会被汇编器转换成
;一条“相对 PC 的加载指令”
MOV R1, #NVIC_IRQ0_ENABLE ; 把立即数传送到 R1 中
STR R1, [R0] ; *R0=R1,执行完此指令后 IRQ #0 被使能。
如果汇编器不能识别某些特殊指令的助记符,你就要“手工汇编” ——查出该指令的确切二进制机器码,然后使用 DCI 编译器指示字。例如, BKPT 指令的机器码是 0xBE00,即可以按如下格式书写:
DCI 0xBE00 ; 断点(BKPT),这是一个 16 位指令
(在使用 DCI 时也必须在前面留出空白符——译注)
类似地,你还可以使用 DCB 来定义一串字节常数,字节常数还允许以字符串的形式来表达;还可以使用 DCD 来定义一串 32 位整数。它们最常被用来在代码中书写表格。例如:
LDR R3, =MY_NUMBER ; R3= MY_NUMBER
LDR R4, [R3] ; R4= *R3
…
LDR R0, =HELLO_TEXT ; R0= HELLO_TEXT
BL PrintText ; 呼叫 PrintText 以显示字符串, R0 传递参数
…
MY_NUMBER
DCD 0x12345678
HELLO_TEXT
DCB ”Hello\n”,0
请注意: 不同汇编器的指示字和语法都可以不同。上述示例代码都是按 ARM 汇编器的语法格式写的。如果使用其它汇编器,最好看一看它附带的示例代码。