七、控制器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:
表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所示:
图3. OUT, IN, SUB, LDI, ADD, AND, OR七条指令的机器码结构。
在图3中,'X'表示不使用,其中OPC2只有OUT指令用到,用于表示设备DEV1(OPC2=0时)或DEV2(OPC2=1时)。这7条指令中,除了LDI外,都是单字节指令,LDI的完整指令格式如图4所示:
图4. LDI指令机器码结构。
在图4中,第1字节和其它6条指令结构一致(LDI指令不使用OPC2),第二字节为无符号立即数,等价于C语言中的unsigned char类型的常量。
第二种情况:对于JNZ, LDR, STR, 和JMP这4条指令,由于它们都需要涉及地址操作,因此它们都是2字节指令,其机器码结构如图5所示:
图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个控制信号需要自己设计逻辑电路实现。硬布线的逻辑结构如下图所示:
图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芯片中,如下图所示:
然后复位Gater8(即RST置0,然后CLK拨动两次,再RST置1),不断拨动CLK二进制开关产生时钟信号,并观察每个寄存器、Zflag、控制器状态等值的变化,以及输出设备DEV1和DEV2的LED点亮情况。前面图2给出了当输入设备DEV0值为0x04时,程序最终运行状态截图,可以看到DEV1的LED灯显示值为0x04,而DEV2的LED灯显示值为0x6F。图8给出了输入输出局部运行结果截图。
图8. 输入输出部分运行结果截图。
并且,程序最后停在最后这条机器码为A0的指令(就是程序中最后的那条JMP指令)处进行无限循环,一切如预期一样正常,中途观察各寄存器的值变化也都正确。最后STR写入RAM处的值如图9显示,也都正确:
图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位操作码的设计。
图10. 用5位操作码表示32个状态。
接下来就是关键的7400系列芯片搭建,当然还有汇编器的编写,敬请期待。
http://blog.sina.com.cn/s/blog_6f38945b0102w98y.html