文章目录
-
- 前言
- 一、GNU工具链常用工具说明
-
- 1.1 gcc (GNU Compiler Collection)
- 1.2 ld (GNU Linker)
- 1.3 objcopy (Binary File Converter)
- 1.4 objdump (Object File Disassembler)
- 二、Makefile
-
- 2.1 Makefile基本语法
- 2.2 简易版Makefile
- 2.3 链接脚本
- 2.4 多目录工程Makefile
- 三、 内存段(Memory Segments)
-
- 3.1 bss段
- 3.2 COMMON段
- 3.3 data段
- 3.4 rodata段(只读数据段)
- 3.5 各段的特点比较
- 3.6 在链接脚本中的定义
- 3.7 关于段名中的通配符说明
- 四、C语言实现点亮LED
- 五、C语言实现蜂鸣器

前言
在上篇【ARM裸机开发(IMX6ULL-mini)】arm上的点灯大师:环境配置、基础知识学习及点亮LED灯文章中我们完成了对imx6ull上的LED点灯操作,但是大家会发现,当你修改过代码后,再次编译下载又需要执行这么多指令。效率太低,这时候就得想到老朋友Makefile了。在这篇文章中将分享GNU工具链的说明,也就是上篇文章指令使用到的相关工具的说明,然后就是Makefile工具和引入NXP公司封装的库以及用借助库C语言实现点亮LED灯和使蜂鸣器发声。
一、GNU工具链常用工具说明
1.1 gcc (GNU Compiler Collection)
功能:编译器(Compiler)
- 将C/C++等高级语言源代码转换为汇编代码
- 再将汇编代码转换为目标文件
- 支持多种编程语言和目标平台
- 提供丰富的优化选项和编译控制选项
1.2 ld (GNU Linker)
功能:链接器(Linker)
- 将多个目标文件链接成可执行文件
- 解析和处理符号引用
- 进行地址分配和重定位
- 处理库文件的链接
1.3 objcopy (Binary File Converter)
功能:目标文件格式转换器
- 在不同格式的目标文件之间进行转换
- 将ELF格式转换为纯二进制格式(.bin)
- 从目标文件中提取特定的段(sections)
- 修改目标文件的内容
- 常用于嵌入式开发中的固件生成
1.4 objdump (Object File Disassembler)
功能:目标文件分析器/反汇编器
- 将机器码反汇编为汇编代码
- 查看目标文件的详细信息
- 分析程序的执行指令
- 显示各种文件格式信息
二、Makefile
2.1 Makefile基本语法
对于Makefile,大家可以先看看下面这篇文章,关于Makefile的基本使用。 linux系统中项目工程中的多文件管理(makefile)
Makefile有三个要素:
- 目标(目标文件)
- 依赖(源文件)
- 规则
2.2 简易版Makefile
在了解Makefile的基本语法后,我们可以根据指令写出这样一个简易版的Makefile:
start.bin : start.S
arm–linux–gnueabihf–gcc –g –c start.S –o start.o
arm–linux–gnueabihf–ld –Ttext 0X87800000 start.o –o start.elf
arm–linux–gnueabihf–objcopy –O binary –S –g start.elf start.bin
arm–linux–gnueabihf–objdump –D start.elf > start.dis
clean :
rm –f start.bin start.elf start.o start.dis
但在实现C语言点亮LED灯后我们又会发现,会有main.c start.S多个源文件需要编译。试想一下随着后面功能的增多,会有越来越多的源文件,像这样写Makefile,在每次增加功能后都需要更改Makefile。所以,我们对上面的Makefile进行完善:
target = led
objs = start.o main.o led.o
$(target).bin : $(objs)
arm–linux–gnueabihf–ld –Ttext 0X87800000 $(objs) –o $(target).elf
arm–linux–gnueabihf–objcopy –O binary –S –g $(target).elf $(target).bin
arm–linux–gnueabihf–objdump –D $(target).elf > $(target).dis
%.o : %.S
arm–linux–gnueabihf–gcc –g –c $^ –o $@
%.o : %.c
arm–linux–gnueabihf–gcc –g –c $^ –o $@
clean :
rm –f $(target).bin $(target).elf $(target).dis $(objs)
不难发现,在arm-linux-gnueabihf-ld -Ttext 0X87800000 $(objs) -o $(target).elf这条指令中,我们写了一个固定的数据-Ttext 0X87800000;
为什么这样写,这就得解释一下了:由于.o 是单独编译的碎片,CPU 不能直接跑一堆零散.o;ld链接器将多个目标文件链接成可执行文件,链接的本质就是把碎片拼成一整个程序,并且规定好每一块在内存的位置,最后打包成 elf。
特别注意:- start.o 是启动汇编,必须放在最开头,上电第一条指令- main.o、led.o 是业务 C 代码,放在后面;
链接器把所有.o里面各自的.text全部合并成一个大的.text 段;所有.rodata合并成一个大.rodata,以此类推。
像这样直接写死地址,对我们在后续的代码编写中是不方便的,因此,我们将会使用一个链接脚本来代替这个地址。
2.3 链接脚本
链接脚本: 链接脚本是给链接器 ld 看的配置文件,告诉 arm-linux-gnueabihf-ld: 把一堆编译出来的 .o 目标文件,怎么拼接、放到哪个内存地址、怎么划分段,最终生成 elf 文件。概括来说就是哦:规定程序的内存布局。
语法格式: 
0x10000000;:定程序的链接起始地址,告诉链接器:所有代码从 0x10000000 这个内存地址开始链接。
划分各个段:.text / .rodata / .data / .bss
- .text:代码(汇编、C 指令)
- .rodata:const 只读常量
- .data:已经初始化的全局变量
- .bss:未初始化全局变量(不占用 bin 文件大小)
后面会单独一个章节来讲这些段的含义。
ALIGN(4):ARM32 要求 4 字节对齐,避免内存访问异常。ALIGN(4)让段起始地址 4 字节对齐。
上面只是一个框架,实际代码需要根据实际程序来编写,来看我们点亮LED的链接脚本:
SECTIONS
{
. = 0x87800000;
.text :
{
start.o
*(.text)
}
.rodata ALIGN(4) : {*(.rodata*)}
.data ALIGN(4) : {*(.data)}
. = ALIGN(4);
__bss_start = .;
.bss ALIGN(4) : {*(.bss) *(COMMON)}
__bss_end = .;
}
链接脚本写好了,接下来就是应用到Makefile中了: 将-Ttext 0X87800000替换为-Timx6ull.lds(链接脚本的名字,lds是固定后缀)就可以了。
arm–linux–gnueabihf–ld –Timx6ull.lds $(objs) –o $(target).elf
2.4 多目录工程Makefile
这样的Makefile算得上比较完整了,但是到后面你又会发现,这么多文件,什么库文件、源文件、外设相关的文件都在一个文件夹里,显得十分杂乱,不好使用,因此我们一般将其放在四个文件夹中:bsp、imx6ull、obj、project;
- bsp:存放外设相关的文件(板载文件);
- imx6ull:存放芯片相关的库文件;
- obj:存放相关的编译输出文件;
- project:存放主函数及启动文件。
由于这些文件的路径发生了改变,因此Makefile的代码也需要改变:
target = led
cross_compiler = arm-linux-gnueabihf-
cc = $(cross_compiler)gcc
ld = $(cross_compiler)ld
objcopy = $(cross_compiler)objcopy
objdump = $(cross_compiler)objdump
incdirs = bsp imx6ull
srcdirs = bsp project
include = $(patsubst %, -I%, $(incdirs))
cfiles = $(foreach dir, $(srcdirs), $(wildcard $(dir)/*.c))
sfiles = $(foreach dir, $(srcdirs), $(wildcard $(dir)/*.S))
cfilenodir = $(notdir $(cfiles))
sfilenodir = $(notdir $(sfiles))
cobjs = $(patsubst %, obj/%, $(cfilenodir:.c=.o))
sobjs = $(patsubst %, obj/%, $(sfilenodir:.S=.o))
objs = $(cobjs) $(sobjs)
VPATH = $(srcdirs)
$(target).bin : $(objs)
$(ld) -Timx6ull.lds -o$(target).elf $^
$(objcopy) -O binary -S -g $(target).elf $@
$(objdump) -D $(target).elf > $(target).dis
$(sobjs) : obj/%.o : %.S
@mkdir -p obj
$(cc) -Wall -nostdlib -c $(include) -o $@ $<
$(cobjs) : obj/%.o : %.c
@mkdir -p obj
$(cc) -Wall -nostdlib -c $(include) -o $@ $<
.PHONY : clean
clean:
rm -rf $(objs) $(target).elf $(target).bin $(target).dis
load:
./../imxdownload $(target).bin /dev/sdb
上面就是一个较为完整且通用的Makefile啦。
三、 内存段(Memory Segments)
3.1 bss段
- 用于存放未初始化或初始化为0的数据
- 在运行时会被自动清零
- 典型例子:
static int bss_var; // 静态变量(全局或局部)
int explicit_var = 0; // 显式初始化为0的全局变量
3.2 COMMON段
- 用于存放未初始化的全局变量
- 链接时才确定最终大小和位置
- 允许多个目标文件定义同名符号
- 典型例子:
int common_var; // 未初始化的非静态全局变量
3.3 data段
- 用于存放已初始化的全局变量和静态变量(非零值)
- 需要在程序文件中保存实际的初始值
- 典型例子:
int global_var = 100; // 初始化为非零值
int global_array[] = {1,2,3}; // 初始化为非零数组
3.4 rodata段(只读数据段)
- 存放程序的只读数据
- 运行时受保护,不可修改
- 包含的数据类型:
- 字符串常量
- const修饰的全局变量
- 全局只读数组
- switch跳转表
- 浮点数常量
- 典型例子:
const int MAX_VALUE = 100; // const全局变量
char* str = "Hello World"; // 字符串常量
const int lookup[] = {1,2,3}; // 只读数组
3.5 各段的特点比较
| .bss | 自动清零 | 可写 | 编译时确定大小 |
| COMMON | 自动清零 | 可写 | 链接时确定大小 |
| .data | 需要初始值 | 可写 | 编译时确定大小 |
| .rodata | 需要初始值 | 只读 | 编译时确定大小 |
3.6 在链接脚本中的定义
SECTIONS
{
.text : { ... } // 代码段
.rodata ALIGN(4) : {*(.rodata*)} // 只读数据段
.data ALIGN(4) : {*(.data)} // 已初始化数据段
.bss ALIGN(4) : {*(.bss) *(COMMON)} // 未初始化数据段
}
3.7 关于段名中的通配符说明
在链接脚本中,经常会看到类似*(.rodata*)这样的写法,这里包含两个*通配符,它们有不同的含义:
第一个*:
- 表示收集所有输入文件中的相关段
- 如果不加这个*,就只会收集特定文件的段
第二个*(如rodata*中的*):
- 用于匹配所有以.rodata开头的段名
- 实际编译时会产生多种rodata相关的段,例如:
- .rodata:基本的只读数据段
- .rodata.str1.1:长度为1字节对齐的字符串常量
- .rodata.str1.4:长度为4字节对齐的字符串常量
- .rodata.cst8:8字节常量(如double类型的常量)
- .rodata.cst4:4字节常量(如float类型的常量)
如果只写*(.rodata)而不是*(.rodata*):
- 只会收集严格命名为.rodata的段
- 其他相关的只读数据段(如.rodata.str1.1等)会被遗漏
- 这些被遗漏的数据可能导致程序无法正常工作
这种通配符的使用是链接脚本中的常见做法,类似的还有:
- *(.text*):收集所有代码相关的段
- *(.data*):收集所有数据相关的段
四、C语言实现点亮LED
在启动代码中我们让其在复位异常中跳转到C的主函数中,这样就引导程序进入了C的环境,就可以使用C语言来编写程序了。
实现流程: 
所谓的操作外设,其实就是对其所对应的寄存器进行操作,根据我们上篇文章中的汇编代码,我们可以这样来写C代码:
#define CCM_CCGR0 (*((volatile unsigned int *)0x020C4068)) //20C_4068h
#define CCM_CCGR1 (*((volatile unsigned int *)0x020C406C)) //20C_406Ch
#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))
#define SW_MUX_CTL_PAD_GPIO1_IO03 (*((volatile unsigned int *)0x020E0068)) //20E_0068h
#define SW_PAD_CTL_PAD_GPIO1_IO03 (*((volatile unsigned int *)0x020E02F4)) //20E_02F4h
#define GPIO1_GDIR (*((volatile unsigned int *)0x0209C004)) //209_C004
#define GPIO1_DR (*((volatile unsigned int *)0x0209C000)) //209_C000
裸机寄存器操作不加 volatile,编译器可能会优化掉读写代码,代码直接失效! 例如r1 = r1这种操作,在编译器眼里可能是没有作用的,就不执行这行代码了。
将LED这个外设所设计到的寄存器进行宏定义,通过操作这个宏来达到操作寄存器,从而操作外设的目的;但是不难发现,外设涉及到的寄存器这么多。每换一个外设就需要定义新的宏,并且还需要不停的翻手册查地址,这样编写程序的效率太低了,因此,大部分的芯片厂商都会提供一个封装好了的函数库,方便用户编写程序。下面就是厂家提供的相关的库。 
专门简化 IO 引脚复用、PAD 电气属性配置。
比如:#define __IO volatile,SDK 里寄存器声明用__IO uint32_t。
在手册中我们可以发现很多寄存器的地址是连续的,因此在厂家提供的库函数中,它对寄存器的宏定义的原理是这样的:
//使用结构体来使用寄存器
typedef struct
{
volatile unsigned int DR;
volatile unsigned int GDIR;
volatile unsigned int PSR;
volatile unsigned int ICR_1;
volatile unsigned int ICR_2;
volatile unsigned int IMR;
volatile unsigned int ISR;
volatile unsigned int EDGE_SEl;
}GPIO_TYPE_t;
#define GPIO1 ((GPIO_TYPE_t *)(0x0209C000))
这样就可以通过访问这个宏指向的不同寄存器进行操作了。
利用库函数完成LED代码示例: main.c
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "led.h"
void enable_clocks(void)
{
CCM->CCGR0 = 0xFFFFFFFF;
CCM->CCGR1 = 0xFFFFFFFF;
CCM->CCGR2 = 0xFFFFFFFF;
CCM->CCGR3 = 0xFFFFFFFF;
CCM->CCGR4 = 0xFFFFFFFF;
CCM->CCGR5 = 0xFFFFFFFF;
CCM->CCGR6 = 0xFFFFFFFF;
}
void delay(unsigned int n)
{
while(n—);
}
int main(void)
{
enable_clocks();
led_init();
led_on();
while(1)
{
led_nor();
delay(0xaffff);
}
return 0;
}
led.c:
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "led.h"
void led_init()
{
//引脚复用功能
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0);
//引脚电气属性
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0x10B0);
//引脚方向
GPIO1->GDIR |= (1 << 3);
}
void led_on()
{
GPIO1->DR &= ~(1 << 3);
}
void led_off()
{
GPIO1->DR |= (1 << 3);
}
void led_nor()
{
GPIO1->DR ^= (1 << 3);
}
五、C语言实现蜂鸣器
实现流程: 
由于IMX6ULL开发板的蜂鸣器是一个有源蜂鸣器,所以只需要对齐输出高低电平就可以令其发声。
通过原理图,我们可以找到蜂鸣器所接的引脚的默认功能为SNVS TAMPER1,进而我们可以在手册中找到

然后根据手册的解释,就可以完成对这个引脚的相关操作了。
代码示例: beep.c:
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "beep.h"
void beep_init(void)
{
//引脚复用功能
IOMUXC_SetPinMux(IOMUXC_SNVS_SNVS_TAMPER1_GPIO5_IO01, 0);
//引脚电气属性
IOMUXC_SetPinConfig(IOMUXC_SNVS_SNVS_TAMPER1_GPIO5_IO01, 0x10B0);
//引脚方向
GPIO5->GDIR |= (1 << 1);
}
void beep_on(void)
{
GPIO5->DR &= ~(1 << 1);
}
void beep_off(void)
{
GPIO5->DR |= (1 << 1);
}
void beep_nor(void)
{
GPIO5->DR ^= (1 << 1);
}
main.c:
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "led.h"
#include "beep.h"
void enable_clocks(void)
{
CCM->CCGR0 = 0xFFFFFFFF;
CCM->CCGR1 = 0xFFFFFFFF;
CCM->CCGR2 = 0xFFFFFFFF;
CCM->CCGR3 = 0xFFFFFFFF;
CCM->CCGR4 = 0xFFFFFFFF;
CCM->CCGR5 = 0xFFFFFFFF;
CCM->CCGR6 = 0xFFFFFFFF;
}
void delay(unsigned int n)
{
while(n—);
}
int main(void)
{
enable_clocks();
led_init();
beep_init();
//beep_on();
led_on();
while(1)
{
led_nor();
beep_nor();
delay(0xaffff);
}
return 0;
}



