You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

101 lines
12 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 19 | 建立数据通路(下):指令+运算=CPU
上一讲,我们讲解了时钟信号是怎么实现的,以及怎么利用这个时钟信号,来控制数据的读写,可以使得我们能把需要的数据“存储”下来。那么,这一讲,我们要让计算机“自动”跑起来。
通过一个时钟信号我们可以实现计数器这个会成为我们的PC寄存器。然后我们还需要一个能够帮我们在内存里面寻找指定数据地址的译码器以及解析读取到的机器指令的译码器。这样我们就能把所有学习到的硬件组件串联起来变成一个CPU实现我们在计算机指令的执行部分的运行步骤。
## PC寄存器所需要的计数器
我们常说的PC寄存器还有个名字叫程序计数器。下面我们就来看看它为什么叫作程序计数器。
有了时钟信号我们可以提供定时的输入有了D型触发器我们可以在时钟信号控制的时间点写入数据。我们把这两个功能组合起来就可以实现一个自动的计数器了。
加法器的两个输入一个始终设置成1另外一个来自于一个D型触发器A。我们把加法器的输出结果写到这个D型触发器A里面。于是D型触发器里面的数据就会在固定的时钟信号为1的时候更新一次。
![](https://static001.geekbang.org/resource/image/1e/4c/1ed21092022057ed192a7d9aff76144c.jpg)
这样我们就有了一个每过一个时钟周期就能固定自增1的自动计数器了。这个自动计数器可以拿来当我们的PC寄存器。事实上PC寄存器的这个PC英文就是Program Counter也就是**程序计数器**的意思。
每次自增之后我们可以去对应的D型触发器里面取值这也是我们下一条需要运行指令的地址。前面第5讲我们讲过同一个程序的指令应该要顺序地存放在内存里面。这里就和前面对应上了顺序地存放指令就是为了让我们通过程序计数器就能定时地不断执行新指令。
加法计数、内存取值,乃至后面的命令执行,最终其实都是由我们一开始讲的时钟信号,来控制执行时间点和先后顺序的,这也是我们需要时序电路最核心的原因。
在最简单的情况下我们需要让每一条指令从程序计数到获取指令、执行指令都在一个时钟周期内完成。如果PC寄存器自增地太快程序就会出错。因为前一次的运算结果还没有写回到对应的寄存器里面的时候后面一条指令已经开始读取里面的数据来做下一次计算了。这个时候如果我们的指令使用同样的寄存器前一条指令的计算就会没有效果计算结果就错了。
在这种设计下我们需要在一个时钟周期里确保执行完一条最复杂的CPU指令也就是耗时最长的一条CPU指令。这样的CPU设计我们称之为**单指令周期处理器**Single Cycle Processor
很显然,这样的设计有点儿浪费。因为即便只调用一条非常简单的指令,我们也需要等待整个时钟周期的时间走完,才能执行下一条指令。在后面章节里我们会讲到,通过流水线技术进行性能优化,可以减少需要等待的时间,这里我们暂且说到这里。
## 读写数据所需要的译码器
现在我们的数据能够存储在D型触发器里了。如果我们把很多个D型触发器放在一起就可以形成一块很大的存储空间甚至可以当成一块内存来用。像我现在手头这台电脑有16G内存。那我们怎么才能知道写入和读取的数据是在这么大的内存的哪几个比特呢
于是,我们就需要有一个电路,来完成“寻址”的工作。这个“寻址”电路,就是我们接下来要讲的译码器。
在现在实际使用的计算机里面内存所使用的DRAM并不是通过上面的D型触发器来实现的而是使用了一种CMOS芯片来实现的。不过这并不影响我们从基础原理方面来理解译码器。在这里我们还是可以把内存芯片当成是很多个连在一起的D型触发器来实现的。
如果把“寻址”这件事情退化到最简单的情况,就是在两个地址中,去选择一个地址。这样的电路,我们叫作**2-1选择器**。我把它的电路实现画在了这里。
我们通过一个反相器、两个与门和一个或门就可以实现一个2-1选择器。通过控制反相器的输入是0还是1能够决定对应的输出信号是和地址A还是地址B的输入信号一致。
![](https://static001.geekbang.org/resource/image/38/a0/383bfbb085c1eeb9b9473ae6f18e97a0.jpeg)
2-1选择器电路示意图
一个反向器只能有0和1这样两个状态所以我们只能从两个地址中选择一个。如果输入的信号有三个不同的开关我们就能从$2^3$也就是8个地址中选择一个了。这样的电路我们就叫**3-8译码器**。现代的计算机如果CPU是64位的就意味着我们的寻址空间也是$2^{64}$那么我们就需要一个有64个开关的译码器。
![](https://static001.geekbang.org/resource/image/40/01/4002b5f8f60a913e655d5268348ee201.jpeg)
当我们把译码器和内存连到一起时,通常会组成这样一个电路
所以说其实译码器的本质就是从输入的多个位的信号中根据一定的开关和电路组合选择出自己想要的信号。除了能够进行“寻址”之外我们还可以把对应的需要运行的指令码同样通过译码器找出我们期望执行的指令也就是在之前我们讲到过的opcode以及后面对应的操作数或者寄存器地址。只是这样的“译码器”比起2-1选择器和3-8译码器要复杂的多。
## 建立数据通路构造一个最简单的CPU
D触发器、自动计数以及译码器再加上一个我们之前说过的ALU我们就凑齐了一个拼装一个CPU必须要的零件了。下面我们就来看一看怎么把这些零件组合起来才能实现指令执行和算术逻辑计算的CPU。
![](https://static001.geekbang.org/resource/image/68/71/6863e10fc635791878d1ecd57618b871.jpeg)
CPU实现的抽象逻辑图
1. 首先我们有一个自动计数器。这个自动计数器会随着时钟主频不断地自增来作为我们的PC寄存器。
2. 在这个自动计数器的后面我们连上一个译码器。译码器还要同时连着我们通过大量的D触发器组成的内存。
3. 自动计数器会随着时钟主频不断自增从译码器当中找到对应的计数器所表示的内存地址然后读取出里面的CPU指令。
4. 读取出来的CPU指令会通过我们的CPU时钟的控制写入到一个由D触发器组成的寄存器也就是指令寄存器当中。
5. 在指令寄存器后面我们可以再跟一个译码器。这个译码器不再是用来寻址的了而是把我们拿到的指令解析成opcode和对应的操作数。
6. 当我们拿到对应的opcode和操作数对应的输出线路就要连接ALU开始进行各种算术和逻辑运算。对应的计算结果则会再写回到D触发器组成的寄存器或者内存当中。
这样的一个完整的通路也就完成了我们的CPU的一条指令的执行过程。在这个过程中你会发现这样几个有意思的问题。
第一个,是我们之前在[第6讲](https://time.geekbang.org/column/article/94075)讲过的程序跳转所使用的条件码寄存器。那时讲计算机的指令执行的时候我们说高级语言中的if…else其实是变成了一条cmp指令和一条jmp指令。cmp指令是在进行对应的比较比较的结果会更新到条件码寄存器当中。jmp指令则是根据条件码寄存器当中的标志位来决定是否进行跳转以及跳转到什么地址。
不知道你当时看到这个知识点的时候有没有一些疑惑为什么我们的if…else会变成这样两条指令而不是设计成一个复杂的电路变成一条指令到这里我们就可以解释了。这样分成两个指令实现完全匹配好了我们在电路层面“译码-执行-更新寄存器“这样的步骤。
cmp指令的执行结果放到了条件码寄存器里面我们的条件跳转指令也是在ALU层面执行的而不是在控制器里面执行的。这样的实现方式在电路层面非常直观我们不需要一个非常复杂的电路就能实现if…else的功能。
第二个,是关于我们在[第](https://time.geekbang.org/column/article/98872)[17讲](https://time.geekbang.org/column/article/98872)里讲到的指令周期、CPU周期和时钟周期的差异。在上面的抽象的逻辑模型中你很容易发现我们执行一条指令其实可以不放在一个时钟周期里面可以直接拆分到多个时钟周期。
我们可以在一个时钟周期里面去自增PC寄存器的值也就是指令对应的内存地址。然后我们要根据这个地址从D触发器里面读取指令这个还是可以在刚才那个时钟周期内。但是对应的指令写入到指令寄存器我们可以放在一个新的时钟周期里面。指令译码给到ALU之后的计算结果要写回到寄存器又可以放到另一个新的时钟周期。所以执行一条计算机指令其实可以拆分到很多个时钟周期而不是必须使用单指令周期处理器的设计。
因为从内存里面读取指令时间很长所以如果使用单指令周期处理器就意味着我们的指令都要去等待一些慢速的操作。这些不同指令执行速度的差异也正是计算机指令有指令周期、CPU周期和时钟周期之分的原因。因此现代我们优化CPU的性能时用的CPU都不是单指令周期处理器而是通过流水线、分支预测等技术来实现在一个周期里同时执行多个指令。
## 总结延伸
好了今天我们讲完了怎么通过连接不同功能的电路实现出一个完整的CPU。
我们可以通过自动计数器的电路来实现一个PC寄存器不断生成下一条要执行的计算机指令的内存地址。然后通过译码器从内存里面读出对应的指令写入到D触发器实现的指令寄存器中。再通过另外一个译码器把它解析成我们需要执行的指令和操作数的地址。这些电路组成了我们计算机五大组成部分里面的控制器。
我们把opcode和对应的操作数发送给ALU进行计算得到计算结果再写回到寄存器以及内存里面来这个就是我们计算机五大组成部分里面的运算器。
我们的时钟信号,则提供了协调这样一条条指令的执行时间和先后顺序的机制。同样的,这也带来了一个挑战,那就是单指令周期处理器去执行一条指令的时间太长了。而这个挑战,也是我们接下来的几讲里要解答的问题。
## 推荐阅读
《编码隐匿在计算机软硬件背后的语言》的第17章用更多细节的流程来讲解了CPU的数据通路。《计算机组成与设计 硬件/软件接口》的4.1到4.4小节从另外一个层面和角度讲解了CPU的数据通路的建立推荐你阅读一下。
## 课后思考
CPU在执行无条件跳转的时候不需要通过运算器以及ALU可以直接在控制器里面完成你能说说这是为什么吗
欢迎在留言区写下你的思考和疑惑,你也可以把今天的内容分享给你的朋友,和他一起学习和进步。