FPGA入门
文章目录
- FPGA入门
- 前言
- [一、核心原理:从 D 触发器到时序计数器](#一、核心原理:从 D 触发器到时序计数器)
-
- [1.1 时序逻辑的基石:D 触发器](#1.1 时序逻辑的基石:D 触发器)
- [1.2 时序逻辑计数器:D 触发器的扩展应用](#1.2 时序逻辑计数器:D 触发器的扩展应用)
- [1.3 从计数器到 LED 闪烁:逻辑闭环设计](#1.3 从计数器到 LED 闪烁:逻辑闭环设计)
- [二、LED 闪烁项目代码解析](#二、LED 闪烁项目代码解析)
-
- 2.1顶层模块led_twinkle.v
- [2.2 仿真测试文件led_twinkle_tb.v](#2.2 仿真测试文件led_twinkle_tb.v)
- 三、仿真波形调试:从误差到精准的全过程
- 总结
前言
哈喽大家好!在前几章我们学习了组合逻辑电路,这一章我们将正式进入时序逻辑电路的世界。时序逻辑是 FPGA 开发的核心,而计数器则是时序逻辑中最基础也最重要的单元之一。今天我们就从 D 触发器讲起,一步步实现一个基于计数器的 LED 闪烁项目,并通过仿真波形调试代码,彻底搞懂时序逻辑的工作原理。
一、核心原理:从 D 触发器到时序计数器
1.1 时序逻辑的基石:D 触发器
时序逻辑电路的核心是触发器,而 D 触发器 D Flip-Flop, DFF是最常用的触发器类型,它的功能是在时钟的控制下,将输入 D 的值传递到输出 Q。

- 输入D:数据输入端
- 输入CK:时钟输入端(三角形表示边沿触发,此处为上升沿触发)
- 输出Q:数据输出端
D 触发器的工作规则非常简单: - 只有在时钟上升沿到来时,输出Q才会更新为此时D的值;
- 其他任何时候,不管D如何变化,Q都保持原来的值不变
对应的时序波形图也能清晰体现这一特性:在时钟上升沿,Q的值会同步更新为D在上升沿时刻的值,其余时间保持不变。这就是时序逻辑 同步更新、状态保持 的核心思想

1.2 时序逻辑计数器:D 触发器的扩展应用
计数器本质上就是一个带加法器的 D 触发器,它的功能是每来一个时钟脉冲,计数值加 1,当计数值达到设定的最大值时,自动复位并触发一次翻转。

- 计数器由加法器(+1)和 D 触发器组成;
- D 触发器的输出cnt[3:0]反馈到加法器输入端,加法器的输出再接到 D 触发器的D端;
- 每来一个时钟上升沿,D 触发器就会把加法器的结果(当前值 + 1)锁存到cnt中,实现计数功能。
我们项目中的 25 位计数器就是这个结构的扩展:用 25 个 D 触发器组成 25 位寄存器,配合加法器实现0~24999999的循环计数。
其实就相当于c语言中的
c
// 全局变量a(对应FPGA的寄存器counter)
int a = 0;
void time(){
a++; // 每个周期计数+1
if(a >= 10){ // 计数到10就触发动作
a=0; // 计数器清零
led=!led; // LED翻转
}
}
1.3 从计数器到 LED 闪烁:逻辑闭环设计
LED 闪烁的核心逻辑是:让 LED 以固定频率翻转电平,比如 1 秒亮 1 秒灭,周期 2 秒。要实现这个功能,我们需要用计数器来 "计时":
- 开发板时钟通常为 50MHz,时钟周期为 20ns;
- 要实现 500ms 翻转一次 LED,需要计数的次数为:500ms / 20ns = 25,000,000次;
- 当计数器计数到24999999(即25,000,000-1)时,让 LED 翻转一次,同时计数器复位,重新开始计数。
这样就形成了完整的逻辑闭环:计数器循环计数,每 500ms 触发一次 LED 翻转,实现 1 秒周期的闪烁效果。
二、LED 闪烁项目代码解析
项目包含两个文件:顶层模块led_twinkle.v和仿真测试文件led_twinkle_tb.v。
2.1顶层模块led_twinkle.v
我先给大家看实测结果:
- 代码写 counter == 25_000_00 → 仿真波形周期:50.000020ms(多了 20ns,误差 1 个时钟周期)
- 代码写 counter == 25_000_00-1 → 仿真波形周期:50.000000ms(精准无误差)
根本原因:计数器从 0 开始计数!
- 计数器初始值 = 0
- 计 1 个数:0 → 1
- 计 25000000 个数:需要从 0 数到 24999999(也就是25_000_00-1)
- 如果数到25_000_00,就多计了 1 个时钟周期(20ns),导致计时不准!
c
module led_twinkle(
input Clk, // 系统时钟,50MHz
input Reset_n, // 复位信号,低电平有效
output reg Led // LED控制信号
);
// 定义25位计数器,最大计数到2^25-1=33554431,足够容纳25000000
reg [24:0] counter;
// 计数器逻辑:0~24999999循环计数
always @(posedge Clk or negedge Reset_n) begin
if(!Reset_n) begin
counter <= 25'd0; // 复位时计数器清零
end
else if(counter == 25'd25_000_00 - 1) begin
counter <= 25'd0; // 计数到最大值时复位
end
else begin
counter <= counter + 1'd1; // 每个时钟周期加1
end
end
// LED翻转逻辑:计数到最大值时翻转LED电平
always @(posedge Clk or negedge Reset_n) begin
if(!Reset_n) begin
Led <= 1'b0; // 复位时LED熄灭
end
else if(counter == 25'd25_000_00 - 1) begin
Led <= !Led; // 计数到最大值时翻转LED
end
// 时序逻辑中,条件不满足时会自动保持原值,可省略else分支
end
endmodule
2.2 仿真测试文件led_twinkle_tb.v
c
`timescale 1ns / 1ns // 定义仿真时间单位和精度
module led_twinkle_tb();
reg Clk;
reg Reset_n;
wire Led;
// 实例化顶层模块
led_twinkle led_twinkle(
.Clk(Clk),
.Reset_n(Reset_n),
.Led(Led)
);
// 生成50MHz时钟,周期20ns
initial Clk = 1'b1;
always #10 Clk = !Clk;
// 复位与仿真激励
initial begin
Reset_n = 1'b0; // 初始复位
#201; // 保持复位201ns,确保系统稳定
Reset_n = 1'b1; // 释放复位
#2000000000; // 仿真2秒,足够看到两次LED翻转
$stop; // 停止仿真
end
endmodule
关键代码解析:
- 时钟生成:always #10 Clk = !Clk生成周期 20ns 的时钟,对应 50MHz 频率,与开发板时钟一致;
- 复位激励:先保持复位 201ns,避免复位信号与时钟沿冲突,确保系统稳定后再释放复位;
- 仿真时长:#2000000000即仿真 2 秒,足够观察 LED 两次翻转(亮→灭→亮),验证闪烁周期是否为 1 秒。
三、仿真波形调试:从误差到精准的全过程
我们通过仿真波形的实测数据,直观验证「减 1」的必要性。

错误代码:counter == 25_000_00(未减 1)
现象:LED 翻转周期 = 50.000020ms
原因:计数器从 0 数到 25000000,多计了 1 个时钟周期(20ns),计时偏移
结论:不满足精准闪烁需求
修正代码:counter == 25_000_00-1(减 1)
现象:LED 翻转周期 = 50.000000ms
原因:计数器从 0 数到 24999999,刚好 25000000 个时钟周期,无任何误差
结论:这就是通过波形调整代码的标准答案!


总结
通过这一章的学习,我们从 D 触发器的基础原理出发,理解了时序逻辑计数器的工作机制,并实现了 LED 闪烁项目。这个项目虽简单,但包含了时序逻辑开发的核心要点:
- 理解 D 触发器的边沿触发和状态保持特性;
- 掌握计数器的设计方法:位宽计算、最大值设置、复位逻辑;
- 学会通过仿真波形验证时序逻辑,排查问题;
- 养成良好的代码规范,如参数化、清晰的分支结构。