Arm_Cortex-M3权威指南

这本权威指南只是对应某一种具体的处理器内核,深入一种处理器内核对于我们理解整个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提供如下的功能:

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

存储器映射

这里我就详细讲一下中断的流程,可能很多人不懂中断向量表存在哪,里面是什么,具体的中断处理函数存在哪,以及怎么触发中断的,中断相关寄存器的干啥的
中断向量表里面存的是中断处理函数的地址,也就是说中断向量表只存了地址,然后找到对应的中断跳到具体的处理函数去执行,中断向量表存在代码区,一般是0x00000000地址开始

具体的中断处理函数也是存在代码区,因为中断向量表里面存了处理函数的地址,于是找到具体中断就可以通过地址跳到中断处理函数去执行

中断的触发一般是由硬件触发,硬件检测到某种情况然后可能就会触发中断,比如在stm32中的gpio中断,我们可以配置下降沿触发,当电平为下降沿时GPIO会自动触发中断,然后CPU这个硬件会自动去查这个中断是第几号,并且计算地址,然后去这个地址读出函数地址,然后PC指针直接跳过去

中断相关寄存器是配置某种中断是否打开,因为一开始所有外设的中断都是关着的,屏蔽的,我们配置寄存器本质就是告诉硬件允许这个中断触发

中断处理流程是这样的:代码跑着 → 中断硬件触发 → CPU 自动暂停 → 自动去 0x00000000 查表 → 自动跳到中断函数 → 执行完自动回来。

哪如何访问外设控制外设,比如ADC,GPIO这种外设呢。通过把片上外设的寄存器映射到外设区,就可以简单地以访问内存的方式来访问这些外设的寄存器,从而控制外设的工作

总线接口

Cortex-M3内部有若干个总线接口,以使CM3能同时取址和访存,分别是:

  1. 指令存储区总线(两条)
  2. 系统总线
  3. 私有外设总线
    指令存储区的两条总线分别是 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 的品性简评

这里我直接粘贴原文了
高性能

  1. 许多指令都是单周期的------包括乘法相关指令。并且从整体性能上,Cortex‐M3 比得过绝大多数其它的架构。
  2. 指令总线和数据总线被分开,取值和访内可以并行不悖
  3. Thumb‐2 的到来告别了状态切换的旧世代,再也不需要花时间来切换于 32 位 ARM 状态和16 位 Thumb 状态之间了。这简化了软件开发和代码维护,使产品面市更快。
  4. Thumb‐2 指令集为编程带来了更多的灵活性。许多数据操作现在能用更短的代码搞定,这意味着 Cortex‐M3 的代码密度更高,也就对存储器的需求更少。
  5. 取指都按 32 位处理。同一周期最多可以取出两条指令,留下了更多的带宽给数据传输。 Cortex‐M3 的设计允许单片机高频运行(现代半导体制造技术能保证 100MHz 以上的速度)。即使在相同的速度下运行,CM3 的每指令周期数(CPI)也更低,于是同样的 MHz 下可以做更多的工作;另一方面,也使同一个应用在 CM3 上需要更低的主频。

先进的中断处理功能

  1. 内建的嵌套向量中断控制器支持多达 240 条外部中断输入。向量化的中断功能剧烈地缩短了中断延迟,因为不再需要软件去判断中断源。中断的嵌套也是在硬件水平上实现的,不需要软件代码来实现。
  2. Cortex‐M3 在进入异常服务例程时,自动压栈了 R0‐R3, R12, LR, PSR 和 PC,并且在返回时自动弹出它们,这多清爽!既加速了中断的响应,也再不需要汇编语言代码了(第 8 章有详述)。
  3. NVIC 支持对每一路中断设置不同的优先级,使得中断管理极富弹性。最粗线条的实现也至少要支持 8 级优先级,而且还能动态地被修改。
  4. 优化中断响应还有两招,它们分别是"咬尾中断机制"和"晚到中断机制"。
  5. 有些需要较多周期才能执行完的指令,是可以被中断-继续的------就好比它们是一串指令一样。这些指令包括加载多个寄存器(LDM),存储多个寄存器(STM),多个寄存器参与的 PUSH,以及多个寄存器参与的 POP。
  6. 除非系统被彻底地锁定,NMI(不可屏蔽中断)会在收到请求的第一时间予以响应。对于很多安全‐关键(safety‐critical)的应用,NMI 都是必不可少的(如化学反应即将失控时的紧急停机)。

低功耗

  1. Cortex‐M3 需要的逻辑门数少,所以先天就适合低功耗要求的应用(功率低于 0.19mW/MHz)
  2. 在内核水平上支持节能模式(SLEEPING 和 SLEEPDEEP 位)。通过使用"等待中断指令(WFI)"
    和"等待事件指令(WFE)",内核可以进入睡眠模式,并且以不同的方式唤醒。另外,模块的时钟是尽可能地分开供应的,所以在睡眠时可以把 CM3 的大多数"官能团"给停掉。
  3. CM3 的设计是全静态的、同步的、可综合的。任何低功耗的或是标准的半导体工艺均可放心饮用。

系统特性

  1. 系统支持"位寻址带"操作(8051 位寻址机制的"威力大幅加强版"),字节不变的大端模式,并且支持非对齐的数据访问。
  2. 拥有先进的 fault 处理机制,支持多种类型的异常和 faults,使故障诊断更容易。
  3. 通过引入 banked 堆栈指针机制,把系统程序使用的堆栈和用户程序使用的堆栈划清界线。如果再配上可选的 MPU,处理器就能彻底满足对软件健壮性和可靠性有严格要求的应用。

调试支持

  1. 在支持传统的 JTAG 基础上,还支持更新更好的串行线调试接口。
  2. 基于 CoreSight 调试解决方案,使得处理器哪怕是在运行时,也能访问处理器状态和存储器内容。
  3. 内建了对多达 6 个断点和 4 个数据观察点的支持。
  4. 可以选配一个 ETM,用于指令跟踪。数据的跟踪可以使用 DWT
  5. 在调试方面还加入了以下的新特性,包括 fault 状态寄存器,新的 fault 异常,以及闪存修补 (patch)
    操作,使得调试大幅简化。
  6. 可选 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 中的特殊功能寄存器包括:

  1. 程序状态寄存器组(PSRs 或曰 xPSR)
  2. 中断屏蔽寄存器组(PRIMASK, FAULTMASK,以及 BASEPRI)
  3. 控制寄存器(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 小端配置和大端配置。

存储器映射

相关推荐
Freak嵌入式6 小时前
ESP32 实现在线动态安装库和自动依赖安装-使用uPyPI包管理平台
arm开发·ide·嵌入式·micropython·电子·upypi
平凡的阳阳16 小时前
千里通 RK3576 ARM架构智能边缘计算盒
arm开发·架构·边缘计算
ai产品老杨1 天前
异构计算时代的视频底座:基于 X86/ARM 与 GPU/NPU 的边缘云协同架构解析
arm开发·架构·音视频
AI服务老曹2 天前
异构计算时代的安防底座:基于 Docker 与 ZLMediaKit 的 X86/ARM 混合架构解析
arm开发·docker·架构
披着羊皮不是狼2 天前
在 QEMU 上实现 ARM 裸机程序与底层原理解析
arm开发
somi72 天前
ARM-12-I.MX6U LCD
arm开发·单片机·嵌入式硬件·自用
ai产品老杨2 天前
异构计算新范式:基于 X86/ARM 的 AI 视频管理平台架构深度解析
arm开发·人工智能·架构
EnglishJun3 天前
ARM嵌入式学习(十五)--- IMX6ULL的ADC接口使用
arm开发·学习
笨笨饿3 天前
博客目录框架
c语言·开发语言·arm开发·git·嵌入式硬件·神经网络·编辑器