【龙芯1c库】上电初始化汇编代码start.S注解(pmon类似)

参考pmon源码,将start.S、Makefile和链接脚本移植到裸机程序,实现纯粹的真正的裸机程序。这样就不再需要pmon,上电后直接运行裸机程序。

本文涉及的异常和地址空间的相关知识,需要结合《龙芯1c的芯片手册》、《see mips run》和《北京龙芯的龙芯1c开发板手册》。这几个文档都已经放到龙芯1c库的git上了,最新最完整的代码也请移步到git查看。龙芯1c库的git地址是https://gitee.com/caogos/OpenLoongsonLib1c

背景知识

使用mipsel-linux-objdump反汇编

为什么需要使用反汇编

为什么这里首先讨论使用objdump反汇编呢?可能大家习惯了仿真,单步调试。很少单独使用反汇编。可是目前龙芯1c是不能仿真和单步调试的(至少目前我不知道),所以手动反汇编就有必要了,通过查看反汇编,可以很清楚的查看程序的运行流程,可以看到上电后CPU运行的第一条汇编指令是什么。
举个例子吧,在调试上电初始化这部分汇编程序的过程中,发现汇编源码和pmon中的差不多,可是串口没有打印helloworld。经过一番排除,最后用objdump反汇编发现,链接后执行的第一条语句不是汇编,而是c程序。原因是ld链接时,c文件放在了依赖文件列表的前面,改为汇编文件在前面,就可以了。

怎样反汇编

为了能在反汇编的结果中同步显示源码,在编译时,需要增加选项” -g ”,
例如“make cfg all tgt=rom DEBUG=-g”,
使用mipsel-linux-objdump反汇编,
例如
root@ubuntu:/home/develop/loongson1-pmon-master/Targets/LS1X/compile/ls1c# mipsel-linux-objdump -S pmon.gdb > /mnt/hgfs/VmShare/pmon-gdb-objdump.S
比如,pmon反汇编后,得到如下内容

pmon.gdb:     file format elf32-tradlittlemips


Disassembly of section .text:

80010000 <_ftext>:
80010000:	40806000 	mtc0	zero,$12
80010004:	40806800 	mtc0	zero,$13
80010008:	3c080040 	lui	t0,0x40
8001000c:	40886000 	mtc0	t0,$12
80010010:	3c1d8001 	lui	sp,0x8001
80010014:	27bdc000 	addiu	sp,sp,-16384
80010018:	3c1c800c 	lui	gp,0x800c
8001001c:	279c6cf0 	addiu	gp,gp,27888
80010020:	3c08bfe8 	lui	t0,0xbfe8
80010024:	24090017 	li	t1,23
80010028:	a1090004 	sb	t1,4(t0)
8001002c:	24090005 	li	t1,5
80010030:	a1090006 	sb	t1,6(t0)
80010034:	3c04bfd0 	lui	a0,0xbfd0
80010038:	348411c0 	ori	a0,a0,0x11c0
8001003c:	8c850040 	lw	a1,64(a0)
80010040:	34a50001 	ori	a1,a1,0x1
80010044:	ac850040 	sw	a1,64(a0)
80010048:	041101b9 	bal	80010730 <locate>
8001004c:	00000000 	nop
	...

反汇编结果是如何与源码一一对应的

这里主要讨论一下,反汇编得到的汇编代码,与start.S中的汇编代码的对应关系

左边为start.S中的汇编源码,右边为反汇编的结果。图中用线将其一一对应了。

异常入口点(CP0的SR寄存器的BEV)

上电运行的第一条指令在什么地方,地址是多少

mips系列cpu的异常和中断是两个不同的概念,中断一般指外设中断,所有外设中断共用一个异常入口,即外设中断是一种特定类型的异常。《龙芯1c的芯片手册》中目前几乎没怎么讲这部分内容,而《see mips run》中却讲得很详细,专门用一章来讲异常。
本文不是要讨论上电初始化那部分汇编代码吗?怎么这里研究异常呢?在mips系列cpu上,上电(冷复位)也属于一种异常,异常入口固定为ROM入口点0xBFC00000,如下图

《龙芯1c的芯片手册》中也有对应描述,如下

图中,明确说了,根据系统启动方式将内存地址0xBFC00 0000 -- )XBFCF FFFF映射到SPI或NAND,即从地址0xBFC0 0000处取出的指令,就是SPI或NAND的地址0处的指令。也是上电后运行的第一条指令。

刚上电时,把BEV置1

截图中,讲清楚了,在cpu刚上电时,cache还未初始化之前,只能使用不经过cache的kseg1。

初始化完成后,把BEV清零

(内存,cache等)初始化完成后,就可以使用cache了,通过把协处理器0的SR寄存器中的BEV清零,使所有异常入口从ROM入口点(0xBFC0 0000)改为RAM入口点(BASE + 0x180),其中BASE为寄存器EBase的值。《see mips run》中的描述为

Pmon中对应的代码为

地址空间的划分

see mips run中关于程序地址空间的划分情况

see mips run中kseg0和kseg1的详细描述

因为kseg0(0x8000 0000 - 0x9fff ffff)和kseg1(0xa000 0000 - 0xBfff ffff)实际上是映射到低端同一块512M的物理地址上,只是kseg1不需要cache,而kseg0必须等cache初始化后才能使用。
所以,把固件拷贝到kseg1上,等等于拷贝到了kseg0上。

龙芯1c芯片手册中关于地址空间分配的描述

链接时的起始地址(代码段的首地址)

标号start的值为0x80010000

代码段是从0x8001 0000处开始的,初始化完成后,异常入口改为RAM入口,即BASE+0x180=0x8000 0000+0x180

栈空间在什么地方

在代码段之前有0x4000大小的栈空间,代码如下

把固件本身从ROM(SPI nor flash)拷贝到内存RAM中

上电时,CPU映射了1M的Boot内存到SPI(NOR FLASH)或NAND(FLASH),可是这部分内存是只读的,并且不经过cache。当内存和cache初始化完成后,需要将ROM(位于kseg1地址段)上的代码拷贝到kseg0地址段内,kseg0上的内存可写,同时经过cache,还有利于提高性能。
链接时指定的起始地址为0x80010000,而上电后运行的起始地址是0xBFC00000,所以在拷贝时需要做地址修正。

汇编代码(start.S)详解

主要参考《北京龙芯的1c开发板用户手册》v0.60,其中对start.S的注解非常好。如下

参考这个文档,我讲文档中的讲解以注释的形式添加到了代码中,并增加了一些我的理解。下面以start.S的程序执行流程来讲解,可能和北京龙芯的1c开发板手册中讲解的顺序有点不一样。
其中,pmon中汇编代码初始化后,跳转到函数initmips,而1c库中是直接跳转到main函数。Pmon中有压缩解压固件的功能,我认为裸机程序中不需要这个功能,所以1c库中没有移植这个功能,如果有需要的自行移植。开发板手册中的描述如下

初始化基础寄存器

主要是初始化协处理器0的STATUS寄存器和CAUSE寄存器、sp寄存器和gp寄存器。代码如下

	.set	noreorder
	.set	mips32
	.globl	_start
	.globl	start
	.globl	__main
_start:
start:
    /*
    设置栈指针为start地址之前0x4000的位置
    mips架构堆栈寄存器实际只是通用寄存器,并没有规定生长方向,但软件约定“堆栈指针向下生长”
    */
	.globl	stack
stack = start - 0x4000		/* Place PMON stack below PMON start in RAM */

/* NOTE!! Not more that 16 instructions here!!! Right now it's FULL! */
/*
    根据“《see mips run》第5.3节——异常向量:异常处理开始的地方”中的描述,
    异常向量间的距离为128字节(0x80),可容纳32条指令(每条指令4字节)。
    而这里原来的英文注释为“ Not more that 16 instructions here!!!”,即最大16条指令
    我认为需要进一步斟酌,到底是最大16字节,还是32字节
*/
	mtc0	zero, COP_0_STATUS_REG  // 清零cp0 status寄存器
	mtc0	zero, COP_0_CAUSE_REG   // 清零cp0 cause寄存器

    /*
    设置启动异常向量入口地址为ROM地址(0xbfc00000)
    将寄存器cp0 status的BEV置1,使CPU采用ROM(kseg1)空间的异常入口点
    */
	li	t0, SR_BOOT_EXC_VEC	/* Exception to Boostrap Location */
	mtc0	t0, COP_0_STATUS_REG
	
	la	sp, stack       // 加载栈地址
	la	gp, _gp         // 加载全局指针gp

如果是SPI启动,设置SPI控制寄存器

spi初始化代码如下

	/* initialize spi */
	li  t0, 0xbfe80000      //地址0xbfe80000为SPI0的寄存器基地址
	li  t1, 0x17	        // div 4, fast_read + burst_en + memory_en double I/O 模式 部分SPI flash可能不支持
	sb  t1, 0x4(t0)	        // 设置寄存器sfc_param
	li  t1, 0x05
	sb  t1, 0x6(t0)         // 设置寄存器sfc_timing
其实,我认为这步或许可以省略掉,因为cpu上电后,能从spi nor flash执行代码,说明上电后默认就能正常读SPI NOR FLASH,没必要再次初始化。

设置PLL和各级时钟(包括CPU和SDRAM的时钟)

/* config pll div for cpu and sdram */
#define PLL_MULT            (0x54)  // 晶振为24Mhz时,PLL=504Mhz
#define SDRAM_DIV           (0)     // SDRAM为CPU的2分频
#define CPU_DIV             (2)     // CPU为PLL的2分频

	li	t0, 0xbfe78030          // 地址0xbfe78030为PLL/SDRAM频率配置寄存器的地址
	/* 设置PLL倍频 及SDRAM分频 */
	li	t2, (0x80000008 | (PLL_MULT << 8) | (0x3 << 2) | SDRAM_DIV)
	/* 设置CPU分频 */
	li	t3, (0x00008003 | (CPU_DIV << 8))
	/* 注意:首先需要把分频使能位清零 */
	li	t1, 0x2
	sw	t1, 0x4(t0)         // 清零CPU_DIV_VALID,即disable
	sw	t2, 0x0(t0)         // 写寄存器START_FREQ
	sw	t3, 0x4(t0)         // 写寄存器CLK_DIV_PARAM
	DELAY(2000)

初始化调试串口

等调试串口初始化完成后,就可以打印调试信息了。
可是为什么没有把串口初始化再提前一点呢?因为串口的波特率计算需要用到时钟,所以把串口初始化放在了PLL初始化之后。这个位置(初始化串口)已经是非常靠前了。

调用汇编函数initserial

	/* initialize UART */
	li a0, 0
	bal	initserial          // 初始化串口
	nop
	PRINTSTR("\r\asm uart2 init ok!\r\n");  // 打印一条提示信息,表示串口初始化成功了

汇编函数initserial的实现

除了计算波特率稍微复杂点,串口初始化函数其实很简单,只需要设置几个串口控制寄存器和引脚复用就可以了。
其实计算波特率本身并不复杂,只是下面的代码是使用汇编函数实现的,并且用汇编函数读取PLL频率和各个分频系数,这样代码的行数就看起来就显得有点多,其实并不复杂,我已经添加了注释。计算波特率的那二三十行代码可以用一条汇编语句替代,如下
li    v1, ((APB_CLK / 4) * (PLL_MULT / CPU_DIV)) / (16*CONS_BAUD) / 2
这行代码默认被注释了。
完整的汇编函数initserial的源码如下

LEAF(initserial)
	move AT,ra                      // 把返回地址暂时保存在寄存器AT中
	
	la	v0, UART_BASE_ADDR          // 加载串口基地址到寄存器v0中
#ifdef	HAVE_MUT_COM
	bal	1f
	nop

	li	a0, 0
	la	v0, COM3_BASE_ADDR
	bal	1f
	nop

	jr	AT
	nop
#endif
1:
	li	v1, FIFO_ENABLE|FIFO_RCV_RST|FIFO_XMT_RST|FIFO_TRIGGER_4    // 清空Rx,Tx的FIFO,申请中断的trigger为4字节
	sb	v1, LS1C_UART_FCR_OFFSET(v0)        // 写FIFO控制寄存器(FCR)
	li	v1, CFCR_DLAB                       // 访问操作分频锁存器
	sb	v1, LS1C_UART_LCR_OFFSET(v0)        // 写线路控制寄存器(LCR)

	/* uart3 config mux 默认第一复用 */
#if (UART_BASE_ADDR == 0xbfe4c000)
	li		a0, 0xbfd011c4
//	lw		a1, 0x00(a0)
//	and		a1, 0xfffffff9
//	sw		a1, 0x00(a0)
	lw		a1, 0x10(a0)
	ori		a1, 0x06
	sw		a1, 0x10(a0)
//	lw		a1, 0x20(a0)
//	and		a1, 0xfffffff9
//	sw		a1, 0x20(a0)
//	lw		a1, 0x30(a0)
//	and		a1, 0xfffffff9
//	sw		a1, 0x30(a0)

/*	li		a0, 0xbfd011f0
	lw		a1, 0x00(a0)
	ori		a1, 0x03
	sw		a1, 0x00(a0)*/
#elif (UART_BASE_ADDR == 0xbfe48000)
	/* UART2 使用gpio36,gpio37的第二复用*/
	li		a0, LS1C_CBUS_FIRST1        // 加载复用寄存器CBUS_FIRST1的地址到寄存器a0
	lw		a1, 0x10(a0)                // 加载复用寄存器CBUS_SECOND1的值到寄存器a1
	ori		a1, 0x30                    // a1 |= 0x30,即GPIO36,GPIO37配置为第二复用
	sw		a1, 0x10(a0)                // 将寄存器a1的值写入寄存器CBUS_SECOND1中
#elif (UART_BASE_ADDR == 0xbfe44000)
	/* UART1 */
	li		a0, 0xbfd011f0
	lw		a1, 0x00(a0)
	ori		a1, 0x0c
	sw		a1, 0x00(a0)
#endif

    // 设置波特率
    // 计算pll频率
	li		a0, 0xbfe78030      // 0xbfe78030为PLL/SDRAM频率配置寄存器START_FREQ,将地址0xbfe78030加载到寄存器a0中
	lw		a1, 0(a0)           // 加载寄存器START_FREQ的值到寄存器a1中
	srl		a1, 8               // a1 >>= 8
	andi	a1, 0xff            // a1 &= 0xff,即a1=PLL_MULT(PLL倍频系数)
	li		a2, APB_CLK         // a2 = APB_CLK = 24Mhz(外部晶振频率)
	srl		a2, 2			    // a2 = a2 >> 2 = APB_CLK/4
	multu	a1, a2              // hilo = a1 * a2 = PLL_MULT * APB_CLK /4
	mflo	v1				    // v1 = lo,将a1 * a2的结果的低32位放到v1中,即v1为pll频率
	// 判断是否对时钟分频
	lw		a1, 4(a0)           // 加载寄存器CLK_DIV_PARAM的值到寄存器a1中
	andi	a2, a1, DIV_CPU_SEL // a2 = a1 & DIV_CPU_SEL,即读取位CPU_SEL的值,如果=1,则分频时钟;如果=0,则晶振输入时钟(bypass模式)
	bnez	a2, 1f              //if (a2 != 0) 则跳转到下一个标号1处
	nop
	li		v1, APB_CLK         // v1 = APB_CLK,即cpu时钟为晶振频率
	b		3f
	nop
1:  // 判断cpu分频系数是否有效
	andi	a2, a1, DIV_CPU_EN  // a2 = a1 & DIV_CPU_EN,即读取位CPU_DIV_EN的值,判断配置参数是否有效
	bnez	a2, 2f              // if (a2 != 0) 则跳转到下一个标号2处
	nop
	srl		v1, 1			    //v1 >>= 1,即v1 = APB_CLK/4 * PLL_MULT     / 2
	b		3f
	nop
2:  // 计算cpu频率
	andi	a1, DIV_CPU         // a1 &= DIV_CPU
	srl		a1, DIV_CPU_SHIFT   // a1 >>= DIV_CPU_SHIFT,即a1为cpu分频系数
	divu	v1, a1              // lo = v1 / a1; hi = v1 % a1
	mflo	v1				    // v1 = lo,即v1为cpu频率
3:
//	li	v1, ((APB_CLK / 4) * (PLL_MULT / CPU_DIV)) / (16*CONS_BAUD) / 2
	li		a1, 16*CONS_BAUD    // a1 = 16 * 波特率
	divu	v1, v1, a1          // v1 = v1 / a1
	srl     v1, 1               // v1 >>= 1,即v1 /= 2
	sb	v1, LS1C_UART_LSB_OFFSET(v0)    // 将低8位写入分频锁存器1
	srl	v1, 8                           // v1 >>= 8
	sb	v1, LS1C_UART_MSB_OFFSET(v0)    // 将低8位写入分频锁存器2
	
	li	v1, CFCR_8BITS                  // 8个数据位,1个停止位,无校验
	sb	v1, LS1C_UART_LCR_OFFSET(v0)    // 写线路控制寄存器(LCR)
//	li	v1, MCR_DTR|MCR_RTS             // 使能DTR和RTS
//	sb	v1, LS1C_UART_MCR_OFFSET(v0)    // 写MODEM控制寄存器(MCR)
	li	v1, 0x0                         // 关闭所有中断
	sb	v1, LS1C_UART_IER_OFFSET(v0)    // 写中断使能控制寄存器(IER)
	j   ra
	nop
END(initserial)

其中使用了两个宏

// 配置调试串口
#define UART_BASE_ADDR      LS1C_UART2_BASE     // 串口2作为调试串口
#define CONS_BAUD           B115200             // 波特率115200

配置内存SDRAM

配置SDRAM的代码不多,只需要设置一个64位的寄存器。因为寄存器长度为64位,一条汇编只能修改其中的(高或低)32位,为了保证正确设置,要求写3次寄存器,最后一次才使能。代码如下

	/* 配置内存 */
	li msize, MEM_SIZE    
#if !defined(NAND_BOOT_EN)

    /* 
       手册建议,先写寄存器SD_CONFIG[31:0],然后再写寄存器的SD_CONFIG[63:32],
       即先写低32位,再写高32位。
       写三次寄存器,最后一次将最高位置一,即使能
    */

    // 写第一次
	li  	t1, 0xbfd00410      // 寄存器SD_CONFIG[31:0]的地址为0xbfd00410
	li		a1, SD_PARA0        // 宏SD_PARA0在sdram_cfg.S中定义的
	sw		a1, 0x0(t1)         // 将宏SD_PARA0的值写入寄存器SD_CONFIG[31:0]
	li		a1, SD_PARA1
	sw		a1, 0x4(t1)         // 同理,将宏SD_PARA1的值写入寄存器SD_CONFIG[63:32]

	// 写第二次
	li		a1, SD_PARA0
	sw		a1, 0x0(t1)
	li		a1, SD_PARA1
	sw		a1, 0x4(t1)

    // 写第三次	
	li		a1, SD_PARA0
	sw		a1, 0x0(t1)
	li		a1, SD_PARA1_EN     // 使能
	sw		a1, 0x4(t1)
//	DELAY(100)
#endif
其中使用的宏如下

// 配置内存大小
#define MEM_SIZE    (0x02000000)        // 32MByte


//#define	SD_FREQ	(6 * PLL_M) / (2 * SDRAM_PARAM_DIV_NUM)
#define	SD_FREQ	(((APB_CLK / 4) * (PLL_MULT / CPU_DIV)) / SDRAM_PARAM_DIV_NUM)

/*
    以型号为EM63A165TS的SDRAM为例,
    物理参数为,
    容量:32MB
    行宽:13位,即2的13次方,即8K
    列宽:9位,即2的9次方,即512
    位宽:16位

    所以,
    颗粒的行数=ROW_8K
    颗粒的列数=COL_512
    颗粒的位宽=WIDTH_16

    再结合宏SD_PARA0和芯片手册中寄存器SD_CONFIG,相信一看就能明白
 */
 
/* 颗粒行数 */
#define	ROW_1K		0x7
#define	ROW_2K		0x0
#define	ROW_4K		0x1
#define	ROW_8K		0x2
#define	ROW_16K		0x3
/* 颗粒列数 */
#define	COL_256		0x7
#define	COL_512		0x0
#define	COL_1K		0x1
#define	COL_2K		0x2
#define	COL_4K		0x3
/* 颗粒位宽 */
#define	WIDTH_8		0x0
#define	WIDTH_16	0x1
#define	WIDTH_32	0x2

#define	TRCD		3
#define	TCL			3
#define	TRP			3
#define	TRFC		8
#define	TRAS		6
#define	TREF		0x818
#define	TWR			2

#define	DEF_SEL		0x1
#define	DEF_SEL_N	0x0
#define	HANG_UP		0x1
#define	HANG_UP_N	0x0
#define	CFG_VALID	0x1

/* mem = 32MByte */
#define	SD_PARA0	(0x7f<<25 | \
					(TRAS << 21) | \
					(TRFC << 17) | (TRP << 14) | (TCL << 11) | \
					(TRCD << 8) | (WIDTH_16 << 6) | (COL_512 << 3) | \
					ROW_8K)

#define	SD_PARA1	((HANG_UP_N << 8) | (DEF_SEL_N << 7) | (TWR << 5) | (TREF >> 7))

#define	SD_PARA1_EN	((CFG_VALID << 9) | (HANG_UP_N << 8) | \
					(DEF_SEL_N << 7) | (TWR << 5) | (TREF >> 7))
如果是两片SDRAM,需要设置片选

	/* 设置sdram cs1复用关系,开发板使用ejtag_sel gpio_0引脚(第五复用)作为第二片sdram的片选
	  注意sw2拨码开关的设置,使用ejtag烧录pmon时需要调整拨码开关,烧录完再调整回来 */
	li		a0, 0xbfd011c0
	lw		a1, 0x40(a0)
	ori	a1, 0x01
	sw		a1, 0x40(a0)

初始化CACHE

初始化Cache这部分代码没有仔细研究

调用汇编函数cache_init

do_caches:
	/* Init caches... */
	li	s7, 0                   /* no L2 cache */
	li	s8, 0                   /* no L3 cache */

    bal     cache_init          // 调用汇编函数cache_init
    nop

	mfc0   a0, COP_0_CONFIG         // 将协处理器0的config寄存器的值加载到寄存器a0
	and    a0, a0, ~((1<<12) | 7)   // a0 = a0 & ~((1<<12) | 7)
	or     a0, a0, 2                // a0 |= 2
	mtc0   a0, COP_0_CONFIG         // 将寄存器a0的值写入协处理器0的config寄存器

汇编函数cache_init的具体实现

	.ent		cache_init
	.global	cache_init
	.set		noreorder
cache_init:
	move t1, ra
####part 2####
cache_detect_4way:
	.set	mips32
	mfc0	t4, CP0_CONFIG,1        // 将cp0的config1寄存器的值加载到寄存器t4中
	lui		v0, 0x7		            // v0 = 0x7 << 16
	and		v0, t4, v0              // v0 = t4 & v0
	srl		t3, v0, 16              // t3 = v0 >> 16  Icache组相联数 IA

	li		t5, 0x800 		//32*64
	srl		v1, t4,22		//v1 = t4 >> 22
	andi	v1, 7			//Icache每路的组数 64x2^S IS
	sll		t5, v1			//InstCacheSetSize
	sll		t5, t3			//t5 InstCacheSize


	andi	v0, t4, 0x0380
	srl		t7, v0, 7		//DA

	li		t6, 0x800       // 32*64
	srl		v1, t4,13
	andi	v1, 7			//DS
	sll		t6, v1          // DataCacheSetSize
	sll		t6, t7          // t5 DataCacheSize

####part 3####
#	.set	mips3
	lui		a0, 0x8000			//a0 = 0x8000 << 16
	addu	a1, $0, t5
	addu	a2, $0, t6
cache_init_d2way:
/******************************/	//lxy
//	addiu	t3, t3, 1
//	li	t4, 0
//5:
/******************************/
// a0=0x80000000, a1=icache_size, a2=dcache_size
// a3, v0 and v1 used as local registers
	mtc0	$0, CP0_TAGHI
	addu	v0, $0, a0		//v0 = 0 + a0
	addu	v1, a0, a2		//v1 = a0 + a2
1:	slt		a3, v0, v1		//a3 = v0 < v1 ? 1 : 0
	beq		a3, $0, 1f		//if (a3 == 0) goto 1f
	nop
	mtc0	$0, CP0_TAGLO
	cache	Index_Store_Tag_D, 0x0(v0)	        // 1 way
4:	beq		$0, $0, 1b
	addiu	v0, v0, 0x20
1:
cache_flush_i2way:
	addu	v0, $0, a0
	addu	v1, a0, a1
1:	slt		a3, v0, v1
	beq		a3, $0, 1f
	nop
	cache	Index_Invalidate_I, 0x0(v0)	        // 1 way
4:	beq		$0, $0, 1b
	addiu	v0, v0, 0x20
1:
cache_flush_d2way:
	addu	v0, $0, a0
	addu	v1, a0, a2
1:	slt		a3, v0, v1
	beq		a3, $0, 1f
	nop
	cache	Index_Writeback_Inv_D, 0x0(v0) 	    // 1 way
4:	beq		$0, $0, 1b
	addiu	v0, v0, 0x20
/******************************/	//lxy
//	addiu	t4, t4, 1
//	addiu	a0, a0, 1
//	slt		t5, t4, t3
//	bne		t5, $0, 5b
//	nop
/******************************/
//	.set	mips0

1:
cache_init_finish:
	jr	t1
	nop
	.set	reorder
	.end	cache_init

搬运固件到内存

内存和cache初始化之后,就可以把固件搬运到内存,这样代码就可以在内存运行了。

拷贝text和data段

拷贝固件分为两步:
一,先将执行拷贝pmon到内存任务的代码,拷贝到内存0xa0000000;
二,将固件拷贝到起始地址为0xa0010000的内存空间。
已经在代码中添加了详细的注释,直接看代码吧

 DEBUGGING AND COPY SELF TO RAM***********************/
//#include "newtest.32/mydebug.S"
bootnow:
	/* copy program to sdram to make copy fast */
    /* 先将执行拷贝pmon到内存任务的代码,拷贝到内存0xa0000000 */
    
    /* 先确定需要拷贝的代码段为标号121到标号122之间的代码
     * 由于链接时指定的起始地址是0x80010000,
     * 而目前正在ROM(SPI NOR FLASH,起始地址为0xBFC00000)运行
     * 所以需要用寄存器s0来修正一下地址
     */
	la		t0, 121f            // 将下一个标号121所在地址,加载到寄存器t0
	addu	t0, s0              // 使用寄存器s0修正t0中的(标号121的)地址
	la		t1, 122f            // 将下一个标号122所在地址,加载到寄存器t1
	addu	t1, s0              // 使用寄存器s0修正t1中的(标号122的)地址
	
	li		t2, 0xa0000000      // 将立即数0xa0000000(起始地址)加载到寄存器t2
1:
	lw		v0, (t0)            // 将寄存器t0所指的内存地址开始4字节的数据加载到寄存器v0
	sw		v0, (t2)            // 将寄存器v0的内容保存到寄存器t2所指的内存中
	addu	t0, 4               // 寄存器t0向后移4字节
	addu	t2, 4               // 寄存器t2向后移4字节
	ble	t0, t1, 1b              // 如果t0 <= t1,则跳转到上一个标号1处,继续拷贝后面的4字节
	nop

	li		t0, 0xa0000000      // 将立即数0xa0000000加载到寄存器t0
	jr		t0	                // 跳转到起始地址0xa0000000处开始执行(拷贝任务)
	nop		

121: 
	/* Copy PMON to execute location... */
    /* 将固件拷贝到起始地址为0xa0010000的内存空间
       由于kseg0(0x8000 0000 - 0x9FFF FFFF)和kseg1(0xA000 0000 - 0xBFFF FFFF)是映射到物理内存的相同区域
       即拷贝到0xA000 0000开始的kseg1,就相当于拷贝到0x8000 0000开始的kseg0
       这就是为什么链接时,指定的地址是0x8001 0000,而拷贝的目标起始地址是0xA001 0000
    */
	la		a0, start           // 加载符号start所在地址0x80010000加载到寄存器a0中
	addu	a1, a0, s0          // 使用寄存器s0修正寄存器a0中的地址,a1=0xBFC00000
	la		a2, _edata          // 加载_edata(链接脚本中的一个符号)到寄存器a2
	or		a0, 0xa0000000      // a0 = a0 | 0xa0000000 = 0xa0010000
	or		a2, 0xa0000000      // a2 = a2 | 0xa0000000,修正地址_edata
	subu	t1, a2, a0          // t1 = a2 - a0,即计算从start到_edata之间的长度(字节数)
	srl	t1, t1, 2               // t1 >>= 2,即t1除以4。(和前面类似,每次拷贝4字节,所以除以4)
	                            // 似乎t1计算结果没有被使用,马上就被后面的覆盖了

	move	t0, a0              // t0 = a0 = 0xa0010000 (目标起始地址)
	move	t1, a1              // t1 = a1 = 0xBFC00000 (start在ROM中的地址,源起始地址)
	move	t2, a2              // t2 = a2 (_edata在ROM中的地址,源结束地址)

	/* copy text section */
1:	and	t3, t0, 0x0000ffff      // t3 = t0 & 0x0000ffff,取低16位
	bnez	t3, 2f              // 如果t3不等于0,则跳转到下一个标号2处继续执行,t3的计算结果似乎没被使用,就被后面的覆盖了
	nop
2:	lw		t3, 0(t1)           // 从源地址t1处加载4字节到寄存器t3中
	nop
	sw		t3, 0(t0)           // 将寄存器t3中的4字节数据保存到目标地址t0处
	addu	t0, 4               // 目标地址t0后移4字节
	addu	t1, 4               // 源地址t1    后移4字节
	bne	t2, t0, 1b              // 如果t2不等于t0,则跳到上一个标号1处继续拷贝,总的来说就是判断拷贝是否结束
	nop
	/* copy text section done. */

初始化BSS

程序编译链接后,会生成三段:text段,data段和bss段。
其中,text段为代码段,用于存放程序执行代码;Data段为数据段,用于存放程序中已初始化的全局变量;bss段也是数据段,不过存放的是程序中未初始化的全局变量。
三段中,text段和data段在前面已经拷贝到内存了,而bss段是不需要拷贝的,因为存放的是程序中未初始化的全局变量。只需要将这片内存区域全部清零即可。如下所示

	/* Clear BSS */
    /* BSS段为未初始化的全局变量的内存区间,这部分不需要从ROM中拷贝,也就不需要做地址修正 */
	la		a0, _edata          // 加载_edata的地址到寄存器a0
	la		a2, _end            // 加载_end的地址到寄存器a2
2:	sw		zero, 0(a0)         // 将寄存器a0所指的4字节清零
	bne	a2, a0, 2b              // 如果a2不等于a0,则跳到上一个标号2处,继续清零下一个4字节
	addu	a0, 4               // a0 += 4,注意,这条汇编在延迟槽内,所以仍然会被执行到
	/* Copy PMON to execute location done */

跳转到main函数

在基础寄存器(SP,GP等)初始化后,内存和cache也都初始化了,并且把固件搬运到内存后,c语言运行环境就已经具备了,这时就直接跳到main函数,执行c语言程序了。至此汇编初始化任务就完成了。

    /* 将内存大小(单位M)作为入参,放在寄存器a0中 */
	move	a0, msize           // a0 = msize(内存大小)
	srl	a0, 20                  // a0 >>= 20,将单位转换为M

    /* 调用函数main */
	la		v0, main            // 将main()函数地址加载到寄存器v0中
	jalr	v0                  // 调用寄存器v0所指的函数
	nop
跳转到main函数的代码很简单,就一两条汇编语句就可以了。Pmon中将内存的大小作为入参传递给了main函数,这里也没删,其实在裸机程序中内存大小就是一个宏,需要用的时候,直接读取宏的值。换句话说就是内存大小是否以入参形式传递给main函数,其实不重要。

Makefile详解

pmon编译时,会将使用的命令打印出来,这里将其重定向到一个文本文档。下面就以这个编译输出作为参考,详细分析纯粹的裸机程序的编译链接的参数等。
首先,需要获取编译pmon时的打印信息,这个可以通过在命令后面跟一个重定向即可,即把打印信息重定向到一个文本文档,比如“pmon_build.log”,假设现在已经有了这个文档。

编译参数有哪些

汇编语言的编译参数

在编译log文档中搜索关键字“mipsel-linux-gcc”,得到的第一条命令就是编译start.S的,如下图所示

从中提取出编译参数为“-mno-abicalls -fno-pic -G 0 -mips2 -Wall -mno-abicalls -fno-builtin”,如下图所示

c语言的编译参数

紧接着start.S的就是c文件的编译,所有c文件的编译参数都是一样的,这里就以紧接着start.S的那个c文件为例,编译记录如下

从中提取出的编译参数为“-mno-abicalls -fno-pic -g -Wall -Wstrict-prototypes -Wno-uninitialized -Wno-format -Wno-main -O2 -G 0 -mips2 -fno-builtin”,如下图所示

链接参数有哪些

和编译类似,以关键字“mipsel-linux-ld”搜索,得到的第一个结果就是链接pmon的,如下所示

从中提取出的链接参数为“-m elf32ltsmip -G 0 -static -n -nostdlib -N”,如下所示

链接脚本

从前面的链接命令中,可以得到使用的链接脚本为“ld.script”。那么就以“ld.script”为关键字搜索,得到

从上图可知,最终的“ld.script”是由“ld.script.S”生成的,生成的“ld.script”所在目录为“../Targets/LS1X/conf/ld.script”。那么直接将其拷贝出来即可,完整的链接脚本文件如下

OUTPUT_FORMAT("elf32-tradlittlemips", "elf32-tradbigmips",
              "elf32-tradlittlemips")
OUTPUT_ARCH(mips)
ENTRY(_start)
SECTIONS
{

  . = 0xffffffff80010000;
  .text :
  {
    _ftext = . ;
    *(.text)
    *(.rodata)
    *(.rodata1)
    *(.reginfo)
    *(.init)
    *(.stub)

    *(.gnu.warning)
  } =0
  _etext = .;
  PROVIDE (etext = .);
  .fini : { *(.fini) } =0
  .data :
  {
    _fdata = . ;
    *(.data)
   . = ALIGN(32);
   *(.data.align32)
   . = ALIGN(64);
   *(.data.align64)
   . = ALIGN(128);
   *(.data.align128)
   . = ALIGN(4096);
   *(.data.align4096)
    CONSTRUCTORS
  }
  .data1 : { *(.data1) }
  .ctors :
  {
                __CTOR_LIST__ = .;
                LONG((__CTOR_END__ - __CTOR_LIST__) / 4 - 2)
               *(.ctors)
                LONG(0)
                __CTOR_END__ = .;
  }
  .dtors :
  {
                __DTOR_LIST__ = .;
                LONG((__DTOR_END__ - __DTOR_LIST__) / 4 - 2)
               *(.dtors)
                LONG(0)
                __DTOR_END__ = .;
  }
  _gp = ALIGN(16) + 0x7ff0;
  .got :
  {
    *(.got.plt) *(.got)
   }



  .sdata : { *(.sdata) }
  .lit8 : { *(.lit8) }
  .lit4 : { *(.lit4) }
  _edata = .;
  PROVIDE (edata = .);
  __bss_start = .;
  _fbss = .;
  .sbss : { *(.sbss) *(.scommon) }
  .bss :
  {
   *(.dynbss)
   *(.bss)
   . = ALIGN(32);
   *(.bss.align32)
   . = ALIGN(64);
   *(.bss.align64)
   . = ALIGN(128);
   *(.bss.align128)
   . = ALIGN(4096);
   *(.bss.align4096)
   *(COMMON)
  }
  _end = . ;
  PROVIDE (end = .);


  .stab 0 : { *(.stab) }
  .stabstr 0 : { *(.stabstr) }




  .debug 0 : { *(.debug) }
  .debug_srcinfo 0 : { *(.debug_srcinfo) }
  .debug_aranges 0 : { *(.debug_aranges) }
  .debug_pubnames 0 : { *(.debug_pubnames) }
  .debug_sfnames 0 : { *(.debug_sfnames) }
  .line 0 : { *(.line) }

  .gptab.sdata : { *(.gptab.data) *(.gptab.sdata) }
  .gptab.sbss : { *(.gptab.bss) *(.gptab.sbss) }
}

Makefile文件原文

# 虚拟机里的交叉编译工具链
#CROSS_COMPILE 	=mipsel-linux-
#COPY   = cp
# windows下的交叉编译工具链
CROSS_COMPILE 	=mips-linux-gnu-
COPY    = copy

#
# Include the make variables (CC, etc...)
#

AS		= $(CROSS_COMPILE)as
LD		= $(CROSS_COMPILE)ld
CC		= $(CROSS_COMPILE)gcc
CPP		= $(CC) -E
AR		= $(CROSS_COMPILE)ar
NM		= $(CROSS_COMPILE)nm
STRIP		= $(CROSS_COMPILE)strip
OBJCOPY		= $(CROSS_COMPILE)objcopy
OBJDUMP		= $(CROSS_COMPILE)objdump
SIZE		= $(CROSS_COMPILE)size


HEADERS = $(wildcard lib/*.h example/*.h app/*.h)
SRC_S = $(wildcard lib/*.S)
SRC_C = $(wildcard lib/*.c example/*.c app/*.c)
SRCS = $(SRC_S) $(SRC_C)
OBJS = $(patsubst %.S, %.o, $(SRC_S)) $(patsubst %.c, %.o, $(SRC_C))  # 注意汇编文件一定要在前面

#头文件查找路径
INCLUDES = -Ilib -Iexample -Iapp
#链接库查找路径
LIBS = 

#编译参数
CCFLAGS = -mno-abicalls -fno-pic -g -Wall -Wstrict-prototypes -Wno-uninitialized -Wno-format -Wno-main -O2 -G 0 -mips2 -fno-builtin
AFLAGS  = -mno-abicalls -fno-pic -G 0 -mips2 -Wall -mno-abicalls -fno-builtin
#链接参数
LDFLAGS = -m elf32ltsmip -G 0 -static -n -nostdlib -N

# 最终的目标文件
OUTPUT = OpenLoongsonLib1c.elf

all:$(OUTPUT)

$(OUTPUT):$(OBJS)
	$(LD) $(LDFLAGS) -T ld.script -e start -o $@ $^
	$(COPY) $(OUTPUT) OpenLoongsonLib1c_debug.elf
	$(STRIP) -g -S --strip-debug $(OUTPUT)
	$(OBJCOPY) -O binary $(OUTPUT) OpenLoongsonLib1c.bin
	$(SIZE) $(OUTPUT)
#	cp $(OUTPUT) /tftpboot/
    
.c.o:
	$(CC) $(CCFLAGS) $(INCLUDES) -c -o $@ $^

.S.o:
	$(CC) $(AFLAGS) $(INCLUDES) -c -o $@ $^
    
clean:
#	rm -f $(OBJS) $(OUTPUT) OpenLoongsonLib1c.bin
	del lib\*.o example\*.o app\*.o $(OUTPUT) OpenLoongsonLib1c.bin


猜你喜欢

转载自blog.csdn.net/caogos/article/details/78984158
今日推荐