Menu Close

Testbench 编写进阶(4)–数学函数及$clog2(x)

上两节内容我们讲解了在Testbench中应用最广泛的$display函数和$monitor过程,还有其他函数虽然应用较少,但如果能合理使用这些函数在比较复杂的设计中可以简化testbench的编写,仿真中的错误的定位会有很大的帮助,下面我们就来介绍这些函数的使用。

1.数学函数

在数学运算的建模中,充分利用仿真库中提供的数学函数,为行为建模和仿真提供了方便。

注意:这些函数只能用来仿真,不能综合。

function

函数

Description 描述
$ln(x) Natural logarithm log(x) 自然对数In(x)
$log10(x) Decimal Logarithm log10(x) 10为底的对数
exp(x) Exponential of x (ex) where e=2.718281828… 自然指数,e=2.718281828…
sqrt(x) Square root of  x 平方根
$pow(x, y) xy X 的y次幂
$floor(x) Floor (x) 求底函数
$ceil(x) Ceiling( x) 求顶函数
$sin(x) Sine of x where x is in radians 正弦函数
$cos(x) Cosine of x where x is in radians 余弦函数
$tan(x) Tangent of x where x is in radians 正切函数
$asin(x) Arc-Sine of x 反正弦函数
$acos(x) Arc-Cosine of x 反余弦函数
$atan(x) Arc-tangent of x 反正切函数
$atan2(x, y) Arc-tangent of (x/y) x/y的反正切
$hypot(x, y) Hypotenuse of x and y : sqrt(x2 + y2) 直角三角形斜边函数
$sinh(x) Hyperbolic Sine of  x 双曲正弦函数
$cosh(x) Hyperbolic-Cosine of x 双曲余弦函数
$tanh(x) Hyperbolic-Tangent of x 双曲正切函数
$asinh(x) Arc-hyperbolic Sine of x 反双曲正弦函数
$acosh(x) Arc-hyperbolic Cosine of x 反双曲余弦函数
$atanh(x) Arc-hyperbolic tangent of x 反双曲正切函数

$ceil(x) 函数返回实数X最大的整数部分,如:x=2.33, 则$ceil(x)=3; x=2.99, $ceil(x)=3;

$floor(x)函数返回实数x的最小整数部分,如:x=2.33, 则$floor(x)=2; x=2.99, $floor(x)=2;

其它函数数学意义比较明确,这里不再累述。

例:

`timescale 1ns / 1ps


module test_bench(
    );
    
real x, y;
initial 
begin
    x = 1000;
    $display("$log10(%0.3f) = %0.3f", x, $log10(x));

    x = 1;
    $display("$ln(%0.3f) = %0.3f", x, $ln(x));
    
    x = 2;
    $display("$exp(%0.3f) = %0.3f", x, $exp(x));
    
    x = 16;
    $display("$sqrt(%0.3f) = %0.3f", x, $sqrt(x));
    
    x = 4;
    y = 3;
    $display("$pow(%0.3f, %0.3f) = %0.3f", x, y, $pow(x,y));
    
    x = 3.542;
    $display("$floor(%0.3f) = %0.3f", x, $floor(x));
    
    x = 13.2548;
    $display("$ceil(%0.3f) = %0.3f", x, $ceil(x));
    
    x = 30 * 3.1415926/ 180;
    $display("$sin(%0.3f) = %0.3f", x, $sin(x));

    x = 45 * 3.1415926/ 180;
    $display("$cos(%0.3f) = %0.3f", x, $cos(x));

    x = 45 * 3.1415926/ 180;
    $display("$tan(%0.3f) = %0.3f", x, $tan(x));
    
    x = 0.5;
    $display("$asin(%0.5f) = %0.5f rad, %0.5f deg", x, $asin(x),$asin(x)*7.0/22.0 * 180);
    
    x = 0;
    $display("$acos(%0.5f) = %0.5f rad, %0.5f deg", x, $acos(x),$acos(x)*7.0/22.0 * 180);
    
    x = 1;
    $display("$atan(%0.5f) = %0.5f rad, %0.5f deg", x, $atan(x),$atan(x)*7.0/22.0 * 180);
    
end


endmodule











 

仿真结果如下:

数学函数在Verilog程序设计中的作用:

  • 行为建模,在实体模块成功开发之前利用数学函数可以评估系统的功能。
  • 为设计实体提供输入,特别是采用定点或浮点小数运算时需要提供各种形式的输入,在基于FPGA的DSP开发中应用很广泛。
  • 结果对比;实体程序实现的数学算法,利用数学函数的结果与实体程序的输出做对比,可以评估结果是否正确,以及结果的精度是否满足要求。

以上的函数将会在后续课程中结合具体实例进行讲解。

还有一个函数$clog2也被广泛使用,下面将详细讲解该函数的使用。

2. $clog2函数

该函数可以认为是由两个函数构成,$log2 (以2为底的log函数)+  $ceil ()。 即先求$log2, 再求$ceil, 也可以可以理解为 $ceil($log2(x))。例如: x=6, 则y=$clog2(x)的结果y=3。这个函数对于求位宽非常有用。注意:$clog2(x)函数是可综合的函数。$clog2(x)函数在仿真时,x可以是reg,wire,integer 变量或parameter,localparam 声明的参数。而综合时只支持parameter或localparam 声明的参数。

例1: $clog2(x) 函数测试

程序如下:

`timescale 1 ns /1 ps 
module tb ( ); 

wire [7:0] a = 253; 
reg  [3:0] b; 

initial 
begin 
    b = 0;
    #10 b = $clog2(a);
    #10;
end
endmodule

仿真结果如图1所示,从仿真结果看变量a = 253, $clog2(a)的结果为8。可见如果要表达最大值为253的数值范围,最低需要8位位宽的变量。

图1

修改上面的测试程序,添加实体模块,继续测试在实体模块中$clog2的行为,程序如下:

(1)实体模块程序

module test_clog
(
    input  [7:0] addr, 
    output [7:0] data
); 

assign data = $clog2(addr);
endmodule

(2) 仿真激励程序

`timescale  1 ns /1 ps
module tb
();
 
wire  [7:0]  a = 253; 
wire  [7:0]  data;
reg   [7:0]  addr;
reg   [3:0]  b;

initial  begin
    b = 0;
    addr = 0;
    #10 b = $clog2(a);
    addr = 100;
    #10;
end
 
test_clog  test_clog_inst
(
    .addr(addr),
    .data(data)
);
endmodule

仿真结果如图2:

图2

从图2的仿真结果可以看出,wire,reg,integer类型变量在仿真时,无论在testbench 模块还是实体程序中$clog2函数都可以正确使用。但在实体模块中,$clog2是可综合的系统调用,但仅限于对常数求解,因此如果将上例中的实体模块进行综合,会得到图3中的错误提示。

图3

出现图3中的这种错误的原因是实体module里对$clog2(x)进行变量求解造成的,虽然此时变量值也是整数。但在利用参数(parameter, localparam)或宏(`define)声明的常量时,在实体模块中是可以综合的,特别是在IPcore 设计中对于输入、输出位宽,可变宽度、可变深度的存储器的数据位宽,地址位宽的参数化设计是非常有效而且是可行的办法。

例2: 设计一个容量为参数化的RAM,用$clog2函数计算地址的宽度

(1)实体程序如下:

`timescale  1 ns /1 ps
 
module test_clog2
#(parameter DEEP = 256)
(
    input    [$clog2(DEEP)-1:0]  addr,
    output   [7:0]               data
);
reg  [7:0] ram [0:DEEP-1];   //定义ram, 位宽为8位, 深度为DEEP。
 
assign data = ram[addr];    // 根据地址读取ram中的值。
integer i;
initial begin                        //ram 初始化
    for(i = 0; i < DEEP; i = i + 1)
        ram[ i ] = i + 1;
end
 
endmodule

上面程序中 语句“input [$clog2(DEEP)-1:0] addr,”中用到了$clog2函数求地址宽度。参数化传递在求地址端口位宽,意义非常明显,比如DEEP等于100,则计算出$clog2(DEEP)=7, 也就是需要7位地址才能完成整个存储器的寻址。

(2)仿真激励程序如下:

`timescale  1 ns /1 ps
module tb  ();
parameter DEEP = 256;

wire  [7:0]  data;
reg   [$clog2(DEEP)-1:0]  addr;

initial  begin
    addr = 0;
    #10
    for(addr = 0;addr <= DEEP; addr = addr + 1)
        #10;
    #10;
    $stop;
end


test_clog2 
#(.DEEP(DEEP) )
test_clog_inst 
(
    .addr(addr),
    .data(data)
);
 
endmodule

 

(3)仿真波形如图4,5

 

图4

图5

从图4,5 的仿真结果看,的确将实体模块中的RAM内容正确读出。在图5中我们还注意到一个细节,当我们需要读取256个数据时,由于addr既是RAM的地址,同时也是for循环的判断条件,因此在终止for循环时,地址比给定的边界值大1(如256)才能满足条件。这不是我们希望的结果,因为真实例化的RAM单元地址可能不存在(0 — 255),虽然在本程序中影响不大。分析一下,下面程序的运行结果,

`timescale  1 ns /1 ps  
module tb ();  
parameter DEEP = 100;

wire  [7:0]  data;
reg   [7:0]  addr;

initial  
begin
    addr = 0;
    #10
    for(addr = 0; addr <= 255; addr = addr + 1)
        #10; 
    #10;
    $stop;
end
 
test_clog2 
#(.DEEP(DEEP)) 
test_clog_inst
(
    .addr(addr),
    .data(data)
); 
endmodule

 

先看看在边界时的仿真结果,如图6

 

图6

从图6可以看出,RAM的内容读取没有问题,但程序在地址addr达到255后并没有停下来,而是继续运行。为什么会出现这个原因呢?

可见问题出在for循环的边界判断上。对于条件addr<=255,实际上当addr=256时才满足终止条件,而addr的位宽是8位的(“reg [7:0] addr;”),因此addr的值永远不可能达到8位,因此才会出现上面的结果。解决方案如下:

  1. 调整addr的位宽,由8位变成9位,如修改addr的声明为”reg [8:0] addr”,但这并不是一个好的方案,因为增加位宽后,应该评估对其它程序的影响。
  2. 比较好的方案引入一个临时整型变量i,由i作为for循环的控制变量,在for循环体内修改addr的值, 修改后程序如下,
`timescale  1 ns /1 ps 
module tb ();

parameter DEEP = 256;
wire  [7:0]  data;
reg   [7:0]  addr;
integer      i; 
initial  
begin
    addr = 0;
    #10
    for(i = 0; i <= 255; i = i + 1) begin
        #10  addr = addr + 1;
    end
    #10;
    $stop;
end
 
test_clog2 
#(.DEEP(DEEP))
test_clog2_inst 
(
    .addr(addr),
    .data(data)
);
 
endmodule

 

图7

从图7的仿真结果可以看出,经过修改后for循环可以按照预期设定运行。

由于clog2函数Verilog代码可以由函数(function)实现,参考代码如下:

function integer clog2(input integer depth);
begin
    depth = (depth == 0) ? 0 : depth - 1;
    for(clog2 = 0; depth > 0; clog2 = clog2 + 1)
        depth = depth >> 1;
end    
endfunction

 

仿真程序如下:

`timescale 1ns / 1ps

module test_bench(

    );

function integer clog2(input integer depth);
begin
    depth = (depth == 0) ? 0 : depth - 1;
    for(clog2= 0; depth > 0; clog2= clog2+ 1)
        depth = depth >> 1;
end    
endfunction

integer i;    
initial 
begin
    for(i = 0; i < 34; i = i + 1)
    begin
        $display(" clog2(%0d) = %d",    i, clog2(i));
        $display("$clog2(%0d) = %d \n", i, $clog2(i));
    end

    $stop;
end

endmodule


仿真结果如下:

Vivado Simulator 2018.2
Time resolution is 1 ps
 clog2(0) =           0
$clog2(0) =           0 

 clog2(1) =           0
$clog2(1) =           0 

 clog2(2) =           1
$clog2(2) =           1 

 clog2(3) =           2
$clog2(3) =           2 

 clog2(4) =           2
$clog2(4) =           2 

 clog2(5) =           3
$clog2(5) =           3 

 clog2(6) =           3
$clog2(6) =           3 

 clog2(7) =           3
$clog2(7) =           3 

 clog2(8) =           3
$clog2(8) =           3 

 clog2(9) =           4
$clog2(9) =           4 

 clog2(10) =           4
$clog2(10) =           4 

 clog2(11) =           4
$clog2(11) =           4 

 clog2(12) =           4
$clog2(12) =           4 

 clog2(13) =           4
$clog2(13) =           4 

 clog2(14) =           4
$clog2(14) =           4 

 clog2(15) =           4
$clog2(15) =           4 

 clog2(16) =           4
$clog2(16) =           4 

 clog2(17) =           5
$clog2(17) =           5 

 clog2(18) =           5
$clog2(18) =           5 

 clog2(19) =           5
$clog2(19) =           5 

 clog2(20) =           5
$clog2(20) =           5 

 clog2(21) =           5
$clog2(21) =           5 

 clog2(22) =           5
$clog2(22) =           5 

 clog2(23) =           5
$clog2(23) =           5 

 clog2(24) =           5
$clog2(24) =           5 

 clog2(25) =           5
$clog2(25) =           5 

 clog2(26) =           5
$clog2(26) =           5 

 clog2(27) =           5
$clog2(27) =           5 

 clog2(28) =           5
$clog2(28) =           5 

 clog2(29) =           5
$clog2(29) =           5 

 clog2(30) =           5
$clog2(30) =           5 

 clog2(31) =           5
$clog2(31) =           5 

 clog2(32) =           5
$clog2(32) =           5 

 clog2(33) =           6
$clog2(33) =           6

 

从上面的仿真结果可以看出,系统提供的$clog2函数对于输入参数为1时,$clog2函数结果为0,即 20= 1 。但是这个结果不适合计算总线宽度。所以下文中给出的自定义函数bus_width函数修正了这一错误,当输入参数为1时,bus_width(1)  =  1  。

可以在module内用函数生成,module例化后可以在其它模块中调用。但该程序是可综合的变量输入的clog2函数,但不能做系统调用。注意输入参数depth要求大于0。至于如何写系统调用函数,并能在系统中调用,后续课程会讲解。

 

function integer bus_width(input integer depth);
begin
    if(depth[31:1] != 0)
    begin
        depth =  depth - 1;
        for(bus_width= 0; depth > 0; bus_width= bus_width+ 1)
            depth = depth >> 1;
    end
    else bus_width= depth;
end    
endfunction

 

实验课:设计一段程序在硬件开发板上(pra006或prx100D)观察RAM的实现,以及RAM初始化的实现。要求分别在Quartus和Vivado下实现。

 

对应视频:

Posted in FPGA, FPGA 教材教案, Quartus II, Quartus II, Verilog, Verilog, 开发工具, 开发工具, 开发语言, 教材与教案, 文章, 编程语言

发表回复

相关链接