Verilog RTL 编码指南
1. 模式检测
根据用户意图判断工作模式:
WRITE 模式(用户要求创建/实现/设计模块):
- 遵循下方所有编码规则
- 如果要实现的模块匹配已知设计模式(FSM、FIFO、pipeline 等),先读取
references/design-patterns.md中对应章节作为起点
REVIEW 模式(用户要求检查/审查/debug 已有代码):
- 读取
references/review-checklist.md,按照结构化流程逐项审查 - 如果发现的问题匹配已知错误类型,参考
references/common-mistakes.md
2. 硬件思维 — Verilog 不是软件
这是写出正确 Verilog 的根基。在写每一行代码前,用以下思维替代软件的顺序执行模型:
并行执行:每个 always 块是一个独立的硬件电路,所有 always 块同时运行。它们不是"按顺序执行的函数",而是"同时工作的独立电路"。
非阻塞赋值 <=(时钟块):在 always @(posedge clk) 中,所有 RHS 在时钟沿同时采样,然后所有 LHS 同时更新。这意味着:
always @(posedge clk) begin
a <= b; // a 拿到的是 b 的旧值
b <= a; // b 拿到的是 a 的旧值 → 实现了交换
end
这不是"先执行第一行再执行第二行",而是一个寄存器组在时钟沿同时锁存。
阻塞赋值 =(组合块):在 always @(*) 中使用,描述组合逻辑的连线关系。虽然有执行顺序,但综合结果是同一个门级电路。
绝对禁止混用:同一个 always 块内不得同时出现 <= 和 =。时钟块用 <=,组合块用 =。
wire 是导线,reg 是存储或组合输出:reg 不一定是寄存器——在组合 always 块中的 reg 综合后是纯组合逻辑。关键在于它被 always 块驱动。
3. 可综合性规则
RTL 代码中禁止使用以下仿真专用语法:
| 禁止的语法 | 原因 | 替代方案 |
|---|---|---|
initial 块 |
无硬件对应物 | 使用复位信号初始化 |
#10 等延时 |
不可综合 | 用时钟周期控制时序 |
$display, $monitor, $finish |
系统任务 | 仅在 TB 中使用 |
fork/join |
无硬件对应物 | 用并行 always 块 |
force/release/deassign |
不可综合 | 用正常赋值逻辑 |
必须遵守的规则:
- 完整条件:所有
if必须有else,所有case必须有default。不完整的条件会推断出 latch,这几乎从来不是你想要的 - 敏感列表:组合逻辑用
always @(*),不要手动列出信号。时序逻辑用always @(posedge clk or negedge rst_n)(异步复位)或always @(posedge clk)(同步复位) - 复位:每个触发器都必须有复位值。使用 active-low 异步复位 (
rst_n) 作为项目惯例 - 单驱动:同一信号只能在一个
always块或一条assign中驱动,不可多驱动 - 无内部三态:内部信号不要用三态,用 mux 代替
- 整数除法:非 2 的幂的除法和取模通常不可综合,用移位代替
4. 模块结构模板
所有模块遵循以下统一结构,与项目现有风格保持一致:
module module_name #(
parameter DATA_WIDTH = 8,
parameter ADDR_WIDTH = 4
) (
input wire clk,
input wire rst_n,
// ---- Input ports ------------------------------------------------
input wire [DATA_WIDTH-1:0] data_in,
input wire valid_in,
// ---- Output ports -----------------------------------------------
output reg [DATA_WIDTH-1:0] data_out,
output reg valid_out
);
// =========================================================================
// Local parameters
// =========================================================================
localparam DEPTH = 1 << ADDR_WIDTH;
// =========================================================================
// Internal signals
// =========================================================================
reg [DATA_WIDTH-1:0] data_reg;
wire some_condition;
// =========================================================================
// Combinational logic
// =========================================================================
assign some_condition = /* ... */;
// =========================================================================
// Sequential logic
// =========================================================================
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
data_reg <= {DATA_WIDTH{1'b0}};
data_out <= {DATA_WIDTH{1'b0}};
valid_out <= 1'b0;
end else begin
// 功能逻辑
end
end
endmodule
结构约定:
clk和rst_n始终是前两个端口- 端口用
// ---- 注释行 ----分组 - 模块内部按区域划分:Local parameters → Internal signals → Combinational → Sequential
- 区域之间用
// ===注释分隔(匹配项目现有风格) - 参数名
UPPER_SNAKE_CASE,信号名snake_case,模块名snake_case - Active-low 信号加
_n后缀(如rst_n,cs_n) - 时钟信号加
clk前缀(如clk,clk_div2)
5. 关键编码规则
5.1 计数器边界
计数器 wrap-around 比较 MAX_VAL - 1,不是 MAX_VAL。这是最常见的 off-by-one 错误:
// 正确:0 到 N-1 共 N 个计数
if (count == MAX_COUNT - 1) begin
count <= {WIDTH{1'b0}};
end else begin
count <= count + 1'b1;
end
5.2 位宽匹配
显式指定所有字面量的位宽,不要使用无宽度字面量:
// 错误
count <= count + 1; // 1 是 32 位整数
data <= 0; // 0 是 32 位整数
// 正确
count <= count + 1'b1;
data <= {DATA_WIDTH{1'b0}};
使用 $clog2() 从深度推导地址宽度:
localparam ADDR_WIDTH = $clog2(DEPTH);
5.3 复位方法论
异步 active-low 复位,复位分支始终放在 if-else 的第一个分支:
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// 所有寄存器的复位值 — 不要漏掉任何一个
state <= IDLE;
count <= {WIDTH{1'b0}};
data_out <= {DATA_WIDTH{1'b0}};
valid_out <= 1'b0;
end else begin
// 正常功能逻辑
end
end
5.4 FSM 编码
使用 localparam 定义状态,不要用 `define。始终有 default 分支回到安全状态:
localparam [1:0] S_IDLE = 2'd0,
S_LOAD = 2'd1,
S_PROC = 2'd2,
S_DONE = 2'd3;
reg [1:0] state, state_next;
组合逻辑中的 next-state 信号用 _next 后缀,寄存器版本不加后缀。
5.5 流水线规则
- 所有数据信号必须经过相同级数的流水线寄存器
valid信号必须与数据同步流水- 添加反压(backpressure)时,stall 信号必须同时冻结数据和 valid
5.6 Valid/Ready 握手协议
valid不能依赖ready(发送端不看接收端是否准备好就可以发起请求)ready可以依赖validvalid一旦拉高,在ready为低期间不得撤回valid为高期间数据必须保持稳定
5.7 内存/数组声明
// 正确:使用 [0:DEPTH-1] 显式声明范围
reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
// 读写模式
always @(posedge clk) begin
if (wr_en)
mem[wr_addr] <= wr_data;
rd_data <= mem[rd_addr]; // 读优先,1 周期延迟
end
6. 输出自检清单
生成 Verilog 代码后,在呈现给用户之前,逐条对照以下清单验证:
- 每个
if都有else,每个case都有default - 没有
initial块、#delay、系统任务($display等) - 复位分支中列出了所有寄存器的复位值,无遗漏
- 时钟
always块只用<=,组合always块只用=,无混用 - 所有字面量显式指定位宽(无裸
0、1等整数字面量) - 组合逻辑敏感列表是
@(*),时序逻辑是@(posedge clk or negedge rst_n) - 计数器边界条件正确(比较 N-1 而不是 N,注意 off-by-one)
- 无多驱动信号(同一信号在多个 always/assign 中赋值)
-
clk,rst_n是模块的前两个端口 - 参数名
UPPER_SNAKE_CASE,信号名snake_case - 组合 always 块中的信号在所有分支中都有赋值(防止 latch 推断)
- FSM 的 case 语句有 default 分支,且 default 回到安全状态
7. 参考文件
以下参考文件在需要时读取,不要每次都全部加载:
references/design-patterns.md— 当实现的模块匹配以下任何模式时读取: FSM、FIFO、pipeline、handshake/skid buffer、arbiter、counter、shift register、 edge detector、debouncer、clock divider、dual-port RAM、synchronizerreferences/common-mistakes.md— 当调试综合错误、仿真不匹配、或需要理解某个 常见 Verilog 陷阱时读取references/review-checklist.md— 在 REVIEW 模式下读取,按照结构化流程审查代码
8. Icarus Verilog 兼容性
RTL 代码使用 Verilog-2001/2005 语法,确保与 Icarus Verilog (iverilog -g2012) 兼容:
- RTL 中使用
reg/wire,不要用 SystemVerilog 的logic - 不要在 RTL 中使用
always_ff/always_comb/always_latch(这些是 SV 语法,仅 TB 中使用) $clog2()在-g2012模式下支持generate块正常支持- TB 文件是 SystemVerilog (
.sv),由tb-creatorskill 处理,本 skill 不管 TB
9. 项目工作流提示
生成 RTL 文件后,提醒用户:
- RTL 文件放在
RTL/目录下 - 运行 VS Code 的 "compile and run" 任务来编译和仿真
- 如果有新的 RTL 文件,
gen_filelist.py会自动扫描RTL/目录