跳到主要内容
SystemVerilog

SystemVerilog 设计专用语法

SystemVerilog设计专用语法指南,涵盖always变体、时序逻辑建模、复位策略、generate等硬件设计专属内容

SV 设计

SystemVerilog 设计专用语法

这些语法主要用于RTL设计,描述实际硬件电路行为


层级总览

第三层  时序       always, always_ff, always_comb, always_latch
        ↓            (什么时候动)
第四层  生成       generate, genvar
        ↓            (编译期批量生成)

第三层:时序 — 什么时候动

3.1 always 及三种专用变体

// ① 时序 — 上升沿触发(最常用)
always @(posedge clk) begin
    if (!rst_n)  count <= 0;
    else         count <= count + 1;
end

// ② 时序 — 带异步复位
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)  result <= 0;
    else         result <= data;
end    // rst_n 下降沿直接复位, 不等 clk

// ③ 组合 — 老写法
always @(*) begin
    sum = a + b;     // *=任何输入变化都触发
end

// ④ SV 推荐写法
always_comb begin                    // 明确组合逻辑
    result_comb = mult_stage1 + c_stage1;
end

always_ff @(posedge clk) begin       // 明确时序逻辑
    q <= d;
end

always_latch begin                   // 明确表示有意生成锁存器
    if (latch_en)
        latch_q <= d;
end
写法意图推荐?
always @(posedge clk)时序✅ 通用
always_ff @(posedge clk)明确时序✅ SV 推荐
always @(*)组合⚠️ 老写法
always_comb明确组合✅ SV 推荐
always_latch明确锁存器⚠️ 仅在确实需要锁存器时使用

使用专用过程块时需要注意:

  • always_comb 会自动推导敏感信号,并在仿真开始时执行一次;所有组合输出都要完整赋值,避免意外推导锁存器。
  • always_ff 用于触发器建模,通常使用非阻塞赋值 <=
  • always_latch 用于有意建模电平敏感锁存器,综合/检查工具可据此发现建模意图不一致。
  • 由这些专用过程块写入的变量不应再由其他过程块同时驱动。

3.2 initial vs always 对比

特性initialalways
执行次数1 次无限循环
可综合取决于目标器件和工具;ASIC 通常不使用,部分 FPGA 支持初始化取决于事件控制和块内语句
用在TB 初始化;部分 FPGA 初始化场景可综合 RTL 或带延时的 TB 进程,取决于内容
多个块之间并行并行

always 每次执行完都会重新开始,因此必须包含事件控制或延时;否则会形成不推进仿真时间的死循环。带 # 延时的 always 通常只用于 Testbench,不可综合。

3.3 clk — 芯片的心跳

clk = clock(时钟)的缩写。一根不停在 0 和 1 之间跳变的线。

clk:    ┌──┐  ┌──┐  ┌──┐  ┌──┐  ┌──┐  ┌──
        │  │  │  │  │  │  │  │  │  │  │
────────┘  └──┘  └──┘  └──┘  └──┘  └──┘
       ← 一个周期 →      ← 频率 = 1/周期
概念直白解释
clk周期性 0/1 跳变的线
频率每秒完成多少个周期(1 GHz = 每秒 10 亿个周期)
周期相邻两个同类型边沿之间的时间,T=1/fT=1/f
边沿间隔对 50% 占空比时钟,相邻上升沿和下降沿相差半个周期
为什么叫 clock芯片的“时钟”;同一时钟域内的寄存器按共同节拍工作

设计中的时钟来源:

// 设计中通常由外部提供时钟
input logic clk;

// 需要倍频、分频或相移时,例化目标工艺提供的 PLL/MMCM/时钟管理单元

3.4 信号命名约定 — rst / rst_n / _n 后缀

rst = reset(复位)。当 rst 有效时,需要复位的寄存器回到规定状态。并非所有数据通路寄存器都必须复位;工程中通常优先复位状态机、计数器、有效位和安全相关控制状态,避免无必要的复位树负担。

两种常见命名:

信号名有效电平怎么判断在复位
rst高有效if (rst)
rst_n低有效if (!rst_n)

后缀约定:

后缀全称含义常见度
(无后缀)高电平有效,默认约定⭐⭐⭐
_nnegative低电平有效⭐⭐⭐
_ppositive高电平有效,显式标出项目相关

很多项目约定无后缀表示高有效,_n 表示低有效;也有项目使用 _b_l 等后缀。命名并非语言标准,必须以项目规范为准并保持一致。

rst    = 1  →  正在复位
rst_n  = 0  →  正在复位    (低有效,反直觉所以要后缀提醒)

if (!rst_n) 等价于 if (rst_n == 0) —— “复位被拉低了?”

低有效复位在芯片和板级设计中很常见,但它并非天然比高有效更安全。复位极性通常由 POR 电路、标准单元复位端、板级连接和项目方法学共同决定。


时序逻辑建模

基本时序逻辑

// D触发器
always_ff @(posedge clk) begin
    q <= d;
end

// 带使能的D触发器
always_ff @(posedge clk) begin
    if (en)
        q <= d;
end

// 带同步低有效复位的D触发器
always_ff @(posedge clk) begin
    if (!rst_n)
        q <= 1'b0;
    else
        q <= d;
end

非阻塞赋值(<=)在设计中的应用

// 流水线寄存器
always_ff @(posedge clk) begin
    stage1 <= data_in;
    stage2 <= stage1;
    stage3 <= stage2;
end

// 移位寄存器
// 该写法要求参数 WIDTH >= 2
always_ff @(posedge clk) begin
    shift_reg <= {shift_reg[WIDTH-2:0], serial_in};
end

// 计数器
always_ff @(posedge clk) begin
    if (!rst_n)
        count <= 0;
    else if (count == MAX)
        count <= 0;
    else
        count <= count + 1;
end

通用实例:带宽计数器中的实时计数与窗口快照

带宽计数器是一个很典型的时序逻辑案例:同一次 AXI W 握手会同时更新“总线占用字节”和“实际有效字节”,所以两个计数器的使能相同,但增量不同。

wire w_hs = pi_wvalid & pi_wready;

wire wr_bus_inc = active & w_hs;
wire wr_act_inc = active & w_hs;

localparam [CNT_WIDTH-1:0] BUS_DELTA = STRB_WIDTH;
wire [CNT_WIDTH-1:0] wr_bus_delta = BUS_DELTA;
wire [CNT_WIDTH-1:0] wr_act_delta =
    {{(CNT_WIDTH-8){1'b0}}, popcnt(pi_wstrb)};

两者都以“统计窗口有效且 W 数据真正握手”为事件:

  • wr_bus_cnt 每拍增加 STRB_WIDTH,表示这一拍占用了完整数据总线容量;
  • wr_act_cnt 增加 WSTRB 中 1 的数量,表示这一拍真正有效的写字节数。

例如总线每拍宽 128 字节,而本拍 WSTRB 只有 100 位为 1,则总线字节增加 128,实际字节增加 100。使能相同不代表两个指标相同,区别由 delta 决定。

饱和计数器为什么多算一位

普通定宽加法溢出后会回绕。性能计数器通常希望达到最大值后保持全 1,因此先用多一位的临时变量接住进位:

function automatic [CNT_WIDTH-1:0] sat_add;
    input [CNT_WIDTH-1:0] a;
    input [CNT_WIDTH-1:0] b;
    reg [CNT_WIDTH:0] sum;
    begin
        sum = {1'b0, a} + {1'b0, b};
        sat_add = sum[CNT_WIDTH]
                ? {CNT_WIDTH{1'b1}}
                : sum[CNT_WIDTH-1:0];
    end
endfunction

sum[CNT_WIDTH] 是额外的进位位:它为 1 表示原位宽已经溢出,此时返回全 1;否则返回正常求和结果。

分支顺序就是硬件行为的优先级

实时计数器按以下优先级更新:

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        wr_bus_cnt <= '0;
    end else if (session_start) begin
        wr_bus_cnt <= w_hs ? wr_bus_delta : '0;
    end else if (window_end && mode_q) begin
        wr_bus_cnt <= wr_bus_inc ? wr_bus_delta : '0;
    end else if (wr_bus_inc) begin
        wr_bus_cnt <= sat_add(wr_bus_cnt, wr_bus_delta);
    end
end

含义依次是:

  1. 复位优先级最高,立即清零;
  2. 新 session 开始时丢弃旧 session,但保留开始当周期的握手;
  3. 清零模式的窗口边界处,为下一个窗口装入边界当周期的增量;
  4. 其他周期正常饱和累加。

非阻塞赋值形成“旧值快照”

输出寄存器在另一个时序块中于 window_end 保存内部计数:

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        bw_wr_bus_bytes <= '0;
    else if (window_end)
        bw_wr_bus_bytes <= wr_bus_cnt;
end

假设时钟沿到来前 wr_bus_cnt=256,边界当周期又发生一次增量为 128 的 W 握手。两个时序块都读取沿到来前的旧值,因此:

bw_wr_bus_bytes = 256   // 刚结束窗口的快照
wr_bus_cnt       = 128  // mode_q=1 时,下一个窗口的初值

如果 mode_q=0 表示累计模式,则内部计数变为 384,但本次输出快照仍是 256。边界事件不会进入刚结束窗口的快照,而会反映在后续窗口中。这正是非阻塞赋值“同时读旧值、统一提交新值”的实际用途。


复位策略

异步复位

// 异步低有效复位:rst_n 的断言和释放都会直接作用于异步复位端
always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        // 异步复位:rst_n下降沿立即生效
        state <= IDLE;
        data <= 0;
    end else begin
        // 正常工作:只在clk上升沿更新
        state <= next_state;
        data <= next_data;
    end
end

这段代码本身不保证同步释放。如果 rst_n 来自当前时钟域之外,释放边沿可能违反触发器的 recovery/removal 要求,因此通常先经过后文的复位同步器,再送入该时钟域。

敏感列表@(posedge clk or negedge rst_n)

  • negedge rst_n 在敏感列表中,因此复位断言不等时钟
  • rst_n 释放不会触发该过程块,但会直接释放硬件触发器的异步复位端,仍需满足 recovery/removal

同步复位

// 同步复位:只在clk边沿检查复位
always_ff @(posedge clk) begin
    if (!rst_n) begin
        // 同步复位:只在clk上升沿时检查rst_n
        state <= IDLE;
        data <= 0;
    end else begin
        state <= next_state;
        data <= next_data;
    end
end

敏感列表@(posedge clk)

  • 复位检查只在时钟边沿发生

异步复位 vs 同步复位对比

特性异步复位同步复位
复位断言不需要时钟即可生效只能在有效时钟边沿被采样
敏感列表negedge rst_n只含posedge clk
主要时序要求释放时满足 recovery/removal;异步来源通常要同步释放复位路径满足普通 setup/hold;异步来源仍需先同步
时钟停止时仍可进入复位无有效时钟时不能更新寄存器状态
面积和时序取决于标准单元、复位树、数据路径和综合实现同样取决于具体实现,不能笼统认定更小或更快
选择依据上电/安全需求、工艺库、CDC/RDC、DFT 与项目规范时序收敛、复位延迟、工艺库、DFT 与项目规范

异步复位同步释放

// 两级复位同步器:异步断言、同步释放
// 每个异步复位控制的时钟域都应有独立的复位同步器
(* ASYNC_REG = "TRUE" *) logic [1:0] rst_sync_ff;
logic rst_n_sync;

always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        rst_sync_ff <= 2'b00;                    // 异步断言
    end else begin
        rst_sync_ff <= {rst_sync_ff[0], 1'b1};  // 连续两个时钟沿后释放
    end
end

assign rst_n_sync = rst_sync_ff[1];

// 使用同步后的复位
always_ff @(posedge clk or negedge rst_n_sync) begin
    if (!rst_n_sync)
        state <= IDLE;
    else
        state <= next_state;
end

第一级寄存器可能在外部复位释放时进入亚稳态,第二级用于降低亚稳态传播概率。ASYNC_REG 属性的准确拼写和约束方式依综合/实现工具而定;ASIC 流程通常还需要配套 RDC、复位树和时序约束检查。


第四层:生成 — 编译期批量生成

4.1 generate / genvar — 编译期批量生成

genvar i;
generate
    for (i = 0; i < 16; i++) begin : gen_block
        assign out[i] = data[i] & mask[i];
    end
endgenerate
// 等价于手写 16 行 assign, 不用复制粘贴

4.2 generate if — 条件生成

// 根据参数决定是否真正生成奇偶校验逻辑
generate
    if (ENABLE_PARITY) begin : gen_parity
        assign parity = ^data;
    end else begin : gen_no_parity
        assign parity = 1'b0;
    end
endgenerate

ENABLE_PARITY 必须是 elaboration 阶段可确定的常量,例如 parameterlocalparam。它与运行时的普通 if 不同:未选中的分支不会生成硬件。

4.3 generate for — 循环生成

// 批量例化 NUM_LANES 个结构相同的 lane 模块
generate
    for (genvar i = 0; i < NUM_LANES; i++) begin : gen_lane
        lane u_lane (
            .clk  (clk),
            .rst_n(rst_n),
            .din  (lane_in[i]),
            .dout (lane_out[i])
        );
    end
endgenerate

如果只是让整个向量经过一组寄存器,直接在一个 always_ff 中写 q <= d; 更简洁,不需要逐位 generate。

4.4 generate case — 多路选择生成

generate
    case (MODE)
        0: begin : gen_mode0
            assign out = a & b;
        end
        1: begin : gen_mode1
            assign out = a | b;
        end
        default: begin : gen_default
            assign out = 1'b0;
        end
    endcase
endgenerate

MODE 同样必须是 elaboration 阶段可确定的常量。SystemVerilog 允许省略显式的 generate/endgenerate,但保留它们有时更便于初学者识别生成区域;同一生成区域不能重复嵌套 generate


时钟生成

设计中的时钟

// 普通 RTL 模块通常接收已经处理好的时钟
// 顶层或专用时钟模块可例化 PLL/MMCM/工艺时钟单元

module my_design (
    input logic clk,        // 外部输入时钟
    input logic rst_n,
    // ...
);
    // 直接使用clk
    always_ff @(posedge clk) begin
        // ...
    end
endmodule

不要用普通组合逻辑随意门控时钟,也不要把普通寄存器翻转产生的信号直接作为大范围时钟。需要暂时停止某段逻辑时,优先使用 clock enable:

always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= '0;
    else if (clk_en)
        q <= d;
end

确实需要生成、切换或门控时钟时,应使用目标 FPGA/ASIC 提供的专用时钟资源,并添加相应的时钟约束。

仿真中的时钟生成

// TB中生成时钟(不可综合)
timeunit 1ns;
timeprecision 1ps;

localparam time CLK_PERIOD = 10ns;

initial begin
    clk = 1'b0;
    forever #(CLK_PERIOD/2) clk = ~clk;
end
// 每 5ns 翻转一次 → 周期 10ns → 频率 100MHz

附录:设计语法速查表

语法用途可综合
always_ff时序逻辑
always_comb组合逻辑
always_latch锁存器
generate编译期生成
genvar生成变量
initialTB 初始化;部分 FPGA 支持有限的硬件初始化工具/目标相关
forever无限循环(TB)

最后更新: 2026-07-23