您的位置:首页 > 编程语言

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

2018-03-21 22:51 423 查看

http://kellen.wang/zh/blog/2015/03/03/what-is-good-verilog-coding-style/

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 规则总览

在设计这个模块的时候,我主要遵从了以下几条规则:Verilog2001标准的端口定义
DUMMY模块
逻辑型信号用参数赋值
内嵌断言
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语言学习者与我共同交流,有任何疑问可以到本博“答疑专区”提出,我必知无不言,言无不尽。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: