资讯动态

FPGA实现Booth乘法器:从Modelsim仿真到上板验证的完整工程实践

发布时间:2026/8/21 3:44:33 来源:尧图企业网站定制
你有没有遇到过这样的情况一个算法在软件仿真里跑得飞快时序报告也一片绿色但一旦烧录到FPGA板子上要么结果不对要么干脆不工作问题可能就出在仿真和真实硬件之间那道看不见的“墙”上。很多人把Booth算法在Modelsim里跑通波形就以为大功告成但真正的考验是从“仿真正确”到“上板可用”这段路。今天要聊的就是如何用FPGA这里以嘉豪开发板为例完整地实现Booth乘法器并跨越从Modelsim仿真到实际上板验证的鸿沟。这不仅仅是写几行Verilog代码、点一下“Run Simulation”那么简单。关键在于你需要建立一套从算法理解、RTL设计、仿真验证到物理约束、时序收敛、板级调试的完整工作流。很多人卡在最后一步就是因为前面的环节只做了“形式正确”而忽略了硬件实现的“物理现实”。1. 理解Booth算法为什么它天生适合FPGA在动手写代码之前我们得先搞清楚为什么在FPGA上做乘法Booth算法是一个经典选择。这关乎选型的底层逻辑。1.1 从“硬算”到“巧算”Booth的核心优势最直观的乘法就是移位相加。对于一个N位的乘数你可能需要做N次加法。Booth算法的聪明之处在于它通过观察乘数中连续的‘1’或‘0’将多次加法合并为一次加减操作。具体来说它编码乘数根据相邻两位的值当前位和低位来决定当前的操作是加被乘数、减被乘数还是什么也不做。这带来的直接好处是减少操作次数对于有连续‘1’或连续‘0’的乘数效率提升明显。平均下来它能将加法次数减少到原来的一半左右。统一处理有符号数Booth算法特别是Radix-2 Booth能天然地处理二进制补码形式的有符号数无需像无符号乘法那样进行额外的符号位扩展和校正。这对于FPGA处理实际数据流如图像像素、音频采样值至关重要。1.2 FPGA视角下的契合点FPGA由大量的查找表LUT、寄存器FF和专用进位链组成。Booth算法的实现结构——一个状态机控制下的移位、加减运算——能很好地映射到这些资源上移位操作对应FPGA中的布线资源成本极低。加减法可以利用FPGA内部的专用加法器DSP Slice或由LUT构成的加法器链高效实现。控制逻辑简单的状态机或计数器消耗少量逻辑资源。因此Booth算法在FPGA上实现往往能在面积资源消耗和速度时钟频率之间取得不错的平衡。它不是最快的比如比不上完全流水线化的阵列乘法器但通常是兼顾性能和资源效率的务实选择。2. 从零构建Booth乘法器的Verilog实现与仿真理论清晰后我们进入实践。这一阶段的目标是得到一个在行为级仿真中功能正确的设计。2.1 设计一个可配置的Booth乘法器模块一个健壮的Booth乘法器模块应该考虑可重用性。我们设计一个参数化模块。module booth_multiplier #( parameter WIDTH 8 // 定义操作数位宽 )( input wire clk, input wire rst_n, input wire start, // 启动信号 input wire signed [WIDTH-1:0] multiplicand, // 被乘数有符号 input wire signed [WIDTH-1:0] multiplier, // 乘数有符号 output reg signed [2*WIDTH-1:0] product, // 乘积2倍位宽 output reg done // 计算完成标志 ); // 状态定义 localparam IDLE 2b00; localparam CALC 2b01; localparam DONE 2b10; reg [1:0] state, next_state; reg [WIDTH:0] multiplier_reg; // 多一位用于Booth编码低位补0 reg signed [2*WIDTH:0] product_reg; // 中间乘积寄存器 reg [$clog2(WIDTH1)-1:0] count; // 计数器 // 状态机同步逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; end else begin state next_state; end end // 状态转移与数据处理逻辑组合逻辑部分 always (*) begin next_state state; done 1b0; case (state) IDLE: begin if (start) begin next_state CALC; // 初始化乘数低位补0乘积寄存器高位置被乘数低位置乘数 multiplier_reg {multiplier, 1b0}; product_reg {{WIDTH{multiplicand[WIDTH-1]}}, multiplicand, {WIDTH{1b0}}}; // 符号扩展 count WIDTH; end end CALC: begin if (count 0) begin next_state DONE; end else begin // Booth编码逻辑 (根据multiplier_reg[1:0]判断) case (multiplier_reg[1:0]) 2b01: product_reg product_reg (multiplicand WIDTH); // 加被乘数 2b10: product_reg product_reg - (multiplicand WIDTH); // 减被乘数 default: ; // 2b00, 2b11: 无操作 endcase // 算术右移一位 product_reg product_reg 1; multiplier_reg multiplier_reg 1; count count - 1; end end DONE: begin done 1b1; next_state IDLE; end endcase end // 输出赋值 always (posedge clk) begin if (state DONE) begin product product_reg[2*WIDTH-1:0]; // 输出最终乘积 end end endmodule关键点解析参数化 (parameter)方便后续复用如轻松改为16位乘法。有符号数处理使用signed关键字声明让综合工具和仿真器理解这是补码运算。注意初始化时的符号扩展 ({{WIDTH{multiplicand[WIDTH-1]}}})。Booth编码核心在CALC状态的case语句依据乘数寄存器最低两位决定操作。算术右移 ()这是处理有符号数移位的关键高位补符号位而非逻辑右移()的补0。控制流使用状态机 (IDLE,CALC,DONE) 清晰管理计算流程done信号便于上级模块同步。2.2 编写完备的Testbench进行Modelsim仿真仿真不是随便给几个数看看。一个有效的Testbench应该覆盖边界情况、典型情况和错误情况。timescale 1ns/1ps module tb_booth_multiplier(); parameter WIDTH 8; reg clk, rst_n, start; reg signed [WIDTH-1:0] A, B; wire signed [2*WIDTH-1:0] P; wire done; booth_multiplier #(.WIDTH(WIDTH)) uut ( .clk(clk), .rst_n(rst_n), .start(start), .multiplicand(A), .multiplier(B), .product(P), .done(done) ); // 时钟生成 initial begin clk 0; forever #10 clk ~clk; // 假设100MHz时钟周期20ns end // 测试激励 initial begin // 初始化 rst_n 0; start 0; A 0; B 0; #100; rst_n 1; #20; // 测试用例1: 正数相乘 $display([%t] Test 1: Positive * Positive, $time); A 8d10; // 10 B 8d20; // 20 start 1; #20; start 0; wait(done); #10; if (P 200) $display( PASS: 10 * 20 %d, P); else $display( FAIL: Got %d, Expected 200, P); #50; // 测试用例2: 负数相乘 $display([%t] Test 2: Negative * Negative, $time); A -8sd5; // -5 B -8sd4; // -4 start 1; #20; start 0; wait(done); #10; if (P 20) $display( PASS: (-5) * (-4) %d, P); else $display( FAIL: Got %d, Expected 20, P); #50; // 测试用例3: 正负相乘 $display([%t] Test 3: Positive * Negative, $time); A 8d7; B -8sd3; start 1; #20; start 0; wait(done); #10; if (P -21) $display( PASS: 7 * (-3) %d, P); else $display( FAIL: Got %d, Expected -21, P); #50; // 测试用例4: 边界值 (最大值、最小值) $display([%t] Test 4: Boundary Values, $time); A 8sb01111111; // 127 B 8sb01111111; // 127 start 1; #20; start 0; wait(done); #10; if (P 16129) $display( PASS: 127 * 127 %d, P); else $display( FAIL: Got %d, Expected 16129, P); #50; A 8sb10000000; // -128 B 8sb10000000; // -128 start 1; #20; start 0; wait(done); #10; // -128 * -128 16384 注意16位有符号数能表示的范围 if (P 16384) $display( PASS: (-128) * (-128) %d, P); else $display( FAIL: Got %d, Expected 16384, P); #50; // 测试用例5: 包含0的乘法 $display([%t] Test 5: Multiply with Zero, $time); A 8d55; B 8d0; start 1; #20; start 0; wait(done); #10; if (P 0) $display( PASS: 55 * 0 %d, P); else $display( FAIL: Got %d, Expected 0, P); #50; $display([%t] All tests completed., $time); $stop; end endmodule仿真要点时钟与复位必须模拟真实的时钟边沿和复位信号。wait(done)使用wait语句或(posedge done)来同步等待计算完成这是验证时序逻辑的关键。边界测试必须测试正最大数127、负最小数-128以及它们之间的运算这是有符号数运算的“雷区”。自动检查使用if-else和$display在仿真中自动判断结果对错提高验证效率。在Modelsim中运行此Testbench观察波形。你需要确认在start信号有效后的下一个时钟上升沿状态机是否进入CALC。在CALC状态count是否逐周期递减product_reg是否按Booth算法规则正确移位和加减。当count为0时状态机是否跳转到DONE并产生一个时钟周期的done脉冲。最终输出product的值是否正确。如果波形一切正常恭喜你行为级仿真通过了。但这只意味着你的“逻辑”是正确的。距离在真实的嘉豪FPGA板上运行还差最关键的一步物理实现。3. 跨越鸿沟从仿真到上板的关键步骤这是最常出问题的环节。仿真环境是理想的“数字世界”而FPGA板是一个有延迟、有噪声、有具体资源的“物理世界”。你需要做一系列转换和约束。3.1 综合、实现与时序约束在Vivado、Quartus等开发工具中综合 (Synthesis)将你的Verilog代码翻译成由LUT、FF、DSP等基本单元组成的网表。检查综合报告看是否有警告如锁存器推断或错误。实现 (Implementation)包括布局布线Place Route将网表中的逻辑单元映射到FPGA芯片的具体位置并用真实的连线连接起来。这一步会引入真实的走线延迟。时序约束 (Timing Constraints)这是连接仿真世界和物理世界的桥梁。你必须告诉工具你的时钟频率是多少。创建一个.xdc(Vivado) 或.sdc(Quartus) 文件。最基本的约束是创建时钟create_clock -period 20.000 -name clk [get_ports clk]假设目标100MHz周期10ns这里设20ns更保守。如果你使用了生成的时钟或跨时钟域还需要更复杂的约束。关键动作查看实现后的时序报告Timing Report。重点看“WNS (Worst Negative Slack)”和“WHS (Worst Hold Slack)”。如果WNS为负说明有路径不满足建立时间要求你的设计无法在你约束的时钟频率下稳定工作。这时你需要降低时钟频率约束。优化代码如插入流水线寄存器、重新设计关键路径。让工具尝试更激进的布局布线策略。3.2 管脚分配与物理约束你的clk,rst_n,start以及输入输出数据信号必须映射到嘉豪开发板具体的物理引脚上。查阅原理图找到嘉豪开发板的原理图或用户手册确定时钟引脚通常接外部晶振、按键用于复位和启动、LED或数码管用于显示结果对应的FPGA引脚号。编写管脚约束在约束文件中添加。例如set_property PACKAGE_PIN “E3” [get_ports clk] # 假设E3是50MHz时钟输入脚 set_property IOSTANDARD LVCMOS33 [get_ports clk] # 设置IO电平标准 set_property PACKAGE_PIN “C4” [get_ports rst_n] set_property IOSTANDARD LVCMOS33 [get_ports rst_n] ... # 分配其他端口电平标准务必正确如LVCMOS333.3V。错误会导致信号无法识别或损坏器件。3.3 生成并下载比特流当时序收敛WNS0且管脚分配无误后工具会生成一个.bit文件。通过JTAG或SPI接口将这个文件下载编程到嘉豪FPGA的配置存储器中。此时你的设计就“烧录”到了硬件里。4. 板级调试当结果不如预期时怎么办上板后如果LED显示乱码或结果不对不要慌。按照从易到难的顺序排查4.1 基础信号排查时钟和复位这是心脏和起点。用示波器或逻辑分析仪测量clk和rst_n引脚确认时钟频率正确、复位信号在上电后确实释放从低变高。很多问题源于复位信号异常。输入信号手动触发start信号比如通过按键用仪器测量是否产生了干净、无毛刺的上升沿。同时检查输入数据A和B的引脚电平是否与你预设的值一致。输出信号观察done信号和product输出。done是否在预期的时间点产生了脉冲product的输出电平是否稳定4.2 内部信号探针调试如果支持像Vivado的ILA (Integrated Logic Analyzer) 或Quartus的SignalTap II是FPGA调试的“神器”。它们允许你在FPGA运行时像在Modelsim里一样观察内部信号如状态机state、计数器count、中间结果product_reg的波形。用法在代码中标记想要观察的信号重新综合、实现、生成带调试核的比特流下载后设置触发条件如start上升沿然后捕获波形。价值这能直接对比“板级实际波形”和“Modelsim仿真波形”。如果两者在start脉冲后的行为不一致那问题很可能出在时序如亚稳态、建立保持时间违例或约束上。4.3 常见问题与解决思路问题现象可能原因排查方向完全无反应输出恒定1. 时钟未工作2. 复位信号未释放3. 管脚分配错误/电平标准错误测量时钟和复位引脚。核对约束文件。结果间歇性错误1. 时序违例关键路径延迟过大2. 输入信号有毛刺3. 跨时钟域问题如果有时1. 查看时序报告重点看WNS。2. 用示波器看输入信号质量。3. 检查是否有时钟域交叉未做同步处理。结果系统性偏差如总是差某个值1. 有符号数处理错误如移位方式2. 初始化值不对3. 位宽溢出1. 回顾Booth算法算术右移()的实现。2. 检查IDLE状态下的寄存器初始化。3. 确认乘积寄存器位宽足够2*WIDTH。Modelsim对上板错1. 未加时序约束或约束错误2. 综合/实现优化改变了行为如移除中间寄存器3. 代码中存在不可综合或仿真与综合语义不一致的语句1. 确保有时钟约束并已满足。2. 使用(* keep “true” *)等属性防止优化关键信号。3. 检查代码是否使用了纯仿真语句如#delay。4.4 一个务实的调试流程框架静态复查在下载前三重检查约束文件时钟、管脚、电平。最小系统验证先做一个最简单的测试比如让FPGA上电后点亮一个LED确认下载链路和基础外设正常。分模块验证如果你的设计包含Booth乘法器、控制单元、显示驱动等先单独验证乘法器核心。可以写一个简单的顶层模块用拨码开关设置固定的A和B用LED显示乘积的低几位看是否正确。引入调试核对于复杂问题尽早使用ILA/SignalTap。它提供的信息量远超LED。回归仿真如果硬件行为诡异带着硬件观察到的现象比如哪个信号在哪个时刻异常回到Modelsim尝试复现和定位。有时需要在Testbench中加入更接近实际电路的延迟模型。从仿真到上板本质是将一个理想化的逻辑模型经过综合、布局布线变成一个受物理规律制约的硬件电路。Booth算法的实现是清晰的但确保它在特定时钟频率下在嘉豪FPGA的具体资源布局和连线延迟中稳定工作需要的是对数字设计后端流程的尊重和严谨的工程方法。成功的上板验证不是你运气好而是你通过约束、调试和排查亲手消除了所有不确定性之后的水到渠成。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价