跳到主要内容
SystemVerilog

SystemVerilog 通用语法(设计验证都用)

SystemVerilog核心语法指南,涵盖数据类型、运算符、流程控制、模块定义等设计和验证都需要的基础知识

SV 通用

SystemVerilog 通用语法(设计验证都用)

层级总览 — 按依赖关系排

下面的层看不懂,上面的就没法学。你的代码文件里每个知识点都落在这几层里的某一层。

第〇层  硬件直觉   0/1, 位, 时钟沿, 寄存器, 连线
        ↓            (脑子里有图就行)
第一层  声明       module, input/output, parameter, 数据类型, 数组
        ↓            (盒子长什么样)
第二层  赋值       assign, =, <=
        ↓            (值怎么传)
第四层  流程控制   if/else, case/casez, for, foreach
        ↓            (逻辑怎么组织)
第五层  封装       task, function, interface, 模块例化, typedef, enum, struct
        ↓            (大块怎么搭)
第六层  表达式     运算符(~ ! === ?: {} [M:N] [+: ] &|^归约 <<>>), 字面量('b 'h '0'), 系统函数($display...), 注释
                   (细节胶水, 可以随手查)

为什么 SV 里全是固定搭配

不是语法设计者故意限制你——是每句语法都对应一块真实的物理电路。SV 只是把物理规律翻译成了固定句式。

物理世界有什么SV 里就必须有对应的描述
一根线,持续导通assign
时钟边沿触发的寄存器always @(posedge clk) + <=
一个盒子,外面只有接口module / input / output
一根线被多个源驱动wire
一堆相同的寄存器unpacked array
寄存器只在沿上更新<= 非阻塞
8 根并排的物理导线logic [7:0] data

所以当你看到 always @(posedge clk),你不用猜它在描述什么——它只能是寄存器。看到 assign,它只能是组合逻辑连线。这就是”固定搭配”的价值:看一句话就知道电路长什么样。

第〇层:硬件直觉

在看任何代码之前,先搞清这些概念。它们的细节在后面的层里展开,但脑子里的图应该先有。

  • 位 (bit) — 一根线,只能 0 或 1
  • 向量 (vector) — 一排线,[7:0] = 8 根并排
  • 时钟 (clk) — 一根不停 0→1→0→1 的线,芯片的心跳
  • 时钟沿 — 0→1 的瞬间(上升沿) 或 1→0 的瞬间(下降沿)
  • 寄存器 — 只在时钟沿上干活;平时不看输入,沿到了才采样
  • 组合逻辑 — 没有时钟,输入一变输出立刻变,像一根导线
      1 ─────────┐          ┌──────────
                 │          │
                 │  上升沿   │  上升沿
      0 ────────┘          └──────────

寄存器行为:

  clk:    ──┐     ┌──┐     ┌──┐     ┌──
            └─────┘  └─────┘  └─────┘
             ↑       ↑       ↑       ↑
           每个上升沿, 把输入"咔嚓"采样进去

第一层:声明 — 盒子长什么样

1.1 module — 所有代码的骨架

module adder (
    input  logic [7:0] a, b,
    output logic [7:0] sum,
    output logic       carry
);
    // 逻辑实现
    assign {carry, sum} = a + b;

endmodule                         // 盒子关

1.2 input / output — 端口方向

input  logic        clk;       // 信号从外部进来
output logic [7:0]  result;    // 信号从这里出去
方向谁驱动模块内部能读吗模块内部能写吗
input外部模块
output本模块

1.3 parameter — 可配置的常量

parameter D_WIDTH = 16;     // 默认 16, 例化时可以改
parameter A_MAX = 32;

命名约定:

前缀全称含义示例
D_Data数据相关D_WIDTH = 数据位宽(每个存储单元多少位)
A_Address地址相关A_WIDTH = 地址位宽(地址总线多少根线)
A_MAX = 最大地址数 / 存储深度(总共多少个单元)
A_MAX = 2^(A_WIDTH)       →   A_WIDTH=5  →  A_MAX=32

地址位宽决定了能寻址多少个单元,数据位宽决定了每个单元存多少位。

这种 D_ / A_ 前缀是 RAM、FIFO 等存储模块的通用命名习惯,看到前缀就知道这个参数管的是数据还是地址。

第二层:赋值 — 值怎么传

2.1 assign — 一根物理连线

assign sum = a + b;

电路原理图:

  a ──┬──
      │   \
  b ──┼─── + ──── sum
      │   /

没有时钟。ab 任意时刻变化,sum 立刻更新。assign 只能用 = (阻塞赋值)。

常见用法:

assign sum = a + b;
assign {carry, out} = a + b + cin;     // 位拼接输出
assign max = (a > b) ? a : b;          // 三目选择
assign bus = (oe) ? internal : 8'bz;   // 总线使能

2.2 always — 事件驱动的过程块

always @(posedge clk) begin
    q <= d;       // q=输出, d=输入, D触发器的传统命名 (q←output, d←data)
end

电路原理图:

  clk ──┤> 触发器

  d ──────────┐│
              D Q ──── q

  只有 clk 上升沿这一刻, d 被采样进寄存器

2.3 = vs <= — 阻塞 vs 非阻塞

特性= 阻塞<= 非阻塞
执行顺序立刻执行,一行完了才下一行同时执行,时刻结束时统一更新
用在assign / always_combalways @(posedge clk)
电路组合逻辑时序逻辑(寄存器)
// <= 非阻塞 — 并行交换 a 和 b ✅
always @(posedge clk) begin
    a <= b;
    b <= a;
end

// = 阻塞 — 两个都变成 b, 交换失败 ❌
always @(posedge clk) begin
    a = b;
    b = a;
end

规则:时序逻辑一律 <=,组合逻辑(assign/always_comb)用 =

2.4 initial — 只跑一次(TB 专用)

initial begin
    clk = 0;
    rst_n = 0;
    #10 rst_n = 1;          // 10ns 后释放复位
    #100 $finish;            // 再过 100ns 结束仿真
end

不可综合——真实芯片里没有”只跑一次”这种东西,只用于测试。

2.5 begin / end — 多行打包

// 一行可省 begin/end
if (we)  memory[addr] <= data;

// 多行必须 begin/end
if (we) begin
    memory[addr] <= data;
    $display("wrote %h to %h", data, addr);
end

2.6 #N — 等 N 个时间单位

#10;                    // 空等 10ns
#10 clk = ~clk;         // 等 10ns 后翻转时钟
#20 data = 8'hFF;       // 等 20ns 后赋值

2.7 @(posedge) / @(negedge) — 等到边沿

posedgenegedge 是固定关键字,后面的信号名可以换成任何信号。

@(posedge 任意信号)    // "等到它从 0 变 1 的那一刻"
@(negedge 任意信号)    // "等到它从 1 变 0 的那一刻"

@(posedge clk)              // clk 的上升沿
@(posedge clk_write)        // clk_write 的上升沿
@(posedge ready)            // ready 的上升沿
@(negedge rst_n)            // rst_n 的下降沿
@(posedge clk or negedge rst_n)   // 任意一个来就触发

第四层:流程控制 — 逻辑怎么组织

4.1 if / else — 条件分支

// 单分支
if (write_enable)
    memory[addr] <= data;

// 双分支
if (!rst_n)
    count <= 0;
else
    count <= count + 1;

// 多分支 — 总线 slave 的典型行为
if (bus.write) begin                 // 总线发起写请求
    memory[addr] <= bus.wdata;        //   把总线上的数据写入 memory
    bus.ready <= 1;                   //   回应"写好了"
end else if (bus.read) begin          // 总线发起读请求
    bus.rdata <= memory[addr];        //   从 memory 取数据放到读数据线上
    bus.ready <= 1;                   //   回应"数据放好了"
end else begin                        // 总线没有请求(空闲)
    bus.ready <= 0;                   //   "没在干活"
end

bus = 总线(bus),一组共享信号线(write/read/wdata/rdata/ready/addr),CPU 和所有外设都挂在这组线上通信。这个 if/else 是典型的从设备(slave)响应逻辑:

总线动作存储器行为回应
发起写 (write=1)wdata 存进 memory[addr]ready=1(写好了)
发起读 (read=1)memory[addr] 取数据放到 rdataready=1(数据放好了)
无请求空闲ready=0(没在干活)

4.2 case / casez — 多路分支

// 基础 case — 状态机最常用
case (state)
    IDLE     : next_state = WORK;
    WORK     : next_state = DONE;
    DONE     : next_state = IDLE;
    default  : next_state = IDLE;    // 没匹配到时走这里
endcase

// casez — z 位不参与比较(z = "随便你是什么")
// irq = interrupt request(中断请求),外设需要CPU处理时拉高irq信号
casez (irq)
    4'b???1 : $display("irq[0] is 1");  // 只关心 bit[0], 其他位不管
    4'b??1? : $display("irq[1] is 1");
    4'b?1?? : $display("irq[2] is 1");
endcase
类型比较规则常用场景
case完全相等状态机、译码器
casezz 位不关心中断优先级
casexz 和 x 都不关心极少用,别用

4.3 for / foreach — 循环

// for — 和 C 一样
for (int i = 0; i < 16; i++)
    memory[i] = 0;

// foreach — 自动推断范围 (SV 推荐)
foreach (memory[i])
    memory[i] = 0;

// 多维 foreach
foreach (matrix[row, col])
    $display("matrix[%0d][%0d] = %0d", row, col, matrix[row][col]);

foreach = 对数组的每个元素都执行一遍。SV 推荐用它替代 for——不用写起点、终点、步长,自动根据数组维度推断。

foreach (数组名[索引变量])

foreach (memory[i])         // 一维:自动遍历 memory[0] 到 memory[最后一个]
foreach (matrix[r][c])      // 二维:自动遍历所有行和列
特性forforeach
范围手动写 i=0; i<16; i++自动推断
写错边界可能越界不会
多维手写嵌套foreach(arr[i][j]) 一行搞定
推荐⚠️ 能用✅ SV 推荐

第五层:封装 — 大块怎么搭

5.1 task — 有延时的子程序

// 1. 复位任务
task reset_dut;
    begin
        reset = 1;
        #20;
        reset = 0;
        #10;
    end
endtask

// 调用
reset_dut;

// 2. 写寄存器任务
task write_reg(input [7:0] addr, data);
    begin
        @(posedge clk);
        wr_en = 1;
        wr_addr = addr;
        wr_data = data;
        @(posedge clk);
        wr_en = 0;
    end
endtask

// 调用
write_reg(8'h00, 8'hFF);
write_reg(8'h10, 8'h55);

// 3. 等待就绪任务
task wait_ready(input int timeout = 100);
    begin
        int cnt = 0;
        while (!ready && cnt < timeout) begin
            @(posedge clk);
            cnt++;
        end
        if (!ready) $error("Timeout!");
    end
endtask

// 调用
wait_ready(50);

// 4. 时钟生成(注意:时钟生成直接用 always,不用 task)
always #10 clk = ~clk;  // 最常用写法

5.2 function — 有返回值的子程序

function int adder(int a, int b);
    return a + b;
endfunction

int result;
result = adder(3, 5);    // result = 8

5.3 task vs function

特性taskfunction
返回值✅ 必须 return
# / @(posedge)✅ 能❌ 不能
消耗仿真时间❌(立刻算完)
用在接口操作、时序流程数学计算、数据转换
调用task_name(args);result = func(args);

5.4 模块例化 — 盒子插盒子

模块例化就是把一个模块当作”盒子”插到另一个模块里使用。模块定义是模板,例化是创建实际使用的实例。

// 模块名 #(参数) 实例名 (.端口(信号), ...);
// 实例名可以是任意合法标识符,Testbench中常用uut(Unit Under Test)

ram #(8, 5, 32) RAM (
    .clk_write(clk_write),       // .端口名(连接的信号)
    .data_write(data_write),
    .write_enable(write_enable),
    .clk_read(clk_read),
    .address_read(address_read),
    .data_read(data_read)        // 最后一个端口不加逗号
);
连接方式语法说明
按名连接.端口(信号)推荐,顺序随便
按位连接ram RAM(s1, s2, ...)顺序必须和声明一致
通配连接dut(.*)同名信号自动连

参数覆盖:

  • =赋值是设置默认值(模块定义时用)
  • #传递是在例化时从外部覆盖默认值(模块例化时用)
// 模块定义:= 给默认值
module debounce #(parameter DELAY = 10) (...);
//                                  ↑ 默认10

// 模块例化:# 传新值覆盖
debounce #(.DELAY(20)) u_debounce (...);
//            ↑ 外部传20,覆盖默认的10

5.5 interface — 信号组封装

interface bus_if(input logic clk);
    logic [31:0] addr, wdata, rdata;
    logic        write, read, ready;

    task do_write(input logic [31:0] a, d);
        @(posedge clk);
        addr  <= a;
        wdata <= d;
        write <= 1;
        @(posedge ready);
        write <= 0;
    endtask
endinterface

module master(bus_if bus);    // 一个 bus 替代 6 根信号
module slave(bus_if bus);

module top;
    logic clk;
    bus_if bus(clk);
    master m(bus);
    slave  s(bus);
endmodule

好处:增删信号只改 interface 一处,不用每个模块改一遍。

5.6 typedef / enum / struct

// typedef — 自定义类型别名
typedef logic [7:0] data_t;

// enum — 给值起名字
typedef enum {IDLE, READ, WRITE, DONE} state_t;

// struct — 打包相关信号
typedef struct packed {
    bit [7:0]  opcode;
    bit [15:0] addr;
    bit [7:0]  data;
} instruction_t;    // 总计 32 位

第六层:表达式 — 细节胶水

这一层的内容随手查就行,不需要背。

6.1 字面量

<位宽>'<进制符><数值>

  8    '    b     1010_0101
  ↑         ↑     ↑
位宽       进制   值(_是分隔符, 无意义)
符号进制合法字符
'b二进制0 1 x z _
'h十六进制0-9 A-F a-f x z _
'd十进制0-9 x z _
'o八进制0-7 x z _
8'b1000_0000       // 无符号 → 128
8'sb1000_0000      // 有符号 → -128 (s = signed)
8'sh80             // 有符号十六进制 → -128
'hF                 // 默认 32 位 → 32'h0000_000F (不写位宽有坑)

'0    // 全 0, 宽度由上下文推断
'1    // 全 1
'x    // 全 x
'z    // 全 z

6.2 运算符

取反

~   // 按位取反 — 每一位 0→1, 1→0   (~8'b1010 = 8'b0101)
!   // 逻辑取反 — 只看整体真假     (!8'b1010 = 1'b0)
运算符~ 按位取反! 逻辑取反
操作每一位单独翻整个值判真假
结果位宽和原值一样永远是 1 位
用在clk = ~clk、数据取反if (!rst_n)

翻转时钟原理:

initial clk = 0;
always #5 clk = ~clk;

 0ns: clk=0  →  5ns: ~0=1  →  10ns: ~1=0  →  15ns: ~0=1  →  ...
  1  ┌──┐     ┌──┐     ┌──
     │  │     │  │     │
  0  ┘  └─────┘  └─────┘
     ←5ns→ ←── 周期10ns ──→

比较

===    // 全等 — 能比较 x 和 z (1'bx === 1'bx = 1)
==     // 逻辑相等 — 比到 x 返回 x (1'bx == 1'bx = x)

位拼接

{a, b} = {8'd3, 8'd5};        // 打包赋值
{carry, sum} = a + b;          // 拆包

切片

data[7:0]           // 固定切片 — M 和 N 必须是常数
data[base +: 8]     // 可变切片 — base 可变量, 往上取 8 位
data[15 -: 8]       // 往下取 8 位 (= data[15:8])

归约

&data    // 归约与: 全 1 得 1
|data    // 归约或: 有 1 就得 1
^data    // 归约异或: 奇数个 1 得 1

移位

data << 2     // 逻辑左移 — 低位补 0
data >> 2     // 逻辑右移 — 高位补 0
data >>> 2    // 算术右移 — 高位补符号位 (保持正负性)

// byte b = -128 (8'b1000_0000)
// b >>> 1 = 8'b1100_0000 = -64  ← 保持负数
// b >>  1 = 8'b0100_0000 = 64   ← 变正数了!

三元

out = (sel) ? a : b;    // 等价于 if(sel) out=a else out=b

6.3 常用系统函数

函数作用
$display("fmt", args)打印到控制台
$sformatf("fmt", args)格式化到字符串
$time当前仿真时间
$finish结束仿真
$dumpfile("dump.vcd")指定波形文件名
$dumpvars(N, module)指定要 dump 的信号

$display 格式符 — 和 C 语言 printf 一样:

$display("wrote %h to %h", data, addr);
         ↑               ↑         ↑
       格式串          第一个%h   第二个%h
                       代入data   代入addr
格式符含义示例(值=255)
%h十六进制 hexff
%d十进制 decimal255
%b二进制 binary11111111
%0h十六进制,不补空格ff
%0d十进制,不补空格255

不加 0 时默认按位宽对齐补前导空格。实际写 TB 时 %0h 最常用——打印出来的东西不会莫名其妙有空格。

6.4 注释

// 单行注释

/*
   多行注释
   和 C/C++/Java 一样
*/

附录A:你代码里的实物对照

// adder.v — assign = 组合逻辑
assign sum = a + b;                    // 没有时钟, 永远在线

// RAM.sv — always(posedge) = 时序逻辑
always @(posedge clk_write) begin
    if (write_enable)
        memory[addr] <= data_write;    // <= 非阻塞
end

// sv_basics.sv — always_ff + always_comb 配合
always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n)  result <= 0;          // <= 非阻塞 + 异步复位
    else         result <= result_comb;
end

always_comb begin
    result_comb = mult_stage1 + c_stage1; // = 阻塞 + 组合逻辑
end
语法你在哪见过
module ... endmodule所有文件
parameterRAM.sv, sv_basics.sv
input / outputRAM.sv, adder.v, sv_basics.sv, sv_interface.sv
assignadder.v
always @(posedge ...)RAM.sv, sv_interface.sv
always #N clk = ~clkadder.v, sv_basics.sv, sv_interface.sv
always_ffsv_basics.sv
always_combsv_basics.sv
initialtestbench.sv, adder.v, data_type.sv, arrays.sv, sv_basics.sv
begin / end所有文件
if / elseRAM.sv, data_type.sv, sv_interface.sv
case(暂无, 但第一册已收录)
for / foreacharrays.sv
tasktestbench.sv, sv_interface.sv
function(暂无, 但第一册已收录)
#N;testbench.sv, adder.v, 所有 TB
@(posedge ...)sv_basics.sv, sv_interface.sv
{a,b} 拼接adder.v
===data_type.sv
^ 归约异或data_type.sv
! / ~sv_basics.sv, adder.v
$display所有 TB 文件
$dumpfile / $dumpvarstestbench.sv, adder.v, sv_basics.sv
$finish / $timeadder.v, sv_basics.sv
#(参数) + .端口(信号)testbench.sv, sv_basics.sv
.* 通配连接sv_basics.sv
interfacesv_interface.sv
'0 填充sv_basics.sv

附录B:通用语法完整地图

按层级排,全部 ✅ 就代表通用语法毕业。

第〇层 硬件直觉

  • ✅ 0/1/位/向量
  • ✅ 时钟(clk)/上升沿/下降沿
  • ✅ 寄存器 vs 组合逻辑

第一层 声明

  • module/endmodule input/output parameter
  • ✅ 数据类型 (logic/bit/int/byte/enum/struct)
  • ✅ 数组 (packed/unpacked/dynamic/queue/assoc)

第二层 赋值

  • assign always = vs <=

第四层 流程控制

  • if/else case/casez for/foreach

第五层 封装

  • task function 模块例化 (.端口, #参数, .*)
  • interface typedef enum/struct

第六层 表达式

  • ~ / ! / === ? : {a,b} 拼接
  • [M:N] / [+: ] 归约 & | ^ << >> <<< >>>
  • 'b/'h/'d/'o 'sb/'sh '0/'1/'x/'z
  • $display/$dumpfile/$finish/$time
  • ///* */ 注释

附录C:常见坑

说明
时序逻辑里用 =always @(posedge clk) 里用 = 会导致意外顺序, 用 <=
组合逻辑里用 <=assignalways_comb 里用 <= 编译报错
忘记复位仿真一开始寄存器是 x, 不复位永远是 x
位宽不够assign sum = a + b; a,b 8 位 → sum 要 9 位装进位
always_comb 不完整if 没写 else 可能综合出锁存器 (latch)
byte 是有符号的8'hFF 赋给 byte = -1, 不是 255
bit 吞 x含 x 的 DUT 信号接给 bit 变量 → x 变 0 → bug 藏掉
不写位宽'hF 默认 32 位, 截断时可能对也可能错

最后更新: 2026-07-16