FPGA入门实战:Verilog实现边沿检测电路(附Testbench仿真)
一、 前言
在FPGA逻辑设计中,边沿检测是一个非常基础且极其重要的功能。无论是在按键消抖、串口通信起始位判断,还是在高速数据的时钟同步中,我们都需要精准地捕捉信号的跳变瞬间(上升沿、下降沿或双边沿)。
本文将通过一个简单的Verilog实例,详细讲解单时钟沿同步的边沿检测原理,并提供完整的Testbench代码进行仿真验证。
二、 设计思路
边沿检测的核心思想非常简单:打拍(Delay)。
由于FPGA是同步时序电路,我们通常在时钟的上升沿对信号进行采样。如果我们把当前时刻的信号 sig与上一时刻的信号 sig_dly进行比较,就能判断出信号是否发生了变化。
假设时钟为 clk:
1.打一拍:
在 clk的驱动下,将输入信号 sig寄存一拍,得到 sig_dly。
2.逻辑判断:
上升沿:上一拍是0,这一拍是1。即 rise_edge = ~sig_dly & sig。
下降沿:上一拍是1,这一拍是0。即 fall_edge = sig_dly & ~sig。
双边沿:只要两拍数据不同,就说明发生了跳变。即 dual_edge = sig_dly ^ sig。
三、 RTL代码实现
下面是边沿检测模块的Verilog代码。
- 边沿检测模块 (edge_test1.v)
c
module edge_test1(
input clk, // 时钟信号
input rst_n, // 复位信号 (低有效)
input sig, // 待检测信号
output wire rise_edge, // 上升沿检测信号 (脉冲)
output wire fall_edge, // 下降沿检测信号 (脉冲)
output wire dual_edge // 双边沿检测信号 (脉冲)
);
reg sig_dly; // 将待检测信号延迟一拍
// 时序逻辑:打拍
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
sig_dly <= 1'b0; // 复位时清零
end
else begin
sig_dly <= sig; // 将当前信号存入寄存器
end
end
// 组合逻辑:边沿判断
assign rise_edge = ~sig_dly & sig; // 上升沿:0 -> 1
assign fall_edge = sig_dly & ~sig; // 下降沿:1 -> 0
assign dual_edge = sig_dly ^ sig; // 双边沿:0<->1 变化
endmodule
代码解析:
sig_dly:这是关键寄存器,它保存了上一时钟周期 sig的值。
上升沿:如果 sig_dly是0,sig是1,说明信号从低变高,rise_edge输出一个时钟周期的高电平。
下降沿:如果 sig_dly是1,sig是0,说明信号从高变低,fall_edge输出一个时钟周期的高电平。
双边沿:异或运算(XOR)的特性是"相同为0,不同为1"。只要 sig和 sig_dly不一样,就代表发生了跳变。
RTL视图如下:

四、 Testbench 仿真
为了验证设计的正确性,我们需要编写Testbench。
- 仿真激励文件 (tb_edge_test1.v)
c
`timescale 1ns/1ps // 时间单位/精度
module tb_edge_test1();
reg clk;
reg rst_n;
reg sig;
wire rise_edge;
wire fall_edge;
wire dual_edge;
parameter T = 20; // 时钟周期 20ns (50MHz)
// 生成时钟
always #(T/2) clk = ~clk;
// 激励信号
initial begin
clk = 1'b0;
rst_n = 1'b0;
sig = 1'b0;
#15
rst_n = 1'b1; // 释放复位
#10
sig = 1'b0; // 保持低电平
#10
sig = 1'b1; // 产生一个上升沿
#30
sig = 1'b0; // 产生一个下降沿
#10
sig = 1'b1; // 上升沿
#20
sig = 1'b0; // 下降沿
#100 $stop; // 停止仿真
end
// 实例化待测模块 (DUT)
edge_test1 u_edge_test1(
.clk (clk),
.rst_n (rst_n),
.sig (sig),
.rise_edge (rise_edge),
.fall_edge (fall_edge),
.dual_edge (dual_edge)
);
endmodule
五、 仿真波形分析

结果验证:
1.当 sig从 0 跳变到 1 时,rise_edge和 dual_edge同时拉高了一个时钟周期。
2.当 sig从 1 跳变到 0 时,fall_edge和 dual_edge同时拉高了一个时钟周期。
六、注意事项
1.信号同步问题:
本例中的 sig假设是同步信号(由同一时钟域产生)。如果 sig是外部输入(如按键、异步数据),必须先进行两级触发器同步(打两拍),否则可能会产生亚稳态,导致系统崩溃。
2.脉冲宽度:
本设计输出的脉冲宽度等于一个时钟周期。如果后续逻辑需要更宽的脉冲,或者需要跨时钟域处理,可能需要额外的计数器或握手机制。
3.资源消耗:
这种写法综合出来通常只需要几个LUT(查找表),资源消耗极低,适合大规模使用。
七、补充知识:亚稳态
在FPGA和数字电路设计中,"亚稳态"(Metastability) 是一个绕不开的核心痛点。为了让你彻底弄懂它,我们抛开枯燥的学术定义,从它的本质来掰扯清楚。
你可以把亚稳态简单理解为:信号在"0"和"1"之间"卡住"了。
为了让你知其然也知其所以然,我们从以下几个维度来彻底解剖它:
- 直观比喻:站在电梯里的尴尬时刻
想象一个数字系统里的高电平(1)是站在电梯地面,低电平(0)是蹲在电梯底部。
正常情况下,信号要么稳稳站在地面(1),要么稳稳蹲在底部(0)。
但是,如果外部信号(比如按键输入)恰好在电梯关门的一瞬间(时钟上升沿)冲进电梯,会发生什么?
它既没完全蹲下去,也没完全站起来,而是卡在了半空中(中间电压值,比如 1.2V)。这就是亚稳态。
在这个状态下,系统根本不知道它到底是 0 还是 1,整个系统就会陷入逻辑混乱。 - 专业视角:违背了"建立时间"与"保持时间"
在数字电路中,所有的触发器(Flip-Flop,也就是代码里的 reg)都像是一个严格的安检口。
为了保证数据能被稳定采样,输入信号必须满足两个硬性规定:
建立时间 (Setup Time):在时钟上升沿到来之前,数据必须提前稳定多久。
保持时间 (Hold Time):在时钟上升沿到来之后,数据必须继续保持稳定多久。
亚稳态产生的根本原因就是:输入信号的变化(跳变)刚好发生在触发器的建立时间和保持时间内。
此时,触发器"懵"了,它无法判断这个信号究竟是上一个值还是下一个值,于是输出端就会产生一个介于 0 和 1 之间的非法电压,也就是进入了亚稳态。 - 亚稳态的危害:一颗老鼠屎坏了一锅粥
你可能会想:"卡住就卡住呗,等下个时钟周期不就好了?"
没那么简单,亚稳态的危害在于它的传染性:
逻辑误判:当前级电路输出一个不确定的中间电压,后级电路可能把它当成 0,也可能当成 1。这会导致整个系统的逻辑走向完全错误。
毛刺传播:亚稳态不仅是一个不确定的电平,它还可能在一定时间内发生振荡(高低跳动)。这个"抖动"一旦传递给后面的其他寄存器,就会导致错误的信号在整个芯片里像瘟疫一样扩散,最终引发系统死机或跑飞。 - 怎么消灭亚稳态?------"打两拍"绝招
既然亚稳态这么可怕,那怎么防备呢?在FPGA中,对付异步信号(比如你从外部引脚直接输入的 sig),业界公认的黄金法则就是:同步器(Synchronizer),俗称"打两拍"。
具体怎么做?
不要用外部信号直接去当条件判断,而是让它连续经过两个级联的寄存器,这两个寄存器都用系统时钟 clk驱动:
c
reg sig_dly1, sig_dly2;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sig_dly1 <= 1'b0;
sig_dly2 <= 1'b0;
end else begin
sig_dly1 <= sig; // 第一拍:大概率解除亚稳态
sig_dly2 <= sig_dly1; // 第二拍:100%稳定输出
end
end
// 后续逻辑全部使用 sig_dly2 进行判断
原理是什么?
第一拍 (sig_dly1):把疯狂跳动的异步信号强行抓进系统时钟域。这一步有极小概率抓到亚稳态,但没关系。
第二拍 (sig_dly2):因为一个时钟周期的时间远大于触发器退出亚稳态的时间,所以在第二个时钟上升沿到来时,sig_dly1早就已经稳定下来了。这样,sig_dly2输出的信号就绝对是干净、稳定、安全的!
(注:这也是为什么我在上文代码里强调,如果 sig是外部按键或异步数据,必须先打两拍同步后再做边沿检测的原因。)
希望这篇博文对你有帮助!如果有任何问题,欢迎在评论区留言讨论。😊