一、烧录代码镜像到IMX6ULL上去运行

之前有过C51,STM32之类的单片机开发经验。做单片机开发的时候,我们都会有一块板子,有一个用于下载和调试代码的仿真器。开发用的都是IDE软件,里面集成了编译器,编译和下载按键。我们可以直接在IDE里面编写代码,然后用仿真器连接板子和电脑,稍微设置下就能够在IDE里面一键编译和下载代码到单片机上去运行,非常方便。
现在我们要学习Linux,那就不能使用单片机了,要使用能够运行Linux的芯片。这种芯片功能比单片机更强大,架构也要比单片机复杂很多,而且没有统一的IDE和仿真调试工具可以使用,不同厂家的芯片都有自己烧写代码镜像的方式,所以学习起来要比单片机困难一些。
但是,再困难也要有一个切入点,不然就没法继续下去了。要使用IMX6ULL芯片来学习Linux,那么首先要学会使用IMX6ULL,要了解和学习IMX6ULL的资源。对比单片机开发的经验,首先要学会如何烧录代码镜像到IMX6ULL上面去运行,我们先把IMX6ULL当作一款高级的单片机来使用。
通过学习烧录代码镜像,我们可以学习到IMX6ULL的启动模式,代码镜像组成以及启动设备等概念和知识。

一、首先我们需要了解IMX6ULL的启动过程,这部分的实现是芯片厂家自定义的,主要的参考资料是《IMX6ULL参考手册》的 Chapter 8 - System Boot,以下是我经过阅读理解后提炼的启动过程:

  1. 首先,IMX6ULL芯片内部有一片片内ROM空间(下图的红框部分),其地址从0x0000_0000开始,大小为96K

片内ROM地址映射
IMX6ULL的片内ROM
这片ROM里面存放了一段BOOT代码,或者叫启动代码或自举代码,用于引导IMX6ULL的启动,所以这片ROM在文档里称为 “boot ROM”,意思就是存放启动代码的ROM (根据上下文语境的不同,有时候boot ROM又特指启动代码,而不是指那片ROM空间)。要注意的是,启动代码是由芯片在生产的时候就已经固化到boot ROM里面的,专门用来启动IMX6ULL,我们是无法修改的。

  1. 我们给IMX6ULL上电的时候(文档中称为Power-ON Reset),硬件会强制ARM的CPU核去执行boot ROM里面的启动代码,开始启动过程。启动代码会根据内部的寄存器位BOOT_MODE[1:0]以及一些熔丝或引脚设置来决定执行什么操作,我们称为启动模式(Boot modes),不同的设置会进入不同的启动模式,IMX6ULL具有如下四种启动模式:
    IMX6ULL的启动模式
    boot ROM启动之后,具体进入哪种模式由寄存器位BOOT_MODE[1:0]来决定,那BOOT_MODE[1:0]是怎么设置的呢?它是通过在Power-On Reset的上升沿,采集BOOT_MODE0和BOOT_MODE1引脚的电平信号来设置的,下表是这两个引脚的默认设置:
    BOOT引脚默认设置可以看到,两个引脚默认是下拉的,也就是BOOT_MODE[1:0]的默认设置是00,也就是IMX6ULL启动后,默认会进入"Boot From Fuses"的启动模式。如果我们要设置为其它启动模式,那么就要在上电复位时将BOOT_MODE0和BOOT_MODE1这两个引脚设置为对应的电平状态。在正点原子的IMX6ULL-Alpha开发板上,这两个引脚被引出来连接到了拨码开关上,我们可以通过拨码开关设置这两个引脚的电平,如下图所示:
    Alpha开发板上的拨码开关

  2. 知道了IMX6ULL有几种启动模式,怎么设置启动模式之后,我们就要大概了解一下这几种启动模式是用来干嘛的,我们要烧录镜像的话, 应该选择哪种模式。

  • Boot From Fuses mode
    这种模式下,熔丝位BT_FUSE_SEL表示启动设备里是否已固化了代码镜像。如果BT_FUSE_SEL=0,表示代码镜像还未固化到启动设备里面,这时会跳转到Serial Downloader模式尝试下载代码镜像到启动设备。如果BT_FUSE_SEL=1,则表示代码镜像已固化到启动设备内,则执行正常的启动流程,这时的流程总体上跟后面要介绍的Internal Boot相似,仅有一点不同 —— 不能通过使用GPIO引脚来选择启动设备。所以,这种模式适用于产线量产,参数可以直接固化在熔丝里面,不需要占用额外的GPIO引脚来设置。

  • Serial Downloader
    使用这种模式,我们可以通过USB或串口下载代码镜像到DDR上面去运行。注意,这种模式是直接将镜像下载到DDR里面去运行,如果我们需要将镜像固化到非易失性存储设备里面,则应先进入这种模式下载一个Downloader应用(手册里面叫做provisioning program)到DDR里面去运行,然后在这个应用里面再通过USB或UART下载真正的代码镜像并进行固化。进入这个模式后,boot ROM会开启WDOG1(默认64秒超时),并且不断检测USB OTG1和UART 1/2,如果未检测到任何下载活动并且WDOG1超时,则CPU核复位重启,下图是该启动模式下的执行流:
    serial downloader boot flow
    下图是USB OTG端口和UART的配置参数:
    serial downloader mode使用的USB OTG参数
    serial downloader mode使用的UART参数
    serial downloader mode使用的UART引脚和熔丝使能配置

那么很明显,我们要下载代码镜像的话,肯定要进入这个模式了。但是还有两个问题需要解决:1)代码下载到哪里,是内部ROM,还是外部的FLASH或SD卡?该怎么设置?2)是否有可用的上位机工具?如果没有的话,那岂不是要自己去找这个下载协议,然后自己去开发上位机?

先来看第二个问题,《IMX6ULL参考手册》的 8.9.3 Serial Download Protocol (SDP) 小节部分介绍了串行USB下载协议,我们是可以按照协议描述自己去实现上位机软件开发的,但是需要熟悉USB接口开发相关的知识(虽然文档中说明了串口也可以用于实现下载,但是我并没有找到串口使用的下载协议)。NXP官方提供了一个Windows上位机工具MfgTool用于烧写Uboot,Linux内核和文件系统,这个工具针对不同的启动设备都提供了烧写脚本,可以选择烧写到SD卡,EMMC或NAND FLASH等等,使用方法可以参考正点原子的《I.MX6U嵌入式Linux驱动开发指南》里面的系统烧写章节部分。但是我们现在只是要烧写普通的代码镜像,而不是Linux内核和文件系统,有什么办法呢?第一种方法是偷梁换柱,用我们的代码镜像替换掉Uboot代码镜像,IMX6ULL选择进入Serial Downloader模式,然后在Windows上使用MfgTool工具进行烧写。这种方法虽然可以使用现成的工具,并且可以选择要烧写到哪里,但是因为每次都要烧写kernel和文件系统(虽然用不到),故很消耗时间。 第二种方法就是我们不用通过IMX6ULL芯片的Serial Downloader模式进行下载,而是直接把代码镜像固化到启动设备里面,最简单的就是固化到SD卡里面,因为SD卡是可拆卸的(EMMC, NAND FLASH等都是直接焊接在板子上的),可以通过读卡器插到电脑上进行固化操作,然后插回到开发板上当作启动设备,正点原子的裸机开发部分就是使用的这种方法。

现在,我们先来实践第一种方法,通过使用MsgTool工具进行代码镜像下载。先找一个现成的裸机代码镜像来做实验,这里就使用正点原子的裸机部分实验-RGBLED显示实验的代码镜像。我们编译工程后,得到lcd.bin,然后用正点原子的imxdownload工具,在lcd.bin前面添加上DCD数据,得到load.imx文件。然后把我们的load.imx复制到mfgtools\Profiles\Linux\OS Firmware\files文件夹里面,并替换掉之前的uboot镜像文件。这样,我们使用MfgTool下载的时候,它以为下载的是uboot,实际上是我们的LCD实验代码镜像。我们选择EMMC烧写脚本,将LCD代码镜像连同linux kernel和rootfs一起烧写到EMMC。烧写完毕后,拔掉USB线,设置拨码为从EMMC启动,然后重启开发板,可以发现本来应该是运行UBOOT代码的,现在运行的就是我们偷换掉的LCD测试工程代码,如下图所示:
替换UBOOT的方法烧录代码镜像
这种方法可以用,但是太麻烦,烧录时间长,EMMC反复烧录会影响其使用寿命,而且正点原子的硬件设计导致SD卡和USB不能同时使用,我们就无法通过USB烧录代码镜像到SD卡里面。

接下来,我们使用第二种方法来烧录代码镜像到SD卡里面,然后通过从SD卡启动来运行我们烧录的代码镜像。这种方法比较简单,可以参考正点原子的《I.MX6U嵌入式Linux驱动开发指南》里面的裸机开发部分。我们使用正点原子的imxdownload工具,在lcd.bin前面添加DCD数据得到load.imx后,烧录到SD卡里面。然后把SD卡插入到开发板上,拨码选择从SD卡启动,同样可以成功运行LCD测试代码,如下图所示:
从SD卡启动
显然,第二种方法要方便一些,虽然也要反复插拔SD卡,但是烧录起来很快,比使用MfgTool要好用一些。

  • Internal Boot
    这种启动模式是我们开发最常用的模式,这种模式下,boot ROM代码会继续执行,它会初始化硬件并且从启动设备里面加载代码镜像到指定的地方去运行,该模式可以通过使用GPIO引脚来设置一些熔丝位参数。我们通过Serial Downloader模式烧录代码镜像后,就可以通过这个模式从启动设备里面加载代码镜像来运行了,正点原子的Alpha开发板通过SD, EMMC, NAND启动,实际上用的都是这个模式。

二、知道怎么下载代码镜像后,我们需要了解启动设备的概念。所谓的启动设备,是指用于存放需要运行的代码镜像的外部设备。注意启动设备并不是指固化了启动代码的boot ROM,为了区分,大家可以把boot ROM里面的代码理解自举代码,自举代码是由芯片厂商预置在芯片内部的。我们自己的代码可以理解为用户代码,保存在启动设备里面,可以是EMMC, SD卡,NAND等外部设备。系统上电或复位后,会强制从自举代码开始运行,然后再由自举代码从启动设备加载用户代码运行。

既然启动设备是可以选择的,那怎么设置IMX6ULL的启动设备呢?这里需要参考《IMX6ULL参考手册》的 Chapter 5 Fusemap - 5.3 Fusemap Descriptions Table 章节,下图的红框部分是用于选择启动设备的熔丝选项:
启动设备的熔丝配置位
可以看到,对于BOOT_CFG1这个熔丝,如果要将SD卡设置为启动设备,则应配置为0b010XXXXX。如果要将EMMC设置为启动设备,应配置为0b011XXXXX。不同启动设备所用的端口以及参数,都可以在 Table 5-1. Boot Device Select 熔丝配置表里面找到,这里就不截图了。
可以看出,启动设备是通过配置BOOT_CFG1这个熔丝来进行选择的。在BOOT_MODE[1:0] = 0b10,Internal Boot启动模式下,熔丝BT_FUSE_SEL=0表示使用GPIO引脚的设置来覆盖BOOT_CFG1熔丝的设置,也就是可以通过配置GPIO引脚来选择启动设备,而不是BOOT_CFG1熔丝。默认BT_FUSE_SEL是等于0的,也就是默认情况下,Internal Boot模式是通过设置GPIO引脚来选择启动设备的,下图红框中的是用于覆盖BOOT_CFG1熔丝各个位所对应的GPIO引脚:
用于覆盖BOOT_CFG1各个熔丝位对应的GPIO引脚
通过前面的分析,可以知道BOOT_CFG1[7:4]是用于选择启动设备的,也就是LCD1_DATA07 ~ LCD1_DATA04这几个GPIO引脚。在正点原子的Alpha开发板上,这几个引脚被引出连接到拨码开关上,如下图所示:
在这里插入图片描述
BOOT_MODE0和BOOT_MODE1用于决定启动方式,DATA7~DATA4用于选择启动设备,还有两个引脚是用于设置必要熔丝参数的GPIO。

三、最后,我们需要了解IMX6ULL代码镜像的构成,这样才能生成在IMX6ULL上运行的代码镜像,用于下载做实验。这部分内容主要参考《IMX6ULL参考手册》的 8.7 Program image 章节部分。

IMX6ULL的代码镜像由四部分组成, Image vector table(IVT) + Boot data + Device configuration data(DCD) + User code and data ,其中,最后的 User code and data是我们编译程序源文件后生成的二进制文件,在此基础上添加上前面的三部分内容,才能作为最终的代码镜像由IMX6ULL执行,接下来详细介绍下IVT。

  • Image vector table
    IVT为boot ROM提供了一些启动时需要的重要数据内容,比如程序镜像的入口,指向DCD部分的指针,还有启动过程中需要用到的一些其它指针。对于不同的启动设备,boot ROM会从不同的位置去加载IVT,下表是不同启动设备的IVT在启动设备内的偏移地址:

不同启动设备的IVT偏移量
可以看到,对于SD卡和EMMC,IVT应位于1K字节的偏移位置处;而对于NAND,IVT应位于256字节的偏移处。因为IVT位于代码镜像的最前面,故在固化代码镜像到启动设备的时候,要固化到IVT指定的偏移位置上去,比如如果使用SD卡作为存储设备,则应将代码镜像固化到SD卡的1K偏移处,这样boot ROM才能从SD卡里正确获取到IVT,然后才能进一步获取到DCD和User code等等。所以,我们可以看到正点原子的imxdownload工具的源码里面有如下一行代码:

sprintf(cmdbuf, "sudo dd iflag=dsync oflag=dsync if=load.imx of=%s bs=512 seek=2",argv[2]);	

其中seek = 2表示输出的偏移位置,单位为obs。默认情况下,obs = 512,所以这条dd命令就是将代码镜像固化到SD卡和EMMC的1K字节偏移处。

下图是启动设备中的IVT和DDR中的IVT的对应关系:
IVT字段映射关系图
boot ROM将IVT从左边的启动设备中复制到DDR中,entry指向代码的起始地址,也就是链接地址;self指向IVT在DDR中的地址;dcd指向DCD起始地址。下图是IVT字段的说明:
IVT字段说明
我们来看下前两个字段header和entry。其中,header的格式如下图所示:
IVT header
按照描述,header的内容应为D1_0020_40。然后看正点原子的imxdownload.h有如下一个数组定义:

const int imx6_512mb_ivtdcd_table[256] = {
0X402000D1,0X87800000,0X00000000,0X877FF42C,0X877FF420,0X877FF400,0X00000000,0X00000000,
0X877FF000,0X00200000,0X00000000,0X40E801D2,0X04E401CC,0X68400C02,0XFFFFFFFF,0X6C400C02,
0XFFFFFFFF,0X70400C02,0XFFFFFFFF,0X74400C02,0XFFFFFFFF,0X78400C02,0XFFFFFFFF,0X7C400C02,
0XFFFFFFFF,0X80400C02,0XFFFFFFFF,0XB4040E02,0X00000C00,0XAC040E02,0X00000000,0X7C020E02,
0X30000000,0X50020E02,0X30000000,0X4C020E02,0X30000000,0X90040E02,0X30000000,0X88020E02,
0X30000C00,0X70020E02,0X00000000,0X60020E02,0X30000000,0X64020E02,0X30000000,0XA0040E02,
0X30000000,0X94040E02,0X00000200,0X80020E02,0X30000000,0X84020E02,0X30000000,0XB0040E02,
0X00000200,0X98040E02,0X30000000,0XA4040E02,0X30000000,0X44020E02,0X30000000,0X48020E02,
0X30000000,0X1C001B02,0X00800000,0X00081B02,0X030039A1,0X0C081B02,0X0B000300,0X3C081B02,
0X44014801,0X48081B02,0X302C4040,0X50081B02,0X343E4040,0X1C081B02,0X33333333,0X20081B02,
0X33333333,0X2C081B02,0X333333F3,0X30081B02,0X333333F3,0XC0081B02,0X09409400,0XB8081B02,
0X00080000,0X04001B02,0X2D000200,0X08001B02,0X3030331B,0X0C001B02,0XF3526B67,0X10001B02,
0X630B6DB6,0X14001B02,0XDB00FF01,0X18001B02,0X40172000,0X1C001B02,0X00800000,0X2C001B02,
0XD2260000,0X30001B02,0X23106B00,0X40001B02,0X4F000000,0X00001B02,0X00001884,0X90081B02,
0X00004000,0X1C001B02,0X32800002,0X1C001B02,0X33800000,0X1C001B02,0X31800400,0X1C001B02,
0X30802015,0X1C001B02,0X40800004,0X20001B02,0X00080000,0X18081B02,0X27020000,0X04001B02,
0X2D550200,0X04041B02,0X06100100,0X1C001B02,0X00000000,0X00000000,0X00000000,0X00000000,
0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,
0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,
0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,
0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,
0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,
0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,
0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,
0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,
0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,
0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,
0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,
0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,
0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,
0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,
0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000,0X00000000
};

这个数组是从NXP官方的代码镜像头部取出来的,但是第一个数据却是0X402000D1,我们分析的是D1_0020_40,怎么回事呢?《IMX6ULL参考手册里面》有如下一句说明:
端模式
IMX6ULL只支持小端模式,故0X402000D1的int型数据在内存里面的存储形式其实是D1002040,当我们按照char型数据去复制的时候,得到的就是D1_00_20_40,跟我们的分析其实是一致的,就是IVT的header。

然后来看entry,也就是数组中的第二个int数据,它表示程序的链接地址,表中是0X87800000,所以,我们的代码链接地址设置的都是0X87800000,比如下面的链接脚本:

SECTIONS{
	. = 0X87800000;
	.text :
	{
		obj/start.o 
		*(.text)
	}
	.rodata ALIGN(4) : {*(.rodata*)}     
	.data ALIGN(4)   : { *(.data) }    
	__bss_start = .;    
	.bss ALIGN(4)  : { *(.bss)  *(COMMON) }    
	__bss_end = .;
}

我们用 entry - self 可以得到IVT头部到Application之间的间距是3072字节,也就是3K的间距,这样有了IVT数据和Application,两者按照3K间距拼接起来就得到了最终的镜像文件。

关于IVT更详细的分析就不再去做了,因为我们可以直接使用从NXP头部提取出来的IVT数据,直接添加到到我们程序的前面就好了,如果确实有修改某些参数的必要再去细看调整。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值