
简介压缩包内是一套与斐波那契数列计算相关的 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循环写组合逻辑一综合就发现面积爆炸或者用状态机写了迭代但没做溢出处理仿真到几十项之后结果就开始乱跳。这个标题把verilog、fpga、mips几个词捆在一起本质是在问斐波那契这个递归/迭代问题怎么用硬件语言描述怎么在 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,73432位能容纳 )( 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 2d0, CALC 2d1, FINISH 2d2; 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 1b1; 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_RADIXHEX;\nDATA_RADIXHEX;\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 的addiu、sw为例软件侧可以这样控制// 硬件加速器地址映射基地址由 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]) 2h0: start_reg wdata[0]; 2h1: 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 为 startbit1 为 clearbit2 为 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 1b1; 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 位综合器可能生成比预期更大的加法器。解决方法是显式写16d1或者用1b1。查表法里也有类似问题MIF 文件数据必须和模块里的数据位宽严格一致否则仿真能读但综合后 BRAM 读出来会错位。验证最终结果时可以在顶层模块里把done信号接一个 LED把result的高 4 位接多个 LED用计数器产生start脉冲计算固定 n 值。上板后观察点灯规律再和仿真波形比对。如果点灯闪烁频率异常先用板载逻辑分析仪抓内部信号比如用 Xilinx ILA 或者 iverilog 配合腐败的 VCD 文件离线分析。注意不要用$display观察综合后的信号那只是仿真行为真实上板必须依靠外部抓取或片上逻辑分析仪。斐波那契的 RTL 实现不难难的是把迭代逻辑在特定 FPGA 资源、时钟约束和总线协议下做稳。把位宽、复位、握手三个点控制好这个模块就能作为通用加速器复用到更多场景中。本文还有配套的精品资源点击获取