什么是良好的Verilog代码风格?

描述

1. 前言

之前在公司负责制定代码规范,费了九牛二虎之力,终于整理出来一份文档。由于保密规定的缘故,无法与大家直接分享这份文档,但是文档中的大部分规范都是我自己长期总结出来的,在这里也与大家分享一下。

2. 代码示范

为求直观,首先贴上一份示范代码,然后我再进行逐条详细解释。

以下代码是我之前做的一个同步FIFO模块,代码如下:

001 //======================================================
002 // Copyright (C) 2015 By Kellen.Wang
003 // mail@kellen.wang, All Rights Reserved
004 //======================================================
005 // Module : sync_fifo
006 // Author : Kellen Wang
007 // Contact : kellen.wang124@gmail.com
008 // Date : Jan.17.2015
009 //=======================================================
010 // Description :
011 //========================================================
012 module sync_fifo #(
013  parameter DEPTH = 32,
014  parameter DATA_W = 32
015 ) (
016  input wire clk ,
017  input wire rst_n ,
018  input wire wreq ,
019  input wire [DATA_W-1:0] wdata ,
020  output wire full_flg ,
021  input wire rreq ,
022  output wire [DATA_W-1:0] rdata ,
023  output wire empty_flg
024 );
025 `ifdef DUMMY_SYNC_FIFO
026 assign full_flg = 1'd0;
027 assign rdata = 32'd0;
028 assign empty_flg = 1'd0;
029 `else
030 `include "get_width.inc"
031 //====================================================
032 // Constant Definition :
033 //===================================================
034 localparam DLY = 1'd1;
035 localparam FULL = 1'd1;
036 localparam NOT_FULL = 1'd0;
037 localparam EMPTY = 1'd1;
038 localparam NOT_EMPTY = 1'd0;
039 localparam ADDR_W = get_width(DEPTH-1);
040 //==================================================
041 // Variable Definition :
042 //==================================================
043 reg [ADDR_W-1:0] waddr;
044 reg [ADDR_W-1:0] raddr;
045 wire [ADDR_W-1:0] waddr_nxt;
046 wire [ADDR_W-1:0] raddr_nxt;
047 //==================================================
048 // Logic Design :
049 //==================================================
050 assign waddr_nxt = waddr + 1;
051 assign raddr_nxt = raddr + 1;
052 assign full_flg = (waddr_nxt == raddr)? FULL : NOT_FULL;
053 assign empty_flg = (waddr == raddr)? EMPTY : NOT_EMPTY;
054 assign iwreq = wreq & ~full_flg;
055 assign irreq = 1'd1;
056  
057 always @(posedge clk or negedge rst_n) begin
058  if (!rst_n) begin
059  waddr <= #DLY 0;
060  end
061  else if(wreq & (full_flg == NOT_FULL)) begin
062  waddr <= #DLY waddr_nxt;
063  end
064 end
065  
066 always @(posedge clk or negedge rst_n) begin
067  if (!rst_n) begin
068  raddr <= #DLY 0;
069  end
070  else if(rreq & (empty_flg == NOT_EMPTY)) begin
071  raddr <= #DLY raddr_nxt;
072  end
073 end
074  
075 //synopsys translate_off
076 `ifdef DEBUG_ON
077 iError_fifo_write_overflow:
078 assert property (@(posedge wclk) disable iff (!rst_n) (iwreq & !full_flg));
079 iError_fifo_read_overflow:
080 assert property (@(posedge rclk) disable iff (!rst_n) (irreq & !empty_flg));
081 `endif
082 //synopsys translate_on
083  
084 //====================================================
085 // Sub-Module :
086 //====================================================
087 shell_dual_ram #(
088  .ADDR_W (ADDR_W ),
089  .DATA_W (DATA_W ),
090  .DEPTH (DEPTH )
091 ) u_shell_dual_ram (
092  .wclk (clk ),
093  .write (iwreq ),
094  .waddr (waddr ),
095  .wdata (wdata ),
096  .rclk (clk ),
097  .read (irreq ),
098  .raddr (raddr ),
099  .rdata (rdata )
100 );
101 `endif // `ifdef DUMMY_SYNC_FIFO
102 endmodule

下面详细讲解一下我在进行这个模块设计的时候遵循了哪些希望向大家推荐的代码风格。

3. 代码风格

3.1 规则总览

在设计这个模块的时候,我主要遵从了以下几条规则:

  1. Verilog2001标准的端口定义

  2. DUMMY模块

  3. 逻辑型信号用参数赋值

  4. 内嵌断言

  5. memory shell

3.2 规则解释

接下来我们逐一解释以下为什么要这么做。

3.2.1 Verilog2001标准的端口定义
01 module sync_fifo #(
02  parameter DEPTH = 32,
03  parameter DATA_W = 32
04 ) (
05  input wire clk ,
06  input wire rst_n ,
07  input wire wreq ,
08  input wire [DATA_W-1:0] wdata ,
09  output wire full_flg ,
10  input wire rreq ,
11  output wire [DATA_W-1:0] rdata ,
12  output wire empty_flg
13 );

相对于verilog1995的端口定义,这种定义方式将端口方向,reg或wire类型,端口位宽等信息都整合到了一起,减少了不必要的重复打字和出错几率,也使得代码长度大大缩短,非常紧凑。另外,用于控制模块编译的例化参数都被放置于端口定义之前,有利于在模块例化时进行配置,也是IP化模块最好的编写方式。例如在这个同步fifo设计中,我希望这个模块的深度和数据位宽是可以配置的,那么我就把这2个参数放在端口声明的前面。另外要说明的一点是,一旦在模块中出现了可以配置的例化参数,最好在文件头的描述部分增加有关这些参数有效值范围的说明。

3.2.2 DUMMY模块

在做项目的时候,一个大的系统会被分割成很多细小的部分,由不同的人负责,设计完成后上传到具有版本管理功能的服务器上。有时候有的人忘记在上传代码之前进行严格测试,或者根本传错了版本,就会造成其他人仿真报错。有时候我们希望用FPGA进行原型验证,但是有的模块设计根本还没有完成,而反复修改FPGA顶层文件又会显著提高版本出错的几率,最好的办法就是将这些有问题的模块临时替换成dummy模块。dummy模块不仅可以隔离问题模块,还可以显著加速仿真过程,可谓一举两得。传统上大家在完成设计之后会另外建立一个只有接口代码的空文件,例如dummy_sync_fifo.v,当需要将sync_fifo变成dummy的时候,就将文件清单中的文件名改掉,但这样的方式会增加文件,容易造成管理的混乱,反复修改文件清单显然也不是一个好的做法。我推荐的dummy方式如下所示:

1

`ifdef DUMMY_SYNC_FIFO

2

assign full_flg = 1'd0;

3

assign rdata = 32'd0;

4

assign empty_flg = 1'd0;

5

`else

6

...

7

`endif // `ifdef DUMMY_SYNC_FIFO

这里推荐的方式是在模块的顶层文件中写一个宏控制的综合控制逻辑,当DUMMY_SYNC_FIFO宏被定义的时候,综合工具就只会将整个模块综合成没有任何逻辑的dummy模块了。

3.2.3 逻辑型信号用参数赋值

很多人做RTL设计的时候为了省事,在代码中对数值型信号和逻辑型信号完全不做区分,用同样的方式赋值。如果这种时候稍微做一点点改变,就能让你的代码可读性大大提高,例如:

1 assign full_flg = (waddr_nxt == raddr);

1 localparam FULL = 1'd1;
2 localparam NOT_FULL = 1'd0;
3 assign full_flg = (waddr_nxt == raddr) ? FULL : NOT_FULL;

你觉得哪一个阅读起来更直观?而将所有逻辑型信号的数值参数化的另外一个好处,就是在如veridi这样业界良心的仿真软件中,你可以在仿真波形中直接看到FULL或NOT_FULL这样的文字参数,大大提高了波形的友好程度,比起你在那痛苦地目测这根线到底是高电平还是低电平轻松多了。

3.2.4 内嵌断言

有的IC设计工程师觉得断言是验证工程师才需要学习的东西,其实不然,好的模块内嵌断言可以及时发现模块内部的错误状态,防止模块的不当使用,极大地提高模块的验证效率。但是,断言属于不可综合的语句(在ZEBU这种变态系统中使用除外),直接放在模块设计代码中需要进行必要的特殊处理,如下所示:

1 //synopsys translate_off
2 `ifdef DEBUG_ON
3 iError_fifo_write_overflow:
4 assert property (@(posedge wclk) disable iff (!rst_n) (iwreq & !full_flg));
5 iError_fifo_read_overflow:
6 assert property (@(posedge rclk) disable iff (!rst_n) (irreq & !empty_flg));
7 `endif
8 //synopsys translate_on

首先使用了综合指令的注释synopsys translate_off以防综合工具对这段语句进行综合,然后再加上一个DEBUG_ON的宏进行二次保护。上例中的断言可以保证这个sync_fifo在使用过程中一旦发生“过读”或者“过写”就会立刻打印报错信息。

3.2.5 memory shell

在IC设计中经常需要用到memory,memory通常不是用verilog描述实现的(这种方式实现不是不可以,而是性价比太低了),而是需要调用FPGA里的存储资源,或是由后端生成。但是在进行仿真的时候,我们不妨用verilog写一个行为模型来替代实现。这种原型验证和仿真验证的不一致,导致了跟dummy模块设计一样的麻烦,那就是需要对代码进行反复修改。另外,在不同项目中有可能根据不同的情况采用不同的后端物理层来生成memory,或者由于不同的工艺生成不同的memory,这种memory的接口协议可能多少会有一些不一样,同样会导致需要在不同工艺和项目中修改IP代码,造成出错的风险。比较好的做法就是像以下例子中那样使用一个memory shell来隔离这种修改。

01 shell_dual_ram #(
02  .ADDR_W (ADDR_W ),
03  .DATA_W (DATA_W ),
04  .DEPTH (DEPTH )
05 ) u_shell_dual_ram (
06  .wclk (clk ),
07  .write (iwreq ),
08  .waddr (waddr ),
09  .wdata (wdata ),
10  .rclk (clk ),
11  .read (irreq ),
12  .raddr (raddr ),
13  .rdata (rdata )
14 );

这个memory shell定义了一组标准的接口,用于在IP模块中进行例化。而在这个memory shell模块内部,可使用宏控制的综合分支控制语句根据不同情况综合不同的memory或仿真模型。当同一个size的memory被多个模块调用的时候,这种设计的好处更加明显,因为当接口协议变化时,你只需要改动memory shell文件内部的连接逻辑就可以了,这个shell在不同模块中的例化语句都是不需要改动的。

4. 总结

良好的代码风格可以提高代码的可读性,减少犯错机会,也可以提高代码调试的效率,但积累良好的代码风格不是一朝一夕的事,需要一步一个脚印,一点点积累。本文长期更新,如果你有好的想法和建议,欢迎在本文底部留言。另外也欢迎其他verilog语言学习者与我共同交流,有任何疑问可以到本博“答疑专区”提出,我必知无不言,言无不尽。

​​审核编辑 :李倩


打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分