在FPGA上实现MIPS乘除法指令:手把手教你添加HiLo寄存器与修复Verilog代码
FPGA实战:MIPS乘除法指令实现与HiLo寄存器深度解析
在数字电路与计算机体系结构的学习中,FPGA实现MIPS处理器是一个极具挑战性又充满成就感的项目。本文将聚焦于MIPS指令集中乘除法指令的实现细节,特别是HiLo寄存器的设计与常见Verilog代码陷阱的规避方法。
1. MIPS乘除法指令基础与HiLo寄存器原理
MIPS架构中的乘除法运算与其他算术运算有着本质区别——它们使用专用的HiLo寄存器对来存储运算结果,而非通用寄存器。这种设计源于乘除法运算的特殊性:
- 乘法运算:两个32位操作数相乘产生64位结果,高32位存储在Hi寄存器,低32位存储在Lo寄存器
- 除法运算:32位被除数除以32位除数,商存储在Lo寄存器,余数存储在Hi寄存器
在Verilog实现时,HiLo寄存器本质上是由两个独立的32位寄存器组成,但需要特别注意它们的协同工作特性。以下是HiLo模块的基本结构:
module HiLo ( input wire clk, input wire rst, input wire [31:0] wHiData, input wire [31:0] wLoData, input wire whi, input wire wlo, output reg [31:0] rHiData, output reg [31:0] rLoData ); reg [31:0] hi_reg, lo_reg; always @(posedge clk) begin if (rst) begin hi_reg <= 32'b0; lo_reg <= 32'b0; end else begin if (whi) hi_reg <= wHiData; if (wlo) lo_reg <= wLoData; end end assign rHiData = hi_reg; assign rLoData = lo_reg; endmodule2. 乘除法指令的Verilog实现细节
2.1 指令译码阶段修改
在ID模块中,需要为乘除法指令添加专门的译码逻辑。关键点包括:
- 识别乘除法指令操作码(mult, multu, div, divu)
- 设置正确的寄存器读写信号
- 确保结果写入HiLo而非通用寄存器
always @(*) begin case (opcode) `INST_MULT: begin op = `OP_MULT; regaRead = 1'b1; regbRead = 1'b1; regcWrite = 1'b0; // 结果写入HiLo,不写通用寄存器 // 其他控制信号... end // 其他乘除法指令类似处理 endcase end2.2 执行阶段关键实现
EX模块是乘除法运算的核心,需要处理四种运算情况:
| 指令类型 | 运算方式 | 结果存储 | 注意事项 |
|---|---|---|---|
| mult | 有符号乘法 | Hi:高32位, Lo:低32位 | 使用$signed处理负数 |
| multu | 无符号乘法 | Hi:高32位, Lo:低32位 | 直接使用*运算符 |
| div | 有符号除法 | Hi:余数, Lo:商 | 注意除零处理 |
| divu | 无符号除法 | Hi:余数, Lo:商 | 同样需要除零保护 |
实现代码示例:
always @(*) begin case (op_i) `OP_MULT: begin {wHiData, wLoData} = $signed(regaData) * $signed(regbData); whi = 1'b1; wlo = 1'b1; end `OP_DIV: begin if (regbData != 32'b0) begin // 除零保护 wLoData = $signed(regaData) / $signed(regbData); wHiData = $signed(regaData) % $signed(regbData); whi = 1'b1; wlo = 1'b1; end end // 其他指令处理... endcase end3. 常见问题与调试技巧
3.1 信号未正确清零问题
在调试过程中,最常见的陷阱是控制信号未正确初始化或清零。特别是在复位状态下,必须确保所有写使能信号被禁用:
always @(*) begin if (rst) begin whi = 1'b0; wlo = 1'b0; wHiData = 32'b0; wLoData = 32'b0; end else begin // 正常操作逻辑 end end典型症状:仿真中观察到HiLo寄存器被意外写入,导致后续运算结果错误。
解决方案:
- 检查所有控制信号的复位状态
- 确保在非乘除法指令执行时写使能信号为低
- 添加仿真断言检查信号状态
3.2 时序问题与流水线冲突
当乘除法指令与其他指令混合执行时,可能产生以下问题:
- 数据冒险:后续指令需要读取HiLo寄存器中的结果
- 结构冒险:多条乘除法指令同时尝试访问HiLo寄存器
建议解决方案:
- 插入适当的流水线停顿(stall)
- 实现结果转发(forwarding)机制
- 添加明确的互锁逻辑
4. 功能验证与测试案例设计
完善的测试方案是确保设计正确的关键。建议构建以下测试案例:
基本功能测试:
- 正数×正数(如12×32)
- 负数×正数(如-1×32)
- 边界值测试(如最大32位整数相乘)
除法特殊情况:
- 除零测试(应保持寄存器不变)
- 有符号除法符号处理
- 余数正确性验证
指令混合测试:
- 乘除法指令与算术逻辑指令交替执行
- 连续多条乘除法指令测试
示例测试代码:
initial begin // 测试无符号乘法 12×32=384 (0x180) instmem[0] = 32'h34011100; // ori $1, $0, 0x1100 instmem[1] = 32'h34020020; // ori $2, $0, 0x0020 instmem[2] = 32'b000000_00001_00010_00000_00000_011001; // multu $1, $2 // 测试有符号乘法 -1×32=-32 (0xFFFFFFE0) instmem[3] = 32'h3407FFFF; // ori $7, $0, 0xFFFF instmem[4] = 32'b000000_00000_00111_00111_10000_000000; // sll $7, $7, 16 instmem[5] = 32'b000000_00111_00010_00000_00000_011000; // mult $7, $2 end5. 性能优化与高级实现技巧
对于追求更高性能的实现,可以考虑以下优化方向:
- 布斯算法加速乘法:将乘法分解为移位和加法操作
- 流水线化除法器:减少除法运算的时钟周期数
- 早期终止机制:根据操作数值提前结束运算
布斯算法实现示例:
// 简化的布斯乘法实现 reg [63:0] product; reg [31:0] multiplicand; integer i; always @(posedge clk) begin if (start) begin product = {32'b0, regbData}; multiplicand = regaData; for (i = 0; i < 32; i = i+1) begin case (product[1:0]) 2'b01: product[63:32] = product[63:32] + multiplicand; 2'b10: product[63:32] = product[63:32] - multiplicand; default: ; endcase product = product >>> 1; end {wHiData, wLoData} = product; end end6. 实际项目中的经验分享
在真实的FPGA模型机项目中,乘除法指令的实现往往会遇到一些教科书上未提及的挑战:
复位信号处理:确保HiLo寄存器在系统复位时被正确清零,但不会被后续的非相关指令意外修改
仿真与综合差异:某些仿真器对乘除法的处理与实际硬件可能不同,特别是涉及有符号数运算时
时序收敛问题:乘法器可能成为关键路径,必要时可考虑多周期实现或使用DSP块
验证完备性:除了常规测试案例,还应考虑:
- 操作数包含0的情况
- 结果为0的情况
- 操作数互为补数的情况
- 最大/最小边界值组合
一个实用的调试技巧是在仿真中添加HiLo寄存器的监控逻辑:
always @(posedge clk) begin if (whi || wlo) begin $display("[%t] HiLo Write: whi=%b, wlo=%b, HiData=%h, LoData=%h", $time, whi, wlo, wHiData, wLoData); end end对于希望进一步扩展功能的开发者,可以考虑实现MIPS架构中的mfhi/mflo/mthi/mtlo等指令,这些指令允许在通用寄存器和HiLo寄存器之间传输数据,为更复杂的数学运算提供支持。
