Verilog设计中如何匹配变量的位宽?($clog2系统函数)

本文转载自:孤独的单刀博客

1、位宽太小
在FPGA设计中,我们经常需要用寄存器来寄存某些“数量类”的变量,比如FIFO的深度啦、或者计数器的最大值啦;又或者输入输出信号也需要将位宽用parameter参数化以便更好的调用等。

举个简单的小例子:系统频率100M(周期10ns),假设需要要求设计一个计时器计时100ns,那么需要计数次数为:100ns/10ns - 1 = 9,9这个数需要用多大位宽的寄存器表示呢?很简单,以2为底取对数就行,答案是最少4位宽。为了方便地复用这个模块,我们把计时时间参数化并放到模块外,如下:

module counter #(
parameter TIME = 'd10 //计时时间,单位10ns
)(
input clk_100M ,
input rst
);
reg [3:0] cnt; //计数器

//计时器
always@(posedge clk_100M)begin
if(rst)
cnt <= 0;
else if(cnt == TIME - 1)
cnt <= 0;
else
cnt <= cnt + 1;
end

endmodule

假设我们下次设计需要一个计时器的话,直接调用上面的counter模块并把TIME这个参数改成自己需要的参数就可以,这样做理论上是可以的,只是会有一个致命的隐患。不妨再假设:我现在调用了counter模块,并将TIME设置为20,以实现计时200ns的功能。当TIME = 20这个参数传递到被例化模块后,可以发现由于cnt寄存器的位宽仅为4位,其能表示的最大值为4'b1111(即十进制下的数字15),每次其到达15后就溢出为0重新开始了,也就是说这个200ns的计时器实际上根本就计数不到200ns。

这个隐患发生的原因就是在设计寄存器cnt时的位宽只有4位,无法满足“大量时间的计时任务”。

2、自己写一个Function
现在来想一下如何解决上述的位宽不匹配的问题。将寄存器的位宽设计为一个较大的数值(如固定为32bit)不失为一个不错的方法,但是如果将这条规则适用到每一个寄存器,则势必造成大量的资源浪费(你资源多你随便玩)。而且该方法指标不治本,我们需要做的是,这个寄存器应该有多大就设计多大的位宽(有多大的脚就穿多大的鞋,鞋子太大一定能穿,但你脚不一定舒服)。

前面说过寄存器的位宽的计算方法:以2为底取对数。所以我们只需要设计一个Function(可综合),来实现此项功能即可。刚好在Xilinx的许多源码都出现了这个简单的Function,我们直接拿过来用就是的:
// function 实现
function integer clogb2 (input integer bit_depth);
begin
for(clogb2=0; bit_depth>0; clogb2=clogb2+1)
bit_depth = bit_depth >> 1;
end
endfunction

// 使用案例
localparam integer C_TRANSACTIONS_NUM = clogb2(C_M_AXI_BURST_LEN-1);
reg [C_TRANSACTIONS_NUM : 0] write_index;
reg [C_TRANSACTIONS_NUM : 0] read_index;

上面的代码就是定义了一个求位宽的function,用其求得某类寄存器的位宽,然后再对寄存器赋值时就直接使用求得的位宽来赋值,这样复用起来就比较方便了。

我们将这个代码放到上面的计数器模块中后,不管需要计数多大时间,都能计算出相匹配的寄存器位宽了。

3、无法在输入输出端口使用
自己写Function实现对2取对数的功能也有一定的局限性:无法对输入输出端口信号使用该Function。Function是定义在模块内部,所以若输入输出端口也需要根据输入的parameter参数来以2为底取对数的话此种方法就无能为力了。比如:设计一个同步FIFO,输出信号fifo_cnt(计数器)是对写入FIFO的数据进行计数的寄存器,其最大值即为FIFO的深度DATA_DEPTH ,所以fifo_cnt的位宽就需要在定义模块输入输出端口时确定,显然这无法使用自己构造的 cblogb2 Function。那该当如何?
//计数器法实现同步FIFO
module sync_fifo_cnt
#(
parameter DATA_WIDTH = 'd8 , //FIFO位宽
parameter DATA_DEPTH = 'd16 //FIFO深度
)
(
input clk , //系统时钟
input rst_n , //低电平有效的复位信号
input [DATA_WIDTH-1:0] data_in , //写入的数据
input rd_en , //读使能信号,高电平有效
input wr_en , //写使能信号,高电平有效

output reg [DATA_WIDTH-1:0] data_out, //输出的数据
output empty , //空标志,高电平表示当前FIFO已被写满
output full , //满标志,高电平表示当前FIFO已被读空
output reg [$clog2(DATA_DEPTH) : 0] fifo_cnt //$clog2是以2为底取对数
);

//省略部分代码

endmodule

4、$clog2系统函数
其实办法也有,在上面的代码中也展示出来了,就是使用 $clog2 这个Verilog的系统函数。$clog2是Verilog--2005标准新增的一个系统函数,功能就是对输入整数实现以2为底取对数,其结果向上取整(如5.5取6)。有一点需要说明的是,目前Vivado2017以上的版本都是支持这个系统函数的(Quartus II不清楚 )。但是百度搜索这条结果的时候有两条结论是错误的:
1、Vivado不支持$clog2系统函数

2、$clog2系统函数在Vivado实现的是以e为底取对数,而不是2

接下来写个简单的模块验证下Vivado对$clog2系统函数的支持如何

`timescale 1ns / 1ps

module clog2_test#(
parameter integer num = 325
)
(
input clk,
input rst,
output reg [$clog2(num) - 1:0] result
);

always @(posedge clk)begin
if(rst)
result <= 0;
else
result <= result + 1;
end

endmodule

我们直接看reg result的位宽综合出来到底是多少。如果以e为底向上取整,则位宽应是6;如果以2为底向上取整,则位宽应是9。Vivado综合的原理图局部如下:


可以看到最后编译出的结果是9位的,也就说明Vivado是支持这个系统函数的(版本:2019.2)。

其他变量的位宽设计同理。

最新文章