Verilog对数据进行四舍五入(round)与饱和(saturation)截位

1. 引言

在利用Verilog写数字信号处理相关算法的过程中往往需要对数据进行量化以及截位。而在实际项目中,一种比较精确的处理方式就是先对截位后的数据进行四舍五入(round),如果在四舍五入的过程中由于进位导致数据溢出,那么我们一般会对信号做饱和(saturation)处理。所谓饱和处理就是如果计算结果超出了要求的数据格式能存储的数据的最大值,那么就用最大值去表示这个数据,如果超出了要求的数据格式能存储的数据的最小值,那么就用最小值去表示这个数据。

为了叙述方便,做出如下规定:如果一个有符号数的总位宽为32位(其中最高位为符号位),小数位宽为16位,那么这个有符号数的数据格式记为32Q16。依此类推,10Q8表示这个数是一个有符号数(最高位为符号位),且总位宽为10位,小数位宽为8位。16Q13表示这个数是一个有符号数(最高位为符号位),且总位宽为16位,小数位宽为13位。总而言之,下文如果定义一个数据为mQn(m和n均为正数且m>n)格式,那么我们可以得到三个重要信息:

  • mQn是一个有符号数,最高位为符号位;
  • mQn数据的总位宽为m;
  • mQn数据的小数位宽为n。

2. Verilog中有符号数据的补位与截位

2.1 有符号数与无符号数

有符号数指的就是带有符号位的数据,其中最高位就是符号位(如果高位为0,那么表示是正数,如果最高位为1,那么表示是负数);无符号数就是不带有符号位的数据。

考虑一个4位的整数"4'b1011",如果它是一个无符号数,那么它表示的值为:\(1\times 2^{3} + 0\times 2^{2} + 1\times 2^{1} + 1\times 2^{0} = 11\)。如果它是一个有符号数,那么它表示的值为:\((-1)^{1}\times 2^{3} + 0\times 2^{2} + 1\times 2^{1} + 1\times 2^{0} = -5\),所以相同的二进制数把它定义为有符号数和无符号数表示的数值大小有可能是不同的。同时,这里也告诉大家,有符号数和无符号数转化为10进制表示的时候唯一区别就是最高位的权重不同,拿上例来说,无符号数最高位的权重是\(2^{3}\)而有符号数最高位的权重是\((-1)^{1}\times 2^{3}\)

正因为有符号数和无符号数最高位的权重不同,所以它们所表示的数据范围也是不同的。比如,一个4位无符号整数的数据范围为0$\sim\(15,分别对应二进制4'b0000\)\sim\(4'b1111,而一个4位的有符号整数的数据范围为-8\)\sim\(7,分别对应二进制4'b1000\)\sim$4'b0111。

扩展到一般情况,一个位宽为m的无符号整数的数据范围为\([0, 2^{m}-1]\),而一个位宽为\(m\)的有符号整数的数据范围为:\([-2^{m-1},2^{m-1}-1]\)

2.2 有符号整数的符号位扩展

一个4位的有符号整数4'b0101,显然由于最高位为0,它是一个正数,如果要把它扩展成6为,那么只需要在最前加2个0即可,扩展之后的结果为:6'b000101。

一个4位有符号整数为4'b1011,显然由于最高位为1,它是一个负数,如果要把它扩展成6位,此时最前面添加的是两个1,而非2个0,扩展之后的结果为:6'b11101。为了确保扩数据扩位以后没有发生错误,做一个简单验证:
$ 4'b1011 = (-1)^{1}\times 2^{3} + 0\times 2^{2} + 1\times 2^{1} + 1\times 2^{0}=-8+0+2+1=-5$
$ 6'b111011 = (-1)^{1}\times 2^{5}+1\times 2^{4} + 1\times 2^{3} + 0\times 2^{2} + 1\times 2^{1} + 1\times 2^{0}=-32+16+8+2+1=-5$
显然,扩位以后数据大小并未发生改变。

对一个有符号数进行扩位的时候为了保证数据大小不发生改变,扩位的时候应该添加的是符号位

2.3 有符号小数

有了前面两小节的基础,接下来研究一下有符号小数。前面已经规定了有符号小数的记法。

假设一个有符号小数为4'b1011,它的数据格式为4Q2,也就是说它的小数位为2位。那么看看这个数表示的十进制数是多少

\[4'b10.11=(-1)^{1}\times 2^{1} + 0\times 2^{0} + 1\times 2^{-1} + 1\times 2^{-2} = -2 + 0 + 0.5 + 0.25 = -1.25 \]

显然,小数的计算方法实际上和整数的计算方法是一样的,只不过我们要根据小数点的位置确定对应的权重。

接下来看看有符号小数的数据范围。就拿4Q2格式的数据来说,它的数据范围为\([-2,2-\frac{1}{2^{2}}]\),分别对应二进制[4'b1000,4'b0111]。扩展到一般情况,mQn格式数据的范围为\([-2^{m-n-1},2^{m-n}-\frac{1}{2^{n}}]\)

最后再来看看有符号小数的数据扩展。假设一个有符号小数为4'b1011,它的数据格式为4Q2,现在要把这个数据用6Q3格式的数据存储。显然需要把整数部分和小数部分分别扩展一位。整数部分采用上一节提到的符号位扩展,小数部分则在最后添加一个0,扩展以后的结果为6'b110110,接下来仍然做一个验证。

\(4'b10.11=1\times (-2^{1})+0\times 2^{0}+1\times 2^{-1}+1\times 2^{-2}=-2+0+0.5+0.25=-1.25\)
\(6'b110.110=1\times (-2^{2})+1\times 2^{1}+0\times 2^{0}+1\times 2^{-1}+1\times 2^{-2}+0\times 2^{-3}=-4+2+0+0.5+0.25+0=-1.25\)
显然,扩位以后数据大小并未发生变化。

有符号小数进行扩位时整数部分进行符号位扩展,小数部分在末尾添0。

2.4 两个有符号数的和

两个有符号数相加,为了保证和不溢出,首先应该把两个数据进行扩展使小数点对齐,然后把扩展后的数据继续进行一位的符号位扩展,这样相加的结果才能保证不溢出。

例子:现在要把5Q2的数据5'b100.01和4Q3的数据4'b1.011相加。
step1: 由于5Q2的数据小数位只有2位,而4Q3的数据小数点有3位,所以先把5Q2的数据5'b100.01扩展为6Q3的数据6'b100.010,使它和4Q3数据的小数点对齐;
step2: 小数点对齐以后,然后把4Q3的数据4'b1.011进行符号位扩展成6Q3的数据6'b111.011;
step3: 两个6Q3数据相加,为了保证和不溢出,和应该用7Q3的数据来存储。所以需要先把两个6Q3的数据进行符号位扩展成7Q3的数据,然后相加,这样才能保证计算结果是完全正确的。

以上就是两个有符号数据相加需要做的一系列转化。回过头来思考为什么两个6Q3的数据相加必须用7Q3的数据才能准确地存储它们的和。因为6Q3格式数据的数据范围为\([-4,4-1/2^{3}]\);那么两个6Q3格式的数据相加和的范围为\([-8,8-1/2^{2}]\);显然如果和仍然用6Q3来存储一定会溢出,而7Q3格式数据的数据范围为[-8,8-1/2^{3}],因此用7Q3格式的数据来存两个6Q3格式数据的和一定不会溢出。

在用Verilog做加法运算时,两个加数一定要对齐小数点并做符号位扩展以后相加,和才能保证不溢出。

2.5 两个有符号数的积

两个有符号数相乘,为了保证积不溢出,积的总数居位宽为两个有符号数的位宽之和,积的小数数据位宽为两个有符号数的小数位宽之和。简单来说,两个4Q2数据相乘,要想保证积不溢出,积应该用8Q4格式来存储。这是因为4Q2格式数据的范围为:\([-2,2-1/2^{2}]\),那么两个4Q2数据乘积的范围为:\([-4+1/2,4]\),而8Q4格式的数据范围为:\([-8,8-1/2^{4}]\),一定能够准确地存放两个4Q2格式数据的积。

mQn和aQb数据相乘,积应该用(m+a)Q(n+b)格式的数据进行存储。

2.6 四舍五入(round)

前面讲的都是对数据进行扩位,这一节说的是对数据截位时如何进行四舍五入以提高截位后数据的精度。

假设一个9Q6个数的数据为:9'b011.101101,现在只想保留3位小数位,显然必须把最后三位小数位截掉,但是不能直接把数据截成6'b011.101,这样是不精确的,工程上一般也不允许这么做,正确的做法是先看这个数据是正数还是负数,因为9'b011.101101的最高位为0,所以它是一个正数,然后再看截掉部分(此例中截掉部分是最末尾的101)的最高位是0还是1,在数据是正数的情况下,如果截掉部分的最高位为1,那么需要产生进位的,所以最终9'b011.101101应该被截成6'b011.110。

如果是负数则正好相反。假设一个9Q6格式数据为:9'b100.101101,由于最高位是1,所以这个数是一个负数,然后再看截断部分的最高位以及除最高位的其他位是否有1,此例中截断部分(截断部分为末尾的101)的最高位为1,而除最高位以外的其他位也有为1的情况,由于负数最高位的权重为(\(-2^{2}\)),所以对于这种情况是不需要进位的,与正数不同的是,负数不进位是需要加1的。因此最终9'b100.101101应该被截成6'b100.110。

假设a是一个9Q6格式的数据,要求把小数点截成3位。下面是Verilog代码:

assign carry_bit = a[8] ? (a[2]&(|a[1:0])):a[2];
assign a_round = {a[8],a[8:3]} + carry_bit;

上面代码的第一行是通过判断符号位a[8]和截断部分数据特征来确定是否需要进位,如果a[8]是0,计算得到的carry_bit为1,则表示a是正数,且截断是需要进位的;如果a[8]是1,计算得到的carry_bit为1,则表示a是负数,且截断是不需要进位的,负数不进位需要加1。代码第二行是为了保证进位后数据不溢出,所以扩展了符号位。

2.7 饱和(saturation)截位

所谓饱和处理就是如果计算结果超出了要求的数据格式能够存储的最大值,那么就用最大值去表示这个数据,如果结算结果超出了数据格式能够存储的数据的最小值,那么就用最小值去表示这个数据。

例1:有一个6Q3的数据为6'b011.111,现在要求用4Q2格式的数据去存储它,显然6'b011.111转化为10进制如下:
\(6'b011.111 = 1*2^{1}+1*2^{0}+1*2^{-1}+1*2^{-2}+1*2^{-3}=3.875\)
而4Q2格式的数据能够表示的数据的最大值为4'b01.11,转化为10进制为1.75,因此4Q2格式的数据根本无法准确地存放3.875这个数据,这样就是所谓的饱和情况。在这种情况下,饱和处理就是把超过了1.75的所有数据全部用1.75来表示,也就是说,6Q3的数据6'b011.111如果非要用4Q2格式的数据来存储的话,在进行饱和处理的情况下最终的存储结果为:4'b01.11。

例2:有一个6Q3的数据为6'b100.111,现在要求用4Q2格式的数据去存储它,显然6'b100.111转化为10进制如下:
\(6'b100.111=1*(-2)^2+1*2^{-1}+1*2^{-2}+1*2^{-3}=-4+0.5+0.25+0.125=-3.125\)

而4Q2格式的数据能表示的数据的最小值为4'b10.00,转化为10进制为-2,因此4Q2格式的数据根本无法准确的存放-3.125这个数据,这是另一中饱和情况。在这种饱和情况下,饱和处理就是小于-2的所有数据全部用-2来表示,也就是说,6Q3的数据6'b100.111,如果非要用4Q2格式的数据来存储的话,在进行饱和处理的情况下最终的存储结果为:4'b10.00。

3. 实例(a+b*c)

3.1 要求

假设a,b的数据格式均为16Q14,c的数据格式为16Q15,计算出s=a+b*c的值,其中s的数据格式为16Q14。在截位的过程中利用四舍五入(round)的方式保证数据精度,如果有溢出的情况,用饱和截位的方式进行处理。编写完Verilog代码以后利用Matlab产生a,b,c的数据对Verilog代码进行仿真,并保证Matlab运算得到的数据和Verilog运算得到的数据全部相同。最后,有条件的利用VCS统计代码覆盖率(Code Coverage),确保条件覆盖率(Condition Coverage)达到100%。

3.2 要求分析

  • 先分析b*c。由于b的数据格式为16Q14,c的数据格式为16Q15,所以为了保证b*c的乘积不溢出,那么b*c的乘积的数据格式为(16+16)Q(14+15),即32Q29;
  • 再分析加法。由于a的数据格式为16Q14,而b*c的积的数据格式为32Q29,所以相加之前要先把a扩展成32Q29格式的数据,又为了保证相加的结果不溢出,相加之前还要把两个32Q29格式的数据进行1位符号位扩展成33Q29格式的数据以后再相加,相加得到的和的数据格式为33Q29。
  • 最后,由于要求最终的结果为16Q14,所以需要把33Q29的数据截位为16Q14,如果出现数据溢出的情况,需要用饱和截位的方式进行处理。

3.3 代码

module dsp
(
  input  wire               clk   ,
  input  wire               rst_n ,
  input  wire signed [15:0] a     , //16Q14
  input  wire signed [15:0] b     , //16Q14
  input  wire signed [15:0] c     , //16Q14
  output wire signed [15:0] s       //16Q14
);
/****************************************************
* regs to register the input
*****************************************************/
reg  signed [15:0] ra_16q14;
reg  signed [15:0] rb_16q14;
reg  signed [15:0] rc_16q14;

always@(posedge clk or negedge rst_n)
begin
  if(!rst_n)
  begin
    ra_16q14 <= 16'd0;
    rb_16q14 <= 16'd0;
    rc_16q14 <= 16'd0;
  end
  else
  begin
    ra_16q14 <= a;
    rb_16q14 <= b;
    rc_16q14 <= c;
  end
end

/*******************************************************
* operation results
********************************************************/
wire signed [31:0] b_mul_c_32q29;
wire signed [32:0] s_33q29;
wire signed [31:0] a_32q29;

assign b_mul_c_32q29 = b * c;
assign a_32q29 = {a[15],a,{15{1'b0}}};
assign s_33q29 = {a[31],a_32q29} + {b_mul_c_32q29[31], b_mul_c_32q29};

/******************************************************
* round
*******************************************************/
wire carry_bit;
wire [18:0] s_19q14_round;

assign carry_bit = s_33q29[32] ? (s_33q29[14] & (|s_33q29[13:0])) : s_33q29[14];
assign s_19q14_round = {s_33q29[32],s_33q29[32:15]} + carry_bit;

assign s = (s_19q14_round[18:15]==4'b0000 || s_19q14_round[18:15]==4'b1111) ? 
            s_19q14_round[15:0] :
            {s_19q14_round[18],{15{!s_19q14_round[18]}}};

endmodule

3.4 测试文件

`timescale 1ns / 1ps  
module tb_dsp;    
reg I_clk ;     
reg I_rst_n ;    
reg [15:0] I_a ;     
reg [15:0] I_b ;     
reg [15:0] I_c ;     
wire [15:0] O_s ;
 
parameter C_DATA_LENGTH = 4096 ;
 
reg [15:0] M_mem_a[0:C_DATA_LENGTH - 1] ;
reg [15:0] M_mem_b[0:C_DATA_LENGTH - 1] ;
reg [15:0] M_mem_c[0:C_DATA_LENGTH - 1] ;
reg [13:0] R_mem_addr ;
reg R_data_vaild ;
reg R_data_vaild_t ;
 
dsp u_dsp
(
    .I_clk,
    .I_rst_n(I_rst_n),
    .I_a(I_a), // 16Q14
    .I_b(I_b), // 16Q14
    .I_c(I_c), // 16Q15
    .O_s(O_s)
);
 
 
 
initial begin
    I_clk = 0 ;
    I_rst_n = 0 ;
    #67 I_rst_n = 1 ;
end
 
initial begin
    $readmemh("E:/VIVADO_WORK/cnblogs12_round_saturation/Matlab/a_16Q14.txt",M_mem_a);
    $readmemh("E:/VIVADO_WORK/cnblogs12_round_saturation/Matlab/b_16Q14.txt",M_mem_b);
    $readmemh("E:/VIVADO_WORK/cnblogs12_round_saturation/Matlab/c_16Q15.txt",M_mem_c);
end
 
always #5 I_clk = ~I_clk ;
 
always @(posedge I_clk or negedge I_rst_n)
begin
    if(!I_rst_n)
        begin
            I_a <= 16'd0 ;
            I_b <= 16'd0 ;
            I_c <= 16'd0 ;
            R_mem_addr <= 14'd0 ;
            R_data_vaild <= 1'b0 ;
        end
    else if(R_mem_addr == C_DATA_LENGTH )
        begin
            R_mem_addr <= C_DATA_LENGTH ;
            R_data_vaild <= 1'b0 ;
        end
    else
        begin
            I_a <= M_mem_a[R_mem_addr] ;
            I_b <= M_mem_b[R_mem_addr] ;
            I_c <= M_mem_c[R_mem_addr] ;
            R_mem_addr <= R_mem_addr + 1'b1 ;
            R_data_vaild <= 1'b1 ;
        end
end
 
always @(posedge I_clk or negedge I_rst_n)
begin
    if(!I_rst_n)
        R_data_vaild_t <= 1'b0 ;
    else
        R_data_vaild_t <= R_data_vaild ;
end
 
 
integer fid ;

initial begin
    fid = $fopen("E:/VIVADO_WORK/cnblogs12_round_saturation/Matlab/s_vivado.txt" , "w");
    if(!fid)
        begin
            $display("**********************Can Not Openle*************************************");
            $finish;
        end
    else
        begin
            $display("**********************Open Fileccess*************************************");
        end
end
 
always @(posedge I_clk )
begin
    if(R_data_vaild_t)
        $fdisplay(fid,"%d",$signed(O_s));
    else if(R_mem_addr == C_DATA_LENGTH)
        begin
            $fclose(fid) ;
            $finish ;
        end
end

endmodule

3.5 利用Matlab产生测试数据

Matlab里面又现成的量化数据的函数,它们分别是quantizer函数和quantize函数。其中quantizer函数用来产生量化格式,quantize函数用来调用quantizer函数的结果读数据进行量化,具体用法,可以查看相关文档。
产生a,b,c三个数据的Matlab代码如下:

clear
clc
data_length = 4096 - 8 ; % 定义数据长度,其中排除8种边界条件

a_min = -2 ;             %a的数据格式为16Q14,所以它的最小值为-2
a_max = 2 - 1/(2^14) ;    %a的数据格式为16Q14,所以它的最大值为2 - 1/(2^14)

b_min = -2 ;            %b的数据格式为16Q14,所以它的最小值为-2
b_max = 2 - 1/(2^14) ;    %b的数据格式为16Q14,所以它的最大值为2 - 1/(2^14)

c_min = -1 ;            %c的数据格式为16Q15,所以它的最小值为-1
c_max = 1 - 1/(2^15) ;    %c的数据格式为16Q15,所以它的最大值为1 - 1/(2^15)

% 产生4088个均匀分布在a、b、c最大值与最小值之间的随机数
a_rand = a_min + (a_max - a_min)*rand(1,data_length) ;
b_rand = b_min + (b_max - b_min)*rand(1,data_length) ;
c_rand = c_min + (c_max - c_min)*rand(1,data_length) ;

% 产生8种边界条件
a_boundary = [a_min a_min a_min a_min a_max a_max a_max a_max] ;
b_boundary = [b_min b_min b_max b_max b_min b_min b_max b_max] ;
c_boundary = [c_min c_max c_min c_max c_min c_max c_min c_max] ;

% 随机数与边界值组合成为待量化的数据
a = [a_boundary a_rand];
b = [b_boundary b_rand];
c = [c_boundary c_rand];

% 定义量化规则,根据题目要求量化需采用四舍五入与饱和截位的方式
quan_16Q14_pattern = quantizer('fixed','round','saturate',[16,14]);
quan_16Q15_pattern = quantizer('fixed','round','saturate',[16,15]);
quan_33Q29_pattern = quantizer('fixed','round','saturate',[33,29]);

% 把a、b、c三个数据按照要求进行量化
a_16Q14 = quantize(quan_16Q14_pattern,a);
b_16Q14 = quantize(quan_16Q14_pattern,b);
c_16Q15 = quantize(quan_16Q15_pattern,c);

% 计算a + b * c的值
s = a_16Q14 + b_16Q14 .* c_16Q15 ;

% 根据题目要求,s的数据格式为16Q14,所以这里把s量化为16Q14格式的数据
s_16Q14 = quantize(quan_16Q14_pattern,s);

% 把量化后的a、b、c变成整数方便写入.txt文件中
a_integer = a_16Q14 * 2^14 ;
b_integer = b_16Q14 * 2^14 ;
c_integer = c_16Q15 * 2^15 ;
s_integer = s_16Q14 * 2^14 ;

% 由于在Verilog中测试激励文件的系统调用$readmemh读入的数据格式为16进制,所以
% 把数据写入.txt文件中之前需要把数据转化为补码的格式,这样负数才不会写错
a_complement = zeros(1,length(a_integer));
b_complement = zeros(1,length(b_integer));
c_complement = zeros(1,length(c_integer));

% 把量化后的a转化为补码
for i = 1:length(a_complement)
   if(a_integer(i) < 0)
       a_complement(i) = 2^16 + a_integer(i) ;
   else
       a_complement(i) = a_integer(i) ;
   end
end

% 把量化后的b转化为补码
for i = 1:length(b_complement)
   if(b_integer(i) < 0)
       b_complement(i) = 2^16 + b_integer(i) ;
   else
       b_complement(i) = b_integer(i) ;
   end
end

% 把量化后的c转化为补码
for i = 1:length(c_complement)
   if(c_integer(i) < 0)
       c_complement(i) = 2^16 + c_integer(i) ;
   else
       c_complement(i) = c_integer(i) ;
   end
end

% 把量化后的a的补码写入txt文件
fid_a = fopen('a_16Q14.txt','w');
fprintf(fid_a, '%x\n', a_complement);
fclose(fid_a);

% 把量化后的b的补码写入txt文件
fid_b = fopen('b_16Q14.txt','w');
fprintf(fid_b, '%x\n', b_complement);
fclose(fid_b);

% 把量化后的c的补码写入txt文件
fid_c = fopen('c_16Q15.txt','w');
fprintf(fid_c, '%x\n', c_complement);
fclose(fid_c);

% 把量化后的s以整数形式写入txt文件,方便和vivado计算的结果进行对比
fid_s = fopen('s_matlab.txt','w');
fprintf(fid_s, '%d\n', s_integer);
fclose(fid_s);

3.6 验证代码的正确性

clear
clc

filename1 = 's_matlab.txt' ;
filename2 = 's_vivado.txt' ;

% s_matlab = textread(filename1, '%d') ;
% s_vivado = textread(filename2, '%d') ;

% 把txt文件中的数据读入并转化为一维数组,上面2行注释的代码和下面6行代码的作用是完全一样的
% 由于Matlab目前的版本不推荐使用textread,所以我使用了textscan函数进行处理
fid1 = fopen(filename1, 'r');
fid2 = fopen(filename2, 'r');
s_matlab = textscan(fid1, '%d') ;
s_vivado = textscan(fid2, '%d') ;
s_matlab = cell2mat(s_matlab) ;
s_vivado = cell2mat(s_vivado) ;

count = 0 ;

% 对s_vivado.txt的数据与s_matlab.txt的数据进行对比
for i = 1:length(s_vivado)
    if(s_vivado(i) == s_matlab(i))
        msg = sprintf('s_vivado(%d) is equal s_matlab(%d), Verification Pass', i , i) ;
        disp(msg) ;
        count = count + 1 ;
    else
        msg = sprintf('s_vivado(%d) is not equal s_matlab(%d), Verification Fail', i , i) ;
        disp(msg) ;
    end
end

msg = sprintf('Total Pass Number is %d', count) ;
disp(msg) ;

转载自:Verilog对数据进行四舍五入(round)与饱和截位(saturation)

posted @ 2022-01-17 16:31  Vinson88  阅读(1230)  评论(0编辑  收藏  举报