1
1
mirror of https://github.com/foxsen/archbase.git synced 2026-04-28 12:31:29 +08:00

initial import to public repository

This commit is contained in:
Zhang Fuxin
2021-10-27 19:14:51 +08:00
commit c632bed67e
362 changed files with 53748 additions and 0 deletions

909
18-microarch.Rmd Normal file
View File

@@ -0,0 +1,909 @@
# (PART) Part IV CPU的微结构 {-}
# 运算器设计
## 二进制与逻辑电路
### 计算机中数的表示 {#sec-number-presentation}
人们使用计算机处理信息。无论被处理信息的实质形态如何千差万别,计算机内部只能处理离散化的编码后的数据。目前,计算机系统内部的所有数据均采用二进制编码。这样做的原因主要有两点。
1二进制只有“0”和“1”两个数其编码、计数和运算规则都很简单特别是其中的符号“0”和“1”恰好可以与逻辑命题的“假”和“真”两个值相对应因而能通过逻辑电路方便地实现算术运算。
2二进制只有两种基本状态使用有两个稳定状态的物理器件就能表示二进制数的每一位而制造有两个稳定状态的物理器件要比制造有多个稳定状态的物理器件容易得多。例如电压的“高” “低”磁极的“N”“S”脉冲的“正”“负”磁通量的“有”“无”。半导体工艺无论是TTL、ECL还是CMOS都是用电压的高低来表示二进制的两个基本状态。
所有数据在计算机内部最终都交由机器指令处理,被处理的数据分为数值数据和非数值数据两类。数值数据用于表示数量的多少;非数值数据就是一个二进制比特串,不表示数量多少。对于给定的一个二进制比特串,要确定其表达的数值,需要明确三个要素:进位制、定/浮点表示和编码规则。
1.二进制
二进制同人们日常使用的十进制原理是相同的,区别仅在于两者的基数不同。
一般地任意一个R进制数R是正整数
$$A=a_{n}a_{n-1}\ldots a_{1}a_{0}.a_{-1}a_{-2}\ldots a_{1-m}a_{-m} (m,n为正整数)$$
其值可以通过如下方式求得:
$$\begin{aligned}Value(A)&=a_{n}\times R^{n}+a_{n-1}\times R^{n-1}+\ldots +a_{1}\times R^{1}+a_{0}\times R^{0}+a_{-1}\times R^{-1}\\ &\ +a_{-2}\times R^{-2}+\ldots +a_{1-m}\times R^{1-m}+a_{-m}\times R^{-m}\end{aligned}$$
其中R称为基数代表每个数位上可以使用的不同数字符号的个数。$R_{i}$称为第i位上的权即采用“逢R进一”。
二进制即是上述一般性定义中R=2的具体情况。
上面的定义只回答了非负数或无符号整数的二进制表示问题。有关正负整数的表示问题会在\@ref(sec-number-presentation)节讨论。下面举例说明无符号二进制整数的表示和加法。
例用4位二进制编码计算5+9。
解5的4位二进制表示为$0101_{2}$9的4位二进制表示为$1001_{2}$。5+9列竖式计算如下
```{r vertical-calculation, echo = FALSE, message=FALSE}
readr::read_csv('./materials/chapter8/vertical_calculation.csv') %>%
flextable() %>%
border_remove() %>%
delete_part(part='header') %>%
align(i = NULL, j = NULL, align = "right", part = "all") %>%
hline(i = 2, j = NULL) %>%
#hline_top() %>%
autofit()
```
上面的竖式计算过程和人们日常的十进制竖式加法计算过程极为相似。所不同的仅在于,十进制是“逢十进一”,二进制是“逢二进一”。
计算机内部的所有数据都采用二进制编码表示但是在表示绝对值较大的数据时需要很多位不利于书写和阅读因此经常采用十六进制编码来记录数据。因为16恰为$2^4$所以二进制和十六进制相互转换时不会出现除不尽的情况可以非常快捷地进行两种进制的转换运算。具体做法是将一个数由二进制编码转换为十六进制编码时从小数点开始向左、向右两个方向每4个二进制位一组不足时小数点左侧的左补0小数点右侧的右补0直接替换为一个十六进制位。十六进制编码转换为二进制编码的方法类似只是每个十六进制位替换为4个二进制位。
2. 定点数的表示
常见的数有整数和实数之分整数的小数点固定在数的最右边通常省略不写而实数的小数点则不是固定的。但是计算机中只能表示0和1无法表示小数点因此计算机中表示数值数据必须要解决小数点的表示问题。我们通过约定小数点的位置来解决该问题。小数点位置约定在固定位置的数称为定点数小数点位置约定为可以浮动的数称为浮点数。其中浮点数的表示将在下面介绍。这里将介绍计算机中最常见的两种定点数表示方法原码和补码。
在明确了进位制和小数点位置的约定之后,整数在计算机中的表示还有一个正负号如何表示的问题要解决。针对这一问题,原码和补码这两种编码规则采用了不同的解决思路。
1原码
数的原码表示采用“符号-数值”的表示方式,即一个形如$A=a_{n-1}a_{n-2}\ldots a_{1}a_{0}$的原码表示,最高位$a_{n-1}$是符号位0表示正数1表示负数其余位$a_{n-2}\ldots a_{1}a_{0}$表示数值的绝对值。如果$a_{n-1}$是0则A表示正数$+a_{n-2}\ldots a_{1}a_{0}$;如果$a_{n-1}$是1则A表示负数$-a_{n-2}\ldots a_{1}a_{0}$。例如,对于+19和-19这两个数如果用8位二进制原码表示则+19的原码是$00010011_{2}$-19的原码是$10010011_{2}$。
原码表示有两大优点:
1与人们日常记录正负数的习惯接近与真实数值之间的对应关系直观利于与真实数值相互转换。
2原码实现乘除运算比较简便直接。
但是原码表示亦存在两个缺点:
1存在两个0即一个+0一个-0。这不仅有悖于人们的习惯也给使用带来不便。
2原码的加减运算规则复杂这对于逻辑实现的影响很大。在进行原码加减运算时需要首先判断是否为异号相加或同号相减的情况如果是的话则必须先根据两个数的绝对值的大小关系来决定结果的正负号再用绝对值大的数减去绝对值小的数。
权衡上述利弊,现代计算机中基本不使用原码来表示整数。原码仅在表示浮点数的尾数部分时采用。
2补码
补码是定点数的另一种表示方法。现代计算机中基本都是采用补码来表示整数。它最大的好处就是可以用加法来完成减法运算,实现加减运算的统一。这恰好解决了原码表示所存在的最大问题。
在补码表示中其最高位同原码一样也作为符号位0表示正数1表示负数。补码表示和原码表示的差异在于其数值的计算方法。对于一个形如$A=a_{n-1}a_{n-2}\ldots a_{1}a_{0}$的补码表示,其值等于$-2^{n-1}\times a_{n-1}+a_{n-2}\ldots a_{1}a_{0}$。如果$a_{n-1}$是0则补码和原码一样A表示正的$a_{n-2}\ldots a_{1}a_{0}$;如果$a_{n-1}$是1则A表示$a_{n-2}\ldots a_{1}a_{0}$减去$10\ldots 0_{2}$共n-1个0得到的数。
求一个数的补码是个取模运算。关于模运算系统的准确数学描述感兴趣的读者可以自行查阅相关资料。这里举一个最为常见的模运算系统的例子——时钟。这个模系统的模数为12。假定现在时钟指向6点需要将它拨向10点那么你有两种拨法一种是顺时针向前拨4个小时另一种是逆时针向后拨8个小时。这种相同的效果用数学的语言来说即4≡-8 (mod 12)。基于模运算系统的概念对于具有1位符号位和n-1位数值位的n位二进制整数的补码来说其补码的定义是
$$[X]_{\mbox{补}}=2^{n}+X (mod 2^{n})-2^{n-1}≤X<2^{n-1}$$
利用补码基于模运算的这个特点,可以把减法转换成加法来做,因此在计算机中不用把加法器和减法器分开,只要有加法器就可以做减法。
根据上述补码的定义并不容易写出一个数值的补码形式而前面提到的原码可以很直观地与其数值进行转换。这里介绍一个原码和补码之间的转换方法最高位为0时原码与补码相同最高位是1时原码的最高位不变其余位按位取反后末位加1。举个例子譬如+19这个数如果用8位二进制原码表示是$00010011_{2}$最高位是0所以其二进制补码也是$00010011_{2}$。那么对于-19这个数其原码就是把+19原码的最高位从0变为1即$10010011_{2}$。在求-19的补码时原码最高位的1保持不变原码余下的7位$0010011_{2}$按位取反得到$1101100_{2}$末位再加一个1得到$1101101_{2}$,最终得到-19的8位补码是$11101101_{2}$,这个值实际上是由+19的8位补码减去$10000000_{2}(128_{10})$得到的。
3溢出
无论采用原码表示还是补码表示当一个二进制数的位数确定后其能够覆盖的数值范围也就确定了。例如n位的二进制有符号数其原码表示范围是[$-2^{n-1}+1$ , $2^{n-1}-1$],其补码表示范围是[$-2^{n-1}$ , $2^{n-1}-1$]。当同符号数相加或异符号数相减时结果的数值就可能会超过该长度编码下可表示的范围称之为溢出。例如使用4位二进制编码计算-7+5-7的补码是$1001_{2}$+5的补码是$0101_{2}$,两者相加是$1110_{2}(-2_{10})$两异号数相加不会溢出。又比如使用4位二进制编码计算5+4+5的补码是$0101_{2}$+4的补码是$0100_{2}$,两者相加得到$1001_{2}(-7_{10})$,这显然是溢出,两个正数相加得到了一个负数。
加法溢出的判断方法是如果A和B的最高位一样但是A+B结果的最高位与A和B的最高位不一样表示溢出即两个正数相加得到负数或两个负数相加得到正数。减法溢出的判断方法类似即负数减正数结果是正数或正数减负数结果是负数这就表示溢出。
3. 浮点数的表示
计算机中用于数据存储、传输和运算的部件的位数都是有限的,所以采用定点数表示数值数据时有一个不足之处,就是表示范围有限,太大或太小的数都不能表示。同时定点数表示精度也有限,用定点做除法不精确。此外,定点数也无法表示数学中的实数。所以,计算机还定义了浮点数,用来表示实数并弥补定点数的不足。
1二进制的科学记数法
在具体介绍计算机浮点数表示规格前,我们先回忆一下日常书写实数时所采用的科学记数法。譬如$0.000000001_{10}$可以记为$1.0_{10}\times 10^{-9}$$-31576000_{10}$可以记为$-3.1576_{10}\times 10^{7}$。一个采用科学记数法表示的数,如果尾数没有前导零且小数点左边只有一位整数,则可称为规格化数。既然我们可以用科学记数法来表示十进制实数,也可以用科学记数法来表示二进制实数。其一般的表示形式为:
$$(-1)^{s}\times f\times 2^{e}$$
其中s表示符号f为尾数域的值e为指数域的值。
譬如二进制实数的科学记数法表示:$1.1_{2}\times 2^{4}=2.4_{10}\times 10^{1}$$-1.0_{2}\times 2^{-7}=-7.8125_{10}\times 10^{-3}$。
2IEEE 754浮点数标准
计算机中的浮点数表示沿用了科学记数法的表示方式即包含了符号、尾数和阶码三个域。符号用一位二进制码表示0为正1为负。然而在计算机内部位宽是有限的余下的尾数和阶码两者间存在一个此消彼长的关系需要设计者在两者间权衡增加尾数的位宽会提高表示的精度但是会减少表示的范围而增加阶码的位宽虽然扩大了表示的范围但是会降低表示的精度。因为浮点数规格的定义融入了设计者自身的考虑所以直到20世纪80年代初浮点数表示格式还没有统一标准不同厂商的计算机内部的浮点数表示格式存在差异。这导致在不同厂商计算机之间进行含有浮点数的数据传送或程序移植时必须进行数据格式的转换更为糟糕的是有时这种数据格式转换会带来运算结果不一致的问题。因此从20世纪70年代后期开始IEEE成立委员会着手制定统一的浮点数标准最终在1985年完成了浮点数标准IEEE 754的制定。该标准的主要起草者是美国加州大学伯克利分校数学系教授William Kahan他帮助Intel公司设计了8087浮点协处理器并以此为基础形成了IEEE 754标准他本人也因此获得了1987年的图灵奖。自IEEE 754标准颁布后目前几乎所有的计算机都遵循该标准来表示浮点数。在过去的几十年间IEEE 754标准也根据工业界在CPU研发过程中遇到的新需求、实现的新结构及时进行演进和完善。其中一个比较重要的版本是2008年更新的IEEE 754-2008。该版本中明确了有关融合乘加Fused Multiply-Add运算、半精度浮点数等方面的内容。本书仅介绍IEEE 754标准中涉及单精度、双精度浮点数表示的基本内容对其他内容感兴趣的读者可查阅相关文献。
3IEEE 754标准浮点数格式
IEEE 754标准中定义了两种基本的浮点数格式32位的单精度格式和64位的双精度格式如图\@ref(fig:IEEE754float)所示。
```{r IEEE754float, fig.cap='IEEE 754浮点数格式', fig.align='center', echo = FALSE, out.width='100%'}
knitr::include_graphics('./images/chapter8/IEEE754_float.png')
```
32位单精度格式中包含1位符号、8位阶码和23位尾数64位双精度格式中包含1位符号、11位阶码和52位尾数。两种格式下基数均隐含为2。
IEEE 754标准中尾数用原码表示。由于表示同一个数的时候尾数可以有多种表示例如$0.001_{2}$可以表示为$0.1_{2}\times 2^{-2}$,也可以表示成$1.0_{2}\times 2^{-3}$因此需要一个规格化的表示来使得表示唯一。IEEE 754标准中规格化尾数的表示统一为1.xxxx的形式。尾数规格化后第一位总为1因而可以在尾数中缺省这一位1。隐藏该位后尾数可以多一位表示精度提高一位。
IEEE 754标准中阶码是用减偏置常量的移码表示但是所用的偏置常量并不是通常n位移码所用的$2^{n-1}$,而是$(2^{n-1}-1)$因此单精度和双精度浮点数的偏置常量分别为127和1023。
IEEE 754标准对浮点数的一些情况做了特殊的规定总的来说可以分为5种情况主要用阶码进行区分表\@ref(tab:tabIEEE754float)给出了IEEE 754标准中单精度和双精度不同浮点数的表示。
```{r tabIEEE754float, echo = FALSE, message=FALSE, tab.cap='IEEE 754 浮点数格式'}
readr::read_csv('./materials/chapter8/IEEE754float.csv') %>%
flextable() %>%
colformat_md() %>%
#align(i = NULL, j = NULL, align = "center", part = "all") %>%
delete_part(part='header') %>%
add_header_row(values=c(' ', '符号','阶码','尾数','值', '符号','阶码','尾数','值')) %>%
add_header_row(values=c(' ', '单精度','单精度','单精度','单精度','双精度','双精度','双精度','双精度')) %>%
merge_v(part='header') %>%
merge_h(part='header') %>%
#align(i = NULL, j = NULL, align = "center", part = "all") %>%
theme_box() %>%
align(i = NULL, j = NULL, align = "center", part = "all") %>%
autofit()
```
1无穷大阶码全1尾数全0
引入无穷大是为了在出现浮点计算异常时保证程序能够继续执行下去,同时也为程序提供一种检测错误的途径。$+\infty$在数值上大于所有有限浮点数,$-\infty$在数值上小于所有有限浮点数。无穷大不仅可以是运算的结果也可以作为运算的源操作数。当无穷大作为源操作数时根据IEEE 754标准规定可以得到无穷大或非数的结果。
2非数NaN阶码全1尾数非0
非数NaN表示一个没有定义的数。引入非数的目的是检测非初始化值的使用而且在计算出现异常时程序能够继续执行下去。非数根据尾数的内容又可以分为发信号非数Signaling NaN和不发信号非数Quiet NaN两种。如果源操作数是Quiet NaN则运算结果还是Quiet NaN如果源操作数是Signaling NaN则会触发浮点异常。
3规格化非0数阶码非全0非全1
阶码e的值落在[1, 254](单精度)和[1, 2046]双精度范围内且尾数f是非0值的浮点数是规格化的非0数。其尾数经过规格化处理最高位的1被省略因此如果符号位是0则表示数值为$1.f\times 2^{e-127}$(单精度)和$1.f\times 2^{e-1023}$双精度如果符号位是1则表示数值为$-1.f\times 2^{e-127}$(单精度)和$-1.f\times 2^{e-1023}$(双精度)。
4非规格化非0数阶码全0尾数非0
在规格化非0数中能表示的浮点数的最小阶值是-126单精度和-1022双精度如果浮点数的绝对值小于$1.0\times 2^{-126}$(单精度)和$1.0\times 2^{-1022}$双精度该如何表示呢IEEE 754允许特别小的非规格化数此时阶码为0尾数的小数点前面的那个1就不再添加了。因此如果符号位是0则表示数值为$0.f\times 2^{e-126}$(单精度)和$0.f\times 2^{e-1022}$双精度如果符号位是1则表示数值为$-0.f\times 2^{e-126}$(单精度)和$-0.f\times 2^{e-1022}$双精度。非规格化数填补了最小的规格化数和0之间的一段空隙使得浮点数值可表示的精度进一步提升了很多。
5阶码全0尾数全0
根据符号位的取值,分为+0和-0。
### MOS晶体管工作原理 {#sec-MOS-principle}
从原理上看只要有一个二值系统并且系统中能够进行与、或、非这样的基本操作就能够搭建出一台计算机。最早期的电子计算机使用继电器或电子管实现二值系统而现代计算机中则采用晶体管来实现二值系统。晶体管可以根据控制端电压或电流的变化来实现“开启”和“关闭”的功能从而表达二进制。晶体管主要分为双极型晶体管Bipolar Junction Transistor和金属-氧化物半导体场效应晶体管Metal Oxide Semiconductor Field Effect Transistor简称MOSFET或MOS。当前绝大多数CPU都采用MOS晶体管实现其中又以CMOSComplementary Metal Oxide Semiconductor晶体管电路设计最为常见。
1. 半导体
MOS晶体管使用硅作为基本材料。在元素周期表中硅是IV族元素它的原子最外层有4个电子可以与相邻的4个硅原子的最外层电子配对形成共价键。图\@ref(fig:silicium)a给出了纯净硅中原子连接关系的一个简单二维平面示意实际上纯净硅中原子构成的是一个正四面体立体网格。通过与相邻原子形成的共价键纯净硅中所有原子的最外层都具有8个电子达到相对稳定所以纯净硅的导电性很弱。但是如果在纯净硅中掺杂少量5价的原子如磷这些原子将挤占原有硅原子的位置而由于这些原子的最外层有5个电子除了与原有硅原子形成共价键用掉4个电子外还多余一个处于游离状态的电子如图\@ref(fig:silicium)b所示。在电场的作用下处于游离状态的电子就会逆着电场方向流动形成负电流。这类材料被称为NNegative型材料。同样如果在纯净的硅中掺杂少量3价的原子如硼那么这些原子挤占原有硅原子的位置后其最外层还缺少一个电子和相邻的硅原子形成共价键形成空穴如图\@ref(fig:silicium)c所示。在电场的作用下周围的电子就会跑过来填补这个空穴从而留下一个新的空穴相当于空穴也在顺着电场方向流动形成正电流。这类材料被称为PPositive型材料。当非4价元素掺杂的含量较小时产生的电子和空穴也就比较少用—号表示当非4价元素掺杂的含量较大时产生的电子和空穴也就比较多用+号表示。因此P-表示掺杂浓度低的P型材料里面只有少量的空穴N+表示掺杂浓度高的N型材料里面有大量电子。
```{r silicium, fig.cap='半导体硅原子结构示意图', fig.align='center', echo = FALSE, out.width='100%'}
knitr::include_graphics('./images/chapter8/sicicium_atomic_structure.png')
```
2. NMOS和PMOS晶体管
如图\@ref(fig:MOS-struc)所示MOS晶体管是由多层摞放在一起的导电和绝缘材料构建起来的。每个晶体管的底部叫作衬底是低浓度掺杂的半导体硅。晶体管的上部接出来3个信号端口分别称为源极Source、漏极Drain和栅极Gate。源极和漏极叫作有源区该区域内采用与衬底相反极性的高浓度掺杂。衬底是低浓度P型掺杂有源区是高浓度N型掺杂的MOS晶体管叫作NMOS晶体管衬底是低浓度N型掺杂有源区是高浓度P型掺杂的MOS晶体管叫作PMOS晶体管。无论是NMOS管还是PMOS管其栅极与衬底之间都存在一层绝缘体叫作栅氧层其成分通常是二氧化硅SiO~2~。最早期的MOS晶体管栅极由金属制成后来的栅极采用掺杂后的多晶硅制成。掺杂后的多晶硅尽管其电阻比金属大但却比半导体硅的电阻小很多可以作为电极。并且同普通金属相比多晶硅更耐受高温不至于在MOS晶体管生产过程中融化。不过最新的工艺又有重新采用金属栅极的。
```{r MOS-struc, fig.cap='MOS晶体管组成结构示意图', fig.align='center', echo = FALSE, out.width='100%'}
knitr::include_graphics('./images/chapter8/MOS_structure.png')
```
上面简述了MOS晶体管的基本构成下面以NMOS晶体管为例介绍MOS晶体管的工作原理。如果单纯在源极、漏极之间加上电压两极之间是不会有电流流过的因为源极和漏极之间相当于有一对正反相对的PN结如图\@ref(fig:NMOS-work)a所示。如果先在栅极上加上电压因为栅氧层是绝缘的就会在P衬底里形成一个电场。栅极上的正电压会把P衬底里面的电子吸引到栅氧层的底部形成一个很薄的沟道电子层相当于在源极和漏极之间架起了一座导电的桥梁。此时如果再在源极、漏极之间加上电压那么两极之间的电流就能流过来了如图\@ref(fig:NMOS_work)b所示。NMOS的基本工作原理就是这样但是其实际的物理现象却很复杂。
```{r NMOS-work, fig.cap='NMOS晶体管工作原理示意图', fig.align='center', echo = FALSE, out.width='80%'}
knitr::include_graphics('./images/chapter8/NMOS_workingprinciple.png')
```
当我们屏蔽掉底层的物理现象细节对MOS晶体管的工作行为进行适度抽象后NMOS晶体管的工作行为就是在栅极上加上电就通不加电就断。PMOS晶体管的工作行为与NMOS晶体管的恰好相反加上电就断不加电就通。这样我们可以简单地把MOS晶体管当作开关。NMOS晶体管是栅极电压高时打开栅极电压低时关闭PMOS晶体管反过来栅极电压低时打开栅极电压高时关闭。如图\@ref(fig:MOS-switch)所示。随着工艺的发展MOS晶体管中栅氧层的厚度越来越薄使得开启所需的栅极电压不断降低。晶体管的工作电压从早期工艺的5.0V降到后来的2.5V、1.8V现在都是1V左右或更低。
```{r MOS-switch, fig.cap='MOS晶体管开关行为', fig.align='center', echo = FALSE, out.width='50%'}
knitr::include_graphics('./images/chapter8/MOS_switch.png')
```
尽管MOS晶体管可以表现出开关的行为但是单纯的PMOS晶体管或者NMOS晶体管都不是理想的开关。例如NMOS晶体管适合传输0而不适合传输1PMOS晶体管恰好相反适合传输1而不适合传输0。在后面讲述常见CMOS电路时将会论及如何解决这一问题。
### CMOS逻辑电路
在了解了MOS晶体管的组成和基本原理后我们接下来了解如何用MOS晶体管构建逻辑电路。
1. 数字逻辑电路
1布尔代数
数字逻辑基于的数学运算体系是布尔代数。布尔代数是在二元集合{0, 1}基础上定义的。最基本的逻辑运算有三种AND&、或OR|、非NOT~)。这三种逻辑关系定义如下:
```{r boolean, echo = FALSE, message=FALSE}
readr::read_csv('./materials/chapter8/boolean.csv') %>%
flextable() %>%
border_remove() %>%
align(i = NULL, j = NULL, align = "center", part = "all") %>%
vline(i = NULL, j = 2) %>%
hline_top() %>%
autofit()
```
常用的布尔代数运算定律有:
恒等律:$A \ |\ 0 = A,\ A \ \&\ 1 = A$
0/1律$A \ |\ 1 = 1,\ A \ \&\ 0 = 0$
互补律:$A \ |\ ( \sim A) = 1,\ A \ \&\ (\sim A) = 0$
交换律:$A \ |\ B = B \ |\ A,\ A \ \&\ B = B \ \&\ A$
结合律:$A \ |\ (B \ |\ C) = (A \ |\ B) \ |\ C,\ A \ \&\ (B \ \&\ C) = (A \ \&\ B) \ \&\ C$
分配律:$A \ \&\ (B \ |\ C) = (A \ \&\ B) \ |\ (A \ \&\ C),\ A \ |\ (B \ \&\ C) = (A \ |\ B) \ \&\ (A \ |\ C)$
德摩根DeMorgan定律$\sim (A \ \&\ B) = (\sim A) \ |\ (\sim B),\ \sim (A \ |\ B) = (\sim A) \ \&\ (\sim B)$。
上述定律虽然很简单,但使用起来变化无穷。
根据电路是否具有数据存储功能,可将数字逻辑电路分为组合逻辑电路和时序逻辑电路两类。
2组合逻辑
组合逻辑电路中没有数据存储单元电路的输出完全由当前的输入决定。在组合逻辑的各种表达方式中最简单的就是真值表即对每一种可能的输入组合给出输出值。显然一个N输入的电路就有$2^{N}$种不同的输入组合。常见的门级组合逻辑除了与门AND、或门OR、非门NOT还有与非门NAND、或非门NOR、异或门XOR。图\@ref(fig:logicgate)给出了这些常见的门逻辑符号及其真值表。
```{r logicgate, fig.cap='常用基本逻辑门电路', fig.align='center', echo = FALSE, out.width='100%'}
knitr::include_graphics('./images/chapter8/logic_gate.png')
```
利用基本逻辑门电路可以构成具有特定功能的更大规模的组合逻辑部件,如译码器、编码器、多路选择器、加法器等。加法器和乘法器两类运算逻辑电路我们将在后续章节中介绍。表\@ref(tab:3-8decoder)所示是3-8译码器真值表把3位信号译码成8位输出当输入000时8个输出里面最低位为1输入为001时次低位为1依次类推。表\@ref(tab:8-1selector)所示是一个8选1选择器的真值表当CBA为000的时候选择输出第0路$D_{0}$为001的时候选择输出第1路$D_{1}$,依次类推。可以看出选择器可以用译码器加上与门来实现。
```{r 3-8decoder, echo = FALSE, message=FALSE, tab.cap='3-8译码器真值表'}
readr::read_csv('./materials/chapter8/3-8decoder_true_table.csv') %>%
flextable() %>%
colformat_md() %>%
delete_part(part='header') %>%
add_header_row(values=c('输入', '输入','输入','输出','输出','输出','输出','输出','输出','输出','输出')) %>%
merge_h(part='header') %>%
vline(i = NULL, j = 3) %>%
hline_top(part='all') %>%
align(i = NULL, j = NULL, align = "center", part = "all") %>%
autofit()
```
```{r 8-1selector, echo = FALSE, message=FALSE, tab.cap='8选1选择器真值表'}
#readr::read_csv('./materials/chapter8/3-8decoder_true_table.csv') %>%
readr::read_csv('./materials/chapter8/8-1selector_true_table.csv') %>%
flextable() %>%
colformat_md() %>%
delete_part(part='header') %>%
add_header_row(values=c('输入', '输入','输入','输出')) %>%
merge_h(part='header') %>%
vline(i = NULL, j = 3) %>%
hline_top(part='all') %>%
align(i = NULL, j = NULL, align = "center", part = "all") %>%
autofit()
```
3时序逻辑
时序逻辑电路包含时钟信号和数据存储单元两个要素。时序逻辑电路的特点在于,其输出不但与当前输入的逻辑有关,而且与在此之前曾经输入过的逻辑值有关。
时钟信号是时序逻辑电路的基础,它用于确定时序逻辑元件中的状态在何时发生变化。如图\@ref(fig:clocksignal)所示时钟信号是具有固定周期的标准脉冲信号。每个时钟周期分为高、低电平两部分其中低电平向高电平变化的边沿称为上升沿高电平向低电平变化的边沿称为下降沿。在CPU设计中通常使用边沿触发方式来确定时序逻辑状态变化的时间点。所谓边沿触发就是将时钟信号的上升沿或下降沿作为采样的同步点在每个采样同步点对时序逻辑电路的状态进行采样并存储到数据存储单元中。
```{r clocksignal, fig.cap='时钟信号', fig.align='center', echo = FALSE, out.width='80%'}
knitr::include_graphics('./images/chapter8/clock_signal.png')
```
数据存储单元是时序逻辑电路中的核心。数据存储单元多由锁存器构成。首先介绍RS锁存器。图\@ref(fig:RSlatch)是RS锁存器的逻辑图和真值表。RS锁存器包含置位端SSet和复位端RReset两个输入端口R为0、S为1时置输出为1R为1、S为0时输出为0。在图\@ref(fig:RSlatch)中下面与非门的输出接到上面与非门的一个输入同样上面与非门的输出接到下面与非门的一个输入通过两个成蝶形连接的与非门构成RS锁存器。RS锁存器与组合逻辑的不同在于R, S的值从0, 11, 0变成1, 1时能够保持输出值的状态不变从而实现数据的存储。组合的输出只跟输入相关但是RS锁存器的输入变了它的输出还能保持原来的值。
```{r RSlatch, fig.cap='RS锁存器', fig.align='center', echo = FALSE, out.width='50%'}
knitr::include_graphics('./images/chapter8/RS_latch.png')
```
在RS锁存器前面连接上两个与非门再用时钟CClock控制D输入就构成了如图\@ref(fig:Dlatch)a所示的电路。当C=0时R和S都为1RS锁存器处于保持状态也就是说当时钟处于低电平时无论输入D怎样变化输出都保持原来的值。当C=1时输出Q与输入D值相同相当于直通。这就是D锁存器D Latch的原理通过时钟C的电平进行控制高电平输入低电平保持。
两个D锁存器以图\@ref(fig:Dlatch)b所示的方式串接起来就构成了一个D触发器D Flip-Flop。当C=0时第一个D锁存器直通第二个D锁存器保持当C=1时第一个D锁存器保持第二个D锁存器直通C从0变为1时D的值被锁存起来。这就是D触发器的基本原理它是通过时钟的边沿进行数据的锁存。
```{r Dlatch, fig.cap='D锁存器和D触发器', fig.align='center', echo = FALSE, out.width='100%'}
knitr::include_graphics('./images/chapter8/D_latch.png')
```
实际情况下由于器件中电流的速度是有限的并且电容充放电需要时间所以电路存在延迟。为了保证D触发器正常工作需要满足一定的延迟要求。例如为了把D的值通过时钟边沿锁存起来要求在时钟变化之前的某一段时间内D的值不能改变这个时间叫作建立时间Setup Time。另外在时钟跳变后的一段时间内D的值也不能变这个时间就是保持时间Hold Time。建立时间和保持时间可以是负数。此外D触发器还有一个重要的时间参数叫作“Clock-to-Q”时间也就是时钟边沿到来后Q端数据更新为新值的时间。D触发器整个的访问延迟是建立时间加上“Clock-to-Q”时间。图\@ref(fig:Dlatchtime)给出了上升沿触发的D触发器的建立时间、保持时间以及“Clock-to-Q”时间的示意。
```{r Dlatchtime, fig.cap='D触发器建立时间、保持时间和Clock-to-Q时间', fig.align='center', echo = FALSE, out.width='50%'}
knitr::include_graphics('./images/chapter8/Dlatch_timing.png')
```
2. 常见CMOS电路
本节通过若干具体示例讲述如何用MOS晶体管实现逻辑电路且所列举的电路都是CMOS电路。关于CMOS电路的基本特点将在“非门”示例之后予以说明。
1非门
```{r NOTgate, fig.cap='CMOS电路非门', fig.align='center', echo = FALSE, out.width='100%'}
knitr::include_graphics('./images/chapter8/CMOS_NOT_gate.png')
```
图\@ref(fig:NOTgate)a是非门也称作反相器的CMOS电路它由一个PMOS晶体管和一个NMOS晶体管组成其中PMOS晶体管以下简称“P管”的源极接电源NMOS晶体管以下简称“N管”的源极接地两管的漏极连在一起作为输出栅极连在一起作为输入。如果输入为0接地则P管导通N管关闭P管的电阻为0实际电路中P管其实有一定的电阻大约在几千欧姆N管的电阻无穷大输出端的电压就是电源电压$V_{dd}$,如图\@ref(fig:NOTgate)b所示。反之当输入为1的时候N管导通P管关闭N管的电阻为0实际电路中N管其实有一定的电阻大约在几千欧姆P管的电阻无穷大输出端与电源断开与地导通输出端电压为0如图\@ref(fig:NOTgate)c所示。这就是反相器CMOS电路的工作原理。
从反相器的工作原理可以看出CMOS电路的基本特征其关键就在“C”Complementary互补即由上下两个互补的部分组成电路上半部分由P管构成下半部分由N管构成。上半部分打开的时候下半部分一定关上下半部分打开的时候上半部分一定关闭。这种电路设计的好处是在稳定状态电路中总有一端是关死的几乎没有直流电流可以大幅度降低功耗。
2与非门
图\@ref(fig:NANDgate)所示的是一个两输入与非门的CMOS电路电路上面两个P管并联下面两个N管串联。两个P管并联后一头接电源另一头与两个串联的N管连接。两个N管串联后一头与并联的P管连接另一头接地。与非门的两个输入A和B分别连接到一个N管和一个P管输出端是Y。当A和B中有一个为0则上面的P管网络导通下面的N管网络断开输出端被连接到电源上即输出Y为1。
```{r NANDgate, fig.cap='CMOS电路与非门', fig.align='center', echo = FALSE, out.width='50%'}
knitr::include_graphics('./images/chapter8/CMOS_NAND_gate.png')
```
3或非门
```{r NORgate, fig.cap='CMOS电路或非门', fig.align='center', echo = FALSE, out.width='50%'}
knitr::include_graphics('./images/chapter8/CMOS_NOR_gate.png')
```
图\@ref(fig:NORgate)所示的是一个两输入或非门的CMOS电路电路上面两个P管串联下面两个N管并联。两个P管串联后一头接电源另一头与两个并联的N管连接。两个N管并联后一头与串联的P管连接另一头接地。或非门的两个输入A和B分别连接到一个N管和一个P管输出端是Y。当A和B中有一个为1则上面的P管网络断开下面的N管网络导通输出端与电源断开连通到地上即输出Y为0。
4传输门
```{r TRANSgate, fig.cap='CMOS电路传输门', fig.align='center', echo = FALSE, out.width='40%'}
knitr::include_graphics('./images/chapter8/CMOS_TRANS_gate.png')
```
前面提到过单纯的PMOS晶体管或是NMOS晶体管都不是理想的开关但是在设计电路时有时需要一个接近理想状态的开关该开关无论对于0还是1都可以传递得很好。解决的方式也很直观如图\@ref(fig:TRANSgate)所示一个P管和一个N管彼此源极连在一起漏极连在一起两者的栅极上接上一对极性相反的使能信号。当$EN$=0、$\overline{EN}$=1时P管和N管都关闭当$EN$=1、$\overline{EN}$=0时P管和N管都开启。当P管和N管都开启时无论信号是0还是1都可以通过最适合传递该信号的MOS晶体管从A端传递到B端。
5D触发器
在前面讲述逻辑电路时介绍过如何用逻辑门搭建D触发器在用CMOS电路实现D触发器时我们也可以利用CMOS的逻辑门搭建出RS锁存器进而搭建出D锁存器并最终得到D触发器。但是考虑到构建D触发器时我们其实真正需要的是开关电路和互锁电路所以这种构建D触发器的方式消耗的资源过多。图\@ref(fig:DFlipFlop)中给出现代计算机中常用的一种D触发器电路结构。该电路的左边虚线框内部可以视作一个去除了输出缓冲器的D锁存器该锁存器存储的值体现在其内部N1点的状态。当$CLK$=0$\overline{CLK}$=1时传输门G1开启、G2关闭D点的值经由反相器I1和传输门G1传递进来并通过反相器I2和三态反相器T1反馈至N1点使该点到达一个稳定状态。当$CLK$=1$\overline{CLK}$=0时传输门G1关闭、G2开启D点值的变化不再影响到内部N1点同时N1点的状态经由传输门G2并通过反相器I3和三态反相器T2反馈至N2点使N2点处于稳定状态并将该值传递至输出端Q。
```{r DFlipFlop, fig.cap='CMOS电路D触发器', fig.align='center', echo = FALSE, out.width='100%'}
knitr::include_graphics('./images/chapter8/CMOS_D_Flip-Flop.png')
```
3. CMOS电路延迟
前面在介绍MOS晶体管原理的时候曾经提到过真实世界中PMOS晶体管和NMOS晶体管即便是在导通状态下源极和漏极之间也是有电阻的栅极和地之间也存在寄生电容。因此CMOS电路工作时输入和输出之间存在延迟该延迟主要由电路中晶体管的RC参数来决定。
图\@ref(fig:inverter)a是一个CMOS反相器的示意图。其输出端有一个对地电容主要由本身P管和N管漏极的寄生电容、下一级电路的栅电容以及连线电容组成。反相器输出端从0到1变化时需要通过P管的电阻对该电容充电从1到0变化时该电容的电荷需要通过N管的电阻放电到地端。图\@ref(fig:inverter)b示意了输出电容的充放电过程其中左图代表充电过程右图代表放电过程。因此该反相器输出从0到1变化时的延迟由P管打开时的电阻和输出电容决定从1到0变化时的延迟由N管打开时的电阻和输出电容决定。图\@ref(fig:inverter)c示意了在该反相器输入端从0变到1、再变回到0的过程中图中虚线表示输出端值变化的过程。从中可以看出反相器从输入到输出的变化是有延迟的而且反相器的输出不是理想的矩形而是存在一定的斜率。
```{r inverter, fig.cap='CMOS反相器的延迟', fig.align='center', echo = FALSE, out.width='80%'}
knitr::include_graphics('./images/chapter8/CMOS_inverter_delay.png')
```
在芯片设计的时候,需要根据单元的电路结构建立每个单元的延迟模型。一般来说,一个单元的延迟由其本身延迟和负载延迟所构成,而负载延迟又与该单元的负载相关。需要指出的是,用早期工艺生成的晶体管,其负载延迟与负载呈线性关系,但对于深亚微米及纳米工艺,晶体管的负载延迟不再与负载呈线性关系。在工艺厂家给出的单元延迟模型中,通常通过一个二维的表来描述每个单元的延迟,其中一维是输入信号的斜率,另外一维是输出负载。即一个单元的延迟是由输入信号的斜率和输出负载两个值通过查表得到的。
## 简单运算器设计
在计算机发展的早期阶段运算部件指的就是算术逻辑单元Arithmetic Logic Unit,简称ALU。ALU可以做算术运算、逻辑运算、比较运算和移位运算。后来功能部件不断发展扩充可以执行乘法、除法、开方等运算。本节主要介绍定点补码加法器的设计。
加法是许多运算的基础。根据不同的性能和面积需求加法器有很多种实现方式。进位处理是加法器的核心。根据进位处理方法的不同常见的加法器包括行波进位加法器Ripple Carry Adder, 简称RCA先行进位加法器Carry Look-ahead Adder, 简称CLA跳跃进位加法器Carry Skip Adder, 简称CSKA进位选择加法器Carry Select Adder, 简称CSLA进位递增加法器Carry Increment Adder, 简称CIA等等。其中行波进位加法器最为简单直接而先行进位加法器使用较为广泛。
### 定点补码加法器
1.一位全加器
一位全加器是构成加法器的基本单元。一位全加器实现两位本地二进制数以及低位的进位位相加求得本地和以及向高位的进位。它有三个1位二进制数输入A、B和Cin其中A和B分别为本地的加数和被加数C~in~为低位来的进位。它有两个1位二进制数输出S和C~out~其中S是本地和C~out~是向高位的进位。一位全加器的真值表如表\@ref(tab:fulladder-truetable)所示。
根据表\@ref(tab:fulladder-truetable),可以写出全加器的逻辑表达式如下:
$$S=\sim A \ \& \ \sim B \ \& \ C_{in} \ | \ \sim A \ \& \ B \ \& \ \sim C_{in} \ | \ A \ \& \ \sim B \ \& \ \sim C_{in} \ | \ A \ \& \ B \ \& \ C_{in}$$
$$C_{out}=A \ \& \ B \ | \ A \ \& \ C_{in} \ | \ B \ \& \ C_{in}$$
上述表达式中,\~表示取反操作,&表示与操作,|表示或操作,其中~操作的优先级最高,&操作次之,|操作优先级最低。上述表达式还可以简单解释为当输入的三个数中有奇数个1时本地和为1当输入的三个数中有两个1时向高位的进位为1。
```{r fulladder-truetable, echo = FALSE, message=FALSE, tab.cap='一位全加器真值表'}
readr::read_csv('./materials/chapter8/fulladder_truetable.csv') %>%
flextable() %>%
vline(i = NULL, j = 3) %>%
align(i = NULL, j = NULL, align = "center", part = "all") %>%
colformat_md(part="all") %>%
autofit()
```
根据上面的逻辑表达式,图\@ref(fig:FULLadder)给出了用非门和与非门搭建的一位全加器的逻辑电路图及其示意图。如果我们不严格区分非门和与非门以及不同数目输入与非门之间的延迟差异则可近似认为每个一位全加器需要2或3级的门延迟。
```{r FULLadder, fig.cap='一位全加器逻辑电路图与示意图', fig.align='center', echo = FALSE, out.width='100%'}
knitr::include_graphics('./images/chapter8/1bit_full_adder_circuit.png')
```
接下来将介绍如何用一位全加器构建一个NN>1位的带进位加法器。
2.行波进位加法器
构建N位带进位加法器的最简单的方法是将N个一位全加器逐个串接起来。图\@ref(fig:32bitRCA)给出了32位行波进位加法器的示意图。其中输入A=a~31~\ldots a~0~和B=b~31~\ldots b~0~分别是加数和被加数C~in~是最低位的进位输出加和S=s~31~\ldots s~0~以及最高位向上的进位C~out~。所谓“行波”是指每一级的一位全加器将来自低位的一位全加器的进位输出C~out~作为本级的进位输入C~in~如波浪一般层层递进下去。这种串行的进位传递方式与人们日常演算十进制加法时采用的进位方式原理一样非常直观。但是这种加法器的电路中每一位的数据相加都必须等待低位的进位产生之后才能完成即进位在各级之间是顺序传递的。回顾一下上一节关于一位全加器的延迟的大致估算可知一位全加器输入到S的最长延迟是3级门、输入到C~out~的最长延迟是2级门。因此32位行波进位加法器中从最低位的输入A~0~、B~0~、C~in~到最高位的进位输出C~out~存在一条进位链其总延迟为2×32=64级门从最低位的输入A~0~、B~0~、C~in~到最高位的进位输入C~in~的延迟为2×31=62级门所以从最低位的输入A~0~、B~0~、C~in~到最高位的加和S~31~的总延迟为62+3=65级门。从这个例子可以看出虽然行波进位加法器直观简单但是其延迟随着加法位数N的增加而线性增长N越大时行波进位加法器的延迟将越发显著。在CPU设计中加法器的延迟是决定其主频的一个重要参数指标如果加法器的延迟太长则CPU的主频就会降低。例如对于一个64位的高性能通用CPU来说在良好的流水线切分下每级流水的延迟应控制在20级门以内所以64位行波进位加法器高达129级门的延迟太长了。
```{r 32bitRCA, fig.cap='32位行波进位加法器', fig.align='center', echo = FALSE, out.width='100%'}
knitr::include_graphics('./images/chapter8/32bit_RCA.png')
```
3.先行进位加法器
为了改进行波进位加法器延迟随位数增加增长过快的缺点,人们提出了先行进位加法器的电路结构。其主要思想是先并行地计算每一位的进位,由于每一位的进位已经提前算出,这样计算每一个的结果只需要将本地和与进位相加即可。下面详细介绍先行进位(或者说并行进位)加法器的设计原理。
1并行进位逻辑
假设两个N位数A和B相加A记作a~N-1~a~N-2~$\ldots$a~i~a~i-1~\ldots a~1~a~0~B记作b~N-1~b~N-2~$\ldots$b~i~b~i-1~$\ldots$b~1~b~0~。定义第i位的进位输入为c~i~进位输出为c~i+1~且将加法器的输入C~in~记作c~0~以方便后面描述的统一。每一位进位输出c~i+1~的计算为:$$c_{i+1}=a_{i}\ \&\ b_{i}\ |\ a_{i}\ \&\ c_{i}\ |\ b_{i}\ \&\ c_{i}=a_{i}\ \&\ b_{i}\ |\ (a_{i}\ |\ b_{i}\ \&\ c_{i}$$
设$g_{i} = a_{i} \ \&\ b_{i}p_{i} = a_{i} \ |\ b_{i}$则c~i+1~的计算可以表达为:
$$c_{i+1}=g_{i} \ |\ p_{i} \ \& \ c_{i}$$
从上式可以看出当g~i~=1时在c~i+1~必定产生一个进位与c~i~无关当p~i~=1时如果c~i~有一个进位输入则该进位可以被传播至c~i+1~。我们称g~i~为第i位的进位生成因子p~i~为第i位的进位传递因子。
下面以4位加法器的进位传递为例根据公式$c_{i+1}=g_{i} \ |\ p_{i}\ \&\ c_{i}$逐级展开可得到:
$c_{1}=g_{0} \ |\ p_{0} \ \&\ c_{0}$
$c_{2}=g_{1}\ |\ p_{1}\ \&\ g_{0}\ |\ p_{1}\ \&\ p_{0}\ \&\ c_{0}$
$c_{3}=g_{2}\ |\ p_{2}\ \&\ g_{1}\ |\ p_{2}\ \&\ p_{1} \ \&\ g_{0} \ |\ p_{2}\ \&\ p_{1}\ \&\ p_{0}\ \&\ c_{0}$
$c_{4}=g_{3}\ |\ p_{3}\ \&\ g_{2}\ |\ p_{3}\ \&\ p_{2}\ \&\ g_{1}\ |\ p_{3}\ \&\ p_{2}\ \&\ p_{1}\ \&\ g_{0}\ |\ p_{3}\ \&\ p_{2}\ \&\ p_{1}\ \&\ p_{0}\ \&\ c_{0}$
扩展之后每一位的进位输出c~i+1~可以由仅使用本地信号生成的g和p直接得到不用依赖前一位的进位输入c~i~。图\@ref(fig:4bitCLA)给出了4位先行进位的逻辑电路图及其示意图。从图\@ref(fig:4bitCLA)中可以看出采用先行进位逻辑产生第4位的进位输出只需要2级门延迟而之前介绍的行波进位逻辑则需要8级门延迟先行进位逻辑的延迟显著地优于行波进位逻辑。当然这里为了电路逻辑的简洁以及计算的简便我们使用了四输入、五输入的与非门这些与非门的延迟比行波进位逻辑中采用的二输入、三输入的与非门的延迟要长但我们不再做进一步细致的区分均视作相同延迟。而且实际实现时也很少采用五输入的与非门其N管网络上串接5个NMOS管电阻值较大电路速度慢。
```{r 4bitCLA, fig.cap='块内并行的4位先行进位逻辑', fig.align='center', echo = FALSE, out.width='100%'}
knitr::include_graphics('./images/chapter8/4bit_CLA.png')
```
2块内并行、块间串行逻辑
理论上可以把上述并行进位方法扩展成更多位的情况,但那需要很多输入的逻辑门,在实现上是不现实的。实现更多位的加法器时通常采用分块的进位方法,将加法器分为若干个相同位数的块,块内通过先行进位的方法计算进位,块间通过行波进位的方法传递进位。图\@ref(fig:32bitCLA)给出了16位加法器中采用该方式构建的进位逻辑。由于块内并行产生进位只需要2级门延迟因此从p~i~和g~i~产生c~16~最多只需要18级门延迟而非行波进位逻辑的32级门延迟。
```{r 32bitCLA, fig.cap='块内并行块间串行的16位先行进位加法器的进位逻辑', fig.align='center', echo = FALSE, out.width='100%'}
knitr::include_graphics('./images/chapter8/32bit_CLA.png')
```
3块内并行、块间并行逻辑
为了进一步提升加法器的速度可以在块间也采用先行进位的方法即块内并行、块间也并行的进位实现方式。与前面类似对于块内进位定义其的进位生成因子为g和进位传递因子为p对于块间的进位传递定义其进位生成因子为G和块间进位传递因子为P则其表达式如下
$$P= p_{3}\ \&\ p_{2}\ \&\ p_{1}\ \&\ p_{0}$$
$$G= g_{3}\ |\ p_{3}\ \&\ g_{2}\ |\ p_{3}\ \&\ p_{2}\ \&\ g_{1}\ |\ p_{3}\ \&\ p_{2}\ \&\ p_{1}\ \&\ g_{0}$$
上面的表达式可以解释为当G为1时表示本块有进位输出生成当P为1时表示当本块有进位输入时该进位可以传播至该块的进位输出。图\@ref(fig:4bitCLAwithc)给出了包含块间进位生成因子和进位传递因子的4位先行进位的逻辑电路及其示意图。
定义上述的块间进位生成因子和进位传递因子是因为这种逻辑设计具有很好的层次扩展性即以层次化的方式构建进位传递逻辑把下一级的P和G输出作为上一级的p~i~和g~i~输入。图\@ref(fig:16bitCLA)给出了一个采用两层并行进位结构的16位先行进位逻辑采用了5块4位先行进位逻辑。其计算步骤是
1下层的4块4位先行进位逻辑根据各块所对应的p~i~和g~i~生成各自的块间进位生成因子G和块间进位传递因子P
2上层的4位先行进位逻辑把下层的先行进位逻辑生成的P和G作为本层的p~i~和g~i~输入生成块间的进位c~4~、c~8~
3下层的每块4位先行进位逻辑分别把c~0~以及上层计算出的c~4~、c~8~和c~12~作为各自块的进位输入c~0~再结合本地的p~i~和g~i~分别计算出本块内部所需要的每一位进位。
```{r 4bitCLAwithc, fig.cap='包含块间进位生成因子和进位传递因子的4位先行进位逻辑', fig.align='center', echo = FALSE, out.width='100%'}
knitr::include_graphics('./images/chapter8/4bit_CLA(include carry factor).png')
```
```{r 16bitCLA, fig.cap='块内并行且块间并行的16位先行进位逻辑', fig.align='center', echo = FALSE, out.width='100%'}
knitr::include_graphics('./images/chapter8/16bit_CLA.png')
```
可以看出从p~i~和g~i~生成下层各块的P、G需要2级门延迟上层根据自身p~i~和g~i~输入生成进位输出c~1~\~c~3~需要2级门延迟下层各块从c~0~输入至生成进位输出c~1~\~c~3~也需要2级门延迟。所以整体来看从p~i~和g~i~生成进位c~1~\~c~16~最长的路径也只需要6级门延迟这比前面介绍的块内并行但块间串行的电路结构更快。而且进一步分析可知块间并行的电路结构中最大的与非门的扇入为4而前面分析块间串行电路结构延迟时那个电路中最大的与非门的扇入为5。
这种块间并行的电路结构在设计更多位的加法器时只需要进一步进行层次化级联就可以。例如仍采用4位先行进位逻辑作为基本块通过3层的树状级联就可以得到64位加法器的进位生成逻辑其从p~i~和g~i~输入到所有进位输出的最长路径的延迟为10级门。感兴趣的读者可以自行推导一下其具体的结构和连接关系。
采用块内并行且块间并行的先行进位逻辑所构建的加法器其延迟随着加法位数的增加以对数的方式增长因而在高性能通用CPU设计中被广泛采用。
### 减法运算实现
在\@ref(sec-number-presentation)节中我们提到,现代通用计算机中定点数都是用补码表示的。补码表示的一个显著优点就是补码的减法可以通过补码加法来实现,即补码运算具有如下性质:
$$[A]_{\mbox{补}}-[B]_{\mbox{补}}=[A-B]_{\mbox{补}}=[A]_{\mbox{补}}+[-B]_{\mbox{补}}$$
而-B~补~可以通过将B~补~“按位取反末位加1”的法则进行计算。所以只需要将被减数直接接到加法器的A输入减数按位取反后接到加法器的B输入同时将加法器的进位输入C~in~置为1就可以用加法器完成A~补~-B~补~的计算了,如图\@ref(fig:SUBer)a所示。在此基础之上可以将加法和减法统一到一套电路中予以实现如图\@ref(fig:SUBer)b所示其中SUB作为加、减法的控制信号。当SUB信号为0时表示进行加法运算当SUB信号为1时表示进行减法运算。
```{r SUBer, fig.cap='利用加法器实现减法', fig.align='center', echo = FALSE, out.width='50%'}
knitr::include_graphics('./images/chapter8/subtracter.png')
```
### 比较运算实现
常见基本运算中除了加减法外还有比较运算。比较运算主要包含两种类型:一是判断两个数的相等情况,二是判断两个数的大小情况。
判断两个数相等所需要的逻辑电路比较简单,图\@ref(fig:comparer)给出了一个4位相等比较的逻辑电路及其示意图。电路首先采用异或逻辑逐位比较输入A和B的对应位是否相同所得到的结果中只要出现一个1则表示两者不相等输出结果为0否则结果为1。更多位数的相等比较的电路原理与所举的例子基本一致只是在实现时判断异或结果中是否有1需要多级逻辑完成以降低逻辑门的扇入数目。
```{r comparer, fig.cap='4位相等比较器逻辑电路及其示意图', fig.align='center', echo = FALSE, out.width='50%'}
knitr::include_graphics('./images/chapter8/4bit_comparer.png')
```
我们通过分析A-B的结果来比较A和B的大小。这里需要注意的是结果溢出的情况。如果减法操作没有发生溢出则减法结果的符号位为1时表示A < B如果发生溢出则结果符号位为0时才表示A < B。假设A和B是两个64位的有符号数A=a~63~$\ldots$a~0~B=b~63~$\ldots$b~0~A-B的结果为S=s~63~$\ldots$s~0~则A < B成立的条件可以表示为
$$\begin{aligned}Cond_{A< B}&=\ \sim Overflow \ \&\ s_{63} \ |\ Overflow \ \&\ s_{63} \\ &=\ a_{63}\ \&\ s_{63}\ |\ \sim b_{63}\ \&\ s_{63}\ |\ a_{63}\ \&\ \sim b_{63}\end{aligned}$$
当然最终的表示方式也可以直接得到即A < B成立的条件仅包括三种情况A是负数且B是非负数A是负数且B也是负数且结果是负数B是非负数且A是非负数且结果是负数。
由于能够通过减法来做大小的比较且相等比较的逻辑资源并不多所以在设计ALU时比较操作的实现并不会新增很多逻辑资源消耗。
### 移位器
常见基本运算中除了加减、比较运算外还有移位运算。移位运算不仅在进行位串处理时十分有用而且常用于计算乘以或除以2的幂次方。移位运算通常有四种逻辑左移、逻辑右移、算术右移和循环右移。其中左移、右移的概念如同其名字中的表述是直观明了的。逻辑右移和算术右移的区别在于前者从高位移入的是0后者从高位移入的是源操作数的符号位。算术右移之所以名字中使用“算术”这个词是因为当用移位操作计算有符号数补码表示除以2的幂次方时只有从高位移入符号位才能保证结果的正确。由此也可以知晓为什么没有定义“算术左移”这种移位操作。因为无论是有符号数还是无符号数其乘以2的幂次方都只需要在低位移入0就可以了。循环右移操作顾名思义右移时从最低位移出去的比特位并不被丢弃而是重新填入到结果的最高位。也正是因为这种循环移位的特点循环左移操作其实可以用循环右移操作来实现故不单独定义循环左移操作。例如5位二进制数11001其逻辑左移2位的结果是00100逻辑右移2位的结果是00110算术右移2位的结果是11110循环右移2位的结果是01110。
N位数的移位器实现实质上是N个N:1的多路选择器。图\@ref(fig:shifter)中依次给出了4位数的逻辑左移、逻辑右移、算术右移和循环右移的逻辑电路示意图。其中A=a~3~a~2~a~1~a~0~是被移位数shamt~1..0~是移位量Y=y~3~y~2~y~1~y~0~是移位结果。更多位数的移位器的实现原理与示例一致,只是选择器的规模更大。由于位数多时多路选择器消耗的电路资源较多,所以在实现时,可以将逻辑右移、算术右移和循环右移的电路糅合到一起,以尽可能复用多路选择器的资源。
```{r shifter, fig.cap='4位移位器逻辑', fig.align='center', echo = FALSE, out.width='100%'}
knitr::include_graphics('./images/chapter8/4bit_shifter.png')
```
## 定点补码乘法器
本节介绍定点补码乘法器的设计。乘法指令在科学计算程序中很常见矩阵运算、快速傅里叶变换操作中都有大量的定点或浮点乘法操作。在计算机发展的早期由于硬件集成度较低只通过ALU实现了加减法、移位等操作乘法这样的复杂操作需要由软件通过迭代的移位-累加操作来实现。随着处理器运算部件的升级,现代处理器已经使用硬件方式来实现定点和浮点乘法操作。
### 补码乘法器
对于定点乘法器而言,最简单的实现方式就是通过硬件来模拟软件的迭代操作,这种乘法实现方式被称为移位加。其逻辑结构如图\@ref(fig:iter-sourcecode-MUL)所示。
```{r iter-sourcecode-MUL, fig.cap='迭代式硬件原码乘法器', fig.align='center', echo = FALSE, out.width='50%'}
knitr::include_graphics('./images/chapter8/iterative_sourcecode_multipilier.png')
```
以两个8位数的乘法为例乘法器的输入包括一个8位的乘数和一个8位的被乘数输出则是16位的乘法结果。通过触发器将这3个数存储下来并执行以下步骤
1最初时将乘法结果设置为0。
2在每个时钟周期判断乘数的最低位如果值为1则将被乘数加到乘法结果如果值为0则不进行加法操作。此后将乘数右移1位将被乘数左移1位将参与运算的3个数锁存进入下一个时钟周期。
3执行8次操作得到正确的乘法结果。
实现上述移位加算法需要的硬件很简单组合逻辑延迟也较小缺点是完成一条乘法需要很多个时钟周期对于64位的乘法而言就需要64拍。但是上述算法是将操作数视为一个无符号二进制数来设计的如果计算的输入是补码形式那么就需要先根据输入的正负情况判断出结果的符号位随后将输入转换为其绝对值后进行上述迭代运算最后再根据结果符号位转换回补码形式。很显然这样操作略显复杂有没有直接根据补码形式进行运算的方法呢
在8.1.1小节中介绍过,现代处理器中的定点数都是按照补码形式来存储的,同时有[X]~补~+[Y]~补~=[X+Y]~补~的特性。那么,应该如何计算[X×Y]~补~呢?是否可以简单地将[X]~补~与[Y]~补~相乘得到呢?
还是以8位乘法为例。假定有8位定点数Y[Y]~补~的二进制格式写作y~7~y~6~y~5~y~4~y~3~y~2~y~1~y~0~根据补码定义Y的值等于
$$Y=-y_{7}\times 2^{7}+y_{6}\times 2^{6}+y_{5}\times 2^{5}+\ldots +y_{1}\times 2^{1}+y_{0}\times 2^{0}$$
由此推出:
$$\begin{aligned}[X\times Y]_{\mbox{补}}&=[X\times (-y_{7}\times 2^{7}+y_{6}\times 2^{6}+\ldots +y_{1}\times 2^{1}+y_{0}\times 2^{0})]_{\mbox{补}} \\ &=[X\times -y_{7}\times 2^{7}+X\times y_{6}\times 2^{6}+\ldots +X\times y_{1}\times 2^{1}+X\times y_{0}\times 2^{0}]_{\mbox{补}}\end{aligned}$$
根据补码加法具有的特性,有:
$$[X\times Y]_{\mbox{补}}=[X\times -y_{7}\times 2^{7}]_{\mbox{补}}+[X\times y_{6}\times 2^{6}]_{\mbox{补}}+\ldots +[X\times y_{0}\times 2^{0}]_{\mbox{补}}$$
需要注意这个公式中位于方括号外的加法操作为补码加法而之前两个公式中位于方括号内部的加法为算术加法。由于y~i~只能取值为0或者1再根据补码减法的规则继续推导公式
$$[X\times Y]_{\mbox{补}}=-y_{7}\times [X\times 2^{7}]_{\mbox{补}}+y_{6}\times [X\times 2^{6}]_{\mbox{补}}+\ldots +y_{0}\times [X\times 2^{0}]_{\mbox{补}}$$
公式中最开头的减号是补码减法操作。为了继续运算,需要引入一个定理:
$$[X\times 2^{n}]_{\mbox{补}}=[X]_{\mbox{补}}\times 2^{n}$$
该定理的证明可以较容易地根据补码的定义得出,留作本章的课后习题。据此定理,补码乘法的公式可以继续推导如下:
$$\begin{aligned}[X\times Y]_{\mbox{补}}&=-[X]_{\mbox{补}}\times y_{7}\times 2^{7}+[X]_{\mbox{补}}\times y_{6}\times 2^{6}+\ldots +[X]_{\mbox{补}}\times y_{0}\times 2^{0}\\ &=[X]_{\mbox{补}}\times -y_{7}\times 2^{7}+y_{6}\times 2^{6}+\ldots +y_{0}\times 2^{0}\end{aligned}$$
最后得到的公式与移位加算法的原理很类似,但是存在两个重要区别:第一,本公式中的加法、减法均为补码运算;第二,最后一次被累加的部分积需要使用补码减法来操作。这就意味着[X]~补~×[Y]~补~不等于[X×Y]~补~。图\@ref(fig:complement-MUL)给出两个4位补码相乘的例子。注意在补码加法运算中需要进行8位的符号位扩展并仅保留8位结果。
```{r complement-MUL, fig.cap='补码乘法计算示例', fig.align='center', echo = FALSE, out.width='70%'}
knitr::include_graphics('./images/chapter8/complement_multiplication.png')
```
简单地修改之前的迭代式硬件原码乘法器,就可以实现补码乘法,如图\@ref(fig:iter-complement-MUL)所示。
```{r iter-complement-MUL, fig.cap='迭代式硬件补码乘法器', fig.align='center', echo = FALSE, out.width='70%'}
knitr::include_graphics('./images/chapter8/iterative_complement_multipilier.png')
```
依此方法也可以计算32位数、64位数的补码乘法。运算数据更宽的乘法需要更多的时钟周期来完成。
### Booth乘法器
Booth乘法器由英国的Booth夫妇提出。按照8.3.1小节中的补码乘法算法需要特地挑出第N个部分积并使用补码减法操作这就需要实现一个额外的状态机来控制增加了硬件设计复杂度。因此他们对补码乘法公式进行变换试图找到更适合于硬件实现的算法。
Booth一位乘变换的公式推导如下
$$\begin{aligned}&\quad-y_{7}\times 2^{7}+y_{6}\times 2……{6}+\ldots +y_{1}\times 2^{1}+y_{0}\times 2^{0}\\
&=-y_{7}\times 2^{7}+y_{6}\times 2^{7}-y_{6}\times 2^{6}+y_{5}\times 2^{6}-y_{5}\times 2^{5}+\ldots + \\ &\quady_{1}\times 2^{2}-y_{1}\times 2^{1}+y_{0}\times 2^{1}-y_{0}\times 2^{0}+0\times 2^{0}\\
&=y_{6}-y_{7}\times 2^{7}+y_{5}-y_{6}\times 2^{6}+\ldots +y_{0}-y_{1}\times 2^{1}+y_{-1}-y_{0}\times 2^{0}\end{aligned}$$
其中y~-1~取值为0。经过变换公式变得更加规整不再需要专门对最后一次部分积采用补码减法更适合硬件实现。这个新公式被称为Booth一位乘算法。
为了实现Booth一位乘算法需要根据乘数的最末两位来确定如何将被乘数累加到结果中再将乘数和被乘数移一位。根据算法公式很容易得出它的规则如表\@ref(tab:booth-one-mul-rule)所示。
```{r booth-one-mul-rule, echo = FALSE, message=FALSE, tab.cap='Booth一位乘运算规则'}
readr::read_csv('./materials/chapter8/booth_one_rule.csv') %>%
flextable() %>%
colformat_md(part='all') %>%
merge_h(part='header') %>%
vline(i = NULL, j = 2) %>%
#hline_top(part='all') %>%
align(i = NULL, j = NULL, align = "center", part = "all") %>%
autofit()
```
注意算法开始时要隐含地在乘数最右侧补一个y-1的值。图\@ref(fig:Booth-1-mul)给出了Booth一位乘算法的示例。
```{r Booth-1-mul, fig.cap='Booth一位乘示例', fig.align='center', echo = FALSE, out.width='80%'}
knitr::include_graphics('./images/chapter8/booth_one_multiplication.png')
```
在Booth一位乘算法中为了计算N位的补码乘法依然需要N-1次加法。而数据宽度较大的补码加法器面积大、电路延迟长限制了硬件乘法器的计算速度因此重新对补码乘法公式进行变换得到Booth两位乘算法
$$\begin{aligned}&\quad-y_{7}\times 2^{7}+y_{6}\times 2^{6}+\ldots +y_{1}\times 2^{1}+y_{0}\times 2^{0}\\
&=-2\times y_{7}\times 2^{6}+y_{6}\times 2^{6}+y_{5}\times 2^{6}-2\times y_{5}\times 2^{4}+\ldots \\
&\quad +y_{1}\times 2^{2}-2\times y_{1}\times 2^{0}+y_{0}\times 2^{0}+y_{-1}\times 2^{0}\\
&=y_{5}+y_{6}-2y_{7}\times 2^{6}+y_{3}+y_{4}-2y_{5}\times 2^{4}+\ldots +y_{-1}+y_{0}-2y_{1}\times 2^{0}\end{aligned}$$
根据Booth两位乘算法需要每次扫描3位的乘数并在每次累加完成后将被乘数和乘数移2位。根据算法公式可以推导出操作的方式参见表\@ref(tab:booth-two-mul-rule)。注意被扫描的3位是当前操作阶数i加上其左右各1位。因此操作开始时需要在乘数最右侧隐含地补一个0。
```{r booth-two-mul-rule, echo = FALSE, message=FALSE, tab.cap='Booth两位乘运算规则'}
readr::read_csv('./materials/chapter8/booth_two_rule.csv') %>%
flextable() %>%
colformat_md(part='all') %>%
merge_h(part='header') %>%
vline(i = NULL, j = 3) %>%
#hline_top(part='all') %>%
align(i = NULL, j = NULL, align = "center", part = "all") %>%
autofit()
```
还是以4位补码乘法为例如图\@ref(fig:Booth-2-mul)所示。
```{r Booth-2-mul, fig.cap='Booth两位乘示例', fig.align='center', echo = FALSE, out.width='80%'}
knitr::include_graphics('./images/chapter8/Booth_two_multiplication.png')
```
如果使用Booth两位乘算法计算N位的补码乘法时只需要N/2-1次加法如果使用移位加策略则需要N/2个时钟周期来完成计算。龙芯处理器就采用了Booth两位乘算法来实现硬件补码乘法器大多数现代处理器也均采用该算法。
同理可以推导Booth三位乘算法、Booth四位乘算法。其中Booth三位乘算法的核心部分为
$$y_{i-1}+y_{i}+2y_{i+1}-4y_{i+2}\times 2^{i} (i=0, 每次循环i+3)$$
对于Booth三位乘而言在扫描乘数低位时有可能出现补码加3倍[X]~补~的操作。不同于2倍[X]~补~可以直接通过将[X]~补~左移1位来实现3倍[X]~补~的值很难直接获得需要在主循环开始之前进行预处理算出3倍[X]~补~的值并使用额外的触发器记录下来。对于越复杂的Booth算法需要的预处理过程也越复杂。所以相比之下Booth两位乘算法更适合硬件实现更为实用。本节接下来将介绍这个算法的电路实现方式。
Booth乘法的核心是部分积的生成共需要生成N/2个部分积。每个部分积与[X]~补~相关,总共有-X、-2X、+X、+2X和0 五种可能,而其中减去[X]~补~的操作,可以视作加上按位取反的[X]~补~再末位加1。为了硬件实现方便将这个末位加1的操作提取出来假设[X]~补~的二进制格式写作x~7~x~6~x~5~x~4~x~3~x~2~x~1~x~0~,再假设部分积P等于p~7~p~6~p~5~p~4~p~3~p~2~p~1~p~0~+c那么有
$$p_{i}=
\begin{cases}
\sim x_{i}& \text{选择-X}\\
\sim x_{i-1}& \text{选择-2X}\\
x_{i}& \text{选择+X}\\
x_{i-1}& \text{选择+2X}\\
0& \text{选择0}
\end{cases}$$
$$c=
\begin{cases}
1& \text{选择-X或-2X}\\
0& \text{选择+X或+2X或0}
\end{cases}$$
当部分积的选择为2X时可以视作X输入左移1位此时p~i~就与x~i-1~相等。如果部分积的选择是-X或者-2X则此处对x~i~或者x~i-1~取反并设置最后的末位进位c为1。
根据上述规则经过卡诺图分析可以得出每一位p~i~的逻辑表达式:
$$p_{i}=\sim (\sim (S_{-X}\ \&\ \sim x_{i})\ \&\ \sim (S_{-2X}\ \&\ \sim x_{i-1})\ \&\ \sim (S_{+X}\ \&\ x_{i})\ \&\ \sim (S_{+2X}\ \&\ x_{i-1}))$$
其中S~+x~信号在部分积选择为+X时为1其他情况为0另外三个S信号含义类似。画出p~i~的逻辑图,如图\@ref(fig:Booth-select-logic)所示。
```{r Booth-select-logic, fig.cap='Booth结果选择逻辑', fig.align='center', echo = FALSE, out.width='80%'}
knitr::include_graphics('./images/chapter8/Booth_select_logic.png')
```
下文将使用图中箭头右侧的小示意图来代表p~i~的生成逻辑。生成逻辑中需要使用部分积选择信号因此还需要考虑如何根据y~i-1~、y~i~和y~i+1~三个信号生成图\@ref(fig:Booth-selectsignal)用到的4个选择信号。根据表\@ref(tab:booth-two-mul-rule)中的规则,很容易通过卡诺图化简得到:
$$\begin{aligned}S_{-x}&=\sim (\simy_{i+1}\ \&\ y_{i}\ \&\ \sim y_{i-1})\ \&\ \sim (y_{i+1}\ \&\ \sim y_{i}\ \&\ y_{i-1})) \\ S_{+x}&=\sim (\sim\sim y_{i+1}\ \&\ y_{i}\ \&\ \sim y_{i-1})\ \&\ \sim (\sim y_{i+1}\ \&\ \sim y_{i}\ \&\ y_{i-1})) \\ S_{-2x}&=\sim (\sim (y_{i+1}\ \&\ \sim y_{i}\ \&\ \sim y_{i-1})) \\ S_{+2x}&=\sim (\sim (\sim y_{i+1}\ \&\ y_{i}\ \&\ y_{i-1}))\end{aligned}$$
画出选择信号生成部分的逻辑图,并得到如图\@ref(fig:Booth-selectsignal)所示的示意图。:
```{r Booth-selectsignal, fig.cap='Booth选择信号生成逻辑', fig.align='center', echo = FALSE, out.width='80%'}
knitr::include_graphics('./images/chapter8/Booth_selectsignal_logic.png')
```
将两部分组合起来形成每个Booth部分积的逻辑图并得到如图\@ref(fig:Booth-partial-product)所示的示意图。
```{r Booth-partial-product, fig.cap='Booth部分积生成逻辑', fig.align='center', echo = FALSE, out.width='80%'}
knitr::include_graphics('./images/chapter8/Booth_partial_product.png')
```
这个逻辑就是两位Booth乘法的核心逻辑。调用该逻辑并通过移位加策略实现两位Booth补码乘的结构如图\@ref(fig:Booth-SHIFT)所示。
```{r Booth-SHIFT, fig.cap='使用移位加实现Booth乘法', fig.align='center', echo = FALSE, out.width='50%'}
knitr::include_graphics('./images/chapter8/Booth_multiplication with shifter.png')
```
乘法操作开始时乘数右侧需要补1位的0而结果需要预置为全0。在每个时钟周期的计算结束后乘数算术右移2位而被乘数左移2位直到乘数为全0时乘法结束。对于N位数的补码乘法操作可以在N/2个时钟周期内完成并有可能提前结束。在这个结构中被乘数、结果、加法器和Booth核心的宽度都为2N位。
### 华莱士树
即使采用了Booth两位乘算法使用移位加策略来完成一个64位的乘法操作也需要32个时钟周期并且不支持流水操作即第一条乘法全部完成之后才能开始计算下一条。现代处理器通常可以实现全流水、4个时钟周期延迟的定点乘法指令其核心思想就是将各个部分积并行地加在一起而非串行迭代累加。
以64位数据的乘法为例共有32个部分积如果按照二叉树方式来搭建加法结构第一拍执行16个加法第二拍执行8个加法以此类推就可以在5个时钟周期内结束运算。这个设计还支持流水式操作当上一条乘法指令到达第二级此时第一级的16个加法器已经空闲可以用来服务下一条乘法指令了。
这种设计的硬件开销非常大其中128位宽度的加法器就需要31个而用于锁存中间结果的触发器更是接近4000个。本节将要介绍的华莱士树Wallace Tree结构可以大幅降低多个数相加的硬件开销和延迟。
```{r 1bit-full-adder, fig.cap='一位全加器示例', fig.align='center', echo = FALSE, out.width='30%'}
knitr::include_graphics('./images/chapter8/1bit_full_adder.png')
```
华莱士树由全加器搭建而成。根据8.2.1节的介绍,全加器的示例如图\@ref(fig:1bit-full-adder)所示。
$$\begin{aligned}S&=\sim A\ \&\ \sim B\ \&\ C\ |\ \sim A\ \&\ B\ \&\ \sim C\ |\ A\ \&\ \sim B\ \&\ \sim C\ |\ A\ \&\ B\ \&\ C \\ C&=A \ \&\ B\ |\ A\ \&\ C\ |\ B\ \&\ C\end{aligned}$$
全加器可以将3个1位数A、B、C的加法转换为两个1位数S和C的错位加法
A+B+C=S+(C≪1)
如果参与加法的数据较宽可以通过调用多个全加器将3个数的加法转换为两个数的加法。图\@ref(fig:4full-adder)给出了3个4位数相加的例子。
```{r 4full-adder, fig.cap='使用全加器实现3个4位数相加', fig.align='center', echo = FALSE, out.width='100%'}
knitr::include_graphics('./images/chapter8/FULL_adder for three 4bit number.png')
```
其中4位数A的二进制表示为A~3~A~2~A~1~A~0~,可以很容易得知:
$$\{A_{3}A_{2}A_{1}A_{0}\}+\{B_{3}B_{2}B_{1}B_{0}\}+\{D_{3}D_{2}D_{1}D_{0}\}=\{S_{3}S_{2}S_{1}S_{0}\}+\{C_{2}C_{1}C_{0}0\}$$
公式中所有加法都为补码加法操作宽度为4位结果也仅保留4位的宽度这也导致C~3~位没有被使用而是在C~0~右侧再补一个0参与补码加法运算。
那么问题来了如果需要相加的数有4个又应该如何呢很自然地想到可以先将其中3个数相加再调用一层全加器结构将刚得到的结果与第4个数相加即可。不过要注意全加器的C输出需要左移1位才能继续参与运算。如图\@ref(fig:full-adder-for4number)所示。
```{r full-adder-for4number, fig.cap='使用全加器实现4个4位数相加', fig.align='center', echo = FALSE, out.width='100%'}
knitr::include_graphics('./images/chapter8/FULL_adder for four 4bit number.png')
```
最后结果中最高位进位C~3~和C~3'~都不会被使用。第二级的最右侧全加器需要在其中一个输入位置补0参与运算。从图\@ref(fig:full-adder-for4number)中可以看出整个结构呈现重复特征提取出圆角矩形框选中的部分这部分称为一位华莱士树。准确地说图中灰色部分呈现的是4个数相加的一位华莱士树结构它除了输入的4个被加数、输出的C与S之外还有级联的进位信号。通过M个这样的一位华莱士树就可以实现4个M位数的相加。
可以简单地计算一下使用华莱士树进行相加的优势。根据图\@ref(fig:full-adder-for4number)的结构4个数相加的华莱士树需要两层全加器当前位的进位信号在第一层产生并接到了下一位的第二层这意味着C~out~与C~in~无关。全加器的S输出需要3级门延迟而C输出需要2级门延迟因此不论参与加法的数据宽度是多少位将4个数相加转换为两个数相加最多只需要6级门延迟最后把这两个数加起来还需要一个加法器。整套逻辑需要一个加法器的面积再加上两倍数据宽度个全加器的面积。如果不使用华莱士树而是先将四个数捉对相加再把结果相加计算的延迟就是两倍的加法器延迟面积则是3倍的加法器面积。对于64位或者更宽的加法器它的延迟肯定是远远超过6级门的面积也比64个全加器要大得多。
因此使用华莱士树进行多个数相加可以明显地降低计算延迟数据宽度越宽其效果越明显。通过本节后续的介绍可以归纳出使用华莱士树进行M个N位数相加可以大致降低延迟logN倍而每一层华莱士树包含的全加器个数为⌊2M'/3⌋M'是当前层次要加的数字个数)。
回到本节最开始的问题Booth乘法需要实现N/2个2N宽度的部分积相加如果可以先画出N/2个数的一位华莱士树结构通过2N次使用就可以达到这个要求。为了描述的简洁下面我们具体分析N=16即8个数相加情况下的一位华莱士树结构如图\@ref(fig:1bit-wallace-tree-for8)所示。
```{r 1bit-wallace-tree-for8, fig.cap='8个数相加的一位华莱士树', fig.align='center', echo = FALSE, out.width='100%'}
knitr::include_graphics('./images/chapter8/1bit_wallace_tree_for8.png')
```
从图\@ref(fig:1bit-wallace-tree-for8)中可以看出通过华莱士树可以用4级全加器即12级门的延迟把8个数转换成两个数相加。华莱士树的精髓在于通过连线实现进位传递从而避免了复杂的进位传递逻辑。不过需要指出的是在华莱士树中每一级全加器生成本地和以及向高位的进位因此在每一级华莱士树生成的结果中凡是由全加器的进位生成的部分连接到下一级时要连接到下一级的高位。像图\@ref(fig:wrong-wallace-tree)中左侧的搭建方法就是没有保证这一点,所以是错误的。
```{r wrong-wallace-tree, fig.cap='两种错误的8个数相加的一位华莱士树', fig.align='center', echo = FALSE, out.width='100%'}
knitr::include_graphics('./images/chapter8/WORONG_exaxple of walllace tree.png')
```
图\@ref(fig:wrong-wallace-tree)右侧的搭建方式修正了图左侧级间进位传递逻辑的错误,但是它的搭建方式依然存在问题。为了理解问题出在哪里,我们需要从整个乘法器的设计入手。
为了构成一个16位定点补码乘法器需要使用8个Booth编码器外加32个8个数相加的一位华莱士树再加上一个32位加法器。值得注意的是根据上一节提出的Booth乘法核心逻辑除了有8个部分积需要相加之外还有8个“末位加1”的信号。在华莱士树中最低位对应的华莱士树上有空闲的进位输入信号根据图\@ref(fig:1bit-wallace-tree-for8)的结构共有6个进位输入可以接上6个“末位加1”的信号。还剩下两个“末位加1”的信号只能去最后的加法器上想办法最后的加法器负责将华莱士树产生的2N位的C和S信号错位相加其中C信号左移一位低位补0。据此设计这两个“末位加1”的信号可以一个接到加法器的进位输入上另一个接到C左移后的补位上。分析到这里应该能够理解为什么说图\@ref(fig:1bit-wallace-tree-for8)中的华莱士树才是合适的因为这种搭建方法才能出现6个进位输入。
最终的乘法器示意图如图\@ref(fig:16bitmulti-unit)所示。
```{r 16bitmulti-unit, fig.cap='16位乘法器示意图', fig.align='center', echo = FALSE, out.width='100%'}
knitr::include_graphics('./images/chapter8/16bit_multiplying_unit.png')
```
图中间标注为switch的部分负责收集8个Booth核心生成的8个32位数进行类似矩阵转置的操作重新组织为32组、8个一位数相加的格式输出给华莱士树并将Booth核心生成的8个“末位加1”信号从switch部分右侧接出提供给华莱士树最右侧的一位树及最后的加法器。此外图中没有画出的是被乘数X送到8个Booth编码器时需要先扩展到32位并按照编码器所处的位置进行不同偏移量的左移操作。
## 本章小结
本章首先回顾了计算机中数的二进制编码及定点数和浮点数的表示介绍了晶体管原理以及由晶体管构建的基本逻辑电路。然后介绍了CPU中简单运算器设计时常见的加法、减法、比较和移位运算的实现重点是补码加法器的设计实现。最后介绍了定点补码乘法器的设计重点是补码乘法的规则、两位Booth算法和华莱士树。
## 习题
1. 请将下列无符号数据在不同的进制表达间进行转换。
a) 二进制转换为十进制101011~2~、001101~2~、01011010~2~、0000111010000101~2~。
b) 十进制转换为二进制42~10~、79~10~、811~10~、374~10~。
c) 十六进制转换为十进制8AE~16~、C18D~16~、B379~16~、100~16~。
d) 十进制转换为十六进制81783~10~、1922~10~、345208~10~、5756~10~。
2. 请给出32位二进制数分别视作无符号数、原码、补码时所表示的数的范围。
3. 请将下列十进制数表示为8位原码和8位补码或者表明该数据会溢出45~10~、-59~10~、-128~10~、119~10~、127~10~、128~10~、0、-1~10~。
4. 请将下列数据分别视作原码和补码从8位扩展为16位00101100~2~、11010100~2~、10000001~2~、00010111~2~。
5. 请将下列浮点数在不同进制间进行转换。
a) 十进制数转换为单精度数0、116.25、-4.375。
b) 十进制数转换为双精度数:-0、116.25、-2049.5。
c) 单精度数转换为十进制数0xff800000、0x7fe00000。
d) 双精度数转换为十进制数0x8008000000000000、0x7065020000000000。
6. 请写出下图所示晶体管级电路图的真值表,并给出对应的逻辑表达式。
```{r hwCMOS, fig.align='center', echo = FALSE, out.width='40%'}
knitr::include_graphics('./images/chapter8/hwCMOS.png')
```
7. 请写出下图所示逻辑门电路图的真值表。
```{r hwlogicgate, fig.align='center', echo = FALSE, out.width='30%'}
knitr::include_graphics('./images/chapter8/hw logic gate.png')
```
8. 请用尽可能少的二输入NAND门搭建出一个具有二输入XOR功能的电路。
9. 请用D触发器和常见组合逻辑门搭建出一个具有同步复位为0功能的触发器的电路。
10. 证明[X+Y]~补~=[X]~补~+[Y]~补~。
11. 证明[X-Y]~补~=[X]~补~+[-Y]~补~。
12. 假设每个“非门”“与非门”“或非门”的扇入不超过4个且每个门的延迟为T请给出下列不同实现的32位加法器的延迟。
a) 行波进位加法器;
b) 4位一块且块内并行、块间串行的加法器
c) 4位一块且块内并行、块间并行的加法器。
13. 作为设计者,在什么情况下会使用行波进位加法器而非先行进位加法器?
14. 请利用图\@ref(fig:4bitCLAwithc)所示的4位先行进位逻辑组建出块内并行且块间并行的64位先行进位加法器的进位逻辑并证明其正确性。
15. 请举例说明[X×Y]~补~≠[X]~补~×[Y]~补~。
16. 请证明[X×2^n^]~补~=[X]~补~×2^n^。
17. 假设每个“非门” “与非门”“或非门”的扇入不超过4个且每个门的延迟为T请给出下列不同实现将4个16位数相加的延迟
a) 使用多个先行进位加法器;
b) 使用华莱士树及先行进位加法器。
18. 请系统描述采用两位Booth编码和华莱士树的补码乘法器是如何处理[-X]~补~和[-2X]~补~的部分积的。
19. 用Verilog语言设计一个32位输入宽度的定点补码乘法器要求使用Booth两位一乘和华莱士树。
20. 单精度和双精度浮点数能表示无理数$\pi$吗?为什么?