Verilog 代码规范
本文是《FPGA入门到实战》专栏第18篇。前面学了大量语法和实战项目,本篇系统梳理企业 FPGA 工程中的代码规范标准。规范不是束缚,而是让代码在团队协作、版本维护、代码评审中高效运转的基础。对比学习规范写法和错误写法,将显著提升代码质量,减少 review 来回轮次。
Verilog 代码规范
- 1. 信号与模块命名约定
- 1.1 常用前缀规则
- 1.2 命名规则
- 1.3 时钟与复位命名
- 2. 注释规范
- 2.1 文件头注释
- 2.2 段落注释
- 2.3 行内注释
- 3. 模块化与层次化设计原则
- 3.1 单一职责原则
- 3.2 接口设计规范
- 3.3 顶层模块职责
- 4. 可综合代码禁用写法清单
- 4.1 禁止在可综合代码中使用 `initial`
- 4.2 禁止使用 `#` 延时
- 4.3 谨慎使用 `casex`
- 4.4 禁止异步组合反馈环路
- 4.5 禁止在 always 内对同一信号多处赋值
- 4.6 禁用写法速查表
- 5. 代码评审常见问题 Top 10
- 问题1:敏感列表不完整
- 问题2:组合 always 中缺少 default 导致 Latch
- 问题3:宽度不匹配
- 问题4:时钟边沿后立即采样(Setup violation 风险)
- 问题5:parameter 被外部覆盖引发溢出
- 问题6:复位未覆盖所有寄存器
- 问题7:异步时钟域直接连线
- 问题8:generate 块滥用
- 问题9:顶层约束与端口不匹配
- 问题10:仿真用的语法混入了综合代码
- 附:规范代码模板
- module_template.v
1. 信号与模块命名约定
1.1 常用前缀规则
企业级代码通常采用前缀体系,一眼看出信号的方向和类型:
| 前缀 | 含义 | 示例 |
|---|---|---|
i_ | 模块输入(input) | i_clk、i_rst_n、i_data |
o_ | 模块输出(output) | o_tx、o_valid、o_data |
r_ | 寄存器(reg,时序逻辑驱动) | r_cnt、r_state、r_shift |
w_ | 线网(wire,组合逻辑或连线) | w_sum、w_carry、w_mux_sel |
c_或p_ | 参数常量(localparam/parameter) | p_CLK_FREQ、c_MAX_CNT |
tb_ | Testbench 内部信号 | tb_clk、tb_data_in |
// 规范写法示例 module uart_tx #( parameter p_CLK_FREQ = 100_000_000, parameter p_BAUD_RATE = 115200 )( input wire i_clk, input wire i_rst_n, input wire i_tx_start, input wire [7:0] i_tx_data, output reg o_tx, output reg o_tx_busy ); localparam c_BAUD_DIV = p_CLK_FREQ / p_BAUD_RATE - 1; reg [15:0] r_baud_cnt; wire w_baud_tick; assign w_baud_tick = (r_baud_cnt == c_BAUD_DIV); // ... endmodule1.2 命名规则
| 规则 | 说明 | 示例 |
|---|---|---|
| 全小写加下划线(snake_case) | 信号名 | tx_busy、baud_cnt |
| 常量全大写 | parameter/localparam | CLK_FREQ、MAX_CNT |
| 模块名与文件名一致 | 便于工具自动识别 | uart_tx.v内module uart_tx |
实例名用u_前缀 | 区分模块和实例 | u_uart_tx、u_fifo |
| 有意义的名称 | 禁止a、b、tmp、x1 | r_data_latch而非r_d |
| 总线按位宽明确标注 | 避免歧义 | [7:0] r_rx_data |
1.3 时钟与复位命名
// 时钟统一命名 i_clk // 单时钟系统 i_clk_100m // 多时钟系统(标明频率) i_clk_axi // 按总线命名 // 复位统一命名(低有效加 _n 后缀) i_rst_n // 系统复位,低有效 i_arst_n // 异步复位,低有效2. 注释规范
2.1 文件头注释
每个.v文件开头必须有文件头注释:
// ============================================================ // 文件名 : uart_tx.v // 模块名 : uart_tx // 描述 : UART 发送模块(8N1,三段式 FSM 实现) // 参数 : p_CLK_FREQ - 系统时钟频率(Hz),默认 100MHz // p_BAUD_RATE - 波特率,默认 115200 // 端口 : i_clk - 系统时钟(上升沿有效) // i_rst_n - 异步复位(低有效) // i_tx_start - 发送触发(高脉冲,1拍有效) // i_tx_data - 待发送 8 位数据 // o_tx - 串行数据输出 // o_tx_busy - 发送忙标志(高有效) // 版本 : v1.0 - 初始版本 // ============================================================2.2 段落注释
用分隔线和标题划分代码段,便于快速定位:
// ── 波特率分频计数器 ────────────────────────────────────────── always @(posedge i_clk or negedge i_rst_n) begin // ... end // ── 状态寄存器(第一段)────────────────────────────────────── always @(posedge i_clk or negedge i_rst_n) begin // ... end2.3 行内注释
对非显而易见的代码加行内注释:
// 好的行内注释(解释为什么,而不是重复代码在做什么) localparam c_BAUD_DIV = p_CLK_FREQ / p_BAUD_RATE - 1; // -1:计数从0开始 assign w_baud_tick = (r_baud_cnt == c_BAUD_DIV); // 每个波特周期产生1拍脉冲 // 不好的行内注释(重复代码本身) r_cnt <= r_cnt + 1; // r_cnt 加 1 ← 废话注释,删掉3. 模块化与层次化设计原则
3.1 单一职责原则
每个模块只做一件事,接口清晰:
// 好的设计:职责分明 uart_tx u_tx (...); // 只管发送 uart_rx u_rx (...); // 只管接收 baud_gen u_baud(...); // 只管波特率 uart_fifo_tx u_buf (...); // 只管发送缓冲 // 不好的设计:一个模块包揽所有 uart_everything u_uart(...); // 发送+接收+波特率+FIFO 全在一起,难以复用和测试3.2 接口设计规范
模块接口遵循valid-ready握手协议(AXI-Stream 标准):
// 标准握手接口 output reg o_valid, // 数据有效 input wire i_ready, // 下游准备好接收 output reg [7:0] o_data, // 数据 // 握手条件:valid && ready 时数据传输成功 assign data_transfer = o_valid && i_ready;3.3 顶层模块职责
顶层模块(top)只做例化和连线,不含业务逻辑:
// 好的顶层:只有例化和 assign 连线 module top ( input wire i_clk, i_rst_n, // ... ); // 只有连线和例化,无 always 块 uart_tx u_tx (.i_clk(i_clk), .i_rst_n(i_rst_n), ...); uart_rx u_rx (.i_clk(i_clk), .i_rst_n(i_rst_n), ...); assign w_tx_data = w_rx_data; // 简单连线 endmodule4. 可综合代码禁用写法清单
4.1 禁止在可综合代码中使用initial
// 错误:initial 只能用于仿真 initial begin r_cnt = 0; // 综合工具会忽略或报错 end // 正确:用复位初始化 always @(posedge i_clk or negedge i_rst_n) begin if (!i_rst_n) r_cnt <= 8'd0; else r_cnt <= r_cnt + 1; end4.2 禁止使用#延时
// 错误:延时在综合中被忽略,只能用于仿真 assign o_out = #5 i_in; // 综合后等于 assign o_out = i_in // 错误:always 中的延时 always @(*) begin #2 r_data = i_data; // 综合无效,且可能引发仿真/综合不一致 end4.3 谨慎使用casex
// 风险:casex 将 x(未知)和 z(高阻)都作为无关位 // 仿真时 x 态蔓延可能引发意外匹配,仿真与综合行为不一致 casex (sel) 4'b1xxx: ... // 危险:仿真中 sel=4'bxxxx 也会匹配 // 推荐:用 casez(只把 z/? 作为无关位) casez (sel) 4'b1???: ... // 明确:? 表示无关,x 不被特殊处理4.4 禁止异步组合反馈环路
// 错误:a 和 b 相互驱动,形成振荡 assign a = b & en; assign b = a | clr; // b 依赖 a,a 依赖 b → 组合回路! // 正确:用寄存器打断回路 always @(posedge i_clk) begin r_b <= r_a | i_clr; end assign w_a = r_b & i_en;4.5 禁止在 always 内对同一信号多处赋值
// 错误:r_cnt 在同一 always 内被赋值两次(可能产生意外行为) always @(posedge i_clk) begin r_cnt <= r_cnt + 1; if (i_clr) r_cnt <= 0; // 哪个生效?Verilog 规定后者,但容易出错 end // 正确:用 if-else 明确优先级 always @(posedge i_clk) begin if (i_clr) r_cnt <= 0; else r_cnt <= r_cnt + 1; end4.6 禁用写法速查表
| 禁用写法 | 原因 | 替代方案 |
|---|---|---|
initial | 不可综合 | 用复位初始化 |
#延时 | 综合忽略 | 用时序逻辑控制 |
casex | 仿真/综合不一致 | 用casez或普通case |
fork...join | 不可综合 | 用时序状态机 |
wait(条件) | 不可综合 | 用时序逻辑轮询 |
task内含时钟 | 部分工具不支持 | 仅在 Testbench 中使用 |
| 组合逻辑回路 | 振荡/X态 | 寄存器打断回路 |
| 多驱动 | 综合报错 | 每个 reg 只在一个 always 中驱动 |
5. 代码评审常见问题 Top 10
问题1:敏感列表不完整
// 错误:a、b 不在敏感列表,仿真行为与综合不一致 always @(sel) begin if (sel) y = a; // a 变化时不触发 always,仿真中 y 不更新! else y = b; end // 正确: always @(*) begin // 或 always @(sel, a, b) if (sel) y = a; else y = b; end问题2:组合 always 中缺少 default 导致 Latch
// 错误:sel=2/3 时 y 未赋值 → Latch always @(*) begin case (sel) 2'd0: y = a; 2'd1: y = b; // 缺少 default! endcase end问题3:宽度不匹配
// 错误:8位 + 8位可能溢出,结果被截断 wire [7:0] sum; assign sum = a + b; // a、b 均为 8 位,结果应为 9 位 // 正确: wire [8:0] sum; assign sum = {1'b0, a} + {1'b0, b}; // 扩位后相加问题4:时钟边沿后立即采样(Setup violation 风险)
// Testbench 中错误:在时钟上升沿同时改变信号,触发 setup violation always @(posedge clk) data = new_data; // 正好在沿上变化,存在建立时间风险 // 正确:沿后延迟 1~2ns 再改变 always @(posedge clk) #2 data = new_data; // 仅用于 Testbench问题5:parameter 被外部覆盖引发溢出
// 设计时应加参数范围校验(Verilog-2001 不支持,System Verilog 可用) // 或在注释中明确约束 parameter CLK_FREQ = 100_000_000; // 范围:1MHz ~ 200MHz问题6:复位未覆盖所有寄存器
// 错误:r_state 在复位分支中未赋值 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin r_cnt <= 0; // 忘记 r_state <= IDLE; end // ... end问题7:异步时钟域直接连线
// 错误:直接跨时钟域驱动,产生亚稳态 module cdc_bad ( input wire i_clk_a, i_clk_b, input wire i_data_a, output wire o_data_b ); assign o_data_b = i_data_a; // 危险!i_data_a 是 clk_a 域的信号 endmodule问题8:generate 块滥用
// 不必要的 generate:for 循环就够用 generate genvar i; for (i = 0; i < 8; i = i + 1) begin assign w_out[i] = w_in[i] & i_en; end endgenerate // 更简洁: assign w_out = w_in & {8{i_en}};问题9:顶层约束与端口不匹配
XDC 中的端口名必须与顶层 module 的端口名完全一致(区分大小写),否则实现时会报约束找不到端口的错误。
问题10:仿真用的语法混入了综合代码
// 错误:$display、$finish、`timescale 出现在可综合模块中 module uart_tx (...); `timescale 1ns/1ps // 综合时会被忽略,但容易误导 initial $display("start"); // 不可综合 endmodule附:规范代码模板
module_template.v
// ============================================================ // 文件名 : module_template.v // 模块名 : module_template // 描述 : <模块功能一句话描述> // 参数 : p_PARAM_A - <说明,单位,默认值,有效范围> // 端口 : i_clk - 系统时钟,上升沿有效 // i_rst_n - 异步复位,低有效 // i_xxx - <输入描述> // o_xxx - <输出描述> // 版本 : v1.0 - 初始版本 // ============================================================ module module_template #( parameter p_PARAM_A = 8, parameter p_PARAM_B = 100 )( input wire i_clk, input wire i_rst_n, input wire i_valid, input wire [p_PARAM_A-1:0] i_data, output reg o_valid, output reg [p_PARAM_A-1:0] o_data ); // ── 本地参数 ──────────────────────────────────────────── localparam c_MAX_CNT = p_PARAM_B - 1; // ── 内部信号声明 ───────────────────────────────────────── reg [7:0] r_cnt; wire w_cnt_done; // ── 组合逻辑 ───────────────────────────────────────────── assign w_cnt_done = (r_cnt == c_MAX_CNT); // ── 计数器 ─────────────────────────────────────────────── always @(posedge i_clk or negedge i_rst_n) begin if (!i_rst_n) r_cnt <= 8'd0; else if (w_cnt_done) r_cnt <= 8'd0; else if (i_valid) r_cnt <= r_cnt + 1'b1; end // ── 输出寄存器 ─────────────────────────────────────────── always @(posedge i_clk or negedge i_rst_n) begin if (!i_rst_n) begin o_valid <= 1'b0; o_data <= {p_PARAM_A{1'b0}}; end else begin o_valid <= w_cnt_done; o_data <= i_data; end end endmodule