如何编写Makefile控制编译Arm(IMX6UL),及烧写程序(详细讲解Makefile语法7-)

        在上一篇文章当中,已经详细的讲解了LED驱动的编写,这一讲我们就来详细讲解一下代码的编译何下载。使用官方SDK库编写IMX6UL的LED灯驱动(超详细原理分析)-CSDN博客

        首先先简单介绍一下,Makefile 是一个文本文件,用于自动化构建软件项目。它的主要作用是简化编译和链接过程,确保项目的各个组件(如源代码文件、头文件、库文件等)
        在编译时按照正确的顺序和方式进行处理。当项目包含多个源代码文件时,Makefile 可以自动编译这些文件,并生成可执行文件或其他目标文件。例如现在我们编写的文件都是.c文件,但是后面会通过.c生成main.o,最后通过一系列操作生成ledc.bin,再用ledc.bin通过可执行软件生成load.imx烧写等多项过程,为了方便处理所以通过Makefile的自动化编译,依赖关系管理,以及自行构建编译规则,以及可重用性等特点来进行代码处理。

        

        接上个文章的LED驱动实验中,在下方新建一个Makefile,然后开始编写。

        CROSS_COMPILE  变量被定义为一个环境变量,它的默认值是arm-linux-gnueabihf-。首先简单环境变量来说就是将某些数据,文件或文件夹设置为系统默认值,这样你调用的时候就不用给出完整路径和地址或进行设置,直接用名字就可以了。因为例如  CROSS_COMPILE  的一般都是在外部定义的,而不是在内部。所以在我们没有提供它变量的值的时候,就默认执行值arm-linux-gnueabihf-
        ?=是一个赋值操作符,用于设置变量的值,如果变量未被定义才进行赋值,如果被定义?=不会修改现有的值。这一行意味着,如果   CROSS_COMPILE 变量尚未定义,则将其设置为arm-linux-gnueabihf-
        那么对于arm-linux-gnueabihf-是什么意思呢,这个属于前面介绍到的交叉编译器,现在我们开始详细介绍他的用法。 于arm-linux-gnueabihf是一个交叉编译工具链的前缀。它用于指示使用特定的交叉编译工具来编译 ARM 架构的代码,以在非 ARM 架构的系统上生成可执行文件。
        1.   arm-            指定了目标处理器的架构,即ARM
        2.   Linux-          指定了目标操作系统,即Linux
        3.   gnueabihf-   指定了编译器的版本和特性

        同样在下方定义了一个环境变量,变量名为NAME,如果NAME尚未被定义则赋值为ledc

        下方我们定义了变量CC,:=为变量CC赋值, $(CROSS_COMPILE)gcc,首先$符号是一个特殊字符,用于表示变量的引用。这里就说明在引入CROSS_COMPILE变量,所以形成的完整的工具链就是arm-linux-gnueabihf-gcc,在编译的过程中 $(CROSS_COMPILE)gcc会被替换为 arm-linux-gnueabihf-gcc,然后 Makefile 会使用这个完整的路径来调用交叉编译器的 )gcc版本。


        这里需要说明一下为什么上面的CROSS_COMPILE为什么是环境变量并且使用?=赋值,而这里的CC就是变量其次使用的:=赋值
        首先:=名字叫做立即赋值操作符,也就是右侧的表达式会被立即计算,并且结果会被赋值给左侧的变量。这意味着,变量一旦被赋值,后续对变量的引用将会使用这个固定的值。但是后买你仍然可以再次使用这个符号重新赋值
        对于?=名字叫条件赋值操作符,如果左侧的变量还没有被赋值,那么它会被赋予右侧的值。如果变量已经被赋值,那么 ?= 不会对变量进行任何操作,变量的值将保持不变
        那么对于变量之间的区别,CC在Makefile中内部,也就是它被定义为arm-linux-gnueabihf-gcc,那么CROSS_COMPILE环境变量是在Makefile之外设置的,当Makefile被运行时,它会是同环境变量的值来设置CC变量
        简单来说,CC变量是Makefile内部的,而CROSS_COMPILE环境变量是外部设置的,Makefile中的CC变量依赖于CROSS_COMPILE环境变量来工作

        那么讲解完CC      := $(CROSS_COMPILE)gcc,下面的指令又是上面意思呢,也是设置了LD的变量名,使用:=赋值,还是通过$引入变量CROSS_COMPILE,加上ld也就是作为交叉编译的链接器,作用是将目标文件(也就是.o文件)链接成可执行文件或库文件的程序

        定义变量名OBJCOPY,其余的变量引入以及赋值都还是同上,主要说明OBJCOPY的作用,用于将目标文件(也就是.o文件)转换为二进制文件(.bin文件)

        以及OBJDUMP,OBJDUMP变量的值被设置为交叉编译工具链中的 OBJDUMP工具。具体来说,它被设置为 ROSS_COMPILE变量指定的交叉编译工具链中的OBJDUMP工具,这是用于分析目标文件(.o文件)的工具,可以显示目标文件的格式和内容,包括指令、寄存器、栈帧等。也就是生成.dis反汇编文件。

        设置一个OBJS变量,OBJS变量被赋值为 start.o和 main.o这两个文件。这意味着 OBJS 变量现在包含了一个包含两个元素的列表,这两个元素分别是  start.o 和  main.o。OBJS变量中包含了.o文件文件列表的变量。这个列表中包含了需要编译和链接的目标文件。行代码的作用是告诉 Makefile,OBJS变量应该包含两个目标文件,即 start.o  和 main.o。在构建过程中,Makefile 可能会使用这个变量来指定需要编译的文件列表,或者在链接阶段指定需要链接的文件列表。

        这里是我们自己创建的规则,这些规则定义了如何构建一个名为$(NAME).bin文件,: $(OBJS): 这是依赖关系。冒号 : 表示目标文件 $(NAME).bin 依赖于右侧的列表 $(OBJS),冒号 : 用于定义规则(targets),它是 Makefile 语法中的一个重要部分。target: 是规则的目标,即 Makefile 试图构建的文件。OBJS我们已经定义为了start.o和main.o也就是要生成这两个文件,然后下放即是执行的命令以及命令序列,也就是用于构建我们的目标文件

        下方的代码$(LD): 这是链接器,它使用交叉编译工具链中的链接器。在这里讲解一下这个链接器和之后编写链接脚本的关系,在开发中使用一个链接脚本来指导链接器如何将目标文件组合成一个可执行文件或库文件。链接脚本定义了内存布局、内存区域、重定位和符号解析等细节。,链接脚本本身并不包含用于执行链接操作的程序代码。它需要一个链接器来执行实际的链接任务。链接器是一个程序,它读取链接脚本并执行链接操作。我们这里使用GCC也就是交叉编译工具链中的链接器。(arm-linux-gnueabihf-ld)也就是前面被定义的($LD),来链接目标文件,也就是下方imx6ul.lds这个文件,待会儿也会带来此文件的编写。总结来说,链接脚本定义了链接过程的细节,而交叉编译工具链中的链接器执行实际的链接操作,确保生成的可执行文件可以在目标硬件上正确运行。
        接着上方-T表示使用指定的链接脚本是imx6ul.lds,-o 是一个链接器(ld)的选项,用于指定链接过程的输出文件名。当链接器执行链接操作时,它会将多个目标文件(.o 文件)合并成一个可执行文件、共享库文件或静态库文件等。-o 选项告诉链接器将链接后的结果输出到指定的文件名。在这里生成的文件名是(NAME).elf,也就是生成文件名字为ledc.elf
        对于$^: 这是一个特殊变量,它代表依赖关系中的所有目标文件。在这个命令中,$^ 被扩展为 $(OBJS) 变量的值,即 start.o 和 main.o。那么这里为什么不直接使用$(OBJS)替换呢?是因为$^ 被自动替换为 $(OBJS) 变量的值,即 start.o 和 main.o。这意味着链接器将使用 start.o 和 main.o 作为输入文件,并将它们链接成一个名为 $(NAME).elf 的 ELF 文件。因为 $^ 是 Makefile 的一个特殊变量,它允许 Makefile 在解析时自动替换依赖文件列表,从而简化代码并提高可读性。如果直接使用 $(OBJS),则需要在命令中手动列出每个依赖文件,这会使代码更加复杂和容易出错。

         这是对象文件转换工具,用于将 ELF 格式的文件转换为二进制文件。-O binary: 选项,指定输出文件的格式为二进制。使用 -S 选项时,$(OBJCOPY) 工具会创建一个二进制文件,该文件不包含符号表信息。这对于一些不需要符号信息的应用来说是有用的,例如,在某些嵌入式系统中,二进制文件通常不包含符号信息,这样可以减少文件的大小,并且有时可以提高性能。使用 -S 选项取决于具体需求和目标平台。如果需要符号信息来进行调试,则不应该使用 -S 选项。这里 $@ 被替换为 $(NAME).bin,所以最终命令是将 $(NAME).elf 转换为 $(NAME).bin。
        要区别的地方是,$@ 变量代表当前规则的目标文件。无论规则中有多少个依赖文件,$@ 总是引用最后一个目标文件。$^ 变量代表当前规则中的所有依赖文件。无论规则中有多少个依赖文件,$^ 总是引用所有依赖文件的列表。第一步是应用目标文件生成ledc.elf下面一步是通过应用目标文件,也就是通过前面的命令生成目标文件ledc.bin

        最后一段$(OBJDUMP): 这是 objdump 命令,用于反汇编 ELF 文件。-D: 这个选项告诉 objdump 以反汇编的形式输出文件。-m arm: 这个选项告诉 objdump 输出 ARM 架构的反汇编代码。$(NAME).elf: 这是输入文件,即链接过程生成的 ELF 文件。> $(NAME).dis: 这是一个重定向操作,它告诉 objdump 将输出重定向到 $(NAME).dis 文件中。这意味着反汇编代码将不会在终端中显示,而是被写入到 $(NAME).dis 文件中。-D: 这个选项告诉 objdump 以反汇编的形式输出文件。-m arm: 这个选项告诉 objdump 输出 ARM 架构的反汇编代码。$(NAME).elf: 这是输入文件,即链接过程生成的 ELF 文件。> $(NAME).dis: 这是一个重定向操作,它告诉 objdump 将输出重定向到 $(NAME).dis 文件中。这意味着反汇编代码将不会在终端中显示,而是被写入到$(NAME).dis 文件中

        这三行代码定义了三个规则,用于编译不同的源代码文件(.s、.S 和 .c)到目标文件(.o)。每个规则都指定了如何编译特定的源文件类型到目标文件

        %.o 这是目标文件的模式,表示任何以 .o 结尾的目标文件。模式匹配意味着这行代码将匹配任何以 .o 结尾的目标文件。%.S: 这是依赖文件的文件模式,表示任何以 .S 结尾的源文件。模式匹配意味着这行代码将匹配任何以 .S 结尾的源文件。冒号 : 的作用是定义依赖关系,它告诉Makefile 如何构建目标文件,即通过依赖的源文件。这是 Makefile 自动化构建过程的基础。这行代码的含义是,对于任何匹配 %.o 模式的目标文件和匹配 %.S 模式的源文件,Makefile 都将执行这行代码后面的命令
        这个规则的作用是告诉 Makefile 如何编译 .S 源文件(汇编语言源文件)到 .o 目标文件(编译后的汇编代码)。当 Makefile 检测到 .o 文件不存在或比 .S 件更新时,它会执行 $(CC) -Wall -nostdlib -c -O2 -o$@ $< 命令来编译源文件并生成目标文件。

         第一段定义的第一个规则,这个规则说明,对于任何以.S结尾的源文件,都应该使用$(CC)来编译它,并生成一个同名的.o目标文件。-Wall:这是一个编译器选项,告诉编译器显示所有警告信息。这有助于开发者发现潜在的问题,比如未使用的变量、不明确的类型转换等。-nostdlib:这个选项告诉编译器在链接阶段不要使用标准库。标准库包含了C语言标准中定义的所有函数和对象,比如printf、malloc等。如果你的程序不需要这些标准库函数,或者你在编写一个操作系统内核等底层软件,那么使用-nostdlib可能是必要的。但是,会限制你的程序能够使用的功能。-c:这个选项告诉编译器只进行编译和汇编操作,不要进行链接。这意味着编译器会生成一个.o文件(对象文件),但不会尝试将多个.o文件或库文件链接成一个可执行文件。-O2:这是一个优化选项,告诉编译器以中等优化级别编译代码。优化级别越高,编译器尝试进行的优化就越多,生成的代码可能就越快,但编译时间也可能越长,并且生成的代码可能更难以调试。-O2是一个常用的优化级别,它提供了合理的性能和编译时间之间的平衡

        -o $@:这里的-o选项用于指定输出文件的名称。$@是一个自动变量,在Makefile规则中代表规则中的目标文件名。在这个例子中,如果目标是main.o,那么-o $@就会被替换为-o main.o。$<:这是另一个自动变量,在Makefile规则中代表规则中的第一个依赖文件名。如果目标是main.o并且依赖于main.c或main.S,那么$<就会被替换为main.c或main.S

        第二个规则与第一个规则非常的相似,但它针对的是以.c结尾的C语言源文件。编译器同样使用$(CC),但这次处理的是C代码。所以后面的意思也是相同的。

        总的Makefile如下,($(NAME).bin: $(OBJS))叫链接和转换规则。这个规则指定了如何从对象文件($(OBJS))生成最终的目标文件($(NAME).bin)。它首先使用链接器($(LD))和链接脚本(imx6ul.lds)将对象文件链接成ELF格式的可执行文件($(NAME).elf)。然后,它使用objcopy将ELF文件转换为纯二进制文件($(NAME).bin),并使用objdump生成ELF文件的反汇编输出($(NAME).dis)。

        对于下方的(%.o: %.S 和 %.o: %.c)叫做编译规则,这两个规则定义了如何从汇编源文件(.S)和C源文件(.c)生成对象文件(.o)。它们使用相同的编译器($(CC)),但源文件类型不同。每个规则都指定了编译器选项(如-Wall、-nostdlib、-c、-O2)和输出文件($@)及输入文件($<)。当Make尝试构建某个对象文件(如start.o或main.o)时,它会查找与源文件扩展名相匹配的规则,并应用该规则来编译源文件。

        然后再编写一下clean规则,每次在编译的过程当中都需要生成很多文件,有些时候修改了代码过后还需要重新编译,也就意味着需要一个一个删掉代码,实在是太麻烦了。所以我们同样是编写一个规则,只需要输入这个给规则就可以将生成的代码一次性全部都删掉。,也就是下方的clean:这个很简单,也就是引入了我们的变量生成的文件,然后最后删除掉,不多赘述

        至此Makefile的文件我们现在就已经全部讲解完了,但是现在任然没有完全写完,因为还差一个链接文件。首先我们需要知道什么是链接文件,为什么需要自己编写链接文件。

        首先链接文件(通常指的是链接脚本或链接器脚本)是一个用于指导链接器(如ld)如何将程序中的不同部分(如代码、数据、库等)组合成最终的可执行文件或库文件的脚本文件。链接脚本定义了程序的内存布局,包括程序的入口点、各个段(如代码段、数据段、堆栈段等)的位置和大小,以及它们之间的关系。

        为什么要自己编写
1.        在某些情况下,特别是嵌入式系统或需要精确控制程序内存布局的应用中,默认的链接器行为可能不满足需求。通过编写自定义的链接脚本,开发者可以精确地指定代码和数据应该放在内存的哪个位置,以及它们应该如何被组织。

2.        通过合理安排代码和数据的内存布局,可以提高程序的运行效率。例如,将频繁访问的数据放在靠近处理器的位置减少内存访问延迟。

3.        某些硬件平台对程序的内存布局有特定的要求,如特定的启动地址、中断向量表的位置等。通过编写链接脚本,可以确保生成的程序满足这些要求。

4.        在某些系统中,可能存在特殊的内存区域(如DMA缓冲区、特定的硬件寄存器映射区域等)。通过链接脚本,可以将特定的代码或数据段放置在这些区域中。

        等原因所以接下来我们需要在自己再编写一下链接脚脚本。在我们的当前文件夹下再新建一个叫做imx6ul.lds的文件,在这里面编写,链接的文件以.lds为后缀,然后来解释一下我们的代码

        

SECTIONS{} 块内部包含了多个段定义,每个段定义都指定了一个段名(如 .text.data.bss 等),以及该段应包含的内容。这些内容可以来自特定的对象文件、所有对象文件的通配符匹配、或者是由链接器自动生成的段(如 .bss,它通常不包含实际的数据,但在内存中为未初始化的变量预留空间)。

        在 SECTIONS{} 块中,还可以设置段的属性,如对齐要求(使用 ALIGN() 指令)、段的起始地址(通过设置当前位置 . = address;),以及定义用于在程序中引用的全局符号(如 BSS 段的起始和结束地址)。例如下方,通俗的方式来形容SECTIONS{}在链接脚本中的作用,可以想象成是一个“装修指南”或者“布局蓝图”,但它不是用来装修房子,而是用来“装修”你的程序在内存中的布局。当你编写了一个程序,并且将它编译成多个对象文件(.o 文件)之后,这些对象文件就像是一堆装修材料,包括不同种类的木板(代码段)、瓷砖(数据段)、还有需要预留空间的区域(未初始化数据段,即BSS段)。

SECTIONS  
{  
    . = 0x10000; // 设置当前位置为 0x10000,这是链接器开始放置段的地址  
  
    .text : // 定义代码段  
    {  
        *(.text) // 将所有输入文件的 .text 段放置在此  
    } >FLASH // 指定 .text 段应该被放置在名为 FLASH 的内存区域中(需要在 MEMORY 块中定义)  
  
    .data : // 定义可写数据段  
    {  
        *(.data) // 将所有输入文件的 .data 段放置在此  
    } >RAM // 指定 .data 段应该被放置在名为 RAM 的内存区域中  
  
    .bss : // 定义未初始化数据段(BSS段)  
    {  
        *(.bss) // 将所有输入文件的 .bss 段放置在此  
        *(COMMON) // 将所有未分配的COMMON块也放置在此  
    } >RAM // BSS段也放置在RAM中,通常紧跟在.data段之后  
}  
  
// ...(可能还有 MEMORY 块和其他链接脚本指令)

        基地址设置:. = 0X87800000; 这行代码设置了链接器开始放置段的基地址。在这个例子中,所有的段都将从这个地址开始放置,但实际上.text段会首先被放置,随后的段将紧跟在.text段之后(除非有特定的偏移或内存区域限制)。在链接脚本中,.text段是用来存放程序可执行代码的部分。当编译器将源代码编译成对象文件(.o 或 .obj 文件)时,源代码中的函数和全局变量(如果它们被声明为const或者只读的)的定义会被放在对象文件的.text段中实际上全局变量的只读部分通常放在.rodata段

        text : 这一行标记了一个新段的开始,即代码段(.text)。链接器会按照这里的指示来组织代码段的内容。
        start.o 和 main.o 是特定对象文件的名称。这里明确指定了链接器应该将start.o和main.o中的.text段内容放置在.text段的开头(或者更准确地说,是紧随当前位置的下一个可用位置,但在这个上下文中,由于它们是第一个被列出的,所以它们会位于.text段的开始)。这通常用于确保程序的入口点(如启动代码或main函数)位于内存中的特定位置,这对于程序的启动和初始化很重要。
        *(.text) 是一个通配符表达式,它告诉链接器将所有其他输入对象文件中的.text段内容也放置在这个.text段中,紧随start.o和main.o的.text段之后。这意味着,无论你的程序有多少个对象文件,只要它们包含了.text段,链接器都会将这些段的内容收集起来,并按照链接脚本中指定的顺序(在这个例子中是先start.o,然后main.o,最后是所有其他文件)放置在最终的输出文件的.text段中。

        ALIGN(4) 用于指定段的对齐方式。在这个例子中,.rodata 和 .data 以及 .bss 段都被对齐到4字节边界。这有助于提高内存访问的效率,尤其是在某些对内存访问有特定要求的硬件上。__bss_start 和 __bss_end 是链接脚本中定义的全局符号,它们分别表示BSS段的起始和结束地址。这些符号可以在C代码中通过extern关键字声明并使用,以便在程序启动时清零BSS段或进行其他操作。

        这样我们的链接文件也编写好了,接下来我们就开始验证我们的所有代码是否正确了,首先这里还需要强调编写完的文件一定要记得保存,不然在编译的时候很可能会报错。

        左侧是我们写好的所有文件,然后点击终端中的新建终端,

        可以看到我们的代码就开始通过我们Makefile定义好的规则开始编译了,在左侧也生产了我们的新文件,也有我们需要的.bin文件

        这个时候我们需要通过imxdownload这个可执行文件将.bin文件烧写进去,还是使用我们的FTP文件传输,传输到我们的Linux这个代码文件夹下,这里就演示了,然后我们的这个文件刚开始传过来是没有权限的,所以我们需要给这个软件权限,输入这个命令给与软件可执行权限

        然后将我们的SD卡插在读卡器上,将读卡器插到电脑USB口上,弹出来的窗口选择连接到虚拟机上,然后我们的虚拟机上可以看到U盘样式的图标,就说明连接成功了,我们再通过命令方式来验证一下。输入以上命令行这些是系统自带的,如果只显示这些说明没有连接成功

        现在我们将U盘插在电脑上,重新输入一遍命令行,比上面多了两个接口,这个就是读卡器的接口

        然后我们开始烧写程序,输入以上指令,原理上面讲过也不赘述了,也是看到我们烧写成功了,检查一下下面的下载速度是不是几十kb,如果是则没什么问题,但是如果下载速度达到即使MB或者几百MB那就说明下载有问题,需要重新下载。然后这里我们下载成功了,我们把读卡器的SD卡直接拔下来,插入到我们的开发板上,然后按一下复位按钮就可以看到我们的代码是不是被成功下载了。

        整个SDK开发LED驱动的教程我们就讲到这里了,知识浅薄可能有些地方讲的有纰漏,欢迎各位指正博主,或者一起讨论,需要工程源码的可以直接联系博主,这一节知识就讲到这里。

  • 26
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值