Verilog是一种硬件描述语言,以文本形式来描述数字系统硬件的结构和行为的语言,用它可以表示逻辑电路图、逻辑表达式,还可以表示数字逻辑系统所完成的逻辑功能。
Verilog 和 C 的区别:
逻辑电路中有四种值,即四种状态
用于定义模块名、端口名和信号名等
Verilog的标识符可以是任意一组字母、数字、$和_(下划线)符号的组合,但标识符的第一个字符必须是字母或者下划线。区分大小写
不建议大小写混合使用,普通内部信号建议全部小写,参数定义建议大写
建议:
用有意义的有效的名字如sum、cpu_addr等
用下划线区分词语组合,如cpu_addr
采用一些前缀或后缀
比如:时钟采用clk前缀:clk_50m,clk_cpu;低电平采用_n后缀:enable_n
统一缩写,如全局复位信号rst
同一信号在不同层次保持一致性,如同一时钟信号必须在各模块保持一致。
自定义的标识符不能与保留字(关键词)同名
参数统一采用大写,如定义参数使用SIZE
Verilog数字进制格式包括二进制、八进制、十进制和十六进制,一般常用的为二进制、十进制和十六进制。
当没有指定数字的位宽与进制时,默认为32位的十进制,比如100,实际上表示的值为32’d100。
主要有三大类数据类型,即寄存器类型、线网类型和参数类型。真正在数字电路中起作用的数据类型是寄存器类型和线网类型。
寄存器类型表示一个抽象的数据存储单元,它只能在always语句和initial语句中被赋值,并且它的值从一个赋值到另一个赋值过程中被保存下来。
如果语句描述的是时序逻辑,即always语句带有时钟信号,则该寄存器变量对应为寄存器;如果该过程语句描述的是组合逻辑,即always语句不带有时钟信号则该寄存器变量对应为硬件连线
寄存器类型的缺省值是x(未知状态)。
寄存器数据类型有很多种,如reg、integer、real等,其中最常用的就是reg类型
reg [31:0] delay_cnt; // 延时计数器
reg key_flag; // 按键标志
线网表示Verilog结构化元件间的物理连线。
值由驱动元件的值决定,例如连续赋值或门的输出。
如果没有驱动元件连接到线网,线网的缺省值为z(高阻态)。
线网类型,如tri和wire等,其中最常用的就是wire类型,它的使用方法如下:
wire data_en; // 数据使能信号
wire [7:0] data; // 数据
参数其实就是一个常量,常被用于定义状态机的状态、数据位宽和延迟大小等
可以在编译时修改参数的值,因此又常被用于一些参数可调的模块中,使用户在实例化模块时,可以根据需要配置参数。
在定义参数时,可以一次定义多个参数,参数与参数之间需要用逗号隔开。
要注意的是参数的定义是局部的,只在当前模块中有效。
parameter DATA_WIDTH = 8; // 数据位宽为8
类型:
算术运算符
+ - * / %
Verilog实现乘除比较浪费组合逻辑资源,尤其是除法。一般2的指数次幂的乘除法使用移位运算来完成运算.
非2的指数次幂的乘除法一般是调用现成的IP,QUARTUS/ISE等工具软件会有提供,不过这些工具软件提供的IP也是由最底层的组合逻辑(与或非门等)搭建而成的。
关系运算符
> < >= <= == !=
用来进行条件判断,在进行关系运算符时,如果声明的关系是假的,则返回值是0,如果声明的关系是真的,则返回值是1;
所有的关系运算符有着相同的优先级别,关系运算符的优先级别低于算术运算符的优先级别
逻辑运算符
! && ||
连接多个关系表达式,可实现更加复杂的判断,一般不单独使用,都需要配合具体语句来实现完整的意思
条件运算符
? :
从两个输入中选择一个作为输出的条件选择结构,功能等同于always中的if-else语句
位运算符
~ & | ^
直接对应数字逻辑中的与、或、非门等逻辑门
位运算符一般用在信号赋值上
移位运算符
<< >>
移位运算符包括左移位运算符和右移位运算符,这两种移位运算符都用0来填补移出的空位。
一般使用左移位运算代替乘法,右移位运算代替除法,但是只能表示2的指数次幂的乘除法。
拼接运算符
{a,b}
可以把两个或多个信号的某些位拼接起来进行运算操作
/* */
//
建议使用 //
常用关键字
区分大小写
LED流水灯程序
module led(
input sys_clk, // 系统时钟,50M
input sys_rst_n, // 系统复位,低电平有效
output reg[3:0] led // 4位led灯
);
// parameter define
parameter WIDTH = 25;
parameter COUNT_MAX = 25_000_000; // 分频
// reg define
reg [WIDTH-1:0] counter;
reg [1:0] led_ctrl_cnt;
wire counter_en;
// 计数到最大值时产生高电平使能信号 --> 不做分频而是产生一个使能信号
assign counter_en = (counter == (COUNT_MAX - 1'b1)) ? 1'b1:1'b0;
// 产生0.5秒使能信号的计数器
always @ (posedge sys_clk or negedge sys_rst_n) begin
if(sys_rst_n == 1'b0)
counter <= 1'b0;
else if(counter_en)
counter <= 1'b0;
else
counter <= counter + 1'b1;
end
// led流水控制计数器
always @(posedge sys_clk or negedge sys_rst_n) begin
if(sys_rst_n == 1'b0)
led_ctrl_cnt <= 2'b0;
else if(counter_en)
led_ctrl_cnt <= led_ctrl_cnt + 2'b1;
end
// 通过控制IO口的高低电平实现发光二极管的亮灭
always @ (posedge sys_clk or negedge sys_rst_n) begin
if(sys_rst_n == 1'b0)
led <= 4'b0;
else begin
case(led_ctrl_cnt)
2'd0: led <= 4'b0001;
2'd1: led <= 4'b0010;
2'd2: led <= 4'b0100;
2'd3: led <= 4'b1000;
default: ;
endcase
end
end
endmodule
在一个always块中,后面的语句会受到前语句的影响,具体来说,在同一个always中,一条阻塞赋值语句如果没有执行结束,那么该语句后面的语句就不能被执行,即被“阻塞”。
也就是说always块内的语句是一种顺序关系
符号“=”用于阻塞的赋值(如:b = a;),阻塞赋值“=”在begin和end之间的语句是顺序执行,属于串行语句。
其后面的赋值语句从概念上来讲是在前面一条语句赋值完成后才执行的。
符号“<=”用于非阻塞赋值(如:b <= a;),非阻塞赋值是由时钟节拍决定,在时钟上升到来时,执行赋值语句右边,然后将begin-end之间的所有赋值语句同时赋值到赋值语句的左边,
begin—end之间的所有语句,一起执行,且一个时钟只执行一次,属于并行执行语句。
非阻塞赋值的操作过程可以看作两个步骤:
赋值开始的时候,计算RHS( 等号右边的表达式 );
赋值结束的时候,更新LHS( 等号左边的表达式 )。
非阻塞的概念是指,在计算非阻塞赋值的RHS以及LHS期间,允许其它的非阻塞赋值语句同时计算RHS和更新LHS。
在描述组合逻辑电路的时候,使用阻塞赋值,比如assign赋值语句和不带时钟的always赋值语句,这种电路结构只与输入电平的变化有关系
assign data = (data_en == 1'b1)?8'd255:8'd0;
always@(*)begin
if(en) begin
a = a0;
b = b0;
end
else begin
a = a1;
b = b1;
end
end
在描述时序逻辑的时候,使用非阻塞赋值,综合成时序逻辑的电路结构,比如带时钟的always语句;这种电路结构往往与触发沿有关系,只有在触发沿时才可能发生赋值的变化
always @(poseedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
a<= 1'b0;
b<= 1'b0;
end
else begin
a <= c;
b <= d;
end
end
assign 语句使用时不能带时钟。
always 语句可以带时钟,也可以不带时钟。
在always不带时钟是,逻辑功能和assign 完全一致,都是只产生组合逻辑。
比较简单的组合逻辑推荐使用assign语句,比较复杂的组合逻辑推荐使用always语句。
带时钟 和 不带时钟的always:
always 语句可以带时钟,也可以不带时钟。
在always不带时钟时,逻辑功能和assign完全一致,虽然产生的信号定义为reg类型,但是该语句产生的还是组合逻辑
在always带时钟信号时,这个逻辑语句才能产生真正的寄存器
latch指锁存器,是一种对脉冲电平敏感的存储单元电路
锁存器和寄存器都是基本存储单元,锁存器是电平触发的存储器,寄存器是边沿触发的存储器。
两者的基本功能是一样的,都可以存储数据。
锁存器是组合逻辑产生的,而寄存器是在时序电路中使用,由时钟触发产生的。
latch的主要危害是会产生毛刺(glitch),这种毛刺对下一级电路是很危险的。并且其隐蔽性很强,不易查出。
在设计中,应尽量避免latch的使用。
代码里出现latch的两个原因是在组合逻辑中, if或者case语句不完整的描述,比如if缺少else分支,case缺少default分支,导致代码在综合过程中出现了latch。解决办法就是if必须带else分支,case必须带default分支。
只有不带时钟的always语句if或者case语句不完整才会产生latch,带时钟的语句if或者case语句不完整描述不会产生latch。
Verilog是硬件描述语言,硬件电路是并行执行的,当需要按照流程或者步骤来完成某个功能时,代码中通常会使用很多个if嵌套语句来实现,这样就增加了代码的复杂度,以及降低了代码的可读性,这个时候就可以使用状态机来编写代码。
状态机相当于一个控制器,它将一项功能的完成分解为若干步,每一步对应于二进制的一个状态,通过预先设计的顺序在各状态之间进行转换,状态转换的过程就是实现逻辑功能的过程。
状态机,全称是有限状态机(Finite State Machine,缩写为FSM),是一种在有限个状态之间按一定规律转换的时序电路,可以认为是组合逻辑和时序逻辑的一种组合。状态机通过控制各个状态的跳转来控制流程,使得整个代码看上去更加清晰易懂,在控制复杂流程的时候,状态机优势明显,因此基本上都会用到状态机,如SDRAM控制器等。
根据状态机的输出是否与输入条件相关,可将状态机分为两大类,即摩尔(Moore)型状态机和米勒(Mealy)型状态机
根据状态机的实际写法,状态机还可以分为一段式、二段式和三段式状态机。
一段式:整个状态机写到一个always模块里面,在该模块中既描述状态转移,又描述状态的输入和输出。
不推荐,一般都会要求把组合逻辑和时序逻辑分开,组合逻辑和时序逻辑混合在一起不利于代码维护和修改
二段式:用两个always模块来描述状态机,其中一个always模块采用同步时序描述状态转移;另一个模块采用组合逻辑判断状态转移条件,描述状态转移规律以及输出,需要定义两个状态,现态和次态,然后通过现态和次态的转换来实现时序逻辑
三段式:在两个always模块描述方法基础上,使用三个always模块,一个always模块采用同步时序描述状态转移,一个always采用组合逻辑判断状态转移条件,描述状态转移规律,另一个always模块描述状态输出(可以用组合电路输出,也可以时序电路输出)。
三段式状态机的基本格式是:第一个always语句实现同步状态跳转;第二个always语句采用组合逻辑判断状态转移条件;第三个always语句描述状态输出(可以用组合电路输出,也可以时序电路输出)。
demo
7分频
状态跳转
module divider7_fsm (
//系统时钟与复位
input sys_clk ,
input sys_rst_n ,
//输出时钟
output reg clk_divide_7
);
// 状态参数
parameter S0 = 7'b0000001; //独热码定义方式
parameter S1 = 7'b0000010;
parameter S2 = 7'b0000100;
parameter S3 = 7'b0001000;
parameter S4 = 7'b0010000;
parameter S5 = 7'b0100000;
parameter S6 = 7'b1000000;
// 定义两个7位的寄存器,一个用来表示当前状态,一个用来表示下一状态
reg [6:0] curr_st ; //当前状态
reg [6:0] next_st ; //下一个状态
// 三个always语句
// 第一个always采用同步时序描述状态转移
// 第二个always采用组合逻辑判断状态转移条件
// 第三个always描述状态输出
//***************************
//** main code
//***************************
// 描述状态转移
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
curr_st <= S0;
else
curr_st <= next_st;
end
// 判断状态转移条件
always @(*)begin
case(curr_st)
S0: next_st = S1;
S1: next_st = S2;
S2: next_st = S3;
S3: next_st = S4;
S4: next_st = S5;
S5: next_st = S6;
S6: next_st = S0;
default: next_st = S0; // 防止latch
endcase
end
// 状态输出
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
clk_divide_7 <= 1'b0;
else if((curr_st == S0) | (curr_st == S1) | (curr_st == S2) | (curr_st == S3))
clk_divide_7 <= 1'b1;
else if ((curr_st == S4) | (curr_st == S5) | (curr_st == S6))
clk_divide_7 <= 1'b0;
else ;
end
endmodule
状态机的第三段可以使用组合逻辑输出,也可以使用时序逻辑输出一般推荐使用时序电路输出,使用同步时序方式设计,可以提高设计的稳定性,消除毛刺。
划分模块的基本原则是子模块功能相对独立、模块内部联系尽量紧密、模块间的连接尽量简单。
在进行模块化设计中,对于复杂的数字系统,我们一般采用自顶向下的设计方式。可以把系统划分成几个功能模块,每个功能模块再划分成下一层的子模块;每个模块的设计对应一个module,一个module设计成一个Verilog程序文件。因此,对一个系统的顶层模块,我们采用结构化的设计,即顶层模块分别调用了各个功能模块。
FPGA逻辑设计中通常是一个大的模块中包含了一个或多个功能子模块,Verilog通过模块调用或称为模块实例化的方式来实现这些子模块与高层模块的连接,有利于简化每一个模块的代码,易于维护和修改。
如果子模块内部使用parameter定义了一些参数,Verilog也支持对参数的例化(也叫参数的传递),即顶层模块可以通过例化参数来修改子模块内定义的参数。
子模块名是指被例化模块的模块名,而例化模块名相当于标识,当例化多个相同模块时,可以通过例化名来识别哪一个例化,一般命名为“u_”+“子模块名”
参数的例化,参数的例化是在模块例化的基础上,增加了对参数的信号定义
// 例子
time_count #(
.MAX_NUM (TIME_SHOW) // 参数例化
)u_time_count(
.clk (sys_clk),
.rst_n (sys_rst_n), // 信号例化
.flag (add_flag)
);
localparam代表的意思同样是参数定义,用法和parameter基本一致,区别在于parameter定义的参数可以做例化,而localparam定义的参数是指本地参数,上层模块不可以对localparam定义的参数做例化。
工程的组织形式一般包括如下几个部分,分别是doc、par、rtl和sim四个部分
doc:一般存放工程相关的文档,包括该项目用到的datasheet(数据手册)、设计方案等。
par:主要存放工程文件和使用到的一些IP文件
rtl:主要存放工程的rtl代码,是工程的核心,文件名与module名称应当一致,建议按照模块的层次分开存放
sim:主要存放工程的仿真代码,复杂的工程里面,仿真也是不可或缺的部分,可以极大减少调试的工作量。
每一个Verilog文件的开头,都必须有一段声明的文字。
包含文件的版权、作者、创建日期,以及内容简介等等
//*************************************Copyright(c)*******************//
// FileName:
// Last modified Date:
// Last Version:
// Descriptions:
//*******************************************************************//
一个.v只包括一个module,这样模块会比较清晰易懂
module led(
input sys_clk, // 系统时钟
input sys_rst_n , // 系统复位
output reg [3:0] led // 4位LED灯
);
一个module中的wire/reg变量声明需要集中放在一起,不建议随处乱放
由于不同的解释器对于TAB翻译不一致,所以建议不使用TAB,全部使用空格