SystemVerilog RTL 编码规则

发布时间:2026/7/19 6:38:35
SystemVerilog RTL 编码规则 本项目采用严格的 SystemVerilog RTL 编码风格。除非有明确的特殊要求所有新生成或修改后的 RTL 代码都必须遵守以下规则。1. RTL 设计总体原则RTL 风格应优先考虑可读性可维护性代码评审友好性调试友好性团队协作可扩展性长期稳定复用能力相比过度压缩或过于“聪明”的写法应优先选择显式、清晰、容易理解的代码。RTL 不是代码高尔夫少写几行不等于少掉几个坑。2. 语言要求RTL 代码使用 SystemVerilog 编写。优先使用以下语法结构logicalways_ffalways_combtypedef enum logictypedef struct packedlocalparamfunction automatic仅限可综合、纯组合、可复用的小型功能除非出于工具兼容性要求否则应避免使用传统 Verilog 风格。2.1 RTL 编码边界RTL 应以 Verilog 可综合 RTL 为基础进行编写同时在合适的位置使用 SystemVerilog 编码风格。规则当模块端口需要数组端口时使用 SystemVerilog 风格的数组声明。RTL 逻辑应保持显式、结构清晰。允许在可综合 RTL 中使用可综合形式的function automatic但仅限纯组合、可复用的小型功能封装。不要为了压缩代码行数把普通组合逻辑、模块主流程、FSM 控制逻辑或关键数据通路隐藏在 function 中。不要在可综合 RTL 模块内部声明或使用task。优先使用直接的组合逻辑、always_comb、always_ff、generate以及结构清晰的子模块function 只作为可读性和复用性的辅助工具。原因提升综合结果的可预测性。提升波形调试的可见性。避免关键组合逻辑被过度隐藏。便于 ECO 和代码评审。使 RTL 行为在模块层级保持显式可见。推荐写法module example ( input logic [3:0][7:0] i_data, output logic [3:0][7:0] o_data ); always_comb begin o_data[0] i_data[0]; o_data[1] i_data[1]; o_data[2] i_data[2]; o_data[3] i_data[3]; end endmodule不推荐写法仅为了压缩少量代码将简单映射隐藏在 function 中。module example ( input logic [31:0] i_data_flat, output logic [31:0] o_data_flat ); function automatic logic [7:0] swap_byte(input logic [7:0] data); begin swap_byte {data[3:0], data[7:4]}; end endfunction assign o_data_flat[7:0] swap_byte(i_data_flat[7:0]); endmodule2.2 可综合 function 使用规则允许使用可综合形式的function但应避免滥用。适合使用 function 的场景同一类纯组合计算在多个位置复用。功能边界清楚封装后能提升可读性和降低重复出错风险。逻辑规模较小适合作为工具型函数使用。典型示例包括 ECC/parity 计算、固定格式字段打包/解包、简单 mask 生成、饱和裁剪、Gray code 转换等。使用约束优先使用function automatic。function 内部只能描述纯组合逻辑。function 不应包含时钟、复位、锁存器、非阻塞赋值、事件控制、延时控制、wait、fork/join或task调用。function 的输入应尽量通过参数传入避免直接读取或修改模块内部全局信号。function 不应产生副作用不应在函数内部修改外部变量。function 的返回值或输出必须在所有路径上被完整赋值避免推导出 latch。如果 function 内部使用for循环循环边界必须是静态可确定的参数、常量或 localparam并遵守组合逻辑for循环使用约束。function 内容应保持短小清晰如果逻辑复杂到影响波形调试、代码评审或 ECO应拆成显式组合逻辑或独立子模块。推荐写法将可复用、边界清晰的小型组合计算封装为 function。function automatic logic [3:0] calc_ecc4(input logic [7:0] data); begin calc_ecc4[0] data[0] ^ data[1] ^ data[3] ^ data[4]; calc_ecc4[1] data[0] ^ data[2] ^ data[3] ^ data[5]; calc_ecc4[2] data[1] ^ data[2] ^ data[3] ^ data[6]; calc_ecc4[3] data[4] ^ data[5] ^ data[6] ^ data[7]; end endfunction assign w_ecc calc_ecc4(i_data);不推荐写法用 function 包住模块主数据通路或复杂控制逻辑。function automatic logic [71:0] build_src_data_path( input logic [71:0] data_a, input logic [71:0] data_b, input logic sel, input logic mode_en ); begin ... end endfunction经验原则function 是复用和表达设计意图的工具不是隐藏 RTL 复杂度的盒子。能让代码更清楚、更少重复错误就可以用只是为了少写几行就不要用。3. 命名规范3.1 端口命名端口统一使用以下前缀类型前缀输入端口i_输出端口o_双向端口io_示例input logic i_clk; input logic i_rst_n; output logic o_done; inout tri io_gpio;3.2 内部信号命名内部信号统一使用以下前缀类型前缀时序寄存器信号r_组合逻辑信号w_示例logic [7:0] r_sum_data; logic w_collect_done;不要过度使用_q/_d。只有当_q/_d能明显提升流水线或寄存器传输关系的可读性时才使用它们。例外FSM 当前状态和下一个状态信号固定使用curr_state和next_state。这两个信号不要求使用r_/w_前缀。3.3 宏、常量和枚举命名以下对象使用ALL_CAPS大写命名风格宏定义全局常量局部参数枚举值示例define RTL_TEMPLATE_VERSION 32h2026_0417 localparam int unsigned GLOBAL_TIMEOUT_CYCLE 1000; typedef enum logic [1:0] { ST_IDLE 2b00, ST_COLLECT 2b01, ST_DONE 2b10, ST_ERROR 2b11 } fsm_state_e;3.4 寄存器输入命名寄存器配置类输入必须使用以下前缀i_reg_推荐写法i_reg_gam_pol_en i_reg_gam_chop_mode i_reg_src_dir避免写法i_cfg_gamma_polarity_enable i_config_source_direction3.5 信号名长度规则信号名应满足含义清晰风格一致长度合理不要把完整文档层级或过长的功能描述全部塞进信号名中。推荐格式i_reg_block_feature_meaning推荐缩写完整单词缩写gammagamsourcesrcpolaritypolenableentimingtmcountcntselectseldummydmy同一个项目内应保持缩写方式一致。3.6 推荐命名示例推荐写法i_reg_gam_norm_chop_mode i_reg_gam_pol_en i_reg_gam_pol_tm i_reg_gam_dmy_pol_en避免写法i_cfg_gamma_normal_chop_mode i_cfg_gamma_polarity_timing_map i_cfg_gamma_dummy_polarity_enable在专用子模块内部如果不存在歧义可以省略模块前缀。示例i_reg_pol_en i_reg_pol_tm3.7 命名原则信号命名优先级如下前缀正确。含义清晰。长度合理。项目内保持一致。目标不是最短的名字。目标是在不产生歧义的前提下尽量简洁。4. FSM 编码风格状态机使用三段式 FSM 结构。FSM 状态信号命名固定使用信号含义命名说明curr_state当前状态时序寄存器豁免r_前缀要求next_state下一个状态组合逻辑信号豁免w_前缀要求这样可以让状态机代码更接近通用 FSM 表达方式也便于阅读、评审和波形调试。第 1 段状态寄存器使用always_ff。使用非阻塞赋值。只更新当前状态寄存器curr_state。always_ff (posedge i_clk or negedge i_rst_n) begin : p_state_reg if (!i_rst_n) begin curr_state ST_IDLE; end else begin curr_state next_state; end end第 2 段次态组合逻辑使用always_comb。使用阻塞赋值。先给出默认次态。使用unique case。保留default分支。always_comb begin : p_state_next next_state curr_state; unique case (curr_state) ST_IDLE: begin ... end default: begin next_state ST_ERROR; end endcase end第 3 段寄存器输出与数据通路使用always_ff。使用非阻塞赋值。默认优先使用寄存器输出。当数据通路寄存器与 FSM 行为强耦合时可将其放在该段中。always_ff (posedge i_clk or negedge i_rst_n) begin : p_output_reg if (!i_rst_n) begin r_done 1b0; end else begin ... end end5. 状态编码规则枚举状态应显式编码。原因波形调试更稳定。更容易与 SPEC 对齐。避免新增状态后编码漂移。更适合可复用模板。推荐写法typedef enum logic [1:0] { ST_IDLE 2b00, ST_COLLECT 2b01, ST_DONE 2b10, ST_ERROR 2b11 } fsm_state_e;6. 复位风格低有效复位信号使用以下命名风格i_rstn i_soft_clr_n对于时序逻辑只有在确有需要时才使用异步复位。合适时优先使用同步清除。所有输出寄存器和关键数据通路寄存器都应显式复位。7. 赋值规则时序逻辑使用非阻塞赋值r_data w_data_next;组合逻辑使用阻塞赋值w_data_next r_data;不要错误混用阻塞赋值和非阻塞赋值。8. 代码格式规则8.1 begin / end除非语句确实非常简单并且可以清晰地写在一行内否则应使用begin和end。推荐写法if (i_enable) begin r_valid 1b1; end8.2 else 风格使用以下格式end else begin不要使用以下格式end else begin8.3 对齐在实际可读性允许的情况下对以下内容进行对齐端口声明信号声明参数声明模块例化连接连续赋值示例logic [DATA_W-1:0] r_sum_data; logic [COUNT_W-1:0] r_item_cnt; logic r_done; logic r_error;8.4 组合逻辑 for 循环使用约束在可综合逻辑块中应谨慎使用for循环。规则不要仅为了减少代码行数而使用for循环。综合工具通常会将静态展开的for循环展开为等价硬件因此使用for循环本身并不会自动减小芯片面积。当展开后的逐项赋值更直观、更利于 review、debug 和 ECO 时应优先写成显式赋值。只有当for循环能明显提升可读性、降低重复出错风险或描述规则化阵列/位段操作更清晰时才建议使用。使用for循环时循环边界必须是静态可确定的常量、参数或 localparam不允许依赖运行时动态变量形成不可综合或难以预测的逻辑。避免在一个always_comb中使用过深或嵌套过多的for循环如果逻辑过重应拆分为更小的组合逻辑块或结构清晰的子模块。推荐写法当映射关系很少且有明确硬件含义时使用显式赋值。always_comb begin : p_byte_map o_data[0] i_data[0]; o_data[1] i_data[1]; o_data[2] i_data[2]; o_data[3] i_data[3]; end允许写法当数组规模较大且映射关系完全规则时可以使用for循环提升可读性。always_comb begin : p_mask_expand for (int i 0; i SRC_NUM; i) begin o_src_mask[i] i_enable i_mask_seed[i]; end end不推荐写法仅为了压缩代码把少量且具有实际映射含义的逻辑隐藏到for循环中。always_comb begin : p_byte_map_compact for (int i 0; i 4; i) begin o_data[i] i_data[i]; end end经验原则for循环不是面积优化手段而是代码描述手段。能让 RTL 更清楚就用只是让代码更短就不要用。8.5 always_ff 寄存器分组规则优先一个always_ff块只描述一个寄存器或一个紧密相关的寄存器组。这样可以提升ECO 友好性。综合和调试的可追踪性。代码评审清晰度。寄存器局部修改的安全性。推荐写法always_ff (posedge i_clk or negedge i_rst_n) begin : p_done_reg if (!i_rst_n) begin r_done 1b0; end else begin r_done w_done_next; end end只有当多个寄存器关系紧密时才允许放在同一个always_ff块中。允许的情况包括同步更新的流水线寄存器。同一个 beat 内的 valid/data/control 寄存器。同步器触发器。具有相同使能和复位条件的小型状态寄存器组。必须作为一个行为整体进行评审的 FSM 输出或数据通路寄存器。示例always_ff (posedge i_clk or negedge i_rst_n) begin : p_cmd_pipe_reg if (!i_rst_n) begin r_cmd_valid 1b0; r_cmd_data 0; r_cmd_last 1b0; end else if (i_cmd_ready) begin r_cmd_valid i_cmd_valid; r_cmd_data i_cmd_data; r_cmd_last i_cmd_last; end end避免将互不相关的寄存器放入同一个always_ff块中。不推荐写法always_ff (posedge i_clk or negedge i_rst_n) begin : p_mixed_reg if (!i_rst_n) begin r_done 1b0; r_error_cnt 0; r_sram_addr 0; r_dbg_flag 1b0; end else begin ... end end经验原则一个always_ff块应该只有一个清晰的寄存器设计目的。8.6 注释块风格使用轻量级分节注释来划分功能模块。推荐风格// // 1. US/HS falling-edge pulse generation // // // 2. Source local clock counter // 指导原则主要逻辑块使用分节注释。注释应简短并突出功能目的。必要时使用编号。避免过度冗长的注释。优先说明设计目的而不是解释显而易见的语法。好的写法// Source local clock counter不推荐写法// This counter is used to count local source clocks for timing control operationRTL 应保持视觉上干净方便代码评审时快速浏览。9. 注释规则所有 RTL 注释必须使用英文。9.1 分节注释风格使用分节注释划分主要功能块。推荐写法// // 1. US/HS falling-edge pulse generation // // // 2. Source local clock counter // // // 3. Gamma polarity generation // 指导原则用于主要功能块。合适时使用编号。标题应简洁且有描述性。一个分节应对应一个主要功能。9.2 局部注释风格局部注释使用单行注释并紧贴在相关代码上方。推荐写法// Detect source line start pulse assign w_src_line_start_pulse ...; // Generate gamma polarity update pulse assign w_gam_pol_update ...; // Count source local clock always_ff (posedge i_clk or negedge i_rst_n) begin ... end指导原则解释目的而不是解释语法。注释保持简短。注释放在其描述代码的正上方。避免冗余注释。好的写法// Generate frame start pulse不推荐写法// This logic is used to generate a pulse when frame starts9.3 注释原则注释应描述功能目的设计意图硬件行为特殊约束避免使用只是重复 RTL 语法本身的注释。代码应说明“如何实现”。注释应说明“为什么这样设计”。10. Struct 使用规则当多个信号在功能上天然属于同一组时使用typedef struct packed。适合使用 struct 的场景流式数据总线。SRAM 请求/响应总线。控制/配置总线。状态总线。接口信号分组。示例typedef struct packed { logic [DATA_W-1:0] data; logic valid; logic last; } stream_word_t;指导原则struct 分组应小而有明确含义。避免定义过大的、不透明的 struct。必要时使用内部别名提升可读性。不要把重要控制信号隐藏得过深。11. 输出寄存器策略量产 RTL 默认优先使用寄存器输出。原因模块时序边界更清晰。降低毛刺风险。更便于 STA。更便于波形调试。更利于模块级集成。只有在有明确理由时才允许使用纯组合输出。12. case 语句规则状态机在合适场景下使用unique case。可复用 RTL 模板中必须保留default分支。default分支应将 FSM 或相关逻辑驱动到安全状态。示例default: begin next_state ST_ERROR; end13. 模块内部结构顺序RTL 模块内部建议按照以下顺序组织Timescale / 宏定义模块声明参数端口局部参数Typedef 定义可综合 function 定义仅限确有复用价值的本地工具函数内部信号声明连续赋值FSM 状态寄存器FSM 次态逻辑输出/数据通路寄存器其他时序逻辑或组合逻辑子模块例化Endmodule14. Testbench 风格对于简单 testbench使用清晰的 task 名称。使用英文注释。使用 watchdog timeout避免仿真死等。使用$error和$fatal进行检查。将复位、激励和检查逻辑分开组织。示例 task 名称do_reset(); kick_start(); send_word();15. 代码生成偏好生成新 RTL 时使用 SystemVerilog。遵守i_ / o_ / io_ / r_ / w_命名规则。FSM 状态信号使用curr_state/next_state不强制使用r_/w_前缀。可综合 function 仅用于纯组合、可复用的小型功能避免滥用 function 隐藏主流程逻辑。状态机使用三段式 FSM。枚举状态使用显式编码。默认使用寄存器输出。RTL 注释只使用英文。避免不必要的_q/_d。避免含义不清的缩写。除非明确编写 testbench否则代码应保持可综合。优先保持清晰的模块边界。优先考虑可维护性而不是最少代码行数。16. 显示驱动 / Source Driver 项目偏好对于显示驱动相关 RTL命名应优先体现功能归属。示例src_tcon_top src_tcon_power src_tcon_chop_con src_data_ucs src_data_fiaa src_data_order src_data_adaptive_sb src_sram_xbar src_remap_sram_con模块名和信号名应清楚体现其所属功能例如时序控制数据通路SRAM 控制电源控制Source AMP 控制Gamma/source choppingFIAA remapSRC_DIRDATA_ORDERAdaptive SB17. 最终原则好的 RTL 模板不是为了少写代码。而是为了让后续工程师更快理解设计、更安全地修改代码、更容易完成评审。