详见正点原子第十章

文件一

start.s

1 .global _start /* 全局标号 */
2 
3
/*
4 * 描述: _start 函数,程序从此函数开始执行,此函数主要功能是设置 C
5 * 运行环境。
6 */
7 _start:
8 
9
/* 进入 SVC 模式 */
10 mrs r0, cpsr
11 bic r0, r0, #0x1f /* 将 r0 的低 5 位清零,也就是 cpsr 的 M0~M4 */即为程序状态寄存器,可以参考正点原子6.3
12 orr r0, r0, #0x13 /* r0 或上 0x13,表示使用 SVC 模式 *
13 msr cpsr, r0 /* 将 r0 的数据写入到 cpsr_c 中 */
14
15 ldr sp, =0X80200000 /* 设置栈指针 */
16 b main /* 跳转到 main 函数 */

Cortex-A 有九个运行模型,这里我们设置处理器运行在 SVC 模式下。处理器模式的设置是通过修改 CPSR(程序状态)寄存器来完成的,

  • 配置cpsr来进入svc模式
  • 设置栈大小

文件二

main.h
定义CCM、gpio、IOMUX相关寄存器地址
要理解“点一个灯需要配置这么多寄存器”,核心在于嵌入式系统中“硬件直接操作”的本质——任何外设(包括LED灯)的工作,都依赖于芯片内部多个模块的协同配合,每个模块都需要通过寄存器进行精确配置。下面从具体模块入手,拆解这些配置的必要性:

一、为什么需要配置这些模块?

嵌入式芯片(如IMX6ULL、STM32等)的引脚和外设,并非“通电就能用”,而是需要通过多个核心模块的配合才能正常工作。以“点灯”为例(通常通过GPIO控制LED),至少需要4个核心模块的配置,缺一不可:

模块 作用 为什么点灯需要它?
CCM(时钟控制模块) 管理芯片内部时钟分配 任何硬件模块(包括GPIO)工作都需要时钟驱动,否则模块处于“休眠状态”,无法响应操作。
IOMUX(引脚复用模块) 配置引脚的功能模式 一个引脚可能有多种功能(如GPIO、UART、SPI等),需指定其为“GPIO功能”才能控制LED。
PAD(引脚电气特性模块) 配置引脚的电气参数 包括上拉/下拉电阻、驱动强度、速度等,确保引脚能稳定输出高低电平(否则可能无法驱动LED)。
GPIO(通用输入输出模块) 控制引脚的输入/输出方向和电平 最终通过GPIO的寄存器设置“输出模式”,并写入高低电平,实现LED的亮/灭。

二、每个模块的具体配置解析

结合你给出的代码,我们逐个看这些寄存器的作用:

1. CCM模块:给GPIO“供电”(时钟使能)

#define CCM_CCGR0 *((volatile unsigned int *)0X020C4068)
// ... 其他CCM_CCGRx寄存器
  • CCM(Clock Controller Module) 是芯片的“时钟总管”,负责给各个外设分配时钟信号。
  • CCM_CCGRx(Clock Gating Register) 是“时钟门控寄存器”,通过配置其位值,可控制某个模块的时钟是否使能(如CCM_CCGR1可能包含GPIO1的时钟控制位)。
  • 必要性:如果不给GPIO模块使能时钟,即使配置了其他寄存器,GPIO也不会工作(相当于“没电的电机,再按开关也转不起来”)。

2. IOMUX模块:让引脚“成为GPIO”(功能复用)

#define SW_MUX_GPIO1_IO03 *((volatile unsigned int *)0X020E0068)
  • 芯片的引脚是“多功能的”。例如GPIO1_IO03这个引脚,可能同时支持“GPIO”“UART1_RX”“SPI1_SCK”等多种功能(由芯片硬件设计决定)。
  • SW_MUX_GPIO1_IO03 是“复用选择寄存器”,通过写入特定值(如0x5),可将该引脚指定为“GPIO功能”。
  • 必要性:如果不配置复用,引脚可能默认是其他功能(如UART),此时操作GPIO寄存器也无法控制引脚电平。

3. PAD模块:让引脚“稳定工作”(电气特性配置)

#define SW_PAD_GPIO1_IO03 *((volatile unsigned int *)0X020E02F4)
  • PAD(Pad Control) 负责配置引脚的电气参数,例如:
    • 上拉/下拉:防止引脚悬空(如设置为“下拉”,确保未输出时引脚为低电平);
    • 驱动强度:决定引脚能输出的电流大小(LED需要一定电流才能点亮,电流不足会导致灯不亮);
    • 速度:引脚电平切换的响应速度(点灯对速度要求低,但高速外设需匹配速度)。
  • 代码中的SW_PAD_GPIO1_IO03 = 0X10B0,就是在配置这些电气参数,确保引脚能稳定输出高低电平驱动LED。

4. GPIO模块:最终“控制灯的亮灭”

#define GPIO1_DR *((volatile unsigned int *)0X0209C000)  // 数据寄存器(输出高低电平)
#define GPIO1_GDIR *((volatile unsigned int *)0X0209C004) // 方向寄存器(输入/输出)
  • GPIO模块是直接控制引脚的“执行单元”:
    • GPIO1_GDIR:配置引脚方向(如设置某一位为1,表示“输出模式”);
    • GPIO1_DR:写入0或1,控制引脚输出低电平(灯亮)或高电平(灯灭)。
  • 必要性:前面的配置都是“准备工作”,最终通过GPIO寄存器实现对LED的直接控制。

三、为什么“高层开发”感觉不到这些配置?

如果你用过Arduino或Python的GPIO库(如RPi.GPIO),会发现“点灯”只需要一两行代码(如pinMode(13, OUTPUT); digitalWrite(13, HIGH);)。这是因为:

  • 高层库(如Arduino的Wire.h、树莓派的RPi.GPIO)已经将上述寄存器配置封装成了函数,隐藏了底层细节;
  • 但本质上,这些库的内部实现,依然是通过配置CCM、IOMUX、PAD、GPIO寄存器来完成的。

总结:底层配置的核心逻辑

“点灯”看似简单,但其底层依赖于芯片内部“时钟使能→引脚功能定义→电气特性配置→GPIO控制”的完整流程。这些寄存器配置并非“多余”,而是嵌入式系统中“直接操作硬件”的必然要求——越靠近硬件底层,越需要关注这些细节

理解这些配置后,你会明白:无论是点灯、控制传感器还是通信,嵌入式底层开发的核心逻辑都是一致的——通过寄存器“唤醒”硬件模块、配置其功能和特性,最终实现具体功能。这也是嵌入式开发“知其然,更要知其所以然”的关键。

文件三 详见10.4

main.c
最后配置时钟(ccm)、pad、mux、gpio值来点亮

写makefile来编译

写makefile来编译start.s和main.c来生成.o文件,最后生成.bin可执行文件可下载到sd卡中

objs := start.o main.o
2 3
ledc.bin:$(objs)
4 arm-linux-gnueabihf-ld -Ttext 0X87800000 -o ledc.elf $^
5 arm-linux-gnueabihf-objcopy -O binary -S ledc.elf $@
6 arm-linux-gnueabihf-objdump -D -m arm ledc.elf > ledc.dis
7 8
%.o:%.s
9 arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<
10
11 %.o:%.S
12 arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<
13
14 %.o:%.c
15 arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<
16
17 clean:
18 rm -rf *.o ledc.bin ledc.elf ledc.dis

这些命令使用的是同一套工具链arm-linux-gnueabihf-*),但分工不同,共同完成从源代码到可执行文件的编译、链接、格式转换和反汇编过程。下面详细解析每个工具的作用:

一、工具链统一性:都是arm-linux-gnueabihf-系列

所有命令前缀均为arm-linux-gnueabihf-,说明它们属于同一套交叉编译工具链,用于为ARM架构的嵌入式Linux系统开发程序:

  • arm:目标处理器架构为ARM;
  • linux:目标操作系统为Linux;
  • gnueabihf:使用GNU标准库(glibc),支持硬浮点(hf)运算。

这套工具链包含编译器、链接器、格式转换工具等,各司其职但协同工作。

二、各工具的具体作用

1. arm-linux-gnueabihf-gcc:编译源代码为目标文件(.o

对应命令:

%.o:%.s    # 汇编文件(.s)编译为目标文件(.o)
  arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<

%.o:%.S    # 带预处理的汇编文件(.S)编译为目标文件(.o)
  arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<

%.o:%.c    # C语言文件(.c)编译为目标文件(.o)
  arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<
  • 核心作用:将源代码(汇编.s/.S、C语言.c)编译为ARM架构的二进制目标文件(.o),包含机器码和符号表,但未解决跨文件引用。
  • 关键参数
    • -c:只编译不链接,生成目标文件;
    • -nostdlib:不链接标准库(嵌入式裸机/内核开发常用,避免依赖Linux用户态库);
    • -Wall:开启所有警告,提升代码健壮性。
  • 不同源文件的处理
    • .s:纯汇编代码,直接汇编为目标文件;
    • .S:带C预处理指令(如#include#define)的汇编文件,会先预处理再汇编;
    • .c:C语言代码,经预处理、编译、汇编生成目标文件。

2. arm-linux-gnueabihf-ld:链接目标文件为可执行文件(.elf

对应命令:

arm-linux-gnueabihf-ld -Ttext 0X87800000 -o ledc.elf $^
  • 核心作用:将多个目标文件(.o)链接为一个完整的可执行文件(.elf),解决跨文件的函数/变量引用,并确定代码加载到内存中的地址。
  • 关键参数
    • -Ttext 0X87800000:指定代码段(text section)的加载地址为0X87800000(嵌入式系统中需与硬件内存布局匹配,如DDR内存的起始地址);
    • -o ledc.elf:输出的可执行文件名为ledc.elf
    • $^:Makefile自动变量,表示所有依赖的目标文件(如start.o main.o)。

3. arm-linux-gnueabihf-objcopy:转换可执行文件格式(.elf.bin

对应命令:

arm-linux-gnueabihf-objcopy -O binary -S ledc.elf $@
  • 核心作用:将ELF格式的可执行文件(.elf)转换为纯二进制文件(.bin),去除调试信息和符号表,只保留可执行的机器码。
  • 关键参数
    • -O binary:指定输出格式为纯二进制;
    • -S:去除所有符号表和重定位信息(减小文件体积);
    • $@:Makefile自动变量,表示目标文件(通常为ledc.bin)。
  • 必要性:嵌入式系统的 bootloader 或烧录工具通常只识别纯二进制文件(.bin),而ELF格式包含额外信息,无法直接加载到内存执行。

4. arm-linux-gnueabihf-objdump:反汇编可执行文件(生成.dis

对应命令:

arm-linux-gnueabihf-objdump -D -m arm ledc.elf > ledc.dis
  • 核心作用:将ELF文件(.elf)反汇编为汇编代码(.dis),用于调试和分析程序的机器码对应的汇编指令。
  • 关键参数
    • -D:对整个文件进行反汇编(包括所有段);
    • -m arm:指定目标架构为ARM(确保反汇编指令正确);
    • > ledc.dis:将反汇编结果输出到文本文件ledc.dis
  • 必要性:嵌入式开发中,若程序运行异常(如崩溃、死循环),可通过反汇编文件分析机器码是否符合预期,定位底层问题(如内存访问越界、指令错误)。

三、总结:工具链的协同流程

这些工具虽然功能不同,但遵循“源代码→目标文件→可执行文件→二进制文件”的编译链路,共同完成程序的构建:

  1. gcc:将.s/.S/.c编译为.o(目标文件);
  2. ld:将.o链接为.elf(带地址信息的可执行文件);
  3. objcopy:将.elf转换为.bin(可烧录的纯二进制文件);
  4. objdump:生成.dis(反汇编文件,用于调试)。

编写链接脚本

配置不同内存区段
上面只是简单的手动规定了text段的存放地址:arm-linux-gnueabihf-ld -Ttext 0X87800000 -o ledc.elf $^所有的文件都会链接到以 0X87800000 为起始地址的区域
可以编写.lds文件来编写更细节的段

示例代码 10.4.2.2 imx6ul.lds 链接脚本代码

1 SECTIONS{
2 . = 0X87800000;
3 .text :
4 {
5 start.o
6 main.o
7 *(.text)
8 }
9 .rodata ALIGN(4) : {*(.rodata*)}
10 .data ALIGN(4) : { *(.data) }
11 __bss_start = .;
12 .bss ALIGN(4) : { *(.bss) *(COMMON) }
13 __bss_end = .;
14 }

最后在makefile中替换:arm-linux-gnueabihf-ld -Timx6ul.lds -o ledc.elf $^