图形渲染计算机硬件分析
图形渲染计算机硬件分析
1 汇编语言
汇编语言可以广泛地定义为机器指令的文本表示。在构建处理器之前,需要了解不同机器指令的语义,在这方面,对汇编语言的严格研究将是有益的。汇编语言专用于ISA和编译器框架,因此,汇编语言有许多优点。本节将描述不同汇编语言变体的基本原理,一些通用概念和术语。随后,将描述针对基于ARM的处理器的ARM汇编语言,描述针对Intel/AMD处理器的x86汇编语言。
1.1 为什么用汇编语言?
先从软件开发者的视角阐述之。
人类懂得自然语言,如中文、英语和西班牙语。通过一些额外的训练,人类还可以理解计算机编程语言,如C或Java。然而,如前面所述,计算机是一台愚蠢的机器,不够聪明,无法理解人类语言(如英语)或编程语言(如C)中的命令,它只能理解零和一。因此,要给计算机编程,必须给它一个0和1的序列。事实上,一些早期的程序员曾经通过打开或关闭一组开关来编程计算机,打开一个开关对应于1,打开它意味着0。对于今天的大规模数百万行程序来说,不是一个可行的解决方案,需要另寻更好的方法。
因此,需要一个自动转换器,它可以将用C或Java等高级语言编写的程序转换为一系列0和1,称为机器代码(machine code)。机器代码包含一组称为机器指令的指令,每个机器指令都是由零和一组成的序列,并指示处理器执行特定的操作。可以将用高级语言编写的程序转换为机器代码的程序称为编译器(见下图)。
编译过程。
请注意,编译器是一个可执行程序,通常在应该为其生成机器代码的机器上运行。可能出现的一个自然问题是——谁编写了第一个编译器?
第一,鉴于编译器的普遍存在,几乎所有的程序都是用高级语言编写的,编译器用来将它们转换为机器代码,但这一规则也有重要的例外。请注意,编译器的作用有两方面:首先,它需要正确地将高级语言的程序翻译成机器指令;其次,它需要生成不占用大量空间且速度快的高效机器代码。因此,多年来编译器中的算法变得越来越复杂,但并不总是能够满足这些要求,例如在某些情况下,编译器可能无法生成足够快的代码,或者无法提供程序员期望的某种功能:
- 首先,编译器中的算法受到它们对程序执行的分析量的限制。例如,不希望编译过程极其缓慢,编译器领域的许多问题在计算上很难解决,因此很耗时。
- 其次,编译器不知道代码中的广泛模式。例如,某个变量可能只取一组有限的值,在此基础上,可能进一步优化机器代码,编译器很难理解这一点。聪明的程序员有时可以生成比编译器更优化的机器代码,因为他们知道一些广泛的执行模式,可以胜过编译器。
- 处理器供应商也可能在ISA中添加新指令。在这种情况下,用于旧版本处理器的编译器可能无法利用新指令,需要在程序中手动添加它们。流行的编译器(如gcc,GNU编译器集合)是相当通用的,它们不使用处理器在生成机器代码时提供的所有可能的机器指令。通常,操作系统和设备驱动程序(与打印机和扫描仪等设备接口的程序)需要大量遗漏的指令,因为它们需要对硬件的低级别访问,因此系统程序员有强烈的动机偶尔绕过编译器。
在所有这些情况下,程序员都有必要在程序中手动嵌入一系列机器指令。如上所述,这样做的两个主要原因是效率和额外的功能。因此,从系统软件开发人员的角度来看,有必要了解机器指令,以便他们在工作中更有效率。
现在,目标是让现代程序员远离0和1的复杂细节。理想情况下,不希望程序员像50年前那样通过手动打开和关闭开关来编程,由此开发了一种称为汇编语言的低级语言。汇编语言是机器代码的一种人类可读形式,每个汇编语言语句通常对应于一条机器指令。此外,它通过不强迫程序员记住编码指令所需的0/1的确切序列,大大减轻了程序员的负担。
- 低级编程语言(low level programming language)使用通常只对应于一条机器指令的简单语句,这些语言是ISA特有的。
- 汇编语言(assembly language)是指一系列特定于每个ISA的低级编程语言,具有由一系列汇编语句组成的泛型结构。通常,每个汇编语句有两部分:
- 一个指令代码,是基本机器指令的助记符。
- 一个操作数列表。
从实际角度来看,可以编写独立的汇编程序,并使用称为汇编器的程序将其转换为可执行程序,也可以在高级语言(如C或C++)中嵌入汇编代码片段,后者更为常见。
汇编器(assembler)是将汇编程序转换为机器代码的可执行程序。
编译器确保能够将组合程序编译为机器代码。汇编语言的好处是多方面的:
- 可读性。因为汇编代码中的每一行对应于一条机器指令,所以它和机器代码一样具有表达力。
- 高效性。由于这种一对一的映射,不必通过在汇编中编写程序来提高效率。
- 简化性。它是一种可读的、优雅的文本表示机器代码的形式,大大简化了使用它编写程序的过程,也可以在用C等高级语言编写的软件中清晰地嵌入汇编代码片段,它定义了一个高于实际机器代码的抽象级别。两个处理器可能与同一种汇编语言兼容,但实际上对同一条指令有不同的机器编码。在这种情况下,汇编程序将在这两个处理器上兼容。
再从硬件设计者的视角阐述之。
硬件设计师的职责是设计能够实现ISA中所有指令的处理器。他们的主要目标是设计一个在面积、功率效率和设计复杂性方面最佳的高效处理器,从他们的角度来看,ISA是软件和硬件之间的关键纽带。这回答了他们的基本问题——构建什么?因此,对他们来说,理解不同指令集的精确语义是非常重要的,这样他们就可以为它们设计处理器。将指令仅仅看作一个0和1的序列是很麻烦的,通过查看机器指令的文本表示,他们可以获得很多好处,很清晰地知道是一条怎样的汇编指令。
汇编语言专用于指令集和汇编器。本节使用流行的GNU汇编器的汇编语言格式来解释典型汇编语言文件的语法,请注意,其他系统具有类似的格式,并且概念大致相同。
1.2 汇编语言基础
机器模型
汇编语言不将指令存储器和数据内存视为不同的实体,假设一个抽象的冯·诺依曼机器增加了寄存器。
有关机器模型的图示,请参见下图。程序存储在主内存的一部分中,中央处理单元(CPU)逐条指令读出程序指令,并适当地执行指令,程序计数器(PC)跟踪CPU正在执行的指令的内存地址,大多数指令都希望从寄存器中获取其输入操作数。回想一下,每个CPU都有固定数量的寄存器(通常<64),然而大量指令也可以直接从内存中获取操作数。CPU的工作是协调主存和寄存器之间的传输,CPU还需要执行所有算术/逻辑计算,并与外部输入/输出设备保持联系。
大多数类型的汇编语言在大多数语句中都采用这种抽象机器模型。但由于使用汇编语言的另一个目的是对硬件进行更细粒度和侵入性的控制,因此有相当多的汇编指令可以识别处理器的内部。
这些指令通常通过改变一些关键内部算法的行为来修改处理器的行为,它们修改内置参数,如电源管理设置,或读/写一些内部数据。最后请注意,汇编语言不区分机器无关指令和机器相关指令。
每台机器都有一组寄存器,这些寄存器对汇编程序员是可见的。ARM有16个寄存器,x86(32位)有8个寄存器,而x86_64(64位)有16个。寄存器有名称,ARM将它们命名为r0、r1、...、r14、r15,x86将它们命名成eax、ebx、ecx、edx、esi、edi、ebp和esp,可以使用这些名称访问寄存器。
在大多数ISA中,返回地址寄存器用于函数调用。让假设一个程序开始执行一个函数,它需要记住执行函数后需要返回的内存地址,此地址称为返回地址。在跳转到函数的起始地址之前,可以将返回地址的值保存在这个寄存器中。通过将保存在返回地址寄存器中的值复制到PC上,可以简单地实现返回语句。在ARM和MIPS等汇编语言中,程序员可以看到返回地址寄存器,然而x86不使用返回地址寄存器,使用堆栈。
在ARM处理器中,程序员可以看到PC,它是最后一个寄存器(r15)。可以读取PC的值,也可以设置其值,设置PC的值意味着希望分支到程序中的新位置。然而x86的PC是隐式的,程序员不可见。
内存视图
内存可以看作是一个大的字节数组,每个字节都有一个唯一的地址,基本上就是它在数组中的位置。第一字节的地址是0,第二字节的地址为1,以此类推。没有一种方法来唯一地寻址给定的位,地址在32位机器中是32位无符号整数,在64位机器中则是64位无符号。
在冯·诺依曼机器中,假设程序作为字节序列存储在内存中,程序计数器指向将要执行的下一条指令。
假设内存是一个大的字节数组,如果所有的数据项都只有一个字节长,那么就可以了,像C和Java这样的语言有不同大小的数据类型:char(1字节)、short(2字节)、integer(4字节)和long integer(8字节)。对于多字节数据类型,必须在内存中为其创建一个表示,在内存中表示多字节数据类型有两种可能的方式——小端和大端。其次,还需要找到表示内存中数据数组或列表的方法。
- 小端和大端表示
让考虑在位置0-3存储整数的问题。让整数为0x87654321,它可以分为四个字节:87、65、43和21。一个选项是将最重要的字节87存储在最低的内存地址0中,下一个位置可以存储65、43、21,这被称为大端(big endian)表示,因为从最大字节的位置开始。相比之下,可以先将最小的字节保存在位置0,然后继续将最大的字节存储在位置3,这种表示方式称为小端(big endian)。下图显示了差异。
大端和小端表示。
因此,没有理由选择一种代表而不是另一种代表,例如,x86处理器使用little-endian格式。早期版本的ARM处理器曾经是小端的,然而,现在它们是双端的,意味着ARM处理器可以根据用户设置同时作为小端和大端机器工作。传统上,IBM POWER处理器和Sun SPARC处理器都是大端的。
- 表示数组
数组是一组线性有序的对象,其中对象可以是简单的数据类型(如整数或字符),也可以是复杂的数据类型。
int a[100];
char c[100];
让考虑一个简单的整数数组a。如果数组有100个条目,那么内存中数组的总大小等于100 4=400字节。如果数组的起始内存位置为loc,然后将[0]存储在位置(loc + 0)、(loc + 1)、(loc + 2)、(loc + 3)中。请注意,有大端和小端两种方法保存数据。下一个数组条目a[1]保存在位置(loc + 4) ... (loc + 7)中,依此类推,注意到条目a[i]保存在(loc + 4 x i) ... (loc + 4 x i + 3)中。
大多数编程语言都定义以下形式的多维数组:
int a[100][100];
char c[100][100];
它们通常在内存中表示为规则的一维数组,多维数组中的位置与等效的一维数组之间存在映射函数,可以扩展该方案以考虑维度大于2的多维数组。
观察到,通过以行优先(row major)方式保存二维数组,可以将其保存为一维数组,意味着数据按行保存。保存第一行,然后保存第二行,以此类推。同样,也可以以列优先(column major)的方式保存多维数组,其中保存第一列,然后再保存第二列,依此类推。
行优先(row major):数组按行保存在内存中。
列优先(column major):数组按列保存在内存中。
1.3 汇编语言语法
汇编文件的确切语法取决于汇编程序,不同的汇编程序可以使用不同的语法,尽管它们可能在基本指令及其操作数格式上达成一致。本节将解释GNU系列汇编语言的语法,它们是为GNU汇编程序设计的,是GNU编译器集合(gcc)的一部分。与所有GNU软件一样,该汇编程序和相关编译器可免费用于大多数平台,汇编程序可在gnu.org上找到。请注意,其他汇编程序(如NASM、MASM)都有自己的格式,但总体结构在概念上与本节描述的没有太大区别。
汇编文件结构
程序集文件是一个常规文本文件,后缀是.s。如果安装了GNU编译器gcc,则可以通过发出以下命令快速生成C程序的汇编文件,当然也可以使用在线GCC。
gcc -S test.c
生成的程序集文件将命名为test.s。GNU程序集的结构非常简单,如下图所示。不同部分的示例包括文本(实际程序)、数据(具有初始化值的数据)和bss(初始化为0的通用数据)。每个段(section)以节标题开头,这是以“.”开头的节的名称符号,例如,文本部分以“.text”行开头。接着是汇编语言语句列表,每条语句通常以换行符结尾,同样,数据部分包含数据值列表。
汇编文件以包含格式为“.file <文件名>”的行的文件段开头。当使用gcc编译器从C程序生成程序集文件时,.file部分中的文件名通常与原始C程序(test.C)相同。文本段是必填的,其余段是可选的,可能有一个或多个数据段,也可以使用.section指令定义新的节。本节主要关注文本部分,因为对学习指令集的本质感兴趣。
汇编语言文件结构。
汇编基本语句
一条基本的汇编语言语句指定了一条汇编指令,有两部分:指令及其操作数列表,如下图所示。该指令是实际机器指令的文本标识符,操作数列表包含每个操作数的值或位置。操作数的值是一个数值常量,也被称为立即值(immediate value),操作数位置可以是寄存器位置或内存位置。
汇编语言语句。
在计算机架构中,指令中指定的常数值也称为立即数(immediate)。
假设有以下语句:
add r3, r1, r2
在这个ARM汇编语句中,add指令指定了希望将两个数字相加并将结果保存在某个预先指定的位置的事实。在这种情况下,加法指令的格式如下:<指令><目标寄存器><操作数寄存器1><操作数寄存器2>。指令的名称为add,目标寄存器为r3,操作数寄存器为r1和r2。指令的详细步骤如下:
1.读取寄存器r1的值。让将该值称为v1。
2.读取寄存器r2的值。让将该值称为v2。
3.计算v3=v1+v2。
4.将v3保存在寄存器r3中。
现在让再举一个以类似方式工作的两条指令的示例:
sub r3, r1, r2
mul r3, r1, 3
sub指令减去存储在寄存器中的两个数,mul指令将存储在寄存器r1中的一个数乘以数值常数3,这两条指令都将结果保存在寄存器r3中,它们的操作模式与加法指令类似。此外,算术指令(如add、sub和mul)也称为数据处理指令。还有其他几类指令,例如从内存加载或存储值的数据传输指令,以及实现分支的控制指令。
通用语句结构
汇编语句的一般结构如下图所示,它由三个字段组成:标签(指令的标识符)、键(汇编指令或汇编程序指令)和注释。这三个字段都是可选的,但是,任何汇编语句都需要至少具有其中一个。
汇编语句的通用结构。
语句可以选择以标签开头,标签是语句的文本标识符,换句话说,标签在汇编中唯一地标识汇编语句。请注意,不允许在同一汇编文件中重复标签,标签在执行分支指令时非常有用。
下面的示例代码中显示了一个标签的示例,这里标签的名称是“label1”,后面是冒号。在标签之后,编写了一条汇编指令,并给它一个操作数列表。标签可以由有效的字母数字字符[a-z] [A-Z] [0-9] 以及符号“.”、“_”和“$”。通常,不能以数字作为标签的开头。在指定标签之后,可以将该行保持为空,也可以指定键(汇编语句的一部分)。如果键以“.”开头,那么它是一个汇编程序指令,对所有计算机都有效,它指示汇编程序执行某个操作,此操作可以包括启动新节或声明常量。该指令还可以采用参数列表,如果键以字母开头,则它是一条常规的汇编指令。
label1: add r1, r2, r3
在标签、汇编指令和操作数列表之后,可以选择插入注释。GNU汇编程序支持两种类型的注释,可以在插入类似C或Java风格的注释。在ARM汇编中,通过在注释前面加上“@”字符,也可以有一个小的单行注释。
label1: add r1, r2, r3 @ Add the values in r2 and r3
label2: add r3, r4, r5 @ Add the values in r4 and r5
add r5, r6, r7 /* Add the values in r6 and r7 */
汇编语句可能只包含标签,而不包含键。在这种情况下,标签本质上指向一个空语句,不是很有用。因此,汇编程序假定在这种情况下,标签指向最近的包含键的后续汇编语句。
1.4 指令类型
按功能分类
按功能,可分为四种主要类型,说明如下:
- 数据处理指令:数据处理指令通常是算术指令,如加法、减法和乘法,或按位或、异或计算的逻辑指令,比较指令也属于这个类型。
- 数据传输指令:这些指令在两个位置之间传输值,位置可以是寄存器或内存地址。
- 分支指令:分支指令帮助处理器的控制单元根据操作数的值跳转到程序的不同部分,在实现for循环和if-then-else语句时很有用。
- 异常生成指令:这些专用指令有助于将控制权从用户级程序转移到操作系统。
将介绍数据处理、数据传输和控制指令。
按操作数分类
GNU汇编程序中的所有汇编语言语句都具有相同的结构,它们以指令的名称开头,后面是操作数列表。可以根据指令所需的操作数对其进行分类,如果一条指令需要n个操作数,那么通常称它是n地址格式,例如,不需要任何操作数的指令是0地址格式指令,如果它需要3个操作数,则它是3地址格式指令。
如果一条指令需要n个操作数(包括源和目标),那么称其为n地址格式指令。
在ARM中,大多数数据处理指令采用3地址格式,数据传输指令采用2地址格式。然而,在x86中,大多数指令都是2地址格式。想到的第一个问题是,3地址格式指令与2地址格式指令的逻辑是什么?这里一定有一些权衡。
让阐述一些一般的经验法则。如果一条指令有更多的操作数,那么它将需要更多的位来表示该指令,因此需要更多的资源来存储和处理指令。然而,这一论点有另一面,拥有更多的操作数也会使指令更加通用和灵活,将使编译器编写者和汇编程序员的生活变得更加轻松,因为使用更多操作数的指令可以做更多的事情。反向逻辑适用于占用较少操作数的指令,占用更少的存储空间,也不那么灵活。
让考虑一个例子。假设试图将两个数字3和5相加,得到结果8。用于添加的ARM指令如下所示:
add r3, r1, r2
此指令将寄存器r1(3)和r2(5)的内容相加,并将其保存在r3(8)中。然而,x86指令如下所示:
add edx, eax
此处假设edx包含3,eax包含5,执行加法,结果8存储回edx。因此,在这种情况下,x86指令采用2地址格式,因为目标寄存器与第一源寄存器相同。
1.5 操作数类型
现在让看看不同类型的操作数,在汇编语句中指定和访问操作数的方法称为寻址模式。
在汇编语句中指定和访问操作数的方法称为寻址模式(addressing mode)。
指定操作数的最简单方法是将其值嵌入指令中,大多数汇编语言允许用户将整数常量的值指定为操作数,这种寻址模式被称为立即寻址模式(immediate addressing mode),此方法对于初始化寄存器或内存位置或执行算术运算非常有用。
一旦必需的常数集被加载到寄存器和内存位置,程序就需要通过对寄存器和内存进行操作来继续,这个空间有几种寻址模式。在介绍它们之前,让以寄存器转移符号(register transfer notation)的形式介绍一些额外的术语。
寄存器转移符号
这个符号允许指定指令和操作数的语义,让看看表示指令基本动作的各种方法:
\[r1 \leftarrow r2 \]
此表达式有两个寄存器操作数r1和r2,r1是目标寄存器,r2是源寄存器,正在将寄存器r2的内容转移到寄存器r1。可以用一个常数指定一个加法运算,如下所示:
\[r1 \leftarrow r2 + 4 \]
还可以使用此符号指定寄存器上的操作,将r2和r3的内容相加,并将结果保存在r1中:
\[r1 \leftarrow r2 + r3 \]
也可以使用此符号表示内存访问:
\[r1 \leftarrow [r2 + 4] \]
上述语句中,内存地址等于寄存器r2的内容加4,然后从该内存地址的内容开始提取整数,并将其保存在寄存器r1中。
操作数的通用寻址模式
让将操作数的值表示为V。在随后的讨论中,使用了\(V\leftarrow r1\)等表达式,并不意味着有一个新的称为V的存储位置,意味着操作数的数值由RHS指定(右侧)。让通过示例简要地看一下一些最常用的寻址模式:
- 立即:\(V\leftarrow imm\)。使用常量imm作为操作数的值。
- 寄存器:\(V\leftarrow r1\)。在此寻址模式下,处理器使用寄存器中包含的值作为操作数。
- 寄存器间接:\(V\leftarrow [r1]\)。寄存器保存包含该值的内存位置的地址。
- 基准偏移量:\(V\leftarrow [r1 + offset]\)。offset是一个常数,处理器从r1获取基本内存地址,将常量offset添加到该地址,然后访问新的内存位置以获取操作数的值。offset也称为位移。
- 基址变址:\(V\leftarrow [r1 + r2]\)。r1是基址寄存器,r2是索引寄存器,存储器地址等于(r1+r2)。
- 基址变址偏移:\(V\leftarrow [r1 + r2 + offset]\)。包含该值的内存地址为(r1+r2+offset),其中offset为常数。
- 内存直接:\(V\leftarrow addr\)。该值从地址addr开始包含在内存中,addr是一个常数。在这种情况下,存储器地址直接嵌入指令中。
- 内存间接:\(V\leftarrow [[r1]]\)。该值存在于存储器位置中,其地址包含在内存位置M中,M的地址包含在寄存器r1中。
- PC相关:\(V\leftarrow [PC + offset]\)。offset量是一个常数,内存地址计算为PC+offset,其中PC表示PC中包含的值。此寻址模式对分支指令很有用,注意此处的PC是指程序计数器。
让通过考虑基偏移寻址模式来引入一个称为有效内存地址(effective memory address)的新术语。内存地址等于基址寄存器的内容加上偏移量,计算出的内存地址称为有效内存地址。在内存操作数的情况下,可以类似地为其他寻址模式定义有效地址。
多种寻址模式。
x86寻址模式计算。
1.6 常见指令说明
本小节将以ARM为基准,阐述常见的RISC指令用法。
数据传输指令
数据传输指令包含以下几类:
- LDR和STR
可加载寄存器和存储寄存器,包含32位字、8位无符号字节、半字、无符号字节、双字等。
LDR和STR都有四种可能的形式:零偏移量、预索引偏移、程序相关、后索引偏移。四种形式的语法顺序相同,分别为:
op{cond}{B}{T} Rd, [Rn]
op{cond}{B} Rd, [Rn, FlexOffset]{!}
op{cond}{B} Rd, label
op{cond}{B}{T} Rd, [Rn], FlexOffset
以上是针对32位字或8位无符号字节,如果需要双字则B改成D:
op{cond}D Rd, [Rn]
op{cond}D Rd, [Rn, Offset]{!}
op{cond}D Rd, label
op{cond}D Rd, [Rn], Offset
半字、有符号字节语法如下:
op{cond} type Rd, [Rn]
op{cond} type Rd, [Rn, Offset]{!}
op{cond} type Rd, label
op{cond} type Rd, [Rn], Offset
示例:
; 示例1
SUB R1, PC, #4 ; R1 = address of following STR instruction
STR PC, [R0] ; Store address of STR instruction + offset,
LDR R0, [R0] ; then reload it
SUB R0, R0, R1 ; Calculate the offset as the difference
; 示例2
LDRD r6,[r11]
LDRMID r4,[r7],r2
STRD r4,[r9,#24]
STRD r0,[r9,-r2]!
LDREQD r8,abc4
; 示例3
LDREQSH r11,[r6] ; (conditionally) loads r11 with a 16-bit halfword from the address in r6. Sign extends to 32 bits.
LDRH r1,[r0,#22] ; load r1 with a 16 bit halfword from 22 bytes above the address in r0. Zero extend to 32 bits.
STRH r4,[r0,r1]! ; store the least significant halfword from r4 to two bytes at an address equal to contents(r0) plus contents(r1). Write address back into r0.
LDRSB r6,constf ; load a byte located at label constf. Sign extend.
- LDM和STM
LDM、STM加载和存储多个寄存器,寄存器r0到r15的任何组合都可以被传送。语法如下:
op{cond}mode Rn{!}, reglist{^}
示例:
LDMIA r8,{r0,r2,r9}
STMDB r1!,{r3-r6,r11,r12}
STMFD r13!,{r0,r4-r7,LR} ; Push registers including the stack pointer
LDMFD r13!,{r0,r4-r7,PC} ; Pop the same registers and return from subroutine
- PLD
PLD缓存预加载。语法:
PLD [Rn{, FlexOffset}]
示例:
PLD [r2]
PLD [r15,#280]
PLD [r9,#-2481]
PLD [r0,#av*4] ; av * 4 must evaluate, at assembly time, to an integer in the range -4095 to +4095
PLD [r0,r2]
PLD [r5,r8,LSL 2]
- SWP
在寄存器和存储器之间交换数据,使用SWP实现信号量。语法:
SWP{cond}{B} Rd, Rm, [Rn]
通用数据处理指令
此类指令又包含以下几种:
- 灵活第二操作数
大多数ARM通用数据处理指令都有一个灵活的第二操作数,在每条指令的语法描述中显示为Operand2。语法有两种形式:
#immed_8r
Rm{, shift}
示例:
ADD r3,r7,#1020 ; immed_8r. 1020 is 0xFF rotated right by 30 bits.
AND r0,r5,r2 ; r2 contains the data for Operand2.
SUB r11,r12,r3,ASR #5 ; Operand2 is the contents of r3 divided by 32.
MOVS r4,r4, LSR #32 ; Updates the C flag to r4 bit 31. Clears r4 to 0.
- ADD、SUB、RSB、ADC、SBC和RSC
此类指令的语法如下:
op{cond}{S} Rd, Rn, Operand2
示例:
ADD r2,r1,r3
SUBS r8,r6,#240 ; sets the flags on the result
RSB r4,r4,#1280 ; subtracts contents of r4 from 1280
ADCHI r11,r0,r3 ; only executed if C flag set and Z flag clear
RSCLES r0,r5,r0,LSL r4 ; conditional, flags set
- AND、ORR、EOR和BIC
逻辑操作,语法如下:
op{cond}{S} Rd, Rn, Operand2
示例:
AND r9,r2,#0xFF00
ORREQ r2,r0,r5
EORS r0,r0,r3,ROR r6
BICNES r8,r10,r0,RRX
- MOV、MVN、CMP、CMN、TST、TEQ、CLZ
移动、对比、测试、计数前导零指令,语法如下:
MOV{cond}{S} Rd, Operand2
MVN{cond}{S} Rd, Operand2
CMP{cond} Rn, Operand2
CMN{cond} Rn, Operand2
TST{cond} Rn, Operand2
TEQ{cond} Rn, Operand2
CLZ{cond} Rd, Rm
示例:
MOV r5,r2
MVNNE r11,#0xF000000B
MOVS r0,r0,ASR r3
CMP r2,r9
CMN r0,#6400
CMPGT r13,r7,LSL #2
TST r0,#0x3F8
TEQEQ r10,r9
TSTNE r1,r5,ASR r1
CLZ r4,r9
CLZNE r2,r3
算术指令
算术指令包含大量的乘法指令,乘法的指令较多较复杂。常见算术指令如下表所示:
指令 |
语法 |
说明 |
MUL、MLA |
MUL{cond}{S} Rd, Rm, Rs |
乘法和乘法累加(32位乘32位,取底部32位结果) |
UMULL、UMLAL、SMULL、SMLAL |
Op{cond}{S} RdLo, RdHi, Rm, Rs |
无符号和有符号长乘法和乘法累加(32位乘32位,64位累加或结果)。 |
SMULxy、SMLAxy、SMULWy、SMLAWy、SMLALxy |
SMLA{cond} Rd, Rm, Rs, Rn |
有符号乘法(16、32位乘16、32位,结果是32位或64位,部分指令有累积)。 |
MIA、MIAPH、MIAxy |
MIA{cond} Acc, Rm, Rs |
XScale协处理器0指令。 |
示例:
MIA acc0,r5,r0
MIALE acc0,r1,r9
MIAPH acc0,r0,r7
MIAPHNE acc0,r11,r10
MIABB acc0,r8,r9
MIABT acc0,r8,r8
MIATB acc0,r5,r3
MIATT acc0,r0,r6
MIABTGT acc0,r2,r5
分支指令
分支语句的描述如下表:
指令 |
语法 |
说明 |
B、BL |
B/BL {cond} label |
分支和带链接的分支 |
BX |
BX{cond} Rm |
分支和交换指令集 |
BLX |
BLX{cond} Rm |
使用Link分支,并可选地交换指令集。本说明有两种可选形式: |
示例:
MUL r10,r2,r5
MLA r10,r2,r1,r5
MULS r0,r2,r2
MULLT r2,r3,r2
MLAVCS r8,r6,r3,r8
UMULL r0,r4,r5,r6
UMLALS r4,r5,r3,r8
SMLALLES r8,r9,r7,r6
SMULLNE r0,r1,r9,r0 ; Rs can be the same as other registers
SMLAWB r2,r4,r7,r1
SMLAWTVS r0,r0,r9,r2
B loopA
BLE ng+8
BL subC
BLLT rtX
BX r7
BXVS r0
BLX r2
BLXNE r0
BLX thumbsub
条件执行
几乎所有ARM指令都可以包含可选条件代码。这在语法描述中显示为{cond}。只有当CPSR中的条件代码标志满足指定条件时,才执行带有条件代码的指令。可以使用的条件代码如下表所示(部分)。
后缀 |
标记 |
含义 |
EQ |
Z设置 |
= |
NE |
Z清除 |
!= |
CS、HS |
C设置 |
>=(无符号) |
CC、LO |
C清除 |
=(无符号) |
MI |
N设置 |
负数 |
PL |
N清除 |
非负数 |
VS |
V设置 |
溢位 |
VC |
V清除 |
无溢位 |
HI |
C设置且Z清除 |
>(无符号) |
LS |
C清除或Z设置 |
<=(无符号) |
GE |
N和V一样 |
>=(有符号) |
LT |
N和V不一样 |
<(有符号) |
GT |
Z清除且N和V一样 |
>(有符号) |
LE |
Z设置或N和V不一样 |
<=(有符号) |
AL |
任意 |
总是(通常省略) |
几乎所有ARM数据处理指令都可以根据结果选择性地更新条件代码标志。要使指令更新标志,请包含S后缀,如指令的语法描述所示。
有些指令(CMP、CMN、TST和TEQ)不需要S后缀,它们的唯一功能是更新标志,总是更新标志。
标志将保留到更新,未执行的条件指令对标志没有影响,一些指令更新标志的子集,其他标志不受这些指令的影响。详细信息在说明说明中指定。可以根据另一条指令中设置的标志,有条件地执行指令,或者:
- 紧接在更新标志的指令之后。
- 在没有更新标志的任何数量的介入指令之后。
示例:
ADD r0, r1, r2 ; r0 = r1 + r2, don't update flags
ADDS r0, r1, r2 ; r0 = r1 + r2, and update flags
ADDCSS r0, r1, r2 ; If C flag set then r0 = r1 + r2, and update flags
CMP r0, r1 ; update flags based on r0-r1.
其它指令
ARM还有协调处理器、伪指令、杂项指令等其它指令,本文限于篇幅就不接受了。
2 二进制位
计算机不像人类那样理解单词或句子,只理解0和1的序列,存储、检索和处理数十亿个0和1非常容易。其次,使用硅晶体管(silicon transistor)实现计算机的现有技术与处理0和1的概念非常兼容。基本硅晶体管是一种开关,可以根据输入将输出设置为逻辑0或1,硅晶体管是今天拥有的所有电子计算机的基础,从手机的处理器到超级计算机的处理器。十九世纪末制造的一些早期计算机处理十进制数字,本质上大多是机械的。首先让明确定义一些简单的术语:
位(bit):可以有两个值的变量:0或1。
字节(byte):8个位的序列。
2.1 逻辑操作
二进制变量(0或1)最早由乔治·布尔(George Boole)于1854年描述,他使用这些变量及其相关运算来描述数学意义上的逻辑,他设计了一个完整的代数,由简单的二元变量、一组新的运算符和基本运算组成。为了纪念乔治·布尔,二进制变量也称为布尔变量(Boolean variable),布尔变量的代数系统称为布尔代数(Boolean algebra)。逻辑位操作如下:
- NOT
逻辑补码(logical complement)称为NOT运算符,任何布尔运算符都可以通过真值表来表示,真值表列出了所有可能的输入组合的运算符输出,NOT运算符的真值表如下表所示。
原始值 |
NOT操作后 |
0 |
1 |
1 |
0 |
- OR
OR运算符表示任一操作数等于1的事实。例如,如果A=1或B=1,则A或B等于1。OR运算符的真值表如下所示。
A |
B |
A OR B |
0 |
0 |
0 |
0 |
1 |
1 |
1 |
0 |
1 |
1 |
1 |
1 |
- AND
AND运算符的操作是所有操作数为1,则结果才为1,其它则为0。例如,当A和B都为1时,A和B等于1。AND运算符的真值表如下所示。
A |
B |
A AND B |
0 |
0 |
0 |
0 |
1 |
0 |
1 |
0 |
0 |
1 |
1 |
1 |
- NAND、NOR
另外的两个简单的运算符,即NAND和NOR非常有用。NAND是AND的逻辑补码,NOR是OR的逻辑补。它们的真值表如下所示。
A |
B |
A NAND B |
A NOR B |
0 |
0 |
1 |
1 |
0 |
1 |
1 |
0 |
1 |
0 |
1 |
0 |
1 |
1 |
0 |
0 |
NAND和NOR是非常重要的运算符,因为它们被称为通用运算符,可以只使用它们来构造任何其他运算符。
- XOR
XOR是异或运算符,当A和B相等时,值为0,否则为1。真值表如下所示。
A |
B |
A XOR B |
0 |
0 |
0 |
0 |
1 |
1 |
1 |
0 |
1 |
1 |
1 |
0 |
2.2 布尔代数
让来看看NOT运算符的一些规则:
- 定义:\(\overline{0}=1, \text { 且 } \overline{1}=0\),即NOT运算符的定义。
- 双重否定:\(\overline{\overline{A}}=A\),非A的非等于A本身。
OR和AND运算符:
- 恒等式:A+0=A,A.1=A,亦即如果计算布尔变量A与0的OR或与1的AND,结果等于A。
- 相消性:A+1=1,A.0=0,亦即如果计算A OR 1,那么结果总是等于1。类似地,A AND 0总是等于0,因为第二个操作数的值决定了最终结果。
- 幂等性:A+A=A,A.A=A,亦即计算A与自身的OR或AND的结果为A。
- 互补性:A+\(\overline{A}\)=1,A.\(\overline{A}\)=0,亦即A=1或\(\overline{A}\)=1。在任何一种情况下,A+\(\overline{A}\)都有一个项,它是1,因此结果是1。同样,A.\(\overline{A}\)中的一个项是0,因此结果为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),亦即可以使用这个定律来打开括号并简化表达式。
可以使用这些规则以各种方式操作包含布尔变量的表达式,下面看看布尔代数中的一组基本定理。
- 摩根定律(De Morgan's Laws)
有两个摩根定律可以通过构造LHS和RHS的真值表来验证。
\[\overline{A+B}=\overline{A} . \overline{B} \\ \overline{A B}=\overline{A}+\overline{B} \]
- 逻辑门(Logic Gate)
现在让尝试实现电路来实现复杂的布尔公式,“逻辑门”定义为实现布尔函数的器件,可以由硅、真空管或任何其他材料制成。
逻辑门是实现布尔函数的设备。
给定一组逻辑门,可以设计一个电路来实现任何布尔函数,不同逻辑门的符号如下图所示。
逻辑门列表。
3 晶体管
硅是周期表中的第14种元素,有四个价电子,虽然与碳和锗属于同一组,但其化学反应性不如后两者。
90%以上的地壳由硅基矿物组成,二氧化硅是沙子和石英的主要成分,它供应充足,而且制造起来相当便宜。硅具有一些有趣的特性,使其成为设计电路和处理器的理想衬底。让考虑一下硅的分子结构,它有一个致密的结构,每个硅原子都与其他四个硅原子相连,紧密相连的一组硅原子结合在一起形成一个强晶格,其他材料(尤其是金刚石)具有类似的晶体结构。因此,硅原子比大多数金属更紧密。
由于缺乏自由电子,硅没有很好的导电性能,介于良导体和绝缘体之间,因此被称为半导体(semiconductor)。通过可控的方式添加一些杂质,可以稍微改变其性质,这个过程被称为掺杂(doping)。
3.1 掺杂
通常,向硅中添加两种杂质以改变其特性:n型和p型。n型杂质通常由周期表中的V族元素组成,磷是最常见的n型掺杂剂,偶尔也会使用砷。添加具有价电子的V族掺杂剂的效果是,额外的电子从晶格中分离出来,并可用于传导电流。这种掺杂过程有效地提高了硅的导电性。
同样,可以向硅中添加III族元素,如硼或镓,以产生p型掺杂硅,会产生相反的效果,会在晶格中创建一个空隙,此空隙也称为孔(hole),孔表示没有电子。像电子一样,孔可以自由移动,也有助于传导电流。电子带负电荷,孔在概念上与正电荷相关。
现在已经制作了两种半导体材料:n型和p型,下面看看如果连接它们形成p-n结会发生什么。
3.2 P-N结
让考虑一个p-n结,如下图所示。p型区有过量的孔,n型区有过剩的电子。在结处,一些孔交叉并移动到n区,因为它们被电子吸引。类似地,一些电子越过并聚集在p区一侧。孔和电子的这种迁移称为扩散,见证这种迁移的交界处周围的区域被称为耗尽区。然而,由于电子和孔的迁移,在耗尽区中产生了与迁移方向相反的电场,这个电场感应出一种称为漂移电流的电流。在稳态下,漂移电流和扩散电流相互平衡,因此实际上没有电流流过结。
如果将p侧连接到正端子,将n侧连接到负端子,则这种配置称为正向偏置。在这种情况下,孔从结的p侧流向n侧,电子则反向流动。因此,该结传导电流。
如果将p侧连接到负端子,将n侧连接到正端子,则这种配置称为反向偏置。在这种情况下,孔和电子被拉离结。因此,没有电流流过结,并且在这种情况下p-n结不导电。所描述的简单p-n结被称为二极管(diode),它只在一个方向传导电流,即当它处于正向偏置时。
二极管(diode)是一种典型地由单个p-n结制成的电子器件,其仅在一个方向上传导电流。
3.3 NMOS晶体管
现在,让将两个p-n结相互连接,如下图(a)所示,这种结构被称为NMOS(负金属氧化物半导体)晶体管。在这张图中,有一个p型掺杂硅的中心衬底。两侧有两个小区域含有n型掺杂硅,这些区域分别被称为漏极和源极。注意,由于结构是完全对称的,这两个区域中的任何一个都可以被指定为源极或漏极,源极和漏极中间的区域称为通道。在沟道的顶部有一个通常由二氧化硅(SiO2)制成的薄绝缘层,它由金属或多晶硅基导电层覆盖,就是所谓的门(gate)。
因此,典型的NMOS晶体管有三个端子:源极、漏极和栅极,它们中的每一个都可以连接到电压源。现在有两个栅极电压选项——逻辑1(\(V_{dd}\)伏)或逻辑0(0伏)。如果栅极处的电压为逻辑1(Vdd伏),则沟道中的电子被吸引到栅极。事实上,如果栅极处的电压大于某个阈值电压(在当前技术中通常为0.15V),则由于电子的积累,在漏极和源极之间形成低电阻导电路径。因此,电流可以在漏极和源极之间流动。如果沟道的有效电阻是R沟道,那么有\(V_{drain}=IR_{channel}+V_{source}\)。如果流经晶体管的电流量低,则由于低沟道电阻(R沟道),\(V_{drain}\)大致等于\(V_{source}\)。因此,可以将NMOS晶体管视为开关(见上图b)。当栅极电压为1时,它被打开。
现在,如果将栅极电压设置为0,那么由电子组成的导电路径就无法在沟道中形成。因此,晶体管将不能传导电流,将处于o状态。在这种情况下,开关关闭。
NMOS晶体管的电路符号如上图(c)所示。
3.4 PMOS晶体管
像NMOS晶体管一样,可以有一个PMOS晶体管,如下图(a)所示,源极和漏极是由p型硅构成的区域,晶体管操作的逻辑与NMOS晶体管的逻辑完全相反。在这种情况下,如果栅极处于逻辑0,则空穴被吸引到沟道并形成导电路径。然而,如果栅极处于逻辑1,则孔被沟道排斥,不形成导电路径。
PMOS晶体管也可以被视为开关(图b),当栅极电压为0时,它打开,当栅极处的电压为逻辑1时,它关闭。PMOS晶体管的电路符号如图(c)所示。
3.5 NAND和NOR门
下图显示了如何在CMOS技术中构建NAND门。两个输入端A和B连接到每个NMOS-PMOS对的栅极,如果A和B都等于1,则PMOS晶体管将关断,NMOS晶体管将导通,将输出设置为逻辑0。但是,如果其中一个输入等于0,则其中一个NMOS晶体管将关闭,其中一个PMOS晶体管将打开。因此,输出将设置为逻辑1。
请注意,使用AND运算的运算符“.”,这种符号在表示布尔公式时被广泛使用。同样,对于OR运算,使用“+”符号。
下图显示了如何构建NOR门。在这种情况下,两个输入端A和B也连接到每个NMOS-PMOS对的栅极。然而,与NAND门相比,拓扑结构有所不同。如果其中一个输入为逻辑1,则其中一个NMOS晶体管将导通,其中一个PMOS晶体管将截止,输出将设置为0。如果两个输入都等于0,则两个NMOS晶体将截止,两个PMOS晶体将导通,输出将等于逻辑1。
NAND门的一些用途如下:
NOR门的一些用途如下:
4 组合逻辑电路
4.1 XOR门
让实现异或(XOR)的逻辑函数,使用运算符进行XOR运算,如果两个输入不相等,则异或操作返回1,否则返回0。已知\(A \oplus B=A \cdot \overline{B}+\overline{A} \cdot B\),则真值表和实现异或门的电路如下所示。
基本逻辑门如下所示:
4.2 多路复用器和信号分离器
多路复用器(Multiplexer)的框图如下图左所示,采用n个输入位和log(n)个选择位,并根据选择位的值,选择一个输入作为输出(参见图中带箭头的线)。多路复用器在处理器设计中大量使用,需要从一组输入中选择一个输出。多路复用器也称为多路复用器。
信号分离器将log(n)位二进制数作为输入,1位输入,并将输入传输到n条输出线中的一条,参见下图右。多路分解器用于存储单元的设计,其中输入必须精确地反映在一条输出线中。
左:单个多路复用器结构图。中:4输入的多路复用器。右:信号分离器。
多路复用器输入至程序计数器。
4.3 编码器和解码器
解码器将log(n)位二进制数作为输入,并具有n个输出。根据输入,它将其中一个输出设置为1。
解码器的设计如下图左所示,具有两个输入和四个输出的2x4解码器的设计。假设输入是A和B。生成所有可能的组合:\(\overline{A B}, \overline{A} B, A \overline{B}, AB\)。这些布尔组合是通过计算A和B的逻辑“非”,然后将这些值路由到一组“与”门来生成的。
现在让考虑一个与解码器逻辑相反的电路,其框图如下图右所示。该电路有n个输入和log(n)个输出,n个输入中的一个假定为1,其余假定为0,输出位提供等于1的输入二进制编码。例如,在8输入、3输出编码器中,如果第f行等于1,则输出等于100(计数从0开始)。
左:2x4解码器的设计。右:n位编码器框图。
现在假设不存在只有一个输入行可以等于1的限制,假设有多个输入可以等于1。在这种情况下,需要报告具有最高索引(优先级)的输入行的二进制编码。例如,如果是第3行和第5行,那么需要报告第5行的二进制编码,和上图右一样。此外,4-2位编码器的电路图如下图所示。
用解码器实现解复用器:
时钟SR锁存器(下图左)和D锁存器(下图右):
J–K锁存器:
基本锁存器的比较:
5 时序逻辑电路
前面已经研究了在比特上计算不同函数的组合逻辑电路,本小节将讨论如何保存位以供以后使用,这些结构被称为顺序逻辑元件(sequential logic element),因为输出取决于过去的输入,这些输入在事件序列中较早出现。逻辑门的基本思想是修改输入值以获得所需的输出,在组合逻辑电路中,如果输入被设置为0,那么输出也被重置。为了确保电路存储一个值并在处理器通电时保持该值,需要设计一种具有某种“内置存储器”的不同类型的电路。让从制定一组要求开始:
1、电路应能自维持,并在外部输入复位后保持其值。不应依赖外部信号来维持其存储的元件。
2、应该有一种方法来读取存储的值而不破坏它。
3、应该有一种方法将存储值设置为0或1。
确保电路保持其值的最佳方法是创建反馈路径,并将输出连接回输入,先看看最简单的逻辑电路:SR锁存器(SR latch)。
5.1 SR锁存器
下图显示了SR锁存器。有两个输入S(设置)和R(重置),有两个输出Q及其补码Q,包含了两个交叉耦合NAND门的电路。请注意,如果与非门的一个输入为0,则输出保证为1。然而,如果其中一个输入是1,另一个输入则为a,则输出为a。
用NOR门实现的SR锁存器:
5.2 时钟和信号
一个典型的处理器包含数百万或可能数十亿个逻辑门和数千个锁存器,不同的电路需要不同的时间,例如多路复用器可能需要1ns,解码器可能需要0.5ns。电路完成计算后,就可以转发输出了。如果没有全局时间的概念,很难在不同的单元之间同步通信,尤其是那些具有可变延迟的单元,导致难以设计、操作和验证处理器。由此需要时间概念,例如可以说加法器需要两个时间单位,在两个单元结束时,预期数据将在锁存器X中找到,其他单元可以在两个时间单元后从锁存器中获取值并继续计算。
考虑一个需要向打印机发送一些数据的处理器的例子。为了传输数据,处理器通过一组铜线发送一系列比特,打印机读取这些比特,然后打印数据。问题是,处理器什么时候发送数据?计算完成后,需要发送数据。可以问的下一个问题是,处理器如何知道计算何时结束?它需要知道不同单元的确切延迟,一旦计算的总持续时间过去,可以将输出数据写入锁存器,并设置用于通信的铜线的电压。因此,处理器确实需要时间概念。其次,设计者需要告诉处理器不同子单元所需的时间。与处理2.34ns和1.92ns等数字相比,处理1、2和3等整数要简单得多。这里的1、2、3表示时间单位,时间单位可以是任何数字,例如0.9333ns。
时钟信号(clock signal):发送到大型电路或处理器的每个部分的周期性方波。
时钟周期(clock cycle):时钟信号的周期。
时钟频率(clock frequency):时钟周期的倒数。
因此,大多数数字电路与时钟信号同步,该时钟信号在完全相同的时间向处理器的每个部分发送周期性脉冲。时钟信号为方波,如下图所示,大多数时间,时钟信号是由主板上的专用单元从外部生成的。让考虑时钟信号从1转变到0(向下/负边缘)的点作为时钟周期的开始,从时钟的一个向下沿到下一个向下边缘测量时钟周期,时钟周期的持续时间也称为时钟周期,时钟周期的倒数被称为时钟频率。
一个时钟信号。
电脑、笔记本电脑、平板电脑或移动电话通常会在其规格中列出频率。例如,规范可能会说处理器运行在3GHz,这个数字是指时钟频率。
典型的计算模型是:电路中执行所有基本动作所需的时间是按照时钟周期来测量的,如果生产者单元占用n个时钟周期,那么在n个时钟循环结束时,它将其值写入锁存器。其他用户单元知道此延迟,并且在第(n+1)个时钟周期开始时,它们从锁存器读取值。由于所有单元都与时钟明确同步,并且处理器知道每个单元的延迟,因此很容易对计算进行排序、与I/O设备通信、避免竞争条件、调试和验证电路。想向打印机发送数据的简单示例可以通过使用时钟轻松解决。
5.3 时钟SR锁存器
下图显示了SR锁存器,其增加了两个与非门,时钟作为输入之一,另外两个输入分别是S位和R位。如果时钟为0,则交叉耦合NAND门的两个输入都为1,将保持先前的值。如果时钟为1,则交叉耦合NAND门的输入分别为S和R,这些输入与基本SR锁存器相同。请注意,时钟锁存器通常称为触发器(flip-flop)。
时钟SR锁存器图例。
触发器(flip-flop)是一个时钟锁存器,可以保存一位(0或1)。
通过使用时钟,部分解决了输入和输出同步的问题。在这种情况下,当时钟为0时,输出不受输入的影响。当时钟为1时,输出受输入影响。这种锁存器也称为电平敏感锁存器(level sensitive latch)。
电平敏感锁存器(level sensitive latch)取决于时钟信号的值:0或1。通常,它只能在时钟为1时读取新值。
在电平敏感锁存器中,电路有半个时钟周期来计算正确的输出(当时钟为0时)。当时钟为1时,输出可见。最好有一个完整的时钟周期来计算输出,这需要一个边缘敏感锁存器(edge sensitive latch),边缘敏感锁存器仅在时钟的向下边缘反映输出端的输入。
边缘敏感锁存器(edge sensitive latch)仅在固定的时钟边缘(例如向下边缘,从1到0的转换)反映输出端的输入。
5.4 边缘敏感SR触发器
下图显示了边缘敏感SR触发器的结构图,连接了两个边缘敏感SR触发器,唯一的区别是第二个触发器使用了时钟信号组合。第一个触发器为主(master),而第二个为从(slave)。这种触发器也被称为主-从SR触发器。这就是这个电路的工作原理。
除了主从SR触发器,还有其它各种类型的触发器,如JK触发器、D触发器、主从D触发器等。
从上到下:JK触发器、D触发器、主从D触发器。
5.5 寄存器
可以通过使用一组n个主从D触发器来存储n位数据,每个D触发器连接到输入线,其输出端连接到输出线,这种n位结构被称为n位寄存器。可以并行加载n位,也可以在每个负时钟边沿并行读取n位。因此,这种结构被称为并行输入——并行输出寄存器。其结构如下图所示。
现在让考虑一个串行输入-并行输出寄存器,如下图所示,有一个输入被馈送到最左边的D触发器, 每个周期,输入都会移动到右侧的相邻触发器。因此,要加载n位将需要n个周期。 第一位将在第一个周期被加载到最左边的触发器中,它需要n个周期才能到达最后一个触发器。 到那时,其余的n - 1触发器将加载其余的n - 1位,然后可以并行读取所有 n 位(类似于并行并行输出寄存器)。 该寄存器也称为移位寄存器,用于实现高速I/O总线中使用的电路。
8位并行寄存器的结构图如下:
5位移位寄存器:
行波计数器:
6 内存
6.1 静态内存(SRAM)
SRAM是指静态随机存取存储器,基本SRAM单元包含两个交叉耦合的反相器,如下图所示。相比之下,基本SR触发器或D触发器包含交叉耦合的NAND门。设计如下所示。
SRAM单元的核心包含4个晶体管(每个反相器中有2个),这种交叉耦合布置足以节省单个比特(0或1)。然而,需要一些额外的电路来读取和写入值。此时,在锁存器中使用交叉耦合反相器到底是不是一个坏主意,它们毕竟需要更少的晶体管。将看到,实现用于读取和写入SRAM单元的电路的开销是非常重要的,开销不足以证明以SRAM单元为核心制作锁存器的合理性。
交叉耦合的反相器连接到每一侧(W1、W2)上的晶体管,W1和W2的栅极连接到被称为字线的相同信号,两个反相器W1和W2中的四个晶体管构成SRAM单元,它总共有六个晶体管。现在,如果字线上的电压低,则W1和W2关断,不可能读取或写入SRAM单元。然而,如果字线上的信号为高,则W1和W2导通,可以访问SRAM单元。
下图显示了一个典型的SRAM阵列,SRAM单元被布置为二维矩阵。一行中的所有SRAM单元共享字线,一列中的所有SRAM单元共享一对位线。要激活某个SRAM单元,必须打开其相关的字线,由解码器完成,获取地址位的子集,并打开适当的字线。一行SRAM单元可能包含100多个SRAM单元,通常,会对32个SRAM单元(在32位机器上)的值感兴趣。在这种情况下,列复用器/解复用器选择属于感兴趣的SRAM单元的位线,使用地址中的位的子集作为列选择位。这种设计方法也称为2.5D存储器组织。
SRAM单元阵列。
6.2 内容寻址内存(CAM)
下图是10晶体管CAM单元,如果SRAM单元中存储的值V不等于输入位\(A_i\),那么希望将匹配线的值设置为0。在CAM单元中,上半部分是具有6个晶体管的常规SRAM单元,下半部有4个额外的晶体管。现在让考虑晶体管T1,它连接到全局匹配线,晶体管T2。T1由存储在SRAM单元中的值V控制,T2由\(\overline{A_i}\)控制。假设V=\(\overline{A_i}\),如果两者都为1,则晶体管T1和T2处于导通状态,并且匹配线和地之间存在直接导电路径。因此,匹配线的值将设置为0。然而,如果V和\(\overline{A_i}\)都为0,则通过T1和T2的路径不导通。但是,在这种情况下,通过T3和T4的路径变得导通,因为这些晶体管的栅极分别连接到\(\overline{V}\)和\(A_i\)。两个栅极的输入都是逻辑1,因此匹配线将被下拉到0。读取器可以反过来验证,如果V=\(A_i\),则不形成导通路径。因此,如果存储的值与输入位\(A_i\)不匹配,则CAM单元将匹配线驱动到逻辑0。
10晶体管CAM单元。
下图显示了CAM单元阵列。该结构主要类似于SRAM阵列。可以通过索引寻址一行,并执行读/写访问,此外可以将CAM单元的每一行与输入A进行比较。如果任何行与输入匹配,则相应的匹配线的值将为1。可以计算所有匹配线的逻辑OR,并确定CAM阵列中是否匹配,此外可以将CAM阵列的所有匹配线连接到优先级编码器,以查找与数据匹配的行的索引。
CAM单元阵列。
6.3 动态内存(DRAM)
现在来看看一种只使用一个晶体管来节省一点时间的存储器技术,它非常密集、面积大,而且能效高,但比SRAM和锁存器慢得多,适用于大型片外存储器。
基本DRAM(动态内存)单元如下图所示。单个晶体管的栅极连接到字线,从而启用或禁用它,其中一个端子连接到存储电荷的电容器。如果存储的位是逻辑1,则电容器带电,否则不带电。
一个动态内存的单元。
DRAM和SRAM单元的对比图。
因此,读取和写入值非常容易。需要首先设置字线,以便可以访问电容器。为了读取该值,需要感测位线上的电压。如果它处于地电位,则单元存储0,否则如果它接近电源电压,则存储1。类似地,要写入值,需要将位线(BL)设置为适当的电压,并设置字线,电容器将相应地充电或放电。
然而,就DRAM而言,并非一切都是免费的。让假设电容器被充电到等于电源电压的电压,实际上,电容器将通过电介质和晶体管逐渐泄漏一些电荷。该电流很小,但在长时间内电荷的总损失可能很大,最终会使电容器放电。为了防止这种情况,有必要定期刷新DRAM单元的值,亦即需要读取并写回数据值。这也需要在读取操作之后完成,因为电容器在对位线充电时会损失一些电荷。现在让尝试制作一个DRAM单元阵列。
可以用创建SRAM单元阵列的方法构建DRAM单元阵列(下图),有三点不同:
- 存在一条位线而不是两条位线。
- 有一个连接到位线的专用刷新电路。这在读取操作后使用,也会定期调用。
- 在这种情况下,感测放大器出现在列复用器/解复用器之前。读出放大器还为整个DRAM行(也称为DRAM页)缓存数据。它们确保对同一DRAM行的后续访问是快速的,因为它们可以直接从读出放大器进行服务。
DRAM单元阵列。
接下来简述现代DRAM的时序方面。在过去的好日子里,DRAM内存是异步访问的,意味着DRAM模块没有做出任何时序保证。但现在每个DRAM操作都与系统时钟同步,因此,如今的DRAM芯片是同步DRAM芯片(SDRAM芯片)。
截至目前,同步DRAM存储器通常使用DDR4或DDR5标准,DDR代表双倍数据速率,使用最早标准DDR1的设备在时钟的上升沿和下降沿向处理器发送8字节的数据包,DDR也被称为双峰(double pump)操作。DDR1的峰值数据速率为1.6 GB/s,后续的DDR世代通过以更高的频率传输数据来扩展DDR1,例如,DDR2的数据速率是DDR1设备的两倍(3.2 GB/s),DDR3通过使用更高的总线频率将峰值传输速率进一步提高了一倍,自2007年开始使用(峰值速率为6.4GB/s)。
6.4 只读内存(ROM)
只读内存可分为普通ROM和PROM(可编程ROM),下面分别是它们的单元图例。
(a) 存储逻辑0的ROM单元;(b) 存储逻辑1的ROM单元。
PROM单元。
6.5 可编程逻辑阵列
事实证明,可以很容易地用类似于PROM单元的存储单元制作组合逻辑电路,这种器件被称为可编程逻辑阵列或PLA。PLA在实践中用于实现由数十或数百个小项(minterm)组成的复杂逻辑函数,相对于由逻辑门组成的硬连线电路的优势在于它是灵活的,可以在运行时更改PLA实现的布尔逻辑,相比之下,由硅制成的电路永远不会改变其逻辑。其次,PLA的设计和编程更简单,而且有很多软件工具可以设计和使用PLA。最后,PLA可以有多个输出,因此可以很容易地实现多个布尔函数。这种额外的灵活性是有代价的,代价是性能。
下图(a)所示的PLA单元原则上类似于基本PROM单元。如果栅极处的值(E)等于1,则NMOS晶体管处于导通状态。因此,NMOS晶体管的源极和漏极端子之间的电压差非常小。换句话说,可以简单地假设结果线的电压等于信号的电压,X. If (E = 0),NMOS晶体管处于截止状态。结果线是浮动的,并保持其预充电电压。在这种情况下,建议推断逻辑1。
现在让构建一行PLA单元,其中每个PLA单元在其源极端子处连接到输入线,如图(b)所示。输入编号为X1…Xn,所有NMOS晶体管的漏极连接到结果线,PLA单元的晶体管的栅极连接到一组使能信号E1…En。如果任何一个使能信号等于0,则该特定晶体管被禁用,可以将其视为从PLA阵列中逻辑移除。
一个PLA单元。
现在让创建一个PLA单元格数组,如下图所示,每行对应一个minterm。对于3变量示例,每行由6列组成,每个变量有2列(原始和补充),例如,前两列分别对应于A和\(\overline{A}\)。在任何一行中,这两列中只有一列包含PLA单元,因为A和\(\overline{A}\)不能同时为真。在第一行,计算最小项\(\overline{ABC}\)的值,因此第一行包含对应于\(\overline{A}\)、\(\overline{B}\)和\(\overline{C}\)的列中的PLA单元。在其余行中为剩余的minterm进行类似的连接,PLA阵列的这一部分被称为AND平面,因为正在计算变量值(原始值或补码值)的逻辑AND。PLA阵列的AND平面独立于希望计算的布尔函数,给定输入,它计算所有可能的最小项的值。
PLA单元阵列。
典型的内存封装引脚和信号。
256 KB内存组织。
1MB内存组织。
DDR代次演进图。
非易失性RAM技术。
上:简化的DRAM读取时序;下:Signetics 7489 SRAM的脉冲串。
7 计算机算术
本节将设计算术运算的硬件算法,先阐整数运算的算法,如两个二进制数相加的基本算法,有很多方法可以完成这些基本操作,每种方法都有自己的优缺点。注意,二进制减法的问题在概念上与2的补码系统中的二进制加法相同。因此,不需要单独对待它。随后,将看到,n个数的相加问题与乘法问题密切相关,而且这是一个硬件上的快速操作。遗憾的是,整数除法并不存在非常有效的方法。然而,将考虑两种用于划分正二进制数的流行算法。
整数算术之后,将研究浮点(带小数点的数字)算术的方法,大多数整数算法稍作修改后都可以移植到浮点数领域。与整数除法相比,浮点除法可以非常有效地完成。
7.1 加法
让看看将两个1位数字a和b相加的问题,a和b都可以取两个值:0或1,因此,a和b有四种可能的组合,它们的二进制和可以是00、01或10。当a和b均为1时,它们的和将是10,两个1位数字的总和可能有两位长。让将结果的LSB称为和,将MSB称为进位,例如,把8和9相加,和是7,进位是1。
可以将和和进位的概念扩展到加三个1位数字。如果将三个1位数字相加,那么结果的范围是二进制的00到11之间。
和(sum):总和是两个或三个1位数字相加结果的LSB。
进位(carry):进位是两个或三个1位数字相加结果的MSB。
对于可以将两个1位数字相加的加法器,将有两个输出位:和s和进位c,将两个位相加的一个加法器称为半加法器(half adder)。半加法的真值表如下表所示。
a |
b |
s |
c |
0 |
0 |
0 |
0 |
0 |
1 |
1 |
0 |
1 |
0 |
1 |
0 |
1 |
1 |
0 |
1 |
半加法器。
可以加3位的加法器称为全加法器(full adder),它的电子构造如下:
n位加法器被称为纹波进位加法器(ripple carry adder),其设计如下所示:
考虑将两个数字A和B相加的问题,首先将比特集划分为4比特的块,如下图所示,每个块包含一个a片段和一个B片段。通过考虑块的输入进位将这两个片段相加,并生成一组和位和一个进位,此进位是后续块的输入进位。
此外,还有超前进位加法器(Carry Lookahead Adder),其分为两个阶段,每个阶段都拥有复杂的电子构造。
7.2 乘法
现与加法类似,先看看两个十进制数相乘的最简单的方法,不妨尝试将13乘以9。在这种情况下,13被称为被乘数(multiplicand),9被称为乘数(multiplier),117是乘积(product)。
下图(a)显示了十进制的乘法,(b)显示了二进制的乘法。请注意,两个二进制数相乘的方法与十进制数完全相同。需要考虑乘数从最小显著位置到最大显著位置的每一位。如果该位为1,那么将被乘数的值写在该行下方,否则将写0。对于每个乘数位,将被乘数向左移动一位。其原因是每个乘数位代表2的更高幂。将每个这样的值称为部分和(见图7.10(b))。如果乘法器有m位,那么需要将m个部分和相加以获得乘积。在这种情况下,乘积是十进制117,二进制1110101。读者可以验证它们实际上表示相同的数字。为了便于以后的表示,让定义另一个称为部分积的术语。它是部分和的连续序列的和。
(a)十进制乘法;(b)二进制乘法。
常规的乘法器是\(O(n^2)\),而改进版的Booth乘法器或Wallace树形乘法器(下图)可以做到\(O(log(n))\)的算法复杂度。
Wallace树形乘数。
7.3 除法
现在让看看整数除法。不幸的是,与加法、减法和乘法不同,除法是一个明显较慢的过程。任何除法运算都可以表示如下:
\[N=DQ+R \]
N是被除数,D是除数,Q是商,R是余数。假定除数和被除数为正,除法过程需要满足以下属性:
- \(R < D\)且\(R \ge 0\)。
- Q是满足上述等式的最大正整数。
如果想除掉负数,那么首先将它们转换成正数,进行除法,然后调整商和余数的符号,部分ISA试图确保余数始终为正。在这种情况下,需要将商减1,并将除数与余数相加,使其为正。
实现除法的方式有迭代除法、佘数恢复除法(Restoring Division)、非余数恢复除法(Non-Restoring Division)等。本文忽略这些算法的具体描述,有兴趣的童鞋可以自行查阅资料。
7.4 浮点数运算
浮点加法和减法的问题实际上是同一问题的不同方面。A-B可以用两种方式解释,可以说正在从A中减去B,也可以说在将-B加到A中。因此,与其单独看减法,不如将其视为加法的特例。浮点数的二进制表示、属性和特殊含义可以参见:17.2.2 浮点数。
下图显示了一个示例,说明了如何将有效位解压缩,并将其放入普通浮点数的寄存器中。在32位IEEE 754格式中,尾数有23位,小数点前有0或1。因此,有效位需要24位,如果希望添加前导符号位(0),那么需要25位存储。让把这个号码保存在一个寄存器中,并称之为W。
展开有效位并放入寄存器。
IEEE 754格式。
浮点数的运算涉及舍入等考量,下图显示了两个浮点数相加的算法,考虑了0值。
累加两个浮点值的流程图。
浮点数相乘算法与泛型加法算法的形式完全相同,只需几步。让尝试乘以A x B以获得乘积C,乘法的流程图如下图所示。在乘法的情况下,不必对齐指数,如下初始化算法,将B的符号和装入寄存器W,W的宽度等于操作数大小的两倍,就可以容纳乘积。E寄存器初始化为\(E_A+E_B - bias\),因为在乘法的情况下,指数相加,减去bias以避免重复计数,计算结果的符号很简单。
两个浮点值相乘的流程图。
此外,还有Goldschmidt除法以及Newton-Raphson除法。
Newton-Raphson方法。