1. Verilog信号展宽技术解析
在数字电路设计中,信号展宽(Pulse Stretching)是一个常见但容易被忽视的关键技术。我第一次意识到它的重要性是在设计一个异步串口接收模块时,当时外部输入的1-bit信号脉冲宽度只有10ns,而我的FPGA系统时钟周期是20ns,直接采样时出现了严重的信号丢失问题。这就是信号展宽技术大显身手的典型场景。
Verilog信号展宽的核心目标是将窄脉冲信号转换为足够宽度的信号,确保其能够被目标时钟域可靠捕获。这项技术在以下场景中尤为重要:
- 异步信号同步(如按键消抖、中断信号处理)
- 跨时钟域通信(快时钟域向慢时钟域传递信号)
- 外设接口设计(处理传感器输出的短脉冲)
- 复位电路设计(确保复位信号达到最小脉宽要求)
2. 信号展宽的实现原理
2.1 基本工作原理
信号展宽的本质是通过状态机或计数器来延长输入信号的持续时间。当检测到输入信号的边沿(通常是上升沿)时,启动一个计数器,在计数器达到预设值之前保持输出信号有效。这种实现方式可以确保输出脉冲的宽度精确可控。
以常见的上升沿触发展宽为例,其工作时序如下:
- 输入信号出现上升沿(脉冲开始)
- 立即拉高输出信号
- 启动内部计数器
- 计数器达到预设值时拉低输出信号
- 等待下一个输入脉冲
2.2 关键参数计算
设计信号展宽电路时需要考虑两个关键参数:
-
最小输入脉冲间隔(T_min):
T_min = T_stretch + T_margin
其中T_stretch是展宽后的脉冲宽度,T_margin是为防止信号重叠保留的余量 -
展宽时间(T_stretch):
T_stretch = N * T_clk
N为计数器终值,T_clk为系统时钟周期
重要提示:展宽时间必须大于目标时钟域的2个时钟周期,才能确保可靠采样。这是数字电路设计中著名的"两级同步器"原则的应用。
3. Verilog实现方案对比
3.1 基本计数器实现
这是最直接也最可靠的实现方式,适合大多数应用场景:
verilog复制module pulse_stretch #(
parameter WIDTH = 16,
parameter STRETCH_CYCLES = 100
)(
input clk,
input rst_n,
input signal_in,
output reg signal_out
);
reg [WIDTH-1:0] counter;
reg signal_prev;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
signal_prev <= 0;
signal_out <= 0;
counter <= 0;
end else begin
signal_prev <= signal_in;
// Detect rising edge
if (!signal_prev && signal_in) begin
signal_out <= 1'b1;
counter <= STRETCH_CYCLES;
end else if (counter > 0) begin
counter <= counter - 1;
end else begin
signal_out <= 1'b0;
end
end
end
endmodule
实现特点:
- 精确控制展宽时间(通过STRETCH_CYCLES参数)
- 资源占用少(只需一个计数器和少量逻辑)
- 支持同步复位
- 展宽期间忽略输入信号变化(防抖动)
3.2 状态机实现
对于需要更复杂控制逻辑的场景,可以采用状态机实现:
verilog复制module pulse_stretch_fsm #(
parameter STRETCH_CYCLES = 100
)(
input clk,
input rst_n,
input signal_in,
output reg signal_out
);
typedef enum {IDLE, STRETCH} state_t;
state_t current_state;
reg [15:0] counter;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
current_state <= IDLE;
signal_out <= 0;
counter <= 0;
end else begin
case (current_state)
IDLE: begin
if (signal_in) begin
signal_out <= 1'b1;
counter <= STRETCH_CYCLES;
current_state <= STRETCH;
end
end
STRETCH: begin
if (counter > 0) begin
counter <= counter - 1;
end else begin
signal_out <= 1'b0;
current_state <= IDLE;
end
end
endcase
end
end
endmodule
状态机实现的优势:
- 更清晰的逻辑流程
- 易于扩展额外状态(如添加消抖状态)
- 调试时状态可见性好
4. 高级应用技巧
4.1 动态展宽时间控制
在某些应用中,固定的展宽时间可能不够灵活。我们可以通过增加配置接口来实现动态调整:
verilog复制module dynamic_pulse_stretch #(
parameter WIDTH = 16
)(
input clk,
input rst_n,
input signal_in,
input [WIDTH-1:0] stretch_time,
output reg signal_out
);
reg [WIDTH-1:0] counter;
reg signal_prev;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
signal_prev <= 0;
signal_out <= 0;
counter <= 0;
end else begin
signal_prev <= signal_in;
if (!signal_prev && signal_in) begin
signal_out <= 1'b1;
counter <= stretch_time;
end else if (counter > 0) begin
counter <= counter - 1;
end else begin
signal_out <= 1'b0;
end
end
end
endmodule
应用场景:
- 可配置的按键消抖时间
- 自适应不同速率的通信接口
- 系统调试时动态调整参数
4.2 多级展宽流水线
对于特别关键的信号,可以采用多级展宽技术提高可靠性:
verilog复制module multi_stage_stretch #(
parameter STAGES = 3,
parameter STRETCH_CYCLES = 50
)(
input clk,
input rst_n,
input signal_in,
output signal_out
);
wire [STAGES:0] stage_out;
assign stage_out[0] = signal_in;
assign signal_out = stage_out[STAGES];
genvar i;
generate
for (i=0; i<STAGES; i=i+1) begin : stretch_stage
pulse_stretch #(
.STRETCH_CYCLES(STRETCH_CYCLES)
) stage (
.clk(clk),
.rst_n(rst_n),
.signal_in(stage_out[i]),
.signal_out(stage_out[i+1])
);
end
endgenerate
endmodule
技术特点:
- 每级展宽模块可以设置相同或不同的展宽时间
- 最终输出信号的宽度为各级展宽时间之和
- 特别适合对可靠性要求极高的应用(如安全关键系统)
5. 实际应用中的问题与解决方案
5.1 输入信号抖动问题
在实际硬件中,机械开关或传感器输出的信号往往存在抖动。简单的展宽电路可能会放大这个问题。
解决方案:
- 在展宽模块前添加消抖电路
- 采用"检测-确认"机制:只有持续一定时间的信号才被认为是有效输入
- 增加滤波计数器,只有连续多次采样为高才触发展宽
改进后的消抖展宽电路示例:
verilog复制module debounce_stretch #(
parameter DEBOUNCE_CYCLES = 20,
parameter STRETCH_CYCLES = 100
)(
input clk,
input rst_n,
input signal_in,
output reg signal_out
);
reg [15:0] stretch_counter;
reg [15:0] debounce_counter;
reg signal_stable;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
debounce_counter <= 0;
stretch_counter <= 0;
signal_stable <= 0;
signal_out <= 0;
end else begin
// 消抖逻辑
if (signal_in && !signal_stable) begin
if (debounce_counter < DEBOUNCE_CYCLES) begin
debounce_counter <= debounce_counter + 1;
end else begin
signal_stable <= 1'b1;
debounce_counter <= 0;
end
end else if (!signal_in && signal_stable) begin
if (debounce_counter < DEBOUNCE_CYCLES) begin
debounce_counter <= debounce_counter + 1;
end else begin
signal_stable <= 1'b0;
debounce_counter <= 0;
end
end
// 展宽逻辑
if (signal_stable && !signal_out) begin
signal_out <= 1'b1;
stretch_counter <= STRETCH_CYCLES;
end else if (stretch_counter > 0) begin
stretch_counter <= stretch_counter - 1;
end else begin
signal_out <= 1'b0;
end
end
end
endmodule
5.2 高频信号处理
当输入信号频率接近系统时钟频率时,传统展宽电路可能无法正常工作。
解决方案:
- 使用更高速的时钟域处理高频信号
- 采用双边沿检测(在上升沿和下降沿都启动展宽)
- 添加输入信号频率检测电路,自动调整展宽参数
双边沿检测实现示例:
verilog复制module dual_edge_stretch #(
parameter STRETCH_CYCLES = 50
)(
input clk,
input rst_n,
input signal_in,
output reg signal_out
);
reg signal_prev;
reg [15:0] counter;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
signal_prev <= 0;
signal_out <= 0;
counter <= 0;
end else begin
signal_prev <= signal_in;
// 检测任何边沿变化
if (signal_prev != signal_in) begin
signal_out <= 1'b1;
counter <= STRETCH_CYCLES;
end else if (counter > 0) begin
counter <= counter - 1;
end else begin
signal_out <= 1'b0;
end
end
end
endmodule
5.3 资源优化技巧
在FPGA实现中,当需要处理大量并行信号的展宽时,资源占用可能成为问题。
优化方案:
- 共享计数器:多个展宽电路共享一个全局计数器
- 时分复用:使用更高频率的时钟分时处理多个信号
- 使用LUT实现小型展宽电路(适用于短时间展宽)
共享计数器实现示例:
verilog复制module shared_counter_stretch #(
parameter CHANNELS = 8,
parameter STRETCH_CYCLES = 100
)(
input clk,
input rst_n,
input [CHANNELS-1:0] signals_in,
output [CHANNELS-1:0] signals_out
);
reg [15:0] global_counter;
reg [CHANNELS-1:0] trigger_flags;
reg [CHANNELS-1:0] signals_prev;
assign signals_out = (global_counter > 0) ? trigger_flags : 0;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
global_counter <= 0;
trigger_flags <= 0;
signals_prev <= 0;
end else begin
signals_prev <= signals_in;
// 检测各通道上升沿
for (integer i=0; i<CHANNELS; i=i+1) begin
if (!signals_prev[i] && signals_in[i]) begin
trigger_flags[i] <= 1'b1;
end
end
// 全局计数器控制
if (|trigger_flags && global_counter == 0) begin
global_counter <= STRETCH_CYCLES;
end else if (global_counter > 0) begin
global_counter <= global_counter - 1;
if (global_counter == 1) begin
trigger_flags <= 0;
end
end
end
end
endmodule
6. 验证与测试方法
6.1 仿真测试要点
完善的测试平台应该覆盖以下场景:
- 最小脉冲间隔测试
- 连续快速脉冲测试
- 随机间隔脉冲测试
- 复位功能测试
- 参数边界测试
典型的测试平台结构:
verilog复制module pulse_stretch_tb;
reg clk = 0;
reg rst_n = 0;
reg signal_in = 0;
wire signal_out;
// 实例化被测设计
pulse_stretch #(
.STRETCH_CYCLES(100)
) dut (
.clk(clk),
.rst_n(rst_n),
.signal_in(signal_in),
.signal_out(signal_out)
);
// 时钟生成
always #5 clk = ~clk;
initial begin
// 复位释放
#100 rst_n = 1;
// 测试用例1:单脉冲
signal_in = 1;
#10 signal_in = 0;
#2000;
// 测试用例2:连续脉冲
repeat (5) begin
signal_in = 1;
#10 signal_in = 0;
#150;
end
// 测试用例3:随机间隔脉冲
repeat (10) begin
signal_in = 1;
#10 signal_in = 0;
#($urandom_range(50,500));
end
$finish;
end
initial begin
$dumpfile("wave.vcd");
$dumpvars(0, pulse_stretch_tb);
end
endmodule
6.2 硬件测试技巧
在实际硬件验证时,建议采用以下方法:
- 使用逻辑分析仪捕获输入输出信号
- 逐步缩短输入脉冲宽度,观察展宽效果
- 测试极限情况(如连续快速脉冲)
- 测量功耗变化(特别是多通道实现时)
调试提示:在FPGA中嵌入ILA(集成逻辑分析仪)可以实时观察信号展宽过程,比外部逻辑分析仪更方便。
7. 性能评估与优化
7.1 时序分析关键点
信号展宽电路的主要时序约束包括:
- 输入信号到第一个触发器的建立/保持时间
- 计数器逻辑的传播延迟
- 输出信号的时钟到输出时间
典型的时序约束示例(Xilinx FPGA):
tcl复制# 输入时钟约束
create_clock -name clk -period 10 [get_ports clk]
# 输入信号约束
set_input_delay -clock clk -max 3 [get_ports signal_in]
# 输出信号约束
set_output_delay -clock clk -max 2 [get_ports signal_out]
7.2 资源占用对比
不同实现方式的资源占用比较(以Xilinx Artix-7为例):
| 实现方式 | LUTs | 寄存器 | 最大频率 |
|---|---|---|---|
| 基本计数器 | 15 | 18 | 250MHz |
| 状态机实现 | 22 | 20 | 230MHz |
| 动态展宽 | 18 | 20 | 240MHz |
| 多级展宽(3级) | 45 | 54 | 220MHz |
| 共享计数器(8通道) | 35 | 25 | 200MHz |
7.3 功耗优化策略
针对低功耗应用的优化技巧:
- 使用门控时钟:在无信号输入时关闭展宽电路的时钟
- 动态调整展宽时间:根据系统状态自动调整
- 采用时钟分频:在满足时序要求的前提下使用更低频率
门控时钟实现示例:
verilog复制module low_power_stretch #(
parameter STRETCH_CYCLES = 100
)(
input clk,
input rst_n,
input signal_in,
output reg signal_out
);
reg [15:0] counter;
reg signal_prev;
wire gated_clk;
reg clk_enable;
// 门控时钟逻辑
assign gated_clk = clk & clk_enable;
always @(posedge signal_in or negedge rst_n) begin
if (!rst_n) begin
clk_enable <= 0;
end else begin
clk_enable <= 1'b1;
end
end
always @(posedge gated_clk or negedge rst_n) begin
if (!rst_n) begin
signal_prev <= 0;
signal_out <= 0;
counter <= 0;
clk_enable <= 0;
end else begin
signal_prev <= signal_in;
if (!signal_prev && signal_in) begin
signal_out <= 1'b1;
counter <= STRETCH_CYCLES;
end else if (counter > 0) begin
counter <= counter - 1;
end else begin
signal_out <= 1'b0;
clk_enable <= 1'b0; // 展宽结束,关闭时钟
end
end
end
endmodule
8. 工程实践建议
经过多个项目的实践验证,我总结了以下经验教训:
-
参数化设计:始终将展宽时间、计数器宽度等关键参数设计为可配置的,这将在后期调试时节省大量时间。我曾经在一个项目中因为把展宽时间硬编码而不得不重新综合整个设计,仅仅是为了调整10个时钟周期的参数。
-
跨时钟域处理:如果展宽电路的输入信号来自异步时钟域,务必添加两级同步器。我曾经遇到过亚稳态导致系统死机的问题,最终发现是因为忽略了这简单的同步步骤。
-
测试覆盖率:不要只测试正常工作场景,特别要关注以下边界情况:
- 复位期间输入信号变化
- 展宽结束前新的输入脉冲到来
- 连续快速脉冲输入
- 参数设置为最小值的情况
-
资源与性能权衡:在FPGA中,如果只需要处理少量信号,简单的计数器实现是最佳选择。但当需要处理数十或数百个信号时,考虑采用共享计数器架构可以显著节省资源。
-
文档记录:明确记录设计中的以下信息:
- 最小输入脉冲宽度要求
- 最大输入脉冲频率
- 展宽时间的时钟周期数
- 复位行为描述
-
版本控制:对参数配置和设计变更做好版本标记。我曾经因为忘记哪个版本修改了展宽参数而浪费了半天时间回溯问题。
信号展宽虽然是一个相对简单的功能,但在实际应用中却可能成为系统可靠性的关键。通过合理的设计和充分的验证,可以确保这项基础功能在各种工况下都能稳定工作。
