第2章 ARM体系结构与编程模型

ARM处理器,说白了就是嵌入式世界的“普通话”。不管你用哪家芯片厂的产品,内核都是ARM的。我刚开始接触嵌入式时,总觉得学ARM架构太理论,不如直接写代码来得痛快。后来踩了几个坑才明白——不理解处理器的工作模式,你连中断服务程序都写不对。

这一章,咱们把ARM的底裤扒开看看。

2.1 ARM处理器系列

ARM公司自己不造芯片,只卖IP核。这就像卖乐高图纸,各家厂商按图纸拼出自己的芯片。ARM处理器家族主要分这几个系列:

系列 典型内核 应用场景 我见过的坑
Cortex-A A7, A53, A72 应用处理器(手机、平板、服务器) 跑Linux必须用A系列,别拿M系列硬扛
Cortex-R R4, R5, R52 实时处理器(汽车、工业控制) 硬实时场景首选,但生态不如A系列
Cortex-M M0, M3, M4, M7 微控制器(IoT、传感器、电机控制) 裸机或RTOS,别想着跑完整Linux
经典ARM ARM7, ARM9, ARM11 老项目维护、教学 新项目不建议选,工具链快绝迹了

我个人习惯,做BSP开发时先看芯片用的是哪个系列。如果是Cortex-A,那大概率要跑Linux;如果是Cortex-M,基本就是裸机或FreeRTOS。选错了方向,后面全白干。

小提示: 判断一个芯片能不能跑Linux,最简单的方法就是看它有没有MMU(内存管理单元)。Cortex-A系列都有,Cortex-M系列都没有。Cortex-R系列部分有,但一般不跑完整Linux。

2.2 处理器工作模式

ARM处理器有7种工作模式(Cortex-A系列)。为什么需要这么多模式?说白了就是为了隔离——不同场景下,处理器看到的寄存器是不一样的。

  • 用户模式(USR): 普通程序运行的模式,权限最低。想访问硬件?得通过系统调用。
  • 快速中断模式(FIQ): 处理高优先级中断。有自己专属的寄存器,切换快。
  • 普通中断模式(IRQ): 处理普通中断。我刚开始写驱动时,总把IRQ和FIQ搞混,结果中断响应时间死活降不下来。
  • 管理模式(SVC): 操作系统内核运行的模式。复位后默认进入这个模式。
  • 中止模式(ABT): 访问非法内存地址时进入。嗯,写驱动时最常见的崩溃场景。
  • 未定义模式(UND): 执行未定义指令时进入。我遇到过编译器生成了一条ARM不认识的指令,直接卡死。
  • 系统模式(SYS): 和用户模式用同一组寄存器,但有更高权限。Linux内核里用得不多。
重点: 用户模式和系统模式共用同一组寄存器,但用户模式不能直接切换到其他模式。必须通过异常(中断、系统调用等)才能进入特权模式。这是操作系统的保护机制基础。

2.3 寄存器组织

ARM有37个寄存器,但你看不到那么多。为什么?因为不同模式下,有些寄存器是“影子寄存器”——同一个编号,在不同模式下指向不同的物理寄存器。

咱们重点看这组:

  • R0-R12: 通用寄存器。所有模式共享R0-R7,R8-R12在FIQ模式下有自己专属的副本。
  • R13(SP): 栈指针寄存器。每个模式都有自己的栈指针。我曾经犯过一个低级错误——在IRQ模式里用了用户模式的栈,结果栈溢出把内核数据给踩了。
  • R14(LR): 链接寄存器。保存子程序返回地址。异常发生时,LR会被自动设置。
  • R15(PC): 程序计数器。指向当前正在执行的指令地址。
  • CPSR: 当前程序状态寄存器。存着条件标志、中断使能位、当前模式等信息。
  • SPSR: 保存的程序状态寄存器。异常发生时,用来保存CPSR的副本。
避坑指南: 我曾经在中断服务程序里修改了CPSR的I位(IRQ中断使能位),结果导致中断嵌套失控。记住——除非你清楚自己在做什么,否则不要在中断里乱改CPSR。

2.4 指令集概述

ARM指令集有两个“方言”:ARM指令集(32位)和Thumb指令集(16位)。Cortex-M系列只支持Thumb,Cortex-A系列两者都支持。

我个人的经验:写启动代码和中断向量表时用ARM指令集,因为每条指令都能干完整的事;写普通C代码时编译器会自动用Thumb,代码密度高,省内存。

ARM指令按功能分这几类:

  • 数据处理指令: MOV、ADD、SUB、AND、ORR等。注意ARM的“桶形移位器”——一条指令里可以同时做移位和运算。
  • 加载/存储指令: LDR、STR、LDM、STM。ARM是加载-存储架构,不能直接操作内存里的数据。
  • 分支指令: B、BL、BX、BLX。BL会保存返回地址到LR,BX可以切换指令集。
  • 协处理器指令: MCR、MRC。用来操作系统控制协处理器CP15,比如配置MMU和Cache。
  • 异常产生指令: SWI(软中断)、SVC。用户程序通过它们进入内核。
; 一个简单的ARM汇编例子:点亮LED
; 假设GPIO控制寄存器地址在0x40000000
LDR R0, =0x40000000    ; 加载GPIO基地址
LDR R1, [R0]           ; 读取当前GPIO状态
ORR R1, R1, #0x01      ; 设置第0位为1
STR R1, [R0]           ; 写回GPIO寄存器
小技巧: 调试时用objdump反汇编,看看编译器生成的指令。我经常这么干,能发现很多编译器优化带来的“惊喜”。

2.5 异常处理

异常,就是处理器遇到了“意外情况”。ARM的异常处理机制非常优雅——硬件自动完成现场保存和模式切换,软件只需要写好处理函数。

ARM的异常向量表固定在地址0x00000000或0xFFFF0000(由CP15配置)。每个异常占用4字节,放一条跳转指令。

异常类型 向量地址 进入模式 典型触发原因
复位 0x00000000 SVC 上电或按复位键
未定义指令 0x00000004 UND 执行了不认识的指令
软件中断(SWI) 0x00000008 SVC 用户程序调用系统服务
预取中止 0x0000000C ABT 取指令时内存访问失败
数据中止 0x00000010 ABT 读写数据时内存访问失败
普通中断(IRQ) 0x00000018 IRQ 外设触发中断
快速中断(FIQ) 0x0000001C FIQ 高优先级外设触发中断

异常处理流程是这样的:

  1. 处理器完成当前指令(如果是同步异常)或立即响应(如果是异步异常)。
  2. 保存PC到LR(注意有偏移量,不同异常偏移不同)。
  3. 保存CPSR到SPSR。
  4. 修改CPSR,切换到对应的异常模式,关闭中断(IRQ和FIQ异常会关闭对应中断)。
  5. 从向量表取指令执行。

我曾经在调试一个网卡驱动时,发现中断响应特别慢。查了半天,原来是中断处理函数里做了太多事情,导致后续中断被长时间屏蔽。解决方案很简单——中断处理只做最紧急的事,比如清中断标志、把数据搬到队列里,剩下的交给下半部(Bottom Half)处理。

核心原则: 中断处理函数要短、平、快。能放下半部做的事,绝不在中断上下文里做。这是嵌入式系统稳定性的基石。
ARM异常处理流程 正常程序执行 异常发生(硬件自动处理) 保存PC到LR,CPSR到SPSR 切换模式,关闭中断 从向量表取指令执行 注意点 • 不同异常LR偏移不同 • FIQ有自己的影子寄存器 • 向量表可重定位到0xFFFF0000 • 异常处理完用MOVS PC, LR返回

嗯,ARM体系结构的内容就这些。看起来知识点不少,但实际写代码时,你真正打交道的就那几个——工作模式决定了你能访问哪些资源,寄存器组织决定了你怎么传参和保存现场,异常处理决定了系统的实时性和稳定性。把这些搞明白,后面写BSP和驱动就顺了。