回顾
一、核心关键字:volatile
1.1 作用
- 告诉编译器:被修饰的变量会被 “意外修改”(如硬件寄存器的值可能被外设自动更新),禁止编译器对该变量进行优化(如缓存到寄存器、删除未显式修改的代码)。
- 本质:确保每次访问变量时,都直接读取 / 写入内存地址,而非使用编译器缓存的 “旧值”。
1.2 寄存器操作中的必要性
以 GPIO 寄存器为例,若不加volatile
:
// 错误:编译器可能优化为“只写一次”,后续操作失效
#define GPIO1_DR *((unsigned int *)0x0209C000)
GPIO1_DR &= ~(1<<3); // 期望拉低引脚
GPIO1_DR |= (1<<3); // 期望拉高引脚(编译器可能认为“无用”,直接删除)
加volatile
后:
// 正确:每次操作都直接访问0x0209C000地址
#define GPIO1_DR *((volatile unsigned int *)0x0209C000)
二、基础 C 语言点灯实现
2.1 寄存器地址定义
两种常见方式:直接宏定义、结构体封装(推荐后者,更易维护)。
方式 1:直接宏定义
// I.MX6ULL 关键寄存器(引脚复用、GPIO控制)
//int 指令/寄存器是四个字节
#define IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 *((volatile unsigned int *)0x020E0068) // 引脚复用控制
#define IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 *((volatile unsigned int *)0x020E02F4) // 引脚电气属性(上拉/驱动能力)
#define GPIO1_DR *((volatile unsigned int *)0x0209C000) // GPIO数据寄存器
#define GPIO1_GDIR *((volatile unsigned int *)0x0209C004) // GPIO方向寄存器(输入/输出)
// volatile 防止编译器优化: reg = reg //会被优化掉
// const char *p;
// char * const p;
// ------定义时钟门寄存器地址
#define CCM_CCGR0 *((volatile unsigned int *)0x020C4068)
#define CCM_CCGR1 *((volatile unsigned int *)0x020C406C)
#define CCM_CCGR2 *((volatile unsigned int *)0x020C4070)
#define CCM_CCGR3 *((volatile unsigned int *)0x020C4074)
#define CCM_CCGR4 *((volatile unsigned int *)0x020C4078)
#define CCM_CCGR5 *((volatile unsigned int *)0x020C407C)
#define CCM_CCGR6 *((volatile unsigned int *)0x020C4080)
方式 2:结构体封装(优化访问)
按寄存器地址偏移顺序定义结构体,直接映射到基地址:
// GPIO寄存器结构体(对应I.MX6ULL GPIO模块寄存器偏移)
struct GPIO_t {
volatile unsigned int DR; // 数据寄存器(0x00)
volatile unsigned int GDIR; // 方向寄存器(0x04)
volatile unsigned int PSR; // 状态寄存器(0x08)
volatile unsigned int ICR1; // 中断控制1(0x0C)
volatile unsigned int ICR2; // 中断控制2(0x10)
volatile unsigned int IMR; // 中断屏蔽(0x14)
volatile unsigned int ISR; // 中断状态(0x18)
volatile unsigned int EDGE_SEL; // 边沿选择(0x1C)
};
// 宏定义GPIO1:结构体指针指向GPIO1基地址0x0209C000
#define GPIO1 (*((struct GPIO_t *)0x0209C000))
2.2 核心功能代码
1. 时钟初始化(必须先使能)
I.MX6ULL 外设默认时钟关闭,需打开对应时钟门控(CCM_CCGRx
):
void clock_init(void) {
// 打开所有外设时钟(简化操作,实际可按需使能)
CCM_CCGR0 = 0xFFFFFFFF;
CCM_CCGR1 = 0xFFFFFFFF;
CCM_CCGR2 = 0xFFFFFFFF;
CCM_CCGR3 = 0xFFFFFFFF;
CCM_CCGR4 = 0xFFFFFFFF;
CCM_CCGR5 = 0xFFFFFFFF;
CCM_CCGR6 = 0xFFFFFFFF;
}
2. LED 初始化(引脚复用 + GPIO 配置)
void led_init(void) {
// 1. 引脚复用:将GPIO1_IO03配置为GPIO功能(复用值0x05)
IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = 0x05;
// 2. 引脚电气属性:上拉、100MHz驱动、速度等级1(0x10B0为标准配置)
IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 = 0x10B0;
// 3. GPIO方向:设置GPIO1_IO03为输出(GDIR对应位写1)
GPIO1_GDIR |= (1 << 3)
}
3. LED 控制函数
4.整体main.c
2.3 makefile 编译图表讲解
2.4 优化 Makefile(交叉编译)
针对 ARM 架构(I.MX6ULL)的交叉编译脚本,支持编译、链接、生成 bin 文件、下载:
makefile
# 交叉编译器前缀(需确保环境变量已配置)
COMPLITER = arm-linux-gnueabihf-
CC = $(COMPLITER)gcc # 编译器
LD = $(COMPLITER)ld # 链接器
OBJCOPY = $(COMPLITER)objcopy # 格式转换(elf→bin)
OBJDUMP = $(COMPLITER)objdump # 反汇编(elf→dis)
# 目标文件与最终目标
OBJS = start.o main.o # 依赖的目标文件(start.S是启动汇编)
TARGET = led # 目标名称
# 生成bin文件:依赖elf,elf依赖o文件
$(TARGET).bin : $(OBJS)
$(LD) -Ttext 0x87800000 $^ -o $(TARGET).elf # 链接到I.MX6ULL运行地址0x87800000
$(OBJCOPY) -O binary -S -g $(TARGET).elf $@ # elf转bin(删除调试信息)
$(OBJDUMP) -D $(TARGET).elf > $(TARGET).dis # 生成反汇编文件(调试用)
# 汇编文件(.S)编译为.o
%.o : %.S
$(CC) -c $^ -o $@ -g # -g:保留调试信息
# C文件(.c)编译为.o
%.o : %.c
$(CC) -c $^ -o $@ -g
# 清理目标文件
clean:
rm $(OBJS) $(TARGET).elf $(TARGET).bin $(TARGET).dis -f
# 下载到SD卡(使用imxdownload工具)
load:
./imxdownload $(TARGET).bin /dev/sdb # /dev/sdb是SD卡设备节点
三、NXP I.MX6ULL SDK 移植
3.1 SDK 使用原则
- SDK(Software Development Kit)包含完整 IDE(需下载器 / 仿真器,成本高),实际仅使用其头文件(标准化寄存器定义,避免硬编码)。
- 1.SDK文件存放位置
路径:IMAX6ULL/SDK/
(1).SDK(Software development tools)移植
(2).完整开发工具就是一个IDE, 集代码编写、编译、下载于一体的集成开发环境, 类似于keil这种工具,要是用这个需要额外购买一些设备如下载器、编程器、仿真器
(3).所以只用它的头文件。- 关键文件:
cc.h
、core_ca7.h
、fsl_common.h
、fsl_iomuxc.h
、MCIMX6Y2.h
(I.MX6ULL 芯片定义)。
3.2 移植步骤(新建工程led_sdk
)
-
工程结构初始化:
- 拷贝旧工程的
start.S
(启动汇编)、main.c
、Makefile
到led_sdk
。 - 拷贝 SDK 所有头文件到工程根目录(或单独文件夹)。
- 拷贝旧工程的
-
用 SDK 重构代码(简化寄存器操作):
SDK 头文件已封装CCM
、IOMUXC
、GPIO
为结构体,直接用->
访问:#include "MCIMX6Y2.h" // 包含SDK芯片定义 #include "fsl_iomuxc.h" // 包含引脚复用函数 void clock_init(void) { // SDK已定义CCM结构体,直接访问CCGR寄存器 CCM->CCGR0 = 0xFFFFFFFF; CCM->CCGR1 = 0xFFFFFFFF; // ... 其余CCGR寄存器同上 } void led_init(void) { // 1. SDK函数:设置引脚复用(GPIO1_IO03→GPIO功能,参数2为额外配置) IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0); // 2. SDK函数:设置引脚电气属性(0x10B0为标准配置) IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0x10B0); // 3. GPIO方向配置(SDK已定义GPIO1结构体) GPIO1->GDIR |= (1 << 3); }
3.3 ----led_sdk------更新程序
1、查阅手册 
main.c
led.c
led.h
start.S
四、BSP(板级支持包)工程管理
4.1 工程目录结构(模块化,易维护)
1.project :存放必要程序
main.c start.S
2.imx6ull :存放NXP提供的i.mx6ull头文件
cc.h core_ca7.h fsl_common.h fsl_iomuxc.h MCIMX6Y2.h
3.bsp :存放硬件外设相关功能模块
led.c led.h beep.c beep.h
4.Makefile: 需要遍目录
led_bsp/
├── project/ # 主程序目录
│ ├── main.c # 主函数(调用BSP模块)
│ └── start.S # 启动汇编(初始化栈、清BSS)
├── imx6ull/ # SDK头文件目录
│ ├── cc.h
│ ├── core_ca7.h
│ ├── fsl_common.h
│ ├── fsl_iomuxc.h
│ └── MCIMX6Y2.h
├── bsp/ # 硬件外设模块目录(按外设拆分)
│ ├── led/
│ │ ├── led.c # LED驱动实现
│ │ └── led.h # LED驱动声明
│ └── beep/
│ ├── beep.c # 蜂鸣器驱动实现
│ └── beep.h # 蜂鸣器驱动声明
├── Makefile # 多目录编译脚本(需支持遍历bsp/)
└── imx6ull.lds # 链接脚本
4.2 BEEP 蜂鸣器驱动(程序)
- 硬件:
S8550(PNP型三极管)
,基极高电平导通(蜂鸣器响)。- 引脚:假设使用
GPIO1_IO04
,驱动逻辑与 LED 类似。
beep.h
#ifndef __BEEP_H
#define __BEEP_H
#include "MCIMX6Y2.h"
void beep_init(void); // 蜂鸣器初始化
void beep_on(void); // 蜂鸣器响
void beep_off(void); // 蜂鸣器停
#endif
beep.c
#include "beep.h"
#include "fsl_iomuxc.h"
void beep_init(void) {
// 1. 引脚复用:GPIO1_IO04→GPIO功能
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO04_GPIO1_IO04, 0);
// 2. 引脚电气属性配置
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO04_GPIO1_IO04, 0x10B0);
// 3. GPIO方向:输出(默认熄灭,先拉低)
GPIO1->GDIR |= (1 << 4);
GPIO1->DR &= ~(1 << 4);
}
void beep_on(void) {
GPIO1->DR |= (1 << 4); // 基极高电平→PNP导通→蜂鸣器响
}
void beep_off(void) {
GPIO1->DR &= ~(1 << 4); // 基极低电平→PNP截止→蜂鸣器停
}
main.c
makefile 改动
五、链接脚本(imx6ull.lds)
5.1 作用
- 告诉链接器:代码段、数据段、BSS 段的存放地址和顺序(为 I.MX6ULL 启动做准备)。
- 关键:
start.S
(启动代码)需放在最前面,且需初始化BSS段
(未初始化全局变量清 0)。- 1.链接脚本: imx6ull.lds
- 2.链接主要在链接阶段,为连接器提供蓝图;
- 3.启动代码需要在进入C语言第一条指令前,将.bss COMMON段初始化清0
5.2 内存段总结----------链接脚本知识点
5.3 脚本内容
SECTIONS
{
. = 0x87800000; // 程序运行起始地址(I.MX6ULL DDR地址)
// 代码段(.text):先放start.o(启动汇编),再放其他代码
.text :
{
obj/start.o // 启动代码优先(需确保编译时生成到obj目录)
*(.*) // 其他所有.text段(C代码、SDK函数等)
}
// 只读数据段(.rodata):对齐4字节
.rodata ALIGN(4) : {*(.rodata*)}
// 已初始化数据段(.data):对齐4字节
.data ALIGN(4) : {*(.data)}
// BSS段(未初始化全局变量):标记起始/结束地址,供启动代码清0
__bss_start = .; // BSS段起始地址
.bss ALIGN(4) : {*(.bss) *(COMMON)} // 包含BSS和COMMON段
__bss_end = .; // BSS段结束地址
}
5.4 关键注意点
- 启动代码(
start.S
)需添加BSS段清0
逻辑:// 清BSS段:从__bss_start到__bss_end,逐个字节写0 ldr r0, =__bss_start ldr r1, =__bss_end mov r2, #0 bss_loop: cmp r0, r1 bge bss_end str r2, [r0], #4 b bss_loop bss_end:
1.链接脚本的作用?各个段存放什么类型数据
链接脚本的核心作用
- 告诉链接器:代码段、数据段、BSS 段的存放地址和顺序(为 I.MX6ULL 启动做准备)。
- 关键:
start.S
(启动代码)需放在最前面,且需初始化BSS段
(未初始化全局变量清 0)。- 定义程序的加载地址(如嵌入式系统中指定程序在 RAM 中的运行地址,如 i.MX6ULL 的
0x87800000
);- 规定目标文件中各个 “段(Section)” 的排列顺序和内存分配;
- 标记特殊段(如
.bss
)的起始和结束地址,供启动代码初始化(如将.bss
段清 0);- 确保代码段、数据段等按正确的内存对齐方式(如 4 字节对齐)排列,避免硬件访问错误。
各段的作用及存放数据类型
程序被编译后会拆分为多个 “段”,链接脚本通过
SECTIONS
命令定义这些段的位置和内容:
段名称 作用及存放数据类型 .text
代码段,存放可执行代码,包括:汇编指令(如 start.S
中的初始化代码)、C 语言函数(如main
、led_init
)。.rodata
只读数据段,存放常量数据,如:字符串常量( "hello"
)、const
修饰的全局变量(const int a = 10
)。.data
初始化数据段,存放已初始化的全局变量和静态变量,如: int g_var = 5
(非const
且有初始值)。.bss
未初始化数据段,存放未初始化的全局变量、或初始化为0的数据、静态变量及 COMMON
段(用于存放未初始化的非静态全局变量,如未初始化的大数组int buf[100]
)。
特点:程序加载时不占用磁盘空间,运行时需通过启动代码清 0(避免随机值影响)。__bss_start
/__bss_end
不是实际的段,而是链接脚本定义的标记符号,分别表示 .bss
段的起始和结束地址,供启动代码遍历清 0。2.编译过程需要哪些工具,分别什么作用?
从源代码(
.c
、.S
)到可执行程序,需经过预处理→编译→汇编→链接→格式转换5 个阶段,对应工具及作用如下:1. 预处理工具:
gcc -E
(预处理器)
- 作用:处理源代码中的预处理指令(以
#
开头),生成纯 C 代码(.i
文件)。- 具体操作:
- 展开
#include
头文件(如将#include "led.h"
替换为头文件内容);- 替换
#define
宏定义(如将LED_PIN
替换为实际值);- 删除注释、处理条件编译(
#if
/#else
/#endif
)。- 示例:
arm-linux-gnueabihf-gcc -E main.c -o main.i
2. 编译工具:
gcc -S
(编译器)
- 作用:将预处理后的
.i
文件(纯 C 代码)转换为汇编代码(.s
文件)。- 核心功能:进行语法检查、语义分析、代码优化(如循环展开),最终生成对应架构的汇编指令(如 ARM 架构的
ldr
、str
指令)。- 示例:
arm-linux-gnueabihf-gcc -S main.i -o main.s
3. 汇编工具:
gcc -c
或as
(汇编器)
- 作用:将汇编代码(
.s
)转换为机器码(二进制目标文件,.o
)。- 特点:
.o
文件是 “relocatable(可重定位)” 的,即代码中的地址是相对地址(需后续链接器处理)。- 示例:
arm-linux-gnueabihf-gcc -c main.s -o main.o
或arm-linux-gnueabihf-as main.s -o main.o
4. 链接工具:
ld
(链接器)
- 作用:将多个
.o
目标文件(如start.o
、main.o
、led.o
)合并为一个可执行文件(.elf
)。- 核心操作:
- 解析符号引用(如
main
函数调用led_init
时,找到led_init
在.text
段的实际地址);- 按链接脚本(如
imx6ull.lds
)分配各段的内存地址(将相对地址转换为绝对地址);- 处理段的对齐和拼接。
- 示例:
arm-linux-gnueabihf-ld -T imx6ull.lds start.o main.o -o led.elf
5. 格式转换工具:
objcopy
- 作用:将链接生成的
.elf
文件(包含符号表、调试信息等)转换为纯二进制文件(.bin
),适用于嵌入式系统加载。- 特点:
.bin
文件仅保留可执行代码和数据,去除调试信息,体积更小,可直接被 CPU 执行。- 示例:
arm-linux-gnueabihf-objcopy -O binary led.elf led.bin
6. 辅助工具:
objdump
(反汇编器)
- 作用:将
.elf
文件反汇编为汇编代码(.dis
),用于调试(如查看 C 代码对应的汇编指令、定位错误地址)。- 示例:
arm-linux-gnueabihf-objdump -D led.elf > led.dis
总结
- 链接脚本是内存布局的 “规划图”,决定各段在内存中的位置和内容;
- 编译过程是 “源代码→机器码” 的转换链,每个工具负责一个阶段,最终生成可在目标硬件上运行的二进制文件。