Vivado使用技巧(28):支持的Verilog语法

复杂的电路设计通常使用自顶向下的设计方法,设计过程中的不同阶段需要不同的设计规格。比如架构设计阶段,需要模块框图或算法状态机(ASM)图表这方面的设计说明。一个框图或算法的实现与寄存器(reg)和连线(wire)息息相关。Verilog便具有将ASM图表和电路框图用计算机语言表达的能力,本文将讲述Vivado综合支持的Verilog硬件描述语言。

Verilog提供了行为化和结构化两方面的语言结构,描述设计对象时可以选择高层次或低层次的抽象等级。使用Verilog设计硬件时,可以将其视作并行处理和面向对象编程。Vivado综合支持IEEE 1364标准。Vivado综合对Verilog的支持可以用最有效的方式描述整体电路和各个模块。综合会为每个模块选择最佳的综合流程,将高层次的行为级或低层次的结构级转换为门级网表。

本文将介绍Vivado综合支持的所有Verilog语法。

1.可变部分选择
除了用两个明确的值限定选择边界外(如assign out = data[8:2]),还可以使用变量从向量中选择一组bit。设置一个起始点和截取的宽度,起始点可以动态变化。示例如下:
reg [3:0] data;
reg [3:0] select;
wire [7:0] byte = data[select +: 8]; //+、-表示从起始点开始增加或减少

2.结构化Verilog
Verilog可以进行多个代码块设计,并按一定的设计层次组合起来。下面给出于此相关的重要概念:

  • 组件(Component):结构化设计中的一个基本块;
  • 申明(Declaration):组件与外部交流的信息;
    主体(Body):组件内部的行为或结构;

  • 端口(Port):组件的I/O;
  • 信号(Signal):组件与组件之间的连线;
  • 一个组件用常见的模块(module)来表示。组件之间的连接由实例化(instantiation)声明实现。实例化声明规定一个组件在另外一个组件或电路中的实例,赋予标识符,并用关系列表设定信号与端口之间的联系。

    除了自己设计的组件外,结构化Verilog还支持实例化预定义的原语:逻辑门、寄存器、Xilinx特定的原语(如CLKDLL、BUFG)。这些原语都定义在Xilinx Verilog库文件unisim_comp.v中。逻辑门原语包括AND、OR、XOR、NAND、NOR、NOT。实例化这些逻辑门来搭建更大的逻辑电路,示例如下:

    //实现2输入或非逻辑功能
    module build_xor
    (
    input a, b,
    output c
    );

    wire a_not, b_not;
    //每个实例必须有不同的实例化名称
    not a_inv (a_not, a);
    not b_inv (b_not, b);
    and a1 (x, a_not, b);
    and a2 (y, b_not, a);
    or out (c, x, y);

    endmodule

    3.Verilog参数
    参数化代码提高了可读性和代码紧凑型、容易维护和再使用。一个Verilog参数(parameter)就是一个常数(不支持字符串),且实例化参数化模块时可以改写参数值。下面给出示例:
    //Verilog参数控制实例化块寄存器的宽度
    module myreg #(parameter SIZE = 1)
    (
    input clk, clken,
    input [SIZE-1:0]d,
    output reg [SIZE-1:0]q
    );

    always @(posedge clk)
    if (clken) q <= d;

    endmodule

    //顶层模块
    module test #(parameter SIZE = 8)
    (
    input clk, clken,
    input [SIZE-1:0] di,
    output [SIZE-1:0] do
    );

    myreg #SIZE inst_reg (clk, clken, di, do);

    endmodule

    4.Verilog使用限制
    在Vivado综合中用到的Verilog语法有如下3点限制:

  • 大小写敏感:Verilog是一种大小写敏感的语言,但在Vivado中,只有实例和信号名称会区分大小写。如果两个module名称只有大小写不同,综合时会报错。尽管如此,也不推荐仅用大小写区分两个不同的对象,在混合语言工程中可能会引起意料之外的问题。
  • 阻塞和非阻塞赋值:不要混合使用阻塞和非阻塞赋值。尽管综合时可能不会报错,但在仿真时会出现错误。下面给出两个错误例子:
  • //同一信号不要混用阻塞和非阻塞赋值
    always @(in1)
    if (in2) out1 = in1;
    else out1 <= in2;

    //同一信号的不同bit不要混用
    if (in2) begin
    out1[0] = 1'b0;
    out1[1] <= in1;
    end
    else begin
    out1[0] = in2;
    out1[1] <= 1'b1;
    end

  • 整数处理:某些情况下,Vivado综合器处理整数时与其它综合工具方法不同,因此必须使用特定的代码编写方式。在Case语句或拼接语句中,使用未定义大小的整数都会导致无法预料的结果。下面给出例子:
  • //case语句
    reg [2:0] condition1;
    always @(condition1) begin
    case(condition1)
    4 : data_out = 2; //生成错误结果
    3'd4 : data_out = 2; //正常工作
    endcase
    end

    //拼接语句
    reg [31:0] temp;
    assign temp = 4'b1111 % 2; //未确定位宽的运算用临时信号存储
    assign dout = {12/3,temp,din}; //12/3运算位宽不确定,结果错误

    5.Verilog构造和系统任务
    Vivado综合支持的Verilog构造与系统任务包括:

    整数、实数、assign(有限制)、deassign(有限制)、repeat语法(重复值必须是常数)、for语法(范围必须是静态的)、disable(不能用于for循环和repeat循环)、module定义、defparam、实例数组、`default_nettype、`define、`ifdef、`ifndef、`elsif、`include、`file、`line、$fclose、$fgets、$fopen、$fscanf、$readmemb、$readmemh、$signed、$unsigned、$floor(仅用于参数)、$ceil(仅用于参数)。

    Vivado综合不支持和会忽视的的Verilog构造和系统任务包括:

    字符串、网络类型(tri0、tri1、trireg)、驱动强度、实数和实时寄存器、命名事件、事件(@)、延迟(#)、force、release、forever语法、wait、并行块、设定块、macromodule定义、层次结构名称、`celldefine、`endcelldefine、`resetall、`timescale、`unconnected_drive、`nounconnected_drive、`uselib、$display、$fdisplay、$finish、$fwrite、$monitor、$random、$stop、$strobe、$time、$write、$clog2(仅SystemVerilog支持)、$rtoi、$itor、all others。

    介绍其中几个非常常用的系统任务:

  • $signed和$unsigned可以强制规定输入数据为带符号数或无符号数,并作为返回值,不用管之前的符号。
  • $readmemb和$readmemh可以用于初始化块存储器,两者分别用2进制和16进制表示。如“$readmemb(“ram.data”, ram, 0, 7)”;。
  • 6.Verilog原语
    Vivado支持上文列出的Verilog门级原语,但不支持上拉下拉、驱动强度和延迟、原语矩阵这些类型的门级原语。也不支持如下转换级原语:cmos、nmos、pmos、rcmos、rnmos、rpmos、rtran、rtranif0、rtranif1、tran、tranif0、tranif1。

    实例化门级原语的示例如下:
    gate_type instance_name (output, inputs); //语法模板
    and U1 (out, in1, in2);
    bufif1 U2 (triout, data, trienable);

    7.行为级Verilog
    行为级Verilog中的变量都申明为整数,数据类型可以是reg(程序块中赋值)、wire(连续赋值)和integer(会被转换为寄存器类型)。所有变量的默认位宽为1bit,称作标量(scalar);定义的N bits位宽变量称作向量(Vector)。reg和wire可以定义为带符号数signed或无符号数unsigned。变量的每个bit可以是如下值:1(逻辑1)、0(逻辑0)、x(未知逻辑值)、z(高阻)。
    reg [3:0] arb_priority;
    wire [31:0] arb_request;
    wire signed [8:0] arb_signed;

    寄存器在定义时可以初始化,初始值是一个常数或参数,不能是函数或任务的调用。在全局复位或上电时,Vivado综合会将初始化值作为寄存器的输出(作为寄存器的INIT属性值)。而且,该初始值与本地复位是相互独立的。
    //定义时初始化寄存器
    reg arb_onebit = 1'b0;
    reg [3:0] arb_priority = 4'b1011;

    //本地置位/复位
    always @(posedge clk)
    if (rst) arb_onebit <= 1'b0;

    Verilog支持定义wire和reg的数组,支持一位数组和二维数组,但每次从数组中选择的元素不能超过一个,数组也不能作为任务或函数的传递参数。数组的定义示例如下:
    //有32个元素的数组,每个元素4bits位宽
    reg [3:0] mem_array [31:0];
    //包含64个8bits位宽元素的数组
    wire [7:0] mem_array [63:0];
    //包含256*16个8bits位宽wire元素的二维数组
    wire [63:0] array2 [0:255][0:15];
    //包含256*8个64bits位宽reg元素的二维数组
    reg [63:0] array2 [255:0][7:0];

    Vivado支持的所有表达式列在下表中:

    其中“===”和“!==”在综合时与“==”和“!=”功能相同,没有任何差别。但在仿真中,可以用来判断变量是否与’x’和’z’是否相等。下表给出常用操作符的运算结果,以供查阅。

    initial和always是两个程序块,每个块内部组织了一些语法声明,用begin和end表示范围。块内部的语法声明按顺序执行。综合时只会处理always块,会忽略initial块。

    8.模块module
    Verilog中描述组件(component)的方法便是模块(module),模块必须申明与实例化。模块申明包括模块名称、电路I/O端口列表、定义功能的主体,并以endmodule结束。

    每个电路I/O端口要有名称、端口模式(input、output、inout),如果端口是数组类型还要有范围信息。下面给出两种模块申明方法的示例:

    //方法1,老版本Verilog
    module example (A, B, O);

    input A, B;
    output O;

    assign O = A & B;

    endmodule

    //方法2,推荐用法
    module example
    (
    input A, B,
    output O
    );

    assign O = A & B;

    endmodule

    实例化模块时,要定义一个实例化名称和一个端口关系表。列表要规定实例与顶层模块之间如何连接,列表中的每一个元素将模块申明中的一个形式端口(port)和顶层模块中的实际网络(net)连接在一起。下面给出一个实例化上述模块的例子:
    module top
    (
    input A, B, C,
    output O
    );

    wire tmp;

    example inst_example (.A(A), .B(B), .O(tmp));
    assign O = tmp | C;

    endmodule

    Vivado综合支持两种连续赋值方式(只适用于wire和三态数据类型),用简洁的方式完成组合逻辑赋值,但是综合时会忽略连续赋值中的延迟和强度定义。显式连续赋值用assign关键词开头,紧跟一个已经申明过的网络:“wire mysignal; assign mysignal = select ? b : a;”。隐式连续赋值在申明时便完成赋值:“wire misignal = a | b;”。

    9.过程赋值
    如上所述,wire和三态类型要用连续赋值,reg类型变量则需要用过程赋值,借助always块、任务(task)、函数(function)实现。学习Verilog难免会遇到阻塞赋值和非阻塞赋值的概念,但其实在设计中只需要明白阻塞赋值(=)用于仿真;非阻塞赋值(<=)用于设计中的过程赋值即可。

    always块中的组合逻辑由Verilog时间控制语句有效地建模。其中,延迟时间控制语句[#]仅用于仿真,综合时会忽略;组合逻辑建模主要由事件控制时间控制语句[@]实现。

    每个always块都有一个敏感列表,列在“always @”后面的括号中。如果敏感列表中一个信号的相关事件发生(值变化或边沿到来),就会激活该always块。在always块中,如果信号没有在if或case的所有分支中明确地赋值,综合会产生一个锁存器保持之前的值。一个程序块中可以使用如下语句:

    [1].if-else语句:
    使用true和false条件来执行语句,执行多条语句要使用begin…end关键词。

    [2].case语句:
    比较表达式和分支的值,比较顺序按照编写分支的顺序进行,执行第一个匹配的分支。如果没有匹配项则执行default分支。case语句中不要使用未指定位宽大小的整数,否则可能会产生错误结果。

    casez将分支的任意bit位上的z值视作不关心;casex将分支的任意bit位上的x值视作不关心。casez和casex中不关心的bit用‘?’代替。下面给出一个使用case的示例代码:

    module mux4
    (
    input [1:0] sel,
    input [1:0] a, b, c, d,
    output reg [1:0] outmux
    );

    always @ *
    case(sel)
    2'b00 : outmux = a;
    2'b01 : outmux = b;
    2'b10 : outmux = c;
    2'b11 : outmux = d;
    endcase

    endmodule

    上述代码在评估输入值时,按照一定的优先级顺序进行。如果希望能并行地处理这个过程,使用paralled_case属性,将case语句替换为“(* paralled_case *)” case(sel)”。

    [3].For语句与Repeat语句:
    使用循环可以完成一些重复性工作。For循环的边界必须是常数,停止循环条件需要使用>、<、>=、<=四种运算符。使用“var = var +或- step”来控制执行下一轮运算,var为循环变量,step是一个常数值。

    使用repeat语句,重复次数也必须是常数值。

    [4].While循环:
    While的测试表达式可以是任意合法的Verilog表达式。为了避免造成无限循环,可以使用-loop_iteration_limit选项。该语法很少使用,下面给出一个示例代码:

    parameter P = 4;
    always @(ID_complete)
    begin : UNIDENTIFIED
    integer i;
    reg found;
    unidentified = 0;
    i = 0;
    found = 0;
    while (!found && (i < P))
    begin
    found = !ID_complete[i];
    unidentified[i] = !ID_complete[i];
    i = i + 1;
    end
    end

    [5].顺序always块:
    always块可以描述带有顺序性的电路,敏感列表中需要包含如下边沿触发事件(上升沿posedge或下降沿negedge):必须有一个时钟事件、可选的置位/复位事件。如果不需要异步信号,always块模板如下:
    always @(posedge CLK)
    begin
    //同步部分
    end

    如果需要异步控制信号,always块模板如下:
    always @(posedge CLK or posedge ACTRL1 or à )
    begin
    if (ACTRL1)
    //异步部分
    else
    //同步部分
    end

    下面给出四个不同触发方式的顺序always块示例代码:
    //上升沿触发时钟控制的8bits寄存器
    module seq1
    (
    input [7:0]DI,
    input CLK,
    output reg [7:0] DO
    );

    always @(posedge CLK)
    DO <= DI ;
    endmodule

    //添加一个高电平有效异步复位信号
    module seq1
    (
    input [7:0]DI,
    input CLK, ARST,
    output reg [7:0] DO
    );

    always @(posedge CLK or posedge ARST)
    if (ARST == 1'b1) DO <= 8'h00;
    else DO <= DI ;
    endmodule

    //再添加一个低电平有效异步置位信号
    module seq1
    (
    input [7:0]DI,
    input CLK, ARST, ASET
    output reg [7:0] DO
    );

    always @(posedge CLK or posedge ARST or negedge ASET)
    if (ARST == 1'b1) DO <= 8'h00;
    else if (ASET == 1'b1) DO <= 8'hFF;
    else DO <= DI ;
    endmodule

    //不使用异步控制逻辑,使用同步复位
    module seq1
    (
    input [7:0]DI,
    input CLK, SRST,
    output reg [7:0] DO
    );

    always @(posedge CLK)
    if (SRST == 1'b1) DO <= 8'h00;
    else DO <= DI ;
    endmodule

    最后再补充一些与赋值有关的内容。如果表达式左边位宽大于右边的位宽,赋值时需要在高位填充:

  • 如果表达式右边为无符号数,则高位补0;
  • 如果表达式右边为带符号数,则高位补符号位;
  • 如果表达式右边的最高位为x或z,则无论该数为无符号数还是带符号数,高位都补充为x或z。
  • 10.任务与函数
    如果设计中要多次使用重复的代码,可以使用任务task和函数function来减少代码量,提升可维护性。任务和函数必须在模块中申明和使用,函数头只包含输入参数,任务头包含输入、输出和双向参数。函数的返回值可以申明为无符号数或带符号数,函数内容与always块类似。下面分别给出一个函数和任务的示例代码:

    //函数function使用示例
    module test
    (
    input [3:0] A, B,
    input CIN,
    output [3:0] S,
    output COUT
    );

    wire [1:0] S0, S1, S2, S3;

    function signed [1:0] ADD;
    input A, B, CIN;
    reg S, COUT;
    begin
    S = A ^ B ^ CIN;
    COUT = (A&B) | (A&CIN) | (B&CIN);
    ADD = {COUT, S};
    end
    endfunction

    assign S0 = ADD (A[0], B[0], CIN),
    S1 = ADD (A[1], B[1], S0[1]),
    S2 = ADD (A[2], B[2], S1[1]),
    S3 = ADD (A[3], B[3], S2[1]),
    S = {S3[0], S2[0], S1[0], S0[0]},
    COUT = S3[1];

    endmodule

    //任务task使用示例
    module test
    (
    input [3:0] A, B,
    input CIN,
    output [3:0] S,
    output COUT
    );

    reg [1:0] S0, S1, S2, S3;

    task ADD;
    input A, B, CIN;
    output [1:0] C;
    reg [1:0] C;
    reg S, COUT;
    begin
    S = A ^ B ^ CIN;
    COUT = (A&B) | (A&CIN) | (B&CIN);
    C = {COUT, S};
    end
    endtask

    always @(A or B or CIN)
    begin
    ADD (A[0], B[0], CIN, S0);
    ADD (A[1], B[1], S0[1], S1);
    ADD (A[2], B[2], S1[1], S2);
    ADD (A[3], B[3], S2[1], S3);
    S = {S3[0], S2[0], S1[0], S0[0]};
    COUT = S3[1];
    end

    endmodule

    Verilog还支持递归任务和递归函数,要使用automatic关键词申明。递归次数由-recursion_iteration_limit选项设置,默认为64,以避免无限递归。下面给出一个计算阶乘的递归函数的例子。
    function automatic [31:0] fac;
    input [15:0] n;
    if (n == 1) fac = 1;
    else fac = n * fac(n-1);
    endfunction

    Vivado综合支持函数调用来计算常数值,将其称之为常数函数。下面给出一个使用常数函数的例子:
    module test #(parameter ADDRWIDTH = 8, DATAWIDTH = 4)
    (
    input clk, we,
    input [ADDRWIDTH-1:0] a,
    input [DATAWIDTH-1:0] di,
    output [DATAWIDTH-1:0] do
    );

    function integer getSize;
    input addrwidth;
    begin
    getSize = 2**addrwidth;
    end
    endfunction

    reg [DATAWIDTH-1:0] ram [getSize(ADDRWIDTH)-1:0];
    always @(posedge clk)
    if (we) ram[a] <= di;

    assign do = ram[a];

    endmodule

    Verilog中的常数可以用2进制、8进制、10进制和16进制表示,没有明确表示时默认为10进制。如下面4’b1010、4’o12、4’d10、4’ha表示同一个数。

    11.Verilog宏
    Verilog可以像这样定义宏“`define TESTEQ1 4’b1101”。定义的宏可以用在后面的代码中,如“if (request == `TESTEQ1)”。使用`ifdef和`endif可以检测是否定义了某个宏,相当于条件编译。如果`ifedf调用的宏被定义过,则内部的代码将会编译;如果宏没有定义,则会编译`else中的代码。`else不是必须的,但必须有`endif。

    使用宏可以在不修改源代码的情况下修改设计,在IP核生成和流程测试中很有用。下面给出两个使用宏的例子:

    //示例1
    'define myzero 0
    assign mysig = 'myzero;

    //示例2,条件编译
    'ifdef MYVAR
    module if_MYVAR_is_declared;
    ...
    endmodule
    'else
    module if_MYVAR_is_not_declared;
    ...
    endmodule
    'endif

    12.Include文件
    Verilog可以将源代码分散在多个文件中,当需要引用另一个文件中的代码时,可以使用如下语句:“`include
    ”。该代码可以将指定文件的内容全部插入到当前文件的`include行中。Vivado首先会在指定路径中查找,如果没有找到则会在-include_dirs选项设置的目录中查找。可以同时使用多个`include语句。

    13.Generate语法
    Verilog的注释和C++语言相同,支持单行注释和多行注释,这里不再举例。最后再说说常用的Generate语法。使用generate可以简化代码编写工作,generate…endgenerate中的内容再RTL分析阶段会被转换为对应的电路。

    使用generate语法可以创建原语或模块实例、initial或always程序块、连续赋值、网络和变量申明、参数重定义、任务或函数定义。Vivado支持全部三种generate语法:generate循环(generate-for)、generate条件(generate-if-else)和generate情况(generate-case)。

    [1]. generate-for
    使用generate-for主要用来创建多个实例化,与for循环用法基本相同,但必须使用genvar变量,且begin语句必须有一个单独的命名。下面给出一个示例代码:
    generate genvar i;
    for (i=0; i<=7; i=i+1)
    begin : for_name
    adder add (a[8*i+7 : 8*i], b[8*i+7 : 8*i], ci[i], sum_for[8*i+7 : 8*i],
    c0_or[i+1]);
    end
    endgenerate

    [2]. generate-if-else
    主要用来控制生成哪一个对象,每一个分支用begin…end限定,begin语句必须有一个单独的命名。下面给出一个示例代码:
    //根据数据位宽选择不同的乘法器实现方式
    generate
    if (IF_WIDTH < 10)
    begin : if_name
    multiplier_imp1 # (IF_WIDTH) u1 (a, b, sum_if);
    end
    else
    begin : else_name
    multiplier_imp2 # (IF_WIDTH) u2 (a, b, sum_if);
    end
    endgenerate

    [3]. generate-case
    主要用来控制在哪种条件下生成哪个对象。case的每一个分支用begin…end限定,begin语句必须有一个单独的命名。下面给出一个示例代码:
    //根据数据位宽选择不同的加法器实现方式
    generate
    case (WIDTH)
    1:
    begin : case1_name
    adder #(WIDTH*8) x1 (a, b, ci, sum_case, c0_case);
    end
    2:
    begin : case2_name
    adder #(WIDTH*4) x2 (a, b, ci, sum_case, c0_case);
    end
    default:
    begin : d_case_name
    adder x3 (a, b, ci, sum_case, c0_case);
    end
    endcase
    endgenerate


    文章来源:FPGADesigner的博客
    *本文由作者授权转发,如需转载请联系作者本人

    最新文章

    最新文章