1. 二进制除法的手算原理与硬件实现困境
小时候学除法时老师总让我们列竖式,这种"试商-减法-移位"的操作在二进制世界里反而变得更简单——因为每一位只有0或1两种可能。但当你试图用FPGA实现除法器时,会发现这个看似简单的运算藏着不少坑。
先看个8位被除数10011001(153)除以4位除数1010(10)的例子。传统手算步骤是:
- 取被除数高5位10011(19)与除数比较
- 19>10,商1,做减法得01001(9)
- 引入下一位0,得010010(18)
- 18>10,商1,减法得01000(8)
- 继续处理直到所有位完成
这种算法在硬件实现时会遇到两个致命问题:首先是动态位宽比较电路复杂,每次需要根据剩余位数调整比较范围;其次是状态控制困难,特别是当出现连续商0时需要特殊处理。实测发现,直接照搬手算方法的电路需要约37个LUT实现8位除法,时序延迟高达15ns。
2. 移位-比较-减法算法的精妙之处
针对上述问题,工程师们发明了移位-比较-减法算法。其核心思想可以用三句话概括:
- 固定比较位宽:始终比较被除数[最高位:最高位-除数位宽]区间
- 减法替代试商:直接做减法,若结果非负则商1
- 移位补偿:每次比较后左移被除数,相当于"引入下一位"
还是刚才的例子,改进后的流程:
- 初始化:被除数寄存器=0_10011001(前面补0)
- 比较1010与01001(9<10),左移得0_100110010
- 比较1010与10010(18>10),商1,减法得0_010010010
- 左移得0_100100100,比较1001(9<10)...
- 最终得到商1111(15),余数0011(3)
这个算法的Verilog实现有个巧妙设计:用N+1位寄存器存储N位被除数,最高位专门用来检测减法溢出。当减法结果为负时,最高位会自动置1,省去了额外的比较电路。
3. 三状态机的硬件架构设计
实际工程中我常用三个状态来控制除法流程:
3.1 空闲(IDLE)状态
这是模块的初始状态,负责接收启动信号和输入校验。特别注意两个边界条件处理:
if (divisor == 0) begin error <= 1'b1; // 除零错误 quotient <= 0; remainder <= 0; end else if (dividend == 0) begin // 被除数为零直接输出 quotient <= 0; remainder <= 0; quotient_vld <= 1'b1; end3.2 除数左移(ADIVR)状态
这个状态很多人会忽略,但它对性能提升至关重要。其核心逻辑是:
if ((divisor_r[MSB] == 0) && (dividend_r[UPPER_BITS] >= {divisor_r[L_DIVR-2:0],1'b0})) begin divisor_r <= divisor_r << 1; // 除数左移 shift_divisor <= shift_divisor + 1; end通过将除数左移到最高有效位为1,可以大幅减少后续减法次数。比如除数0010(2)左移两位变成1000(8),后续只需要做两次减法就能完成原本需要四次的操作。
3.3 除法运算(DIV)状态
这是最核心的状态,实现了前文描述的移位-比较-减法流程。关键代码段:
always @(posedge clk) begin if (comparison[MSB] && !max) begin // 需要移位 dividend_r <= dividend_r << 1; quotient_r <= quotient_r << 1; shift_dividend <= shift_dividend + 1; end else if (!comparison[MSB]) begin // 可做减法 dividend_r[UPPER_BITS] <= comparison; quotient_r[0] <= 1'b1; // 商位置1 end end这里有个设计细节:商寄存器采用"左移+最低位置1"的方式,比传统的加法器节省约20%的逻辑资源。
4. 性能优化实战技巧
经过多次流片验证,我总结出几个关键优化点:
4.1 流水线设计
将状态机拆分为两级流水:
- 第一拍:完成比较/减法计算
- 第二拍:更新寄存器状态 实测表明这种结构在Artix-7上能跑到250MHz,比单周期实现提升近3倍。
4.2 位宽自适应
使用SystemVerilog的参数化设计:
module div #( parameter L_DIVN = 8, parameter L_DIVR = 4 )( // 端口声明 );配合自动位宽计算函数:
function integer clogb2(input integer depth); for(clogb2=0; depth>0; clogb2=clogb2+1) depth = depth >> 1; endfunction这样同一套代码可以适配从4位到64位的各种位宽需求。
4.3 时序收敛技巧
在布局约束中加入:
set_property CLOCK_DEDICATED_ROUTE BACKBONE [get_nets clk] set_property ASYNC_REG TRUE [get_cells sync_reg*]能有效改善建立时间违规问题。某次项目中,这组约束让时序裕量从-0.3ns提升到0.8ns。
5. 仿真验证与调试
编写测试平台时有几个注意点:
initial begin // 随机测试用例生成 repeat(100) begin dividend = $urandom_range(0, 2**L_DIVN-1); divisor = $urandom_range(1, 2**L_DIVR-1); // 避免除零 start = 1; @(negedge clk); start = 0; wait(quotient_vld); #10; end end特别要检查边界条件:
- 被除数为0
- 除数为1
- 被除数等于除数
- 商超过半位宽的情况
建议在Vivado中设置交叉触发条件,当quotient_error或rem_error触发时自动暂停仿真,方便定位问题。