基于Verilog的FPGA斐波那契计算器设计与MIPS软核协同实现
2026/9/16 5:30:53 网站建设 项目流程

简介:压缩包内是一套与斐波那契数列计算相关的 Verilog、FPGA 及 MIPS 实验工程,面向学习硬件描述语言、数字逻辑或计算机组成课程的本科生,也适合需要 FPGA 入门项目的开发者。包内共 157 个文件,大小仅 1.27MB,其中包括 46 个 TDF 设计文件、43 个 Verilog 源码、35 个 BSF 符号封装、3 个 BDF 原理图和 4 个 VWF 仿真波形,还带有 SOF/POF 下载文件与 QSF 工程配置,打开工程即可编译烧录。目前已有 277 人学习下载,证明这类实验主题有较强的共性需求。结合包内“3-1 斐波那契”模块与 MIPS 相关文件,读者可以对照学习 Verilog 中顺序和并行两种斐波那契实现,理解状态机、寄存器、加法器协同工作的硬件设计思路;同时可通过 MIPS 数据通路文件延伸到处理器指令执行等更深入内容,从软件与硬件两个层面把握斐波那契计算的完整流程,提升数字系统设计实践能力。

1. 斐波那契计算:从 RTL 设计到 FPGA 上板的关键问题

先抛一个反直觉的结论:在 FPGA 上实现斐波那契数列,真正难的不是写出f(n)=f(n-1)+f(n-2)的迭代逻辑,而是决定用哪种电路结构、位宽设到多少、以及如何把计算结果安全地交给外部总线。很多初学者用for循环写组合逻辑,一综合就发现面积爆炸;或者用状态机写了迭代,但没做溢出处理,仿真到几十项之后结果就开始乱跳。这个标题把verilogfpgamips几个词捆在一起,本质是在问:斐波那契这个递归/迭代问题,怎么用硬件语言描述,怎么在 FPGA 上跑起来,又怎么和 MIPS 这类处理器协同。本文会给出完整的设计思路、可复现的代码、仿真方法和排错技巧,适合刚入门 Verilog 的工程师,也对想把这套逻辑挂到总线上的开发者有参考价值。

2. 用 Verilog 设计斐波那契计算器的三种思路与选型

2.1 组合逻辑展开:只适合固定小规模

斐波那契的数学定义很简洁,但直接映射成组合电路会面临资源爆炸。比如想要f(10),如果写成一个纯组合的展开:

// 组合展开:f(0)=0, f(1)=1, f(n)=f(n-1)+f(n-2) wire [9:0] f0 = 0; wire [9:0] f1 = 1; wire [9:0] f2 = f0 + f1; wire [9:0] f3 = f1 + f2; // ... 继续展开

这种方式每一级都需要一组加法器,而且级联深度导致组合路径延迟非常大。当 n 从 10 增加到 20,加法器数量和位宽同时增长,综合后时序几乎必然失败。我一般只在演示教学里用这种写法,或者用 generate 块生成固定长度的波形查表,不会用在真实工程里。它的优点是代码直观,仿真调试容易,适合验证定义是否写对。

2.2 时序状态机迭代:资源与速度的平衡点

工程上最常用的是用状态机做迭代计算,用寄存器保存当前项和前一项,每个时钟周期更新一次。这样无论 n 多大,占用的资源都只是几个寄存器和加法器,代价是计算出结果需要 n 个时钟周期。下面是一个参数化的迭代模块:

module fibonacci_iter #( parameter WIDTH = 32, parameter MAX_N = 47 // 斐波那契第47项为29,740,734,32位能容纳 )( input wire clk, input wire rst_n, input wire start, // 启动一次计算 input wire [WIDTH-1:0] n, // 请求第 n 项 output reg [WIDTH-1:0] result, output reg done ); reg [WIDTH-1:0] cur, prev; reg [WIDTH-1:0] count; reg [1:0] state; localparam IDLE = 2'd0, CALC = 2'd1, FINISH = 2'd2; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; cur <= 0; prev <= 0; count <= 0; done <= 0; result <= 0; end else begin case (state) IDLE: begin done <= 0; if (start) begin cur <= 1; prev <= 0; count <= 0; state <= CALC; end end CALC: begin if (count == n) begin result <= cur; state <= FINISH; end else begin // 迭代公式:cur <= cur + prev; prev <= cur; // 需要临时变量防止被覆盖 reg [WIDTH-1:0] tmp; // 注意不可在此声明 end end endcase end end endmodule

上面代码里有个故意留的错误:reg 不能放在 always 块内声明。正确的写法是在模块声明区加一个reg [WIDTH-1:0] tmp;,然后在CALC分支里这样更新:

CALC: begin if (count == n) begin result <= cur; state <= FINISH; done <= 1; end else begin tmp = cur + prev; prev <= cur; cur <= tmp; count <= count + 1'b1; end end FINISH: begin // 结果已在 result 中,等待外部拉低 start 回到 IDLE if (!start) state <= IDLE; end

这里的时序逻辑注意两点:tmp是组合逻辑临时量,阻塞赋值放在时钟沿触发的 always 里其实不推荐,但为了演示单周期更新的正确顺序,我用了先相加再更新的写法。更好的做法是直接用非阻塞赋值拆开两条语句:cur <= cur + prev; prev <= cur;这样不需要临时变量,因为非阻塞赋值在同一个时钟沿只更新一次。我建议新手直接采用后者,写起来更安全。

参数说明表:

参数名默认值含义注意事项
WIDTH32数据位宽决定最大可表示范围,斐波那契第47项是29.7亿,48项超32位
MAX_N47可计算的最大项数索引仅用于仿真时提示溢出,真实溢出检测靠硬件

2.3 查表与 BRAM 实现:固定序列的高速方案

如果需要的斐波那契数列是固定长度,比如只是给测试激励提供一组黄金分割参考值,完全可以用 ROM 初始化文件把前 N 项存进去,然后用地址索引读取。这种方案没有加法器,时序最好,时钟频率可以跑得很高。Xilinx 和 Intel FPGA 都支持用.coe.mif文件初始化 BRAM。生成斐波那契查表的 Python 脚本:

#!/usr/bin/env python3 # 生成 Fibonacci 前 32 项的 MIF 文件 width = 32 depth = 32 with open("fib32.mif", "w") as f: f.write("WIDTH={};\nDEPTH={};\nADDRESS_RADIX=HEX;\nDATA_RADIX=HEX;\nCONTENT BEGIN\n".format(width, depth)) a, b = 0, 1 for i in range(depth): f.write("{} : {:08X};\n".format(i, a)) a, b = b, a + b f.write("END;\n")

这个方式适合不需要任意 n 的场景。比如做流水线测试、图像处理里生成平滑权重,查表比实时计算省资源。但要注意:查表的数据位宽要提前规划好,否则第 47 项之后的 32 位数据会溢出,仿真不会报错,只能靠波形观察。

3. 在 FPGA 上把斐波那契逻辑变成可跑模块:关键参数与仿真

3.1 接口定义:时钟复位使能与数据位宽怎么定

上一章的迭代模块已经给出了基本接口。设计接口时最重要的参数是WIDTH,它不只是决定能算到第几项,还影响综合后的 LUT 和 FF 数量。32 位加法器和 64 位加法器面积差异明显,如果只是做验证,建议先定 16 位,快速跑通流程后再扩位宽。接口中start信号必须做上升沿检测,否则持续拉高会导致重复启动。复位建议用异步复位同步释放,以下代码演示了标准的复位处理:

reg rst_n_sync1, rst_n_sync2; always @(posedge clk) begin rst_n_sync1 <= rst_n; rst_n_sync2 <= rst_n_sync1; end wire rst_n_synced = rst_n_sync2;

用同步后的复位信号去驱动模块,避免真实上板时复位释放沿不一致导致时序异常。n寄存器在CALC状态中是不变的,因此不需要额外锁存;但如果是外部变化的总线信号,最好在 IDLE 时用寄存器捕获一次。

3.2 参数化位宽、溢出检测与流水线设计

参数化不仅仅是定义WIDTH,还要考虑当n超过能表示的范围时怎么办。斐波那契增长极快,第 94 项就超过 64 位无符号整数的范围。工程上的常见做法是设置一个OVERFLOW标志:

// 在加法器后判断溢出(无符号加法) wire [WIDTH-1:0] sum; assign sum = cur + prev; wire overflow = (sum < cur) && (sum < prev); // 无符号溢出:和小于任一加数

每次迭代时检查这个标志,一旦为真就将状态跳到FINISH,同时把done拉高,外部就知道这次计算被截断了。如果要算更大的项,可以把内部寄存器扩展到2*WIDTH,输出时再截断,但这样会增加资源使用。流水线设计是另一个进阶思路:把cur + prev这步拆成两级,比如预先计算prev的延迟副本,让关键路径里的加法器前级不依赖同一周期的输出,从而使时钟频率从 100MHz 提到 150MHz。下面是一个简单流水线分割后的迭代片段:

// 流水线版本:用两拍完成一次迭代 wire [WIDTH-1:0] add_result; reg [WIDTH-1:0] prev_d; always @(posedge clk) prev_d <= prev; // 延迟一拍 assign add_result = cur + prev_d; // 加法输入与输出错开 always @(posedge clk) begin if (state == CALC) begin cur <= add_result; prev <= cur; end end

这个改法让加法器输入来自一个时钟前的prev_d,输出不反馈到同一时钟沿的自身路径,时序获益明显,但代价是计算结果晚一个周期,需要在使能和完成逻辑上补拍。

3.3 测试平台与仿真命令:用 Icarus Verilog 快速验证

写一个完整的测试平台,覆盖普通计算、溢出、复位抖动三种场景。用开源工具 iverilog 加 GTKWave 仿真:

`timescale 1ns/1ps module tb_fibonacci_iter; reg clk = 0; reg rst_n = 0; reg start = 0; reg [15:0] n; wire [15:0] result; wire done; fibonacci_iter #(.WIDTH(16), .MAX_N(20)) uut ( .clk(clk), .rst_n(rst_n), .start(start), .n(n), .result(result), .done(done) ); always #5 clk = ~clk; // 100MHz initial begin $dumpfile("fib.vcd"); $dumpvars(0, tb_fibonacci_iter); #20 rst_n = 1; // 计算第 10 项,期望 55 @(negedge clk); n = 10; start = 1; @(negedge clk); start = 0; wait (done); $display("fib(10) = %0d", result); // 计算第 20 项,期望 6765 @(negedge clk); n = 20; start = 1; @(negedge clk); start = 0; wait (done); $display("fib(20) = %0d", result); $finish; end endmodule

仿真命令:

iverilog -o fb_tb tb_fibonacci_iter.v fibonacci_iter.v vvp fb_tb gtkwave fib.vcd &

逻辑说明:测试平台在negedge clk拉高start,避免与时钟上升沿竞争;wait(done)等待硬件完成,然后打印结果。这里没有检查溢出场景,你可以在仿完正确结果后将n设为 40,观察overflow标志位是否拉高。参数WIDTH改为 16 后,第 24 项 46368 就超过了 16 位无符号整数上限 65535,但迭代逻辑不会自动停止,所以溢出检测在真实工程里是必须的。

4. MIPS 与软核协同:当斐波那契被当作硬件加速器

4.1 为什么在 MIPS 处理器里跑斐波那契要单独硬件

MIPS 架构以简洁的指令集著称,但斐波那契无论用递归还是循环,都会占用几十条甚至上百条指令。软核跑在 50MHz 时,执行一条整数加法至少 4 个时钟周期,计算第 30 项需要几万条指令,耗时毫秒级。而硬件迭代模块只需要 30 个时钟周期,差距三个数量级。所以标题把 MIPS 和斐波那契放在一起,核心动机就是做硬件加速:把循环计算下沉到 RTL,处理器只负责发起任务和读取结果。常见做法是在 FPGA 里例化一个 MIPS 软核(比如自己写的教学核,或者 OpenMIPS),通过自定义总线或存储器映射的寄存器与斐波那契模块交互。

4.2 总线接口设计:用寄存器映射代替复杂握手

最简单的加速器对接方式是把计算模块的输入输出映射到一组寄存器上,处理器用lw/sw指令读写这些寄存器。以 MIPS 的addiusw为例,软件侧可以这样控制:

// 硬件加速器地址映射(基地址由 SoC 决定) #define FIB_BASE 0x80000100 #define FIB_CTRL 0x00 // [0] start, [1] done #define FIB_N 0x04 // n 输入 #define FIB_RES 0x08 // 结果输出 unsigned int fib_hw(unsigned int n) { volatile unsigned int *ctrl = (volatile unsigned int *)(FIB_BASE + FIB_CTRL); volatile unsigned int *nreg = (volatile unsigned int *)(FIB_BASE + FIB_N); volatile unsigned int *res = (volatile unsigned int *)(FIB_BASE + FIB_RES); *nreg = n; // 写入 n *ctrl = 1; // 触发 start // 等待 done 位被硬件置 1 while ((*ctrl & 0x02) == 0); return *res; }

硬件端相应的寄存器接口如下:

// 寄存器堆简化代码:根据总线地址写控制寄存器 reg start_reg; reg [WIDTH-1:0] n_reg; wire done_wire; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin start_reg <= 0; n_reg <= 0; end else if (we == 1) begin case (addr[3:2]) 2'h0: start_reg <= wdata[0]; 2'h1: n_reg <= wdata[WIDTH-1:0]; default: ; endcase end else if (done_wire) begin start_reg <= 0; // 自动清 start,避免重复启动 end end

这里需要注意:MIPS 是字节寻址,寄存器偏移要按字节对齐。addr[3:2]取了 32 位字偏移的两位,实际偏移 0x0、0x4、0x8。软件里的volatile是必须的,否则编译器会把while循环优化成无限循环,因为寄存器内容在 C 语言眼里没有可变理由。汇编级别也可以直接写:

# a0 中存放 n,结果返回 v0 la $t0, FIB_BASE sw $a0, 4($t0) # 写 n 到偏移 4 li $t1, 1 sw $t1, 0($t0) # start 拉高 li $t1, 2 lw $t2, 0($t0) and $t3, $t2, $t1 # 检测 done 位

4.3 交互时序与总线握手:解决 done 信号丢拍问题

上面软件用轮询等待 done 位,但硬件可能在轮询第一次读取前就已经完成计算,此时 done 位被自动清除,软件就会死等。解决方法是硬件侧让done位持续保持,直到软件写ctrl清除。改进后的状态机在FINISH里不通过start下降沿回 IDLE,而是等一个clear位:

FINISH: begin if (clear_reg) state <= IDLE; end

软件侧则在检测到 done 后写ctrl = 0清除标志。常见做法是在寄存器接口里设置ctrl的 bit0 为 start,bit1 为 clear,bit2 为 busy 只读。这样轮询流程变成:写 start → 读 busy 直到为 0 → 读结果 → 写 clear。这个流程在高速总线下有两拍延迟,但不会丢状态。

5. 验证与排错:从仿真波形到上板的三个关键技巧

5.1 用板载时钟源优化慢速观察

FPGA 板卡一般有 50MHz 或 100MHz 的晶振,直接观察点灯时切换太快,人眼无法分辨。常见做法是分频到 1~2Hz。比如用计数器对 50MHz 分频:

reg [24:0] cnt; reg slow_clk; wire clk_1hz; always @(posedge clk) begin cnt <= cnt + 1'b1; if (cnt == 25000000-1) begin cnt <= 0; slow_clk <= ~slow_clk; end end assign clk_1hz = slow_clk;

更建议的是用锁相环产生低频时钟,但分频逻辑在演示项目里更简单。注意:时钟分频后设计中的所有时序约束都要放在原始clk域,slow_clk只是给观察用的脉冲。如果你要把分频后的时钟作为复位或使能信号,应声明为enable而不是clk,否则综合工具可能生成异步时钟导致时序分析变得复杂。

5.2 仿真通过了但上板结果错误:优先检查复位与跨时钟域

我遇到过最频繁的问题是:仿真用的rst_n是立即拉高的,但板子上的复位信号可能在上电后几十毫秒才稳定。如果复位与时钟沿对齐不好,寄存器初始值可能是未知态。解决办法是在仿真里模拟真实复位:在#20拉高前先给几十纳秒的毛刺,并且用上一章的同步释放电路。另外,如果斐波那契模块的start信号来自 MIPS 总线时钟域的写脉冲,而模块本身跑系统时钟,就要做脉冲同步:

// 简单的双触发器同步器 reg start_sync1, start_sync2; always @(posedge clk) begin start_sync1 <= bus_start; start_sync2 <= start_sync1; end wire start_pulse = start_sync1 & ~start_sync2;

这个脉冲只保持一个时钟周期,避免总线域的慢脉冲被系统域多拍采样导致计算重复启动。

5.3 位宽溢出与综合优化陷阱

最后提一个容易踩的坑:Verilog 里未指定位宽的字面量是 32 位。比如cur <= cur + prev;cur是 16 位,系统会自动截断,但如果你写cur <= cur + prev + 1;,最后一个1是 32 位,综合器可能生成比预期更大的加法器。解决方法是显式写16'd1或者用1'b1。查表法里也有类似问题,MIF 文件数据必须和模块里的数据位宽严格一致,否则仿真能读但综合后 BRAM 读出来会错位。

验证最终结果时,可以在顶层模块里把done信号接一个 LED,把result的高 4 位接多个 LED,用计数器产生start脉冲计算固定 n 值。上板后观察点灯规律,再和仿真波形比对。如果点灯闪烁频率异常,先用板载逻辑分析仪抓内部信号,比如用 Xilinx ILA 或者 iverilog 配合腐败的 VCD 文件离线分析。注意不要用$display观察综合后的信号,那只是仿真行为,真实上板必须依靠外部抓取或片上逻辑分析仪。

斐波那契的 RTL 实现不难,难的是把迭代逻辑在特定 FPGA 资源、时钟约束和总线协议下做稳。把位宽、复位、握手三个点控制好,这个模块就能作为通用加速器复用到更多场景中。

本文还有配套的精品资源,点击获取

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

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

立即咨询