RTOS 学习笔记1:ARM 基础——运行模式,寄存器,指令系统,汇编,异常中断处理

1,918 阅读10分钟

本系列学习笔记基于 32 位 ARM,暂不提 ARM64 和 16位的 Thumb 模式

笔记博主一年前修的西电课程《嵌入式系统》课堂笔记整理,博主非嵌入式方向,也不打算做嵌入式开发,仅仅就只是单纯学着玩的,这笔记主要是给自己看的,也就图一乐

ARM 运行模式

ARM 一共有三大类、共七种运行模式,可以根据需要在运行模式之间进行切换

  • USR:用户模式,运行用户代码。除此之外的模式均为特权模式
  • SYS:系统模式,运行操作系统代码
  • 异常模式:因各种原因,让程序不能再继续执行时进入的模式
    • FIQ:快中断,用于高速数据传输
    • IRQ:外部中断,用于通常的中断处理
    • SVC:管理模式,操作系统用的保护模式(高权限),复位和软中断进入
    • ABT:Abort,数据访问中止模式,数据或指令预取终止时进入,可用于虚拟内存、存储保护
    • und:指令未定义中止。该异常可用于处理未知指令的情况,遇到不认识的指令时,为避免程序跑飞,会跳转到异常处理程序中。也可以用于自定义指令、支持协处理器等

CPU 的【取指-译码-执行】循环

只要是 CPU 在正常运行,就会永远执行 【取指-译码-执行】 的流程。该流程可以分为三种模式:

  • 顺序执行:取指-译码-执行->取下条指令-译码-执行……
  • 跳转:
    • 子函数调用:跳转到另一个位置执行指令,执行后会返回跳走的位置
    • 分支:跳转后不再返回
  • 中断:执行完当前正在执行的指令后,跳转到中断服务程序。完成中断服务程序后再返回原位的下一条指令继续执行。(中断服务程序里面可以做不少事情,比如虚拟内存的换页——因此程序员是感觉不到换页的)

ARM 寄存器

寄存器是 CPU 内部与 ALU 通过内部总线直接连起来的空间。

ARM 总共有 37 个寄存器,比 x86 丰富多了。。

  • 31 * 通用寄存器

    • R0~R14 以及 PC 是所有模式都可见的(ARM 可以对 PC 进行运算)
      • R0~R7 所有模式共用,可以用来在切换状态时传参
      • R13 是堆栈指针(SP),指向当前栈顶。每个异常都有自己的 SP,这意味着每个异常都可以有自己的栈
      • R14 是链接寄存器(LR),其可在异常发生时自动保存返回地址
    • R15 虽然也是通用存储器,但一般用作 PC 。想跳到哪里可以直接向 R15 写值,非常方便。
    • 剩下的是特供某些模式的,比如 USR、SYS 下可用 R13、R14,就和 SVC 下的 R13_SVC、R14_SVC 相互独立(模式独立的寄存器非常好用,比如对于 FIQ,其有专属的 R8_FIQ~R12_FIQ,因此跳走时可以用这几个寄存器来代替压栈,实现现场的保护)
  • 6 * 状态寄存器

    • 1 * CPSR (Current Program State Register):和 PSW 是一回事
    • 5 * SPSR (Saved Program Status Register):每个异常都有自己的 SPSR,用于在进异常时保存 CPSR 值,以便异常处理结束后返回原工作状态

ARM 指令系统

ARM 是 Load/Store 结构的处理器,能且仅能在寄存器中进行操作,而不能在内存中做运算。使用时要从存储器读值送寄存器,算完再放回去

指令可以分为这么几类:数据处理、Load/Store(寄存器、内存数据传输)、跳转、CPSR处理、异常产生、协处理器

寻址方式

  • 立即寻址(立即数寻址):操作数直接放在指令流中

  • 寄存器寻址:去寄存器取值

  • 寄存器位移寻址:MOV R0, R2, LSL #3 R2 内的值逻辑左移 3 位送 R0(立即数以 # 开头)

  • 基址寻址:值放在内存中,用寄存器值作为存储器的地址,如:

    LDR R0, [R1]	@ R1 地址的值读到 R0
    STR R0, [R1] 	@ R0 寄存器的值写入地址 R1
    
  • 变址寻址:可用 基址+偏移量 作为地址值去读内存

    LDR R0, [R1, #4]	@ 偏移量为立即数
    LDR R0, [R1, R2]	@ 偏移量为寄存器值
    LDR R0, [R1, R2, LSL#2]	@ 偏移量为 R2 值左移两位
    
  • 相对寻址:使用 PC 作为变址寻址的基址寄存器

  • 块拷贝寻址

  • 堆栈寻址

ARM 汇编

比较杂,由于我不想把笔记写成字典,因此此处就只记录一些代码示例,用例子代替那些到处都能查到的手册一般的解释

@ 通用格式:<opcode>{<cond>}{S} <Rd>, <Rn>, {, {oprand2}}
@ opcode 是指令,cond 是指令条件比如什么 EQ、NE
@ Rd是目标寄存器,Rn是第一操作数所在的寄存器
@ oprand2是第二操作数,可以是寄存器也可以是立即数


ADD R2, R1, R0   @ 加法,三个操作数, R1+R0 结果值送 R2

ADDS R1, R1, #1  @ 加法,立即数1作为第二个操作数:R1+1结果送R1。
				 @ S 代表运算影响 CPSR(嗯,ARM 的 PSW 是可以这么手动控制的)
				 
BEQ DATAEVEN	 @ B 是跳转,EQ 是条件 Equal。DATAEVEN 是目的位置

SUBNES R1, R1, #0x0D  @ SUB:减法; NE:NotEq; S:结果影响 CPSR。R1-0x0D 结果送 R1

MOV R0, R2, LSL #3    @ R2左移3位,结果送R0。其实就是 R2*8

ADD R3, R2, LSR R4	  @ R2右移R4中值那么多位,结果送 R3

其他还有什么 ADC带进位加、SBC带借位减、AND按位与、ORR按位或、EOR异或、TST按位与、TEQ按位或

LDR、STR 单寄存器整个字(word,32位)存取,STRH 半字(half-word,16位),STRB 字节(8位)

LDM、STM 多寄存器存取,LDMFD R0!, {R1-R4} 从[R0] 开始的四个连续内存单元写 R1-R4,这里可以通过在R0后面写!说明需要回写,通过在{R1-R4}后面加^说明要把 SPSR 复制到 CPSR

SWP 交换俩寄存器的数据,这个可以用于进程间的同步和互斥

MRS、MSR:实现 CPSR、SPSR 的值和通用寄存器 Rn 互写。比如:

  • MRS R0, CPSR把 CPSR 送 R0
  • MSR R0, SPSR 把 R0 送 SPSR

跳转

跳转有两种方式,一是直接往 PC (即,R15) 中写目标地址值,该方式可以跳前后 4GB 的空间;

二是用如下跳转指令(范围是当前指令前后 32MB 的空间)

  • B 直接跳
  • BL 带返回的跳,跳走前会将当前 PC 值送 R14,后面再回来
  • BX 带状态切换,可以实现 ARM、Thumb 的模式切换
  • BLX L且X,含义与前面俩相同

伪操作和伪指令

其实伪指令是包含在伪操作中的。这俩玩意都是给汇编器看的。这伪的玩意虽然和正儿八经的汇编一起写到汇编文件中,但汇编器会在汇编时对这些伪的玩意进行处理,给整成真的机器指令或者指令序列

伪操作可以用来定义符号、定义数据、进行控制、进行信息报告(用于在汇编时报错等)、做做段定义、指定代码类型、定义程序入口点之类的

这里一个个写的话也会写成字典,所以还是写几个大例子吧

求最大公约数:

      AREA gcd, CODE, READONLY	@ 定义代码段
      ENTRY			@ 标识程序入口点,一个源文件至多有一个
      				@ 若有多个 ENTRY,需要由链接器指定入口
start CMP R0, R1	@ 标号 start 及指令序列,这里做个比较
      BEQ stop		@ 相等则跳到 stop 标号处,实现分支跳转
      BLT less		@ 小于则跳到 less 标号处
      SUB R0,R0,R1
      B start		@ 跳回来,其实是循环
less  SUB R1, R1, R0
      B start		@ 跳回 start 标号开始新的循环
stop  NOP			@ 空操作,不影响 CPSR
      MOV PC, LR
      END			@ 指定段结尾

AREA gcd, CODE, READONLY 定义了这个小程序的代码段。ARM 汇编以段为单位组织源文件,一个段就是一个相对独立、有特定名称的指令 / 数据序列。多个段经过编译、链接后会成为一个可执行映像文件。这个文件中包含至少一个代码段,可以有零或多个【含或不含初始化数据的】数据段

ARM 异常中断处理

当 CPU 正常运行时,每执行完一条指令,PC 值都会增加 4 (即往后移动 32 位,指向下一条指令)

异常中断时,则会:执行完当前指令后,保护现场并跳转到异常中断处理程序处(CPU 通过将 PC 值改为异常向量表中对应异常处理程序的地址,实现跳转)开始执行。处理程序执行完毕,再回到中断前的位置,恢复现场并继续执行

异常类型及来源:

  • 复位(Reset):复位引脚有效
  • 未定义指令(Undefined Instruction):读到了无法解码的指令
  • 软件中断(SWI,Software Interrupt):指令引起的异常
  • 指令预取中止(Prefetch)、数据访问终止(DataAbort):取数或取指时,没有取到期望的值
  • 外部中断(IRQ)、快速中断(FIQ):来自引脚有效

向量表是异常的入口地址,发生对应异常时CPU会跳到对应的向量地址,然后再跳转到向量地址中的异常处理地址,实现异常处理。

异常向量表一般都存在地址的最低端,分布如下表

地址异常
0x00reset
0x04undefined
0x08SWI
0x0cPrefetch
0x10DataAbort
0x14NOP
0x18IRQ
0x1cFIQ

其中,0x00 也是系统刚上电时 CPU 跑到的第一个地址

响应和返回步骤

响应

  1. CPSR 存入对应 SPSR。这个 SPSR 指的是要跳到的中断模式的 SPSR,比如要从 USR 跳到 FIQ,那就把用户模式的 CPSR 送 FIQ 的SPSR
  2. 重设 CPSR 各域值,实现把 CPU 切换到对应的模式,比如 FIQ 模式位就是 10001,然后就可以用该模式下的独立寄存器了
  3. 把返回地址存入对应模式的 LR
  4. 设置 PC 为中断向量地址,实现强制跳转

返回

  1. 从 SPSR 恢复 CPSR

  2. LR 值减去偏移量后送 PC(减偏移量是因为异常来的时候,硬件可能来不及调整地址。下面是一些异常和对应返回的位置)

    指令预取中止:LR-4

    数据中止:LR-8

    IRQ:LR-4

    FIQ:LR-4

    未定义、SWI直接返回 LR 即可。(复位就不用返回了)

  3. 视实际情况恢复各寄存器值

跳转(设置向量表)

一般可以用 B 来跳,如果想要远跳则需要写 PC

AREA boot, CODE, READONLY
ENTRY
    LDR PC, Reset_Add		@ 中断向量表
    LDR PC, Undefined_Add
    @...
    LDR PC, FIQ_Add
    
Reset_Add       DCD Start_Boot	@ 中断处理程序入口,放在内存中
								@ DCD:分配一段连续的空间并初始化
								@ 意思是:标号 Reset_Add 的值为 Start_Boot 起的一段
Undefined_Add   DCD Undefined_Handler
@...
FIQ_Add         DCD FIQ_Handler

Start_Boot  @...进行实际处理的代码,即中断处理程序实现
Undefined_Handler @...进行实际处理的代码
FIQ_Handler @...进行实际处理的代码

实际中断处理程序例子:IRQ_Handler

这个可以当模板用,其他的中断处理也就是改改调用的函数(吧?)

EXPORT IRQ_Handler		@ EXPORT,声明全局标号,全程序可见,可在其他文件中引用
AREA IRQ_Handler, CODE, READONLY
SUB LR, LR, #0x4        @ 设置返回地址为 LR-4
STMFD SP!, {R0-R12, LR} @ 寄存器压栈,保护现场。进中断后这些寄存器随便用
MRS R4, SPSR			@ SPSR 存 R4
STMFD SP!, {R4}			@ 压栈,R4 存入 SP 指向的内存位置(即栈顶)
BL IRQ_Function			@ 跳转到真正的中断处理子函数
LDMFD SP!, {R4}			@ 恢复
MSR SPSR_cxsf, R4		@ cxsf是指四个8位的域,此处是整个32位寄存器
LDMFD SP!, {R0-R12, PC}^ @ 恢复现场并返回
END