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

内容纲要

1. 前言

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

2. 代码示范

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

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

//==============================================================================
// Copyright (C) 2015 By Kellen.Wang
// mail@kellen.wang, All Rights Reserved
//==============================================================================
// Module : sync_fifo
// Author : Kellen Wang
// Contact : kellen.wang124@gmail.com
// Date : Jan.17.2015
//==============================================================================
// Description :
//==============================================================================
module sync_fifo #(
 parameter DEPTH = 32,
 parameter DATA_W = 32
) (
 input wire clk ,
 input wire rst_n ,
 input wire wreq ,
 input wire [DATA_W-1:0] wdata ,
 output wire full_flg ,
 input wire rreq ,
 output wire [DATA_W-1:0] rdata ,
 output wire empty_flg
);
`ifdef DUMMY_SYNC_FIFO
assign full_flg = 1'd0;
assign rdata = 32'd0;
assign empty_flg = 1'd0;
`else
`include "get_width.inc"
//==============================================================================
// Constant Definition :
//==============================================================================
localparam DLY = 1'd1;
localparam FULL = 1'd1;
localparam NOT_FULL = 1'd0;
localparam EMPTY = 1'd1;
localparam NOT_EMPTY = 1'd0;
localparam ADDR_W = get_width(DEPTH-1);
//==============================================================================
// Variable Definition :
//==============================================================================
reg [ADDR_W-1:0] waddr;
reg [ADDR_W-1:0] raddr;
wire [ADDR_W-1:0] waddr_nxt;
wire [ADDR_W-1:0] raddr_nxt;
//==============================================================================
// Logic Design :
//==============================================================================
assign waddr_nxt = waddr + 1;
assign raddr_nxt = raddr + 1;
assign full_flg = (waddr_nxt == raddr)? FULL : NOT_FULL;
assign empty_flg = (waddr == raddr)? EMPTY : NOT_EMPTY;
assign iwreq = wreq & ~full_flg;
assign irreq = 1'd1;

always @(posedge clk or negedge rst_n) begin
 if (!rst_n) begin
 waddr <= #DLY 0;
 end
 else if(wreq & (full_flg == NOT_FULL)) begin
 waddr <= #DLY waddr_nxt;
 end
end

always @(posedge clk or negedge rst_n) begin
 if (!rst_n) begin
 raddr <= #DLY 0;
 end
 else if(rreq & (empty_flg == NOT_EMPTY)) begin
 raddr <= #DLY raddr_nxt;
 end
end

//synopsys translate_off
`ifdef DEBUG_ON
iError_fifo_write_overflow:
assert property (@(posedge wclk) disable iff (!rst_n) (iwreq & !full_flg));
iError_fifo_read_overflow:
assert property (@(posedge rclk) disable iff (!rst_n) (irreq & !empty_flg));
`endif
//synopsys translate_on 

//==============================================================================
// Sub-Module :
//==============================================================================
shell_dual_ram #(
 .ADDR_W (ADDR_W ),
 .DATA_W (DATA_W ),
 .DEPTH (DEPTH )
) u_shell_dual_ram (
 .wclk (clk ),
 .write (iwreq ),
 .waddr (waddr ),
 .wdata (wdata ),
 .rclk (clk ),
 .read (irreq ),
 .raddr (raddr ),
 .rdata (rdata )
);
`endif // `ifdef DUMMY_SYNC_FIFO
endmodule

由于博客刚刚开通,代码高亮似乎还调得不是很好,大家先将就着看好了。下面详细讲解一下我在进行这个模块设计的时候遵循了哪些希望向大家推荐的代码风格。

3. 代码风格

3.1 规则总览

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

  • Verilog2001标准的端口定义
  • DUMMY模块
  • 逻辑型信号用参数赋值
  • 内嵌断言
  • memory shell

    3.2 规则解释

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

    3.2.1 Verilog2001标准的端口定义

module sync_fifo #(
    parameter DEPTH = 32,
    parameter DATA_W = 32
) (
    input wire clk ,
    input wire rst_n ,
    input wire wreq ,
    input wire [DATA_W-1:0] wdata ,
    output wire full_flg ,
    input wire rreq ,
    output wire [DATA_W-1:0] rdata ,
    output wire empty_flg
);

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

3.2.2 DUMMY模块

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

`ifdef DUMMY_SYNC_FIFO
assign full_flg = 1'd0;
assign rdata = 32'd0;
assign empty_flg = 1'd0;
`else
...
`endif //DUMMY_SYNC_FIFO

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

3.2.3 逻辑型信号用参数赋值

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

assign full_flg = (waddr_nxt == raddr);

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

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

3.2.4 内嵌断言

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

//synopsys translate_off
`ifdef DEBUG_ON
iError_fifo_write_overflow:
assert property (@(posedge wclk) disable iff (!rst_n) (iwreq & !full_flg));
iError_fifo_read_overflow:
assert property (@(posedge rclk) disable iff (!rst_n) (irreq & !empty_flg));
`endif
//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来隔离这种修改。

shell_dual_ram #(
    .ADDR_W (ADDR_W ),
    .DATA_W (DATA_W ),
    .DEPTH (DEPTH )
) u_shell_dual_ram (
    .wclk (clk ),
    .write (iwreq ),
    .waddr (waddr ),
    .wdata (wdata ),
    .rclk (clk ),
    .read (irreq ),
    .raddr (raddr ),
 .rdata (rdata )
);

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

4. 总结

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

工程师,个人公众号:IF推理同好会,个人微信小游戏:IF迷你侦探
文章已创建 32

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部