咖啡图片
正在将巧克力泡入咖啡
ntainer" style="display: none">
文章

Verilog 语法

知识储备

Verilog 语法

Verilog HDL(简称 Verilog)是一种硬件描述语言,用于数字电路的系统设计和仿真,同一语言可用于生成模拟激励,以及指定测试的约束条件。Verilog 可对算法级、门级、开关级等多种抽象设计层次进行建模,使用模块实例化可描述任何层次;主要有三种建模的描述风格:行为级描述——使用过程化结构建模;数据流描述——使用连续赋值语句建模;结构化方式——使用门和模块例化语句描述。

Verilog 可用 primitive 创建自定义原语(UDP),既可以是组合逻辑,也可以是时序逻辑。Verilog 还支持其他编程语言接口(PLI)进行进一步扩展,PLI 允许外部函数访问 Verilog 模块内部信息。Verilog 的语法与 C 语言很类似,相同的语法只在此处简单罗列:区分大小写。空格没有实际意义。每个语句以分号结束。注释用双斜线。标识符的第一个字符必须是字母或者下划线。关键字是预留的用于定义语言结构的特殊标识符,Verilog 中关键字全部为小写。表达式由操作符和操作数构成,可以在出现数值的任何地方使用。操作符及其优先级顺序同 C 语言。

数值表示

Verilog 有下列四种基本的值来表示硬件电路中的电平逻辑:

  • 0:逻辑 0 或 “假”
  • 1:逻辑 1 或 “真”
  • x 或 X:未知,信号数值的不确定
  • z 或 Z:高阻,常见于信号没有驱动时的逻辑结果,其逻辑值和上下拉的状态有关系

声明数值的规范格式为位宽 + 基数 + 数值,声明数值可选择是否指明位宽,不指定时默认为十进制,建议显式指明位宽以降低调试难度。数值相同的情况下,不同基数格式完全等效。声明较长的数值建议每四位增加一个下划线以增强可读性。负数可以直接在十进制数字前加负号(如 -5),也可以用二进制补码表示(如 4'b1011 表示 -5 的 4 位补码)。由于负数在硬件中使用二进制补码表示,不显式指定位宽的情况下,编译器会自动选择默认位宽,可能在赋值或截断时产生意想不到的结果,因此建议负数表示建议显式指定位宽。

1
2
3
4
1'b0;
4'd3;
6'sd-15; 	// 负数建议显式指定位宽; s表示有符号数。
6'b11_0001; // 二进制中以补码表示,即正数部分逐位取反再加一; 二进制默认为无符号数。

声明变量时要根据变量的操作符对变量的位宽进行合理声明,不要让结果溢出,否则导致高位被截断造成数据丢失。如无符号数乘法的结果变量位宽应该声明为 2 个操作数的位宽之和。

数据类型

在 verilog 中,变量分为网线型(net)和过程型(variable)两类。net 用于表示信号的物理连接,必须由驱动源(门、模块输出、连续赋值 assign)来驱动;variable 只能在过程块中被赋值,有阻塞赋值=和非阻塞赋值<=。最常用的网线型变量和过程型变量分别是 wire 和 reg,其余类型可以理解为这两种数据类型的扩展或辅助,并且在入门阶段均不常见,此处不作介绍。

  • wire 表示硬件单元之间的物理连线,用于连接驱动源(提供值的地方)和被驱动端口(接收值的地方),抽象为电路中的导线。wire 由其连接的器件输出端连续驱动,即驱动源变化,wire 立即变化。典型的驱动源包括原语/模块的输出、连续复制语句 assign 等。没有驱动元件连接到 wire 型变量时,其缺省值为 “Z”。wire 可以在定义时赋值或定义后用 assign 赋值;但无论哪种方式,由于 wire 型变量由其它电路驱动,每个 wire 变量最多只能有一个驱动源,因此只能被赋值一次。
  • reg 表示存储单元,会保持数据原有的值直到被改写,不需要驱动源。reg 是过程变量,由过程块赋值;反过来,在 initial 或 always 这样的过程块中赋值的变量需要声明为 reg 类型。初始化的 reg 在仿真开始为 X,默认为无符号数。reg 在时钟敏感表的过程块中赋值时被综合为触发器,而在组合敏感的过程块中赋值,且所有分支对它都有输出时,被综合成组合逻辑网络(即门电路+导线)。如果组合逻辑中存在没有赋值的分支,则会综合出锁存器,这是入门阶段最容易踩的坑。

当位宽大于 1 时,变量会被声明为向量的形式,可以通过索引或指定 bit 位后固定位宽的向量域选定向量的某一位或若干相邻位进行操作,语法为[bit+: width]或[bit-: width],表示从起始 bit 位开始递增或递减 width 位。此外,Verilog 支持可变的向量域选择。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
reg [31:0] data1;
reg [7:0] byte1 [3:0]; // byte1 是一个 4 字节(数组)的存储器,每个元素都是 8 位宽的寄存器

// 指定向量域
A = data1[31:24];
A = data1[31-:8];
B = data1[0:7];
B = data1[0+:8]

// 用大括号可将信号组合成向量
wire [31:0] temp1, temp2;
assign temp1 = {byte1[0][7:0], data1[31:8]};
assign temp2 = {32{1'b0}};

//把data1[7:0]…data1[31:24]依次赋值给byte1[0][7:0]…byte[3][7:0]
always @(*) begin
    for (int j=0; j<=3; j++) begin
        byte1[j] = data1[(j+1)*8-1 : j*8];
    end
end

在 Verilog 中允许声明 reg, wire, int, time, real 及其向量类型的数组。数组维数没有限制。数组中的每个元素都可以作为一个标量或者向量,形如:<数组名>[<下标>]。多维数组需要说明其每一维的索引。数组与向量的访问方式在一定程度上类似,但两者是截然不同的数据结构。向量是一个单独的元件,位宽为 n;数组由多个元件组成,其中每个元件的位宽为 n 或 1,比如存储器变量就是用于描述 RAM 或 ROM 行为的寄存器数组。

1
2
3
4
5
6
7
8
9
10
11
integer flag[7:0];
reg [3:0] counter[3:0];
wire [7:0] addr_bus[3:0];
wire data_bit[7:0][5:0];
reg [31:0] data_4d[11:0][3:0][3:0][255:0];

flag[1] =  32'd0;
counter[3] = 4'hF;
assign addr_bus[0] = 8'b0;
assign data_bit[0][1] = 1'b0;
data_4d[0][0][0][0][15:0] = 15'd3;

整数类型用关键字 integer 或 int 声明,是有符号数,声明时位宽和编译器有关(一般为 32 bit)。实数类型用关键字 real 声明,可用十进制或科学计数法来表示,默认值为 0,不能指定位宽,因为 real 在计算机内部为双精度浮点数,位宽固定为 64 位。如果将一个实数赋值给一个整数,则只有实数的整数部分会赋值给整数。

1
2
3
4
5
6
7
8
9
10
real data1;
integer temp;
initial begin
    data1 = 2e3;
    data1 = 3.75;
end

initial begin
    temp = data; // temp = 3
end

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

1
2
3
4
5
time current_time
initial begin
    #100
    current_time = $time;
end

参数用来表示常量,用关键字 parameter 声明,只能赋值一次。通过实例化的方式,可以更改参数在模块中的值。当参数只在本模块中调用时,可用局部参数 localparam 来声明,其作用和用法与 parameter 相同,区别在于它的值不能被改变。

1
2
3
parameter data_width = 10'd32;
parameter i=1, j=2, k=3;
parameter mem_size = data_width * 10;

字符串是由双引号包起来的单字节(8bit) ASCII 字符队列,保存在 reg 类型的变量中。注意寄存器变量的宽度应该足够大以保证不会溢出,如果寄存器变量的宽度大于字符串的大小,则使用 0 来填充左边的空余位;如果寄存器变量的宽度小于字符串大小,则会截去字符串左边多余的数据。

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

赋值方式

Verilog 中有连续赋值过程赋值两类赋值方式。对于简单的组合逻辑,用连续赋值可以建模;对于复杂组合逻辑和时序逻辑,则需要使用过程赋值。连续赋值适合建模硬件导线关系,始终生效,必须驱动 wire 类型。连续赋值代表并行的硬件,总是处于激活状态,任何操作数的改变都会影响表达式的结果。过程赋值适合建模复杂逻辑关系和时序逻辑,必须出现在过程块中且必须用 reg 类型变量。这些变量在被赋值后,其值将保持不变,直到重新被赋予新值。过程赋值只有在语句执行的时候,才会起作用。

过程赋值包括两种语句:阻塞赋值与非阻塞赋值。阻塞赋值(=)属于顺序执行语句,即下一条语句执行前,当前语句一定会执行完毕。在过程块中会立即更新目标变量的值。非阻塞赋值(<=)属于并行执行语句,即下一条语句的执行和当前语句的执行是同时进行的,它不会阻塞位于同一个语句块中后面语句的执行。在时钟沿触发的过程块中,非阻塞赋值不会立即生效,在当前时钟周期结束时才进行赋值。

如下面的例子,两个 always 块中语句并行执行,赋值操作右端操作数使用的是上一个时钟周期的旧值,此时 a<=b 与 b<=a 就可以相互不干扰的执行,达到交换寄存器值的目的。

1
2
3
4
5
6
7
always @ (posedge clk) begin
	a <= b;
end

always @ (posedge clk) begin
    b <= a;
end

实际 Verilog 代码设计时,切记不要在一个过程结构中混合使用阻塞赋值与非阻塞赋值。两种赋值方式混用时,时序不容易控制,很容易得到意外的结果。更多时候,在设计电路时,always 时序逻辑块中多用非阻塞赋值,always 组合逻辑块中多用阻塞赋值;在仿真电路时,initial 块中一般多用阻塞赋值。

过程连续赋值是过程赋值的一种,一般仅用于仿真中,综合工具通常不支持。这种赋值语句能够替换其他所有 wire 或 reg 的赋值,可改写 wire 或 reg 型变量的当前值。与过程赋值不同的是,过程连续赋值的表达式能被连续的驱动到 wire 或 reg 型变量中,即过程连续赋值发生作用时,右端表达式中任意操作数的变化都会引起过程连续赋值语句的重新执行。过程连续性赋值主要有 assign-deassign 和 force-release 两种。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module diff_assign(
    input rstn,
    input clk,
    input D,
    output reg Q);

    always @ (posedge clk) begin
    	Q <= D;
    end

    always @ (negedge rstn) begin
        if (!rstn) begin
        	assign Q = 1'b0; // 过程连续赋值,Q为reg型
        end
        else begin
        	deassign Q;
        end
    end
endmodule

force (强制赋值操作)与 release(取消强制赋值)表示第二类过程连续赋值语句。使用方法和效果,和 assign 与 deassign 类似,但赋值对象可以是 reg 型变量,也可以是 wire 型变量。因为是无条件强制赋值,故一般多用于交互式调试过程,不要在设计模块中使用。当 force 作用在寄存器上时,寄存器当前值被覆盖;release 时该寄存器值将继续保留强制赋值时的值。之后,该寄存器的值可以被原有的过程赋值语句改变。当 force 作用于 wire 变量时,其值也会被强制赋值。但是,一旦 release 该线网型变量,其值马上变为原有的驱动值。

操作数和操作符

只介绍 C 语言中没有或存在差异的部分。

  • 操作数可以是任意的数据类型,只是某些特定的语法结构要求使用特定类型的操作数,比如过程块中不能为 wire 类型变量赋值。

  • 算术操作符和关系操作符中,如果操作数某一位为 x,则计算结果为 x。

  • 逻辑操作符的计算结果是一个 1bit 的值。如果一个操作数不为 0,则等价于逻辑 1;如果一个操作数等于 0,则等价于逻辑 0。如果它任意一位为 x 或 z,则等价于 x。

  • ===(全等运算符)不做类型转换,值和数据类型都相同,才返回 true。==(相等运算符)先尝试类型转换,把不同类型的值转成同一类型后再比较值是否相等,推荐使用更安全的===来减少 bug 风险。全等比较时,如果按位比较有相同的 x 或 z,返回结果也可以为 1;因此全等比较的结果一定不包含 x。

  • 按位操作符对 2 个操作数的每 1bit 数据进行按位操作。如果 2 个操作数位宽不相等,则用 0 向左扩展补充较短的操作数。

  • 归约操作符只有一个操作数,作用是对单个向量操作数进行逐位操作,最终得到 1 位结果 ,常用来判断向量中某些位的整体特征(如是否全 1、是否有 1 等)。

  • 部分逻辑操作符、按位操作符和归约操作符使用相同的符号表示,由作用的操作数数量和计算结果(即上下文)加以区分。

  • 拼接操作符用大括号 {A, B} 来表示,用于将多个操作数拼接成新的操作数,信号间用逗号隔开。拼接符操作数必须指定位宽,常数也需要指定位宽。

  • “左移” 一般默认指逻辑左移,逻辑左移和算术左移的区别在于对有符号数符号位的处理。逻辑左移将二进制数当无符号数处理,所有位包括最高位均左移,右侧补 0;算术左移将二进制数当有符号数处理,保留符号位不变仅对数值位左移,右侧补 0。逻辑右移和算术右移类似,逻辑右移时左边高位补 0;算术右移时左边高位补符号位,以保证数据缩小后值的正确性。

  • 条件操作符可以嵌套以实现多次选择的逻辑

    1
    2
    3
    4
    
    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 ;
    

编译指令

以反引号开始的某些标识符是 Verilog 系统编译指令,编译指令的用法和 C 语言基本类似,包含条件编译指令、宏定义和取消、文件包含、时间单位指定等。

在 Verilog 模型中,时延有具体的单位时间表述,用 `timescale 定义时延及仿真的单位和精度,将时间单位与实际时间相关联。其中 time_precision 的大小需要小于等于 time_unit 的大小。

1
`timescale      time_unit / time_precision

由于 Verilog 中没有默认的 `timescale,如果不显式指定,模块就有可能继承前面编译模块参数并导致设计出错。一个设计中的多个模块都带有 `timescale 时,则 time_precision 确定为所有模块中的最小值,其它 time_precision 都相应地换算为这个精度,time_unit 不受影响。如果有并行子模块,子模块间的 `timescale 并不会相互影响。time_precision 越小,仿真时占用内存越多,实际使用的仿真时间就越长,所以在满足需要的前提下应尽量将 time_precision 设置得大一些。

过程块

过程块是行为级建模的基本语句,包括initial ... endalways @always @*三种,在特定条件触发时,执行一次块中的语句。一个模块中可以包含多个 initial 和 always 语句,由于过程块本身代表独立的过程,一个过程块产生一个独立的控制流,执行时间均从 0 时刻开始,因此 initial 和 always 均不支持嵌套。各个过程块在模块间并行执行,与其在模块的前后顺序没有关系,类似于多个线程并行的概念;但过程块内部是顺序执行的(非阻塞赋值除外)。另外,也正是由于块内是顺序执行的,过程块中不能为 wire 类型变量赋值。

initial 语句从 0 时刻开始执行,只执行一次,多个 initial 块之间是相互独立的。如果 initial 块内包含多个语句,需要使用关键字 begin 和 end 组成一个块语句。如果 initial 块内只要一条语句,关键字 begin 和 end 可使用也可不使用。initial 理论上来讲是不可综合的,多用于初始化、信号检测等。

与 initial 语句相反,always 语句是重复执行的。always 语句块从 0 时刻开始执行其中的行为语句;当执行完最后一条语句后,便再次执行语句块中的第一条语句,如此循环反复。由于循环执行的特点,always 语句多用于仿真时钟的产生,信号行为的检测等。

敏感表是过程块中括号部分的内容,是一个描述触发条件的列表,其作用是明确哪些信号发生变化时,需要触发执行这个过程块。敏感表分为组合敏感表和时钟敏感表两类,分别用于描述组合逻辑和时序逻辑。写错敏感表可能导致仿真和综合结果不一致。另外,连续赋值由硬件电路自动驱动,不需要敏感表。一个或逻辑的敏感表如下

1
2
3
4
5
6
7
8
always @ (posedge clk or negedge rstn) begin
    if (!rstn) begin
    	q <= 1'b;
    end
    else begin
    	q <= d;
    end
end

当组合逻辑输入变量很多时,编写敏感表会很繁琐,此时更简洁的写法是 @_ 或 @(_),表示对语句块中所有输入变量的变化都是敏感的。

1
2
3
always @ (*) begin
	assign s = a ? b + c : d ? e + f
end

语句块

在过程块中,可以使用顺序块并行块两种类型的语句块。顺序块用关键字 begin 和 end 来表示。顺序块中的语句是一条条执行的,当然非阻塞赋值除外。顺序块中每条语句的时延总是与其前面语句执行的时间相关。并行块用关键字 fork 和 join 来表示。并行块中的语句是并行执行的,即便是阻塞形式的赋值。并行块中每条语句的时延都与块语句开始执行的时间相关。顺序块和并行块可以嵌套使用。

可以给语句块命名,命名块中可以声明局部变量,并可以通过层次名引用的方法访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
`timescale 1ns/1ns

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

命名块也用关键字 disable 来禁用,disable 可以终止命名块的执行,用于从循环中退出、处理错误等,与 C 语言中的 break 类似,但是 break 只能退出当前所在循环,而 disable 可以禁用设计中任何一个命名的块。disable 可退出当前的 while 块。需要说明的是,disable 在 always 或 forever 块中使用时只能退出当前回合,下一次语句还是会在 always 或 forever 中执行。因为 always 块和 forever 块是一直执行的,此时的 disable 有点类似 C 语言中的 continue 功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
`timescale 1ns/1ns

module test;

    initial begin: runoob_d //命名模块名字为runoob_d
        integer    i_d ;
        i_d = 0 ;
        while(i_d<=100) begin: runoob_d2
            # 10 ;
            if (i_d >= 50) begin       //累加5次停止累加
                disable runoob_d3.clk_gen ;//stop 外部block: clk_gen
                disable runoob_d2 ;       //stop 当前block: runoob_d2
            end
            i_d = i_d + 10 ;
        end
    end

    reg clk ;
    initial begin: runoob_d3
        while (1) begin: clk_gen  //时钟产生模块
            clk=1 ;      #10 ;
            clk=0 ;      #10 ;
        end
    end

endmodule

时序控制

这部分的结构层次比较多,先列出总览的思维导图

Verilog 提供了两大类时序控制方法:时延控制事件控制

时延是连续赋值语句中的延时,用于控制任意操作数发生变化到语句左端赋予新值之间的时间延时。连续赋值时延一般可分为普通赋值时延、隐式时延声明时延寄存器时延也是可以控制的,这部分在时序控制里加以说明。时延的值可以是数字、标识符或者表达式。时延一般是不可综合的。下面 3 个例子实现的功能是等效的,分别对应 3 种不同连续赋值时延的写法。

1
2
3
4
5
6
7
8
9
10
11
12
//普通时延,A&B计算结果延时10个时间单位赋值给Z。
wire Z, A, B;
assign #10 Z = A & B;

//隐式时延,声明一个wire型变量时对其进行包含一定时延的连续赋值。
wire A, B;
wire #10 Z = A & B;

//声明时延,声明一个wire型变量时指定一个时延。因此对该变量所有的连续赋值都会被推迟到指定的时间。除非门级建模中,一般不推荐使用此类方法建模。
wire A, B;
wire #10 Z;
assign Z = A & B;

在上述例子中,A 或 B 任意一个变量发生变化,那么在 Z 得到新的值之前,会有 10 个时间单位的时延。如果在这 10 个时间单位内,即在 Z 获取新的值之前,A 或 B 任意一个值又发生了变化,那么计算 Z 的新值时会取 A 或 B 当前的新值。所以称之为惯性时延,即信号脉冲宽度小于时延时,对输出没有影响。因此仿真时,时延一定要合理设置,防止某些信号不能进行有效的延迟。

时延控制出现在表达式中,它指定了语句从开始执行到执行完毕之间的时间间隔。根据在表达式中的位置差异,时延控制又可以分为常规时延内嵌时延。遇到常规延时时,该语句需要等待一定时间,然后将计算结果赋值给目标信号。遇到内嵌延时时,该语句先将计算结果保存,然后等待一定的时间后赋值给目标信号。内嵌时延控制加在赋值号之后。

1
2
3
4
5
6
// 常规时延
reg value_test, value_general;
# 10 value_general = value_test;
// 内嵌时延
reg value_test, value_embed;
value_embed = # 10 value_test;

当延时语句的赋值符号右端是常量时,两种时延控制都能达到相同的延时赋值效果。当延时语句的赋值符号右端是变量时,两种时延控制可能会产生不同的延时赋值效果。常规时延赋值方式:遇到延迟语句后先延迟一定的时间,然后将当前操作数赋值给目标信号,并没有”惯性延迟”的特点,不会漏掉相对较窄的脉冲。内嵌时延赋值方式:遇到延迟语句后,先计算出表达式右端的结果,然后再延迟一定的时间,赋值给目标信号。

在 Verilog 中,事件是指某一个 reg 或 wire 型变量发生了值的变化。事件控制主要分为边沿触发事件控制电平敏感事件控制。边沿触发事件控制又分一般事件控制命名事件控制。事件控制用@表示,语句执行的条件是信号的值发生特定的变化。关键字 posedge 指信号发生边沿正向跳变,negedge 指信号发生负向边沿跳变,未指明跳变方向时,则两种情况的边沿变化都会触发相关事件。例如:

1
2
3
4
5
always @ (clk) q <= d; // 信号clk只要发生变化,就执行q<=d,双边沿D触发器模型
always @ (posedge clk) q <= d; //在信号clk上升沿时刻,执行q<=d,正边沿D触发器模型
always @ (negedge clk) q <= d; //在信号clk下降沿时刻,执行q<=d,负边沿D触发器模型

q = @ (posedge clk) d; //立刻计算d的值,并在clk上升沿时刻赋值给q,不推荐这种写法

不推荐最后一种写法的原因是,=是阻塞赋值,不符合常规的过程赋值语法规范。阻塞赋值通常用于组合逻辑或明确的 “立即执行” 场景。事件控制更常见于 always 块的敏感列表或 always 块内的时序控制。若在 initial 块或普通 always 块(非时序逻辑专用的 always 块)中使用,容易打破 “组合逻辑并行、时序逻辑同步触发” 的设计习惯;若在复杂场景(如多个信号交互、多时钟域)中使用,会导致仿真行为与硬件电路的实际并行执行逻辑脱节。Verilog 中,时序逻辑(如 D 触发器)的标准写法是用 always 块 + 非阻塞赋值(<=)。

命名事件控制是指,用户可以声明 event(事件)类型的变量,并触发该变量来识别该事件是否发生。命名事件用关键字 event 来声明,触发事件用 -> 运算符。

1
2
3
4
5
6
7
8
event start_receiving;
always @ (posedge clk_sample) begin
    -> start_receiving;
end

always @ (start_receiving) begin
    data_buf = {data_if[0], data_if[1]};
end

电平敏感事件控制是指后面语句的执行需要等待某个条件为真,使用关键字 wait 来表示这种电平敏感情况。

1
2
3
4
5
6
7
initial begin
    wait (start_enable);
    forever begin
        @ (posedge clk_sample);
    	data_buf = {data_if[0], data_if[1]};
	end
end

控制流语句

控制流语句的使用和 C 语言基本相同,此处仅列出各个控制流语句的例子和注意事项。对于任何控制流语句,一般建议使用 begin 与 end 关键字避免歧义,编译器一般按就近原则编译,不加 begin 与 end 关键字可能导致不安全的行为。

条件语句

1
2
3
4
5
6
7
8
9
10
11
12
if (condition1) begin
    true_statement1;
end
else if (condition2) begin
    true_statement2;
end
else if (condition3) begin
    true_statement3;
end
else begin
    default_statement;
end

多路分支语句:条件选项是并发比较的,执行效果为谁在前且条件为真谁被执行,因此条件选项不要求互斥。default 语句可选,且在一个 case 语句中不能有多个 default 语句。多个条件选项下需要执行相同的语句时,条件选项之间用逗号分开,放在同一个语句块的候选项中。case 语句中的条件选项表单式不必都是常量,也可以是 x 值或 z 值,但 case 语句中的 x 或 z 的比较逻辑是不可综合的,所以一般不建议在 case 语句中使用 x 或 z 作为比较值。casex、 casez 语句是 case 语句的变形,用来表示条件选项中的无关项。casex 用 “x” 来表示无关值,casez 用问号 “?” 来表示无关值。两者功能是完全一致的,语法与 case 语句也完全一致。但是 casex、casez 一般是不可综合的,多用于仿真。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// example 1
case (case_expr)
    condition1: begin
        true_statement1;
    end

    condition2: begin
        true_statement2;
    end

    default: begin
        default_statement;
    end
endcase

// example 2
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

循环语句:有 while,for,repeat,和 forever 循环四种类型,只能在 always 或 initial 块中使用,可以包含延迟表达式。while 和 for 的用法同 C 语言。repeat 的功能是执行固定次数的循环,不能像 while 循环那样用一个逻辑表达式来确定循环是否继续执行。repeat 循环的次数必须是一个常量、变量或信号。如果循环次数是变量信号,则循环次数是开始执行 repeat 循环时变量信号的值。即便执行期间,循环次数代表的变量信号值发生了变化,repeat 执行次数也不会改变。forever 语句表示永久循环,不包含任何条件表达式,一旦执行便无限的执行下去,系统函数 $finish 可退出 forever。forever 相当于 while(1) 。通常,forever 循环是和时序控制结构配合使用的。另外,verilog 中没有自增自减运算符,因此没有 i++或 i–的写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
while (condition) begin
    
end

for(initial_assignment; condition ; step_assignment)  begin
    
end

repeat (loop_times) begin
    
end

forever begin
    
end

模块和端口

结构建模方式有三类描述语句: Gate(门级)例化语句,UDP (用户定义原语)例化语句和 module (模块) 例化语句,此处介绍最常用的 module,module 定义的语法格式如下

1
2
3
4
5
module module_name
    #(parameter_list)
    (port_list);
    // Declarations and Statements;
endmodule

模块内部由可选的五部分组成,分别是变量声明,数据流语句,行为级语句,低层模块例化及任务和函数,这五部分的出现顺序、出现位置都是任意的,但各种变量均需在使用之前声明。变量具体声明的位置不要求,但必须保证在使用之前的位置。

端口是模块与外界交互的接口。对于外部环境来说,模块内部是不可见的,对模块的调用通过端口连接进行。根据端口信号的方向,端口类型有 input、output 和 inout 三种。input 和 inout 由外部信号驱动,需要实时反映外部信号变化,不能声明为只在过程赋值时才更新的 reg 类型;output 则可以声明为 wire 或 reg 数据类型。另外,端口类型和端口数据类型可以同时声明。

模块的定义中包含一个可选的端口列表,端口列表中可以仅写信号名,按默认规则(wire、1bit)处理,后续在变量声明中进行说明;也可以在完整描述端口类型和端口数据类型。另外,一个模块如果和外部环境没有交互,则可以不用声明端口列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module pad(DIN, OEN, PULL, DOUT, PAD);
    input wire DIN, OEN;
    input wire [1:0] PULL;
    inout wire PAD;
    output reg DOUT;
    // Statements
endmodule

module pad(
	input wire DIN, OEN,
    input [1:0] wire PULL,
	input wire PAD,
    output reg DOUT);
    // Statements
endmodule

模块例化

在一个模块中引用另一个模块并对其端口进行连接,叫做模块例化。模块例化建立了描述的层次。信号端口可以通过位置或名称关联,端口连接也必须遵循一些规则。

顺序端口连接:将需要例化的模块端口按照模块声明时端口的顺序与外部信号进行匹配连接,位置需要保持一致。虽然代码看起来更简洁了,但可读性会降低,且增大了调试难度。在端口较多且需要改动端口顺序时,顺序端口连接的方式是不方便的,建议采用下面的命名端口连接对模块进行例化。

1
full_adder1  u_adder1(a[1], b[1], co_temp[0], so_bit1, co_temp[1]);

命名端口连接:将需要例化的模块端口与外部信号按照其名字进行连接,端口顺序随意,只要端口名字与外部信号匹配即可。如果某些端口不需要在外部连接,例化时可以悬空不连接甚至删除。一般来说,input 端口在例化时不能删除,output 端口在例化时可以删除。

1
2
3
4
5
6
full_adder u_adder0(
    .Ai(a[0]),
    .Bi(b[0]),
    .Ci(c==1'b1 ? 1'b0 : 1'b1),
    .So(so_bit0),
    .Co(co_temp[0]));

端口连接规则:

  • 输入端口:从模块外部来讲, input 端口可以连接 wire 或 reg 型变量,但模块声明时必须是 wire 型变量。

  • 输出端口:从模块外部来讲,output 端口必须连接 wire 型变量,但模块声明时可以是 wire 或 reg 型变量。

  • 输入输出端口:从模块外部来讲,inout 端口必须连接 wire 型变量,模块声明时同样。

  • 悬空端口:不需要与外部信号进行连接交互的信号可以悬空,即端口例化时不写即可。output 端口悬空时甚至可以删除;input 端口悬空时,悬空信号的逻辑功能表现为高阻状态(逻辑值为 z),但不能删除。一般来说,建议 input 端口不要悬空,无其他外部连接时赋值为常量即可。

  • 例化端口与连续信号位宽不匹配时,端口会通过无符号数的右对齐或截断方式进行匹配。

generate 例化:当例化多个相同的模块时,一个一个的手动例化会比较繁琐。用 generate 语句进行多个模块的重复例化,可大大简化程序的编写过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
module full_adder4(
    input [3:0]   a ,   // adder1
    input [3:0]   b ,   // adder2
    input         c ,   // input carry bit

    output [3:0]  so ,  // adding result
    output        co    // output carry bit
    );

    wire [3:0]    co_temp ;
    // 此处第一个例化模块格式有所差异,需要单独例化
    full_adder1  u_adder0(
        .Ai     (a[0]),
        .Bi     (b[0]),
        .Ci     (c==1'b1 ? 1'b1 : 1'b0),
        .So     (so[0]),
        .Co     (co_temp[0]));

    genvar        i ;
    generate
        for(i=1; i<=3; i=i+1) begin: adder_gen
        full_adder1  u_adder(
            .Ai     (a[i]),
            .Bi     (b[i]),
            .Ci     (co_temp[i-1]), // 上一个全加器的溢位是下一个的进位
            .So     (so[i]),
            .Co     (co_temp[i]));
        end
    endgenerate

    assign co    = co_temp[3] ;

endmodule

层次访问:每一个例化模块的名字,每个模块的信号变量等,都使用一个特定的标识符进行定义。在整个层次设计中,每个标识符都具有唯一的位置与名字。Verilog 中,通过使用一连串的 . 符号对各个模块的标识符进行层次分隔连接,就可以在任何地方通过指定完整的层次名对整个设计中的标识符进行访问。层次访问多见于仿真中。

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

第一种是用关键字 defparam 通过模块层次调用的方法,来改写低层次模块的参数值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// module defination
module  ram_4x4
    (
     input               CLK ,
     input [4-1:0]       A ,
     input [4-1:0]       D ,
     input               EN ,
     input               WR ,    //1 for write and 0 for read
     output reg [4-1:0]  Q    );

    parameter        MASK = 3 ;

    reg [4-1:0]     mem [0:(1<<4)-1] ;
    always @(posedge CLK) begin
        if (EN && WR) begin
            mem[A]  <= D & MASK;
        end
        else if (EN && !WR) begin
            Q       <= mem[A] & MASK;
        end
    end
endmodule

// instantiation
defparam     u_ram_4x4.MASK = 7 ;
ram_4x4    u_ram_4x4
    (
        .CLK    (clk),
        .A      (a[4-1:0]),
        .D      (d),
        .EN     (en),
        .WR     (wr),    //1 for write and 0 for read
        .Q      (q)    );

第二种是例化模块时,将新的参数值写入模块例化语句,以此来改写原有 module 的参数值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// module defination
module  ram
    #(  parameter       AW = 2 ,
        parameter       DW = 3 )
    (
        input                   CLK ,
        input [AW-1:0]          A ,
        input [DW-1:0]          D ,
        input                   EN ,
        input                   WR ,    //1 for write and 0 for read
        output reg [DW-1:0]     Q
     );

    reg [DW-1:0]         mem [0:(1<<AW)-1] ;
    always @(posedge CLK) begin
        if (EN && WR) begin
            mem[A]  <= D ;
        end
        else if (EN && !WR) begin
            Q       <= mem[A] ;
        end
    end

endmodule

// instantiation
ram #(.AW(4), .DW(4))
    u_ram
    (
        .CLK    (clk),
        .A      (a[AW-1:0]),
        .D      (d),
        .EN     (en),
        .WR     (wr),    //1 for write and 0 for read
        .Q      (q)
     );

带参数例化时,和模块端口实例化一样,也有顺序端口连接和命名端口连接两种方式,不过仍建议使用命名端口连接的方式。利用 defparam 和带参数例化均可以改写模块在端口声明时声明的参数,不过建议不要采用 defparam 在对已有模块进行例化时改写相关参数。建议模块在编写时,如果预先知道将被例化的模块中有需要改写的参数,将这些参数都写入到模块端口声明之前的地方(用关键字井号 # 表示),提高可读性且方便调试。

函数和任务

Verilog 中用函数和任务的机制来提取封装重复的行为级设计,避免重复代码的多次编写。

函数只能在模块中定义,位置任意,可在模块的任何地方引用,作用范围也局限于此模块。函数有以下特点:不含有任何延迟、时序或时序控制逻辑,也不含有非阻塞赋值;至少有一个输入变量且输入端口声明不能包含 inout,有且只有一个返回值(函数名本身对应的值),没有输出;函数可以调用其它函数,但不能调用任务;函数总在零时刻就开始执行;函数不能单独作为一条语句出现,而只能作为赋值语句的右值。下面是一个用函数实现数据大小端转化的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
module endian_rvs
    #(parameter N = 4)
        (
            input             en,     //enable control
            input [N-1:0]     a ,
            output [N-1:0]    b
    );

        reg [N-1:0]          b_temp ;
        always @(*) begin
        if (en) begin
                b_temp =  data_rvs(a);
            end
            else begin
                b_temp = 0 ;
            end
    end
        assign b = b_temp ;

    // function entity
    function [N-1:0] data_rvs (
        input [N-1:0] data_in
    );
        parameter MASK = 32'h3;
        integer k;
        begin
            for(k=0; k<N; k=k+1) begin
                data_rvs[N-k-1] = data_in[k];
            end
        end
    endfunction
endmodule

常数函数是指在仿真开始之前,在编译期间就计算出结果为常数的函数。常数函数不允许访问全局变量或者调用系统函数,但是可以调用另一个常数函数。这种函数能够用来引用复杂的值,因此可用来代替常量。

1
2
3
4
5
6
7
8
9
10
parameter    MEM_DEPTH = 256 ;
reg  [logb2(MEM_DEPTH)-1: 0] addr ; //可得addr的宽度为8bit

    function integer     logb2;
    input integer     depth ;
        //256为9bit,我们最终数据应该是8,所以需depth=2时提前停止循环
    for(logb2=0; depth>1; logb2=logb2+1) begin
        depth = depth >> 1 ;
    end
endfunction

一般函数的局部变量是静态的,即每次调用时,函数的局部变量都使用同一个存储空间。若某个函数在两个不同的地方同时并发的调用,那么两个函数调用行为同时对同一块地址进行操作,会导致不确定的函数结果。使用关键字 automatic 修饰函数,调用时可以自动分配新内存空间。automatic 函数中声明的局部变量不能通过层次命名进行访问,但是 automatic 函数本身可以通过层次名进行调用。

1
2
3
4
5
6
7
8
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

任务也用于描述共同的代码段,在模块内任意位置定义和调用,作用范围局限于此模块。函数一般用于组合逻辑的各种转换和计算,而任务则是一个过程,不仅可实现函数的功能,还可以包含时序控制逻辑。任务有以下特点:不能出现 always 语句,但可以包含其它时序控制;可以没有或者有多个输入,输入端口声明可以包含 inout,没有返回值,可以没有或有多个输出;任务可以调用函数和任务;任务可以在非零时刻开始执行;任务可以作为一条单独的语句出现在语句块中。任务调用时,端口必须按顺序对应。

进行任务的逻辑设计时,可以把 input 声明的端口变量看做 wire 型,把 output 声明的端口变量看做 reg 型,但无需用 reg 对 output 端口再次说明(当然也可以注明)。对 output 信号赋值时不要用关键字 assign 而建议采用阻塞赋值来避免时序错乱。task 最多的应用场景是用于 testbench 中进行仿真。另外,task 在一些编译器中不支持综合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
`timescale 1ns/1ns

module test ;
    reg          clk, rstn ;

    initial begin
        rstn      = 0 ;
        #8 rstn   = 1 ;
        forever begin
            clk = 0 ; # 5;
            clk = 1 ; # 5;
        end
    end

    reg  [3:0]   a, b;
    wire [3:0]   co ;
    initial begin
        a         = 0 ;
        b         = 0 ;
        sig_input(4'b1111, 4'b1001, a, b);
        sig_input(4'b0110, 4'b1001, a, b);
        sig_input(4'b1000, 4'b1001, a, b);
    end

    task sig_input ;
        input [3:0]       a ;
        input [3:0]       b ;
        output [3:0]      ao ;
        output [3:0]      bo ;
        @(posedge clk) ;
        ao = a ;
        bo = b ;
    endtask ; // sig_input

    xor_oper         u_xor_oper
    (
      .clk              (clk  ),
      .rstn             (rstn ),
      .a                (a    ),
      .b                (b    ),
      .co               (co   ));

    initial begin
        forever begin
            #100;
            if ($time >= 1000)  $finish ;
        end
    end

endmodule // test

任务可以看作过程型赋值,任务的 output 端信号在任务中所有语句执行完毕后才会返回。任务的内部变量只在任务中可见,如果需要观察任务对变量的操作过程,则需要将变量声明在任务之外模块之内,即全局变量。

与函数类似,任务调用时的局部变量都是静态的,可以用 automatic 关键字修饰,任务调用时各存储空间可以动态分配,各个调用的任务各自独立地对自己的地址空间进行操作,可以并发执行多个相同的任务。否则并行的任务对同一个地址空间操作,则信号之间会出现干扰,出现不确定的结果。

参考资料

Verilog 语法

wire 和 reg 区别

几种赋值语句

本文由作者按照 CC BY 4.0 进行授权
/body>