前言:
会不会产生这样疑问:为什么我们就要使用硬件描述语言来解决呢?
因为诸如 C++语言,解决这类问题,多数使用的是循环语句,通过一遍又一遍的重复来找到触发条件,我们能做的是不断减少循环时间以优化程序,而对于硬件描述语言则省略了这样的步骤,直接实现了与硬件的对接,减少了运行时间。
知识点:
模块 module 基础概念
一个复杂电路系统的完整 Verilog HDL 代码是由不同模块组成的,因为模块之间允许层次嵌套,故而这些模块又可以是由更小型模块组成。
Verilog 模型可以是实际电路的不同级别抽象,5 种:
- 系统级(system-level):用语言提供的高级结构能够实现待设计模块的外部性能的模型
- 算法级(algorithm-level):用语言提供的高级结构能够实现算法运行的模型
- RTL 级(register transfer level):描述数据在寄存器之间的流动和如何处理、控制这些数据流动的模型
- 门级(gate-level):描述逻辑门以及逻辑门之间连接的模型
- 开关级(switch-level):描述器件中三极管和储存节点以及它们之间连接的模型
模块由描述接口、描述逻辑功能两部分组成。
即是对哪些对象将要进行操作以及怎么操作。
模块的应用可以采用“.”实现,被引用的模块称之为子模块,在引用时必须要原名引用,且另外再表明当下模块(引用子模块)的模块名,在引用时,有两种引用方式:按位置引用与按名称引用。
例子可见 Hdlbits 中 Verilog Language ·Modules:Hierarchy·Modules、Connecting ports by position、Connecting ports by name,两者区别在于:
(设子模块名为 mod_a,子模块中有变量 A 1、A 2;主模块名为 mod_b,主模块中有变量 B 1、B 2)
Connecting ports by position 按位置命名,顾名思义,位置决定了如何引用,写为:
引用 mod_a mod_b (B 1, B 2);
Connecting ports by name 按名称命名,顾名思义,位置决定了如何引用,写为:
引用 mod_a mod_b (. A (B 1), .A(B 2));
按位置引用时,子模块与主模块的对应变量的位置必须要一一吻合,而按名称引用则不需要,比如上述例子也可以这样写 mod_a mod_b (.A(B 2), . A (B 1));
模块 module 必须以 module 开始,endmodule 结束。
![[1687858972860.png]]
在模块的功能定义部分有三种方式:
- 语句 assign 声明(此点可详见另一章内容)
- 用实例元件(此点可详见另一章内容)
- 用 always 块(块 always 相当于一个大的 assign 语句,只不过在这个大的 assign 语句中能完成一些复杂操作)
模块大致分为两类:组合模块和时序模块。
而在 Verilog 中务必记住所有的过程块、连续赋值语句、实例引用都是并行的,等于是一块已经构思好的逻辑图,而我们要做的是用 Verilog 同时将这个逻辑图上的关系描述清楚。
温习 tips:
- 《Verilog 数字系统设计教程·第四版·夏宇闻》第二章
基于SparkRoad的《Verilog数字系统设计教程·第三版(夏宇闻)》学习(3)——第2章 - 江左子固 - 博客园 (cnblogs.com)
- 《Verilog 数字系统设计教程·第四版·夏宇闻》第三章
基于SparkRoad的《Verilog数字系统设计教程·第三版(夏宇闻)》学习(4)——第3章 - 江左子固 - 博客园 (cnblogs.com)
- 模块部分 Hdlbits 习题解答 基于SparkRoad的Hdlbits网站刷题(2) - VeriMake
常用语句
与 C ++语言相同,Verilog 也提供了多种语句供使用,常用的有 if-else、case、while、for、forever、repeat,下面将一一挑重点讲解。
If-else 语句
If-else 语句可用汉语中“如果不是……那么就是……”理解,对应于数据选择器, assign out=(condition?)a: b,理解为 condition 是否为 1,如果是则,输出 out 为 a,否则为 b。这样的写法可以扩展为 if-else 写法,则需要注意 begin-end,最好都写上,或者也可以简写为?:的三目运算符模式。
Else 可以不写,另外 if 与 else 引导的语句,若只有一句则不用以 begin-end 引导,但最好还是写全,以免遗忘造成代码报错;
若只有一对 if-else,则正常描述,若有多个情况,可写作 if-else if-else-if……-else;
If-else 用“表达式”(这个表达式见下代码,例如 a>2, b=3)是否成立来判断是否执行相应情况下的代码;
if-else 可以多重套用,即 else 中可以进一步嵌套子语句 if-else 语句,示例如下:
if(表达式1)
begin
……
end
else if(表达式2)
begin
……
end
else if(表达式3)
……
……
……
else
begin //这几个begin-end在单句的情况下都是可以省略的,是为了严谨而写全
……
end
“表达式 1”的写法是 Verilog 的略写,等同于“表达式 1=1”;“!表达式 1”等同于“表达式 1=!1=0”。
Case 语句
Case 语句可用汉语中“当……时,就……;当……时,就……”理解,case 语句的提出解决了 if-else 语句在多种情况下需要针对每一种情况展开的复杂书写问题,且针对不同情况又延伸出 casex、casez 的形式。
Case 语句在包含的表达式多余一项时,需要使用 case-endcase 来引导。
与 if-else 相同,case-endcase 在包含语句多余一项时需要用 begin-end 引导,且 case 允许嵌套声明。
从汉语角度不难理解,我们总会说“当……时,就……;当……时,就……”,严谨的人总会补上一句“对于剩下的情况,则有……”以补全所有的可能性,对应此,在 Verilog 中引入 default。引入了 default 即弥补上了逻辑可能存在的漏洞,在数电中即是避免了锁存的存在,因为其保证了所有情况都能有“代码反应”。
与 if-else 语句不同的在于,case 中的“表达式”并不是一个判断句,而是单单的一个变量。
示例如下:
case(表达式)
<情况一>begin
……
end
<情况二>begin
……
end
……
……
endcase
但是 case 的每一个分支对应的情况必须不同,以防报错。
语句 case 的所有表达式的位宽必须相等,以便代码运行,尤其是使用'bx 时,默认的是机器的 32 字符,需注意。
Casex、casez 则是对于不定态和高阻态的特殊使用形式。
循环语句
循环语句在 Verilog 中有四种,分别为 forever、repeat、while、for。
Forever 语句常用于 testbench 中生成周期波形,且 forever 语句不能单独写在 always 模块中,而是要写在 initial 模块中;
Repeat 语句实现了对某一表达式的重复;
While 语句不可综合用的较少,且使用方式于 C++中相同;
For 语句因为有时并不知道会综合成什么形式,所以使用也要谨慎,这点与 C++之类不同。
顺序块和并行块
Begin-end 为顺序块;
fork-join 为并行块,多用在 testbench 中,操控所有激励同时发生。
温习 tips:
- 《Verilog 数字系统设计教程·第三版(夏宇闻)》P 51 第五章
- 基于SparkRoad的《Verilog数字系统设计教程·第三版(夏宇闻)》学习(6)——第5章 - 江左子固 - 博客园 (cnblogs.com)
实例题目:
将会尝试在这一部分的习题中多多尝试不同的实现方式,以体会不同。
一、简单门电路的实现:
比较器
例 1.1 现有两个两位输入 x、y,用 x 1 x 2、y 1 y 2 表示,输出 L 1、L 2、L 3,L 1 表示 x>y,L 2 表示 x<y,L 3 表示 x=y,完成代码编写与仿真并在 SparkRoad 上实现。
(点评与想法: 尝试分别用门级描述方式和数据流描述方式实现)
解:
方法一:卡诺图实现如下,x=x[1]x[0],y=y[1]y[0]:
module bijiaoqi( x,y,L1,L2,L3);
output L1,L2,L3;
input [1:0]x,y;
wire [1:0]x,y;
wire L1,L2,L3;
assign L1=(x[0]*(~y[1])*(~y[0]))|(x[1]*(~y[1])|x[0]*x[1]*(~y[0]));
assign L2=((~x[1])*y[1])|((~x[0])*(~x[1])*y[0])|((~x[0])*y[0]*y[1]);
assign L3=((~x[0])*(~x[1])*(~y[0])*(~y[1]))|((~x[0])*x[1]*(~y[0])*y[1])|
(x[0]*x[1]*y[0]*y[1])|(x[0]*(~x[1])*y[0]*(~y[1]));
endmodule
可以看出这种 RTL 级描述方式最贴切门电路,但是写出来可能会很复杂。
方法二:if-else 实现如下:
module bijiaoqi_2( x,y,L1,L2,L3);
output L1,L2,L3;
input [1:0]x,y;
wire [1:0]x,y;
reg L1,L2,L3;
always@(*)
begin
if(x>y)
L1=1;
else
L1=0;
if(x<y)
L2=1;
else
L2=0;
if(x==y)
L3=1;
else
L3=0;
end
endmodule
可以看出行为级描述确实在书写上相对更简单。
方法三:case 实现如下:
module bijiaoqi_3( x,y,L1,L2,L3);
output L1,L2,L3;
input [1:0]x,y;
wire [1:0]x,y;
reg L1,L2,L3;
/*
always@(*)
begin
case({x[1],x[0],y[1],y[0]})
0000:L3=1;
1111:L3=1;
0101:L3=1;
1010:L3=1;
0001:L2=1;
0010:L2=1;
0011:L2=1;
1011:L2=1;
0110:L2=1;
0111:L2=1;
0100:L1=1;
1000:L1=1;
1001:L1=1;
1100:L1=1;
1101:L1=1;
1110:L1=1;
endcase //当然了,这样的枚举有一点点的呆
end
*/
always@(*)
begin
case({x[1],x[0],y[1],y[0]})
4'b0000:L3=1;
4'b1111:L3=1;
4'b0101:L3=1;
4'b1010:L3=1;
4'b0001:L2=1;
4'b001?:L2=1;
4'b1011:L2=1;
4'b0110:L2=1;
4'b0111:L2=1;
4'b0100:L1=1;
4'b100?:L1=1;
4'b1100:L1=1;
4'b1101:L1=1;
4'b1110:L1=1;
endcase
end
endmodule
可以看出 case 语句在枚举上可以通过使用 z、x 或者?来简略书写,并且避免了 if-else 的复杂。
Testbench 实现如下:
`timescale 1ns/1ps
module bijiaoqi_tb;
reg [1:0]x,y;
wire L1,L2,L3;
initial
begin
x[1]=0;
x[0]=0;
y[1]=0;
y[0]=0; //注意是对激励赋初值,不然不定态或者高阻态会影响仿真
end
always
begin
#100 x[1]=~x[1];
#200 x[0]=~x[0];
#400 y[1]=~y[1];
#800 y[0]=~y[0];
end
bijiaoqi bijiaoqi_tb_1(
.x(x),
.y(y),
.L3(L3),
.L2(L2),
.L1(L1)
);
endmodule
此处针对的是对使用 if-else 的 Verilog 设计。
波形仿真验证如下:
![[1687944424186.png]]
引脚设置如下:
set_pin_assignment { bijiao_1[0] } { LOCATION = R5; }
set_pin_assignment { bijiao_1[1] } { LOCATION = T4; }
set_pin_assignment { bijiao_2[0] } { LOCATION = T6; }
set_pin_assignment { bijiao_2[1] } { LOCATION = T5; }
set_pin_assignment { dayu } { LOCATION = M3; }
set_pin_assignment { dengyu } { LOCATION = M4; }
set_pin_assignment { xiaoyu } { LOCATION = N3; }
实物验证如下:
设置 SW0、SW 1 为 x,SW 14、SW 15 为 y;LED 0、LED 1、LED 2 为 L1、L 2、L 3。
![[1687938103487.png]]
输入 x=00,y=00,输出 L1=0、L 2=0、L 3=1,运行成功。
![[1687940798503.png]]
输入 x=01,y=10,输出 L1=0、L 2=1、L 3=0,运行成功。
![[1687940866746.png]]
输入 x=10,y=01,输出 L1=1、L 2=0、L 3=0,运行成功。
更进一步地想想,今天我们处理了两位数之间的比较,那如果明天出来一个三位数或者四位数之间的比较时,我们的代码是需要重新写吗?
答案是不需要的。
对于方法一的卡诺图和方法三的 case 语句,确实只能重头再来了,但是对于 if-else 语句却有一种可以“一劳永逸”的方法——使用常数 parameter(详见第一章)。相关代码如下:
module bijiaoqi_2( x,y,L1,L2,L3);
output L1,L2,L3;
input [width-1:0]x,y;
wire [width-1:0]x,y;
reg L1,L2,L3;
parameter width=8;
always@(*)
begin
if(x>y)
L1=1;
else
L1=0;
if(x<y)
L2=1;
else
L2=0;
if(x==y)
L3=1;
else
L3=0;
end
endmodule
则每次只需要根据要求改变 parameter 数值即可。
比较器是一种经典的设计,在数电实验中也曾用过,即 7485 芯片。
![[1687956754120.png]]
![[1687956725607.png]]
普通编码器与普通译码器
例 2.1 实现 3-8 译码器,并在 SparkRoad 上实现。
解:
直接采用 case 语句实现:
module yimaqi_1( x,y );
input [2:0]x;
output [7:0]y;
wire [2:0]x;
reg [7:0]y;
always@(*)
begin
case(x)
3'b000:y=8'b00000001;
3'b001:y=8'b00000010;
3'b010:y=8'b00000100;
3'b011:y=8'b00001000;
3'b100:y=8'b00010000;
3'b101:y=8'b00100000;
3'b110:y=8'b01000000;
3'b111:y=8'b10000000;
endcase
end
endmodule
Testbench 实现如下
`timescale 1ns/1ps
module yimaqi_tb;
reg [2:0]x;
wire [7:0]y;
initial
begin
x[2]=1'b0;
x[1]=1'b0;
x[0]=1'b0;
end
always@(*)
begin
forever
begin
#100 x[0]=~x[0];
#200 x[1]=~x[1];
#400 x[2]=~x[2];
end
end
yimaqi_1 yimaqi_tb_1(
.x(x),
.y(y));
endmodule
波形图验证如下:
![[Pasted image 20230629133233.png]]
引脚设置如下:
set_pin_assignment { x[0] } { LOCATION = T5; }
set_pin_assignment { x[1] } { LOCATION = R5; }
set_pin_assignment { x[2] } { LOCATION = T4; }
set_pin_assignment { y[0] } { LOCATION = M3; }
set_pin_assignment { y[1] } { LOCATION = M4; }
set_pin_assignment { y[2] } { LOCATION = N3; }
set_pin_assignment { y[3] } { LOCATION = N4; }
set_pin_assignment { y[4] } { LOCATION = M5; }
set_pin_assignment { y[5] } { LOCATION = P4; }
set_pin_assignment { y[6] } { LOCATION = N5; }
set_pin_assignment { y[7] } { LOCATION = P5; }
实物验证如下:
设置 SW0、SW 1 、SW 2为 x,LED 0、LED 1、LED 2 至 LED 7为 y。
![[1688005577752.png]]
输入为 000,译码后为 0000_0001。
![[1688005878500.png]]
输入为 010,译码后为 0000_0100。
![[1688006080952.png]]
输入为 110,译码后为 0100_0000。
对于普通编码器的设计,同普通译码器一样可以使用 case 语句解决,只不过是将过程反过来而已。
那我们也可以尝试一些别的有意思的事,比如使用 for 语句来实现,虽然 for 对于综合来说有一定的不确定性,但是可以一试。
转换一下思路。对于普通译码(编码)器而言,不过是在某个适当的位置改变了数值,当我们设定默认值为 0000_0000,且在输入为 001,则将默认值中的对应倒数的第二位的 0 变为 1,实现了译码功能,而这个使用了 for 语句,思路是有些类似于软件编程的。
module yimaqi_2( input x,output y);
wire [2:0]x;
reg [7:0]y;
integer i;
always@(*)
begin
y=8'b0000_0000;
for(i=0;i<=7;i=i+1)
begin
if(x==i)begin
y[i]=1;
end
end
end
endmodule
代码同样可以实现。
优先编码器与优先译码器
例 3.1 实现 8-3 优先编码器,并在 SparkRoad 上实现。
解:
Verilog 代码实现如下:
module bianmaqi_youxian( x,y );
input x;
output y;
wire [7:0]x;
reg [2:0]y;
always@(*)
begin
casez(x)
8'b1???_????:y=3'b111;
8'b01??_????:y=3'b110;
8'b001?_????:y=3'b101;
8'b0001_????:y=3'b100;
8'b0000_1???:y=3'b011;
8'b0000_01??:y=3'b010;
8'b0000_001?:y=3'b001;
8'b0000_0001:y=3'b000;
default: y='b0;
endcase
end
endmodule
Testbench 代码如下:
`timescale 1ns/1ps
module bianmaqi_youxian_tb;
reg [7:0]x;
wire [2:0]y;
/*initial
begin
x[7]=8'b0;
x[6]=8'b0;
x[5]=8'b0;
x[4]=8'b0;
x[3]=8'b0;
x[2]=8'b0;
x[1]=8'b0;
x[0]=8'b0;
end*/
initial
begin
x=0;
#200 x=8'b0000_0010;
#300 x=8'd4;
#400 x=8'd8;
#500 x=8'd16;
#600 x=8'd32;
#700 x=8'd64;
#100 x=8'd128;
end
bianmaqi_youxian bianmaqi_youxian_tb_1(
.x(x),
.y(y));
endmodule
波形仿真如下:
![[1688028165040(1).png]]
引脚设置如下:
set_pin_assignment { x[0] } { LOCATION = P8; }
set_pin_assignment { x[1] } { LOCATION = N6; }
set_pin_assignment { x[2] } { LOCATION = P6; }
set_pin_assignment { x[3] } { LOCATION = M6; }
set_pin_assignment { x[4] } { LOCATION = T6; }
set_pin_assignment { x[5] } { LOCATION = T5; }
set_pin_assignment { x[6] } { LOCATION = R5; }
set_pin_assignment { x[7] } { LOCATION = T4; }
set_pin_assignment { y[0] } { LOCATION = N3; }
set_pin_assignment { y[1] } { LOCATION = M4; }
set_pin_assignment { y[2] } { LOCATION = M3; }
实物验证如下:
设置 LED0、LED 1 、LED 2为 y,SW 0、SW 1、SW 2 至 SW 7为 x。
![[1688028907434.png]]
输入为 0000_0000,输出为 000,运行成功。
![[1688029011322.png]]
输入为 1000_0000,输出为 111,运行成功。
![[1688029103850.png]]
输入为 0101_0000,输出为 110,运行成功。