0 前言1 使用到的元件1.1 常用逻辑电路1.1.1 基本元件1.1.2 电平触发1.1.3 边沿触发1.1.4 组合电路1.2 算术运算电路1.2.1 串行并行加法器1.2.2 取反器与减法器1.2.3 移位器与乘法器1.2.4 加减器与除法器1.3 时序逻辑电路1.3.1 数码寄存器1.3.2 加法计数器1.3.3 脉冲发生器1.3.4 压栈与弹栈2 计算机的硬件2.1 计算机2.1.1 计算机的介绍2.1.2 计算机的设计2.1.3 计算机的性能2.2 控制器2.2.1 宽指令程序2.2.2 指令译码器2.2.3 计数器读写2.3 运算器2.3.1 逻辑运算单元2.3.2 算术运算单元2.3.3 条件判断单元2.4 存储器2.4.1 控制存储器2.4.2 高速缓存器2.4.3 随机存储器2.5 适配器2.5.1 输入与输出2.5.2 控制台设备2.5.3 声输出设备3 计算机的指令3.1 指令系统与汇编语言3.1.1 运算符的汇编别名3.1.2 条件判断汇编别名3.1.3 其他类型汇编别名3.2 流程控制与图灵完备3.2.1 跳转语句代码模板3.2.2 条件语句代码模板3.2.3 循环语句代码模板3.3 函数实现与代码实例3.3.1 函数调用与栈结构3.3.2 传入参数与返回值3.3.3 汇编语言代码实例4 计算机的改进4.1 指令系统的改进4.1.1 指令系统概述4.1.2 指令系统分类4.1.3 典型指令功能4.1.4 指令系统改进4.2 系统硬件的改进4.2.1 控制器的改进4.2.2 运算器的改进4.2.3 存储器的改进4.2.4 适配器的改进4.3 系统软件的改进5 后记
图灵完备(Turing Complete)这款游戏通过主线关卡的任务,引导玩家从逻辑门开始搭建电路,最终搭建出两种架构的可运行计算机,用自己设置的汇编代码实现条件语句、循环语句与函数,并在此基础上完成简单的编程。
这款游戏涉及到的知识有数字电路、计算机组成原理与汇编语言,不过即使没有这些前置知识也是可以玩的,因为游戏会提供知识卡片,较难的关卡也会有所提示。恰好这学期学了数字电路,便去买了这款游戏玩,感觉十分有趣,收获也颇丰。
我不打算展示每一关的解法,网上已经有人做了详细的攻略;在这里,我只想介绍一下使用到的元件、最终搭建的计算机及其汇编语言。一方面这是对游戏总结与知识的回顾,另一方面这份笔记或者说报告也算是一个知识的备忘录。
没有学过计算机组成原理,从零开始搭建的电路也没有系统性地预先设计,所以许多地方可能显得比较笨拙。不过我想,作为第一次尝试已经足够了——让我这个电脑小白了解到了许多知识,也在探索的过程中享受到了游戏的乐趣。
基本逻辑门有与门(And Gate)、或门(Or Gate)、非门(Not Gate)、与非门(Nand Gate)、或非门(Nor Gate)、异或门(XOR Gate)、同或门(XNOR Gate),其中由与非门或者或非门均可构造出所有其他的逻辑门。
在实现逻辑门功能的元件中,目前应用最广泛的是 TTL 电路(Transistor-Transistor Logic)与 CMOS 电路(Complementary Metal-Oxide Semiconductor,),其中 TTL 电路的输入引脚悬空则相当于高电平,而CMOS 电路的输入引脚悬空则相当于高阻态。
TTL 电路由双极性三体管组成,CMOS 电路由绝缘场效应管组成,它们都易于制造出 n 位与非门或 n 位或非门,并且一般来说,与非门的性能比或非门更好(速度更快),因此工程实践中一般将逻辑表达式化为最简与非式。
图灵完备这款游戏则采用了另一种做法,将 2 位与门、2 位或门、非门、2 位与非门、2 位或非门视为最基础的逻辑门,它们的延迟都记作 2;异或门、同或门由它们组成,故延迟为 6;3 位的与门、或门、与非门、或非门均由 2 位的逻辑门组成,延迟为 4。这样会导致一些功能的最简电路与 TTL 或 CMOS 电路的实现方案略有差异。
三态门(TSL,Three State Logic)又称为三态缓冲器,在这款游戏中则称为开关。当选通输入端为高电平时,输出等于输入;当选通输入端为低电平时,输出始终为高阻态。
一般来说,两个输出端不能直接相连,否则当输出不同时便会发生短路而烧毁电路。而使用三态门则可以避免这个问题,只需要保证输出端相连的三态门在每个时刻至多只有一个选通。利用这个性质可以实现数据选择器的功能,并且由于输入数量的任意性,由三态门组成的选择器一般比普通逻辑门组成的选择器的元件利用率要高。因此三态门在总线传输中应用十分广泛。
在上一小节中提到了逻辑门的延迟,理论上有延迟就有可能存在竞争与冒险现象。不过这款游戏做了简化,延迟量仅作为衡量电路的指标,而不会产生任何实际的影响,实际上所有门电路都可视为在一瞬间导通。当然在现实生活中,如果需要用三态门组成选择器,则需要考虑这一点,否则不仅可能产生毛刺现象,严重时还可能烧毁电路。
上一小节中说到,该游戏中逻辑门的延迟不会产生任何实际的影响,这也就意味着该游戏并没有做类似 Multisim 或 ModelSim 等软件的仿真,而只在电路发生变化或时钟脉冲到来时,才会更新所有电路的状态。并且更新电路时依据的不是上一个元件的状态,而需要追根溯源到最初的输入端。
这也就意味着,输出端不能连至自身的输入端,即循环依赖,否则理论上程序将陷入死循环。因此当出现循环依赖时,游戏便会报错且禁止运行,甚至无法保存该电路。不过游戏还是用其他方式支持了一类特殊的循环依赖(白名单),即或非门锁存器、与非门锁存器、与或锁存器,由此可以构造出一些电平触发器.
即使如此,大部分触发器与时序逻辑电路仍然无法实现,因为涉及到其他类型的循环依赖。为了弥补这一点,游戏提供了一位延时器,即对于每一个时刻,延时器都会输出前一时刻的输入。因此在更新电路状态时,向前追溯到延时器的输出便可以停止了,从而将其输出连至自身输入时不会产生循环依赖。由此可以构造边沿触发器.
该游戏的方形输入引脚表示通过延时器连至输出端,而圆形引脚表示不通过延时器。对于不包含延时器的电路,循环依赖会导致程序出现死循环,不过特殊的,游戏考虑并支持了以下三种循环依赖:
循环依赖白名单 | 逻辑电路与说明 |
---|---|
或非门锁存器 | ![]() |
与非门锁存器 | ![]() |
与或锁存器 | ![]() |
与非门锁存器即基本 RS 触发器,理论上可以与 D 触发器、JK 触发器、T 触发器相互转换。于是我们可以做出同步 RS 触发器、主从 RS 触发器与同步 D 触发器,如下表所示。
可实现的触发器 | 逻辑电路 | 引脚图 |
---|---|---|
同步 RSFF | ![]() | ![]() |
主从 RSFF | ![]() | ![]() |
同步 DFF | ![]() | ![]() |
尽管如此,大部分触发器我们仍然无法实现(如下表所示),因为会涉及到上述锁存器之外的循环依赖,如边沿 D 触发器、同步 JK 触发器、主从 JK 触发器等。
不允许的触发器 | 逻辑电路 |
---|---|
JKFF | ![]() |
TFF | ![]() |
实际上,单个延时器的功能相当于边沿 D 触发器,其 CP 端始终连至全局的可调频率时钟脉冲。而 D 触发器可构造出 RS 触发器、JK 触发器、T 触发器,如下表所示:
边沿触发器 | 逻辑电路图 | 引脚图 |
---|---|---|
边沿 DFF | ![]() | ![]() |
边沿 RSFF | ![]() | ![]() |
边沿 JKFF | ![]() | ![]() |
边沿 TFF | ![]() | ![]() |
边沿 T'FF | ![]() | ![]() |
需要注意的是,测试上述电路时,改变测试的输入信号后,一般要等待两个时刻才能传到输出,这是因为改变测试用的输入信号时不会立即影响延时器,而需等待一个时刻才能写入延时器,再过一个时刻才能输出。而在实际应用时,改变的输入则会立即写入延时器,因此可以实现边沿触发器的功能。
用上述边沿触发器只能搭建同步时序逻辑电路,不过这对于我们的计算机来说已经足够了——我们只需要实现数码寄存器、同步计数器、栈和脉冲发生器。理论上可以通过改变延时器的延时量,从而实现 2n 分频器。
在计算机硬件中,译码器最主要的应用是指令译码与寄存器寻址。
可利用一位译码器构成三位译码器,这里直接使用非门搭建。
这里利用三位译码器和八位开关实现六位译码器。相比于最简与非式电路,这样做增加了 2 延时,但是需要的逻辑门数量大大减少。
同理,为了简化电路,这里直接使用六位译码器和六十四位开关进行设计。
在计算机硬件中,选择器最主要的应用是实现存储器的读取操作。
使用开关比与非门更简:
这里利用译码器与开关实现三位选择器:
这里利用一位选择器和三位选择器实现四位选择器:
八位选择器类似可得(使用选择器或译码器),由于电路庞大,这里不再列出。
在计算机硬件中,比较器最主要的应用是实现程序跳转的判断。
为清晰起见,这里先给出 Verilog 代码:
xxxxxxxxxx
61module if_eq(A, B, Y);
2 input[7:0] A, B;
3 output Y;
4
5 assign Y (A B);
6endmodule
电路如下图所示:
有符号数的最高位即符号位,它不表示后七位是否乘上 -1,而表示是否加上 -128,如 1111_1111
表示 -1 而非 -127,这样做看似为负数的表示带来了困难,但实际上 -1 与该二进制数表示的无符号数 255 属于同一个模 256 等价类,这样做不仅便于理解与制造取反器、减法器、除法器等电路,还可以简化有符号小于的逻辑表达式。如果使用乘上 -1 的表示法,则电路图如下:
加法器有着诸多的应用,如实现计数器、乘法器、除法器等等。
由两片 4 位超前进位并行加法器串联可以实现一字节的加法器:
全减器的设计与全减器是类似的。为了清晰起见,先给出 Verilog 代码
xxxxxxxxxx
101module FS(A, B, Cin, S, Cout);
2 input A, B, Cin;
3 output S, Cout;
4 wire temp;
5
6 assign temp B Cin;
7 assign S A temp;
8 assign Cout (A temp) (B Cin);
9 // 优先级由高到低: ~ & |, 因此括号是不必要的;
10endmodule
引脚图及电路图如下图所示:
易知取反加以记为负数。
由
最直接的思路是罗列所有的情况后,使用开关选取输出(即八选一选择器)。
另一种思路是根据每一位的权重进行移位,这种思路将逻辑门的开支从 83 降到了 51,但是延时量从 8 增加到了 12。
利用通用移位器与加法器可以简单地实现乘法器,不过元件利用率较低。
计算除法最简单的思路是用被除数反复减去除数,直到被除数小于除数为止,留下的便是余数,减去的次数即位商数。按照这个思路,如果要求在一个时钟脉冲内完成,则需要大量的逻辑门并且延时高;如果使用多个时钟脉冲,则需要大量的时钟刻。无论如何,运算速度很慢,并且对于不同的数据,运算时间差异较大。
另一种思路是使用并行的除法器,即后面两节中将要介绍的,在一个时钟脉冲内完成计算。虽然只需要一个时钟脉冲,但是电路庞大,一字节的除法器便需要上千个逻辑门,延时量也高达上百,无论是逻辑门数量,还是延时量,都与一个简易的 8 位计算机差不多了。
第一类思路主要是从软件(程序)上实现的,优化的思路是使用二分法;第二类思路主要是从硬件上实现的,优化的思路是并行改为串行;还有一类折中的思路,用若干时钟脉冲和较低的延时量实现除法器,这里不做介绍。
为了实现不恢复余数的除法器,需要使用到可控加减法器,引脚图、电路图与电路说明如图所示:
自定义元件的引脚与形状是根据内部电路图自动生成的,这就是我如上排版的原因。为了使得下一小节的电路图看着更简便、更紧凑,我修改了很多遍可控加减法器的电路布局及其引脚分配,最终得到了下一小节中除法器的电路图,看着还是很舒服的。
以无符号的八位并行除法器为例,电路图如下图所示:
其中加减法电路仍为串行,因此可以使用并行加减法电路进一步降低延时量。
延时器及 D 触发器,由 D 触发器可实现数码寄存器:
对于八位的情况,分别存储每一位即可:
在八位寄存器的基础上,可增加始终读取端以方便使用,如下图所示:
思路一:始终读取数据,使用选择器选出需要的数据。这种思路虽然可以压缩空间,但是对于始终读取数据并不是一个好主意。如果将存储数据的不是寄存器,而是外部的输入输出,那么我们将无法延迟读取数据。
思路二:注意到寄存器在不读取数据时,输出为高阻态,于是可以只在输入端使用开关。这个思路虽然比思路一多用了一个逻辑门,但是可以进一步压缩空间,而且并非始终读取数据,通用性更强。
最简单的思路是用寄存器、加法器和数据选择器实现计数器及其置数功能:
可用 JK 触发器实现四位二进制计数器 74161,其中 JK 触发器由 D 触发器(即延时器)搭建。
由两片四位二进制计数器 74161 级联,可得到八位二进制计数器:
相比于由寄存器、加法器和数据选择器搭建的方案,这样做的好处是逻辑门数量少、延时量低。
脉冲发生器用于实现程序指令的顺序读取。这里以周期为 16 的 8 位输出脉冲发生器为例,用四位数据选择器和四位二进制加法计数器 74161 实现,电路如下图所示:
实际上,这就实现了 8 位操作系统位宽、4 位控制存储器地址的程序指令顺序读取。
为了实现栈,首先需要存储空间,这里直接使用 256 字节的随机存储器(RAM),利用加减计数器和延时器(D 触发器)实现。其中 RAM 可由数据选择器实现。
在第一小节中,我将介绍一般的计算机的硬件结构。
“计算机之父” 约翰·冯·诺依曼在 1945 年的报告《First Draft of a Report on the EDVAC》中提出了冯诺依曼体系结构,其基本原则是:
采用二进制逻辑,以简化计算机的设计;
程序存储执行,以提高存储容量与运算速度;
计算机由五个部分组成:运算器、控制器、存储器、输入设备、输出设备。
其中程序分为指令和数据,统一存储在控制存储器中,使用的时候依次读取,或指定一次读取的程序中哪一部分是指令、哪一部分是数据。
哈佛结构是在冯诺依曼结构的基础上,将程序中的指令和数据分开存储,使用的时候分别读取,其结构复杂,但是有利于提高存储器带宽、数据吞吐率和架构稳定性。
中央处理器(CPU,Central Processing Unit)由运算器、控制器和输入输出设备组成。冯诺依曼结构计算机主要由 CPU 和程序存储器组成,哈佛结构的计算机则分为 CPU、程序存储器和数据存储器。
取指(IF,Instruction Fetch)即将一条指令从主存储器取到指令寄存器的过程。
具体来说,以程序计数器(PC,Programmer Counter)的输出作为主存储器的地址读取数据,当一条指令被取出之后,程序计数器中的数值将根据指令字长度自动递增。
指令译码(ID,Instruction Decode)即取出指令后,通过指令译码器对指令进行拆分和解释,识别指令类型和操作数。
执行指令阶段(EX,Execute),通过算术逻辑等单元具体实现指令的功能。
访存取数阶段(MEM,Memory),即访问存储器、取出地址中的具体数据。指令译码后得到操作数在主存中的地址,从主存中读取该操作数用于运算。部分指令不需要访问主存,则可以跳过该阶段。
结果写回阶段(WB,Write Back),一般在执行指令、对数据运算之后,需要将运行的数据结果写回存储器,一般为 CPU 的内部存储器中,以便被后续的指令快速地读取;有些指令的运行结果则写入程序计数器,以控制程序的运行流程。
一般来说,控制器由程序状态寄存器、系统状态寄存器、程序计数器、指令寄存器、指令译码器等组成。控制器主要完成取出指令、指令译码与结果写回的功能。
具体来说,由程序计数器提供地址,从控制存储器中取出指令,并进行译码,从而调用运算器,最后将运算器的结果写回存储器。若写回 CPU 内部寄存器,则可用于后续计算;若写回程序计数器中,则得以控制程序流程。
一般来说,运算器由算术逻辑单元、中间寄存器、运算累加器、描数字寄存器、B 寄存器等组成。运算器主要完成执行指令和访存取数的功能。
具体来说,根据指令译码结果,从 CPU 内部存储器(高速缓冲存储器)中取出操作数,并进行相应的算术逻辑运算,如与、或、非、异或、同或、加、减、乘、除、取余、移位等。
存储器可以根据地址存储或读入数据,有多种分类方式:
按存储介质:半导体存储器、磁表面存储器(如磁盘、磁带);
按存取方式:随机存储器、顺序存储器、半顺序存储器;
内容可变性:只读存储器(ROM)、随机读写存储器(RAM);
信息易失性:易失性存储器、非易失性存储器;
系统中位置:内部存储器、外部存储器;
系统中作用:主存储器、高速缓冲存储器、辅助存储器、控制存储器。
适配器可将输入的各种类型的数据转为二进制数据,将输出的二进制数据转为十进制、字符串、图象、声音等其他类型数据。
上方的若干条横向蓝线为控制总线(CB,Control Bus),用于从控制单元传输程序指令;
中间的若干条竖向蓝线为数据总线(DB,Data Bus),用于向计算单元传输存储数据。
右侧的若干条竖向蓝线为地址总线(AB,Address Bus),用于在指定 RAM 中读写数据。
左上角为程序计数器(PC,Program Counter);
中上侧为指令译码器(ID,Instruction Decoder);
中间偏右蓝色总线包围的部分均为运算器,其中:
左上角为逻辑运算单元(Logic Unit);
右上角为算术运算单元(Arithmetic Unit);
左下角位无符号数值判断单元(Unsigned Condition Unit);
右下角为有符号数值判断单元(Signed Condition Unit);
最右侧一列为存储器,从上往下依次为:
256 B 快速存储器;
64 KB 快速存储器;
64 KB 控制台存储器;
两个 256 B 的栈。
中下方的红色元件为一字节输入输出设备;
左侧上方为关卡显示屏,提供游戏可视化信息,本质上不属于该计算机;
左侧下方为控制台的显示屏,实时显示控制台存储器中的内容;
右下角为声输出设备。
该计算机为冯诺依曼体系(即指令与数据位于同一个存储器中)、LEG 架构;
CPU 的字长为 32 位,其中前 8 位为操作码,后 24 位为地址码;
数据总线宽度:16 位用于运算器从存储器中取数,8 位用于写回数据;
最大延时量:568,理论上最大延时量直接影响了 CPU 工作频率的上限;
逻辑门数量:3302019,绝大部分来自快速随机存储器;
CPI:即执行一条指令所需的平均时钟周期数,这里严格为 1,不过有些功能(如函数的返回、一字节乘法)需要两条指令才能完成;
高速缓冲存储器容量:6 字节,可直接寻址,且向运算器传输数据只需一个时钟周期;
内部存储器的总容量:128.75 KB,可直接寻址,但向运算器传输数据需要两个时钟周期;
存储器带宽:存储器的位宽均为 8 位,因此带宽在数值上等于 CPU 频率;
CPU 工作频率可直接在仿真时设置,因此在这里没有参考价值;
吞吐量、利用率、响应时间、CPU 执行时间等参数不便计算,故略;
目前只支持定点运算(加减乘除取余等),不支持浮点操作;
可见,目前该计算机十分简陋,性能并不好,在第四节中我将讨论可能的改进方向。
上图中的 PROGRAM 元件的介绍见 2.4.1 控制存储器,这里只介绍其它部分:
左侧中间的元件是为 64 位程序计数器(PC),其输出作为地址在控制存储器读取程序数据,每个时钟周期内取出指令与数据,并使地址加一,从而实现程序流程的顺序执行;
如果只有程序计数器和 8 位字长的控制存储器,那么我们只能实现 8 位的 CPU。这里在此基础上,通过译码器和寄存器,在
上述电路图仅做原理性说明,实际是图中的控制存储器已经集成了宽指令的功能,只需要将程序计数器的步长设置为 4,便可以从控制存储器右上方的四个引脚直接得到宽指令;
最后得到的宽指令中,第一个字节为操作码,用于指定运算的类型;后三个字节均为地址码,其中前两个地址码一般用于指定前两个操作数的寄存器地址,第三个地址码一般用于指定运算器输出结果写回的寄存器地址。
图中最上方的一条横向蓝线为操作码的总线,经分线器拆成八个一位数据;
图中左上角操作码下方的三条数据线,分别位参数一、参数二和参数三的地址码;
图中左下角的五条竖线位数据总线,左边三条分别是从寄存器中读出的参数一、参数二和参数三。
操作码中最高的两位,即 [7:6]
,表示立即数。
若操作码的第 7 位为 0,则使用寄存器中的数据,否则为立即数模式,利用左上角的开关将参数一改为指令中相应的地址码。最后的数据输出到第四条数据总线中。
操作码的第 6 位同理,最后的数据输出到第五条数据总线中。
参数三一般不支持立即数模式。
操作码的中间三位,即 [5:3]
,表示运算的类型;后三位,即 [2:0]
,表示具体的运算
若中间三位为 000,则为逻辑运算单元,后三位的说明见 2.3.1 逻辑运算单元;
若中间三位为 001,则为算术运算单元,后三位的说明见 2.3.2 算术运算单元;
若中间三位为 100,则为无符号条件判断单元,后三位的说明见 2.3.3 条件判断单元;
若中间三位为 101,则为有符号条件判断单元,后三位的说明见 2.3.3 条件判断单元;
若中间三位为 010,则为从存储器中读取数据,后三位的说明见 2.4 存储器;
若中间三位为 110,则为向存储器中写入数据,后三位的说明见 2.4 存储器;
中间三位为 011 和 111 的指令暂未设置,为空。
最上方的四条横向蓝线为控制总线
第一条为操作码,用于指定指令的具体运算;
第二条和第三条为参数一和参数二,用于指定操作数一和操作数二;
第三条为参数三,用于指定结果写回的寄存器地址。
上方的两条横向黄线分别为操作码第 7 位和第 6 位,高电平说明为立即数模式,禁用译码器。
目前只使用到了每个字节后三位的地址码:
000 - 101 分别位六个一字节数码寄存器的地址;
110 位程序计数器的地址;
111 位输入输出设备的地址。
若增加使用前五位地址码,则可以增加 248 个一字节寄存器(高速缓冲寄存器).
图中左侧六条竖线中,前两条、中间两条、后两条,分别为参数 1~3 地址码的译码结果后两位;
左下方的三条横线中,上方为程序计数器的输出,中间为程序计数器的置数使能端,下方为程序计数器的置入数据;
中间两个一位开关右侧的两条竖线用于控制寄存器的读写(这一部分未用到);
再右侧一条蓝线,为运算器结果数据写回的总线;
最右侧三条蓝线,分别是用三个参数的地址码从寄存器或计数器或输入端读取的数值;
右上角的两条横向黄线,上面一条为无符号条件判断的输出数据线,下面一条为有符号条件判断的输出数据线。
地址码的参数 1~3 可控制是否读入程序计数器的数据;
如果操作码的中间三位表示无符号判断,并且相应的判断结果为真,则向计数器中写入地址码的参数三对应寄存器中的数值;
如果操作码的中间三位表示有符号判断,并且相应的判断结果为真,则向计数器中写入地址码的参数三(是一个地址)。
左侧的两条竖向蓝线为输入数据总线
第一条线为参数一,第二条线为参数二;
在普通模式下传输地址码对应寄存器中存储的值;
在立即数模式下直接传输地址码的地址数据;
中间的八条竖向黄线属于控制总线
具体来说,这八条线是操作码后三位的译码结果;
用于控制逻辑运算单元中选取哪一路运算结果作为输出;
右侧的一条竖向蓝线为输出数据总线
任何时候,有且仅有一路数据输出到该总线中;
在该电路图之外,还有一个控制器,当且仅当操作码中间三位为 000(即逻辑运算单元)时,才会将该输出数据总线中的数据写回参数三对应地址的寄存器中。
当操作码中间三位为 000 时,启用逻辑运算单元。操作码后三位及其功能分别为:
操作码后三位 | 功能 |
---|---|
000 | 操作数一和操作数二与运算后输出 |
001 | 操作数一和操作数二或运算后输出 |
010 | 操作数一取非后输出 |
011 | 操作数一和操作数二异或运算后输出 |
100 | 操作数一和操作数二与非运算后输出 |
101 | 操作数一和操作数二或非运算后输出 |
110 | 直接输出操作数一(用于拷贝数据) |
111 | 操作数一和操作数二同或运算后输出 |
其中逻辑门均为八位按位操作,异或、与非、或非、同或由与、或、非实现。
见逻辑运算单元的线路说明,唯一不同的是,当且仅当操作码中间三位为 001 时,才会启用算术运算单元的输出。
操作码后三位 | 功能 |
---|---|
000 | 操作数一除以操作数二,得到商数 |
001 | 操作数一除以操作数二,得到余数 |
010 | 操作数一乘以操作数二,得低四位 |
011 | 操作数一乘以操作数二,得高四位 |
100 | 操作数一加上操作数二,不考虑进位 |
101 | 操作数一减去操作数二,不考虑进位 |
110 | 操作数一左移操作数二,低位补零 |
111 | 操作数一右移操作数二,高位补零 |
左侧两条蓝色竖线和中间两条蓝色竖线完全相同,均为输入数据总线
第一条线为参数一,第二条线为参数二;
在普通模式下传输地址码对应寄存器中存储的值;
在立即数模式下直接传输地址码的地址数据;
左边八条黄色竖线和右边八条黄色竖线完全相同,属于控制总线
具体来说,这八条线是操作码后三位的译码结果;
用于控制条件运算单元中选取哪一路运算结果作为输出;
下侧两条黄色横线为输出数据总线
任何时候,有且仅有一路数据输出到该总线中;
当且仅当操作码中间三位为 100 时启用第一条黄线;
当且仅当操作码中间三位为 101 时启用第二条黄线;
条件判断单元的数据写回及其功能见 2.2.3 计数器读写.
后三位操作码 | 功能 |
---|---|
000 | 当且仅当参数一等于参数二时输出 1 |
001 | 当且仅当参数一不等于参数二时输出 1 |
010 | 当且仅当参数一无符号小于参数二时输出 1 |
011 | 当且仅当参数一无符号小于等于参数二时输出 1 |
100 | 当且仅当参数一无符号大于参数二时输出 1 |
101 | 当且仅当参数一无符号大于等于参数二时输出 1 |
110 | 始终输出 1 |
111 | 始终输出 0 |
后三位操作码 | 功能 |
---|---|
000 | 当且仅当参数一等于参数二时输出 1 |
001 | 当且仅当参数一不等于参数二时输出 1 |
010 | 当且仅当参数一有符号小于参数二时输出 1 |
011 | 当且仅当参数一有符号小于等于参数二时输出 1 |
100 | 当且仅当参数一有符号大于参数二时输出 1 |
101 | 当且仅当参数一有符号大于等于参数二时输出 1 |
110 | 始终输出 1 |
111 | 始终输出 0 |
说明:
图中的 PROGRAM 元件即为控制存储器(CU,Control Unit),用于存储程序,包括指令和数据。
控制存储器本质上为数据选择器,通过左上角的引脚输入地址,作为控制端选择对应地址的数据作为输出。
控制存储器一般为只读存储器(ROM,Read-Only Memory),这里的字长为 8 位,内存为 256 字节(实际上最大为 64 KB,但我只用到了 256 字节)。
特殊的,在该游戏中,可以通过右上角的按钮编辑程序、添加汇编别名、监视各种存储器的状态等。
CPU 内部可由地址码直接寻址的寄存器称为高速缓存寄存器,具体说明见 2 地址码译码,这里寄存器寻址只用到了地址码后三位译码结果的低六位。
以一号寄存器为例,其逻辑电路图如下图所示:
中间的倒三角形元件为一字节寄存器
左上角的引脚有效时,读取寄存器中保存的数据;
左边中间的引脚有效时,向寄存器中写入数据;
左下角的引脚为待写入的数据;
右侧的引脚为读取数据时的输出端,不读取数据时为高阻态。
右侧三条蓝色竖线为数据总线,分别对应参数一、参数二和参数三;
数据的读取
左侧的或门对应的三条线路,分别为三个参数的译码结果,即当某一参数的地址指向该寄存器时,启用寄存器的读取端;
上方三条黄色横线也是三个参数的译码结果,当读取端为高电平时,通过控制开关的使能端,向相应的数据总线传输数据。
数据的写入
最下方的倒数第一条黄色横线表示参数一的译码结果,当从随机存储器中读取数据时,左侧的黄色竖线为高电平,向参数一的地址指向的寄存器中写入数据;
最下方的倒数第二条黄色横线表示参数三的译码结果,通过算术逻辑单元运算时,右侧的黄色竖线为高电平,向参数三的地址指向的寄存器中写入数据。
左边四条蓝色竖线为控制总线:
第一条线为指令码;
第二、三条线分别为操作数一和操作数二(即参数一和参数二地址指向的寄存器中保存的数值;或者在立即数模式下,直接传输地址);
第四条线为参数四,是一个地址,用于指定使用的随机存储器。
右侧的蓝色竖线为输出数据总线。
高速随机存储器(Fast RAM)的原理与 2.4.1 控制存储器的原理类似。
元件性能
普通的随机存储器存储容量与逻辑门数量相等,但是延迟高,延迟量也等于逻辑门数量;
高速随机存储器使用十倍的逻辑门数量,可将延迟量降为 10;
高速随机存储器几乎是所有访存设备中读写速度最快的。
元件功能
左上角第一个引脚高电平时,读取数据至输出端;
左上角第二个引脚高电平时,将输入数据保存至相应地址中;
左上角第三个引脚为八位的地址;
左上角第四个引脚为八位的输入数据;
右上角的引脚用于输出数据,当读取使能端为低电平时,输出引脚为高阻态。
操作码后六位
为 010 000 时,从高速随机存储器中读取地址为操作数二的数据,并将其写入操作数一指定的寄存器中;
为 110 000 时,向高速随机存储器写入操作数一指定的寄存器中保存的数据,地址由操作数二指定。
操作数三:用于指定一字节地址高速随机存储器,图中的 RAM 对应于 0000 0000。理论上可以有
实际功能
增加存储空间,从而可以实现更多的功能;
便于实现数组(用一个寄存器保存 RAM 中数组的起始地址)。
同单字节地址,只不过这时操作数三不再是地址,而是该地址指定的寄存器中保存的数据。即我们不再手动指定 RAM 的编号,而是在一个 RAM 里通过两字节的地址直接读写数据,此时一个 RAM 的内存为 64 KB。
操作码后六位
为 010 001 时,从高速随机存储器中读取数据,并将其写入操作数一指定的寄存器中;
为 110 001 时,向高速随机存储器写入操作数一指定的寄存器中保存的数据;
操作数一:用于指定高速缓冲寄存器的地址;
操作数二和操作数三:用于指定高速随机存储器的地址。
在结构上与上一小节中的高速随机存储器完全相同;
只不过这里存储的数据是用于在控制台中显示的 ASCII 码数据。
当操作码后六位为 010 010 时,从 RAM 中读取数据;
当操作码后六位为 110 010 时,向 RAM 中写入数据。
左边四条蓝色竖线为控制总线:
第一条线为指令码;
后三条线分别为操作数 1~3(即参数 1~3 的地址指向的寄存器中保存的数值;或者在立即数模式下,直接传输地址);
右侧的蓝色竖线为输出数据总线。
左下方的两条黄线用于在弹栈时控制寄存器的数据写入。
栈的逻辑电路见 1.3.4 压栈与弹栈. 这里只介绍其引脚功能:
左上角第一个引脚为高电平时,弹栈(POP),即将最新输入的数据输出,并删除该数据;
左上角第二个引脚为高电平时,压栈(PUSH),即保存数据;
左上角第三个引脚为数据的输入端;
右侧的引脚为数据的输出引脚,当弹栈引脚为低电平时,输出引脚为高阻态。
操作码后六位
为 010 011 时,启用第一个栈(用于保存函数的地址);
为 010 100 时,启用第二个栈(用于保存函数改写的寄存器中的原始数据)。
操作数一
后两位为 01 时,为弹栈;
后两位为 10 时,为压栈;
操作数二:即栈的输入数据。
线路与高速缓冲寄存器几乎相同,这里不再重述;
唯一不同的是,左上角多了一个一位开关,用于在不读取输入时关闭输入使能端。寄存器不需要这个元件,因为即使不需要,正常读取也无妨,但是输入端并非存储一字节,同一数据不能反复读取,而只能读取下一字节的数据;
最下方的连个与门和开关,用于控制各个寄存器和输入输出设备的使能端;
功能:控制台可将指定 RAM 中的二进制数据按 ASCII 码表译码后显示;
存储器:见 3 控制台存储。
见 4 其他适配器。
左侧第一条蓝色竖线为控制总线中的操作码;
左侧后三条蓝色竖线为数据总线,分别为操作数 1~3。
当操作码后六位为 110 011 时,启用声音设备;
操作数一为声音设备的命令
为 0 时,表示无操作;
为 1 时,表示播放;
为 2 时,表示重置并播放;
为 3 时,表示停止播放;
操作数二表示音符编号;
操作数三表示音高偏移量。
这里记操作数一为 $ A $,操作数二为
二进制数 | 汇编别名 | 功能说明 |
---|---|---|
00 000 000 | and | |
00 000 001 | or | |
00 000 010 | not | |
00 000 011 | xor | |
00 000 100 | nand | |
00 000 101 | nor | |
00 000 110 | cp | |
00 000 111 | xnor |
从下述例子中可见,只用与非门实现其他基本逻辑门,至多需要 5 个时钟脉冲和 2 个单位的存储空间。注意每一条指令都必须是四字节的。
xxxxxxxxxx
441# 用与非门实现其他逻辑运算
2const A r0
3const B r1
4const temp1 r2
5
6cp in _ A # 从输入读取数据至 A, 其中 _ 表示 0
7cp in _ B # 从输入读取数据至 B
8# 如果定义 to = 0, 那么上式可写为 cp in to A
9# 看着更接近自然语言了, 但太多的汇编别名也不是好事
10
11# and gate: 2 ticks, 1 temp
12nand A B temp1
13nand temp1 temp1 out
14
15# or gate: 3 ticks, 2 temps
16nand A A temp1
17nand B B temp2
18nand temp1 temp2 out
19
20# not gate: 1 tick, 1 temp
21nand A A out
22
23# xor gate: 5 ticks, 2 temps
24nand A A temp1
25nand temp1 B temp2
26nand B B temp1
27nand temp1 A temp1
28nand temp1 temp2 out
29
30# nand gate: 1 tick, 1 temp
31nand A B out
32
33# nor gate: 4 ticks, 2 temps
34nand A A temp1
35nand B B temp2
36nand A B temp1
37nand temp1 temp1 out
38
39# xnor gate: 5 ticks, 2 temps
40nand A A temp1
41nand B B temp2
42nand temp1 temp2 temp1
43nand A B temp2
44nand temp1 temp2 out
二进制数 | 汇编别名 | 功能说明 |
---|---|---|
00 001 000 | div | |
00 001 001 | mod | |
00 001 010 | mul0 | |
00 001 011 | mul1 | |
00 001 100 | add | |
00 001 101 | sub | |
00 001 110 | shl | |
00 001 111 | shr |
例子:
xxxxxxxxxx
231const A r0
2const B r1
3const temp r2
4
5cp in _ A # 从输入读取数据至 A
6cp in _ B # 从输入读取数据至 B
7
8# 下二式等价
9div+_i A 2 out
10shr+_i A 1 out
11
12# 下二式等价
13mod+_i A 2 out
14and+_i A 1 out
15
16# 上式与下二式等价
17shl+_i A 7 temp
18shr+_i temp 7 out
19
20# 第一式与后二式等价 (负数)
21sub+i_ 256 A out
22not A _ temp
23add+_i temp 1 out
二进制数 | 汇编别名 | 功能说明 |
---|---|---|
00 100 000 | if_eq | |
00 100 001 | if_ne | |
00 100 010 | if_lt | |
00 100 011 | if_le | |
00 100 100 | if_gt | |
00 100 101 | if_ge | |
00 100 110 | gotoi | 程序始终跳转 |
00 100 111 | (无) | 程序永不跳转 |
说明:无符号条件判断在跳转时,程序计数器的地址改写为参数三(是一个地址)。
二进制数 | 汇编别名 | 功能说明 |
---|---|---|
00 101 000 | (无) | |
00 101 001 | (无) | |
00 101 010 | (无) | |
00 101 011 | (无) | |
00 101 100 | (无) | |
00 101 101 | (无) | |
00 101 110 | goto | 程序始终跳转 |
00 101 111 | (无) | 程序永不跳转 |
00 001 000 | signed | 无实际含义 |
说明:
有符号条件判断在跳转时,程序计数器的地址改写为操作数三,即参数三地址指向的寄存器中存储的数据。
这里没有给有符号条件判断添加汇编别名,不过可以用无符号条件判断的汇编别名加上 signed
得到,如 if_eq+signed r0 r1 label_1
。
二进制数 | 汇编别名 | 功能说明 |
---|---|---|
00 000 000 | _ | 如果一条指令不需要使用某个参数, 则可使用该汇编别名作为占位符。 |
10 000 000 | i_ | 操作数一直接等于参数一, 而不是其指向寄存器中的值。 |
01 000 000 | _i | 操作数二直接等于参数二, 而不是其指向寄存器中的值。 |
11 000 000 | ii | 操作数一和操作数二均为立即数模式。 |
例子:
xxxxxxxxxx
61cp+i_ 1 _ r0 # r0 = 1;
2label test
3add r0 r0 r0 # r0 *= 2;
4sub+i_ 256 r0 r0 # r0 = 256 - r0, 即 r0 = -r0;
5if_ne+signed+_i r0 3 test # 有符号数 r0 不等于 8 时, 重复上述循环
6# end_test
二进制数 | 汇编别名 | 功能说明 |
---|---|---|
0000 0000 | r0 | 0 号寄存器 |
0000 0001 | r1 | 1 号寄存器 |
0000 0010 | r2 | 2 号寄存器 |
0000 0011 | r3 | 3 号寄存器 |
0000 0100 | r4 | 4 号寄存器 |
0000 0101 | r5 | 5 号寄存器 |
0000 0110 | cnt | 程序计数器 |
0000 0111 | in / out | 输入 / 输出 |
二进制数 | 汇编别名 | 功能说明 |
---|---|---|
00 010 000 | ram0 | 256 B RAM |
00 010 001 | ram1 | 64 KB RAM |
00 010 011 | stkfun | 函数地址的栈 |
00 010 100 | stkpar | 暂存数据的栈 |
00 000 000 | read | 不可单独使用; 用于读取数据。 |
00 100 000 | write | 不可单独使用; 用于写入数据。 |
00 000 001 | pop | 不可单独使用; 用于弹栈。 |
00 000 010 | push | 不可单独使用; 用于压栈。 |
例 1:
xxxxxxxxxx
171const addr0 r0 # 低位地址
2const addr1 r1 # 高位地址
3const temp r2 # 暂时存储
4const number 5 # 读取数量
5
6label write_data # 开始从输入读取数据,并写入内存
7write+ram1 in addr1 addr0 # 从输入读取数据并写入 ram1
8add+_i addr0 1 addr0 # 低位地址加一
9if_ne+_i addr0 number write_data # 直到写入 5 个数据才结束循环
10# end_write_data
11
12cp+i_ 0 _ addr0 # 将地址重置为零
13label read_data # 开始从内存读取数据,并依次输出
14read+ram1 out addr1 addr0 # 从 ram1 读取数据并输出
15add+_i addr0 1 addr0 # 低位地址加一
16if_ne+_i addr0 number read_data # 直到读取 5 个数据才结束循环
17# end_read_data
例 2:
xxxxxxxxxx
61stkpar push in _ # 入栈 1
2stkpar push in _ # 入栈 2
3stkpar pop _ out # 出栈 2
4stkpar push in _ # 入栈 3
5stkpar pop _ out # 出栈 3
6stkpar pop _ out # 出栈 1
二进制数 | 汇编别名 | 功能说明 |
---|---|---|
00 010 010 | cin | 从控制台读取数据 |
00 110 010 | cout | 向控制台输出数据 |
00 110 011 | sound | 控制声音设备 |
下述例子可以向控制台输出 Hello World 等信息,注意还需要预先定义 ASCII 码表,见。效果见 3 示例视频。
xxxxxxxxxx
671const addr0 r0
2const addr1 r1
3const temp r2
4
5cp+i_ 0 _ addr0
6cout+i_ ch_H addr1 addr0 add+_i addr0 1 addr0
7cout+i_ ch_e addr1 addr0 add+_i addr0 1 addr0
8cout+i_ ch_l addr1 addr0 add+_i addr0 1 addr0
9cout+i_ ch_l addr1 addr0 add+_i addr0 1 addr0
10cout+i_ ch_o addr1 addr0 add+_i addr0 1 addr0
11cout+i_ ch_space addr1 addr0 add+_i addr0 1 addr0
12cout+i_ ch_w addr1 addr0 add+_i addr0 1 addr0
13cout+i_ ch_o addr1 addr0 add+_i addr0 1 addr0
14cout+i_ ch_r addr1 addr0 add+_i addr0 1 addr0
15cout+i_ ch_l addr1 addr0 add+_i addr0 1 addr0
16cout+i_ ch_d addr1 addr0 add+_i addr0 1 addr0
17cout+i_ ch_excla addr1 addr0 add+_i addr0 1 addr0
18
19cp+i_ 0 _ temp
20label wait_1
21add+_i temp 1 temp
22if_ne+_i temp 10 wait_1
23# end wait_1
24
25label bs_chars_1
26sub+_i addr0 1 addr0
27cout+i_ 0 addr1 addr0
28if_ne+_i addr0 5 bs_chars_1
29# end bs_chars_1
30
31cp+i_ 80 _ addr0
32cout+i_ ch_S addr1 addr0 add+_i addr0 1 addr0
33cout+i_ ch_l addr1 addr0 add+_i addr0 1 addr0
34cout+i_ ch_e addr1 addr0 add+_i addr0 1 addr0
35cout+i_ ch_e addr1 addr0 add+_i addr0 1 addr0
36cout+i_ ch_p addr1 addr0 add+_i addr0 1 addr0
37cout+i_ ch_C addr1 addr0 add+_i addr0 1 addr0
38cout+i_ ch_l addr1 addr0 add+_i addr0 1 addr0
39cout+i_ ch_o addr1 addr0 add+_i addr0 1 addr0
40cout+i_ ch_u addr1 addr0 add+_i addr0 1 addr0
41cout+i_ ch_d addr1 addr0 add+_i addr0 1 addr0
42cout+i_ ch_excla addr1 addr0 add+_i addr0 1 addr0
43
44cp+i_ 0 _ temp
45label wait_2
46add+_i temp 1 temp
47if_ne+_i temp 10 wait_2
48# end wait_2
49
50label bs_chars_2
51sub+_i addr0 1 addr0
52cout+i_ 0 addr1 addr0
53if_ne+_i addr0 80 bs_chars_2
54# end bs_chars_2
55
56cp+i_ 5 _ addr0
57label bs_chars_3
58sub+_i addr0 1 addr0
59cout+i_ 0 addr1 addr0
60if_ne+_i addr0 0 bs_chars_3
61# end bs_chars_3
62
63cp+i_ 0 _ temp
64label wait_3
65add+i_ temp 1 temp
66if_ne+_i temp 10 wait_3
67# end wait_3
我们已经可以通过控制器和运算器向寄存器中读取数据、进行运算并写入数据了,如果可以实现程序的跳转,则该计算机具有图灵完备性。不过准确来说,图灵机要求内存无限,而我们只能实现有限的内存,因此实际上实现的是有限下推自动机(Push-down Automata)。
程序计数器以输出数据为地址,使控制存储器顺序输出程序指令。该计算机提供了三种控制程序流程的方式:
xxxxxxxxxx
81# 法一
2cp in _ cnt # 直接将输入数据写入计数器
3cp r0 _ cnt # 直接将寄存器的数据写入计数器
4
5# 法二
6cpi label_a _ cnt # 直接将 label_a 处的字节数写入计数器
7# ... (假装这里有很多代码)
8label label_a
xxxxxxxxxx
71# 法一: 判断后跳转
2label label_1
3# ... (假装这里有很多代码)
4if_eq+_i r0 2 label_1
5
6# 法二: 强制跳转
7gotoi _ _ label_1
xxxxxxxxxx
81# 法一: 判断后跳转
2label label_s
3# ... (假装这里有很多代码)
4if_eq+_i+signed r0 2 label_s
5
6# 法二: 强制跳转
7const addr r0
8goto _ _ addr
从硬件的角度来说,实际上只有 if not
的语句,当表达式成立时则跳过下一片段,不成立时则执行下一片段。该计算机的指令集中并没有 if
与 switch
等关键字,不过可以通过其他方式实现相同的功能。高级语言的事交给编译器吧,这里不作讨论。
另外,汇编代码中可以定义常量,在编译时将其替换为相应数值即可;定义变量,实际上就是为该变量分配内存,在书写时即把内存地址作为常量赋给变量名。为了便于使用,一般使用高速缓冲寄存器的地址;虽然 RAM 也是可以直接寻址的,但是运算前需要一个时钟脉冲将 RAM 中的数据读取至寄存器中,运算后还需要一个时钟脉冲将结果写回 RAM。
由跳转语句实现 if 语句的功能:
xxxxxxxxxx
131if_ge+_i a 3 a_ge_3
2# if (a < 3)
3# do something here
4gotoi _ _ end_if_a
5label a_ge_3
6if_ge+_i a 10 a_ge_10
7# else if (a < 10)
8# do something here
9gotoi _ _ end_if_a
10label a_ge_10
11# else, i.e. a >= 10
12# do something here
13label end_if_a
上述语句相当于 C 语言中的:
xxxxxxxxxx
71if (a < 3) {
2 //
3} else if (a < 10) {
4 //
5} else {
6 //
7}
为了清晰,可以使用下述结构的代码:
xxxxxxxxxx
171if_eq+_i a 0 a_eq_0
2if_eq+_i a 1 a_eq_1
3if_eq+_i a 2 a_eq_2
4gotoi _ _ default
5
6label a_eq_0
7# do something here
8gotoi _ _ end_switch
9label a_eq_1
10# do something here
11gotoi _ _ end_switch
12label a_eq_2
13# do something here
14gotoi _ _ end_switch
15label default
16# do something here
17label end_switch
上述代码相当于 C 语言中的:
xxxxxxxxxx
141switch (a) {
2 case 0: {
3 //
4 } break;
5 case 1: {
6 //
7 } break;
8 case 2: {
9 //
10 } break;
11 default: {
12 //
13 }
14}
条件语句要求跳过某一程序片段,而循环语句要求重复执行某一程序片段。硬件上只能实现 while (true) {}
,而依靠条件语句跳转出循环。
xxxxxxxxxx
51label a_loop
2# do something
3if_eq+_i a 0 a_loop
4if_eq+_i a 1 end_a_loop
5label end_a_loop
上述代码相当于 C 语言中的:
xxxxxxxxxx
51while (true) {
2 // do something
3 if (a == 0) continue;
4 if (a == 1) break;
5}
xxxxxxxxxx
91label while_loop
2if_ne+_i a 1 end_while_loop # 注意是 if_ne、end_while_loop
3# do something
4label end_while_loop
5
6label do_while_loop
7# do something
8if_eq+_i a 1 do_while_loop # 注意是 if_eq、do_while_loop
9label end_do_while_loop
上述代码相当于 C 语言中的:
xxxxxxxxxx
71while (a == 1) {
2 //
3}
4
5do {
6 //
7} while (a == 1);
xxxxxxxxxx
91const n 5
2const i r0
3cp+i_ 0 _ i
4label for_loop
5if_eq+_i i n end_for_loop
6# do something here
7add+_i i 1 i
8gotoi _ _ for_loop
9label end_for_loop
上述代码相当于 C 语言中的:
xxxxxxxxxx
41int n = 5;
2for (int i = 0; i < n; ++i) {
3 //
4}
所谓函数,就是为了重复利用某段代码,以简化编程流程。与之前的循环语句不同,函数要求在程序中的任意一个地方都可以调用(call)这段代码,并且在调用结束后可以从函数结尾跳转回到(return)程序中原来的位置。
具体来说,可以将函数调用点处的地址存储起来,当函数调用结束后,再将其写入程序计数器中。但是函数的调用存在递归与嵌套的问题,用普通的寄存器无法实现地址的自动存储与取出。先调用的函数后返回,最后调用的函数最先返回,而栈的工作模式刚好满足这样的需求。
为了实现函数的功能,需要完成两件事:
函数的调用(call)
将程序计数器的值和指令长度相加;
将上述值压入栈顶;
令程序计数器跳转到函数入口处。
函数的返回(return)
从栈顶弹出地址值;
将上述值写入程序计数器中。
注意为了实现函数的返回,应当返回调用函数的下一条指令,否则将陷入死循环中。
思路一:直接使用随机存储器,通过程序在软件上实现栈;
思路二:从硬件上实现栈,程序直接调用该功能(指令)即可。
我们可以通过 RAM 从程序上实现栈,从而实现函数的功能:
xxxxxxxxxx
241const addr r0
2const temp r1
3# 初始状态默认为零
4
5# begin main()
6# 调用函数
7add+_i cnt 8 temp
8gotoi _ _ function_1
9
10gotoi _ _ end_main
11# end main()
12
13# 函数定义
14label function_1
15write+ram0 temp addr 0 # 将 temp 压入栈
16add+_i addr 1 addr # 压栈后地址自增
17# begin function_1()
18# do something here # 函数主体
19# end function_1()
20sub+_i addr 1 addr # 弹栈前地址自减
21read+ram0 temp addr 0 # 弹出栈至 temp
22goto _ _ temp # 注意不是 gotoi
23
24label end_main
在 1.3.4 压栈与弹栈中,我们在硬件上实现了栈结构;在 4 压栈与弹栈中,我们将两个栈结构添加到了计算机硬件中。由此,我们可以直接利用栈结构更加便捷地实现函数,而无需花一个字节存储地址、手动指定栈的压入与弹出:
xxxxxxxxxx
201const temp r0
2
3# begin main()
4# 调用函数
5add+_i cnt 8 temp
6gotoi _ _ function_1
7
8gotoi _ _ end_main
9# end main()
10
11# 函数定义
12label function_1
13stkfun push temp _ # 将 temp 压入栈
14# begin function_1()
15# do something here # 函数主体
16# end function_1()
17stkfun pop _ temp # 弹出栈至 temp
18goto _ _ temp # 跳转至程序调用处
19
20label end_main
在 3.2 的基础上,我们可以在硬件中直接实现 2.1 中函数调用的三个步骤和函数返回的两个步骤。这并不困难,不过需要增加电路和指令,这里略去。
在 C 语言中,函数传参有三种方式:
值传递的方式,将实际参数的值拷贝至形式参数,函数在执行过程中使用形式参数,即使修改了形式参数,实际参数的值夜不会发生变化。
地址传递的方式,将实际参数的存储地址传给形式参数,尽管函数在执行过程中,修改形式参数不会改变实际参数的存储地址,但是可以直接对存储地址上的数据进行操作,从而改变实际参数的值。
引用传递的方式,直接使用实际参数进行运算,在函数内修改参数,也会直接影响到实际参数的值。
最直接的方式是使用引用传递,即将要传递的参数固定放在某个寄存器中,函数在调用时直接使用该寄存器的值,而不进行任何其他的处理。不过仍要注意,函数调用过程中可能需要新的内存,需要确保函数不会改变无关的有用信息,因此一种处理方式是将函数可能改变的寄存器中的值,都预先拷贝到 RAM 中,或者压入栈中,待函数结束后,再从 RAM 或栈中取出数据。
值传递则是将函数可能改变的所有寄存器中的值(包括传入的参数)都预先压入栈中,于是虽然使用的形式参数的地址与原先实际参数的地址相同,但由于执行结束后将从栈中取出数据,覆盖原地址中的值,因此最终不会改变实际参数的值。
这里以引用传递为例,实现两个数字的交换:
xxxxxxxxxx
271const A r0
2const B r1
3const temp r2
4
5# begin main()
6cp in _ A # 输入 A
7cp in _ B # 输入 B
8
9add+_i cnt 8 temp # 预处理, 以实现程序的返回
10gotoi _ _ swap # 调用函数, 交换 A 和 B
11
12gotoi _ _ end_main # 结束主函数
13# end main()
14
15label swap
16stkfun push temp _ # 这两行代码用一行就够了,
17stkpar push temp _ # 只是作为例子体现不同的思路.
18# begin swap(A, B)
19cp A _ temp
20cp B _ A
21cp temp _ B
22# end swap(A, B)
23stkpar pop _ temp
24stkfun pop _ temp
25goto _ _ temp
26
27label end_main
欲实现函数的返回值,只需要预先指定存储返回值的寄存器,并在函数结束前将需要返回的数据存入该寄存器即可。与传入的参数不同,函数的返回值不需要使用栈的结构。返回值使用固定的寄存器,因此只是暂时的,如果需要返回值,则可以将其拷贝到另一寄存器中。即高级语言中 max(a, b)
和 c = max(a, b)
的区别。
由于直接在硬件的层面操作,函数的返回值可以直接设置为多个,也可以返回数组的地址,只需要预先指定相应的寄存器即可。
xxxxxxxxxx
881const disk_nr r0
2const source r1
3const dest r2
4const spare r3
5const temp r4
6const switch 5
7# not r5, though r5 = 5.
8const delta 8
9
10# begin main()
11# scan input
12cp in _ disk_nr
13cp in _ source
14cp in _ dest
15cp in _ spare
16
17# call function
18add+_i cnt delta temp
19gotoi _ _ fun_move
20
21# return
22gotoi _ _ end
23# end main()
24
25label fun_move
26stkfun push temp _
27# begin fun_move(, , , )
28if_ne+_i disk_nr 0 exe_subtasks
29# if (disk_nr == 0)
30# move disk to destination
31cp source _ out
32cp+i_ switch _ out
33cp dest _ out
34cp+i_ switch _ out
35gotoi _ _ end_move
36label exe_subtasks
37# else if (disk_nr != 0)
38# subtask 1
39# 1.1 push params (save)
40stkpar push disk_nr _
41stkpar push source _
42stkpar push dest _
43stkpar push spare _
44# 1.2 recursive call
45sub+_i disk_nr 1 disk_nr
46cp spare _ temp
47cp dest _ spare
48cp temp _ dest
49add+_i cnt delta temp
50gotoi _ _ fun_move
51# 1.3 pop params (read)
52stkpar pop _ spare
53stkpar pop _ dest
54stkpar pop _ source
55stkpar pop _ disk_nr
56
57# subtask 2
58# move disk to destination
59cp source _ out
60cp+i_ switch _ out
61cp dest _ out
62cp+i_ switch _ out
63
64# subtask 3
65# 3.1 push params (save)
66stkpar push disk_nr _
67stkpar push source _
68stkpar push dest _
69stkpar push spare _
70# 3.2 recursive call
71sub+_i disk_nr 1 disk_nr
72cp spare _ temp
73cp source _ spare
74cp temp _ source
75add+_i cnt delta temp
76gotoi _ _ fun_move
77# 3.3 pop params (read)
78stkpar pop _ spare
79stkpar pop _ dest
80stkpar pop _ source
81stkpar pop _ disk_nr
82label end_move
83# gotoi address saved in temp
84stkfun pop _ temp
85goto _ _ temp
86# end fun_move(, , , )
87
88label end
xxxxxxxxxx
481const left 0
2const move 1
3const right 2
4const skip 3
5const interact 4
6const laser 5
7
8const conveyor 92
9const obj r0
10const num r1
11const temp r2
12const i r3
13
14cp+i_ right _ out
15cp+i_ move _ out
16cp+i_ right _ out
17
18label move4
19cp+i_ move _ out
20add+_i temp 1 temp
21if_lt+_i temp 4 move4
22
23cp+i_ right _ out
24cp+i_ move _ out
25cp+i_ left _ out
26cp+i_ move _ out
27
28label input_fruit
29cp+i_ skip _ out
30cp in _ obj
31if_eq+_i obj conveyor input_fruit
32
33cp+i_ 0 _ i
34label check_fruit
35if_eq i num end_check_fruit
36read+ram0 temp i 0
37if_ne obj temp check_fruit
38
39cp+i_ right _ out
40cp+i_ interact _ out
41cp+i_ left _ out
42gotoi _ _ input_fruit
43label end_check_fruit
44
45write+ram0 obj num 0
46add+_i num 1 num
47gotoi _ _ input_fruit
48label end_input_fruit
xxxxxxxxxx
501const width 16
2const maxH 10
3const V r0
4const i r1
5const j r2
6const temp1 r3
7const temp2 r4
8const side r5
9
10label read_all
11cp in _ temp1
12write+ram0 temp1 i 0
13add+_i i 1 i
14if_ne+_i i width read_all
15label end_read_all
16
17cp+i_ 0 _ i
18label for_i # vertical
19cp+i_ 0 _ j
20cp+i_ 0 _ side
21label for_j # horizontal
22if_ne+_i side 0 left_side
23# if parity is even
24read+ram0 temp1 j 0
25if_lt temp1 i end_if_p
26# else it's not empty
27cp+i_ 1 _ side
28cp j _ temp1
29gotoi _ _ end_if_p
30label left_side
31# if parity is odd
32read+ram0 temp2 j 0
33if_lt temp2 i end_if_p
34sub j temp1 temp2
35sub+_i temp2 1 temp2
36add V temp2 V
37cp j _ temp1
38label end_if_p
39
40add+_i j 1 j
41if_gt+_i j width end_for_j
42gotoi _ _ for_j
43label end_for_j
44
45add+_i i 1 i
46if_gt+_i i maxH end_for_i
47gotoi _ _ for_i
48label end_for_i
49
50cp V _ out
xxxxxxxxxx
151# 生成随机数
2const seed r0
3const temp1 r1
4const temp2 r2
5
6cp in _ seed
7label dance
8shr+_i seed 1 temp1
9xor seed temp1 temp1
10shl+_i temp1 1 temp2
11xor temp1 temp2 temp2
12shr+_i temp2 2 seed
13xor temp2 seed seed
14and+_i seed 0b11 out
15gotoi _ _ dance
xxxxxxxxxx
621const num r0
2const i r1
3const j r2
4# inum = num
5const jnum r3
6const temp1 r4
7const temp2 r5
8# In order to compare, we have to
9# define both temp1 and temp2.
10
11# Code below can scan 256 bytes
12# at most, until input is 0,
13# which misunderstands the question.
14# label scan_all
15# cp in _ temp1
16# if_eq+_i temp1 0 end_scan_all
17# write+ram0 temp1 num 0
18# add+_i num 1 num
19# gotoi _ _ scan_all
20# label end_scan_all
21
22cp+i_ 15 _ num
23label scan_all
24write+ram0 in i 0
25add+_i i 1 i
26if_eq i num end_scan_all
27gotoi _ _ scan_all
28label end_scan_all
29
30cp+i_ 0 _ i
31label for_i
32# same as for (int i=1; i<n; ++i)
33add+_i i 1 i
34if_ge i num end_for_i
35
36cp+i_ 0 _ j
37sub num i jnum
38label for_j
39# for (int j=0; j<n-i; ++j)
40if_ge j jnum for_i
41# same as break and gotoi
42read+ram0 temp1 j 0
43add+_i j 1 j
44read+ram0 temp2 j 0
45if_le temp1 temp2 for_j
46# if (a[j] > a[j+1])
47write+ram0 temp1 j 0
48sub+_i j 1 j
49write+ram0 temp2 j 0
50add+_i j 1 j
51gotoi _ _ for_j
52# Continue for_i from for_j, so
53# it's needless to write it here.
54label end_for_i
55
56cp+i_ 0 _ i
57label print_all
58read+ram0 out i 0
59add+_i i 1 i
60if_eq i num end_print_all
61gotoi _ _ print_all
62label end_print_all
xxxxxxxxxx
191const isCapital r1
2const letter r2
3const delta 32
4const space 32
5cp+i_ 1 _ isCapital
6
7label capitalize
8if_eq+_i isCapital 0 notCapital
9sub+_i in delta out
10cp+i_ 0 _ isCapital
11gotoi _ _ capitalize
12label notCapital
13cp in _ letter
14if_ne+_i letter space send
15cp+i_ 1 _ isCapital
16label send
17cp letter _ out
18gotoi _ _ capitalize
19# end_capitalize
xxxxxxxxxx
1281const ch_nul = 0
2const ch_soh = 1
3const ch_stx = 2
4const ch_etx = 3
5const ch_eot = 4
6const ch_enq = 5
7const ch_ack = 6
8const ch_bel = 7
9const ch_bs = 8
10const ch_tab = 9
11const ch_lf = 10
12const ch_vt = 11
13const ch_ff = 12
14const ch_ck = 13
15const ch_so = 14
16const ch_si = 15
17const ch_dle = 16
18const ch_dc1 = 17
19const ch_dc2 = 18
20const ch_dc3 = 19
21const ch_dc4 = 20
22const ch_nak = 21
23const ch_syn = 22
24const ch_etb = 23
25const ch_can = 24
26const ch_em = 25
27const ch_sub = 26
28const ch_esc 27
29const ch_fs = 28
30const ch_gs = 29
31const ch_es = 30
32const ch_us = 31
33const ch_space = 32
34const ch_excla = 33
35const ch_dquote = 34
36const ch_hash = 35
37const ch_dollar = 36
38const ch_percent = 37
39const ch_and = 38
40const ch_squote = 39
41const ch_lpqty = 40
42const ch_rpqty = 41
43const ch_times = 42
44const ch_plus = 43
45const ch_comma = 44
46const ch_minus = 45
47const ch_dot = 46
48const ch_slash = 47
49const ch_0 = 48
50const ch_1 = 49
51const ch_2 = 50
52const ch_3 = 51
53const ch_4 = 52
54const ch_5 = 53
55const ch_6 = 54
56const ch_7 = 55
57const ch_8 = 56
58const ch_9 = 57
59const ch_colon = 58
60const ch_semic = 59
61const ch_lt = 60
62const ch_eq = 61
63const ch_gt = 62
64const ch_quest = 63
65const ch_at = 64
66const ch_A = 65
67const ch_B = 66
68const ch_C = 67
69const ch_D = 68
70const ch_E = 69
71const ch_F = 70
72const ch_G = 71
73const ch_H = 72
74const ch_I = 73
75const ch_J = 74
76const ch_K = 75
77const ch_L = 76
78const ch_M = 77
79const ch_N = 78
80const ch_O = 79
81const ch_P = 80
82const ch_Q = 81
83const ch_R = 82
84const ch_S = 83
85const ch_T = 84
86const ch_U = 85
87const ch_V = 86
88const ch_W = 87
89const ch_X = 88
90const ch_Y = 89
91const ch_Z = 90
92const ch_lbqty = 91
93const ch_bslash = 92
94const ch_rbqty = 93
95const ch_xor = 94
96const ch__ = 95
97const ch_grave = 96
98const ch_a = 97
99const ch_b = 98
100const ch_c = 99
101const ch_d = 100
102const ch_e = 101
103const ch_f = 102
104const ch_g = 103
105const ch_h = 104
106const ch_i = 105
107const ch_j = 106
108const ch_k = 107
109const ch_l = 108
110const ch_m = 109
111const ch_n = 110
112const ch_o = 111
113const ch_p = 112
114const ch_q = 113
115const ch_r = 114
116const ch_s = 115
117const ch_t = 116
118const ch_u = 117
119const ch_v = 118
120const ch_w = 119
121const ch_x = 120
122const ch_y = 121
123const ch_z = 122
124const ch_lBqty = 123
125const ch_vqty = 124
126const ch_rBqty = 125
127const ch_sim = 126
128const ch_delta = 127
目前可以通过 putchar 输出可打印字符、退格并删除字符、换行,不过其他一些控制符的功能暂未实现。
xxxxxxxxxx
841# putchar
2const addr0 r0
3const addr1 r1
4const char r2
5const temp r3
6const temp2 r4
7const delta 8
8
9# begin main()
10cout+i_ ch_H addr1 addr0 add+_i addr0 1 addr0
11cout+i_ ch_e addr1 addr0 add+_i addr0 1 addr0
12cout+i_ ch_l addr1 addr0 add+_i addr0 1 addr0
13cout+i_ ch_l addr1 addr0 add+_i addr0 1 addr0
14cout+i_ ch_o addr1 addr0 add+_i addr0 1 addr0
15cout+i_ ch_space addr1 addr0 add+_i addr0 1 addr0
16cout+i_ ch_w addr1 addr0 add+_i addr0 1 addr0
17cout+i_ ch_o addr1 addr0 add+_i addr0 1 addr0
18cout+i_ ch_r addr1 addr0 add+_i addr0 1 addr0
19cout+i_ ch_l addr1 addr0 add+_i addr0 1 addr0
20cout+i_ ch_d addr1 addr0 add+_i addr0 1 addr0
21cout+i_ ch_excla addr1 addr0 add+_i addr0 1 addr0
22
23label bs6chars
24cp+i_ ch_bs _ char
25add+_i cnt delta temp
26gotoii _ _ putchar
27add+_i temp2 1 temp2
28if_ne+_i temp2 6 bs6chars
29# end bs5chars
30
31# cp+i_ ch_lf _ char
32# add+_i cnt delta temp
33# gotoii _ _ putchar
34
35cp+i_ 80 _ addr0
36
37cout+i_ ch_L addr1 addr0 add+_i addr0 1 addr0
38cout+i_ ch_E addr1 addr0 add+_i addr0 1 addr0
39cout+i_ ch_G addr1 addr0 add+_i addr0 1 addr0
40cout+i_ ch_excla addr1 addr0 add+_i addr0 1 addr0
41
42gotoi _ _ end_main
43# end main()
44
45label putchar
46stkfun push temp _
47# begin putchar()
48if_lt+_i char 32 is_null
49# if printable
50cout char addr1 addr0
51add+_i addr0 1 addr0
52gotoi _ _ end_if_char
53label is_null
54if_ne+_i char ch_nul is_bs
55# if NULL
56cout char addr1 addr0
57# address stay unchanged
58gotoi _ _ end_if_char
59label is_bs
60if_ne+_i char ch_bs is_tab
61# if backspace
62andi addr0 0b1111 temp
63if_eq+_i temp 0 end_if_char
64subi2 addr0 1 addr0
65cout+i_ ch_nul addr1 addr0
66# you can delete last line
67gotoi _ _ end_if_char
68label is_tab
69if_ne+_i char ch_tab is_lf
70# if tab
71add+_i addr0 4 addr0
72gotoi _ _ end_if_char
73label is_lf
74if_ne+_i char ch_lf end_if_char
75# if enter (new line)
76add+_i addr0 80 addr0
77andi addr0 0xC0 addr0
78gotoi _ _ end_if_char
79label end_if_char
80# end putchar()
81stkfun pop _ temp
82goto _ _ temp
83
84label end_main
总的来说,指令 = 操作码(op) + 地址码(
三地址指令:
源操作数地址
终点操作数地址
结果地址
二地址指令:
存储器-存储器(SS)型指令
寄存器-寄存器型(SS)型指令
寄存器-存储器(RS)型指令
一地址指令:如
零地址指令.
在本次实现的计算机中,主要使用三地址指令,少数运算使用二地址指令,没有一地址指令与零地址指令。
指令字长是相对于机器字长而言的,如对于 32 位的 CPU,单字长指令即 32 位指令,半字长指令即 16 位指令,双字长指令即 64 位指令。
对于一个计算机的指令系统来说,如果各种指令字长相等,则称为等长指令字结构,否则则称为边长指令字结构。
本次实现的计算机为 32 位 CPU、单字长指令,即每条指令宽 32 位,前 8 位为操作码,后 24 位为三个参数的地址码。
指令有两种寻址方式:顺序寻址和跳跃寻址。
每个时钟脉冲,程序计数器的输出都会加一,以此为控制存储器的地址,可以实现程序指令的顺序寻址。
通过将运算器的结果利用置数端写回程序计数器,可以实现程序指令的跳跃寻址。一般使用运算器中的条件判断单元实现跳跃寻址。
不定长指令系统的寻址方式则更加复杂,即使是顺序寻址,也需要根据指令的类型判断其长度,进而决定程序计数器输出的增加量。
相比于指令寻址,操作数的寻址更加复杂,常用的寻址方式有:
隐含寻址:如以累加寄存器 AC 作为第二操作数地址;
立即寻址:值为立即数;
直接寻址:值为地址;
间接寻址:值为指示器地址,其中的值才为操作数地址;
寄存器直接寻址:值为寄存器的编号;
寄存器间接寻址:值为寄存器的编号,其中的值才为操作数地址;
偏移寻址:相对寻址、基址寻址、变址寻址;
段寻址;
堆栈寻址。
在本次实现的计算机中,主要使用立即寻址、直接寻址、间接寻址、寄存器直接寻址、寄存器间接寻址与堆栈寻址。
早起计算机的主频低、运算速度慢,为了在有限的时钟周期内完成尽可能多的功能,人们希望通过一条指令便可以实现更多的功能。例如由与非运算可以得到其他六种基本逻辑运算,但是至多需要五次运算,效率较低。而如果直接将各种逻辑运算都在硬件的层面实现了,使用时则只需一条指令,在一个时钟周期内即可实现。
由此便诞生了复杂指令集(CISC,Complex Instruction Set Computer),即把越来越多的复杂指令添加到系统中。在复杂指令集中,控制字的数量及时钟周期的数量对于每一条指令来说都可以是不同的,即变长指令系统。
指令丰富,功能强大,相同数量的时钟周期内可以完成更多功能;
指令众多,寻址方式灵活,有着较强的处理高级语言的能力;
因此完成特定功能需要的指令数量少,可执行程序占用内存小。
指令利用率低,80% 的指令使用频率只占 20%,造成了硬件上的浪费;
复杂的指令系统使计算机的结构复杂,既增加了设计的时间与成本,还容易产生设计失误;
难以将 CISC 的全部硬件集成在个芯片上,阻碍了单片机的发展。
随着 CPU 工作频率的提高,复杂指令系统中逻辑门的延时逐渐开始限制了工作频率的上限,使得执行操作速度变慢;同时复杂指令系统中大部分指令并不常用,倘若删去部分不常用指令,看似削弱了功能,但实际上却能简化结构、减少延时、提高运算速度,并且它们的功能也可以由多条常用指令实现。
由此动机,便诞生了精简指令集(RISC,Reduced Instruction Set Computer),指令长度较短、指令数量较少,但是运行速度较快、运行效率较高,复杂指令也可由若干精简指令合成。
指令数少、寻址方式少、指令格式少、指令长度相等、指令时钟周期相等,在硬件上易于实现;
单个指令实现的功能相对简单,需要的逻辑门较少,延时低,从而可以实现更高的工作频率,运算速度较高;
指令集精简,控制单元电路较少,从而使得全部硬件得以集成在一块芯片上;
设计成本较低、可靠性较高,并且可以支持高级语言。
编译后指令长度较长,内存需求较大;
大寄存器组使寻址更加复杂,并降低了速度;
硬件连线控制部灵活,不易发现和修改错误。
算术运算指令
二进制定点:加、减、乘、除、取余等;
浮点数:加、减、乘、除等;
十进制:加、减、乘、除等;
求反、求补、数值比较 。
逻辑运算指令
与、或、非、与非、或非、异或、同或;
逻辑移位、循环移位、算术移位。
字符串处理指令
字符串传送、字符串转换、字符串抽取;
字符串比较、字符串查找、字符串替换。
指令分类
取数指令、存数指令(输入、输出指令);
传送指令、成组传送指令、字节交换指令、清寄存器指令、堆栈操作指令等;
指令功能
实现主存与寄存器之间、寄存器与寄存器之间的数据传送;
启动外围设备、检查其工作状态、实现信息传送。
程序控制指令又称为转移指令:
条件转移指令:进位标志、结果为零标志、结果为负标志、结果溢出标志、结果奇偶标志等;
无条件转移指令;
转子程序指令(call);
返回主程序指令(return);
中断返回指令。
特权指令、状态寄存器置位、复位指令、测试指令、暂停指令、空操作指令等。
只选取使用频率最高的一些简单指令,指令数量少;
指令长度固定,指令格式种类少,寻址方式种类少;
只有取数和存数指令访问存储器,其余指令的操作都在寄存器之间进行。
数据传送:传送、存数、取数、交换、清零、置一、入栈、出栈;
算术运算:加法、减法、乘法、除法、绝对值、负、加一、减一;
逻辑运算:与、或、非、异或、测试、比较、设置控制变量、逻辑移位、循环移位;
控制传递:无条件转移、条件转移、转子、返回。
对于一般的指令系统,需要尽可能满足以下四个要求:
完备性:指令足够完成所有需要的功能;
有效性:程序占据存储空间小、执行速度快;
规整性:对称性、匀齐性、指令格式和数据格式的一致性;
兼容性:向上兼容。
目前实现的计算机在指令系统上具有以下特点:
属于精简指令系统(RISC)
指令长度相等、格式相同、数量较少(并且可扩展);
寻址方式较少,只有存数和取数可以访问存储器,其余指令操作在寄存器之间实现。
指令类别
支持 8 种算术运算、8 种逻辑运算;
支持无符号条件转移、有符号条件转移;
支持数据传送(Cache)、存数与取数(RAM)、入栈与出栈(Stack);
数据类型:8 位无符号整型(char)、8 位地址、整型数组;
实现方式
数据的传送实际上通过算术运算中的
通过 ram0
、ram1
等指定存储器,通过操作码 read+
、write+
可实现存储器的读写操作;
在汇编代码编辑器中,通过操作码 +i_
、+_i
、+ii
实现参数一与参数二的立即数模式;
无符号条件转移中包含无条件转移,通过操作码 +signed
可转为有符号条件转移(同样包含无条件转移);
函数的调用与返回则是通过多条指令合成而得
数据类型:增加浮点型数据、双精度型数据等;
运算指令:无需增加。不过目前有与、或、非、与非、或非、同或、异或,这其实是一种硬件上的浪费,可以考虑仅保留与、或、非、异或,其余指令通过它们合成而得,这样至多需要两个时钟脉冲和一个字节的存储空间;
函数调用:目前函数的调用是通过多条指令保存计数器的值,加上 CPU 字长后压栈,执行完函数后的返回,则是通过指令先出栈再写入 PC。这种方式略显繁琐,可以将其集成在硬件中,即增加 call 与 ret 指令;
数据传送:目前想要实现两个寄存器数据的交换,需要使用三个时钟脉冲和一字节内存,可以将其集成在硬件中,即增加 exc 指令;此外还可以增加成组拷贝数据的功能。
此外,没有必要实现多字节指令、变长指令等,以简化电路结构、便于设计、提高运行速度。
目前使用的控制存储器地址只有 8 位,即最多只能存储 256 字节的程序,这是非常少的。可以增加至 16 位,于是有 65536 字节程序,对于大多数情况来说已经够用了。可以通过 4 片 74161 芯片级联得到 16 位二进制计数器,在内存中通过两个一字节寄存器读写程序计数器的计数数值,从而实现程序的跳转。
由于数据的传送、写入与读取每次只有一字节,而程序计数器需要两字节,因此一种方式是固定使用两个寄存器用于程序计数器的读写。需要程序跳转时,将程序地址写入这两个寄存器(而不是直接由指令中的地址码决定),之后再通过条件判断决定是否跳转。
另一种思路是,将所需寄存器由一字节改为二字节,此时所有的运算器都需要相应改变,以适应二字节的运算。
目前是 32 位的 CPU,可以通过 2.4.1 控制存储器中类似的思路,将控制存储器改为 64 位。在硬件上,只需要添加四个一位寄存器、将 2-4 译码器改为 3-8 译码器即可。
不过目前 32 位的 CPU 仍有 23 条指令与 248 个存储器地址未使用,因此无需使用 64 位,而可以考虑如何合理地分配剩余指令的功能。
由于才开始没有系统地设计,指令译码的部分有些混乱。虽然能够实现所需功能,但是译码电路的排版并不美观,甚至出现了多种方式的译码,为后续功能的增加带来了一定的不便。译码方式有:
直接对地址码的后 3 位使用 3-8 译码器译码,而不考虑前 5 位(因为暂未使用)。如果使用立即数模式,则不进行译码。
始终进行算术运算、逻辑运算与两类条件判断,即对操作码的后三位进行译码,只不过仅在操作码中间三位为相应模式时,才启用对应的总线输出。
对于存储器,由于只用到了一类指令中的几条,而大部分指令未曾使用,因此直接使用了数值比较器,判断指令是否使用了相应的存储器。对于只有少数存储器的情况下,这样是简便的,但如果增加其他的指令及其存储器,电路则会显得繁琐。
可以通过 3 位译码器实现 8 位译码器,输出 4 组 64 位译码结果,之后通过分线器连接相应地址的寄存器或相应的运算器。这样可以增加电路的易读性,便于设计和增加新的指令功能。
如 4.1.3.4 所述,可以减去部分不常用指令(如与非、或非、同或),其功能改用其他指令合成而得。余下的指令可分配给其它功能使用。
非门常用于全部取反,异或常用于部分取反,同或常用于判断相等,与门和或门常用于提取字节,最常用的可能是与、或、非、异或。
目前已实现一字节的加减乘除与逻辑移位,可以增加的功能有:算术移位、循环移位;取反、自增、自减可以直接通过加法与减法实现,而无需单独搭建。也可以直接参考集成算术逻辑单元 74181。
此外,目前的加减乘除未考虑进位(溢出),可以增加转移条件,包括进位标志、结果为零标志、结果为负标志、结果溢出标志、结果奇偶标志等。
目前已实现无符号条件判断和有符号条件判断,但是后者不常用,可以考虑删去。如需实现有符号条件判断的功能,则可以通过逻辑运算和有符号条件判断的指令共同实现。
此外,可以考虑在一次判断中直接输出结果是否相等、是否小于、是否大于,从而减少对指令数量的需求。
目前该计算机中使用了一字节地址码中的后三位,含有 6 个一字节的寄存器(高速缓冲寄存器),另有一个地址用于读写程序计数器,一个地址用于输入输出。在不改变计算机架构与指令系统的情况下,若使用剩余 5 个地址码,则高速缓存可以增加至 254 字节。
一条指令中第一个字节为操作码,用于指定读写的 RAM,第二个字节用于指定寄存器,后两个字节用于指定 RAM 的地址,因此一个 RAM 的内存上限为 64 KB。
这是直接寻址的方式,此外还可以采取段寻址的方式,进一步提高 CPU 的内存上限。例如增加一字节的段寄存器,可得到 16 MB 存储空间的直接寻址能力;增加二字节的段寄存器,可得到 4 GB 存储空间的直接寻址能力。不过坏处是,这样会导致一次读写数据需要多个时钟脉冲,运行速度较慢。
目前的栈只有 256 字节的存储空间,一方面,可以用更多的逻辑门(而不会造成更大的延时量)来得到更大的存储空间;另一方面,可以在栈的数据溢出时发出警告,例如改变系统的状态寄存器中的值。
虽然目前控制台的存储器具有 64 KB 的存储空间,但是无法显示所有信息。可以通过偏移量的设置,实现页面滚动的效果。
目前可以向控制台输出数据,也可以从控制台读取数据(读取之前输出的数据)。可以通过输入设备逐字节输入数据,但是无法通过键盘直接交互。因此可以增加键盘设备,每当检测到按键事件时,向控制台输出相应字符。
具体来说,可以使用两个 2 字节的地址,一个地址(写入地址)用于指向控制台的最后一位数据之后,一方面用于 CPU 直接输出,一方面用于用户从键盘输入;另一个地址(读取地址)用于读取数据,即指向未读取的第一个地址。当用户按下回车键后,一方面根据控制台的宽度设置写入地址的偏移量,一方面通过读取地址逐位读取数据至寄存器中。
目前该计算机十分简陋,尽管可以实现可计算的程序,但编写程序只能使用汇编语言并转换为机器语言,十分繁琐。为了简化这一过程,可以使用软件。计算机软件一般分为系统程序和应用程序,其中系统程序又可分为以下几类:
各种服务性程序,如诊断程序、排错程序、练习程序等;
语言程序,如汇编程序、编译程序、解释程序等;
操作系统;
数据库管理系统。
为了更好地实现一些系统程序,可能还需要在硬件上做出相应的改进,例如增加新的常用指令、增大内部存储空间、增加交互设备及相关指令等。不过本次游戏的探索主要是硬件上的,在软件上暂时不打算深入了解。在分享中心可以看到,有人已经实现了在游戏中搭建的计算机上运行 Rust 程序。
这个暑假里,我花了两天的时间(7.8、7.12)打通了图灵完备这款游戏,之后陆陆续续又花了十二天的时间(7.13 - 7.24)优化了部分关卡的解法、搭了一些游戏之外的电路、写下了这份笔记或者说报告。
优化解法:在部分关卡寻找更少逻辑门或更低延时量的解法、为最后搭建的计算机添加存储器与输出设备、整理并使用加法简化汇编别名等;
其他电路:六位与八位的译码器、四位选择器、超前进位并行加法器、全减器、可控加减法器、八位并行除法器、一些电平触发器、常用的边沿触发器、74161、16 位计数器等;
书写报告:目录安排、电路截图与命名、查阅相关资料、编写示例代码与说明、书写报告正文。
本次 “计算机之旅” 中,大部分时间是在查阅资料和书写报告;而在游玩过程中,大部分时间在花在了电路排版上。可以参考除法器、小盒子、加法器等电路,整整齐齐的看着十分舒适。
总的来说,我觉得暑假花这些时间玩游戏、写报告是很值得的,我也会推荐身边的朋友入手这款游戏。图灵完备游戏的作者通过关卡与任务引导我们搭建出计算机,在很短的时间内入门数字电路、计算机组成原理与汇编语言。尽管深度很浅,但却可以点燃玩家的兴趣。
还有什么比自主探索更有趣的呢?我喜欢数学,一方面是因为数学的定理优美、证明巧妙,另一方面是因为数学允许我自由探索——随意地摆弄那些公式,随意地探索新的世界。现在发现计算机与程序同样允许我们自由探索,并且创造属于自己的东西就如小时候搭积木一样容易入门。
也许玩这个游戏的过程不过是拾人牙慧——倘若没有任务与提示,一个人恐怕一辈子也想不出冯诺依曼架构。不过那也无妨,最重要的是探索,即使是走前人走过的路,我们也可以欣赏前人欣赏过的风景;站在巨人的肩膀上,我们可以见得更广,也可能走得更远。
我大概不会专门花时间去系统地学习计算机组成原理,不过这次游玩确实是一次不错的科普;激发兴趣后查阅资料了解的更多的知识,也是十分有益的。在此由衷地感谢游戏的开发者们为我们提供了这样的一个快速了解计算机架构的有趣方式。