RTL硬件实现 | 基本数值运算

RTL的数值运算有很多需要注意的地方,特别是涉及到负数的时候,本章从最基础的数值存储(补码)开始,给出加减法、乘法和绝对值的实现方法,只涉及组合逻辑,不涉及时序逻辑,时序逻辑要多考虑一步,下次再说。

一、补码存在的意义

补码被设计出来就是为了做减法的,在硬件的世界里,减法也是按照加法做的,比如3-5其实可以理解为3+(-5),这个-5就是按照补码的形式存储的。

先理解一下模的概念,模是指一个计量系统的计数范围,我们知道数据的位宽是有限的,比如定义某个数的位宽为4bit,相当于定义了它的模就是16,它能表达的数据范围是0~15,也就是逢16进1,这也是十六进制的由来,虽然称为十六进制,但是你肯定看不到“16”这个数字,因为对于4bit的数据位宽来说,16其实是溢出的。

溢出的真实意思是,现有的位宽放不下实际的计算结果,还是以4bit举例,15是4bit能表示的最大数值,15+1=16,这个16用二进制表示出来是1_0000,对于硬件来说,只定义了4bit,多出来的那个1是不存在的,所以15+1的结果是0,在4bit的世界里,凡是超出实际位宽且能被16整除的部分都会被丢掉,留下的其实是余数,余数就是“模”,这像不像一个圆?只要位宽被定义出来,那么无论是加多少,结果都肯定在这个圆里面。

既然是个圆,加法被定义为正着走,那减法就可以被理解为逆着走,比如时钟的12个刻度,此刻是8点,要把时钟调到10点,可以顺时钟调2个刻度,也可以逆时钟调10个刻度,2+10=12,就是时钟的模,只要保证正向走+逆向走的结果等于模就是同一种操作。

那同样的,对于硬件的4bit位宽来讲,3+(-5)等同于3+(16-5),(16-5)的结果就是-5的补码,硬件里面存的-5就是1011,这样表示实现了减法到加法的转换。如果直接计算3-5的结果,则如(1)所示。

                                          3-5=4'b0011-4'b0100=4'b1110                                (1)

对3-5做个等价替换,改成补码的形式做加法运算,计算结果如式(2)所示。

           3+(-5)=3+(16-5)=3+11=4'b0011+4'b1011=4'b1110         (2)

(1)和(2)的结果是一样的,在硬件的世界里,它是不管正负的,于是大家约定位宽的最高bit就代表符号位,这样对于一个4bit的数据,如果你认为它是正数,就默认没有符号位,表示范围0~15,如果你认为它是负数,就要把最高位默认为符号位,表示范围-8~7,下表是4bit有符号数的存储数据。

二进制

十进制

二进制

十进制

0000

0

1111

-1

0001

1

1110

-2

0010

2

1101

-3

0011

3

1100

-4

0100

4

1011

-5

0101

5

1010

-6

0110

6

1001

-7

0111

7

1000

-8

上面这个表中0被归类到了正数部分,如果观察正负的绝对值,会觉得负数的绝对值总是比正数大了1,所以正常情况下补码的运算应该是除符号位以外,其他位减1再取反。什么?又要做减法,那不是白忙活一通?那就转换一下思路,能不能先取反再加1呢?答案是可以的,只要把补码设计成表格上的那个样子,就可以做到。

为什么一定要取反,因为硬件的基本单元是与非门,而取反用最简单的非门就可以实现,这就是负数为什么要用补码,而补码的算法又恰好是取反加1的原因,就是为了既让硬件容易做,又能方便的定义出0。

二、加减法

明白了负数的补码,再看加减法就会容易很多,只要位宽给够基本不会出问题。对于两个数相加,比如下面这种:

wire   [7:0] a;
wire   [3:0] b;
wire   [8:0] c;
assign  c = {1’b0,a}+{5’b0,b};

就两个重点,第一是c的位宽要比a和b的最大位宽再多1bit,a+b有可能大于a,多给1bit是很自然的,第二是加的时候要补齐位宽,虽然不补位宽计算也不会出错,但是有些编译器会报warning,排查起来很烦。

减法的运算也举个例子,下面这种:

wire   [7:0] a;
wire   [7:0] b;
wire   [8:0] c;
assign  c = {1’b0,a}-{1’b0,b};

重点还是c的位宽要多给1bit,这1bit是符号位。

上面两个都是正数的加减,如果是正负混合运算呢?这个时候就要介绍signed的使用方法了,signed的真实作用是决定如何对操作数扩位,如果是有符号数,强调一下signed,会自动补齐到左边的赋值位宽。举个例子:

wire   [3:0] a;
wire   [3:0] b;
wire   [8:0] c;
assign  c = $signed(a)-$signed(b);

此时signed会把4bit的a和b的最高位补齐到9bit。

三、乘法

乘法的计算其实会涉及到时序问题,但是如果位宽不太大,用组合逻辑也一样能做,比如a和b的位宽都是不超过8bit的正数,可以直接乘。

wire   [7:0] a;
wire   [3:0] b;
wire  [11:0] c;
assign  c = {4’b0,a}*{8’b0,b};

这里c的位宽就不是加1了,是a的位宽加上b的位宽,总之,左赋值的时候位宽要给够,还不能浪费。

如果是正负混合乘法呢?还是使用signed,举例如下:

wire   [7:0] a;
wire   [3:0] b;
wire  [10:0] c;
assign  c = $signed(a)*$signed(b);

    注意这里,为什么c比前面定义的少了1bit,因为a和b都是有符号数,乘完之后留一个符号位就可以了,够用就好,多了浪费,作为一个ICer浪费是可耻的。

四、绝对值

    就是计算(a-b)的绝对值,有两种写法。

1. 直接做

先做c=a-b,接下来判断c的符号位,符号位为1代表是负数,需要除符号位外按位取反再加1,符号位为0代表是正数,直接截掉最高位赋值。

wire   [7:0] a;
wire   [7:0] b;
wire   [8:0] c;
wire   [7:0] c_abs;

assign  c = {1’b0,a}-{1’b0,b};
assign  c_abs = c[8] ? (~c[7:0]+1’b1) : c[7:0];

这种写法的好处是省资源,只有一个减法器和一级选择逻辑,“+1”因为加的是常数,编译的时候会优化掉,所以那个不算加法器。坏处是timing会差一点儿,因为两级计算之间是级联的,要多用一点儿时间。

2. 比大小

  a和b两个数比较大小,用大的减小的,保证得到的是正数,不用求绝对值。

wire   [7:0] a;
wire   [7:0] b;
wire   [7:0] c;

assign  c = (a > b) ? (a - b) : (b - a);

    这种做法的好处是节省时间,timing会好,(a>b)、(a-b)和(b-a)这三个步骤是并行计算的,最后做一个选择,肯定比直接做要快一些,坏处是浪费资源,这里用了一个比较器,两个减法器和一级选择逻辑,资源大于第一个。

    硬件没有非对即错,硬件是一种tradeoff(权衡、平衡),需要根据实际需求做取舍,有时候这是一门关于平衡的艺术。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大凝的IC进阶之路

一起学习一起进步

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值