打印

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

[复制链接]
537|0
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
Hi 大家好
熊猫又来给大家推荐干货了
FPGA大家肯定第一反应会是难
对Verilog语言苦手的同学们
请注意了
敲黑板
这是某位工程师费九牛二虎之力整理出来的文档
有兴趣的小伙伴可以参考一下


1、代码示范

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

以下代码是我之前做的一个同步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_** ,
     input wire rreq ,
     output wire [DATA_W-1:0] rdata ,
     output wire empty_**
    );
    `ifdef DUMMY_SYNC_FIFO
    assign full_** = 1'd0;
    assign rdata = 32'd0;
    assign empty_** = 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_** = (waddr_nxt == raddr)? FULL : NOT_FULL;
    assign empty_** = (waddr == raddr)? EMPTY : NOT_EMPTY;
    assign iwreq = wreq & ~full_**;
    assign irreq = 1'd1;

    always @(posedge clk or negedge rst_n) begin
     if (!rst_n) begin
     waddr <= #DLY 0;
     end
     else if(wreq & (full_** == 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_** == 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_**));
    iError_fifo_read_overflow:
    assert property (@(posedge rclk) disable iff (!rst_n) (irreq & !empty_**));
    `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

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

2、代码风格

2.1 规则总览

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

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

2.2 规则解释

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

2.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_** ,
input wire rreq ,
output wire [DATA_W-1:0] rdata ,
output wire empty_**
);

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

2.2.2 DUMMY模块

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

`ifdef DUMMY_SYNC_FIFO
assign full_** = 1'd0;
assign rdata = 32'd0;
assign empty_** = 1'd0;
`else
...
`endif // `ifdef DUMMY_SYNC_FIFO

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

2.2.3 逻辑型信号用参数赋值

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

assign full_** = (waddr_nxt == raddr);



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

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

2.2.4 内嵌断言

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

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

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

2.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在不同模块中的例化语句都是不需要改动的。

3、总结

良好的代码风格可以提高代码的可读性,减少犯错机会,也可以提高代码调试的效率,但积累良好的代码风格不是一朝一夕的事,需要一步一个脚印,一点点积累。


免责声明:整理本文出于传播相关技术知识,版权归原作者所有。

感谢您的阅读~

可以的话,给我们的公众号来个关注呗~么么哒

以上内容来源于公众号「嵌入式ARM」,扫码关注更多


使用特权

评论回复

相关帖子

发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

17

主题

55

帖子

2

粉丝