八位「Booth二位乘算法」乘法器

八位「Booth二位乘算法」乘法器

原理

補碼乘法器

以前介紹了幾篇無符號乘法器或加法器的寫法,固然,稍做修改也就能夠改爲符合有符號數的乘法器或加法器。算法

可是呢,咱們以前寫的乘法器或加法器,其實都是默認是正數來寫的,並且是以正數的原碼來寫的,因此上面說稍做修改也就能夠成爲有符號數的乘法器或加法器,其實就是對咱們覺得的原碼進行取補碼,再進行乘法或加法的運算。學習

隨着計算機硬件部件的升級,處理器技術的發展,現代處理器中的定點數(小數點位置固定)都是按照補碼形式來存儲的。測試

因此在以前寫的無符號加法器中,只要利用:設計

\[X_補+Y_補=[X+Y]_補 \]

就能夠輕易將原先的加法器改寫成有符號加法器——只要對結果再取一次補碼便可。code

可是乘法器呢?稍做學習能夠知道,補碼的乘法是這樣的:three

\[X*Y_補=[X*Y]_補 \]

咱們再考慮一下以前所說的:在現代處理器中的定點數都是按照補碼形式來存儲的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它的原碼是多少呢?根據:

\[[X_補]_補=X \]

Y補的原碼Y應該爲:

\[Y=(y_7*2^7+y_6*2^6+y_5*2^5+……+y_0*2^0)-1*2^8 \]

稍微化簡一下:

\[Y=-y_7*2^7+(y_6*2^6+y_5*2^5+……+y_0*2^0) \]

因此咱們若是想求X*Y,能夠先求其補碼:

\[[X*Y]_補=[X*(-y_7*2^7)+X*(y_6*2^6+y_5*2^5+……+y_0*2^0)]_補 \]

根據補碼加法「X補+Y補=[X+Y]補」再稍微化簡一下:

\[[X*Y]_補=-y_7*[X*2^7]_補+y_6*[X*2^6]_補+y_5*[X*2^5]_補+……+y_0*[X*2^0]_補 \]

再引入一個定理:

\[[X*2^n]_補=X_補*2^n \]

因此上式又能夠換一種寫法:

\[[X*Y]_補=X_補*(-y_7*2^7+(y_6*2^6+y_5*2^5+……+y_0*2^0))=Y*X_補 \]

哦這不就是上面介紹過的補碼乘法嘛:

\[[X*Y]_補=Y*X_補=X*Y_補 \]

若是令一個數Y1補=y6y6y5y4y3y2y1y0,去掉了首位,那麼上式是否是能夠理解爲:

\[[X*Y]_補=X_補*Y1_補-y_7*X_補*2^7 \]

其中的Y1補不就恰好是Y補的後7位嘛?也就是說一個乘法能夠分爲兩部分理解:首位的乘法和其餘位的乘法。首位的乘法產生的部分積符號是減,其餘位的部分積符號爲加

通過上面的推導你們應該會對補碼乘法的原理有了必定的概念,咱們來把它寫成豎式的形式,以(-6)x(-7)爲例,原碼乘應該是1110x1111,在計算機中是以補碼的形式存儲,因此補碼乘是1010x1001,代入公式,令X補=1010Y補=1001,其運算過程以下:

這裏可能有一些迷惑的是:爲何第一步運算獲得的結果是11111010?爲何要在前面填充1111

這也就是所謂的符號填充,咱們以前的設計中都沒有涉及到符號位,因此默認都是填充0,如今遇到了負數問題,也就須要填充符號了,可是這樣看起來是否是一點都以爲很奇怪?若是沒辦法理解的話,我建議你能夠嘗試對它求補碼,看看是否是能夠保持首位符號位不變,餘位取反加一。驚歎於設計師的機智。

補碼乘法器的原理講明白了,具體電路實現的話,你們能夠嘗試一下,本節重點不在於此。

Booth一位乘

在上面已經討論了補碼乘法器的原理,那麼什麼是Booth乘法器呢?Booth乘法器是由英國的Booth夫婦提出的,並無什麼特殊含義,因此咱們直接快進到內容。

通過補碼乘法器的推導:

\[[X*Y]_補=X_補*(-y_7*2^7+(y_6*2^6+y_5*2^5+……+y_0*2^0)) \]

參考中學數學:

\[2^n=2*2^{n-1} \]

其核心計算思想是括號裏的形式,也就是Y補的原碼Y因此咱們對括號裏的內容再進行分解合併,也就是對Y分解合併。先分解:

\[Y=-y_7*2^7+((2-1)y_6*2^6+(2-1)y_5*2^5+……+(2-1)y_0*2^0) \]

這樣應該挺直觀了吧:

\[Y=-y_7*2^7+(y_6*2^7-y_6*2^6)+(y_5*2^6-y_5*2^5)+……+(y_0*2^1-y_0*2^0) \]

再合併:

\[Y=(y_6-y_7)*2^7+(y_5-y_6)*2^6+(y_4-y_5)*2^5+……+(0-y_0)*2^0 \]

最後有個0-y0的項,看起來有點不合羣,因此令:

\[y_{-1}=0 \]

代入上式,即:

\[Y=(y_6-y_7)*2^7+(y_5-y_6)*2^6+(y_4-y_5)*2^5+……+(y_{-1}-y_0)*2^0 \]

這也就是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二位乘就不同了,經過增長必定的空間複雜度,將運算週期減爲一半!

Booth二位乘

仍是根據補碼乘法器,咱們將Y的表達式再進行變換——先分解:

\[Y=-2*y_7*2^6+y_6*2^6+(y_5*2^6-2*y_5*2^4)+……+y_0*2^0+y_{-1}*2^0 \]

再整合:

\[Y=(y_5+y_6-2*y_7)*2^6+(y_3+y_4-2*y_5)*2^4)+……+(y_{-1}+y_0-2*y_1)*2^0 \]

好了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特性

考慮到上述減法變加法的操做後,容易總結出:減法變加法,其實就是對補碼的符號位取反,也就是對減數每一位取反後再加一。

再回讀一邊上述的理論部分,可能你會發現,在乘法運算中,只用到了補碼「負補碼」兩種概念的數字。而在vivado中(至關於在處理器中),數字默認是以補碼形式存儲的,即輸入的乘數默認就是補碼形式,這樣只須要再求出「負補碼」便可。設X[3:0]表示一個乘數,默認是以補碼形式存儲,則其「負補碼」:

\[X_{負補碼}=!X + 1 \]

至於其原碼:

\[X_{原碼}=(X[3],!X[2:0]) + 1 \]

其實根本用不着。

有了以上知識儲備,咱們就能夠寫代碼啦~

設計文件

//因爲實力不夠,沒能設計成改一個數字變一個規模的程序
`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

仿真波形

將其改爲有符號十進制數形式顯示,能夠驗證電路設計正確。

相關文章
相關標籤/搜索