以下内容是学习裸机开发过程中的一些细节内容的记录。
1、汇编语言函数细节
用汇编写的函数,末尾应该添加mov pc,lr语句。
2、裸机代码相关文件
3、关于链接地址
4、关于重定位的理解
(1)在sram内部重定位
这是在sram内部重定位,因此不需要初始化DDR。
根据S5PV210地址映射图,链接脚本(略)指定的链接地址0xd0024000是在SRAM中。
/** 描述: 演示重定位(在SRAM内部重定位)*/#define WTCON 0xE2700000 #define SVC_STACK 0xd0037d80.global _start // 把_start链接属性改为外部,这样其他文件就可以看见_start了_start:// 第1步:关看门狗(向WTCON的bit5写入0即可)ldr r0, =WTCONldr r1, =0x0str r1, [r0]// 第2步:设置SVC栈ldr sp, =SVC_STACK// 第3步:开/关icachemrc p15,0,r0,c1,c0,0; // 读出cp15的c1到r0中//bic r0, r0, #(1<<12) // bit12 置0 关icacheorr r0, r0, #(1<<12) // bit12 置1 开icachemcr p15,0,r0,c1,c0,0;// 第4步:重定位。(这里的代码细节说明adr是与运行相关的,ldr是与链接相关的。)adr r0, _start // adr指令用于加载_start当前运行地址 // adr加载时就叫短加载 ldr r1, =_start // ldr指令用于加载_start的链接地址:0xd0024000 // ldr加载时如果目标寄存器是pc就叫长跳转,如果目标寄存器是r1等就叫长加载// bss段的起始地址ldr r2, =bss_start // 就是我们重定位代码的结束地址,重定位只需重定位代码段和数据段即可cmp r0, r1 // 比较_start的运行时地址和链接地址是否相等beq clean_bss // 如果相等说明不需要重定位,所以跳过copy_loop,直接到clean_bss// 如果不相等说明需要重定位,那么直接执行下面的copy_loop进行重定位// 重定位完成后继续执行clean_bss。// 用汇编来实现的一个while循环 copy_loop:ldr r3, [r0], #4 // 源str r3, [r1], #4 // 目的 这两句代码就完成了4个字节内容的拷贝cmp r1, r2 // r1和r2都是用ldr加载的,都是链接地址,所以r1不断+4总能等于r2bne copy_loop// 清bss段,其实就是在链接地址处把bss段全部清零 clean_bss:ldr r0, =bss_start ldr r1, =bss_endcmp r0, r1 // 如果r0等于r1,说明bss段为空(即不存在bss段),直接下去beq run_on_dram // 清除bss完之后的地址mov r2, #0clear_loop:str r2, [r0], #4 // 先将r2中的值放入r0所指向的内存地址(r0中的值作为内存地址),cmp r0, r1 // 然后r0 = r0 + 4bne clear_looprun_on_dram: // 长跳转到led_blink开始第二阶段ldr pc, =led_blink // ldr指令实现长跳转//bl led_blink // bl指令实现短跳转// 汇编最后的这个死循环不能丢b .
(2)重定位至DDR
这里重定位至DDR,因此需要初始化DDR。
根据S5PV210地址映射图,链接脚本(略)指定的链接地址0x2000 0000 在DDR中。
/** 文件名: led.s * 作者: 朱老师* 描述: 演示重定位*/#define WTCON 0xE2700000 #define SVC_STACK 0xd0037d80.global _start _start://..............// 第4步:初始化ddrbl sdram_asm_init //此函数末尾记得添加mov pc,lr// 第5步:重定位,后面的代码和之前的完全一样。故不写。
(3)总结
对比可知,两者没有什么区别,重定位至DDR只是多了一个内存初始化操作而已。
5、SRAM的地址0xd002_0010映射到0x0000_0000地址
Makefile用 -Ttext 0x0 指定链接地址为0x0。这意味着我们认为这个程序将来会放在0x0地址中运行,但实际上运行时的地址是0xd0020010(我们用dnw下载时指定的下载地址)。
这两个地址看似不同但实际相同,因为S5PV210内部把SRAM的地址0xd002_0010映射到地址0x0000_0000。把链接地址设为0x0000_0000,就等价于链接地址是0xd002_0010。
BL0执行完后会自动跳转到0xd0020010这个地址开始运行,这个地址是CPU设计决定的。
6、S5PV210内置的拷贝函数
关注下0xD0037F98这个地址,它是S5PV210内置的拷贝函数的入口地址。该函数的参数之一是从哪个通道拷贝(SD0还是SD2,对应着inand和SD卡),参数之二是从哪个扇区开始拷贝,参数之三是拷贝至哪里,参数之四是拷贝多少。
BL0内部利用这个函数,将16KB(由参数之四决定)的BL1,从SD卡或者inand(由参数之一决定)的第一个扇区(由参数之二决定),拷贝到 SRAM 的 0xd002_0010 地址(由参数之三决定)。由于BL0是写死在IROM中的,我们唯一能做的就是通过启动介质拨码开关来决定参数之一,参数之二、三、四是固定的。
BL1中如果需要拷贝SD卡中的某些内容,则还是使用S5PV210内置的拷贝函数,只不过因为BL1是我们编写的,我们可以自由设置参数一、二、三、四。
比如将一个大文件分成两个文件BL1和BL2,先将BL1烧写至SD卡第1扇区,BL2烧写至SD卡合适的扇区位置即可(假如烧录至第49扇区)。
上电后IROM中的BL0将利用拷贝函数,把BL1从SD卡第1扇区拷贝到 SRAM的 0xd002_0010地址。BL1在执行时也利用拷贝函数,将BL2从SD卡的第49扇区拷贝到一个合适的地址(这个地址可能位于IRAM或者DDR中,它是由链接脚本规定的,我们从链接脚本得知这个地址,然后作为参数传入拷贝函数),最后BL1代码末尾处执行跳转语句,跳转到这个地址,就可以接着执行BL2。
7、验证6中所讲述的内容
(1)BL1的相关内容
首先,BL1的链接脚本的链接地址是0xd0020010。这就很合理,这个地址就是BL1该呆在的地方,因为CPU设计时规定的一开始运行的地址就是0xd0020010。
SECTIONS {. = 0xd0020010;.text : {start.osdram_init.o* (.text)}.data : {* (.data)}bss_start = .; .bss : {* (.bss)}bss_end = .; }
其次,BL1的Makefile中,需要把BL1做16字节填充的,这也很合理。
接着,start.S中初始化DDR后,利用拷贝函数把存储在SD卡中的BL2,复制到DDR的某个位置,并跳转到该位置执行BL2。
#define WTCON 0xE2700000 #define SVC_STACK 0xd0037d80.global _start // 把_start链接属性改为外部,这样其他文件就可以看见_start了 _start:// 第1步:关看门狗(向WTCON的bit5写入0即可)ldr r0, =WTCONldr r1, =0x0str r1, [r0]// 第2步:设置SVC栈ldr sp, =SVC_STACK// 第3步:开/关icachemrc p15,0,r0,c1,c0,0; // 读出cp15的c1到r0中//bic r0, r0, #(1<<12) // bit12 置0 关icacheorr r0, r0, #(1<<12) // bit12 置1 开icachemcr p15,0,r0,c1,c0,0;// 第4步:初始化ddrbl sdram_asm_init// 第5步:重定位,从SD卡第45扇区开始,复制32个扇区内容到DDR的0x23E00000bl copy_bl2_2_ddr// 汇编最后的这个死循环不能丢b .
#define SD_START_BLOCK 45 #define SD_BLOCK_CNT 32 #define DDR_START_ADDR 0x23E00000 //这地址在DDR中,不在IRAM中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);//给(指向拷贝函数的)函数指针传入参数,开始读取SD卡到DDR中p1(2, SD_START_BLOCK, SD_BLOCK_CNT, (unsigned int *)DDR_START_ADDR, 0); // 第二步,跳转到DDR中的BL2去执行//定义一个函数指针并指向BL2的下载地址pBL2Type p2 = (pBL2Type)DDR_START_ADDR;//通过函数指针这种方式跳转到DDR中的BL2的下载地址,去执行BL2数p2(); }
(2)BL2的相关内容
首先,链接地址应该由BL1的拷贝函数将BL2拷贝到哪里决定。由BL1代码可知BL2被拷贝到了0x23E00000,那么BL2的链接地址应该是0x23E00000。
另外,BL2有main函数文件、start.S文件等文件,怎么知道先执行start.S文件呢?这是由链接脚本中的.o文件的顺序决定的。
SECTIONS {. = 0x23E00000;.text : {start.o* (.text)}.data : {* (.data)}bss_start = .; .bss : {* (.bss)}bss_end = .; }
其次,BL2的makefile中不会再添加16字节填充的操作,经查验果真如此。
start.S文件内容如下,可知接下来会到main函数的文件中执行main函数。
#define WTCON 0xE2700000 #define SVC_STACK 0xd0037d80.global _start _start:ldr pc, =main // ldr指令实现长跳转// 汇编最后的这个死循环不能丢b .
(3)总结
BL1所使用的拷贝函数还是BL0所使用的拷贝函数,即S5PV210内置的拷贝函数。
BL2的入口地址,是BL2的链接脚本指定链接地址。该地址由BL1将BL2拷贝到哪里决定。
BL2链接脚本中.o的顺序,决定了BL2众多的程序文件中,先执行哪些文件。
8、将数据烧写至SD卡的方式
方式1:在linux中利用write2sd文件
write2sd文件内容如下,可知BL1和BL2分别烧录至第1扇区、第45扇区开始的地方。
#!/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:利用九鼎提供的烧写软件
它只能将一个文件(uboot.bin或者裸机镜像文件)烧写至SD卡的第1扇区开始的地方。
注意,它只能烧写一个文件,而且必须烧写至SD卡第1扇区。
方式3:利用三星提供的sd_fusing文件夹
具体见利用三星提供的sd_fusing.sh将uboot烧写到SD卡。
注意,这个文件夹是将uboot.bin烧写至sd的方法,特指烧写uboot镜像,而非其他的?错误的认识,可以烧写其他镜像,但得16字节的校验头。
补充说明
其实方式3最后也利用方式1作为方式3的最后一个步骤。方式3前面还包括将sd卡分区、截取uboot.bin前8k作为BL1(而整个uboot.bin作为BL2)等步骤。将sd卡分区的意义何在呢?sd卡不是本身就已经分区了吗?(待解决)
9、裸机代码中的前几个固定步骤
裸机代码的前几个步骤比较固定,都是一些初始化操作,而且有些操作在BL0中已经完成,这里重新再设置一遍也不会出错。另外,ddr的初始化和重定位这两个步骤看情况是否需要。
(0)开发板制锁
如果不进行这个步骤,得一直按着POWER键。
// 第0步:开发板置锁ldr r0, =0xE010E81Cldr r1, [r0]ldr r2, =0x301orr r1, r1, r2str r1, [r0]
(1)关看门狗
开启看门狗是为了防止机器故障时自动复位。如果开启看门狗后,没有及时去喂狗,则系统会自动复位。没有操作系统前,喂狗这个操作需要编写代码完成,因此为了减少编码工作,这里选择直接把看门狗关闭。(其实BL0已经关了看门狗,这里重新设置一遍。)
// WTCON(0xE2700000),其中bit5是看门狗的开关:0代表关,1代表开// 第1步:关看门狗(向WTCON的bit5写入0即可)ldr r0, =WTCONldr r1, =0x0str r1, [r0]
(2)初始化时钟
这一部分的内容,见博客:S5PV210的时钟系统_天糊土的博客-CSDN博客
// 第2步:初始化时钟bl clock_init
(3)设置SVC栈
C语言的运行需要一定的条件,这些条件由汇编来提供。C语言运行时主要是需要栈。比如C语言中的局部变量都是用栈来实现的。如果汇编部分没有给C部分设置合理的栈地址,那么C代码中定义的局部变量就会落空,整个程序就死掉了。
我们现在要设置栈,不可能也没有必要去设置所有的栈,我们先要设置自己的模式,然后设置自己的模式下的栈到合理合法的位置即可。
我们如何访问SVC模式下的SP呢?很简单,先把模式设置为SVC,再直接操作SP。但是因为我们复位后就已经是SVC模式了,所以直接设置SP即可。
由于栈必须是当前一段可用的内存(这段内存必须已经初始化,而且这段内存只会被我们用作栈,不会被其他程序征用),当前CPU刚启动时,外部的SDRAM尚未初始化,目前可用的内存只有内部的SRAM(因为它不需初始化即可使用)。因此只能在SRAM中找一段内存来作为SVC的栈。结合iROM_application_note中的memory map,可知SVC栈应该设置为0xd0037D80。
// 第3步:设置SVC栈ldr sp, =SVC_STACK //#define SVC_STACK 0xd0037d80
(4)开关icache
iROM中的BL0已经打开了icache,我们这里重新设置一遍也没有错。
// 第4步:开/关icachemrc p15,0,r0,c1,c0,0; // 读出cp15的c1到r0中//bic r0, r0, #(1<<12) // bit12 置0 关icacheorr r0, r0, #(1<<12) // bit12 置1 开icachemcr p15,0,r0,c1,c0,0;
寄存器和SDRAM之间的读取速度差异太大,SDRAM的读取速度远不能满足寄存器的需要,没有cache的话,会拉低系统的整体速度。210内部有32KB icache和32kb dcache,icache是用来缓存指令的;dcache是用来缓存数据的。
(5)DDR的初始化(可能需要)
见S5PV210——SDRAM的初始化_天糊土的博客-CSDN博客
(6)代码的重定位(可能需要)
见重定位的简介与操作(涉及位置无关码)_天糊土的博客-CSDN博客