大家好,我是楚生辉,在未来的日子里我们一起来学习硬件描述语言相关的技术,一起努力奋斗,遇见更好的自己!
如果你对数字电路设计和硬件描述语言感兴趣,本文将带你踏上一个令人兴奋的学习旅程。本文以蔡觉平主编教材为主,夏宇闻主编教材为辅,我们将从零基础开始,逐步介绍 Verilog 的核心概念和语法规则。我们将学习如何描述数字电路的结构和行为,以及如何使用 Verilog 进行设计和验证,同时本文修改了教材中多处瑕疵,例如电路逻辑错误,生成了锁存器问题等。无论你是想了解数字电路设计的基本原理,还是希望实际动手实现一些简单的电路,本文都将为你提供所需的知识和指导。
建议与 Verilog硬件描述语言视频教程 搭配食用最佳,看学习资料的同时一定要去HDLBits上刷题
注意:本文旨在提供入门级的 Verilog 参考。如需更深入和专业的知识,请参考官方文档和进阶教材。
下面给出本文的PDF笔记以供下载,在遇到问题或文档发现错误可以反馈[email protected]
。
零基础掌握Verilog HDL笔记下载
Verilog是一种硬件描述语言(HDL),用于对数字电路进行建模、仿真和综合。它是一种用于硬件设计和验证的专用编程语言,允许工程师描述电路的结构和行为,从而实现数字系统的设计和验证。
以下是Verilog的一些重要特点和用途:
硬件描述:Verilog允许工程师以高级抽象的方式描述硬件电路的结构和行为。它可以用来设计各种数字电路,包括处理器、存储器、通信接口、显示控制器等。
行为建模:Verilog允许设计者使用并发和顺序块来描述电路的行为。并发块表示电路中的并行操作,而顺序块表示电路中的时序操作。这使得Verilog非常适合对复杂的数字系统进行建模。
仿真:Verilog允许设计者通过在仿真器中执行代码来验证电路的功能和时序。仿真器可以模拟电路的行为,并可以通过检查波形图来观察信号的变化和交互。
综合:Verilog代码可以被综合工具转换为实际的门级电路,从而可以在FPGA或ASIC中实现。
分层设计:Verilog支持模块化设计,可以将电路划分为小的模块,然后在更高层次上将这些模块组合在一起。这有助于提高设计的可重用性和可维护性。
配置灵活:Verilog允许设计者对电路进行灵活的配置和参数化,从而可以根据不同的需求生成不同的设计。
Verilog在数字电路设计和验证领域广泛使用,是一种功能强大的工具,可以帮助工程师实现复杂的数字系统,并对其进行验证和优化。它已经成为数字系统设计的标准语言之一,并在许多电子设计自动化(EDA)工具中得到广泛支持。
system verilog可以一些数学模型去进行仿真
中间的D触发器,连线太麻烦,使用verilog就可以大大的简化繁琐的操作
module shiftregist4 (clk,din,Reset,qout);
input clk,Reset,din;
output [3:0] qout;
reg [3:0] qout;
always @(posedge clk or posedge Reset)
begin
if(Reset) qout<=4'b0000;
else
begin
qout[3]<=qout[2];
qout[2]<=qout[1];
qout[1]<=qout[O];
qout[0]<=din;
end
end
endmodule
module shiftregist8 (clk,din,Reset,qout);
input clk,Reset,din;
output [7:0] qout;
reg [7:0] qout;
always @(posedge clk or posedge Reset)
begin
if(Reset)qout<=8'b00000000;
else
begin
qout[7:1]<=qout[6:0];
qout 0]<=din;
end
end
endmodule
在Verilog中,"IP core"是指一种已经设计和验证过的可复用的硬件模块。它是由一个供应商(通常是芯片设计公司)开发的,经过了严格的测试和验证,并具有特定的功能和接口。
IP核是以硬件描述语言(如Verilog)编写的,并且可以用于在FPGA(现场可编程门阵列)或ASIC(应用特定集成电路)中实现特定的功能。它们为设计人员提供了一种快速、可靠和可重复使用的方法来集成常见的功能模块,例如处理器核、存储控制器、接口模块等。
IP核通常具有标准化的接口和参数化的配置选项,这使得它们可以方便地集成到不同的设计中,并根据特定的需求进行定制。供应商通常会提供文档和示例代码,以帮助设计人员正确地使用和配置IP核。
使用IP核可以加快设计过程,减少错误,并提高设计的可靠性和可维护性。它们还可以帮助设计人员专注于更高级别的设计任务,而不必从头开始设计和验证底层的硬件模块。
verilog共有19中数据类型,数据类型用来表示数字电路硬件中数据存储与传送元素的,本章先介绍4个最基本的数据类型,分别为:wire类型,reg类型,integer类型,parameter类型
在数字电路中的信号只有两种形态,一种是传输,通过线(wire),另一种是通过寄存器(reg)
supply也即没有电阻,直接连上
highz也即相当于电源断开
+-
size位宽:默认32位,有多少位,多少比特,信号有几根线
base_format进制:默认十进制
创建的时候一定要选择size与base_format
8'b10001101 // 位宽为8位的二进制数10001101
8'ha6 // 位宽为8位的十六进制数a6
5'o35 // 5位八进制数35
4'd6 // 4位十进制数6
4'b1x_01 // 4位二进制数1x01
用的最多的是二进制与16进制
?
来代替,提高程序的可读性4'b10x0 // 位宽为4的二进制数从低位数起第2位位不定值
4'b101z // 位宽为4的二进制数从低位数起第1位位高阻值
12'dz // 位宽为12的十进制数,其值为高阻值
12'd? // 位宽为12的十进制数,其值为高阻值
下划线:分隔数的表达以提高程序的可读性,只能用于具体的数字之间
8'b0011_1010
参数型parameter
在verilog中使用parameter定义常量
语法:parameter 参数名1=表达式,参数名2=表达式,参数名3=表达式
注意:等号右边的表达式只能是常量,也即该表达式只能包含数字或已定义过的表达式
parameter msb=7; // 定义参数msb为常量7
parameter e=15,f=29; // 定义两个参数常量
parameter byte_size=8,byte_msb=byte_size-1; // 用常数表达式赋值
使用场景:parameter常用于定义延迟时间和变量宽度。在模块或实例引用时,可以通过参数传递改变被引用模块已定义的参数
例:模块Decode有两个参数,WIdth,Polarity的值都为1,在Top模块中调用Decode模块的时候,修改其参数的值
module Decode(A,F);
parameter Width=1,Polarity=1;
......
endmodule
module Top;
wire[3:0] A4;
wire[4:0] A5;
wire[15:0] F16;
wire[31:0] F32;
//调用Decode时,参数WIdth,Polarity的值分别为4,0
Decode #(4,0) D1(A4,F16);
//调用Decode时,参数WIdth,Polarity的值分别为5,1
Decode #(5) D2(A5,F32);
endmodule
网络类型表示结构实体(例如门)之间的物理连接,网络类型的变量不能存储值,而它必须受到驱动器(门或连续赋值语句,assign)的驱动。如果没有驱动器连接到网络,那么该变量就是高阻,值为z。
wire在物理结构上只是一根线,在Verilog HDL描述时,对线型变量赋值用assign即可,连续赋值assign语句独立于过程块,所以不能在always过程块中使用assign语句。
assign相当于把x和y两个信号进行连接,真实的物理连接
wire是最常用的net型数据变量,net型数据相当于硬件电路中的各种物理连接,其特点是输出的值随输入值的变化而变化。net 型数据的值取决于驱动的值,对net型变量有两种驱动方式,一种方式是在结构描述中将其连接到一个门元件或模块的输出端;另一种方式是用持续赋值语句assign对其进行赋值。如果net型变量没有连接到驱动,则其值为高阻态z (trireg 除外)。
wire型用于单个门驱动或者多个连续赋值语句驱动的网络型数据
tri型变量用于表示多驱动器驱动的网络型数据
如果wire型与tri型没有定义逻辑强度,那么在多驱动源的情况下,逻辑值会产生不确定值
类型 | 功能 | 可综合性 |
---|---|---|
wire,tri | 连线类型 | ✔ |
wor, trior | 具有线或特性的多重驱动连线 | |
wand, triand | 具有线与特性的多重驱动连线 | |
tril, tri0 | 分别为上拉电阻和下拉电阻 | |
supply1, supply0 | 分别为电源(逻辑1)和地(逻辑0) | ✔ |
trireg | 具有电荷保持作用的连线,可用于电容的建模 |
下面的表中假设两个驱动源强度一致
wire/tri | 0 | 1 | x | z |
---|---|---|---|---|
0 | 0 | x | x | 0 |
1 | x | 1 | x | 1 |
x | x | x | x | x |
z | 0 | 1 | x | z |
wire型数据常用表示以assign关键字指定的组合逻辑信号。verilog程序输入输出信号默认定义为wire型
wire型变量定义:
wire a,b; //声明2个wire型变量a和b(1位)
wire[7:0] databus; //databus(数据总线)的宽度是8位
wire[19:0] addrbus; //addrbus(地址总线)的宽度是20位
reg型式数据存储单元的抽象类别,其对应的硬件电路元件具有状态保持作用,能够存储数据,如触发器,锁存器等,常用于行为级描述,由过程赋值语句对其进行赋值
在物理结构上相对比较麻烦,左端有一个输入端口 D,右端有一个输出端口 Q, 并且 reg 型存储数据需要在CLK(时钟)沿的控制下完成,在 Verilog HDL 描述时也相对麻烦。在always模块内被赋值的每一个信号都要定义成reg型。
reg rega; // 定义一个1位的名为rega的reg变量
reg [3:0] regb; // 定义一个4位的名为regb的reg变量
reg [4:1] regc,regd;// 定义两个4位的名为regc,regd的reg变量
存储器变量可以描述RAM型,ROM型存储器以及reg文件,数组中的每一个单元通过一个数组索引进行寻址,memory型数据是通过扩展reg数据的地址范围来生成的。
语法格式:
reg [n-1:0] 存储器名称[m-1:0];
reg [n-1:0] 存储器名称[m:1];
reg [n-1:0]
表示存储器中每一个存储单元的大小,即存储单元是一个n位的寄存器
[m-1:0]
表示该存储器中有m个这样的存储器
例子:
reg [7:0] mema[255:0] // 定义一个由256个8位寄存器的存储器
reg [n-1:0] rega; // 一个n位的寄存器
reg mema[n-1:0]; // 一个由n个1位寄存器构成的存储器
注意:一个n位的寄存器可以在一条赋值语句里进行赋值,而一个完整的存储器则不行
rega = 0; // 合法
mema = 0; // 非法
想要对存储单元进行读写,必须指定该单元在存储器中的地址
mema[3] = 0; // 给mema中的第3个存储单元的值赋为0
加+,减-,乘*,除/,取模%
算术表达式结果的长度由最长的操作数决定,在赋值语句下,算术操作结果的长度由左边目标长度决定
reg [3:0] A,B,C;
reg [5:0] D;
A=B+C; // 4位
D=B+C; // 6位
电路中运算符表示加法器,乘法器,取模运算器等,这些符号都代表各种电路
更关注输入信号与输出信号之间的关系
有符号数与无符号数的使用
尽量采用无符号数作为程序设计的基础
module arith_tb;
reg[3:0]a;
reg[2:0]b;
initial
begin
a=4'b1111; // 15
b=3'b011; // 3
$display("%b",a*b); // 乘法运算,结果为4'b1101,高位被舍去,等于45的低4位
$display("%b",a/b); // 除法运算,结果为4'b0101
$display("%b",a+b); // 加法运算,结果为4'b0010
$display("%b",a-b); // 减法运算,结果为4'b1100
$display("%b",a%b); // 取模运算,结果为4'b0000
end
endmodule
$display
:相当于在控制台打印
补:二进制的乘法运算,1111×011
也即15×3
,相当于15×2再加15
,而×2相当于左移一位,因此为11110+1111=101101
加法与减法与乘法器是可以直接综合的,也就是说设计工具会直接生成一个加法电路或者是减法电路
有一些专有的库,会可以直接生成取模电路与除法电路
> < >= <=
module rela_tb;
reg[3:0]a,b,c,d;
initial
begin
a=3;b=6;c=1;d=4'hx;
$display(ab); // 结果为假0
$display(a<=c); // 结果为假0
$display(d<=a); // 结果为未知数x
end
endmodule
不管操作数的形式如何,最后生成的结果一定是1bit
规定:任何不定状态的数与已知状态数的比较结果一定是未知状态
== != === !==
:等于 不等于 全等 非全等
比较的结果有三种,真1,假0,不定值x
module equal_tb;
reg[3:0]a,b,c,d;
initial
begin
a=4'b0xx1;
b=4'b0xx1;
c=4'b0011;
d=2'b11;
$display(a==b); // 结果为不定值x
$display(c==d); // 结果为真1
$display(a===b); // 结果为真1
$display(c===d); // 结果为假0
end
endmodule
使用场景:verilog使用场景对电路要求比较松,因此可以使用全等号进行一个约束,将电路中的不定状态转化为0与1
在可靠性高的设计中间,是不允许位宽不统一的数进行比较的,不允许不定状态与高阻状态进行比较,因此我们一般采用==
===
与!==
会对x与z也进行比较,比较是否完全一样
=== | 0 | 1 | x | z | == | 0 | 1 | x | z |
---|---|---|---|---|---|---|---|---|---|
0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | x | x |
1 | 0 | 1 | 0 | 0 | 1 | 0 | 1 | x | x |
x | 0 | 0 | 1 | 0 | x | x | x | x | x |
z | 0 | 0 | 0 | 1 | z | x | x | x | x |
if(A==1'bx) $display("Aisx"); // 当A为x的时候,不执行
if(A===1'bx) $display("Aisx");// 当A为x的时候,执行
&& || !与或非
操作数中存在不定态x,则逻辑运算的结果也是不定态
例:a=4'b1001,则 !a=1'b0 b=4'b0000,则 !b=1'b1
,只要使用这个符号,就相当于形成了一个或非门电路(4输入1输出)不唯一
也即信号的对应位置进行操作
按位取反 ~ 按位与 & 按位或 | 按位异或 ^ 按位同或 ^~
module bit_tb;
reg[2:0]a;
reg[4:0]b;
initial
begin
a=5'b101; // 运算的时候a自动变为5'b00101
b=5'b11101;
$display("%b",~a); // 结果为5b11010
$display("%ob",~b); // 结果为5b00010
$display("%b",a&b); // 结果为5'b00101
$display("%b",alb); // 结果为5b11101
$display("%b",ab); // 结果为5b11000
end
endmodule
与 & 或 | 异或 ^ 以及相应的非操作 ~& ~| ~^
前一位与后一位依次进行的操作
module cut_tb;
reg[5:0]a;
initial
begin
a=6'b101011;
$display("%b",&a); // 结果为1'b0
$display("%b",la); // 结果为1'bl
$display("%b",^a); // 结果为1'b0
end
endmodule
左移运算符<<,右移运算符>>
运算过程是将操作数向左(右)移动,移动的位数由右边的操作数来决定,然后用0来填补移出的空位
module shift_tb;
reg[5:0]a,b,c,d;
reg[7:0]e;
initial
begin
a=6'b101101:
b=a<<2;
c=a>>3;
d=a<<7;
e=a<<2;
$display("%b",b); // 结果为6b110100
$display("%b",c); // 结果为6'b000101
$display("%b",d); // 结果为6'b000000
$display("%b",e); // 结果为8b10110100
end
endmodule
语法格式:<条件表达式>?<表达式1>:<表达式2>
条件表达式的计算结果由真1,假0和未知态x三种,为真1的时候,执行表达式1,为假0的时候,执行表达式2
例:2选1数据选择器
module mux2(in1,in2,sel,out);
input [3:0]in1,in2;
input sel;
output [3:0]out;
reg [3:0]out;
assign out=(!sel)?in1:in2;
//sel为0时out等于inl,反之out等于in2
endmodule
连接运算符:把多个信号的某些位拼接连接成新的信号
语法格式:{信号1的某几位,信号2的某几位,信号3的某几位,信号4的某几位.........}
module con rep_tb;
reg[2:0]a;
reg[3:0]b;
reg[7:0]c;
reg[4:0]d;
reg[5:0]e;
initial
begin
a=3'b101;
b=4'b1110:
c={a,b};
d={a[2:1],b[2:0]}; // 连接操作
e={2{a}}; // 复制操作符
$display("%b",c); // 结果8'b01011110
$display("%b",d); // 结果5b10110
$display("%b",e); // 结果6'b101101
end
endmodule
使用连接与复制运算符要比移位运算符会取得更好的一个效果。
为什么说连接复制运算符要比移位运算符重要?
因为连接运算符适用范围更广,而移位运算符只能补零,连接运算符更灵活
a=3'b101
a<<2 = 5'b10100
等效于
{a[2,0],2'b00}
注意:在使用拼接表达式中不允许存在没有指明位数的信号,因为在计算拼接信号位宽的大小时,必须知道每个信号的位宽。
模块是Verilog语言的基本单元,代表一个基本的功能块,用于描述某个设计的功能或结构以及与其他模块通信的外部端口
模块的端口声明了模块的输入输出端口,其格式如下
module 模块名(口1,口2,口3,口4...........)
在引用模块时,可以用以下两种方式去连接
端口顺序:在引用时,严格按照模块定义的端口顺序来连接,不用表名原模块定义时规定的端口名
例:模块名 <参数值列表> 实例名(连接端口1信号名,连接端口2信号名,连接端口3信号名,............)
使用.
符号,标明原端口是定义时规定的端口名
语法:模块名 <参数值列表> 实例名(.端口1名(连接信号1名),.端口2名(连接信号2名),.端口3名(连接信号3名)............)
优点:提高了程序的可读性与可移植性
MyDesignMk M1(.sin(SerialLn),.pout(ParallelOut),.......)
// .sin与.pout都是M1的端口名,而M1是与MyDesignMk一模一样的模块,已经定义好的MyDesignMk有两个端口sin与pout,与sin口连接的信号是SerialLn,与pout连接的信号是ParallelOut
模块内容包括IO说明,内部信号声明与功能定义
IO说明
输入口:input[信号位宽-1,0] 端口名
输出口:output[信号位宽-1,0] 端口名
输出输出口:inout[信号位宽-1,0] 端口名
IO说明也可以写在端口声明语句里,其格式如下:
module module_name(input port1,input port2,......output port1,output port2,........)
内部信号说明
在模块中用到的和与端口有关的wire和reg类型变量的声明
reg [width-1,0] R变量1,R变量2;
wire [width-1,0] W变量1,W变量2;
功能定义
模块中最重要是逻辑功能定义部分,有下面3种方式可在模块中产生逻辑
使用assign
声明语句,例如:asign a=b&c
使用实例元件,例如:and #2 ul(q,a,b)
用always块
采用assign是最常用的描述组合逻辑电路的方法之一,而always块既可以用于描述组合逻辑电路也可以描述时序电路。用always块的例子生成了一个带有异步清除端的D触发器,always块可用很多描述手段来表达逻辑
always@(posedge clk or posedge clr)
begin
if(clr) q<=0;
else if(en) q<=d;
end
理解:在verilog中,assign,实例元件与always之间是同时并行执行的,但是always块中的代码是按照顺序执行的,因此always也被
称之为过程块的一种(另一种常见的过程快是initial),注意,多个always模块之间是同时执行的,而模块内部的语句是按照顺序执行
initial:只执行一次,而always只要触发条件满足就会执行,直到仿真结束
详细见3.3.1 模块级建模
Verilog HDL 对电路功能的描述有三种方式:结构化描述、数据流描述、行为级描述。三种描述方式抽象级别不同,各有优缺点,相辅相成,需要配合使用。
行为级建模:抽象级别最高,概括能力最强,类似于编程软件的结构。
数据流建模:抽象级别较高,不再需要清晰的刻画具体的数字电路,而比较直观的表达底层逻辑。显示的表达了模块的行为,又隐式的刻画了模块的电路结构。基于信号的驱动方式,描述了信号之间的直接赋值关系。
结构级建模:抽象级别最低,是最接近实际硬件结构的描述方式,需要描述实现功能所需数字电路的逻辑关系,常用于层次化模块间的调用、以及ip核的例化等,使用模块和端口连接的层次结构方式。
特点:不用关注电路实现,只描述数据逻辑,更容易地描述和验证电路的功能。由于抽象级别高,综合效率低,电路可控性差
连续赋值语句不能在语句块与过程语句中使用
一个程序模块可以有多个initial和always过程块,每个initial与always在仿真的一开始同时立刻执行,initial语句只执行一次,而always语句会不断的重复,直到仿真的结束。但always块的语句块是否执行,取决于always的触发条件是否满足,满足一次执行一次,直到仿真结束。
在信号定义方面,无论是时序逻辑还是组合逻辑,只要是在过程语句(initial与always)中,被赋值的信号必须定义为reg类型
当使用initial语句在仿真开始对变量进行初始化的时候,这个初始化过程不需要任何仿真时间,也即在0ns之间内,完成了初始化
使用场景:多用于测试文件和虚拟模块的编写,用来产生仿真测试信号的设置信号记录等仿真环境
initial语句块内被赋值的信号必须定义为reg类型
语法结构:
initial
begin
语句1;
语句2;
....
语句n;
end
例1:使用initia过程语句对变量A,B,C进行赋值
module initial_tb1;
reg A,B,C;
initial
begin
A=0;B=1;C=0;
// #100表示延迟信号
#100 A=1;B=0;
#100 A=0;C=1;
#100 B=1;
#100 B=0;C=0
end
endmodule
例2:用initial语句产生测试信号
module initial_tb2:
reg S1;//被赋值信号定义reg类型
initial
begin
S1=0;
#100 S1=1;
#200 S1=0;
#50 S1=1;
// $finish表示结束,退出仿真环境
#100 $finish;
end
endmodule
语法结构:always <时序控制><语句>
always语句由于在仿真过程中不断活跃,因此只有跟一定的时序控制结合起来使用才有意义。在时序电路中,如果一个always语句没有时序控制,就会产生死锁。always
既可以描述组合逻辑电路,也可以描述时序逻辑电路。
例:仿真器产生死锁
always areg = ~areg;
加上如下的时钟控制语句,就会产生一个周期为2aaa的无限延续信号波形,常用于描述时钟信号
always #aaa areg = ~areg;
敏感列表
有一个或者多个发生变化都能触发执行,可以用or
来连接,也可以用,
问题:敏感事件表为什么没有与的概念?
a or b
包含了ab都变化的情况
@(a or b) 相当于 @(a,b)
注意:组合逻辑电路需要把所有的输入信号放入敏感信号列表,时序电路的时钟必须要写进入
如果组合逻辑语句的输入变量很多,一个一个的输入敏感列表容易出错,因此可以使用 always @(*)
来代替
例1:用always语句描述4选1数据选择器
module mux4_1(out,in0,in1,in2,in3,sel);
output out;
input in0,in1,in2,in3;
input[1:0]sel;
reg out; // 被赋值信号定义为“reg”类型
always @(in0 or in1 or in2 or in3 or sel)
// 敏感信号列表
case(sel)
2'b00: out=in0;
2'b01: out=in1;
2'b10: out=in2;
2'b11: out=in3;
default: out=2'bx;
endcase
endmodule
例2:用always语句描述模256的同步置数,同步清零计数器
module counter1 (out,data,load,reset,clk);
output[7:0]out;
input[7:0]data;
input load,clk,reset;
reg[7:0]out;
// clk上升沿触发
always@(posedge clk)
begin
// 同步清0,低电平有效
if (!reset) out=8'h00;
// 同步置数
else if (load) out=data;
else
out=out+1;
end
endmodule
例3:用always语句描述异步清零计数器
module counter2(clear,clk,out);
output[7:0]out;
input clk,clear;
reg[7:0]out;
always @(posedge clk or negedge clear)
// clk上升沿和clearf低电平清零有效
begin
if(Iclear)
// 异步清零
out=0;
else out=out+1;
end
endmodule
异步时序电路在高性能处理中用的非常小,延迟不可控,一般适用于超低功耗的器件例如心脏起搏器等
串行语句块:begin-end
并行语句块:fork-join
begin-end
可以用在可综合电路的设计中间,后一条语句与前一条延迟的时间是相对延迟
只有执行完上面的语句才能执行下一条的,只有程序执行完,才能跳出语句
begin
语句1
语句2
....
语句n
end
或者
begin:块名
语句1
语句2
....
语句n
end
fork join
块内语句同时执行,每条语句的执行延迟是一个绝对延迟
主要用于测试与仿真,延迟是一个绝对延迟,不可用于可综合电路程序
按照时间顺序,当最后执行的语句运行结束或一个disable语句执行时,跳出该程序块
fork
语句1
语句2
....
语句n
join
或者
fork:块名
语句1
语句2
....
语句n
join
例:分别采用串行语句块与并行语句块产生下面的信号波形
module wave_tb1;
reg wave;
parameter T=10;
initial
begin
wave=0;
#T wave=1;
#T wave=0;
#T wave=1;
#T wave=0;
#T wave=1;
end
endmodule
module wave_tb2;
reg wave;
parameter T=10;
initial
fork
wave=0;
#T wave=1;
#(2*T) wave=0;
#(3*T) wave=1;
#(4*T) wave=0;
#(5*T) wave=1;
join
endmodule
等于号:阻塞性赋值语句
特点:赋值语句执行完成后立刻就改变,在时序电路中会产生意外效果,在串行语句块中按照下顺序执行,在并行语句块中则同时执行,赋值语句执行完成后,块才结束。
<= 非阻塞型赋值语句
特点:在语句块中,上面赋值的变量值不能立刻被下面的语句所用,块结束后才能完成赋值操作
在编写可综合的时序电路模块时,这是最常用的赋值方法
例:使用两种赋值语句确定reg型信号b和c
// 1:非阻塞型
always @(posedge clk)
begin
b<=a;
c<=b;
end
// 2:阻塞型
always @(posedge clk)
begin
b=a;
c=b;
end
总结:只有在行为级建模中间的串行语句块中使用阻塞型赋值语句的时候,才是串行的结构,其余都是并行
注意:
- 阻塞赋值会立即更新信号的值,可以用于表示组合逻辑。
- 非阻塞赋值在同一个时间步中不会立即更新信号的值,适用于描述时序逻辑和触发器的行为,表示下一个时钟周期的赋值操作
- 在顺序执行的过程中,阻塞赋值会覆盖之前的赋值,而非阻塞赋值不会相互影响。
- 在时序逻辑中,一般使用非阻塞赋值,以确保时序逻辑的正确性。
在数字电路中,由于信号输入的延迟,多个信号输入端会有一个电平变化的过程,因此就会造成信号的先后顺序不确定,从而产生尖峰脉冲(噪声),这会导致输出出现不确定性,同时由于不一定会出现尖峰脉冲,但这是一次冒险的行为,会造成后级负载的误工作。
而在verilog中,当有多条语句在同一时刻执行,但由于语句的排列顺序不同,因而造成了输出结果的不同,这就会造成冒险与竞争现象,为了避免这种竞争,需要理解阻塞和非阻塞赋值在执行时间上的差别。而数字电路解决冒险与竞争可以通过接入滤波电容,引入选通脉冲,修改电路设计等方式去解决。
在Verilog HDL中,过程性连续赋值语句有两种类型:赋值,重新赋值语句(assign,deassign)
和强制,释放语句(force,release)
语法格式:
assign <寄存器型变量>=<赋值表达式>;
deassign <寄存器型变量>;
不可综合
例:使用assign与deassign设计异步清零D触发器
module assign_dff(d,clr,clk,q);
input d,clr,clk;
output q;
reg q;
always@(clr)
begin
if(!clr)
assign q=0; //时钟沿来临时,d的变化对q无效。
else
deassign q;
end
always@(negedge clk)q=d;
endmodule
语法格式:
force <寄存器型变量>=<赋值表达式>;
release <寄存器型变量>;
例:force和release使用
作用:如果不对的信号会影响到下一个信号,因此我们在验证与测试的时候可以临时改变量,强制拉到正确或不正确的位置看后续的反应
module force_release(a,b,out);
input a,b;
output out;
wire out;
and #1(out,a,b);
initial
begin
// 强制赋值给out
force out=a|b;
#5;
// 5个时间单位后,释放
release out;
end
endmodule
module release_tb;
reg a,b;
wire out;
force_release U1(a,b,out);
initial
begin
a=1;b=0;
end
endmodule
在数字电路中就相当于二选一的数据选择器,因此可以综合
// 形式1
if(条件表达式) 语句块;
// 形式2
if(条件表达式)
语句块1;
else
语句块2;
// 形式3
if(条件表达式)
语句块1;
else if(条件表达式)
语句块2;
else if(条件表达式)
语句块3;
else
语句块4;
例1:当sel为1时,输出端out得到a的值,当sel为0时,out得到b的值
module mux2_1(a,b,sel,out);
input a,b,sel;
output out;
reg out;
always@(a,b,sel)
begin
if(sel) out=a;
else out=b;
end
endmodule
例2:首先判断a是否大于b,然后判断a是否等于b,蕴含优先级的特性,这种特性会在综合后的电路中体现出来
module compare_a_b(a,b,out);
input a,b;
output [1:0]out;
reg [1:0]out;
always@(a,b)
begin
if(a>b)
out=2'b01;
else if(a==b)
out=2'b10;
else
out=2'b11;
end
endmodule
注意:当if语句里面只有一行操作语句的时候,可以省略begin-end,当有多行操作语句的时候,必须使用begin-end关键字将几条语句包含起来成为一个复合块语句,同时当出现if嵌套的时候,由于else只会与最上面的if进行配对,因此我们可以使用begin-end来确定配对关系,begin-end限定了内嵌if-else语句的范围
例:
if(a>b)
begin
out1<=int1;
out2<=int2;
end
else
begin
out1<=int2;
out2<=int1;
end
相对于if语句只有两个分支而言,case语句是一种可实现多路分支选择控制的语句,比if更直观。一般用于多条件译码电路设计
case(控制表达式)
值1:语句块1
值2:语句块2
.......
值n:语句块n
default:语句块n+1
endcase
case | 0 | 1 | x | z |
---|---|---|---|---|
0 | 1 | 0 | 0 | 0 |
1 | 0 | 1 | 0 | 0 |
x | 0 | 0 | 1 | 0 |
z | 0 | 0 | 0 | 1 |
注意:
例1:用case语句描述的BCD数码管译码
使用case语句时,应包含所有状态,如果没包含全,那么缺省项必须写,否则将产生锁存器,这在同步时序电路设计是不允许的
例2:产生锁存器的case语句
module latch_case(a,b,sel,out);
input a,b;
input [1:0]sel;
output out;
reg out;
always@(a,b,sel)
case(sel)
2'b00:out=a;
2'b11:out=b;
endcase
endmodule
例3:不会产生锁存器的case语句
module non latch_case(a,b,sel,out);
input a,b;
input [1:0]sel;
output out;
reg out;
always@(a,b,sel)
case(sel)
2'b00:out=a;
2'b11:out=b;
default:out=0;
endcase
endmodule
casez与casex
所谓的不关心也即在表达式的比较中,不将该位的状态考虑在内
使用场景:可以灵活的设置对信号的某些位进行比较,后面可以描述组合逻辑电路的真值表与时序逻辑电路的有限状态机
casez | 0 | 1 | x | z | casex | 0 | 1 | x | z |
---|---|---|---|---|---|---|---|---|---|
0 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 1 |
1 | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 1 | 1 |
x | 0 | 0 | 1 | 1 | x | 1 | 1 | 1 | 1 |
z | 1 | 1 | 1 | 1 | z | 1 | 1 | 1 | 1 |
例:分别使用casez与casex进行比较
reg[7:0] ir;
casez(ir)
8'b1???????:instruction1(ir);
8'b01??????:instruction2(ir);
8'b00010???:instruction3(ir);
8'b000001??:instruction4(ir);
endcase
reg[7:0] r,mask;
casex(r^mask)
8'b001100xx:stat1;
8'b1100xx00:stat2;
8'b00xx0011:stat3;
8'bxx001100:stat4;
endcase
概念:锁存器(Latch)是一种对脉冲电平敏感的存储单元的电路,它们可以在特定输入脉冲电平作用下改变状态。锁存,就是把信号暂存以维持某种电平状态。
问题:为什么不要生成锁存器?
在组合逻辑中,有无效状态就会产生锁存器
仅作为语法学习,最好不要用在可综合电路中间
forever永久循环
永久循环中不包含任何条件表达式,只执行无限的循环,直到系统任务$finish
为止,如果需要退出,需要使用disable语句
例:使用forever语句产生时钟信号
module forever_tb;
reg clock;
initial
begin
clock=0;
// 每过50个时间单位,clock就翻转一次
forever #50 clock=~clock;
end
endmodule
repeat语句:产生固定循环次数的循环
例:产生固定周期数时钟信号
module repeat_tb;
reg clock;
initial
begin
clock=0;
// 产生8次的循环,也即4个脉冲
repeat(8) #50 clock=~clock;
end
endmodule
使用场景:时钟网络在数字集成电路的30%以上,有时候需要时钟信号频率关闭,因此会需要这个语法格式
while:表达式为真,就循环
例:使用while循环产生时钟信号
module while_tb;
reg clock;
initial
begin
clock=0;
while(1)
#50 clock=~clock;
end
endmodule
for循环
只能表述逻辑的概念,不是信号量的时候,才可以用for进行设计
例:使用for循环产生时钟信号
module for_clk;
reg clk;
integer i;
initial
begin
clk=0;
for(i=0;i>=0;i=i+1)
#50 clk=~clk;
end
endmodule
在数字电路中,为什么不用循环语句呢?
因为循环的次数是由计数器来定义的,因此循环语句就没有存在的价值
对于基本的单元逻辑电路,使用verilog语言提供的门级器件模型描述电路非常方便,但随着电路复杂度的提高,使用的逻辑门越多,门级描述的工作效率降低。数据流描述主要用较高抽象级别描述电路的逻辑功能,通过逻辑综合软件,自动的将数据流描述转为门级电路。
在数据流建模中,通过使用赋值语句将输入信号与输出信号之间建立关系,描述了电路中信号之间的数据流动。
数据流建模使用的连续赋值语句,由关键词assign开始,后面跟着操作数和运算符等组成的逻辑表达式,可以描述所有的组合逻辑电路
在assign语句中,左边变量类型必须是wire型。
一般用法:
wire [位宽说明] 变量名1,变量名2,......变量名n;
assign 变量名=表达式;
显式连接赋值语句:
<net_declaration><range><name>;
assgin #<delay><name> = Assignment expressionl;
隐式连接赋值语句
<net_declaration><dirve_strength><range>#<delay><name>=Assignment expressionl;
为连线型变量类型
为位宽
赋值驱动强度,只能在隐式连续赋值语句中指定,指定连线型变量的驱动强度
一般使用显式连接赋值语句比较多,可读性比较高
module examplel_assignment (a,b,m,n,c,y);
input[3:0]a,b,m,n;
output[3:0]c,y;
wire[3:0]a,b,m,n,c,y;
assign y=m|n;
assign #(3,2,4)c=a&b;
endmodule
module example2_assignment(a,b,m,n,c,y,w);
input[3:0]a,b,m,n;
output 3:0c,y,w;
wire[3:0]a,b,m,n;
wire[3:0]y=m|n;
wire[3:0]#(3,2,4)c=a&b;
wire(strong0,weak1)[3:0]#(2,1,3)w=(a^b)&(m^n):
endmodule
连续赋值语句是对组合逻辑电路的基本描述
连续赋值语句注意事项:
数字电路是基于mos的电路,既然是集成电路,在信号传输的过程中间一定会有延迟,这个延迟我们称之为硬件电路的延迟。同时这还影响到了器件的工作频率,延迟越小,工作频率越高。随着器件的速度越来越快,延迟非常小的时候,在电路中间就会有一些毛刺现象,就会带到输出端。
消除毛刺的方法有
- 接入滤波电容,利用电容电压不能突变,消弱尖峰幅度,但同时破坏了原有波形
- 引入选通脉冲
- 修改电路逻辑设计
结构描述方式就是将硬件电路描述成一个分级子模块系统,通过逐层调用这些模块构成功能复杂的数字逻辑电路和系统的一种描述方式。在这种描述方式下,组成硬件电路的各个子模块之间的相互层次关系以及相互连接关系都需要得到说明。
优点:代码结构要比电路图的结构简单,连线型变量与模块之间的关系非常清楚
通过调用子模块的不同抽象级别,将模块的结构描述方式分为下面三类
通过调用用户自己描述产生的module模块对电路结构进行说明,模块中被调用模块属于低一层次的模块,如果当前模块不再被其他模块所调用,那么这个模块一定是一个所谓的顶层模块。在一个硬件系统描述中,有且仅有一个顶层模块。
语法:模块名 <参数值列表> 实例名 (端口名列表)
实例名是被调用的模块在当前模块的名称,端口列表指明了模块实例与外部信号的连接
例:一个简单的模块调用
module and_2(a,b,c); // 2输入与门模块
input a,b;
output c;
assign c=a&b;
endmodule
module logic(in1,in2,q); // 顶层模块
input in1,in2;
output q;
and_2 U1(in1,in2,q); // 模块的调用
endmodule
如果同一个模块在当前模块中被调用多次,则需要用不同的实例名来标识,但可在同一条模块调用语句中被定义,只需要各自的实例名和各自的端口互相用逗号隔开即可
模块名 <参数值列表>实例名1 (端口名列表1),
<参数值列表>实例名2 (端口名列表2),
........
<参数值列表>实例名n (端口名列表n),
当对同一个模块调用多次的时候,还可以采用阵列调用的方法,语法格式如下:
<被调用模块名><实例阵列名>[阵列左边界:阵列右边界](<端口连接表>);
其中,阵列左边界与阵列右边界是两个常量表达式,用来指定调用后生成模块实例阵列的大小
例:
module AND(andout,ina,inb); //基本的与门模块
input ina,inb;
output andout;
assign andout=ina&inb;
endmodule
module ex_arrey(out,a,b); // 顶层模块,用来调用与门模块
input[15:0]a,b;
output [15:0]out;
wire [15:0]out;
AND AND_ARREY[15:0](out,a,b);
endmodule
// 代码等价于
AND AND_ARREY15(out[15],a[15],b[15]);
...............
AND AND_ARREY1(out[1],a[1],b[1]);
AND AND_ARREY0(out[0],a[0],b[0]);
模块端口对应方式
例:模块名 <参数值列表> 实例名(连接端口1信号名,连接端口2信号名,连接端口3信号名,............)
2.使用.
符号,标明原端口是定义时规定的端口名
语法:模块名 <参数值列表> 实例名(.端口1名(连接信号1名),.端口2名(连接信号2名),.端口3名(连接信号3名)............)
优点:提高了程序的可读性与可移植性
MyDesignMk M1(.sin(SerialLn),.pout(ParallelOut),.......)
// .sin与.pout都是M1的端口名,而M1是与MyDesignMk一模一样的模块,已经定义好的MyDesignMk有两个端口sin与pout,与sin口连接的信号是SerialLn,与pout连接的信号是ParallelOut
模块参数值
使用下面两种方法可以改变模块实例的参数值,分别是使用带有参数的模块实例语句修改参数值和使用定义参数语句(defparam语句
)修改参数值
使用带有参数的模块实例语句修改参数值:在这种方法中,模块实例的本身就能指定新的参数值,其语法格式是
模块名<参数值列表>调用名(端口名列表)
,其中参数值列表又分为位置对应和名称对应两种方式
module para1(C,D);
parameter a=1;
parameter b=1;
endmodule
module para2;
// 语句1:位置对应
para1 #(4,3) U1(C1,D1);
// 语句2:名称对象
para1 #(.b(6),.a(5)) U2(C2,D2);
endmodule
就是被调用的模块中存在参数,但我又想在调用时改变参数的值。可以看到被调用的模块para1中定义了两个参数a=1,b=2;但我想在调用它时,想让a=4,b=3,所以在调用时,把4和3放在参数列表传递进去
使用定义参数语句(defparam语句
)修改参数值
语法格式:
defparam 参数名1=参数值1,
参数名2=参数值2,
......
参数名n=参数值n;
注意:参数名必须采用分级路径的形式,才能锁定需要修改的参数是哪个模块中的
例:使用defparam语句修改参数值
module halfadder(a.b.s.c);
// 半加器模块
halfadder
input a,b;
output c,s;
parameter xor_delay=2,and_delay=3;
assign #xor_delay s=a^b;
assign #and_delay c=a&b;
endmodule
module fulladder(p,q,ci,co,sum);
// 全加器模块
fulladder
input p,q,ci;
output co,sum;
parameter or_delay=1;
wire w1,w2,w3;
halfadder U1(p,q,w1,w2)
halfadder U2(ci,w1,sum,w3);
or #or_delay U3(C0,W2,w3);
endmodule
module top1(top1a,top1b,top1s,top1c);
// 修改半加器模块参数的模块top1
input top1a.top1b.
output top1s,top1e.
defparam U1.xor_delay=4,U1.and_delay=5;
// 名为U1的半加器实例中对参数xor_delay和参数and_delay值的修改
halfadder U1(top1a.top1b.top1s.top1c)
endmodule
U2比U1层次高,可以U2.U1去更改U1里面的参数,在实际电路中就可以用最顶级的模块修改所有低层次的模块里的参数
Verilog内置26个基本元件,其中14个位门级元件,12个位开关元件
- 基本门
- 多输入门:and,nand,or,nor,xor,xnor
- 多输出门:buf,not
- 三态门:buif(),bufif1,notif(),notif1()
- mos开关:nmos,pmos,cmos,rnmos,rpmos,rcmos
- 双向开关:tran,tranif(),tranif1,rtran,rtranif(),rtranif1
- 上拉,下拉电阻:pullup,pulldown
门级模块的调用
门类型 <实例名>(<输出端口>,<输入端口1>,<输入端口2>........<输入端口n>)
and A1 (out1,in1,in2);
or 02 (a,b,c,d);
xor X1(x_out,p1,p2);
元件名 <实例名>(<输出端口1>,<输出端口2>........<输出端口n>,<输入端口>)
not NOT_1 (out1,out2,in);
buf BUF_1 (bufout1,bufout2,bufout3,bufin);
元件名 <实例名>(<数据输出端口>,<数据输入端口>,<控制输入端口>)
控制输入端口也即控制该元件什么时候工作
bufif1 BF1 (data_bus,mem_data,enable);
bufif0 BFO (a,b,c);
notif1 NT1 (out,in,ctrl);
notif0 NTO (addr,a bus,select);
例:使用门机元件实现如图所示的译码器
module
decoder2_4(in0,in1,en,outo,out1,out2,0ut3)
output out0,out1,out2,out3;
input in0,in1,en;
wire wire1,wire2;
not U1(wire1,in0),
U2(wire2,in1);
nand U3(outo,en,wire1,wire2),
U4(out1,en,wire1,in1),
U5(out2,en,in0,wire2),
U6(out3,en,in0,in1);
endmodule
注意:输入输出引脚不要超过10
verilog语言提供了十几种开关机单元,他们是实际的MOS管的抽象,这些开关级基本分为两大类MOS开关与双向开关,而每一大类又可以分为电阻型(前缀用r表示)和非电阻型。本节主要以非电阻型开关为例,介绍MOS开关和双向开关。
MOS开关:模拟了实际的MOS器件的功能,包括nmos,pmos,cmos三种
nmos与pmos的实例化语言格式为nmos或pmos 实例名 (out,data,control)
cmos开关的实例化语言格式为:cmos 实例名 (out,data,ncontrol,pcontrol)
双向开关:MOS开关只提供了信号的单向驱动能力,为了模拟实际的具有双向驱动能力的门级开关,因此引入双向开关
每个脚都可以被声明为inout类型,都可以作为输入驱动另一脚,也可以作为输出被另一脚驱动
包括无条件双向开关(tran)和有条件双向开关(tranif0,tranif1)
无条件双向开关例化语言格式为:tran 实例名 (inout1,inout2);
有条件双向开关例化语言格式为:tranif0或tranif1 实例名 (inout1,inout2,control);
例:基本的nmos开关电路
在 Verilog 中,设计思想指的是在编写代码时所采用的方法和策略,以实现特定的功能和满足设计需求。而可综合性是指代码能够被综合工具转换为等效的硬件电路。
总目标:在计算机上将代码通过综合工具生成一个简单的元器件的连接关系(netlist)
例1:描述一个256位的计数器
可综合程序描述方式
module counter(count,clk,reset);
output count;
input clk,reset;
reg[7:0] count;
always @(posedge clk)
if(!reset)
count<=0;
else if(count == 8'b11111111)
count<=0;
else count = count+1;
endmodule
常见的错误描述方式
module counter(count,clk,reset);
output count;
input clk,reset;
reg[7:0] count;
integer i;
always @(posedge clk)
begin
if(!reset) count<=0;
else
for(i=0;i<=255,i=i+1)
count <= count+1;
end
endmodule
同时Verilog HDL的电路描述方式具有多样性,这也决定了对于电路设计的多样性。
例2:用Verilog HDL设计数字多路选择器
采用真值表形式的代码
module MUX(out,data,sel);
output out;
input[3:0] data;
input[1:0] sel;
reg out;
always @(data or sel)
case(sel)
2'b00:out<=data[0];
2'b01:out<=data[1];
2'b10:out<=data[2];
2'b11:out<=data[3];
endcase
endmodule
采用逻辑表达式形式的代码
module MUX(out,data,sel);
output out;
input[3:0] data;
input[1:0] sel;
wire w1,w2,w3,w4;
assign w1=(~sel[1])&(~sel[0])&data[0];
assign w2=(~sel[1])&sel[0]&data[1];
assign w3=sel[1]&(~sel[0])&data[2];
assign w4=sel[1]&sel[0]&data[3];
assign out=w1|w2|w3|w4;
endmodule
采用结构性描述的代码
module MUX(out,data,sel);
output out;
input [3:0] data;
input [1:0] sel;
wire w1,w2,w3,w4,w5,w6;
not U1(w1,sel[1]);
U2(w2,sel[0]);
and U3(w3,w1,w2,data[0]);
U4(w4,w1,sel[0],data[1]);
U5(w5,sel[1],w2,data[2]);
U6(w6,sel[1],sel[0],data[3]);
or U7(out,w3,w4,w5,w6);
endmodule
在现阶段,作为设计人员熟练掌握Verilog HDL程序设计的多样性和可综合性,是至关重要的。作为数字集成电路的基础,基本数字逻辑电路的设计是进行复杂电路的前提。本章通过对数字电路中基本逻辑电路的Verilog HDL程序设计进行讲述,掌握基本逻辑电路的可综合性设计,为具有特定功能的复杂电路的设计打下基础。
组合电路的设计需要从以下几个方面考虑:
特点:电路中任意时刻的稳态输出仅仅取决于该时刻的输入,而与电路原来的状态无关。
描述组合逻辑电路有四种方式:结构描述、逻辑代数、真值表、抽象描述。
问题:卡诺图为什么忽略?
因为卡诺图优化的方式被计算机完全取代了,也即全部交给综合工具去做
例1:设计一个3个裁判的表决电路,当两个或两个以上裁判同意时,判决器输出“1”,否则输出“0”
真值表方式
真值表是对电路功能最直接和简单的描述方式。根据电路的功能,可以通过真值表直接建立起输出与输入之间的逻辑关系。例4.2-1有三个输入端A、B、C和一个输出端OUT。
module design(OUT,A,B,C);
output OUT;
input A,B,C;
reg OUT;
always @(A or B or C)
case({A,B,C})
3'b000:OUT<=0;
3'b001:OUT<=0;
3'b010:OUT<=0;
3'b100:OUT<=0;
3'b011:OUT<=1;
3'b101:OUT<=1;
3'b110:OUT<=1;
3'b111:OUT<=1;
endcase
endmodule
逻辑代数方式
对于组合电路的另一种表达方法是逻辑代数方法。主要思想是将真值表用卡诺图表示,然后化简电路,得出逻辑函数表达式。通过对卡诺图的化简,可以得到组合电路逻辑输出与输入之间的逻辑
函数表达式:OUT=AB+BC+AC
module design(OUT,A,B,C);
output OUT;
input A,B,C;
assign OUT=(A&B)|(B&C)|(A&C);
endmodule
结构性描述
结构性描述方式是对电路最直接的表示,早期的数字电路设计通常采用的原理图设计实际上就是种结构性描述方式。
module design(OUT,A,B,C);
output OUT;
input A,B,C;
and U1(w1,A,B);
and U2(w2,B,C);
and U3(w3,A,C);
or U4(OUT,w1,w2,w3);
endmodule
抽象型描述方式(RTL的抽象描述方式)
从电路出发,将三个输入的判决相加,和大于1表示投票成功,如果前面的信号不只是三个,这样的描述方式就大大的简化了步骤
module desingn(OUT,A,B,C);
output OUT;
input A,B,C;
wire [1:0]sum;
reg OUT;
assign sum=A+B+C;
always @(sum)
if (sum>1) OUT=1;
else OUT=0;
endmodule
EDA综合工具可以将Verilog HDL程序综合成物理电路形式,通过电路优化,可以得到符合设计要求的最简化电路。采用Synplify软件对上面四种方法设计的Verilog HDL程序进行综合(采用Altera公司Stratixll器件),可以得相同的最简化电路。
数字加法器是最为常用的一种数字运算逻辑,被广泛用于计算机、通信和多媒体数字集成电路中
例1:2输入1bit信号全加器
如果运算考虑了来自低位的进位那么该运算就为全加运算,实现全加运算的电路称为全加器。
代数逻辑表达式为:
SUM = A ⊕ B ⊕ C_IN
C_OUT = AB + (A ⊕ B) C_IN
电路图为
Verilog HDL可以用不同的描述方式写出一位全加器,其综合电路是相同的,仅仅是描述风格不同。
利用连续赋值语句实现
module one_bit_fulladder(SUM,C_OUT,A,B,C_IN);
input A,B,C_IN;
output SUM,C_OUT;
assign SUM=(A^B)^C_IN;
assign C_OUT=(A&B)|((A^B)&C_IN);
endmodule
利用行为描述实现
module one_bit_fulladder(SUM,C_OUT,A,B,C_IN);
input A,B,C_IN;
output SUM,C_OUT;
assign {C_OUT,sum}=A+B+C_IN;
endmodule
{C_OUT, SUM}
表示一个联合赋值,将 C_OUT 分配给高位,SUM 分配给低位。
例:4位超前进位加法器
超前进位加法器是一种高速加法器,每级进位由附加的组合电路产生,高位的运算不需等待低位运算完成,因此可以提高运算速度。
module four_bits_fast_addder(sum_out,c_out,a,b,c_in);
input [3:0] a,b // 加数,被加数
input c_in; // 来自前位的进位
output [3:0] sum_out;
output c_out; // 进位输出
wire[4:0] g,p,c; // 产生函数,传输函数和内部进位信号
assign c[0]=c_in;
assign p=a|b;
assign g=a&b;
assign c[1]=g[0]|(p[0]&c[0]);
assign c[2]=g[1]|(p[1]&(g[0]|(p[0]&c[0])));
assign c[3]=g[2]|(p[2]&(g[1]|(p[1]&(g[0]|(p[0]&c[0])))));
assign c[4]=g[3]|(p[3]&(g[2]|(p[2]&(g[1]|(p[1]&(g[0]|(p[0]&c[0])))))));
assign sum_out=p^c[3:0];
assign c_out=c[4];
endmodule
C1到C4都是进位标志,等效Cn=G(n-1)+P(n-1)C(n-1)
接下来就是套娃,可以根据CO直接算出Cn的值,好处就是并行输出效率高,缺点电路越往下越复杂
数据比较器是用来对两个二进制数的大小进行比较,或检测是否相等的逻辑电路。数据比较器包含两个部分:一是比较两个数的大小;二是检测两个数是否一致。
例:设计4位数值比较器
module four_bits_compl(F,A,B,C);
parameter comp_width = 4;
output [2:0] F;
input [2:0] C; // 控制信号C
input [comp_width-1:0] A;
input [comp_width-1:0] B;
reg [2:0] F;
always @(A or B or C)
if(A>B) F=3'b100;
else if(A<B) F=3'b001;
else F=C;
endmodule
根据开关的控制,在多路输入数据中选择一路输出
例:8选1数据选择器
可以使用多个2选1数据选择器方式进行设计,分成3级实现
module mux8to1_2(d_out,d_in,sel);
output d_out;
input [7:0] d_in;
input [2:0] sel;
wire[3:0] w1;
wire[1:0] w2;
assign w1=sel[0]?{d_in[7],d_in[5],d_in[3],d_in[1]}:{d_in[6],d_in[4],d_in[2],d_in[0]};
assign w2=sel[1]?{w1[3],w1[1]}:{w1[2],w1[0]};
assign d_out=sel[2]?w2[1]:w2[0];
endmodule
使用抽象的描述方式,直接case语句就很容易得到
module mux8to1_2(out,sel,data_in);
output out;
input [7:0] data_in;
input [2:0] sel;
reg out;
always @(data_in or sel)
case(sel)
3'b000:out<=data_in[0];
3'b001:out<=data_in[1];
3'b010:out<=data_in[2];
3'b011:out<=data_in[3];
3'b100:out<=data_in[4];
3'b101:out<=data_in[5];
3'b110:out<=data_in[6];
3'b111:out<=data_in[7];
endmodule
用文字、符号或数码表示特定对象的过程称为编码。在数字电路中用二进制代码表示有关的信号称为二进制编码。
实现编码操作的电路叫做编码器。
为什么要用编码器?:信号非常多,如果每一个信号都要产生信号位的话,这会增加处理器的负担
例1:3位二进制8-3线编码器
例2:8-3线优先编码器
module mux8to3_p data_out,Ys,Yex,sel,data_in);
output [2:0]data_out;
output Ys,Yex;
input [7:0]data_in;
input sel;
reg [2:0]data_out;
reg Ys,Yex;
always @(data_in or sel)
if (sel) {data_out,Ys,Yex}={3'b111,1'b1,1'b1};
else
begin
casex(data_in)
8'b0???????:{data_out,Ys,Yex}={3'b000,1'b1,1'b0};
8'b10??????:{data_out,Ys,Yex}={3'b001,1'b1,1'b0};
8'b110?????:{data_out,Ys,Yex}={3'b010,1'b1,1'b0};
8'b1110????:{data_out,Ys,Yex}={3'b011,1'b1,1'b0};
8'b11110???:{data_out,Ys,Yex}={3'b100,1'b1,1'b0};
8'b111110??:{data_out,Ys,Yex}={3'b101,1'b1,1'b0};
8'b1111110?:{data_out,Ys,Yex}={3'b110,1'b1,1'b0};
8'b11111110:{data_out,Ys,Yex}={3'b111,1'b1,1'b0};
8'b11111111:{data out,Ys,Yex}={3'b111,1'b0,1'b1};
endcase
end
endmodule
译码思想:编码的逆过程,将二进制代码所表达的信息翻译成状态新奇,将输入信号映射到特定输出的过程。译码器用于将一个或多个输入值与输出值进行关联,以实现特定的逻辑功能或信号转换。
问题:为什么3-8译码器被广泛的使用?
只需要3根引脚,就可以解决8条信号线的问题
代码举例:采用抽象级描述方法
module decode 2to4 (Y,E,A);
output [3:0] Y;
input [1:0] A;
input E;
reg [3:0] Y;
always @(E or A)
case ({E,A})
3'b1??:Y=4'b0000;
3'b000:Y=4'b0001;
3'b001:Y=4'b0010;
3'b010:Y=4'b0100;
3'b011:Y=4'b1000;
default:Y=4'b0000;
endcase
endmodule
奇偶校验器的功能是检测数据中包含“1”的个数是奇数还是偶数。在计算机和一些数字通信系统中,常用奇偶校验器来检查数据传输和数码记录中是否存在错误。
奇偶校验包含两种方式:奇校验和偶校验。奇校验保证传输数据和校验位中“1”的总数为奇数。如果数据中包含奇数个“1”,则校验位置“0”,如果数据中包含偶数个“1”,则校验位置“1”。
偶校验保证传输数据和校验位中“1”的总数为偶数。如果数据中包含奇数个“1”,则校验位置“1”,如果数据中包含偶数个“1”,则校验位置“0”。奇偶校验只能检测部分传输错误,它不能确定错误发生在哪位或哪几位,所以不能进行错误校正。当数据发生错误时只能重新发送数据。
使用场景:运用于能够重新操作的计算机硬件中,例如SCSI总线和微处理器中的高速缓存,在发生错误的时候,这些部件可以丢掉数据,重新获取数据。
串行通讯的数据帧,串行通信验证数据的正确性
实际开发:芯片是在硅上面进行,在高速的集成电路中间,一般不会使用8为和16位去排总线,一般采用9位或者18位,也即每8位增加1bit的校验位,就可以确保输入的信号更可能正确
例:8bits奇偶校验器
结构性描述代码
module checker(Fod,Fev,b);
output Fod,Fev;
input [7:0] b;
wire w1,w2,w3,w4,w5,w6;
xor U1(w1,b[0],b[1]);
xor U2(w2,b[2],b[3]);
xor U3(w3,b[4],b[5]);
xor U4(w4,b[6],b[7]);
xor U5(w5,w1,w2);
xor U6(w6,w3,w4);
xor U7(Fod,w5,w6);
not U8(Fev,Fod);
endmodule
采用抽象性描述代码
module checker(Fod,Fev,b);
output Fod,Fev;
input [7:0] b;
assign Fod=^b; // 按位异或
assign Fev=~Fod; // 按位取反
endmodule
与组合逻辑电路不同,时序逻辑电路的输出不仅与当前时刻输入变量的取值有关,而且与电路的原状态,即与过去的输入情况有关。
时序逻辑电路有两个特点:
所有的时序电路都可以拆分成组合逻辑电路与存储电路
流程:状态转移图->三大方程->逻辑电路
例:设计一个111的序列检测器,当输入三个或三个以上的1时,电路输出为1,否则为0
So:初始状态,表示电路还没有收到一个有效的1。
S1:表示电路收到了一个1。
S2:表示电路收到了连续两个1。
S3:表示电路收到了连续三个1。
module checker(Z,X,clk);
parameter S0=2'b00,S1=2'b01,S2=2'b11,S3=2'b10;
output Z;
input X,clk;
reg [1:0] state,next_state;
reg Z;
always @(X,state)
case(state)
S0:
if(X)
begin
next_state<=S1;
Z=0;
end
else
begin
next_state<=S0;
Z=0;
end
S1:
if(X)
begin
next_state<=S2;
Z=0;
end
else
begin
next_state<=S0;
Z=0;
end
S2:
if(X)
begin
next_state<=S3;
Z=1;
end
else
begin
next_state<=S0;
Z=0;
end
S3:
if(X)
begin
next_state<=S3;
Z=1;
end
else
begin
next_state<=S0;
Z=0;
end
endcase
always @(posedge clk)
state<=next_state;
endmodule
方式二:基于状态化简的结构性描述方法
对状态转移图化简,仅剩三个状态,需要两位二进制表示,即需要两个D触发器储存状态。设Q1表示高位寄存器的输出,Q0表示低位寄存器的输出将状态的跳转以及输出Z用卡诺图表的形式示出,如下:
由卡诺图可以得出电路的输出方程与状态方程
// 序列检测器模块
module checker(Z,X,clk);
output Z;
input X,clk;
wire w1,w2;
DFF U1(.clk(clk),.D(X),.Q(w1));
DFF U2(.clk(clk),.D(w1),.Q(w2));
assign Z = X & w1 & w2;
endmodule
// D触发器模块
module DFF(Q,D,clk);
output Q;
input D,clk;
reg Q;
always @(posedge clk)
Q<=D;
endmodule
使用综合工具对上面的代码进行综合,可以得到下面的电路
ㅤㅤㅤㅤ
方法三:抽象性描述
只要信号中有连续三个1,就输出为1,之前的状态可以用移位寄存器来存储,输入X作为移位寄存器的输入,然后只需要在三个寄存器连接一个与门,也即只有三个状态都为1的时候,才会输入1。
module checker(Z,X,clk)
output Z;
input X,clk;
reg [2:0] q;
reg Z;
always @(posedge clk)
q<={q[1:0],X};
always @(posedge clk)
if(q==3'b111) Z=1;
else Z=0;
endmodule
此处指出教材中(蔡觉平老师编写)的一个逻辑错误的点,书上P91
根据代码逻辑,当 q 的值等于 3’b111 时,Z 被赋值为 1,否则 Z 被赋值为 0。然而,第二个 always 块也使用了 posedge clk 触发条件,这意味着只有在时钟上升沿触发时才会执行。这可能会导致 Z 的赋值与预期不符。
如果想要在 q 变为 3’b111 时立即更新 Z 的值,可以将第二个 always 块修改为组合逻辑,如下所示:
module checker(Z, X, clk);
output Z;
input X, clk;
reg [2:0] q;
reg Z;
always @(posedge clk)
q <= {q[1:0], X};
always @(*)
if (q == 3'b111) Z = 1;
else Z = 0;
endmodule
触发器是时序电路的最基本电路单元,主要有D触发器、JK触发器、T触发器和RS触发器等。根据功能要求的不同,触发器还具有置位、使能、选择等功能
最简D触发器:D触发器是数据集成电路中用的最广泛的触发器
module DFF(q,clk,data_in);
output q;
input clk,data_in;
reg q;
always @(posedge clk)
q<=data_in;
endmodule
输入端的数据D在时钟的上升沿被送入触发器,使得Q=D
例1:带复位端口的D触发器
同步清零
module DFF_rst(q,clk,rst_n,data_in);
output q;
input clk,rst_n,data_in;
reg q;
always @(posedge clk)
if (!rst_n)
q<=0;
else
q<=data_in;
endmodule
异步清零
module DFF_rst(q,clk,rst_n,data_in);
output q;
input clk,rst_n,data_in;
reg q;
always @(posedge clk or rst_n)
if (!rst_n)
q<=0;
else
q<=data_in;
endmodule
同步信号的reset不写在敏感列表
异步信号只需要rst_n发生改变,且rst_n等于0的时候,清零,因此清零信号必须写在敏感列表中
例2:复杂功能D触发器,同步清零,置1和异步清零,置1共同在一个触发器上的复杂D触发器
module DFF_1 (q,clk,rst_n1,rst_n2,data_in);
output q;
input clk,rst_n1,rst_n2,data_in;
reg q;
always @(posedge clk)
if (!rst_n1)
q<=0;
else
q<=data_in;
always @(posedge clk or negedge rst_n2)
if (!rst_n2) q<=0;
else
q<=data_in;
endmodule
例3:T触发器
T触发器的逻辑功能为:当时钟的有效边沿到来时,如果T=1,则触发器翻转;如果T=0,则触发器的状态保持不变。R为复位端,异步复位,低电平有效。
module TFF(data_out,T,clk,rst_n);
output data_out;
input T,clk,rst_n;
reg data_out;
always @(posedge clk or rst_n)
if (!rst_n)
data out<=1'b0;
else if (T)
data_out<=~data_out;
endmodule
在时序逻辑电路中,如果在if或case语句中的条件没有覆盖所有可能的情况,有时可能会导致锁存器的产生。这种情况通常被称为未编码的状态(Undecoded State)或未定义的行为(Undefined Behavior)。
当条件没有覆盖所有可能的情况时,如果没有提供默认的赋值或者默认的行为,那么在某些情况下,逻辑电路可能会处于不确定的状态,这可能导致锁存器的产生。锁存器是一种在时序逻辑电路中不希望出现的状态,因为它会引起电路的不稳定性和不可预测性。
为了避免锁存器的产生,应该始终确保if或case语句中的条件覆盖了所有可能的情况,并提供适当的默认赋值或行为。在使用case语句时,应该始终包含一个默认的情况(default),以处理未覆盖的情况。如果使用if语句,可以考虑添加else语句来提供默认的赋值或行为。
在编写时序逻辑电路时,要仔细检查条件和逻辑,确保所有可能的情况都被处理,以避免产生锁存器和未定义的行为。同时,使用模拟和数字电路仿真工具进行验证,以确保电路在各种情况下的正确性和稳定性
综上所述,T触发器的代码确实会产生锁存器,因此我们可以对此做出修改,修改后的代码如下:
module TFF(data_out,T,clk,rst_n);
output data_out;
input T,clk,rst_n;
reg data_out;
always @(posedge clk or rst_n)
if (!rst_n)
data out<=1'b0;
else if (T)
data_out<=~data_out;
else
data_out <= data_out; // 添加 else 语句,保持 data_out 不变
endmodule
计数器是应用最广泛的逻辑部件之一,计数器可以统计输入脉冲的个数,具有计时,计数,分频,定时,产生节拍脉冲等功能。
例:由D触发器实现的二进制计数器,也即二分频计数器
module comp2bit(Q,clk,rst_n);
output Q;
input clk,rst_n;
reg Q;
always @(posedge clk or rst_n)
if (!rst n)
Q<=1'b0;
else
Q<=~Q;
endmodule
任意进制计数器
任意模值M的计数器,第一步需要确定计数器所需要触发器个数。N个触发器对应了2的N次方个状态。应有2的N次方>M。任意模值计数器选取满足条件的最小N,N为计数器中触发器的个数。有两种方法实现:反馈清零法和反馈置数法。
反馈清零法:当加到某一位置的时候,把当前的值清掉,然后重新开启计数。
module comp_11(count,clk,rst);
input clk,rst;
output [3:0] count;
reg[3:0] count;
always@(posedge clk)
if(rst) count<=4'b0000;
else if(count==4'b1010)
count<=4'b0000;
else
count<=count+1;
endmodule
反馈置位法:从一个有效值开始,来一个时钟就减去一个,一直减到零
module comp_11(count,clk,rst);
input clk,rst;
output [3:0] count;
reg[3:0] count;
always@(posedge clk)
if(rst) count<=4'b0000;
else if(count==4'b0000)
count<=4'b1010;
else
count<=count-1;
endmodule
移位寄存器可以用来实现数据的串并转换,也可以构成移位行计数器,进行计数、分频,还可以构成序列码发生器、序列码检测器等,它也是数字系统中应用非常广泛的时序逻辑部件之一
例:环形移位寄存器:N为环形寄存器由N个移位寄存器组成,它可以实现环形移位
在这个例子中,将每个寄存器的输出作为下一个寄存器的输入,并将高位寄存器的输出作为循环的输入
module shiftregist1(D,clk,rst_n);
parameter shiftregist_width=4;
input clk,rst_n;
output [shiftregist_width-1:0] D;
reg [shiftregist_width-1:0] D;
always @(posedge clk)
if(!rst_n)
D<=4'b0000;
else
D<={D[shiftregist_width-2:0],D[shiftregist_width-1]};
endmodule;
解释:D <= {D[shiftregist_width-2:0], D[shiftregist_width-1]};
将 D
进行左移操作,将最高位的值赋给最低位,其它位依次向左移动。这样的移位操作就可以实现数据的存储与移动,适用于各种数字电路应用中。
问题:为什么移位寄存器通过最高位赋给最低位,就能实现数据的存储与移动?
移位寄存器是一种线性的存储器件,由一组触发器组成,每个触发器存储一个位(0或1)。当移位寄存器接收到一个时钟脉冲时,所有触发器中的位都向前移动一位,同时新的数据可以从最高位输入到最低位。通过连续地进行这样的移位操作,数据在寄存器内部逐位地向左移动,最后被存储在最高位触发器中。这种操作可以用于各种应用,例如数据的串行传输、位操作和状态机设计等。
移位寄存器(Shift Register)是一种常见的时序电路,由触发器和数据输入端构成,能够按照特定的时钟信号将输入数据进行平移或循环移位。移位寄存器在高速电路设计中具有以下几个优点,使其被广泛应用:
并行-串行转换:移位寄存器可以将并行输入数据转换为串行输出数据。这在通信系统中非常有用,可以实现并行数据的传输和处理。通过将多个移位寄存器级联,可以实现更宽的数据宽度。
时钟同步:移位寄存器的操作是同步于时钟信号的。在高速电路中,时钟信号是非常重要的,能够确保电路中的各个部分按照同步的时序进行操作,避免时序冲突和数据错误。移位寄存器的时钟同步特性使得它能够适用于高速数据处理和传输。
级联连接:多个移位寄存器可以通过级联连接形成更大的数据宽度或更深的位移操作。这种级联连接的结构使得移位寄存器可以灵活地适应不同的设计需求,并扩展其功能。
数据存储和缓存:移位寄存器可以存储数据,并在需要时提供缓存功能。在高速数据处理中,移位寄存器常用于数据暂存、流水线操作、时序控制等场景,能够提高数据处理的效率和速度。
综上所述,移位寄存器由于其并行-串行转换、时钟同步、级联连接和数据存储等特性,使其在高速电路设计中具有重要的作用。它可以用于数据传输、流水线操作、数据缓存等各种场景,提供了高速、可靠的数据处理能力。
简介:序列信号发生器是一种用于生成特定序列模式的电路。它可以根据预定义的规则和条件生成不同的序列信号。通过在Verilog中描述这些规则和条件,可以实现各种序列信号的生成,例如特定的数据序列、状态序列或控制序列。
按照序列循环长度M和触发器数目n的关系一般可以分为三种:
序列信号发生器是能够产生一组或多组序列信号的时序电路,它可以由纯时序电路构成,也可以由包含时序和组合逻辑的混合电路构成。
小m序列,与M序列相比,少了一个全0的状态
主要讲任意循环长度序列
例1:设计一个产生10011序列的信号发生器
由移位寄存器构成
由于移位寄存器输入和输出信号之间没有组合电路,不需要进过组合逻辑的反运算,因此这种序列产生电路的工作频率很高。缺点是移位寄存器长度取决于列长度,因此占用电路的面积很大。
module signal_maker(out,clk,load,D);
parameter M=6;
output out;
input clk,load;
input [M-1,0] D;
reg [M-1,0] Q;
initial Q=6'b10011;
always @(posedge clk)
if(load) Q<=D;
else Q<={Q[M-2,0],Q[M-1]};
assign out=Q[M];
endmodule
由移位寄存器和组合逻辑电路构成
反馈移位型序列码发生器的结构框图如图所示,它由移位寄存器和组合逻辑网络组成,从移位寄存器的某一输出端可以得到周期性的序列码
步骤:
与上面的序列信号发生器相比,各个寄存器的输出需要经过反馈网络,然后才连接到移位寄存器的输入端。因此,电路的速度必然下降,但反馈网络的好处在于它可以节省寄存器。
对于“100111”序列的信号发生器。
首先,确定所需移位寄存器的个数n。因M=6,故n≥3。
然后,确定移位寄存器的六个独立状态
按照规律每三位一组,划分六个状态为100、001、011、111、111、110。其中状态111重复出现,故取n=4,并重新划分状态,得到:1001、0011、0111、1111、1110、1100。因此确定n=4。
第三,列态序表和反馈激励函数表,求反馈函数F的表达式。首先列出态序表,然后根据每一状态所需要的移位输入即反馈输入信号,列出反馈激励函数表,如下表所示。求得反馈激励函数:
F = Q3` + Q0`Q1` + Q2`Q3
module signal_maker(out,clk,load,D);
parameter M=4;
output out;
input clk,load;
input [M-1:0] D;
reg [M-1:0] Q;
wire w1;
always @(posedge clk)
if (load)
Q<=D;
else
Q<={Q[M-2:0],w1};
assign w1=(~Q[3])|((~Q[1])&(~Q[0]))|(Q[3]&(~Q[2]));
assign out=Q[M-1];
endmodule
ㅤㅤㅤㅤ由计数器构成
计数型序列信号发生器和反馈型序列信号发生器大体相同,它们都是由时序电路和组合电路两部分构成。不同在于,反馈型序列信号发生器的时序状态由移位寄存器产生,输出取寄存器的最高位;而在计数型序列信号发生器中,采用计数器代替移位寄存器产生时序状态,输出由组合电路产生。
计数型的好处在于,计数器的状态设置与输出序列没有直接关系,不需要像上面一样,根据输出确定状态。只需要将反馈网络设计好就可以了。因此计数结构对于输出序列的更改比较方便,而且只要连接到不同的反馈网络,它可以同时产生多组序列码。
步骤总共分为两步:
对于“100111”序列的信号发生器。序列信号的M值为6,因为需选用模6的计数器。计数器的状态选择从000到 101。可以得到输出的组合逻辑真值表。
由真值表可画出输出Z的卡诺图,得到输出函数:
Z = Q2 + Q2`Q0` + Q1Q0
module signal_maker(OUT,clk,reset);
parameter M=3;
output OUT;
input clk,reset;
reg [M-1:0]counter;
always @(posedge clk)
if(!reset) counter<=3'b000;
else counter<=counter+1;
assign OUT=counter[2]((~counter[1])&(~counter[0]))|(counter[1]&counter[0]);
endmodule
例2:设计伪随机代码发生器
随机码是一种变化规律与随机码类似的二进制代码,可以作为数字通信中的个信号源,通过信道发送到接收机,用于检测数字通信系统错码的概率,即误码率。
在传统的数字电路设计中,伪随机序列信号发生器是用移位存型计数器来实现的,反馈网络输入信号从移位寄存器的部分输出端中取出,它的输出端F反馈到移位寄存器的串行输入端。
根据N=4的最长线形序列移存型计数器的功能实现的伪随机码发生器的代码为:
module signal15(out,clk,load_n,D_load);
output out;
input load_n,clk;
input [3:0] D_load;
reg[3:0] Q;
wire F;
always @(posedge clk)
if (~load_n)
Q<=D_load;
else
Q<={Q[2:0],F};
assign F:=(Q[1]^Q[0])|(~Q[3]&~Q[2]&~Q[1]&~Q[0]);
assign out=Q[3];
endmodule
使用场景:
有限状态机可以分为同步与异步,目前只讨论同步有限状态机,有限状态机是时序电路的通用模型,任何时序电路都可以表示为有限状态机,各个状态之间的转移总是在时钟的触发下进行的,状态信息存储在寄存器中,因为状态的个数是有限的,所以称为有限状态机。
使用有限状态机,如果出现错误,将会在电路中出现很大的问题
同其它时序电路一样,有限状态机也是由两部分组成:存储电路和组合逻辑电路。存储电路,用来生成状态机的状态;组合逻辑电路,用来提供输出以及状态机跳转的条件。
如果能力允许的话,在设计的电路的过程中,尽量将电路转化为基本单元的,最底层的电路,或者是具有描述性的电路
根据输出信号的产生方式,有限状态机可以分为米利型(Mealy)和摩尔型(Moore)
以后工作中使用moore型较多
在刚开始学习的时候,可以不用知道有限状态机的任何特点,只需要按照模板写就行了
使用格雷编码的不足:例如在处理下面的状态转换图的时候,不光是临近变化,同时还会变化到其他的状态上去,但是格雷码肯定要比二进制的效率要高,可以解决跳转到邻近状态的问题。
one hot独热码编码:有n个状态,就用n位来编码,缺点:当状态多的时候,会影响速度,资源占用的多得多。
有限状态机的写法有很多,通常可以分为两段式与三段式:
两段式:输出方程与驱动方程混在一起写,状态方程一定要分开
三段式:输出方程与驱动方程分开写
状态机两段式描述方式
//第一个进程,同步时序always模块,格式化描述次态寄存器迁移到现态寄存器
always@(posedge clk or negedge rst_n) //异步复位
if(!rst_n) current_state<=IDLE;
else current_state<=next_state; //注意,使用的是非阻塞赋值
//第二个进程,组合逻辑always模块,描述状态转移条件判断
always@(current_state) //电平触发
begin
next_state=x; //要初始化,使得系统复位后能进入正确的状态
case(current_state)
S1:if(...)
next_state=S2; //阻塞赋值
out1<=1'b1; //注意是非阻塞逻辑
endcase
end
第一段描述的就是状态转移函数,因为只有状态转移功能才有存储的功能,所以要把复位信号加在里面
current_state:当前状态
不建议初始化,因为这个随便初始化赋值之后,虽然这是语法通了,但是实际电路可能后面出问题容易你也不知道
简而言之即使怕你用语法掩盖了case语句的不完整,还是会出问题,因此还是建议将条件语句的各种情况补全。
状态机三段式描述方式
// 第一个进程,同步时序always模块,格式化描述次态寄存器迁移到现态寄存器
always @(posedge clk or negedge rst_n) // 异步复位
if(!rst_n) current_state <=IDLE;
else current_state <= nest_state; // 使用非阻塞赋值
// 第二个进程,组合逻辑always模块,描述状态转移条件判断
always @(current_state or 输入信号) // 电平触发
begin
next_state = x; // 初始化:使得系统复位后进入正确的状态
case(current_state)
S1:if(...)
next_state=S2; // 阻塞复制
.....
endcase
end
// 第三个进程,同步时序always模块,格式化描述次态寄存器输出
always @(posedge clk or negedge rst_n)
begin
// 初始化
case(next_state or 输入信号)
S1:out1<=1'b1; // 注意:使用非阻塞赋值
S2:out2<=1'b0;
default:...; // default的作用是免除综合工具综合出锁存器】
endcase
end
例1:设计顺序脉冲发生器
顺序脉冲发生器又称脉冲分配器,它将高电平的脉冲依次分配到不同输出上。保证在每个时钟内只有一路输出上是高电平脉冲,不同时钟上脉冲电平依次出现在所有输出。
以4位顺序脉冲发生器为例,它有四路输出W0 W1 W2 W3,每路输出上高电平脉冲依次出现,输出在1000,0100,0010,0001之间循环。4位顺序脉冲发生器的状态转移图,如图所示。它由4个状态构成,每个状态中“1”的个数都是1个,表示每个时钟周期内只有一路输出端为高电平(脉冲),而且是轮流出现,因此生成顺序脉冲信号。
使用两段式描述方式:上面是状态转移条件判断,下面是状态转移
module state(out,clk,rst_n)
input clk,rst_n;
output [3:0] out;
reg[3:0] out;
reg[1:0] STATE,NEXT_STATE;
always@(STATE)
case(STATE)
2'b00:
begin
out=4'1000;
NEXT_STATE = 2'b01;
end
2'b01:
begin
out=4'0100;
NEXT_STATE = 2'b10;
end
2'b10:
begin
out=4'0010;
NEXT_STATE = 2'b11;
end
2'b11:
begin
out=4'0001;
NEXT_STATE = 2'b00;
end
endcase
always@(posedge clk or rst_n)
if(!rst_n) STATE<=2'b00;
else STATE <= NEXT_STATE;
endmodule
考试笔试:重点
例2:设计一个卖报机,报纸价钱八角,纸币有1角,2角,5角,一元。该卖报机不考虑投币为大额面值等特殊情况。
下图是卖报机的状态转移图,图中S0~S7为状态机的8个状态,角标代表已投币的总合,如S0代表没有投币,S1代表已投入1角,依此类推。M代表输入,M1表示投入 1角硬币,M2代表投入2角硬币,M5代表投入5角硬币,M10代表投入一元。
data_out=1表示给出报纸,data_out_return1=1表示找回1角硬币,data_out_return:2=1表示找回2角硬币。
module auto_sellor(current_state,data_out,data_out_return1,data_out_return2,clk,rst_n,data_in)
parameter state_width=3,data_in_width=3;
output [state_width-1:0] current state;
output data_out,data_out_return1,data_out_return2;
input [data_in_width-1:0] data_in;
input clk,rst_n;
reg [state_width-1:0] current_state,next_state;
reg data_out,data_out_return1,data_out_return2;
always @(current_state or data_in)
// 先判断当前属于哪个状态
case(current_state)
3'b000:
// 根据当前投币的值,跳转状态与找零
case(data_in)
3'b000:
begin
next_state<=3'b000;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
3'b001:
begin
next_state<=3'b001;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
3'b010:
begin
next_state<=3'b010;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
3'b011:
begin
next_state<=3'b101;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
3'b100:
begin
next_state<=3'b000;
data_out<=1'b1;
data_out_return1<=1'b0;
data_out_return2<=1'b1;
end
endcase
3'b001:
case(data_in)
3'b000:
begin
next_state<=3'b001;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
3'b001:
begin
next_state<=3'b010;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
3'b010:
begin
next_state<=3'b011;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
3'b011:
begin
next_state<=3'b110;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
endcase
3'b010:
case(data_in)
3'b000:
begin
next_state<=3'b010;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
3'b001:
begin
next_state<=3'b011;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
3'b010:
begin
next_state<=3'b100;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
3'b011:
begin
next_state<=3'b111;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
endcase
3'b011:
case(data_in)
3'b000:
begin
next_state<=3'b011;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
3'b001:
begin
next_state<=3'b100;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
3'b010:
begin
next_state<=3'b101;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
3'b011:
begin
next_state<=3'b000;
data_out<=1'b1;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
3'b100:
case(data_in)
3'b000:
begin
next_state<=3'b100;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
3'b001:
begin
next_state<=3'b101;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
3'b010:
begin
next_state<=3'b110;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
3'b011:
begin
next_state<=3'b00;
data_out<=1'b1;
data_out_return1<=1'b1;
data_out_return2<=1'b0;
end
endcase
3'b101:
case(data_in)
3'b000:
begin
next_state<=3'b101;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
3'b001:
begin
next_state<=3'b110;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
3'b010:
begin
next_state<=3'b111;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
3'b011:
begin
next_state<=3'b000;
data_out<=1'b1;
data_out_return1<=1'b0;
data_out_return2<=1'b1;
end
endcase
3'b110:
case(data_in)
3'b000:
begin
next_state<=3'b110;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
3'b001:
begin
next_state<=3'b111;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
3'b010:
begin
next_state<=3'000;
data_out<=1'b1;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
endcase
3'b111:
case(data_in)
3'b000:
begin
next_state<=3'b111;
data_out<=1'b0;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
3'b001:
begin
next_state<=3'b000;
data_out<=1'b1;
data_out_return1<=1'b0;
data_out_return2<=1'b0;
end
endcase
endcase
always @(posedge clk or rst_n)
if(!rst_n)
current_state<=3'b000;
else
current_state<=next_state;
endmodule
在计算机领域中,总线(Bus)是用于在计算机内部或不同计算机部件之间传输数据、控制信号和电源的一组物理线路或电子信号的集合。它起到了连接各种硬件设备和组件的桥梁作用,它允许不同的硬件设备(如中央处理器、内存、输入输出设备等)相互通信和交换数据。总线传输的数据可以是指令、数据或控制信号,可以大大的提高数据流通的速度。
总线通常被划分为三类:
数据总线(Data Bus):用于传输数据和指令。它是双向的,能够在不同设备之间传递数据。
地址总线(Address Bus):用于指定内存或设备的物理地址。它是单向的,只能从主控制器传输到其他设备。
控制总线(Control Bus):用于传输控制信号,如读/写信号、中断信号和时钟信号等。控制总线也是双向的。
总线的设计和规格可以根据计算机体系结构的不同而有所差异。常见的总线标准包括系统总线(如PCI和PCI Express)、串行总线(如USB和Thunderbolt)以及内部总线(如Front Side Bus)。每种总线都有自己的特定功能和性能特征,用于满足不同系统的需求。
总之,总线在计算机系统中起到了连接和协调各个硬件组件之间通信的重要作用,使得计算机能够高效地运行和交换数据。
实际举例:
一个常见的总线例子是系统总线,如PCI(Peripheral Component Interconnect)总线。PCI总线是一种用于连接计算机内部各个设备的并行总线标准。它提供了高速数据传输和通信的能力,用于连接主板上的各种设备,例如显卡、声卡、网卡、硬盘控制器等。
PCI总线的设计允许多个设备共享相同的总线,通过总线控制器协调数据传输和访问。每个设备都有一个唯一的设备号和地址,可以通过总线进行数据读取和写入操作。PCI总线具有可扩展性和兼容性,允许不同速度和功能的设备在同一总线上进行通信。
除了PCI总线,还有其他类型的总线,如PCI Express(PCIe)总线,它是一种更快速和更高带宽的串行总线标准,用于连接高性能设备,如现代显卡和固态硬盘。
这只是一个例子,总线的类型和应用因计算机系统的不同而有所差异。不同的设备和组件可能使用不同类型的总线进行通信和数据传输。
流水线设计技术(Pipeline Design)是一种计算机体系结构设计方法,旨在提高指令执行的效率和吞吐量。通过将指令执行过程分为多个阶段,并使不同指令在不同阶段同时执行,流水线设计技术能够实现指令级并行(Instruction-Level Parallelism)并实现更高的指令吞吐量,在现代计算机体系结构,中央处理器(CPU)、图形处理器(GPU)和其他处理器架构得到广泛应用,。
以下是流水线设计技术的主要原理和特点:
阶段划分:指令执行过程被划分为多个独立的阶段,每个阶段负责执行特定的任务。常见的阶段包括指令获取、解码、执行、访存和写回。
并行执行:不同指令在不同阶段同时执行,使得多条指令可以同时处于不同的执行阶段,从而提高指令执行的效率和吞吐量。
流水线寄存器:每个阶段之间都有一个流水线寄存器,用于存储当前阶段的结果并传递给下一个阶段。这样可以实现流水线的解耦和流程控制。
数据冒险和冲突:由于指令之间的数据相关性,可能会出现数据冒险(Data Hazard)和冲突(Conflict),如读后写、写后读和写后写等。为了解决这些问题,可以采用技术手段如数据前推(Data Forwarding)、乱序执行(Out-of-Order Execution)和指令重排(Instruction Reordering)等。
流水线的延迟:由于流水线设计需要将指令执行过程划分为多个阶段,因此每个指令的执行时间会增加流水线的延迟。这意味着单个指令的延迟可能会增加,但整体的吞吐量和执行效率会提高。
总而言之,所谓的流水线设计实际上就是把规模较大,层次较多的组合逻辑电路分为好几级,在每一级插入寄存器组并暂存中间数据,K级流水线就是从组合逻辑的输入到输出有k个寄存器组。
通过流水线设计可以降低组合逻辑电路之间的延迟,如下图:
仿真,也叫模拟,是通过使用EDA仿真工具,通过输入测试信号,比对输出信号(波形、文本或者VCD文件)和期望值,来确认是否得到与期望所一致的正确的设计结果,验证设计的正确性。
验证是一个证明设计思路如何实现,保证设计在功能上正确的一个过程。
验证在Verilog HDL设计的整个流程中分为4个阶段:
功能验证:RTL级代码内部结构进行验证
综合后验证:是对电路的连接关系的考察
时序验证:有一个综合后验证后,综合完成会生成一个sdf延迟文件,将两个文件结合起来进行验证
板级验证:时序验证通过后,板级验证一般不会有问题
在仿真的时候Testbench用来产生测试激励给待验证设计(Design Under Verification,DUV),或者称为待测设计(Design Under Test,.DUT)。
激励向量就是给设计产生输入
例1:T触发器测试程序实例
module Tflipflop_tb;
//数据类型声明
reg clk,rst_n,T;
wire data_out;
//对被测模块实例化
TFF U1(.data_out(data_out),.T(T),.clk(clk),.rst_n(rst_n));
//产生测试激励
always
#5 clk=~clk;
initial
begin
clk=0;
#3 rst_n=0;
#5 rst_n=1;
T=1;
#30 T=0;
#20 T=1;
end
//对输出响应进行收集
initial
begin
$monitor($time,"T=%b,clk=%b,rst_n=%b,data_out=%b",T,clk,rst_n,data_out);
end
endmodule
仿真波形与部分文本输出如下:
OT =x,clk=0,rst_n=x,data_out=x
3T =x,clk=0,rst_n=0,data_out=0
5T =x,clk=1,rst_n=0,data_out=0
8T =1,clk=1,rst_n=1,data_out=1
10T=1,clk=0,rst_n=1,data_out=1
编写Testbench注意事项:
(1) testbench代码不需要可综合:Testbench代码只是硬件行为描述不是硬件设计。
(2) 行为级描述效率高:Verilog HDL语言具备5个描述层次,分别为开关级、门级、RTL级、算法级和系统级。
(3) 掌握结构化、程式化的描述方式:
结构化的描述有利于设计维护,可通过initial、always以及assign语句将不同的测试激励划分开来。
一般不要将所有的测试都放在一个语句块中。
测试平台
为了验证设计模块功能的正确性,需要再Testbench中编写信号激励给设计模块,同时观察这些激励在设计模块中的响应是否与设计目标相一致。建立Testbench进行仿真的流程总共有三个步骤,分别为编写仿真激励,搭建仿真环境,确定仿真结果。为一个设计建立仿真平台,将这个设计在该平台中实例化,然后再在平台中产生的测试激励输入给设计模块,再观察DUT的响应是否与期望值相同。
仿真结果确定
直接观察波形
通过直接观察各信号波形的输出,比较测试值和期望值的大小,来确定仿真结果的正确性,电路规模不大可以直接观察
打印文本输出法
通过调用系统任务打印:
$display
:直接输出到标准输出设备
$monitor
:监控参数的变化
module adder1_tb;
wire so,co;
wire a,b,ci;
adder1 U1(a,b,ci,so,co);//模块例化
initial
//测试信号产生
begin
a=0;b=0;ci=0;
#20 a=0;b=0;ci=1;
#20 a=0;b=1;ci=0;
#20 a=0;b=1;ci=1;
#20 a=1;b=0;ci=0;
#20 a=1;b=0;ci=1;
#20 a=1;b=1;ci=0;
#20 a=1;b=1;ci=1;
#200 $finish;
end
initial $monitor($time,"%b %b %b->%b %b",a,b,ci,so,co);
endmodule
输出结果为:
其输出的结果是:
0 0 0 0 -> 00
20 0 0 1 -> 10
40 0 1 0 -> 10
60 0 1 1 -> 01
80 1 0 0 -> 10
不推荐屏幕打印:因为屏幕打印会调用很多资源,造成资源的浪费,只打印关键信号
自动检查仿真结果
自动检查仿真结果是通过在设计代码中的关键节点添加断言监控器,形成对电路逻辑综合的注释或是对设计特点的说明,以提高设计模块的观察性。
使用VCD文件
Verilog HDL提供一系列系统任务用于记录信号值变化保存到标准的 VCD(Value Change Dump)格式数据库中。
VCD文件是一种标准格式的波形记录文件,只记录发生变化的波形。在5.3.6会详细的讲解
对于超大规模的电路,只能通过VSD文件观察
仿真效率
因为要通过串行软件代码完成并行语义的转化,Verilog HDL行为级仿真代码的执行时间比较长,随着代码量的增加,会使得仿真验证过程非常漫长,从而导致仿真效率的降低,这会成为整体设计的瓶颈。下面列举了几个建议:
注意,从本质上讲,减少代码的执行时间并不一定会提高代码的验证效率,因此上述的建议还需结合代码可读性,可维护性以及验证覆盖率等方面综合起来考虑。
验证覆盖率(Coverage)是一种评估验证测试向量的有效性和完整性的指标。它衡量了测试用例中的代码和功能覆盖程度,帮助确认设计是否被充分测试。
验证覆盖率通过跟踪设计中的特定代码或功能的执行情况来衡量。当测试用例执行时,覆盖率工具会记录已经触发的代码或功能,并生成相应的报告,显示已经覆盖和未覆盖的部分。这些报告可以帮助验证工程师确定需要进一步改进的测试用例,以确保设计的完整性和正确性。
常见的验证覆盖率指标包括:
- 语句覆盖率(Statement Coverage):衡量测试用例中执行的语句数量与总语句数量之间的比例。
- 分支覆盖率(Branch Coverage):衡量测试用例中执行的条件分支数量与总条件分支数量之间的比例。
- 条件覆盖率(Condition Coverage):衡量测试用例中覆盖的条件(例如,if语句中的条件)数量与总条件数量之间的比例。
- 路径覆盖率(Path Coverage):衡量测试用例中执行的路径数量与总路径数量之间的比例。路径是指从一个节点到另一个节点的序列。
- FSM(有限状态机)覆盖率:针对设计中的状态机,衡量被测试用例覆盖过的状态和状态转换占总状态和状态转换的比例。
通过分析验证覆盖率报告,验证工程师可以确定测试用例是否足够全面,是否需要增加额外的测试向量来提高覆盖率。达到高覆盖率可以增加对设计中潜在问题的发现,提高验证的可靠性。
验证覆盖率工具通常与Verilog仿真器集成使用,例如ModelSim、VCS等。这些工具提供了用于生成覆盖率报告的功能,并且可以帮助验证工程师评估测试质量和设计的完整性。
组合逻辑电路的设计验证,主要是检查设计结果是否符合该电路真值表的功能,因此在组合逻辑电路仿真环境时,用initial语句块把被测电路的输入按照真值表提供的数据变化作为测试条件。组合逻辑电路的特点决定了仿真中只需要对输入信号进行设计即可,没有时序,定时信号,全局复位,置位等信号要求
例:搭建全加器的仿真环境
A | B | CI | SO | CO |
---|---|---|---|---|
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 |
module adder1(a,b,ci,so,co);
input a,b,ci;
output so,co;
assign{co,so}=a+b+ci;
endmodule
根据全加器的真值表(表5.2-1)编写的全加器测试程序如下:
module adder1 tb;
wire so,co;
reg a,b,ci;
adder1 U1(a,b,ci,so,co);
// 模块例化
initial
// 测试信号产生
begin
a=0;b=0;ci=0;
#20 a=0;b=0;ci=1;
#20 a=0;b=1;ci=0;
#20 a=0;b=1;ci=1;
#20 a=1;b=0;ci=0;
#20 a=1;b=0;ci=1;
#20 a=1;b=1;ci=0;
#20 a=1;b=1;ci=1;
#200 $finish;
end
endmodule
全加器的输入a、b和ci定义为reg型变量;把输出so和co定义为wire型变量用模块例化语句adder1 U1(a,b,ci,so,co);
把全加器设计电路例化到测试仿真环境中;
用initial块语句改变输入的变化并生成测试条件,输入的变化语句完全根据全加器的真值表编写
时序逻辑电路仿真环境搭建基本与组合逻辑电路基本相同,主要区别在于时序环境需要考虑时序、定时信息和全局复位、置位等信号要求,并定义这些信号。
例:编写十进制加法计数器的源代码与测试程序代码
该模块 cnt10
实现了一个带有时钟、复位和使能控制的 4 位计数器,能够在使能信号为高电平时递增计数,当计数器达到 9 时,自动归零,并输出计数器的值和进位信号。
module cnt10(clk,rst,ena,q,cout);
input clk,rst,ena;
output [3:0] q;
output cout;
reg [3:0] q:
always @(posedge clk or posedge rst)
begin
if(rst) q=4'b0000;
// ena表示计数器的使能信号,控制计数器是否递增
else if(ena)
begin
if(q<9) q=q+1;
else q=0;
end
else
q = q; // 明确指定计数器的行为,保持当前值不变,防止产生锁存器
end
// cout是进位信号,最高位与最低位都为1(9的二进制为1001),就表示产生进位
assign cout=q[3]&q[0];
endmodule
测试程序代码为:
module cnt10_b;
reg clk,rst,ena;
wire [3:0]q;
wire cout;
// 模块实例化
cnt10 U1(clk,rst,ena,q,cout);
// 时钟信号产生
always #50 clk=~clk;
initial begin
// 控制信号产生
clk=0;rst=0;ena=1;
#1200 rst=1;
#120 rst=0;
#2000 ena=0;
#200 ena=1;
#20000 $finish;
end
endmodule
实例化语句cnt10U1(clk,rst,ena,q,cout);
把十进制计数模块例化到仿真环境中;在always中用语句#50clk=~clk;
产生周期为100(标准时间单位)的时钟方波:用initial块生成复位信号rst和使能控制信号ena的测试条件。测试结果如图:
语法格式如下:
// $display自动地在输出后进行换行
$display("" ,<signal1,signal2,...,signaln>);
// $write输出特定信息时不自动换行
$write("" ,<signal1,signal2,…,signaln>);
:通常称为格式控制
:则为“信号输出列表
例:
module disp_tb;
reg[31:0]rval;
pulldown(pd);
initial
begin
rval=101;
$display("\\\t%%\n\"\123");
$display("rval=%h hex %d decimal",rval,rval);
$display("rval=%o otal %b binary",rval,rval);
$display("rval has %c ascii character value",rval);
$display("pd strength value is %v",pd);
$display("current scope is %m");
$display("%s is ascii value for 101",101);
$write("simulation time is");
$write("%t\n",$time);
end
endmodule
输出结果为:
\%
"S
rval=00000065 hex 101 decimal
rval=00000000145 otal 00000000000000000000000001100101 binary
rval has e ascii character value
pd strength value is StX
current scope is disp
e is ascii value for 101
simulation time is 0
八进制数123就是字符S
monitor与stobe都提供了监控和输出参数列表中字符或变量的值的功能
monitori:功能等同于display,但是这两个是在信号列表中发生变化的时候才打印
语法格式:$monitor(<"format_specifiers>",
例如:$monitor(p1,p2,.....,pn)
任务$monitor提供了监控和输出参数列表中的表达式或变量值的功能。每当参数列表中变量或表达式的值发生变化时,整个参数列表中变量或表达式的值都将输出显示。
moitor还提供了下面两个常用的系统任务:monitoron,monitoroff。分别用来控制监控任务的启动与停止
例1:
module monitor_tb;
integer a, b;
initial
begin
a = 2;
b = 4;
forever
begin
#5 a = a + b;
#5 b = a - 1;
end
end
initial #40 $finish;
initial
begin
$monitor($time, "a = %d, b = %d", a, b);
end
endmodule
输出结果为:
0 a = 2, b = 4
5 a = 6, b = 4
10 a = 6, b = 5
15 a = 11, b = 5
20 a = 11, b = 10
25 a = 21, b = 10
30 a = 21, b = 20
35 a = 41, b = 20
使用场景:monitor与display的不同处在于monitor往往在initial块中调用,只要不调用monitoroff,monitor便不间断地对所设定的信号进行监视。
strobe:探测任务用于在某时刻所有时间处理完后,在这个时间步的结尾输出一行格式化的文本。
语法格式:$strobe(
$strobe("
$strobe:在所有时间处理完后,以十进制格式输出一行格式化的文本;
$strobeb:在所有时间处理完后,以二进制格式输出一行格式化的文本;
$strobeo:在所有时间处理完后,以八进制格式输出一行格式化的文本;
$strobeh:在所有时间处理完后,以十六进制格式输出一行格式化的文本。
注意:strobe任务在被调用的时刻,所有的赋值语句都完成了,才会输出相对应的文字信息
例:display与strobe系统任务的比较
module strobe_tb;
reg a,b;
initial
begin
a=0;
$display("a by display is:",a);
$strobe("a by strobe is:",a);
a=1;
end
initial
begin
b<=0;
$display("b by display is:",b);
$strobe ("b by strobe is:",b);
#5;
$display("#5 b by display is:",b);
$strobe ("#5 b by strobe is:",b);
b<=1;
end
endmodule
显示结果是:
a by display is:0
b by display is:x
a by strobe is:1
b by strobe is:0
#5 b by display is:0
#5 b by strobe is:1
display到了就打印,而store是调用的时刻,所有的赋值任务都完成了再打印
用这两个时间系统函数可以得到当前的仿真时刻,所不同的是,time函数以64位整数值的形式返回仿真时间,而realtime函数则以实数型数据返回仿真时间。
系统函数time
timescale 1ns/1ns
module time_tb;
reg ts;
parameter delay =2;
initial begin
#delay ts=1;
#delay ts=0;
#delay ts=1;
#delay ts=0;
end
initial
//使用函数$time
$monitor($time,,"ts=%b",ts);
endmodule
输出结果为:
0 ts = X
2 ts = 1
4 ts = 0
6 ts = 1
8 ts = 0
realtime:返回的时间数字是一个实型数,该数字也是以时间尺度为基准的。
timescale 10ns/1ns
module realtime_tb;
reg set;
parameter p=1.55;
initial
begin
$monitor($realtime,"set=b%",set);//使用函数$realtime
#p set-0;
#p set=1;
end
endmodule
输出结果为:
0 set=x
1.6 set=0
3.2 set=1
$realtime是将仿真时刻经过尺度变换(根据时间精度四舍五入)后输出,返回的时刻是实数型。
finish:退出仿真器,返回主操作系统,也就是结束仿真过程。
任务sfinish可以带参数,根据参数的值输出不同的特征信息。如果不带参数,默认$sfinish的参数值为1。
$finish;$finish(n);
$stop:把EDA工具(例如仿真器)置成暂停模式,在仿真环境下给出一个交互式的命令提示符,将控制权交给用户。这个任务可以带有参数表达式。根据参数值(0,1或2)的不同,输出不同的信息。参数值越大,输出的信息越多。
$stop;$stop(n)
random:产生随机数的系统函数,每次调用该函数将返回一个32位的随数,该随机数是一个带符号的整数。
语法格式:$random%
,给出一个范围在(-number+1):(number-1)中的随机数
这个系统函数提供了一个产生随机数的手段。当函数被调用时返回一个32bt的随机数。它是一个带符号的整形数
可以与位拼接操作符{ }
,将$random返回的有符号数变为无符号数
aaa = 20 * ({$random} % 6) // aaa在0~100变化
bbb = 20 * ( 1 + {$random} % 3) // bbb在20~60变化
VCD(Value Change Dump)是一种用于存储和表示数字电路模拟结果的文件格式。VCD 文件通常用于仿真工具生成和保存仿真过程中信号的数值变化。包含了在仿真过程中每个时刻的信号值变化,以及与这些变化相关的时间信息。同时也记录电路中的输入信号、输出信号和内部信号在仿真过程中的变化情况。
VCD 文件通常包含以下主要的元素:
现在有许多大规模设计的仿真,设计者可以把选定的信号转储到VCD文件中,并使用后处理工具去调试,分析和验证仿真输出结果
可以使用 $dumpfile
和 $dumpvars
系统任务来控制生成 VCD 文件并指定要转储的信号。以下是这两个系统任务的用法和示例说明:
$dumpfile
系统任务:
$dumpfile("")
$dumpfile("simulation.vcd")
将生成一个名为 “simulation.vcd” 的 VCD 文件。$dumpvars
系统任务:
语法:$dumpvars([
说明:指定要转储的信号和作用域,并指定转储的级别。
示例:
// 转储所有信号
$dumpvars(0, <level>);
// 转储指定模块中的信号
$dumpvars("" , <level>);
// 转储指定模块实例中的信号
$dumpvars("" , <level>);
这些系统任务的示例代码如下:
module ExampleModule(input a, output b);
reg [3:0] counter;
always @(posedge a) begin
if (b)
counter <= counter + 1;
else
counter <= counter - 1;
end
initial begin
$dumpfile("simulation.vcd"); // 指定生成的 VCD 文件名
$dumpvars(0, 3); // 转储顶层模块及其子模块实例的所有信号
// 进行仿真操作
#10;
a <= 1'b0;
#10;
b <= 1'b1;
#10;
$finish;
end
endmodule
在上述示例中,$dumpfile
系统任务指定了生成的 VCD 文件名为 “simulation.vcd”,$dumpvars
系统任务指定了要转储的信号级别为 3,即转储顶层模块及其子模块实例的所有信号。在仿真过程中,当满足特定条件时,会修改输入信号 a 和 b 的值,并在仿真结束时调用 $finish
系统任务结束仿真。生成的 VCD 文件将包含所指定信号的值变化。
在 Verilog 中,可以使用宏定义来定义符号常量、参数、条件编译和代码片段的替代文本。
语法格式:
宏定义:``define <标识符> <宏内容>`
宏调用:``标识符`
使用场景:能用一个简单的名称去代替长字符串
例:
定义符号常量:
`define WIDTH 8
可以使用 WIDTH
来代替数字 8
,在代码中使用 WIDTH
将被展开为 8
。
定义带参数的宏:
`define ADD(x + y) x + y
可以使用 ADD(x, y)
来代替 x + y
,在代码中使用 ADD(x, y)
将被展开为 x + y
。
定义代码片段:
`define COUNTER \
if (reset) begin \
count <= 0; \
end else begin \
count <= count + 1; \
end
可以使用 COUNTER
来代替一段代码,使用 COUNTER
将被展开为相应的代码片段。通过\宏定义跨行
注意:宏名称也即标识码名称一般采用大写字母,将其与变量进行区分,宏定义只是做简单的置换,不做语法检查。后面不用加;
,不然会连同分号一起进行置换宏定义可以在任何地方使用,并在预处理阶段进行文本替换。宏定义不会进行类型检查或表达式求值,只是简单的文本替换。因此,在使用宏定义时要注意它们的上下文和展开结果。
与parameter的区别
使用案例
module define_demo(clk,a,b,c,d,q)
`define bsize 9
`define c a+b
input clk;
input [`bsize:0] a,b,c,d;
output [`bsize:0] q;
reg [`bsize:0] q;
always @(posedge clk)
begin
q <= `c + d;
end
endmodule
一个源文件可以将另一个源文件的全部内容包含进来
语法格式:``include “文件名”`
例:文件包含使用案例
// 文件a1.v
module a1(a,b,out);
input a,b;
output out;
wire out;
assign out=a^b;
endmodule
// 文件b1.v
include "a1.v"
module b1(c,d,e,out);
input c,d,e;
output out;
wire out_a;
wire out;
a1 U1(.a(c),.b(d),.out(out_a));
assign out=e&out_a;
endmodule
该命令用来说明在该命令后的模块的时间单位与时间精度。时间单位参量是用来定义模块中仿真时间和延迟时间的基准单位的。时间精度参量是用来声明该模块的仿真时间的精确程度的,该参量被用来对延迟时间值进行取整操作(仿真前),因此该参量又可以被称为取整精度。如果在同一个程序设计里,存在多个``timescale`命令,则用最小的时间精度值来决定仿真的时间单位
语法格式如下:``timescale <时间单位>/<时间精度>`
注意:时间精度不能大于时间单位
生活中的例子:比如尺寸,单位是厘米,但是精度是毫米
用于说明时间单位和时间精度参量值的数字必须是整数,其有效数字为1、10、100,单位为秒(s)、毫秒(ms)、微秒(us)、纳秒(ns)、皮秒(ps)、毫皮秒(fs)。下面举例说明timescale命令的用法。
例1:``timescale 1ns/1ps`
模块中所有的时间值都表示是1ns的整数倍。这是因为在timescale命令中,定义了时间单位是1ns。模因为timescale命令定义时间精度为1ps,块中的延迟时间可表达为带三位小数的实型数。
例2:``timescale 10us/100ns`
在这个例子中,模块中时间值均为10us的整数倍。因为timesacle命令定义的时间单位是10us。延迟时间的最小分辨度为十分之一微秒(100ns),即延迟时间可表达为带一位小数的实型数。
例3:由于时间精度位1ns,因此d要四舍五入到1.6,也即d的实际延迟是16ns
`timescale 10ns/1ns
module delay_tb;
reg set;
parameter d=1.55;
initial
begin
// 经过16ns后,set为0
#d set=0;
// 经过32ns后,set为1
#d set=1;
end
endmodule
在集成电路设计与验证阶段,经常需要对特定信号进行延时来实现相应的时序控制,或者避免信号冲突形成电路中的热点,下面是信号的时间延迟的两类方式
**电路的热点(Hotspot)**是指电路中的某个区域或组件因为功耗过高而产生的显著热量集中的区域。这种热点通常是由于局部集中的功耗密度较高或散热不良导致的。
热点在电路设计中是一个重要的关注点,因为高温可能会导致以下问题:
电子元件的可靠性问题:高温可能导致电子元件的寿命缩短、性能下降甚至损坏。温度对于许多电子元件的性能和可靠性都有很大影响,如晶体管、电容器和电阻器等。
信号完整性问题:温度变化可能导致电路中信号的延迟、传输速度和噪声特性发生变化,从而影响电路的工作稳定性和信号完整性。
整体系统性能下降:当热点集中在某个区域时,该区域周围的其他部分也可能受到热量的影响而发生性能下降,例如时钟频率的降低、功耗增加和系统稳定性问题等。
延时语法说明
外部延迟控制
时间控制出现在整个过程赋值语句的最左端,也就是出现赋值目标变量的左边的时间控制方式,其语法结构如下例所示:
#5 a=b;
在仿真执行时就相当于如下几条语句的执行:
initial
begin
#5;
a=b;
end
内部延迟控制
过程赋值语句中的时间控制部分还可以出现在“赋值操作符”和“赋值表达式”之间的时间控制方式。其语法结构如下例所示: a=#5b;
其中时间控制部分“5”就出现在赋值操作符“=”和赋值表达式“b”的中间,
因此在这条过程赋值语句内带有内部时间控制方式的时间控制。它在执行时就相当于如下几条语句的执行:
initial
begin
// 先求b的值
temp=b;
#5;
a=temp;
end
这个延时阻塞了a的赋值
串行延时控制
最为常见的延时控制,由begin-end过程快加上延时赋值语句构成,这里不再多说
并行延时控制
通过fork-join语句块加上延时赋值语句构成,由于是并行执行,因此里面的延时都是绝对延时
initial
fork
aaa=1'b0;
#50 aaa=1'b1;
#150 aaa=1'b0;
#250 aaa=1'b1;
#300 aaa=1'b0;
#400 aaa=1'b1;
#450 aaa=1'b0;
#500 aaa=1'b1;
#600 aaa=1'b0;
join
阻塞式延时控制
所谓的阻塞式延时控制就是再阻塞式过程赋值语句的基础上加上延时控制
initial
begin
a=0;
a=#5 1;
a=#10 0;
a=#15 1;
end
非阻塞式延时控制
所谓的非阻塞式延迟控制是在非阻塞式过程赋值基础上带有延时控制的情况。
initial
begin
a<=0;
a<=#5 1;
a<=#10 0;
a<=#15 1;
end
边沿触发事件控制
语法格式:always@(敏感事件表)
always不一定要跟敏感事件表,后面的@()是一个单独的语句,称为边沿触发语句
例:时钟脉冲计数器
module clk_counter(clk,count_out);
input clk;
output count_out;
reg [3:0]count_out;
initial
count_out =0;
always@(posedge clk)
count_out = count_out+1;
// 在ck的每个正跳变边沿count_out增加1
endmodule
在Verilog HDL语言中提供了任务和函数,可以将较大的行为级设计划分为较小的代码段,允许设计者将需要在多个地方重复使用的相同代码提取出来,编写成任务和函数,这样可以使代码更加简洁和易懂
任务的定义
任务定义是嵌入在关键字task和endtask之间的,其中关键词task标志着一个任务定义结构的开端,endtask标志着一个任务定义结构的结束。<任务名>
是所定义任务的名称。在<任务名>
后面不能出现输入输入端口列表。
语法格式:
task<任务名>;
端口和类型声明
局部变量声明
begin
语句1;
语句2;
语句n;
end
endtask
例:读存储器数据
task read_memory;
//任务定义的开头,指定任务名为read_memory
input[15:0] address;
//输入端口说明
output[31:0] data;
//输出端口说明
reg[3:0] counter;
//变量类型说明
reg[7:0] temp[1:4];
//变量类型说明
begin
//语句块,任务被调用时执行
for(counter=1;counter<=4;counter=counter+1)
temp[counter]=mem[address+counter-1];
data={temp[1],temp[2],temp[3],temp[4]};
end
endtask
//任务定义结束
注意事项:
任务的调用
任务的调用是通过“任务调用语句”来实现的。任务调用语句列出了传入任务的参数值和接收结果的变量值。
任务的调用格式如下:<任务名>(端口1,端口2......端口n)
例1:以测试仿真中常用的方式来说明任务的调用
module demo_task_invo_tb;
reg[7:0] mem[127:0];
reg[15:0] a;
reg[31:0] b;
initial
begin
a=0; read_mem(a,b);//任务的第一次调用
#10;
a=64;read_mem(a,b);//任务的第二次调用
end
task read_mem;//任务定义部分
input [15:0] address;
output [31:0] data;
reg [3:0] counter;
reg[7:0] temp[1:4];
begin
for(counter=1;counter<=4;counter=counter+1)
temp[counter]=mem[address+counter-1];
data={temp[1],temp[2],temp[3],temp[4]};
end
endtask
endmodule
模块的调用需要有实例名,而任务的调用不需要,只能调用当前模块下定义的任务
例2:以实际中的交通灯控制为例说明任务定义调用的特点
module traffic_lights(red,amber,green);
output red,amber,green;
reg [2:1]order;
reg clock,red,amber,green;
parameter ON=1,OFF=0,RED_TICS=350,AMBER_TICS=30,GREEN_TICS=200;
//产生时钟脉冲
always
begin
#100 clock=0;
#100 clock=1;
end
//任务的定义,该任务用于实现交通灯的开启
task light;
output red;
output amber;
output green;
input [31:0] tic_time;
input [2:1] order;
begin
red=OFF;
green=OFF;
amber=OFF:
case(order)
2'b01:red=ON;
2'b10:green=ON;
2'b11:amber=ON;
endcase
repeat(tic_time)@(posedge clock);
red=OFF;
green=OFF;
amber=OFF;
end
endtask
//任务的调用,交通灯初始化
initial
begin
order=2'b00;
light(red,amber,green,0,order);
end
//任务的调用,交通灯控制时序
always
begin
order=2'b01;
//调用开灯任务开红灯
light(red,amber,green,RED_TICS,order);
order=2'b10;
//调用开灯任务,开绿灯
light(red,amber,green,GREEN_TICS,order);
order=2'b11;
//调用开灯任务,开黄灯
light(red,amber,green,AMBER_TICS,order);
end
endmodule
为什么这里case不加default?因为这是测试仿真,而不是可综合电路设计,可综合电路必须要写全所有的情况
函数的定义
函数定义是嵌入在关键字function和endfunction之间的,其中关键词function标志着一个函数定义结构的开端,endfunction标志着个函数定义结构的结束。<函数名
是给被定义函数取的名称。这个函数名在函数定义结构内部还代表着一个内部变量,函数调用后的返回值是通过这个函数名变量传递给调用语句的
语法格式:
function<返回值类型或位宽><函数名>;
<输入参量与类型声明>
<局部变量说明>
begin
语句1;
语句2;
语句n;
end
endfunction
<返回值类型或位宽>是一个可选项,它是用来对函数调用返回数据的类型或宽度进行说明,它可以有如下三种形式:
[msb:lsb]
:这种形式说明函数名所代表的返回数据变量时一个多位的寄存器变量,它的位宽由[msb:lsb]指定,比如如下函数定义语句: function [7:0] adder;
就定义了一个函数adder
,它的函数名adder
还代表着一个8位宽的寄存器变量,其中最高位为第7位,最低位为第0位。
integer
:这种形式说明函数名代表的返回变量是一个整数型变量。
real
:这种形式说明函数名代表的返回变量是一个实数型变量。
<输入参量与类型声明>是对函数各个输入端口的宽度和类型进行说明,在函数定义中,必须至少有一个输入端口(input)的声明,不能有输出端口( output)的声明。数据类型声明语句用来对函数内用到的局部变量进行宽度和类型说明,这个说明语句的语法与进行模块定义时的相应说明语句语法是一致的。
<局部变量说明>是对函数内部局部变量进行宽度和类型的说明:由“begin”与“end”关键词界定的一系列语句和任务一样,用来指明函数被调用时要执行的操作,在函数被调用时,这些语句将以串行方式得到执行。
例:统计输入数据中0的个数
function[3:0] out0;
input[7:0] x;
reg[3:0] count;
integer i;
begin
count=0;
for(i=0;i<=7;i=i+1)
if(x[0]==1'b0) count=count+1;
out0=count;
end
endfunction
注意事项:
函数的调用
函数的调用是通过将函数作为表达式中的操作数来实现的。函数的调用格式如下:
<函数名>(<输入表达式1>,<输入表达式2>..<输入表达式n>);
其中,输入表达式应与函数定义结构中说明的输入端口一一对应,它们代表着各个输入端口的输入数据。函数调用时要注意以下几点:
例:下面使用阶乘运算为例说明函数的调用方式
module tryfact_tb;
//函数的定义部分
function[31:O] factorial;
input[3:0] operand;
reg[3:0] index;
begin
factorial=1;
for(index=1;index<=operand;index=index+1)
factorial = index * factorial;
end
endfunction
reg[31:0] result;
reg[3:0] n;
initial
begin
result=1;
for(n=1;n<=9;n=n+1)
begin
//函数的调用部分
result=factorial(n);
$display("n=%d result=%d",n,result);
end
end
endmodule
上例由函数定义和initial过程块构成,其中定义了一个名为factorial的函数,该函数是一个进行阶乘运算的函数,具有一个4位的输入端口,同时返回一个32位的寄存器类型的值;在initial块中定义了两个寄存器变量,分别为32位的result和4位的n,initial块对1至9进行阶乘运算,并打印出结果值。
n= 1 result= 1
n= 2 result= 2
n= 3 result= 6
n= 4 result= 24
n= 5 result= 120
n= 6 result= 720
n= 7 result= 5040
n= 8 result= 40320
n= 9 result= 362880
在硬件描述语言中,任务与函数都是从高级程序语言中继承过来的,其语法使用范围具有一定的局限性。
实际上,在数字电路的设计过程中,更倾向于使用模块来解决代码的重复性问题,这种模块的表示方法和电路的组成相近,更加的直观
所有的initial语句在0时刻开始都同时开始进行,因为是过程赋值语句,因此只要是被赋值的信号量,都必须为reg类型
在Verilog HDL语言中,有两种方法可以初始化变量:一种是利用初始化变量:另一种就是在定义变量时直接赋值初始化。这两种初始化任务是不可综合的,主要用于仿真过程。
initial初始化方式
在大多数情况下,Testbench中变量初始化的工作通过initial过程块来完成,可以产生丰富的仿真激励。
initiali语句只执行一次,即在设计被开始模拟执行时开始(0时刻)直到过程结束,专门用于对输入信号进行初始化和产生特定的信号波形。一个 Testbench可以包含多个initial过程语句块,所有的initiali过程都同时执行。
需要注意的是,initiali语句中的变量必须为reg类型。
例:利用initial初始化方式的测试向量产生
module counter_demo2(clk,cnt);
output [3:0] cnt;
reg clk;
reg [3:0] temp;
initial temp=0;
initial clk=0;
endmodule
定义变量时初始化
在定义变量时初始化的语法非常简单,直接用“=”在变量右端赋值即可,如:
// 就是将8比特的寄存器变量cnt初始化为0。
reg [7:0] cnt=8'b00000000;
数据信号的产生有两种形式:其一是初始化和产生都在单个initial块中进行;其二是初始化在initiali语句中完成,而产生在always语句块中完成。前者适合不规则数据序列,并且要求长度短;后者适合具有一定规律的数据序列,长度不限。
例1:产生位宽为4的质数序列{1、2、3、5、7、11、13},并且重复两次,其中样值间隔为4个仿真时间单位由于该序列无明显规律,因此利用initiali语句固定写死最为合适。
`timescale 1ns/1ps
module sequence_tb;
reg[3:0] q_out;
parameter sample_period=4;
parameter queue_num 2;
initial begin
q_out = 0;
repeat(queue_num)
begin
# sample_period q_out=1;
# sample_period q_out=2;
# sample_period q_out=3;
# sample_period q_out=5;
# sample_period q_out=7;
# sample_period q_out=11;
# sample_period q_out=13;
end
end
endmodule
问题1:13能直接赋值给q_out吗?
可以直接将13赋值给q_out。由于q_out的宽度为4位(reg[3:0]
),因此可以使用4位二进制表示的数值来赋值给它。
在代码中的q_out=13;语句中,13的二进制表示为1101,可以适配到q_out的4个位上。因此,将13赋值给q_out是有效的。
问题2:如果将20赋值给一个4位宽的二进制变量呢?
如果要将整数20赋值给4位的二进制变量,需要对整数进行截断或进行适当的位宽转换。
20二进制表示为10100,给上述代码中的q_out赋值,这将保留整数的低4位,那么q_out= 4’b0100
时序电路应用广泛,其中,时钟信号是时序电路设计最关键的参数之一,因此本节专门介绍如何产生仿真验证过程所需要的各种时钟信号
例1:产生占空比为50%的时钟信号,其波形如图所示:
基于initial语句的方法
module clk1(clk);
output clk;
parameter clk_period =10;
reg clk;
initial
begin
clk=0;
forever #(clk_period/2) clk=~Ck;
end
endmodule
基于always语句的方法
module clk2(clk);
output clk;
parameter clk_period =10;
reg clk;
initial clk = 0;
always #(clk_period/2)
clk=~clk;
endmodule
由于always不断活跃,always如果没写敏感列表就会一直执行,产生死锁,具体看3.1.1.2章
例2:产生占空比可自由设置的时钟信号
module Duty_Cycle(clk);
output clk;
parameter High_time=5,Low_time:=20;
//占空比为High_time/(High_time + Low_time)
reg clk;
always begin
clk=1;
#High_time;
clk=0;
#Low_time;
end
endmodule
由于这是对clk直接赋值,所以不需要initial语句初始化clk信号
例3:产生具有相位偏移的时钟信号
相位偏移是两个时钟信号之间的相对概念,如图所示,其中clk_a为参考信号,clk_b为具有相位偏移的信号
module shift Duty_Cycle(clk_a,clk_b);
output clk_a,clk_b;
parameter High_time=5,Low_time=5,pshift_time=2;
reg clk_a;
wire clk_b;
always
begin
clk_a=1;
# High_time;
clk_a=0;
# Low_time;
end
assign # pshift_time clk_b=clk_a;
endmodule
例4:产生固定数目的时钟信号
产生了5个周期的时钟信号
module fix_num_clk(clk);
output clk;
parameter clk_cnt=5,clk_period=2;
reg clk;
initial
begin
clk=0;
repeat(clk_cnt)
# clk_period/2 clk=~clk;
end
endmodule
要保证时钟信号切在眼图的中间
在 Verilog 中,“眼图”(Eye Diagram)是一种用于分析和评估数字通信系统性能的图形表示方法。它通过观察接收到的数字信号的波形特征,提供了对信号质量和传输误差的可视化评估。
眼图通常用于评估时序信号在高速数字通信中的传输质量。在眼图中,水平轴表示时间,垂直轴表示信号幅度。它展示了多个连续的位间隔(bit interval)内的信号样本,并在图形中形成一系列重叠的“眼睛”形状。每个“眼睛”表示一个位间隔内的单个数字比特。
通过观察眼图,可以获得以下信息:
信号失真:眼图可以显示信号的幅度、时间偏移和抖动等失真情况。失真可能导致位误码率(BER)的增加和通信质量的下降。
眼开口:眼图的开口大小表示了系统传输质量的容忍度。开口越大,系统的容错能力越高。
噪声和干扰:眼图可以显示噪声和干扰对信号质量的影响。干净的眼图表示较低的噪声和干扰水平。
时钟抖动:眼图可以显示时钟信号的抖动情况。时钟抖动会导致眼睛变形,影响位同步和信号采样。
眼图通常通过使用仿真工具或实际测试设备来生成。在仿真中,可以采集到每个位间隔内的信号样本,并根据样本数据绘制眼图。在实际测试中,使用示波器或信号分析仪来捕捉和分析信号波形,生成眼图。
通过分析和解释眼图,可以评估和优化数字通信系统的性能,并识别任何需要调整或改进的问题,以实现更好的数据传输和接收。眼图是数字通信系统工程师和设计人员常用的工具之一。
总线是运算部件之间数据流通的公共通道。在RTL级描述中,总线指的是由逻辑单元、寄存器、存储器、电路输入或其它总线驱动的一个共享向量。而总线功能模型则是一种将物理的接口时序操作转化成更高抽象层次接口的总线模型,如图所示。
在总线中,对于每个请求端,有一个输入来选择驱动该总线所对应的请求端。选择多个请求端会产生总线冲突,根据总线的类型,冲突会产生不同的结果。当有多个请求端发出请求时,相应的操作由总线的类型决定。在Verilog HDL测试中,总线测试信号通常是通过将片选信号,读(或者写)使能信号、地址信号以及数据信号以task任务的形式来描述,通过调用以task形式的总线信号测试向量来完成相应的总线功能。
下面以工作频率为100MHz的AHB总线写操作为例,说明以task方式产生总线信号测试向量的方式。下图是AHB总线写操作的时序图,其中,在完成数据的写操作后将片选和写使能信号置为无效(低电平有效)
例:产生一组具有写操作AHB总线功能模型
module bus_wr_tb;
reg clk;
reg cs;
reg wr;
reg [31:0] addr;
reg [31:0] data;
initial
begin
cs = 1'b1;
wr = 1'b1;
#30;
bus_wr(32'h1100008a, 32'h11113000);
bus_wr(32'h1100009a, 32'h11113001);
bus_wr(32'h110000aa, 32'h11113002);
bus_wr(32'h110000ba, 32'h11113003);
bus_wr(32'h110000ca, 32'h11113004);
addr = 32'bx;
data = 32'bx;
end
initial clk = 1;
always #5 clk = ~clk;
task bus_wr;
input [31:0] ADDR;
input [31:0] DATA;
begin
cs = 1'b0;
wr = 1'b0;
addr = ADDR;
data = DATA;
#30;
cs = 1'b1;
wr = 1'b1;
end
endtask
endmodule
可以看出,在片选信号与写使能信号均有效时,每三个周期输出一组地址和数据,当完成地址和数据的输出后,则将片选信号和写使能信号置为无效。
Bottom-Up:规模做不大,适合初期
Top-Down:
首先先简单说明一下二进制乘法,以二进制数101(5)与二进制数110(6)相乘为例:
1 0 1 (5)
× 1 1 0 (6)
----------
0 0 0 (第一次乘法结果)
1 0 1 (第二次乘法结果)
1 0 1 (第三次乘法结果)
---------------
1 1 1 1 0 (最后相加得到最终的结果30)
在二进制乘法运算中,乘数的每一位与被乘数的每一位相乘,得到部分乘积。然后将部分乘积按照位数对齐并相加,得到最终的乘积结果。记得进位的处理和对齐的方式与十进制乘法类似。通过逐位相乘和累加的方式,可以计算出二进制数的乘积。
加法器树乘法器的设计思想是“移位后加”,并且加法运算采用加法器树的形式。乘法运算的过程是,被乘数与乘数的每一位相乘并且乘以相应的权值(先判断乘数是否为1,为0直接不用管,为1就乘以权值,而乘以权值就相当于移位,权值为2,4,8,就分别左移1,2,3位),最后将所得的结果相加,便得到了最终的乘法结果。
例1:下图是一个4位的乘法器结构,用Verilog HDL设计一个加法器树 4位乘法器
module mul_addtree(mul_a,mul_b,mul_out);
input [3:0] mul_a,mul_b;
output [7:0] mul_out;
wire [7:0] mul_out;
wire [7:0] store0,store1,store2,store3;
wire [7:0] addr01,addr22;
assign store3 = mul_b[3]?{1'b0,mul_a,3'b0}:8'b0;
assign store2 = mul_b[2]?{2'b0,mul_a,2'b0}:8'b0;
assign store1 = mul_b[1]?{3'b0,mul_a,1'b0}:8'b0;
assign store0 = mul_b[0]?{4'b0,mul_a}:8'b0;
assign addr01 = store0 + store1;
assign addr23 = store2 + store3;
assign mul_out = addr01 + addr23;
endmodule
测试代码:
module mul_addtree_tb;
reg[3:0] mult_a;
reg[3:0] mult_b;
wire[7:0] mult_out;
// 模块例化
mul_addtree U1(.mul_a(mult_a),.mul_b(mult_b),.mul_out(mult_out));
initial
begin
mult_a = 0;
mult_b = 0;
repeat(9)
begin
#20;
mult_a = mult_a + 1;
mult_b = mult_b + 1;
end
end
endmodule
注意:这个电路的延迟会非常高,因为延迟主要来自于加法的延迟
由于该乘法器采用加法树结构,每个乘法部分的计算都是并行的,因此在计算过程中速度相对较快。然而,该电路中的每个加法操作都会引入延迟,因此整个乘法器的速度可能会受到加法延迟的影响。
可以通过流水线提高电路速度,其基本思想就是在加法器树中插入寄存器,将加法器树乘法器设计成流水线型
例2:设计一个两级流水线4位加法器树乘法器
通过在第一级与第二级、第二级与第三级加法器之间插入D触发器组,可以实现两级流水线设计。
module mul_addtree_plus(clk,clr,mul_a,mul_b,mul_out);
input clk,cl
input [3:0] mul_a,mul_b;
output [7:0] mul_out;
reg [7:0] add_temp1,add_temp2,mul_out;
wire [7:0] store0,store1,store2,store3;
assign store3 = mul_b[3]?{1'b0,mul_a,3'b0}:8'b0;
assign store2 = mul_b[2]?{2'b0,mul_a,2'b0}:8'b0;
assign store1 = mul_b[1]?{3'b0,mul_a,1'b0}:8'b0;
assign store0 = mul_b[0]?{4'b0,mul_a}:8'b0;
always(posedge clk or negedge clr)
begin
if(!clr)
begin
add_temp1 = 8'b00000000;
add_temp2 = 8'b00000000;
mul_out = 8'b00000000;
end
else
begin
add_temp1 <= store0 + store1;
add_temp2 <= store2 + store3;
mul_out <= add_temp1 + add_temp2;
end
end
endmodule
测试代码如下:
module mul_addtree_plus_tb;
reg clk,clr;
reg[3:0] mult_a,mult_b;
wire[7:0] mult_out;
// 模块例化
mul_addtree_plus U1(.clk(clk),.clr(clr),.mul_a(mult_a),.mul_b(mult_b),.mul_out(mult_out));
initial
begin
clk = 0;
clr = 0;
mult_a = 1;
mult_b = 1;
#5 clr = 1;
end
always #10 clk=~clk;
initial
begin
repeat(5)
begin
#20;
mult_a = mult_a + 1;
mult_b = mult_b + 1;
end
end
endmodule
Vallace树乘法器运算原理如下图所示,其中FA为全加器HA为半加器。其基本原理是,加法从数据最密集的地方开始,不断地反复使用全加器半加器来覆盖“树”。这一级全加器是一个3输入2输出的器件,因此全加器又称为3-2压缩器。通过全加器将树的深度不断缩减,最终缩减为一个深度为2的树。最后一级则采用一个简单的两输入加法器组成。
module wallace(x,y,out);
// 参数定义
parameter size = 4;
// 端口声明
input [size-1:0] x,y;
output [2*size-1:0] out;
// 连线类型声明
wire [size*size-1:0] a;
wire [1:0] b0,b1,c0,c1,c2,c3;
wire [5:0] add_a,add_b;
wire [6:0] add_out;
wire [2*size-1:0] out;
assign
// 部分积
a={x[3],x[3],x[2],x[2],x[1],x[3],x[1],x[0],x[3],x[2],x[1],x[0],x[2],x[1],x[0],x[0]}&{y[3],y[2],y[3],y[2],y[3],y[1],y[2],y[3],y[0],y[1],y[1],y[2],y[0],y[0],y[1],y[0]};
// 实例化半加器
hadd U1(.x(a[8]), .y(a[9]), .out(b0));
hadd U2(.x(a[11]), .y(a[12]), .out(b1));
hadd U3(.x(a[4]), .y(a[5]), .out(c0));
// 实例化全加器
fadd U4(.x(a[6]), .y(a[7]), .z(b0[0]), .out(c1));
fadd U5(.x(a[13]),.y(a[14]),.z(b0[1]), .out(c2));
fadd U6(.x(b1[0]),.y(a[10]),.z(b1[1]), .out(c3));
// 加数
assign add_a = {c3[1],c2[1],c1[1],c0[1],a[3],a[1]};
assign add_b = {a[15],c3[0],c2[0],c1[0],c0[0],a[2]};
assign add_out = add_a + add_b;
assign out = {add_out,a[0]};
endmodule
// 全加器
module fadd(x,y,z,out);
output [1:0] out;
input x,y,z;
assign out = x+y+z;
endmodule
// 半加器
module hadd(x,y,out);
output [1:0]out;
input x,y;
assign out = x+y;
endmodule
测试代码:
module wallace_tb;
reg [3:0] x,y;
wire [7:0] out;
//模块例化
wallace m(.x(x),.y(y),.out(out));
//测试信号
initial
begin
x=3;y=4;
#20 x = 2; y = 3;
#20 x = 6; y = 8;
end
endmodule
复数乘法的算法是:设复数x=a+bi,y=c+di
,则复数乘法结果 x * y=(a+bi)(c+di)=(ac-bd)+i(ad+bc)
复数乘法器的电路结构如下图所示。将复数x的实部与复数y的实部相乘,减去x的虚部与y的虚部相乘,得到输出结果的实部。将x的实部与y的虚部相乘加上x的虚部与y的实部相乘,得到输出结果的虚部。
例:设计实部虚部均为4位二进制数的复数乘法器
module complex(a,b,c,d,out_real,out_im);
input[3:0] a,b,c,d;
output[8:0] out_real,out_im;
wire[7:0] sub1,sub2,add1,add2;
wallace U1(.x(a),.y(c),.out(sub1));
wallace U2(.x(b),.y(d),.out(sub2));
wallace U3(.x(a),.y(d),.out(add1));
wallace U4(.x(b),.y(c),.out(sub2));
assign out_real = sub1 - sub2;
assign out_im = add1 + add2;
endmodule;
测试代码:
module complex_tb;
reg[3:0] a,b,c,d;
wire[8:0] out_real,out_im;
complex U1(.a(a),.b(b),.c(c),.d(d),.out_real(out_real),.out_im(out_im));
initial
begin
a = 2;b = 2;c = 5;d = 4;
#10
a = 4;b = 3;c = 2;d = 1;
#10
a = 3;b = 2;c = 3;d = 4;
end
endmodule