以下内容源于朱有鹏嵌入式课程的学习与整理,如有侵权请告知删除。
参考博客:
S5PV210 SD卡启动 - 简书
关于存储器的相关基础知识,见博文:
外存——SD卡/iNand芯片与S5PV210的SD/MMC/iNand控制器-CSDN博客
RAM、ROM和FLASH三大类常见存储器简介
一、SteppingStone技术简介
1、SoC为何支持SD卡启动?
首先,SoC支持的启动方式越多,将来使用时就越方便,用户的可选择性就越大,SoC的适用面就越广。
其次,SD卡也有一些好处:(1)比如可以在不借用专用烧录工具(比如Jlink)的情况下对SD卡进行刷机,然后刷机后的SD卡插入卡槽,SoC即可启动。(2)比如可以用SD卡启动进行量产刷机。像X210这个开发板,刚开始时内部的iNand是空的,无法直接启动。官方刷机时,把事先准备好的SD卡插入SD卡卡槽,然后打到SD卡方式启动。因为此时iNand是空的所以第一启动失败,转而第二启动,也就是从外部SD2通道的SD卡启动了。启动后再执行刷机操作,对iNand进行刷机,这时iNand中已经有image了,所以可以直接启动了。刷机完成后将SD卡拔掉,烧机48小时无死机即可装箱待发货。
2、SD卡启动的难点在哪里?
SRAM、DDR都是总线式访问的,CPU可以直接和SRAM、DRAM打交道。
NorFlash也可以总线式访问,所以Norflash启动非常简单,可以直接启动。
SD卡需要时序访问,CPU不能直接和SD卡打交道,这是SD卡启动的难点所在。
3、SteppingStone技术简介
以前只有Norflash可以作为启动介质,比如台式机笔记本的BIOS就是Norflash做的。
后来三星在2440中使用了SteppingStone的技术,让Nandflash也可以作为启动介质。
SteppingStone 技术,翻译为启动基石技术,就是在 SoC 内部内置 4KB 的 SRAM,然后开机时 SoC 根据 OMpin 判断用户设置的启动方式,如果是 NandFlash 启动,则 SoC 的启动部分的硬件直接从外部 NandFlash 中读取开头的 4KB 到内部 SRAM 作为启动内容。
启动基石技术进一步发展,在6410芯片中得到完善,在210芯片时已经完全成熟。210 中有 96KB 的 SRAM,并且有一段 iROM 代码作为 BL0,BL0 再去启动 BL1。210中的BL0做的事情在2440中也有,只不过2440是硬件自动完成的,而且体系没有210中这么详细。
简单地理解,对于210而言,SteppingStone 技术就是“ 96KB的内部SRAM + 写死在64KB iROM 中的代码”。
4、iROM 如何读取 SD卡 的数据?
我们知道,210启动流程是这样的:210上电时,首先执行内部的iROM(也就是BL0),BL0 会根据 OMpin 来判断从哪个设备启动,如果启动设备是SD卡,则 BL0 会从SD卡读取前16KB(也就是BL1)到SRAM中去启动执行。这个过程就是SteppingStone 技术的体现。
而BL1代码是怎样的以及之后的事情,就是编程人员的事情了,SoC不用去操心。
那 iROM 内部到底是如何读取SD卡(或者说NandFlash)的呢?
在 iROM 内部,烧录了一些用来初始化SD卡的代码,以及一些用来将 SD 卡某些扇区内容拷贝到SRAM 中的代码。在 BL0 执行阶段,SoC 调用这些代码来初始化 SD 卡,并将 SD 卡中以第 1 扇区为开始位置的 16KB 内容(也就是BL1)拷贝到 SRAM 中。
(1)三星规定的SD卡各扇区内容
BL0 是写死的,它规定了将 SD卡中以第1扇区为开始位置的16KB 内容(也就是BL1)拷贝到SRAM中,所以我们在烧写SD卡时,要将BL1烧写到SD卡的第1扇区开始的地方。
换句话说,三星规定了 BL1 要烧写在SD卡的第1扇区开始的地方。
另外也规定了BL2,Kernel,FS这些内容的分布,如下所示:
附:扇区与块的概念
早期的块设备就是软盘硬盘这类磁存储设备,这种设备的存储单元不是以字节为单位,而是以扇区为单位。磁存储设备读写的最小单元就是扇区,不能只读取或写部分扇区。这个限制是磁存储设备本身物理方面的原因造成的,也成为了我们编程时必须遵守的规律。
一个扇区有许多字节。早期的磁盘扇区是512字节,后来的磁盘扇区可以做得比较大,比如1024字节、2048字节、4096字节。但是因为最早是512字节,很多操作系统和文件系统已经默认了“一个扇区512个字节”,所以后来的磁盘虽然物理上可以支持更大的扇区,但是实际上一般还是兼容512字节扇区这种操作方法。
块(Block)是指由多个字节组成的一个共同的操作单元块。
由于一个扇区可以看成一个块,所以我们把这一类设备统称为块设备,包括:磁存储设备如硬盘、软盘;光学存储设备如DVD、CD;Flash存储设备如U盘、SSD、SD卡、NandFlash、Norflash、eMMC、iNand芯片等等。Linux里有个MTD驱动,就是用来管理这类块设备的。
由于磁盘和Flash以块为单位来读写,因此下面的拷贝函数只能以块为单位读取SD卡的数据。
(2)拷贝函数简介
具体见手册《S5PV210_iROM_ApplicationNote_Preliminary_20091126.pdf》,以下内容源于手册的第14页前后。
由上表格可知,210内置有很多拷贝函数,用于从不同外部存储器中拷贝数据到内存中。
由于这里讨论的是SD卡启动方式,因此只关注将SD卡数据拷贝到内存(包括 IRAM 和SDRAM)的拷贝函数,即 0xD0037F98 这个地址处的拷贝函数 CopySDMMCtoMem。
该函数的使用声明如下:
/**
* This Function copy MMC(MoviNAND/iNand) Card Data to memory.
* Always use EPLL source clock.
* This function works at 20Mhz.
* @z : 这个参数是SD卡的通道号,在我的开发板中通道0连接的是内部的iNAND,通道2是我要用的。
* @a : param u32 StartBlkAddress : Source card(MoviNAND/iNand MMC)) Address.(It must block address.) 起始的block号。
* @b : param u16 blockSize : Number of blocks to copy. 共复制的block数目。
* @c : param u32* memoryPtr : Buffer to copy from. 复制到SDRAM中的地址。
* @e : param bool with_init : determined card initialization. 一般置0,不做处理。
* @return bool(u8) - Success or failure.
*/
/*
参数1:表示从哪个通道拷贝数据。这里可选0(表示iNand)或者2(表示SD卡)。
参数2:表示从SD卡或者iNand的第几个扇区开始拷贝数据。
参数3:表示一共拷贝拷贝多少个扇区的数据。
参数4:表示将数据拷贝到哪个地址。
参数5:一般置为0,不作处理。
*/
#define CopySDMMCtoMem(z,a,b,c,e) ( ((bool(*)(int, unsigned int, unsigned short, unsigned int*, bool)) (*((unsigned int *)0xD0037F98)))(z,a,b,c,e) )
//这个是强制类型转换--------------------------------------------- //
上面是以宏定义方式来调用拷贝函数,好处是简单方便,坏处是编译器不能帮我们做参数的静态类型检查。
分析一下上面的宏定义右边的内容,0xD0037F98是函数指针的指针,这基于两个事实,一个是0xD0037F98是iRAM上的地址,而拷贝函数的本体一定是写在iROM中的,所以(*((unsigned int *)0xD0037F98)
代表的就是指向拷贝函数的指针(即函数指针),而(bool(*)(int, unsigned int, unsigned short, unsigned int*, bool))
是函数指针类型的强制数据类型转换。
我们也可以用函数指针的方式调用拷贝函数,如下所示(具体案例见开发板——在X210开发板上进行裸机开发的细节第7点):
// 第二种方法:用函数指针方式调用
typedef bool(*pCopySDMMC2Mem)(int, unsigned int, unsigned short, unsigned int*, bool);
// 实际使用时
pCopySDMMC2Mem p1 = (pCopySDMMC2Mem)0xD0037F98;
p1(x, x, x, x, x); // 第一种调用方法
(*p1)(x, x, x, x, x); // 第二种调用方法
*p1(x, x, x, x, x); // 错误,因为p1先和()结合,而不是先和*结合。
二、S5PV210的SD卡启动方式
1、三星推荐的SD卡启动方式
如上所示,根据S5PV210的用户手册,三星推荐的启动方式如下(假定 bootloader 文件为 80KB):
(1)开机上电后BL0运行,BL0 从SD卡或者iNand 的第1扇区处,读取bootloader的前16KB(即BL1)到 SRAM 中运行。
(2)BL1运行时,会将BL2(bootloader中80-16=64KB)加载到SRAM(SRAM中第16KB处)中运行。
(3)BL2运行时会初始化DDR,并且将OS搬运到DDR,然后去执行OS,启动完成。
也就是说,三星推荐将BL1放在SRAM中运行,将BL2也放在SRAM中运行,这意味着bootloader必须小于96KB。
2、分散加载方式启动
上面所讲的三星推荐的SD卡启动方式只是一种推荐,我们不一定要遵守。
另外这种推荐方式要求bootloader必须小于96KB,不适用于bootloader大于96KB的情形。
(1)分散加载的含义
这里介绍一种名为“分散加载”的启动方式,它适用于bootloader大于16KB的所有情形。
如果文件大于16KB(只要大于16KB,哪怕是17KB,或者是700MB都是一样的),则需要将整个文件分割成两个独立的部分BL1和BL2,其中BL1小于等于16KB,BL2为任意大小,然后分别烧录到SD卡的不同扇区。
当系统上电时,BL0将BL1加载到SRAM中运行。BL1负责将DDR初始化,然后将BL2从SD卡中加载到DDR中合适的位置,然后在BL1最后(使用绝对地址)强制跳转到BL2所在的地址,转而执行BL2。这种启动方式就叫做分散加载。
(2)代码示例
完整的代码文件见链接,下面是文件组织结构:
(3)注意事项
1)BL1必须烧写在SD卡的第1扇区开始的地方(这是三星官方规定的),我们将 BL1 定为16KB大小(也就是32个Block),则BL2理论上可以从33扇区开始,但实际上会留一些空扇区作为隔离,比如可以从45扇区开始,并根据BL2的大小来分配长度。
因此write2sd文件的内容如下:
#!/bin/sh
sudo dd iflag=dsync oflag=dsync if=./BL1/BL1.bin of=/dev/sdb seek=1
sudo dd iflag=dsync oflag=dsync if=./BL2/BL2.bin of=/dev/sdb seek=45
2)BL1将DDR初始化好之后,整个DDR都可以使用了,这时在其中选择一段长度足够存储BL2的DDR空间即可,因为我们的BL1中只初始化了DDR1,地址空间范围是0x20000000~0x2FFFFFFF,这里我们选择一个合适的地址:0x23E00000。
由于我们这里选择0x23E00000,所以BL2文件夹中的链接脚本的链接地址是0x23E00000(如上图所示)。
另外BL1中调用拷贝函数拷贝BL2到DDR中时,表示将数据拷贝到哪儿的参数4要设置为0x23E00000,比如sd_relocate.c文件内容如下:
#define SD_START_BLOCK 45
#define SD_BLOCK_CNT 32
#define DDR_START_ADDR 0x23E00000
typedef unsigned int bool;
// 通道号:0,或者2
// 开始扇区号:45
// 读取扇区个数:32
// 读取后放入内存地址:0x23E00000
// with_init:0
typedef bool(*pCopySDMMC2Mem)(int, unsigned int, unsigned short, unsigned int*, bool);
typedef void (*pBL2Type)(void);
// 从SD卡第45扇区开始,复制32个扇区内容到DDR的0x23E00000,然后跳转到23E00000去执行
void copy_bl2_2_ddr(void)
{
// 第一步,读取SD卡扇区到DDR中
pCopySDMMC2Mem p1 = (pCopySDMMC2Mem)0xD0037F98);
p1(2, SD_START_BLOCK, SD_BLOCK_CNT, (unsigned int *)DDR_START_ADDR, 0); // 读取SD卡到DDR中
// 第二步,跳转到DDR中的BL2去执行
pBL2Type p2 = (pBL2Type)DDR_START_ADDR;
p2();
}
3)BL1需要这些任务:关看门狗、设置栈、开iCache、初始化DDR、从SD卡复制BL2到DDR中特定位置,并跳转到BL2的开始位置。
比如BL1文件夹中start.S文件内容如下:
#define WTCON 0xE2700000
#define SVC_STACK 0xd0037d80
.global _start // 把_start链接属性改为外部,这样其他文件就可以看见_start了
_start:
// 第1步:关看门狗(向WTCON的bit5写入0即可)
ldr r0, =WTCON
ldr r1, =0x0
str r1, [r0]
// 第2步:设置SVC栈
ldr sp, =SVC_STACK
// 第3步:开/关icache
mrc p15,0,r0,c1,c0,0; // 读出cp15的c1到r0中
//bic r0, r0, #(1<<12) // bit12 置0 关icache
orr r0, r0, #(1<<12) // bit12 置1 开icache
mcr p15,0,r0,c1,c0,0;
// 第4步:初始化ddr
bl sdram_asm_init
// 第5步:重定位,从SD卡第45扇区开始,复制32个扇区内容到DDR的0x23E00000
bl copy_bl2_2_ddr
// 汇编最后的这个死循环不能丢
b .
4)BL1和BL2其实是2个独立的程序,链接时也是独立分开链接的(从代码可知 BL2的是 0x23E00000,BL1的是0xD0020010),因此不能使用ldr pc, =main这种通过链接地址的方式跳转到BL2。但是可以使用地址(函数指针)进行强制跳转,因为BL1知道BL2将被加载到哪个地址,所以BL1最后直接去执行这个地址即可(见 copy_bl2_2_ddr 函数中的最后两行代码)。
(4)分散加载方式的缺点
第一,代码完全分2部分,完全独立,代码编写和组织上麻烦;
第二,无法让工程项目兼容SD卡启动和Nand启动、NorFlash启动等各种启动方式。
3、uboot的启动方式
uboot文件大小一般超过200KB,不适合用三星推荐的启动方式;另外如果采用分散加载,则又无法兼容SD卡启动、Nand启动、NorFlash启动等启动方式,于是uboot采取了另外一种启动流程:
1)上电后BL0运行,BL0从SD卡扇区1开始读取16KB内容(即BL1)到SRAM中。
2)BL1运行时会初始化DDR,然后从SD卡的49扇区开始拷贝整个uboot(BL1+BL2)到DDR中,然后利用 ldr pc, =main 这种远跳转方式,从SRAM中运行的BL1跳转到DDR中运行的BL2。
要执行上面1)2)流程,需要事先使用sd_fusing.sh脚本将uboot烧写到SD卡中,该文件部分内容如下所示。可见它把uboot前8KB内容烧写到了SD卡的扇区1开始的地方,把整个uboot烧写到了第49扇区开始的地方。虽然这个脚本实际截取8KB的内容烧写至SD卡第1扇区,但我猜想BL0还是会从SD卡扇区1开始读取16KB内容。
#<BL1 fusing>
bl1_position=1
uboot_position=49
echo "BL1 fusing"
./mkbl1 ../u-boot.bin SD-bl1-8k.bin 8192
dd iflag=dsync oflag=dsync if=SD-bl1-8k.bin of=$1 seek=$bl1_position
rm SD-bl1-8k.bin
####################################
#<u-boot fusing>
echo "u-boot fusing"
dd iflag=dsync oflag=dsync if=../u-boot.bin of=$1 seek=$uboot_position
3)然后在DDR中继续执行BL2。
uboot的这种启动方式,和分散加载启动方式一样,把代码分为了BL1和BL2两部分;但uboot的这种启动方法,把整个uboot当做一个整体加载到DDR中的链接地址处,因此可以在BL1末尾处使用“ldr pc, =main ”这种远跳转方式,从BL1跳转到BL2。
注意,这里的“uboot的启动方式”只是一种思路与方法,这个文件不一定就是uboot,但课程中没有提供案例代码。