verilog的编码风格

本文

主要介绍了一些verilog的编码风格准则。

版本 说明
0.1 初版发布

参考

写在前头

本文主要从三个角度来对verilog编码风格进行说明:

  • 利于模拟的编码风格:可以加快模拟速度或减少模拟占用内存。
  • 利于综合的编码风格:可以减少综合前后模拟不一致问题。
  • 利于维护的编码风格:有利于设计的维护、交互与修改,提高设计效率。

利于模拟的编码风格

  • 多条件下尽量使用 case 语句,不使用嵌套 if-else if-else 语句,两者占用内存差不多,但模拟速度 case 比 if 要快。
  • 不使用多余的 begin-end, always 或 if 语句中只有一条语句,是不需要使用 begin-end 的,多余的 begin-end 会占用更多内存和减缓模拟速度。
  • 时钟信号产生,使用 always 语句描述模拟速度更快,如下代码:
1
2
3
reg clk;
initial clk = 0;
always #(`CYCLE/2) clk = ~clk;
  • 尽量避免将赋值语句分散到多个 always 块,如寄存器的赋值,建议尽量将多个寄存器描述在一个 always 块语句,而不是将每个寄存器都用一个 always 块语句来描述,这样可以减少内存和提高模拟速度。
  • 尽量减少模块的端口数,模块端口数越多,模拟速度越慢,因此需要对设计合理划分模块。
  • 加载测试向量时,避免在时钟的上下沿变化,而是在延时一个时间单位后,这样更能够模拟真实电路的行为,为了便于维护,可以定义CYCLE数值,延时单位采用 “0.1*`CYCLE” 形式。
  • 显示非阻塞赋值语句的变量值,使用 $strobe ,而不是 $display 和 $write 。下面介绍一下两者的区别,举例如下:
1
2
3
4
5
6
7
initial
begin
a = 0;
$display(a);          // displays 0
$strobe(a);           // displays 1 ...
a = 1;                // ... 因为这条语句
end

可见 $strobe 是在整个块语句结束后执行的,而 $display 是在程序位置执行的。这里是一个顺序执行的块语句,而对于非阻塞赋值语句不是顺序执行的,所以在需要显示非阻塞赋值语句的变量值时,使用 $strobe ,确保能够取到稳定的赋值后的值。

以上是公认的编码原则,强烈建议遵循,而下面内容,仅供参考:

  • 尽量减少设计层次,层次越多模拟速度越慢。
  • parameter 比 define 更占用内存,但模拟速度相当。
  • `timescale 精度越高,模拟速度越慢。
  • $time 等系统调用程序非常影响模拟速度,可以使用但不要滥用。
  • 不建议设计文件中写 `timescale ,建议写在单独文件,统一进行 `include ,或在仿真工具中参数化输入。

利于综合的编码风格

  • 顶层模块不要有粘连逻辑,顶层模块甚至次顶层模块只用来例化和连接子模块,这样不仅可以节省顶层的编译时间,还有利于布局布线的实施。
  • 减少不必要的设计层次,更多的层次会降低综合性能,当然也可以先使用工具将设计层次铺平,如 dc 的 ungroup 。
  • 组合逻辑与时序逻辑尽量分开描述,换句话说,寄存器always语句描述中应该只有赋值,而无其他运算。
  • 将不同类的逻辑分散到不同模块,这样综合工具可以针对不同的逻辑类型采用适当的优化技术。
  • 相关的组合逻辑不要拆散到不同模块,否则会限制综合工具的优化力度,好的设计就是所有输出都是寄存器输出,对于组合逻辑分散到不同模块,虽然通过工具在综合前将设计铺平的方法可以缓解问题,但作为设计师还是尽量遵循此原则。
  • 所有输出都是寄存器,这样所有组合逻辑都在前端,最后寄存输出,这也是所说的 “cloud-register原则” ,这样既可以得到更好的综合结果,而且简化了综合的约束设计。
  • 敏感列表要完整,当然现在可以使用*代替,或者使用 SystemVerilog 的 always_comb 。
  • 注意阻塞赋值的语句顺序。
  • 避免 if/case 条件不完整,否则会产生 latch,可以使用 Systemverilog 中的 always_comb、always_latch、always_ff 语句,编译时会严格检查,也可以在块描述语句 if/case 前对信号赋初值。
  • 用 always 语句描述组合逻辑使用阻塞赋值 = 。
  • 描述 latch 时使用阻塞赋值 = 。
  • 描述时序逻辑使用非阻塞赋值 <= 。
  • 在同一个块语句中不要混用两种赋值方式。
  • 不要使用 #0 延迟赋值。
  • FSM 中的状态名应该使用 parameter。
  • FSM 计算下一状态的组合逻辑应该放在一个单独的 always 块。
  • FSM 计算下一状态的组合逻辑应该使用 case 语句。
  • 每个 FSM 使用单独的 module 描述。

利于维护的编码风格

  • 每个项目都使用相同的目录结构,相同的目录结构有利于脚本移植,以及增加复用性和可维护性,同时利于工作交接。
  • 项目中每个设计都保持类似的目录结构,理由同上。
  • 项目内脚本尽量使用相对路径,需要全路径名称则使用环境变量。
  • 对 `include 加载的文件尽量使用完整相对路径,方便设计的集成和移植,或者对所有设计的 include 文件规范命名规则。
  • 每个文件只包含一个 module 。
  • 文件名和模块名保持一致。
  • 测试文件名与被测试模块名相对应,添加 _tb 后缀。
  • 每个文件都添加头信息:版权声明、文件描述、创建时间、修改时间、原始作者信息、当前作者信息。
  • 注释描述代码功能,而不是行为。
  • 每个 parameter 声明后要注释。
  • function 要加注释。
  • 每个主要逻辑段落要加注释。
  • 信号名要有实际意义,并且输入输出信号名信息中要包含输入输出模块。
  • 废代码删掉,而不是注释。
  • 每个模块逻辑代码尽量不要超过400行,可读性可调试性强。
  • 每级缩进建议四个空格,并且不建议使用TAB缩进,不同编辑器对TAB支持不同,可能会导致代码显示乱掉。
  • 每行最多72个字符,在恰当的地方换行,并保持对齐。
  • 控制流嵌套不超过三级。
  • 每行只写一条语句。
  • 连续声明语句对齐,并且每行只声明一个。
  • 句尾的注释尽量对齐。
  • 多次使用的逻辑使用 function 。
  • 操作符周围使用一个空格与其他项隔开。
  • 所有信号名尽量使用小写字母,并且用下划线将单词分隔开。
  • 常数定义和宏定义使用大写字母。
  • 模块的名字要体现层次结构。
  • 模块的例化名与模块名基本保持一致,不过例化名可不体现层次结构,加 “i_” 或 “inst_” 前缀。
  • 信号名要有意义,至少5个字符。
  • 模块根据功能命名,不要依据类型或作者命名。
  • 项目内命名要规范,避免模块重名。
  • 根据功能或逻辑顺序声明端口,而不是方向。
  • 低电平有效信号加后缀 “_n” ,如复位信号 “rst_n” 。
  • 建议时钟信号统一加前置 “clk_” 。
  • 建议复位信号统一加前缀 “rst_” 。
  • 建议寄存器信号统一加后缀 “_ff” ,下一拍寄存器加载的值信号添加后缀 “_nxt” 。
  • 总线范围定义为 “N-1:0” 。

文章原创,可能存在部分错误,欢迎指正,联系邮箱 cao_arvin@163.com。