本部分将不再介绍Vivado工程的整体流程,将主要精力放在代码上面,具体的流程可参考:https://blog.csdn.net/crodemese/article/details/130438348
本部分代码也已上传到github:https://github.com/linxunxr/VerilogStudy
1. 全加器
那么什么是全加器呢?我们都知道加法,即1+1=2,当个位数相加大于9时就需要进位。在二进制中也是如此,因此,一位二进制的相加的真值表便如下图:
a | b | sum | count |
---|---|---|---|
0 | 0 | 0 | 0 |
0 | 1 | 1 | 0 |
1 | 0 | 1 | 0 |
1 | 1 | 0 | 1 |
图中a、b为输出,sum为相加的结果,count为进位,即当输入都为1时,相加的结果为0,进位信号输出1。这就是一个一位半加器。而全加器就是在此基础上再加一个输入,这个输入用来获取前一个的进位信号,其真值表如下图:
count | a | b | sum | count |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
0 | 0 | 1 | 1 | 0 |
0 | 1 | 0 | 1 | 0 |
0 | 1 | 1 | 0 | 1 |
1 | 0 | 0 | 1 | 0 |
1 | 0 | 1 | 0 | 1 |
1 | 1 | 0 | 0 | 1 |
1 | 1 | 1 | 1 | 1 |
这就是一个全加器的真值表。
2. 设计思路
我们都知道Verilog最大的特点就是自顶向下的设计方式,因此我们尝试对上面的全加器进行模块划分,即使用两个半加器和一个或门来实现,其模块如下图:
这样就可以实现一个一位全加器。
3. 模块化实现
- 首先我们来实现一个简单的或门模块,其具有两输入一输出,因此在
module
命名中对其定义输入a b
输出c
,然后将a
和b
进行或运算并赋值给c
,如下:
// 或门模块
module or_gate (
input a,
input b,
output c
);
// 或运算
assign c = a | b;
endmodule
- 接下来我们来实现较为复杂的半加器,其具有两输入两输出,通过我们上面对半加器真值表的观察可知,输出的相加信号可视为两输入信号的异或运算,输出的进位信号可视为两输入信号的与运算,因此实现如下:
// 半加器模块
module half_adder (
input a,
input b,
output sum,
output carry
);
// 异或运算完成相加
assign sum = a ^ b;
// 与运算完成进位
assign carry = a & b;
endmodule
- 接下来就是根据设计思路通过对半加器模块和或门模块的引用来完成一位全加器的顶层模块设计:
- 其基本语法仍然是赋值语句,只不过多了运算符的操作,与C语言类似,Verilog也有多种运算符(默认具有C语言基础,仅对与C语言不同的进行解释):
- 算数运算符:
+
:加法运算符,或正值运算符-
:减法运算符,或负值运算符*
:乘法运算符/
:除法运算符%
:模运算符
- 位运算符:
~
:取反&
:按位与|
:按位或^
:按位异或^~
:按位同或
- 逻辑运算符:
&&
:逻辑与||
:逻辑或!
:逻辑非
- 关系运算符:
<
:小于>
:大于<=
:小于等于>=
:大于等于
- 等式运算符:
==
:等于!=
:不等于===
:等于,与上面的等于不同的是,这里的等于对两个操作数的要求是严格的,即对不定值x
和高阻值z
也会进行比较,只有两端严格相等时其结果才为1
,因此其结果不会出现不定值x
和高阻值z
,下面的不等于类似。!==
:不等于
- 移位运算符:
<<
:左移运算符>>
:右移运算符
- 位拼接运算符:
{}
使用大括号可以将两个或多个信号的某些位拼接起来进行运算操作,其使用方法如下:{信号1的某几位,信号2的某几位,...,信号n的某几位}
- 如果我们需要将
a
信号和b
信号进行拼接,可用{a,b}
来进行操作 - 除此之外,还可以通过重复发来进行简化操作,如
{4{w}}
其相当于{w,w,w,w}
- 可以通过嵌套的方式来表达,如
{b,{3{a,b}}}
其相当于{b,a,b,a,b,a,b}
- 缩减运算符:缩减运算符是单目运算符,其也有与、或、非运算,但是与位运算不同的是,缩减运算符是对单个操作数进行或、与、非运算,最后的结果为1位的二进制数,如与运算:
- 算数运算符:
// 全加器模块
module full_adder (
input a,
input b,
input cin,
output sum,
output cout
);
// 对中间的变量进行定义
wire s1, c1, c2;
// 对半加器的实例化
half_adder half_adder1(a, b, s1, c1);
half_adder half_adder2(s1, cin, sum, c2);
// 对或门的实例化
or_gate or_gate1(c1, c2, cout);
endmodule
reg [3:0] B;
reg C;
// 对B进行缩减与运算,其运算过程相当于 C = ((B[0] & B[1]) & B[2]) & B[3]
C = &B;
- 除此之外我们还需要对Verilog的数据类型有一定的了解:
- 常量
- 数字:
- 整数:
- 二进制整数:
b或B
- 八进制整数:
o或O
- 十进制整数:
d或D
- 十六进制整数:
h或H
- 其表示方法有三种:
- <位宽><进制><数字>,如
2'b00
就是一个两位的二进制数字00
- <进制><数字>,在这种情况下,数字的位宽采用默认位宽(由机器系统决定),最少为32位,因此不推荐
- <数字>,在这种描述方式中,采用默认进制(十进制),但是描述不清,一般仍采用第一种表示方法
- <位宽><进制><数字>,如
- 二进制整数:
x
和z
值:数电中,x
代表不定值,z
值代表高阻值。如:4'b10x0
表示4位的二进制数从地位数起的第二位为不定值12'dz或12'd?
表示12为十进制数,其值为高阻值8'h4x
表示8位十六进制,其低4位为不定值
- 负数:在位宽表达式前加上一个减号,如:
-8'd5
- 下划线:可以具体的数字之间分隔开来以提高程序可读性,如:
16'b1010_0000_1111_0101
- 整数:
- 参数:即用
parameter
定义一个标识符代表一个常量,以提高程序可读性和可维护性,常用于定义延迟时间和变量宽度。如:parameter msb = 7;
定义参数msb
为常量7。
- 数字:
- 变量:
wire
型:常用来表示用以assign
关键字指定的组合逻辑信号,Verilog模块中的输入和输出信号默认自动定义为wire
型。可用做任何方程式的输入,也可以用做assign
语句或实例元件的输出。其定义格式如下:wire [n-1:0] 数据名1,数据名2,...,数据名i
,其表示共有i条总线,每条总线内有n条线路reg
型:即寄存器数据类型,通过赋值语句可改变寄存器存储的值,其初始值为不定值x,因此在使用reg
型时一般需要赋初值,否则在仿真中会出现不定值x,无法通过仿真确认其结果。同时reg
型只表示被定义的信号将用在always
模块,在always
模块内被赋值的每一个信号都必须被定义成reg
型。其定义格式如下:reg [n-1:0] 数据名1,数据名2,...,数据名i
。memory
型:通过对reg
型变量建立数组来对存储器建模,可以描述RAM型存储器、ROM存储器和reg文件。数组中的每一个单元通过一个数组索引进行寻址。其定义格式如下:reg [n-1:0] 存储器名[m-1:0];
- 常量
4.仿真文件
在生成bit文件下载到板子之前,我们一般会通过编写仿真文件对已完成的模块进行验证,其仿真文件如下:
module full_adder_test_top;
// 定义输入到全加器的信号
reg a_test, b_test, cin_test;
// 定义从全加器输出的信号
wire sum_test, cout_test;
// 将输入信号和输出信号与全加器相连接
full_adder full_adder_test(
.a(a_test),
.b(b_test),
.cin(cin_test),
.sum(sum_test),
.cout(cout_test)
);
// 开始仿真
initial begin
// 初识时刻为0,对各个输入信号进行初始化
a_test = 0;
b_test = 0;
cin_test = 0;
// 过了20ns后改变信号值
#20 a_test = 1;
b_test = 0;
cin_test = 0;
#20 a_test = 1;
b_test = 1;
cin_test = 0;
#20 a_test = 1;
b_test = 1;
cin_test = 1;
#20 a_test = 0;
b_test = 0;
cin_test = 1;
// 调用系统函数使仿真停止
#20 $stop;
end
endmodule
从这里我们看到一般仿真文件的编写规则:首先,仿真模块无输入无输出;第二,定义仿真的全加器输入信号一般使用reg
型,输出一般使用wire
型;第三,使用initial
块确定仿真过程中的各种输入值的初始化以及赋值;第四,其基本单位为ns。—
highlight: github
本部分将不再介绍Vivado工程的整体流程,将主要精力放在代码上面,具体的流程可参考:Verilog学习一: 控制LED灯的亮灭 - 掘金 (juejin.cn)
1. 全加器
那么什么是全加器呢?我们都知道加法,即1+1=2,当个位数相加大于9时就需要进位。在二进制中也是如此,因此,一位二进制的相加的真值表便如下图:
a | b | sum | count |
---|---|---|---|
0 | 0 | 0 | 0 |
0 | 1 | 1 | 0 |
1 | 0 | 1 | 0 |
1 | 1 | 0 | 1 |
图中a、b为输出,sum为相加的结果,count为进位,即当输入都为1时,相加的结果为0,进位信号输出1。这就是一个一位半加器。而全加器就是在此基础上再加一个输入,这个输入用来获取前一个的进位信号,其真值表如下图:
count | a | b | sum | count |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
0 | 0 | 1 | 1 | 0 |
0 | 1 | 0 | 1 | 0 |
0 | 1 | 1 | 0 | 1 |
1 | 0 | 0 | 1 | 0 |
1 | 0 | 1 | 0 | 1 |
1 | 1 | 0 | 0 | 1 |
1 | 1 | 1 | 1 | 1 |
这就是一个全加器的真值表。
2. 设计思路
我们都知道Verilog最大的特点就是自顶向下的设计方式,因此我们尝试对上面的全加器进行模块划分,即使用两个半加器和一个或门来实现,其模块如下图:
这样就可以实现一个一位全加器。
3. 模块化实现
- 首先我们来实现一个简单的或门模块,其具有两输入一输出,因此在
module
命名中对其定义输入a b
输出c
,然后将a
和b
进行或运算并赋值给c
,如下:
// 或门模块
module or_gate (
input a,
input b,
output c
);
// 或运算
assign c = a | b;
endmodule
- 接下来我们来实现较为复杂的半加器,其具有两输入两输出,通过我们上面对半加器真值表的观察可知,输出的相加信号可视为两输入信号的异或运算,输出的进位信号可视为两输入信号的与运算,因此实现如下:
// 半加器模块
module half_adder (
input a,
input b,
output sum,
output carry
);
// 异或运算完成相加
assign sum = a ^ b;
// 与运算完成进位
assign carry = a & b;
endmodule
- 接下来就是根据设计思路通过对半加器模块和或门模块的引用来完成一位全加器的顶层模块设计:
- 其基本语法仍然是赋值语句,只不过多了运算符的操作,与C语言类似,Verilog也有多种运算符(默认具有C语言基础,仅对与C语言不同的进行解释):
- 算数运算符:
+
:加法运算符,或正值运算符-
:减法运算符,或负值运算符*
:乘法运算符/
:除法运算符%
:模运算符
- 位运算符:
~
:取反&
:按位与|
:按位或^
:按位异或^~
:按位同或
- 逻辑运算符:
&&
:逻辑与||
:逻辑或!
:逻辑非
- 关系运算符:
<
:小于>
:大于<=
:小于等于>=
:大于等于
- 等式运算符:
==
:等于!=
:不等于===
:等于,与上面的等于不同的是,这里的等于对两个操作数的要求是严格的,即对不定值x
和高阻值z
也会进行比较,只有两端严格相等时其结果才为1
,因此其结果不会出现不定值x
和高阻值z
,下面的不等于类似。!==
:不等于
- 移位运算符:
<<
:左移运算符>>
:右移运算符
- 位拼接运算符:
{}
使用大括号可以将两个或多个信号的某些位拼接起来进行运算操作,其使用方法如下:{信号1的某几位,信号2的某几位,...,信号n的某几位}
- 如果我们需要将
a
信号和b
信号进行拼接,可用{a,b}
来进行操作 - 除此之外,还可以通过重复发来进行简化操作,如
{4{w}}
其相当于{w,w,w,w}
- 可以通过嵌套的方式来表达,如
{b,{3{a,b}}}
其相当于{b,a,b,a,b,a,b}
- 缩减运算符:缩减运算符是单目运算符,其也有与、或、非运算,但是与位运算不同的是,缩减运算符是对单个操作数进行或、与、非运算,最后的结果为1位的二进制数,如与运算:
- 算数运算符:
// 全加器模块
module full_adder (
input a,
input b,
input cin,
output sum,
output cout
);
// 对中间的变量进行定义
wire s1, c1, c2;
// 对半加器的实例化
half_adder half_adder1(a, b, s1, c1);
half_adder half_adder2(s1, cin, sum, c2);
// 对或门的实例化
or_gate or_gate1(c1, c2, cout);
endmodule
reg [3:0] B;
reg C;
// 对B进行缩减与运算,其运算过程相当于 C = ((B[0] & B[1]) & B[2]) & B[3]
C = &B;
- 除此之外我们还需要对Verilog的数据类型有一定的了解:
- 常量
- 数字:
- 整数:
- 二进制整数:
b或B
- 八进制整数:
o或O
- 十进制整数:
d或D
- 十六进制整数:
h或H
- 其表示方法有三种:
- <位宽><进制><数字>,如
2'b00
就是一个两位的二进制数字00
- <进制><数字>,在这种情况下,数字的位宽采用默认位宽(由机器系统决定),最少为32位,因此不推荐
- <数字>,在这种描述方式中,采用默认进制(十进制),但是描述不清,一般仍采用第一种表示方法
- <位宽><进制><数字>,如
- 二进制整数:
x
和z
值:数电中,x
代表不定值,z
值代表高阻值。如:4'b10x0
表示4位的二进制数从地位数起的第二位为不定值12'dz或12'd?
表示12为十进制数,其值为高阻值8'h4x
表示8位十六进制,其低4位为不定值
- 负数:在位宽表达式前加上一个减号,如:
-8'd5
- 下划线:可以具体的数字之间分隔开来以提高程序可读性,如:
16'b1010_0000_1111_0101
- 整数:
- 参数:即用
parameter
定义一个标识符代表一个常量,以提高程序可读性和可维护性,常用于定义延迟时间和变量宽度。如:parameter msb = 7;
定义参数msb
为常量7。
- 数字:
- 变量:
wire
型:常用来表示用以assign
关键字指定的组合逻辑信号,Verilog模块中的输入和输出信号默认自动定义为wire
型。可用做任何方程式的输入,也可以用做assign
语句或实例元件的输出。其定义格式如下:wire [n-1:0] 数据名1,数据名2,...,数据名i
,其表示共有i条总线,每条总线内有n条线路reg
型:即寄存器数据类型,通过赋值语句可改变寄存器存储的值,其初始值为不定值x,因此在使用reg
型时一般需要赋初值,否则在仿真中会出现不定值x,无法通过仿真确认其结果。同时reg
型只表示被定义的信号将用在always
模块,在always
模块内被赋值的每一个信号都必须被定义成reg
型。其定义格式如下:reg [n-1:0] 数据名1,数据名2,...,数据名i
。memory
型:通过对reg
型变量建立数组来对存储器建模,可以描述RAM型存储器、ROM存储器和reg文件。数组中的每一个单元通过一个数组索引进行寻址。其定义格式如下:reg [n-1:0] 存储器名[m-1:0];
- 常量
4.仿真文件
在生成bit文件下载到板子之前,我们一般会通过编写仿真文件对已完成的模块进行验证,其仿真文件如下:
module full_adder_test_top;
// 定义输入到全加器的信号
reg a_test, b_test, cin_test;
// 定义从全加器输出的信号
wire sum_test, cout_test;
// 将输入信号和输出信号与全加器相连接
full_adder full_adder_test(
.a(a_test),
.b(b_test),
.cin(cin_test),
.sum(sum_test),
.cout(cout_test)
);
// 开始仿真
initial begin
// 初识时刻为0,对各个输入信号进行初始化
a_test = 0;
b_test = 0;
cin_test = 0;
// 过了20ns后改变信号值
#20 a_test = 1;
b_test = 0;
cin_test = 0;
#20 a_test = 1;
b_test = 1;
cin_test = 0;
#20 a_test = 1;
b_test = 1;
cin_test = 1;
#20 a_test = 0;
b_test = 0;
cin_test = 1;
// 调用系统函数使仿真停止
#20 $stop;
end
endmodule
从这里我们看到一般仿真文件的编写规则:首先,仿真模块无输入无输出;第二,定义仿真的全加器输入信号一般使用reg
型,输出一般使用wire
型;第三,使用initial
块确定仿真过程中的各种输入值的初始化以及赋值;第四,其基本单位为ns。