Skip to content

LV020-映像文件

假设使用 SD/TF 卡启动,卡上的程序有多大?它应该被复制到 DDR 哪里去? 这一部分我们主要参考《i.MX 6ULL Applications Processor Reference Manual》的 8.7 Program image 一节。

一、格式概述

1. 不能烧写 bin 文件?

前边我们学习过单片机的话,我们知道程序有很多的格式,它们包含的信息不同。比如 lcd.bin 可以直接烧写到 Flash 上(需要注意的是平时调试 STM32 单片机程序用的其实是 Hex 文件,而在 IAP 升级固件时要用到 Bin 文件,hex 文件比 bin 文件多了一些地址信息,这些地址信息是下载工具解析的时候要用到的)。它们是自启动的,这什么意思?比如一上电,运行的是 lcd.bin 前面的代码,它会初始化内存,把自己从 Flash 上复制到内存里去执行。要注意:是 自己把自己复制到内存

但是对于 IMX6ULL,烧写在 EMMC、 SD/TF 卡上的程序,并不能“自己复制自己”,是“别人把它复制到内存里”。一上电首先运行的是 boot ROM 上的程序,它从 EMMC、 SD/TF 卡上把程序复制进内存里。所以: boot ROM 程序需要知道从启动设备哪个位置读程序,读多大的程序,复制到哪里去。

所以,在启动设备上,不能仅仅烧写 bin 文件,还需要再添加额外的信息

2. Device Configuration Data 是什么?

还有一个问题, IMX6ULL 的 boot ROM 程序可以把程序读到 DDR 里,那需要先初始化 DDR。每种板子接的 DDR 可能不一样, boot ROM 程序需要初始化这些不同的 DDR。 boot ROM 从哪里得到这些不同的参数?

还有, IMX6ULL 支持各种启动设备,比如各种 Nor Flash。为了通用, bootROM 程序将会使用最保守的参数,也就是最慢的时序来访问 Nor Flash。为加快启动程序, boot ROM 程序可以根据我们提供的信息初始化硬件,让它以更优的参数运行。

这些参数信息,被称为“ Device Configuration Data”,设备配置数据(DCD),这些 DCD 将会跟 bin 文件一起打包烧写在启动设备上。 boot ROM 程序会从启动设备上读出 DCD 数据,根据 DCD 来写对应的寄存器以便初始化芯片。DCD 中列出的是对某些寄存器的读写操作,我们可以在 DCD 中设置 DDR 控制器的寄存器值,可以在 DCD 中使用更优的参数设置必需的硬件。这样 boot ROM 程序就会帮我们初始化 DDR 和其他硬件,然后才可以把 bin 程序读到 DDR 中并运行。

3. 总结一下?

总结起来,烧写在 EMMC、 SD 卡或是 TF 卡上的,除了程序本身,还有位置信息、 DCD 信息,这些内容合并成一个映像文件,如下图

image-20230716095427193

I.MX6U 的最终可烧写文件组成如下:

  • (1) Image vector table,简称 IVT, IVT 里面包含了一系列的地址信息,这些地址信息在 ROM 中按照固定的地址存放着。
  • (2)Boot data,启动数据,包含了映像要拷贝到哪个地址,拷贝的大小是多少等等。
  • (3)Device configuration data,简称 DCD,设备配置信息,重点是 DDR3 的初始化配置。
  • (4)用户代码可执行文件,比如 led.bin。

这 4 部分内容合并成为一个映像文件,烧写在 EMMC、 SD 卡或 TF 卡等启动设备的 某个固定地址, boot ROM 程序去这个固定地址读出映像文件。启动设备不同,固定地址不同。所以其实最终烧写到 I.MX6U 中的程序组成为: IVT + Boot data + DCD + .bin,这些最终会组成一个 .imx 格式为文件,这个 .imx 文件才是我们最终烧写到 I.MX6U 中的可运行的程序。

以正点原子的 led.bin 裸机程序为例,我们将会在 ubuntu 中通过一个 imxdownload 的工具(正点原子提供的,但是后面发现 u-boot 源码中有个工具也可以生成 imx 文件,后边会再分析这个工具)以 led.bin 为基础的 load.imx 文件,然后将 load.imx 文件烧写到开发板中,这样,开发板就会可以运行这个裸机程序了。load.imx 就是在 led.bin 前面加上 IVT+Boot data+DCD。内部 BootROM 会将 load.imx 拷贝到 DDR 中,用户代码是要一定要从 0X87800000 这个地方开始的,因为链接地址为 0X87800000, load.imx 在用户代码前面又有 3KByte(为什么是 3KB?后面会拿到答案的) 的 IVT+Boot Data+DCD 数据 。因此 load.imx 在 DDR 中的起始地址就是 0X87800000-3072 = 0X877FF400。

二、格式详解

1. Image Vector Table 和 Boot Data

【说明】下边有些图中有 C 语言格式的结构体,这些结构体来源于 u-boot 源码 的 tools 目录下的 imximage.h(这个是当时刚学习的时候用的 2016 版本的 buoot 中的 imximage.h - tools/imximage.h - U-boot source code v2016.03 - Bootlin) 后来这个文件换目录了,至少在 2019.04 版本中已经挪到了 source/include 目录下:imximage.h - include/imximage.h - U-boot source code v2019.04 - Bootlin

1.1 概述

映像向量表(Image Vector Table, IVT)是 ROM 从提供程序映像的引导设备中读取的数据结构,该程序映像包含成功启动所需的数据组件。IVT 包括程序映像入口点、指向设备配置数据(DCD)的指针和 ROM 在引导过程中使用的其他指针。内部 boot ROM 要求 IVT 应该放到指定的地址 ,该地址由连接到芯片的引导设备确定,而 IVT 在整个 .imx 文件的最前面,其实就相当于要求 .imx 文件在烧写的时候应该烧写到存储设备的指定位置去 。每个引导设备类型的 IVT 相对基址(存储设备的起始地址)的偏移量和初始加载区域大小在下表中定义:

image-20260114124406544

以 SD/EMMC 为例, IVT 偏移为 1Kbyte, IVT+Boot data+DCD 的总大小为 4KByte-1KByte = 3KByte。假如 SD/EMMC 每个扇区为 512 字节,那么 load.imx 应该从第三个扇区开始烧写,前两个扇区要留出来。也就是说,后面我们用到的 .imx 从第 3KByte 开始才是真正的.bin 文件(.imx 文件开头就是 IVT 数据)。那么 IVT 里面究竟存放着什么东西呢?后边会再详细说明。

IVT 的位置是 ROM 唯一固定的要求,其余的或映像存储器映射是灵活的,由 IVT 的内容决定。

image-20230716104348196

1.2 Image Vector Table(IVT)

IVT 会被放在固定的地址, IVT 中是 一系列的地址, boot ROM 程序会根据这些地址来确定映像文件中其他部分在哪里。

image-20230716105531162

图二-1-1.2-1 IVT 格式

要注意的是上图中这 4 个部分:

  • (1)header:里面有 3 项: tag、 length、 version。 length 表示 IVT 的大小,它是 32 字节。要注意是的,它是大字节序的。
  • image-20260114124508033

图二-1-1.2-2 IVT header 格式

其中 Tag 为一个字节长度,固定为 0XD1, Length 是两个字节,保存着 IVT 长度,为 大端模式,也就是高字节保存在低内存中。最后的 Version 是一个字节,为 0X40 或者 0X41。

  • (2)entry:用户程序运行时第 1 条指令的地址,就是程序的链接地址、程序被复制到内存哪里。
  • (3)dcd:映像被复制到内存后,其中的 DCD 数据的地址。
  • (4)boot data:映像被复制到内存后,其中的 boot data 的地址。
  • (5)self:映像被复制到内存后, IVT 自己所在的地址。

1.3 Boot data

映像被复制到内存后,整个映像文件(IVT 之前还有几个扇区数据,比如分区表)所在的地址。 Boot Data 的数据格式如图 :

image-20230716110045197

图二-1-1.3-1 Boot data 格式

  • (1)start:这是映像文件在内存中的地址

以 SD/TF 卡为例:映像文件 =(1K 数据,内含分区表等信息)+IVT+BootData+DCD+用户数据(.bin 文件)。

注意, IVT 并不在映像文件的最前面, start 也不是 IVT 在内存中的地址,而是整个映像文件在内存中的地址:start = IVT 在内存中的地址 - IVT offset。什么意思?假设 IVT 被保存在启动设备 TF 卡 1024 偏移地址处, IVT 被复制到内存地址 0x87000000,那么 start = 0x87000000 -1024。

所以 start 表示的是启动设备开头的数据,被复制到内存哪里去。从它的含义也可以推理出: boot ROM 程序会把启动设备开头的数据,复制到内存;而不仅仅是从 IVT 开始复制。

  • (2)length:保存在启动设备上的整个映像文件的长度,从 0 地址开始(不是从 IVT 开始)。
  • (3)plugin:这是一个标记位,当它为 1 时表示这个映像文件是“plugin”,即插件。

boot ROM 程序可以支持有限的启动设备,如果想支持更多的启动设备比如网络启动、 CDROM 启动,就需要提供对应的驱动。这些驱动就是“ plugin ”,暂时还没学习那么深入的东西,先不管这里,该标记位为 0。

Boot data 就是用来表示映像文件应该被复制到哪里去,以及它的大小。boot ROM 程序就是根据它来把整个映像文件复制到内存去的

1.4 实际情况分析

那实际情况是怎样的呢?我们使用 winhex 软件(或者可以以十六进制阅读二进制文件的软件都行)打开 load.imx(正点原子裸机程序里边的,其实 u-boot 编译后生成的 imx 程序也一样) ,winhex 可以直接查看一个文件的二进制格式数据,但是这个软件还需要安装,我们其实还可以通过 notepad++,只是需要在安装一个 Hex-Editor 的软件,这个在软件的插件选项中搜索安装即可。

image-20221014100751696

图二-1-1.4-1 load.imx 文件

我们将前 44 个字节的数据按照 4 个字节一组组合在一起就是: 0X402000D1、0X87800000、 0X00000000、 0X877FF42C、0X877FF420、 0X877FF400、 0X00000000、 0X00000000、0X877FF000、 0X00200000、 0X00000000。这 44 个字节的数据就是 IVT 和 Boot Data 数据,与上边对应起来就是:

  • IVT 数据分析
IVT 结构数据描述
header0X402000D1根据上边 图二-1-1.2-2 中 header 格式,第一个字节 Tag 为 0XD1,第二和第三这两个字节为 IVT 大小,为大端模 式,所以 IVT 大小为 0X20 = 32 字节。第四个字节 为 0X40。完全符合上边 图二-1-1.2-2 中 header 的格式。
entry0X87800000入口地址,也就是映像第一行指令所在的位置, 0X87800000 就是我们的链接地址。
reserved10X00000000未使用,保留。
dcd0X877FF42CDCD 地址,映像地址为 0X87800000, IVT+Boot Data+DCD 整个大小为 3KByte。因此 load.imx 的起始地址就是 0X87800000-0XC00 = 0X877FF400。因 此 DCD 起始地址相对于 load.imx 起始地址的偏移 就是 0X877FF42C-0X877FF400 = 0X2C,也就是说从 图 1.4.1-1 中的 0X2C 这个地址开始就是 DCD 数据 了。
boot data0X877FF420boot 地址, header 里面已经设置了 IVT 大小是 32 个 字 节 , 所 以 boot data 的地址就是 0X877FF400 + 32 = 0X877FF420。
self0X877FF400IVT 复制到 DDR 中以后的首地址。
csf0X00000000CSF 地址。
reserved20X00000000保留,未使用。
  • Boot Data 数据分析
Boot Data 结构数据描述
start0X877FF000整个 load.imx 的起始地址,包括前面 1KByte 的地址偏移
length0X00200000映像大小,这里设置 2MByte,映像大小不能超过 2MByte
plugin0X00000000插件

上边的这两个表中写出了 load.imx 的 IVT+Boot Data 每 32 位数据所代表的意义。这些数据都是由 imxdownload 这个软件添加进去的(但其实应该是 u-boot 中有工具可以完成的)。

2. Device Configuration Data (DCD)

2.1 概述

复位后,芯片使用系统中所有外设的默认寄存器值。但是,这些设置对于实现最佳系统性能来说通常并不理想,甚至有些外设在使用之前必须进行配置。为此 I.MX6U 提出了 DCD(DeviceConfig Data)的概念,和 IVT、 Boot Data 一样, DCD 也是添加到 load.imx 里面的,紧跟在 IVT 和 Boot Data 后面, IVT 里面也指定了 DCD 的位置。

简单地说 DCD 就是设备的配置信息,就是 I.MX6U 寄存器地址和对应的配置信息集合, Boot ROM 会使用这些寄存器地址和配置集合来初始化相应的寄存器,比如开启某些外设的时钟、初始化 DDR 等等 。实际上 DCD 还可以更复杂,它支持多种命令: write data、 check data、nop、 unlock。我们可以通过 write data 命令写寄存器,通过 check data 命令等待寄存器就绪。

2.2 DCD 的格式

image-20230716124901718

图二-2-2.2-1 DCD 数据格式

  • (1)DCD 以 Header 开始

其中 Tag 是单字节,固定为 0XD2, Length 为两个字节,表示 DCD 区域的大小,包含 header,同样是 大端模式, Version 是单字节,固定为 0X40 或者 0X41。

  • (2)接下来就是各个“ CMD”

我们可以在一个“ CMD”里操作多个寄存器,比如在一个“write data command”中,写多个寄存器。

2.3 DCD 的 CMD 介绍

以“ write data command”为例简单介绍一下,它的格式为:

image-20260114124659279

图二-2-2.3-1

其中 Tag 为一个字节,固定为 0XCC。 Length 是两个字节,包含写入的命令数据长度,包含 header,同样是大端模式。 Parameter 为一个字节,这个字节的每个位含义如图 :

image-20260114124930710

Parameter 中 b [2:0] 用来表示写操作的字节数,是以字节、半字(2 byte),还是字(4 byte)来操作。而 b [4]、 b [3] 决定了是写值(write value),清位(clearbitmask),还是设位(set bitmask)。

既然是写命令,那自然就有“地址、值”,上图中就是多个“ Address、Value/Mask”。

2.4 实际情况分析

image-20221014102921736

有前边分析可知,DCD 数据是从 0X2C 地址开始的。根据我们分析的 DCD 结构可以得到 load.imx 的 DCD 数据 :

DCD 结构 数据 描述
header 0X40E801D2根据 图二-2-2.2-1 的 header 格式,第一个字节 Tag 为 0XD2,第二和三这两个字节为 DCD 大小,为大端模式,所以 DCD 大小为 0X01E8=488 字节。第四个字节为 0X40。完全符合 图二-2-2.2-1 中的格式。
Write Data Command0X04E401CC根据 图二-2-2.3-1,第一个为 Tag,固定为 0XCC,第二和三这两个字节是大端模式的命令总长度,为0X01E4 = 484 个字节。第四个字节是 Parameter,为 0X04,表示目标位置宽度为 4 个字节。
Address 0X020C4068寄存器 CCGR0 地址
Value 0XFFFFFFFF要写入寄存器 CCGR0 的值,表示打开 CCGR0 控制的所有外设时钟。
… … … … CCGR1~CCGR5 这些寄存器的地址和值。
Address 0X020C4080寄存器 CCGR6 地址
Value 0XFFFFFFFF要写入寄存器 CCGR6 的值,表示打开 CCGR6 控制的所有外设时钟。
Address 0X020E04B4寄存器 IOMUXC_SW_PAD_CTL_GRP_DDR_TYPE 寄存器地址。
Value 0X000C0000设置 DDR 的所有 IO 为 DDR3 模式。
Address 0X020E04AC寄存器 IOMUXC_SW_PAD_CTL_GRP_DDRPKE 地址。
Value 0X00000000所有 DDR 引脚关闭 Pull/Keeper 功能。
Address 0X020E027C寄存器 IOMUXC_SW_PAD_CTL_PAD_DRAM_SDCLK0_P
Value 0X00000030DRAM_SDCLK0_P 引脚为 R0/6。
… … … … 全部是 DDR 引脚设置
Address 0X020E0248寄存器 IOMUXC_SW_PAD_CTL_PAD_DRAM_DQM1
Value 0X00000030DRAM_DQM1 引脚驱动能力为 R0/6
Address 0X021B001CMMDC_MDSCR 寄存器
Value 0X00008000MMDC_MDSCR 寄存器值
… … … … MMDC 相关寄存器地址及其寄存器值。
Address 0X021B0404MMDC_MAPSR 寄存器
Value 0X00011006MMDC_MAPSR 寄存器配置值
Address 0X021B001CMMDC_MDSCR 寄存器
Value 0X00000000MMDC_MA1:C24DSCR 寄存器清0

从这个表中可以看出, DCD 里面的初始化配置主要包括三方面:

①、设置 CCGR0~CCGR6 这 7 个外设时钟使能寄存器,默认打开所有的外设时钟。

②、配置 DDR3 所用的所有 IO。

③、配置 MMDC 控制器,初始化 DDR3。

3. User code and data

就是用户程序或数据,原原本本地添加到映像文件里就可以。

三、实例分析

我们制作映像文件的目的什么?目的就是把我们自己的程序烧写到启动设备,让 boot ROM 程序启动它。 这一部分我用 NXP 官方的 u-boot 的 imx 镜像文件为例进行分析。我用的文件是 mfgtools-with-rootfs-mine/mfgtools/Profiles/Linux/OS Firmware/files 目录中的 u-boot-imx6ull14x14evk_emmc.imx 文件(这个文件应该是当时学习自定义烧写工具的时候复制正点原子出厂 uboot 的那个文件,不过自己随便编一个都一样的,这里主要是学习一下映像文件各个部分怎么分析)。

1. 各项值的计算

制作映像文件的起点是:我们编写的程序(这里以 u-boot 为例)。 制作过程中各项值的计算方法如下图所示:

image-20230716134713200

我们不需要手工去计算,一个 mkimage 命令就可以完成,这个工具在 u-boot 源码的 tools 目录下就有。

  • (1)确定入口地址 entry

我们的程序运行时要放在内存中哪一个位置,这是我们决定的。它被称为入口地址、链接地址。

  • (2)确定映像文件在内存中的地址 start

boot ROM 程序启动时,会把“ Initial Load Region”读出来,“ Initial load Region”里含有 IVT、 Boot data、 DCD。 boot ROM 根据 DCD 初始化设备后,再把整个映像文件读到内存。

在启动设备上,“ Initial Load Region”之后紧跟着我们的程序,反过来说就是我们程序的前面,放着“ Initial Load Region”。假设“ Initial LoadRegion”的大小为 load_size,那么在内存中“ Initial Load Region”的位置 start = entry – load_size。

注意:“ Initial Load Region ” 位于启动设备 0 位置,它的头部并不是 IVT,而是一些无用的数据(或是分区信息)。

在 IMX6ULL 参考手册中有一个表格,列出了不同启动设备对应的“ Initial LoadRegion Size”:

image-20260114124406544

  • (3)确定 IVT 在内存中的地址 self

我们知道 IVT 在启动设备上某个固定的位置,上图中的“ Image Vector Table Offset”: ivt_offset。那么在内存中它的位置可以如下计算:

markdown
self = start + ivt_offset = entry - load_size + ivt_offset
  • (4)确定 Boot data 在内存中的地址 boot_data

IVT 的大小是 32 字节, IVT 之后就是 Boot data,而 IVT 中的 boot_data 值表示 Boot data 在内存中的位置,计算如下:

markdown
boot_data = self + 32 = entry - load_size + ivt_offset + 32
  • (5)确定 DCD 在内存中的地址 dcd

Boot data 的大小是 12 字节, Boot data 之后就是 DCD,而 IVT 中的 dcd 值表示 DCD 在内存中的位置,计算如下:

markdown
dcd = boot_data + 12 = entry - load_size + ivt_offset + 44
  • (6)写入 DCD 的数据

DCD 是用初始化硬件的,特别是初始化 DDR。而 DDR 的初始化非常的复杂、专业,我们一般是使用硬件厂家提供的代码。 在正点原子的代码中,提供了一个名为 imxdownload 的程序,这个程序会写入相应的 DCD 数据,但其实后来看了另外几家的教程,发现其实这些 DCD 数据厂家是有提供的,我们在 u-boot 源码的 tools 目录下有工具可以帮助使我们生成 imx 文件,同时也会将相应的 DCD 数据写入,比如我们可以使用类似下面的指令来制作映像文件:

shell
cd u-boot # 进入 u-boot 源码目录
./tools/mkimage -n ./tools/imximage.cfg.cfgtmp -T imximage -e 0x80100000 -d led.bin led.imx

imximage.cfg.cfgtmp 文件(这个文件好像是通过 imximage.cfg 生成的)是被拷贝到 tools 目录下的,我们在 NXP 官方原版的 u-boot 源码中,i.mx6ull 的 imximage.cfg 文件位于 u-boot 源码的 board/freescale/mx6ullevk/ 目录下,

image-20230716140050787

从上图也可以看到 imximage.cfg.cfgtmp 文件中基本是对寄存器的写操作。mkimage 程序来自 u-boot,它会把 imximage.cfg.cfgtmp 中的内容转换为 DCD 数据。我们只需要了解它的大概作用:

(a)设置时钟: DDR 也需要时钟。

(b)设置引脚: DDR 需要很多引脚。

(c)设置 DDR 控制器: Multi-mode DDR controller (MMDC)。

  • (7)写入用户程序
  • (8)经过上述 7 个步骤,整个映像文件就构造出来了,可以把它烧入启动设备。

2. u-boot-imx6ull14x14evk_emmc.imx

对于.imx 文件而言,文件开头就是 IVT,我们可以把它烧写到 TF 卡 1024 偏移位置处,可以通过 USB 把 imx 文件直接下载到板子上,并运行。用 notepad++打开后以十六进制显示,低地址存放的是低位,如下图:

image-20230716142524764

比如第一行,组合起来就是 0x402000d1、 0X87800000、 0X00000000、 0X877ff42c,一行相当于是 16 个字节,连续 4 个字节算作一组。格式分析如下,前边每一部分其实都分析过了,这里就不详细分析了:

image-20230716143538969

四、映像的烧写和运行

我们编译出来的映像文件有 2 类后缀: imx、 img。 imx 文件开头就是 IVT,可以把它烧写到 TF 卡 1024 偏移位置处(这就意味着我们烧写到 TF 卡的时候需要先做一个 1KB 的空文件); img 文件开头是 1024 字节的 0 值数据,后面才是 IVT 等,它可以直接烧写到 TF 卡 0 偏移位置处。

image-20241101201219384

另外,我们可以通过 USB 把 imx 文件直接下载 到板子上,并运行。注意:通过 USB 下载方式,可以烧写程序到 EMMC、 TF 卡上,但是并非“直接烧写”。它的过程如下:

(1)通过 USB 下载 u-boot 到内存。

(2)通过 USB 下载用户程序到内存。

(3)通过 USB 发送命令运行 u-boot。

(4)用 u-boot 烧写把内存中的用户程序烧写到 EMMC、 TF 卡上。