SystemVerilog

一、基础语法

1.1 timescale

设置时间单位和精度。

`timescale time_unit / time_precision
`timescale 10ns / 1ns

如上面第二行的设置,#1表示10ns,#1.55表示16ns。

1.2 常用系统函数

  • $display()$write():区别是 display 会换行。语法和 printf 类似。%0d0表示左对齐。

二、数据类型与对象

2.1 数据类型

2.1.1 整数类型(integral)

整数类型 state 位宽 默认符号 说明
byte 2 8 signed 相当于char
shortint 2 16 signed 相当于short
int 2 32 signed 相当于int
longint 2 64 signed 相当于long long
bit 2 1 unsigned 2值1bit数据
logic 4 1 unsigned 4值1bit数据
reg 4 1 unsigned 同logic,避免使用
integer 4 32 signed 很少用
time 4 64 unsigned 很少用

整数类型分为2-state类型和4-state类型:

  • 2-state类型:只有01两种取值(效率较高),且初始值为0。比如仿真时用bit定义clk比较方便。若需要定义为无符号数或有符号数可以加unsignedsigned

    bit					reset;
    bit [31:0] 			data;
    bit signed [31:0] 	value;
    byte 				array[8];	// [8]相当于[0:7]

  • 4-state类型:取值可以为01x(未知)、z(高阻)。

    logic类型不能有多个驱动源,线网默认是logic类型的。

    logic				reset;
    logic [31:0] 		data;

2.1.2 实数类型

  • real:相当于double。
  • shortreal:相当于float。
  • realtime:同real。

实数赋值给整数会四舍五入。

2.1.3 字符串类型(string)

string是由byte构成的字符串,相当于char*

string str1 = "system";
string str2 = "Verilog";

initial begin
    $display(str1);
    str1[0] = "S";					// 修改
    $display(str1);
    $display(str1 == str2);			// 比较
    $display(str2.len());			// 调用函数
    $display("%s", {str1, str2});	// 拼接
end

// system
// System
// 0
// 7
// SystemVerilog

2.1.4 枚举类型(enum)

未指定类型默认为int。枚举项的值默认从0开始,可以指定,如果指定第一个则后面的依次递增。

enum {GREEN, YELLOW, RED} light1, light2;
enum logic [1:0] {GOLD=2'd1, SILVER=2'd2, BRONZE=2'd3} medal = BRONZE;

typedef enum {S0, S1, S2} state_t;
typedef enum {S[3]} state_t;	// 相当于{S0, S1, S2}
typedef enum {S[1:3]} state_t;	// 相当于{S1, S2, S3}
state_t s = S1;

2.2 数据对象

2.2.1 线网(net)

线网的值会根据驱动源的变化而按照指定的线网延迟而变化。线网必须是4-state类型的。

线网类型 说明
wire 最常用,普通的连线
wandwor 线与、线或
其他 几乎用不到
wire        n1;
wire logic  n2;		// 默认就是logic,加不加都行
wire [15:0] n3;
wire #5     n4;		// 线网延迟

// 线与,多个驱动源会先连接到生成的与门再连接到n5
wand        n5;
assign n5 = n1 & n2;
assign n5 = ~n4;

2.2.2 变量

变量的值只有在赋值命令执行时才变化。只要不是线网类型就默认为变量。

var         v1;		// 默认是logic
var logic   v2;		// 最完整,但有些多余
bit         clk;
logic       reset;

2.2.3 常量

  • parameter常量

    elaboration-time(编译时)常量。必须在定义时指定值。

    • parameter:可以用于模块间参数传递。
    • localparam:模块内使用。

    parameter  WIDTH   = 4'd10;		// 位宽4
    parameter  WIDTH32 = 12;		// 位宽32
    localparam left    = 100;

    const常量

    run-time(运行时)常量。运行时才给定值。如代码中直接使用的4'b0101等。

2.3 初始值设置

  • 位值扩展:'0'1'x'z可以将所有位设置为这些值。

    logic [7:0] a = '1;							// 8'hff

  • 可以指定某一位的值和其余默认值。

    logic [7:0] b = '{default: 1};				// 8'hff
    logic [7:0] c = '{7: 0, 3: 0, default: 1};	// 8'h77

2.4 数组

2.4.1 紧凑数组(packed)

名称前面具有紧凑维度的数组。声明为紧凑数组可以保证矢量的位在连续的区域中存储,可以进行分段操作

logic [3:0][7:0] arr1;

initial begin
    arr1 = '1;
    arr1[1] = '0;
    arr1[0][1] = '0;
    $display("%h", arr1);	// ffff00fd
    arr1[2:1] = 16'habcd;
    arr1[0][7:4] = 4'he;
    $display("%h", arr1);	// ffabcded
end

2.4.2 非紧凑数组(unpacked)

名称后面具有非紧凑维度的数组。它的存储区域是不连续的。非紧凑数组只需指定大小,如下面的声明是等价的。

logic matrix [0:7][0:127];
logic matrix [8][128];

不能进行分段操作。

logic [7:0] arr2 [3:0];

initial begin
    // arr2 = '1;				// error
    arr2[1] = '0;
    arr2[0][1] = '0;
    // arr2[2:1] = 16'habcd;	// error
    arr2[0][7:4] = 4'he;
end

2.4.3 动态数组

仅适用于非紧凑数组

int         array1[];								// 声明
logic [3:0] array2[] = '{4'h1, 4'h2, 4'h4, 4'h8};	// 声明并初始化

initial begin
    array1 = new[5];			// 初始化
    $display(array1);			// '{0,0,0,0,0}
    $display(array2);			// '{1,2,4,8}
    $display(array2.size());	// 返回数组大小 4
    array1.delete();			// 清空数组
    $display(array1);			// '{}
end

2.4.4 关联数组

类似python的字典。常用于字符串作为索引。

int data[string] = '{
    "a": 100,
    "b": 200
};

initial begin
    $display(data);			// '{"a":100,"b":200}
    $display(data["a"]);	// 100
    data["c"] = 300;		// 添加元素
    data["a"] += 50;		// 操作元素
    $display(data);			// '{"a":150,"b":200,"c":300}
end

2.4.5 队列

int     q1[$];							// 声明空队列
string  q2[$] = {"a", "b", "c", "d"};	// 声明并赋初值,注意{}前没有'
int t;

initial begin
    $display(q2.size());	// 返回大小 4
    $display(q2);			// '{"a","b","c","d"}
    $display(q2[1:2]);		// 指定区间 '{"b","c"}
    q1.push_back(10);		// 在队列最后面插入一个元素
    q1.push_back(20);
    q1.push_front(5);		// 在队列最前面插入一个元素
    $display(q1);			// '{5,10,20}
    t = q1.pop_back();		// 删除并返回最后面的元素
    $display(t);			// 20
    t = q1.pop_front();		// 删除并返回最前面的元素
    $display(t);			// 5
    $display(q1);			// '{10}
end

三、过程与进程

3.1 仿真过程

从仿真器中启动执行的电路仿真指令块,也叫过程语句。

3.1.1 initial 过程

仅在仿真开始时执行一次。

initial begin
    reset = 0;
end

3.1.2 always 过程

仿真开始时启动并重复执行。

敏感事件列表:电路依赖的所有信号必须被列举在敏感事件列表中。

  • 通用 always 过程

    always #5 clk = ~clk;
    always @(a) $display(a);
    always @(posedge clk) q <= b;

  • always_comb 过程

    用于描述组合逻辑电路。会自动生成敏感事件列表。

    always_comb begin
        {c, s} = a + b;
    end

    always @(*)也有类似的功能。有如下区别:

    • always_comb会考虑子例程(如函数)中使用的信号,always @(*)不会;
    • always_comb中左值不能在其他过程块中设定,always @(*)可以。
  • always_latch 过程

    用于描述锁存电路。

    always_latch begin
        if (sel)
            s = a;
    end

  • always_ff 过程

    用于描述可综合的时序逻辑电路。需要有事件控制(用posedgenegedge设置控制信号);不能使用阻塞赋值;左值不能在其他过程中被赋值。

    always_ff @(posedge clk) begin
        s <= a;
    end

3.1.3 final 过程

仿真结束时执行。不能描述需要电路耗时执行的指令,一般用来显示仿真结果。

final begin
    $display(s);
end

3.2 语句块

比如if-else语句,语法上要求后面只能用一条语句,这时就可以使用语句块将多条语句打包为一条语句。

可以在beginfork后面加上语句块的名称,便于引用。

begin: name
	......
end

3.2.1 顺序语句块(begin-end)

begin-end语句块中的语句从上到下依次执行

3.2.2 并行语句块(fork-join)

fork-join语句块中的语句是并行执行的。

  • fork-join

    join会阻塞父进程,直到全部子进程结束。

    function void print(input int value);
        $display("@%0t value=%0d", $time, value);
    endfunction
    
    initial begin
        fork
            #10 print(1);
            #20 print(2);
            #30 print(3);
        join
        $display("@%0t main completed", $time);
    end
    
    // @10000 value=1
    // @20000 value=2
    // @30000 value=3
    // @30000 main completed

  • fork-join_any

    join_any会阻塞父进程,直到某一个子进程结束。

    initial begin
        fork
            #10 print(1);
            #20 print(2);
            #30 print(3);
        join_any
        $display("@%0t main completed", $time);
    end
    
    // @10000 value=1
    // @10000 main completed
    // @20000 value=2
    // @30000 value=3

  • fork-join_none

    join_none仅生成子进程(子进程不会立刻开始执行),父进程继续执行,直到父进程被阻塞时(如父进程使用#0)才执行子进程。

    initial begin
        fork
            print(1);
            #10 print(2);
            #20 print(3);
        join_none
        $display("@%0t scheduled", $time);
        #0 $display("@%0t main completed", $time);
    end
    
    // @0 scheduled
    // @0 value=1
    // @0 main completed
    // @10000 value=2
    // @20000 value=3

fork中生成的子进程在父进程被阻塞后才会开始执行,因此在必要的时候子进程需要保存父进程的环境(如使用automatic保存父进程的循环变量)。

3.3 定时控制

3.3.1 延时控制(#)

#操作符。遇到延时控制时,不管延时的值为多少(如前面提到的#0),当前进程都会放弃其执行权。

3.3.2 事件控制(@)

@操作符。,or都可以作为事件的or使用,但更推荐,

  • 边缘敏感事件控制

    事件 说明
    posedge 0到其他值,或xz1
    negedge 1到其他值,或xz0
    edge posedge 或 negedge 事件

    always @(posedge clk)
    always @(posedge clk, negedge rst_n)

  • 状态敏感事件控制

    always @a
    always @(a, b)

3.3.3 赋值定时控制

如果直接互相赋值会出现竞争冒险。改用赋值定时控制,会先暂时保存右边的值,事件发生后再将暂存的值赋值给左边。

fork
    a = #5 b;
    b = #5 a;
    c = @(posedge clk) d;
join

3.3.4 事件等待控制(wait)

wait语句。等待直到条件被满足时才执行。

initial begin
    reset = 0;
    #50 reset = 1;
end
initial begin
    wait(reset);
    $display("%0t", $time);		// 50000
end

四、赋值语句

4.1 连续赋值语句

即用assign修饰的赋值语句。连续赋值语句不断地将表达式右侧的值赋给左侧的线网/变量。

4.2 行为赋值语句

仅在语句执行条件满足时才为变量进行赋值。因此左侧只能是变量,而不能是线网,因为线网需要连续赋值。

即使是生成组合逻辑的 always_comb 左侧也不能是线网。

4.2.1 阻塞赋值语句

阻塞赋值的运算符包括:=+=-=*=/=%=&=|=^=<<=>>=<<<=>>>=

在顺序语句块中,一条语句执行完毕才执行后面的语句,即后面的语句被阻塞;而在并行语句块中,不会阻塞,同时执行。

4.2.2 非阻塞赋值语句

运算符为<=。用于多个没有依赖关系的变量同时赋值。时序逻辑电路必须使用非阻塞赋值语句

五、运算符

5.1 优先级

记得一些常用的优先级就行:

  • 一元运算符(如-!~&++等)优先级最高
  • */%是同一级;
  • 算术运算符高于比较运算符;
  • 比较运算符高于逻辑运算符;
  • 逻辑运算符中&&高于||

5.2 算术运算符

都是二元运算符,包括+-*/%**

a % b的结果符号和a的符号相同。

**是求幂,指数可以是小数,如x ** 0.5

5.3 比较运算符

返回值:1bit的值。

大于小于这些就不说了,重点看一下相等的比较。

运算符 说明
a === b 完全相等才返回1,包括对xz的比较,结果不会变成x
a !== b ===的否定
a == b 如果a或b包含xz,则结果为x
a != b ==的否定

5.4 逻辑运算符

输入:非零值视为1,零值视为0

返回值:1bit的值。

&&||!。如果判定非法则得到x

5.5 位运算符

与(&)、或(|)、非(~)、异或(^)、同或(^~~^)、逻辑移位(<<>>),算术移位(<<<>>>)。

5.6 单变量逻辑运算符

包括&~&|~|^~^^~。对操作数的位从左到右重复按位运算。如对于3位的a,&a = a[2] & a[1] & a[0]

5.7 条件运算符

cond ? expr1 : expr2

5.8 拼接运算符

拼接的表达式必须有固定的位数。

assign c = {a[2:1], 2'b11};
assign d = {a, {5{1'b0}}};

5.9 比特流运算符

>>表示从左到右提取,<<表示从右到左提取。中间的数表示一次提取的位数,省略则为1,但对于>>来说没意义。可以用来反转或交换。

{ << {6'b001011}})		// 110100
{ << 2 {6'b001011}})	// 111000
{ << 4 {6'b001011}})	// 101100
{ >> {6'b001011}})		// 001011

5.10 部分选择

vec[msb_expr:lsb_expr]就不说了。

logic [31:0] a_vec;
logic [0:31] b_vec;

a_vec[0  +: 8]		// [7:0]
a_vec[15 -: 8]		// [15:8]
b_vec[0  +: 8]		// [0:7]
b_vec[15 -: 8]		// [8:15]

六、执行语句

6.1 条件语句

SystemVerilog中新增了几个关键字,用于 if 和 case 语句,适当使用可以优化综合结果。

修饰符 说明 综合结果 使用场景
普通if 优先级编码器 简单场景
unique 条件必须互斥;必须匹配一个分支 并行逻辑 互斥场景
unique0 条件必须互斥;可以不匹配任何分支 并行逻辑
priority 按优先级选择;必须匹配一个分支 优先级编码器 优先级场景

6.1.1 if 语句

// 比如sel只可能等于0,2,7
always_comb begin
    unique if (sel == 0)
        a = 0;
    else if (sel == 2)
        a = 1;
    else if (sel == 7)
        a = 2;
end

6.1.2 case 语句

除了普通的case,还有casexcasez

  • casezz?为通配符;
  • casexxz?为通配符。

6.2 循环语句

循环中可以使用breakcontinue语句。

6.2.1 for 语句

for循环的控制变量具有automatic属性。

initial begin
    int array[];
    array = new[10];
    for (int i = 1; i <= array.size(); i++) begin
        array[i] = i;
    end
end

6.2.2 foreach 语句

可用于所有的数组类型。只需指定数组名称循环控制变量(写在[]中,编译器会自动创建)。

int q1[$] = {10, 20, 30};
initial begin
    foreach (q1[i]) begin
        $write("%0d ", q1[i]);
    end
end

6.2.3 repeat 语句

repeat语句按照指定的次数来执行循环,推荐不需要循环控制变量的循环使用repeat

int q1[$];
initial begin
    repeat (10) q1.push_back($urandom_range(0,7));
    $display(q1);		// '{3,3,4,6,5,1,3,0,5,7}
end
repeat(3) @(posedge clk) $display("%0t", $time);

6.2.4 forever 语句

forever语句将进行无限次的循环。

forever #10 clk = ~clk;

6.2.5 while 和 do-while 语句

和C语言同样的用法不再赘述。

6.3 generate 语句

在编译时会展开generate中的语句。

  • 所有的表达式都必须是常量表达式。
  • 可以在generate中使用forifcase语句。
  • 循环变量必须使用genvar声明(声明在 generate 内外都可以)。
  • generate中可以使用assign语句、always语句、实例化模块等。
generate
    genvar i;
    for (i = 0; i < 3; i++) begin
        alu #(.WIDTH(8)) uut (.*, .sel(select), .c());
    end
endgenerate

七、任务和函数

特性 任务 函数
返回值 不返回数值,而是通过outputinout端口输出。 可以返回一个单一数值,通过return或函数名返回
消耗时间 可以包含任何定时控制语句 不能包含任何定时控制语句,不消耗仿真时间
用途 代表一个行为过程 类似数学函数,用于表达式

7.1 任务

任务相当于把一段语句进行了包装,当执行到任务时仿真器会跳转执行任务内部的语句,执行完毕后再跳转回来接着执行。除了ref,传参都是直接复制的。

  • 端口方向

    端口方向 说明
    input 值传递
    inout 值传递
    output 值传递
    ref 引用传递,可以在函数内直接修改外部变量

    如果没有指定方向,则按以下规则:

    • 如果第一个端口没有指定方向,则为input
    • 如果第二个及以后的端口没有指定方向,则与前一个端口相同。
  • 端口数据类型

    端口都是变量,不能是线网。

    如果没有指定数据类型,则按以下规则:

    • 如果第一个端口没有指定数据类型,则为logic
    • 如果指定了方向而没有指定数据类型,则为logic
    • 否则和前一个端口相同。
task reverse (input a, output b);
    #10;
    if (a == 0)
        b = 1;
    else
        b = 0;
endtask

initial begin
    x = 0;
    y = 0;
    reverse(x, y);
    reverse(.b(y), .a(x));	// 可以指定端口
end

7.2 函数

  • 端口方向同样可以指定inputinoutoutputref。如果没有指定方向或数据类型,规则同上。

  • 函数名会被定义为一个用于函数值返回的变量,因此可以直接对函数名赋值从而进行返回。除此也可以用return返回。如果没有返回值则声明为void

  • 函数可以设置参数默认值

    如下面的函数以动态数组a[]为形参,可以接受各种数组类型。

    int a[5] = {1, 2, 3, 4, 5};
    
    function int sum(input int a[], input int left=0, input int right=0);
        sum = 0;
        for (int i = left; i < right; i++) begin
            sum += a[i];
        end
    endfunction
    
    initial begin
        $display(sum(a));							// 0
        $display(sum(a, 0, 4));						// 10
        $display(sum(.a(a) ,.left(1), .right(3)));	// 5
        $display(sum(a, .right(2)));				// 3
    end

  • 函数可以递归调用,此时需要声明为automatic使得变量在堆栈区中分配。

    function automatic int factorial(input int n);
        factorial = n < 2 ? 1 : n * factorial(n - 1);
    endfunction
    
    initial begin
        $display(factorial(5));		// 120
    end

八、模块

8.1 定义

module alu #(
    WIDTH = 32
)(
    input  [WIDTH-1:0] a, b,
    input  [1:0]       sel,
    output [WIDTH-1:0] c
);

endmodule

下面是Verilog风格的端口定义,不推荐。

module alu #(WIDTH = 32)(a, b, c, sel);
    input  [WIDTH-1:0] a, b;
    input  [1:0]       sel;
    output [WIDTH-1:0] c;
    
endmodule

8.2 端口信号列表

  • 端口方向

    端口方向可以为inputinoutoutputref。如果没有指定方向,则按以下规则:

    • 如果第一个端口没有指定方向,则为inout
    • 如果第二个及以后的端口没有指定方向,则与前一个端口相同。
  • 端口信号类型

    对于数据类型:如果省略了数据类型,默认为logic

    对于数据对象:可以是线网或变量。如果没有指定,则按以下规则:

    • 对于inputinout端口,设置为标准线网类型(wire);
    • 对于output
      • 如果指定了数据类型,则为变量;
      • 否则为标准线网类型(仅指定了位宽也是线网);
    • 此外inout端口不能为变量,ref端口只能是变量。
module mh(wire x);			// inout  wire logic
module mh(integer x);		// inout  wire integer 
module mh(input x);			// input  wire logic
module mh(input var x);		// input  var  logic
module mh([5:0] x);			// inout  wire logic
module mh(output x);		// output wire logic
module mh(output var x);	// output var  logic
module mh(output logic x);	// output var  logic

8.3 参数化模块

可以在端口信号列表前添加模块的参数(模块参数列表),这与在模块内部使用parameter效果是一样的。

  • 如果使用了模块参数列表,后面定义的parameter会被当成是localparam

  • 如果端口中使用了这些参数则只能在模块参数列表定义。

  • 模块参数列表中加不加parameter都可以。

module alu #(WIDTH = 32)();
	parameter DEPTH = 1;	// =localparam
endmodule

8.4 实例化模块

alu #(.WIDTH(8)) uut (a, b, sel, c);
alu #(.WIDTH(8)) uut (.a(a), .b(b), .sel(sel), .c(c));

连线信号名称与端口信号名称一致时可以简写,对于不一致的端口单独写出即可。

alu #(.WIDTH(8)) uut (.a, .b, .sel, .c);		// 简写
alu #(.WIDTH(8)) uut (.*);						// 同上,更简写
alu #(.WIDTH(8)) uut (.*, .sel(select), .c());	// 单独指定不一样的

SystemVerilog
https://shuusui.site/blog/2025/11/07/systemverilog/
作者
Shuusui
发布于
2025年11月7日
更新于
2025年11月14日
许可协议