Digital Building Blocks
# 5.2 Arithmetic Circuits
运算电路包括:
- 加法器
- 减法器
- 比较器
- 移位器
- 乘法器
- 除法器
# 5.2.1 加法器
值得注意的是,HDL 中提供了 + 运算符来表示 CPA,并且综合工具会自动实现三种 CPA 类型中的一种。
system verilog 描述为:
module adder
#(parameter N = 8)
(input logic [N–1:0] a, b,
input logic cin,
output logic [N–1:0] s,
output logic cout
);
assign {cout, s} = a + b + cin;
endmodule
2
3
4
5
6
7
8
9
10
verilog 描述为:
module adder
#(parameter N = 8)
(input [N–1:0] a,
input b,
input cin,
output [N–1:0] s,
output cout
);
assign {cout, s} = a + b + cin;
endmodule
2
3
4
5
6
7
8
9
10
11
# 5.2.2 Subtraction
减法器的设计非常简单: a - b = a + (-b) , 所以可以使用上一节中学的加法器加上一点 trick 就可以构成减法器。
Trick: 补码非的位级表示 (CSAPP Section2.3.3)
位级补码非 = 位级的反 + 1
-x = ~x + 1
所以有:
可以将加法器的 Cin 设置为 1, 对一个输入 B 进行取反操作再作为输入即可:

system verilog 描述为:
module subtractor
#(parameter N = 8)
(input logic [N–1:0] a, b,
output logic [N–1:0] y
);
assign y = a − b;
endmodule
2
3
4
5
6
7
8
verilog 描述为:
module subtractor
#(parameter N = 8)
(input [N–1:0] a,
input b,
output [N–1:0] y
);
assign y = a − b;
endmodule
2
3
4
5
6
7
8
9
# 5.2.3 Comparators
比较器有两种类型:
Equality Comparator: 只判断两个数是否相等,类似于 C 语言中的
==
Equality Comparator 的实现比较简单,对两个数的每一位都使用 XNOR (异或非门,~^) 进行比较即可:

Magnitde Comparator: 判断两个数的大小,类似于 C 语言中的
<
Magnitde Comparator 使用减法器进行比较,然后看最高位是否为 1:
- A - B > 0,最高位为 0
- A - B < 0,最高位为 1

通过使用上面两种比较器 (==, <),就能实现:eaual, not equal, less than, less than or equal, greater than, greater than or equal
module comparator
#(parameter N = 8)
(input logic [N–1:0] a, b,
output logic eq, neq, lt, lte, gt, gte
);
assign eq = (a == b); // equal
assign neq = (a != b); // non equal
assign lt = (a < b); // less than
assign lte = (a <= b); // less than or equal
assign gt = (a > b); // greater than
assign gte = (a >= b); // greater than or equal
endmodule
2
3
4
5
6
7
8
9
10
11
12
13
综合的电路图如下:

# 5.2.3 ALU
(Arithmetic/Logical Unit)
ALU 是计算机的核心部件,ALU 中集成了一系列的算术操作 (addition, subtraction, magnitude comparison) 和逻辑操作 (AND, OR).

笔记
- BB 表示 B 或
- STL 表示 set if than less,即 Magnitude Comparator (上一节介绍)
时,4:1MUX 的输出为 AND 时,4:1MUX 的输出为 OR 时,4:1MUX 的输出为 Adder 或者 Subtractor 时,4:1MUX 的输出为 Magnitude Comparator - ALU 中还可以有 flag,比如 overflow flag 来判断运算结果是否溢出,zero flag 来判断运算结果是否为 0
- 这是基本的 ALU 的组成,ALU 中还可以有其他的功能,如 XOR, equality comparison
上述的 ALU 的 system verilog 表示为:
module alu32(input logic [31:0] A, B,
input logic [2:0] F,
output logic [31:0] Y);
logic [31:0] S, BB;
assign BB = F[2] ? ~B : B;
assign S = A + BB + F[2];
always_comb
case (F[1:0])
2'b00: Y <= A & BB;
2'b01: Y <= A | BB;
2'b10: Y <= S;
2'b11: Y <= S[31];
endcase
endmodule
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
verilog 表示为:
module alu32(input [31:0] A, B,
input [2:0] F,
output reg [31:0] Y);
wire [31:0] S, BB;
assign BB = F[2] ? ~B : B;
assign S = A + BB + F[2];
always@(*)
case (F[1:0])
2'b00: Y <= A & BB;
2'b01: Y <= A | BB;
2'b10: Y <= S;
2'b11: Y <= S[31];
endcase
endmodule
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
带 overflow 和 zero flag 的 ALU:
module alu32(input logic [31:0] A, B,
input logic [2:0] F,
output logic [31:0] Y,
output logic Zero, Overflow);
logic [31:0] S, BB;
assign BB = F[2] ? ~B : B;
assign S = A + BB + F[2];
case (F[1:0])
2'b00: Y <= A & BB;
2'b01: Y <= A | BB;
2'b10: Y <= S;
2'b11: Y <= S[31]; // verilog中默认使用zero extension / zero padding扩展
endcase
assign Zero = (Y == 32'b0);
always_comb
case (F[2:1])
2'b01: Overflow <= A[31] & B[31] & ~S[31] | // S = A + B A和B都为负 S为正
~A[31] & ~B[31] & S[31]; // S = A + B A和B都为正 S为负
2'b11: Overflow <= ~A[31] & B[31] & S[31] | // S = A - B = A + ~B + 1 // A为正 B为负 结果为负
A[31] & ~B[31] & ~S[31]; // S = A - B = A + ~B + 1 // A为负 B为正 结果为正
default: Overflow <= 1'b0;
endcase
endmodule
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
关于溢出和进位标志的一些感悟:
- 无符号数中使用 carry flag (进位标志) 来检测无符号算数运算操作 (加减乘除) 是否发生错误 (无符号数的运算不考虑溢出标志)
- 有符号数中使用 overflow flag (溢出标志) 来检测有符号数的算数运算操作 (加减乘除) 是否发生错误 (有符号的运算不考虑进位标志)
提示
在进行算数运算时,ALU 是不知道一个数是有符号数还是无符号数的,它只是根据结果给出溢出和进位的标志位,所以如果你进行输入时,如果把数当作有符号数,你就应该去看 / 检查溢出标志位来判断你的算数运算是否发生错误。 反之,如果把输入数当作有符号数,你就应该去看 / 检查进位标志位来判断你的算数运算是否发生错误。
- 在进行无符号加法运算时,进位标志顾名思义表示是否有进位产生;在进行无符号减法运算时,进位标志有两种不同的用法,具体见参考资料第二个链接。
- 进位在 ALU 中的判断方法:
{out_c,out_s} =in_x + in_y;
- 溢出在 ALU 中的判断方法:
对于有符号数的加法溢出,只有两种情况会发生溢出:
- in_x 和 in_y 都为正,out_s 为负
- in_x 和 in_y 都为负,out_s 为正
对于有符号数的减法溢出,只有两种情况会发生溢出: - in_x 为正且 in_y 为负,out_s 为负
- in_x 为负且 in_y 为正,out_s 为正
基于这些的情况,我们可以给出有符号加法在 ALU 的判断方法:
- 一种判断方法就是上面 "带 overflow 和 zero flag 的 ALU" 中的方法
- 另一种方法是可以统一有符号数加法和减法溢出的判断:
assign t_no_Cin = {n{ Cin }}^B;
assign {Carry,Result} = A + t_no_Cin + Cin;
assign Overflow = (A[n-1] == t_no_Cin[n-1]) && (Result [n-1] != A[n-1]);
2
3
问:为什么不能使用下面的方法?
assign t_add_Cin =( {n{Cin}}^B )+ Cin; // 在这里请注意^运算和+运算的顺序
assign { Carry, Result } = A + t_add_Cin;
assign Overflow = (A[n-1] == t_add_Cin[n-1]) && (Result [n-1] != A[n-1]);
2
3
在这种情况下进行减法运算溢出判断时,t_add_Cin = ~B + 1,当 B = TMin 时,~B + 1 发生了溢出 (TMax + 1 = TMin),所以 t_add_Cin 都是错误的,再使用其用于 Overflow 的判断就不对。
举例: A = 0, B = TMin,进行减法运算时会发生溢出,但是使用上面的方法判断不会发生溢出。
参考资料:
The CARRY flag and OVERFLOW flag in binary arithmetic (opens new window)
Carry flag wiki (opens new window)
# 5.2.5 Shifters and Rotators
Shifter 有两种:
- Logic Shifter(>>, <<)
Ex: 11001 LSR 2 = 00110; 11001 LSL 2 = 00100 - Arithmetc Shifter(>>>, <<<)
Ex: 11001 ASR 2 = 11110; 11001 ASL 2 = 00100
Rotator: rotates numbers in circle
Ex: 11001 ROR 2 = 01110;(ROR 2 means rotate 2bit to right)
11001 ROL 2 = 00011;(ROR 2 means rotate 2bit to right)
移位运算使用 MUX 实现,比如 4bit 的移位存器使用 4 个 4:1MUX 实现:

4bit 的 rotator:

# 5.2.6 Multiplication
二进制的乘法可以用与门进行运算,比如:1 * 1 == 1 & 1, 1 * 0 == 1 & 0
所以使用与门和加法器即可实现乘法器 (可以看出乘法器的实现图和乘法运算的竖式是一样的形状):

system verilog 表示为:
module multiplier
#(parameter N = 8)
(input logic [N–1:0] a, b,
output logic [2*N–1:0] y);
assign y = a * b;
endmodule
2
3
4
5
6
7
8
# 5.2.7 Division
除法器的设计较为复杂,具体可参考:
除法运算过程 (opens new window)
除法器实现 (opens new window)
# 5.3 Number Systems
有理数 (rational number) 需要使用定 / 浮点数才能表示:
- 定点数 (fixed-point numbers)
- 浮点数 (floating-point numbers)
- 定点数
定点数在整数 (integer) 和小数 (fraction) 之前使用二进制小数点 (binary point) 分割,定点数有两种表示方法:
比如对于有理数-2.375,使用 4bits integer 和 4bit fraction 可以表示为:
- sign and magnitude representation (原码表示,最高位为符号位)
1010.0110 - two's complement (补码表示)
1101.1010
提示
有理复数的补码表示可以使用一点 trick -x = ~x + 1 计算得来,本例子中另 x 为 2.375 ,二进制表示为 0010.0110 ,然后可以得到
- 浮点数
浮点数使用了科学计数法 (scientific notation) 的概念来表示有理数。
十进制数 4100 的科学计数法为
类似于科学计数法,浮点数中也存在 sign, mantissa (M), base (B), exponent (E):
比如对于浮点数
Sign: 0
Exponent: 0000 0111
Mantissa: 111 0010 0000 0000 0000 0000 (小数点一定在最高位 1 的右边)

但是这样的表示方法是不够高效的,我们可以优化上面的 32bit 浮点数的表示方法 (从而得到 IEEE 754 的浮点数表示):
首先先规定一些术语:
- 符号 (sign): s 决定这数是负数 (s=l) 还是正数 (s=O), 而对于数值 0 的符号位解释
作为特殊情况处理。 - 尾数 (Mantissa): M 是一个二进制小数
- 阶码 (exponent): E 的作用是对浮点数加权,这个权重是 2 的 E 次幕 (可能是负数)。
将浮点数的位表示划分为三个字段,分别对这些值进行编码:
- 一个单独的符号位 s 直接编码符号 s。
- k 位的阶码字段
。编码阶码 E。 - n 位小数字段
编码尾数 M。

在上面的浮点数编码方法中,尾数 (Mantissa) 的最高位一定为 1,所以在编码过冲中,可以将这个 1 省略 (这个 1 被称为 implicit leading one),使得小数字段得到更大的表示范围 (小数字段的表示范围 + 1)。

其实对于一个浮点数指数部分有正有负 (比如
的阶码就为 - 4),所以我们可以用设置偏置值 (Biase),使用 exp - Bias 表示阶码 E (这里使用 "阶码的值" 表示),从而如果 exp > Biase 时,可以得到一个大于 0 的阶码,如果 E < Bias 时,会得到一个小于 0 的阶码,从而使得指数部分有正有负。在本例子中阶码 E 为 ,所以可以求出阶码字段

在 IEEE 754 浮点数的编码规则中,将浮点数分为几种类型表示:
- 规格化的值 (Normalized)
如果阶码位不全为 0 也不全为 1,称浮点数为规格化的值,此时取阶码 E = e - Bias,尾数(包含隐含的 1) - 非规格化的值 (Denormalized)
如果阶码为全为 0,称浮点数为非规格化的值,此时取阶码 E = 1 - Bias,尾数(不包含隐含的 1) - 特殊值
用来表示,NaN

使用 IEEE 浮点数的表示方法是无法表示所有有理数的,比如 1.7 会被表示为 1.699999..。为了能够精准表示每一个有理数,可以使用 BCD (binary coded decimal) 的编码方式,BCD 中每一个十进制位都使用 4bits 中的 0~9 进行编码,比如 1.7 的 BCD 码可以表示为 0001.0111。但是使用这种编码的方式的缺点是在进行浮点数运算时,会引入更加复杂的计算逻辑,并且 4bit 的数组浪费了 A-F 的编码。
浮点数的加法运算过程见 DDCA 课本 Section5.3.2.
# 5.4 Sequential Building Blocks
本节介绍:
- 计数器 (counters)
- 移位寄存器 (shift registers)
# 5.4.1 Counters
一个 N 位的 binary counter 在时钟的上升沿计数加一,它的输入有时钟信号和复位信号,输入为 Q,如下图所示:

一个 N 为的 binary counter 可以由一个加法器和可复位 D 触发器组成,结构图如上所示。
system verilog 语言描述如下:
module counter
#(parameter N = 8)
(input logic clk,
input logic reset,
output logic [N–1:0] q);
always_ff @(posedge clk, posedge reset)
if (reset) q <= 0;
else q <= q + 1;
endmodule
2
3
4
5
6
7
8
9
10
verilog 描述如下:
module counter
#(parameter N = 8)
(input clk,
input reset,
output [N–1:0] q);
always @(posedge clk, posedge reset)
if (reset) q <= 0;
else q <= q + 1;
endmodule
2
3
4
5
6
7
8
9
10
# 5.4.2 Shift Registers
移位寄存器 (也被称为 serial-to-parallel converters) 将数据以串行的方式从
N 位的移位寄存器可以用 N 个 D 触发器构成,结构图如下所示:

输入在每个时钟周期串行的送入一位给
移位寄存器和移位器的区别:
- 移位寄存器是一个时序逻辑电路,在每个时钟的上升或者下降沿移动一位。
- 移位器是一个组合逻辑电路,根据输入移动指定的位数。
parallel-to-serial converters: 并串转化器一次并行载入 Nbit 的数据,然后在每个时钟周期输出一位。
一个移位寄存器可以修改为一个并串转化器,并保留移位寄存器 (串并转化器) 的功能。
- 当 Load = 1 时为串并转化器
- 当 Load = 0 时为移位寄存器 (并串转化器)

system verilog 描述为:
module shiftreg #(parameter N = 8)
(input logic clk,
input logic reset, load,
input logic sin,
input logic [N–1:0] d,
output logic [N–1:0] q,
output logic sout);
always_ff @(posedge clk, posedge reset)
if (reset) q <= 0;
else if (load) q <= d;
else q <= {q[N–2:0], sin};
assign sout = q[N–1];
endmodule
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 5.5 Memory Arrays
本节主要内容:
- Overview(chracteristics of all memory arrays)
- DRAM(dynamic random access memory - volatile)
- SRAM(static random access memory - volatile)
- ROM(read only memory - nonvolatile)
# 5.5.1 Overview
memory array 的结构模型为:

其中 Decoder 见 Section 2.8.2
memory read:
首先将某根 wordline 被置位 1,该行的所用 bit cells 都驱动 bitlines 为高或者低.
memroy write:
bit lines 首先被驱动为高或者低电平,然后对应的 wordlines 被置位 1,使得电平被写入 bit cells.
下面是单端口 (只有一个读端口和一个写端口) 和三端口 (有两个读端口和一个写端口) 的存储模型:

# 5.5.2 Dynamic Random Access Memory(DRAM)
DRAM 使用电容和 nMOS 实现。从 SRAM 中读取数据会使得电容上的电压降低,所以再读取数据后,必须进行重写 / 刷新操作。 甚至就算不读取数据,电容的漏电也会使得电容的电压降低,所有每几毫秒就需要进行一次刷新。

# 5.5.3 Static Random Access Memory(SRAM)
SRAM 使用 cross-coupled inverters 实现,如果噪声使得存储的比特的电压降低,cross-coupled inverters 会重新存储。

# 5.5.4 Area and Delay
D 触发器,SRAM 和 DRAM 都可以用来作为存储介质,下面是他们的在晶体管数量上和延迟上的对比:
| Memory Type | Transistors per Bit Cell | Latency |
|---|---|---|
| flip-flops | ~20 | fast |
| SRAM | 6 | medium |
| DRAM | 1 | slow |
使用 D 触发器实现寄存器时需要消耗更多的晶体管,所以寄存器可以使用多端口的 SRAM 实现 (缺点是读取响应速度没有触发器实现的快),下面是两个读端口和一个写端口的 32 位寄存器:
- 两个寄存器的读和一个寄存器的写可以同时进行

# 5.5.6 Read Only Memory
ROM 的 bit cell 的结构图:
- 使用带 transistor 的电路来存储 0
- 使用不带 transistor 的电路存储 1
读取 ROM 的方法:
To read the cell, the bitline is weakly pulled HIGH. Then the wordline is turned ON. If the transistor is present, it pulls the bitline LOW. If it is absent, the bitline remains HIGH.

因为 ROM 是由组合逻辑电路组成的,不像 RAM 一样拥有存储电子的电路 (DRAM->capacitor, SRAM->corss-coupled inverter),所以 ROM 就不会存在掉电存储内容丢失的问题。
ROM 的表示方法: dot notation
- 交点上的 dot 表示为 1, 没有 dot 的表示为 0
- 下图是 Section 5.5.1 中 4*3 array 使用 ROM dot notation 的表示方法:

从概念上来说,ROM 可以由 Decoder 和 OR Gate 构成,可以将上图表示的 ROM 转化为 Decoder + OR 的形式:

但是在实际的生产中,ROM 的制造都不是选用 Decoder + OR 的形式,而是使用 transistor 直接组成 ROM,这样能减少使用晶体管的数量,具体在 [section 5.6.3] 中进行介绍。
ROM 的 bit cell 的结构图中介绍的 ROM 在生产时就需要决定是否使用 transistor,是真正的 Read Only Memory,一旦生产出来,就不能重新修改每个 bit cell 中存 0 还是存 1。但是现代使用的 ROM 都是可读可写的:
PROM(Programmable ROM)
在生产是不需要确定 bit cell 中存 0 还是存 1,在使用时可以通过熔断保险丝的方法进行烧录,只能被编程一次的存储器。EPROM(Erasable PROM)
EPROM 的存储位被预先烧录,但可以通过特殊的紫外线擦除器擦除。在编程时,需要将 EPROM 从电路板上取下,放入编程器中进行编程,然后再将 EPROM 重新安装到电路板上。EPROM 的优点是可以被多次编程和擦除,但缺点是编程和擦除需要将 EPROM 从电路板上取下,比较麻烦。EEPROM(Electrically EPROM)
是一种可以在电路板上直接编程和擦除的存储器。EEPROM 的存储位可以通过在电路板上施加电压来编程和擦除。现代计算机都使用 EEPROM。
EEPROM 和 FLASH 的区别:
- EEPROM 的每个 bit cells 都是可擦除的
- FLASH 只能擦出一个更大的 block,但是更便宜,因为只需要较少的擦除电路
# 5.5.7 Logic Using Memory Arrays
ROM 不仅可以用来作为存储介质,还可以将 ROM 作为 combinational logic function 使用:
比如在 Section 2.8.2 中,我们的 XNOR 会被综合工具编译为一个 ROM (具体细节可看书上)。
如果存储阵列 (Memory Arrays) 被作为组合逻辑函数使用,我们称存储阵列为 loopup tables (LUTs):
- 该 LUT 使用了
的存储阵列 - 该 LUT 实现了 Y=AB

提示
为什么要把作为组合逻辑函数使用的存储阵列叫做 LUT?
- 存储阵列的每一行输入地址组合对应了组合逻辑函数真值表的一行输入
- 存储阵列的每一行输出对应了组合逻辑函数真值表的一行输出
Using memory to perform logic, the user can look up the output value for a given input combination
# 5.5.8 Memory HDL
的 system verilog 表示
(读在任何时刻都可进行,写只有在时钟上升沿时才能进行写操作)
module ram #(parameter N = 6, M = 32)
(input logic clk,
input logic we, // write
input logic [N–1:0] adr, // address
input logic [M–1:0] din, // data input
output logic [M–1:0] dout); // data output
logic [M–1:0] mem [2**N–1:0]; // memory array: 2^N * M (N: row, M: column)
always_ff @(posedge clk)
if (we) mem [adr] <= din;
assign dout = mem[adr];
endmodule
2
3
4
5
6
7
8
9
10
11
12
13
14
的 system verilog 表示
module rom(input logic [1:0] adr,
output logic [2:0] dout):
always_comb
case(adr)
2'b00: dout <= 3'b011;
2'b01: dout <= 3'b110;
2'b10: dout <= 3'b100;
2'b11: dout <= 3'b010;
endcase
endmodule
2
3
4
5
6
7
8
9
10
# 5.6 Logic Arrays
本节介绍两种逻辑阵列 (Logic Arrays):
- PLA(Programmable Logic Arrays)
- FPGA(Field Programmable Gate Arrays)
PLA 和 FPGA 的区别:
- PLA 出现更早,PLA 上只能实现组合逻辑函数
- FPGA 可以实现组合逻辑和时序逻辑
# 5.6.1 PLA
PLA 由 two-level combinatinal logic 组成 SOP 形式,比如

笔记
ROM 是 PLA 的一种特殊形式,不同的是一个
# 5.6.2 FPGA
FPGA 可以实现组合逻辑电路和时序逻辑电路,现代的 FPGA 还添加了高速的 I/Os, 数模转化器,RAM array 和处理器等。
FPGA 的结构:
- FPGA 由可配置的 LE 阵列组成
- LE 阵列被称为称为 CLB (configurable logic blocks)
- LE 阵列周围被 IOE (input/output elements) 所包围,用来和外部进行交流
- LE 可以通过 IOE 和其他的 LE 进行连接
- LE 借助 IOE 从外界获取输入,然后通过 IOE 将输出传递到外界

# 5.? Edge Detectors
边沿检测器用于检测输入信号的上升沿或下降沿;边沿检测器可以分为正边沿检测器和负边沿检测器,具体取决于它们检测的是上升沿还是下降沿。边沿检测器的输出通常是一个脉冲信号,它的宽度很短,通常只有一个时钟周期的时间,用于触发后续的电路操作。
因为 D 触发器有一个特性,即输出信号相对于输入信号有滞后性,比如下图中假设初始时

verilog 描述:
module top_module (
input clk,
input I,
output DETECT
);
reg Q; // Delay of input signal
always@(posedge clk)
Q <= I; // nonblocking assignment
assign DETECT = I & (~Q);
endmodule
2
3
4
5
6
7
8
9
10
11
12
13
但是这个边沿检测器时有问题的,当
解决办法:使用两个 D 触发器分别延时一个周期 (reg0) 和两个周期 (reg1):

这样,当 D 发生变化时,reg0 比 D 延迟小于一个周期的时间变化,reg1 比 reg1 延迟一个周期变化,这样当 reg0 从 0->1 的一个周期内,reg1 还是 0,将这个状态作为判断上升沿的检测条件;
module top_module (
input clk,
input in,
output out
);
reg reg1; // Delay of input signal
reg reg2;
initial
begin
reg1 = 0;
reg2 = 0;
end
always@(posedge clk)
begin
reg1 <= in; // nonblocking assignment
reg2 <= reg1;
end
assign out = reg1 & (~reg2);
endmodule
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
同理当 reg0 从 1->0,reg1 还是 1 时可以作为下降沿的判断条件,波形图如下所示:

参考资料:
Verilog Interview Questions Part-13 Edge Detector (opens new window)
https://blog.csdn.net/weixin_45633643/article/details/108702774 (opens new window)
ps: 这是我第一次引用 CSDN 的链接,有点耻辱😳😕