imx6ull 基础复盘*

imx6ull 基础复盘

这篇文档,主要从裸机,底层寄存器,单片机的角度,重新过一遍imx6ull的armv7架构,基础的汇编语法,基础的代码结构,内存分布,启动流程,中断管理这些。

因为已经学过stm32和freertos了,对CM3的内核的理解比之前更加深了。

为后面移植uboot,kernel,和外设驱动,做一个基础的复盘。

IMX6ULL 硬件板卡资源

alt text

单核Cotex-A7,多处理器架构,arm-cortex的关系

之前我们使用的stm32的核心是CM3,单核的。而现在我们使用的这个IMX6的开发板,同样也是单核的,核心是CA7(cotex-A7),

下面介绍一下 Cortex-A7 架构的一些基本知识,如处理器模型、Cortex-A7 寄存器组

alt text

这个是之前看到的介绍处理器的一张图,但是当时有些混淆:

  1. cortex xxx 和 armv7, armv8的关系?
  2. 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核的运行模型

alt text

以前的 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(备份程序状态,用于切换工作模式)
    alt text

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

alt text

蓝绿色是该工作模式独占,浅色表示共用一个物理寄存器。

为了节省资源,肯定不是每种工作模式,都配一组18个的物理寄存器。肯定有的是可以复用的。

可以看出:

  1. 低寄存器组 R0~R7是共享同一组物理寄存器的
  2. R8-R12, 除了FIQ有独立的寄存器外,其余的也都是共享一组物理寄存器
  3. SP,LR,除了sys和hyp模式外,都是独立拥有寄存器(每个模式有自己的栈指针和返回地址寄存器
  4. PC,所有模式共用一个物理寄存器
  5. CPSR,所有模式共用一个物理寄存器
  6. SPSR,每个模式都有自己的寄存器,用来保存本模式的当前cpu状态。

下面详细说说这些寄存器,在某些模式下有什么用,为什么要独占,为什么共享?

  1. R0-R7,大家都共用。无特殊用途,也叫做未备份寄存器

  2. R8-R12,可以看到只有FIQ模式有自己的独占

    FIQ 是快速中断模式,看名字就是知道这个中断模式要求快速执行,让这个模式有独占寄存器,就可以不用保存恢复现场了,非常迅速,从而加速中断的执行过程

  3. R13,SP,

    • 基本上每种模式都有一个自己的 R13 物理寄存器,应用程序会初始化 R13,使其指向该模式专用的栈地址,这就是常说的初始化 SP 指针
  4. R14 也称为连接寄存器(LR),作用有两个

    1. 每种处理器模式使用 R14(LR)来存放当前子程序的返回地址,如果使用 BL 或者 BLX来调用子函数的话,R14(LR)被设置成该子函数的返回地址,在子函数中,将 R14(LR)中的值赋给 R15(PC)即可完成子函数返回
    2. 当异常发生以后,该异常模式对应的 R14寄存器被设置成该异常模式将要返回的地址,
    3. R14 也可以当作普通寄存器使用
  5. R15, PC, 大家共用

    • 保存着当前执行的指令地址值加 8 个字节(一句指令占4字节 = 地址大小),这是因为 ARM的流水线机制导致的

      R15 (PC)值 = 当前执行的程序位置 + 8 个字节

  6. CPSR, 大家共用,任何模式下被访问

    1. 包含了条件标志位、中断禁止位、当前处理器模式标志等一些状态位以及一些控制位
  7. SPSR

    • 7 个模式每个都配备了一个专用的物理状态寄存器,叫做 SPSR(备份程序状态寄存器),当特定的异常 中断发生时,SPSR 寄存器用来保存当前程序状态寄存器CPSR的值,当异常退出以后可以用 SPSR 中保存的值来恢复 CPSR
    • User 和 Sys 这两个模式不是异常模式,所以并没有配备 SPSR

alt text

上图,是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
/*
label 即标号,表示地址位置
1. 指令的地址: 有些指令前面可能会有标号,这样就可以通过这个标号得到指令的地址,
2. 数据地址: 标号也可以用来表示数据地址。

注意 label 后面的“:”,任何以“:”结尾的标识符都会被识别为一个标号。

instruction 即指令,也就是汇编指令或伪指令。

@符号,表示后面的是注释,就跟 C 语言里面的“/*”和“*/”一样

*/

举例:

add:
MOVS R0, #0X12 @设置 R0=0X12

2. 用户可以使用.section 伪操作来定义一个段

.section .testsection @定义一个 testsetcion 段
/*
每个段以段名开始,以下一段名或者文件结尾结束
*/

举例:

.section .test

.test:
ldr r0, =0x12 @r0=0x12
ldr r0, =0x12 @r0=0x12 //这里的两句指令就是一个段
.xxx:
xxx

3. 汇编系统预定义了一些段名

  • .text 表示代码段
  • .rodata 只读数据段
  • .data 初始化的数据段
  • .bss 未初始化的数据段

4. 汇编程序的默认入口标号是_start

.global _start 

_start:
ldr r0, =0x12 @r0=0x12

我们也可以不用在代码里面用_start来指定程序的入口,也可以在链接脚本lds中使用 ENTRY 来指明其它的入口点


5. 伪操作

.byte   //定义单字节数据,比如.byte 0x12。
.short //定义双字节数据,比如.short 0x1234。
.long //定义一个 4 字节数据,比如.long 0x12345678。
.equ //赋值语句,格式为:.equ 变量名,表达式,比如.equ num, 0x12,表示 num=0x12。
.align //数据字节对齐,比如:.align 4 表示 4 字节对齐。
.end //表示源文件结束。
.global //定义一个全局符号,格式为:.global symbol,比如:.global _start。

6. 函数

函数名:
函数体
返回语句

举例:

/* 未定义中断 */
Undefined_Handler:
ldr r0, =Undefined_Handler
bx r0 //“bx”指令是返回指令,函数返回语句不是必须的

Cortex-A7 常用汇编指令

这里的指令,就是上面GNU汇编语法里面的instruction部分了

处理器内部 数据传输指令

主要是:

  • MOV(<-)
    • 将数据从一个寄存器拷贝到另外一个寄存器
    • 将一个立即数传递到寄存器里面
  • MRS(<-)
    • 读特殊寄存器(如 CPSR 和 SPSR)
  • MSR(<-)
    • 写特殊寄存器(如 CPSR 和 SPSR)
MOV R0,R1              //将寄存器 R1 中的数据传递给 R0,即 R0=R1
MOV R0, #0X12 //将立即数 0X12 传递给 R0 寄存器,即 R0=0X12

MRS R0, CPSR //将特殊寄存器 CPSR 里面的数据传递给 R0,即 R0=CPSR

MSR CPSR, R0 //将 R0 中的数据复制到 CPSR 中,即 CPSR=R0

存储器访问指令

ARM 不能直接访问存储器,比如 RAM 中的数据

记住,核心能够直接访问的,只有自己的内核寄存器,访问RAM,要用存储器访问指令。常用的存储器访问指令有两种:

  • LDR(<-)
  • STR(->)
LDR Rd, [Rn , #offset]              //从存储器 Rn+offset 的位置读取数据存放到 Rd 中。

LDR R0, =0X0209C004 //将寄存器地址 0X0209C004 加载到 R0 中,即 R0=0X0209C004
LDR R1, [R0] //读取地址 0X0209C004 中的数据到 R1 寄存器中

STR Rd, [Rn, #offset] //将 Rd 中的数据写入到存储器中的 Rn+offset 位置。

STR R1, [R0] //将 R1 中的值写入到 R0 中所保存的地址中

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>         //将寄存器列表存入栈中

PUSH {R0~R3, R12} //将 R0~R3 和 R12 压栈


POP <reg list> //从栈中恢复寄存器列表。

POP {R0~R3,R12} //在恢复 R0~R3,R12

栈空间的内存模型,一次压入一个寄存器,32位,4字节。

alt text

跳转指令

指令跳转:

  • B(直接跳转,不返回)
  • BL(直接跳转,返回)
  • BX(间接跳转,不返回)
  • BLX(间接跳转,返回)

    L表示LR,表示记录下返回的地址

    X表示寄存器,表示跳转地址记录在寄存器里面

    这些跳转指令,后面接label,也就是地址/指令的别名,比如main, reset_handler

当然还有直接跳转:直接向 PC 寄存器里面写入数据

算术运算指令 + 逻辑运算指令

这些都是加减法的运算指令,比如

  • ADD

  • SUB
    等等,都是(<-)

  • AND

  • ORR

  • BIC

  • ORN

  • EOR
    都是(<-)

启动流程

上面,分析了处理器单核的运行模型,了解了配套的18个寄存器,然后补了一下GNU汇编基础。

下面,来分析一下,imx6ull的启动方式,看看裸机代码是如何跑起来的。

启动方式

首先,了解一下,imx6ull这块开发板支持的启动方式:

  1. FUSE启动
  2. 串行下载
    1. 通过otg1/uart 烧录到外部ROM里面,一般最终量产用
  3. 内部boot模式
    1. 芯片上电执行内部bootROM代码,进行硬件初始化
      1. 初始化时钟
        1. 内核时钟ARM PLL 396Mhz
        2. System PLL 528Mhz
        3. USB PLL 480Mhz
        4. AHB 132Mhz
        5. IPG 66Mh
      2. 打开MMU和cache,加快执行速度(下载镜像,验证镜像,完成后关闭)
      3. 中断向量偏移设置到bootROM的起始位置,启动用户代码后,重新设置中断向量偏移(用户代码开始的地方)
    2. bootROM从boot设备(emmc,sd卡)拷贝到RAM中(ddr)
    • 通过拨码(硬件gpio选择),可以选择具体的启动设备(emmc,sd,eeprom,nandflash)

镜像制作,代码烧录

我们自己编写了一个裸机的代码(start.S + main.c)

然后我们肯定会通过交叉编译工具,来编译出我们自己的bin文件。

  1. xxx-gcc .s/.c -> .o
  2. xxx-ld .o -> .elf (包含了运行地址:0x8780 0000)
  3. xxx-copy .elf -> bin (转换二进制格式)

之后,会用imxdownload来为我们的逻辑,添加一些头

  1. imxdownload .bin -> .imx = (IVT+BD+DCD+.bin)

学习 STM32 的时候我们可以直接将编译生成的.bin 文件烧写到 STM32 内部 flash 里面,

I.MX6U 不能直接烧写编译生成的.bin 文件

新增的头包括:

  1. IVT: Image vector table,IVT 里面包含了一系列的地址信息,这些地址信息在ROM 中按照固定的地址存放着
  2. Boot data,启动数据,包含了镜像要拷贝到哪个地址(运行地址),拷贝的大小是多少等等
  3. 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 在烧写的时候应该烧写到存储设备的指定位置去。整个位置都是相对于存储设备的起始地址的偏移

alt text

(加载地址计算) 以 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

  1. header (BootROM 靠它来识别这块数据是不是合法的 IVT)

    1. 0xD1
    2. IVT的长度
    3. version
  2. 入口地址:

    1. 程序入口绝对地址(DDR地址)。BootROM 把你的代码搬运完毕后,PC 指针跳转去执行的第一个指令地址(通常就是你 start.S 中 _start 的链接物理地址,0x87800000)

      entry (入口地址):0X8780 0000,也就是代码镜像第一行指令链接的位置。

  3. dcd:

    1. 设备配置数据(DCD)绝对地址(DDR地址),让 BootROM 在跳转执行你的代码前帮你初始化 DDR 等核心外设,这样你的程序一启动就能直接跑在 DDR 里

      dcd (DCD 地址):0X877F F42C

  4. boot data:Boot Data 绝对地址(DDR地址)。包含镜像的整体搬运信息(要把整个固件搬运到内存的哪个源地址、目标地址,以及固件总长度)

    boot data (Bd 地址):0X877F F420

  5. self: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的内容有:

  1. 设置 CCGR0~CCGR6 这 7 个外设时钟使能寄存器,默认打开所有的外设时钟。
  2. 配置 DDR3 所用的所有 IO。
  3. 配置 MMDC 控制器,初始化 DDR3。

总结

// 这是我对豆包的描述
我理解的imx6ull的启动:初始上电后,pc指针先指向内部rom,里面是bootrom程序,他会先把外部rom的ivt,bootdata,dcd,这三块拷贝到内部ram中,然后开始读取这个镜像头,从bd里面得知了,整个景象的源地址(外部rom),目的地址(ddr),拷贝大小,从ivt里面得知了ivt,dcd,bd,和用户程序最终在外部ram,ddr中的位置。从dcd里面得知初始化ddr的配置信息,

接下来bootRom要开始做的事情是,按照dcd的内容,初始化ddr,然后按照bd的内容把我们整个imx的景象拷贝到ddr的位置(保证用户代码bin的开头处于0x87800000),接下来就是pc指针跳转到87800000开始执行我们的用户代码


我们的用户代码,里面会做设置核心的状态啊,初始化sp指针准备c环境,然后跳转c的世界,然后在c的世界里面,做一些时钟的初始化啊,外设的初始化之类的。是这样吗

你的理解总结(我先复述一遍,确保我没听错)

  • 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 里初始化时钟、外设…

流程图:

上电复位
|
v
[PC = 芯片内部 BootROM 地址] <-- 出厂固化,不可改
|
|--- 1. 读取启动引脚 BOOT_CFG
| 决定从哪里读镜像:SD / NAND / SPI Nor / 等
|
v
[从外部ROM(Flash/SD) 读取头部]
|
|--- 只读一小段:IVT + DCD + Boot Data
|--- 拷贝到 【芯片内部 RAM (OCRAM)】
|
v
[解析头部]
|
|--- IVT: 入口地址、各类表的指针
|--- DCD: DDR 初始化参数
|--- BootData: 源地址、目的地址、拷贝长度
|
v
[执行 DCD,初始化 DDR]
|
|--- DDR 此时才被点亮!
|--- 之前只能用内部 RAM
|
v
[根据 BootData 搬运整个镜像]
|
|--- 从外部ROM → 搬到 DDR 中
|--- 通常目的地址:0x87800000
|
v
[跳转到 IVT 中指定的入口地址]
|
|--- 通常就是 0x87800000
|--- 进入用户写的程序
|
v
[用户启动代码(你的 start.S)]
|
|--- 关看门狗
|--- 设置 CPU 模式、栈指针 SP
|--- 初始化 .data / .bss
|--- 准备好 C 语言运行环境
|
v
[跳转到 C 入口:main / board_init]
|
|--- 时钟初始化
|--- 外设初始化
|--- 业务逻辑
|
v
正常运行

注意,我们的链接地址0x87800000这个不是固定的,是我们的lds链接脚本决定的。然后imxdownload会根据这个来生成我们的IVT,指向lds里面设置的链接地址。

1. 你写 .lds 链接脚本
↓(指定运行地址 = DDR 里的地址,比如 0x87800000)

//. = 0x87800000; // 这样即可。

2. 编译出来的 elf → bin
↓(bin 里第一条指令的地址就是 0x87800000)

3. 运行 imxdownload 烧录
↓(工具会读取你的 bin 文件第一条指令地址)

4. imxdownload **自动生成 IVT**
↓(把 IVT 里的 entry 字段填成 0x87800000)

5. 烧录到 SD 卡
↓

6. 上电
BootROM 读 IVT → 知道要跳去 0x87800000

内存分布,对比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
0x08000000 +-----------------------+
| 栈顶地址(MSP) | <-- 复位硬件自动加载到SP
+-----------------------+
| 复位向量(Reset_Handler) | <-- 硬件自动PC跳这里
+-----------------------+
| 其他中断向量表 |
+-----------------------+
| startup_stm32f10x.s | <-- start.s 代码
+-----------------------+
| .text 程序代码 |
+-----------------------+
| .rodata 常量 |
+-----------------------+
| .data 初始化数据(初值) | <-- 存在Flash里
+-----------------------+
| (Flash剩余空间) |
0x0807FFFF +-----------------------+



//内部ram
0x2000FFFF +-----------------------+
| (未定义垃圾值) |
| |
| 全是随机数 |
| |
0x20000000 +-----------------------+

3. 执行 start.s 过程中(关键三步)

  • 设置栈指针 SP = 0x2000FFFF(或稍小)
  • 从 Flash 拷贝 .data 到 RAM
  • RAM 中 .bss 段清零

4. start.s 执行完成后(进入 main 前)

//内部rom flash
0x08000000 +-----------------------+
| 栈顶地址 |
+-----------------------+
| 复位向量 |
+-----------------------+
| 中断向量表 |
+-----------------------+
| start.s 代码 |
+-----------------------+
| .text / .rodata |
+-----------------------+
| .data 源数据(Flash) |
+-----------------------+
0x0807FFFF +-----------------------+


//内部ram
0x2000FFFF +-----------------------+
| 栈 (Stack) | <-- SP指向这里
| (向下生长) |
+-----------------------+
| |
| 动态内存/堆(可选) |
| |
+-----------------------+
| .data 段 | <-- 从Flash拷贝过来:有初值的全局/静态变量
+-----------------------+
| .bss 段 | <-- 已被start.s清零:全局/静态未初始化变量
+-----------------------+
0x20000000 +-----------------------+
疑问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:
/* 1. 从Flash拷贝 .data 到 RAM */
ldr r0, =_sidata @ Flash中.data的源地址(链接脚本定义)
ldr r1, =_sdata @ RAM中.data的目标起始
ldr r2, =_edata @ RAM中.data的目标结束
copy_data:
ldr r3, [r0], #4
str r3, [r1], #4
cmp r1, r2
bne copy_data

/* 2. 清零 .bss 段 */
ldr r0, =_sbss @ RAM中.bss的起始
ldr r1, =_ebss @ RAM中.bss的结束
mov r2, #0 @ 要写入的0
clear_bss:
str r2, [r0], #4
cmp r0, r1
bne clear_bss

/* 3. 跳转到C语言入口main */
b main

你看,这就是你 “没看见” 的那部分代码:

  • .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 里用到的地址。

所以整个流程是:

  1. 链接脚本:画好地图(内存布局)
  2. 硬件:只负责把 PC 和 SP 指向地图的入口
  3. start.S:拿着地图,一步一步把东西搬到正确的地方

那为什么stm32的start.s中没有看到实际拷贝.data, .bss的部分?

下面是stm32的demo的start.s中的PC执行的部分,没有拷贝的指令,但是实际上是拷贝了。

Reset_Handler   PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main ; 重点:导入了__main(不是你写的main!)
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0 ; 第一步:初始化系统时钟
LDR R0, =__main ; 第二步:跳转到__main
BX R0
ENDP

原因:

__main 是 ARM 编译器(ARMCC)提供的C 库启动函数,它藏在编译器的运行时库(RTL)里,你看不到源码

实际流程:

Reset_Handler → SystemInit → __main → 你的main()
↑ ↑
时钟初始化 核心操作:
1. 拷贝.data段(Flash→RAM)
2. 清零.bss段(RAM写0)
3. 初始化堆(Heap)
4. 最终跳转到你的main()

简单说:MDK 把 “拷贝.data/ 清零.bss” 这些脏活,都封装到了 __main 里,所以你在 start.s 里看不到,但它确实执行了。

汇编led点灯 裸机代码分析

下面分析一下,汇编版本的led点灯代码

Makefile

led.bin:led.s
arm-linux-gnueabihf-gcc -g -c led.s -o led.o
arm-linux-gnueabihf-ld -Ttext 0X87800000 led.o -o led.elf
arm-linux-gnueabihf-objcopy -O binary -S -g led.elf led.bin
arm-linux-gnueabihf-objdump -D led.elf > led.dis
clean:
rm -rf *.o led.bin led.elf led.dis

led.s

/**************************************************************
Copyright © zuozhongkai Co., Ltd. 1998-2019. All rights reserved.
文件名 : mian.c
作者 : 左忠凯
版本 : V1.0
描述 : 裸机实验1 汇编点灯
使用汇编来点亮开发板上的LED灯,学习和掌握如何用汇编语言来
完成对I.MX6U处理器的GPIO初始化和控制。
其他 : 无
论坛 : www.wtmembed.com
日志 : 初版V1.0 2019/1/3 左忠凯创建
**************************************************************/

.global _start /* 全局标号 */

/*
* 描述: _start函数,程序从此函数开始执行此函数完成时钟使能、
* GPIO初始化、最终控制GPIO输出低电平来点亮LED灯。
*/
_start:
/* 例程代码 */
/* 1、使能所有时钟 */
ldr r0, =0X020C4068 /* CCGR0 */
ldr r1, =0XFFFFFFFF
str r1, [r0]

ldr r0, =0X020C406C /* CCGR1 */
str r1, [r0]

ldr r0, =0X020C4070 /* CCGR2 */
str r1, [r0]

ldr r0, =0X020C4074 /* CCGR3 */
str r1, [r0]

ldr r0, =0X020C4078 /* CCGR4 */
str r1, [r0]

ldr r0, =0X020C407C /* CCGR5 */
str r1, [r0]

ldr r0, =0X020C4080 /* CCGR6 */
str r1, [r0]


/* 2、设置GPIO1_IO03复用为GPIO1_IO03 */
ldr r0, =0X020E0068 /* 将寄存器SW_MUX_GPIO1_IO03_BASE加载到r0中 */
ldr r1, =0X5 /* 设置寄存器SW_MUX_GPIO1_IO03_BASE的MUX_MODE为5 */
str r1,[r0]

/* 3、配置GPIO1_IO03的IO属性
*bit 16:0 HYS关闭
*bit [15:14]: 00 默认下拉
*bit [13]: 0 kepper功能
*bit [12]: 1 pull/keeper使能
*bit [11]: 0 关闭开路输出
*bit [7:6]: 10 速度100Mhz
*bit [5:3]: 110 R0/6驱动能力
*bit [0]: 0 低转换率
*/
ldr r0, =0X020E02F4 /*寄存器SW_PAD_GPIO1_IO03_BASE */
ldr r1, =0X10B0
str r1,[r0]

/* 4、设置GPIO1_IO03为输出 */
ldr r0, =0X0209C004 /*寄存器GPIO1_GDIR */
ldr r1, =0X0000008
str r1,[r0]

/* 5、打开LED0
* 设置GPIO1_IO03输出低电平
*/
ldr r0, =0X0209C000 /*寄存器GPIO1_DR */
ldr r1, =0
str r1,[r0]

/*
* 描述: loop死循环
*/
loop:
b loop

这个就是非常简单的一个汇编代码,所以也不需要c语言的环境

实际内存分变化:

1. 上电前(仅外部 ROM 有数据,DDR 空)

┌─────────────────────────────────┐  外部ROM (SD卡)
│ 0x00000000 ~ 0x0000xxxx │ ← imxdownload烧录的内容:
│ ┌─────────────┐ │ IVT + DCD + BootData + 你的汇编指令
│ │ IVT │ │ (IVT里entry=0X87800000)
│ │ DCD │ │
│ │ BootData │ │
│ │ 汇编指令 │ │
│ └─────────────┘ │
│ 剩余空间:空 │
└─────────────────────────────────┘

┌─────────────────────────────────┐ DDR (0X80000000 ~ 0X9FFFFFFF)
│ 0X87800000 ~ 0X8780xxxx │ ← 全是随机垃圾值(未初始化)
│ 全未使用,无有效数据 │
│ 剩余空间:空 │
└─────────────────────────────────┘

2. 上电后(BootROM 完成搬运,DDR 有指令)

┌─────────────────────────────────┐  外部ROM (SD卡)
│ 0x00000000 ~ 0x0000xxxx │ ← 内容不变(只读)
│ ┌─────────────┐ │
│ │ IVT │ │
│ │ DCD │ │
│ │ BootData │ │
│ │ 汇编指令 │ │
│ └─────────────┘ │
└─────────────────────────────────┘

┌─────────────────────────────────┐ DDR (核心变化!)
│ 0X87800000 ~ 0X8780xxxx │ ← BootROM搬运过来的汇编指令
│ ┌─────────────────────────┐ │
│ │ _start: │ │ ← 第一条指令地址(0X87800000)
│ │ ldr r0, =0X020C4068 │ │
│ │ ldr r1, =0XFFFFFFFF │ │
│ │ ...(时钟/IO配置指令)│ │
│ │ loop: │ │
│ │ b loop │ │
│ └─────────────────────────┘ │
│ 剩余空间:空 │
└─────────────────────────────────┘

3. 最终执行阶段(PC 跳转到 DDR 执行)

┌─────────────────────────────────┐  DDR
│ 0X87800000 │ ← PC指针指向这里,开始执行指令
│ ┌─────────────────────────┐ │
│ │ 执行:使能所有时钟 │ │
│ │ 执行:配置GPIO1_IO03复用 │ │
│ │ 执行:配置IO属性 │ │
│ │ 执行:设置GPIO为输出 │ │
│ │ 执行:GPIO输出低电平(点灯)│
│ │ 执行:b loop(死循环) │ │
│ └─────────────────────────┘ │
└─────────────────────────────────┘

代码特性:这段汇编只有指令(.text 段),没有定义任何全局变量、静态变量,自然不需要.data(存初始化值)和.bss(存未初始化值);

启动简化:因为没有.data/.bss,你的汇编代码里也不需要像 STM32 那样做 “拷贝 / 清零” 操作,_start 直接开始配置硬件 —— 这是裸机汇编最简洁的优势

c语言led点灯 裸机代码分析

下面看看c语言版本的逻辑代码

Makefile

objs := start.o main.o

ledc.bin:$(objs)
arm-linux-gnueabihf-ld -Timx6ul.lds -o ledc.elf $^
arm-linux-gnueabihf-objcopy -O binary -S ledc.elf $@
arm-linux-gnueabihf-objdump -D -m arm ledc.elf > ledc.dis

%.o:%.s
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $<

%.o:%.S
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $<

%.o:%.c
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $<

clean:
rm -rf *.o ledc.bin ledc.elf ledc.dis

lds链接脚本

SECTIONS{
. = 0X87800000;
.text :
{
start.o
main.o
*(.text)
}
.rodata ALIGN(4) : {*(.rodata*)}
.data ALIGN(4) : { *(.data) }
__bss_start = .;
.bss ALIGN(4) : { *(.bss) *(COMMON) }
__bss_end = .;
}

可以看到,lds指定了ddr里面的内存分布情况

start.s

.global _start  		/* 全局标号 */

/*
* 描述: _start函数,程序从此函数开始执行,此函数主要功能是设置C
* 运行环境。
*/
_start:

/* 进入SVC模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x13 /* r0或上0x13,表示使用SVC模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */

ldr sp, =0X80200000 /* 设置栈指针 */
b main /* 跳转到main函数 */

main.c

int main(void)
{
clk_enable(); /* 使能所有的时钟 */
led_init(); /* 初始化led */

while(1) /* 死循环 */
{
led_off(); /* 关闭LED */
delay(500); /* 延时大约500ms */

led_on(); /* 打开LED */
delay(500); /* 延时大约500ms */
}

return 0;
}

注意,我们的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:
/* 1. 切换SVC模式 */
mrs r0, cpsr
bic r0, r0, #0x1f
orr r0, r0, #0x13
msr cpsr, r0

/* 2. 设置栈指针 */
ldr sp, =0X80200000

/* 3. 清零.bss段(新增!) */
ldr r0, =__bss_start /* 链接脚本定义的bss起始 */
ldr r1, =__bss_end /* 链接脚本定义的bss结束 */
mov r2, #0
bss_clear:
str r2, [r0], #4
cmp r0, r1
bne bss_clear

/* 4. 跳main */
b main

和 STM32 的本质区别:

  • STM32 是 “Flash 存初始值,RAM 跑程序”(需要软件拷贝),
  • i.MX6ULL 是 “外部 ROM 存初始值,DDR 跑程序”(BootROM 一次性搬过去,无需二次拷贝)
  • 但是记住,bss段都需要清零,否则容易有异常风险

下面展示内存的分布变化

1. 上电前(仅外部 ROM 有数据,DDR 全空)

┌─────────────────────────────────────────┐ 外部ROM (SD卡)
│ 0x00000000 ~ 0x0000xxxx │ ← imxdownload烧录的完整镜像
│ ┌─────────────┬─────────────────────┐ │
│ │ IVT+DCD+BD │ 用户镜像 │ │
│ │ (头信息) │ ┌─────────────┐ │ │
│ │ │ │ .text │ │ │ ← start.s+main.c的指令
│ │ │ │ .rodata │ │ │ ← 常量(如字符串,本例无)
│ │ │ │ .data │ │ │ ← 有初值全局变量(本例无)
│ │ │ └─────────────┘ │ │
│ └─────────────┴─────────────────────┘ │
│ 剩余空间:空 │
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐ DDR (0X80000000 ~ 0X9FFFFFFF)
│ 0X87800000 ~ 0X8780xxxx │ ← 全是随机垃圾值(未初始化)
│ 0X80200000 (栈地址):随机值 │
│ 剩余空间:空 │
└─────────────────────────────────────────┘

2. 上电后 → BootROM 完成搬运(未执行 start.s)

┌─────────────────────────────────────────┐ 外部ROM (SD卡)
│ 内容不变(只读) │ ← 头信息+用户镜像仍在
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐ DDR (核心变化:搬运完成)
│ 0X87800000 ~ 0X8780xxxx │ ← BootROM原样搬运外部ROM的用户镜像
│ ┌─────────────┬─────────────────────┐ │
│ │ .text │ 指令(start.s+main.c)│ │ ← 可直接执行
│ │ .rodata │ 常量(本例无) │ │
│ │ .data │ 有初值全局变量(本例无)│
│ │ .bss │ 随机垃圾值 │ │ ← BootROM不处理,仍为DDR原始值
│ └─────────────┴─────────────────────┘ │
│ 0X80200000 (栈地址):随机值 │
└─────────────────────────────────────────┘

3. 执行 start.s(清零.bss + 设置栈)

┌─────────────────────────────────────────┐ DDR
│ 0X87800000 ~ 0X8780xxxx │
│ ┌─────────────┬─────────────────────┐ │
│ │ .text │ 指令(无变化) │ │
│ │ .rodata │ 常量(无变化) │ │
│ │ .data │ 有初值全局变量(无变化)│
│ │ .bss │ 全0(已清零) │ │ ← start.s的bss_clear循环执行完成
│ └─────────────┴─────────────────────┘ │
│ 0X80200000 (栈地址):SP指向这里 │ ← 栈指针已设置,C环境就绪
└─────────────────────────────────────────┘

4. 跳转到 main 执行(点灯循环)

┌─────────────────────────────────────────┐ DDR
│ 0X87800000 ~ 0X8780xxxx │
│ ┌─────────────┬─────────────────────┐ │
│ │ .text │ 执行中: │ │
│ │ │ - clk_enable() │ │
│ │ │ - led_init() │ │
│ │ │ - while(1)点灯循环 │ │
│ │ .rodata │ 无 │ │
│ │ .data │ 无 │ │
│ │ .bss │ 全0(无使用) │ │
│ └─────────────┴─────────────────────┘ │
│ 0X80200000 (栈地址):栈空间使用中 │ ← main里的局部变量(如delay计数)存在这里
└─────────────────────────────────────────┘

这里有一个疑问,

  • 为什么stm32中,start.s中,有分配栈的大小,且栈被lds分配到ram的最后部分,然后.data, .bss在ram的前部。

  • 而在imx6ull这里,因为DDR的地址范围0x80000000 - xxxxxxxx. 但是栈在0x80000000 - 0x80200000,在前面部分。, 而.text, .data这些在0x87800000这里的后面部分。

A 核裸机栈管理的 “隐形规则”

栈的 “实际可用大小” 不是由链接脚本 / 代码定义的,而是由栈顶地址和下一个可用内存地址的间距决定的

中断

下面来复盘一下中断,这个结束后,所有的裸机外设就结束了

stm32中断复盘

1. 中断向量表

  1. 中断向量就是中断程序的入口地址
  2. 中断服务程序在中断向量表中的位置由半导体厂商定好
  3. 中断向量表在整个程序的最前面
    1. __initial_sp就是第一个中断向量,存放栈顶指针
    2. 第二个是reset_handler

      Cortex-M 架构引入了一个新的概念——中断向量表偏移

      通过中断向量表偏移就可以将中断向量表存放到任意地址处,中断向量表偏移配置在函数 SystemInit 中完成,通过向 SCB_VTOR 寄存器写入新的中断向量表首地址即可

SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET;  //第 8 行是将中断向量表设置到 RAM 中
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; //10 行是将中断向量表设置到 ROM 中

基本都是将中断向量表设置到 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的中断:

  1. 设置中断优先级
  2. 使能中断

同理,如果要使用 I.MX6U 的某个中断的话也需要使能其对应的中断

4. 中断服务函数

这个就是具体跳转的中断处理函数了,stm32里面,会把中断处理函数重定向,指向HAL里面来进行中断的处理。

imx里面,就要先了解中断的机制。

Cortex-A7 中断系统简介

跟 STM32 一样,Cortex-A7 也有中断向量表,中断向量表也是在代码的最前面。CortexA7 内核有 8 个异常中断

alt text

中断向量表里面都是中断服务函数的入口地址, Cortex-A7 一共有 8 个中断,而且还有一个中断向量没有使用,实际只有 7 个中断

下面简单介绍一下这7个中断:

  • 复位中断(Rest)
    • CPU 复位以后就会进入复位中断,我们可以在复位中断服务函数里面做一些初始化工作,比如初始化 SP 指针、DDR 等等
  • 未定义指令中断(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 中断服务函数里面就可以读取指定的寄存器来判断发生的具体是什么中断,进而根据具体的中断做出相应的处理

具体外部中断如下

alt text

所以我们需要在 IRQ 中断服务函数中判断究竟是左侧的哪个中断发生了,然后再做出具体的处理

我们常用的就是复位中断和 IRQ 中断,所以我们需要编写这两个中断的中断服务函数,稍后我们会讲解如何编写对应的中断服务函数

创建中断向量表

首先我们要根据表 17.1.2.1的内容来创建中断向量表,中断向量表处于程序最开始的地方,比如我们前面例程的 start.S 文件最前面,中断向量表如下


.global _start /* 全局标号 */

/*
* 描述: _start函数,首先是中断向量表的创建
* 参考文档:ARM Cortex-A(armV7)编程手册V4.0.pdf P42,3 ARM Processor Modes and Registers(ARM处理器模型和寄存器)
* ARM Cortex-A(armV7)编程手册V4.0.pdf P165 11.1.1 Exception priorities(异常)
*/
_start:
ldr pc, =Reset_Handler /* 复位中断 */
ldr pc, =Undefined_Handler /* 未定义中断 */
ldr pc, =SVC_Handler /* SVC(Supervisor)中断 */
ldr pc, =PrefAbort_Handler /* 预取终止中断 */
ldr pc, =DataAbort_Handler /* 数据终止中断 */
ldr pc, =NotUsed_Handler /* 未使用中断 */
ldr pc, =IRQ_Handler /* IRQ中断 */
ldr pc, =FIQ_Handler /* FIQ(快速中断)未定义中断 */

/* 复位中断 */
Reset_Handler:
//........
b main /* 跳转到main函数 */

/* 未定义中断 */
Undefined_Handler:
ldr r0, =Undefined_Handler
bx r0

/* SVC中断 */
SVC_Handler:
ldr r0, =SVC_Handler
bx r0

/* 预取终止中断 */
PrefAbort_Handler:
ldr r0, =PrefAbort_Handler
bx r0

/* 数据终止中断 */
DataAbort_Handler:
ldr r0, =DataAbort_Handler
bx r0

/* 未使用的中断 */
NotUsed_Handler:

ldr r0, =NotUsed_Handler
bx r0

/* IRQ中断!重点!!!!! */
IRQ_Handler:
//.....

/* FIQ中断 */
FIQ_Handler:

ldr r0, =FIQ_Handler
bx r0


_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:虚拟快速 FIQ
  • VIRQ:虚拟外部 IRQ
  • FIQ:快速中断 IRQ
  • IRQ:外部中断 IRQ

    注意,GIC是接收外部中断的,内核系统中断和CM3一样,是由单独寄存器控制的。

alt text

VFIQ 和 VIRQ 是针对虚拟化的,我们不讨论虚拟化,剩下的就是 FIQ 和 IRQ 了

我们只使用 IRQ,所以相当于 GIC 最终向 ARM 内核就上报一个 IRQ信号, 那么GIC是如何向内核上报IRQ信息的呢?

框图

alt text

左侧部分就是中断源,中间部分就是 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 分配给 SGI
  • ID16~ID31:这 16 个 ID 分配给 PPI
  • ID32~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”小节

alt text

在SDK的芯片定义文件 MCIMX6Y2C.h 中有列出所有的中断ID

alt text

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 的所有寄存器

alt text

获取到 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>

//cond: 指令执行的条件码,如果忽略的话就表示无条件执行。
//opc1:协处理器要执行的操作码。
//Rt:ARM 源寄存器,要写入到 CP15 寄存器的数据就保存在此寄存器中。
//CRn:CP15 协处理器的目标寄存器。
//CRm:协处理器中附加的目标寄存器或者源操作数寄存器,如果不需要附加信息就将CRm 设置为 C0,否则结果不可预测。
//opc2:可选的协处理器特定操作码,当不需要的时候要设置为 0。

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 通过不同的搭配,其得到的寄存器含义是不同的

alt text

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

c0 作为 MDIR 寄存器的时候其含义

alt text

  • 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 寄存器主要是完成控制功能的

alt text

CP15协处理器,相当于内核控制寄存器了。有点类似CPSR

如果要读写 SCTLR 的话,就可以使用如下命令:

MRC p15, 0, <Rt>, c1, c0, 0   //读取 SCTLR 寄存器,数据保存到 Rt 中。
MCR p15, 0, <Rt>, c1, c0, 0 //将 Rt 中的数据写到 SCTLR(c1)寄存器中。

c12 寄存器

c12 寄存器通过不同的配置,其代表的含义也不同,具体对应关系不多展示

当 MRC/MCR 指令中的 CRn=c12,opc1=0,CRm=c0,opc2=0 的时候就表示此时 c12 为 VBAR 寄存器,也就是向量表基地址寄存器。设置中断向量表偏移的时候就需要将新的中断向量表基地址写入 VBAR 中

代码链接的起始地址为0X87800000,而中断向量表肯定要放到最前面,也就是 0X87800000 这个地址处。所以就需要设置 VBAR 为 0X87800000,设置命令如下

ldr r0, =0X87800000           //r0=0X87800000
MCR p15, 0, r0, c12, c0, 0 //将 r0 里面的数据写入到 c12 中,即 c12=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 基地址
ADD r1, r1, #0X2000 //GIC 基地址加 0X2000 得到 CPU 接口端寄存器起始地址
LDR r0, [r1, #0XC] //读取 CPU 接口端起始地址+0XC 处的寄存器值,也就是寄存器GIC_IAR 的值

关于 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 寄存器,此寄存器用来决定使用几级优先
级

alt text
alt text

I.MX6U 是 Cortex-A7内核,所以支持 32 个优先级,因此 GICC_PMR 要设置为 0b11111000

抢占优先级和子优先级位数设置

抢占优先级和子优先级各占多少位是由寄存器 GICC_BPR 来决定的

alt text
一般将所有的中断优先级位都配置为抢占优先级,比如 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;

中断优先级设置总结

有关优先级设置的内容就讲解到这里,优先级设置主要有三部分

  1. 设置寄存器 GICC_PMR,配置优先级个数,
    1. 比如 I.MX6U 支持 32 级优先级
  2. 设置抢占优先级和子优先级位数
    1. 一般为了简单起见,会将所有的位数都设置为抢占优先级
  3. 设置指定中断 ID 的优先级,也就是设置外设优先级

可以看出,整体流程,和stm32差不多。流程都一样,只是寄存器有些差异罢了

gpio中断实验

start.s

/***************************************************************
Copyright © zuozhongkai Co., Ltd. 1998-2019. All rights reserved.
文件名 : start.s
作者 : 左忠凯
版本 : V1.0
描述 : ZERO-I.MX6UL/I.MX6ULL开发板启动文件,完成C环境初始化,
C环境初始化完成以后跳转到C代码。
其他 : 无
论坛 : www.wtmembed.com
日志 : 初版V1.0 2019/1/3 左忠凯修改
V2.0 2019/1/4 左忠凯修改
添加中断相关定义
**************************************************************/

.global _start /* 全局标号 */

/*
* 描述: _start函数,首先是中断向量表的创建
* 参考文档:ARM Cortex-A(armV7)编程手册V4.0.pdf P42,3 ARM Processor Modes and Registers(ARM处理器模型和寄存器)
* ARM Cortex-A(armV7)编程手册V4.0.pdf P165 11.1.1 Exception priorities(异常)
*/
_start:
ldr pc, =Reset_Handler /* 复位中断 */
ldr pc, =Undefined_Handler /* 未定义中断 */
ldr pc, =SVC_Handler /* SVC(Supervisor)中断 */
ldr pc, =PrefAbort_Handler /* 预取终止中断 */
ldr pc, =DataAbort_Handler /* 数据终止中断 */
ldr pc, =NotUsed_Handler /* 未使用中断 */
ldr pc, =IRQ_Handler /* IRQ中断 */
ldr pc, =FIQ_Handler /* FIQ(快速中断)未定义中断 */

/* 复位中断 */
Reset_Handler:

cpsid i /* 关闭全局中断 */

/* 关闭I,DCache和MMU
* 采取读-改-写的方式。
*/
mrc p15, 0, r0, c1, c0, 0 /* 读取CP15的C1寄存器到R0中 */
bic r0, r0, #(0x1 << 12) /* 清除C1寄存器的bit12位(I位),关闭I Cache */
bic r0, r0, #(0x1 << 2) /* 清除C1寄存器的bit2(C位),关闭D Cache */
bic r0, r0, #0x2 /* 清除C1寄存器的bit1(A位),关闭对齐 */
bic r0, r0, #(0x1 << 11) /* 清除C1寄存器的bit11(Z位),关闭分支预测 */
bic r0, r0, #0x1 /* 清除C1寄存器的bit0(M位),关闭MMU */
mcr p15, 0, r0, c1, c0, 0 /* 将r0寄存器中的值写入到CP15的C1寄存器中 */


#if 0
/* 汇编版本设置中断向量表偏移 */
ldr r0, =0X87800000

dsb
isb
mcr p15, 0, r0, c12, c0, 0
dsb
isb
#endif

/* 设置各个模式下的栈指针,
* 注意:IMX6UL的堆栈是向下增长的!
* 堆栈指针地址一定要是4字节地址对齐的!!!
* DDR范围:0X80000000~0X9FFFFFFF
*/
/* 进入IRQ模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x12 /* r0或上0x13,表示使用IRQ模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0x80600000 /* 设置IRQ模式下的栈首地址为0X80600000,大小为2MB */

/* 进入SYS模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x1f /* r0或上0x13,表示使用SYS模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0x80400000 /* 设置SYS模式下的栈首地址为0X80400000,大小为2MB */

/* 进入SVC模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x13 /* r0或上0x13,表示使用SVC模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0X80200000 /* 设置SVC模式下的栈首地址为0X80200000,大小为2MB */

cpsie i /* 打开全局中断 */
#if 0
/* 使能IRQ中断 */
mrs r0, cpsr /* 读取cpsr寄存器值到r0中 */
bic r0, r0, #0x80 /* 将r0寄存器中bit7清零,也就是CPSR中的I位清零,表示允许IRQ中断 */
msr cpsr, r0 /* 将r0重新写入到cpsr中 */
#endif

b main /* 跳转到main函数 */

/* 未定义中断 */
Undefined_Handler:
ldr r0, =Undefined_Handler
bx r0

/* SVC中断 */
SVC_Handler:
ldr r0, =SVC_Handler
bx r0

/* 预取终止中断 */
PrefAbort_Handler:
ldr r0, =PrefAbort_Handler
bx r0

/* 数据终止中断 */
DataAbort_Handler:
ldr r0, =DataAbort_Handler
bx r0

/* 未使用的中断 */
NotUsed_Handler:

ldr r0, =NotUsed_Handler
bx r0

/* IRQ中断!重点!!!!! */
IRQ_Handler:
push {lr} /* 保存lr地址 */
push {r0-r3, r12} /* 保存r0-r3,r12寄存器 */

mrs r0, spsr /* 读取spsr寄存器 */
push {r0} /* 保存spsr寄存器 */

mrc p15, 4, r1, c15, c0, 0 /* 从CP15的C0寄存器内的值到R1寄存器中
* 参考文档ARM Cortex-A(armV7)编程手册V4.0.pdf P49
* Cortex-A7 Technical ReferenceManua.pdf P68 P138
*/
add r1, r1, #0X2000 /* GIC基地址加0X2000,也就是GIC的CPU接口端基地址 */
ldr r0, [r1, #0XC] /* GIC的CPU接口端基地址加0X0C就是GICC_IAR寄存器,
* GICC_IAR寄存器保存这当前发生中断的中断号,我们要根据
* 这个中断号来绝对调用哪个中断服务函数
*/
push {r0, r1} /* 保存r0,r1 */

cps #0x13 /* 进入SVC模式,允许其他中断再次进去 */

push {lr} /* 保存SVC模式的lr寄存器 */
ldr r2, =system_irqhandler /* 加载C语言中断处理函数到r2寄存器中*/
blx r2 /* 运行C语言中断处理函数,带有一个参数,保存在R0寄存器中 */

pop {lr} /* 执行完C语言中断服务函数,lr出栈 */
cps #0x12 /* 进入IRQ模式 */
pop {r0, r1}
str r0, [r1, #0X10] /* 中断执行完成,写EOIR */

pop {r0}
msr spsr_cxsf, r0 /* 恢复spsr */

pop {r0-r3, r12} /* r0-r3,r12出栈 */
pop {lr} /* lr出栈 */
subs pc, lr, #4 /* 将lr-4赋给pc */



/* FIQ中断 */
FIQ_Handler:

ldr r0, =FIQ_Handler
bx r0


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 中断服务函数主要的工作就是:

  1. 保存现场

    1. LR入栈
    2. R0-R3,R12入栈
    3. SPSR 入栈
  2. 区分当前发生的什么中断(中断 ID)?

    1. 读CP15的R0,获得GIC基地址
    2. 读GIC的IAR地址到R1,得到中断ID到R0
    3. R0,R1入栈
  3. 然后针对不同的外部中断做出不同的处理(SVC模式)

    1. 切换SVC管理员模式
    2. 保存LR入栈
    3. 跳转system_irqhandler,中断ID作为参数保存至R0
    4. 执行完毕
    5. LR出栈
  4. 恢复现场

    1. 切换IRQ模式
    2. 出栈R0,R1(GIC的寄存器们)
    3. 写GIC的EOIR,中断执行完成
      1. (当一个中断处理完成以后必
        须向 GICC_EOIR 寄存器写入其中断号表示中断处理完成)
    4. 恢复SPSR
    5. 恢复R0-R3,R12
    6. 恢复LR
    7. 跳转PC->LR-4,返回执行。

关于为什么要LR-4赋值给PC

0X2000 MOV R1, R0 ;执行
0X2004 MOV R2, R3 ;译指
0X2008 MOV R4, R5 ;取值 PC

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

#ifndef _BSP_INT_H
#define _BSP_INT_H
#include "imx6ul.h"


/* 中断服务函数形式 */
typedef void (*system_irq_handler_t) (unsigned int giccIar, void *param);


/* 中断服务函数结构体*/
typedef struct _sys_irq_handle
{
system_irq_handler_t irqHandler; /* 中断服务函数 */
void *userParam; /* 中断服务函数参数 */
} sys_irq_handle_t;


/* 函数声明 */
void int_init(void);
void system_irqtable_init(void);
void system_register_irqhandler(IRQn_Type irq, system_irq_handler_t handler, void *userParam);
void system_irqhandler(unsigned int giccIar);
void default_irqhandler(unsigned int giccIar, void *userParam);



#endif

int.c

#include "bsp_int.h"

/* 中断嵌套计数器 */
static unsigned int irqNesting;

/* 中断服务函数表 */
static sys_irq_handle_t irqTable[NUMBER_OF_INT_VECTORS];

/*
* @description : 中断初始化函数
* @param : 无
* @return : 无
*/
void int_init(void)
{
GIC_Init(); /* 初始化GIC */
system_irqtable_init(); /* 初始化中断表 */
__set_VBAR((uint32_t)0x87800000); /* 中断向量表偏移,偏移到起始地址 */
}

/*
* @description : 初始化中断服务函数表
* @param : 无
* @return : 无
*/
void system_irqtable_init(void)
{
unsigned int i = 0;
irqNesting = 0;

/* 先将所有的中断服务函数设置为默认值 */
for(i = 0; i < NUMBER_OF_INT_VECTORS; i++)
{
system_register_irqhandler((IRQn_Type)i,default_irqhandler, NULL);
}
}

/*
* @description : 给指定的中断号注册中断服务函数
* @param - irq : 要注册的中断号
* @param - handler : 要注册的中断处理函数
* @param - usrParam : 中断服务处理函数参数
* @return : 无
*/
void system_register_irqhandler(IRQn_Type irq, system_irq_handler_t handler, void *userParam)
{
irqTable[irq].irqHandler = handler;
irqTable[irq].userParam = userParam;
}

/*
* @description : C语言中断服务函数,irq汇编中断服务函数会
调用此函数,此函数通过在中断服务列表中查
找指定中断号所对应的中断处理函数并执行。
* @param - giccIar : 中断号
* @return : 无
*/
void system_irqhandler(unsigned int giccIar)
{

uint32_t intNum = giccIar & 0x3FFUL;

/* 检查中断号是否符合要求 */
if ((intNum == 1023) || (intNum >= NUMBER_OF_INT_VECTORS))
{
return;
}

irqNesting++; /* 中断嵌套计数器加一 */

/* 根据传递进来的中断号,在irqTable中调用确定的中断服务函数*/
irqTable[intNum].irqHandler(intNum, irqTable[intNum].userParam);

irqNesting--; /* 中断执行完成,中断嵌套寄存器减一 */

}

/*
* @description : 默认中断服务函数
* @param - giccIar : 中断号
* @param - usrParam : 中断服务处理函数参数
* @return : 无
*/
void default_irqhandler(unsigned int giccIar, void *userParam)
{
while(1)
{
}
}




不同的中断源对应不同的中断处理函数,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

#ifndef _BSP_GPIO_H
#define _BSP_GPIO_H
#define _BSP_KEY_H
#include "imx6ul.h"
/***************************************************************
Copyright © zuozhongkai Co., Ltd. 1998-2019. All rights reserved.
文件名 : bsp_gpio.h
作者 : 左忠凯
版本 : V1.0
描述 : GPIO操作文件头文件。
其他 : 无
论坛 : www.wtmembed.com
日志 : 初版V1.0 2019/1/4 左忠凯创建
V2.0 2019/1/4 左忠凯修改
添加GPIO中断相关定义

***************************************************************/

/*
* 枚举类型和结构体定义
*/
typedef enum _gpio_pin_direction
{
kGPIO_DigitalInput = 0U, /* 输入 */
kGPIO_DigitalOutput = 1U, /* 输出 */
} gpio_pin_direction_t;

/*
* GPIO中断触发类型枚举
*/
typedef enum _gpio_interrupt_mode
{
kGPIO_NoIntmode = 0U, /* 无中断功能 */
kGPIO_IntLowLevel = 1U, /* 低电平触发 */
kGPIO_IntHighLevel = 2U, /* 高电平触发 */
kGPIO_IntRisingEdge = 3U, /* 上升沿触发 */
kGPIO_IntFallingEdge = 4U, /* 下降沿触发 */
kGPIO_IntRisingOrFallingEdge = 5U, /* 上升沿和下降沿都触发 */
} gpio_interrupt_mode_t;

/*
* GPIO配置结构体
*/
typedef struct _gpio_pin_config
{
gpio_pin_direction_t direction; /* GPIO方向:输入还是输出 */
uint8_t outputLogic; /* 如果是输出的话,默认输出电平 */
gpio_interrupt_mode_t interruptMode; /* 中断方式 */
} gpio_pin_config_t;


/* 函数声明 */
void gpio_init(GPIO_Type *base, int pin, gpio_pin_config_t *config);
int gpio_pinread(GPIO_Type *base, int pin);
void gpio_pinwrite(GPIO_Type *base, int pin, int value);
void gpio_intconfig(GPIO_Type* base, unsigned int pin, gpio_interrupt_mode_t pinInterruptMode);
void gpio_enableint(GPIO_Type* base, unsigned int pin);
void gpio_disableint(GPIO_Type* base, unsigned int pin);
void gpio_clearintflags(GPIO_Type* base, unsigned int pin);

#endif

gpio.c

#include "bsp_gpio.h"
/***************************************************************
Copyright © zuozhongkai Co., Ltd. 1998-2019. All rights reserved.
文件名 : bsp_gpio.h
作者 : 左忠凯
版本 : V1.0
描述 : GPIO操作文件。
其他 : 无
论坛 : www.wtmembed.com
日志 : 初版V1.0 2019/1/4 左忠凯创建
V2.0 2019/1/4 左忠凯修改:
修改gpio_init()函数,支持中断配置.
添加gpio_intconfig()函数,初始化中断
添加gpio_enableint()函数,使能中断
添加gpio_clearintflags()函数,清除中断标志位

***************************************************************/

/*
* @description : GPIO初始化。
* @param - base : 要初始化的GPIO组。
* @param - pin : 要初始化GPIO在组内的编号。
* @param - config : GPIO配置结构体。
* @return : 无
*/
void gpio_init(GPIO_Type *base, int pin, gpio_pin_config_t *config)
{
base->IMR &= ~(1U << pin);

if(config->direction == kGPIO_DigitalInput) /* GPIO作为输入 */
{
base->GDIR &= ~( 1 << pin);
}
else /* 输出 */
{
base->GDIR |= 1 << pin;
gpio_pinwrite(base,pin, config->outputLogic); /* 设置默认输出电平 */
}
gpio_intconfig(base, pin, config->interruptMode); /* 中断功能配置 */
}

/*
* @description : 读取指定GPIO的电平值 。
* @param - base : 要读取的GPIO组。
* @param - pin : 要读取的GPIO脚号。
* @return : 无
*/
int gpio_pinread(GPIO_Type *base, int pin)
{
return (((base->DR) >> pin) & 0x1);
}

/*
* @description : 指定GPIO输出高或者低电平 。
* @param - base : 要输出的的GPIO组。
* @param - pin : 要输出的GPIO脚号。
* @param - value : 要输出的电平,1 输出高电平, 0 输出低低电平
* @return : 无
*/
void gpio_pinwrite(GPIO_Type *base, int pin, int value)
{
if (value == 0U)
{
base->DR &= ~(1U << pin); /* 输出低电平 */
}
else
{
base->DR |= (1U << pin); /* 输出高电平 */
}
}

/*
* @description : 设置GPIO的中断配置功能
* @param - base : 要配置的IO所在的GPIO组。
* @param - pin : 要配置的GPIO脚号。
* @param - pinInterruptMode: 中断模式,参考枚举类型gpio_interrupt_mode_t
* @return : 无
*/
void gpio_intconfig(GPIO_Type* base, unsigned int pin, gpio_interrupt_mode_t pin_int_mode)
{
volatile uint32_t *icr;
uint32_t icrShift;

icrShift = pin;

base->EDGE_SEL &= ~(1U << pin);

if(pin < 16) /* 低16位 */
{
icr = &(base->ICR1);
}
else /* 高16位 */
{
icr = &(base->ICR2);
icrShift -= 16;
}
switch(pin_int_mode)
{
case(kGPIO_IntLowLevel):
*icr &= ~(3U << (2 * icrShift));
break;
case(kGPIO_IntHighLevel):
*icr = (*icr & (~(3U << (2 * icrShift)))) | (1U << (2 * icrShift));
break;
case(kGPIO_IntRisingEdge):
*icr = (*icr & (~(3U << (2 * icrShift)))) | (2U << (2 * icrShift));
break;
case(kGPIO_IntFallingEdge):
*icr |= (3U << (2 * icrShift));
break;
case(kGPIO_IntRisingOrFallingEdge):
base->EDGE_SEL |= (1U << pin);
break;
default:
break;
}
}


/*
* @description : 使能GPIO的中断功能
* @param - base : 要使能的IO所在的GPIO组。
* @param - pin : 要使能的GPIO在组内的编号。
* @return : 无
*/
void gpio_enableint(GPIO_Type* base, unsigned int pin)
{
base->IMR |= (1 << pin);
}

/*
* @description : 禁止GPIO的中断功能
* @param - base : 要禁止的IO所在的GPIO组。
* @param - pin : 要禁止的GPIO在组内的编号。
* @return : 无
*/
void gpio_disableint(GPIO_Type* base, unsigned int pin)
{
base->IMR &= ~(1 << pin);
}

/*
* @description : 清除中断标志位(写1清除)
* @param - base : 要清除的IO所在的GPIO组。
* @param - pin : 要清除的GPIO掩码。
* @return : 无
*/
void gpio_clearintflags(GPIO_Type* base, unsigned int pin)
{
base->ISR |= (1 << pin);
}



添加了一个新枚举类型:gpio_interrupt_mode_t,枚举出了 GPIO 所有的中断触发类型。还修改了结构体 gpio_pin_config_t,在里面加入了 interruptMode 成员变量。最后就是添加了一些跟中断有关的函数声明,bsp_gpio.h
文件的内容总体还是比较简单的

实际上,再开一层exti的驱动,把gpio驱动用起来。

exti.h

#include "imx6ul.h"

/* 函数声明 */
void exit_init(void); /* 中断初始化 */
void gpio1_io18_irqhandler(void); /* 中断处理函数 */

#endif

exti.c

#include "bsp_exit.h"
#include "bsp_gpio.h"
#include "bsp_int.h"
#include "bsp_delay.h"
#include "bsp_beep.h"

/*
* @description : 初始化外部中断
* @param : 无
* @return : 无
*/
void exit_init(void)
{
gpio_pin_config_t key_config;

/* 1、设置IO复用 */
IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18,0); /* 复用为GPIO1_IO18 */
IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18,0xF080);

/* 2、初始化GPIO为中断模式 */
key_config.direction = kGPIO_DigitalInput;
key_config.interruptMode = kGPIO_IntFallingEdge;
key_config.outputLogic = 1;
gpio_init(GPIO1, 18, &key_config);

GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn); /* 使能GIC中对应的中断 */
system_register_irqhandler(GPIO1_Combined_16_31_IRQn, (system_irq_handler_t)gpio1_io18_irqhandler, NULL); /* 注册中断服务函数 */
gpio_enableint(GPIO1, 18); /* 使能GPIO1_IO18的中断功能 */
}

/*
* @description : GPIO1_IO18最终的中断处理函数
* @param : 无
* @return : 无
*/
void gpio1_io18_irqhandler(void)
{
static unsigned char state = 0;

/*
*采用延时消抖,中断服务函数中禁止使用延时函数!因为中断服务需要
*快进快出!!这里为了演示所以采用了延时函数进行消抖,后面我们会讲解
*定时器中断消抖法!!!
*/

delay(10);
if(gpio_pinread(GPIO1, 18) == 0) /* 按键按下了 */
{
state = !state;
beep_switch(state);
}

gpio_clearintflags(GPIO1, 18); /* 清除中断标志位 */
}



最终
main.c

#include "bsp_clk.h"
#include "bsp_delay.h"
#include "bsp_led.h"
#include "bsp_beep.h"
#include "bsp_key.h"
#include "bsp_int.h"
#include "bsp_exit.h"

/*
* @description : main函数
* @param : 无
* @return : 无
*/
int main(void)
{
unsigned char state = OFF;

int_init(); /* 初始化中断(一定要最先调用!) */
imx6u_clkinit(); /* 初始化系统时钟 */
clk_enable(); /* 使能所有的时钟 */
led_init(); /* 初始化led */
beep_init(); /* 初始化beep */
key_init(); /* 初始化key */
exit_init(); /* 初始化按键中断 */

while(1)
{
state = !state;
led_switch(LED0, state);
delay(500);
}

return 0;
}