从流水灯到中断处理:手把手教你用Verilog在FPGA上玩转MIPS模型机

张开发
2026/5/3 0:10:16 15 分钟阅读

分享文章

从流水灯到中断处理:手把手教你用Verilog在FPGA上玩转MIPS模型机
从流水灯到中断处理手把手教你用Verilog在FPGA上玩转MIPS模型机1. 引言当MIPS模型机遇上FPGA在数字逻辑与计算机组成原理的学习中MIPS架构因其简洁规整的指令集而成为教学的首选。但纸上得来终觉浅当我们将这个经典的32位RISC处理器实现在FPGA开发板上时才能真正理解CPU各模块如何协同工作。想象一下你刚完成了一个单周期MIPS模型机的Verilog实现所有模块都通过了基础测试。但接下来呢如何验证这个五脏俱全的小CPU真的能处理复杂任务本文将带你从最简单的流水灯开始逐步实现开关控制、定时器中断等高级功能构建一个完整的验证体系。2. 基础篇让LED流动起来2.1 内存映射I/O设计在FPGA上我们需要通过内存映射的方式访问外设。以下是一个典型的内存地址分配方案地址范围设备类型功能描述0x0000_0000RAM数据存储器0x7000_0010GPIO按键输入寄存器0x7000_0040GPIOLED输出寄存器对应的Verilog地址解码逻辑如下module MIOC( input wire [31:0] memAddr, output reg ioCe ); always (*) begin if(memAddr 32h7000_0000 memAddr 32h8000_0000) ioCe 1b1; else ioCe 1b0; end endmodule2.2 第一个测试程序循环移位让我们用MIPS汇编编写最简单的流水灯程序lui $1, 0x7000 # $1 0x70000000 ori $1, $1, 0x0040 # $1指向LED寄存器地址 ori $3, $0, 0x0001 # 初始化LED模式 loop: sw $3, 0($1) # 写入LED寄存器 sll $3, $3, 1 # 左移一位 bne $3, $0, loop # 非零则循环 ori $3, $0, 1 # 重新初始化 j loop对应的机器码可以直接存入指令存储器(InstMem)模块。在Modelsim中仿真时你可以观察到程序计数器(PC)按预期递增寄存器$3的值不断左移存储指令正确将数据写入0x70000040地址3. 进阶篇加入交互控制3.1 读取开关状态单纯的流水灯略显单调让我们增加开关控制功能。扩展测试程序使其能响应外部输入lui $1, 0x7000 # $1 0x70000000 ori $1, $1, 0x0010 # $1指向KEY寄存器 lui $2, 0x7000 ori $2, $2, 0x0040 # $2指向LED寄存器 ori $3, $0, 0x0001 # 初始化LED模式 control_loop: lw $4, 0($1) # 读取开关状态 beq $4, $0, shift_left srl $3, $3, 1 # 开关开启时右移 j update_led shift_left: sll $3, $3, 1 # 开关关闭时左移 update_led: sw $3, 0($2) # 更新LED j control_loop3.2 状态保持与边界检测上述代码存在一个问题当LED模式移出边界时会归零。改进方案check_left: andi $5, $3, 0x8000 # 检测最高位 bne $5, $0, reset_left sll $3, $3, 1 j update_led reset_left: ori $3, $0, 0x0001 j update_led check_right: andi $5, $3, 0x0001 # 检测最低位 bne $5, $0, reset_right srl $3, $3, 1 j update_led reset_right: ori $3, $0, 0x8000 j update_led4. 高级篇实现定时器中断4.1 CP0协处理器配置MIPS的中断处理依赖于协处理器CP0。关键寄存器包括寄存器地址功能描述Count9递增计数器Compare11触发中断的阈值Status12中断使能控制Cause13中断原因记录EPC14中断返回地址配置定时器的Verilog实现module CP0( input wire clk, input wire [4:0] addr, output reg [31:0] data ); reg [31:0] Count; reg [31:0] Compare; always (posedge clk) begin Count Count 1; if(Count Compare) Cause[15] 1b1; // 触发定时器中断 end always (*) begin case(addr) 5d9: data Count; 5d11: data Compare; // 其他寄存器... endcase end endmodule4.2 中断服务程序当中断发生时CPU需要保存现场到EPC跳转到0x00000050的中断向量地址执行中断服务程序(ISR)使用ERET指令返回示例ISR代码.ktext 0x50 mfc0 $k0, $13 # 读取Cause寄存器 andi $k0, $k0, 0x8000 beq $k0, $0, exit_isr # 定时器中断处理 lui $k1, 0x7000 ori $k1, $k1, 0x0040 lw $k0, 0($k1) xori $k0, $k0, 0xFFFF # LED状态取反 sw $k0, 0($k1) # 重置定时器 mtc0 $0, $9 # 清零Count lui $k0, 0x1000 mtc0 $k0, $11 # 设置Compare exit_isr: eret5. 系统集成与调试技巧5.1 顶层模块设计完整的SoC顶层模块需要集成module SoC( input wire clk, input wire rst, input wire [1:0] switch, output wire [15:0] led ); // 时钟分频 clk_div clk_div0(.clk(clk), .rst(rst), .Clk_CPU(Clk_CPU)); // MIPS核心 MIPS mips0(.clk(Clk_CPU), .rst(rst), .instruction(instruction), ...); // 存储器与外设 InstMem instmem0(.addr(pc), .data(instruction)); DataMem datamem0(.clk(Clk_CPU), .addr(memAddr), .data(memData)); IO io0(.addr(ioAddr), .sel(switch), .led(led)); endmodule5.2 常见问题排查当功能不正常时建议按以下步骤检查时钟与复位确认时钟信号是否到达所有模块复位信号是否保持足够周期数据通路// 调试代码示例 always (posedge clk) begin $display(PC%h, Inst%h, pc, instruction); $display(Reg[3]%h, LED%h, reg3, ledReg); end中断问题检查CP0寄存器配置确认Status寄存器中的中断使能位验证中断向量地址是否正确6. 扩展思路打造你的专属CPU实验平台掌握了基础验证方法后你可以进一步扩展添加新外设七段数码管显示UART串口通信PWM输出控制优化CPU架构实现流水线设计添加缓存系统支持更多MIPS指令高级调试技术嵌入式逻辑分析仪(SignalTap)通过UART输出调试信息性能计数器的实现提示在FPGA资源允许的情况下建议保留足够的调试接口这会让后续开发事半功倍。例如添加一个通过开关控制的调试模式可以暂停CPU运行并查看寄存器状态。

更多文章