之前的文章给大家介绍了带小数的加减法实现,学习本篇文章之前大家可以先复习一下之前这篇文章。
lawliet:如何将算法翻译成RTL(二):带小数的加减法实现8 赞同 · 1 评论文章
本篇文章的内容是之前的深入,既有浮点,又有符号,接下来我们进入正文。
注:这里的有符号不是说用signed声明,而是你默认这个数是有符号的数,按照有符号的方式对这些数进行处理。
1、有符号数加减法模块简介
我们考虑两个有符号小数的加减法:
- 其中输入a为3位整数,4位小数,有符号,因此一共是8位;
- 输入b为2位整数,3位小数,有符号,因此一共是6位;
- 输出c为3为整数,1位小数,有符号,要求四舍五入输出,一共是5位;
同样的,我们需要对c做溢出保护。
2、Python的仿真模型
我们养成好习惯,对于这种算法类的IP先用高级语言进行建模,确定是否符合预期,可行了再编写RTL代码。
我把注释都写在代码里面好了,这样大家可能看起来更加方便一点。
import math # a 3位整数 4位小数 位宽为8 # b 2位整数 3位小数 位宽为6 # c 3位整数 1位小数 位宽为5 要做四舍五入 # 都是有符号数 def plus_signed(a, b): a2 = math.floor(a*(2**4)) # 这一步RTL是不需要做的,RTL没有什么小数的概念,一输入进来就已经是a2了 # 限定a2的最大值,这一步是用来模仿RTL限制位宽的情况。因为你用高级语言如Python写的时候是没有位宽的概念,这里是模拟位宽限制的概念 if a2 > 2**7-1: a2 = a2-2**8 # 这个时候说明是负数了,你应该减去一大圈。比如说只有3位,一位符号位。你看上去是100,实际上是-4,你应该用4-8得到这个-4 elif a2 < -2**7: a2 = a2+2**8 # 这个时候在位宽受限的情况下,是小不到这个程度的。说明此时已经会算出正数了,你应该加上一圈 # 比如你用-4再减去1,你以为是-5,实际上三位有符号根本表示不了-5,你用100减去1得到011实际上是(-5+8)=3 # 这些都是在模拟你写RTL的时候,在位宽受限的情况下可能出现的情况。 # 将b定点化,位宽和a对齐,七位位宽 b2 = math.floor(b*(2**3))*2 if b2 > 2**6-1: b2 = b2-2**7 elif b2 < -2**6: b2 = b2+2**7 # c2:4位小数,4位整数,有符号,位宽为9位 c2 = a2 + b2 if c2 > 2**8-1: c2 = c2-2**9 elif c2 < -2**8: c2 = c2+2**9 # c3:5位整数,2位小数,有符号,位宽为8位,四舍五入多保留一位小数,再加1 c3 = math.floor(c2/2**2) + 1 if c3 > 2**7-1: c3 = c3-2**8 elif c3 < -2**7: c3 = c3+2**8 # 移除掉多保留的那一位小数,位宽为7位 c4 = math.floor(c3/2) if c4 > 2**6-1: c4 = c4-2**7 elif c4 < -2**6: c4 = c4+2**7 # 保护 if c4 > 2**4-1: c = 2**4 - 1 elif c4 < -2**4: c = -2**4 else: c = c4 # RTL中没有下面这一部分,因为没有小数点的概念。是你认为小数点在那个位置 # 这里除以2是在在模拟那个小数点的概念,所以要除以2 c = c/2 return c
完成了模型的编写,我们写一个Python的tb,如下所示。我们让输入a从-8开始,步长为2^-4次方,逐渐增加,其实就对应我们规定好的3位整数,4位小数可能的变化范围。b也是类似的。我们相应的可以得到c的结果,然后和真正的c的结果c_real进行对比。我们可以看到误差在0.25以内,符合我们的预期。说明我们的算法没有问题,规定的位宽也没有问题。于是我们就可以开始写RTL了。
注意,这里不是用int,而是用math.floor。应该要向下取整。二者在正数的时候效果相同,负数的时候int默认向上取整。
from plus_signed import plus_signed 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 axi') plt.ylabel('Y axi') plt.title('plus_signed') plt.show() err_group = [] for cnt1 in np.arange(-8, 8-2**-4, 2**-4): a = cnt1 for cnt2 in np.arange(-4, 4-2**-3, 2**-3): b = cnt2 c = plus_signed(a, b) c_real = a+b if c_real > 7.5: c_real = 7.5 elif c_real <= -8: c_real = -8 err = abs(c_real-c) if err >= 0.5: print("{}+{}={} not equal to {}".format(a, b, c, c_real)) 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!我们假设a2和b2都是负数,那么你直接相加出来完全可能c2[8]还是0,你用两个负数相加得到一个正数,那肯定不对啊!我举个简单的例子,比如a2是1000_0000,而b2是000_0000。直接相加得到1000_0000,你赋值给c2的时候就是0_1000_0000,一个负数加一个0得到一个正数,那当然不对啊。
回顾一下这篇文章的2.7节是怎么做的,对这种有符号数相加并且限制位宽的情况,我们应该扩展1bit,然后再相加。在这个例子中的b应该扩展2bit,让位宽一致再运算。还是上面的例子,a2是1000_0000,而b2是000_0000。扩展以后就是1_1000_0000+0_0000_0000得到1_1000_0000。这种情况下符合我们的预期。再比如a2是1000_0000,b2是111_1111。扩展以后就是1_1000_0000加上1_1111_1111得到1_0111_1111。也就是-128加上负一得到了-129。符合我们的预期。
当然你c2的比特宽度要声明对,比如你c2声明100比特,那肯定加出来是个正数。其实还有更简单的方法,那就是用signed声明,这种方式的话,你根本不用管有没有溢出,默认扩展符号位运算,不会出错的。大家可以试试。大家只要用signed的时候注意我上一篇文章说的注意事项,就不会出错。
https://zhuanlan.zhihu.com/p/648593117
总而言之,有符号数相加,还带位宽限制的情况,非常容易出错,需要小心处理。
我还想说明一下,其实出现结果不符合预期,说白了就是因为仿真器按照无符号数处理,但是你心里认为它是有符号数,那二者就存在偏差了!比如010加010算出来100,你能说仿真器错了吗?它没错,只不过你认为100是-4,2+2算出来了-4不符合你预期,你应该按照你的预期去处理这种情况,告诉仿真器,在代码里面体现这种情况应该怎么算。这样才不会出错。
https://zhuanlan.zhihu.com/p/645127918
c3对应四舍五入多保留一位小数再加1;
c4把多保留的那个小数移动一位,得到真正四舍五入的结果;
c是c4做溢出保护,分正负数两种情况进行讨论。如果是正数的话,但凡[5:4]有一个是1则溢出,负数的话[5:4]都是1才认为不溢出;
这样我们就完成了RTL的编写,可以认为RTL和行为模型一模一样。
module plus_signed( input [7:0] a, input [5:0] b, output reg [4:0] c ); wire [8:0] c2; wire [7:0] c3; wire [6:0] c4; wire [7:0] a2; wire [6:0] b2; //----------------------------------- assign a2 = a; assign b2={b,1'b0}; assign c2={a2[7],a2}+{b2[6],b2[6],b2}; assign c3=c2[8:2]+6'd1; assign c4=c3[7:1]; always @(*) begin if(~c4[6]) //c4>=0 begin if(|c4[5:4]) //overflow c=5'd15; else c=c4[4:0]; end else begin if(~(&c4[5:4])) c=5'd16; else c={c4[6],c4[3:0]}; end end endmodule
然后我们写相应的Testbench,同样的信号声明和波形生成逻辑我也不写了。值得注意的是a,b要用signed进行声明,不然赋值的时候前面补数会有问题。我们主要写整体逻辑,和Python的tb实际上是一模一样的,其实就是遍历输入a,b。比较一下DUT和reference model是否一致。
`timescale 1ns/1ps module tb; initial begin a=0; b=0; cnt1=-8; while(cnt1<8-0.0625) begin a=int'(cnt1*(2**4)); cnt2=-4; while(cnt2<4-0.125) begin b=int'(cnt2*(2**3)); c_real=cnt1+cnt2; if(c_real>7.5) c_real=7.5; else if(c_real<=-8) c_real=-8; cnt2 = cnt2+0.125 #10; end cnt1 = cnt1 + 0.0625; end #100; $finish; end assign c2=real'(c)/2.0; assign err=$abs(c_real-c2); plus_signed u_plus_signed ( .a(a), .b(b), .c(c) ) endmodule
最后大家可以将RTL代码中,变量都改成signed声明,想一想如果是signed声明的变量,相应的逻辑应该怎么写。