SystemVerilog 代码风格指南
前言
代码被阅读的频率远远超过编写的频率。在团队中保持一致的编码风格能够显著提升代码的可读性,这是节省工程时间最有效(也是最简单)的方法之一。
在众多编程语言中,Python 可以说是最优雅的。阅读他人编写的 Python 代码非常轻松,即使是复杂的代码逻辑也不会让人望而却步。更重要的是,初学者编写的代码与核心开发者的代码在风格上高度一致。这主要归功于 PEP8 这一 Python 代码风格指南,整个社区对这份文档的采纳程度令人惊叹。
本风格指南借鉴了 PEP8 的成功经验和部分结构,同时结合了 UVM 库的最佳实践,避免重复造轮子。
PEP8 核心理念
风格指南的本质在于一致性。遵循本指南的一致性很重要,项目内部的一致性更重要,单个模块或功能内的一致性是最重要的。
但要知道何时打破常规——有时风格指南的建议并不适用。遇到疑惑时,请运用最佳判断。
代码布局规范
缩进规则
基本原则: 每个缩进级别使用 4 个空格。
✅ 推荐写法:
// 参数换行时,第二行从函数名下方开始
foo = long_function_name(var_one, var_two, var_three,var_four);// 或者与第一个参数对齐(4空格缩进可选)
foo = long_function_name(var_one, var_two,var_three, var_four);// 函数声明中增加额外缩进,区分函数体
function void long_function_name(var_one, var_two,var_three, var_four);int x;// ...函数体...
endfunction: long_function_name// 条件表达式换行时增加缩进
if (expr_one && expr_two &&expr_three) begindo_something();
end
❌ 不推荐写法:
// 第二行参数位置不当
foo = long_function_name(var_one, var_two,var_three, var_four);// 函数声明缩进不清晰,容易与函数体混淆
function void long_function_name(var_one, var_two,var_three, var_four);int x;// ...
endfunction: long_function_name
制表符与空格的选择
推荐使用空格进行缩进。 制表符仅在与已有制表符缩进的代码保持兼容时使用。
编辑器配置示例:
Vi/Vim 配置:
" 将以下内容添加到 ~/.vimrc
set tabstop=4
set shiftwidth=4
set expandtab
Emacs 配置:
; 将以下内容添加到 ~/.emacs
(setq-default indent-tabs-mode nil)
(setq-default tab-width 4)
(setq indent-line-function 'insert-tab)
最大行长度限制
建议将所有行(包括注释)限制为最多 100 个字符。
传统建议是 80 个字符,但考虑到 UVM 的长宏定义特点:
`uvm_object_utils
`uvm_info(get_name(), "detailed message here", UVM_MEDIUM)
在这些声明中,仅宏和函数名就占用了约 30 个字符。如果严格遵循 80 字符限制,会频繁遇到换行困扰。因此,100 字符的限制更为实用和合理。
重要提醒: 确保换行时进行适当的缩进对齐。
begin & end 语句块
begin
与它所属的块语句在同一行end
独占一行
✅ 推荐写法:
always_ff @(posedge clk) begin// 逻辑代码
endif (big_endian == 1) beginm_bits[count+i] = value[size-1-i];
end
else beginm_bits[count+i] = value[i];
endfor (int i = 0; i < size; i++) beginif (big_endian == 1) beginm_bits[count+i] = value[size-1-i];endelse beginm_bits[count+i] = value[i];end
end
if & else 条件语句
else
从新行开始
✅ 推荐写法:
if (big_endian == 1) beginm_bits[count+i] = value[size-1-i];
end
else beginm_bits[count+i] = value[i];
end
❌ 不推荐写法:
if (big_endian == 1) beginm_bits[count+i] = value[size-1-i];
end else beginm_bits[count+i] = value[i];
end
强烈建议: 始终与条件语句一起使用 begin/end
。不使用花括号是产生 bug 的温床。
❌ 危险的写法:
// 避免这种写法
if (big_endian == 1)m_bits[count+i] = value[size-1-i];
elsem_bits[count+i] = value[i];// 特别要避免嵌套时不用花括号:
// 虽然代码能正常工作,但其他人可能会在 else 后添加代码
// 并误以为会在 else 条件下执行,从而引入难以发现的 bug
for (int i = 0; i < size; i++)if (big_endian == 1)m_bits[count+i] = value[size-1-i];elsem_bits[count+i] = value[i];
空行使用原则
- 用空行包围类、函数和任务定义
- 相关的单行代码之间可以省略空行
- 在函数和任务中谨慎使用空行来区分逻辑段落
空格使用规范
函数与任务调用
基本规则: 函数名与左括号之间不加空格,左括号与第一个参数之间也不加空格。
✅ 推荐写法:
function void foo(x, y, z);
foo(x, y, z);
❌ 不推荐写法:
function void foo (x, y, z);
foo (x, y, z);
foo( x, y, z );
默认参数处理: 默认参数值的等号周围不加空格。
✅ 推荐写法:
function void foo(name="foo", x=1, y=20);
❌ 不推荐写法:
function void foo(name = "foo", x = 1, y = 20);
赋值和运算符规范
基本原则: 不要为了对齐而在赋值运算符周围添加多余的空格。
✅ 推荐写法:
x = 1;
y = 2;
long_variable = 3;
❌ 不推荐写法:
x = 1;
y = 2;
long_variable = 3;
运算符空格规则: 以下二元运算符两侧应该始终保持空格:
- 赋值操作符:
=
- 复合赋值:
+=
,-=
,*=
,/=
等 - 比较操作符:
==
,===
,<
,>
,!=
,!==
,<=
,>=
- 逻辑操作符:
&
,&&
,|
,||
// 正确的操作符使用
result = (a + b) * c;
if (count >= max_value && enable == 1'b1) beginstatus += increment_value;
end
循环和条件语句
基本规则:
if
,for
,while
等关键字与左括号之间加一个空格- for 循环中的各个部分之间保持空格:
int i = 0; i < 10; i++
✅ 推荐写法:
if (x == 10)
for (int ii = 0; ii < 20; ii++)
while (condition_true)
❌ 不推荐写法:
if(x == 10)
if( x == 10 )
for(int ii=0;ii<20;ii++)
复合语句: 通常不建议在同一行写多个语句。
❌ 不推荐写法:
if (foo == 1) $display("bar");
always 块的格式
✅ 推荐写法:
always_ff @(posedge clk) begin// 时序逻辑
endalways_comb begin// 组合逻辑
end
注释规范
关于注释的重要提醒
与代码相矛盾的注释比没有注释更糟糕。当代码更改时,始终优先保持注释的及时更新!
注释基本规范:
- 注释应该是完整的句子
- 如果注释是短语或句子,第一个单词应大写(除非是小写标识符)
- 短注释可以省略末尾的句点
- 块注释通常由完整句子组成,每句都应以句点结尾
版权横幅
对于许可/版权横幅,请使用以下样式注释块:
/************************************************************************ Copyright 2007-2011 Mentor Graphics Corporation* Copyright 2007-2010 Cadence Design Systems, Inc.* Copyright 2010 Synopsys, Inc.* Copyright 2013 NVIDIA Corporation* All Rights Reserved Worldwide** Licensed under the Apache License, Version 2.0 (the* "License"); you may not use this file except in* compliance with the License. You may obtain a copy of* the License at** http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in* writing, software distributed under the License is* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR* CONDITIONS OF ANY KIND, either express or implied. See* the License for the specific language governing* permissions and limitations under the License.**********************************************************************/
文档字符串
Docstring(文档字符串)是位于文件顶部的注释,提供该文件中代码功能的高级描述。将文档字符串放在版权横幅的正后方。不要将它们混合在一起。对此段使用以下样式:
* Ending of copyright banner**********************************************************************/
/** Module `ABC`** This is the 1st paragraph. Separate paragraphs with* an empty line with just the ` *` character.** This is the 2nd paragraph. Do not fence the docstring* in a banner of `*****`. Only the copyright segment* above the docstring gets a fence.*/
块注释
块注释通常适用于其后面的某些(或全部)代码,并与该代码保持相同的缩进级别。
块注释的每一行都以 //
和一个空格开头(除非是注释内的缩进文本)。块注释中的段落由包含单个 //
的行分隔。
你也可以使用多行块注释格式 /* */
:
// 这是块注释的第一行
// 这是第二行
//
// 这是块注释的第二段/* * 这种注释描述了* 下面代码的功能*/foo = bar + 1;
内联注释
避免使用内联注释,它们会影响代码的整洁性:
// 不要这样做
x = x + 1; // 增加数据包计数
注释使用建议
为了赶上项目截止日期,注释往往会被忽略。但当你在一段时间后重新审视代码时,总是会后悔这个决定。因此,现在花一点时间写注释,能为将来省去很多痛苦。未来的你会感谢现在的你。
避免使用注释围栏,例如:
/***********************/
//######################
//////////////
这些围栏会让代码显得杂乱,实际帮助有限。良好的块注释就能提供清晰的代码分隔效果。只有版权横幅应该使用围栏格式。
命名约定
为了统一理解,我们先定义几种常见的命名约定:
- PascalCase - 每个单词的第一个字母都大写
- camelCase - 每个单词的第一个字母(除第一个单词外)均大写
- lowercase_with_underscores - 小写字母加下划线
- UPPERCASE_WITH_UNDERSCORES - 大写字母加下划线
文件名
文件名应使用 lowercase_with_underscore
crc_generator.sv
tb_defines.svh
module_specification.docx
input_message_buffer.sv
类和模块
类和模块名称应使用 lowercase_with_underscore
。如果文件中只有一个类或模块,则其名称应与文件名相同。
class packet_parser_agent;
endclass: packet_parser_agentmodule packet_parser_engine;
endmodule: packet_parser_engine
类实例应被视为变量,并应使用 lowercase_with_underscore
格式。模块实例应使用纯驼峰命名法,不带任何下划线。
// Class
packet_parser_agent parser_agent;
parser_agent = new();// Module
packet_parser_engine ppe0(.*);
packet_parser_engine packetParserEngine4a(.*);
packet_parser_engine packetParserEngine4b(.*);
接口
- 接口定义使用
lowercase_with_underscores
以 "_io" 结尾 - 接口实例以 "_if" 结尾
- clocking 块使用
camelCase
- modport 最好只是一个词
lowercase
interface bus_io(input bit clk);logic vld;logic [7:0] addr, data;clocking ioDrv @posedge(clk);input addr;output vld;output data;endclocking: ioDrvmodport dut(input addr, output vld, data);modport tb(clocking ioDrv);
endinterface: bus_iomodule tb_top;bus_io bus_if(clk);
endmodule: tb_top
变量
变量名称应始终使用 lowercase_with_underscore
ethernet_agent eth_agent;
int count_packets, count_errors;
logic [15:0] some_long_var;
如有必要,使用前缀轻松识别和分组变量:
logic [31:0] pe_counter_0;
logic [31:0] pe_counter_1;
logic [31:0] pe_counter_2;
结构、联合和枚举
typedef
所有结构、联合和枚举。他们应该使用驼峰命名法,并具有以下区别:
- 结构体以
_s
结尾 - 联合以
_u
结尾 - 枚举以
_e
结尾。此外,枚举应使用UPPERCASE_WITH_UNDERSCORES
typedef struct packed {logic [47:0] macda;logic [47:0] macsa;logic [15:0] etype;
} ethPacket_s;typedef union packed {logic [15:0] tx_count;logic [15:0] rx_count;
} dataPacketCount_u;typedef enum logic [1:0] {IPV4_TCP,IPV4_UDP,IPV6_TCP,IPV6_UDP
} packetType_e;
类型变量名称
类型变量名称应为 UPPERCASE
,最好只有一个单词。
// Following examples were extracted from the UVM code base.
// The file path where they can be found is also mentioned.// tlm1/uvm_exports.svh
class uvm_get_peek_export #(type T=int);
class uvm_blocking_master_export #(type REQ=int, type RSP=REQ);// base/uvm_traversal.svh
virtual class uvm_visitor_adapter #(type STRUCTURE=uvm_component,VISITOR=uvm_visitor#(STRUCTURE)) extends uvm_object;
宏命名规范
宏的命名需要根据用途进行区分:
- 函数/任务宏: 使用 UPPERCASE 宏名称和 lowercase 参数名称
- 类/代码片段宏: 使用 lowercase 宏名称和 UPPERCASE 参数名称
- 单词分隔: 统一使用下划线分隔
// 函数/任务宏:UPPERCASE 宏名 + lowercase 参数
`define PRINT_BYTES(arr, startbyte, numbytes) \function print_bytes(logic[7:0] arr[], int startbyte, int numbytes); \for (int ii=startbyte; ii<startbyte+numbytes; ii++) begin \if ((ii != 0) && (ii % 16 == 0)) \$display("\n"); \$display("0x%x ", arr[ii]); \end \endfunction: print_bytes// 类定义宏:lowercase 宏名 + UPPERCASE 参数
`define uvm_analysis_imp_decl(SFX) \class uvm_analysis_imp``SFX #(type T=int, type IMP=int) \extends uvm_port_base #(uvm_tlm_if_base #(T,T)); \`UVM_IMP_COMMON(`UVM_TLM_ANALYSIS_MASK,`"uvm_analysis_imp``SFX`",IMP) \function void write( input T t); \m_imp.write``SFX( t); \endfunction \endclass// 代码片段宏:lowercase 宏名 + UPPERCASE 参数
`define uvm_create_on(SEQ_OR_ITEM, SEQR) \begin \uvm_object_wrapper w_; \w_ = SEQ_OR_ITEM.get_type(); \$cast(SEQ_OR_ITEM , create_item(w_, SEQR, `"SEQ_OR_ITEM`"));\end
扩展阅读
想了解更多 SystemVerilog 宏的使用技巧,可以参考宏使用详细指南。
结束标识符
在适用的情况下始终使用结束标识符:
endclass: driver_agent
endmodule: potato_block
endinterface: memory_io
endtask: cowboy_bebop
编程实践建议
由于 SystemVerilog 横跨设计和验证两个领域,拥有丰富的语言特性。对于本风格指南中未明确提及的其他语言结构,如断言、覆盖率、约束、时序控制等,都可以参照以上原则进行相应扩展。
结语
编写优雅的代码并非易事。在紧张的项目交付期限和繁重的工作任务中,很难有精力去关注、回顾、重构和优化匆忙产生的代码。在这样的时刻,一些前辈的智慧话语能够帮助我们坚持编写优雅代码的初心:
让我们改变对程序构建的传统态度:与其想象我们的主要任务是指示计算机做什么,不如让我们专注于向人类解释我们希望计算机做什么。
— 唐纳德·克努斯
所以,漂亮的代码是清晰的,易于阅读和理解;它的组织、形状、架构和声明性语法一样揭示了意图。每个小部分都是连贯的,其目的是单一的,尽管所有这些小部分都像复杂马赛克的碎片一样组合在一起,但当一个元素需要更改或替换时,它们很容易分开。
— 维克拉姆·钱德拉 (Vikram Chandra) 作者《Geek Sublime》
参考资料
- UVM 源代码
- PEP8 - Python 代码风格指南
- PEP7 - C 代码风格指南
- 《极客崇高》- 维克拉姆·钱德拉
- 《美丽的代码》- 安迪·奥拉姆、格雷格·威尔逊
- 空白大辩论