FPGA入门学习(一、Verilog初识)
Vivado创建、仿真运行第一个Verilog源文件
写在前面
本文旨在记录笔者学习 FPGA 的入门日常,作为阶段性总结与备忘。笔者目前处于研 0 阶段,更新时间不定,主要以自学方式推进。本笔记内容包括 FPGA 基础知识、Verilog 语言学习、仿真与综合流程、调试方法等。
目前的学习路线选择[B 站 - 小梅哥 Xilinx FPGA 基础入门]+ Vivado + Verilog + Modelsim。由于截至 2025 年 4 月 2 日尚无开发板,学习进度暂以仿真和理论为主,待开发板到货后,会结合实际硬件进行实践,补充相关实验记录与调试经验。
本笔记主要用于个人学习记录,内容可能较为零散,适合有 FPGA 基础或同步学习的读者参考。如有不足或错误,欢迎交流指正。
1、打开 Vivado 建立工程文件
目前学习使用 Vivado 2023.2 版本进行 FPGA 相关开发与实验。由于 Vivado 安装流程较为常规,且网络上已有许多详细的安装教程,因此本文省略具体安装步骤。若有安装或环境配置方面的疑问,可参考其他优秀博主的文章或官方文档进行学习。如有特殊需求,也可后台私聊笔者交流。
本笔记主要关注 Vivado 的基本使用、工程创建、综合 (Synthesis)、实现 (Implementation)、仿真 (Simulation) 以及 调试 (Debugging) 等关键环节,结合 Verilog 语言编写实践,以期快速上手 FPGA 开发流程。
Vivado 软件一打开,就给人一种专业且高端的感觉。相比于 Keil 那种简洁的界面,Vivado 的 UI 设计让人耳目一新的同时,也不禁让笔者虎躯一震产生一丝忧虑:能否顺利掌握这样一款强大的工具?
不过,回顾其他专业软件(如 Keil等)的使用流程,初始步骤其实大同小异。软件的逻辑往往是相似的,只是功能更加丰富,界面更加复杂罢了。以创建新工程为例,可以直接在 Vivado 启动界面点击 “Create Project” 进入项目创建向导,或者通过左上角 “File” -> “Project” -> “New …” 进入相同的流程。两种方式本质相同,选择适合自己的方式即可。
弹出客套页面,提示做好准备,可无脑Next。
进入项目路径设置页面,此处Project name处,笔者命名为mux2_2,意在创建第二个二路选择器项目(细心的读者应该能从上图看到此前笔者已经创建完毕的mux2项目),读者可省去’_2’。
下图给出此前创建的mux2项目的路径,可供参考。
选择项目类型,此处笔者按照视频步骤选择,选择’Do not specify sources at this time’,意在何为?
选择板子类型?(梦回Keil),此处笔者按照视频步骤选择,在’Serach’输入’XC7A75TFGG484’,此处型号应具体考虑具体板子,此处等笔者的开发板到货后更新。
2、向工程文件加入 .v 源文件
在左侧边栏’PROJECT MANAGE’ 点击 ‘Add Sources’, 在弹出界面处选中’Add or create design sources’。
在’Add Sources’页面点击’Create File’,然后在弹出的’Create Source File’界面处的’File name’输入文件名称,笔者此处建议mux2,若有其他想法,当笔者没说。
设置完毕后,一路OK+Finish到’Define Module’界面,点击’OK’后可无视弹出的提示界面(点击Yes)。
之后后台一顿操作,界面updating后如下图所示出现刚刚创建好的源文件。右侧的工程信息汇总也是相当的人性化。
双击源文件后可以看到,源文件内容页面覆盖了工程信息页面,目前源文件页面停靠在的子页面太小,可以点击’口(象形)'和神似分享按钮的那个图形,前者可以最大化子页面,后者则是脱离子页面后可最大化页面。
3、熟悉 Verilog 语法
系统创建的源文件自带如下图的内容,这些内容主要是方便维护各个文件,这里我们一并删除全部内容,即获得一个没有字符的空白文件,从头开始。
输入以下内容,梦回C嘎嘎和MATLAB,内容对缩进没有强制要求,但尽量满足吧。
值得注意的是,在第一行module中,右括弧结束之后需要有’;'结尾,匪夷所思。
module mux2(a, b, sel, out);
input a, b, sel;
output out;
// Output one from two input
assign out = (sel == 1)? a : b;
endmodule
以下,逐行逐关键字解释说明,加深理解。
module mux2(a, b, sel, out);
// 定义一个 Verilog 模块 module,类似于C/C++的抽象类Class,名称为 mux2,用于实现 2:1 选择器。
// 这个模块有四个端口:a、b、sel、out,它们是模块的输入输出接口,像C/C++的成员?,但本质上是硬件描述。
// 注意右括弧结束之后需要有';'结尾
input a, b, sel;
// input 关键字在 Verilog 中默认是 wire 类型,即它们只能被外部驱动,而不能在模块内部赋值。
// 声明 a、b、sel 为输入端口,表示它们是由外部电路驱动的信号。
output out;
// output 关键字在 Verilog中默认 wire 类型,即它们只能被外部驱动,而不能在模块内部赋值。
// 声明 out 端口,它表示多路复用器的输出信号。
// input 和 output 的区别
// input:只能 读,不能在模块内部赋值。
// output:可以在模块内部赋值,并输出给外部使用。
// inout(双向端口):既可以作为输入,也可以作为输出(类似于 C++ 的 & 引用,或 MATLAB 的 global 变量)。
assign out = (sel == 1)? a : b;
// assign 语句用于组合逻辑(Combinational Logic),它会在输入信号变化时立即更新 out。
// X? Y:Z 是三目运算符,即X为True时,执行Y,否则为Z.
// out 的值取决于 sel:
// sel == 1 时,out = a
// sel == 0 时,out = b
endmodule
// 结束 mux2 模块的定义,相当于 C 语言的 }。
结束编程后,可保存(Ctrl+S)查看是否出现问题,如下图所示,笔者在第一行末尾处少写了’;',保存后,编辑器自动会出现一系列的保存提示,包括但不限于熟悉的红色波浪号,警告标识和具体文件。
可以在左侧边栏中’RTL ANALYSIS -> Open Elaborated Design -> Schematic’分析综合前电路结构,是可视化的电路图,这个阶段的电路结构是基于 Verilog/VHDL 代码进行逻辑推导后的结果,并未进行优化。
综合操作,如下图所示将 HDL 代码转换为门级电路,生成网表(Netlist)。
4、向工程文件加入仿真文件以验证
与添加源文件步骤类似,如下图所示。
需要注意的是’File name’处建议在文件名后添加’_tb’用于验证设计文件,意在与实现实际电路逻辑的.v文件区分。
随后一路OK带Finish带Yes的,这部分步骤省略,最终成功创建mux2_tb.v文件。界面updating后如下图所示出现刚刚创建好带_tb的文件。右侧子界面展示的是该文件的内容。
随后,重复最大化子界面的操作,并删除文件内的全部内容,获得一个空白的文件。
输入以下内容。
`timescale 1ns / 1ns
module mux2_tb();
reg in_a, in_b, in_sel;
wire out_out;
mux2 myMux2(.a(in_a),
.b(in_b),
.sel(in_sel),
.out(out_out));
initial begin
in_a = 0; in_b = 0; in_sel = 0;
#200;
in_a = 0; in_b = 0; in_sel = 1;
#200;
in_a = 0; in_b = 1; in_sel = 0;
#200;
in_a = 0; in_b = 1; in_sel = 1;
#200;
in_a = 1; in_b = 0; in_sel = 0;
#200;
in_a = 1; in_b = 0; in_sel = 1;
#200;
in_a = 1; in_b = 1; in_sel = 0;
#200;
in_a = 1; in_b = 1; in_sel = 1;
#200;
$stop;
end
endmodule
以下,逐行逐关键字解释说明,加深理解。
`timescale 1ns / 1ns
// `timescale <时间单位> / <仿真步进精度>
// 时间单位 是仿真的时间单位,下面出现的 #200, 代表延时200时间单位
// 步进精度 是用于舍入计算精度
// 若时间单位为1ns,步进精度为1ps,则#200.5代表200ns+500ps = 200.5ns
// 若时间单位为1ns,步进精度为1ns,则#200.5代表200ns + 0ps = 200ns,即0.5ns这种ps级别的会被忽略
module mux2_tb();
// 定义 mux2_tb 测试模块(Testbench 模块)
// Testbench 不需要端口(I/O),因为它不是硬件,而是一个 仿真代码,用于测试 mux2 设计模块。
reg in_a, in_b, in_sel;
// reg 变量:in_a、in_b、in_sel 定义为 寄存器(reg),它们将在 initial 语句块中被赋值,以驱动被测模块。
wire out_out;
// wire 变量:out_out 不能用 reg,因为它是 mux2 设计模块的输出,需要由 mux2 计算得出。
mux2 myMux2(.a(in_a),
.b(in_b),
.sel(in_sel),
.out(out_out));
// 实例化 mux2 设计模块,并连接 Testbench 变量
// 模块实例名 myMux2 可以是任意名称,这里取 myMux2 只是为了清晰表示它是 mux2 的实例。
// . 号表示端口连接,等效于:mux2 myMux2(in_a, in_b, in_sel, out_out);
initial begin
// initial 语句块用于 初始化 Testbench。该代码块只执行一次(不像 always 是循环的)。通常用于定义测试输入序列。
in_a = 0; in_b = 0; in_sel = 0;
// 测试用例,设置输入值等待信号传播并观察 out_out 变化。
// 以下只是改变输入值,可配合真值表进行查阅
#200;
// 延迟 200ns
in_a = 0; in_b = 0; in_sel = 1;
#200;
in_a = 0; in_b = 1; in_sel = 0;
#200;
in_a = 0; in_b = 1; in_sel = 1;
#200;
in_a = 1; in_b = 0; in_sel = 0;
#200;
in_a = 1; in_b = 0; in_sel = 1;
#200;
in_a = 1; in_b = 1; in_sel = 0;
#200;
in_a = 1; in_b = 1; in_sel = 1;
#200;
$finish;
// 终止仿真,相当于 C 语言的 exit(0);。
// 另一个常见的终止仿真命令是 $stop;暂停仿真,可以继续。
end
endmodule
开始运行仿真文件。
仿真结果如下图所示,只需要关心是否有波形子界面。
查看是否符合预期效果(二选一选择器),最大化波形子界面,点击如下图示按钮,使波形填充完整。
将波形填充完整后,可以看到最大时间仅为1000ns。
如下图所示,每个端口,输出对应的波形一览无余。
5、 仿真结果可能出现的若干问题
目前笔者学习中只遇到了出现蓝色波形这一种情况,如果后续学习中出现新的问题,笔者会一一补充,此外,如果读者遇到问题,欢迎在评论区或者后台与笔者讨论,笔者也会添加至此。
1.出现蓝色波形
观察该波形对应的变量,是否出现声明但未关联现象。
检查程序,是否出现如下图示情况。
结语
学习 FPGA 是一个循序渐进的过程,初期或许会遇到许多挑战,未来随着开发板的到货,笔者将结合硬件进行更深入的学习,并在本笔记中记录相关的实验和心得。希望这份笔记能对你有所帮助!