本文撰写的参考书目是陈彦辉老师的《数字逻辑电路基础》
一.Verilog语法知识简介
1.模块结构
Verilog程序的最基本设计单元是“模块”,模块从关键字**module**开始,到**endmodule**结束,其中每条语句以";"分隔。
1
一个完整的模块由以下四个部分组成:
●模块定义行:定义模块名称、输入输出参数列表;
●说明部分:定义不同的项,其中包括端口类型(input输入、output输出和inout双向端口)、寄存器(reg)、连线(wire)、参数(parameter)、函数(function)和任务(task);
●描述体部分:这一部分描述模块的行为和功能;
●结束行:以endmodule结束
下面以一个例子进行一个大致的认知,暂且先不管语句是什么意思,只需要理解结构即可。
//模块定义行,test为模块名,括号内为参数列表(这里可以不说明其输入输出类型)
module test(A,B,C,D,F1,F2);
//说明部分,将A,B,C,D定义为输入,F1、F2为输出,wire和reg类型暂且先不管
input A,B,C,D;
output F1,F2;
wire F1;
reg F2;
//描述体部分,F2是F1经过一个D触发器,将数据延迟一拍(在D的上升沿才将F1赋值给F2)
always@(posedge D)
F2 <= F1;
//描述体部分,F1= AB + (~A)(~C)
assign F1 = (A & B)|(~A & ~C);
//结束行
endmodule
(1)模块声明
格式如下:
module 模块名(端口名1,端口名2,…,端口名n);
●注意模块名只能以下划线和字母****开头!!!
(2)端口定义
●输入端口定义为
input 端口名1,端口名2,…,端口名n;
●输出端口定义为
output 端口名1,端口名2,…,端口名n;
●双向端口(不常用)
inout 端口名1,端口名2,…,端口名n
注意:定义完要有分号;
(3)信号类型声明
最常用的类型有wire(连线)和reg(寄存器);
●wire类型表示直通,一条连线,只要输入有变化输出马上无条件反映;
●reg类型表示一定要有触发,输出才会反应输入;
声明类型时也可以声明其位宽,如reg [2:0] A,其表示A为3bit位宽的寄存器。
不声明类型时默认为wire类型。
其实初学的时候比较难分清什么时候用wire,什么时候用reg,在这里先给大家说一下不严谨的判别方法,在设计文件里:
●输入一般都作为wire类型,因为他是模块外部驱动的传入到模块内部;
●输出既可以为wire也可以为reg,当希望某一输入或变量一变化输出立刻变化则采用assign语句赋值时,如assign A = B & C;这时将A定义为wire类型;当这个变量在过程块语句中被赋值时,将其定义为reg类型,可参考上面给出的例子。
可以这么理解,**assign(持续赋值语句)**赋值,该数据会一直被驱动,输入一变输出立刻变化,而过程块语句赋值(always过程块)往往需要等到时钟或某一变量的跳变等才能变化,但他需要把上一时刻的状态保存起来,因此需要存放在寄存器里。(但是这样理解不够严谨)
(4)逻辑功能定义
通常采用assign持续赋值语句、always过程赋值块和调用元件(元件例化,可以理解成C语言中调用函数,但不一样,verilog自身也有function)等方式构成逻辑功能。
●assign是持续赋值语句,只能用于对wire(连线)类型变量的赋值;
2.行为语句
(1)过程语句
always语句为过程语句,其表达式为
always@(<触发条件列表>)
触发条件列表又称为敏感信号表达式,触发条件写在敏感信号表达式之中,当触发条件满足时,其后的语句才能被执行,触发条件列表中的多个条件之间采用“or”来连接。
●当初发条件列表为“*****”时,只要有输入变量发生变化就触发条件,如下,只要A,B,C发生变化,就会执行always块中的语句。
input A,B,C;
output D;
reg D;
always@(*)
begin
D <= A & B & C;
end
例子如下:
always@(A) //A发生变化就执行后面的语句
always@(A or B) //A或B发生变化就执行后面语句
always@(posedge A) //在A的上升沿时执行后面语句
always@(negedge B) //在B的下降沿时执行后面语句
always@(posedge A or negedge B) //在A的上升沿或B的下降沿执行后面的语句
(2)块语句
在begin-end串行块中,语句按照串行方式顺序执行。
举例如下,想要在clk的上升沿处实现A=B,C=D,E=F功能时,
●若不采用begin-end需要如下代码
always@(posedge clk)
A = B;
always@(posedge clk)
C = D;
always@(posedge clk)
E = F;
●采用begin-end时
always@(posedge clk)
begin
A = B;
C = D;
E = F;
end
需要注意的是,在一个always块语句中,各语句之间是串行的关系;但多个always块语句是并行的,那上面的例子来说,不采用begin-end时,A=B,C=D,E=F三条语句并行执行,A,C,E同时获得值;采用begin-end时,A=B执行完才执行C=D,以此类推。
(3)赋值语句
①用assign持续赋值
该语句一般用于组合逻辑的赋值,成为连续赋值;例如F=AB + AC;
assign F = (A & B) |(A & C);
②用always过程赋值
当过程赋值较多时,通常采用begin-end构成串行块,在该块中可以对多个变量进行赋值操作;在过程赋值中,只有寄存器类型的变量才能被赋值;
赋值有非阻塞赋值和阻塞赋值;
●非阻塞赋值(**** < =****)
always@(posedge clk)
begin
b <= 1'b1;//1'b表示一位二进制,再例如4’b 1111表示四位二进制数1111
a <= b;
end
假设初始时b为0,当触发后b和a同时分别赋值1和0;也就是说a赋值的是b之前的值。
●阻塞赋值(=)
always@(posedge clk)
begin
b = 1'b1;//1'b表示一位二进制,再例如4’b 1111表示四位二进制数1111
a = b;
end
假设初始时b为0,当执行b=1后,b变为1,然后再执行a=b,a赋值为1。
(4)条件语句
两种条件语句if-else语句和case语句,他们都是顺序语句且只能****放在always块内。
①if-else语句
module test(A,B,C);
input A,B;
output C;
reg C;
always@(*)
begin
if(A == B)
C <= 1;
else
C <= 0;
endmodule
②case语句
语句格式:
case(条件表达式)
值1 :语句1;
值2 : 语句2;
…
值n : 语句n;
default : 语句n+1;
endcase
例:当A为01时,输出B为0,A为00,10,11时为1。
module test(A,B);
input [1:0] A;
output B;
reg B;
always@(*)
begin
case(A)
2'b00 : B <= 1;
2'b01 : B <= 0;
2'b10 : B <= 1;
2'b11 : B <= 1;
default :B <= 0;
endcase
end
endmodule
●关于casex和casez的用法可以参考课本P57以及P59下方的例题
3.运算量与运算符
这部分的知识建议参考课本P54,内容不多,只需要记住常用的几种即可,如parameter的使用、变量的声明、运算符(算数运算符、位运算符、逻辑运算符、关系运算符、移位运算符)即可。
●这里特别提一下逻辑运算符和位运算符,位运算符就是按位进行操作,如1011**** &**** 0111,就是对应位进行相与,结果就是0011;而采用逻辑运算符时,只要不为0即视作1,如1011 ****&& ****0101那结果就为1。
(1)条件运算符“ ?:”
举个例子,assign a = (b==3) ? 4 : 5;
意思就是如果b=3,那就将4赋值给a,如果不等于就将5赋值给a;
(2)拼接运算符“{ }”
假设a=3’b011,b=3’b101,执行如下语句
c = { a , b };
那c的结果就为011101,但前提是c这个变量的位宽必须要大于6,如果他只有4bit位宽,那结果就变为1101。
二、verilog实例
1.表决器电路
表决器的具体分析这里不再给出,功能就是对于三输入变量,当输入变量中至少有两个为1时输出就为1,
因此我们统计一下为1的个数,然后将它和2比较得到输出。
module biaojueqi(A,B,C,F);
input A,B,C;
output F;
reg F;
//定义一个变量,统计1的个数,因为1的个数最大值为3所以定义位宽为2bit
wire[1:0]count;
assign count = A + B +C;//统计三个变量中1的个数
always@(*)
begin
if(count >= 2)
F <= 1;
else
F <= 0;
end
endmodule
2.数据选择器
这里我们设计一个四选一选择器,对应地址与数据选择关系如下:
addr data_out
2’b00 D0
2’b01 D1
2’b10 D2
2’b11 D3
显然采用case语句很方便,假设输入数据位宽是8bit,verilog代码如下:
module mux_4_1(addr,D0,D1,D2,D3,data_out);
input [1:0] addr;
input [7:0] D0;
input [7:0] D1;
input [7:0] D2;
input [7:0] D3;
output [7:0] data_out;
reg [7:0] data_out;//因为要使用case,在always语句中赋值所以定义为reg类型
always@(addr)
begin
case(addr)
2'b00 : data_out <= D0;
2'b01 : data_out <= D1;
2'b10 : data_out <= D2;
2'b11 : data_out <= D3;
endcase
end
endmodule
3.3-8译码器
对于3-8译码器,显然也是采用case语句很方便,观察真值表我们可以发现首先只有在E1、E2、E3均为1时才能工作,然后经过译码后的输出为0,其余输出为1,verilog代码如下:
module decoder_8(E1,E2_n,E3_n,A,Y_n);
input E1,E2_n,E3_n;
input [2:0] A;
output [7:0] Y_n;
reg [7:0] Y_n;
//首先判断是否使能,只有当E1,E2_n,E3_n为1,0,0时才使能
wire enable;
assign enable = E1 & (~E2_n) & (~E3_n);
always@(enable or A)
begin
case(A)
3'b000 : Y_n<=8'b11111110;
3'b001 : Y_n<=8'b11111101;
3'b010 : Y_n<=8'b11111011;
3'b011 : Y_n<=8'b11110111;
3'b100 : Y_n<=8'b11101111;
3'b101 : Y_n<=8'b11011111;
3'b110 : Y_n<=8'b10111111;
3'b111 : Y_n<=8'b01111111;
endcase
end
endmodule
4.加法器
对于一位加法器来说,有两个三个输入数据,分别是两个加数和一个低位对本位的进位,有两个输出,分别为本位和以及本位对高位的进位。
module add(A,B,Cin,S,Cout);
input A,B,Cin;
output S,Cout;
assign {S,Cout} = A+B+Cin;//S和cout拼接后,cout代表1bitS代表0bit,如果有进位那么会给Cout赋1
endmodule
那对于两位数的相加呢?其实我们可以把他拆为两个一位数相加,然后用上面的模块直接实现一位数相加。
那这里涉及到模块的例化(简单理解成函数的调用),下面给出例化的例子。
add为要例化的模块名,inst_add为例化给他指定的名字(自己随便定义,只要符合起名字的规则),
.Cout为使用模块的信号名,括号里的Cout为调用时传递给它的信号名,两个可以不一样。
两位数加法verilog
module add_2(A,B,Cin,Cout,S);
input [1:0] A;
input [1:0] B;
input Cin;
output Cout;
output [2:0] S;
wire cout1;
add inst_add1 (.A(A[0]), .B(B[0]), .Cin(Cin), .S(S[0]), .Cout(cout1));
add inst_add2 (.A(A[1]), .B(B[1]), .Cin(cout1), .S(S[1]), .Cout(Cout));
endmodule
第一次调用模块的输出Cout作为第二次调用模块的输入Cin
5.边沿D触发器
同步复位是指,复位信号只有在时钟上升沿或下降沿处才进行判断,如该D触发器是低电平复为有效,在复位信号变低后复位信号不会立刻发挥作用,而是等到下一个时钟CP的上升沿或下降沿才起作用。
而异步复位是指,当复位信号变低时,在复位信号的下降沿复位信号便开始发挥作用(这里说下降沿时默认复位信号低电平有效,若是高电平有效那就是在复位信号的上升沿判断)。
可以参考如下文章:
从零开始的FPGA学习5-同步复位D触发器、异步复位D触发器
(1)同步复位的D触发器
always@(posedge clk)
begin
if(!rst_n) //rst_n信号为0时进行复位
Q <= 0;
else
Q <= D;
end
(2)异步复位的D触发器
always@(posedge clk or negedge rst_n) //在rst_n的下降沿也可以触发
begin
if(!rst_n) //rst_n信号为0时进行复位
Q <= 0;
else
Q <= D;
end
6.计数器
实现一个计数器,计数16次再从0开始(这其实就是模16计数器)
module count(clk,count);
input clk;
output [3:0] count;
reg [3:0] count; //也可以将上下两行用一行语句完成 output reg [3:0] count;
always@(posedge clk)
begin
if(count == 4'b1111)
count <= 0;
else
count <= count + 1;
end
endmodule
其实这里也可以不需要加if-else判断,因为count为4bit最大为4’b1111,如果直接+1会变为0,因为不考虑尾款情况下加1的结果为5’b10000,但因为只有4bit所以只取低4位。
7.分频器
分频器有奇分频和偶分频,先说偶分频
(1)偶分频
首先我们考虑两分频,两分频意思即是将时钟周期变为原来的二倍,也就是说我们可以将原来一个时钟周期内高电平和低电平的时间全部变为高电平或者低电平,那我们可以采取每次在原时钟上升沿到来的同时,对新时钟的值取一个反即可。
always@(posedge clk)
clk_new = ~clk;
那对于六分频八分频这样高分频的情况呢,我们可以利用计数器来实现,比如对于6分频,我们需要将三个时钟周期变为新时钟的半个周期(为高电平或者为低电平),那我们采用计数器。
module div(clk,clk_new);
input clk;
output clk_new;
reg [1:0] count;
wire clk_new;
always@(posedge clk)
begin
if(count == 2)
count <= 0;
else
count <= count + 1;
end
assign clk_new = (count == 2)? (~count):count;
endmodule
(2)奇分频
这里以3分频为例:
module fenpin(
input clk,
input rst_n,
output reg clk_3
);
reg [2:0] count;
always @(posedge clk or negedge clk or negedge rst_n)
begin
if(~rst_n)
count <= 0;
else if(count==2)
count <= 0;
else
count <= count+1;
end
always @(posedge clk or negedge clk or negedge rst_n)
begin
if(~rst_n)
clk_3 <= 0;
else if(count==2)
clk_3 <= ~clk_3;
end
endmodule
8.序列检测器
采用移位寄存器实现“11010110”序列检测
module detect(clk,x,y);
input clk;
input x;
output y;
reg[7:0] ram;
always@(posedge)
ram <= {ram[6:0],x};
assign y = (ram == 8'b11010110)? 1 : 0;
endmodule
————————————————
版权声明:本文为CSDN博主「呜呼啦呼_l」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_43433724/article/details/127508892