跳到主要内容
SystemVerilog

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

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

SV 通用

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

层级总览 — 按依赖关系排

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

第〇层  硬件直觉   0/1, 位, 时钟沿, 寄存器, 连线
        ↓            (脑子里有图就行)
第一层  声明       module, input/output, parameter, 数据类型, 数组
        ↓            (盒子长什么样)
第二层  赋值       assign, =, <=
        ↓            (值怎么传)
第三层  时序控制   initial, #N, @(posedge/negedge)
        ↓            (什么时候发生)
第四层  流程控制   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 的瞬间(下降沿)
  • 寄存器 — 只在时钟沿上干活;平时不看输入,沿到了才采样
  • 组合逻辑 — 没有时钟,输入一变输出立刻变,像一根导线

寄存器行为:

  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 DEPTH = 32;

命名约定:

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

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

这种 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;          // 三目选择,如果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

在 ASIC 流程中通常不可综合,因此主要用于测试平台;部分 FPGA 工具支持特定寄存器或存储器的 initial 初始化,具体以工具和器件能力为准。

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 个时间单位

timeunit 1ns;
timeprecision 1ps;

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

未声明 timeunit 时,#10 的实际时间由编译单元的 timescale 决定,不能一概理解为 10ns。

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)   // 任意一个来就触发

@(posedge ready) 只等待 ready 从低到高的事件;它不等价于 ready/valid 握手。若 ready 已经为高,等待该事件不会立即返回;握手协议应在时钟沿检查 valid && ready

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

4.1 if / else — 条件分支

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

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

// 简化的同步 slave:只在 valid && ready 的握手周期接受请求
always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        bus.ready <=  0;
        bus.rdata <= '0;
    end else begin
        bus.ready <= 1;                // 本例始终可以接受请求
        if (bus.valid && bus.write)
            memory[bus.addr] <= bus.wdata;
        else if (bus.valid && bus.read)
            bus.rdata <= memory[bus.addr];
    end
end

// 简化的 Master:发起请求后保持 valid,直到 valid && ready 握手完成
// posedge clk:正常工作时,所有寄存器在时钟上升沿更新
// negedge rst_n:rst_n 从 1 变为 0 时立即复位,属于低电平有效的异步复位
always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        // 异步复位分支:不需要等待 clk 上升沿
        bus.valid <= 0;                // 当前没有有效的总线事务
        bus.write <= 0;                // 撤销写控制信号
        bus.read  <= 0;                // 撤销读控制信号

    end else if (!bus.valid && master_write) begin
        // bus.valid=0 表示总线空闲,master_write=1 表示收到上层写请求
        // 该分支写在读分支前面,所以 master_write 和 master_read 同时为 1 时写优先
        bus.write <= 1;                // 指定本次事务为写操作
        bus.read  <= 0;                // 确保读控制信号无效
        bus.addr  <= master_addr;      // 锁存写地址
        bus.wdata <= master_wdata;     // 锁存要写入的数据
        bus.valid <= 1;                // 地址、数据和控制信号从此开始有效

    end else if (!bus.valid && master_read) begin
        // 总线空闲且收到上层读请求,开始一次读事务
        bus.write <= 0;                // 确保写控制信号无效
        bus.read  <= 1;                // 指定本次事务为读操作
        bus.addr  <= master_addr;      // 锁存读地址
        bus.valid <= 1;                // 地址和读控制信号从此开始有效

    end else if (bus.valid && bus.ready) begin
        // valid 和 ready 在同一个时钟沿都为 1,表示当前事务握手完成
        if (bus.read)                  // 如果完成的是读事务
            master_rdata <= bus.rdata; // 锁存 Slave 返回的读数据

        bus.valid <= 0;                // 撤销 valid,Master 回到空闲状态
        bus.write <= 0;                // 清除写事务标志
        bus.read  <= 0;                // 清除读事务标志
    end
    // 如果以上条件都不满足,不执行赋值,寄存器自动保持原值。
    // 因此等待 ready 期间,valid、addr、wdata、write 和 read 都保持稳定。
end

bus = 总线(bus),一组共享信号线(write/read/wdata/rdata/ready/addr/valid),CPU 和所有外设都挂在这组线上通信。

代码中的 <= 是非阻塞赋值:各语句读取当前时钟沿到来前的旧值,并在当前时间步结束时统一更新,适合描述触发器时序逻辑。该示例一次只能处理一个事务;完成当前事务后,下一次时钟判断才能接收新请求。

Slave vs Master 对比

特征SlaveMaster
角色响应请求发起请求
产生信号readyvalid
主动行为等待并响应发送地址和数据

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 — case 表达式中的 Z,以及 case item 中的 Z/? 视为 don't-care
// 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]) 一行遍历数组维度
推荐计数循环、非数组循环遍历数组元素时更简洁

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

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
        @(negedge clk);      // 在 DUT 采样沿前驱动
        wr_en = 1;
        wr_addr = addr;
        wr_data = data;
        @(negedge 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
返回值可通过 output/inout 返回多个值可有返回值;也可写成 function void
# / @(posedge)✅ 能❌ 不能
消耗仿真时间❌(立刻算完)
用在接口操作、时序流程数学计算、数据转换
调用task_name(args);result = func(args); 或单独调用 void'(...)

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, valid, ready;

    task do_write(input logic [31:0] a, d);
        @(negedge clk);       // 在采样沿前驱动,避免 race
        addr  <= a;
        wdata <= d;
        write <= 1;
        read  <= 0;
        valid <= 1;
        do @(posedge clk); while (!ready);
        @(negedge clk);
        valid <= 0;
        write <= 0;
    endtask

    modport master (input clk, ready, rdata,
                    output addr, wdata, write, read, valid);
    modport slave  (input clk, addr, wdata, write, read, valid,
                    output ready, rdata);
endinterface

module master(bus_if.master bus);    // 一个 bus 替代多根信号,并限制方向
module slave(bus_if.slave bus);

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

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

5.6 typedef /enum/ struct

  • typedef — 给类型起别名,简化声明
  • enum — 为有限集合的取值命名;显式类型转换仍可构造其他位模式
  • struct — 把多个变量打包成一个整体
// typedef — 自定义类型别名
typedef logic [7:0] data_t;

// enum — 给值起名字;为避免同一作用域重名,使用前缀
typedef enum {
    S_IDLE, 
    S_READ, 
    S_WRITE, 
    S_DONE
} simple_state_t;

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

下面使用同一组信息进行封装前后的对比。事务都包含地址、数据、读写类型和突发长度,模块还接收一个状态机状态。

封装前:所有端口都直接使用位宽

module raw_example(
    input  logic [7:0]  state,
    input  logic [31:0] addr,
    input  logic [31:0] data,
    input  logic        rw,
    input  logic [3:0]  burst_len
);
    logic is_idle;
    logic is_write;

    // 必须额外查文档才能知道8'h00和1'b1代表什么
    assign is_idle  = (state == 8'h00);
    assign is_write = (rw    == 1'b1);
endmodule

仅看声明不能确定 state 有哪些合法状态、rw 的 0/1 分别表示什么,也不容易看出后四个信号共同描述一次传输事务。

封装后:仍是同一组信息,但含义和层次更清楚

// typedef:统一基础字段的位宽
typedef logic [31:0] word_t;
typedef logic [3:0]  burst_len_t;

// enum:给状态和读写类型的合法取值命名//枚举常量,元素用,分隔,最后一个元素不需要,。
typedef enum logic [7:0] {
    STATE_IDLE    = 8'h00,
    STATE_RUNNING = 8'h01,
    STATE_STOP    = 8'h02,
    STATE_ERROR   = 8'hFF
} state_t;

typedef enum logic {
    RW_READ  = 1'b0,
    RW_WRITE = 1'b1
} rw_t;

// struct:把原来的addr、data、rw、burst_len打包成一个事务//成员声明,每条声明都需要;最后一个也需要;
typedef struct packed {
    word_t       addr;
    word_t       data;
    rw_t         rw;
    burst_len_t  burst_len;
} transaction_t;

module typed_example(
    input state_t     state,
    input transaction_t txn
);
    logic is_idle;
    logic is_write;

    // 使用有含义的枚举名称,不再比较魔法数字
    assign is_idle  = (state  == STATE_IDLE);
    assign is_write = (txn.rw == RW_WRITE);
endmodule

两种写法包含的信息一一对应:

封装前封装后改善
logic [7:0] statestate_t state状态取值有明确名称
logic [31:0] addrtxn.addr地址归入事务
logic [31:0] datatxn.data数据归入事务
logic rwtxn.rw,与 RW_READ/RW_WRITE 比较0和1的含义明确
logic [3:0] burst_lentxn.burst_len位宽统一,字段归入事务

typedef 本身主要负责给类型起名,enum 负责给有限取值起名,struct 才负责把多个字段封装成一个整体。它们通常组合使用,而不是互相替代。

枚举标签位于声明它的作用域中;若多个 enum 放在同一 module/package/class,标签名称不能重复。工程中常用前缀、package 或 class 作用域避免冲突。

$cast 检查枚举转换

当整数来自寄存器、DPI 或文件输入时,可用 $cast 检查它是不是枚举中已定义的值:

state_t     next_state;
logic [7:0] raw_state;

raw_state = 8'h01;
if (!$cast(next_state, raw_state))
    $error("非法状态值:%h", raw_state);

这里 8'h01 对应 STATE_RUNNING,转换成功并返回 1;若 raw_state 为未在 state_t 中列出的 8'h03,转换失败、返回 0,且 next_state 保持原值。相比之下,state_t'(raw_state) 是静态类型转换,不会检查该位模式是否属于枚举,因此处理外部输入时优先使用 $cast

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

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

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 位;表达式上下文仍可能影响最终位宽

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

1'b0'0 看起来相近,但语义不同:

logic [63:0] count;

count = 1'b0;  // 右侧固定为 1 位,赋值时再扩展到 64 位
count = '0;    // 无固定尺寸,直接按左侧上下文生成 64 位全 0

在 SystemVerilog 中还可以用“类型或宽度加撇号”的形式做定宽转换:

parameter int SIZE_WIDTH = 3;

logic [SIZE_WIDTH-1:0] size;
size = SIZE_WIDTH'($clog2(128));  // 先算出 7,再转换成 3 位值

这种写法在参数化代码里很常见,可以明确表达“结果必须按目标宽度保存”,减少无尺寸常量带来的位宽和符号扩展歧义。

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 ──→

逻辑、按位与归约

三类运算的区别在于:把数据看成一个整体、逐位计算,还是把多位数据压缩为 1 位。

logic [3:0] a = 4'b1010;
logic [3:0] b = 4'b1100;
类型常用运算符操作方式结果位宽
逻辑运算&& || !把整个操作数判断为真或假通常为 1 位
按位运算& | ^ ~对两个向量的对应 bit 分别计算通常与操作数位宽有关
归约运算&a |a ^a把一个多 bit 向量压缩为 1 bit1 位

逻辑运算把全 0 看成假、非 0 看成真,常用于条件判断:

a && b       // 1'b1:a、b 都非 0
a || b       // 1'b1:a、b 至少一个非 0
!a           // 1'b0:a 非 0,所以逻辑取反为 0

if (valid && ready)
    transfer();

按位运算对每一位分别处理:

a & b        // 4'b1000
a | b        // 4'b1110
a ^ b        // 4'b0110,相同为 0,不同为 1
~a           // 4'b0101

masked = data & mask;   // 常用于掩码

归约运算只有一个操作数,把向量中的所有 bit 合成一个结果:

&a            // a[3] & a[2] & a[1] & a[0];全 1 才为 1
|a            // a[3] | a[2] | a[1] | a[0];有一个 1 就为 1
^a            // 1 的数量为奇数时结果为 1,常用于奇偶校验

if (|error_vector)
    report_error();      // error_vector 中至少有一个错误位

all_ready = &ready_vector;
parity    = ^data;

同一个符号可根据操作数数量表示不同运算:

a | b          // 两个操作数:按位或
|a             // 一个操作数:归约或
a & b          // 两个操作数:按位与
&a             // 一个操作数:归约与

比较

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

位拼接

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

大括号还支持重复拼接

{重复次数{要重复的内容}}

{8{1'b0}}                 // 8'b0000_0000
{4{2'b10}}                // 8'b1010_1010
{CNT_WIDTH{1'b1}}         // CNT_WIDTH 位全 1

重复拼接可以继续放进普通拼接,形成嵌套拼接:

{{(CNT_WIDTH-1){1'b0}}, 1'b1}

它由 CNT_WIDTH-1 个 0 和最后一个 1 组成,因此是一个宽度为 CNT_WIDTH、数值为 1 的向量。以 CNT_WIDTH=8 为例,结果是 8'b0000_0001

在“整段清零/置一”时,SystemVerilog 的上下文填充值通常更简洁:

count <= '0;                       // 推荐:由 count 的宽度决定
count <= {CNT_WIDTH{1'b0}};        // 等价:显式生成 CNT_WIDTH 位

切片

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

移位

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 的信号
$clog2(value)返回容纳 value 个状态所需的向上取整二进制位数
$countones(value)统计向量中值为 1 的 bit 数量
$isunknown(value)检查表达式中是否包含 X 或 Z

三个函数在参数化设计和验证环境中经常配合使用:

localparam int FIFO_AW = $clog2(FIFO_DEPTH);

int unsigned valid_bytes;
valid_bytes = $countones(wstrb);       // WSTRB 中 1 的数量 = 有效字节数

assert (!$isunknown({valid, ready}));  // valid/ready 不允许出现 X/Z

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

$display("wrote %h to %h", data, addr);
         ↑               ↑         ↑
       格式串          第一个%h   第二个%h
                       代入data   代入addr
格式符含义示例(值=255)
%h十六进制,使用默认显示宽度ff
%d十进制,使用默认显示宽度255
%b二进制 binary11111111
%0h十六进制,不使用默认宽度填充ff
%0d十进制,不使用默认宽度填充255

不加 0 时使用默认字段宽度;%0h/%0d 使用最小必要宽度。十六进制的前导零、十进制的空格填充会随数据类型和字段宽度而变化,因此不应简单理解为“是否补空格”。

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 <=

第三层 时序控制

  • initial #N @(posedge) / @(negedge)

第四层 流程控制

  • 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) 里用 = 会导致意外顺序, 用 <=
组合逻辑里用 <=通常仍能编译,但调度语义不符合组合逻辑预期;应使用 =,lint 常会报警
忘记复位仿真一开始寄存器是 x, 不复位永远是 x
位宽不够若只写 assign sum = a + b;,8 位 sum 会丢弃进位;应使用 9 位结果或 {carry, sum}
always_comb 不完整if 没写 else 可能综合出锁存器 (latch)
byte 是有符号的8'hFF 赋给 byte = -1, 不是 255
bit 吞 x含 x 的 DUT 信号接给 bit 变量 → x 变 0 → bug 藏掉
不写位宽'hF 至少为 32 位,参与表达式时易出现符号扩展或截断误判

最后更新: 2026-07-22