上一篇文章给大家讲解了溢出保护相关的话题。本篇文章趁热打铁,给大家讲解一下相对复杂的带小数的加减法实现。
1、小数加法模块简介
我们考虑两个小数的加法:
- 其中输入a为3位整数、4位小数,一共7位;
- 输入的b为2位整数、3位小数,一共5位;
- 输出的c为3位整数、1位小数,一共4位;
对于输出c要做四舍五入的操作,并且还要做相关的溢出保护。
首先我们明确一下,由于输入a有着4位小数、输入b有着3位小数,而输出c只有1位小数,那么一定会有精度的损失,这点相信大家可以理解。那我们的精度有多少呢?实际上精度应该是0.25,因为我们只有1位小数,即要么是0.5,要么是0,又由于我们做了四舍五入,因此最终的结果距离真正正确的值,最差只会差0.25。
又由于输出c只有3位整数,即存在溢出的情况,这里我们规定要做溢出保护。
2、Python的仿真模型
对于算法翻译成RTL的例子,其实我们一般都会用高级语言进行仿真,以初步确定是否符合预期的结果,同时也可以用于后期和RTL结果的对比。本例子实际上很简单,可以不写。但为了便于大家理解,在这里还是给大家讲解如何用Python进行建模。
这里不讲解Python的语法,默认大家都会。有一点值得说明一下,如果用Python进行建模的话,Python的函数或者类其实就类似于一个模块或者是一个组合逻辑(Python实际上也做不了时钟级别的精确,只能做行为级别的结果对比)。其规定了输入和输出以及相应的运算,用这种方式和RTL对应写起来非常的方便。
对于输入a而言,由于其有4位小数,因此我们先将其定点化,转换为整数。即乘上2的4次方。(我们用高级语言仿真的时候,是真的会有小数的,因此我们要算好其真正的值),并且我们认为这个数最大是不可以超过7'b1111111的,如果超过这个值,比如它大到8'b10000000了,我们就认为它是0,也就是我们对输入是不做溢出保护的,我们默认它是不能超过规定的最大值的,这个由别的模块来保证,我这个模块不管这件事。
对于输入b也是同样的操作,但输入b乘上2的3次方以后,因为要和a做加法,所以小数点要对齐,要再乘以2。相当于两个小数相加的时候,你小数点本身就要对齐,因为b的小数点后只有3位,你实际上还要补个0,才能保证小数点对齐。所以要再乘以2。
然后基于定点化以后的a和b我们算出了c,这里我们认为c是不会超过2^8-1的,因为输入最大就是2^7-1和2^6-1。又因为我们要做四舍五入,回忆一下之前的文章怎么说的做四舍五入。我们先多保留1位小数点,然后加1。然后再将小数点移动一位,这个值就是四舍五入的值。
这个例子中我们本来是要将小数点向左移动3位的,我们先移动2位,再加1。然后再向小数点向左移动1位。于是就得到了c3,对于c3同样不做溢出保护,因为位宽已经留足够了。紧接着我们再截取1位小数,得到c4结果,其中c4实际上就是3位整数,1位小数了。对于RTL,直接就可以将这个结果作为输出了,因为RTL的定点数实际上是看不到点的,点在哪里是彼此约定好的,其看上起似乎就是一个整数。但是对于Python仿真模型,你应该将结果再除以2,因为Python是真的知道它是个小数的,你不除以2就不符合你仿真的预期了。(大家结合代码好好理解一下)
def plus_float(a, b): #a 3位整数、4位小数 #b 2位整数,3位小数 #c 2位整数、3位小数 a2 = int(a*(2**4)) if a2 > 2**7-1: a2 = a2-2**7 b2 = int(b*(2**3))*2 if b2 > 2**6-1: b2 = b2-2**6 c2 = a2 + b2 #c2 4位小数,4位整数 if c2 > 2**8-1: c2 = c2-2**8 c3 = int(c2/2**2) + 1 #5位整数,2位小数 if c3 > 2**7-1: c3 = c3-2**7 c4 = int(c3/2) # 3位整数,1位小数 # 保护 if c4 > 2**4-1: c = 2**4 - 1 else: c = c4 c = c/2 #非RTL代码 return c
完成了模型的编写,我们写一个Python的tb,如下所示。我们让输入a从0开始,步长为2^-4方,逐渐增加,其实就对应我们规定好的3位整数,4位小数可能的变化范围。b也是类似的。我们相应的可以得到c的结果,然后和真正的c的结果c_real进行对比。我们可以看到误差在0.25以内,符合我们的预期。说明我们的算法没有问题,规定的位宽也没有问题。于是我们就可以开始写RTL了。
注意:这里的plus_float运算得到的c,其实就是我们认为RTL会得到的结果。而c_real是我们认为reference model的结果。
from plus_float import plus_float import matplotlib.pyplot as plt import numpy as np def plot_line_chart(data): x = np.arange(len(data)) plt.plot(x, data) plt.xlabel('X轴') plt.ylabel('Y轴') plt.title('折线图') plt.show() err_group = [] for cnt1 in np.arange(0, 8-2**-4, 2**-4): a = cnt1 for cnt2 in np.arange(0, 4-2**-3, 2**-3): b = cnt2 c = plus_float(a, b) c_real = a+b if c_real > 8-2**-1: c_real = 8-2**-1 err = abs(c_real-c) err_group.append(err) plot_line_chart(err_group)
3、RTL代码编写
有了前面的Python仿真模型,可以证明我们的思路符合预期,我们设计的位宽也没有任何问题。因此我们可以编写相应的RTL代码:
我们首先看对于DUT而言,怎么从Python到RTL:
- 输入输出没什么好说的,根据需求来就行;
- a2其实就对应a,因为Verilog是看不到那个点的,其实仿真运算的时候默认这些数就是定点数;
- b2要乘以2,这样才能和a是对齐的,相加才不出错;(你拿1.34+2.4,你都认为是定点数,于是你用134+24来算,这合理吗?)
- c2就是a2+b2;
- c3对应四舍五入多保留一位小数再加1;
- c4把多保留的那个小数移动一位,得到真正四舍五入的结果;
- c是c4做溢出保护;
这样我们就完成了RTL的编写,可以认为RTL和行为模型一模一样。
module plus_float( input [6:0] a, //3 4 input [4:0] b, //2 3 output [3:0] c //3 1 ); //信号声明我不写了 assign a2=a; assign b2={b,1'b0};; assign c2=a2+b2; assign c3=c2[7:2]+6'd1; assign c4=c3[5:1] assign c=c4>4'hf?4'hf:c4[3:0] endmodule
然后我们写相应的Testbench,同样的信号声明和波形生成逻辑我也不写了。主要写整体逻辑,和Python的tb实际上是一模一样的,其实就是遍历输入a,b。比较一下DUT和reference model是否一致,如下图所示:
`timescale 1ns/1ps module tb; initial begin a=0; b=0; cnt1=0; cnt2=0; while(cnt1<8-0.0625) begin cnt1 = cnt1 + 0.0625; a=int'(cnt1*(2**4)); cnt2=0; while(cnt2<4-0.125) begin b=int'(cnt2*(2**3)); c_real=cnt1+cnt2; if(c_real>7.5) c_real=7.5; #10; end end #100; $finish; end assign c2=real'(c)/2.0; assign err=$abs(c_real-c2); plus_float u_plus_float ( .a(a), .b(b), .c(c) ) endmodule
最终得到仿真的结果和之前Python仿真得到的结果一致,误差在0.25以内,符合预期。到此为止整个带小数考虑溢出保护和四舍五入的加法模块编写完成,希望大家举一反三,彻底掌握定点化的机制所在。以后可以独立完成类似模块的编写。