#IMX6ULL #ARM 裸机开发 #Cortex‑A7 #GICV2 # 嵌入式 # 汇编点灯 #Makefile #GPIO 中断
前言
📚学习平台:IMX6ULL(Cortex‑A7 内核)裸机开发 💻开发环境:Windows VSCode + Ubuntu 交叉编译环境 + FileZilla FTP 文件传输 📝学习内容:DDR 内存布局、GPIO 点灯原理、汇编 & C 实现点灯、Makefile 工程管理、GIC V2 中断控制器、协处理器 CP15、异常向量表
最近在学习 NXP i.MX6ULL Cortex‑A7 裸机开发,裸机开发没有操作系统的加持,所有寄存器、时钟、引脚、中断全部需要我们手动配置。从最经典的点灯实验入门,一步步理解 ARM 架构底层,理解链接、编译、内存布局、中断整套体系。本文把课堂学习笔记做完整扩写,适合零基础入门 ARM 裸机的小伙伴,同时可以作为复习笔记。
开发环境部署说明
- Windows 端安装 VSCode,安装插件:
C/C++、C/C++ Extension Pack、ARM Assembly、Makefile Tools。VSCode 只做代码编辑,真正编译工作交给 Ubuntu。- Ubuntu 系统安装
arm‑linux‑gnueabihf交叉编译工具链,生成适合 ARM 架构的二进制程序。- 使用 FileZilla FTP Client 实现 Windows 与 Ubuntu 之间文件互传,Windows 写代码,Ubuntu 编译。
一、IMX6ULL 硬件基础认知
1. 存储介质区分:NorFlash、NAND/EMMC、DDR3
很多刚从单片机转 ARM 裸机的同学容易混淆存储介质,这里做对比:
- S3C2440 Nor Flash :地址范围
0x0 ~ 0x200000,可以直接 CPU 寻址,CPU 可以直接从 Flash 读取指令运行代码。 - IMX6ULL EMMC(8GB) :属于 NAND 类存储,不支持 CPU 直接寻址,不能直接在 EMMC 运行程序。程序需要先拷贝到 DDR 内存,CPU 再在 DDR 中运行代码。
- DDR3 :IMX6ULL 搭载 512MB 的 DDR3 内存,起始地址
0x80000000,结束地址0xA0000000。DDR 属于运行内存,断电丢失数据,我们裸机程序就是在 DDR 里面运行。
2. DDR3 内存栈划分
Cortex‑A7 有多种工作模式,不同模式拥有独立的栈空间。栈用于函数调用、寄存器压栈保存现场,IRQ 中断模式发生时,必须使用独立 IRQ 栈,不能共用系统栈,否则栈溢出破坏现场。
0x80000000 ---------------------------------------------
系统模式栈 sp_system
0x84000000 ---------------------------------------------
IRQ模式栈 sp_irq(大小32MB)
0x86000000 ---------------------------------------------
剩余可用内存
0xA0000000
程序链接运行地址设置为0x87800000,这个地址落在 DDR 内存区间,使用链接参数‑Ttext 0X87800000指定.text代码段的运行地址。
小提示:栈地址是高地址向低地址增长,栈指针 SP 需要预先设置,汇编启动文件必须初始化各个模式下 SP 指针,否则 C 语言无法运行。
3. GPIO 操作三大模块详解
IMX6ULL 总共有 5 组 GPIO:GPIO1、GPIO2、GPIO3、GPIO4、GPIO5。 想要操作 LED,不是直接操作 GPIO 寄存器,必须配置引脚复用 IOMUXC、GPIO 功能寄存器、CCM 时钟门控这三大模块,缺一不可。
① IOMUXC 模块:引脚复用 + 电气属性
一颗 SOC 芯片引脚数量有限,一个物理引脚可以复用为 GPIO、UART、I2C、SPI 等多种功能,由 IOMUXC 控制器管理。
IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03:引脚复用选择寄存器,把物理引脚设置为 GPIO 功能。如果不配置复用,引脚可能是其他外设功能,点灯不会有任何效果。IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03:PAD 电气属性寄存器。包含摆率、上下拉、驱动能力等参数。- 高压摆率:信号翻转速度快,波形曲线抖动明显,适合高速外设。
- 低压摆率:信号翻转平缓,波形平滑,适合低速设备例如 LED。
② GPIO 功能寄存器组
表格
| 寄存器名称 | 功能说明 |
|---|---|
| GPIOx_GDIR | GPIO 方向寄存器。bit 位 = 1:输出模式;bit 位 = 0:输入模式。点灯必须配置为输出。 |
| GPIOx_DR | 数据输出寄存器。对应 bit 写 1 输出高电平;写 0 输出低电平,用来点亮熄灭 LED。 |
| GPIOx_PSR | 引脚状态寄存器,只读。读取外部引脚真实电平状态,做按键输入的时候使用。 |
| GPIOx_ICR | 中断触发配置寄存器,配置是电平触发(高 / 低电平)还是边沿触发(上升沿 / 下降沿)。 |
| GPIOx_IMR | 中断屏蔽寄存器。1 代表打开该引脚中断,0 屏蔽中断。 |
| GPIOx_ISR | 中断状态寄存器。判断引脚是否产生中断,写 1 清零,读取为 1 表示发生中断。 |
| GPIOx_EDGE_SEL | 双边沿中断选择寄存器,可以配置为同时捕获上升沿和下降沿。 |
③ CCM 时钟控制模块
SOC 的外设默认时钟是关闭的,用来降低功耗。GPIO、UART、TIMER 全部受 CCM 时钟门控管理。 CCM_CCGR0 ~ CCM_CCGR6共 7 个寄存器,管理所有外设时钟。写入0xFFFFFFFF就打开全部外设时钟门。时钟不打开,GPIO 寄存器读写无效,LED 无法点亮。
点灯操作流程总结:
- CCM 打开 GPIO 外设时钟
- IOMUXC 设置引脚复用为 GPIO 功能,配置电气属性
- GPIOx_GDIR 设置引脚为输出模式
- GPIOx_DR 输出高低电平,控制 LED 亮灭
4. 点灯三种实现方式
- 汇编点灯:纯汇编语言直接操作寄存器,不依赖 C 库,最贴近 CPU 底层,适合理解 ARM 汇编指令。
- C 语言点灯:汇编只做启动初始化(设置栈、关闭看门狗),硬件寄存器用 C 指针访问,开发效率高,实际裸机开发最常用。
- NXP 官方固件库点灯:NXP 封装好寄存器操作,直接调用库函数,不用记忆寄存器地址,类似 STM32 库开发。
二、交叉编译、链接、Makefile 工程体系
1. 完整交叉编译命令解析
裸机程序没有操作系统,不能使用 x86 平台 gcc,必须使用 ARM 交叉编译器arm‑linux‑gnueabihf‑gcc。
#1.编译汇编启动文件start.S,生成start.o目标文件,-g加入调试信息
arm‑linux‑gnueabihf‑gcc -g -c start.S -o start.o
#2.编译C语言主程序main.c生成main.o
arm‑linux‑gnueabihf‑gcc -g -c main.c -o main.o
#3.链接:把多个.o文件合并,指定代码段运行地址,输出elf格式文件
arm‑linux‑gnueabihf‑ld -Ttext 0x87800000 start.o main.o -o start.elf
#4.objcopy:elf文件转换为二进制bin文件,烧录到EMMC。-S -g去掉调试符号信息
arm‑linux‑gnueabihf‑objcopy -O binary -S -g start.elf start.bin
工具链分工:
gcc:编译 C / 汇编,输出.o目标文件。ld链接器:把多个目标文件、库文件合并,排布各个段的内存地址,输出 elf。可以使用自定义链接脚本实现复杂段管理。编译参数-I指定头文件搜索路径,‑L指定库文件搜索路径。objcopy:elf 格式转 bin 原始二进制镜像,bin 是真正烧录运行的文件。objdump:反汇编工具,调试时查看汇编代码。
2. ELF 文件五大段含义
elf 文件内部会划分不同段,不同数据存放在不同段:
表格
| 段名 | 存放内容 |
|---|---|
.text |
代码段,存放汇编、C 语言编译出来的指令,‑Ttext就是修改该段运行地址 |
.data |
数据段,存放已经初始化完成的全局变量、静态局部变量 |
.bss |
存放初始化为 0 的全局 / 静态变量,bin 文件中不占用空间,程序启动时自动清零 |
.common |
存放没有初始化的全局变量 |
.rodata |
只读数据段,存放字符串常量、const 修饰只读数据 |
注意:bss 和 common 段不会占用 bin 镜像大小,程序运行起来之后在 DDR 内存分配空间。
3. volatile 关键字(裸机开发重中之重)
C 语言访问硬件寄存器的时候,一定要加volatile修饰。 编译器会对代码做优化,如果不加 volatile,编译器会认为寄存器的值没有变化,直接使用寄存器缓存,不去真实读取硬件地址。
//正确写法,volatile告诉编译器每次都访问真实硬件地址,禁止优化
#define CCM_CCGR0 (*((volatile unsigned int *)0x020C4068))
- volatile 修饰变量:该变量读写不能被编译器优化。
- volatile 修饰指针:指针指向的硬件内存空间,每次必须真实读写。
4. Makefile 规则
裸机工程文件变多之后,手动敲编译命令效率很低,使用 Makefile 自动化编译。 Makefile 的三要素:目标、依赖、规则 语法格式:
目标:依赖文件
#shell命令,前面必须是Tab键,不能空格
- 目标:想要生成的文件,例如 elf、bin、.o
- 依赖:生成目标需要哪些源文件
- 规则:执行的编译链接 shell 命令
5. 推荐工程目录结构
实际项目不要把所有文件堆在同一个文件夹,做模块化分层,参考下面目录结构:
project/ #项目根目录
├─ start.S #汇编启动文件,设置栈、异常向量表
├─ main.c #主业务逻辑
├─ Makefile #编译脚本
├─ imx6ull/ #芯片库文件,寄存器定义
├─ bsp/ #板级外设驱动层
│ ├─ led.c
│ ├─ led.h
│ ├─ beep.c
│ └─ beep.h
└─ obj/ #编译中间产物,存放.o目标文件
三、IMX6ULL GIC V2 中断系统深度讲解
1. 对比 51 单片机中断
51 单片机中断流程相对简单: 外设产生中断 → 设置外设中断允许 → CPU总中断开关 → 设置优先级 → 跳转到中断服务函数
ARM Cortex‑A7 的中断体系复杂很多,引入专门的GIC 通用中断控制器外设来统一管理全部中断源。
2. GIC 介绍
GIC(Generic Interrupt Controller,通用中断控制器),是 ARM 设计的 IP 核,不是 NXP 设计。不同版本 GIC 支持不同 CPU:
- GIC V1:Cortex‑A5 老款芯片
- GIC V2:IMX6ULL、树莓派,32 位处理器,最多支持 1020 路中断,IMX6ULL 实际只用 160 个中断源,支持虚拟化,最多 8 核。
- GIC V3/V4:面向 64 位多核处理器,手机、高端嵌入式芯片。
- GIC V5:面向服务器、车载算力芯片,支持数百核。
IMX6ULL 整体架构:
- Cortex‑A7 内核(ARM 设计)
- GICV2 中断控制器(ARM 设计)
- GPIO、UART、TIMER 等片内外设(NXP 设计)
GICV2 三类中断源
- SPI 共享外设中断:所有片上外设(GPIO、UART、定时器)产生的中断,多个 CPU 核都可以接收处理,裸机开发接触最多。
- PPI 私有中断:CPU 核私有中断,多核之间核间通信,和外部外设无关。
- SGI 软件中断:软件指令主动触发产生的中断,多核通信使用。
3. IMX6ULL 完整中断执行流程
- 外设层 :配置外设寄存器。以 GPIO 按键中断举例:配置
GPIO1_ICR2设置触发方式;GPIO1_IMR打开引脚中断使能。外设检测信号变化,向 GIC 发出中断请求。 - GIC 控制器处理:GIC 接收中断请求,判断中断屏蔽状态,读取中断优先级,对比优先级,把最高优先级中断发送给 CPU。
- CPU 内核处理阶段
- CPSR 寄存器打开 CPU 全局 IRQ 中断总开关。
- CPU 硬件自动切换进入 IRQ 异常模式,保存 CPSR 到 SPSR_irq,PC 跳转至异常向量表 IRQ 入口。
- 异常向量表跳转执行 IRQ 异常处理汇编代码。
- 汇编代码读取 GIC 寄存器
C_IAR,拿到中断号,区分是哪一个外设触发中断。 - 根据中断号调用对应外设中断服务函数。
- 中断处理完成,必须把中断号写入
C_EOIR(中断结束寄存器)通知 GIC 本次中断处理完毕。不写 EOIR 会导致 GIC 不会接收下一次同类型中断。 - 恢复寄存器现场,从 IRQ 模式返回到被打断的程序。
重要寄存器总结 外设侧 (GPIO)
GPIO1_ICR2:中断触发方式GPIO1_IMR:中断使能GPIO1_ISR:中断状态,写 1 清零GIC 控制器侧
C_IAR只读:获取中断号C_EOIR只写:通知 GIC 中断处理结束
4. CP15 协处理器
Cortex‑A7 内核内置协处理器 cp0~cp15,CP15 是系统控制协处理器,没有内存地址,不能通过 C 指针访问,只能用汇编指令访问,用来控制 MMU、Cache、异常向量表重映射等底层功能。
两条核心汇编指令:
mrc:将协处理器寄存器读取到 CPU 通用寄存器mcr:将 CPU 通用寄存器写入协处理器寄存器
指令格式:
mrc <coproc>, <opc1>, <Rt>, <CRn>, <CRm>{, <opc2>}
示例:操作 SCTLR 系统控制寄存器,打开 I‑Cache
mrc p15, 0, r0, c1, c0, 0 ;读取cp15 c1(SCTLR)寄存器到r0
bic r0, r0, #(1 <<13) ;清除对应bit
orr r0, r0, #(1 << 12) ;置位bit12,打开Icache
mcr p15, 0, r0, c1, c0, 0 ;把r0写回cp15 c1寄存器
mcr p15,0,r0,c12,c0,0 ;异常向量表重映射,设置向量表基地址
5. SVC 软中断 & 异常向量表
异常向量表是一片内存,存放各个异常的入口地址。当发生异常(复位、SVC 软中断、IRQ、FIQ、数据中止等)CPU 自动跳转到向量表对应位置。
软中断指令:SVC #<imm24>,例如svc #7。执行这条指令立刻触发 SVC 异常,跳转到异常向量表 SVC 入口,常用于模拟系统调用。
注意:裸机开发需要手动重映射异常向量表,告诉 CPU 异常向量表放在 DDR 内存哪个地址,通过 cp15 c12 寄存器设置。
6. inline 内联函数
C 语言inline内联函数,编译的时候,会把函数代码直接展开到调用处,省去函数调用跳转开销,提升执行效率。使用要求:调用 inline 函数之前必须看到函数定义,不能只有声明。裸机中断处理中可以用来封装简短寄存器操作。
四、整体学习总结
- IMX6ULL 裸机点灯三步走:开启 CCM 外设时钟 → IOMUXC 配置引脚复用与电气属性 → GPIO 配置方向,输出电平。任何一步配置错误 LED 都无法正常工作。
- 裸机 C 语言直接操作硬件寄存器,volatile 必不可少,防止编译器做优化,屏蔽硬件真实读写。
- 编译链路流程:源文件 (.S/.c) 编译生成.o 目标文件 → ld 链接排布内存地址生成 elf → objcopy 转为 bin 镜像烧录运行。不同段 (.text/.data/.bss 等) 各司其职,存放不同类型数据。
- Makefile 管理工程,三要素目标、依赖、规则,简化重复编译命令。项目做好模块化分层,区分启动文件、驱动、主逻辑。
- IMX6ULL 中断由三层协同工作:外设寄存器配置中断条件;GICV2 控制器管理所有中断源,分发中断;CPU 内核配合 CPSR、异常向量表、CP15 协处理器完成异常处理。中断处理结束必须向 GIC 的 C_EOIR 寄存器写入中断号。
- CP15 协处理器只能汇编指令 mrc/mcr 访问,控制 Cache、MMU、异常向量表基地址等底层硬件。SVC 指令触发软中断,属于 ARM 异常中的一类。