连云港市网站建设_网站建设公司_C#_seo优化
2026/1/7 10:57:52 网站建设 项目流程

从零构建MIPS乘法器:手把手教你实现ALU中的定点乘法

你有没有想过,一条简单的mult $t0, $t1指令背后,CPU到底做了什么?
在现代处理器中,乘法早已被硬件加速到一个时钟周期内完成。但如果你正在用Verilog写一个教学级MIPS CPU,或者想深入理解RISC-V的M扩展模块,那么——这个乘法功能,得你自己做出来。

本文不讲空话,不堆术语,带你从数学原理出发,一步步搭建出可综合、可集成的32位定点乘法器,并完整融入MIPS五级流水线架构。无论你是做课程设计、FPGA原型验证,还是为自研RISC-V软核打基础,这篇都能让你真正“看得见、摸得着”底层运算的本质。


为什么不能让主ALU直接算乘法?

我们先来打破一个常见误解:很多人以为ALU应该像计算器一样,支持加减乘除全功能。但实际上,在标准MIPS和RISC-V架构中,乘法并不在主ALU路径中执行

为什么?

因为乘法太慢了

主ALU处理的是单周期操作,比如addsuband,它们的关键路径就是“寄存器→多路选择→加法器→结果回写”,延迟通常控制在几纳秒内。而32位整数乘法如果用纯组合逻辑实现,会涉及32个部分积生成+压缩树(如Wallace树),面积大、延迟高,轻松突破几十纳秒——这会直接拖垮整个流水线的时序。

所以现实做法是:

把乘法做成独立的协处理单元,多周期运行,主流水线暂停等待。

这也正是MIPS中HILO寄存器存在的意义:它们不是通用寄存器,而是专门为长耗时运算(乘、除)准备的结果暂存区。


定点乘法的本质:移位 + 条件相加

别被“乘法”两个字吓到。二进制下的乘法其实非常直观:

A × B = A × (b₀×2⁰ + b₁×2¹ + ... + b₃₁×2³¹) = Σ (if bᵢ == 1, then A << i)

换句话说:逐位检查乘数B的每一位,如果是1,就把被乘数A左移i位后加到累加器里。

举个例子:

1011 (11) × 1101 (13) ------- 1011 ← 1011 × 1 × 2⁰ 0000 ← 1011 × 0 × 2¹ 1011 ← 1011 × 1 × 2² 1011 ← 1011 × 1 × 2³ ------- 10001111 (143)

每一行都是“条件移位”,最后全部加起来。这就是所谓的“部分积累加法”。

但问题是:你要么一次性生成所有部分积(组合逻辑阵列乘法器),代价是面积爆炸;要么一次只处理一位,循环32次——这就是我们要实现的迭代式移位相加乘法器


构建你的第一台32位乘法机

我们现在要做的,是一个典型的基于状态机的时序乘法器。它不需要DSP Slice,完全由LUT和寄存器构成,适合在Artix-7这类中低端FPGA上部署。

核心结构设计

我们需要以下几个关键组件:

模块功能
multiplicand被乘数(A)
multiplier_in乘数(B)
A_reg累加器,保存当前和
Q_reg存放乘数,每次右移一位
B_shifted被乘数不断左移(A<<i)
count循环计数器(0~31)
state控制状态机(空闲/运行/完成)

工作流程如下:

  1. 初始化:A=0,B_shifted = A << 0,Q = multiplier
  2. 循环32次:
    - 如果 Q[0] == 1 → A += B_shifted[63:32] (高32位)
    - Q >> 1,B_shifted << 1
    - count++
  3. 结束:输出{A, Q}作为64位结果

注意:这里我们只保留A的低32位,高位通过最终拼接恢复。这是节省资源的小技巧。


Verilog实现:带状态机的可综合代码

module multiplier_32bit ( input clk, input rst_n, input start, input [31:0] multiplicand, input [31:0] multiplier_in, output reg [63:0] product, output reg done ); reg [31:0] A; // 累加器(低32位) reg [63:0] B_shifted; // 被乘数左移版本(64位) reg [31:0] Q; // 乘数寄存器 reg [5:0] count; // 计数器(最多32) reg busy; localparam S_IDLE = 2'b00; localparam S_RUN = 2'b01; localparam S_DONE = 2'b10; reg [1:0] state; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= S_IDLE; A <= 0; B_shifted <= 0; Q <= 0; count <= 0; product <= 0; done <= 0; busy <= 0; end else begin case (state) S_IDLE: begin done <= 0; if (start && !busy) begin A <= 0; B_shifted <= {multiplicand, 32'b0}; // 初始左移0位 Q <= multiplier_in; count <= 0; busy <= 1; state <= S_RUN; end end S_RUN: begin if (Q[0]) begin A <= A + B_shifted[63:32]; // 加入高32位 end Q <= Q >> 1; // 乘数右移 B_shifted <= B_shifted << 1; // 被乘数左移 count <= count + 1; if (count == 31) begin state <= S_DONE; end end S_DONE: begin product <= {A, Q}; // 拼接得到64位结果 done <= 1; busy <= 0; state <= S_IDLE; end endcase end end endmodule
关键点解析:
  • 为何用64位中间变量?
    因为每次左移都要保持完整数据宽度,防止溢出。

  • 结果怎么拼出来的?
    最终A是低32位累加值,Q已经变成全0(被右移干净了),但它的原始低位参与了最后一次加法判断。最终{A, Q}实际上等于完整的64位乘积。

  • done信号的作用?
    这是你和主控单元通信的“握手信号”。只有当done == 1,才能允许后续mfhi/mflo指令读取结果。


如何把它塞进MIPS流水线?

光有乘法器还不够,你还得让它和CPU其他部件协同工作。

控制信号扩展

在原有的MIPS控制器基础上,增加以下信号:

信号名方向说明
ex_mult_start输出EX阶段触发乘法开始
mult_done输入反馈乘法是否完成
wb_write_hi输出写使能HI寄存器
wb_write_lo输出写使能LO寄存器
stall_pipe输出插入气泡,暂停流水线

控制逻辑修改示例

// 在控制单元中添加对 mult/multu 的识别 always @(*) begin alu_op = `ALU_ADD; // 默认操作 mem_read = 0; mem_write = 0; wb_reg_write = 0; ex_mult_start = 0; stall_pipe = 0; case (opcode) `OP_MULT, `OP_MULTU: begin alu_op = `ALU_NOP; // 主ALU不动作 ex_mult_start = 1; // 启动乘法器 wb_reg_write = 0; // 不写通用寄存器 stall_pipe = !mult_done; // 直到完成前一直停顿 end `OP_MFHI: begin // 从HI寄存器取数据写入rd src_a_sel = `SRC_REG; src_b_sel = `SRC_IMM; alu_op = `ALU_MOV; wb_reg_write = 1; end // ... 其他指令 endcase end

流水线行为模拟

mult $t0, $t1mflo $s0为例:

周期IFIDEXMEMWB
T1multmult
T2addiaddimult (start)
T3lwlwmult (run)mult (wait)
T34beqbeqmflomult (done)write $s0

可以看到,从EX阶段开始,连续32个周期都在等乘法结束,期间插入多个“气泡”阻止新指令推进。


实际部署中的坑与秘籍

你以为写完代码就完了?No。真实世界的问题才刚开始。

⚠️ 坑点1:计数器边界错误

很多初学者写成count == 32才跳转S_DONE,但别忘了:
- 第一次循环是第0位(LSB)
- 最后一次是第31位
- 所以只需要32次循环(0~31)

如果你用了==32,就会多跑一圈,导致结果错乱。

✅ 正确做法:if (count == 31)就进入S_DONE。


⚠️ 坑点2:符号扩展问题

上面的代码只适用于无符号乘法(对应multu)。如果你想支持有符号数(mult),必须先处理补码。

常见做法:
1. 提前取出两个操作数的符号位(最高位)
2. 分别取绝对值(负数则按位取反+1)
3. 使用上述无符号乘法器计算
4. 根据符号位异或决定是否对结果求补

也可以改用Booth编码算法一步到位,但复杂度更高,适合进阶优化。


✅ 秘籍1:资源共享优化FF数量

注意到QA都是32位寄存器吗?其实你可以复用移位逻辑!

观察发现:
-Q每次右移
-B_shifted每次左移
- 二者合起来正好是64位宽

有些设计会合并为一个64位寄存器,利用不同切片实现双向移位,节省约10%寄存器资源。


✅ 秘籍2:提前终止优化性能

如果乘数低位有很多0,其实可以跳过无效循环。

加入判断:

if (Q == 0) begin state <= S_DONE; end

虽然增加了比较逻辑,但在某些场景下能显著减少平均周期数。


✅ 秘籍3:异步通知避免死锁

强烈建议将done信号通过脉冲形式发送给控制单元,并清零自身状态。否则一旦错过时钟边沿,可能造成永久阻塞。


适用场景与未来升级方向

这套方案特别适合:

  • 🎓 大学计算机组成实验(如华科、清华的CPU课设)
  • 🔬 RISC-V M扩展原型验证(RV32IM)
  • 💡 教学FPGA平台(Nexys A7、DE10-Lite)
  • 🔐 抗侧信道攻击定制乘法器(可控时序)

下一步你可以尝试:

  1. Booth编码乘法器:将平均迭代次数从32降到16
  2. 流水线化设计:每个周期处理多位,提升吞吐率
  3. 双周期乘法器:用少量DSP slice加速
  4. 饱和模式支持:用于音频/DSP处理
  5. 与RISC-V兼容封装:定义CSR接口替代HI/LO

写在最后:掌握底层,才有真正的自由

当你亲手写出第一个能在FPGA上跑通的乘法器,你会突然明白:

原来那些看似神秘的指令,不过是移位、加法和状态切换的组合。

这种“原来如此”的顿悟感,是调用IP核永远无法带来的。

更重要的是,一旦你理解了乘法是怎么一步步算出来的,你就有了重新定义它的能力——你可以为AI推理定制低精度乘法,为加密算法设计抗功耗分析的恒定时间乘法,甚至为量子经典混合计算构建专用协处理器。

而这,正是开源硬件的魅力所在。

如果你也在构建自己的MIPS或RISC-V核心,欢迎在评论区分享你的乘法器设计方案。我们一起把这块“硬骨头”啃透。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询