计算机组成原理学习笔记(3)——CPU组成

Posted by 皮皮潘 on 03-08,2022

从物理电路到数字电路

首先说明一下:数字电路中的0和1代表的是低电压和高电压,而非是电流,但是正是电压形成了电流才能使得对应电路可以联通。

实际的物理电路是是由大量的晶体管构成的,而每个晶体管(这里以三极管举例子,别的还有CMOS、二极管等等)都可以看作一端接地(低电压)或者接电源(高电压),一端作为输入,一端作为输出,然后可以根据输入来决定输出(根据一端是接高电压还是低电压,对应输入和输出可能是相反也可能是相同的),当输出端为高电压时可以视作对应的电路联通了,反之则是对应的电路断开了(只有有高低电压差才能形成电流),因此晶体管可以看作是继电器+开关,根据输入的高低电压,决定线路的连接或中断,而通过继电器来理解数字电路中的基础逻辑门非常的简单,各个逻辑门的实现如下:

  1. 非门
    1712_1.png
  2. 与门
    1714_1.png
  3. 或门
    1716_1.png

总而言之可以将底层物理电路单元(晶体管)看作是抽象的继电器的物理实现了,然后再从继电器我们可以将其组合成为逻辑门(与、或、非门)从而看作是数字电路了,自此在之后的CPU组成中我们可以以逻辑门和数字电路为基础去讨论了,而不需要关注最最底层的晶体管了

CPU组成

计算机的一个指令周期由多个CPU周期(我们一般把从内存里面读取一条指令的最短时间称为 CPU 周期)组成,一个CPU周期由多个时钟周期构成
1724_1.png

因此一个指令周期可以分解为如下几个步骤:

  1. Fetch(取得指令),也就是从 PC 寄存器里找到对应的指令地址,根据指令地址从内存里把具体的指令,加载到指令寄存器中,然后把 PC 寄存器自增,好在未来执行下一条指令。
  2. Decode(指令译码),也就是根据指令寄存器里面的指令,解析成要进行什么样的操作,是 R、I、J 中的哪一种指令,具体要操作哪些寄存器、数据或者内存地址。
  3. Execute(执行指令),也就是实际运行对应的 R、I、J 这些特定的指令,进行算术逻辑操作、数据传输或者直接的地址跳转。
  4. 重复进行1~3的步骤

因此要想搭建出来整个 CPU,我们需要在数字电路层面,实现这样一些功能:

  1. 首先,自然是ALU 了,它实际就是一个没有状态的,根据输入计算输出结果的第一个电路。—— 组合逻辑电路
  2. 第二,我们需要有一个能够进行状态读写的电路元件,也就是我们的寄存器。我们需要有一个电路,能够存储到上一次的计算结果。这个计算结果并不一定要立刻拿到电路的下游去使用,但是可以在需要的时候拿出来用。常见的能够进行状态读写的电路,就有锁存器(Latch),以及我们后面要讲的 D 触发器(Data/Delay Flip-flop)的电路。——存储电路
  3. 第三,我们需要有一个“自动”的电路,按照固定的周期,不停地实现 PC 寄存器自增,自动地去执行“Fetch - Decode - Execute“的步骤。我们的程序执行,并不是靠人去拨动开关来执行指令的。我们希望有一个“自动”的电路,不停地去一条条执行指令。我们看似写了各种复杂的高级程序进行各种函数调用、条件跳转。其实只是修改 PC 寄存器里面的地址。PC 寄存器里面的地址一修改,计算机就可以加载一条指令新指令,往下运行。实际上,PC 寄存器还有一个名字,就叫作程序计数器。顾名思义,就是随着时间变化,不断去数数。数的数字变大了,就去执行一条新指令。——计数器电路
  4. 第四,我们需要有一个“译码”的电路。无论是对于指令进行 decode(解码),还是对于拿到的内存地址去获取对应的数据或者指令(寻址),我们都需要通过一个电路找到对应的数据。—— 译码器电路

组合逻辑电路(ALU)

通过门电路去构成高层的运算逻辑单元的过程可以看作是搭积木的过程,因为所有的逻辑都可以通过与、或、非三种基础门组合得到,因此也没什么好提的,把数字电路学学好就行了,主要要注意的是由于电路本身的并行性,可以通过使用更复杂的电路(更多的晶体管)去增加并行性,从而降低运算需要等待的门延迟,以加法器为例:如果是最最简单的串行加法器,那么高位的全加器,需要等待低位的全加器的进位结果才能进行计算(以第4位的进位为例,首先要等待第一位的计算结果并得到对应的进位信息,然后传给第二位,继续等待第二位的计算结果,然后传给第三位,重复上述过程直到第4位),此时复杂度为O(n),而如果将电路变得更加复杂,也即对于高位的运算电路,将所有的低位的bit全部传给这个运算电路,那么根据二进制规则(以第4位的进位为例,其是否要进位可以直接根据第一位、第二位和第三位的情况直接计算:1. A3 B3 两个都是12. A3+B3是1并且 A2+B2 进位。3. A3+B3是1,并且 A2+B2 是1,并且 A1+B1进位。4. A3+B3是1,并且 A2+B2 是1,并且 A1+B1是1,并且下面进位上来的标志C0是1。)通过更加复杂的电路在O(1)时间内就计算出高位是否要进位并进行计算,而不再需要等待前面串行计算的结果了。而这就是CISC和RISC路线之争最朴素的又来,前者通过复杂的电路降低了运算的延迟,而后者通过简单的电路降低了CPU的功耗

存储电路

对于组合逻辑电路而言,电路的输出会随着电路的输入的修改而不断的修改,而对于CPU而言它需要一种方式去存储之前的输出,使之不会因为输入的改变而实时地发生改变,而应该仅仅在满足某种条件的情况下才发生改变,从而达到存储的效果,这种电路就是存储电路,目前常用的存储电路成称为D触发器,其结构如下:
1718_1.png
从左到右,分别由一个反门、一个时钟(震荡晶体管可以每个一段时间改变电压高低,其实就是时钟周期)、两个与门和两个或非门构成

其真正的核心在于将两个或非门的输出作为彼此的输入之一从而实现存储功能,将电路简化如下(最简单的触发器):
1720_1.png

  • 当闭合R时,Q输出为1,此时再断开R,Q的输出保持为1
  • 当闭合S时,Q输出为0,此时再断开R,Q的输出保持为0

再上述的基础上,我们再加入一个反门、一个时钟以及两个与门从而得到了水平触发的D触发器,在该D触发器中,当时钟输出为1时,与门的输出与D的输入有关,因此电路的输出与D的输入有关,当时钟输出为0时,两个与门的输出都是0,因此电路存储的信息输出不会再因为D的输出发生改变并且会与上一次输出相同,从而达到了存储的效果

一个D触发器可以记录一个Bit的信息,如果我们同时拿出多个 D 型触发器并列在一起,并且把用同一个 CLK 信号控制作为所有 D 型触发器的开关,这就变成了一个 N 位的 D 型触发器,也就可以同时控制 N 位的读写,从而实现了寄存器

另外这里我们需要注意的是刚刚我们提到的D触发器是水平触发的(时钟信号为1时就会触发更新,且在1的期间但凡输入变化就会触发更新),然而在现实中真正的D触发器是一个边沿触发器(它比刚刚提及的那个更加复杂,由两级R-S触发器构成),它存储的值仅仅在时钟信号从0变为1的上升沿才会发生改变,在信号为0或者信号为1或者从1变为0的下降沿时都不会发生变化,因此我们也可以看到寄存器中存储的值仅仅在时钟周期上沿才会发生改变

计数器电路

其实通过时钟加D型触发器(寄存器)再加一个加法器就可以实现计数器电路了:
1722_1.png
加法器的两个输入,一个始终设置成 1,另外一个来自于一个 D 型触发器 A,再把加法器的输出结果,写到这个 D 型触发器 A 里面。于是,D 型触发器里面的数据就会在固定的时钟信号上升沿的时候更新一次,每次自增之后,我们可以去对应的 D 型触发器里面取值,这也是我们下一条需要运行指令的地址,然后计算并得到对应的结果,然后在下一次时钟周期的上升沿才会真正写入寄存器,因此指令相对于时钟周期其实是延时的

加法计数、内存取值,乃至后面的命令执行,最终其实都是由一开始讲的时钟信号,来控制执行时间点和先后顺序的,在上升边沿时进行寄存器数据的写入,在其他时间段进行数据的计算

在最简单的情况下,我们需要让每一条指令,从程序计数,到获取指令、执行指令,都在一个时钟周期内完成,这样的 CPU 设计,我们称之为单指令周期处理器(Single Cycle Processor)。很显然,这样的设计有点儿浪费,因为我们需要将时钟周期调慢到最复杂的指令所需要的时间,不然的话在执行一条复杂指令时可能执行到中间就触发了下一个时钟周期,从而导致PC寄存器发生变化提前执行了下一条指令进一步导致中间上一条指令需要用到的寄存器的值发生了变化引起程序错误,因此需要将时钟周期调慢到最复杂的指令所需要的时间,但是这样的话即便只调用一条非常简单的指令,我们也需要等待整个时钟周期的时间走完,才能执行下一条指令(只有在时钟周期上升沿才会引发寄存器值的修改也即PC寄存器发生变化从而触发指令执行),会导致效率非常低下。在后面博文里会提到,通过流水线技术进行性能优化,可以减少需要等待的时间。

译码器电路

除了上述的三种电路之外,我们还需要一种电路来完成”寻址“的操作,也就是所谓的译码器电路。这里译码器电路由于过于复杂因此在该博文中暂且不表,我们只需要知道译码器电路可以用来寻址以及完成指令的翻译即可,其中指令译码器电路通过对于指令的翻译(每个指令都可以通过与门+非门对于指令的每一个bit进行组合来确定,比如一个指令是0110,那么仅仅在(~4 & 3 & 2 & 1)为1,代表是该指令)来决定了CPU通路中的控制信号(每个指令对应各个控制信号是1还是0可以用一张简单的真值表进行表述),而控制信号则决定了用哪一部分的电路模块功能以及各个电路模块的输入应该使用哪个电路模块的输出或者哪个寄存器或者哪个立即数

总结

通过将ALU、D 触发器、自动计数器以及译码器合并在一起,我们就可以拼装成一个CPU了:
1726_1.png
其核心就在于在每次时钟信号上升沿都会将上一轮的信息写入寄存器,从而导致电路输入发生变化从而触发新的运算,然后再一次等待在下一次时钟信号上升沿将新的信息写入寄存器

另外我们可以从中看到,执行一条指令,其实可以不放在一个时钟周期里面,可以直接拆分到多个时钟周期。我们可以在一个时钟周期里面,去自增 PC 寄存器的值,也就是指令对应的内存地址。然后,我们要根据这个地址从 D 触发器里面读取指令,这个还是可以在刚才那个时钟周期内。但是对应的指令写入到指令寄存器,我们可以放在一个新的时钟周期里面。指令译码给到 ALU 之后的计算结果,要写回到寄存器,又可以放到另一个新的时钟周期。所以,执行一条计算机指令,其实可以拆分到很多个时钟周期,而不是必须使用单指令周期处理器的设计,因此,现代优化 CPU 的性能时,用的 CPU 都不是单指令周期处理器,而是通过流水线、分支预测等技术,来实现在一个周期里同时执行多个指令的一部分。

参考

  1. 《深入浅出计算机组成原理》
  2. 《编码:隐匿在计算机软硬件背后的语言》
  3. Coursera-北京大学-《计算机组成》