【ARM 裸机开发 (IMX6ULL-mini)】GNU工具、Makefile 工程构建、链接脚本详解|C 语言点灯 + 蜂鸣器驱动

文章目录

    • 前言
    • 一、GNU工具链常用工具说明
      • [1.1 gcc (GNU Compiler Collection)](#1.1 gcc (GNU Compiler Collection))
      • [1.2 ld (GNU Linker)](#1.2 ld (GNU Linker))
      • [1.3 objcopy (Binary File Converter)](#1.3 objcopy (Binary File Converter))
      • [1.4 objdump (Object File Disassembler)](#1.4 objdump (Object File Disassembler))
    • 二、Makefile
      • [2.1 Makefile基本语法](#2.1 Makefile基本语法)
      • [2.2 简易版Makefile](#2.2 简易版Makefile)
      • [2.3 链接脚本](#2.3 链接脚本)
      • [2.4 多目录工程Makefile](#2.4 多目录工程Makefile)
    • [三、 内存段(Memory Segments)](#三、 内存段(Memory Segments))
      • [3.1 bss段](#3.1 bss段)
      • [3.2 COMMON段](#3.2 COMMON段)
      • [3.3 data段](#3.3 data段)
      • [3.4 rodata段(只读数据段)](#3.4 rodata段(只读数据段))
      • [3.5 各段的特点比较](#3.5 各段的特点比较)
      • [3.6 在链接脚本中的定义](#3.6 在链接脚本中的定义)
      • [3.7 关于段名中的通配符说明](#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:

c 复制代码
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进行完善:

c 复制代码
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)
  1. target :定义变量 target,最终生成中间固件及最后下载程序名字前缀 led
  2. objs :所有需要参与链接的目标文件:汇编start.S、C 文件main.cled.c编译出的.o
  3. (target).bin : (objs) :最终目标 led.bin 依赖 start.o main.o led.o,只要其中任意.o 更新就会重新执行下面命令;
  4. %.o : %.c%.o : %.S ):% 是 Makefile 的通配符 ,用于模式规则(pattern rule)%.o:匹配 xxx.o% 捕获 xxx,含义就是:任意 .o 文件由同名的 .c 文件生成;
  5. -c:只编译不链接;$^代表所有依赖文件;$@代表目标文件

不难发现,在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 文件。概括来说就是哦:规定程序的内存布局

语法格式

  1. 0x10000000;:定程序的链接起始地址 ,告诉链接器:所有代码从 0x10000000 这个内存地址开始链接。

  2. 划分各个段:.text / .rodata / .data / .bss

    • .text:代码(汇编、C 指令)
    • .rodata:const 只读常量
    • .data:已经初始化的全局变量
    • .bss:未初始化全局变量(不占用 bin 文件大小)

    后面会单独一个章节来讲这些段的含义。

  3. ALIGN(4):ARM32 要求 4 字节对齐,避免内存访问异常。ALIGN(4)让段起始地址 4 字节对齐。

上面只是一个框架,实际代码需要根据实际程序来编写,来看我们点亮LED的链接脚本:

c 复制代码
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 = .;
}
  1. . = 0x87800000;指定程序的链接起始地址 :告诉链接器:所有代码从 0x87800000 这个内存地址开始链接。
  2. 划分各个段:.text / .rodata / .data / .bss
  3. .text大括号中:控制目标文件的存放顺序:强制start.o 放在代码段最开头
  4. __bss_start = .; __bss_end = .;定义全局符号,给 C / 汇编代码使用,把当前位置计数器.赋值给符号,导出到 elf。
  5. ALIGN(4):设置内存对齐。

链接脚本写好了,接下来就是应用到Makefile中了:

-Ttext 0X87800000替换为-Timx6ull.lds(链接脚本的名字,lds是固定后缀)就可以了。

c 复制代码
arm-linux-gnueabihf-ld -Timx6ull.lds $(objs) -o $(target).elf

2.4 多目录工程Makefile

这样的Makefile算得上比较完整了,但是到后面你又会发现,这么多文件,什么库文件、源文件、外设相关的文件都在一个文件夹里,显得十分杂乱,不好使用,因此我们一般将其放在四个文件夹中:bspimx6ullobjproject;

  • bsp:存放外设相关的文件(板载文件);
  • imx6ull:存放芯片相关的库文件;
  • obj:存放相关的编译输出文件;
  • project:存放主函数及启动文件。

由于这些文件的路径发生了改变,因此Makefile的代码也需要改变:

c++ 复制代码
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的数据
  • 在运行时会被自动清零
  • 典型例子:
c 复制代码
static int bss_var;           // 静态变量(全局或局部)
int explicit_var = 0;         // 显式初始化为0的全局变量

3.2 COMMON段

  • 用于存放未初始化的全局变量
  • 链接时才确定最终大小和位置
  • 允许多个目标文件定义同名符号
  • 典型例子:
c 复制代码
int common_var;               // 未初始化的非静态全局变量

3.3 data段

  • 用于存放已初始化的全局变量和静态变量(非零值)
  • 需要在程序文件中保存实际的初始值
  • 典型例子:
c 复制代码
int global_var = 100;         // 初始化为非零值
int global_array[] = {1,2,3}; // 初始化为非零数组

3.4 rodata段(只读数据段)

  • 存放程序的只读数据
  • 运行时受保护,不可修改
  • 包含的数据类型:
    1. 字符串常量
    2. const修饰的全局变量
    3. 全局只读数组
    4. switch跳转表
    5. 浮点数常量
  • 典型例子:
c 复制代码
const int MAX_VALUE = 100;    // const全局变量
char* str = "Hello World";    // 字符串常量
const int lookup[] = {1,2,3}; // 只读数组

3.5 各段的特点比较

段名 初始化 运行时可写 链接特性
.bss 自动清零 可写 编译时确定大小
COMMON 自动清零 可写 链接时确定大小
.data 需要初始值 可写 编译时确定大小
.rodata 需要初始值 只读 编译时确定大小

3.6 在链接脚本中的定义

c 复制代码
SECTIONS
{
    .text :     { ... }              // 代码段
    .rodata ALIGN(4) : {*(.rodata*)} // 只读数据段
    .data   ALIGN(4) : {*(.data)}    // 已初始化数据段
    .bss    ALIGN(4) : {*(.bss) *(COMMON)} // 未初始化数据段
} 

3.7 关于段名中的通配符说明

在链接脚本中,经常会看到类似*(.rodata*)这样的写法,这里包含两个*通配符,它们有不同的含义:

  1. 第一个*

    • 表示收集所有输入文件中的相关段
    • 如果不加这个*,就只会收集特定文件的段
  2. 第二个*(如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代码:

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
  1. 0x020C4068:寄存器物理地址
  2. (volatile unsigned int *):把这个数字强制转换成无符号 32 位指针
  3. 最外层 *:解引用指针,直接读写该地址上的值
  4. volatile 非常关键
    告诉编译器:这个变量是硬件寄存器,值会被硬件自动改变,不要做优化、不要缓存到寄存器,每次读写都必须访问真实内存

裸机寄存器操作不加 volatile,编译器可能会优化掉读写代码,代码直接失效!

例如r1 = r1这种操作,在编译器眼里可能是没有作用的,就不执行这行代码了。

将LED这个外设所设计到的寄存器进行宏定义,通过操作这个宏来达到操作寄存器,从而操作外设的目的;但是不难发现,外设涉及到的寄存器这么多。每换一个外设就需要定义新的宏,并且还需要不停的翻手册查地址,这样编写程序的效率太低了,因此,大部分的芯片厂商都会提供一个封装好了的函数库,方便用户编写程序。下面就是厂家提供的相关的库。

  1. MCIMX6Y2 :定义全部外设寄存器地址、寄存器位掩码 。就是:CCM_CCGR1GPIO1_DRGPIO1_GDIR这些宏,官方已经全部帮你写好了,不用自己手动算地址写宏。
  2. fsl_iomuxc.h :IO 复用专用头文件定义所有引脚复用宏、PAD 配置的参数宏。比如 IOMUXC_GPIO1_IO03_GPIO1_IO03 ,直接一句话把引脚复用为 GPIO1_IO03。 专门简化 IO 引脚复用、PAD 电气属性配置。
  3. fsl_common.h 【通用工具函数 / 宏】
    fsl = Freescale(飞思卡尔,被 NXP 收购)
    ✅ 作用:
    提供 SDK 通用基础宏、位操作工具、中断、时钟通用函数,比如IOMUXC_SetPinMux()IOMUXC_SetPinConfig()引脚配置 API 就在这个头文件声明
  4. core_ca7.h 【CMSIS CA7 内核头文件】
    CMSIS ARM 标准内核头文件,Cortex‑A7 专用
    ✅ 作用:
    操作 CPU 内核本身:CPSR、MMU、Cache、中断寄存器、MPU 等内核相关寄存器。
    裸机启动、开启 Cache、操作内核寄存器时用到;普通 LED 驱动一般暂时不用。
  5. cc.h 【编译器适配头文件】
    compiler compatibility,编译器兼容层。
    ✅ 作用:
    对不同编译器(arm-linux-gnueabihf-gcc、armclang、Keil armcc)做适配,定义__IO__I__O宏,替代手写volatile unsigned int。 比如:#define __IO volatile,SDK 里寄存器声明用__IO uint32_t

在手册中我们可以发现很多寄存器的地址是连续的,因此在厂家提供的库函数中,它对寄存器的宏定义的原理是这样的:

c 复制代码
//使用结构体来使用寄存器
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

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:

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:

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:

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;
}
相关推荐
Shadow(⊙o⊙)1 小时前
Linux进阶知识1.0
linux·运维·服务器
>Andre<2 小时前
UFS5.0标准中文全译·卷一:范围、术语与架构
android·linux·嵌入式硬件
用户0510122572962 小时前
Linux下有关QT显示环境变量设置
linux·嵌入式
闲云自留地2 小时前
告别命令恐惧症,openEuler 基础操作 + 文本 + 网络管理实战
linux·运维·云计算
剑指offer.2 小时前
ARM裸机开发-点亮LED灯
linux·服务器·嵌入式硬件
GeW3 小时前
工作忙.基础弱,也能快速拿RHCE?这套方法亲测有效
linux
Julien20043 小时前
自动挂载网络附加存储
linux·服务器·网络
codigger4 小时前
服务器又卡了?一篇讲透 Linux 性能排查(基础四件套 + perf/strace/火焰图)
linux·后端·性能优化
顶点多余4 小时前
9.18 面试题总结 --- 绿盟
linux·面试