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

2019-04-14 12:31发布

参考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 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(" asm uart2 init ok! "); // 打印一条提示信息,表示串口初始化成功了

汇编函数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