BOOT 的处理过程是发生在 I.MX6U 芯片上电以后,芯片会根据 BOOT_MODE[1:0]的设置来选择 BOOT 方式。 BOOT_MODE[1:0]的值是可以改变的,有两种方式,一种是改写 eFUSE(熔丝),一种是修改相应的 GPIO 高低电平。第一种修改 eFUSE 的方式只能修改一次,后面就不能再修改了,所以我们不使用。我们使用的是通过修改 BOOT_MODE[1:0]对应的 GPIO 高低电平来选择启动方式,所有的开发板都使用的这种方式, I.MX6U 有一个 BOOT_MODE1 引脚和BOOT_MODE0 引脚,这两个引脚对应这 BOOT_MODE[1:0]。
I.MX6U 有四个 BOOT 模式,这四个 BOOT 模式由 BOOT_MODE[1:0]来控制,也就是BOOT_MODE1 和 BOOT_MODE0 这两 IO, BOOT 模式配置如表 9.1.1 所示:
【1】串行下载
串行下载的意思就是可以通过 USB 或者 UART 将代码下载到板子上的外置存储设备中,我们可以使用 OTG1 这个 USB口向开发板上的 SD/EMMC、 NAND 等存储设备下载代码 。这个下载是需要用到 NXP 提供的一个软件,一般用来最终量产的时候将代码烧写到外置存储设备中的。
【2】内部 BOOT 模式
在此模式下,芯片会执行内部的 boot ROM 代码,这段 boot ROM 代码会进行硬件初始化(一部分外设),然后从 boot 设备(就是存放代码的设备、比如 SD/EMMC、 NAND)中将代码拷贝出来复制到指定的 RAM 中,一般是DDR。
当 BOOT_MODE 设置为内部 BOOT 模式以后,可以从以下设备中启动:
①、接到 EIM 接口的 CS0 上的 16 位 NOR Flash。
②、接到 EIM 接口的 CS0 上的 OneNAND Flash。
③、接到 GPMI 接口上的 MLC/SLC NAND Flash, NAND Flash 页大小支持 2KByte、 4KByte
和 8KByte, 8 位宽。
④、 Quad SPI Flash。
⑤、接到 USDHC 接口上的 SD/MMC/eSD/SDXC/eMMC 等设备。
⑥、 SPI 接口的 EEPROM。
【1】I.MX6U 的最终可烧写文件组成如下:
①、 Image vector table,简称 IVT, IVT 里面包含了一系列的地址信息,这些地址信息在ROM 中按照固定的地址存放着。
②、 Boot data,启动数据,包含了镜像要拷贝到哪个地址,拷贝的大小是多少等等。
③、 Device configuration data,简称 DCD,设备配置信息,重点是 DDR3 的初始化配置。
④、用户代码可执行文件,比如 led.bin。
可以看出最终烧写到 I.MX6U 中的程序其组成为: IVT+Boot data+DCD+.bin。
【2】IVT 和 Boot Data 数据
VT 包含了镜像程序的入口点、指向 DCD 的指针和一些用作其它用途的指针。内部 Boot ROM 要求 IVT 应该放到指定的位置,不同的启动设备位置不同,而 IVT 在整个 load.imx 的最前面,其实就相当于要求 load.imx 在烧写的时候应该烧写到存储设备的指定位置去。整个位置都是相对于存储设备的起始地址的偏移 。
IVT 和 Boot Data 所示的格式对应起来如表 9.4.1.1 所示:
在表 9.4.1.1 中,我详细的列出了 load.imx 的 IVT+Boot Data 每 32 位数据所代表的意义。这些数据都是由 imxdownload 这个软件添加进去的。
【3】DCD 数据
复位以后, I.MX6U 片内的所有寄存器都会复位为默认值,但是这些默认值往往不是我们想要的值,而且有些外设我们必须在使用之前初始化它。为此 I.MX6U 提出了一个 DCD(DeviceConfig Data)的概念,和 IVT、 Boot Data 一样, DCD 也是添加到 load.imx 里面的,紧跟在 IVT和 Boot Data 后面, IVT 里面也指定了 DCD 的位置。 DCD 其实就是 I.MX6U 寄存器地址和对应的配置信息集合, Boot ROM 会使用这些寄存器地址和配置集合来初始化相应的寄存器,比如开启某些外设的时钟、初始化 DDR 等等。 DCD 区域不能超过 1768Byte
**DCD 里面的初始化配置主要包括三方面: **
①、设置 CCGR0~CCGR6 这 7 个外设时钟使能寄存器,默认打开所有的外设时钟。
②、配置 DDR3 所用的所有 IO。
③、配置 MMDC 控制器,初始化 DDR3。
【1】启动汇编文件
启动汇编文件中没有初始化 DDR3 的代码,但是却将 SVC 模式下的 SP 指针设置到了 DDR3 的地址范围中 ,是因为DCD 数据包含了 DDR 配置参数, I.MX6U 内部的 Boot ROM 会读取 DCD 数据中的 DDR 配置参数然后完成 DDR 初始化的。
【2】直接操作0X020C4068这个寄存器解析
//下面这行代码的意思是直接操作0X020C4068这个寄存器
//具体寄存器的作用是通过手册得到的
#define CCM_CCGR0 *((volatile unsigned int*)0X020C4068)
//假设寄存器为32位
//要设置0X020C4068寄存器值为0X03,可以直接写成
CCM_CCGR0=0X03
为什么这个宏定义可以直接操作这个地址??
//第一步
//表示将0X020C4068强制转换为unsigned int 类型的指针
//0X020C4068代表一个16进制的数据
(unsigned int*)0X020C4068
//第二步
//使用volatile声明变量值,防止被编译器优化,从而可以提供对特殊地址的稳定访问;
(volatile unsigned int*)0X020C4068
//第三步
//对此变量间接寻址,获取指针指向地址的存储值
*((volatile unsigned int*)0X020C4068)
拓展:
由于内存访问速度远不及CPU处理速度,为提高机器整体性能,在硬件上引入硬件高速缓存Cache,加速对内存的访问。
当要求使用volatile声明变量值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。
精确地说就是,遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问;
如果不使用valatile,则编译器将对所声明的语句进行优化。(简洁的说就是:volatile关键词影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错)用volatile定义的变量会在程序外被改变,每次都必须从内存中读取,而不能重复使用放在cache或寄存器中的备份
注意:频繁地使用volatile很可能会增加代码尺寸和降低性能,因此要合理的使用volatile。反例:
#define rURXH0 (*(unsigned char *)0x50000024) //UART 0 Receive buffer
这个就是串行的接收buffer,其地址为0x50000024。
如果我们没有将这个地址强行转换成volatile,那么我们在使用rURXH0时,可能直接从cpu的寄存器中取值。
因为之前rURXH0被访问过,也就是说之前就从内存中取出rURXH0的值保存到某个寄存器中。
之所以直接从寄存器中取值,而不去内存中取值,是因为编译器优化代码的结果(访问cpu寄存器比访问ram快的多)。
用volatile关键字对0x50000024进行强制转换,使得每一次被访问rURXH0时,执行部件都会从0x50000024这个内存单元中取出值来赋值给rURXH0。
【3】解析:
GPIO1_DR &= ~(1<<3); // 将 GPIO1_DR 的 bit3 清零
- 先对1左移3位:0001 -> 1000
- 再按位取反:1000 -> 0111
- 最后“按位”与运算
就相当于把第3位清零了。
#这里要注意 start.o 一定要放到最前面,因为在后面链接的时候start.o 是最先要执行的文件!
objs := start.o main.o
#生成最终的可执行文件 ledc.bin, ledc.bin 依赖 start.o 和 main.o
ledc.bin:$(objs)
#链接起始地址是 0X87800000,自动变量“$^”意思是所有依赖文件的集合
#在这里就是 objs 这个变量的值:start.o 和 main.o
arm-linux-gnueabihf-ld -Ttext 0X87800000 -o ledc.elf $^
#将 ledc.elf 文件转为 ledc.bin,自动变量“$@”意思是目标集合,在这里就是“ledc.bin”
arm-linux-gnueabihf-objcopy -O binary -S ledc.elf $@
#反汇编,生成 ledc.dis 文件
arm-linux-gnueabihf-objdump -D -m arm ledc.elf > ledc.dis
%.o:%.s
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $<
%.o:%.S
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $<
%.o:%.c
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $<
#工程清理规则,过命令“make clean”就可以清理工程。
clean:
rm -rf *.o ledc.bin ledc.elf ledc.dis
上面语句中我们是通过“-Ttext”来指定链接地址是 0X87800000 的,这样的话所有的文件都会链接到以 0X87800000 为起始地址的区域。但是有时候我们很多文件需要链接到指定的区域,或者叫做段里面,比如在 Linux 里面初始化函数就会放到 init 段里面。因此我们需要能够自定义一些段,这些段的起始地址我们可以自由指定,同样的我们也可以指定一个文件或者函数应该存放到哪个段里面去。要完成这个功能我们就需要使用到链接脚本 。
其主要目的是描述输入文件中的段如何被映射到输出文件中,并且控制输出文件中的内存排布。
链接脚本的语法就是编写一系列的命令,这些命令组成了链接脚本,每个命令是一个带有参数的关键字或者一个对符号的赋值,可以使用分号分隔命令。像文件名之类的字符串可以直接键入,也可以使用通配符“*”。最简单的链接脚本可以只包含一个命令“SECTIONS”,我们可以在这一个“SECTIONS”里面来描述输出文件的内存布局。我们一般编译出来的代码都包含在 text、 data、 bss 和 rodata 这四个段内。
#imx6ul.lds
SECTIONS{
#对一个特殊符号“.”进行赋值,“.”在链接脚本里面叫做定位计数器,默认的定位计数器为 0。
. = 0X87800000;
#.text”是段名,后面的冒号是语法要求
#冒号后面的大括号里面可以填上要链接到“.text”这个段里面的所有文件
#“*(.text)”中的“*”是通配符,表示所有输入文件的.text段都放到“.text”中。
.text :
{
start.o
main.o
*(.text)
}
#定义了一个名为“.data”的段,然后所有文件的“.data”段都放到这里面
#“ALIGN(4)”用来对“.data”这个段的起始地址做字节对齐的, ALIGN(4)表示 4 字节对齐
.rodata ALIGN(4) : {*(.rodata*)}
.data ALIGN(4) : { *(.data) }
#__bss_start”和“__bss_end”是符号,对这两个符号进行赋值,其值为定位符“.”,
#这两个符号用来保存.bss 段的起始地址和结束地址。
__bss_start = .;
#定义了一个“.bss”段,所有文件中的“.bss”数据都会被放到这个里面,
#.bss”数据就是那些定义了但是没有被初始化的变量。
.bss ALIGN(4) : { *(.bss) *(COMMON) }
__bss_end = .;
}
编写好了链接脚本文件: imx6ul.lds ,将 Makefile 中的如下一行代码:
arm-linux-gnueabihf-ld -Ttext 0X87800000 -o ledc.elf $^
改为
arm-linux-gnueabihf-ld -Timx6ul.lds -o ledc.elf $^
修改完成以后使用新的 Makefile 和链接脚本文件重新编译工程,编译成功以后就可以烧写到 SD 卡中验证了。
#1~6行定义了一些变量,如果使用其它编译器的话只需要修改第 1 行即可
CROSS_COMPILE ?= arm-linux-gnueabihf-
#TARGET 目标名字,不同的例程肯定名字不一样
TARGET ?= bsp
CC := $(CROSS_COMPILE)gcc
LD := $(CROSS_COMPILE)ld
OBJCOPY := $(CROSS_COMPILE)objcopy
OBJDUMP := $(CROSS_COMPILE)objdump
#变量 INCDIRS 包含整个工程的.h 头文件目录
#符号“\”,相当于“换行符”,表示本行和下一行属于同一行,一般一行写不下的时候就用符号“\”来换行。
INCDIRS := imx6ul \
bsp/clk \
bsp/led \
bsp/delay
#SRCDIRS 包含的是整个工程的所有.c 和.S 文件目录
SRCDIRS := project \
bsp/clk \
bsp/led \
bsp/delay
#变量 INCLUDE 使用到了函数 patsubst,
#加“-I”的目的是因为 Makefile 语法要求指明头文件目录的时候需要加上“-I”
INCLUDE := $(patsubst %, -I %, $(INCDIRS))
#变量 SFILES 保存工程中所有的.s 汇编文件(包含绝对路径)
SFILES := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.S))
CFILES := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.c))
#使用函数 notdir 将 SFILES 和 CFILES 中的路径去掉即可
SFILENDIR := $(notdir $(SFILES))
CFILENDIR := $(notdir $(CFILES))
#变量 SOBJS 和 COBJS 是.S 和.c 文件编译以后对应的.o 文件目录
SOBJS := $(patsubst %, obj/%, $(SFILENDIR:.S=.o))
COBJS := $(patsubst %, obj/%, $(CFILENDIR:.c=.o))
#变量 OBJS 是变量 SOBJS 和 COBJS 的集合
OBJS := $(SOBJS) $(COBJS)
# VPATH 是指定搜索目录的
VPATH := $(SRCDIRS)
#指定了一个伪目标 clean
.PHONY: clean
$(TARGET).bin : $(OBJS)
$(LD) -Timx6ul.lds -o $(TARGET).elf $^
$(OBJCOPY) -O binary -S $(TARGET).elf $@
$(OBJDUMP) -D -m arm $(TARGET).elf > $(TARGET).dis
$(SOBJS) : obj/%.o : %.S
$(CC) -Wall -nostdlib -c -O2 $(INCLUDE) -o $@ $<
$(COBJS) : obj/%.o : %.c
$(CC) -Wall -nostdlib -c -O2 $(INCLUDE) -o $@ $<
clean:
rm -rf $(TARGET).elf $(TARGET).dis $(TARGET).bin $(COBJS) $(SOBJS)