FPGA入门(六):Vivado 工程复用 + 流水灯实战(参数化设计)

📅 2026/7/15 6:38:13 👁️ 阅读次数 📝 编程学习
FPGA入门(六):Vivado 工程复用 + 流水灯实战(参数化设计)

FPGA入门


文章目录

  • FPGA入门
  • 前言
  • 一、实操:历史 decoder_3_8 译码器模块添加至新项目
  • 二、顶层模块 led_run3 代码逐行详解
    • 1、第一个计数器:延时计数器
    • 2、第二个计数器:LED 选择计数器(控制亮哪个灯)
    • 3、例化 3-8 译码器
    • 4、一句话总结
  • 三、parameter MCNT 参数精讲
  • 四、Testbench 仿真文件逐行精讲
  • 总结

前言

做 FPGA 项目经常复用之前调试好的功能模块,重复手写代码费时还容易出错。本文先手把手演示Vivado 导入已有.v 模块到新项目实操,再基于导入的 3-8 译码器搭建流水灯顶层工程,逐行拆解顶层代码逻辑、parameter 参数调试优势,最后配套讲解 Testbench 仿真代码,包含两种参数重载写法,新手跟着实操即可跑通流水灯工程


一、实操:历史 decoder_3_8 译码器模块添加至新项目

  1. 新项目打开添加源文件窗口
    新建工程后,在 Sources 栏右键选择Add Sources,弹出添加页面,点击【Add Files】按钮,进入本地文件选择弹窗。

  2. 定位历史工程根目录

在文件浏览框中,找到存放旧工程的文件夹(示例路径D:/FPGA/class/),选中decoder_3_8旧工程文件夹

  1. 找到源文件目录,选中.v源码
    进入decoder_3_8工程目录,打开decoder_3_8.srcs → sources_1 → new路径,选中里面的decoder_3_8.v译码器源文件,点击【OK】添加。

  2. 关键配置:勾选复制源码到本工程
    回到添加源文件界面,勾选Copy sources into project选项,该选项作用:将原工程的.v文件复制一份到当前新项目本地目录,后续修改旧工程源码不会干扰本项目,是工程开发规范操作;勾选后依次点击Next→Finish,模块导入完成。

补充:导入后在当前工程 Sources 列表即可看到 decoder_3_8 模块,可直接在顶层代码例化调用。

二、顶层模块 led_run3 代码逐行详解

先看整体功能:50MHz 时钟输入 → 延时计数器 → 3 位循环计数器 → 3-8 译码器 → 8 路 LED 流水灯

`timescale1ns/1ps// 仿真时间单位:1纳秒,精度:1皮秒(不用深究,固定写法)moduleled_run3(input Clk,// 系统时钟输入(开发板一般是50MHz)input Reset_n,// 复位按键输入(低电平有效,按下=0,松开=1)output[7:0]Led// 8路LED输出(8个灯));

1、第一个计数器:延时计数器

reg[24:0]counter;// 定义25位的寄存器变量counter(最大能数到2^25-1)// 时序逻辑:时钟上升沿 或 复位下降沿 触发always@(posedge Clk or negedge Reset_n)if(!Reset_n)// 低电平复位:按下复位键,counter清零counter<=0;elseif(counter==满值)// 计数器数到最大值counter<=0;// 清零,重新开始计数elsecounter<=counter+1'd1;// 正常情况:每个时钟+1

FPGA 的时钟每秒跳 5000 万次,如果直接让 LED 跟着时钟闪,人眼完全看不见!
这个计数器的作用就是数数延时:数到固定数值才停一下,给 LED 切换留时间。

2、第二个计数器:LED 选择计数器(控制亮哪个灯)

reg[2:0]counter2;// 3位寄存器(只能数0~7,刚好对应8个LED)// 同样时钟+复位触发always@(posedge Clk or negedge Reset_n)if(!Reset_n)counter2<=0;// 复位清零elseif(counter==满值)// 重点:只有第一个计数器数满了!counter2<=counter2+1'd1;// counter2才+1
  • counter2 是3 位,数值范围:0、1、2、3、4、5、6、7,循环往复;
  • 它不跟着时钟疯狂加 1,只有等第一个计数器数完一轮(延时结束),才加 1;
  • 它的数值,就是告诉译码器:现在亮第几个 LED。

3、例化 3-8 译码器

decoder_3_8decoder_3_8_inst0(// 调用我们之前导入的3-8译码器模块.A0(counter2[0]),// 3位输入:接counter2的0/1/2位.A1(counter2[1]),.A2(counter2[2]),.Y0(Led[0]),// 8位输出:直接接8个LED.Y1(Led[1]),.Y2(Led[2]),.Y3(Led[3]),.Y4(Led[4]),.Y5(Led[5]),.Y6(Led[6]),.Y7(Led[7]));

3-8 译码器的功能:输入 3 位数(0~7),输出只有 1 路是高电平,对应点亮 1 个 LED
counter2=0 → 亮 LED0
counter2=1 → 亮 LED1

counter2=7 → 亮 LED7
循环起来,就是流水灯!

4、一句话总结

时钟驱动延时计数器数数 → 数满一轮 → 3 位计数器 + 1 → 译码器点亮对应 LED → 循环往复 = 流水灯。
完整代码

`timescale1ns/1ps moduleled_run3(Clk,Reset_n,Led);input Clk;input Reset_n;output[7:0]Led;reg[24:0]counter;parameter MCNT=25000000-1;always@(posedge Clk or negedge Reset_n)if(!Reset_n)counter<=0;elseif(counter==MCNT)counter<=0;elsecounter<=counter+1'd1;reg[2:0]counter2;always@(posedge Clk or negedge Reset_n)if(!Reset_n)counter2<=0;elseif(counter==MCNT)counter2<=counter2+1'd1;decoder_3_8decoder_3_8_inst0(.A0(counter2[0]),.A1(counter2[1]),.A2(counter2[2]),.Y0(Led[0]),.Y1(Led[1]),.Y2(Led[2]),.Y3(Led[3]),.Y4(Led[4]),.Y5(Led[5]),.Y6(Led[6]),.Y7(Led[7]));endmodule

三、parameter MCNT 参数精讲

现在我们回头看这个参数:

parameter MCNT=25000000-1;// 延时计数器的满值
  1. 语法作用
    parameter 是可配置的常量,把计数器的最大值定义成一个名字MCNT。
  2. 为什么要用它?(核心好处)
  • 上板调试:开发板用 50MHz 时钟,需要延时很久,所以MCNT=2500万,LED 每秒闪 1 次,肉眼可见;
  • 仿真调试:如果仿真也用 2500 万,电脑要跑几十分钟才能看到灯闪!
    所以我们可以直接改这个参数,改成很小的数(比如 25000),仿真几秒钟就出结果;
  • 一改全改:只改参数值,不用动任何逻辑代码,零出错。

四、Testbench 仿真文件逐行精讲

`timescale1ns/1ns// 仿真单位:1ns,精度1ns(仿真常用)moduleled_run_tb();// 仿真模块(无输入输出)reg Clk;// 激励信号:给主模块的时钟(用reg定义)reg Reset_n;// 激励信号:给主模块的复位(用reg定义)wire[7:0]Led;// 接收主模块的LED输出(用wire定义)// 例化主模块:把仿真信号和主模块连接led_run3 #(.MCNT(25000-1)// 【方法1】实例化时直接修改参数!)led_run3_inst0(.Clk(Clk),.Reset_n(Reset_n),.Led(Led));// defparam led_run3_inst0.MCNT = 250000 - 1; // 【屏蔽的方法2】修改参数initial Clk=1;// 初始化时钟初始值为1always #10Clk=!Clk;// 每10ns翻转一次时钟 → 周期20ns → 50MHz时钟initial begin// 复位激励Reset_n=0;// 先复位(低电平有效)#201;// 保持201nsReset_n=1;// 释放复位,开始运行#40000000;// 仿真运行时间$stop;// 停止仿真end endmodule
  1. 核心信号定义规则
  • 仿真要主动发给主模块的信号(时钟、复位)→ 用 reg;
  • 仿真接收主模块输出的信号(LED)→ 用 wire。

仿真时我们需要把MCNT改小,让仿真跑的快,Verilog 有两种标准写法:
方法 1:例化时直接传参

led_run3 #(.MCNT(25000-1)// 直接把主模块的MCNT覆盖成25000)led_run3_inst0(...);

优点:简洁直观,最常用。
方法 2:defparam 行内参数修改

defparam led_run3_inst0.MCNT=250000-1;
  • defparam = 定义参数;
  • led_run3_inst0:你例化的模块名字;
  • 作用:单独修改实例化模块的参数;
  • 适用场景:模块例化后,想单独改参数时用;
  • 两种方法功能完全一样,用哪个都行!
  • defparam 单独指定某一个实例的参数,独立于例化语句之外;部分老项目、分层嵌套模块大量使用该语法,兼容性强,日常调试备用。
  1. 时钟生成
initial Clk=1;always #10Clk=!Clk;// #10 = 等待10ns
  • 每 10ns 翻转一次电平,一个完整周期 = 20ns;
  • 对应开发板的50MHz 系统时钟,仿真和上板时钟完全一致。
  1. 复位激励
initial begin Reset_n=0;// 先拉低复位#201;// 保持复位Reset_n=1;// 松开复位,电路开始工作end
  1. 时钟:always #10 Clk=!Clk,10ns 电平翻转,时钟周期 20ns,精准模拟开发板 50MHz 时钟;
  2. 复位:先拉低 Reset_n=0 保持 201ns 完成上电复位,随后拉高释放复位,模块正式开始工作;仿真末尾$stop终止仿真。

总结

  1. 开发规范:历史调试完毕的模块优先导入复用,避免重复编码,导入务必勾选 Copy 复制源码;
  2. 流水灯逻辑:分频计数器控延时 + 3 位计数器选灯 + 38 译码器驱动 LED;
  3. parameter 是跨场景调试利器:上板大数、仿真小数,一套源码两用;
  4. 参数重载两种方式:实例 #传参、defparam 修改,掌握后灵活适配各类仿真需求。