前言:对于以前学过C/C++/C#的作者来讲,Verilog的基础语法算是特别简单的。本文主要介绍Verilog的基础语法和Modelsem仿真。
Verilog的基础语法
1 模块声明
FPGA开发是以模块为基础的,每个可综合的.v文件都是一个模块,模块由module和endmodule来声明。在这两个关键字的内部,完成模块功能的实现。
在Vivado的一个空项目中,新建一个.v源文件,会自动生成以下代码(我把多余的注释删除了)
`timescale 1ns / 1ps // 这行以后代码经常会见,表示时间单位是1ns,精度是1ps module verilog_base( // module 模块名(... // 定义模块的输入输出接口
); // );
endmodule // endmodule
下面以一个与门为例,进一步演示如何声明一个模块。
`timescale 1ns / 1ps // 这行以后代码经常会见,表示时间单位是1ns,精度是1ps module verilog_base(input wire a, // input表示这是模块的输入接口 wire是变量类型 a是变量名,除最后一个变量定义外,都以‘,’结束input wire b,output wire c // output表示这是模块的输出接口 wire是变量类型 c是变量名,最后一个变量定义,不以‘,’结束
);assign c = a & b; // 完成与门的组合逻辑幅值endmodule
2 变量类型
在Verilog开发中有两种数据类型,一种是wire (线),一种是reg (寄存器)。在数字电路中信号只有两种形态,一种是传输,一种是存储。传输是通过连接线,存储是用寄存器,因此也就清楚了在Verilog中常用了wire和reg变量了。wire和reg变量模型如下图所示:
wire型变量在物理结构上只是一根线,在Verilog描述时,对线型变量赋值用assign即可,相对简单。
C语言赋值我直接 a = b; Verilog赋值得在前面加关键字。 assign a = b;
reg型变量左端有一个输入端口D,右端有一个输出端口Q,并且reg型存储数据需要在clk(时钟)沿的控制下完成。 clk也即是我们常说的方波,它由晶振产生,是我们描述数字电路是最基本的时间单元,它周期固定,占空比一般为50%(高电平占整个周期的比例)。clk的低电平用数字0表示,高电平用1表示,从低电平转变到高电平的过程叫做上升沿,从高电平转变到低电平的过程叫做下降沿。
在对reg型变量赋值时,必须在always块内完成,可以选择用时钟上升沿,也可以选择时钟下降沿,具体用上升沿还是下降沿可以根据需要定。下面尝试把第一节给出的与门例子的输出定义成reg型,实现两个输入相与之后将结果传输给寄存器的功能。即
Verilog代码如下:
`timescale 1ns / 1ps // 这行以后代码经常会见,表示时间单位是1ns,精度是1ps module verilog_base(input wire clk, // 系统时钟输入,由晶振提供input wire a,input wire b,output reg c // 将输出的类型改成reg
);// reg型变量的赋值必须要在always块里面,注意:赋值符号不是'=',而是 '<='。
always @(posedge clk) beginc <= a & b;
endendmodule
在always块中,@(posedge clk)表示的是,每当遇到clk的上升沿时,执行always块中的语句。
c <= a & b;中的'<='表示的是非阻塞赋值。Verilog中有两种赋值方式:
1 '<=' 非阻塞赋值 适用于给reg变量赋值,always块 - 时序逻辑
2 ‘=’ 阻塞赋值 适用于给wire变量赋值,assign块 – 组合逻辑
阻塞赋值时,输入改变输出是同时改变,在非阻塞赋值中,只有在时钟变化的时候,输出才会发生变化。
begin-end其作用与C C++ C#中的{}是一样的,在Verilog中,就可以视begin-end为{}。
3 多位宽数据表示
在数字电路中,所有数据最终都是以二进制形式呈现的,二进制和十进制都只是一种数据的表达方式,同一个数据无论用二进制还是十进制来表达,本质上代表的值是一样的。十进制数据是0到9组成,二进制是0,1组成。 比如5,它的二进制是3’b101,十进制是’d5。二进制的每一个0/1叫做1位,若想知道数据是多少位。可以用计算器看。
如果我要表示53698745这个数,就需要26个位。 那么在定义的时候就要像下面这样定义
wire [25:0] data;
reg [25:0] data;
wire [26:1] data;
reg [26:1] data;
一般推荐最低位都是从0开始,也就是[25:0]这种写法。
4 赋值语句
前面第二小节已讲了一些相关内容。assign 和 always分别用于组合逻辑的赋值 和 时序逻辑的赋值。 还有一种initial,一般仅用于仿真文件中。
initial 语句是初始化语句,会在上电(电路刚刚运行时)执行一次,不会循环执行。在电路中只有可以存储数据的寄存器才有初始化的必要,所以initial 语句中被赋值的变量也必须为reg。 某些第三方综合软件认为initial 是不可以被综合的,也就是说不可以被写到功能文件中的,所以为了代码的兼容性,我们尽量只在测试文件中写initial 语句。示例代码如下所示。
reg clk;
initial beginclk = 0;forever #(5) clk = ~clk; // forever和always的区别是 forever仅用于仿真
end
示例代码产生的是周期为10ns的时钟信号。
5 运算符
5.1 关系运算符(< 、<= 、== 、>= 、> 、!=)
C语言中可以用if(2<a<6),但Verilog只能用if(a>2 && a<6),其余一致
5.2 逻辑运算符(&& 、||、 !)
逻辑运算符与C 语言中一致:对于&& 只有参与运算的两个数据都为真时,结果才为真,对于|| 只要参与运算的两个数据由一个为真,结果就为真。(!)进行的时逻辑取反。
5.3 位运算符(& 、|、 ~)
位运算是对二进制的运算,运算时,需要将数据都转换位二进制后才能进行计算。当位宽不一样时,位宽少的在高位补0。
5.4 条件运算符
如:assign a = (b>6)?1’b1:1’b0;
当b 大于6 时,a 等于1,否则a 等于0;
5.5 赋值运算符
前面已经介绍过Verilog 中的赋值运算符,<= 和=,其中<= 是非阻塞赋值,用于时序逻辑,=是阻塞赋值,用于组合逻辑。
5.6 移位运算符(<< 、>>)
>>为向右移位,每次右移一位,数据高位补零,向下溢出的数据丢弃。<<与之相反。
5.7 位拼接运算符({})
位拼接运算符,可以将不同数据的位拼接成一个新的数据。
reg[3:0] a = 4'b0110;
reg[4:0] b = 5'b10110;
reg[4:0] c;always@(posedge clk) beginc <= {b[1],a[1:0],b[3],b[1]};
end
拼接完成后,c = 5’b11001;
6 条件判断
同一个变量可以在不同的情况下获得不同的值,不同的情况需要判断语句来描述。Verilog HDL 中经常使用的判断语句有if else 语句和case endcase 语句,两种判断语句必须写在always 语句中,不能写在assign 中。
6.1 If-else
if-else 语句与C 语言当中的使用方法类似。
always@(posedge clk)beginif(a==1) beginb <= 1;endelse if(a==2) beginb <= 2;end else if(a==3) beginb <= 3;endelse if(a==4) beginb <= 4;endelse if(a==5) beginb <= 5;endend
if else 叠加不易过多,不然可能造成线路的延时过多。每一个if else 语句都会生成一选择器,当if else 过多时,选择器链路就会很长,而每两级选择器之间都会有线路的延时,当链路过多时,造成的延时就会很多,这样对于描述的电路的时序影响会很大,时序出问题时,就算是功能仿真正确,下板后电路也是不正确的。
使用if-else 语句时需要考虑优先级的顺序,优先执行的条件放在上方,优先级较低的条件放在后方。最好还是用case吧。
6.2 Case 语句
Verilog的case语句和C语言的switch case类似,在if-else 级数过多的情况下,也可以使用case 语句,case 语句生成的是多路器。
always@(posedge clk) begincase(a)0: b <= 0;1: b <= 1;2: b <= 2;default:b <= 0;endcase
end
Case 语句以case 开始,以endcase 结尾,在其之间,列出要判断的条件,根据条件的值,执行对应的代码。
Modelsem仿真
1 编写仿真程序
新建一个空白的项目的流程在上一篇博客中已经讲过了,这里不在赘述。 按照下面的操作流程,新建一个仿真文件。
Sublime Text (编辑软件)可以利用 源文件 自动生成 仿真文件。
仿真程序输入要用reg型,输出要用wire型
再次强调initial语句也常用在仿真文件,请尽量不要在实际的功能模块中使用。
`timescale 1ns/1ps
module tb_verilog_base (); /* this is automatically generated */reg a;reg b;wire c;verilog_base inst_verilog_base (.a(a), .b(b), .c(c));initial begina = 0;b = 0;#100; // 延时100nsa = 1;b = 0;#100; a = 0;b = 1;#100;a = 1;b = 1;end
endmodule
2 Vivado仿真流程
从仿真中我们可以看到,assign赋值是立即生效的。
3 Modelsem仿真流程
Modelsem软件安装及编译相关的库文件等操作,请参考Vivado:【1】Vivado 2018.3 配置ModelSim仿真_Alex-YiWang的博客-CSDN博客
正常人都能看出来Modelsem的原始界面是比vivado丑的,但Modelsem的界面是可以配置的,可自行查找相关资料。
4 时序逻辑仿真
源文件
`timescale 1ns / 1ps // 这行以后代码经常会见,表示时间单位是1ns,精度是1ps module verilog_base(input wire clk, // 系统时钟输入input wire a,input wire b,output reg c // 将输出的类型改成reg
);// reg型变量的赋值必须要在always块里面,注意:赋值符号不是'=',而是 '<='。
always @(posedge clk) beginc <= a & b;
endendmodule
仿真文件
`timescale 1ns/1ps
module tb_verilog_base (); /* this is automatically generated */reg clk;reg a;reg b;wire c;verilog_base inst_verilog_base (.clk(clk), .a(a), .b(b), .c(c));initial beginclk = 0;forever #(5) clk = ~clk;endinitial begina = 0;b = 0;#100; // 延时100nsa = 1;b = 0;#100; a = 0;b = 1;#100;a = 1;b = 1;endendmodule
仿真结果
时序逻辑赋值,并不是立即生效,而是在相应的“沿”信号处执行。
把源文件的always块修改成下降沿触发 always @(negedge clk) begin
把仿真文件的最后一个#100改成#33。
把仿真文件的 #33 改回 #100。
5 Modelsem小技巧
打开以下这个文件
你修改源文件或者修改仿真文件保存后,可以不用重新在vivado点击仿真。 可以在Modelsem的命令行执行以下三个命令。
1 do tb_verilog_base_compile.do 重新编译一遍
2 restart 有弹窗没关系,直接回车键默认Ok
3 run 运行仿真
这样可以快速修改代码,高效率仿真。
最后愿我们共同进步! 感谢您的阅读,欢迎留言讨论、收藏、点赞、分享。