FPGA可以通过除号直接实现除法,但是当除数或被除数位宽较大时,计算会变得缓慢,导致时序约束不能通过。此时可以通过在除法IP中加入流水线来提高最大时钟频率,这种方式提高时钟频率也很有限。如果还不能达到要求,就只能把除法器拆分,来提高系统时钟频率。
其实最简单的方式是使用计数器对除数进行累加,并且把累加的次数寄存,当累加结果大于等于被除数时,此时寄存的累加次数就是商,而被除数减去累加结果就得到余数。
但这种方式存在一种弊端,当除数很小的时候,被除数特别大时,需要经过很多个时钟周期才能计算除结果。比如被除数为100,除数为1,就需要100个时钟左右才能计算出结果,效率无疑是低下的。因此一般不会使用这种方式实现除法器。
1、除法器原理
首先观察图1中二进制数10011001除以1010的计算过程,除数与被除数位宽相差4,4位除数(1010)大于被除数的高4位(1001),小于被除数的高5位(10011),所以首先是被除数的高5位减去除数(如图紫色数字),得到01001。然后被除数的第2位到6位数据依旧大于除数,则在次减去除数,就这样依次相减,最后得到商为1111,余数为3。
其实通过上面讲述,就可以根据这个规律实现除法器了,每次从被除数高位取除数相同数据位宽的数据与除数进行比较,大于等于时直接做减法,商加1。小于时被除数数据位宽向低位移动,被除数每右移动1位,商左移1位,直到被除数移动到最低位时结束运算。
以上描述的方法在FPGA上不太好实现,最好是改进一下,将被除数和除数比较的位进行固定,那样就会好实现很多。
如图2所示,还是前文的8位10011001除以4位1010,通过一个9位的寄存器存储8位被除数,始终比较被除数寄存器的第4到8位数据与除数的大小,如果大于除数,则进行减法运算,否则将被除数左移1位,然后在次进行判断。
首先是5’b01001小于除数4’b1010,则将被除数左移1位。然后被除数高五位数据5’b10010大于除数,则商1,两者相减得到01001。得到数据作为被除数运算位,此时被除数高5位还是比除数小,将被除数左移1位,相当于运算被除数的低位数据,同时也要把商左移1位。
就如同上图循环判断被除数高5位数据与除数的大小关系,然后对被除数进行移位和减法运算,就能够计算出商和余数。当被除数的最低位数据被移入高5位时,在通过判断被除数高5位数据与除数大小并进行相应减法后,运算结束。也就是说,这种情况下,被除数位宽为8,除数位宽为4,则被除数左移4次后计算结束,余数等于最后被除数的数值右移4位(被除数总共左移的次数)。
这种运算FPGA实现就较为简单,例如被除数位宽为M,除数位宽为N,满足M>=N,只需要比较被除数[M:M-N]与除数的大小,被除数左移M-N次后运算结束,将最终的被除数右移M-N位得到余数。
上述只适用于一般情况,还会出现图3这种,被除数依旧是8位10000001,4位除数为0010,此时如果还拿被除数高5位减去除数,得到的结果仍然大于除数,会导致计算错误。
此时可以把除数进行左移,直到除数的最高位为1或者除数左移后大于被除数的高5位为止,需要考虑除数左移1位,相当于增大2倍,要保持最终结果不变,被除数左移次数需要增加除数左移的次数,并且最后余数右移的次数也会相应增加。所以上述计算变为图4所示。
首先除数根据条件需要左移2位都不会大于被除数的高5位,且不会溢出,所以除数就先左移2位,然后就是判断被除数高5位是否大于除数,大于除数就进行减法运算且商加1,减法运算结果小于除数取代被除数参与运算的数据位。如果被除数高5位小于除数,则被除数和商一起左移,然后在次判断。最终当被除数左移次数达到除数左移次数加被除数与除数位宽之差时计算结束。
2、除法器实现
经过上述分析,首先当输入数据有效时,需要判断除数和被除数是否为0,其中一个为0,则输出均为0,且除数为0时,应该报错。该模块采用一个状态机作为架构,包括空闲状态,左移除数,除法运算三个状态。假设除数位宽L_DIVR,被除数位宽L_DIVN,且L_DIVN>=L_DIVR。
模块初始处于空闲状态,只有上游模块把开始计算信号拉高且被除数与除数均不为0时,状态机跳转到左移除数状态。此时需要把除数和被除数存入对应寄存器,把除数左移次数计数器、被除数左移次数计数器、商和余数暂存器清零。
开始信号只有在状态机处于空闲状态下才会有效,其余时间不会接收上游模块的除数、被除数等数据。
在左移除数的状态下,如果除数最高位为0,且除数左移1位比被除数寄存器的高L_DIVR+1位小,那么将除数左移1位,除数左移次数计数器加1。否则状态机跳转到除法运算状态。
状态机处于除法运算状态时,需要计算被除数高L_DIVR+1减去除数是否大于0。如果大于等于0,则将减法结果作为被除数的高L_DIVR+1位,商的最低位赋值为1,也就是加1。如果小于0且被除数左移次数小于除数左移次数加L_DIVN-L_DIVR时,把被除数和商均左移1位,被除数左移次数计数器加1。
如果被除数左移次数等于除数左移次数加L_DIVN-L_DIVR,则状态机跳转到空闲状态,将被除数右移除数左移次数加L_DIVN-L_DIVR得到余数。
状态机从除法状态跳转到空闲状态时,把商、余数进行输出。
以下代码是模块端口信号:
//当输入除数为0时,error信号拉高,且商和余数为0;
//当ready信号为低电平时,不能将开始信号start拉高,此时拉高start信号会被忽略。
module div #(
parameter L_DIVN = 8 ,//被除数的位宽;
parameter L_DIVR = 4 //除数的位宽;
)(
input clk ,//时钟信号;
input rst_n ,//复位信号,低电平有效;
input start ,//开始计算信号,高电平有效,必须在ready信号为高电平时输入才有效。
input [L_DIVN - 1 : 0] dividend ,//被除数输入;
input [L_DIVR - 1 : 0] divisor ,//除数输入;
output reg ready ,//高电平表示此模块空闲。
output reg error ,//高电平表示输入除数为0,输入数据错误。
output reg quotient_vld ,//商和余数输出有效指示信号,高电平有效;
output reg [L_DIVR - 1 : 0] remainder ,//余数,余数的大小不会超过除数大小。
output reg [L_DIVN - 1 : 0] quotient //商。
);
以下是状态机状态编码和中间信号的定义,以及通过自动计算位宽函数取计算计数器的位宽。
localparam L_CNT = clogb2(L_DIVN) ;//利用函数自动计算移位次数计数器的位宽。
localparam IDLE = 3'b001 ;//状态机空闲状态的编码;
localparam ADIVR = 3'b010 ;//状态机移动除数状态的编码;
localparam DIV = 3'b100 ;//状态机进行减法计算和移动被除数状态的编码;
reg vld ;//
reg [2 : 0] state_c ;//状态机的现态;
reg [2 : 0] state_n ;//状态机的次态;
reg [L_DIVN : 0] dividend_r ;//保存被除数;
reg [L_DIVR - 1 : 0] divisor_r ;//保存除数。
reg [L_DIVN - 1 : 0] quotient_r ;//保存商。
reg [L_CNT - 1 : 0] shift_dividend ;//用于记录被除数左移的次数。
reg [L_CNT - 1 : 0] shift_divisor ;//用于记录除数左移的次数。
wire [L_DIVR : 0] comparison ;//被除数的高位减去除数。
wire max ;//高电平表示被除数左移次数已经用完,除法运算基本结束,可能还需要进行一次减法运算。
//自动计算计数器位宽函数。
function integer clogb2(input integer depth);begin
if(depth == 0)
clogb2 = 1;
else if(depth != 0)
for(clogb2=0 ; depth>0 ; clogb2=clogb2+1)
depth=depth >> 1;
end
endfunction
max用来判断被除数左移次数是否等于除数于被除数位宽差加除数左移次数。而comparison在除数左移状态时,需要计算被除数高位与除数左移后相减的结果,在其余状态下,计算被除数高位与除数相减的结果。
//max为高电平表示被除数左移的次数等于除数左移次数加上被除数与除数的位宽差;
assign max = (shift_dividend == (L_DIVN - L_DIVR) + shift_divisor);
//用来判断除数和被除数第一次做减法的高位两者的大小,当被除数高位大于等于除数时,comparison最高位为0,反之为1。
//comparison的计算结果还能表示被除数高位与除数减法运算的结果。
//在移动除数时,判断的是除数左移一位后与被除数高位的大小关系,进而判断能不能把除数进行左移。
assign comparison = ((divisor[L_DIVR-1] == 0) && ((state_c == ADIVR))) ?
dividend_r[L_DIVN : L_DIVN - L_DIVR] - {divisor_r[L_DIVR-2 : 0],1'b0} :
dividend_r[L_DIVN : L_DIVN - L_DIVR] - divisor_r;//计算被除数高位减去除数,如果计算结果最高位为0,表示被除数高位大于等于除数,如果等于1表示被除数高位小于除数。
下面是状态机跳转代码。
//状态机次态到现态的转换;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为空闲状态;
state_c <= IDLE;
end
else begin//状态机次态到现态的转换;
state_c <= state_n;
end
end
//状态机的次态变化。
always@(*)begin
case(state_c)
IDLE : begin//如果开始计算信号为高电平且除数和被除数均不等于0。
if(start & (dividend != 0) & (divisor != 0))begin
state_n = ADIVR;
end
else begin//如果开始条件无效或者除数、被除数为0,则继续处于空闲状态。
state_n = state_c;
end
end
ADIVR : begin//如果除数的最高位为高电平或者除数左移一位大于被除数的高位,则跳转到除法运算状态;
if(divisor_r[L_DIVR-1] | comparison[L_DIVR])begin
state_n = DIV;
end
else begin
state_n = state_c;
end
end
DIV : begin
if(max)begin//如果被除数移动次数达到最大值,则状态机回到空闲状态,计算完成。
state_n = IDLE;
end
else begin
state_n = state_c;
end
end
default : begin//状态机跳转到空闲状态;
state_n = IDLE;
end
endcase
end
下面是除数和被除数、商的计算过程,与前文描述一致,不再赘述。
//对被除数进行移位或进行减法运算。
//初始时需要加载除数和被除数,然后需要判断除数和被除数的高位,确定除数是否需要移位。
//然后根据除数和被除数高位的大小,确认被除数是移位还是与除数进行减法运算,注意被除数移动时,为了保证结果不变,商也会左移一位。
//如果被除数高位与除数进行减法运算,则商的最低位变为1,好比此时商1进行的减法运算。经减法结果赋值到被除数对应位。
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
divisor_r <= 0;
dividend_r <= 0;
quotient_r <= 0;
shift_divisor <= 0;
shift_dividend <= 0;
end//状态机处于加载状态时,将除数和被除数加载到对应寄存器,开始计算;
else if(state_c == IDLE && start && (dividend != 0) & (divisor != 0))begin
dividend_r <= dividend;//加载被除数到寄存器;
divisor_r <= divisor;//加载除数到寄存器;
quotient_r <= 0;//将商清零;
shift_dividend <= 0;//将移位的被除数寄存器清零;
shift_divisor <= 0; //将移位的除数寄存器清零;
end//状态机处于除数左移状态,且除数左移后小于等于被除数高位且除数最高位为0。
else if(state_c == ADIVR && (~comparison[L_DIVR]) && (~divisor_r[L_DIVR-1]))begin
divisor_r <= divisor_r << 1;//将除数左移1位;
shift_divisor <= shift_divisor + 1;//除数总共被左移的次数加1;
end
else if(state_c == DIV)begin//该状态需要完成被除数移位和减法运算。
if(comparison[L_DIVR] && (~max))begin//当除数大于被除数高位时,被除数需要移位。
dividend_r <= dividend_r << 1;//将被除数左移1位;
quotient_r <= quotient_r << 1;//同时把商左移1位;
shift_dividend <= shift_dividend + 1;//被除数总共被左移的次数加1;
end
else if(~comparison[L_DIVR])begin//当除数小于等于被除数高位时,被除数高位减去除数作为新的被除数高位。
dividend_r[L_DIVN : L_DIVN - L_DIVR] <= comparison;//减法结果赋值给被除数进行减法运算的相应位。
quotient_r[0] <= 1;//因为做了一次减法,则商加1。
end
end
end
参考代码如下所示:
//注意此模块默认被除数的位宽大于等于除数的位宽。
//当quotient_vld信号为高电平且error为低电平时,输出的数据是除法计算的正确结果。
//当输入除数为0时,error信号拉高,且商和余数为0;
//当ready信号为低电平时,不能将开始信号start拉高,此时拉高start信号会被忽略。
module div #(
parameter L_DIVN = 8 ,//被除数的位宽;
parameter L_DIVR = 4 //除数的位宽;
)(
input clk ,//时钟信号;
input rst_n ,//复位信号,低电平有效;
input start ,//开始计算信号,高电平有效,必须在ready信号为高电平时输入才有效。
input [L_DIVN - 1 : 0] dividend ,//被除数输入;
input [L_DIVR - 1 : 0] divisor ,//除数输入;
output reg ready ,//高电平表示此模块空闲。
output reg error ,//高电平表示输入除数为0,输入数据错误。
output reg quotient_vld ,//商和余数输出有效指示信号,高电平有效;
output reg [L_DIVR - 1 : 0] remainder ,//余数,余数的大小不会超过除数大小。
output reg [L_DIVN - 1 : 0] quotient //商。
);
localparam L_CNT = clogb2(L_DIVN) ;//利用函数自动计算移位次数计数器的位宽。
localparam IDLE = 3'b001 ;//状态机空闲状态的编码;
localparam ADIVR = 3'b010 ;//状态机移动除数状态的编码;
localparam DIV = 3'b100 ;//状态机进行减法计算和移动被除数状态的编码;
reg vld ;//
reg [2 : 0] state_c ;//状态机的现态;
reg [2 : 0] state_n ;//状态机的次态;
reg [L_DIVN : 0] dividend_r ;//保存被除数;
reg [L_DIVR - 1 : 0] divisor_r ;//保存除数。
reg [L_DIVN - 1 : 0] quotient_r ;//保存商。
reg [L_CNT - 1 : 0] shift_dividend ;//用于记录被除数左移的次数。
reg [L_CNT - 1 : 0] shift_divisor ;//用于记录除数左移的次数。
wire [L_DIVR : 0] comparison ;//被除数的高位减去除数。
wire max ;//高电平表示被除数左移次数已经用完,除法运算基本结束,可能还需要进行一次减法运算。
//自动计算计数器位宽函数。
function integer clogb2(input integer depth);begin
if(depth == 0)
clogb2 = 1;
else if(depth != 0)
for(clogb2=0 ; depth>0 ; clogb2=clogb2+1)
depth=depth >> 1;
end
endfunction
//max为高电平表示被除数左移的次数等于除数左移次数加上被除数与除数的位宽差;
assign max = (shift_dividend == (L_DIVN - L_DIVR) + shift_divisor);
//用来判断除数和被除数第一次做减法的高位两者的大小,当被除数高位大于等于除数时,comparison最高位为0,反之为1。
//comparison的计算结果还能表示被除数高位与除数减法运算的结果。
//在移动除数时,判断的是除数左移一位后与被除数高位的大小关系,进而判断能不能把除数进行左移。
assign comparison = ((divisor[L_DIVR-1] == 0) && ((state_c == ADIVR))) ?
dividend_r[L_DIVN : L_DIVN - L_DIVR] - {divisor_r[L_DIVR-2 : 0],1'b0} :
dividend_r[L_DIVN : L_DIVN - L_DIVR] - divisor_r;//计算被除数高位减去除数,如果计算结果最高位为0,表示被除数高位大于等于除数,如果等于1表示被除数高位小于除数。
//状态机次态到现态的转换;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为空闲状态;
state_c <= IDLE;
end
else begin//状态机次态到现态的转换;
state_c <= state_n;
end
end
//状态机的次态变化。
always@(*)begin
case(state_c)
IDLE : begin//如果开始计算信号为高电平且除数和被除数均不等于0。
if(start & (dividend != 0) & (divisor != 0))begin
state_n = ADIVR;
end
else begin//如果开始条件无效或者除数、被除数为0,则继续处于空闲状态。
state_n = state_c;
end
end
ADIVR : begin//如果除数的最高位为高电平或者除数左移一位大于被除数的高位,则跳转到除法运算状态;
if(divisor_r[L_DIVR-1] | comparison[L_DIVR])begin
state_n = DIV;
end
else begin
state_n = state_c;
end
end
DIV : begin
if(max)begin//如果被除数移动次数达到最大值,则状态机回到空闲状态,计算完成。
state_n = IDLE;
end
else begin
state_n = state_c;
end
end
default : begin//状态机跳转到空闲状态;
state_n = IDLE;
end
endcase
end
//对被除数进行移位或进行减法运算。
//初始时需要加载除数和被除数,然后需要判断除数和被除数的高位,确定除数是否需要移位。
//然后根据除数和被除数高位的大小,确认被除数是移位还是与除数进行减法运算,注意被除数移动时,为了保证结果不变,商也会左移一位。
//如果被除数高位与除数进行减法运算,则商的最低位变为1,好比此时商1进行的减法运算。经减法结果赋值到被除数对应位。
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
divisor_r <= 0;
dividend_r <= 0;
quotient_r <= 0;
shift_divisor <= 0;
shift_dividend <= 0;
end//状态机处于加载状态时,将除数和被除数加载到对应寄存器,开始计算;
else if(state_c == IDLE && start && (dividend != 0) & (divisor != 0))begin
dividend_r <= dividend;//加载被除数到寄存器;
divisor_r <= divisor;//加载除数到寄存器;
quotient_r <= 0;//将商清零;
shift_dividend <= 0;//将移位的被除数寄存器清零;
shift_divisor <= 0; //将移位的除数寄存器清零;
end//状态机处于除数左移状态,且除数左移后小于等于被除数高位且除数最高位为0。
else if(state_c == ADIVR && (~comparison[L_DIVR]) && (~divisor_r[L_DIVR-1]))begin
divisor_r <= divisor_r << 1;//将除数左移1位;
shift_divisor <= shift_divisor + 1;//除数总共被左移的次数加1;
end
else if(state_c == DIV)begin//该状态需要完成被除数移位和减法运算。
if(comparison[L_DIVR] && (~max))begin//当除数大于被除数高位时,被除数需要移位。
dividend_r <= dividend_r << 1;//将被除数左移1位;
quotient_r <= quotient_r << 1;//同时把商左移1位;
shift_dividend <= shift_dividend + 1;//被除数总共被左移的次数加1;
end
else if(~comparison[L_DIVR])begin//当除数小于等于被除数高位时,被除数高位减去除数作为新的被除数高位。
dividend_r[L_DIVN : L_DIVN - L_DIVR] <= comparison;//减法结果赋值给被除数进行减法运算的相应位。
quotient_r[0] <= 1;//因为做了一次减法,则商加1。
end
end
end
//生成状态机从计算除结果的状态跳转到空闲状态的指示信号,用于辅助设计输出有效指示信号。
always@(posedge clk)begin
vld <= (state_c == DIV) && (state_n == IDLE);
end
//生成商、余数及有效指示信号;
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
quotient <= 0;
remainder <= 0;
quotient_vld <= 1'b0;
end//如果开始计算时,发现除数或者被除数为0,则商和余数均输出0,且将输出有效信号拉高。
else if(state_c == IDLE && start && ((dividend== 0) || (divisor==0)))begin
quotient <= 0;
remainder <= 0;
quotient_vld <= 1'b1;
end
else if(vld)begin//当计算完成时。
quotient <= quotient_r;//把计算得到的商输出。
quotient_vld <= 1'b1;//把商有效是指信号拉高。
//移动剩余部分以补偿对齐变化,计算得到余数;
remainder <= (dividend_r[L_DIVN - 1 : 0]) >> shift_dividend;
end
else begin
quotient_vld <= 1'b0;
end
end
//当输入除数为0时,将错误指示信号拉高,其余时间均为低电平。
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
error <= 1'b0;
end
else if(state_c == IDLE && start)begin
if(divisor==0)//开始计算时,如果除数为0,把错误指示信号拉高。
error <= 1'b1;
else//开始计算时,如果除数不为0,把错误指示信号拉低。
error <= 1'b0;
end
end
//状态机处于空闲且不处于复位状态;
always@(*)begin
if(start || state_c != IDLE || vld)
ready = 1'b0;
else
ready = 1'b1;
end
endmodule
3、除法器仿真
对应的TestBench文件如下所示:
`timescale 1ns/1ns
module test();
localparam L_DIVN = 8 ;//被除数的位宽;
localparam L_DIVR = 4 ;//除数的位宽;
localparam CYCLE = 20 ;//时钟周期;
reg clk ;//时钟信号;
reg rst_n ;//复位信号,低电平有效;
reg start ;//开始计算信号,高电平有效,
reg [L_DIVN - 1 : 0] dividend ;//被除数输入;
reg [L_DIVR - 1 : 0] divisor ;//除数输入;
wire [L_DIVN - 1 : 0] quotient ;//商。
wire [L_DIVR - 1 : 0] remainder ;//余数,余数的大小不会超过除数大小。
wire quotient_vld ;//商和余数输出有效指示信号,高电平有效;
wire ready ;//高电平表示此模块空闲。
wire error ;//高电平表示输入除数为0,输入数据错误。
reg quotient_error ;
reg rem_error ;
//例化待测试的除法器模块;
div #(
.L_DIVN ( L_DIVN ),//被除数的位宽;
.L_DIVR ( L_DIVR ) //除数的位宽;
)
u_div(
.clk ( clk ),//时钟信号;
.rst_n ( rst_n ),//复位信号,低电平有效;
.start ( start ),//开始计算信号,高电平有效,
.dividend ( dividend ),//被除数输入;
.divisor ( divisor ),//除数输入;
.quotient ( quotient ),//商。
.remainder ( remainder ),//余数,余数的大小不会超过除数大小。
.ready ( ready ),//高电平表示此模块空闲。
.error ( error ),//高电平表示输入除数为0,输入数据错误。
.quotient_vld ( quotient_vld ) //商和余数输出有效指示信号,高电平有效;
);
//生成时钟信号;
initial begin
clk = 0;
forever #(CYCLE/2) clk = ~clk;
end
//生成复位信号;
initial begin
rst_n = 1;
#2;
rst_n = 0;//开始时复位5个时钟;
#(5*CYCLE);
rst_n = 1;
end
initial begin
#1;
dividend = 121; divisor = 13;start=0;
#(8*CYCLE);
repeat(20)begin//循环进行20次运算;
#(5*CYCLE);
dividend = {$random};//产生随机数作为被除数;
divisor = {$random} ;//产生随机数作为除数;
start = 1;
#(CYCLE);
start = 0;
@(negedge quotient_vld);
#1;
#(3*CYCLE);
end
#(5*CYCLE);
$stop;//停止仿真;
end
//对模块输出的数据进行判断。
always@(posedge clk or negedge rst_n)begin
if(rst_n==1'b0)begin//初始值为0;
quotient_error <= 1'b0;
rem_error <= 1'b0;
end
else if(quotient_vld && (divisor!=0))begin
quotient_error <= ((dividend / divisor) != quotient);
rem_error <= ((dividend % divisor) != remainder);
end
end
endmodule
仿真结果如下所示,商和余数的错误指示信号均为低电平,表示除法运算的结果没有什么问题。
模块内部信号的详细仿真如图6、7所示,具体含义与前文介绍一致,就不再赘述了。
4、总结
使用该模块需要注意几点:
1. 被除数位宽必须大于等于除数位宽。
2. 当error为高电平时,表示输入除数为0,此时输出的商和余数均为0且无效。
3. 只有当quotient_vld为高电平时,模块输出的商和余数才是有效的。
4. 只有当模块处于空闲(ready为高电平)时,外部输入的start和除数、被除数才能有效。
如果需要源文件,在公众号后台回复“除法器”(不包括引号)即可。由于我只使用vscode和modelsim对工程进行仿真,所以没有vivado和quartus工程。下面视频就是通过vscode调用modelsim进行仿真的视频。