前面我们介绍了所有的基础零件,接下来就可以构建CPU了,这节的内容会比较多。
既然开始构建CPU,就少不了程序,因为CPU就是用来执行程序的。我们知道任何编程语言编写的程序最终都会转换为二进制,所以这里我们直接使用二进制的编程语言(机器语言)。比如我们让CPU计算一个加法3 14,这个加法运算用机器语言来描述的话就类似于下面这段二进制:
看着挺乱是吧,没关系,后面我们会讲解。大家只要清楚这段二进制的程序需要保存在内存里,这样CPU才能读取并执行。
不仅是程序,3和14作为运算的数据也是保存在内存里的,CPU要做的动作是从内存中某个地址读取数据3和14,然后根据程序要求(加、减……)对二个数字进行运算,运算的结果保存在内存中某个地址处。
要实现这样的功能,必须有一个约定,要让CPU能够识别相应的动作,即读取、运算、保存,所以我们建立如下约定:(称为指令表)。
指令表可以理解为是程序的解释器,当CPU拿到一段程序,例如00101110时,它必须知道这意味着什么,而指令表就能起到答疑解惑的作用。
具体来说每段程序(指令)的前四位对应着指令表中的操作码,后四位对应着指令表中的地址。比如00101110,前四位0010是操作码,它的含义是LOAD_A(见指令表),代表读取数据放入寄存器A。再看后四位1110,指令表中描述的内容是“4位内存地址”,这其实就是要读取的数据的内存地址。连在一起的含义就是“在1110这个地址处读取数据保存到寄存器A”。
接下来上电路!首先我们需要一块内存,可以直接使用上节提到的256B内存,但为了方便起见,我们假设它只有16个地址,可以保存16个8位二进制数。另外需要六个寄存器,每个可以保存一个8位二进制数,其中寄存器A-D用于临时存储和操作数据,指令地址寄存器用于记录程序运行到哪里了(程序指令的地址),指令寄存器用于存放指令内容。
接下来分析工作过程,当计算机启动时,所有寄存器初始值都是00000000,CPU开始进入第一个阶段:取指令,也就是从内存中获取指令。指令地址寄存器会连接到内存,读取地址为00000000的数据,即00101110,这个数据会保存在指令寄存器,第一个阶段结束。
第二个阶段:解码,即弄清楚指令要做什么。其实前面我们已经做了铺垫,指令内容是00101110,其中前四位0010是操作码,在指令表中对应的就是LOAD_A,指令后四位是1110,对应的是4位内存地址,整体意思就是从1110的位置读取数据保存在寄存器A。但现在还没有现成的电路能够做解码工作,所以我们需要添加一部分电路,如图。
这部分新添加的电路我们暂且称为解码电路,指令寄存器的前四位数据0010作为解码电路的输入,经过这些门电路的处理,最终会输出1。换句话说,解码电路的作用就是识别指令是否是0010(LOAD_A),只有指令是0010时,输出才是1,否则就是0。
第三个阶段:执行。解码电路的输出会连接到内存的允许读取端口,而指令寄存器的后四位1110会连接到内存的地址端口,这样就相当于允许读取内存地址为1110的数据,这个数据是00000011(十进制3)。
接下来这个数据要如何保存到寄存器A呢?我们需要让解码电路的输出同时连接到寄存器A的允许写入端口,而四个寄存器的数据输入端口要连接到内存的数据端口。当数据00000011被读取出来时,会同时发给四个寄存器,但只有寄存器A是允许写入的,所以数据就被保存在寄存器A中了。
接下来将指令地址寄存器 1,变成00000001,以便取下一条指令,后面的步骤与前面类似。需要注意的是,前面的解码电路只能识别第一条指令LOAD_A,后面每一条指令都需要单独的解码电路支持。我们把所有指令对应的解码电路和指令寄存器、指令地址寄存器等部分统一叫做控制单元。
接下来我们快速分析剩余的指令,现在指令地址是00000001,所以从内存中取出00011111存入指令寄存器。前四位0001对应的指令就是LOAD_B,后四位1111是要读取的内存地址,对应的数据是00001110(十进制14),这个数据会被存放到寄存器B中。然后指令地址寄存器 1(00000010),继续取下一条指令。指令内容是10000100,前四位1000在指令表中对应的是ADD,后四位0100分别是二个寄存器的地址01和00(因为寄存器A-D只有四个,用二位二进制数就可以描述),其中00是寄存器A,01是寄存器B,所以这个指令的作用就是将寄存器A、寄存器B的值相加。涉及到加法,就要用到之前讲过的算术逻辑单元,也称运算单元(ALU)了,我们简化一下电路如下:
寄存器A、B会通过控制单元连接到运算单元的二个输入端,同时控制单元会将操作符也传递给运算单元,这样就可以计算了。计算的结果必须保存起来才行,但指令本身并未明确保存在哪里,所以这里面有个约定,运算结果会保存在指令地址中最后一个寄存器里,二个寄存器地址分别是01和00,后面就是00,也就是寄存器A,所以最终结果会通过控制单元保存到寄存器A中,这个结果是00010001(十进制17)。
指令地址寄存器 1(00000011),继续取下一条指令。指令内容是01000111,前四位0100表明指令是STORE_A,即将寄存器A的数据写入内存,内存地址就是指令的后四位0111,控制单元会发送给寄存器A允许读取信号,发送给内存允许写入信号,将寄存器A的值保存在内存中对应的位置。
终于我们完成了一句简单的程序任务,两数相加,并成功保存结果。我们会发现每个指令的读取、解码、执行,相当于一个周期,CPU就是不断的重复这个周期,进而完成各类任务。在每个周期中算术逻辑单元、控制单元、存储单元(内存)都需要密切配合,节奏不能乱,才能保证最后的结果正确。但如何确保这个节奏是恰当的呢?既不能太快,因为即使是电信号的处理也需要时间,也不能太慢,造成计算效率太低。所以有一个单独的电路在控制这个节奏,就好像一台时钟,精确的指挥各个部分有条不紊的运行。CPU都有一个重要的指标:主频,例如2.6GHz,相当于26亿次周期/秒,意味着一秒钟内CPU会执行26亿次周期,主频越高的CPU代表速度越快。我们把带有时钟电路的算术逻辑单元、控制单元、6个寄存器封装成一个相对独立的部分,这就是CPU!
至此,我们从一个简单的晶体管开关开始,一路添砖加瓦,终于打造了一个完整的CPU,当然也是最基础的CPU。相信这个系列文章能让我们对硬件与软件的结合点有了清晰的认知,我们日常所使用的各类软件都是程序指令编写出来的,每个程序的每条指令在CPU内部都会经历众多晶体管开关的处理,最终完成我们希望的任务。
来源: 孙老师聊人工智能