Verilog HDL学习笔记
Verilog HDL(Hardware Description Language)是在一种硬件描述语言,类似于计算机的高级编程设计语言,它具有灵活性高,容易学习和使用等特点,同时Verilog能够通过文本的形式来描述数字系统的硬件结构和功能。
Verilog普遍适用于FPGA/IC开发领域。Verilog与C语言不一样,它是硬件描述语言,当它编译下载到FPGA后,会生成电路,所以Verilog是并行处理和运行的;C语言则是编译下载到CPU后,不会生成硬件电路,单片机CPU根据根据代码内容取指、译码、执行等串行工作。所以Verilog其能够并行快速运行的特点就决定了其是单片机替代不了的。
目录
⭐Verilog基础知识
关于HDL的共性处理
逻辑状态
标识符
常量与进制
数据类型
运算符
间隔符
字符串
关键字
注释
⭐Verilog的内部门级元件
Verilog中的门器件
多输入门、多输出门与三态门
⭐Verilog的基础语法与框架结构
Verilog的基本结构
模块调用
结构语句
赋值语句
循环语句
条件语句
⭐用Verilog描述MOS电路
N型MOS管和P型MOS管
CMOS管
⭐测试仿真平台
基本结构
实例
仿真过程
⭐用Verilog描述常用组合器件
4线-2线编码器
8线-3线优先编码器
4选1数据选择器
3线-8线译码器
串行加法器
⭐用Verilog描述时序电路
门控D锁存器
D触发器
同步置位清零D触发器
异步置位清零D触发器
4位双向移位寄存器
状态转换图
⭐Verilog基础知识
关于HDL的共性处理
Verilog作为硬件描述语言,在计算机对Verilog进行处理时自然也会遵循计算机对硬件描述语言的处理流程。计算机对HDL处理主要包括两大方面:逻辑仿真和逻辑综合。
逻辑仿真指的是计算机利用特定的HDL仿真器对Verilog程序进行解释,根据解释出来的内容对数字逻辑电路的结构进行布局以及对电路行为进行预测,并且通过文本或者波形图的方式将描述的表示的电路输出。在我们利用Verilog编写完一段程序后,需要将描述进行仿真处理,这样才能够提前发现描述中存在的不足之处,并且对描述进行修改。
逻辑综合是指将HDL描述的电路的逻辑关系转化成门或者触发器等元件的连接关系表,也就是将HDL描述翻译成实际电路元件的连接关系,这与C语言预编译、链接和汇编的流程不同,Verilog描述翻译后得到的是一个可以实际用于PCB设计的网络连接表。
逻辑状态
在C++中,我们描述一个变量的状态可以用BOOL类型来表示,它可能是真true,也可能是假false。但是在Verilog中,根据电路的四种状态分别设定了4种逻辑值,它们分别是:逻辑0、逻辑1、逻辑X和逻辑Z。其中逻辑0表示低电平,也就是GND,逻辑1表示高电平,也就是VCC,逻辑X表示电平状态未知,可能是0或者1,逻辑Z表示高阻态,可能是外部悬空。
标识符
Verilog中的标识符用于定义一个模块的名字、端口的名字和信号的名字等,它就像我们C语言中给变量起的名字,它可以是一组字母、数字、$和_的组合等。但是和C语言类似,它的第一个字符必须是字母或者下划线,并且它也是分大小的!
对于Verilog中的标识符,为了方便处理和认识,需要对其进行规范命名,一般命名可以按照以下的规则:①标识符最好能反映端口/模块的意义②下划线进行名词区分③采用简称前缀、后缀④缩写保持一致⑤模块和时钟信号命名保持一致⑥标识符不和关键字重名⑦参数大写。
常量与进制
在Verilog中包括了两种类型的常量,包括整数型常量和实数型常量,其中整数型常量有两种表达方式,实数型常量也有两种表达方式。
整数型常量表达方式
(1)使用简单的十进制数形式表达整数,这种方式表达的整数也可以称为是有符号常量,可以表达正整数和负整数,其中负整数通过补码形式表示。
(2)通过带基数的形式表示整数,它一般有4个组成部分:
<+/-><size>'<signed>radix integer_number
其中带”<>“的部分表示是可选的,第一部分<+/->表示常量是正整数还是负整数,一般正整数用+,或者省略该部分选项。<size>表示的是常量对应的二进制的位数,或者说位宽。<signed>是这个数字的类型,s或者S都表示它是有符号数,如果是无符号数可以省略该部分选项。radix表示该常量的进制。integer_number表示数据常量基于进制radix的数据。
在Verilog中可以表示的数字常量进制包括了二进制、八进制、十进制和十六进制,根据电路的特点,一般Verilog中常用的是二进制。而在Verilog中各种进制的表达二进制数0101如下所示:
二进制:4'b0101,其中4是位宽,是后面数据对应二进制数的位宽,b可以说是二进制(binary)的首字母简写;
八进制:4'o5,表示八进制数字5,o可以说是二进制(Octal)的首字母简写;
十进制:4'd5,表示十进制数字5,d可以说是二进制(Decimal System)的首字母简写;
十六进制:4'h5,表示十六进制数字5,h可以说是二进制(Hexadecimal)的首字母简写,并且在十六进制中符号a~f是不分大小写的;
如果代码中没有指明数字的位宽和进制时,那么默认该数字是一个32位的十进制数,也就是如果一个数字10,那么默认它是32'd10。
对于无符号数,如果定义的位数比较少,那么数值左边将被截断!如果定义的位数大于数值的位数,那么左边将被填0补齐(如果最左边位是x或者z,则不补0,补上x或者z),而如果常量是有符号数,那么左边将被填上符号位。
(1)3'b10100,实际上是3‘b100;
(2)10’b100,实际上是10‘b0000000100;
(3)5’bx10,实际上是5‘bxxx10;
(4)03’sb10,实际上是3‘sb110。
同时,如果我们的数据比较长,那么为了增加可读性,一般可以在数据之间添加下划线_来对数据进行分块。例如我们有一个十六位二进制数,可以将其写为:16'b1001_0010_1101_0001。
实数型常量表达方式
对于实数型常量,它也有两种表示方法:(1)利用十进制表示法:0.1、7.8、2.2等;(2)利用科学计数法:3.852e2(385.2)、2E-1(0.2)。
数据类型
Verilog中数据类型主要有三大类,包括寄存器类型、线网类型和参数类型,其中在数字电路中起到作用的是寄存器类型和线网类型。
线网类型
线网类型是表示结构元件间的物理连线的抽象,它的值由驱动它的元件的值确定,如果没有驱动输入到线网中,那么线网的缺省值为z,也就是高阻态。线网类型的变量只能够被连续赋值语句、assign和门等驱动,线网的类型包括wire和tri,其中最常用的是wire。例如我们给一个数据使能信号给线网:
wire data_en;
寄存器类型
线网类型提供了描述逻辑元件连接的方法,但是当我们想要描述的一个元件、电路发生行为过程中时,线网类型不能够被赋值,需要引入新的类型。寄存器类型是表示一个抽象的数据存储单元,它只能够在always和initial语句中被赋值,并且它的值从一个赋值到另一个赋值的过程中会被保留下来。并且寄存器变量的表达意义根据语句描述类型的不同而不同。如果语句描述的是时序逻辑,那么寄存器变量对应的是寄存器;如果语句描述的是组合逻辑,那么寄存器存储的是硬件连线。并且其缺省值为X,也就是不确定状态。
寄存器的数据类型包括reg、interger和real等,最常用的是reg,例如我们定义一个寄存器存储按键标志key_flag和一个延迟计数delay_count,从共性上可以看出定义一个寄存器的结构是reg+位宽(可选,缺省为1)+标识符的形式。
reg key_flag;
reg [31:0]delay_count;
参数类型
在Verilog中,参数就是一个常量,用于定义状态机的状态、数据位宽和延迟等,这些参数类型的参数可以在编译时被修改,所以我们一般将其定义在希望调整参数类型的模块中,例如我有一个变量是8位,但是我后面想将它调整成16位,那么我就定义一个参数类型DATA_WIDTH,想让它是8位时将参数的值设置为8位,想让它是16位时将参数的值设置为16位。但是需要注意一个参数类型只能在当前的模块中使用,就像我们C语言中定义的局部变量,不能超出函数体使用。下面就是定义一个参数类型,该参数类型的参数是数据的位宽,初始是8。
parameter DATA_WIDTH=8;
参数类型可以作为一个灵活的变量,例如我们有一个16位的寄存器,但是现在我想让它变成8位,后面又想让它变成4位或者其它的位数,那么我们就可以将寄存器定义时的位宽上限用一个参数类型的变量表示:
parameter WIDTH=8;
...
reg [WIDTH:0]test_reg;
并且在调用模块时,我们可以通过参数类型来修改被调用模块已定义的参数值。
运算符
在Verilog中,它的运算符大概可以分为七大类,包括算术运算符、关系运算符、逻辑运算符、条件运算符、位运算符、移位运算符和拼接运算符。
算术运算符
算术运算符就是数学里面的四则运算包括取模等操作,这些运算符也在C语言中有,并且符号一模一样。但是Verilog中实现乘除是比较耗费资源的,特别是除法,所以除法的实现一般调用底层组合逻辑搭建成的除法IP或者使用移位运算符。其中IP工具,Quartus/ISE会给我们提供。
关系运算符
关系运算符就是条件判断使用的运算符,如果该条件是真的,那么就会返回1,否则返回0。
逻辑运算符
逻辑运算符是连接多个表达式的,可以实现多个表达式关系之间的判断,这些运算符与C语言等编程语言的类似。
条件运算符
条件运算符根据输入来选择输出,它的作用类似于一个if和else语句。
类似于下面的代码:
if(a==true)
{
return b;
}
else
{
return c;
}
位运算符
位运算符是最基本的运算符,可以直接对数据中的与或非进行处理。它看起来功能很像逻辑运算符,但是它们是不一样的,逻辑运算符用于条件语句,位运算符用于单个变量。
移位运算符
移位运算符是将一个二进制数据的每一个位按一定方向进行移动,如果移动后有缺漏,那么将用0补齐。移动方向分为左移和右移,并且左移数据的位宽会增加,右移则不变。例如有一个八位的二进制数00001101(13),将这个数左移两位,那么该数据是0000110100(52)。如果将原来的数据右移三位,那么其数据就是00000001(1)。可以看出,左移就是在后面补上零;右移就是舍弃后面的位,前面补上0。
严格来说,上面的移位运算符是逻辑移位运算符,在Verilog中还有一个是算术移位运算符,算术左移是<<<,算术右移是>>>,其中逻辑左移与算术左移的效果一样。而算术右移与逻辑右移是不一样的,算术右移补位补的是最高位(MSB),不是0,这点与逻辑右移运算符不一样。例如数据5'b10010算术右移两位的结果是5’b11100。
拼接运算符
前面的运算符其实在C语言里几乎都有类似的,但是Verilog中还为我们提供了另外一种运算符--拼接运算符。用这个运算符可以将两个或者多个信号的某些位拼接起来进行运算。
例如我们有一个信号a[7:0] 和一个信号b[11:0],我们想把a和b的后4位合并,那么我们就需要写成:
c={a,b[3:0]};
这样拼接出来的新信号c是8+4=12位,前面8位是a,后面4位是b的后4位。
运算符优先级
和C语言类似,Verilog中也为众多的运算符划分了一个等级,这些等级规定了在多个运算符出现时,哪个操作先执行,哪些后执行,所有运算符的优先级如下图所示:
间隔符
对于Verilog来说,间隔符包括了TAB(\t)、空格(\b)和换行符(\n)三种,只要这三种符号不出现在字符串中,那么在解释Verilog的过程中这三种间隔符都会被忽略,所以我们写程序的时候可以间隔多个空格、换行等方式进行编写,但是最好还是写得好看点。
字符串
字符串是双引号引起的字符序列,这个就不能利用间隔符拆开成多行了,在表达式或者赋值的语句中,字符串要先转换成无符号的整数,用遗传8位的ASCII表示,其中每一个8位的ASCII值表示一个字符。例如字符串”abcd“就是32'h61626364。
关键字
与C语言中定义整型int、浮点数float、双精度double、判断if、循环while和for等需要用关键字类似,Verilog中也提供了众多的关键字,这些关键字是识别语法的关键,在Verilog中共有下面的关键字:
虽然上面的关键字看起来很多,但是日常最常用的关键字是下面这些关键字,值得注意的是,关键字通常是小写的,由于Verilog中区分大小写,所以大写的不算关键字,即IF、END、BEGIN等都不算关键字:
注释
Verilog中的注释方式与C语言类似,可以以/*为开头,*/为结尾进行整段注释,也可也以//为开头进行行注释。
行注释
//reg sig;
段注释
/*
...
*/
⭐Verilog的内部门级元件
Verilog中的门器件
在Verilog中,一共定义了12个基本的门器件模型,它们对应的代码如下图所示,当我们在Verilog中调用这些代码时,就相当于在建立了一个门电路,而门器件一般都是用小写的关键字表示。
多输入门、多输出门与三态门
从上面的门器件表中可以看出,前面的六个门器件都是多输入门器件,它们的特点是只有一个输出,但是可以允许存在多个输入,例如下面的与门,它可以同时允许多个输入,但是只能由一个输出。
多输入与门的代码如下所示,圆括号后面首先是输出信号,后面是n个输入信号。它是先实例一个门电路,然后传入输入和输出的,值得注意的是,实例名A1是可以省略的。
and A1(out,in1,in2,...);
而多输出门与多输入门有点类似,它的代码也是先实例一个门器件,然后传入多个输出h和一个输入,同理,它也可以省略实例化的名称。
not N1(out1,out2,out3,...out N,in);
而最后的四个三态门,三态门就是有一个输出、一个数据输入和一个控制输入的门电路,当控制信号是无效时,三态门的输出是高阻态z,一般其调用形式是:
bufif1 B1(out,in,crtl);
bufif0 B0(out,in,crtl);
notif1 N1(out,in,crtl);
notif0 N0(out,in,crtl);
⭐Verilog的基础语法与框架结构
Verilog的基本结构
对于C语言来说,它的基本部分是函数,而对于Verilog来说,它的基本部分是模块(module),模块可以表示一个门电路或者一个复杂的数字电路,而使用Verilog的目的就是将逻辑电路功能用一个或者多个模块进行表示,并且将这些模块用端口进行连接。一般情况下Verilog中的模块主要描述两部分的内容,一部分是接口,另外一部分是功能。一个基本的程序需要包含两大部分,信号的说明和功能的定义。
一个正常的Verilog模块组成部分如下所示:
module 模块名(端口1,端口2,端口3...);
(1)说明:
端口模式说明(input、output、inout);
参数定义(parameter,可选);
数据类型定义(wire、reg);
(2)逻辑功能描述:
实例底层模块、基本门元件;
连续赋值语句assgin;
过程快结构always、initiatl
行为描述语句;
endmodule
module跟的是模块的名称,是标识模块的标识符;而圆括号里面的是该模块所有的端口,包括输入端口、输出端口等端口的标识符;端口模式说明是用来定义每个端口的信号流经方向,以此来判断哪些是输入端口,哪些是输出端口,其中端口的类型有input-输入端口、output-输出端口和inout-双向端口;参数定义就类似于C语言的宏定义,将常量用符号常量来代替,它是可选的,也就是可以不写;而说明的最后部分是数据类型,数据类型就包括了wire连线型还是reg寄存器等。
第二部分是逻辑功能的描述,它有三种描述方式:(1)实例底层模块、基本门元件(2)连续赋值语句assgin;(3)过程块结构always、initiatl。这三种方式可以相互混合使用或者选其中的一种使用,并且顺序没有严格的限制,根据自己的需求进行选择将即可。
实例底层模块就是调用其它前期已经定义好的模块,通过相互连接端口、向其端口输入信号的方式对电路结构进行描述。或者是通过调用Verilog中的门电路描述元件来描述电路的结构。上面这两种方式统称为结构描述方式。
另一种是使用assign连续赋值语句对电路逻辑功能进行描述,这种方式也成为数据流描述方式。
最后一种方式是通过过程块结构,也就是always和initial语句和其它比较抽象和高级的语句对电路逻辑功能进行实现,这种方式常用于时序罗降低按路的设计,通常被称为行为描述方式。
例如下面的模块就是设计了一个模块,该模块的输入端口是a和b,输出端口是c。这就是IO说明,a、b和c都是wire类型,所以就没有写出来,这个是端口定义,中间有一个wire类型的变量d,这个是内部信号说明,最后定义输出c是a和b的或,这个是功能定义。这里对于输入一般默认是wire类型,而输出如果是在always里面赋值,一般定义成reg类型,而如果在assign赋值,则一般定义成wire类型。
module led(
input a,
input b,
output c
);
wire d;
assign c = a | b;
endmodule
下面是一个简单的组合逻辑电路,我们需要采用Verilog来对电路进行描述,首先写出电路的逻辑表达式:
我们可以利用门电路和连续赋值的方式分别对电路进行描述:
(1)门电路:
module test1(D0,D1,S,Y);
//参数定义
input D0,D1,S;
output Y;
wire Snot,A,B;
//功能描述
not U1(Snot,S);
and U2(A,D0,Snot);
and U3(B,D1,S);
or U4(Y,A,B);
endmodule
(2)连续赋值:
module test1(
input D0,D1,S,
output wire Y
);
//功能描述
assign Y = (~S & D0) | (S & D1);
endmodule
上面利用了门电路和连续赋值assign两种方式进行电路描述,注意上面的描述每一句语句都需要用分号“;”进行分割,但是最后的恩典module不需要。并且上面两种关于数据描述的方法是不一样的,后者是修订版本Verilog-2001/2005标准的写法,模块的端口列表中直接声明端口信号的方向和数据类型,这样写代码将更加紧凑。
模块调用
关于模块调用,类似于C语言的函数调用,这里我们称调用的模块是顶层模块,被调用的模块是子模块。假设我们有一个子模块time_count。
module time_count(
input clk,
input rst,
output reg flag
);
parameter N = 100;
endmodule
在顶层模块led_top中需要调用子模块,那么我们需要根据下面的方法来调用,首先是子模块名井号加括号,括号内部的.N 然后括起来一个(M)表示实例出来的子模块内部的参数类型变量N的数值用顶层模块中的参数类型变量M替代。然后跟着的u_time_count是实例出来的模块的名称。实例出来的模块名称后面跟着的括号里面的.clk (sys_clk)等表示将子模块的输入端口与顶层模块连接,将顶层模块的信号sys_clk、sys_rst输入到子模块,作为子模块的输入信号clk、rst。而子模块的输出端口与顶层模块连接,然后顶层模块需要用一个wire类型的信号add_flag来接收子模块的输出,并且子模块和顶层模块之间传递的输入和输出信号的位宽需要保持一致。
module led_top(
input sys_clk,
input sys_rst,
output reg led
);
parameter M = 6'd100;
wire add_flag;
//调用子模块time_count
time_count #(
.N (M))u_time_count(
.clk (sys_clk),
.rst (sys_rst),
.flag (add_flag)
);
结构语句
initial语句
在Verilog中共有两个比较重要的结构语句,包括前面提到的always和initial语句。其中initial语句常用于测试文件的编写,所谓测试文件就是仿真的文件,也就是说在initial语句中编写产生激励信号,再通过仿真的示波器查看后续的结果;同时,它还可以对存储器的变量进行初值修改。它只会执行一次,例如下面的就是一个initial语句模块,在这个模块中,我们首先将sys_clk信号和sys_rst变量置为0,然后看输出。#20 sys_rst <=1'b1的意思是在延迟20个单位时间后,再将sys_rst变量置为1,再此基础上,再经过10个单位时间后也就是在30个单位时间后再将sys_rst置为0。
initial begin
sys_clk <=1'b0;
sys_rst <=1'b0;
#20 sys_rst <= 1'b1;
#10 sys_rst <= 1'b0;
end
always语句
而除了initial语句外,在Verilog中还有另一种语句always,这种语句与initial语句不同,它会反复执行,例如我们将信号sys_clk设置为一个周期为20个时间单位的方波:
always #10 sys_clk <= ~sys_clk;
而对于always语句,由于其在满足要求的情况下能够一直执行,那么很适合用来等待信号并且判断执行某些程序。例如我们硬件电路中经常需要等待某个下降沿、上升沿后执行一个操作,那么我们就利用always语句来实现,下面这个程序代表当sys_clk的上升沿(posedge)或者sys_rst的下降沿(negedge)到达后即可看是执行count加1的操作,其中or连接起来的事件被称为敏感列表:
always @(posedge sys_clk or negedge sys_rst) begin
count <= count + 1'b1;
end
前面讲了利用always进行边沿信号除法的方式,下面是利用电平信号进行触发的方式,利用电平信号触发的话, 使用方式就和利用边沿触发不同。例如下面就是利用always进行电平变化触发的方式,里面的b和c信号电平变化都会引起always语句进入执行,也就是等号右边的信号都需要写入到敏感列表中,但是有时候等号右边的信号未必全部都能写齐,所以这里使用*来代替全部的信号,需要注意的是,在always中的赋值语句,其左边的数据类型需要是reg类型的变量,也就是下面的a必须是reg类型。
always @(*) begin
a = b + c;
end
/*
always @(b or c) begin
a = b + c;
end
*/
赋值语句
在Verilog中,信号的赋值有两种方式,包括阻塞赋值和非阻塞赋值,其中阻塞赋值是(a=b),非阻塞赋值是(a<=b)。
阻塞赋值
对于阻塞赋值,它的执行理念是先计算等号右边的表达式RHS,然后再去更新等号左边的值,并且它的执行原则是上一条赋值语句结束后,下一条赋值语句才会开始执行,例如下面的例子中c最后的结果是0。因为先执行第一条语句令a=0,然后b=a=0,最后c再等于b等于0。
//初始
a = 1;
b = 2;
c = 3;
//执行的语句
a = 0;
b = a;
c = b;
非阻塞赋值
对于非阻塞赋值,它的执行理念是先计算各语句右边表达式的值,然后将各个值分别存入暂存器,最后在语句结束前将数值赋给左边变量。例如同一个代码下面执行的结果是a=0,b=1,c=2,因为这里所有赋值都是并行的。值得注意的是非阻塞赋值只能用于赋值寄存器变量,所以需要放在always和inital中。
//初始
a = 1;
b = 2;
c = 3;
//执行的语句
a = 0;
b = a;
c = b;
既然阻塞赋值和非阻塞赋值效果不一样,那么我们就应该对两种使用方式进行细致了解。其中在组合逻辑电路中,我们常用阻塞赋值,而在时序逻辑电路中,我们常用非阻塞赋值。但是无论哪种使用方式,我们都应该注意两点:①不要在一个always中使用多种赋值方式;②一个相同变量不能在不同always中多次赋值!!!!
循环语句
与C语言类似,在Verilog中也有循环语句,常用的就是for循环,它的结构和C语言的结构类似,都是有循环初始值,边界和迭代步长:
for(initial_assignment;condition;step_assignment)
statement;
需要注意,在这里循环迭代变化的变量需要是一个integer类型的变量,不能是reg或者其它类型,其次statement可以是赋值、分支等其它语句。
条件语句
if语句
条件语句的编写框架和C语言中的条件判断语句结构类似,不过它也是需要在过程模块,也就是intital和always模块中使用。并且在条件判断中,“假”不仅是低电平状态0,还是高阻态Z和未知状态X。例如下面就是一个判断语句:
//模板
if(condition_expr1) true_statement1;
else if(condition_expr2) true_statement2;
else if(condition_expr3) true_statement3;
...
else true_statementn;
//实例
if(sys_clk == 1) begin
num = 1;
end
else begin
num = 0;
end
但是需要注意,如果我们有一个if,多个else if语句,那么这些else if语句是按顺序从上到下进行判断的,一旦判断到一个符合要求的就不会继续判断下去,所以这里也存在一定的隐形优先级的问题,需要我们多加考虑和斟酌。
switch语句
但是条件语句不仅是if、else,还有分支选择语句,而在Verilog中分支选择语句与C语言类似但是不同,不同的点是Verilog中没有switch,只有case:
//模板
case(case_expr)
item_expr1:statement1;
item_expr2:statement2;
...
default:default_statement;
endcase
//实例
always @(posedge sys_clk or negedge sys_rst) begin
case(flag)
4'b1000: 语句1;
4'b1001: 语句2;
4'b1010: 语句3;
4'b1011: 语句4;
4'b1100: 语句5;
...
4'b1101: 语句6;
default:语句7;
endcase
end
这里十分需要注意五点:①分支表达式的选择值不能一样,否则会混乱;②表达式的位宽必须相等,也就是前面的4'b1000等的位宽必须相等,这就决定了写这些数字时不能用'b的方式,因为这样很容易混乱,如果我都flag是一个4位的,但是分支表达式不写4位也就是默认32位很容易报错;③如果case改成casez,那么比对时不考虑高阻值,也就是说如果我都分支表达式是4'b10ZZ,那么如果flag是4'b1000也符合,但是如果我都分支表达式是4'b10XZ,那么如果flag是4'b1000就不符合了,因为第三个X也需要对上;④而如果case改成casex,那么比对时不考虑高阻值和未定义状态Z,也就是说如果我都分支表达式是4'b10ZZ,那么如果flag是4'b1000也符合,如果我都分支表达式是4'b10XZ,那么如果flag是4'b1000也符合了,因为X不需要对上了。⑤如果使用casex和casez,需要考虑隐形的优先级问题!
⭐用Verilog描述MOS电路
N型MOS管和P型MOS管
通常情况下,我们利用Verilog描述门电路已经是基础单位了,但是随着电路变得更加复杂,我们可能不仅需要考虑逻辑门的运行,还有三极管的运行。为了满足设计者对于三极管的设计需求,Verilog中为我们提供了CMOS电路的描述方法。但是需要注意,在Verilog中,三极管模型只能被当做导通或者截至的开关,不存在模拟电路中的其它特性。
在Verilog中,利用nmos和pmos关键字来分别表示NMOS型三极管和PMOS型三极管,引用时可以通过下面的方式来进行描述:
nmos N1(漏极,源极,栅极);
pmos P1(漏极,源极,栅极);
对于NMOS元件,如果栅极输入是1,那么MOS管导通,此时信号能够从源极流向漏极;而如果栅极输入是0,那么输出就是高阻态z。与之相对,如果PMOS管栅极输入是0,那么MOS管导通,此时信号能够从源极流向漏极;而如果栅极输入是1,那么漏极输出就是高阻态z。
在nmos和pmos以外,还存在另外一种MOS三极管的模型rnmos和rpmos,它们表达的是源极和漏极存在电阻的情况,当信号从源极输入,漏极输出时,它的强度会减弱。其中它们的使用方法和前面一样:
rnmos N1(漏极,源极,栅极);
rpmos P1(漏极,源极,栅极);
除了MOS管,在Verilog中还定义了电源和地线,电源线是关键字supply1,它等效于仿真期间将线网置于高电平1;地线是关键字supply0,它等效于将线网置于低电平0。例如我们定义一个地线信号GND和一个电源信号VDD:
supply1 VDD;
supply0 GND;
CMOS管
除了N/P型MOS管外,我们还经常用到CMOS运输门电路,对于这种门电路,Verilog中也定义了相应的关键字cmos。
而cmos的使用方式和nmos和pmos类似,如果TN管输入是1而TP管输入是0,那么CMOS开关导通;如果TN管输入是0而TP管输入是1,那么CMOS开关关闭,此时输出是高阻态。类似的,关键字rcmos定义了一种输入和输出间存在电阻的三极管模型,这种模型会使得输入的信号输出衰弱。同时由于CMOS的电源和地是与衬底连接的,所以在利用关键字实例化一个CMOS元件时,不需要考虑VDD和GND的连接问题。
cmos C1(输出信号,输入信号,TN管控制信号,TP管控制信号);
rcmos C2(输出信号,输入信号,TN管控制信号,TP管控制信号);
⭐测试仿真平台
基本结构
在Verilog语言中,在完成功能模块后我们往往还需要设计一个测试模块对我们的功能模块进行全面的验证,我们通常将实现逻辑功能的模块和测试的模块分开来写,这样将有助于我们的理解和后续工程的开展。测试模块一般又称为测试平台(Testbench),这个测试平台通常是顶层模块,也就是说我们的功能模块一般放在下面被测试模块调用。
测试平台的结构和功能模块类似,都是通过module开头,endmodule模块结尾,只不过它没有端口列表和端口类型的声明,通常只包括:声明信号类型、实例化引用测试的功能模块、施加激励信号和测试结果输出显示等四部分。它们的具体结构如下所示:
module 测试模块名();
...
reg 输入信号名;
...
Wire 输出信号名;
...
实例化功能模块
initial begin
...//激励添加
end
always begin
...//时钟信号
end
initial begin
...//显示输出
end
endmodule
实例
在进行仿真测试之前,我们根据书上的例子准备一个实现多功能逻辑运算(ALU)的代码,该示意图和代码分别如下所示。它包含了三部分模块、四个输入信号和八个输出信号以及一个中间信号node_y。
首先第一个always表示一个算术运算器,它根据输入信号f的值来对计算方式进行选择,一共有四种选择方式,f从00~11分别表示将输入信号a和b进行加法运算、减法运算、与运算和异或运算。其中加法运算和减法运算分别是f是00和01时。a和b是输入计算的参数,它们是一个四位数字,其中首位表示符号位。而存储运算结果的node_y是一个五位的数字,因为它的最高位要存储进位标志co。而溢出标志位是等于输入数a和b的符号位与输出结果、进位标志位的异或。因为只要满足a和b符号相同,且没有进位,但是结果符号位和他们不一样,那么就表示溢出了,或者说如果两个正数相加得到负数、两个负数相加得到整数,那么它就是溢出了!
第二部分是比较器,这个很简单,就是根据我们输入的数据a和b进行大小比较即可,主要是最后利用位拼接赋值的新方法,它是将a_gt_b, a_eq_b, a_lt_b放到一个大向量里面,然后将三位数字每一位分别赋值给这三个数字,例如当它们相等时,0给a_gt_b,1给a_eq_b,0给a_lt_b。
最后是将临时的结果node_y输出给y,不过需要先判断三态门oe是否开启了控制,否则y是高阻态。其次还需要进行零位判断,就是判断node_y中前4位是否都是0,这里它用了缩位运算符~|,就等于 ~(|node_y[3]|node_y[2]|node_y[1]|node_y[0]);
module alu_4bit(
input[3:0] a,b,
input[1:0] f,
input oe,
output[3:0] y,
output reg co, ov, neg,
output zero,
output reg a_gt_b, a_eq_b,a_lt_b
);
reg[4:0] node_y;
always@(a or b or f)begin:alu//带有:xxx的begin语句被称为有名块
co = 1'b0;
ov = 1'b0;
node_y = 5'b00000;
case(f)
2'b00:
begin
node_y = a+b;
co = node_y[4];
ov = co^node_y[3]^a[3]^b[3];
end
2'b01:
begin
node_y = a-b;
co = node_y[4];
ov = co^node_y[3]^a[3]^b[3];
end
2'b10:
node_y[3:0] = a & b;
2'b11:
node_y[3:0] = a ^ b;
default:
node_y[3:0] = 4'b0000;
endcase
neg = node_y[3];
end
always@(a or b) begin:comparator
if(a>b)
{a_gt_b, a_eq_b,a_lt_b} = 3'b100;
else if(a<b)
{a_gt_b, a_eq_b,a_lt_b} = 3'b001;
else
{a_gt_b, a_eq_b,a_lt_b} = 3'b010;
end
assign y = oe?node_y[3:0]:4'bz;
assign zero = ~|node_y[3:0];
endmodule
上面的功能模块写好后,我们就可以开始准备测试模块了,鉴于电路输入只有4个,我们就假定给一个输入a=1011,b=0110,f就取加法00,oe控制位置1,于是可以根据测试模块的特点写出下面的测试代码:
`timescale 1ns/1ns
module test_alu_4bit;
reg [3:0] a=4'b1011,b=4'b0110;
reg [1:0] f=2'b00;
reg oe = 1;
wire [3:0] y;
wire co, ov, neg, zero, a_gt_b, a_eq_b, a_lt_b;
//实例化
alu_4bit U1(a,b,f,oe,y,co, ov, neg, zero, a_gt_b, a_eq_b, a_lt_b);
//激励输入
initial begin
#20 b=4'b1011;
#20 b=4'b1110;
#20 b=4'b1111;
#80 oe= 1'b0;
#20 $finish; //结束仿真
end
always #23 f= f+1;
endmodule
仿真过程
在前面已经准备好了设计的功能模块和仿真测试模块,接下来就可以开始准备进行仿真了,仿真前首先利用文本编辑器将两个程序分别写入两个文本文件里,文件的后缀是.v,随后将两个文件放到一个新建的文件夹里,这里我将文件夹起名位alu_4bit:
在Modelsim中创建工程,并且引入两个文件,注意工程名称命名为tb_test_alu_4bit,然后点击编译运行两项文件,运行成功会有绿色箭头的提示:
然后点击下方的Library切换到初始的目录,右键点击测试平台,并且点击Simulate进行仿真,添加到仿真器中。
接下来点击Add->To Wave->All items in region添加输入输出波形到波形图
最后切换到Wave窗口,输入150ns,点击run运行即可观察到波形。
⭐用Verilog描述常用组合器件
在前面分析了Verilog中的一些常用语句,并且学习了利用门电路和连续赋值的方式来对电路进行建模,并且也初步学习使用了always、switch和ifelse等语句,接下来要学习的是两种新的建模方法:行为级建模和分层次的结构化建模。
首先行为级建模就是用Verilog来描述一种逻辑功能的实现,而并非像门电路那样用实际的硬件去实现。在这种建模方式中我们常用的就是使用了always、switch、ifelse和for等语句。下面我们使用行为级建模的方式利用Verilog实现几个常见的电路元件。
4线-2线编码器
编码器是我们日常生活很常见的元器件,它能够将一个二进制的代码表示成含对应信息的编码。而对于二进制的编码器,也是我们最常见的编码器之一,通常情况下它的输入是2^n个信号,而输出是n个信号,下面是一个4线-2线编码器,它的输入是4个信号,输出是2个信号。
它对应的逻辑表达式为:
根据它的逻辑表达式,可以写出它的实现代码和测试代码。
Verilog的功能模块代码为:
module bcd(
input [3:0] I,
output reg Y0,Y1
);
always@(I) begin
Y1 = ~I[0]*~I[1]*I[2]*~I[3] +~I[0]*~I[1]*~I[2]*I[3];
Y0 = ~I[0]*I[1]*~I[2]*~I[3] +~I[0]*~I[1]*~I[2]*I[3];
end
测试代码:
`timescale 1ns/1ns
module test_encoder;
reg [3:0] I;
wire Y0,Y1 ;
//实例化
encoder encoder(I,Y0,Y1);
//激励输入
initial begin
I=4'b0001;
#20 I=4'b0010;
#20 I=4'b0100;
#20 I=4'b1000;
#20 $finish; //结束仿真
end
endmodule
最后利用ModelSim得到的仿真结果如下所示,当I = 0,Y0=0,Y1=0;当I = 2,Y0=1,Y1=0;当I = 4,Y0=0,Y1=1;当I = 8,Y0=1,Y1=1;
8线-3线优先编码器
除了我们常见的普通编码器,还有一类编码器叫优先编码器,它能够允许多个输入信号同时有效,根据输入信号的优先级来进行编码输出。
它对应的逻辑表达式为:
根据它的逻辑表达式,可以写出它的实现代码和测试代码。
Verilog的功能模块代码为:
module encoder83(
input EI,
input [7:0] I,
output reg [3:0] Y,
output reg GS, EO
);
always@(I or EI) begin
if(EI == 1'b0) Y = 8'b00000000;
else begin
GS = 0;
EO = 0;
casex(I)
8'b1xxx_xxxx: Y = 3'b111;
8'b01xx_xxxx: Y = 3'b110;
8'b001x_xxxx: Y = 3'b101;
8'b0001_xxxx: Y = 3'b100;
8'b0000_1xxx: Y = 3'b011;
8'b0000_01xx: Y = 3'b010;
8'b0000_001x: Y = 3'b001;
8'b0000_0001: Y = 3'b000;
default: begin
GS = 0;
EO = 1;
Y = 3'b000;
end
endcase
end
end
endmodule
测试代码:
`timescale 1ns/1ns
module test_encoder83;
reg EI;
reg [7:0] I;
wire [3:0] Y;
wire GS, EO;
encoder83 e83(.EI(EI),.I(I),.Y(Y),.GS(GS),.EO(EO));
initial begin
EI = 0;
#20 EI = 1;I=8'b0000_0000;
#20 EI = 1;I=8'b0000_0001;
#20 EI = 1;I=8'b0000_0011;
#20 EI = 1;I=8'b0000_0101;
#20 EI = 1;I=8'b0000_1010;
#20 EI = 1;I=8'b0001_0010;
#20 EI = 1;I=8'b0010_0100;
#20 EI = 1;I=8'b0101_0000;
#20 EI = 1;I=8'b1001_0000;
end
endmodule
仿真结果:为了更好测试它的优先级,我将非优先级那一位后面的数字随机置1,结果测试发现它还是调用优先级高的输入。
4选1数据选择器
数据选择器是根据控制信号将多路数据进行选择,选出合适的一路数据传输到公共数据线上,实现数据选择的逻辑电路。如下所示是一个简单的数据选择器,它有四路输入D0~D3,只有一路输出Y,有两路选择S0~S1。
它的输入和输出选择的逻辑函数为:
Verilog的功能模块代码为:
module data_select(
input [1:0] S,
input [3:0] D,
output reg Y
);
always@(S or D) begin
if(S == 2'b00) Y = D[0];
else if(S == 2'b01) Y = D[1];
else if(S == 2'b10) Y = D[2];
else Y = D[3];
end
endmodule
测试代码:
`timescale 1ns/1ns
module test_data_select;
reg [1:0] S;
reg [3:0] D;
wire Y;
data_select ds(S,D,Y);
initial
begin
D = 4'b1010;
S = 2'b00;
#20 S = 2'b01;
#20 S = 2'b10;
#20 S = 2'b11;
end
endmodule
我将D定义成1010,S一开始取00,然后每隔20ns变成01、10和11,那么Y一开始输出0,然后输出1,然后输出0,最后再输出1。最后得到的仿真结果是:
3线-8线译码器
既然有器件能够将输入信号编码成二进制信号,那么自然也会有器件能够具有特殊含义的二进制代码翻译成输出信号,这个器件就是译码器。译码器有两种类型,一种是将代码转换成有效信号,一种是将代码转换成另一种代码,下面是一种将输入代码转换成信号的3线-8线译码器。
它的逻辑表达式为:
同时3线-8线译码器的功能表对应如下图所示,图片来源于网络:
Verilog的功能模块代码为:
module decoder(
input[2:0] A,
input En,
output reg [7:0] Y
);
integer k;
always@(En or A) begin
Y = 8'b1111_1111;
for(k=0;k<=7;k=k+1) begin
if(En == 1 && A == k)
Y[k] = 0;
else
Y[k] = 1;
end
end
endmodule
测试代码:
`timescale 1ns/1ns
module test_decoder;
reg[2:0] A;
reg En;
wire [7:0] Y;
decoder dc38(.A(A),.En(En),.Y(Y));
initial begin
En = 1;
A = 3'd0;
#20 A = 3'd1;
#20 A = 3'd2;
#20 A = 3'd3;
#20 A = 3'd4;
#20 A = 3'd5;
#20 A = 3'd6;
#20 A = 3'd7;
end
endmodule
由于38译码器中只有一个控制位是高电平有效,所以在代码中,最后我设定输入控制位En为1,将输入信号A的数值每隔20ns变化一次,从000~111,可以看出输出的信号如下图所示在不断变化:
串行加法器
加法作为算术运算中最基本的功能,在数字电路中不可避免地需要引入关于加法运算的算术运算电路。其中关于加法运算的逻辑电路主要分为半加器和全加器。半价器就是输入两个二进制数值,将这两个数值相加的结果输出,同时输出高位进位信号的加法器。而全加器的输入在半加器的基础上增加了来自低位的进位,也就是输入信号除了两个二进制数,还包括一个进位。增加了这种可能性后,我们就可以在全加器的基础上设计串行加法器。
半加器:
逻辑表达式:
全加器:
逻辑表达式:
串行加法器:
不难看出,无论是半加还是全加器,它们只能处理一位数字的加法,但是如果是多位数字,那么我们就需要使用新的加法器,这里主要是串行加法器,它们主要通过全加器串联起来,前一个全加器的高进位输出作为后一个全加器的低进位输入。
Verilog实现四位全加器的功能模块代码为:
module serial_adder #(parameter n = 4)(
input [n-1:0] A,
input [n-1:0] B,
input Ci,
output reg [n-1:0]S,
output reg Co
);
integer k;
reg C_temp;
always@(A or B or Ci) begin
C_temp = Ci;
for(k=0;k<n;k=k+1) begin
S[k] = A[k]^B[k]^C_temp;
C_temp = A[k]*B[k]+(A[k]^B[k])*C_temp;
Co = C_temp;
end
end
endmodule
测试代码:
`timescale 1ns/1ns
module test_serial_adder;
reg [3:0] A;
reg [3:0] B;
reg Ci;
wire [3:0]S;
wire Co;
serial_adder sa(.A(A),.B(B),.Ci(Ci),.S(S),.Co(Co));
initial begin
A = 4'b1010;
B = 4'b1000;
Ci = 0;
#20
A = 4'b0001;
B = 4'b0001;
Ci = 0;
#20
A = 4'b0011;
B = 4'b0001;
Ci = 1;
#20
A = 4'b0101;
B = 4'b0001;
Ci = 1;
#20 $finish; //结束仿真
end
endmodule
最后我在测试模块中我先输入1010+1000应该得到:0010+Co,隔20ns输入0001+0001应该得到:0010;再隔20ns输入0011+0001+Ci应该得到:0101;最后再隔20ns输入0101+0001+Ci应该得到:0111,测试结果如下图所示:
⭐用Verilog描述时序电路
在引入时序电路之前,我们需要学习一下锁存器和触发器,锁存器(Latch)是一种对脉冲电平敏感的双稳态电路,它有0和1两个状态,一旦我们确定了它处于某个状态,那么它将会一直保持下去,知道有外部的脉冲电平输入才有可能改变它当前的数值,常见的锁存器有SR锁存器、D锁存器等。而触发器是一种对边缘信号敏感的双稳态电路,它和锁存器不同,它对时钟脉冲边缘敏感,而锁存器对电平状态敏感,这两种器件在Verilog中的描述方式是不同的。
门控D锁存器
门控D锁存器的电路:
功能列表:
E | D | Q | Q_N | 功能 |
0 | X | 不变 | 不变 | 保持 |
1 | 0 | 0 | 1 | 置0 |
1 | 1 | 1 | 0 | 置1 |
代码:
module d_latch(
input En,
input D,
output reg Q,
output reg Q_N
);
always@(En or D) begin
if(En != 0)
Q <= D;//没有else,此时Verilog综合器会用锁存器实现电路,保持数值不变。
end
assign Q_N = ~Q;
endmodule
D触发器
功能列表:
D | Qn | Qn+1 | 功能 |
0 | 0 | 0 | 次态与D相同 |
0 | 1 | 0 | 次态与D相同 |
1 | 0 | 1 | 次态与D相同 |
1 | 1 | 1 | 次态与D相同 |
代码:
module DFF(
input CP;
input D;
output reg Q;
)
always@(posedge CP) begin
Q <= D;
end
endmodule
同步置位清零D触发器
功能列表:
SD_N | RD_N | CP | D | Q | Q_N |
L | H | ↑ | × | H | L |
H | L | ↑ | × | L | H |
L | L | ↑ | × | 保持 | 保持 |
H | H | ↑ | L | L | H |
H | H | ↑ | H | H | L |
代码:
module set_rst_DFF(
input CP,
input D,
input Rd,
input Sd,
output reg Q
);
always @(posedge CP) begin
if(Rd == 1 && Sd ==0) Q<=1'b1;//第一优先级-置位
else if(Rd == 0 && Sd == 1) Q<=1'b0;//第二优先级-清零
else Q<=D;//第三优先级-D触发器功能
end
endmodule
异步置位清零D触发器
功能列表:
SD_N | RD_N | CP | D | Q | Q_N |
L | H | × | × | H | L |
H | L | × | × | L | H |
L | L | × | × | H | H |
H | H | ↑ | L | L | H |
H | H | ↑ | H | H | L |
代码:
module async_set_rst_DFF(
input Rd,
input Sd,
input D,
input CP,
output reg Q
);
always@(posedge CP or negedge Sd or negedge Rd) begin
if(Sd == 0 || Rd == 0) begin
if(Sd == 0) Q <= 1'b1;//第一优先级-置位
else Q<= 1'b0;//第二优先级-清零
end
else begin
Q<=D; //第三优先级-D触发器功能
end
end
在对锁存器和触发器的Verilog描述有了一定认识后,接下来我们可以开始学习更加深入的时序逻辑电路的Verilog描述。
4位双向移位寄存器
4位双向移位寄存器是一种实现输入数据左移、右移、保持和并行输入输出的多功能寄存器,它能够将通过选择S0和S1的数值实现不同的功能。当CR为0时,输出Qo就会被全部置0,这就是异步清零功能的;当CR不为0,且S0和S1为0,此时无论时钟是否到达、DSR、DSL和Di是否右输入,都是保持状态;当S0、S1为10,那么此时就选择数据右移且输入的功能,DSR进入移位寄存器且取代Di的第一位,Di其余三位向右移动,在下个时钟边缘到达时将更新后的Di全部值赋予给Do;;当S0、S1为01,那么此时就选择数据右移且输入的功能,DSL进入移位寄存器且取代Di的末位,Di其余三位向左移动,在下个时钟边缘到达时将更新后的Di全部值赋予给Do;当S0、S1为11,那么此时代表着并行输入,在下个时钟边缘到达时将Di全部值赋予给Do。
功能列表:
4位双向移位寄存器代码:
module shift74x194_beh(
input DSR,
input DSL,
input [3:0]Di,
input CP,
input CR_N,
input [1:0]S,
output reg [3:0] Qo
);
always@(posedge CP or negedge CR_N) begin
if(!CR_N) Qo<=4'b0000;
else begin
case(S)
2'b00:Qo<=Qo;
2'b01:Qo<={DSR,Qo[3:1]};
2'b10:QO<={Qo[2:0],DSL};
2'b11:Qo<=Di;
endcase
end
end
endmodule
测试代码:这里我设计的测试方式是Di原来是1111,然后DSR是0,右移三次逐渐将0替代1111的过程,这里值得注意的是,在我们使用双向移位器之前,需要先将Di的数据并行输入到每一个数据选择器中,从而在下个时钟周期导入到触发器,否则是仿真不出波形的!
`timescale 1ns/1ns
module test_shift74x194_beh;
reg DSR;
reg DSL;
reg [3:0]Di;
reg CP;
reg CR_N;
reg [1:0]S;
wire [3:0] Qo;
shift74x194_beh shift74x194(.DSR(DSR),.DSL(DSL),.Di(Di),.CP(CP),.CR_N(CR_N),.S(S),.Qo(Qo));
/*
initial begin
CR_N = 1'b0; //清零
CP = 1'b0;//时钟复位
#20 Di = 4'b1111;
S = 2'b01;
DSR = 1'b0;
CR_N = 1'b1;
#20 CP = ~CP;//第一次高电平,发生右移,变成0111
#20 CP = ~CP;
#20 CP = ~CP;//第二次高电平,发生右移,变成0011
#20 CP = ~CP;
#20 CP = ~CP;//第三次高电平,发生右移,变成0001
#20 CP = ~CP;*/
initial begin
//初始化
CR_N = 1'b0; //清零
CP = 1'b0;//时钟复位
Di = 4'b0000;
DSL = 1'b0;
DSR = 1'b0;
S = 2'b00;
//并行加载数据
#20 CR_N = 1'b1;
Di = 4'b1111;
S = 2'b11;
#20 CP = ~CP;//第一次高电平,并行加载数据
#20 CP = ~CP;
//开始右移操作
#20 S = 2'b01;
DSR = 1'b0;
#20 CP = ~CP;//第二次高电平,发生右移,变成0111
#20 CP = ~CP;
#20 CP = ~CP;//第三次高电平,发生右移,变成0011
#20 CP = ~CP;
#20 CP = ~CP;//第四次高电平,发生右移,变成0001
#20 CP = ~CP;
end
endmodule
测试结果:
状态转换图
对于一般的时序逻辑电路,我们往往是利用状态转换图来表示其工作的具体过程,所以我们需要掌握利用Verilog来描述时序电路中状态转换图的方法。关于描述状态转换图的方法有很多,但是总体来说关于状态转换图的设计过程主要包含四大部分:总结起来就是四句:状态定义、状态转换、状态计算和输出逻辑。
(1)利用参数定义语句parameter定义各个状态,并且给每个状态都赋予二进制编码,例如:
parameter S1=2'b00,S2=2'b01,S3=2'b10,S4=2'b11;
parameter [1:0] S1=2'b00,S2=2'b01,S3=2'b10,S4=2'b11;
(2)使用always语句描述时钟控制的电路状态转换部分。
(3)使用always语句和case语句描述电路的组合部分,利用现态和输入信号计算出组合电路的输出,作为下一个状态的值。
(4)在当前状态/输入信号条件下描述状态机的输出逻辑。
例子1:
Verilog描述:这里用的不是传统的四部,它是将状态更新和状态计算都放入了一个时序电路中,这在我们实现穆尔型电路是是可行的,但是如果是米利型电路,则需要分开!这里注意不要遗漏了默认状态以及CR复位的情况
module std_eg1(
input CP,
input CR_N,
input Data,
output reg[1:0]Qo
);
//状态定义
parameter S0=2'b00;
parameter S1=2'b01;
parameter S2=2'b10;
parameter S3=2'b11;
reg[1:0] Now_S;
//状态计算,使用阻塞赋值
always@(posedge CP or negedge CR_N) begin
if(!CR_N)
Now_S<=S0;//复位
case(Now_S)
S0:if(!Data) Now_S<=S1;
S1:if(Data) Now_S<=S2;
else Now_S<=S3;
S2:if(!Data) Now_S<=S3;
S3:if(!Data) Now_S<=S0;
default:Now_S<=S0;//默认状态
endcase
end
assign Qo=Now_S;
endmodule
例子2:
例子2是一个米利型电路的状态转换图,它与穆尔型不一样,它的输出Y是与输入有关的,也就是说输入信号A和当前状态Now_S共同影响Y的取值。
module std_eg2(
input A,
input CR_N,
input CP,
output reg Y
);
//状态定义
parameter Sa=2'b00;
parameter Sb=2'b01;
parameter Sc=2'b10;
parameter Sd=2'b11;
reg [1:0] Now_S;
reg [1:0] Next_S;
//状态更换
always@(posedge CP or negedge CR_N) begin
if(!CR_N) Now_S <= Sa;
else Now_S <= Next_S;
end
//状态计算
always@(*) begin
Next_S = Now_S;//添加默认状态
Y = 0;
case(Now_S)
Sa:Next_S=(A==1)?Sb:Sa;
Sb:Next_S=(A==1)?Sc:Sa;
Sc:begin
if(!A) begin
Y = 1;//赋值
Next_S = Sd;
end
else
Next_S = Sc;
end
Sd:Next_S=(A==0)?Sa:Sb;
default: Next_S=Sa;//默认状态
endcase
end
endmodule
上面我们分别举了穆尔型时序电路的状态转换图和米利型时序电路的状态转换图,而这两种时序电路的具体定义和状态机的详细介绍在下一篇博客。
参考资料:
《电子技术基础 数字部分(第七版)》 康华光,张林
正点原子FPGA配套视频资料