天天看点

Verilog语法

Verilog语法

Verilog简介

Verilog是一种硬件描述语言,以文本形式来描述数字系统硬件的结构和行为的语言,用它可以表示逻辑电路图、逻辑表达式,还可以表示数字逻辑系统所完成的逻辑功能。

Verilog 和 C 的区别:

  • Verilog是硬件描述语言,编译下载到在编译下载到FPGA之后,会生成电路,所以Verilog全部是并行处理与运行的
  • C语言是软件语言,编译下载到单片机/CPU之后,还是软件指令,而不会根据代码生成相应的硬件电路,而单片机/CPU处理软件指令需要取址、译码、执行,是串行执行的。
Verilog逻辑值

逻辑电路中有四种值,即四种状态

  • 逻辑0:表示低电平,对应电路的GND
  • 逻辑1:表示高电平,对应电路的VCC
  • 逻辑X:表示未知,有可能是高电平,也有可能是低电平
  • 逻辑Z:表示高阻态,外部没有激励信号是一个悬空状态
Verilog语法
Verilog标识符

用于定义模块名、端口名和信号名等

Verilog的标识符可以是任意一组字母、数字、$和_(下划线) 符号的组合,但标识符的第一个字符必须是字母或者下划线。区分大小写

不建议大小写混合使用,普通内部信号建议全部小写,参数定义建议大写

建议:

  1. 用有意义的有效的名字如sum、cpu_addr等
  2. 用下划线区分词语组合,如cpu_addr
  3. 采用一些前缀或后缀

    比如:时钟采用clk前缀:clk_50m,clk_cpu;低电平采用_n后缀:enable_n

  4. 统一缩写,如全局复位信号rst
  5. 同一信号在不同层次保持一致性,如同一时钟信号必须在各模块保持一致。
  6. 自定义的标识符不能与保留字(关键词)同名
  7. 参数统一采用大写,如定义参数使用SIZE
Verilog数字进制

Verilog数字进制格式包括二进制、八进制、十进制和十六进制,一般常用的为二进制、十进制和十六进制。

  • 二进制表示如下:4’b0101表示4位二进制数字0101
  • 十进制表示如下:4’d2表示4位十进制数字2
  • 十六进制表示如下:4’ha表示4位十六进制数字a
当没有指定数字的位宽与进制时,默认为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运算符

类型:

  • 算术运算符
+ - * / %      

Verilog实现乘除比较浪费组合逻辑资源,尤其是除法。一般2的指数次幂的乘除法使用移位运算来完成运算.

非2的指数次幂的乘除法一般是调用现成的IP,QUARTUS/ISE等工具软件会有提供,不过这些工具软件提供的IP也是由最底层的组合逻辑(与或非门等)搭建而成的。

  • 关系运算符
> < >= <= == !=      

用来进行条件判断,在进行关系运算符时,如果声明的关系是假的,则返回值是0,如果声明的关系是真的,则返回值是1;

所有的关系运算符有着相同的优先级别,关系运算符的优先级别低于算术运算符的优先级别

  • 逻辑运算符
! && ||      

连接多个关系表达式,可实现更加复杂的判断,一般不单独使用,都需要配合具体语句来实现完整的意思

  • 条件运算符
? :      

从两个输入中选择一个作为输出的条件选择结构,功能等同于always中的if-else语句

  • 位运算符
~ & | ^      

直接对应数字逻辑中的与、或、非门等逻辑门

位运算符一般用在信号赋值上

  • 移位运算符
<<   >>      

移位运算符包括左移位运算符和右移位运算符,这两种移位运算符都用0来填补移出的空位。 一般使用左移位运算代替乘法,右移位运算代替除法,但是只能表示2的指数次幂的乘除法。

  • 拼接运算符
{a,b}      

可以把两个或多个信号的某些位拼接起来进行运算操作

Verilog语法
注释
/* */
//      
建议使用 //
关键字
Verilog语法

常用关键字

  • module 模块开始定义
  • input 输入端口定义
  • output 输出端口定义
  • inout 双向端口定义
  • parameter 信号的参数定义
  • wire wire信号定义
  • reg reg信号定义
  • always 产生reg信号语句的关键字
  • assign 产生wire信号语句的关键字
  • begin 语句的开始标志
  • end 语句的结束标志
  • poseedge / negedge 时序电路的标志
  • case case语句的起始标志
  • default case语句的默认分支标记
  • if else
  • for
  • endmodule 模块结束定义
区分大小写
Verilog语法
程序框架

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      
阻塞赋值(Blocking)

在一个always块中,后面的语句会受到前语句的影响,具体来说,在同一个always中,一条阻塞赋值语句如果没有执行结束,那么该语句后面的语句就不能被执行,即被“阻塞”。

也就是说always块内的语句是一种顺序关系

符号“=”用于阻塞的赋值(如:b = a;),阻塞赋值“=”在begin和end之间的语句是顺序执行,属于串行语句。 其后面的赋值语句从概念上来讲是在前面一条语句赋值完成后才执行的。

非阻塞赋值( Non-Blocking )

符号“<=”用于非阻塞赋值(如: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的区别

assign 语句使用时不能带时钟。

always 语句可以带时钟,也可以不带时钟。

在always不带时钟是,逻辑功能和assign 完全一致,都是只产生组合逻辑。

比较简单的组合逻辑推荐使用assign语句,比较复杂的组合逻辑推荐使用always语句。

带时钟 和 不带时钟的always:

always 语句可以带时钟,也可以不带时钟。

  • 在always不带时钟时,逻辑功能和assign完全一致,虽然产生的信号定义为reg类型,但是该语句产生的还是组合逻辑
  • 在always带时钟信号时,这个逻辑语句才能产生真正的寄存器
latch

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)型状态机

  • Mealy状态机:组合逻辑的输出不仅取决于当前状态,还取决于输入状态
  • Moore状态机:组合逻辑的输出只取决于当前状态。

三段式状态机

根据状态机的实际写法,状态机还可以分为一段式、二段式和三段式状态机。

  • 一段式:整个状态机写到一个always模块里面,在该模块中既描述状态转移,又描述状态的输入和输出。
不推荐,一般都会要求把组合逻辑和时序逻辑分开,组合逻辑和时序逻辑混合在一起不利于代码维护和修改
  • 二段式:用两个always模块来描述状态机,其中一个always模块采用同步时序描述状态转移;另一个模块采用组合逻辑判断状态转移条件,描述状态转移规律以及输出,需要定义两个状态,现态和次态,然后通过现态和次态的转换来实现时序逻辑
  • 三段式:在两个always模块描述方法基础上,使用三个always模块,一个always模块采用同步时序描述状态转移,一个always采用组合逻辑判断状态转移条件,描述状态转移规律,另一个always模块描述状态输出(可以用组合电路输出,也可以时序电路输出)。

    三段式状态机的基本格式是:第一个always语句实现同步状态跳转;第二个always语句采用组合逻辑判断状态转移条件;第三个always语句描述状态输出(可以用组合电路输出,也可以时序电路输出)。

demo

7分频

状态跳转

Verilog语法
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程序文件。因此,对一个系统的顶层模块,我们采用结构化的设计,即顶层模块分别调用了各个功能模块。

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定义的参数做例化。

Verilog编程规范

工程的组织形式一般包括如下几个部分,分别是doc、par、rtl和sim四个部分

  • doc:一般存放工程相关的文档,包括该项目用到的datasheet(数据手册)、设计方案等。
  • par:主要存放工程文件和使用到的一些IP文件
  • rtl:主要存放工程的rtl代码,是工程的核心,文件名与module名称应当一致,建议按照模块的层次分开存放
  • sim:主要存放工程的仿真代码,复杂的工程里面,仿真也是不可或缺的部分,可以极大减少调试的工作量。

文件头声明

每一个Verilog文件的开头,都必须有一段声明的文字。

包含文件的版权、作者、创建日期,以及内容简介等等

//*************************************Copyright(c)*******************//
// FileName:
// Last modified Date:
// Last Version:
// Descriptions:
//*******************************************************************//      

输入输出定义

module led(
    input   sys_clk,     // 系统时钟
    input   sys_rst_n ,  // 系统复位
    output reg [3:0] led // 4位LED灯
);      
  • 一行只定义一个信号
  • 信号全部对齐
  • 同一组的信号放在一起

parameter定义

  • module中的parameter声明,不建议随处乱放,将parameter定义放在紧跟着module的输入输出定义之后
  • parameter等常量命名全部使用大写

wire/reg定义

  • 将reg与wire的定义放在紧跟着parameter之后
  • 建议具有相同功能的信号集中放在一起
  • 信号需要对齐,reg和位宽需要空2格,位宽和信号名字至少空四格
  • 位宽使用降序描述,[6:0]
  • 时钟使用前缀clk,复位使用后缀rst
  • 一行只定义一个信号

信号命名

  • 内部信号不要使用大写,也不要使用大小写混合,建议全部使用小写
  • 模块名字使用小写
  • 异步信号,使用_a作为信号后缀

always块

  • 一个always需要配一个begin和end
  • always前面需要有注释
  • 一个always和下一个always空一行即可,不要空多行
  • 时序逻辑使用非阻塞赋值

assign块

  • assign的逻辑不能太复杂,否则易读性不好
  • assign前面需要有注释
  • 组合逻辑使用阻塞赋值

空格 和 TAB

继续阅读