书接上回,这相当于是实验1.1的进阶版本。
资料:
链接:https://pan.baidu.com/s/16Oc3hAC8lm7CUjsVnmDscA
提取码:8888
实验项目一
项目名称
实验1.2 原型机II-扩充指令集
实验目的
(1) 理解指令集结构及其作用;
(2) 理解计算机的运行过程,对指令集进行修改;
实验资源
(1) 阅读教材,掌握冯诺伊曼体系的相关内容;
(2) 学习《最小系统与原型机I》内容,完成实验1.1
(3)Ubuntu 20.4.3 64位虚拟机
2 实验任务
2.1 实验任务A
任务名称:添加乘法函数
(按照实验指导书内容操作即可)
进入相应目录:

(1) 使用nanocpu.c或gedit cpu.c来打开cpu.c文件(这里我使用的是vimcpu.c);

(2) cpu.c是原型机的核心模块,其中
1)前面的变量申明,例如machine_info,memory,R0,R1,R2,R3等均为原型机的硬件配置信息,因为其在另外一个文件中已经定义,因此采用extern方式引入;
2)然后是各种操作的具体执行过程,例如对于加法操作,其参数为
源寄存器:source[]
目的寄存器:dest[]
操作标志:*result(-1表示出错)

3)最后的一个函数ExecuteInstruction即为CPU的指令执行过程,其中红圈处的strncpy即表示从内存中取出一条指令(每条指令为字符串型,占20个字节),然后用split函数来对指令进行分割,再根据分割后的结果进行判断指令类型,调用前面的各种函数来进行操作。

(3) 在原型机I的基础上,我们对指令集进行扩充,增加一条乘法指令,其格式为 9 Ra Rb,即将寄存器Ra的值与寄存器Rb的值相乘,结果放在Rb寄存器中,因此需要增加一个ExecuteMul函数。
void ExecuteMul(charsource[],char dest[],int *result)
{
char op;
if(0==strcmp(source,"R0"))op=R0;
else if(0==strcmp(source,"R1"))op=R1;
else if(0==strcmp(source,"R2"))op=R2;
else if(0==strcmp(source,"R3"))op=R3;
else *result=-1;
if(0==strcmp(dest,"R0\n"))R0*=op;
else if(0==strcmp(dest,"R1\n"))R1*=op;
else if(0==strcmp(dest,"R2\n"))R2*=op;
else if(0==strcmp(dest,"R3\n"))R3*=op;
else *result=-1;
}

(4) 在ExecuteInstruction增加一个判断分支,从而能够识别此条指令。
case '9':
split(instruction_buffer,"",revbuf,&num);
ExecuteMul(revbuf[1],revbuf[2],result);
if(*result!=-1)*result=2;
PC++;
break;

红字部分在原来代码里没有,如果不加上会在乘法最后一步会直接崩掉。
(5) 增加一个d.txt文件,其中包括有乘法指令
1
5 R0 R1
1
9 R0 R1
8 R1
0
先创建一个d.txt:

写入内容,注意不要在指令中有多余空格:

(6) 修改1.config
4
3
0011
d.txt


(7) 输入make 生成可执行文件
然后使用./vm64 1.config来运行程序:

如图,实现了4乘5运算:

2.2 实验任务B
任务名称:运用加法实现乘法
(1) 增加一个e.txt文件,基于原型机I的指令完成了两个数的乘法操作,其基本思路是将乘法分解为加法,例如对于5*6,执行6次加5的操作:5*6=5+5+5+5+5+5
1
5R0 0000 //输入乘数,并保存在内存地址0000中
1 //输入另一个乘数,保存在R0中
4 1R2 //将R2寄存器赋值为1
50000 R3 //从地址0000中取出乘数的值5
2R3,R1 //将R1的值与R3的值相加,结果保存在R1中
3R2,R0 //被乘数减1
6-3 //如果被乘数不为0,则结果还需要再加一次乘数
8R1 //被乘数已经为0,此时R1中即为两数相乘的结果,输出此结果
0 //停机


注意在指令中不要有多余的空格
(2) 在原型机II上执行上述代码:


2.3 实验任务C
任务名称:实现除法
1. 为原型机II增加整除指令
(1)在对应目录下增加一个f.txt:

进入编辑:

内容为:
1 //从键盘输入被除数存放在R0中
5 R0 0000 //将被除数的值存放在内存地址为0000处
1 //从键盘输入除数存放在R0中
4 0 R2 //将寄存器R2(用于计数)的值赋为0
5 0000 R1 //将被除数去出,并赋值给寄存器R1
3 R0 R1 //R1-R0=>R1
2 R3 R2 //R2+R3=>R2
6 -2 //如果R3为1执行跳转,向前跳2条指令
4 1 R3 //由于在最后一次相减后R3为0,R2会少加一次1,
2 R3 R2 //所以在这里运用两行红色代码对R2中的值进行修正
8 R2 //输出R2中的值
0

(2)上述思路即将除法问题转换为减法问题,利用循环相减配合计数器来记录减的过程发生了几次。
例如计算20÷2,就是让20不断地减2,计算减了几个2使得原本的20可以等于0,那么商就是多少

2. 基于原型机I的指令写出两数整除的代码
(1) 在原型机I的基础上,我们对指令集进行扩充,增加一条乘法指令,其格式为A Ra Rb,即将寄存器Ra的值与寄存器Rb的值相乘,结果放在Rb寄存器中,因此需要增加一个ExecuteDiv函数。
void ExecuteDiv(char source[],chardest[],int *result)
{
char op;
if(0==strcmp(source,"R0"))op=R0;
else if(0==strcmp(source,"R1"))op=R1;
else if(0==strcmp(source,"R2"))op=R2;
else if(0==strcmp(source,"R3"))op=R3;
else *result=-1;
if(0==strcmp(dest,"R0\n"))R0/=op;
else if(0==strcmp(dest,"R1\n"))R1/=op;
else if(0==strcmp(dest,"R2\n"))R2/=op;
else if(0==strcmp(dest,"R3\n"))R3/=op;
else *result=-1;
}

(2) 在ExecuteInstruction增加一个判断分支,从而能够识别此条指令。
case 'A': // 由于只识别一个字符,所以不得不使用字母
split(instruction_buffer, " ",revbuf, &num);
if (3 > num) *result=-1;
else ExecuteDiv(revbuf[1], revbuf[2],result);
if (*result!=-1) *result=9;
PC++;
break;

(3) 增加一个aa.txt文件,其中包括有除法指令
1
5R0 R1
1
AR0 R1
8R1
0
先创建一个aa.txt:

写入内容,注意不要在指令中有多余空格:

(4) 修改1.config
4
3
0011
aa.txt

(5) 输入make 生成可执行文件
然后使用./vm64 1.config来运行程序:

如上图,实现了48除以2运算
3 总结
实验中出现的问题
实验任务A:
这里要加上这一句话,实验指导书中没有

如果不加会在乘法操作最后一步直接崩掉:

实验任务B:
未遇到问题。
实验任务C:
在实现除法时,由于每一条指令的首位只能是一个字符,所以必须使用字母而不可以是“10”

心得体会
通过这次实验,让我学到了两种扩充原型机指令集的方式。一种是“真正”的扩充,即通过在工程文件中增加函数来实现指令的扩展,在物理层面我理解为增加了一个电路来实现要增加的功能;另一种是“虚拟”的扩充,即运用已有指令集来进行扩展,通过对于要扩展的内容的本质剖析,运用已有指令集加以解决,例如乘法n*m可以转化为n个m数相加。
4.思考问题
(1) 原型机I与原型机II完成乘法和除法操作的方式有何不同?
答:
原型机I是通过修改内部函数实现,增加了一个全新的指令集,物理上需要通过增加组合电路和硬件来实现。
原型机II是通过已有指令集来实现乘除法。不需要额外增加组合电路和硬件。
(2) 在指令集中增加乘法、除法等指令时,原型机中需要增加代码,那么硬件实现上需要增加什么样的部件?
答:
乘法的实现通常需要使用乘法器电路。乘法器电路可以是基于硬件的组合逻辑电路,也可以是基于可编程逻辑器件(如FPGA)的可编程电路。
除法的实现通常需要使用除法器电路。除法器电路也可以是基于硬件的组合逻辑电路,也可以是基于可编程逻辑器件的可编程电路。由于除法过程比乘法过程复杂,所以除法器电路实现更为复杂。
此外,实现乘法和除法指令还需要考虑数据通路和控制逻辑等方面的设计。例如,乘法和除法指令的执行需要占用CPU的一定时间,因此需要合理设计流水线结构、控制信号和时序等方面的问题,以确保CPU的正确性和性能。
(3) 如果一台计算机只支持加法、减法操作,那么能否计算三角函数,对数函数?(提示:搜索并阅读“泰勒级数展开”等内容)
答:
如果一台计算机只支持加法和减法操作,那么它无法直接计算三角函数和对数函数。不过,可以使用泰勒级数展开来近似计算这些函数的值。
对于一个无穷可导的函数f(x),其在x=a处的泰勒级数展开式为:f(x)= f(a) + f'(a)(x-a)/1! + f''(a)(x-a)^2/2! + f'''(a)(x-a)^3/3! + ...其中,f'(a)、f''(a)、f'''(a)等表示f(x)在x=a处的一阶、二阶、三阶导数,以此类推。
基于泰勒级数展开,可以近似计算三角函数和对数函数的值。例如,正弦函数在x=0处的泰勒级数展开式为:sin(x)= x - x^3/3! + x^5/5! - x^7/7! + ...
使用这个展开式,可以计算任意角度的正弦函数的值。具体方法是将角度值x转化为弧度制,并根据展开式的前n项计算近似值。随着n的增加,近似值会越来越精确。
类似地,可以使用泰勒级数展开来近似计算余弦函数、正切函数、指数函数、对数函数等其他函数的值。这种方法虽然可以在一定程度上计算这些函数的值,但是在精度和计算效率方面都存在一定的限制。
如何运用加法和减法实现乘法和除法已经在本实验中得以实现。
(4)对于某个需要完成的功能,如果既可以通过硬件上增加电路来实现,也可以通过其他已有指令的组合来实现,那么如何判断哪一种比较合适?(提示:搜索并阅读RISC与CISC)。
答:
在判断是否增加新的硬件电路来实现某个功能的时候,需要考虑以下因素:
①时间:如果某个功能需要频繁使用,而硬件电路的实现需要花费较长的时间,那么在时间成本上使用已有指令的组合可能更加划算。
②空间:对于一些嵌入式设备和移动设备等有空间限制的应用场景,增加新的硬件电路需要占用更多的空间,而使用已有指令的组合可能更为合适。
③灵活性:如果某个功能需要不断优化和升级,而硬件电路的实现则需要重新设计和制造,那么使用已有指令的组合可能更具有灵活性。
此外,RISC和CISC架构在指令设计方面也有一些差异,对于使用已有指令的组合来实现某个功能也需要考虑架构的特点:
①RISC架构的指令集非常简单,每个指令只执行一种操作,因此在使用已有指令的组合来实现某个功能时,可能需要更多的指令和更复杂的指令序列。
②CISC架构的指令集较为复杂,每个指令可以执行多种操作,因此在使用已有指令的组合来实现某个功能时,可能可以利用更少的指令和更简单的指令序列。
因此,在判断是增加新的硬件电路还是使用已有指令的组合来实现某个功能时,需要考虑多个因素,包括时间、空间、灵活性以及架构的特点等。