冯诺依曼式计算机CPU模拟器(单核版)——北邮19/20/21计导大作业

冯诺依曼式计算机CPU模拟器(单核版)


一、课程设计要求简介


        模拟一个简易的冯诺依曼式计算机CPU的工作。

        CPU字长为16位,共11个寄存器,其中3个系统寄存器,分别为程序计数器指令寄存器标志寄存器;8个通用寄存器,即寄存器1、2、3、4(数据寄存器),寄存器5、6、7、8(地址寄存器)。该CPU至多支持32KB内存。内存分两部分,一部分为代码段,从地址0开始。另一部分为数据段,从地址16384开始。

        CPU所支持的指令集中,每条指令固定由32位(编号为0到31)二进制数组成,其中第0到7位为操作码,代表CPU要执行哪种操作;第8到15位为操作对象,如寄存器,内存地址等;第16到31位为立即数。该CPU有一个输入端口和一个输出端口。输入端口的数据由标准输入设备(键盘)输入,输出端口的数据输出到标准输出设备(显示器)上。

        程序开始时要从指定文件中读入一段用给定指令集写的程序至内存(从地址0开始顺序保存),程序计数器初始值也为0。指令加载完成后程序就开始不断重复取指令、分析指令和执行指令的过程。程序每执行一条指令就要输出CPU当前的状态,如各寄存器的值等。当执行到停机指令时,程序按要求输出后就结束了。

  • 取指令:读取程序计数器PC内的指令地址,根据这个地址将指令从内存中读入,并保存在指令寄存器中,同时程序计数器内容加4,指向下一个条指令。

  • 分析指令:对指令寄存器中的指令进行解码,分析出指令的操作码,所需操作数的存放位置等信息等。

  • 执行指令:完成相关计算并将结果写到相应位置。

*指令输入从文件 “dict.dic” 中获取,非手动输入,只有遇到指令集中的输入操作时才从键盘读入。
*输入输出样例见 冯诺依曼结构作业_提取码BUPT,也可移步 我的Github



二、指令集


指令 说明
停机
指令
00000000
00000000
0000000000000000
停止程序执行。
数据传送
指令
00000001
00010000
0000000000000000
将一个立即数传送至寄存器1。
00000001
00010101
0000000000000000
将寄存器5中地址所指向的内存单元(2个字节)的内容传送至寄存器1。
00000001
01010001
0000000000000000
将寄存器1的内容传送至寄存器5中地址所指向的内存单元(2个字节)。
算术运算
指令
00000010
00010000
0000000000000000
将寄存器1内的数与一个立即数相加,结果保存至寄存器1。
00000010
00010101
0000000000000000
将寄存器1内的数与寄存器5中地址所指向的内存单元(2个字节)里存的数相加,结果保存至寄存器1。
00000011
00010000
0000000000000000
将寄存器1内的数减去一个立即数,结果保存至寄存器1。
00000011
00010101
0000000000000000
将寄存器1内的数减去寄存器5中地址所指向的内存单元(2个字节)里存的数,结果保存至寄存器1。
00000100
00010000
0000000000000000
将寄存器1内的数与一个立即数相乘,结果保存至寄存器1。
00000100
00010101
0000000000000000
将寄存器1内的数与寄存器5中地址所指向的内存单元(2个字节)里存的数相乘,结果保存至寄存器1。
00000101
00010000
0000000000000000
将寄存器1内的数除以(C语言的整数除法)一个立即数,结果保存至寄存器1。
00000101
00010101
0000000000000000
将寄存器1内的数除以(C语言的整数除法)寄存器5中地址所指向的内存单元(2个字节)里存的数,结果保存至寄存器1。
逻辑运算
指令
00000110
00010000
0000000000000000
将寄存器1内的数与一个立即数做逻辑与,结果保存至寄存器1。(如果结果为真则保存1,否则保存0)
00000110
00010101
0000000000000000
将寄存器1内的数与寄存器5中地址所指向的内存单元(2个字节)里存的数做逻辑与,结果保存至寄存器1。(如果结果为真则保存1,否则保存0)
00000111
00010000
0000000000000000
将寄存器1内的数与一个立即数做逻辑或,结果保存至寄存器1。(如果结果为真则保存1,否则保存0)
00000111
00010101
0000000000000000
将寄存器1内的数与寄存器5中地址所指向的内存单元(2个字节)里存的数做逻辑或,结果保存至寄存器1。(如果结果为真则保存1,否则保存0)
00001000
00010000
0000000000000000
将寄存器1内的数做逻辑非,结果保存至寄存器1。(如果结果为真则保存1,否则保存0)
00001000
00000101
0000000000000000
将寄存器5中地址所指向的内存单元(2个字节)里存的数做逻辑非,结果仍保存至寄存器5中地址所指向的内存单元。(如果结果为真则保存1,否则保存0)
比较
指令
00001001
00010000
0000000000000000
将寄存器1内的数与一个立即数比较,如两数相等,则标志寄存器被修置为0,如寄存器1大,则标志寄存器被置为1,如寄存器1小,则标志寄存器被置为-1。
00001001
00010101
0000000000000000
将寄存器1内的数与寄存器5中地址所指向的内存单元(2个字节)里存的数比较,如两数相等,则标志寄存器被置为0,如寄存器1大,则标志寄存器被置为1,如寄存器1小,则标志寄存器被置为-1。
跳转
指令
00001010
00000000
0000000000000000
无条件跳转指令,转移至程序计数器加一个立即数处执行。也就是说要修改程序计数器。
00001010
00000001
0000000000000000
如果标志寄存器内的值为0,则转移至程序计数器加一个立即数处执行。也就是说要修改程序计数器。
00001010
00000010
0000000000000000
如果标志寄存器内的值为1,则转移至程序计数器加一个立即数处执行。也就是说要修改程序计数器。
00001010
00000011
0000000000000000
如果标志寄存器内的值为-1,则转移至程序计数器加一个立即数处执行。也就是说要修改程序计数器。
输入输出
指令
00001011
00010000
0000000000000000
从输入端口读入一个整数并保存在寄存器1中。也就是从键盘读一个整数到寄存器1中。
00001100
00010000
0000000000000000
将寄存器1中的数输出到输出端口。也就是将寄存器1中的数以整数的形式输出到显示器上,同时输出一个换行符。


三、思路与分析


主要问题

  1. 设计怎样的数据结构来模拟对应元件
  2. 如何编写函数来完成相关指令

选取适当的数据结构

内存和通用寄存器十分相似,都是字节数据的容器,模拟它们是实现后续操作的前提,
容易想到几种方法:

  • 用int、char等一维数组,每个下标对应1位
  • 同上,但用二维数组,1行8列,模拟1字节对应8位
  • 创建结构体数组,结构体内部放置8个变量,模拟1字节对应8位

这些都是理论可行的方法,然而实际内存开销较大,并且可能出现不易操作的情况。
例如,对于二维char数组,究竟是否需要多加一列来记录终止符’\0’,以便使用字符串函数;记录数据的过程中,究竟使用字符’0’还是ASCII 0(即’\0’),都需要斟酌。

C++提供了按位存储的容器vector<bool>,但由于课程要求,可能无法使用。实际上,C语言中就允许我们使用特殊的结构体,以实现内存里真正的按位存储。
考虑这样的写法:

struct Byte {
   
   
	unsigned char bit0 : 1, bit1 : 1, bit2 : 1, bit3 : 1,
				  bit4 : 1, bit5 : 1, bit6 : 1, bit7 : 1;
};

使用了8个unsigned char来模拟8个位,其后的 “: 1” 说明bit0等各只占1位,而不是1字节。
但即使实现了真正的按位存储,后续操作依然十分繁琐,因为无法使用循环来读写。(你想换成数组?)
为此,union发挥了巨大的作用。考虑如下写法:

union Memory {
   
   
	unsigned char data;
	struct Byte b;
};

union Register {
   
   
	short data;
	struct {
   
   
		struct Byte b0;
		struct Byte b1;
	}b;
};

union在此实现了数据的统一。在Memory中,包含两种类型数据,一个是8位的Byte,另一个是1字节的data,任何对data的操作,都会同时作用于Byte,例如当data = 255时,Byte中就有"11111111",直接对data进行读写,就修改了对应的01序列。这为我们提供了极大的便利。
Register中同理,能够用一个2字节的short统辖16位。(其中又使用了一个结构体,是为了防止b0与b1共用同一字节,从而导致高字节无法体现)

当然,其他数据结构同样可行,但这种方法编程复杂度很低,且极大节省内存,模拟的32KB就是真实的32KB。

至于程序计数器、指令寄存器、标志寄存器等,使用基本数据类型即可。

数据读写

通常而言,操作都是以字节为单位的,有了适当的数据结构,可以方便地进行读写。
我们对元件的操作媒介就是结构体和联合中的data,在寄存器中,它是short(2字节),在内存中,是unsigned char(1字节),如果要修改数据,显然直接修改data即可。

  • 至于为何在内存中选用unsigned char,是因为我们不太关心具体的十进制数,而是侧重存储01序列本身,即它应当能将0-255对应为00000000-11111111

即将执行的指令被程序计数器PC选中,存储于指令寄存器IR中,IR为2字节,在分析指令步骤需要得到指令类型cmd、前后操作对象from和to。指令类型位于高字节,操作对象位于低字节的高4位和低4位。
数据存储的本质还是二进制01序列,不要因为有了data这一便捷的接口就忘记二进制操作。
容易想到如下计算方法:

	IR.data = (memory[PC].data << 8) | memory[PC + 1].data;
	int cmd = IR.data >> 8;
	int from = IR.data & 15, to = (IR.data & 240) >> 4;

IR由指令的前2字节组合而成,按位或换成相加也是可以的。
cmd由IR整体右移8位获取高字节得到。低字节中,from为低4位,和(00001111)B按位与即可;to为高4位,和(11110000)B按位与后右移4位即可。

其余读写也同理,基本都可利用二进制操作。如读写内存(2字节),只需:

	//read
	short num = (memory[pos].data << 8) | memory[pos + 1].data;
	
	//write
	memory[dest].data = immed >> 8;
	memory[dest + 1].data = immed & 255;

其中,immed表示立即数,可以知道立即数是补码表示的,那为何读写时不需要考虑补码因素呢?
事实上,依托于先前建立的数据结构,在整个设计过程中,都不需要考虑补码的影响。
试考虑以下原则:

  • 我操控的任何数,本质上都是01序列
  • 我写入数据时,只负责把01序列填到对应位置
  • 我读取数据时,才依据数据类型来决定按照原码还是补码翻译这个01序列,从而得到十进制数

当前获取的立即数,在程序中看起来是十进制数,实际上是一个01序列,因为规定了它的数据类型是short,故它能表示对应范围的十进制数,而在二进制下操作时,与数据类型是无关的。
因此,在读取2字节数据时,通过位运算可以获取完整的2字节序列,我们规定它是short,所以它按照short翻译,也就是按照补码翻译;在写入2字节数据时,如果是向内存中写入,我们拆开原有的2字节,分别通过位运算写入。
如果读写操作是在寄存器之间的,由short到short,就更为简单了,直接对data赋值即可。


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NoobDream_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值