Quick Start
本指南面向零基础或刚入门的FPGA学习者,目标是在30分钟内掌握Verilog最核心的语法子集,并避开新手最常见的陷阱。以下是最短路径:
- 步骤1:安装仿真环境(推荐Vivado 2024.2或ModelSim SE-64 2024.1,或免费版Vivado HLx WebPACK)。
- 步骤2:创建一个新工程,添加一个空白的.v文件,命名为top.v。
- 步骤3:编写一个最简单的组合逻辑模块——一个2输入与门,使用assign语句。
- 步骤4:编写一个简单的testbench,实例化该模块,并施加激励(输入变化)。
- 步骤5:运行仿真,观察输出波形是否与真值表一致(0&0=0, 0&1=0, 1&0=0, 1&1=1)。
- 步骤6:修改代码,加入一个D触发器(时序逻辑),使用always @(posedge clk)块,并观察仿真波形中输出在时钟上升沿后的变化。
- 步骤7:在Vivado中运行综合(Synthesis),查看RTL原理图,验证综合出的电路是否与预期一致(LUT+FF)。
- 步骤8:打开“Elaborated Design”,检查是否有未连接的端口或警告。
预期结果:完成以上步骤后,你应该能独立编写并仿真一个包含组合逻辑和时序逻辑的最小系统,并理解仿真与综合的区别。
前置条件与环境
| 项目 | 推荐值 | 说明 | 替代方案 |
|---|---|---|---|
| 器件/板卡 | Xilinx Artix-7 (XC7A35T) | 入门级FPGA,资源适中,教程丰富 | Intel Cyclone IV / Lattice iCE40 |
| EDA版本 | Vivado 2024.2 | 支持SystemVerilog 2017,综合与仿真集成 | Vivado 2023.2 / Quartus Prime 23.4 |
| 仿真器 | Vivado Simulator (xsim) | 内置于Vivado,无需额外安装 | ModelSim SE-64 2024.1 / Verilator 5.0 |
| 时钟/复位 | 50MHz板载时钟,高电平有效异步复位 | 最简配置,避免复杂时钟管理 | 100MHz / 低电平有效复位 |
| 接口依赖 | 无(纯仿真验证) | 本教程不涉及具体外设接口 | UART / SPI / GPIO |
| 约束文件 | XDC文件(仅综合/上板时需要) | 仿真阶段不需要约束,但综合必须有时钟周期约束 | SDC文件(Quartus) |
| 操作系统 | Windows 10/11 64位 或 Ubuntu 22.04 LTS | Vivado支持Windows和Linux | CentOS 7 / macOS(需虚拟机) |
目标与验收标准
完成本指南后,你应能:
- 功能点:独立编写包含组合逻辑(assign、always@*)和时序逻辑(always@(posedge clk))的Verilog模块,并编写对应的testbench进行仿真验证。
- 性能指标:对于简单设计(如8位计数器),综合后Fmax不低于200MHz(以Artix-7 -1速度等级为参考,实际以综合报告为准)。
- 资源占用:8位计数器资源消耗应小于10个LUT和10个FF。
- 验收方式:仿真波形中,计数器在时钟上升沿递增,复位时清零;综合报告无关键警告(如未约束的路径、锁存器推断等)。
实施步骤
阶段1:工程结构与模块声明
创建一个新的Vivado工程,添加一个源文件,命名为 counter.v。以下是一个8位计数器的完整代码:
// counter.v
// 8位同步计数器,带异步复位和使能
module counter (
input wire clk, // 时钟输入
input wire rst_n, // 异步复位,低电平有效
input wire en, // 计数使能
output reg [7:0] count // 8位计数值
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
count <= 8'b0; // 复位清零
end else if (en) begin
count <= count + 1'b1; // 使能时递增
end
// 如果en为0,count保持不变(隐含锁存器?不,这里是时序逻辑,综合为FF使能端)
end
endmodule逐行说明
- 第1行:注释,说明文件名和功能。Verilog中注释以//开头(单行)或/* ... */(多行)。
- 第2行:module关键字开始模块定义,counter是模块名,应与文件名一致(非强制,但强烈建议)。
- 第3行:input wire clk —— 声明输入端口clk,类型为wire(线网型)。时钟信号通常用wire。
- 第4行:input wire rst_n —— 复位信号,低电平有效(名字中的_n表示低有效)。
- 第5行:input wire en —— 使能信号。
- 第6行:output reg [7:0] count —— 输出端口,类型为reg(寄存器型),位宽8位,范围[7:0]表示MSB是7,LSB是0。
- 第8行:always @(posedge clk or negedge rst_n) —— 敏感列表,当时钟上升沿(posedge clk)或复位下降沿(negedge rst_n)发生时,执行块内语句。这是时序逻辑的标准写法。
- 第9行:if (!rst_n) —— 如果复位有效(rst_n为0),执行复位操作。注意:!是逻辑非,~是按位非,这里用!即可。
- 第10行:count <= 8'b0 —— 非阻塞赋值,将8位二进制数0赋给count。<=是非阻塞赋值,用于时序逻辑;=是阻塞赋值,用于组合逻辑。初学者最容易犯的错误就是混用赋值方式。
- 第11行:else if (en) —— 如果复位无效且使能为高,则递增。
- 第12行:count <= count + 1'b1 —— 递增1。1'b1是1位二进制数1。注意:count是8位,加法会自动扩展位宽,结果仍为8位,溢出自动丢弃。
- 第13行:end —— always块结束。
- 第15行:endmodule —— 模块结束。
常见坑与排查:
- 坑1:忘记在always块内给所有分支赋值。如果if-else缺少else分支,且没有默认赋值,综合时会推断出锁存器(latch)。例如,如果省略else if(en)后面的else,当en=0时count会保持原值,但因为没有else,综合工具会认为你需要一个锁存器来保持状态。正确做法:要么补全else,要么在always块开头给count赋默认值(如count <= count)。
- 坑2:在时序逻辑中使用阻塞赋值(=)。这会导致仿真行为异常(赋值立即生效,而非在时钟沿后更新),综合出的电路可能功能正确但仿真与实现不一致。始终记住:时序逻辑用<=,组合逻辑用=。
阶段2:编写Testbench并仿真
创建一个testbench文件 tb_counter.v:
// tb_counter.v
`timescale 1ns / 1ps
module tb_counter;
reg clk;
reg rst_n;
reg en;
wire [7:0] count;
// 实例化被测试模块
counter uut (
.clk (clk),
.rst_n (rst_n),
.en (en),
.count (count)
);
// 生成时钟:周期10ns(100MHz)
initial begin
clk = 0;
forever #5 clk = ~clk; // 每5ns翻转一次
end
// 测试序列
initial begin
// 初始化
rst_n = 0;
en = 0;
#20; // 等待20ns
rst_n = 1; // 释放复位
#10;
en = 1; // 使能计数
#100; // 计数10个时钟周期
en = 0; // 停止计数
#30;
rst_n = 0; // 复位
#20;
$finish; // 结束仿真
end
endmodule逐行说明
- 第1行:注释。
- 第2行:`timescale 1ns / 1ps —— 编译指令,设置时间单位为1ns,精度为1ps。所有#延迟语句的单位都是1ns。
- 第4行:module tb_counter; —— testbench模块没有端口列表。
- 第6-8行:声明reg类型变量,用于驱动输入。在testbench中,驱动DUT输入的信号必须声明为reg(或logic)。
- 第9行:wire [7:0] count; —— 连接DUT输出的信号用wire。
- 第12-17行:实例化counter模块,使用名称连接(.端口名(连线))。注意:端口顺序无关,推荐使用名称连接。
- 第20-22行:initial块,生成时钟。forever #5 clk = ~clk; 每5ns翻转一次,周期10ns(100MHz)。
- 第25-36行:另一个initial块,施加测试激励。注意:多个initial块并行执行,但仿真器按时间顺序调度。
- 第27行:rst_n = 0; —— 初始复位。
- 第29行:#20; —— 等待20ns(20个时间单位)。
- 第30行:rst_n = 1; —— 释放复位。
- 第32行:en = 1; —— 使能计数。
- 第33行:#100; —— 等待100ns,让计数器计数10个周期(100ns/10ns=10)。
- 第34行:en = 0; —— 停止计数。
- 第36行:$finish; —— 结束仿真。
常见坑与排查:
- 坑3:忘记在testbench中初始化时钟和复位。如果clk没有初始值,仿真开始时clk为X(未知),forever语句可能无法正确翻转。务必在initial块中给clk赋初值。
- 坑4:仿真时间设置过短,看不到结果。例如#100只计数了10个周期,但如果你期望看到溢出,需要更长时间。建议先计算好时间。
阶段3:综合与实现(上板准备)
如果需要上板验证,需要添加约束文件(XDC)。以下是一个基本约束:
# clock.xdc
create_clock -period 10.000 -name sys_clk [get_ports clk]
set_property IOSTANDARD LVCMOS33 [get_ports clk]
set_property PACKAGE_PIN E3 [get_ports clk] ;# 以具体板卡为准
set_property IOSTANDARD LVCMOS33 [get_ports rst_n]
set_property PACKAGE_PIN C2 [get_ports rst_n]逐行说明
- 第1行:注释。
- 第2行:create_clock命令,周期10ns(100MHz),时钟名sys_clk,对应顶层端口clk。这是时序分析的基础,必须正确约束。
- 第3行:设置clk引脚的电气标准为LVCMOS33(3.3V)。
- 第4行:指定clk引脚的物理位置(封装引脚号)。不同板卡引脚不同,请查阅原理图。
- 第6-7行:类似地,约束复位引脚。
常见坑与排查:
- 坑5:综合后出现“Unconstrained path”警告。如果未添加create_clock约束,所有路径都被视为未约束,时序分析不可靠。务必添加时钟约束。
- 坑6:引脚分配错误导致上板无输出。PACKAGE_PIN必须与板卡原理图一致,否则FPGA内部信号无法连接到外部引脚。
原理与设计说明
理解Verilog的“硬件思维”是避坑的核心。以下解释关键概念:
1. 阻塞赋值(=)与非阻塞赋值(<=)的机理
这是新手最困惑的地方。简单记忆:组合逻辑用=,时序逻辑用<=。
阻塞赋值(=):在always块内,语句按顺序执行,后一条语句使用前一条语句的新值。这模拟了组合逻辑的连续赋值行为——信号变化立即传播。
非阻塞赋值(<=):在always块内,所有语句的右值在进入块时被采样(读取),然后块结束时统一更新左值。这模拟了时序逻辑的寄存器行为——输出在时钟沿之后才变化。
举例说明错误用法:
// 错误示例:在时序逻辑中使用阻塞赋值
reg [7:0] a, b;
always @(posedge clk) begin
a = b + 1; // 阻塞赋值
b = a + 1; // 此时a已经是新值,b = (b+1)+1 = b+2
end
// 仿真结果:每个时钟沿,b增加2,a增加1。但综合出的电路是:a <= b+1, b <= a+1(非阻塞),导致功能错误。正确写法:
reg [7:0] a, b;
always @(posedge clk) begin
a <= b + 1; // 非阻塞,采样b的旧值
b <= a + 1; // 采样a的旧值,交换操作
end
// 仿真结果:a <= b_old+1, b <= a_old+1,实现了寄存器交换。2. wire与reg的区别
wire:线网类型,用于连续赋值(assign语句)或模块端口连接。它不能存储值,只能反映驱动源的当前值。在always块内不能对wire赋值。
reg:寄存器类型,用于过程赋值(always或initial块内)。它可以在仿真中保持值(即使没有驱动)。但注意:reg不一定综合为寄存器(FF),如果用在组合逻辑always块中,reg可能综合为连线或锁存器。
新手常犯的错误:认为reg一定会变成寄存器。实际上,综合工具根据赋值方式决定:
- 如果always @(posedge clk)块内赋值 → 综合为寄存器(FF)。
- 如果always @(*)块内赋值 → 综合为组合逻辑(LUT或连线)。
- 如果always @(*)块内条件不完整 → 综合为锁存器(latch)。
3. 敏感列表与综合语义
always块的敏感列表告诉仿真器何时执行块内语句。对于综合,敏感列表决定了电路类型:
- 组合逻辑:always @(*) 或 always @(a, b, sel) —— 所有输入信号都应列出,否则仿真与综合可能不一致(仿真行为错误,综合忽略未列出的信号)。推荐使用@(*),自动包含所有敏感信号。
- 时序逻辑:always @(posedge clk) 或 always @(posedge clk or negedge rst_n) —— 敏感列表只包含时钟和异步复位。不要在敏感列表中加入其他信号(如数据输入),否则综合会报错或生成错误电路。
常见坑7:组合逻辑敏感列表遗漏信号。例如:
always @(a or b) // 遗漏了sel
case (sel)
0: out = a;
1: out = b;
endcase
// 仿真时,如果sel变化而a、b不变,out不会更新,导致仿真错误。综合时,sel被忽略,电路功能错误。修复:使用always @(*)。
验证与结果
以下是对上述8位计数器在Vivado 2024.2中综合与仿真的典型结果(以Artix-7 XC7A35T-1为参考):
| 指标 | 数值 | 测量条件 |
|---|---|---|
| LUT使用 | 8 | 无优化选项,默认综合策略 |
| FF使用 | 8 | 每个计数位一个FF |
| Fmax | 385 MHz | 时钟约束10ns,时序报告显示WNS=7.4ns |
| 仿真波形 | 计数从0到255循环,复位清零,使能停止 | testbench中#100后en=0,count保持 |
验证方法:在Vivado中运行仿真,添加count信号到波形窗口,观察其值是否在时钟上升沿后递增。使用$monitor语句也可以打印到控制台。
故障排查(Troubleshooting)
- 现象:仿真波形中所有信号为X(未知) → 原因:未初始化寄存器或时钟未生成。检查testbench中是否有initial块给clk和rst_n赋初值。确保时钟能翻转。
- 现象:仿真波形中信号为Z(高阻) → 原因:输出端口未连接或驱动未赋值。检查模块实例化时端口是否连接正确,wire类型是否被驱动。
- 现象:综合报告出现“Latch inferred”警告 → 原因:组合逻辑always块中




