在学习数字电路之前,我们先要了解下什么是数字电路。想要搞明白数字电路,就要搞明白生活中有 两种概念, 数字信号和模拟信号,模拟信号一般包括压力、气温、速度等信号,模拟量的值是可能随时间 连续变化的。而数字量,是指从时间上变化都不是连续的信号, 比如电平信号。
下图分别是模拟信号和数字信号的示意图。
在 IC/FPGA 逻辑设计里面,一般只能处理数字信号, 当然,现在有一些高端的 FPGA,也集成了 AD 模块,也可以采集模拟量信号。
数字进制格式一般包括二进制、八进制、十进制和十六进制, 一般常用的为二进制、十进制和十六进 制。
Verilog 硬件描述语言是 IC/FPGA 开发的主流语言, Verilog 的进制格式是如下表示的:
二进制(BIN 格式)表示如下: 4’b0101 表示 4 位二进制数字 0101;
十进制(DEC 格式)表示如下: 4’d2 表示 4 位十进制数字 2(二进制 0010);
十六进制(HEX 格式)表示如下: 4’ha 表示 4 位十六进制数字 a(二进制 1010),十六进制的计数方式为 0 ,1 ,2…9 ,a ,b ,c ,d ,e ,f,最大计数为 f(f:十进制表示为 15)。当代码中没有指定数字的位宽与进 制时,默认为 32 位的十进制,比如 100,实际上表示的值为 32’d100。
这些进制是如何进行转换的。这个地方推荐两个方式, 第一个方 式是 Window 自带的计算器,另外一个方式是查表对应得到具体的数字。
进制转换表:
十进制 |
二进制 |
16进制 |
0 |
0000 |
0 |
1 |
0001 |
1 |
2 |
0010 |
2 |
3 |
0011 |
3 |
4 |
0100 |
4 |
5 |
0101 |
5 |
6 |
0110 |
6 |
7 |
0111 |
7 |
8 |
1000 |
8 |
9 |
1001 |
9 |
10 |
1010 |
A |
11 |
1011 |
B |
12 |
1100 |
C |
13 |
1101 |
D |
14 |
1110 |
E |
15 |
1111 |
F |
数字电路里面一般包括与门、或门、非门等门电路。另外为了简化逻辑, 实际电路里面还扩展了几个 其他的运算电路,他们是与非门、或非门、异或门和同或门。这些门电路是构成数字电路的基础。
逻辑门电路国际标准符号如下图所示:
通过表格我们可以得到每种门的符号, 以及输入输出对应关系, 还有函数表示方法。
数字电路一般包括组合逻辑和时序逻辑电路。
组合逻辑电路在逻辑功能上的特点是任意时刻的输出仅仅取决于该时刻的输入,与电路原来的状态无 关。组合逻辑电路没有记忆功能,没有反馈环路。下面章节我们会先从“与”、“或”和“非”门开始来学 习组合逻辑。
时序逻辑电路在逻辑功能上的特点是任意时刻的输出不仅取决于当时的输入信号, 而且还取决于电路 原来的状态,或者说,还与以前的输入有关。
硬件描述语言包括 VHDL 和 Verilog HDL 两种。
Verilog HDL(Hardware Description Language)是在用途最广泛的 C 语言的基础上发展起来的一种硬 件描述语言,具有灵活性高、易学易用等特点。Verilog HDL 可以在较短的时间内学习和掌握,目前已经 在 FPGA 开发/IC 设计领域占据绝对的领导地位。
在数字电路里面常用的逻辑门电路有与门、或门、非门等门电路。另外为了简化逻辑,实际电路里面 还扩展了几个其他的运算电路,其中有与非门、或非门、异或门和同或门,这些门电路是构成数字电路的 基本单元。
与门,英文名称是 AND Gate,又称“与电路 ”、逻辑“与 ”电路。与门是门电路里面比较重要的一个 逻辑门。
与门是执行“与 ”运算的基本门电路。与门有多个输入端, 1 个输出端。当多个输入端同时为“逻辑 1 ”高电平时,输出才为“逻辑 1 ”电平,否则输出为“逻辑 0 ”低电平。
与门的表达式是 F = A & B,“& ”代表与的意思,这个符号也是 Verilog 语法定义的与逻辑符。 与门的电路实现如下图所示:
根据上图的电路,可以得出“与”门的电路输入输出关系, 如下表所示:
输入 |
输出 |
|
A(v) |
B(v) |
Y(v) |
0 |
0 |
0 |
0 |
5 |
0 |
5 |
0 |
0 |
5 |
5 |
5 |
“与 ”门的逻辑符号如下图所示:
从与门的输入输出关系,可以列出与门的真值表如下所示:
输入 |
输出 |
|
A |
B |
Y |
0 |
0 |
0 |
0 |
1 |
0 |
1 |
0 |
0 |
1 |
1 |
1 |
从真值表可以看出,只有当输入 A 和 B 同时为 1 的时候,输出 Y 才为 1 。与门电路的规则可以整 理成一句口诀:“有 0 出 0,全 1 出 1。”
与门一般有多个输入端口,二输入端口的与门是最常见也是基本的数字电路单元。
由真值表可以看出, 当信号 A 和信号 B 全为 1 时,输出信号 Y 才为 1;当输入信号任意为 0 时, 输出 信号 Y 的值为 0。
通过上述分析,我们就可以根据真值表进行“与”门电路输入与输出波形的绘制。其波形如下图所 示:
1 module and_gate(
2 input A, //输入 A
3 input B, //输入 B
4
5 output Y //输出 Y
6 );
7
8 //assign 相当于一条连线,输入 A 和输入 B 相与后连接输出 Y。
9 assign Y = A & B;
10
11 endmodule
assign 语句相当于一条连线, 将表达式右边的电路直接通过 wire(线)连接到左边,左边信号必须是 wire 型。当右边变化了左边立马变化, 方便用来描述简单的组合逻辑。
编写 TB 文件
TestBench 用于验证功能模块的设计是否符合预期, 主要分为以下三个步骤:
向被测功能模块的输入接口添加激励;
对被测功能模块的顶层接口进行信号例化;
判断被测功能模块的输出是否满足设计预期。
设计的这个简单的与门模块的输入端口为 A 和 B,输出为 Y。所以仿真只需要对 A 和 B 进行激 励,就可以得到输出信号 Y 的仿真。
与门 TB 模块(tb_and_gate.v)代码编写如下:
1 `timescale 1ns / 1ns
2
3 module tb_and_gate();
4
5 //define reg
6 reg A;
7 reg B;
8
9 //define wire
10 wire Y;
11
12 initial begin
13 A = 1'b0;
14 B = 1'b0;
15 #100
16 A = 1'b0;
17 B = 1'b1;
18 #100
19 A = 1'b1;
20 B = 1'b0;
21 #100
22 A = 1'b1;
23 B = 1'b1;
24 end
25
26
27 and_gate and_gate_inst(
28 .A(A), //输入 A
29 .B(B), //输入 B
30
31 .Y(Y) //输出 Y
32 );
33
34 endmodule
程序中第 1 行代码定义的 1ns/1ns 表示仿真单位和仿真精度都是 1ns。
代码第 3 行定义了仿真模块名称。 需要注意, 通常 testbench 模块的端口列表中不列出端口。 在 initial 和 always 语句块中的变量要为 reg 型变量,输入的变量是需要我们手动在 initial 语句块中激励, 所以要声 明为 reg 型变量。而输出变量在 testbench 中一般都是为 wire 型变量。
在第 12 行到第 24 行使用了 initial 语句块进行输入信号的初始化,在初始化完成后需要对各输入端口 数据进行模拟。 上面的测试代码是先初始化 A 和 B 为 0,然后等 100ns 以后先让 A 为 0,B 为 1,然后再 等 100ns 以后让 A 为 1 ,B 为 0,最后再经过 100ns 以后让 A 和 B 同时为 1,看下 A 与上 B 的结果。
仿真验证:
接下来打开 Modelsim 软件对代码进行仿真,在运行仿真 1us 后,仿真的波形如下图所示:
从仿真波形可以看出,仿真的结果跟与门电路的真值表结果是一致的,说明我们使用 verilog 编写的与 门电路是正确的。
或门,英文名称是 OR Gate,又称“或电路 ”、逻辑“或 ”电路。或门也是门电路里面比较重要的一个 逻辑门。
或的含义是只有当决定一件事情的任意一个条件具备时,这个事件就会发生。
或门是执行“或 ”运算的基本门电路。或门有多个输入端, 1 个输出端。当多个输入端任意一个端口 为“逻辑 1 ”高电平时,输出就为“逻辑 1 ”电平, 只有全部输入条件都不满足时,或门输出为“逻辑 0 ” 低电平。或门的表达式是 F = A | B ,“| ”代表或的意思,这个符号也是 Verilog 语法定义的或逻辑符。
“或 ”门的电路实现如下图所示:
根据上图的电路,我们可以得出“或”门的电路输入输出关系, 如下表所示:
输入 |
输出 |
|
A(v) |
B(v) |
Y(v) |
5 |
0 |
5 |
0 |
5 |
5 |
0 |
0 |
0 |
5 |
5 |
5 |
或”门的逻辑符号,如下图所示:
从或门的输入输出关系,可以列出或门的真值表如下所示:
输入 |
输出 |
|
A |
B |
Y |
0 |
0 |
0 |
0 |
1 |
1 |
1 |
0 |
1 |
1 |
1 |
1 |
从真值表我们可以看出,只要输入 A 和输入 B 有一个为 1,输出 Y 就为 1 。或门电路的规则可以整理 成一句口诀:“有 1 出 1,全 0 出 0。”
或门一般有多个输入端口,二输入端口的或门是最常见也是基本的数字电路单元。
由真值表可以看出, 当信号 A 和信号 B 全为 1 时,输出信号 Y 才为 1;当输入信号任意为 0 时, 输出信号 Y 的值为 0。
通过上述分析,我们就可以根据真值表进行“或”门电路输入与输出波形的绘制。其波形如下图所示:
根据或门的电路图以及真值表,使用 Verilog 代码来描述一个或门的电路。
1 module or_gate(
2 input A, //输入 A
3 input B, //输入 B
4
5 output Y //输出 Y
6 );
7
8 //输入 A 或输入 B
9 assign Y = A | B;
10
11 endmodule
编写 TB 文件
或门的仿真代码完全可以使用此次与门实验的仿真代码进行验证,所以可以直接使用之前与门电路的仿真代码,只需要将例化的模块修改成或门的模块就可以了。
或门 TB 模块(tb_or_gate.v)代码编写如下:
1 `timescale 1ns / 1ns
2
3 module tb_or_gate ();
4
5 //define reg
6 reg A;
7 reg B;
8
9 //define wire
10 wire Y;
11
12 initial begin
13 A = 1'b0;
14 B = 1'b0;
15 #100
16 A = 1'b0;
17 B = 1'B1;
18 #100
19 A = 1'b1;
20 B = 1'b0;
21 #100
22 A = 1'b1;
23 B = 1'b1;
24 end
25
26
27 or_gate or_gate_inst(
28 .A(A), //输入 A
29 .B(B), //输入 B
30
31 .Y(Y) //输出 Y
32 );
33
34 endmodule
仿真验证:
接下来打开 Modelsim 软件对代码进行仿真,在运行仿真 1us 后,仿真的波形如下图所示:
从仿真波形可以看出,仿真的结果跟或门电路的真值表结果是一致的,说明我们使用 verilog 编写的或 门电路是正确的。
非门,英文名称是 NOT Gate,又称“非电路 ”、逻辑“非 ”电路。非门也是门电路里面比较重要的一 个逻辑门。
非的含义是取反的意思,比如一个事情要发生, 那么取反,就是这个事件不会发生。
非门是执行“非 ”运算的基本门电路。非门有 1 个输入端, 1 个输出端。当 1 个输入端端口为“逻辑 1 ”高电平时,输出就为“逻辑 0”低电平;当 1 个输入端端口为“逻辑 0 ”高电平时,输出就为“逻辑 1”高电平。
非门的表达式是 F = ~A ,“~ ”代表非的意思,这个符号也是 Verilog 语法定义的非逻辑符。
“非”门路电路实现如下图所示:
通过上图, 当 A 输入 0V 时候, 三极管截止, Y 输出 5V,当 A 输入 5V 时候,三极管导通, Y 输出 0V,由此我们可以得出“非门 ”的电路输入输出关系如所示:
输入 |
输出 |
A(v) |
Y(v) |
5 |
0 |
0 |
5 |
“非”门的逻辑符号,如下图所示:
从非门的输入输出关系,可以列出非门的真值表如下所示:
输入 |
输出 |
A |
Y |
0 |
1 |
1 |
0 |
从真值表我们可以看出,当输入 A 为 0 时, 输出 Y 为 1;当输入 A 为 1 时, 输出 Y 为 0。
非门可以跟“与”门和“或”门组成“与非”门以及“或非”门,也是基本的数字电路单元。
由真值表可以看出, 非门的输出等于输入取反的值; 当输入信号 A 为 0 时,输出信号 Y 才为 1;当输 入信号 A 为 1 时,输出信号 Y 的值为 0。
通过上述分析,我们就可以根据真值表进行“非”门电路输入与输出波形的绘制。其波形如下图所 示:
根据非门的电路图以及真值表,尝试使用 Verilog 代码来描述一个“非”门的电路。
1 module not_gate(
2 input [3:0] A, //4 位数据的输入信号 A
3
4 output [3:0] Y //4 位数据的输出信号 Y
5 );
6
7 //assign Y[3:0] = ~A[3:0];
8 assign Y = ~A;
9
10 endmodule
代码第 8 行直接使用了一个 assign 语句,将 A 非后的值赋给 Y。这边需要注意一下,如果输入端的位 数与输出端的位数相同,可以直接省略位数的声明(即代码第 8 行注释这种写法)。
编写 TB 文件
我们设计的这个简单的非门模块的 4 位输入的端口 A,输出为 4 位端口的 Y。所以仿真只需要对 A 进 行激励,就可以得到输出信号 Y 的仿真。
非门 TB 模块(tb_not_gate.v)代码编写如下:
1 `timescale 1ns / 1ns
2
3 module tb_not_gate();
4
5 //define reg
6 reg [3:0] A;
7
8 //define wire
9 wire [3:0] Y;
10
11 initial begin
12 A = 4'b0000;
13 #100
14 A = 4'b1111;
15 #100
16 A = 4'b1001;
17 #100
18 A = 4'b0110;
19 #100
20 A = 4'b0101;
21 #100
22 A = 4'b1010;
23 end
24
25
26 not_gate not_gate_inst(
27 .A(A), //输入 A
28
29 .Y(Y) //输出 Y
30 );
31
32 endmodule
在第 11 行到第 23 行使用了 initial 语句块进行输入信号的初始化,在初始化完成后需要对各输入端口 数据进行模拟。 上面的测试代码是先初始化 A 为 0,然后等 100ns 以后先让 A 为 4’b0000,然后再等 100ns 以后让 A 为 4’b1001,然后再经过 100ns 后 A 为 4’b0110,然后再经过 100ns 让 A 为 4’b0101 ,最后再经过 100ns 以后让 A 为 4’b1010。
仿真验证
接下来打开 Modelsim 软件对代码进行仿真,在运行仿真 1us 后,仿真的波形如下图所示:
多路选择器MUX(multiplexer)是一个多输入、单输出的组合逻辑电路, 一个 N 输入的多路选择器就 是一个 N 路的数字开关, 可以根据通道选择控制信号的不同,从 N 个输入中选取一个输出到公共的输出端。 多路选择器也是 FPGA 内部的一个基本资源, 主要用于内部信号的选通。简单的多路选择器还可以通 过级联生成更大的多路选择器。
如下图所示,开发板上的 4 个按键未按下时, 输出高电平,按下后,输出低电平。 图中的每个按键都 连接了一个 10K 电阻,起到限流的作用,以防止按键被按下时电源直接接地造成电路短路。
本节的实验任务是使用开发板来设计一个简单的 2 选 1 多路选择器, 主要功能是通过选通控制信号 sel 确定输入信号 in1 和 in2 哪一个信号作为输出。当选通控制信号 sel 为 1 时,输出为A 端信号;当选通控制 信号 sel 为 0 时, 信号输出为 B 端信号。
本次实验的功能是一个二选一的多路选择器,因此给模块命名为 mux2_ 1。模块中的输入为三个 1 位 的信号,其中 in1 和 in2 为两个输入信号,sel 为输入的控制信号,out 为单 bit 的输出信号。模块图如下图 所示:
根据实验任务所述, 可以列出二选一多路选择器的真值表,如下表所示:
输入(input) |
输出(output) |
||
in1 |
in2 |
sel |
out |
1 |
0 |
0 |
0 |
0 |
1 |
0 |
1 |
1 |
0 |
1 |
1 |
0 |
1 |
1 |
0 |
由模块图以及真值表可知,本次实验需要三个输入信号, 一个输出信号。输入端可以使用两个按键以 及一个复位按键,输出端可以使用一个 LED 灯进行表示。
由真值表可以看出, 当 sel 信号为 1 时,输出信号 out 等于输入信号 in1;当 sel 为 0 时,输出信号 out 等 于 in2。
通过上述分析,我们就可以根据真值表进行多路选择器输入与输出波形的绘制。其波形如下图所示:
实现二选一多路选择器功能的 Verilog 代码形式有很多种,这里选择三种不同的语法来实现代码的编 写。
assign 中条件运算符(三目运算符)实现方法:
1 module mux2_1(
2 input in1, //输入信号 in1
3 input in2, //输入信号 in2
4 input sel, //选择控制信号 sel
5
6 output out //输出信号 out
7 );
8
9 //*****************************************************
10 //** main code
11 //*****************************************************
12
13 //out:组合逻辑输出选择结果
14 //此处使用的是条件运算符(三目运算符) ,当括号里面的条件成立时
15 //执行"?"后面的结果;如果括号里面的条件不成立时,执行": "后面的结果
16 assign out = (sel == 1'b1) ? in1 : in2;
17
18 endmodule
always 语句块中使用 if-else 实现方法:
1 module mux2_1(
2 input in1, //输入信号 in1
3 input in2, //输入信号 in2
4 input sel, //选择控制信号 sel
5
6 output reg out //输出信号 out
7 );
8
9 //*****************************************************
10 //** main code
11 //*****************************************************
12
13 //out:组合逻辑输出选择结果
14 always@(*) begin //"*"为通配符,在这个模块中的任何一个输入信号或电平发生变化时
15 if(sel == 1'b1) //该语句下方的模块将被执行。
16 out <= in1;
17 else
18 out <= in2;
19 end
20
21 endmodule
always 语句块中使用 case 语句的方法:
1 module mux2_1(
2 input in1, //输入信号 in1
3 input in2, //输入信号 in2
4 input sel, //选择控制信号 sel
5
6 output reg out //输出信号 out
7 );
8
9 //*****************************************************
10 //** main code
11 //*****************************************************
12
13 //out:组合逻辑输出选择结果
14 always@(*) begin
15 case(sel)
16 1'b0 :
17 out <= in2;
18 1'b1 :
19 out <= in1;
20 //如果 sel 的情况没有全部列举出来一定要加 default
21 //因为此处 sel 只有两种情况,并且都列举了,所以 defalut 可以省略判断
22 default : ;
23 endcase
24 end
25
26 endmodule
通过上面三种不同编写代码的方式,我们可以了解 Verilog 语言完成一个基本模块的方法。从这些方法中可以看出 Verilog 语言跟 C 语言相似的地方也挺多,但是 verilog 代码最后是映射为具体硬件电路的, C 代码最后会编译为指令在 CPU 上面运行。代码的方式也是多种多样的,所以在以后看到别人的代码书写风 格也不必惊讶,我们只需要关心最后是否实现了硬件所需的功能就可以了。
编写 TB 文件
TestBench 是用于验证功能模块的设计是否符合预期,主要分为以下三个步骤:
向被测功能模块的输入接口添加激励;
对被测功能模块的顶层接口进行信号例化;
判断被测功能模块的输出是否满足设计预期。
二选一多路选择器模块的输入端口为 in1、in2 和 sel,输出 out 是根据控制选择信号的变化, 而选择输 出 in1 或者 in2 的。所以我们仿真只需要对 in1 、in2 和 sel 进行激励就可以得到输出信号 out 的仿真。
二选一多路选择器 TB 模块代码编写如下:
1 `timescale 1ns / 1ns
2
3 module tb_mux2_1();
4
5 reg in1;
6 reg in2;
7 reg sel;
8 wire out;
9
10 //initial 语句是不可以被综合的,一般只在 testbench 中表达而不在 RTL 代码中表达。 11 initial
12 begin //在仿真中 begin...end 块中的内容都是顺序执行的
13 in1 = 1'b0; //初始状态赋值
14 in2 = 1'b0;
15 sel = 1'b0;
16 #100 //经过 100ns 的延时
17 in1 = 1'b0; //100ns 时的输入值
18 in2 = 1'b0;
19 sel = 1'b1;
20 #100 //经过 200ns 的延时
21 in1 = 1'b0; //200ns 时的输入值
22 in2 = 1'b1;
23 sel = 1'b0;
24 #100 //经过 300ns 的延时
25 in1 = 1'b0; //300ns 时的输入值
26 in2 = 1'b1;
27 sel = 1'b1;
28 #100 //经过 400ns 的延时
29 in1 = 1'b1; //400ns 时的输入值
30 in2 = 1'b0;
31 sel = 1'b0;
32 #100 //经过 500ns 的延时
33 in1 = 1'b1; //500ns 时的输入值
34 in2 = 1'b0;
35 sel = 1'b1;
36 #100 //经过 600ns 的延时
37 in1 = 1'b1; //600ns 时的输入值
38 in2 = 1'b1;
39 sel = 1'b1;
40 end
41
42 mux2_1 u_mux2_1(
43 .in1(in1), //输入信号 in1
44 .in2(in2), //输入信号 in2
45 .sel(sel), //选择控制信号 sel
46
47 .out(out) //输出信号 out
48 );
49
50 endmodule
程序中第一行代码定义的 1ns / 1ns 表示仿真单位和仿真精度都是 1ns。
在 testbench 中端口列表为空,所以第三行模块为空, 而在 initial 和 always 语句块中的变量一定是为 reg 型变量, 输入的变量是需要我们手动激励的,所以一定要为 reg 型变量。而输出变量在 testbench 中一 般都是为 wire 型变量。
在第十一行到第四十行使用了 initial 语句块,第十三行到第十五行首先进行对各输入信号的初始化, 在初始化完成后需要对各输入端口数据进行模拟。然后每 100ns 对各输入端口进行赋值,产生的数据完成 对输入端口的激励。
在完成信号的激励后,我们就可以进行测试模块的实例化,其中 mux2_ 1 为被实例化模块的名字, u_mux2_ 1 为我们实例化之后定义的名字。
仿真验证:
接下来打开 Modelsim 软件对代码进行仿真 ,在 运行仿真 1us 后,仿真的波形如 下图所示:
因为 3-8 译码器的上板验证需要用到 8 个 led 灯, 而开发板的 led 灯数目不够, 所以 3-8 译码器我们只 进行仿真验证, 而不再进行上板测试。
本节的实验任务是按照我们开发的流程设计一个 3-8 译码器并进行仿真验证。
在了解我们的实验任务后,按照我们的开发流程首先是进行模块的绘制。 3-8 译码器是通过输入 3 个 信号组成二进制的 8 种情况来控制对应输出 8bit 的 8 种不同状态。根据上面的分析设计出的 Visio 框 图所示:
模块端口与功能描述如下表所示:
信号名 |
位宽 |
方向 |
端口说明 |
A |
1 |
输入 |
输入信号 A |
B |
1 |
输入 |
输入信号 B |
C |
1 |
输入 |
输入信号 C |
out |
8 |
输出 |
译码后的输出信号 out |
框图结构设计完毕后我们就可以通过波形图的方式来描述输入和输出之间具体的关系。 首先输入为 3 个 1bit 信号, 其任意二进制组合有 8 种情况,每种组合与 out 输出 8bit 的 8 种状态一一对应, 实现由 3 种 输入控制对应的 8 种输出的译码效果。根据上面的分析我们可以先列出真值表,然后再根据真值表的输入 与输出的对应关系画波形图。
输入(input) |
输出(output) |
||
A |
B |
C |
out |
0 |
0 |
0 |
0000_0001 |
0 |
0 |
1 |
0000_0010 |
0 |
1 |
0 |
0000_0100 |
0 |
1 |
1 |
0000_ 1000 |
1 |
0 |
0 |
0001_0000 |
1 |
0 |
1 |
0010_0000 |
1 |
1 |
0 |
0100_0000 |
1 |
1 |
1 |
1000_0000 |
根据 38 译码器的真值表,我们可以绘制出 38 译码器模块的波形图如下图所示:
按照我们绘制的波形图,可以进行 Verilog 代码的编写。当然,通过波形图来编写代码实现 38 译码器 功能的方法有很多种。比如可以使用我们上一章所学习的 case 语句或者 if 语句,下面我们使用 case 语句来 进行代码的编写。
1 module decoder3_8(
2 input A,
3 input B,
4 input C,
5
6 output reg [7:0] out
7 );
8
9 //define wire
10 wire [2:0] sel;
11
12 //使用 assign 语句进行位拼接
13 assign sel = {A, B, C}; //使用"{}"位拼接符将 3 个 1bit 数据按照顺序拼成一个 3bit 数据
14
15 //out:根据输入的 3bit sel 信号选择输出对应的 8bit out 信号
16 always@(*) begin
17 case(sel)
18 3'b000 : out = 8'b0000_0001;
19 3'b001 : out = 8'b0000_0010;
20 3'b010 : out = 8'b0000_0100;
21 3'b011 : out = 8'b0000_1000;
22 3'b100 : out = 8'b0001_0000;
23 3'b101 : out = 8'b0010_0000;
24 3'b110 : out = 8'b0100_0000;
25 3'b111 : out = 8'b1000_0000;
26 default : ;
27 endcase
28 end
29
30 endmodule
程序中第 10 行定义了一个计数器 3bit 的 sel 选择位, 第 13 行使用 assign 语句将三个 1bit 的输入通过 位拼接,拼接成一个 3bit 的数据赋值给 sel。
代码第 16 行到第 28 行使用 case 语句将输入与输出的 8 中译码关系全部列举出来, 所以第 26 行
default 可以省略不写。但为了代码的规范性以及一个编写代码的良好习惯,所以我们默认加上 default,可 以指定任意一种情况,也可以省略为空。
编写 TB 文件
38 译码器模块的输入为 A、B 、C 三个 1bit 的输入端口,输出为一个 8bit 的输出端口。输出的数据是 根据三个输入的变化产生相对应的数据,所以 TB 模块我们只需要产生三个输入端口的状态,就可以仿真 出相对应的输出情况。
38 译码器 TB 模块(tb_decoder 3 8.v)代码编写如下:
1 `timescale 1ns / 1ns
2
3 module tb_decoder_3_8();
4
5 //define reg
6 reg A;
7 reg B;
8 reg C;
9
10 //define wire
11 wire [7:0] out;
12
13 initial begin
14 A = 0; B = 0; C = 0;
15 #200
16 A = 0; B = 0; C = 1;
17 #200
18 A = 0; B = 1; C = 0;
19 #200
20 A = 0; B = 1; C = 1;
21 #200
22 A = 1; B = 0; C = 0;
23 #200
24 A = 1; B = 0; C = 1;
25 #200
26 A = 1; B = 1; C = 0;
27 #200
28 A = 1; B = 1; C= 1;
29 #200
30 A = 0; B = 0; C= 0;
31 end
32
33 decoder3_8 decoder3_8_inst(
34 .A (A),
35 .B (B),
36 .C (C),
37
38 .out(out)
39 );
40
41 endmodule
程序中第一行代码定义的仿真单位和仿真精度都是 1ns,代码第 13 行到 31 行我们使用了一个 initial 语 句块,先对三个输入进行初始化的清零,然后每隔 200ns 对三个输入分别进行赋值。
仿真验证:
接下来打开 Modelsim 软件对代码进行仿真,在运行仿真 10us 后,仿真的波形如下图所示:
从仿真波形中, 波形中 3 个输入信号 in 与输出信号 out 之间的对应关系和编写的代码中的译码关系是 完全一致的,完全符合我们代码中的逻辑设计。
从 D 锁存器的电路图中我们可以看出,该电路主要是由两个部分组成,第一个部分是由 G1、G2 两个 与非门组成的 RS 锁存器, 第二个部分是由 G3 、G4 两个与非门组成的控制电路。 C 为控制信号,用来控 制 G3 和 G4 的激励输入。
下面来分析下 D 锁存器的工作原理,当控制信号 C=0 时, 根据与非门的逻辑定律,无论 D 输入 什么信号, RD 和 SD 信号同时为 1。根据由与非门组成的 RS 锁存器的逻辑定律,RD 和 SD 都同时等于 1 的话,锁存器的输出端 Q 将维持原状态不变。那么, 当控制端 C=1 时,如果此时 D=0 ,SD 就等于 1 ,RD 就等于 0,根据 RS 锁存器的逻辑规律,电路的结果就为 0 状态;如果 D =1,那么 RD 就等于 1,SD 也就 等于 0,锁存器的结果就为 1 状态,也就是说, 此时锁存器的状态是由激励输入端 D 来确定的, 并且 D 等 于什么,锁存器的状态就是什么,这就是我们前面所说的,将单路数据 D 存入到锁存器中。
根据上面的描述,我们可以推出 D 锁存器的特性表, Qn 是指触发器当前逻辑状态也即触发前的状 态, Qn+1 是指触发后的状态。
C |
D |
Qn |
Qn+1 |
0 |
X |
0 |
0 |
0 |
X |
1 |
1 |
1 |
0 |
0 |
0 |
1 |
0 |
1 |
0 |
1 |
1 |
0 |
1 |
1 |
1 |
1 |
1 |
通过这个表格,我们可以看出,当 C 为 1 时, D 的状态和 Qn+1 的状态完全一样,当 D=0 时, Qn+1=0,当 D=1 时, Qn+1=1。
我们还可以进一步画出 D 锁存器的波形图。
从 D 锁存器的波形图图中我们可以看出, D 是锁存器的输入信号,C 是锁存器的控制信号, Q 是锁存 器的输出信号,当控制信号 C 为高电平时,输出信号 Q 将跟随输入信号 D 的变化而变化,大家看虚线内, Q 的波形等于 D 的波形。当控制信号 C 从高电平变为低电平时,输入信号 D 的状态将会决定锁存器将 要锁存的状态。大家可以看到,C 由高变低的那两条虚线内,所对应的输入信号 D 为低电平,那么输出信 号 Q 也将会锁存低电平。最后面的那两条虚线也同理,D 为高电平, Q 锁存高电平。
D 锁存器的介绍就到这里, 下面我们来从实际的逻辑设计里面看下锁存器的坏处。
在绝大多数设计中我们要避免产生锁存器。它会让你设计的时序出问题, 并且它的隐蔽性很强,新人 很难查出问题。锁存器最大的危害在于对电平信号敏感,容易产生毛刺和影响工具进行时序分析,这对于 下一级电路是极其危险的。所以,只要能用触发器的地方,就不用锁存器。
由上图示意图我们可以看到, 锁存器没有时钟信号, 只有数据输入和使能以及输出 q 端, 没有时钟信 号也就说明我们没有办法对这种器件进行时序分析, 这个在时序电路里面是非常危险的行为,因为可能引 起时序不满足从而导致电路功能实现有问题。
本节的实验任务是使用 Verilog 代码设计一个锁存器电路。
在前几章中我们也提到了 latch,在代码里面出现 latch 的两个原因:在组合逻辑中,if 或者 case 语句 不完整的描述,比如 if 缺少 else 分支, case 缺少 default 分支,导致代码在综合过程中出现了 latch。解决办 法就是 if 必须带 else 分支, case 必须带 default 分支。
大家需要注意下,只有不带时钟的 always 语句的 if 或者 case 语句不完整才会产生 latch,带时钟的语 句 if 或者 case 语句不完整描述不会产生 latch。下面我们来设计一个不带锁存器的电路,我们先来写一个 不带 else 的 always 语句。
根据程序设计的思路,我们来设计一个 if 语句但缺少 else 分支的锁存器代码(latch.v),代码编写如下:
1 module latch(
2 input a,
3 input b,
4
5 output reg y
6 );
7
8 always@(*) begin
9 if(a == 1)
10 y = b;
11 end
12
13 endmodule
程序中第 8 行的一个组合逻辑电路使用了 if 语句,但是这个语句没有 else 分支,我们可以使用 Vivado 查看 RTL 视图。
从 RTL ANALSIS 视图中可以看出,模块下面显示了 RTL_LATCH,可以看出这个电路就是 latch。
下面我们把 else 补充完整再来看下电路结构,代码如下:
1 module latch(
2 input a,
3 input b,
4
5 output reg y
6 );
7
8 always@(*) begin
9 if(a == 1)
10 y = b;
11 else
12 y = 0;
13 end
14
15 endmodule
这个代码中,添加了 else 分支, 我们可以使用 RTL ANALSIS 视图再看一下综合的电路结构。
上图所示的电路结构是一个 mux 选择电路,可以看出,加了 else 分支的电路就不会有 latch 电路。 下面我们来写一个不带 default 的 case 语句, 代码如下:
1 module latch(
2 input a,
3 input b,
4
5 output reg y
6 );
7
8 always@(*) begin
9 case (a)
10 0: y = b;
11 endcase
12 end
13
14 endmodule
从 RTL ANALSIS 视图中可以看出,模块下面显示了 RTL_LATCH,可以看出这个电路就是 latch。 下面我们把 case 语句的 default 补充完整再来看下电路结构,代码如下:
1 module latch(
2 input a,
3 input b,
4
5 output reg y
6 );
7
8 always@(*) begin
9 case (a)
10 0: y = b;
11 default : y = 0;
12 endcase
13 end
14
15 endmodule
可以看出, 这个语句有 case 的 default 分支, 我们使用 vivado 的 RTL ANALSIS 来看下综合的电路结构。
上图所示的电路结构是一个 mux 选择电路,可以看出,加了 case 的 default 分支的电路就不会有 latch 电路。
寄存器一般是由多个触发器构成的,所以在学习寄存器之前,我们先来了解一下触发器。
从 D 触发器的电路图中我们可以看出,该电路是由两个相同的 D 锁存器以及两个非门连接而成的,图 中的 F1 和 F2 就是 D 锁存器的电路符号, F1 为主锁存器, F2 为从锁存器, 由于主锁存器的输出信号 Q0 就 是从锁存器的输入信号,因而造成了两个锁存器的主从关系,这两个锁存器的控制信号都由外部时钟信号 CLK 提供。
首先当 CLK=0 时, CLK 经过非门后直接作为 F1 的控制信号,那么此时 F1 的控制信号为 1 ,F1 被选 通,F1 处于工作状态。如果现在输入信号 D 为 1 的话,它经过 F1 ,F1 的输出 Q0 就为 1,这里的 Q0 不仅 是 F1 的输出信号,同时也是 F2 的输入信号。不过现在 F2 的控制信号为 0 ,因此 F2 现在被锁存了,处于 保持状态。输入信号 D 没有办法直接改变输出 Q1 的状态,这是前半拍的工作情况,也就是说,输入信号 先存入主锁存器中, 但不直接影响输出 Q1 的状态。
接下来我们再来看后半拍, 当外部的控制信号 CLK 由 0 变为 1 时,经过第一个作为 F1 控制信号的非 门后,此时 F1 的控制信号将变为 0 。主锁存器 F1 就被封锁了, 它的输出 Q0 将保持在当前的状态,即使 现在输入信号 D 再发生改变, Q0 的值也不再受影响了。 经过两个非门后,F2 的控制信号 CLK 此时为 1, 那么 F2 将处于工作状态。Q0 将会作为 F2 这个从锁存器的输入信号,直接影响到输出信号 Q1 的状态。 当 Q0 为 1 时,那么根据 D 锁存器的逻辑规律,输出的 Q1 就为 1,F2 的 Q 非为 0,这就是后半拍的工作情况。在后半拍中我们才能实现整个电路状态的改变, 因此从上面的分析中我们就可以看出, 在 CLK 信号 由 0 变为 1 这样的一个变化周期内, 触发器的输出状态只可能改变一次。
在了解完触发器之后,我们再来看一下寄存器。 一个触发器可以组成一个一位的寄存器(一个触发器 其实可以看作一个寄存器),多个触发器可以组成一个多位的寄存器,多位的寄存器可以存储多 bit 的二进 制数据。
如果多个触发器组成的寄存器输入都是相同信号,那么寄存器的输出也都是相同的信号,这种属于触 发器并联。如果多个触发器组成的寄存器输入是互相传递的(下一个触发器的输入是上一个触发器的输出),那么寄存器的输出也都是不相同的信号, 这种属于触发器串联。
下面我们先来看下触发器的并联,我们以 2bit 的寄存器为例子说明,如下图所示:
通过上图, 我们可以看到,每个触发器的输入都是 D0 ,F1 触发器的 D0 经过时钟上升沿采样后输出是 Q0 ,F2 触发器的 D0 经过时钟上升沿采样后输出是 Q1 。由于两个触发器的 D0 是来自同一个信号,所以每 个触发器的 Q 端输出也是相同的。
下面我们来看下这并联下的 2bit 寄存器的状态表格。
时钟 |
输入 |
输出 |
说明 |
CLK |
D0 |
Q0 、 Q1 |
/ |
上升沿 |
0 |
0 、 0 |
数据采样 |
上升沿 |
1 |
1 、 1 |
数据采样 |
下降沿 |
0 |
保持 |
数据保持 |
下降沿 |
1 |
保持 |
数据保持 |
1 |
0/1 |
不变 |
数据锁存 |
0 |
0/1 |
不变 |
数据锁存 |
由上表格我们可以看出,只有时钟上升沿,数据采样变化,其他时候数据是保持锁存状态。
看完触发器的并联后,接下来我们再来看下触发器的串联,我们还是以 2bit 寄存器为例子说明, 如下图所示:
通过上图, 我们可以看到,第一个触发器的输入是 D0 ,F1 触发器的 D0 经过时钟上升沿采样后输出是 Q0 ,输出的 Q0 为 F2 的输入。 由于 F2 的输入来自于上一个触发器的输出 Q0,因此 F2 的输出比 F1 的输出 要晚一个时钟周期, 这个地方也就是通常所说的延迟一拍的概念,在逻辑电路设计里面,俗称“打一拍”,或者寄存一拍。
下面我们来看下这串联下的 2bit 寄存器的状态表格:
时钟 |
F1输入 |
F1输出/F2输入 |
F2输出 |
说明 |
CLK |
D0 |
Q0 |
Q1 |
/ |
上升沿 |
0 |
0 |
0 |
数据采样 |
上升沿 |
1 |
1 |
0 |
数据采样 |
上升沿 |
1 |
1 |
1 |
下一个上升沿 |
1 |
0/1 |
不变 |
不变 |
数据锁存 |
0 |
0/1 |
不变 |
不变 |
数据锁存 |
由寄存器的状态表可以看出, 当 F1 的输入为 1 时,在时钟上升沿采样后,F1 的输出是 1,此时 F2 的 输出还是 0,当下一个上升沿过去之后,F2 的输出才为 1 。同理,如果更多的触发器依次串联,其实完成 的是一个多级打拍的功能,也是逻辑电路里面移位的功能,就是一个信号在时钟上升沿跳变的时候依次传 递到下一个寄存器中。
本节的实验任务是设计一个 4 级串联寄存器电路。
根据实验任务可知, 我们需要设计一个 4 级串联寄存器的电路模块。它的输入端口为 a,输出端口为 y,在输入到输出之间使用 4 个触发器进行串联。而每个触发器都是需要时钟信号以及复位信号,所以我 们还需要一个时钟端口以及一个复位端口。
模块端口与功能描述如下表所示:
信号名 |
位宽 |
方向 |
端口说明 |
sys_clk |
1 |
输入 |
系统时钟,50MHz |
sys_rst_n |
1 |
输入 |
系统复位按键,低电平有效 |
a |
1 |
输入 |
输入信号a |
y |
1 |
输出 |
输出信号y |
根据模块框图以及端口功能的描述列表可以画出该模块的波形, 波形如下图所示:
从绘制的波形图我们可以看出,因为是时序逻辑电路,所以输入 a 的值会延迟一个时钟周期赋值给 a_reg1。以此类推,每个寄存器的值都会延迟一个时钟周期赋值给下一个寄存器。
4 级的串联寄存器需要 4 个寄存器,每个寄存器都是需要有时钟,复位信号,输入和输出, 根据波形 图我们可以写出如下代码:
1 module shfit_reg(
2 input sys_clk, //50M 系统时钟
3 input sys_rst_n, //全局复位
4 input a, //输入 a
5
6 output y //输出 y
7 );
8
9 reg a_reg1; //寄存器 1
10 reg a_reg2; //寄存器 2
11 reg a_reg3; //寄存器 3
12 reg a_reg4; //寄存器 4
13
14 always@(posedge sys_clk or negedge sys_rst_n) begin
15 if(!sys_rst_n)begin
16 a_reg1 <= 1'b0;
17 a_reg2 <= 1'b0;
18 a_reg3 <= 1'b0;
19 a_reg4 <= 1'b0;
20 end
21 else begin
22 a_reg1 <= a;
23 a_reg2 <= a_reg1;
24 a_reg3 <= a_reg2;
25 a_reg4 <= a_reg3;
26 end
27 end
28
29 assign y = a_reg4;
30
31 endmodule
程序中第 9 行到第 12 行定义了 4 个寄存器,代码第 14 行到第 27 行将 4 个寄存器串联起来,第 29 行 将 a_reg4 的值赋给输出 y。
我们可以使用 VIVADO 的 RTL ANALYSlS 来看一下 RTL 电路。
从上图可以看出, 每个触发器都有 sys_clk 输入信号端口、 一个异步清零端口、 一个输出端口以及一 个输出端口。我们代码中的输入 a 通过 a_reg1 、a_reg2 、a_reg3 以及 a_reg4 的串联输出到 y,从图中可以 看出这个电路就是一个 4 级寄存器组成的移位寄存器。
编写 TB 文件
下面我们编写一个 testbech 激励文件,通过仿真来看下移位寄存器的波形。仿真代码如下:
1 module tb_shfit_reg();
2
3 reg sys_clk;
4 reg sys_rst_n;
5 reg a;
6
7 wire y;
8
9 initial begin
10 sys_clk = 1'b1;
11 sys_rst_n <= 1'b0;
12 a <= 1'b0;
13 #201
14 sys_rst_n <= 1'b1;
15 #100
16 a <= 1'b1;
17 #100
18 a <= 1'b0;
19 end
20
21 always #10 sys_clk <= ~sys_clk;
22
23 shfit_reg shfit_reg_inst(
24 .sys_clk (sys_clk ),
25 .sys_rst_n (sys_rst_n ),
26 .a (a ),
27
28 .y (y )
29 );
30
31 endmodule
编写完我们的仿真代码,就可以对仿真代码进行仿真的验证了。
仿真验证:
我们打开 Modelsim 软件对代码进行仿真, 在运行仿真 1us 后, 仿真的波形如下图所示:
由上图可知,当输入信号 a 为 1 之后,在下一个时钟上升沿到来时,a_reg1 拉高,经过一个时钟后,a_reg2 拉高,再经过一个时钟后, a_reg3 拉高,在 a 拉高之后的第 4 个时钟,a_reg4 也由低变为了高,然 后 a_reg4 的值是通过组合逻辑直接赋值给了 y 信号,因此 y 的波形和 a_reg4 也是完全一样的。可以看出触 发器的采样都是在时钟上升沿进行的, 其他时候触发器是保持之前采样的信号。另外从波形中,我们也能 看到一个移位的效果,就是数据 1 依次移动到下一个触发器中。
这个 3 位计数器的电路图相对之前我们学习的逻辑有点复杂,我们以 F1 、F2 、F3 代表 3 个 D 触发器,两个异或门 G1、G2 和 1 个与门 G3 来进行说明计数器是如何工作的。一般触发器是有复位信号(图 中没有画出),在上电复位之后,电路上电的 Q0 Q1 Q2 初始状态是 000,下面我们来看下这三个 D 触发器 此时的输入信号是什么。
我们先看 F1 ,F1 的输入信号 D,是由 Q0 反馈回来的,我们已知此时的 Q 是 0,那么 Q 就是 1,这个 1 反馈给 F1 输入信号 D,此时 F1 的输入信号就是 1。
下面我们再来看 F2 ,F2 的输入信号是 Q0 和 Q1 的值经过一个异或门之后得到的, 我们已知此时的 Q0 是 0 ,Q1 也是 0,那么这两个 0 经过异或门逻辑处理(异或门两个输入信号值相同输出 0,不同输出 1)后 就是 0 了, 因此 F2 的输入信号也是 0。
再看 F3 ,F3 的输入信号是由 Q0 和 Q1 经过一个与门之后的输出值,与 Q2 的值相异或得到的,我们 来看, Q0 和 Q1 都是 0,两个 0 相与, 输出肯定还是 0,这个输出的 0,再与 Q2 的值相异或,根据异或门 的规律,两个输入现在都是 0,那么异或门的输出也是 0 ,F3 的输入此时就是 0。
到这里,我们已经分析出了 F1 、F2 和 F3 这三个触发器此时的输入值了, 下面我们就可以根据 D 触发 器的逻辑规律知道下一刻电路的输出值了,现在我们给 CLK 端口一个上升沿,也就是 CLK 由 0 变为 1了,那么 3 个边沿 D 触发器将会同时触发, 当 CLK 这个时钟信号的上升沿到来时,D 触发器的输入值将 会被锁存, 根据逻辑规律,下一刻 3 个 D 触发器的输出值就分别为 1 ,0 ,0。这里如果我们把 Q2 的值当 做二进制数的最高位,把 Q0 的值当做二进制数的最低位,那么现在计数器所输出的值,就是二进制数001,也就是十进制的 1。计数器接收到第一个时钟信号的上升沿后,计数器就输出二进制数 001,依次类 推,如果第二个时钟信号的上升沿到来时,这个时候计数器将会输出二进制数 010,也就是十进制数 2,
每当电路多到来一个时钟上升沿,计数器就会作加 1 运算。当电路计到第 8 个脉冲时,电路状态将由 111 又变为 000,完成一个循环周期,所以该电路也称为模 8 同步加法计数器。所谓同步就是指该电路中的四 个边沿型 D 触发器共用一个时钟脉冲 CLK,当时钟上升沿到来时,它们能够同时触发。
讲到这里, 大家应该已经了解了计数器的工作原理了,下面我们根据上面的分析, 画出 3 位加法计数器的特性表,如下图所示。
时钟 CLK |
当前 Q2 Q1 Q0 |
下一刻 Q2 Q1 Q0 |
0 |
0 0 0 |
0 0 1 |
1 |
0 0 1 |
0 1 0 |
2 |
0 1 0 |
0 1 1 |
3 |
0 1 1 |
1 0 0 |
4 |
1 0 0 |
1 0 1 |
5 |
1 0 1 |
1 1 0 |
6 |
1 1 0 |
1 1 1 |
7 |
1 1 1 |
0 0 0 |
由计数器真值表可以看出,每个时钟上升沿之后,计数器的值就加 1,当经过 8 个时钟周期之后,计 数器计数从 111 溢出到 000,然后依次循环往复的计数。
本节的实验任务是使用 Verilog 语言设计一个 3bit 的计数器电路。
本次实验是设计一个 3bit 的计数器,所以我们将模块名命名为 counter。因为是时序逻辑电路,所以输 入肯定有时钟和复位,输出为一个 3bit 的计数器, 模块框图如下图所示:
模块端口与功能描述如下表所示:
信号名 |
位宽 |
方向 |
端口说明 |
sys_clk |
1 |
输入 |
系统时钟,50MHz |
sys_rst_n |
1 |
输入 |
系统复位按键,低电平有效 |
cnt |
3 |
输出 |
3bit的计数器 |
根据计数器的电路图、真值表以及模块框图,我们可以知道每当一个周期的到来时,寄存器就会累加 1。由此绘制出计数器模块的波形图如下图所示:
由上图可知,cnt循环从0计数到7,每当计数器计数到最大值时,就会从零开始重新累加。
根据上一小节的波形图,可以使用 Verilog 编写一个计数器(counter.v)代码,代码如下:
1 module counter(
2 input sys_clk,
3 input sys_rst_n,
4
5 output reg [2:0] cnt
6 );
7
8 always@(posedge sys_clk or negedge sys_rst_n) begin
9 if(!sys_rst_n)
10 cnt <= 3'd0;
11 else
12 cnt <= cnt + 3'd1;
13 end
14
15 endmodule
程序中第 8 行定义了一个 3bit 的 cnt,当 cnt 计数到 3bit 的最大值之后进行清零,然后进行循环的累加 操作。
编写 TB 文件
计数器模块的输入端口只有时钟和复位,因此计数器 TB 模块(tb_counter.v)只需要对时钟以及复位 信号进行激励即可, 仿真代码编写如下:
1 `timescale 1ns / 1ns //仿真单位/仿真精度
2
3 module tb_counter();
4
5 reg sys_clk;
6 reg sys_rst_n;
7
8 wire [2:0] cnt;
9
10 initial begin
11 sys_clk = 1'b1;
12 sys_rst_n <= 1'b0;
13 #201
14 sys_rst_n <= 1'b1;
15 end
16
17 always #10 sys_clk <= ~sys_clk ;
18
19 counter counter_inst(
20 .sys_clk (sys_clk),
21 .sys_rst_n (sys_rst_n),
22
23 .cnt (cnt)
24
25 );
26
27 endmodule
仿真验证
接下来打开 Modelsim 软件对代码进行仿真,在运行仿真 10us 后,仿真的波形如下图所示:
由上图可以看出,仿真波形与我们绘制的波形图是一致的,说明我们本次设计的3bit计数器代码是没 有问题的。
如上图所示,a 信号是一个持续 2 个时钟周期的信号,如果想得到 a 信号的上升沿也就是 a_posedge 信 号,大家想下,我们应该怎么做呢? a_posedge 信号是一个持续 1 个时钟周期的脉冲信号,那么如果我们把 a 信号寄存一拍,得到 a_dly 信号。我们再用 a 信号和 a_dly 取反再相与,是不是就可以得到一个持续 1 个 时钟周期的脉冲信号呢,这个 a_posedge 信号就是 a 的上升沿指示信号。看完上升沿检测电路后,接下来 我们再来看一下下降沿检测电路。
如上图所示,a 信号是一个持续 2 个时钟周期的信号,如果想得到 a 信号的下降沿也就是 a_negedge 信 号,大家再想下,我们应该怎么做呢? 首先 a_negedge 信号是一个持续 1 个时钟周期的脉冲信号,那么如 果我们把 a 信号寄存一拍,得到 a_dly 信号,我们再把 a 信号取反, 用 a 取反的信号和 a_dly 相与,是不是 就可以得到一个持续 1 个时钟周期的脉冲信号呢,这个 a_negedge 信号就是 a 的下降沿指示信号。看完下 降沿检测电路后,接下我们再来看下双沿检测电路。
如上图所示,a 信号是一个持续 2 个时钟周期的信号,如果想得到 a 信号的上升沿和下降沿也就是a_edge 信号,大家想下, 我们应该怎么做呢?从图上可以看出, 取沿的时机正好符合异或门的逻辑。也就 是说 a_edge 信号刚好是 a 和 a_dly 信号不同的地方为高电平, 那么 a 和 a_dly 异或的结果就是 a_edge 信号,这个信号在 a 的上升沿持续一个时钟周期,然后在 a 的下降沿后又持续一个时钟周期。
14.2 实验任务
使用 Verilog 语言设计上升沿、下降沿和双沿检测电路。
14.3 程序设计
14.3.1 编写代码
我们可以根据三种边沿检测电路的波形图以及分析, 来编写边沿检测电路(edge_test.v),代码编写如 下:
代码第 14 行到第 19 行,输入 a 通过时序逻辑延迟一拍赋值给 a_dly。代码第 21 行到代码第 23 行分别 对 3 个输出进行取上升沿、下降沿以及双沿的操作。
接下来我们使用 Vivado 的 RTL ANALYSIS,来看一下我们编写代码的 RTL 视图。
从上图可以看出, a_dly 是 a 寄存一拍的结果, a_posedge 是上升沿检测输出, a_negedge 是下降沿检测 输出, a_edge 是双沿检测输出(异或门)。
14.3.2 仿真验证
编写 TB 文件
边沿检测 TB 模块(tb_edge_test.v)只需要对时钟以及复位信号进行激励,代码编写如下:
实现分频一般有两种方法, 一种方法是直接使用 PLL 进行分频, 比如在 FPGA 或者 ASIC 设计中,都 可以直接使用 PLL 进行分频。但是这种分频有时候受限于 PLL 本身的特性,比如输入 100Mhz 时钟,很多 PLL 都实现不了 1Mhz 的时钟分频,这个就是 PLL 本身特性限制的。另外一种方法是直接使用代码来实现 分频,本节就是带领大家使用 Verilog 代码进行分频器电路的设计。
根据分频器的分频比例(分频前的频率和分频后的频率比值)是偶数还是奇数, 将分频器分为偶数分 频器和奇数分频器。接下来, 我们先看下偶数分频设计。
偶数分频,顾名思义就是分频前的频率和分频后的频率比值是偶数,比如一个 50Mhz 的晶振时钟,进 行二分频后,就是 50Mhz/2=25Mhz。
下面我们先来看一下偶数分频实现原理,假设为 N(偶数)分频,只需计数到 N/2- 1,然后时钟翻转、计数器清零,如此循环就可以得到 N(偶)分频。举个例子,比如晶振时钟是 100Mhz 时钟,想得到 一个 25Mhz 的时钟,那么这个是一个 100/25=4 的四分频设计, 按照我们刚说的计数到 4/2- 1=1,然后时钟 翻转、计数器清零, 就可以得到一个 25Mhz 的时钟。根据偶数分频的原理,可以绘制出偶数分频的波形 图:
下面我们再来看下奇数分频, 奇数分频顾名思义就是分配前的频率和分频后的频率比值是奇数。比如 一个 50Mhz 的晶振时钟, 进行三分频后,就是 50Mhz/3=16.667Mhz。
实现偶数分频可通过一个简单计数器实现,而如果需要进行三分频、五分频以及七分频等奇数分频而 言,一个寄存器肯定时不够的,接下来我们再来看一下奇数分频的原理。
同样假设我们需要分频的倍数为 N(奇数)分频,就需要定义一个个数为 N 的 cnt 。当 cnt=0 时
out_clk1 在 sys_clk 的上升沿拉低,当 cnt 计数到 N/2- 1 时 out_clk1 在 sys_clk 的上升沿进行翻转;而
out_clk2 则在 cnt=0 时的 sys_clk 下降沿进行拉低,当 cnt 计数到 N/2- 1 时, out_clk2 在 sys_clk 下降沿进行 翻转。最后将 out_clk1 和 out_clk2 的波形相与,就得到了我们分频后输出的 out_clk。
这样我们只需要通过修改 N 的值和计数器的位宽就可以实现其他奇数分频,根据这一原理同样可以绘 制出奇数分频器的波形图:
使用 Verilog 语言设计一个 4 分频的分频电路。
本次实验的任务是一个 4 分频的偶数分频电路,所以给模块命名为 divider_4。根据简介部分可知,本 次实验需要两个输入的端口, 分别为系统时钟和系统复位,输出为分频后的 1 位时钟端口,模块框图如下图所示:
模块端口与功能描述如下表所示:
信号名 |
位宽 |
方向 |
端口说明 |
sys_clk |
1 |
输入 |
系统时钟,50MHz |
sys_rst_n |
1 |
输入 |
系统复位按键,低电平有效 |
out_clk |
1 |
输出 |
输出时钟信号 |
接下来我们来看一下 4 分频内部模块框图,如下图所示:
从图中我们可以看出,我们需要设计一个 2bit 的分频计数器,当分频计数器计数到 1(N/2- 1)时,对 计数器进行清零同时对 out_clk 进行取反。
我们可以根据分频器的简介以及图 15.1.1 偶数分频器的波形,可以编写一个 4 分频的分频器 (divider_4.v)的代码,代码如下:
RTL 原理图
在完成了代码设计之后,可以通过 Vivado 软件查看 RTL 分析原理图, 通过原理图可以快速掌握项目 设计的架构图,方便后续对代码进行优化。在左侧导航栏点击 Schematic(位于 Flow Navigator →RTL ANALYSIS →Open Elaborated Design→Schematic),打开 RTL 原理图设计,如下图所示:
由上图可以清晰地看到代码和电路图的对应关系,RTL_ADD 是加法器,通过加法器实现计数器的累 加操作; RTL_MUX 是一个选择器,根据比较器的结果,选择输出 0 或者输出 cnt 累加的结果; 而
RTL_REG_ASYNC 是异步复位的寄存器, 在时钟边沿的触发下,将输入 D 赋值给输出 Q;RTL_ROM 是一 个存储电路,将 cnt 和 2’b01 进行对比, 并输出对比的结果值; RTL_INV 是一个非门, 将 out_clk 输出的结 果通过非门重新赋值给输入 D,经过下一个的使能再输出给 Q。
值得一提的是, Vivado 软件中的 RTL 分析原理图只针对 RTL 层面, 并不是代码和 FPGA 器件内部资 源的对应关系图,如果想要查看代码和 FPGA 内部资源的对应关系可以查看综合后的原理图,可以先对代 码进行综合,然后找到 Flow Navigator →SYNTHESIS→Open Synthesized Design→Schematic,大家可以自 行打开查看。
编写 TB 文件
下面我们编写一个 testbech 测试电路, 这个 testbech 激励只需要提供时钟和复位, 我们通过仿真来看 下分频后的时钟波形。
分频器 TB 模块(tb_divider_4.v)代码编写如下:
仿真验证:
接下来打开 Modelsim 软件对代码进行仿真,在运行仿真 10us 后,仿真的波形如下图所示:
由上图可知,out_clk 信号在计数器计数到 1 时进行翻转,我们在 out_clk 相邻两个上升沿的位置(也 是下一个脉冲可以采集到 out_clk 为高电平的位置) 处分别放置参考线并添加频率显示,我们可以看到显 示的频率为 12.5MHz,也是对 sys_clk 系统时钟的 4 分频,和我们绘制的波形图一致。