欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-03-23 22:45:16
...
参考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反汇编,
例如
aaa@qq.com:/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中的汇编代码的对应关系

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

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

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

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

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

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

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

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

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

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

刚上电时,把BEV置1

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

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

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

初始化完成后,把BEV清零

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

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

Pmon中对应的代码为

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

地址空间的划分

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

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

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

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

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

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

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

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

标号start的值为0x80010000

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

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

栈空间在什么地方

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

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

把固件本身从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的注解非常好。如下

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

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

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

初始化基础寄存器

主要是初始化协处理器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的,如下图所示

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

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

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

c语言的编译参数

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

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

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

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

链接参数有哪些

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

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

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

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

链接脚本

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

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

从上图可知,最终的“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 aaa@qq.com $^
	$(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 aaa@qq.com $^

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