Verilog教程
阅读原文时间:2023年07月10日阅读:3

当用 Verilog 设计完成数字模块后进行仿真时,需要在外部添加激励,激励文件叫 testbench。

Verilog 的主要特性:

  • 可采用 3 种不同的方式进行设计建模:行为级描述——使用过程化结构建模;数据流描述——使用连续赋值语句建模;结构化方式——使用门和模块例化语句描述。
  • 两类数据类型:线网(wire)数据类型与寄存器(reg)数据类型,线网表示物理元件之间的连线,寄存器表示抽象的数据存储元件。
  • 能够描述层次设计,可使用模块实例化描述任何层次。
  • 用户定义原语(UDP)创建十分灵活。原语既可以是组合逻辑,也可以是时序逻辑。
  • 可提供显示语言结构指定设计中的指定端口到端口的时延,以及路径时延和时序检查。
  • Verilog 支持其他编程语言接口(PLI)进行进一步扩展。PLI 允许外部函数访问 Verilog 模块内部信息,为仿真提供了更加丰富的测试方法。
  • 同一语言可用于生成模拟激励和指定测试的约束条件。
  • 设计逻辑功能时,设计者可不用关心不影响逻辑功能的因素,例如工艺、温度等。
  • 主要应用于专用集成电路(ASIC),就是具有专门用途和特殊功能的独立集成电路器件。

1、可编程逻辑器件

FPGA 和 CPLD 是实现这一途径的主流器件。他们直接面向用户,具有极大的灵活性和通用性,实现快捷,测试方便,开发效率高而成本较低。

2、半定制或全定制 ASIC

通俗来讲,就是利用 Verilog 来设计具有某种特殊功能的专用芯片。根据基本单元工艺的差异,又可分为门阵列 ASIC,标准单元 ASIC,全定制 ASIC。

3、混合 ASIC

主要指既具有面向用户的 FPGA 可编程逻辑功能和逻辑资源,同时也含有可方便调用和配置的硬件标准单元模块,如CPU,RAM,锁相环,乘法器等

  • 1.3 仿真软件安装 vivado和modelsim

  • 1.4 Verilog设计方法 自上而下

    需求分析→功能划分→文本描述→功能仿真→逻辑综合→布局布线→时序仿真→FPGA/CPLD下载或ASIC制造工艺生产

2.1 基础语法
  • 区分大小写 格式自由可以一行也可以多行 每个语句必须以分号结束,空白符没有意义

  • 单行注释// 多行注释/**/

  • 标识符可以是字母、数字、$符号和_下划线的组合,第一个字符必须是字母或下划线,区分大小写,Verilog 中关键字全部为小写。

    input clk; //input 为关键字,clk 为标识符

2.2 数值表示

0:逻辑0或“假”

1:逻辑1或“真”

x或X:未知,在实际电路里,信号可能为 1,也可能为 0。

z或Z:高阻,常见于信号(input, reg)没有驱动时的逻辑结果

  • 十进制('d或'D),十六进制('h或'H),二进制('b 或 'B),八进制('o 或 'O)。数值可指明位宽,也可不指明位宽。

    4'b1011 // 4bit 数值

    32'h3022_c0de // 32bit 的数值,下划线 _ 是为了增强代码的可读性。

    counter = 'd100 ; //不指明位宽 一般会根据编译器自动分频位宽,常见的为32bit

    counter = 100 ;

    counter = 32'h64 ;

  • -6'd15 //通常在表示位宽的数字前面加一个减号来表示负数。

    -15 在 5 位二进制中的形式为 5'b10001, 在 6 位二进制中的形式为6'b11_0001。减号不能放在基数和数字之间。

  • 实数表示方法

    十进制:30.123

    科学计数法:1_0001e4 //大小为100010000

  • 字符串表示方法

    是由双引号包起来的字符队列。不能多行书写,即字符串中不能包含回车符。

    reg [0: 14*8-1] str ;
    initial begin
    str = "www.runoob.com";
    end

2.3 Verilog 数据类型

线网(wire)与寄存器(reg)

wire对应于连续赋值,如assign;reg对应于过程赋值,如always,initial

  • wire 类型表示硬件单元之间的物理连线,由其连接的器件输出端连续驱动。如果没有驱动元件连接到 wire 型变量,缺省值一般为 "Z"。

    wire interrupt ;

    wire flag1, flag2 ;

    wire gnd = 1'b0 ;

    线网型还有其他数据类型,包括 wand,wor,wri,triand,trior,trireg 等

    (1)assign 语句中变量需要定义成wire型,使用wire必须搭配assign

    (2)元件例化时候的输出必须用wire .out(dout)

    (3)input、output和inout的预设值都是wire

  • 寄存器(reg)用来表示存储单元,它会保持数据原有的值,直到被改写。

    寄存器不需要驱动源,也不一定需要时钟信号。在仿真时,寄存器的值可在任意时刻通过赋值操作进行改写。

    reg rstn ;
    initial begin
    rstn = 1'b0 ;
    #100 ;
    rstn = 1'b1 ;
    end

(1)变量放在begin……end之内必须使用reg变量(2)在initial语句中使用

  • 向量,当位宽大于 1 时,wire 或 reg 即可声明为向量的形式。

    reg [3:0] counter ; //声明4bit位宽的寄存器counter

    wire [32-1:0] gpio_data; //声明32bit位宽的线型变量gpio_data

    wire [8:2] addr ; //声明7bit位宽的线型变量addr,位宽范围为8:2

    reg [0:31] data ; //声明32bit位宽的寄存器变量data, 最高有效位为0

  • Verillog 还支持指定 bit 位后固定位宽的向量域选择访问。

    [bit+: width] : 从起始 bit 位开始递增,位宽为 width。

    [bit-: width] : 从起始 bit 位开始递减,位宽为 width。

//下面 2 种赋值是等效的

A = data1[31-: 8] ;

A = data1[31:24] ;

//下面 2 种赋值是等效的

B = data1[0+ : 8] ;

B = data1[0:7] ;

  • 对信号重新进行组合成新的向量时,需要借助大括号。

    wire [31:0] temp1, temp2 ;

    assign temp1 = {byte1[0][7:0], data1[31:8]}; //数据拼接

    assign temp2 = {32{1'b0}}; //赋值32位的数值0

整数,实数,时间等数据类型实际也属于寄存器类型。

  • 整数类型用关键字 integer 来声明。声明时不用指明位宽,位宽和编译器有关,一般为32 bit。reg 型变量为无符号数,而 integer 型变量为有符号数。

    integer j ; //整型变量,用来辅助生成数字电路

  • 实数用关键字 real 来声明,可用十进制或科学计数法来表示。实数声明不能带有范围,默认值为 0。如果将一个实数赋值给一个整数,则只有实数的整数部分会赋值给整数。

    real data1 ;

  • Verilog 使用特殊的时间寄存器 time 型变量,对仿真时间进行保存。其宽度一般为 64 bit,通过调用系统函数 $time 获取当前仿真时间。

    time current_time ;

    initial begin

    #100 ;

    current_time = $time ; //current_time 的大小为 100

    end

  • 在 Verilog 中允许声明 reg, wire, integer, time, real 及其向量类型的数组。

    数组维数没有限制。线网数组也可以用于连接实例模块的端口。数组中的每个元素都可以作为一个标量或者向量,以同样的方式来使用,形如:<数组名>[<下标>]。对于多维数组来讲,用户需要说明其每一维的索引。

    integer flag [7:0] ; //8个整数组成的数组

    reg [31:0] data_4d[11:0][3:0][3:0][255:0] ; //声明4维的32bit数据变量数组

    对数组元素的赋值操作:

    向量是一个单独的元件,位宽为 n;数组由多个元件组成,其中每个元件的位宽为 n 或 1。它们在结构的定义上就有所区别。

  • 存储器变量就是一种寄存器数组,可用来描述 RAM 或 ROM 的行为。

    reg membit[0:255] ; //256bit的1bit存储器

    reg [7:0] mem[0:1023] ; //1Kbyte存储器,位宽8bit

    mem[511] = 8'b0 ; //令第512个8bit的存储单元值为0

  • 参数用来表示常量,用关键字 parameter 声明,只能赋值一次

    parameter data_width = 10'd32 ;

    parameter i=1, j=2, k=3 ;

    parameter mem_size = data_width * 10 ;

    通过实例化的方式,可以更改参数在模块中的值。

    局部参数用 localparam 来声明,其作用和用法与 parameter 相同,区别在于它的值不能被改变。

  • 字符串保存在 reg 类型的变量中,每个字符占用一个字节(8bit)。

    字符串不能多行书写,即字符串中不能包含回车符。如果寄存器变量的宽度大于字符串的大小,则使用 0 来填充左边的空余位;如果寄存器变量的宽度小于字符串大小,则会截去字符串左边多余的数据。例如,为存储字符串 "run.runoob.com", 需要 14*8bit 的存储单元:

    initial begin
    str = "run.runoob.com";
    end

  • 转义字符 \ 例如换行符,制表符

    \n换行、\t制表符、%%意为%、"显示"、\ooo显示1到3个八进制数字字符

2.4 表达式
  • 表达式由操作符和操作数构成

    a^b ; //a与b进行异或操作

    address[9:0] + 10'b1 ; //地址累加

    flag1 && flag2 ; //逻辑与操作

  • 操作数可以是任意的数据类型

    操作数可以为常数,整数,实数,线网,寄存器,时间,位选,域选,存储器及函数调用等。

    module test;

    //实数
    real a, b, c;
    c = a + b ;

    //寄存器
    reg [3:0] cprmu_1, cprmu_2 ;
    always @(posedge clk) begin
    cprmu_2 = cprmu_1 ^ cprmu_2 ;
    end

    //函数
    reg flag1 ;
    flag = calculate_result(A, B);

    //非法操作数
    reg [3:0] res;
    wire [3:0] temp;
    always@ (*)begin
    res = cprmu_2 – cprmu_1 ;
    //temp = cprmu_2 – cprmu_1 ; //不合法,always块里赋值对象不能是wire型
    end

    endmodule

  • 大约 9 种操作符,分别是算术、关系、等价、逻辑、按位、归约、移位、拼接、条件操作符。

同类型操作符之间,除条件操作符从右往左关联,其余操作符都是自左向右关联。

操作符

操作符号

优先级

单目运算

+ - ! ~

最高

乘、除、取模

* / %

加减

+ -

移位

<< >>

关系

< <= > >=

等价

== != === !===

归约

& ~&

^ ~^

~

逻辑

&&

||

条件

?:

2.4.2 算术操作符
  • 双目操作符对 2 个操作数进行算术运算,包括乘(*)、除(/)、加(+)、减(-)、求幂(**)、取模(%)。

    reg [3:0] a, b;
    reg [4:0] c ;
    a = 4'b0010 ;
    b = 4'b1001 ;
    c = a+b; //结果为c=b'b1011
    c = a/b; //结果为c=4,取整
    //如果操作数某一位为 X,则计算结果也会全部出现 X。
    b = 4'b100x ;
    c = a+b ; //结果为c=4'bxxxx
    //对变量进行声明时不要让结果溢出。相加的 2 个变量位宽为 4bit,那么结果寄存器变量位宽最少为 5bit。无符号数乘法时,结果变量位宽应该为 2 个操作数位宽之和。

    reg [3:0] mula ;
    reg [1:0] mulb;
    reg [5:0] res ;
    mula = 4'he ;
    mulb = 2'h3 ;
    res = mula * mulb ; //结果为res=6'h2a, 数据结果没有丢失位数
    //+ 和 - 也可以作为单目操作符来使用,表示操作数的正负性。此类操作符优先级最高。
    -4 //表示负4
    +3 //表示正3
    //负数表示时,可以直接在十进制数字前面增加一个减号 -,也可以指定位宽。
    mula = -4'd4 ;
    //关系操作符有大于(>),小于(<),大于等于(>=),小于等于(<=),关系操作符的正常结果有 2 种,真(1)或假(0),如果操作数中有一位为 x 或 z,则关系表达式的结果为 x。 //等价操作符包括逻辑相等(==),逻辑不等(!=),全等(===),非全等(!==)。 //逻辑操作符主要有 3 个:&&(逻辑与), ||(逻辑或),!(逻辑非)。 //按位操作符包括:取反(~),与(&),或(|),异或(^),同或(~^)。 //归约操作符包括:归约与(&),归约与非(~&),归约或(|),归约或非(~|),归约异或(^),归约同或(~^)。归约操作符只有一个操作数,它对这个向量操作数逐位进行操作,最终产生一个 1bit 结果。 A = 4'b1010 ; &A ; //结果为 1 & 0 & 1 & 0 = 1'b0,可用来判断变量A是否全1 ~|A ; //结果为 ~(1 | 0 | 1 | 0) = 1'b0, 可用来判断变量A是否为全0 ^A ; //结果为 1 ^ 0 ^ 1 ^ 0 = 1'b0 //移位操作符包括左移(<<),右移(>>),算术左移(<<<),算术右移(>>>)。算术左移和逻辑左移时,右边低位会补 0。逻辑右移时,左边高位会补 0;而算术右移时,左边高位会补充符号位,以保证数据缩小后值的正确性。
    A = 4'b1100 ;
    B = 4'b0010 ;
    A = A >> 2 ; //结果为 4'b0011
    A = A << 1; //结果为 4'b1000 A = A <<< 1 ; //结果为 4'b1000 C = B + (A>>>2); //结果为 2 + (-4/4) = 1, 4'b0001

    //拼接操作符用大括号 {,} 来表示,用于将多个操作数(向量)拼接成新的操作数(向量),信号间用逗号隔开。必须指定位宽
    A = 4'b1010 ;
    B = 1'b1 ;
    Y1 = {B, A[3:2], A[0], 4'h3 }; //结果为Y1='b1100_0011
    Y2 = {4{B}, 3'd4}; //结果为 Y2=7'b111_1100
    Y3 = {32{1'b0}}; //结果为 Y3=32h0,常用作寄存器初始化时匹配位宽的赋初值
    //条件表达式有 3 个操作符
    condition_expression ? true_expression : false_expression
    //计算时,如果 condition_expression 为真(逻辑值为 1),则运算结果为 true_expression;如果 condition_expression 为假(逻辑值为 0),则计算结果为 false_expression。
    assign hsel = (addr[9:8] == 2'b0) ? hsel_p1 : hsel_p2 ;
    //当信号 addr 高 2bit 为 0 时,hsel 赋值为 hsel_p1; 否则,将 hsel_p2 赋值给 hsel。
    //条件表达式类似于 2 路(或多路)选择器,其描述方式完全可以用 if-else 语句代替。当然条件操作符也能进行嵌套,完成一个多次选择的逻辑。
    assign hsel = (addr[9:8] == 2'b00) ? hsel_p1 :
    (addr[9:8] == 2'b01) ? hsel_p2 :
    (addr[9:8] == 2'b10) ? hsel_p3 :
    (addr[9:8] == 2'b11) ? hsel_p4 ;

2.5 编译指令
  • 以反引号 ` 开始的某些标识符是 Verilog 系统编译指令。

  • 在编译阶段,`define 用于文本替换,类似于 C 语言中的 #define。

    `define DATA_DW 32

    一旦 `define 指令被编译,其在整个编译过程中都会有效,在另一个文件中也可以直接使用 DATA_DW。

    define S $stop; //用S来代替系统函数$stop; (包括分号)
    define WORD_DEF reg [31:0] //可以用WORD_DEF来声明32bit寄存器变量

  • `undef 用来取消之前的宏定义,

    `undef DATA_DW

  • 条件编译指令`ifdef, `ifndef, `elsif, `else, `endif

    ifdef MCU51 parameter DATA_DW = 8 ; elsif WINDOW
    parameter DATA_DW = 64 ;
    else parameter DATA_DW = 32 ; endif

    ifndef WINDOW parameter DATA_DW = 32 ; else
    parameter DATA_DW = 64 ;
    `endif

    //使用 include 可以在编译时将一个 Verilog 文件内嵌到另一个 Verilog 文件中 include "../../param.v"
    `include "header.v"

    //用 timescale 编译指令将时间单位与实际时间相关联。定义时延、仿真的单位和精度,均是由数字以及单位 s(秒),ms(毫秒),us(微妙),ns(纳秒),ps(皮秒)和 fs(飞秒)组成, //时间精度大小不能超过时间单位大小,1秒(s) = 1000 毫秒(ms) = 1,000,000 微秒(μs) = 1,000,000,000 纳秒(ns) = 1,000,000,000,000 皮秒(ps) timescale time_unit / time_precision
    timescale 1ns/100ps //时间单位为1ns,精度为100ps,合法 module AndFunc(Z, A, B); output Z; input A, B ; assign #5.207 Z = A & B endmodule //如果一个设计中的多个模块都带有timescale 时,模拟器总是定位在所有模块的最小时延精度上,并且所有时延都相应地换算为最小时延精度,时延单位并不受影响
    `timescale 10ns/1ns
    module test;
    reg A, B ;
    wire OUTZ ;

    initial begin
        A     = 1;
        B     = 0;
        # 1.28    B = 1;
        # 3.1     A = 0;
    end
    
    AndFunc        u_and(OUTZ, A, B) ;

    endmodule
    //在模块 AndFunc 中,5.207 对应 5.21ns。在模块 test 中,1.28 对应 13ns,3.1 对应 31ns。
    //仿真时,时延精度也会使用 100ps,如果有并行子模块,子模块间的 timescale 并不会相互影响。 //default_nettype将没有被声明的连线定义为线网类型
    default_nettype none//设置为none,那么所有的线网都要清晰的声明, //resetall 可以使得缺省连线类型为线网类型。
    //celldefine,endcelldefine用于将模块标记为单元模块,例如一些与、或、非门,一些 PLL 单元,PAD 模型,以及一些 Analog IP 等。
    celldefine module ( input clk, input rst, output clk_pll, output flag); …… endmodule endcelldefine
    //unconnected_drive,nounconnected_drive 出现在这两个编译指令间的任何未连接的输入端口,为正偏电路状态或者为反偏电路状态。
    unconnected_drive pull0 . . . / *在这两个程序指令间的所有未连接的输入端口为反偏电路状态(连接到低电平) * / nounconnected_drive

3.1 连续赋值

3.1.1 关键词:assign, 全加器

assign LHS_target = RHS_expression ;

assign 为关键词,任何已经声明 wire 变量的连续赋值语句都是以 assign 开头

LHS(left hand side) 指赋值操作的左侧,RHS(right hand side)指赋值操作的右侧

wire      Cout, A, B ;
assign    Cout  = A & B ;     //实现计算A与B的功能
//LHS_target 必须是一个标量或者线型向量,而不能是寄存器类型。
//在 wire 型变量声明的时候同时对其赋值,wire 型变量只能被赋值一次.
wire      A, B ;
wire      Cout = A & B ;
3.1.2 全加器

So = Ai ⊕ Bi ⊕ Ci ;

Co = AiBi + Ci(Ai+Bi)

3.2 时延

//普通时延,A&B计算结果延时10个时间单位赋值给Z
wire Z, A, B ;
assign #10    Z = A & B ;
//惯性延时,在 Z 获取新的值之前,A 或 B 任意一个值又发生了变化,那么计算 Z 的新值时会取 A 或 B 当前的新值。
module time_delay_module(
    input   ai, bi,
    output  so_lose, so_get, so_normal);

    assign #20      so_lose      = ai & bi ;
    assign  #5      so_get       = ai & bi ;
    assign          so_normal    = ai & bi ;
endmodule
//tb.v
`timescale 1ns/1ns

module test ;
    reg  ai, bi ;
    wire so_lose, so_get, so_normal ;

    initial begin
        ai        = 0 ;
        #25 ;      ai        = 1 ;
        #35 ;      ai        = 0 ;        //60ns
        #40 ;      ai        = 1 ;        //100ns
        #10 ;      ai        = 0 ;        //110ns
    end

    initial begin
        bi        = 1 ;
        #70 ;      bi        = 0 ;
        #20 ;      bi        = 1 ;
    end

    time_delay_module  u_wire_delay(
        .ai              (ai),
        .bi              (bi),
        .so_lose         (so_lose),
        .so_get          (so_get),
        .so_normal       (so_normal));

    initial begin
        forever begin
            #100;
            //$display("---gyc---%d", $time);
            if ($time >= 1000) begin
                $finish ;
            end
        end
    end

endmodule
//由于信号 ai 第二个高电平持续时间小于 20ns,so_lose 信号会因惯性时延而漏掉对这个脉冲的延时检测,所以后半段 so_lose 信号仍然为 0。

4.1 过程结构

关键词:initial, always在模块间并行执行

每个 initial 语句或 always 语句都会产生一个独立的控制流,执行时间都是从 0 时刻开始。

  • initial 语句从 0 时刻开始执行,只执行一次,多个 initial 块之间是相互独立的

    包含多个语句时,需要使用关键字 begin 和 end 组成一个块语句。

    不可综合的,多用于初始化、信号检测等

  • always 语句是重复执行的。从 0 时刻开始执行其中的行为语句;当执行完最后一条语句后,便再次执行语句块中的第一条语句,如此循环反复。

    多用于仿真时钟的产生,信号行为的检测

4.2 过程赋值

过程性赋值是在 initial 或 always 语句块里的赋值,赋值对象是寄存器、整数、实数等类型。过程赋值包括 2 种语句:阻塞赋值与非阻塞赋值。

  • 阻塞赋值属于顺序执行,= 作为赋值符。
  • 非阻塞赋值属于并行执行语句,同时进行的,它不会阻塞位于同一个语句块中后面语句的执行。 <= 作为赋值符。使用非阻塞赋值避免竞争冒险
  • always 时序逻辑块中多用非阻塞赋值,always 组合逻辑块中多用阻塞赋值;在仿真电路时,initial 块中一般多用阻塞赋值。

4.3 时序控制

//1.常规延时时,该语句需要等待一定时间,然后将计算结果赋值给目标信号。
reg  value_test ;
reg  value_general ;
#10  value_general    = value_test ;
//另一种写法是直接将井号 # 独立成一个时延执行语句
#10 ;
value_ single         = value_test ;

//2.内嵌延时 先将计算结果保存,然后等待一定的时间后赋值给目标信号。控制加在赋值号之后。
reg  value_test ;
reg  value_embed ;
value_embed        = #10 value_test
//延时赋值效果。右端是常量时相同,变量时不同

//3.边沿触发事件控制,事件是指某一个 reg 或 wire 型变量发生了值的变化。

//3.1一般事件控制用符号 @ 表示,关键字 posedge 指信号发生边沿正向跳变,negedge 指信号发生负向边沿跳变,
//信号clk只要发生变化,就执行q<=d,双边沿D触发器模型
always @(clk) q <= d ;
//在信号clk上升沿时刻,执行q<=d,正边沿D触发器模型
always @(posedge clk) q <= d ;
//在信号clk下降沿时刻,执行q<=d,负边沿D触发器模型
always @(negedge clk) q <= d ;
//立刻计算d的值,并在clk上升沿时刻赋值给q,不推荐这种写法
q = @(posedge clk) d ;

//3.2 命名事件控制,声明 event(事件)类型的变量,并触发该变量来识别该事件是否发生。触发信号用 -> 表示。
event     start_receiving ;
always @( posedge clk_samp) begin
        -> start_receiving ;       //采样时钟上升沿作为时间触发时刻
end

//3.3 敏感列表,当多个信号或事件中任意一个发生变化都能够触发语句的执行时,用关键字 or 或者逗号连接多个事件或信号。变量很多时更为简洁的写法是 @* 或 @(*)

//3.4 电平敏感事件控制,后面语句的执行需要等待某个条件为真,使用关键字 wait
initial begin
    wait (start_enable) ;      //等待 start 信号
    forever begin
        //start信号使能后,在clk_samp上升沿,对数据进行整合
        @(posedge clk_samp)  ;
        data_buf = {data_if[0], data_if[1]} ;
    end
end

4.4 语句块,关键词:顺序块,并行块,嵌套块,命名块,disable

  • 顺序块用关键字 begin 和 end 来表示。

    顺序块中的语句是一条条执行的。当然,非阻塞赋值除外。

    顺序块中每条语句的时延总是与其前面语句执行的时间相关。

  • 并行块有关键字 fork 和 join 来表示,并行执行

    每条语句的时延都是与块语句开始执行的时间相关。

    initial fork
    #5 ai_paral = 4'd5 ; //at 5ns
    #5 bi_paral = 4'd8 ; //at 5ns 并行
    join

    //顺序块和并行块还可以嵌套使用。
    module test ;

    reg [3:0]   ai_sequen2, bi_sequen2 ;
    reg [3:0]   ai_paral2,  bi_paral2 ;
    initial begin
        ai_sequen2         = 4'd5 ;    //at 0ns
        fork
            #10 ai_paral2          = 4'd5 ;    //at 10ns
            #15 bi_paral2          = 4'd8 ;    //at 15ns
        join
        #20 bi_sequen2      = 4'd8 ;    //at 35ns
    end

    endmodule

    //我们可以给块语句结构命名,可以声明局部变量,通过层次名引用的方法对变量进行访问。
    module test;

    initial begin: runoob   //命名模块名字为runoob,分号不能少
        integer    i ;       //此变量可以通过test.runoob.i 被其他模块使用
        i = 0 ;
        forever begin
            #10 i = i + 10 ;
        end
    end
    
    reg stop_flag ;
    initial stop_flag = 1'b0 ;
    always begin : detect_stop
        if ( test.runoob.i == 100) begin //i累加10次,即100ns时停止仿真
            $display("Now you can stop the simulation!!!");
            stop_flag = 1'b1 ;
        end
        #10 ;
    end

    endmodule

4.5 条件语句,关键词:if,选择器

if (condition1)       true_statement1 ;
else if (condition2)        true_statement2 ;
else if (condition3)        true_statement3 ;
else                      default_statement ;
//else if 与 else 结构可以省略,else if 可以叠加多个
//4 路选择器
module mux4to1(
    input [1:0]     sel ,
    input [1:0]     p0 ,
    input [1:0]     p1 ,
    input [1:0]     p2 ,
    input [1:0]     p3 ,
    output [1:0]    sout);

    reg [1:0]     sout_t ;

    always @(*) begin
        if (sel == 2'b00)
            sout_t = p0 ;
        else if (sel == 2'b01)
            sout_t = p1 ;
        else if (sel == 2'b10)
            sout_t = p2 ;
        else
            sout_t = p3 ;
    end
    assign sout = sout_t ;
endmodule

//tb.v
module test ;
    reg [1:0]    sel ;
    wire [1:0]   sout ;

    initial begin
        sel       = 0 ;
        #10 sel   = 3 ;
        #10 sel   = 1 ;
        #10 sel   = 0 ;
        #10 sel   = 2 ;
    end

    mux4to1 u_mux4to1 (
        .sel    (sel),
        .p0     (2'b00),        //path0 are assigned to 0
        .p1     (2'b01),        //path1 are assigned to 1
        .p2     (2'b10),        //path2 are assigned to 2
        .p3     (2'b11),        //path3 are assigned to 3
        .sout   (sout));

   //finish the simulation
    always begin
        #100;
        if ($time >= 1000) $finish ;
    end
endmodule

4.6 多路分支语句

关键词:case,选择器

//4 路选择器
module mux4to1(
    input [1:0]     sel ,
    input [1:0]     p0 ,
    input [1:0]     p1 ,
    input [1:0]     p2 ,
    input [1:0]     p3 ,
    output [1:0]    sout);

    reg [1:0]     sout_t ;
    always @(*)
        case(sel)
            2'b00:   begin
                    sout_t = p0 ;
                end
            2'b01:       sout_t = p1 ;
            2'b10:       sout_t = p2 ;
            default:     sout_t = p3 ;
        endcase
    assign sout = sout_t ;

endmodule

//case 语句中的 x 或 z 的比较逻辑是不可综合的,
case(sel)
    2'b00:   sout_t = p0 ;
    2'b01:   sout_t = p1 ;
    2'b10:   sout_t = p2 ;
    2'b11:     sout_t = p3 ;
    2'bx0, 2'bx1, 2'bxz, 2'bxx, 2'b0x, 2'b1x, 2'bzx :
        sout_t = 2'bxx ;
    2'bz0, 2'bz1, 2'bzz, 2'b0z, 2'b1z :
        sout_t = 2'bzz ;
    default:  $display("Unexpected input control!!!");
endcase

//casex 用 "x" 来表示无关值,casez 用问号 "?" 来表示无关值。实现一个 4bit 控制端的 4 路选择选择器
always @(*)
        casez(sel)
            4'b???1:     sout_t = p0 ;
            4'b??1?:     sout_t = p1 ;
            4'b?1??:     sout_t = p2 ;
            4'b1???:     sout_t = p3 ;
        default:         sout_t = 2'b0 ;
    endcase

4.7 循环语句,while,for,repeat,和 forever 循环。循环语句只能在 always 或 initial 块中使用

  • while 循环中止条件为 condition 为假。

    //counter 执行了 11 次
    while (counter<=10) begin
    #10 ;
    counter = counter + 1'b1 ;
    end
    //for 循环
    for (i=0; i<=10; i=i+1) begin
    #10 ;
    counter2 = counter2 + 1'b1 ;
    end
    //repeat执行固定次数的循环
    repeat (11) begin //重复11次
    #10 ;
    counter3 = counter3 + 1'b1 ;
    end
    //

    //forever 循环,表示永久循环,$finish 可退出 forever,相当于 while(1).
    //使用 forever 语句产生一个时钟
    reg clk ;
    initial begin
    clk = 0 ;
    forever begin
    clk = ~clk ;
    #5 ;
    end
    end
    //实现一个时钟边沿控制的寄存器间数据传输功能:
    reg clk ;
    reg data_in, data_temp ;
    initial begin
    forever @(posedge clk) data_temp = data_in ;
    end

4.8 过程连续赋值,deassign,force,release

  • assign(过程赋值操作)与 deassign (取消过程赋值操作)表示第一类过程连续赋值语句。赋值对象只能是寄存器或寄存器组,而不能是 wire 型变量。

5.1 模块与端口,模块,端口,双向端口,PAD

  • 1.结构建模方式有 3 类描述语句: Gate(门级)例化语句,UDP (用户定义原语)例化语句和 module (模块) 例化语句。

    module test ; //直接分号结束
    …… //数据流或行为级描述
    endmodule

    //2.端口是模块与外界交互的接口
    module pad(
    DIN, OEN, PULL,
    DOUT, PAD); //PAD 模型的端口列表
    //一个模块如果和外部环境没有交互,则可以不用声明端口列表。
    module test ; //直接分号结束
    …… //数据流或行为级描述
    endmodule

    //端口信号在端口列表中罗列出来以后,就可以在模块实体中进行声明了。根据端口的方向,端口类型有 3 种: 输入(input),输出(output)和双向端口(inout)。input、inout 类型不能声明为 reg 数据类型,因为 reg 类型是用于保存数值的,而输入端口只能反映与其相连的外部信号的变化,不能保存这些信号的值。output 可以声明为 wire 或 reg 数据类型。
    //在 Verilog 中,端口隐式的声明为 wire 型变量,即当端口具有 wire 属性时,不用再次声明端口类型为 wire 型。但是,当端口有 reg 属性时,则 reg 声明不可省略。
    module pad(
    input DIN, OEN ,
    input [1:0] PULL ,
    inout PAD ,
    output reg DOUT
    );

5.2 模块例化

在一个模块中引用另一个模块,对其端口进行相关连接,叫做模块例化,建立了描述的层次。

  • 命名端口连接-将需要例化的模块端口与外部信号按照其名字进行连接,

    full_adder1 u_adder0(
    .Ai (a[0]),
    .Bi (b[0]),
    //某些输出端口并不需要在外部连接,例化时 可以悬空不连接,或者不写即删除。
    .Co ());
    //顺序端口连接,将需要例化的模块端口按照模块声明时端口的顺序与外部信号进行匹配连接
    //端口连接规则:输入端口input,输出端口output,输入输出端口inout,
    //input 端口正常悬空时,悬空信号的逻辑功能表现为高阻状态(逻辑值为 z)input 端口不要做悬空处理,无其他外部连接时赋值其常量,

    //位宽匹配,当例化端口与连续信号位宽不匹配时,端口会通过无符号数的右对齐或截断方式进行匹配。
    full_adder4 u_adder4(
    .a (a[1:0]), //input a[3:0]
    .b (b[5:0]), //input b[3:0]
    //端口连续信号类型,可以是,1)标识符,2)位选择,3)部分选择,4)上述类型的合并,5)用于输入端口的表达式。

  • 用 generate 进行模块例化,当例化多个相同的模块时,用 generate 语句进行多个模块的重复例化.

  • 层次访问,使用一连串的 . 符号对各个模块的标识符进行层次分隔连接

    //u_n1模块中访问u_n3模块信号:
    a = top.u_m2.u_n3.c ;

    //u_n1模块中访问top模块信号
    if (top.p == 'b0) a = 1'b1 ;

    //top模块中访问u_n4模块信号
    assign p = top.u_m2.u_n4.d ;

5.3 带参数例化,defparam,参数,例化,ram ---

没看懂

当一个模块被另一个模块引用例化时,高层模块可以对低层模块的参数值进行改写。这样就允许在编译时将不同的参数传递给多个相同名字的模块,而不用单独为只有参数不同的多个模块再新建文件。

参数覆盖有 2 种方式:1)使用关键字 defparam,2)带参数值模块例化。

  • 可以用关键字 defparam 通过模块层次调用的方法,来改写低层次模块的参数值。

    defparam u_ram_4x4.MASK = 7 ;
    ram_4x4 u_ram_4x4
    (
    .CLK (clk),
    .A (a[4-1:0]);

    //第二种方法就是例化模块时,将新的参数值写入模块例化语句
    ram #(.AW(4), .DW(4))
    u_ram
    (
    .CLK (clk),
    .A (a[AW-1:0]);

    //和模块端口实例化一样,带参数例化时,也可以不指定原有参数名字,按顺序进行参数例化,
    ram #(4, 4) u_ram (……) ;

    //利用 defparam 也可以改写模块在端口声明时声明的参数,利用带参数例化也可以改写模块实体中声明的参数
    defparam u_ram.AW = 4 ;
    defparam u_ram.DW = 4 ;
    ram u_ram(……);
    ram_4x4 #(.MASK(7)) u_ram_4x4(……);

    //

6.1 Verilog函数

函数只能在模块中定义,位置任意,并在模块的任何地方引用,作用范围也局限于此模块

  • 不含有任何延迟、时序或时序控制逻辑

  • 至少有一个输入变量

  • 只有一个返回值,且没有输出

  • 不含有非阻塞赋值语句

  • 函数可以调用其他函数,但是不能调用任务

    function [range-1:0] function_id ;

    input_declaration ;

    other_declaration ;

    procedural_statement ;

    endfunction

  • 常数函数是指在仿真开始之前,在编译期间就计算出结果为常数的函数。常数函数不允许访问全局变量或者调用系统函数,但是可以调用另一个常数函数。

    parameter MEM_DEPTH = 256 ;

  • automatic 函数,对函数进行说明,此类函数在调用时是可以自动分配新的内存空间的,也可以理解为是可递归的。

    automatic 函数中声明的局部变量不能通过层次命名进行访问,但是 automatic 函数本身可以通过层次名进行调用。

    wire [31:0] results3 = factorial(4);
    function automatic integer factorial ;
    input integer data ;
    integer i ;
    begin
    factorial = (data>=2)? data * factorial(data-1) : 1 ;
    end
    endfunction // factorial

  • 数码管译码,4 位 10 进制的数码管译码器

6.2 任务

把 input 声明的端口变量看做 wire 型,把 output 声明的端口变量看做 reg 型。但是不需要用 reg 对 output 端口再次说明。

task xor_oper_iner;
    input [N-1:0]   numa;
    input [N-1:0]   numb;
    output [N-1:0]  numco ;
    //output reg [N-1:0]  numco ; //无需再注明 reg 类型,虽然注明也可能没错
    #3  numco = numa ^ numb ;
    //assign #3 numco = numa ^ numb ; //不用assign,因为输出默认是reg
endtask

//任务在声明时,也可以在任务名后面加一个括号,将端口声明包起来。
task xor_oper_iner(
    input [N-1:0]   numa,
    input [N-1:0]   numb,
    output [N-1:0]  numco  ) ;
    #3  numco       = numa ^ numb ;
endtask

6.3 状态机

  • 有限状态机(Finite-State Machine,FSM),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型
  • Moore 型状态机的输出只与当前状态有关,与当前输入无关。输入对输出的影响要到下一个时钟周期才能反映出来。
  • Mealy 型状态机的输出,不仅与当前状态有关,还取决于当前的输入信号。在输入信号变化以后立刻发生变化

6.4 竞争与冒险,信号传输与状态变换时都会有一定的延时。

  • 在组合逻辑电路中,不同路径的输入信号变化传输到同一点门级电路时,在时间上有先有后,这种先后所形成的时间差称为竞争(Competition)。

  • 由于竞争的存在,输出信号需要经过一段时间才能达到期望状态,过渡时间内可能产生瞬间的错误输出,例如尖峰脉冲。这种现象被称为冒险(Hazard)。

  • 竞争不一定有冒险,但冒险一定会有竞争

  • 判断方法,代数法在逻辑表达式,保持一个变量固定不动,将剩余其他变量用 0 或 1 代替,如果最后逻辑表达式能化简成Y=A+A'或者Y=A.A'

    例如逻辑表达式 Y = AB + A'C,在 B=C=1 的情况下,可化简为 Y = A + A'。

  • 消除方法:增加滤波电容,滤除窄脉冲;修改逻辑,增加冗余项;使用时钟同步电路,利用触发器进行打拍延迟;采用格雷码计数器,相邻的数之间只有一个数据 bit 发生了变化,

  • 书写规范

    1)时序电路建模时,用非阻塞赋值。

    2)组合逻辑建模时,用阻塞赋值。

    3)在同一个 always 块中建立时序和组合逻辑模型时,用非阻塞赋值。

    4)在同一个 always 块中不要既使用阻塞赋值又使用非阻塞赋值。

    5)不要在多个 always 块中为同一个变量赋值。

    6)避免 latch 产生。

6.5 Latch

避免锁存器Latch,是电平触发的存储单元,数据存储的动作取决于输入时钟(或者使能)信号的电平值。仅当锁存器处于使能状态时,输出才会随着数据输入发生变化。

当电平信号无效时,输出信号随输入信号变化,就像通过了缓冲器;当电平有效时,输出信号被锁存。激励信号的任何变化,都将直接引起锁存器输出状态的改变,很有可能会因为瞬态特性不稳定而产生振荡现象。

触发器(flip-flop),是边沿敏感的存储单元,数据存储的动作(状态转换)由某一信号的上升沿或者下降沿进行同步的(限制存储单元状态转换在一个很短的时间内)。

寄存器(register),在 Verilog 中用来暂时存放参与运算的数据和运算结果的变量。

Latch 的主要危害有:

1)输入状态可能多次变化,容易产生毛刺,增加了下一级电路的不确定性;

2)在大部分 FPGA 的资源中,可能需要比触发器更多的资源去实现 Latch 结构;

3)锁存器的出现使得静态时序分析变得更加复杂。

不完整的 if - else,case 结构,如果一个信号的赋值源头有其信号本身,或者判断条件中有其信号本身的逻辑,敏感信号列表不完整,会产生 latch。

6.6 仿真激励:testbench,仿真,文件读写

  • 仿真激励文件称之为 testbench,放在各设计模块的顶层,以便对模块进行系统性的例化调用进行仿真。

  • 结构划分:信号声明,复位,时钟,激励,模块例化,自校验,停止仿真。

    1)testbench 模块声明时,一般不需要声明端口。输入端变量应该声明为 reg 型,如 clk,rstn 等,输出端变量应该声明为 wire 型,如 dout,dout_en。

    2)时钟生成

    initial clk = 0 ;//利用取反方法产生时钟时,一定要给 clk 寄存器赋初值
    always #(CYCLE_200MHz/2) clk = ~clk;

    initial begin
    clk = 0 ;
    forever begin
    #(CYCLE_200MHz/2) clk = ~clk;
    end
    end

3)复位生成, 一般赋初值为 0,再经过一段小延迟后,复位为 1 即可。

4)激励部分

(4.1) 对被测模块的输入信号进行一个初始化,防止不确定值 X 的出现。激励数据的产生,我们需要从数据文件内读入。

(4.2) 利用一个 task 去打开一个文件,只要指定文件存在,就可以得到一个不为 0 的句柄信号 fp_rd。fp_rd 指定了文件数据的起始地址。

(4.3) 的操作是为了等待复位后,系统有一个安全稳定的可测试状态。

(4.4) 开始循环读数据、给激励。在时钟下降沿送出数据,是为了被测试模块能更好的在上升沿采样数据。

(4.5) 选择在时钟上升沿延迟 2 个周期后停止输入数据,是为了被测试模块能够正常的采样到最后一个数据使能信号,并对数据进行正常的整合

6.7 流水线

硬件描述语言的一个突出优点就是指令执行的并行性。多条语句能够在相同时钟周期内并行处理多个信号数据。

把一个重复的过程分解为若干个子过程,每个子过程由专门的功能部件来实现。将多个处理过程在时间上错开,依次通过各功能段,这样每个子过程就可以与其他子过程并行进行。

A = A<<1 ;       //乘法器完成A * 2
A = (A<<1) + A ;   //对应A * 3
A = (A<<3) + (A<<2) + (A<<1) + A ; //对应A * 15

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章