$$ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Self-defined math definitions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Math symbol commands \newcommand{\intd}{\,{\rm d}} % Symbol 'd' used in integration, such as 'dx' \newcommand{\diff}{{\rm d}} % Symbol 'd' used in differentiation ... $$

【FPGA学习】- Verilog教程进阶

良好的代码风格

变量命名

  在对变量命名时,要求尽可能使用有意义的名字,方便后续阅读和维护。“好的命名即是注释”。常见的变量命名方式主要有三种。

  驼峰命名法:该命名方法使用大小写混合的方式来区别各个单词,一般每个单词首字母大写,其余字母小写,并且单词之间不使用任何符号隔开。例如:ChangeTime,SelectNumber,QuickSort。

  蛇形命名法:该命名方法使用下划线“_”区别各个单词,单词不区分大小写,使用一种格式。例如:change_time,select_number,QUICK_SORT。

  串行命名法:该命名方法使用连接符“-”区别各个单词,单词不区分大小写,使用一种格式。例如:change-time,select-number,QUICK-SORT。

  诸多教程推荐使用蛇形命名法,原因是在单词较多的情况下,蛇形命名法更加清晰易读,使用也更加方便。

代码注释

  原则上,每一个设计模块的开头都应当包含文件说明信息,诸如版权、模块名字、作者、日期等信息。在Vivado中,文件创建成功时,会自动生成相关的内容。

//////////////////////////////////////////////////////////////////////////////////
// Company: 
// Engineer: 
// 
// Create Date: 2023/03/15 00:00:00
// Design Name: 
// Module Name: what
// Project Name: 
// Target Devices: 
// Tool Versions: 
// Description: 
// 
// Dependencies: 
// 
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
// 
//////////////////////////////////////////////////////////////////////////////////

  注释一般应当以最简短的话语描述当前代码/代码段的功能,为防止出现编码格式的问题,书写注释时,应当使用英语。较短的注释一般在代码之后书写,较长的代码注释应当提前一行书写。此外注释不仅可以使用文字描述功能,也可以画出相应的时序图或者电路结构图,这样更方便后续阅读代码。

//generate new signal
 reg clk_1hz;    //new signal
// _________ ___
//_____________| |_______|
// ___ ____ ____
//____/ \___/ \___________/ \__
逻辑优化

  使用圆括号以确定程序的优先级和逻辑结构,避免优先级的问题产生错误。

  多判断情况下,尽可能使用case语句替代if语句。case语句在判断情况较多时,综合后消耗的资源更少,时序也更加好。

  状态机编写,尽量使用三段式。

  多个功能,尽可能采用实例化的方式进行调用,不仅有利于团队设计,也有利于更新维护。

方便阅读

  端口信号要对齐。单行内容过多,换行书写。模块例化时,端口信号尽量与连接信号隔开,并各自对齐。信号为向量时,指明其位宽,方便阅读和调试。

module testmodule(clk, rst_n, out);
    input            clk   ;
    input            rst_n ;
    output [1:0] out       ;

    ila ila_inst(
    .clk      (clk)    ,
    .prob0  (rst_n)    ,
    .prob1  (out[1:0])
);
endmodule

 严格的代码规范

  变量声明时不应赋初值。信号赋初值会影响电路的逻辑功能,从而影响验证。

always语句

  不可以在一个always语句块中,同时将一个信号的上升沿和下降沿作为触发信号。综合后电路的结果可能不会如预期。

always @(posedge clk, negedge clk)  //这是不被允许的

  一个变量只能在一个always块中被赋值,因为综合后的该变量寄存器会有多个激励信号,此时会影响电路的状态。

//此设计不被允许
always @(posedge clk)
    x = x + 1;
always @(posedge clk)
    x =  x +2;

  一个 always 块中不要存在多个并行或不相关的条件语句,使用多个 always 分别描述。该结构仿真结果不准确。功能结构划分不清晰。

同步与异步

   尽量采用同步设计。

综合

  变量如果需要乘,除,取余运算时,应尽可能使用IP模块或集成模块,但 parameter 类型的常量就可以使用此类操作符,编译之前完成运算,不会消耗过多的硬件资源。

  if语句结构要完整。

  case语句结构要完整。

  敏感信号要罗列完整。

  避免使用不可综合的语句结构。

门级建模

多输入门

  多输入门,指的是只有一个输出端口,有一个或多个输入端口的门级电路。常见的多输入门有:and(与门) nand(与非门) or(或门) nor(或非门) xor(异或门)  xnor(同或门),在Verilog中,可以使用模块例化的方式来完成。例化时,第一个端口是输出,后边的端口是输入。在多输入门中,输出不可能是高阻态(Z)

and a1(out, in1, in2);
or   a2(out, in1, in2);
xor  a3(out, in1, in2, in3);
多输出门

  多输出门,指的是只有一个输入端口,有一个或多个输出端口的门级电路。常见的多输出门有:not(非门),buf(缓冲器)。和多输入门类似,可以使用模块例化的方式对多输出门进行调用。左边端口是输出,右边端口是输入。在多输出门中,输出不可能是高阻态(Z)

buf(out1, out2, out3, in);
not(out, in)
三态门

  Verilog 中还提供了 4 个带有控制端的 buffer 门单元,称为三态门。当使能信号有效时,按照缓冲器或者非门的正常功能输出;使能信号无效时,输出为Z。实例化格式为:(out, in, ctrl)。

  高电平使能三态缓冲器(bufif1);低电平使能三态缓冲器(bufif0);高电平使能三态非门(notif1);低电平使能三态非门(notif1)。

bufif1 buf1     (OUTX, IN1, CTRL1) ;
bufif0 buf2     (OUTY, IN1, CTRL2) ;
notif1 buf3     (OUTZ, IN1, CTRL3) ;
notif0          (OUTX1, IN1, CTRL4) ;
上下拉电阻

  上拉是将不确定的信号通过一个电阻钳制在一个高电平。下拉是将不确定的信号通过一个电阻与地相连,固定在低电平。上拉或下拉电阻,具有限流、提高驱动能力、防静电等作用,可以有效保护电路。此类门电路单元只有输出,没有输入。

pullup p1(in);
pulldown (out);

 门延迟

  实际电路中,门电路之间是有延迟。有三种延迟,上升延迟,下降延迟和关断延迟。其中上升延迟是指信号从"非1"变成"1"所需要的时间(t1);下降延迟是指信号从"非0"变成"0"所需要的时间(t2);关断延迟是指信号从"非Z"变成"Z"所需要的时间(t3)。信号从"非X"变成"X"所需要的时间 = min{ t1, t2, t3 }。

//指定一个延迟,所有的延迟均为该值
and #(1)             (OUT1, IN1, IN2) ;
//指定两个延迟,上升2.1,下降2,其余延迟为为二者最小值
or  #(2.1, 2)        (OUT2, IN1, IN2) ;
//指定三个延迟,上升2,下降1,关断1.3,其余延迟为三者最小值
bufif0 #(2, 1, 1.3)  (OUT3, IN1, CTRL) ;

  #(最小 : 典型 : 最大)。最小延迟:门单元所具有的最小延迟;典型延迟:门单元所具有的典型延迟;最大延迟:门单元所具有的最大延迟。

 用户自定义原语(UDP)

  原语(Primitive),执行期间的不可分割的最小单位。Verilog中常用的内置原语有:and,or等。在Verilog中,支持用户自定义原语,即User Defined Primitive(UDP),UDP内部不可以调用module或者其它Primitive。UDP的格式如下,UDP为多输入门,并且每个端口的宽度只能为1bit。UDP 状态表是 UDP 中最重要的部分,用关键字 table 声明,它定义了如何根据输入状态和当前状态得到输出值,类似于逻辑真值表,状态表不能够处理Z值,会将Z值当作X的进行处理。UDP可以定义在文件的任何位置。

primitive  name_UDP(out, in1,in2,in3, ... );
    input in1, in2, in3, ...;
    output  out;
    
    reg ...;
    
    table
        ...
    endtable
endprimitive
组合逻辑UDP

  组合逻辑的状态表项如下。需要注意的是,状态表中输入端口的顺序应当与UDP输入端口的顺序保持一致;对于不确定的输出,组合逻辑UDP会直接输出X值,所以在编写状态表时应当考虑所有的情况。

<input1> <input2> <input3> ... <inputN> : <output>;
//一个and门
primitive and_udp(out, a, b);
    input   a;
    input   b;
    output out;    

    table
// a b : out; 0 0 : 0; 0 1 : 0; 1 0 : 0; 1 1 : 1;
//多考虑的情况
// 0 x : 0;
// x 0 : 0;
endtable endprimitive
时序逻辑UDP

  时序逻辑的状态表项如下。状态表的输入可以是电平,也可以是跳边沿的形式。输出必须声明为reg型变量。状态表中跳变沿的表示方式为:上升沿 - 01,下降沿 - 10。

<input1> <input2> <input3> ... <inputN> : <current_state>  :  <next_state>;
//D锁存器。清零端有效,输出为0;清零端无效,使能端无效,输出不变
//清零端无效,使能端有效,输入什么,输出什么
primitive d_latch(q, clear, en, d);
   output reg  q ;
   input          d, en, clear ;

   initial  q = 0 ;

   table
    //clear     en      d       :q      :q_next ;
      1            ?       ?       :?      :0 ;    //清零
      0            0       ?       :?      :- ;    //"-" 保持不变
      0            1       0       :?      :0 ;    //输出等于输入
      0            1       1       :?      :1 ;
   endtable
endprimitive

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

参考资料

[1] Verilog教程进阶篇,菜鸟教程,https://www.runoob.com/w3cnote/verilog2-tutorial.html。

[2] 数字电路实验教程,Vlab,https://vlab.ustc.edu.cn/guide/doc_verilog.html

[3] 黄海,于斌,Verilog HDL 设计实用教程[M],北京:清华大学出版社,2021.

posted @ 2023-03-20 10:56  素衣叹风尘  阅读(644)  评论(0)    收藏  举报