CPU的设计与实现(2)--逻辑电路设计

时间:2024-04-12 08:49:22

在上一篇博文CPU的设计与实现(1)--方案设计中,较为详细地讲解了我将要设计实现的Gater8这个基于自制CPU的自制计算机的设计方案。

 
这是本系列第二篇博文,我将详细分析具体设计完成的Gater8的数字逻辑电路。最初计划本系列博文的第二篇应该是打算用各种纯二进制门(与门、非门、或门等)设计数字电路,然后在第三篇博文再讲本文的内容。这样安排是想让即使没有数字电路基础的读者也能顺利无障碍的理解文章的内容,并逐步从基础原理深入到后续的实现。可是由于以下几个原因,我想先不讲解原先的第二篇,并将后续博文都提前:(1)我最近太忙,基本没什么业余时间;(2)设计寄存器和三态控制芯片都不难,就是写文章特别花时间,于是还是因为忙;(3)基本的数字电路原理应该很多书和教程都有详细介绍;(4)从自己设计的数字电路版CPU转换到基于7400芯片的原型电路也需要花时间,而且可能需要额外电路或略微改动原先设计才能转换。
 
其实这被挤掉的第二篇博文我主要是想把以下几个东西的工作原理讲清楚,这对于理解和设计自己的CPU非常重要:(1)D flip flop,即D触发器的工作原理,(2)K-map简化布尔表达式的方法,(3)8位并行加法器的设计,(4)用flip flop和K-map(或布尔代数)设计出给定要求的时序电路,比如特定的计数器。其中(3)是组合电路,而(4)是时序电路。
 
一、自制CPU计算机Gater8原型的概要说明
 
首先,下图1为Gater8的模块结构图,而图2为完成后的基于7400系列芯片的Gater8具体原型图。图1和图2在结构布局对应。
 
CPU的设计与实现(2)--逻辑电路设计图1. Gater8的模块结构图。
 
 
CPU的设计与实现(2)--逻辑电路设计
图2. 基于7400芯片的Gater8原型电路(查看高清大图: 右击->显示图片)。
 
图1和图2中的设计布局大致为:左边部分是ROM和RAM电路,中间部分为各寄存器和ALU,右上部分为I/O,右下角部分为控制器。具体的每个部分的详细说明将在以下章节给出。
 
Gater8的逻辑电路设计在软件LogicWorks 5下完成,并测试运行通过。其设计总体硬件参数如下:
 
数据线位宽:8 位
地址线位宽:12位

控制器类型:硬布线逻辑

运算速度:待定,预计1MHz以上
指令数:11条
存储架构:哈佛半冯诺依曼架构
RAM:4KB
ROM:4KB
输入能力:1个8位输入设备,或8个1位输入设备
输出能力:2个8位输出设备,或16个1位输出设备
 
Gater8虽然是8位的处理能力,即每个数据寄存器均为8bit,但是它的设计过程和设计原理和16位,32位的计算机是一样的。因此完全可以在Gater8的基础上只花一两个小时就能设计出32位的CPU和计算机的原型。之所以Gater8只是8位,是因为16位或32位CPU需要太多的7400系列芯片,我需要考虑最后用实际7400系列芯片搭建时不会有几千上万根杜邦线,这样就太麻烦了。
 
关于运算速度:因为CPU由两大部件构成,即运算器和控制器,因此CPU的每个原子操作(也叫微指令,子操作)运算速度(即时钟周期)由(1)运算时间,(2)控制时间两部分构成。由于不同的子操作的执行时间长度是不同的,因此时钟周期的长度一定要足够大到能完成任何一个子操作。Gater8中每条指令由3至4个子操作组成,执行每条指令需要3至4个时钟周期。这些子操作包括从ALU到内部寄存器赋值、输入设备到内部寄存器赋值、ROM到内部寄存器赋值、RAM到内部寄存器赋值(读RAM)、内部寄存器赋值到RAM(写RAM)、内部寄存器到输出设备寄存器赋值、程序计数器(PC)+1,等等。这些子操作的运算时间就是完成赋值的数据通信时间,而控制时间是子操作对应的控制逻辑所消耗的时间。控制逻辑本质上是一个有限状态机,根据不同的指令操作码、其它条件(如Z, C标志位)以及当前状态确定下一个状态,同时将控制信号发送给CPU各部件的控制端口,以协调各部件的有序运作,完成一个个的子操作。
 
上面这段内容讲得有点抽象,不过没关系,这个阶段是逻辑电路设计,主要关注的是电路的逻辑合理性、硬件设计和连接是否正确、程序是否能够在所设计的电路上正确执行,具体的运算速度将在下一个阶段用7400芯片搭建时调试完成。不过,理论上时钟周期还是能够根据查阅自己设计的数据通路上每一个部件的Data sheet获得相应所需的时间,并累加得到。这个累加得到的是理论值,实际可能还需要再略微长一点。目前预计最长的Data path可能是ROM访问,因为按照常识,ROM芯片的访问相对来说最慢,可能需要100ns(运算时间),再加上控制单元的开销(控制时间),总计约不超过200ns,这么一算的话,1/200ns=5MHz,这个运算速度是我预计的1MHz的5倍。提升CPU的时钟周期的方法有很多,比如现在主流的CPU将总线分为几个层次,连接寄存器之间的内部总线速度最快,将ROM和RAM分离,不直接连接到该总线上,因为ROM和RAM的访问速度很慢,于是专们为ROM,RAM设计的另一个总线,还有更慢的I/O总线等。另外通过增加Cache也一定程度上解决了这个问题。这样CPU的时钟周期就不会受到很多限制,能很好得控制到只开销在必要的一些逻辑门电路上。
 
参见在图2右下角控制器部分,Gater8一共提供了17个控制端口的控制,都是低电平(即0)使能对应的功能。并且,所有部件,包括所有70X825寄存器,70X161、70X163计数器,70X244三态缓冲器,70X154译码器,ALU,控制器等都是在时钟信号从低电平到高电平跳变时起作用,即positive edge
 
以下详细介绍CPU和计算机的每个部件。
 
二、寄存器Register
 
为了使Gater8尽量少用7400系列芯片以便后期搭建,我只用了很少的寄存器。一共使用了以下寄存器:
 
2个8位运算寄存器:A, T
1个12位程序计数器:PC
1个8位指令寄存器:IR
2个8位输出寄存器:DEV1, DEV2
 
对比之前设计的方案,省略了MA(内存地址)寄存器和B寄存器。因为没有MA寄存器,因此不支持间接寻址,没有通用寄存器B,于是也暂时不能实现硬件栈结构。但是这样可以使芯片的使用适量减少。
 
在实现上,所有8位寄存器都使用70X825芯片,825芯片内部集成了8个D flip flop。唯一的12位计数器PC由3个70X161芯片,161芯片是4位计数器,给定时钟信号,161的输出状态将从0至F至0顺序变化,每次递增1。通过连接代表低4位的161芯片的RCO端口和代表高4位的161芯片的T端口,可以提共完整的8位计数器。同理,3片161芯片以这种方式连接后,就可以提供12位计数器,计数器的输出值用于表示当前ROM中指令或常量的地址。
 
每个寄存器都可以通过个控制端口从外部获取数据并存在内部,比如A寄存器的这个端口为Aen,T寄存器的为Ten,这个控制端口由70X825芯片的CLKEN引脚提供。另外寄存器输出数据到外界由另一个控制端口控制,比如A和T寄存器的这个端口分别为Aout和Tout,这个控制端口由70X244芯片的GA/GB引脚提供。根据用途的不同,有些寄存器是单向,比如DEV1和DEV2两个寄存器只有输入,无需控制输出,因此只有DEV1en,DEV2en控制端口。
 
三、总线
 
Gater8有两组总线:数据总线和地址总线。在上面的图2中,最上面的8条横向长长的线为数据总线,而下方的12条横向相对短一点的线为地址总线。
 
3.1 数据总线
 
数据总线用于连接各个运算寄存器的输入和输出、ROM/RAM的输出、ALU的输出、I/O接口等。由于总线对于连接在上面的所有部件是共享的,因此同一时刻只能允许一个部件向总线输出,因此可以看到,A,T寄存器的输出端会通过一个70X244芯片(X代表LS或HCT等任何字符串,因为不同公司生产的70244芯片使用不同的X代码)连接到数据总线上。70X244芯片是8位三态缓冲器,可以控制数据是否向总线输出。
 
3.2 地址总线
 
地址总线一共12位宽,其中低8位与数据总线连通,方便ROM输出低8地址,而高4位地址由IR寄存器(即指令寄存器)的低4位提供。并不是所有的指令的低4位都表示地址,只有读写RAM的两条指令,即LDR和STR才是,具体机器码格式请参见下文。
 
四、RAM与ROM
 
Gater8的RAM与ROM部分在图2的左边,它们的地址引脚都连接在12位地址总线,因此共享4KB地址空间。Gater8复位后,所有寄存器的值均为0,也就是PC寄存器指向ROM的0x000地址处,从这里开始存放第一条指令,并向高地址延申。
 
内存部分设计采用了半哈佛半冯诺依曼架构。半哈佛架构是因为RAM和ROM有各自独立的内容空间,虽然它们共享同一地址空间。例如,Gater8的RAM和ROM都是4KB,但是对于同一地址0x000处,ROM和RAM的内容是各自独立的,是可以不同的。
 
说它是半冯诺依曼架构是因为,Gater8对ROM的读取和RAM的访问不能同时进行(因为ROM和RAM共享数据总线),这和哈佛架构中两者可同时进行不同,而和冯诺依曼架构一样。
 
哈佛架构相比冯氏架构有很多优点,Gater8的内存部分也可以设计成完全的哈佛架构,不过同时也会损失一些其它的功能或*性,而且,目前也不需要ROM和RAM同时进行操作,因此Gater8当前的内存部分设计还是挺理想的。
 
4.1 ROM部分
 
ROM部分一共有PCen, PCLD, ROMout三个核心控制端口。
 
ROM部分最重要的是PC寄存器,它由3片70X161芯片构成,并有4个控制端口,分别为PCen, PCLD, CLK, 以及RST。CLK连接系统时钟信号,RST连接复位信号,因此真正由控制器控制的只有PCenPCLD两个端口。
 
当PCen端口置0(即低电平)时,经过一个CLK时钟周期,PC寄存器内容自加1,实现必要的程序自动向下运行。
 
当PCLD端口置0(即低电平)时,经过一个CLK时钟周期,PC寄存器会从地址总线载入12位数据作为新的PC寄存器的内容,这用于实现JMP, JNZ等跳转指令。
 
另外,ROM芯片采用AT28C64,输出的8位数据由一个70X244三态缓冲芯片控制是否输出到数据总线上。这个是否输出到数据总线上的控制由ROMout端口的值决定。当ROMout端口为0时,当前PC计数器指向的ROM数据输出到数据总线上。
 
4.2 RAM部分
 
RAM部分一共有MEMen, MEMin, MEMout三个核心控制端口。
 
这三个端口一起实现两个功能:(1)数据从数据总线写入到的RAM芯片,以及(2)数据从RAM芯片输出到数据总线上。
 
(1)数据写入RAM
由于RAM芯片采用CY62256,这是一个SRAM芯片,速度快(单次读写可在70ns以内完成),而且无需像DRAM安芯片需要刷新机制(这会使电路更复杂)。查阅它的Data Sheet,可知它由三个控制端口:CE片选、OE输出使能、和WE写入使能,我将其分别重命名为MEMen、MEMout、以及MEMin三个新名字,符合我自己的命名习惯。
 
当需要写入数据到RAM芯片内时,必须在MEMen和MEMin这两个端口上置0,然后连接在RAM芯片8位数据端口DIO0~DIO7的数据就会被写入到在由12位A0~A11端口决定的地址处。
 
由于写入时,待写入的8位数据来源于数据总线,用一片70X244,它的输入端口连接数据总线,输出端口连接RAM的数据端口,它的使能端口GA/GB由MEMen和MEMin这两个信号布尔与(即OR运算)的结果决定,即当且仅当MEMen和MEMin这两个信号都为0时,GA/GB也为0,这时这片70X244就导通了,数据可以从数据总线传输到RAM芯片的数据端口。
 
(2)数据从RAM读出
当需要读取RAM某处的一个字节时,必须在MEMen和MEMout这两个端口上置0,然后连接在RAM芯片的一个字节就会从8位数据端口DIO0~DIO7输出,这个输出的字节位于由12位A0~A11端口决定的地址。
 
由于读取操作会把数据从RAM芯片输出到数据总线上,另一片70X244控制这个输出。这个70X244的GA/GB使能端口也由MEMen和MEMout这两个信号的布尔与的结果决定。
 
(3)关于两片70X244芯片
由于即使当CS端口为1时,这个RAM芯片也会从DIO端口默认输出8个0,这才需要两片70X244芯片控制只在必要时开通一个方向的传输,一片控制写,一片控制读。如果有类似的RAM芯片,能在CS端口为1时,变为第三态,不输出也不输入,这样便可省掉这两片70X244芯片和两个与门,电路便可简化。
 
五、算术逻辑单元ALU
 
ALU部分一共有ALUCn, ALUM, ALUout三个核心控制端口。
 
Gater8的ALU直接采用两片70X181芯片并联构成。70X181芯片提供了加、减、与、或等若干基本算术逻辑运算。单片70X181芯片只能提供2个4位数的运算,通过连接低4位181芯片的Cn+4端口到高4位的181芯片Cn,可实现2个8位数的运算,Cn+4会根据实际运算向高4位进行借位或进位。
 
181芯片的运算功能选择是由S0~S3、M、Cn这6个端口决定的,具体可查阅它的Data Sheet。为了简化电路设计,我直接采用相应运算对应的S0~S3四位值作为我的操作码,比如想让181芯片执行A-B运算,S0~S3的值须分别为0110,同时M和Cn的值须为0和0,这样A-B的值就会从181芯片的F端口输出。我于是对我的指令集中的减法指令SUB的4位操作码定为0110,这样我可以直接连接IR寄存器的高4位到两片181芯片的S0~S3端口就行了,无需额外的转换或映射电路。
 
Gater8一共使用了181芯片的4个运算功能,分别如下:
 
CPU的设计与实现(2)--逻辑电路设计
 
表1. Gater8采用的4条70X181芯片的运算。
 
表1中,前两条为算术运算,后两条为逻辑运算,X表示可以为任意值,对结果不影响。
 
我将M和Cn这两个端口重命名为ALUM和ALUCn。
 
ALUout端口由一片70X244芯片的GA/GB提供,负责数据从ALU向数据总线的输出。
 
另外,70X181芯片还有一些其它输出端口,比如高位181芯片的Cn+4表示最终运算结果的进位/借位,A=B端口可在181执行减法状态下表示结果是否为0。因此,高位181芯片的Cn+4端口可用来提供进行/借位标志位,即Cflag。而A=B端口在特定情形时用来表示零标志位,即Zflag,不过这个特定情形往往不太容易达到,因此我没有用A=B端口,而用了一个8位的或门连联A寄存器来得到Zflag。另外Cflag在Gater8中也没有用到。
 
六、输入输出I/O
 
I/O部分一共有DEV0en, DEV1en, DEV2en三个核心控制端口。
 
其中DEV1en和DEV2en为两个输出设备的控制端口,寄存器小节已讲解过了,不再赘述。
 
DEV0en是用来控制输入设备向数据总线传输数据的控制端口,由一片70X244芯片提供。
 
图1中,两个输出设备分别连接了8个LED灯,输入设备连接了两个Hex Keyboard,用于测试程序是否能在电路上正确执行。后期用实际芯片搭建时,将使用以下设备:
 
输出设备:一个1602 LCD、一个蜂鸣器、若干LED灯。也会考虑连接PWM驱动的电机驱动板,尝试程序实现PWM脉冲,并控制电机转动。
输入设备:若干按键、一些数字传感器,比如数字式土壤湿度传感器、DHT11数字式温湿度传感器、数字式光敏传感器等。
 
七、控制器CU
 
CPU没有控制器也能正常完成各条指令,得到相应的运算结果或操作输入输出设备,只不过CPU内那么多部件的控制端口都需要人为置0或置1才能让它们协调工作。这也就是早期计算机有那么多开关需要人工拨动来控制计算机的原因。有了控制器,CPU就可以摆脱人工干预,自动有序得执行程序,速度当然也比人工操作快成千上万倍。
 
但是控制器(Control Unit)的设计是相对最复杂的部分。它的设计完成需要考虑以下几大内容:
 
(1)数据通道(即Data Path)和子操作
(2)指令集的设计
(3)采用微程序还是硬布线实现CU
 
这三大部分内容并不是独立的,而是相互关联的,往往一部分内容的微调都会影响另两部分的设计。下面分别针对以上几点进行详细分析。
 
7.1 数据通道(Data Path)和子操作
 
数据通道就是CPU内相连的各部件之间的数据传输通路。比如在数据总线上连接有ROM的输出端口和IR寄存器的输入端口,这就形成了一个数据通道,用ROM->IR表示,有了这个通道,ROM就可以输出数据进IR寄存器。但没有通道连接IR寄存器的输出到A寄存器的输入,因此这个操作无法在硬件上完成。执行每个数据通道就够成了一个最基本的硬件子操作,这个子操作不可再被拆分,是CPU的原子操作。
 
为了顺利完成一个ROM->IR子操作的执行,控制器需要发送不同的控制信号给各部件的控制端口。首先,ROMout端口必须置0,以允许ROM输出数据到数据总线上,同时IRen端口也需要置0,使得IR寄存器在下一个时钟周期上升沿把数据总线上的数据存入IR寄存器。ROMout和IRen是这个子操作的核心端口,但仅设置两个0在这两个端口上还不够,因为数据总线是被很多CPU内的部件所共享的,当ROM占用数据总线时,我们同时还得让其它任何共享数据总线的部件不能输出数据到这个总线上,不然就会发生冲突,子操作就无法完成。因此,除了ROMout和IRen置0外,同时PCLD, Aen, Aout, Ten, Tout, ALUout, DEV1en, DEV2en, DEV0en, MEMen, MEMin, MEMout都需要置1,而ALUM和ALUCn这两个属于ALU的控制端口的值可以是任意值,即0或1均可,因为这个子操作不关心ALU运算。
 
计算机的每一条指令都是由若干个这样的子操作构成,Gater8也不例外,但不同的硬件设计,即使相同的功能需要的子操作集合也不一样。比如,对于指令:
IN T
这条指令将从输入设备DEV0读取一个字节到T寄存器,它需要以下3个子操作完成:
 
(i) ROM->IR
(ii) PC+1->PC
(iii) DEV0->T
 
(i)完成从ROM中取指令到指令寄存器IR,(ii)完成PC寄存器自加1,(iii)完成从输入设备DEV0读取1字节到T寄存器。这3个子操作,每个都能在一个时钟周期内完成,所以一条IN T指令需要3个时钟周期完成。
 
上面(i)和(ii)两个子操作是取指操作,它们是每一条指令的最前面两个子操作,(iii)是执行子操作。不同的指令可能有1个或2个执行子操作。比如LDR指令(详见下文)就有2个执行子操作,因此它是4周期指令。
 
很重要一点是,每一个子操作必须在一个时钟周期内完成,同时要求这个子操作对应的数据通路在硬件上必须是存在的。对于不能在同一时钟周期完成的操作必须拆分为几个子操作按顺序完成。
 
7.2 指令集的设计
 
在已完成的硬件上,我们可以设计相应的指令集。指令集的挑选和设计是需要精心考虑的,特别是将用7400系列芯片搭建出来的Gater8,因为如果设计过于复杂的指令集,电路就会变得相对复杂,所需7400芯片就会增多,同时设计一些用不上或可以用其它指令表示的一些很少用得上的指令也是一种硬件资源浪费。
 
由于IR寄存器为8位,Gater8的指令长度为8位,于是我打算用高4位表示操作码(Opcode),低4位表示其它功能或不用。经过慎重考虑最后为Gater8实现以下11条指令:
 
(1)OUT: 用于输出A或T寄存器的值到输出设备DEV1或DEV2,语法格式为: OUT DEV1, A;
(2)IN: 从输入设备DEV0读取1字节到A或T寄存器,语法:IN A;
(3)LDR: 从指定地址的RAM处读取1字节内容到A寄存器,语法:LDR 0x123; 0x123为12位地址,下同。
(4)SUB: 执行减法运算,并将结果存入A或T寄存器,语法:SUB A;
(5)LDI: 从ROM内读取1字节立即数到A或T寄存器,语法:LDI A, #0xAB; '#'符号表示立即数。
(6)ADD: 执行加法运算,并将结果存入A或T寄存器,语法:ADD A;
(7)JMP: 无条件跳转,语法:JMP 0x123;
(8)AND: 执行布尔与运算,并将结果存入A或T寄存器,语法:AND A;
(9)STR: 将A寄存器的内容写入指定地址的RAM中,语法:STR 0x123;
(10)OR: 执行布尔或运算,并将结果存入A或T寄存器,语法:OR A;
(11)JNZ: 当A寄存器值不为0时跳转,否则不跳转,语法:JNZ 0x123;
 
注:在编写实际Gater8的汇编程序时,上述指令中出现的地址,比如0x123可以用汇编程序中的符号地址代替。如此便可实现变量的定义、运算,以及循环程序的编写。
 
上述每条指令对应的子操作和相应的控制端口的置值情况见表2:
 
CPU的设计与实现(2)--逻辑电路设计
表2. Gater8的详细控制逻辑(查看高清大图: 右击->显示图片)。
 
表2中,所有空白格子内为省略的数字1,'X'表示可以为任何值,即0或1均可。
 
CPU的控制器本质上是一个有限状态机。在任意状态下,符合一定条件就会进入下一个不同的状态,下一个状态由当前状态以及给出的条件决定,不一定唯一。每一个状态可用于表达一个数据通路或子操作,比如上面7.1小节分析的IN T指令,一共由三个子操作构成,每一个子操作的不同控制端口输出可以对应到一个状态,假设当前状态为表中的S3,那么就控制器就对17个控制端口分别置S3那行对应的值。
 
上面提到状态变化由不同条件引起,所有条件由所有输入到控制器的信号构成。Gater8的控制器使用以下一共7位输入信号来确定状态的变化
 
(i) IR寄存器的高6位,其中高4位是指令的操作码,相对低的2位是条件码(JNZ, LDR, STR, JMP指令除外);
(ii) Zflag标志位,即零标志位; 
 
其中IR寄存器的高4位是操作码,其值决定了具体的指令,第Zflag标志位信号只有JNZ指令用到,IR寄存器高6位中的最低位(表中名为OPC2)只有OUT指令用到,IR寄存器的高6位中倒数第二位(表中名为OPC)用于决定保存到或读取到A(OPC=0时)或T(OPC=1时)寄存器。
 
我们对IN T指令的例子再重新详细分析一下就是:(i) 最初是S0状态,对应取指令子操作ROM->IR,(ii) 然后无条件转换到下一个状态S1(其实是有条件的,就是发生一个时钟周期),对应的子操作是PC+1->PC,即PC计数器自加1,(iii) 然后查看以下条件:IR寄存器的高4位操作码为0011,对应为IN指令,同时查看高4位后的那一位(取名OPC位)为1,于是对应的子操作是DEV0->T,如果OPC位值为0,对应的子操作为DEV0->A,这两个子操作都对应到状态S3,因为它们都可以在一个时钟周期内完成。
 
 
Gater8的指令集设计采用了不定长格式,LDI, JNZ, LDR, STR, 和JMP这五条为2字节指令,其它均为1字节指令。
 
对于不同的操作码,IR寄存器的低4位含意是不同的。一共有两种情况,下面分别图示加说明。
 
第一种情况:对于OUT, IN, SUB, LDI, ADD, AND, OR这七条指令,其机器码结构如图3所示:
 
CPU的设计与实现(2)--逻辑电路设计
图3. OUT, IN, SUB, LDI, ADD, AND, OR七条指令的机器码结构。
 
在图3中,'X'表示不使用,其中OPC2只有OUT指令用到,用于表示设备DEV1(OPC2=0时)或DEV2(OPC2=1时)。这7条指令中,除了LDI外,都是单字节指令,LDI的完整指令格式如图4所示:
 
CPU的设计与实现(2)--逻辑电路设计
图4. LDI指令机器码结构。
 
在图4中,第1字节和其它6条指令结构一致(LDI指令不使用OPC2),第二字节为无符号立即数,等价于C语言中的unsigned char类型的常量。
 
第二种情况:对于JNZ, LDR, STR, 和JMP这4条指令,由于它们都需要涉及地址操作,因此它们都是2字节指令,其机器码结构如图5所示:
 
CPU的设计与实现(2)--逻辑电路设计
图5. JNZ, LDR, STR, 和JMP这4条指令的机器码结构。
 
这4条指令的第1字节高4位为操作码,低4位提供12位地址的高4位地址,第2字节提供12位地址的低8位地址。为了完整提供12位地址,第1字节的低4位都用于表示地址,不能用于其它目的,也就没有OPC位的存在,因此LDR和STR这两条指令不能选择读取或写入A还是T寄存器,都是固定操作一个默认的寄存器(默认为A)。
 
7.3 Gater8的硬布线控制器实现
 
在控制器设计阶段最重要的成果就是完成7.2小节中的表2。表2完整提供了每条指令的所需要的子操作、每个子操作对应的各部件控制端口的控制信号,以及状态转变的条件。
 
控制器的实现主有两种方式:
(1)微程序方式:需要微程序ROM,用各个条件作为该ROM的地址,对应的输出就是各部件的控制信号。
(2)硬布线方式:完全用布尔电路实现整个状态机。
 
对于Gater8的控制器而言,一共有7个输入和17个输出。如果我去掉一个输出设备,就可以使所需控制的端口数变为16个,这样我就可以用2片8位地址输入8位数据输出的ROM芯片,7个控制器输入位做为地址(最高地址位不用,置0)存储每个状态对应的16个控制信号,就完成了微程序式的控制器实现。Nibbler自制CPU就是这么做的。
 
但我本人更偏向用硬布线实现,也是Gater8的实现方式。因为我一共用了4位操作码,共可产生16个状态,每个状态按上面表2中输出相应的各控制信号。状态转变可以通过计数器(Counter)实现,对4位操作码对应16个状态中的哪一个可以通过译码器(Decoder)实现,而剩下的每个状态输出相应的17个控制信号需要自己设计逻辑电路实现。硬布线的逻辑结构如下图所示:
 
CPU的设计与实现(2)--逻辑电路设计
图6. 硬布线控制器结构图,图片引用自John D. Carpinelli的《Computer Systems Organization & Architecture》第228页。
 
其中计数器有三个动作:LD、INC、CLR,分别表示加载一个新状态值、状态值加1、 状态值清0。这三个动作包含了控制器这个状态机的全部可能的动作。在表2中,我用S0~S15分别表示状态0至状态16,当计算机复位时,计数器值为0,即为S0状态,执行子操作ROM->IR,然后在一个时钟周期后需要进入一下个状态S1,执行PC+1->PC,从S0状态变换到S1状态只需要向计数器触发INC动作便可,它就会顺序变化下一个状态。然后下一步就需要加载IR寄存器的高4位操作码进入计数器,识别是什么指令,这就需要将IR寄存器的高4位连到计数器的输入口(图中Input处),并向计数器触发LD动作便可,计数器就会顺利加载操作码。计数器当前的状态值会从右侧输出口输出至译码器,译码器会将4位二进制值变为对应的16位二进制值,这个16位二进制值只有其中1个位为0,其余15位均为1。比如当计数器当前状态值为1001,即十进制数字9时,译码器输出端第9位就会输出0,其余15位均输出1。对应的这个第9位,我们就可以根据表2中设计的各部件控制信号进行输出。当一条指令的最后一个子操作执行完成后,比如表2中IN T指令最后子操作对应的是状态S3,那么下一个状态就应该回到状态S0,以便进行下一轮取指和译码执行。因此,在S3状态执行完后,我们需要触发计数器的CLR动作,让其清0,回到状态S0。
 
Gater8的设计中计数器用一片70X163芯片,译码器为一片4位至16位的70X154芯片。剩下的生成逻辑控制信号部分是完全是根据Gater8的具体特点设计的,不可能有现成芯片可用,我们需要用一些与门和或门来实现,这可以用70X08芯片提供与门和70X32芯片提供或门。
 
下面分析这个逻辑控制电路的设计。我们从一个简单的控制信号ALUM入手,在前面已经说明,因为我们的电路设计都是0使能部件,1不使能(禁止)部件。查阅表2中ALUM那一列,发现只有S6和S9两个状态下时ALUM为0,因此我们只要当前状态为S6和S9两者之一时就需要向ALUM这个控制端口发送一个0,其它时候可以不发送,因为X可以当作1也禁用部件。这样我们只需用一个与门连接译码器的S6和S9端口,这个与门的输出连接ALUM控制端口就行了,这样就完成了ALUM的控制信号的设计。用同样的方法,根据表2,可以完成其它16个控制端口的逻辑设计。其中Aen这个端口很多状态都会使能它(即给Aen端口置0),而且有些是有条件使能,这时我们可以用K-map或布尔逻辑计算出某一状态下使能Aen的电路,并将它与其它状态下的使能Aen的电路与在一起,最终得到完整的Aen的使能电路。例如表2中,状态S3下使能Aen的电路是S3+OPC,这里'+'表示布尔或,而状态S4时是无条件直接使能Aen,它们都一起与在一起共同输出Aen控制信号。
 
另外Gater8中,在且仅在状态S1后就需要加载操作码,于是只有译码器的S1输出连接到了计数器的LOAD口。而11条指令分别在状态S2, S3, S5, S6, S8, S9, S10, S11, S13, S14, S15后执行结束,于是这些指令与在一起并连接到了计数器的CLR端口。一旦这11个状态中有一个值为0,就表示当前指令执行完毕,需要清计数器为0,准备处理下一条指令。
 
至此,复杂的控制器的设计讲解完了。
 
八、关于复位、时钟信号
 
关于Gater8的复位,我设计了RST信号,它用于连接在每一个寄存器、计数器的CLR端口,一旦RST信号为0,这些连接的部件都会清0。不过我对于控制器里使用的计数器芯片不同于PC计数器的芯片,前者是70X163,它是同步清0,而后者是70X161芯片,它是异步清0。我其它寄存器用的都是70X825芯片,它也是异步清0的。所谓异步清0是指我只要在这些芯片的CLR端口置0,它们就会立即清0生效,无需等待下一个CLK时钟周期让其清0生效。而同步清0则需要一个在CLK端口的时钟信号才能让清0生效。
 
我用了163芯片作为控制器的计数器,让它清0,需要先置RST为0,然后产生一个CLK周期,这样163芯片清0的同时,其它所有的寄存器和计数器也清0了。因为RST信号未来将连接到一个实体按钮上,我将设计按钮按下去产生RST信号0,放开时产生RST信号1,由于人工按键的时延,再加上时钟信号的高速,在人按下和放开中间,必然会有至少1个CLK时钟周期,因此所有的部件将都能成功清0,完成复位的功能。
 
为了设计时测试电路的方使,图1中Gater8的时钟信号我是连着一个二进制开关的。这意味着,产生一个时钟周期,需要人工拨动开关两次。
 
你也可以不用二进制开关,换用时钟信号直接连上去,除了速度变得很快以外,效果一模一样。但测试电路阶段,不利于观察。
 
至此,整个逻辑电路也分析完毕。下面进行编程测试Gater8这个自制CPU是否能正确运行程序。
 
 
九、编写程序测试CPU
 
终于到了写程序的阶段了, 这一部分我们设计一小段程序,并将对应的机器码导入到ROM芯片内,让Gater8运行,看是否能正确执行得到预期的结果。
 
测试程序如下:
 
LDR 0XFFF   ;从RAM的0XFFF地址处读取1字节到A寄存器
LDI T, #0xFF   ;从ROM中读取立即数0xFF到T寄存器,相当于赋值 T=0xFF
IN A   ;从输入设备DEV0读取一个字节到A寄存器
OUT DEV1, A   ;输出A的值到DEV1
ADD A   ;计算A=A+T
LDI T, #0x01   ;赋值T=0x01
Loop:
SUB A   ;计算A=A-1
JNZ Loop   ;如果A≠0就跳到Loop处执行
 
LDI A, #0x55   ;A=0x55
LDI T, #0x0E   ;T=0x0E
ADD A   ;A=A+T=0x55+0x0E=0x63
OR T   ;T=A | T=0x63 | 0x0E=0x6F
OUT DEV2, T   ;输出T的值0x6F到DEV2
STR 0x010   ;保存A的值0x63到RAM的地址0x010处
Stop:
JMP Stop   ;程序执行结束,执行无限循环
 
由于我还没有编写好汇编编译器程序,先用手工将上面这段程序翻译成机器码,并导入到ROM芯片中,如下图所示:
CPU的设计与实现(2)--逻辑电路设计
 
图7. 编译好的程序导入ROM芯片内。
然后复位Gater8(即RST置0,然后CLK拨动两次,再RST置1),不断拨动CLK二进制开关产生时钟信号,并观察每个寄存器、Zflag、控制器状态等值的变化,以及输出设备DEV1和DEV2的LED点亮情况。前面图2给出了当输入设备DEV0值为0x04时,程序最终运行状态截图,可以看到DEV1的LED灯显示值为0x04,而DEV2的LED灯显示值为0x6F。图8给出了输入输出局部运行结果截图。
CPU的设计与实现(2)--逻辑电路设计图8. 输入输出部分运行结果截图。
 
并且,程序最后停在最后这条机器码为A0的指令(就是程序中最后的那条JMP指令)处进行无限循环,一切如预期一样正常,中途观察各寄存器的值变化也都正确。最后STR写入RAM处的值如图9显示,也都正确:
 
CPU的设计与实现(2)--逻辑电路设计
 
图9. 程序中A寄存器的值0x63正确写入RAM的0x010处。
上面的演示程序虽然简单,但演示了所设计的指令。虽然指令集不大,但它是精心设计的,可以充分利用这些指令写出功能更加强大的程序。
 
 
十、小结
 
这个自己设计的Gater8,前前后后被不断修改了很多次,至少目前的这是第3个版本。指令集也是精心挑选,最终确定了11条。整个过程不但可以用来设计8位自己的CPU,同时也可以用来设计16位和32位,甚至64位CPU,它们的原理是一样的。
 
设计逻辑电路本身并不太复杂,可以轻松根据Gater8设计出另一个Gater16或Gater32,但后期用过多的芯片,担心会很难搭建出来。毕竟在软件里设计实验成功的Gater8在实际搭建阶段还可能会遇到一些不确定的问题。所以在逻辑设计阶段,我已尽可能精简电路,并同时保障一定的功能的*性。
 
本来考虑用5位操作码,提供32个状态,这样可以实现更多的指令。而且控制器中的计数器和译码器的连接部分也完成了设计,见图10。后来也是考虑到(1)过多与门芯片和或门芯片问题,(2)内存地址线相应少一根,4KB的地址空间将变为2KB,所以暂时还是保持当前4位操作码的设计。
 
CPU的设计与实现(2)--逻辑电路设计
图10. 用5位操作码表示32个状态。
 
 
接下来就是关键的7400系列芯片搭建,当然还有汇编器的编写,敬请期待。