Thinpad 教学计算机搭配了监控程序,能够接受用户命令,支持输入汇编指令并运行,查看寄存器及内存状态等功能。监控程序可在学生实现的 32 位 MIPS CPU 上运行,一方面可以帮助学生理解、掌握 MIPS 指令系统及其软件开发,另一方面可以作为验证学生 CPU 功能正确性的标准。
监控程序分为两个部分,Kernel 和 Term。其中 Kernel 使用 MIPS32 汇编语言编写,运行在 Thinpad 上学生实现的 CPU 中,用于管理硬件资源;Term 是上位机程序,使用 Python 语言编写,有基于命令行的用户界面,达到与用户交互的目的。Kernel 和 Term 直接通过串口通信,即用户在 Term 界面中输入的命令、代码经过 Term 处理后,通过串口传输给 Kernel 程序;反过来,Kernel 输出的信息也会通过串口传输到 Term,并展示给用户。
Kernel 使用汇编语言编写,使用到的指令有20余条,均符合 MIPS32 Release2 规范。Kernel 提供了三种不同的版本,以适应不同的档次的 CPU 实现。它们分别是:第一档为基础版本,直接基本的I/O和命令执行功能,不依赖异常、中断、CP0等处理器特征,适合于最简单的 CPU 实现;第二档支持中断,使用中断方式完成串口的I/O功能,需要处理器实现中断处理机制,及相关的CP0处理器;第三档在第二档基础上进一步增加了TLB的应用,要求处理器支持基于TLB的内存映射,更加接近于操作系统对处理器的需求。
为了在硬件上运行 Kernel 程序,我们首先要对 Kernel 的汇编代码进行编译。编译时必须使用MTI Bare Metal工具链:Linux版下载 。将下载的压缩包解压到任意目录后,设置环境变量 GCCPREFIX
以便 make 工具找到编译器,例如:
export GCCPREFIX=/usr/local/mipsel-linux-musl-cross/bin/mipsel-linux-musl-
下面是编译监控程序的过程。在kernel
文件夹下面,有汇编代码和 Makefile 文件,我们可以使用 make 工具编译 Kernel 程序。假设当前目录为 kernel
,目标版本为基础版本,我们在终端中运行命令
make ON_FPGA=n
即可开始编译流程。如果顺利结束,将生成 kernel.elf
和 kernel.bin
文件,即可执行文件。要在模拟器中运行它,可以使用命令
make sim
它会在 QEMU 中启动监控程序,并等待 Term 程序连接。本文后续章节介绍了如何使用 Term 连接模拟器。
若要在编译硬件上运行的 kernel(与 QEMU 版本的区别是串口外设不同),首先用 make clean
清除之前编译的结果,最后用命令
make ON_FPGA=y
编译用于硬件的 kernel.bin
。使用开发板提供的工具,将 kernel.bin
写入内存 0 地址(物理地址)位置,并让处理器复位从 0x8000000 地址(MIPS32中对应物理地址为0的虚地址)处开始执行,Kernel 就运行起来了。
Kernel 运行后会先通过串口输出版本号,该功能可作为检验其正常运行的标志。之后 Kernel 将等待 Term 从串口发来的命令,关于 Term 的使用将在后续章节描述。
接下来我们分别说明三个档次的监控程序对于硬件的要求,及简要的设计思想。
基础版本的 Kernel 共使用了21条不同的指令,它们是:
ADDIU
001001ssssstttttiiiiiiiiiiiiiiiiADDU
000000ssssstttttddddd00000100001AND
000000ssssstttttddddd00000100100ANDI
001100ssssstttttiiiiiiiiiiiiiiiiBEQ
000100ssssstttttooooooooooooooooBGTZ
000111sssss00000ooooooooooooooooBNE
000101ssssstttttooooooooooooooooJ
000010iiiiiiiiiiiiiiiiiiiiiiiiiiJAL
000011iiiiiiiiiiiiiiiiiiiiiiiiiiJR
000000sssss0000000000hhhhh001000LB
100000bbbbbtttttooooooooooooooooLUI
00111100000tttttiiiiiiiiiiiiiiiiLW
100011bbbbbtttttooooooooooooooooOR
000000ssssstttttddddd00000100101ORI
001101ssssstttttiiiiiiiiiiiiiiiiSB
101000bbbbbtttttooooooooooooooooSLL
00000000000tttttdddddaaaaa000000SRL
00000000000tttttdddddaaaaa000010SW
101011bbbbbtttttooooooooooooooooXOR
000000ssssstttttddddd00000100110XORI
001110ssssstttttiiiiiiiiiiiiiiii
根据 MIPS32 规范(在参考文献中)正确实现这些指令后,程序才能正常工作。
监控程序使用了 8 MB 的内存空间,其中约 1 MB 由 Kernel 使用,剩下的空间留给用户程序。此外,为了支持串口通信,还设置了一个内存以外的地址区域,用于串口收发。具体内存地址的分配方法如下表所示:
虚地址区间 | 说明 |
---|---|
0x80000000-0x800FFFFF | 监控程序代码 |
0x80100000-0x803FFFFF | 用户代码空间 |
0x80400000-0x807EFFFF | 用户数据空间 |
0x807F0000-0x807FFFFF | 监控程序数据 |
0xBFD003F8-0xBFD003FD | 串口数据及状态 |
串口控制器访问的代码位于kern/utils.S
,其数据格式为:
地址 | 位 | 说明 |
---|---|---|
0xBFD003F8 | [7:0] | 串口数据,读、写地址分别表示串口接收、发送一个字节 |
0xBFD003FC | [0] | 只读,为1时表示串口空闲,可发送数据 |
0xBFD003FC | [1] | 只读,为1时表示串口收到数据 |
Kernel 的入口地址为 0x80000000,对应汇编代码kern/init.S
中的 START:
标签。在完成必要的初始化流程后,Kernel 输出版本信息,随后进入 shell 线程,与用户交互。shell 线程会等待串口输入,执行输入的命令,并通过串口返回结果,如此往复运行。
当收到启动用户程序的命令后,用户线程代替 shell 线程的活动。用户程序的寄存器,保存在从 0x807F0000 到 0x807F0077 的连续120字节中,依次对应 $1 到 $30 用户寄存器,每次启动用户程序时从上述地址装载寄存器值,用户程序运行结束后保存到上述地址。
作为扩展功能之一,Kernel 支持中断方式的I/O,和 Syscall 功能。要启用这一功能,编译时的命令变为:
make ON_FPGA=y EN_INT=y
这一编译选项,会使得代码编译时增加宏定义ENABLE_INT
,从而使能中断相关的代码。
为支持中断,CPU 要额外实现以下指令
ERET
01000010000000000000000000011000MFC0
01000000000tttttddddd00000000lllMTC0
01000000100tttttddddd00000000lllSYSCALL
000000cccccccccccccccccccc001100
此外还需要实现 CP0 寄存器的这些字段:
- Status: IM4, EXL, IE
- Ebase: ExceptionBase
- Cause: BD, IP4, ExcCode
- EPC
CP0 寄存器字段功能定义参见 MIPS32 特权态规范(在参考文献中)。
监控程序对于异常、中断的使用方式如下:
- 入口地址 0x80001180,根据异常号跳转至相应的异常处理程序。
- 串口硬件中断:中断号为 IP4,作用是唤醒shell线程。为此,shell和用户线程运行时屏蔽串口硬件中断,idle线程中打开。
- 系统调用:shell线程调用SYS_wait,CPU控制权转交idle线程。
- 异常帧保存29个通用寄存器(k0,k1不保存)及STATUS,CAUSE,EPC三个相关寄存器。32个字,128字节,0x80字节。
- 禁止发生嵌套异常。
- 支持SYS_wait和SYS_putc两个系统调用。写串口忙等待,与禁止嵌套异常不冲突。
- 当发生不能处理的中断时,表示出现严重错误,终止当前任务,自行重启。并且发送错误信号 0x80 提醒TERM。
- 初始化时设置CP0_STATUS(BEV)=0,CP0_CAUSE(IV)=0,EBase=0x80001000,使用正常中断模式。
- 初始化时设置CP0_STATUS(ERL)=0,使eret指令以EPC寄存器值为地址跳转。
在支持异常处理的基础上,可以进一步使能TLB支持,从而实现用户态地址映射。要启用这一功能,编译时的命令变为:
make ON_FPGA=y EN_INT=y EN_TLB=y
CPU 要额外实现以下指令
TLBP
01000010000000000000000000001000TLBR
01000010000000000000000000000001TLBWI
01000010000000000000000000000010TLBWR
01000010000000000000000000000110
此外还需要实现 CP0 寄存器:
- Context
- Config1: MMUSize
- Index
- Entryhi: VPN2
- Entrylo0/1: PFN, D, V
- Wired
- Random
以及TLB相关的几个异常,其中 Refill 异常入口地址为 0x80001000,与其它异常的入口地址不同。
为了简化,TLB实际的映射是线性映射。将0x80100000-0x803FFFFF放在kuseg地址最低端,将0x80400000-0x807EFFFF放在kuseg的地址最高端。4MB的地址映射在kseg2的页表里只需8KB的页表。因此设CP0的WIRED=2,TLB最低两项存kseg2地址翻译。
在一般中断处理中,需要处理TLB不合法异常。修改异常通过统一置D位为一避免。当访问无法映射的地址时,向串口发送地址访问违法信号,并重启。因为正常访问kseg2不会引发TLB异常,所以异常类型TLBL,TLBS,Mod(修改TLB只读页)都是严重错误,需要发送错误信号 0x80 并重启。
kuseg的映射:
- va[0x00000000, 0x002FFFFF] = pa[0x00100000, 0x003FFFFF]
- va[0x7FC10000, 0x7FFFFFFF] = pa[0x00400000, 0x007EFFFF]
页表:
- PTECODE: va(i*page_size)->[i]->RAM0UBASE[i]
- PTESTACK: va(KSEG0BASE+i*page_size-RAM1USIZE)->[i]->RAM1[i]
初始化过程:
- 从Config1获得TLB大小,初始化TLB
- 设Context的PTEBase并填写页表
- PageMask设零(固定为4K页大小)
- 将用户栈指针设为 0x80000000
- Wired设为2,设置对kseg2的映射。
Term 程序运行在实验者的电脑上,提供监控程序和人交互的界面。Term 支持7种命令,它们分别是
- R:按照$1至$30的顺序返回用户程序寄存器值。
- D:显示从指定地址开始的一段内存区域中的数据。
- A:用户输入汇编指令或者数据,并放置到指定地址上。输入行只有数值时视为数据,否则为指令。
- F:从文件读入汇编指令或者数据,并放置到指定地址上,格式与 A 命令相同。
- U:从指定地址读取一定长度的数据,并显示反汇编结果。
- G:执行指定地址的用户程序。
- T:查看指定的TLB条目。本功能仅在Kernel支持TLB时有效。
- Q:退出 Term
利用这些命令,实验者可以输入一段汇编程序,检查数据是否正确写入,并让程序在处理器上运行验证。
Term 程序位于term
文件夹中,可执行文件为term.py
。对于本地的 Thinpad,运行程序时用 -s 选项指定串口。例如:
python term.py -s COM3
或者 python term.py -s /dev/ttyACM0
(串口名称根据实际情况修改)
连接远程实验平台的 Thinpad,或者 QEMU 模拟器时,使用 -t 选项指定 IP 和端口。例如:
python term.py -t 127.0.0.1:6666
监控程序附带了几个测试程序,代码见kern/test.S
。我们可以通过命令
make show-utest
来查看测试程序入口地址。记下这些地址,并在 Term 中使用G命令运行它们。
根据监控程序设计,用户程序的代码区为0x80100000-0x803FFFFF,实验时需要把用户程序写入这一区域。用户程序的最后需要以jr $31
结束,从而保证正确返回监控程序。
在输入用户程序的过程中,既可以用汇编指令,也可以直接写16进制的数据(机器码)。空行表示输入结束。
以下是一次输入用户程序并运行的过程演示:
MONITOR for MIPS32 - initialized.
>> a
>>addr: 0x80100000
one instruction per line, empty line to end.
[0x80100000] ori $v0,$0,5
[0x80100004] xor $t0,$t0,$t0
[0x80100008] xor $t1,$t1,$t1
[0x8010000c] loop:
[0x8010000c] addu $t1,$t1,$t0
[0x80100010] addiu $t0,$t0,1
[0x80100014] bne $v0,$t0,loop
[0x80100018] nop
[0x8010001c] jr $ra
[0x80100020] nop
[0x80100024]
>> u
>>addr: 0x80100000
>>num: 64
0x80100000: li v0,0x5
0x80100004: xor t0,t0,t0
0x80100008: xor t1,t1,t1
0x8010000c: addu t1,t1,t0
0x80100010: addiu t0,t0,1
0x80100014: bne v0,t0,0x8010000c
0x80100018: nop
0x8010001c: jr ra
0x80100020: nop
0x80100024: nop
0x80100028: nop
0x8010002c: nop
0x80100030: nop
0x80100034: nop
0x80100038: nop
0x8010003c: nop
>> g
>>addr: 0x80100000
elapsed time: 0.000s
>> r
R1 (AT) = 0x00000000
R2 (v0) = 0x00000005
R3 (v1) = 0x00000000
R4 (a0) = 0x00000000
R5 (a1) = 0x00000000
R6 (a2) = 0x00000000
R7 (a3) = 0x00000000
R8 (t0) = 0x00000005
R9 (t1) = 0x0000000a
R10(t2) = 0x00000000
R11(t3) = 0x00000000
R12(t4) = 0x00000000
R13(t5) = 0x00000000
R14(t6) = 0x00000000
R15(t7) = 0x00000000
R16(s0) = 0x00000000
R17(s1) = 0x00000000
R18(s2) = 0x00000000
R19(s3) = 0x00000000
R20(s4) = 0x00000000
R21(s5) = 0x00000000
R22(s6) = 0x00000000
R23(s7) = 0x00000000
R24(t8) = 0x00000000
R25(t9/jp) = 0x00000000
R26(k0) = 0x00000000
R27(k1) = 0x00000000
R28(gp) = 0x00000000
R29(sp) = 0x807f0000
R30(fp/s8) = 0x807f0000
>> q
当处理器和 Kernel 支持异常功能时(即上文所述 EN_INT=y ),用户还可以用 Syscall 的方式打印字符。打印字符的系统调用号为 30。使用时,用户把调用号保存在v0寄存器,打印字符参数保存在a0寄存器,并执行 syscall 指令,a0寄存器的低八位将作为字符打印。例如:
ori $v0, $0, 30 # 系统调用号
ori $a0, $0, 0x4F # 'O'
syscall 0x80
nop
ori $a0, $0, 0x4B # 'K'
syscall 0x80
nop
jr $ra
nop
- CPU采用的MIPS32指令集标准:MIPS32® Architecture For Programmers Volume II: The MIPS32® Instruction Set
- MIPS32中断及TLB等特权态资源:MIPS32® Architecture For Programmers Volume III: The MIPS32® Privileged Resource Architecture
- 初始版本:韦毅龙,李成杰,孟子焯
- 后续维护:张宇翔,董豪宇