设计数学运算逻辑的时候,我们经常会遇到对定点数的截断,我们知道对于无符号数进行四舍五入截断N,需要先加上2**(N-1)再右移N位。例如,把a为U3.4(这里U代表无符号数,3代表整数位有3位,4代表小数位有4位,总位宽位3+4=7位,类似S3.4代表整数部分有3位小数部分有4位的有符号数,总位宽为1+3+4=8位)截断为U3.0。
assign b[2:0] = (a + 7'd8) >> 4;
但是对于有符号数的四舍五入截断,目前我还没发现有资料进行讨论。以前在我在某家欧美公司工作的时候,他们通常不区分有符号数和无符号数,统一按无符号数的四舍五入方法来截断。这种方法是否正确呢?我写了一段测试代码来验证:
module test;
reg signed [11:0] data;
reg signed [7:0] ref_data;
reg signed [11:0] temp;
real r;
initial begin
repeat (1000) begin
data = $urandom_range(0, 2047);
data = data - 1024;
r = $itor(data);
r = r / 16;
ref_data = r;
temp = data + 8;
if (ref_data != temp[11:4]) begin
$display("%0d / 16 = %0d, actual is %0d", data, ref_data, $signed(temp[11:4]));
end
end
end
endmodule
运行这段代码你会发现,对于有些负数,采用无符号数的四舍五入方法计算结果会比标准大1。仔细研究错误全部出在对五的取舍上。例如-856/16=-53.5,按标准方法结果应该是-54,但是如果+0.5之后,恰好变成了-53。这是为什么呢?其实我们仔细研究二的补码就会知道,用补码表示数的范围是不对称的,比如10位有符号整数表示的范围是[-1024,1023],所以对于负数需要再减去一个最低位。记对于负数在右移N位之前需要加的数为2**(N-1)-1。正负数统一起来表示为:
rand(x, n) = (x + 2**(n-1) - x[$bits(x)-1]) >>> n
特别的,当N=1时,
rand(x, 1) = (x + (!x[bits(x)-1])) >>> 1
测试代码如下:
module test;
reg signed [11:0] data;
reg signed [7:0] ref_data;
reg signed [11:0] temp;
real r;
real ref_sum;
real act_sum;
initial begin
repeat (1000) begin
data = $urandom_range(0, 2047);
data = data - 1024;
r = $itor(data);
r = r / 16;
ref_data = r;
temp = data + 8 - data[11];
if (ref_data != temp[11:4]) begin
$display("%0d / 16 = %0d, actual is %0d", data, ref_data, $signed(temp[11:4]));
end
end
end
endmodule