Verilog 代码规范

张开发
2026/4/9 17:28:02 15 分钟阅读

分享文章

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 禁止在可综合代码中使用 initial4.2 禁止使用 # 延时4.3 谨慎使用 casex4.4 禁止异步组合反馈环路4.5 禁止在 always 内对同一信号多处赋值4.6 禁用写法速查表5. 代码评审常见问题 Top 10问题1敏感列表不完整问题2组合 always 中缺少 default 导致 Latch问题3宽度不匹配问题4时钟边沿后立即采样Setup violation 风险问题5parameter 被外部覆盖引发溢出问题6复位未覆盖所有寄存器问题7异步时钟域直接连线问题8generate 块滥用问题9顶层约束与端口不匹配问题10仿真用的语法混入了综合代码附规范代码模板module_template.v1. 信号与模块命名约定1.1 常用前缀规则企业级代码通常采用前缀体系一眼看出信号的方向和类型前缀含义示例i_模块输入inputi_clk、i_rst_n、i_datao_模块输出outputo_tx、o_valid、o_datar_寄存器reg时序逻辑驱动r_cnt、r_state、r_shiftw_线网wire组合逻辑或连线w_sum、w_carry、w_mux_selc_或p_参数常量localparam/parameterp_CLK_FREQ、c_MAX_CNTtb_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/localparamCLK_FREQ、MAX_CNT模块名与文件名一致便于工具自动识别uart_tx.v内module uart_tx实例名用u_前缀区分模块和实例u_uart_tx、u_fifo有意义的名称禁止a、b、tmp、x1r_data_latch而非r_d总线按位宽明确标注避免歧义[7:0] r_rx_data1.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 8d0; 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) 4b1xxx: ... // 危险仿真中 sel4bxxxx 也会匹配 // 推荐用 casez只把 z/? 作为无关位 casez (sel) 4b1???: ... // 明确? 表示无关x 不被特殊处理4.4 禁止异步组合反馈环路// 错误a 和 b 相互驱动形成振荡 assign a b en; assign b a | clr; // b 依赖 aa 依赖 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或普通casefork...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// 错误sel2/3 时 y 未赋值 → Latch always (*) begin case (sel) 2d0: y a; 2d1: 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 {1b0, a} {1b0, b}; // 扩位后相加问题4时钟边沿后立即采样Setup violation 风险// Testbench 中错误在时钟上升沿同时改变信号触发 setup violation always (posedge clk) data new_data; // 正好在沿上变化存在建立时间风险 // 正确沿后延迟 1~2ns 再改变 always (posedge clk) #2 data new_data; // 仅用于 Testbench问题5parameter 被外部覆盖引发溢出// 设计时应加参数范围校验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问题8generate 块滥用// 不必要的 generatefor 循环就够用 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 8d0; else if (w_cnt_done) r_cnt 8d0; else if (i_valid) r_cnt r_cnt 1b1; end // ── 输出寄存器 ─────────────────────────────────────────── always (posedge i_clk or negedge i_rst_n) begin if (!i_rst_n) begin o_valid 1b0; o_data {p_PARAM_A{1b0}}; end else begin o_valid w_cnt_done; o_data i_data; end end endmodule

更多文章