结合Verilog回顾,一并复习基础的数字电路设计

verilog的本质是硬件描述语言,而不是硬件设计语言。

Syntax Explanation:

1
2
3
4
5
6
7
module
...
endmodule 表征模块的开始和结束

example: 模块名可有用户只丁宁,课包含字母、数字及下划线,需以字母开头,区分大小写
assign: 赋值操作关键字,该关键字后可跟一个区赋值表达式,该关键字是实现组合逻辑操作的一种主要描述方式
input/output: 表征该信号输入输出的方向。还有一个inout型。

例一:

tip:声明变量中,位宽的表示:从左到右依次为高位到低位,代表位宽为4的输入和输出端口。

1
2
3
4
5
6
7
8
module gates(
input [3:0] a,
input [3:0] b,
output [3:0] c
); //注释和c类似,//代表注释,
/* 大块注释这样实现 */
assign c = a & b;
endmodule

例二:八输入与门

tip:

  1. 单独输出多位宽信号的一位或者多位的时候可以以这种方式单独处理,即a[7]
  2. &:按位与、归并与操作,如该操作符只有一个操作数的时候,则该操作数的所有位进行相与操作,可以实现注释部分相同的功能,但写法更简洁。
1
2
3
4
5
6
7
module and8(
input [7:0] a,
output y
);
assign y = &a; //&a这种语法是很有效的一种语法,代表将a这个向量变量按位与后的结果,与下面的语句等价:
//assign y = a[7] & a[6] & a[5] & a[4] & a[3] & a[2] & a[1] & a[0];
endmodule

例三:一位全加器,同时还有半加器。

tip: 该部分内容在NJU数电计组教程第145页

1.

不考虑低位进位,仅考虑两个加数的一位编码器称为半加器,反之考虑低位进位的就叫全加器。

以下是半加器的真值表:其中A,B分别为代加的数据,Cout和F分别是进位和加和

1
2
3
4
5
6
7
8
9
A | B | F | Cout
0 0 0 0
0 1 1 0
1 0 1 0
1 1 0 1
可知,
F = A ^ B,
Cout = A & B;

可以设计这样的半加器verilog代码:

1
2
3
4
5
6
7
8
9
module HA(
input A,
input B,
output F,
output Cout
);
assign F = A ^ B;
assign Cout = A & B;
endmodule
1
2
1bit-FA(cout,s) = a + b + cin;
其中cin是传入的低位的进位,a和b分别是两个操作数,而cout则是向后传递的进位,s是我们的和

一位全加器的真值表如下所示:

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
A | B | Cin|S |Cout
0 0 0 0 0
1 0 0 1 0
0 1 0 1 0
0 0 1 1 0
1 0 1 0 1
1 1 0 0 1
0 1 1 0 1
1 1 1 1 1
可知:
S = A~B~C+~AB~C+~A~BC+ABC=A(~B~C+BC)+~A(B~C+~BC)
而由于:~~(~B~C+BC) = ~((B+C)(~B+~C))=~(B~C+~BC)
所以原始 = A~(B~C+~BC)+~A(B~C+~BC)=A^(B~C+~BC)=A^B^C
Cout = BCin + ACin + AB = AB + (A + B)Cin

自然,在我们构造一个n位加法器的时候,可以用n个全加器来串行实现。这样串行实现的加法器被称作串行行波加法器(CRA)。
不过,根据上面我们推导的S和Cout的结果,对于n位加法器,我们可以优化其进位逻辑。从而构造出来并行进位加法器。

对于4位并行加法器的构造,我们可以有如下的方式:设x=[x1,x2,x3,x4],y=[y1,y2,y3,y4]为两个加数,c=[c0,c1,c2,c3,c4]为各自的进位。其中c0为最低位传入进位,c4为最高位输出进位。
c1 = x1y1 + (x1+y1)c0
c2 = x2y2 + (x2+y2)c1 = x2y2 + (x2+y2)x1y1 + (x2+y2)(x1+y2)c0
发现一组乘积xiyi和加和xi+yi可以被预处理出来,这样就可以优化我们对进位的计算了。
设pi=xi+yi,被称为进位传递(pass)函数,其真实意思在于,如果xi或yi中有一个是1,那么若此时低位传入了进位,就相当于越过了这一位,直接传递到更高的一位去了。pici-1就可以这样解读!
设gi=xiyi,被称为进位生成(generate)函数,其真实意思在于,如果该位两个传入的相加操作数都是1,则无论是否有低位传入进位,都会向更高位进位,生成一个进位去,因此叫进位生成函数。
ci = gi + pici-1,这里的ci-1都可以不断递归下去分解。这样我们就能够实现并行处理的进位了。
然后和位:
si = xi ^ yi ^ ci-1
  1. wire: wire是线网型数据类型,verilog语法中的一种主要数据类型,用于表示线网型信号,与实际电路中的信号连线相对应。wire是verilog中的默认数据类型,此例中的输入输出信号没有指定数据类型,则默认为wire型。除wire外,另一种主要的数据类型为reg,表示寄存器类型数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
module FA(
input a,
input b,
input cin,
output s,
output cout
);
wire p, g;
assign p = a | b; //这里ustc的教程是有问题的.....
assign g = a & b;
assign s = p ^ cin;
assign c = g | (p & cin);
endmodule

例四:三态门

tip:

  1. z表示高阻态,在verilog中,信号有四种状态。0、1、x、z,分别表示低电平、高电平、不确定态和高阻态。对于没有进行初始化的信号,一般处于不确定态(x),高阻态表示该信号没有被其他信号驱动,经常用于有多个驱动源的总线型数据上。
  2. 4’bz: 数据格式,表示该信号为4 bit位宽,用二进制的方式表示。
1
2
3
4
5
6
module tristate(
input [3:0] a,
input en,
output [3:0] y
);
assign y = en ? a : 4'bz;

例五:八位二路选择器

tip:

  1. 模块例化(Model Instancilization): 可以通过实例化已经设计好的模块来达到重用模块,简化设计的目的。可以将一个模块重用多次,在同一模块中,实例化名称(本例中为lsbmux, msbmux)可任意指定,但不能相同,也不能使用verilog中的关键字。本例这种列举了verilog语法支持的两种实例化方式,推荐使用第二种方式,虽然代码量增加了一些,但增加了可读性,同时降低了出错的风险。

    对于第一种模块例化方法,应严格保证实例化模块lsbmux中的参数排列顺序与被实例化模块mux2的参数排列顺序严格一致。

    对于第二种模块例化方法,.点后面是被例化模块mux2的接口信号,括号内的是实例化模块(msbmux)的接口信号。

    1
    assign y = {a[2:1], {3{b[0]}}, a[0], 6'b100_010};
  2. 位拼接:可将一个或多个信号的指定位,拼接成一个新的信号,对于上述表达式,如果y是一个12bit的信号,则各位的值分别是:

1
2
3
4
5
a[2],a[1],b[0],b[0],b[0],a[0],1,0,0,0,1,0
具体的,上面中a[2:1],会自左向右拼接a[2],a[1]
重复复制某节段的语句表达为:
{3{b[0]}},这里拼接的最外面要套一个花括号,里面3代表重复次数,3后面也要用花括号框住表示复制的范围
用花括号加逗号的方式来拼接信号的方式,需要记忆。

下面是verilog代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module mux2(
input [3:0] d0,
input [3:0] d1,
input s,
output [3:0] y
);
assign y = s ? d1 : d0;
endmodule


module mux2_8bit(
input [7:0] d0,
input [7:0] d1,
input s,
output [7:0] y
);
mux2 lsbmux(d0[3:0], d1[3:0], s, y[3:0]);
mux2 msbmux(
.d0(d0[7:4]),
.d1(d1[7:4]),
.s(s),
.y(y[7:4])
);
endmodule

例五:D触发器

tip:

  1. 时序逻辑:电路具有记忆功能,电路状态不但与当前输入有关,还与前一时刻的状态有关。
  2. D锁存器:D锁存器只有一个状态驱动信号D,有一个D锁存器使能信号en。若en=1,则此时D锁存器或存储当前状态驱动信号输入的值;而若en=0,则此时D信号无法改变其状态,也即输出Q存储了上一次存储的D状态驱动信号。
  3. D触发器:与D锁存器不同的是,D触发器通过采用时钟边沿触发机制,来提高状态的稳定性。可以通过两个D锁存器用主从的方式来构造。
  4. 同步逻辑:在统一的时钟信号激励下工作,输出只在时钟的上升沿(或者下降沿)发生变化。
  5. reg: 除wire类型外,还有另一种常用的数据类型,一般表示寄存器类型数据,不过并不绝对。记住一条原则,在always块内被赋值的信号应定义成reg型,在assign语句中赋值的信号应定义成wire型。
  6. always块:除了assign外,还有另外一种·实现赋值操作的关键字,两者都不可嵌套,区别在于assign只能实现组合逻辑赋值,且一个assign语句后面只能跟一条赋值表达式,而always可以实现组合逻辑赋值,而且也能实现时序逻辑赋值操作。并且它可以包含多条赋值表达式,位于begin/end对中间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
always @(*) //@(*)的意思是在任何信号发生改变的时候都执行如下的内容,可以简单理解为这样的always块是在构造组合逻辑电路。
begin
a = b;
c = d;
end

等价于:
assign a = b;
assign c = d;

assign a = b ? c : d;
always@(*) a = b ? c : d;


  1. posedge: verilog关键字,表示上升沿的意思。always@(posedge clk)表示在clk信号的上升沿的时刻,执行always块内部的语句,与此对应的,是表示下降沿的关键字negedge。凡是带有posedge或者negedge的always块,都会被综合成时序逻辑电路。
  2. 阻塞/非阻塞赋值:采用“<=”进行赋值的语句,称为非阻塞赋值,采用”=”等号进行赋值的语句,成为阻塞赋值。在always块中,阻塞式赋值方式语句执行有先后顺序,而非阻塞语句则是同时执行·。因此,在时序逻辑电路中,两种赋值方式可能或综合处不同的电路结构.
  3. 一些很重要的编写verilog代码的规则:

(1)在组合逻辑电路中,使用阻塞式赋值方式”=”;在时序逻辑电路中,使用非阻塞式赋值方式”<=”

(2)在同一个always块中,只能存在一种赋值方式。

(3)一个信号,只能在一个always块或一个assign语句下赋值,不可被重复赋值!

(4)原则上来说,一个always块内只处理一个或一类信号,不同的信号可在不同的always块内处理。

(5)always块内只能对reg型信号进行处理,不能对wire型数据赋值,也不能实例化模块。

1
2
3
4
5
6
7
8
module flop(
input clk,
input [3:0] d,
output reg [3:0] q
);
always@(posedge clk)
q <= d;
endmodule

例六:带同步复位的D触发器

tip:

  1. 同步复位:复位只能发生在clk信号的上升沿,若clk信号出现问题,则无法进行复位。
  2. If/else: always块中常用的条件判断语句,可以嵌套,有优先级,一般来说,应将复位处理逻辑放在第一个if语句下,使其拥有最高的优先级,该语句只能在always块中使用。另外一种比较常用的条件判断语句是case,与If/else语句不同,case语句不带优先级。
1
2
3
4
5
6
7
8
9
10
11
12
module flopr(
input clk,
input reset,
input [3:0] d,
output reg [3:0] q
);
always@(posedge clk)
if (reset)
q <= 4'b0;
else
q <= d;
endmodule

例七:异步复位电路

tip:

  1. 异步复位:在always的敏感变量列表中,包含了posedge clk(clk信号上升沿)和posedge reset(reset信号下降沿)两个条件,只要有一个条件发生,便会执行always块内的逻辑。复位处理程序应具有最高的优先级。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
module flopren(
input clk,
input reset,
input en,
input [3:0] d,
output reg [3:0] q
);
//asynchoronous reset and enable
always@(posedge clk, posedge reset)
if (reset)
q <= 4'b0;
else
q <= d;
endmodule