SystemVerilog 设计专用语法
SystemVerilog设计专用语法指南,涵盖always变体、时序逻辑建模、复位策略、generate等硬件设计专属内容
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 对比
| 特性 | initial | always |
|---|---|---|
| 执行次数 | 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 亿个周期) |
| 周期 | 相邻两个同类型边沿之间的时间, |
| 边沿间隔 | 对 50% 占空比时钟,相邻上升沿和下降沿相差半个周期 |
| 为什么叫 clock | 芯片的“时钟”;同一时钟域内的寄存器按共同节拍工作 |
设计中的时钟来源:
// 设计中通常由外部提供时钟
input logic clk;
// 需要倍频、分频或相移时,例化目标工艺提供的 PLL/MMCM/时钟管理单元
3.4 信号命名约定 — rst / rst_n / _n 后缀
rst = reset(复位)。当 rst 有效时,需要复位的寄存器回到规定状态。并非所有数据通路寄存器都必须复位;工程中通常优先复位状态机、计数器、有效位和安全相关控制状态,避免无必要的复位树负担。
两种常见命名:
| 信号名 | 有效电平 | 怎么判断在复位 |
|---|---|---|
rst | 高有效 | if (rst) |
rst_n | 低有效 | if (!rst_n) |
后缀约定:
| 后缀 | 全称 | 含义 | 常见度 |
|---|---|---|---|
| (无后缀) | — | 高电平有效,默认约定 | ⭐⭐⭐ |
_n | negative | 低电平有效 | ⭐⭐⭐ |
_p | positive | 高电平有效,显式标出 | 项目相关 |
很多项目约定无后缀表示高有效,_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
含义依次是:
- 复位优先级最高,立即清零;
- 新 session 开始时丢弃旧 session,但保留开始当周期的握手;
- 清零模式的窗口边界处,为下一个窗口装入边界当周期的增量;
- 其他周期正常饱和累加。
非阻塞赋值形成“旧值快照”
输出寄存器在另一个时序块中于 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 阶段可确定的常量,例如 parameter 或 localparam。它与运行时的普通 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 | 生成变量 | ✅ |
initial | TB 初始化;部分 FPGA 支持有限的硬件初始化 | 工具/目标相关 |
forever | 无限循环(TB) | ❌ |
最后更新: 2026-07-23