第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。选错了方向,后面全白干。
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的副本。
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寄存器
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 | 高优先级外设触发中断 |
异常处理流程是这样的:
- 处理器完成当前指令(如果是同步异常)或立即响应(如果是异步异常)。
- 保存PC到LR(注意有偏移量,不同异常偏移不同)。
- 保存CPSR到SPSR。
- 修改CPSR,切换到对应的异常模式,关闭中断(IRQ和FIQ异常会关闭对应中断)。
- 从向量表取指令执行。
我曾经在调试一个网卡驱动时,发现中断响应特别慢。查了半天,原来是中断处理函数里做了太多事情,导致后续中断被长时间屏蔽。解决方案很简单——中断处理只做最紧急的事,比如清中断标志、把数据搬到队列里,剩下的交给下半部(Bottom Half)处理。
嗯,ARM体系结构的内容就这些。看起来知识点不少,但实际写代码时,你真正打交道的就那几个——工作模式决定了你能访问哪些资源,寄存器组织决定了你怎么传参和保存现场,异常处理决定了系统的实时性和稳定性。把这些搞明白,后面写BSP和驱动就顺了。