imx6ull 基础复盘*
- imx6ull 基础复盘
imx6ull 基础复盘
这篇文档,主要从裸机,底层寄存器,单片机的角度,重新过一遍imx6ull的armv7架构,基础的汇编语法,基础的代码结构,内存分布,启动流程,中断管理这些。
因为已经学过stm32和freertos了,对CM3的内核的理解比之前更加深了。
为后面移植uboot,kernel,和外设驱动,做一个基础的复盘。
IMX6ULL 硬件板卡资源

单核Cotex-A7,多处理器架构,arm-cortex的关系
之前我们使用的stm32的核心是CM3,单核的。而现在我们使用的这个IMX6的开发板,同样也是单核的,核心是CA7(cotex-A7),
下面介绍一下 Cortex-A7 架构的一些基本知识,如处理器模型、Cortex-A7 寄存器组

这个是之前看到的介绍处理器的一张图,但是当时有些混淆:
- cortex xxx 和 armv7, armv8的关系?
- MPcore和cortex-a7有什么关系?
层级拆解
ARMv7(架构 = 建筑图纸规范)- 这是一套规范(ISA),规定了寄存器长什么样、指令怎么写、中断怎么跳。
Cortex-A7(核 / Core = 具体的单间房型)- 这是ARM工程师根据ARMv7规范设计出来的具体电路实现(流水线、ALU、FPU)。
- 这才是真正干活的那个“脑子”。
Cortex-A7 MPCore(处理器子系统 IP = 户型图/套房)- ARM卖给芯片厂(如NXP、ST)的不仅仅是那个“标准卧室”,而是一整套套房设计方案。
- MPCore 的全称是
Multi-Processor Core。它是一个壳子(或者叫Cluster/簇)。 - 在这个壳子里,ARM 允许芯片厂塞进去 1到4个 “Cortex-A7标准卧室”。
- 除了卧室,这个壳子里还必须打包附送:SCU (Snoop Control Unit,负责多核间缓存一致性)、GIC (中断控制器)、L2 Cache控制器。因为多个核在一起工作,必须有这些管家来协调,否则数据就乱了。
当你看到 NXP 的 i.MX6ULL 手册说它是 Cortex-A7 时:
它买了 ARM 的 Cortex-A7 MPCore 授权。
但是出于成本和定位考虑,NXP 在设计时,在这个“套房”里只配置了 1 个核。
虽然只有1个核,它依然带着 MPCore 的外围电路(比如图中的 SCU 依然存在,只是只服务一个核)。
当你看到 树莓派2 (BCM2836) 也说是 Cortex-A7 时:
它也用了 Cortex-A7 MPCore 方案。
它在套房里塞满了 4 个核。
对比 Cortex-M3
Cortex-M3:通常设计用于简单的单核微控制器。
ARM 卖给 ST 的时候,基本就是卖那个“核”加上非常简单的调试接口。它不需要复杂的 SCU(缓存一致性单元),因为 M3 通常不跑多核 SMP(对称多处理)系统。
理解了这些之后,再回头看一下上面的MPcore的多核配置,就已经很清楚了。
Cortex-A 处理器运行模型
前面梳理了一下,架构-核心(处理器)-多处理器架构的关系
现在来看看具体的一个核心,又有那些东西,也就是我们A核的运行模型

以前的 ARM 处理器有 7 种运行模型:User、FIQ、IRQ、Supervisor(SVC)、Abort、Undef
和 System,其中 User 是非特权模式,其余 6 种都是特权模式。
但新的 Cortex-A 架构加入了TrustZone 安全扩展,所以就新加了一种运行模式:
Monitor新的处理器架构还支持虚拟化扩展,因此又加入了另一个运行模式:
Hyp
所以 Cortex-A7 处理器有 9 种处理模式
- 除了 User用户模式以外,其它 8 种运行模式都是特权模式。
- 这几个运行模式可以通过
软件进行任意切换,也可以通过中断或者异常来进行切换
用户模式(非特权模式)访问资源受限,所以当要切换模式的时候,应用程序可以产生异常,在异常的处理过程中完成处理器模式切换
每一种模式都有一组寄存器供异常处理程序使用,这样的目的是为了保证在进入异常模式以后,用户模式下的寄存器不会被破坏
STM32 只有两种运行模式,特权模式和非特权模式,但是 Cortex-A 就有 9 种运行模式
我们平常写stm32裸机程序,或者用freertos都是特权模式,都是在特权级-线程模式里面
当进入中断后,就是运行在特权级-handler模式
但是,当你用freertos-mpu这种要求安全的版本上,就会有非特权模式出现
Cortex-A 寄存器组
上面了解了处理器核心,有哪几种工作模式,其中,user模式是非特权模式,其他的都是特权模式。
下面来看看该核心,使用的寄存器组。
1. 对于每个工作模式来说,一共18个寄存器
ARM 架构提供了 16 个 32 位的通用寄存器(R0~R15)供软件使用
- 前 15 个(R0~R14)可以用作通用的数据存储,
R15 是程序计数器 PC,用来保存将要执行的指令。 - ARM 还提供了一个当
前程序状态寄存器 CPSR和一个备份程序状态寄存器 SPSR,SPSR 寄存器就是 CPSR 寄存器的备份
下图展示了,当核心处于一种工作模式时,所能使用的核心寄存器组,一共18个,可以看到,比较核心的特殊寄存器就是:
- SP(栈指针)
- LR(函数调用返回地址)
- PC(程序计数指针)
- CPSR(程序状态,可以控制核心工作模式,中断开关等)
- SPSR(备份程序状态,用于切换工作模式)

2. 核心寄存器的实际对应关系

蓝绿色是该工作模式独占,浅色表示共用一个物理寄存器。
为了节省资源,肯定不是每种工作模式,都配一组18个的物理寄存器。肯定有的是可以复用的。
可以看出:
- 低寄存器组 R0~R7是共享同一组物理寄存器的
- R8-R12, 除了FIQ有独立的寄存器外,其余的也都是共享一组物理寄存器
- SP,LR,除了sys和hyp模式外,都是独立拥有寄存器(每个模式有自己的栈指针和返回地址寄存器
- PC,所有模式共用一个物理寄存器
- CPSR,所有模式共用一个物理寄存器
- SPSR,每个模式都有自己的寄存器,用来保存本模式的当前cpu状态。
下面详细说说这些寄存器,在某些模式下有什么用,为什么要独占,为什么共享?
R0-R7,大家都共用。无特殊用途,也叫做未备份寄存器
R8-R12,可以看到只有
FIQ模式有自己的独占FIQ是快速中断模式,看名字就是知道这个中断模式要求快速执行,让这个模式有独占寄存器,就可以不用保存恢复现场了,非常迅速,从而加速中断的执行过程R13,SP,
- 基本上每种模式都有一个自己的 R13 物理寄存器,应用程序会初始化 R13,使其指向该模式专用的栈地址,这就是常说的初始化 SP 指针
R14 也称为
连接寄存器(LR),作用有两个- 每种处理器模式使用
R14(LR)来存放当前子程序的返回地址,如果使用BL或者BLX来调用子函数的话,R14(LR)被设置成该子函数的返回地址,在子函数中,将R14(LR)中的值赋给R15(PC)即可完成子函数返回 - 当异常发生以后,该异常模式对应的
R14寄存器被设置成该异常模式将要返回的地址, - R14 也可以当作普通寄存器使用
- 每种处理器模式使用
R15, PC, 大家共用
- 保存着当前执行的指令地址值加
8 个字节(一句指令占4字节 = 地址大小),这是因为 ARM的流水线机制导致的R15 (PC)值 = 当前执行的程序位置 +8 个字节
- 保存着当前执行的指令地址值加
CPSR, 大家共用,任何模式下被访问
- 包含了
条件标志位、中断禁止位、当前处理器模式标志等一些状态位以及一些控制位
- 包含了
SPSR
- 7 个模式每个都配备了一个专用的物理状态寄存器,叫做
SPSR(备份程序状态寄存器),当特定的异常 中断发生时,SPSR 寄存器用来保存当前程序状态寄存器CPSR的值,当异常退出以后可以用 SPSR 中保存的值来恢复 CPSR User和Sys这两个模式不是异常模式,所以并没有配备 SPSR
- 7 个模式每个都配备了一个专用的物理状态寄存器,叫做

上图,是CPSR的寄存器的内部结构。下面列出比较重要的位
- N(bit31) N=1 表示运算对的结果为负数
- Z(bit30) Z=1 表示运算结果为零, 对于
CMP指令,Z=1 表示进行比较的两个数大小相等- J(bit24) 和 T(bit5)位一起 表示当前所使用的指令集
- E(bit9):大小端控制位,E=1 表示大端模式,E=0 表示小端模式
- A(bit8):禁止异步中断位,A=1 表示禁止异步中断
- I(bit7):I=1 禁止 IRQ,I=0 使能 IRQ。
- F(bit6):F=1 禁止 FIQ,F=0 使能 FIQ
- M[4:0]:处理器模式控制位
可以看到,通过修改CPSR,就可以设置当前核心的工作模式,中断使能
以上,就讲解了,多核架构,单核模型,寄存器组。我们所有的底层都是围绕这些寄存器来进行的。包括后面的自己手写OS。这里,就是嵌入式所到达的最底层,再往下,就是芯片设计了。
汇编基础
下面,我们需要掌握汇编语言,因为汇编,是最贴近核心+寄存器底层的语言
我们只了解一些常用的指令即可
I.MX6U-ALPHA 使用的是 NXP 的 I.MX6UL 芯片,这是一款 Cortex-A7 内核的芯片,所以
我们主要讲的是 Cortex-A 的汇编指令
GNU 汇编语法
当你使用stm32的MDK和IAR里面的汇编,发现里面的语法不一样。
我们要编写的是 ARM汇编,编译使用的 GCC 交叉编译器,所以我们的汇编代码要符合 GNU 语法
GNU 汇编语法适用于所有的架构,并不是 ARM 独享的
1. GNU 汇编的基础语法如下:
label:instruction @ comment |
举例:
add: |
2. 用户可以使用.section 伪操作来定义一个段
.section .testsection @定义一个 testsetcion 段 |
举例:
.section .test |
3. 汇编系统预定义了一些段名
- .text 表示代码段
- .rodata 只读数据段
- .data 初始化的数据段
- .bss 未初始化的数据段
4. 汇编程序的默认入口标号是_start
.global _start |
我们也可以不用在代码里面用_start来指定程序的入口,也可以在链接脚本
lds中使用ENTRY来指明其它的入口点
5. 伪操作
.byte //定义单字节数据,比如.byte 0x12。 |
6. 函数
函数名: |
举例:
/* 未定义中断 */ |
Cortex-A7 常用汇编指令
这里的指令,就是上面GNU汇编语法里面的instruction部分了
处理器内部 数据传输指令
主要是:
- MOV(
<-)- 将数据从一个寄存器拷贝到另外一个寄存器
- 将一个立即数传递到寄存器里面
- MRS(
<-)- 读特殊寄存器(如 CPSR 和 SPSR)
- MSR(
<-)- 写特殊寄存器(如 CPSR 和 SPSR)
MOV R0,R1 //将寄存器 R1 中的数据传递给 R0,即 R0=R1 |
存储器访问指令
ARM 不能直接访问存储器,比如 RAM 中的数据
记住,核心能够直接访问的,只有自己的内核寄存器,访问RAM,要用存储器访问指令。常用的存储器访问指令有两种:
- LDR(
<-) - STR(
->)
LDR Rd, [Rn , |
LDR 和 STR 都是按照字(4byte)进行读取和写入的,也就是操作的 32 位数据
如果要按照字节(1byte)、半字(2byte)进行操作的话可以在指令“LDR”后面加上 B 或 H
- 按字节操作的指令就是
LDRB和STRB, - 按半字操作的指令就是
LDRH和STRH
压栈和出栈指令
A函数跳转到B函数,需要保存当前处理器状态(就是保存 R0~R15 这些寄存器值)
只涉及函数跳转,所以是16个寄存器,包括SP,LR,PC.
CPSR的保存发生在模式切换,且保存到SPSR内
当 B 函数执行完成以后再用前面保存的寄存器值恢复R0~R15 即可。保存 R0~R15 寄存器的操作就叫做现场保护,恢复 R0~R15 寄存器的操作就叫做
恢复现场
在进行现场保护的时候需要进行压栈(入栈)操作,恢复现场就要进行出栈操作
也就是说,这个现场,具体要保存在内存里面,这里的内存就是SP指针指向的栈空间
- 压栈的指令为
PUSH(<-) (从右往左,依次入栈) - 出栈的指令为
POP(->) (从左往右,依次出栈)
他们利用当前的栈指针 SP来生成地址
PUSH <reg list> //将寄存器列表存入栈中 |
栈空间的内存模型,一次压入一个寄存器,32位,4字节。
跳转指令
指令跳转:
- B(直接跳转,不返回)
- BL(直接跳转,返回)
- BX(间接跳转,不返回)
- BLX(间接跳转,返回)
L表示LR,表示记录下返回的地址
X表示寄存器,表示跳转地址记录在寄存器里面
这些跳转指令,后面接label,也就是地址/指令的别名,比如
main,reset_handler
当然还有直接跳转:直接向 PC 寄存器里面写入数据
算术运算指令 + 逻辑运算指令
这些都是加减法的运算指令,比如
ADD
SUB
等等,都是(<-)AND
ORR
BIC
ORN
EOR
都是(<-)
启动流程
上面,分析了处理器单核的运行模型,了解了配套的18个寄存器,然后补了一下GNU汇编基础。
下面,来分析一下,imx6ull的启动方式,看看裸机代码是如何跑起来的。
启动方式
首先,了解一下,imx6ull这块开发板支持的启动方式:
- FUSE启动
- 串行下载
- 通过otg1/uart 烧录到外部ROM里面,一般最终量产用
- 内部boot模式
- 芯片上电执行内部bootROM代码,进行硬件初始化
初始化时钟- 内核时钟ARM PLL 396Mhz
- System PLL 528Mhz
- USB PLL 480Mhz
- AHB 132Mhz
- IPG 66Mh
打开MMU和cache,加快执行速度(下载镜像,验证镜像,完成后关闭)中断向量偏移设置到bootROM的起始位置,启动用户代码后,重新设置中断向量偏移(用户代码开始的地方)
- bootROM从boot设备(emmc,sd卡)拷贝到RAM中(ddr)
- 通过拨码(硬件gpio选择),可以选择具体的启动设备(emmc,sd,eeprom,nandflash)
- 芯片上电执行内部bootROM代码,进行硬件初始化
镜像制作,代码烧录
我们自己编写了一个裸机的代码(start.S + main.c)
然后我们肯定会通过交叉编译工具,来编译出我们自己的bin文件。
- xxx-gcc .s/.c -> .o
- xxx-ld .o -> .elf (包含了运行地址:0x8780 0000)
- xxx-copy .elf -> bin (转换二进制格式)
之后,会用imxdownload来为我们的逻辑,添加一些头
- imxdownload .bin -> .imx = (IVT+BD+DCD+.bin)
学习
STM32的时候我们可以直接将编译生成的.bin文件烧写到 STM32内部 flash里面,I.MX6U 不能直接烧写编译生成的.bin 文件
新增的头包括:
IVT: Image vector table,IVT 里面包含了一系列的地址信息,这些地址信息在ROM 中按照固定的地址存放着Boot data,启动数据,包含了镜像要拷贝到哪个地址(运行地址),拷贝的大小是多少等等Device configuration data,简称 DCD,设备配置信息,重点是DDR3 的初始化配置
内部 BootROM 会将 load.imx 拷贝到 DDR 中,用户代码(.bin)是要一定要从 0X8780 0000 这个地方开始的
DDR的地址范围为 0x80000000-xxxxxxxx
因为链接地址为 0X87800000,load.imx 在用户代码前面又有 3KByte 的 IVT+Boot Data+DCD 数据,下面会讲为什么是 3KByte,因此 load.imx 在 DDR 中的起始地址就是 0X8780 0000-3072=0X877F F400
前面这
3K字节的头(IVT这些),是给bootROM看的。(有3千个字节,还是很多的。)
以上,我们已经知道了,镜像的内部结构,以及镜像在运行地址的分布情况。下面,具体分析一下这些头的作用
IVT 部分
IVT 包含了:
- 镜像程序的入口点
- 指向 DCD 的指针
- 一些用作其它用途的指针。
bootROM要求IVT应该放到指定的位置,不同的启动设备位置不同,而 IVT 在整个 load.imx 的最前面,其实就相当于要求 load.imx 在烧写的时候应该烧写到存储设备的指定位置去。整个位置都是相对于存储设备的起始地址的偏移
(加载地址计算) 以
SD/EMMC为例,IVT 偏移为1Kbyte,IVT+Boot data+DCD 的总大小为 4KByte-
1KByte=3KByte。假如 SD/EMMC每个扇区为 512 字节,那么 load.imx 应该从第三个扇区开始
烧写,前两个扇区要留出来,这样,这个才算一个合格的imx镜像,bootrom才愿意去读取
下面来看一下,IVT内部的具体内容,因为这三个头,一共占3k字节。具体内容如下:
这里要事先说明,我们在编译链接阶段里面,让bin有地址信息,但是
bootROM,是无法解析bin文件的,所以对bootROM来说,bin就是一串要拷贝的数据。你用
arm-linux-gnueabihf-objcopy将 .elf 转换成 .bin 烧录到 SD 卡时,ELF里的符号表、链接地址(0x87800000)等所有元数据都被彻底丢弃了。SD 卡里躺着的只是纯粹的机器指令既然 .bin 自己不带说明书,NXP 就设计了 IVT 这个强制规范。你必须在二进制最开头显式告诉硬件瞎子:“搬完代码后,PC 指针去 0x87800000 执行”。
IVT
header (
BootROM 靠它来识别这块数据是不是合法的 IVT)- 0xD1
- IVT的长度
- version
入口地址:
- 程序入口绝对地址(
DDR地址)。BootROM 把你的代码搬运完毕后,PC 指针跳转去执行的第一个指令地址(通常就是你 start.S 中 _start 的链接物理地址,0x87800000)entry (入口地址):
0X8780 0000,也就是代码镜像第一行指令链接的位置。
- 程序入口绝对地址(
dcd:
- 设备配置数据(DCD)绝对地址(
DDR地址),让 BootROM 在跳转执行你的代码前帮你初始化 DDR 等核心外设,这样你的程序一启动就能直接跑在 DDR 里dcd (DCD 地址):
0X877F F42C
- 设备配置数据(DCD)绝对地址(
boot data:Boot Data 绝对地址(
DDR地址)。包含镜像的整体搬运信息(要把整个固件搬运到内存的哪个源地址、目标地址,以及固件总长度)boot data (Bd 地址):
0X877F F420self:IVT 本身的绝对地址(
DDR地址)。供 BootROM 内部做基址偏移计算用。
self (IVT 地址):
0X877F F400,表中明确标注这是“IVT 复制到 DDR 中以后的首地址”。
这些地址是怎么被 imxdownload 算出来的?
它是以你给定的
链接地址(0x87800000)为基准,逆向推导出来的:确定 IVT 首地址(self):已知镜像代码起始地址是 0X87800000,而前面预留给 IVT + Boot Data + DCD 的空间刚好是
3KByte (0xC00),所以 IVT 的首地址 self 就是 0X87800000 - 0xC00 =0X877FF400。确定 DCD 地址:已知
IVT 的首地址是 0X877FF400,而通过查表得知 DCD 数据相对首地址的偏移量是 0X2C,所以 dcd 的绝对地址就是 0X877FF400 + 0X2C = 0X877FF42C。确定 Boot Data 搬运地址:因为 i.MX6ULL 从 SD 卡启动时,硬件要求镜像前面必须空出 1KByte (0x400) 的偏移量。所以整个 load.imx 最终在 DDR 里的总起始地址 start 是 0X877FF400 - 0x400 = 0X877FF000。
解开最大的悖论:DDR 还没初始化,BootROM 怎么去 DDR 读 DCD?
这正是 NXP 硬件设计的精妙之处,也是我之前表述偏颇的地方。实际的启动搬运过程分为两步:
第一步(临时解析配置):上电时,BootROM 确实无法访问 DDR。它会先把 SD 卡最前面这几 KB(包含
IVT和DCD)读取到芯片内部的 SRAM (OCRAM)中。BootROM 在内部 SRAM 里解析并执行这部分 DCD 数据,从而把外部 DDR 的时序、引脚等全部初始化完毕。第二步(整体搬运到目的地):DDR 初始化成功可用后,BootROM 才会去读取
Boot Data里的指令。此时,它按照 start = 0X877FF000 和设定的长度 length = 0X00200000 (2MByte),将 SD 卡上的整个镜像(包括那个 1KB 空白、IVT、DCD 以及你的 LED 代码),完整地搬运到 DDR 的目标地址中。
Boot Data 数据
这个里面就是所谓的,整个imx镜像文件,的源地址(外部ROM),目的地址(DDR),拷贝大小。
这个不用多说了。给bootROM看的,用来拷贝用。
DCD 数据
这个里面,也是给bootROM看的,用来初始化ddr。
复位以后,I.MX6U 片内的所有寄存器都会复位为默认值,但是这些默认值往往不是我们
想要的值,而且有些外设我们必须在使用之前初始化它。
为此 I.MX6U 提出了一个 DCD(Device Config Data)的概念
DCD 其实就是 I.MX6U 寄存器地址和对应的配置信息集合,Boot ROM 会使用这些寄存器地址和配置集合来初始化相应的寄存器,比如开启某些外设的时钟、初始化 DDR 等等
DCD 区域不能超过 1768Byte
我们的imxdownload里面,对dcd的内容有:
- 设置 CCGR0~CCGR6 这 7 个外设时钟使能寄存器,默认打开所有的外设时钟。
- 配置 DDR3 所用的所有 IO。
- 配置 MMDC 控制器,初始化 DDR3。
总结
// 这是我对豆包的描述 |
你的理解总结(我先复述一遍,确保我没听错)
- i.MX6ULL 上电 → PC 指向芯片
内部 BootROM(出厂固化,不可改) - BootROM 先从外部 Flash(SPI/NAND/SD 等) 读取:
- IVT
- Boot Data
- DCD
把这头信息读到
内部 RAM
- 解析:
- IVT:告诉 BootROM 各种表、程序的地址
- DCD:DDR 初始化参数
- Boot Data:告诉从哪拷贝、拷到 DDR 哪里、拷多大
BootROM做:- 用
DCD初始化 DDR - 按
BootData把整个镜像搬到 DDR(你说的 0x87800000)
- 用
- 然后 PC 跳
0x87800000执行用户代码 - 用户代码里:
- 设 CPU 状态
- 初始化 SP,准备 C 环境
- 跳 C 入口
- C 里初始化时钟、外设…
流程图:
上电复位 |
注意,我们的链接地址
0x87800000这个不是固定的,是我们的lds链接脚本决定的。然后imxdownload会根据这个来生成我们的IVT,指向lds里面设置的链接地址。
1. 你写 .lds 链接脚本 |
内存分布,对比stm32
我们先复习一下stm32的启动前后,内存的分布情况。由于stm32f103只有内部ram, 内部rom
stm32的启动
1. 基础地址(STM32F103ZE)
- 内部 Flash (ROM):
0x0800 0000~0x0807 FFFF(512KB) - 内部 SRAM (RAM):
0x2000 0000~0x2000 FFFF(64KB) - 向量表:固定放在
Flash起始 - start.s:做的核心事:初始化栈、拷贝.data、清零.bss
2. 上电瞬间,(刚复位,还没有执行start.s)
//内部rom flash |
3. 执行 start.s 过程中(关键三步)
- 设置栈指针 SP = 0x2000FFFF(或稍小)
- 从 Flash 拷贝
.data到RAM RAM中.bss段清零
4. start.s 执行完成后(进入 main 前)
//内部rom flash |
疑问1: .data,.bss的拷贝时机
我已经知道了,上电开始执行,CM3读取0x0800 0000的4字节地址,作为SP指针,然后,读取0x0800 0004的4字节地址,作为PC指针。
我知道这个是硬件自动执行的。我很好奇,这个RAM里面的.data, .bss这个是什么时候干的?是硬件自动执行的吗?我知道在写程序的时候,MDK里面会有ld链接脚本,规划好了栈空间在RAM的末尾,然后.data, .bss在RAM的低地址。但是,我很好奇,这个是什么时机开始弄得?
就是说,栈空间,我们在链接脚本lds里面,已经规划好了,放到RAM的最后,SP指针,硬件自动设置了。但是开头的.data, .bss是在什么时候拷贝过来的?
首先说明,.data, .bss不是硬件自动执行的。因为这也是c语言的执行环境的一部分,所以需要软件设置,
所以在start.s中设置。
他的具体操作为:
Reset_Handler: |
你看,这就是你 “没看见” 的那部分代码:
- .data:是从 Flash 里的 “备份” 拷贝到 RAM 里的
- .bss:是在 RAM 里把一段空间全部写 0
链接脚本(.ld/MDK 里的设置)又起什么作用?
链接脚本的作用是 “规划”,而不是 “执行”:
- 它告诉编译器:
.text(代码)放在 Flash 哪里.data(有初值的全局变量)在 Flash 里的备份位置,以及在 RAM 里的运行位置.bss(无初值的全局变量)在 RAM 里的位置栈顶(_estack)放在 RAM 的末尾
- 它会生成一些符号,比如
_sidata,_sdata,_edata,_sbss,_ebss,_estack,这些符号就是上面 start.S 里用到的地址。
所以整个流程是:
链接脚本:画好地图(内存布局)硬件:只负责把 PC 和 SP 指向地图的入口start.S:拿着地图,一步一步把东西搬到正确的地方
那为什么stm32的start.s中没有看到实际拷贝.data, .bss的部分?
下面是stm32的demo的start.s中的PC执行的部分,没有拷贝的指令,但是实际上是拷贝了。
Reset_Handler PROC |
原因:
__main 是 ARM 编译器(ARMCC)提供的C 库启动函数,它藏在编译器的运行时库(RTL)里,你看不到源码
实际流程:
Reset_Handler → SystemInit → __main → 你的main() |
简单说:MDK 把 “拷贝.data/ 清零.bss” 这些脏活,都封装到了 __main 里,所以你在 start.s 里看不到,但它确实执行了。
汇编led点灯 裸机代码分析
下面分析一下,汇编版本的led点灯代码
Makefile
led.bin:led.s |
led.s
/************************************************************** |
这个就是非常简单的一个汇编代码,所以也不需要c语言的环境
实际内存分变化:
1. 上电前(仅外部 ROM 有数据,DDR 空)
┌─────────────────────────────────┐ 外部ROM (SD卡) |
2. 上电后(BootROM 完成搬运,DDR 有指令)
┌─────────────────────────────────┐ 外部ROM (SD卡) |
3. 最终执行阶段(PC 跳转到 DDR 执行)
┌─────────────────────────────────┐ DDR |
代码特性:这段汇编只有指令(.text 段),没有定义任何全局变量、静态变量,自然不需要.data(存初始化值)和.bss(存未初始化值);
启动简化:因为没有.data/.bss,你的汇编代码里也不需要像 STM32 那样做 “拷贝 / 清零” 操作,_start 直接开始配置硬件 —— 这是裸机汇编最简洁的优势
c语言led点灯 裸机代码分析
下面看看c语言版本的逻辑代码
Makefile
objs := start.o main.o |
lds链接脚本
SECTIONS{ |
可以看到,lds指定了ddr里面的内存分布情况
start.s
.global _start /* 全局标号 */ |
main.c
int main(void) |
注意,我们的imx6ull,是会把镜像文件全部加载到DDR里面来运行的,所以没有拷贝.data这些。但是真实原因是:
先回顾 STM32 要拷贝的根本原因
STM32 的.data段有两个地址:
- 加载地址(LMA):Flash 里(0x0800xxxx)—— 存初始值
- 运行地址(VMA):RAM 里(0x2000xxxx)—— 程序运行时要访问的地址
所以必须把 Flash 里的初始值拷贝到 RAM,程序才能读到正确的全局变量值。
再看 i.MX6ULL 的关键差异
你这份代码里,.data/.bss的加载地址 = 运行地址(都是 DDR 的 0x8780xxxx):
- BootROM 做的事:把外部 ROM 里的 “整个镜像(包括.text/.rodata/.data)”原样搬到 DDR 的 0x87800000 开始的地址;
- 搬运完成后,.data 段已经在 “运行地址” 上了,且初始值也一起搬过来了 ——不需要二次拷贝;
- 而
.bss段因为没有初始值,BootROM 搬运时不会处理(DDR 里还是随机值),你这份代码刚好没用到全局变量,所以没出问题。所以这个c语言的裸机点灯代码,有隐患
所以,需不需要拷贝的原因,是看
加载地址≠运行地址?
- 是 → 必须拷贝
- 否 → 无需拷贝
你这份代码没出问题,是因为main.c 里没有定义全局变量 / 静态变量:
- 如果定义
int g_led_flag = 1;(.data 段):BootROM 会把初始值 1 一起搬到 DDR,main 里能读到正确值; - 如果定义
int g_delay_cnt;(.bss 段):DDR 里这个地址是随机值,main 里用这个变量会导致延时 / 点灯异常;
正确的做法是:
- 在start.s的b main前,加一段清零.bss 的代码(因为.bss 段没有初始值,BootROM 不会处理)
_start: |
和 STM32 的本质区别:
- STM32 是 “Flash 存初始值,RAM 跑程序”(需要软件拷贝),
- i.MX6ULL 是 “外部 ROM 存初始值,DDR 跑程序”(BootROM 一次性搬过去,无需二次拷贝)
- 但是记住,bss段都需要清零,否则容易有异常风险
下面展示内存的分布变化
1. 上电前(仅外部 ROM 有数据,DDR 全空)
┌─────────────────────────────────────────┐ 外部ROM (SD卡) |
2. 上电后 → BootROM 完成搬运(未执行 start.s)
┌─────────────────────────────────────────┐ 外部ROM (SD卡) |
3. 执行 start.s(清零.bss + 设置栈)
┌─────────────────────────────────────────┐ DDR |
4. 跳转到 main 执行(点灯循环)
┌─────────────────────────────────────────┐ DDR |
这里有一个疑问,
为什么stm32中,start.s中,有分配栈的大小,且栈被lds分配到ram的最后部分,然后
.data,.bss在ram的前部。而在imx6ull这里,因为DDR的地址范围
0x80000000 - xxxxxxxx. 但是栈在0x80000000 - 0x80200000,在前面部分。, 而.text, .data这些在0x87800000这里的后面部分。
A 核裸机栈管理的 “隐形规则”
栈的 “实际可用大小” 不是由链接脚本 / 代码定义的,而是由栈顶地址和下一个可用内存地址的间距决定的
中断
下面来复盘一下中断,这个结束后,所有的裸机外设就结束了
stm32中断复盘
1. 中断向量表
- 中断向量就是中断程序的入口地址
- 中断服务程序在中断向量表中的位置由半导体厂商定好
- 中断向量表在整个程序的最前面
- __initial_sp就是第一个中断向量,存放栈顶指针
- 第二个是reset_handler
Cortex-M 架构引入了一个新的概念——中断向量表偏移
通过中断向量表偏移就可以将中断向量表存放到任意地址处,中断向量表偏移配置在函数
SystemInit中完成,通过向SCB_VTOR寄存器写入新的中断向量表首地址即可
SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; //第 8 行是将中断向量表设置到 RAM 中 |
基本都是将中断向量表设置到 ROM 中,也就是地址0X8000000 处
我们了解了两个跟 STM32 中断有关的概念:中断向量表和中断向量表偏移,那么这个跟 I.MX6U 有什么关系呢
因为 I.MX6U 所使用的 Cortex-A7 内核也有中断向量表和中断向量表偏移,而且其含义和 STM32 是一模一样的!只是用到的寄存器不同而已,概念完全相同
2. NVIC(内嵌向量中断控制器)
中断系统得有个管理机构,对于 STM32 这种 Cortex-M 内核的单片机来说这个管理机构叫做 NVIC,全称叫做 Nested Vectored Interrupt Controller。
Cortex-A 内核的中断管理机构不叫做NVIC,而是叫做 GIC,全称是 general interrupt controller
3. 中断使能
要使用某个外设的中断,肯定要先使能这个外设的中断,就是打开NVIC的中断:
- 设置中断优先级
- 使能中断
同理,如果要使用 I.MX6U 的某个中断的话也需要使能其对应的中断
4. 中断服务函数
这个就是具体跳转的中断处理函数了,stm32里面,会把中断处理函数重定向,指向HAL里面来进行中断的处理。
imx里面,就要先了解中断的机制。
Cortex-A7 中断系统简介
跟 STM32 一样,Cortex-A7 也有中断向量表,中断向量表也是在代码的最前面。CortexA7 内核有 8 个异常中断

中断向量表里面都是中断服务函数的入口地址, Cortex-A7 一共有 8 个中断,而且还有一个中断向量没有使用,实际只有 7 个中断
下面简单介绍一下这7个中断:
- 复位中断(Rest)
- CPU 复位以后就会进入复位中断,我们可以在复位中断服务函数里面做一些初始化工作,比如初始化
SP指针、DDR等等
- CPU 复位以后就会进入复位中断,我们可以在复位中断服务函数里面做一些初始化工作,比如初始化
- 未定义指令中断(Undefined Instruction)
- 如果指令不能识别的话就会产生此中断
- 软中断(Software Interrupt,SWI)
- 由
SWI指令引起的中断,Linux 的系统调用会用 SWI指令来引起软中断,通过软中断来陷入到内核空间
- 由
- 指令预取中止中断(Prefetch Abort)
- 预取指令的出错的时候会产生此中断
- 数据访问中止中断(Data Abort)
- 访问数据出错的时候会产生此中断
- IRQ 中断(IRQ Interrupt)
- 外部中断,芯片内部的外设中断都会引起此中断的发生
- FIQ 中断(FIQ Interrupt)
- 快速中断,如果需要快速处理中断的话就可以使用此中断。
这个就是 Cortex-A 和 Cotex-M 在中断向量表这一块的区别
- 对于
Cortex-M内核来说,中断向量表列举出了一款芯片所有的中断向量,包括芯片外设的所有中断 Cortex-A内核 CPU 的所有外部中断都属于这个IRQ中断,当任意一个外部中断发生的时候都会触发 IRQ 中断- 在 IRQ 中断服务函数里面就可以读取指定的寄存器来判断发生的具体是什么中断,进而根据具体的中断做出相应的处理
具体外部中断如下
所以我们需要在 IRQ 中断服务函数中判断究竟是左侧的哪个中断发生了,然后再做出具体的处理
我们常用的就是复位中断和 IRQ 中断,所以我们需要编写这两个中断的中断服务函数,稍后我们会讲解如何编写对应的中断服务函数
创建中断向量表
首先我们要根据表 17.1.2.1的内容来创建中断向量表,中断向量表处于程序最开始的地方,比如我们前面例程的 start.S 文件最前面,中断向量表如下
|
_start开头的跳转指令,就是我们的中断向量表了。
我们实际需要编写的只有复位中断服务函数 Reset_Handler 和 IRQ 中断服务函数 IRQ_Handler,其它的中断本教程没有用到,所以都是死循环。
在编写复位中断复位函数和 IRQ 中断服务函数之前我们还需要了解一些其它的知识,否则的话就没法编写
GIC 控制器
总览
STM32(Cortex-M)的中断控制器叫做 NVIC,I.MX6U(Cortex-A)的中断控制器叫做 GIC
GIC 是 ARM 公司给 Cortex-A/R 内核提供的一个中断控制器,类似 Cortex-M 内核中的NVIC
目前 GIC 有 4 个版本:V1~V4
- GIC V2 是给
ARMv7-A架构使用的,比如 Cortex-A7、Cortex-A9、Cortex-A15 等 - V3 和 V4 是给
ARMv8-A/R架构使用的,也就是64 位芯片使用的
因此我们主要讲解 GIC V2。GIC V2 最多支持 8 个核
ARM 会根据 GIC 版本的不同研发出不同的 IP 核,那些半导体厂商直接购买对应的 IP 核即可。
比如 ARM 针对 GIC V2 就开发出了 GIC400 这个中断控制器 IP 核
当 GIC 接收到外部中断信号以后就会报给 ARM 内核。但是ARM 内核只提供了四个信号给 GIC 来汇报中断情况:
VFIQ:虚拟快速 FIQVIRQ:虚拟外部 IRQFIQ:快速中断 IRQIRQ:外部中断 IRQ注意,GIC是接收外部中断的,内核系统中断和CM3一样,是由单独寄存器控制的。

VFIQ 和 VIRQ 是针对虚拟化的,我们不讨论虚拟化,剩下的就是 FIQ 和 IRQ 了
我们只使用 IRQ,所以相当于 GIC 最终向 ARM 内核就上报一个 IRQ信号, 那么GIC是如何向内核上报IRQ信息的呢?
框图

左侧部分就是中断源,中间部分就是 GIC 控制器,最右侧就是中断控制器向处理器内核发送中断信息。我们重点要看的肯定是中间的 GIC 部分,GIC 将众多的中断源分为分为三类:
- SPI(Shared Peripheral Interrupt),共享中断
- 顾名思义,所有 Core 共享的中断,这个是最常见的,那些外部中断都属于 SPI 中断。
- 比如按键中断、串口中断等等
- 这些中断所有的 Core 都可以处理,不限定特定 Core。
- PPI(Private Peripheral Interrupt),私有中断
- 我们说了
GIC是支持多核的,每个核肯定有自己独有的中断。这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断
- 我们说了
- SGI(Software-generated Interrupt),软件中断
- 由软件触发引起的中断,通过向寄存器
GICD_SGIR写入数据来触发,系统会使用 SGI 中断来完成多核之间的通信。
- 由软件触发引起的中断,通过向寄存器
中断 ID
中断源有很多,为了区分这些不同的中断源肯定要给他们分配一个唯一 ID,这些 ID 就是中断 ID
每一个 CPU 最多支持 1020 个中断 ID,中断 ID 号为 ID0~ID1019。这 1020 个 ID 包
含了 PPI、SPI 和 SGI, 分配如下
ID0~ID15:这 16 个 ID 分配给 SGIID16~ID31:这 16 个 ID 分配给 PPIID32~ID1019:这 988 个 ID 分配给 SPI- 像 GPIO 中断、串口中断等这些外部中断,
- 至于具体到某个 ID 对应哪个中断那就由半导体厂商根据实际情况去定义了
比如 I.MX6U 的总共使用了 128 个中断 ID,加上前面属于 PPI 和 SGI 的 32 个 ID,I.MX6U 的中断源共有 128+32=160个,这 128 个中断 ID 对应的中断在《I.MX6ULL 参考手册》的“3.2 CortexA7 interrupts”小节

在SDK的芯片定义文件 MCIMX6Y2C.h 中有列出所有的中断ID
GIC 逻辑分块
GIC 架构分为了两个逻辑块:
- Distributor 分发器端
- 此逻辑块负责处理各个中断事件的分发问题,也就是中断事件应该发送到哪个 CPU Interface 上去
- 分发器收集所有的中断源,可以控制每个中断的优先级,它总是将优先级最高的中断事件发送到 CPU 接口端
- 分发器端要做的主要工作如下:
- 全局中断使能控制
- 控制每一个中断的使能或者关闭
- 设置每个中断的优先级
- 设置每个中断的目标处理器列表
- 设置每个外部中断的触发模式:电平触发或边沿触发
- 设置每个中断属于组 0 还是组 1
- CPU Interface,CPU 接口端
- 和 CPU Core 相连接的
- 每个 CPU Core 都可以在 GIC 中找到一个与之对应的 CPU Interface
- 是分发器和 CPU Core 之间的桥梁
- CPU 接口端主要工作如下
- 使能或者关闭发送到 CPU Core 的中断请求信号
- 应答中断
- 通知中断处理完成
- 设置优先级掩码,通过掩码来设置哪些中断不需要上报给 CPU Core
- 定义抢占策略
- 当多个中断到来的时候,选择优先级最高的中断通知给 CPU Core
在内核CA7的核心定义文件中,有定义GIC的寄存器描述,列举出了 GIC 控制器的所有寄存器,可以通过结构体 GIC_Type 来访问 GIC 的所有寄存器
获取到 GIC 基地址以后只需要加上结构体内部偏移,就可以访问对于的寄存器
那么,GIC 控制器的寄存器基地址在哪里呢?这个就需要用到 Cortex-A 的 CP15 协处
理器了
CP15 协处理器
关于 CP15 协处理器和其相关寄存器的详细内容请参考下面两份文档:
《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf》第 1469 页“B3.17 Oranization of the CP15 registers in a VMSA implementation”。
《Cortex-A7 Technical ReferenceManua.pdf》第
55 页“Capter 4 System Control”。, TRM,技术参考手册
CP15 协处理器一般用于存储系统管理,但是在中断中也会使用到。CP15 协处理器一共有16 个 32 位寄存器,CP15 协处理器的访问通过如下另个指令完成:
- MRC: 将 CP15 协处理器中的寄存器数据读到 ARM 寄存器中。
- MCR: 将 ARM 寄存器的数据写入到 CP15 协处理器寄存器中。
- MRC 就是读 CP15 寄存器,
- MCR 就是写 CP15 寄存器,MCR 指令格式如下:
MCR{cond} p15, <opc1>, <Rt>, <CRn>, <CRm>, <opc2> |
MRC 的指令格式和 MCR 一样,只不过在 MRC 指令中 Rt 就是目标寄存器,也就是从
CP15 指定寄存器读出来的数据会保存在 Rt 中。而 CRn 就是源寄存器,也就是要读取的写处
理器寄存器
假如我们要将 CP15 中 C0 寄存器的值读取到 R0 寄存器中,那么就可以使用如下命令
>MRC p15, 0, r0, c0, c0, 0
CP15 协处理器有 16 个 32 位寄存器,c0~c15,本章来看一下 c0、c1、c12 和 c15 这四个寄
存器,因为我们本章实验要用到这四个寄存器,其他的寄存器大家参考上面的两个文档即可
c0 寄存器
CP15 协处理器有 16 个 32 位寄存器,c0~c15,在使用 MRC 或者 MCR 指令访问这 16 个
寄存器的时候,指令中的 CRn、opc1、CRm 和 opc2 通过不同的搭配,其得到的寄存器含义是不同的

在图 17.1.4.1 中当 MRC/MCR 指令中的 CRn=c0,opc1=0,CRm=c0,opc2=0 的时候就表示
此时的 c0 就是 MIDR 寄存器,也就是主 ID 寄存器,这个也是 c0 的基本作用.
c0 作为 MDIR 寄存器的时候其含义

- bit31:24:厂商编号,0X41,ARM。
- bit23:20:内核架构的主版本号,ARM 内核版本一般使用 rnpn 来表示,比如 r0p1,其中 r0后面的 0 就是内核架构主版本号。
- bit19:16:架构代码,0XF,ARMv7 架构。
- bit15:4:内核版本号,0XC07,Cortex-A7 MPCore 内核。
- bit3:0:内核架构的次版本号,rnpn 中的 pn,比如 r0p1 中 p1 后面的 1 就是次版本号
c1 寄存器
c1 寄存器同样通过不同的配置,其代表的含义也不同, 具体对应关系不多展示。
当 MRC/MCR 指令中的 CRn=c1,opc1=0,CRm=c0,opc2=0 的时候就表示
此时的 c1 就是 SCTLR 寄存器,也就是系统控制寄存器,这个是 c1 的基本作用。SCTLR 寄存器主要是完成控制功能的

CP15协处理器,相当于内核控制寄存器了。有点类似CPSR
如果要读写 SCTLR 的话,就可以使用如下命令:
MRC p15, 0, <Rt>, c1, c0, 0 //读取 SCTLR 寄存器,数据保存到 Rt 中。 |
c12 寄存器
c12 寄存器通过不同的配置,其代表的含义也不同,具体对应关系不多展示
当 MRC/MCR 指令中的 CRn=c12,opc1=0,CRm=c0,opc2=0 的时候就表示此时 c12 为 VBAR 寄存器,也就是向量表基地址寄存器。设置中断向量表偏移的时候就需要将新的中断向量表基地址写入 VBAR 中
代码链接的起始地址为0X87800000,而中断向量表肯定要放到最前面,也就是 0X87800000 这个地址处。所以就需要设置 VBAR 为 0X87800000,设置命令如下
ldr r0, =0X87800000 //r0=0X87800000 |
c15 寄存器
c15 寄存器也可以通过不同的配置得到不同的含义.
我们需要 c15 作为 CBAR 寄存器,因为 GIC 的基地址就保存在 CBAR中,我们可以通过如下命令获取到 GIC 基地址:
MRC p15, 4, r1, c15, c0, 0 //获取 GIC 基础地址,基地址保存在 r1 中。 |
获取到 GIC 基地址以后就可以设置 GIC 相关寄存器了:
- 比如我们可以读取当前中断 ID,当前中断 ID 保存在
GICC_IAR中, 寄存器 GICC_IAR 属于 CPU 接口端寄存器,寄存器地址相对于 CPU 接口端起始地址的偏移为 0XC,因此获取当前中断 ID 的代码如下
MRC p15, 4, r1, c15, c0, 0 //获取 GIC 基地址 |
关于 CP15 协处理器就讲解到这里,简单总结一下,
- 通过 c0 寄存器可以获取到处理器内核信息
- 通过 c1 寄存器可以使能或禁止 MMU、I/D Cache 等
- 通过 c12 寄存器可以设置中断向量偏移;
- 通过 c15 寄存器可以获取 GIC 基地址。
关于 CP15 的其他寄存器,大家自行查阅本节前面列举的 2 份 ARM 官方资料
中断使能
中断使能包括两部分,
- 一个是 IRQ 或者 FIQ 总中断使能,
- 另一个就是 ID0~ID1019 这 1020个中断源的使能
IRQ 和 FIQ 总中断使能
就类似家里买的进户总电闸, 然后ID0~ID1019 这 1020 个中断源就类似家里面的各个电器开关
因此要想使用 I.MX6U 上的外设中断就必须先打开 IRQ 中断(本教程不使用
FIQ)
寄存器 CPSR 的
- I=1 禁止 IRQ,
- I=0 使能 IRQ
- F=1 禁止 FIQ
- F=0 使能 FIQ
ID0~ID1019 中断使能和禁止
GIC 寄存器 GICD_ISENABLERn 和 GICD_ICENABLERn 用来完成外部中断的使能和禁止,对于 Cortex-A7 内核来说中断 ID 只使用了 512 个。一个 bit 控制一个中断 ID 的使能,那么就
- 需要 512/32=16 个
GICD_ISENABLER寄存器来完成中断的使能 - 需要 16
个GICD_ICENABLER寄存器来完成中断的禁止
其中
GICD_ISENABLER0的 bit[15:0]对应ID15~0 的 SGI 中断GICD_ISENABLER0的 bit[31:16]对应 ID31~16 的 PPI 中断。- 剩下的
GICD_ISENABLER1~GICD_ISENABLER15就是控制 SPI 中断的
中断优先级设置
优先级数配置
Cortex-M 的中断优先级分为抢占优先级和子优先级,两者是可以配置的
Cortex-A7 的中断优先级也可以分为抢占优先级和子优先级,两者同样是可以配置的
GIC 控制器最多可以支持 256 个优先级,数字越小,优先级越高,Cortex-A7 选择了 32 个优先级
在使用中断的时候需要初始化 GICC_PMR 寄存器,此寄存器用来决定使用几级优先
级


I.MX6U 是 Cortex-A7内核,所以支持 32 个优先级,因此
GICC_PMR要设置为0b11111000
抢占优先级和子优先级位数设置
抢占优先级和子优先级各占多少位是由寄存器 GICC_BPR 来决定的

一般将所有的中断优先级位都配置为抢占优先级,比如 I.MX6U 的优先级位数为 5(32 个优先级),所以可以设置 Binary point 为 2,表示 5 个优先级位全部为抢占优先级
优先级设置
前面已经设置好了 I.MX6U 一共有 32 个抢占优先级,数字越小优先级越高。具体要使用某个中断的时候就可以设置其优先级为 0~31。
某个中断 ID 的中断优先级设置由寄存器D_IPRIORITYR 来完成,前面说了 Cortex-A7 使用了 512 个中断 ID,每个中断 ID 配有一个优先级寄存器,所以一共有 512 个 D_IPRIORITYR 寄存器。
如果优先级个数为 32 的话,使用寄存器 D_IPRIORITYR 的 bit7:4 来设置优先级,也就是说实际的优先级要左移 3 位。
比如要设置ID40 中断的优先级为 5
GICD_IPRIORITYR[40] = 5 << 3; |
中断优先级设置总结
有关优先级设置的内容就讲解到这里,优先级设置主要有三部分
- 设置寄存器 GICC_PMR,配置优先级个数,
- 比如 I.MX6U 支持 32 级优先级
- 设置抢占优先级和子优先级位数
- 一般为了简单起见,会将所有的位数都设置为抢占优先级
- 设置指定中断 ID 的优先级,也就是设置外设优先级
可以看出,整体流程,和stm32差不多。流程都一样,只是寄存器有些差异罢了
gpio中断实验
start.s
/*************************************************************** |
Reset_Handler
- 写CPSR,关闭 IRQ
- 写CP15的C1,关闭 I/D Cache、MMU、对齐检测和分支预测
- 设置不同模式下的 sp 指针
- IRQ 模式, 0X80600000,大小为2MB
- SYS 模式, 0X80400000,大小为2MB
- SVC 模式, 0X80200000,大小为2MB
- 写CPSR,重新打开IRQ 中断
- 跳转到main函数
注意,这里无需拷贝.bss, .data,因为bootrom已经帮你做了
中断服务函数 IRQ_Handler, 所有的外部中断最终都会触发 IRQ 中断.
所以 IRQ 中断服务函数主要的工作就是:
保存现场
- LR入栈
- R0-R3,R12入栈
- SPSR 入栈
区分当前发生的什么中断(中断 ID)?
- 读CP15的R0,获得GIC基地址
- 读GIC的IAR地址到R1,得到中断ID到R0
- R0,R1入栈
然后针对不同的外部中断做出不同的处理(SVC模式)
- 切换SVC管理员模式
- 保存LR入栈
- 跳转
system_irqhandler,中断ID作为参数保存至R0 - 执行完毕
- LR出栈
恢复现场
- 切换IRQ模式
- 出栈R0,R1(GIC的寄存器们)
- 写GIC的EOIR,中断执行完成
- (当一个中断处理完成以后必
须向 GICC_EOIR 寄存器写入其中断号表示中断处理完成)
- (当一个中断处理完成以后必
- 恢复SPSR
- 恢复R0-R3,R12
- 恢复LR
- 跳转PC->LR-4,返回执行。
关于为什么要LR-4赋值给PC
0X2000 MOV R1, R0 ;执行 |
ARM 的指令是三级流水线:取指、译指、执行,pc 指向的是正在取值的地址
当前正在执行 0X2000地址处的指令“MOV R1, R0”,但是 PC 里面已经保存了 0X2008 地址处的指令“MOV R4, R5”。
假设此时发生了中断,中断发生的时候保存在 lr 中的是 pc 的值,也就是地址 0X2008。
当中断处理完成以后肯定需要回到被中断点接着执行,如果直接跳转到 lr 里面保存的地址处(0X2008)开始运行,那么就有一个指令没有执行,那就是地址 0X2004 处的指令“MOV R2, R3”,显然这是一个很严重的错误!
所以就需要将 lr-4 赋值给 pc,也就是 pc=0X2004,从指令“MOV R2,R3”开始执行
关于模式的切换
- IRQ 模式:专门处理普通中断,需要独立栈
- SVC 模式:操作系统内核 / 裸机程序的核心运行模式(特权模式),主程序(main函数)最终运行在 SVC 模式,需要独立且足够大的栈
- SYS 模式:是 “系统模式”,属于特权模式,通常用于兼容 User 模式的特权操作,这里设置是为了兜底(比如某些异常处理可能切到 SYS 模式)。
当外部中断触发时,
Cortex-A7内核会自动从当前模式切换到 IRQ 模式,此时执行IRQ_Handler的前半段(保存现场)时,CPU 处于IRQ 模式
为什么跳转处理函数前要切换成 SVC 模式?
1. 支持中断嵌套
IRQ 模式下无法响应新的 IRQ 中断(CPSR 的 I 位会自动置 1,屏蔽 IRQ),如果中断处理函数执行时间较长,新的中断会被阻塞。切换到 SVC 模式后:
- SVC 模式的 CPSR 寄存器独立于 IRQ 模式;
- 可以在 SVC 模式下重新打开全局中断(cpsie i),从而支持中断嵌套(新的 IRQ 中断可以再次触发,进入 IRQ 模式处理)。
2. 统一的特权操作环境
SVC 模式是裸机 / 内核的核心特权模式,C 语言的中断处理函数(system_irqhandler)可能需要执行特权操作(比如访问外设寄存器、修改全局变量),SVC 模式提供了更通用的特权环境,而 IRQ 模式是 “专用中断模式”,功能受限。
3. 避免 IRQ 模式栈溢出
IRQ 模式的栈空间通常分配得较小(代码中是 2MB,实际裸机中可能更小),如果中断处理函数逻辑复杂(比如调用多层 C 函数),容易导致 IRQ 栈溢出。切换到 SVC 模式后,使用 SVC 模式的大栈空间,更安全。
system_irqhandler
在 start.S 文件中我们在中断服务函数 IRQ_Handler 中调用了 C 函数 system_irqhandler 来处理具体的中断。此函数有一个参数,参数是中断号,但是函数 system_irqhandler 的具体内容还
没有实现
int.h
|
int.c
|
不同的中断源对应不同的中断处理函数,I.MX6U 有 160 个中断源,所以需要 160 个中断处理函数,我们可以将这些中断处理函数放到一个数组里面,中断处理函数在数组中的标号就是其对应的中断号。
当中断发生以后函数 system_irqhandler 根据中断号从中断处理函数数组中找到对应的中断处理函数并执行即可
- 定义了
irqNesting,作为中断嵌套计数 - 定义了中断服务函数数组
irqTable,这是一个 sys_irq_handle_t 类型的结构体数组,数组大小为 I.MX6U 的中断源个数,即 160 个 - 中断初始化函数 int_init,在此函数中首先初始化了 GIC,然后初始化了中断
服务函数表,最终设置了中断向量表偏移 - 中断服务函数表初始化函数
system_irqtable_init,初始化 irqTable,给其赋初
值 - 注册中断处理函数
system_register_irqhandler,此函数用来给指定的中断号注
册中断处理函数。如果要使用某个外设中断,那就必须调用此函数来给这个中断注册一个中断
处理函数 - 前面在 start.S 中调用的
system_irqhandler函数,此函数根据中断号在中断
处理函数表 irqTable 中取出对应的中断处理函数并执行 - 是默认中断处理函数
default_irqhandler,这是一个空函数,主要用来给初始化
中断函数处理表
这个int.c/h,其实就是设置GIC的库
gpio驱动修改
gpio.h
|
gpio.c
|
添加了一个新枚举类型:gpio_interrupt_mode_t,枚举出了 GPIO 所有的中断触发类型。还修改了结构体 gpio_pin_config_t,在里面加入了 interruptMode 成员变量。最后就是添加了一些跟中断有关的函数声明,bsp_gpio.h
文件的内容总体还是比较简单的
实际上,再开一层exti的驱动,把gpio驱动用起来。
exti.h
|
exti.c
|
最终
main.c
|




