以前介绍了几篇无符号乘法器或加法器的写法,固然,稍做修改也就能够改为符合有符号数的乘法器或加法器。算法
可是呢,咱们以前写的乘法器或加法器,其实都是默认是正数来写的,并且是以正数的原码来写的,因此上面说稍做修改也就能够成为有符号数的乘法器或加法器,其实就是对咱们觉得的原码进行取补码,再进行乘法或加法的运算。学习
随着计算机硬件部件的升级,处理器技术的发展,现代处理器中的定点数(小数点位置固定)都是按照补码形式来存储的。测试
因此在以前写的无符号加法器中,只要利用:设计
就能够轻易将原先的加法器改写成有符号加法器——只要对结果再取一次补码便可。code
可是乘法器呢?稍做学习能够知道,补码的乘法是这样的:three
咱们再考虑一下以前所说的:在现代处理器中的定点数都是按照补码形式来存储的。input
因此咱们要想获得两个数的乘法结果,首先应该知道被乘数的原码和补码,再对最终结果取补码,便可获得咱们指望的乘法结果。数学
那么如何求“X*Y补
”呢?在处理器中,一个二进制数Y补
形如y7y6y5y4y3y2y1y0
,也就是表示一个数的补码,那么它的原码是多少呢?it
补码的计算方法,除了“首位不变,余位取反再加一”的方式,还有一种就是“用溢出条件来减这个数”,在咱们以前第一节课说二进制的时候,以钟表为例——“十二进制”,获得结论——“4
是-8
的补码”。table
咱们用第二种取补码的方式:-8的补码=12-8=4
(这里没有考虑符号问题,只是求了补码的值)
因此考虑一下符号的话,-8的补码=8-12=-4
同理:
十进制下,-4的补码=4-10=-6
二进制下,-101补码=1101补码=101-1000=-011=1011
这样解决求补码的方式在接下来的计算方面就更方便了,至于正数嘛,不变就行了。
回到上面的问题,一个二进制数Y补
形如y7y6y5y4y3y2y1y0
,它的原码是多少呢?根据:
Y补
的原码Y
应该为:
稍微化简一下:
因此咱们若是想求X*Y
,能够先求其补码:
根据补码加法“X补+Y补=[X+Y]补
”再稍微化简一下:
再引入一个定理:
因此上式又能够换一种写法:
哦这不就是上面介绍过的补码乘法嘛:
若是令一个数Y1补=y6y6y5y4y3y2y1y0
,去掉了首位,那么上式是否是能够理解为:
其中的Y1补
不就恰好是Y补
的后7位嘛?也就是说一个乘法能够分为两部分理解:首位的乘法和其余位的乘法。首位的乘法产生的部分积符号是减,其余位的部分积符号为加。
通过上面的推导你们应该会对补码乘法的原理有了必定的概念,咱们来把它写成竖式的形式,以(-6)x(-7)
为例,原码乘应该是1110x1111
,在计算机中是以补码的形式存储,因此补码乘是1010x1001
,代入公式,令X补=1010
,Y补=1001
,其运算过程以下:
这里可能有一些迷惑的是:为何第一步运算获得的结果是11111010
?为何要在前面填充1111
?
这也就是所谓的符号填充,咱们以前的设计中都没有涉及到符号位,因此默认都是填充0
,如今遇到了负数问题,也就须要填充符号了,可是这样看起来是否是一点都以为很奇怪?若是没办法理解的话,我建议你能够尝试对它求补码,看看是否是能够保持首位符号位不变,余位取反加一。惊叹于设计师的机智。
补码乘法器的原理讲明白了,具体电路实现的话,你们能够尝试一下,本节重点不在于此。
在上面已经讨论了补码乘法器的原理,那么什么是Booth
乘法器呢?Booth
乘法器是由英国的Booth
夫妇提出的,并无什么特殊含义,因此咱们直接快进到内容。
通过补码乘法器的推导:
参考中学数学:
其核心计算思想是括号里的形式,也就是Y补
的原码Y
,因此咱们对括号里的内容再进行分解合并,也就是对Y
分解合并。先分解:
这样应该挺直观了吧:
再合并:
最后有个0-y0
的项,看起来有点不合群,因此令:
代入上式,即:
这也就是Booth
一位乘算法的原理。其优势就在于不用再像补码乘法器那样,不须要专门对最后一次部分积采用补码减法。
根据上式,还能够列出Booth
一位乘的规则:
y(i-1) | y(i) | y(i-1) - y(i) | 操做 |
---|---|---|---|
0 | 0 | 0 | 加0 |
0 | 1 | -1 | 减X补 |
1 | 0 | 1 | 加X补 |
1 | 1 | 0 | 加0 |
再举个例子来计算,仍以(-6)x(-7)
为例,补码乘是1010x1001
,列出竖式:
但是这里为何仍是有减法呢?和常规的补码乘法器相比,简直是老和尚抹洗头膏,大可没必要。甚至因为每次判断两位数字,增大了电路的复杂度,那么为何booth乘法器如此好用呢?
其实booth
一位乘算法并不经常使用,可是booth二位乘就不同了,经过增长必定的空间复杂度,将运算周期减为一半!
仍是根据补码乘法器,咱们将Y
的表达式再进行变换——先分解:
再整合:
好了Booth
二位乘算法也完事了,类比于Booth
一位乘,咱们也能够列出Booth
二位乘的规则:
y(i-1) | y(i) | y(i+1) | y(i-1) + y(i) - 2*y(i+1) | 操做 |
---|---|---|---|---|
0 | 0 | 0 | 0 | 加0 |
0 | 1 | 0 | 1 | 加X补 |
1 | 0 | 0 | 1 | 加X补 |
1 | 1 | 0 | 2 | 加2*X补 ,即X补<<1 |
0 | 0 | 1 | -2 | 减2*X补 ,即X补<<1 |
0 | 1 | 1 | -1 | 减X补 |
1 | 0 | 1 | -1 | 减X补 |
1 | 1 | 1 | 0 | 加0 |
再举个例子来计算,仍以(-6)x(-7)
为例,补码乘是1010x1001
,列出竖式:
运算周期减半了!
好了,那Booth
乘法器有没有三位乘呢?能够有,可是三位的时候就会出现加3*X补
,2*X补
能够经过左移一位获得,而3*X补
就有点麻烦了,因此再也不介绍,至于四位乘、八位乘,想挑战的同窗能够挑战一下。
首先咱们来解决一个问题,如何把减法消除?咱们知道,减去一个数,等于加上这个数的相反数;减去一个数,也等于加上这个数的补码。这个过程当中的减数也默认是正数,由于正数的补码仍是正数,只有正数前面加一个符号再去补码才有用。那么如上面竖式所写,减去一个负补码,就应该等于加上“这个负补码的补码的相反数”,好比上面的补码乘法器竖式,就应该变换成以下形式:
再说明一下吧:减11010
,就至关于加11010
的补码的相反数,即加10110
的相反数,即00110
。
因此booth
一位乘算法的示例应该变成这样:
booth
二位乘算法的示例应该变成这样:
考虑到上述减法变加法的操做后,容易总结出:减法变加法,其实就是对补码的符号位取反,也就是对减数每一位取反后再加一。
再回读一边上述的理论部分,可能你会发现,在乘法运算中,只用到了补码和“负补码”两种概念的数字。而在vivado
中(至关于在处理器中),数字默认是以补码形式存储的,即输入的乘数默认就是补码形式,这样只须要再求出“负补码”便可。设X[3:0]
表示一个乘数,默认是以补码形式存储,则其“负补码”:
至于其原码:
其实根本用不着。
有了以上知识储备,咱们就能够写代码啦~
//因为实力不够,没能设计成改一个数字变一个规模的程序 `define size 8 module mul_booth_signed( input wire [`size - 1 : 0] mul1,mul2, input clk, input wire [2:0] clk_cnt,//运算节拍,至关于状态机了,8位的话每次运算有4个拍 output wire [2*`size - 1 : 0] res ); //因为传值默认就是补码,因此只须要再计算“负补码”便可 wire [`size - 1 : 0] bmul1,bmul2; assign bmul1 = (~mul1 + 1'b1) ; assign bmul2 = (~mul2 + 1'b1) ;//其实乘数2的负补码也没用到。 //其实能够把状态机的开始和结束状态都写出来,我懒得写了,同窗们能够尝试一下啊~ parameter zeroone = 3'b00, twothree = 3'b001, fourfive = 3'b010, sixseven = 3'b011; //y(i-1),y(i),y(i+1)三个数的判断寄存器,因为有多种状况,也能够当作状态机(也能够改写成状态机形式,你们本身试试吧) reg [2:0] temp; //部分积 reg [2*`size-1 : 0] A; //每一个节拍下把相应位置的数据传给temp寄存器 always @ (posedge clk) begin case(clk_cnt) zeroone : temp <= {mul2[1:0],1'b0}; twothree : temp <= mul2[3:1]; fourfive : temp <= mul2[5:3]; sixseven : temp <= mul2[7:5]; default : temp <= 0; endcase end always @(posedge clk) begin if (clk_cnt == 3'b100) begin//若是节拍到4就让部分积归0,此时已经完成一次计算了 A <= 0; end else case (temp) 3'b000,3'b111 : begin//这些是从高位到低位的判断,别看反了噢 A <= A + 0; end 3'b001,3'b010 : begin//加法操做使用补码便可,倍数利用左移解决 A <= A + ({{8{mul1[`size-1]}},mul1} << 2*(clk_cnt-1)); end 3'b011 : begin A <= A + ({{8{mul1[`size-1]}},mul1} << 2*(clk_cnt-1) + 1); end 3'b100: begin//减法操做利用“负补码”改为加法操做,倍数利用左移解决 A <= A + ({{8{bmul1[`size-1]}},bmul1} << 2*(clk_cnt-1) + 1); end 3'b101,3'b110 : begin A <= A + ({{8{bmul1[`size-1]}},bmul1} << 2*(clk_cnt-1)); end default: A <= 0; endcase end //当节拍到4的时候写入结果寄存器。 assign res = (clk_cnt == 3'b100) ? A : 0; endmodule
这是一个八位Booth
二位乘算法的乘法器,至于Booth
一位和Booth
四位的乘法器,你们各自尝试就好。
此外在这个文件当中,我用到了clk_cnt
这个寄存器,你们是否是觉得我会多用一个模块用来产生clk_cnt
的波形?
身为一个懒人,我直接在测试文件里写了吼吼吼~
37
个元件,36
个IO口,318
根线
`timescale 1ns / 1ps module mul_tb( ); reg [7:0] mul1,mul2; wire [15:0] res; reg clk; wire clk_en; reg [2:0] clk_cnt; initial begin mul1 <= -8'd7; mul2 <= -8'd3; clk <= 0; clk_cnt <= 3'b0; end always # 10 clk = ~clk; //clk_cnt发生器,懒人版 always @(posedge clk) begin clk_cnt <= clk_cnt + 1'b1; if (clk_cnt == 3'b100) clk_cnt <= 3'b00; end //每次运算结束后,让乘数变化,以便产生不一样的数据用以观察 assign clk_en = (clk_cnt == 3'b100) ? 1'b1 : 1'b0; always @ (posedge clk_en) begin mul2 <= mul2 + 1'b1; end mul_booth_signed try(.mul1(mul1),.mul2(mul2),.res(res),.clk(clk),.clk_cnt(clk_cnt)); endmodule
将其改为有符号十进制数形式显示,能够验证电路设计正确。