欢迎光临
我们一直在努力

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

文章目录

    • 前言
    • 一、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
armlinuxgnueabihfgcc g c start.S o start.o
armlinuxgnueabihfld Ttext 0X87800000 start.o o start.elf
armlinuxgnueabihfobjcopy O binary S g start.elf start.bin
armlinuxgnueabihfobjdump 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)
armlinuxgnueabihfld Ttext 0X87800000 $(objs) o $(target).elf
armlinuxgnueabihfobjcopy O binary S g $(target).elf $(target).bin
armlinuxgnueabihfobjdump D $(target).elf > $(target).dis

%.o : %.S
armlinuxgnueabihfgcc g c $^ o $@

%.o : %.c
armlinuxgnueabihfgcc g c $^ o $@

clean :
rm f $(target).bin $(target).elf $(target).dis $(objs)

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

    语法格式: 在这里插入图片描述

  • 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 = .;
    }

  • . = 0x87800000;指定程序的链接起始地址:告诉链接器:所有代码从 0x87800000 这个内存地址开始链接。
  • 划分各个段:.text / .rodata / .data / .bss;
  • .text大括号中:控制目标文件的存放顺序:强制start.o 放在代码段最开头。
  • __bss_start = .; __bss_end = .;定义全局符号,给 C / 汇编代码使用,把当前位置计数器.赋值给符号,导出到 elf。
  • ALIGN(4):设置内存对齐。
  • 链接脚本写好了,接下来就是应用到Makefile中了: 将-Ttext 0X87800000替换为-Timx6ull.lds(链接脚本的名字,lds是固定后缀)就可以了。

    armlinuxgnueabihfld 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

  • 0x020C4068:寄存器物理地址
  • (volatile unsigned int *):把这个数字强制转换成无符号 32 位指针
  • 最外层 *:解引用指针,直接读写该地址上的值
  • volatile 非常关键 告诉编译器:这个变量是硬件寄存器,值会被硬件自动改变,不要做优化、不要缓存到寄存器,每次读写都必须访问真实内存。
  • 裸机寄存器操作不加 volatile,编译器可能会优化掉读写代码,代码直接失效! 例如r1 = r1这种操作,在编译器眼里可能是没有作用的,就不执行这行代码了。

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

  • MCIMX6Y2:定义全部外设寄存器地址、寄存器位掩码。就是:CCM_CCGR1、GPIO1_DR、GPIO1_GDIR这些宏,官方已经全部帮你写好了,不用自己手动算地址写宏。
  • fsl_iomuxc.h:IO 复用专用头文件定义所有引脚复用宏、PAD 配置的参数宏。比如 IOMUXC_GPIO1_IO03_GPIO1_IO03 ,直接一句话把引脚复用为 GPIO1_IO03。

    专门简化 IO 引脚复用、PAD 电气属性配置。

  • fsl_common.h 【通用工具函数 / 宏】 fsl = Freescale(飞思卡尔,被 NXP 收购) ✅ 作用: 提供 SDK 通用基础宏、位操作工具、中断、时钟通用函数,比如IOMUXC_SetPinMux()、IOMUXC_SetPinConfig(),引脚配置 API 就在这个头文件声明。
  • core_ca7.h 【CMSIS CA7 内核头文件】 CMSIS ARM 标准内核头文件,Cortex‑A7 专用 ✅ 作用: 操作 CPU 内核本身:CPSR、MMU、Cache、中断寄存器、MPU 等内核相关寄存器。 裸机启动、开启 Cache、操作内核寄存器时用到;普通 LED 驱动一般暂时不用。
  • cc.h 【编译器适配头文件】 compiler compatibility,编译器兼容层。 ✅ 作用: 对不同编译器(arm-linux-gnueabihf-gcc、armclang、Keil armcc)做适配,定义__IO、__I、__O宏,替代手写volatile unsigned int。

    比如:#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;
    }

    赞(0)
    未经允许不得转载:171主机测评 » 【ARM 裸机开发 (IMX6ULL-mini)】GNU工具、Makefile 工程构建、链接脚本详解|C 语言点灯 + 蜂鸣器驱动
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址