跳转至

时序逻辑元件

本小节我们将带大家学习基础的时序逻辑元件。

1.1 锁存器与触发器

1.1.1 双稳态电路

我们首先观察下面的电路:

Image title

从电路结构可知,若 \(Q=0\),经过下面的非门取反,可得 \(\bar{Q}=1\),再经过上方的非门反馈到输入端,又保证了 \(Q=0\)。由于两个非门首尾相接的逻辑锁定,因而电路能自行保持在 \(Q=0,\bar{Q}=1\) 的状态,对应上方电路的输出结果。反之,若 \(Q=1,\bar{Q}=0\),则对应下方电路的输出结果。

简单概括,这个电路与之前介绍的组合逻辑电路有三个主要的不同

  • 没有输入端口。电路无法接收来自外界的输入信号,因而也无法改变自身的状态。

  • 有两种可能的状态。电路的结构是固定的,但是输出端有两种可能的输出结果。

  • 有反馈。两个非门的输出端口分别连接到了彼此的输入端口,即输出端会反馈影响到输入端。

像上面这样具有 0、1 两种逻辑状态,一旦进入其中一种状态,就能长期保持不变的单元电路,称为双稳态存储电路,简称双稳态电路。接下来所讨论的锁存器和触发器均属于双稳态电路。

★ 1.1.2 锁存器

双稳态电路是众多时序逻辑电路的基础。这是因为它可以存储一定的信息。如果为其增加控制单元以改变内部的内容,我们就得到了锁存器。锁存器(Latch)是一种对脉冲电平敏感的双稳态电路,它具有0和1两个稳定状态,一旦状态被确定,就能自行保持,直到有外部特定输入脉冲电平作用在电路一定位置时,才有可能改变状态。这种特性可以用于置入和存储1位二进制数据。

将双稳态电路的非门换成或非门,则构成下图所示的 RS 锁存器。它是一种具有最简单控制功能的双稳态电路。图中,S 和 R 是两个输入端,\(Q\)\(\bar{Q}\)是两个输出端。我们定义 \(Q=0\) 为锁存器的 0 状态,\(Q=1\) 为锁存器的 1 状态。

Image title

RS 锁存器

我们来分析一下电路的工作原理。

  1. 当 S=0,R=0 时,此时或非门就相当于非门,因为另一端输入为 0 不影响或非运算的结果。RS 锁存器将会保持其原本的状态不变,可以存储 1bit 二进制数据。

  2. 当 S=1,R=0 时,如下图所示,此时 S 端对应的或非门固定输出 0,即 \(\bar{Q}=0,Q=1\)。RS 锁存器处于 1 状态。我们称之为置位(Set)。

    Image title

  3. 当 S=0,R=1 时,如下图所示。此时 R 端对应的或非门固定输出 0,即 \(Q=0\)。RS 锁存器处于 0 状态。我们称之为复位(Reset)。

    Image title

  4. 当 S=1,R=1 时,如下图所示,此时 S 端和 R 端对应的或非门都固定输出 0,即 \(Q=0,\bar{Q}=0\)。此时锁存器处于非 0 非 1 的未定义状态,违背了 \(Q\)\(\bar{Q}\) 始终相反的设计初衷。

    Image title

Tips:未定义状态

当 RS 锁存器的 S 端和 R 端均为 1 时,电路处于未定义状态。此时如果 S 先变为 0,则相当于复位 Reset,若 R 先变为 0,则相当于置位 Set。若二者同时变为 0,则电路会根据或非门的延迟高低决定最终应当跳转到的状态,而这是我们无法预知的。为了保证 RS 锁存器始终处于有效的工作状态,我们一般约定 S 端和 R 端不同时为 1,即 \(S\cdot R\equiv 0\)

除了 RS 锁存器,我们还将学习另一种锁存器:D 锁存器。与 RS 锁存器不同,D 锁存器在工作中不存在非定义状态,因而得到广泛应用。

Image title

如上图所示,D 锁存器在 RS 锁存器的基础上,引入了两个与门和一个非门。除此之外,我们还引入了一个新的控制信号 C。

\(C=0\) 时,无论 D 端的输入是什么,与门的输出都为 0。此时相当于 RS 锁存器的输入为 \(S=0,R=0\),则 D 锁存器处于保持状态;当 \(C=1\) 时,我们便可以忽略与门。此时 RS 锁存器的输入为 \(S=D,R=\bar{D}\),显然有 \(S\cdot R=D\cdot\bar{D}\equiv0\)。因此 D 锁存器没有未定义状态,且内部存储的数值与 D 端的输入保持一致。

★ 1.1.3 触发器

D 锁存器看起来十分完美了,但当 D 端输入不是那么平滑,存在一定的「抖动」时,锁存器内部便会跟着进行状态抖动。换而言之,D 锁存器的稳定性较差。有没有什么办法能够使得其具有良好的稳定性呢?分析可知,D 锁存器在 \(C=1\) 的一段时间都可以进行更新,从而带来了不稳定性。如果我们能够限制 D 锁存器仅在很小的一段时间进行更新呢?假定 C 端信号仅在很小的一段时间内保持 1,其他时刻都为 0。此时信号的突变间隙大于高电平维持长度,因而无法将干扰结果写入锁存器。

然而,我们无法无限制地提升 C 端信号的变化频率,因此学者们换了一个思路:不是在高电平时写入,而是在低电平转换为高电平的瞬间写入。这就得到了触发器。

Image title

D 触发器

如上图所示,通过两个 D 锁存器级联,并加入一个非门,就得到了 D 触发器(D flip flop)。这里我们让控制信号以一定的周期进行高低电平翻转,类似于一个时钟 Clock 信号,因此记作 clk。电路的分析如下:

  1. clk 为低电平时,前一个锁存器处于更新状态,此时 D 端输入可以直接写入前一个锁存器。后一个锁存器处于保持状态,无论前一个锁存器输出如何,后一个锁存器均保持自身原先的数值不变。

  2. clk 为高电平时,前一个锁存器处于保存状态,此时 D 端输入无法写入前一个锁存器。后一个锁存器处于更新状态,将会写入前一个锁存器的值。这个时候毛刺信号均无法影响到后一个锁存器,因而增强了电路的稳定性。

通过非门,两个 D 锁存器的时钟存在一个 180° 的相位差(也就是是相差半个时钟周期),从而实现,只在时钟上升沿的时候读取输入并输出,其他时候输入的变化不会传导到输出端,去除了输入可能存在的毛刺,得到了稳定的输出。

★ 1.2 其他时序元件

下面是我们已经很熟悉的代码:

Counter.v
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
module Counter #(
    parameter   MAX_VALUE = 8'D100
)(
    input                   clk,
    input                   rst,
    output                  out
);

reg [7:0] counter;
always @(posedge clk) begin
    if (rst)
        counter <= 0;
    else begin
        if (counter >= MAX_VALUE)
            counter <= 0;
        else
            counter <= counter + 8'B1;
    end
end

assign out = (counter == MAX_VALUE) ? 1'B1 : 1'B0;
endmodule

我们知道,这段代码包含了一个基本的计数器,以我们设定的间隔输出高电平脉冲。

1.2.1 计数器

我们先前也提到过,always 语句可以使用 posedgenegedge 关键字指定电平变化的事件触发。下面是一个简单的自增计数器的 Verilog 代码实现:

example.v
1
2
3
4
5
6
7
8
module example3 (
    input                 clk,
    output reg [3:0]      q
);
always @(posedge clk) begin
    q <= q + 4'D1;
end
endmodule
提醒

变量 q 这个时候被综合成一个寄存器,而不是普通的导线。

这是一个时序逻辑单元,它应该被综合成一个计数器,每当时钟的上升沿,q 自增一。这段代码在 RTL 综合出的结果如下:

Image title

RTL 生成的计数器

如果我们引入同步复位信号,对应的代码变为

example.v
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
module example4 (
    input                clk, rst,
    output reg [3:0]     q
);
always @(posedge clk) begin
    if (rst)
        q <= 0;
    else
        q <= q + 4'D1;
end
endmodule

Verilog 可以自动识别,并将其连接到寄存器的复位端。

Image title

RTL 生成的同步复位计数器

1.2.2 寄存器堆

寄存器堆(Register File)是由多个可读写的寄存器组成的一种纯粹存储器件。

寄存器堆常用于 CPU 中作为计算机存储结构的最底层,它可以提高数据读取和写入的速度,从而加速 CPU 的运行效率。

以下是一个寄存器堆的 Verilog 代码实现:

RegFile.v
 1
 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
module RegFile (
  input                     clk,          // 时钟信号
  input         [1:0]       ra1,          // 读端口 1 地址
  input         [1:0]       ra2,          // 读端口 2 地址
  input         [1:0]       wa,           // 写端口地址
  input                     we,           // 写使能信号
  input         [31:0]      din,          // 写数据
  output        [31:0]      dout1,        // 读端口1数据输出
  output        [31:0]      dout2         // 读端口2数据输出
);

reg [31:0] reg_file [3:0]; // 4 个 32 位寄存器,规模为 4×32 bits

// 读端口 1
assign dout1 = reg_file[ra1];

// 读端口 2
assign dout2 = reg_file[ra2];

// 写端口
always @(posedge clk) begin
    if (we) begin
        reg_file[wa] <= din;
    end
end

endmodule
Tips

从以下电路综合图可以看到,vivado 把我们的较为规范的寄存器堆直接综合成了 RAM(Random Access Memory),这也印证了寄存器堆的重要性。

Image title
RTL 生成的寄存器堆


休息一会儿

本部分内容到此为止!你掌握了多少呢?

我们用一张图概括时序逻辑元件之间的关系:

Image title

参考资料


最后更新: November 5, 2023

评论