用FPGA写代码时候,尽量不用“/”,因为其占用大量逻辑资源。所以有必要来设计一款除法器,使其不占用那么多逻辑资源,并且所用时钟数能够满足约束要求。(减弱空间复杂度、增加时间复杂度,并且满足时间约束)。
计算机内部乘除法原理
众所周知,计算机所能完成的基本元操作是:+(-),左移,右移等指令。
乘法实现
计算机中用二进制进行计数,例如8(D) = 1000(B)。任何一个数均可以表示为下式:
所以数a乘以X,就变成了下式子:
此即计算机乘法原理对计算机而言,
左移1位代表乘以2,
右移1位代表除以2。
所以,对于a乘以x而言,只是将a左移x为1的位并累加即可。
举例说明:5*3=5 *(2+1)
1. 3=0011(不用分解,计算机就是这么存储的);
2. 3的第0位1,5左移0位仍为5;
3. 3的第一位为1,5左移1位为5 * 2 = 10;
4. 然后将其累加,得到最后结果15。
除法实现
传统的十进制除法计算
当我们在计算51/3=17,抛开9*9乘法表。
1. 从被除数的最高位5开始,从0-9选一个数,使得5-i*3>=0且使5-(i+1)3<0。我们选择了1,余数为2;
2. 将余数 * 10+1=21,继续从0-9中选一个数,使得21-3i>=0且使21-(i+1)*3<0,我们选择了7;
3. 由此,我们找到了答案17。
计算机除法计算
计算机计算除法的过程与人类计算的过程很类似,只是选择范围变成了0或1。
还以51/3为例说明(51:110011;3:11)
1. 从第一位开始,为1,小于11,结果位置0,余数为1;
2. 从第二位开始,余数2+1=11,等于11,结果位置1,余数为0;
3. 从第三、四位开始,余数2+0=0<011,结果位置0,余数为0;
4. 从第5位开始,余数2+1=1<11,结果置0,余数为1;
5. 从第6位开始,余数2+1=11=11,结果置1,余数为0。
把结果位相连恰好是010001(17)。
以上所介绍的乘除法原理都有对应的C语言代码参考>https://blog.csdn.net/zdavb/article/details/47108505,前面介绍只是参考扩充而已,我们主要是用Verilog HDL写代码,慢慢往下文看。
计算机补码
正数的补码是其本身,负数的补码是符号位不变,除此之外的位取反加一;
计算机中补码运算时,符号位都是参与运算的。
比如1_110(-2)+0_101(+5)=0_101(+3);
补码的补码就是原码。
加法运算:
[x+y]补=[x]补+[y]补
可直接相加。
减法运算:
[x-y]补=[x]补+(-[y]补)
x的补码加上y补码的相反数。(符号也要相反)(称为机器负数)。
计算机里,加法与减法是统一的,能够形成统一是由于补码。须知道,俩个正整数相减,可以看成是一个正整数加一个负整数,进一步,俩个正整数相减是用一个正整数的补码加上一个负整数的补码来得到的。
在这里,我主要想介绍的是补码运算时的溢出判断
溢出判断
俩个相同位数的二进制补码相加,得到的正确结果位数与俩加数相同,并且符号位准确。
这里的溢出不是指
二进制无符号数据位相加,然后最高位有溢出则称作溢出的。(FPGA里对有符号的数是怎么运算的?要好好了解一下。)
首先,一个正数和一个负数相加,结果一定不会溢出(因为结果的绝对值一定小于两个加数的绝对值,两个加数都表达出来了,结果定能表达出来)。
所以,溢出只能是符号相同的俩数相加。
正+正:
符号位0,数位相加,如果结果的符号位变成1了,那一定是两个加数的最高位相加进上来的。 发生溢出。
负+负:
符号位都是1,所以符号位一定会进位。数位相加,如果最后符号位是0,说明结果变成正的了,那一定是发生溢出了(负+负!=正)。
大家会困惑“为何负数补码最高位相加,发生进位的不是溢出,反而不进位是溢出呢?”在补码发生溢出的情况中,在补码发生溢出的情况中,
正数是因为太大发生溢出;
但是负数是因为它太小发生的溢出。
有进位说明两个负数较大。(数据位未进位到符号位,符号位自己发生进位溢出了)
大家想,负数比较小,那么他们的补码肯定是比较大的,那数据位相加会有进位可能,而符号位已经都为1了,
那么1+1=0,然后再加上数据位进位的1,所以符号位最后得出的结果还是负数。
当负数比较大的时候,他们的补码比较小,数据位是没有溢出給符号位,可是因为符号位是全1,所以符号位会溢出,
那么最后得到的结果只有扩大一位位宽,用来存放溢出位并作为新的符号位才对。
比如1_001(-7)+1_100(-4)=1_0101(-11)。
双符号位判断溢出:
原来符号位只有一个,现在为了方便再加一个。
正数:00 负数 11
结果01时,结果为正,发生正溢出(正数太大了)
结果10时,结果为负,发生负溢出(负数太小了)
还是00或11就是没有溢出了。
有人会问,难道负溢出就不会是11了吗?
举例:我们想让结果进位到符号位,又要让加数最小(绝对值最大,这样才能溢出)
11100是最好的用例(这是发生进位的最小补码)。
我们把两个 11100 相加
11100+11100=111000 -> 11000 这时发生溢出了吗?
11000是-8,加数 11100是-4。
移位运算:
算数移位:在二进制中,算数移位的左移 每移一位表示*2,右移表示/2 ;
原码移位,符号位不参与移位;
补码移位,符号位参与移位。左移时符号位左移,右移时符号位不变,最高位补符号位;
逻辑移位:把数字看成一串二进制数,让怎么移就怎么移,符号位和数位没区别。
正整数型移位除法器
假设被除数与除数都是八位数据,这里的算法是:
将被除数,扩展成16位的数据,低8位为被除数的值,高八位的初始值全为0。有开始信号,对16位数据data赋值,然后开始运算。比较data的高八位和除数的值,如果大于0,说明被除数大,将此时商置1,赋值给data的最低位,然后将被除数高八位减去除数。然后将data向左移位一位,继续比较。最终计算8次后。Data的高8位数据就为所求的余数,低八位就为所求的商。
原理说白了就是:先移位,在比较(作差),迭代八次。
下面就是写Verilog Code了,我所用的FPGA型号是ALTERA公司的EP4CE30F23C8N。
/*
/
module fifo
#(parameter MSB=3, LSB=0, DEPTH=4)//可以被重写
(port_list );
item;
endmodule
在上层模块对参数重写的方式如下:F1.MSB=4;F1.LSB=2;fifo F1;
/
module fifo
(port_list );
parameter MSB=3, LSB=0, DEPTH=4//可以被重写
endmodule
在上层模块对参数重写方式如下:fifo #(4,2) F1(port_list);
/
module fifo
#(parameter MSB=3, LSB=0)//可以被重写
(port_list );
parameter DEPTH=4; //不能被重写
endmodule
在上层模块对参数重写方式如下:fifo #(.LSB(2), .MSB(4)) fifo(port_list);
*/
module DIV_INT_TYPE #
( parameter E = 16, //Extension of bits
parameter D = 8 //The bits of dividend and divisor
)(
input clk,//50MHz
input rst_n,
input start, //除法开始的使能标志
output reg busy, //start开启后,busy=1代表除法器在忙,除法器被占用。当除法器忙的时候,start就要为0;
input [D-1:0] dividend, //[ˈdɪvɪdend] 被除数
input [D-1:0] divisor, // [dɪˈvaɪzər] 除数
output wire [D-1:0] quotient, // [ˈkwoʊʃnt] 商
output wire [D-1:0] remainder, // [rɪˈmeɪndər] 余数
output reg finish //除法完成
);
/
///以下是一段式状态机来写的整数除法器内核
reg [1:0] state;//状态机
reg [D-1:0] count;
reg [E-1:0] data_next;
always @ ( posedge clk or negedge rst_n )begin
if( !rst_n )begin
count <= D;
state <= 2'd0;
end
else begin
case( state )
2'd0:begin
finish <= 1'b0;
busy <= 1'b0;
if( start == 1'b1 )begin
data_next <= {{E-D{1'b0}},dividend};
state <= 2'd1;
end
else begin
data_next <= 0;
state <= 2'd0;
end
end
2'd1:begin
if( data_next[E-1:D] >= divisor )begin//如果余数大于等于除数
//data_next[0] = 1'b1;
//data_next[E-1:D] = data_next[E-1:D] - divisor;
//如果余数大于除数,那就对data_next做相应运算,可是我们接着要对运算完的data_next进行移位操作,这样才不会吃时钟,所以把这两步操作合为一体91行即是
if( count == 0 )begin
state <= 2'd0;
finish <= 1'b1;
busy <= 1'b0;
data_next <= data_next;
count <= D;
end
else begin
state <= state;
finish <= 1'b0;
busy <= 1'b1;
data_next[E-1:0] <= {{data_next[E-1:D] - divisor},data_next[D-1:1],1'b1} << 1'b1;
count <= count - 1'b1;
end
end
else begin
if( count == 0 )begin
state <= 2'd0;
finish <= 1'b1;
busy <= 1'b0;
data_next <= data_next;
count <= D;
end
else begin
state <= state;
finish <= 1'b0;
busy <= 1'b1;
data_next <= data_next << 1'b1;
count <= count - 1'b1;
end
end
end
default:begin
count <= D;
state <= 2'd0;
end
endcase
end
end
assign quotient = finish?data_next[D-1:0] : quotient;
assign remainder = finish?data_next[E-1:D] : remainder;
/
/
///下面为二段式状态机所写的整数除法器内核
/*
parameter idle = 3'b000;
parameter shift = 3'b001;
parameter done = 3'b010;
reg [2:0] state_current;
reg [2:0] state_next;
reg [E-1:0] data_next;
reg [E-1:0] temp;
reg [D-1:0] count;//count用来计数移位次数
//俩段式状态机,此模块是时序逻辑进行当前状态和下一状态的切换。。俩段式状态机组合逻辑那块用来实现各个输入输出以及状态判断
always @ ( posedge clk or negedge rst_n )begin
if( !rst_n )begin
state_current <= idle;
count <= D;
end
else begin
state_current <= state_next;
if( state_current == shift )begin
count <= count-1'b1;
end
else begin
count <= D;
end
end
end
///
always @ ( posedge clk )begin//always块有俩种触发方式:1,边沿触发;2,电平触发。在这里是说always块里语句里面所有输入信号只要其中一个发生变化就能触发always块语句
case( state_current )
idle:begin
finish = 1'b0;
busy = 1'b0;
if( start == 1'b1 )begin
data_next = {{E-D{1'b0}},dividend};//初始值={0000_0000,dividend};E-D=8,8'{1'b0}={1'b0,1'b0,1'b0,1'b0,1'b0,1'b0,1'b0,1'b0}
state_next = shift;
end
else begin
state_next = idle;
data_next = 0;
end
end
shift:begin
finish = 1'b0;
busy = 1'b1;
data_next = data_next << 1;//{data_next[E-2:0],1'b0};//移位操作
if( data_next[E-1:D] >= divisor )begin//如果余数大于等于除数
data_next[0] = 1'b1;
data_next[E-1:D] = data_next[E-1:D] - divisor;
end
else begin
data_next = data_next;
end
if( count == 1 )begin
state_next = done;
end
else begin
state_next = shift;
end
end
done:begin
finish = 1'b1;
busy = 1'b0;
state_next = idle;
end
default:begin
finish = 1'b0;
busy = 1'b0;
data_next = 0;
end
endcase
end
assign quotient = finish?data_next[D-1:0] : quotient;
assign remainder = finish?data_next[E-1:D] : remainder;
*/
endmodule
module CTRL_DIV #
( parameter D = 8 )
(
input clk,
input rst_n,
input busy,
input finish,
output reg start,
output reg [D-1:0] dividend,
output reg [D-1:0] divisor
);
reg [1:0] i;
always @ ( posedge clk or negedge rst_n )begin
if( !rst_n )begin
i <= 2'd0;
start <= 1'b0;
dividend <= { D{1'b0} };
divisor <= { D{1'b0} };
end
else begin
case( i )
2'd0:
if( busy == 1'b0 )begin
i <= i+1'b1;
divisor <= {{ D-1{1'B0} },1'B1};//1
dividend <= {{ D-1{1'B1} },1'B0};//254
start <= 1'b1;
end
else begin
i <= i;
divisor <= divisor;
dividend <= dividend;
start <= 1'b0;
end
2'd1:
if( finish )begin
dividend <= dividend;
if( divisor == {{ D-1{1'B1} },1'B0} )begin
divisor <= divisor;
start <= 1'b0;
i <= 2'd0;
end
else begin
divisor <= divisor + 1'b1;
start <= 1'b1;
i <= i;
end
end
else begin
dividend <= dividend;
divisor <= divisor;
start <= 1'b0;
i <= i;
end
default:begin
i <= 2'd0;
start <= 1'b0;
dividend <= { D{1'b0} };
divisor <= { D{1'b0} };
end
endcase
end
end
endmodule
module TOP #( parameter D = 8 )
(
input clk,
input rst_n,
output [D-1:0] quotient, // [ˈkwoʊʃnt] 商
output [D-1:0] remainder, // [rɪˈmeɪndər] 余数
output wire finish //除法完成
);
wire busy;
DIV_INT_TYPE #(.E( 16 ),.D( 8 )) DIV_INT_TYPE(
.clk( clk ),
.rst_n( rst_n ),
.start( start ),
.busy( busy ),
.dividend( dividend ),
.divisor( divisor ),
.quotient( quotient ),
.remainder( remainder ),
.finish( finish )
);
wire start;
wire [D-1:0] dividend;
wire [D-1:0] divisor;
CTRL_DIV # ( .D( 8 ))(
.clk( clk ),
.rst_n( rst_n ),
.busy( busy ),
.finish( finish ),
.start( start ),
.dividend( dividend ),
.divisor( divisor )
);
endmodule
Signal Tap II仿真波形见下图:
被除数是8位固定值254,除数是在一次除法完成后就进行累加。经测试,除法器所用时钟为固定不变的11个clk,时钟频率是50MHz。
上面的代码中俩段式状态机代码是错误的,不要看。
吃时钟的整数型除法器设计
介绍此吃时钟除法器设计是为下面“整数型除法器设计”作铺垫的。后面的设计是根据此设计来的。注意,在FPGA里,对于一个IP核若输出或输出有负数,都是用补码形式进行的。所以要对补码的运算有了解,一个正数的补码加一个负数的补码不会溢出。
此设计只止于思维上的分析了,用C语言也完全可以写出来,在时钟消耗方面没有下功夫,未得FPGA精髓。
代码算法理念:
1. 先取除数和被除数的正负关系,然后正值化被除数。除数取负值的补码;
2. 被除数递减与除数,每一次的递减,商数递增;
3. 直到被除数小于除数,递减过程剩下的都是余数;
4. 输出的结果根据除数和被除数的正负关系来划分。
module DIVIDER_SIGN_INT//八位带符号除法器内核
(
input clk, //50MHz时钟
input rst_n, //全局复位信号
input start, //除法器使能信号,为“1”时,除法器状态机才能执行
input [7:0] dividend, //被除数
input [7:0] divisor, //除数
output done, //一次除法完成
output [7:0] quotient, //商
output [7:0] reminder //余数
);
reg [3:0] i; //状态机
reg [7:0] dend; //寄存器,对被除数取正,即存取被除数绝对值的补码
reg [7:0] dsor_n; //寄存器,对除数取负,即存取( -|除数| )补码
reg [7:0] dsor_p; //寄存器,对除数取正,即存取除数绝对值的补码
reg [7:0] q; //寄存器,商。用于always块
reg dif; //商正负符号判别标志
reg done_sig;
always @ ( posedge clk or negedge rst_n )begin
if( !rst_n )begin
i <= 4'd0;
dend <= 8'd0;
dsor_n <= 8'd0;
dsor_p <= 8'd0;
q <= 8'd0;
dif <= 1'b0;
done_sig <= 1'b0;
end
else if( start )begin
case( i )
4'd0:begin
dend <= dividend[7] ? ~dividend + 1'b1 : dividend;//被除数取正
dsor_n <= divisor[7] ? divisor : (~divisor + 1'b1);//除数取负
dif <= dividend[7] ^ divisor[7];//正负判断,之前错误写的是dif <= dividend ^ divisor;
q <= 8'd0;//商初值化
i <= i+1'b1;//进入下一状态机
end
4'd1:begin
if( dsor_p > dend )begin//正除数与每一步的正余数比较,当正除数大于( |被除数|-N*|除数| )时,除法器就可以输出最终值了。算法中思想是,被除数是由几个除数组成。
q <= dif ? (~q + 1'b1) : q;//输出余数补码形式,除法过程本身q是以正数补码叠加的;所以当被除数与除数异号时,q要以对应的负数补码形式输出。
i <= i+1'b1;//一次除法完成,进入下一状态机。
end
else begin
dend <= dend + dsor;//dend在除法动作过程中,肯定是越来越小,因为一个正数和负数的和还是正数,且和小于前面的正加数。
q <= q+1'b1;//每进行一次叠加,就表示对被除数的一次细分
i <= i;//对被除数进行细分,多次细分未完成,状态机不变,只有当dsor_p > dend时,细分才结束,状态机才跳转
end
end
4'd2:begin
done_sig <= 1'b1;//一次除法已经完成标志,除法完成标志是一个周期的正脉冲
i <= i+1'b1;
end
4'd3:begin
done_sig <= 1'b0;
i <= 4'd0;
end
default:begin
i <= 4'd0;
dend <= 8'd0;
dsor <= 8'd0;
q <= 8'd0;
dif <= 1'b0;
done_sig <= 1'b0;
end
endcase
assign done = done_sig;
assign quotient = q;
assign reminder = dend;
endmodule
下面是Signal Tap II仿真波形
仿真结果很直观暴露了该除法器的弱点,即不同的除数,就有不一样的时钟消耗。时钟消耗是根据除数的“数量”来做决定。
整数型除法器设计
在设计中,补码运算采用了双符号来进行对被除数与除数绝对值的比较。正数的补码与负数的补码进行相加,正数补码的符号位就是“00”,负数补码的符号位是“11”,符号位左边为高位,右边为低位。通过对正负数补码相加后得到的值的高位符号位进行判别二者模的大小。如果高位为“1”,则代表负数模大,反之则代表正数模大。此算法与楼上“正整数型除法器设计”的算法思路是一致的,只不过楼上是正整数除法,而这里是整数除法。仿真读者可以自行用Modelsim来写Test Bench模块验证一下,个人建议有板子的话用Quartus自带的Signal Tap II验证更为准确。
module DIV_SIGN_INT(
input clk, //50MHz时钟
input rst_n, //全局复位
input start, //除法器使能信号
input [7:0] dividend; //被除数
input [7:0] divisor, //除数
output finish, //一次除法完成标志
output [7:0] quotient, //商
output [7:0] reminder //余数
);
reg [3:0] i; //状态机
reg [8:0] dsor; //双符号位11_| divisor[6:0] |补码
reg [15:0] temp; //除法器移位比较运算的扩展空间,temp = 0000 00_00 |dividend|补码
reg [15:0] dif_compare; //此空间是用于 |被除数|补码 左移后 与 (-|除数|)补码的比较,注意都是双符号位,当dif[15]为1则代表负数(负除数双符号位的补码表示)大,反之代表正数(指双符号位被除数正值化)大。
reg dif; //被除数与除同号与否的判断
reg finish_sig; //一次除法完成的标志位,完成后输出一个时钟正脉冲
always @ ( posedge clk or negedge rst_n )begin
if( !rst_n )begin
i <= 4'd0;
dsor <= 9'd0;
temp <= 16'd0;
dif_compare <= 16'd0;
dif <= 1'b0;
finish_sig <= 1'b0;
end
else if( start )begin
case( i )
0:begin
dif <= dividend[7] ^ divisor[7];//判断二者符号同异
dsor <= divisor[7] ? { 1'b1,divisor } : { 1'b1,~divisor+1'b1 };//除数取负化,且采用双符号位表示
temp <= dividend[7] ? { 8'd0,~dividend+1'b1 } : { 8'd0,dividend };//被除数取正化放于低八位
dif_compare <= 16'd0;
i <= i+1'b1;
end
1,2,3,4,5,6,7,8:begin
dif_compare = temp + { dsor,7'd0 };
if( dif_compare[15] )begin //如果除数绝对值大,temp继续左移,低位补零
temp <= { temp[14:0],1'b0 };
else begin //如果被除数大于除数绝对值,左移,并且低位补1,最后移位8次后,低位为商,高位为余数
temp <= { dif_compare[14:0],1'b1 };
end
i <= i+1'b1;
end
9:begin
finish_sig <= 1'b1;
i <= i+1'b1;
end
10:begin
finish_sig <= 1'b0;
i <= 2'd0;
end
endcase
end
end
assign finish = finish_sig;
assign quotient = dif ? ( ~temp[7:0]+1'b1 ) : temp[7:0];
assign reminder = temp[15:8];
endmodule