Verilog边沿检测与脉冲展宽电路设计实战详解与常见问题排查

引言
在数字电路设计中,边沿检测(Edge Detection)和脉冲展宽(Pulse Stretching)是两个非常基础且实用的技术。边沿检测用于识别信号的上升沿或下降沿,常用于触发事件、计数器控制等场景;脉冲展宽则用于将短脉冲扩展为更长的脉冲,以确保后续电路能够可靠地捕获该信号。本文将详细讲解如何使用Verilog实现这两种电路,并通过实际代码示例、波形分析以及常见问题排查,帮助读者深入理解并掌握这些技术。
一、边沿检测电路设计
1.1 基本原理
边沿检测的核心思想是通过比较当前时钟周期的信号状态与上一个时钟周期的信号状态,来判断是否发生了上升沿或下降沿。具体来说:
上升沿检测
:当当前信号为高电平且前一时刻信号为低电平时,表示检测到上升沿。
下降沿检测
:当当前信号为低电平且前一时刻信号为高电平时,表示检测到下降沿。
为了实现这一功能,我们需要一个寄存器来存储前一个时钟周期的信号值。
1.2 Verilog代码实现
下面是一个典型的边沿检测模块的Verilog代码:
module edge_detect (
input wire clk, // 系统时钟
input wire rst_n, // 异步复位,低电平有效
input wire signal_in, // 输入信号
output reg pos_edge, // 上升沿检测输出
output reg neg_edge // 下降沿检测输出
);

// 内部寄存器,用于存储前一个时钟周期的信号值
reg signal_prev;

always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// 复位时清空所有寄存器
signal_prev <= 1'b0; pos_edge <= 1'b0; neg_edge <= 1'b0; end else begin // 更新前一时刻的信号值 signal_prev <= signal_in; // 上升沿检测:当前信号为1且前一刻为0 pos_edge <= (signal_in & ~signal_prev); // 下降沿检测:当前信号为0且前一刻为1 neg_edge <= (~signal_in & signal_prev); end end endmodule 代码说明: signal_prev :这是一个寄存器,用于保存上一个时钟周期的 signal_in 值。 pos_edge 和 neg_edge :这两个输出寄存器分别在检测到上升沿和下降沿的当周期拉高,仅持续一个时钟周期。 复位信号 rst_n 为低电平时,所有寄存器被清零。 1.3 仿真波形分析 假设输入信号 signal_in 如下变化: 0 → 1(上升沿) 1 → 0(下降沿) 0 → 0 0 → 1(再次上升沿) 对应的波形如下(以时钟上升沿为基准): 时间点 clk signal_in signal_prev pos_edge neg_edge T0 ↑ 0 0 0 0 T1 ↑ 1 0 1 0 T2 ↑ 0 1 0 1 T3 ↑ 0 0 0 0 T4 ↑ 1 0 1 0 可以看到, pos_edge 和 neg_edge 仅在边沿发生的当周期输出一个时钟周期的高脉冲。 1.4 改进:消除亚稳态(可选) 如果输入信号 signal_in 来自异步域(例如按键或外部中断),直接采样可能会导致亚稳态。此时可以采用两级触发器同步法: reg signal_sync1, signal_sync2; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin signal_sync1 <= 0; signal_sync2 <= 0; end else begin signal_sync1 <= signal_in; // 第一级同步 signal_sync2 <= signal_sync1; // 第二级同步 end end // 使用 signal_sync2 代替原始 signal_in 进行边沿检测 这样可以有效降低亚稳态传播的风险。 二、脉冲展宽电路设计 2.1 基本原理 脉冲展宽的目的是将一个短脉冲(可能只有一个时钟周期)扩展为更长的脉冲,以便后续电路能够可靠地捕获。常见的应用场景包括: 将按键的短按信号展宽,便于LED显示或状态机处理。 在跨时钟域传输中,确保控制信号有足够的保持时间。 实现方式通常有两种: 计数器法 :检测到输入脉冲后启动计数器,计数达到设定值后停止。 状态机法 :使用有限状态机(FSM)控制输出脉冲的宽度。 2.2 Verilog代码实现(计数器法) 以下是一个可配置脉冲宽度的展宽电路: module pulse_stretch #( parameter WIDTH = 8 // 脉冲展宽的时钟周期数,默认为8 )( input wire clk, // 系统时钟 input wire rst_n, // 异步复位,低电平有效 input wire pulse_in, // 输入脉冲(单周期或短脉冲) output reg pulse_out // 展宽后的输出脉冲 ); reg [7:0] counter; // 计数器,支持最大256周期 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin counter <= 8'd0; pulse_out <= 1'b0; end else begin if (pulse_in) begin // 检测到输入脉冲,启动计数器 counter <= WIDTH; pulse_out <= 1'b1; end else if (counter != 8'd0) begin // 计数器递减,输出保持高电平 counter <= counter - 1'b1; pulse_out <= (counter != 8'd1); // 最后一个周期拉低 end else begin pulse_out <= 1'b0; end end end endmodule 代码说明: WIDTH :参数化设计,允许用户自定义展宽周期数。 counter :计数器从 WIDTH 递减到0,期间 pulse_out 保持高电平。 如果输入脉冲 pulse_in 为高电平,立即输出高电平并重置计数器;否则,计数器递减,直到为0时输出低电平。 2.3 仿真波形分析 假设 WIDTH = 3 ,输入脉冲 pulse_in 在时钟周期2为高电平: 时间点 clk pulse_in counter pulse_out T0 ↑ 0 0 0 T1 ↑ 0 0 0 T2 ↑ 1 3 1 T3 ↑ 0 2 1 T4 ↑ 0 1 1 T5 ↑ 0 0 0 输出脉冲从T2持续到T4,共3个周期,实现了展宽。 2.4 状态机实现(可选) 对于更复杂的展宽逻辑(例如支持多次脉冲叠加),可以使用状态机: module pulse_stretch_fsm ( input wire clk, input wire rst_n, input wire pulse_in, output reg pulse_out ); localparam IDLE = 2'b00; localparam STRETCH = 2'b01; localparam DONE = 2'b10; reg [1:0] state, next_state; reg [7:0] counter; always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= IDLE; else state <= next_state; end always @(*) begin case (state) IDLE: next_state = pulse_in ? STRETCH : IDLE; STRETCH: next_state = (counter == 8'd1) ? DONE : STRETCH; DONE: next_state = IDLE; default: next_state = IDLE; endcase end always @(posedge clk or negedge rst_n) begin if (!rst_n) begin counter <= 8'd0; pulse_out <= 1'b0; end else begin case (state) IDLE: begin if (pulse_in) begin counter <= 8'd3; // 展宽3周期 pulse_out <= 1'b1; end end STRETCH: begin counter <= counter - 1'b1; if (counter == 8'd1) pulse_out <= 1'b0; end DONE: begin pulse_out <= 1'b0; end endcase end end endmodule 状态机版本更灵活,但代码量稍大,适合复杂场景。 三、综合应用:边沿检测 + 脉冲展宽 3.1 实际需求场景 假设我们有一个按键输入,需要检测按键的按下事件(上升沿),并将其展宽为100ms的脉冲(假设时钟频率为1kHz,即100个周期)。 3.2 整合代码示例 module key_processor ( input wire clk, // 1kHz时钟 input wire rst_n, input wire key_in, // 按键输入(低电平有效) output wire led_out // LED输出(展宽后控制) ); wire key_pos_edge; // 边沿检测模块实例化 edge_detect u_edge ( .clk(clk), .rst_n(rst_n), .signal_in(~key_in), // 按键低有效,取反后检测上升沿 .pos_edge(key_pos_edge), .neg_edge() ); // 脉冲展宽模块实例化 pulse_stretch #( .WIDTH(100) // 100ms @ 1kHz ) u_stretch ( .clk(clk), .rst_n(rst_n), .pulse_in(key_pos_edge), .pulse_out(led_out) ); endmodule 说明: 按键 key_in 低电平有效,因此取反后检测上升沿。 展宽宽度为100个时钟周期,对应100ms。 四、常见问题排查 4.1 边沿检测问题 问题1:边沿检测输出持续多个周期 原因 :可能是 signal_prev 未正确更新,或者输入信号存在毛刺。 排查方法 : 检查 signal_prev 是否在 always 块中正确赋值。 使用仿真工具查看波形,确认输入信号是否干净。 如果输入信号有毛刺,可以添加滤波逻辑(例如计数器消抖)。 问题2:跨时钟域导致亚稳态 原因 :输入信号与采样时钟异步。 排查方法 : 添加两级同步触发器(如前所述)。 确保同步后的信号用于边沿检测。 4.2 脉冲展宽问题 问题1:输出脉冲宽度不准确 原因 :计数器初始值或递减逻辑错误。 排查方法 : 检查 WIDTH 参数是否正确。 在仿真中观察 counter 的变化,确认递减逻辑。 注意计数器是否在 pulse_in 为高电平时被重置。 问题2:输入脉冲过短导致展宽失败 原因 :输入脉冲宽度小于一个时钟周期。 排查方法 : 确保输入脉冲至少持续一个时钟周期。 如果输入脉冲确实短于一个周期,可以先进行同步处理或使用更快的时钟。 问题3:多次输入脉冲导致输出异常 原因 :前一个展宽未完成时,新的输入脉冲到来。 排查方法 : 在代码中添加“忙”信号(busy flag),拒绝新的输入直到展宽完成。 或者使用状态机,支持脉冲叠加或忽略后续脉冲。 4.3 其他常见问题 问题:复位信号处理不当 现象 :复位后输出异常或寄存器未清零。 排查方法 : 确认复位信号极性(高有效或低有效)。 检查 always 块的敏感列表是否包含复位信号。 异步复位时,注意复位信号的释放是否同步于时钟。 问题:时序违例(Setup/Hold Violation) 现象 :综合后时序报告报错。 排查方法 : 检查组合逻辑路径是否过长,必要时插入流水线。 确保边沿检测和展宽逻辑在同一个时钟域内。 使用同步复位而非异步复位以改善时序。 五、总结 本文详细介绍了Verilog边沿检测和脉冲展宽电路的设计原理、代码实现、波形分析以及常见问题排查。通过这些内容,读者应该能够: 独立编写边沿检测模块,并处理亚稳态问题。 设计可配置的脉冲展宽电路,理解计数器和状态机两种实现方式。 将两者结合,解决实际工程中的按键处理等问题。 识别并解决设计中的常见问题。 在实际工程中,这些模块虽然简单,但却是构建复杂系统(如状态机、中断控制器、通信协议解析等)的基础。建议读者在FPGA开发板上实际验证这些代码,以加深理解。