时钟信号如同数字系统的心跳,精准的时序控制是项目成败的关键。当我们需要为UART、I2C等低速外设提供特定频率时钟时,分频技术便成为FPGA开发者的必备技能。本文将带您深入Quartus和Vivado两大平台,通过PLL配置与Verilog编码两种方式,实现从基础偶数分频到复杂奇数分频的全套解决方案。
在项目初期,工程师常面临关键决策:使用硬件PLL还是编写分频代码?这两种方案各有其适用场景。PLL作为硬件锁相环,能提供极低的时钟抖动和精确的相位控制,特别适合对时钟质量要求严格的场景,如高速SerDes接口或ADC采样时钟。以Intel Cyclone 10 LP系列为例,其PLL可支持从5MHz到472.5MHz的输入范围,输出频率精度可达±100ps。
而Verilog分频器的优势则体现在灵活性上。当我们需要动态调整分频比(如根据传感器数据实时改变I2C时钟速率),或者目标频率超出PLL工作范围时,代码方案就成为不二之选。下表对比了两种方案的核心特性:
| 特性 | PLL方案 | Verilog分频方案 |
|---|---|---|
| 时钟质量 | 低抖动(<50ps),相位可编程 | 中等抖动(≈1ns) |
| 资源占用 | 占用专用PLL资源 | 仅需逻辑单元和寄存器 |
| 动态调整 | 需重新配置PLL参数 | 实时修改分频比寄存器即可 |
| 频率范围 | 受PLL规格限制 | 仅受系统时钟频率限制 |
| 奇数分频支持 | 部分高端PLL支持 | 全支持 |
工程经验:在Xilinx Artix-7器件上,当目标频率低于5MHz时,使用MMCM/PLL可能反而会引入不必要的功耗,此时简单的寄存器分频更为经济。
Intel Quartus Prime提供了直观的PLL配置界面。我们以Cyclone IV E系列为例,演示如何为50MHz系统时钟生成12.5MHz(4分频)和6.25MHz(8分频)时钟:
生成的PLL实例化模板如下:
verilog复制pll_50m pll_inst (
.inclk0(sys_clk),
.areset(~sys_rst_n),
.c0(clk_12_5m),
.c1(clk_6_25m),
.locked(pll_locked)
);
关键约束文件(SDC)配置:
tcl复制create_clock -name sys_clk -period 20.000 [get_ports sys_clk]
derive_pll_clocks
set_false_path -from [get_clocks {clk_12_5m}] -to [get_clocks {clk_6_25m}]
当PLL方案不适用时,我们需要编写可配置的分频器模块。下面展示一个支持奇偶分频的通用设计,重点解决占空比控制和跨时钟域问题:
verilog复制module universal_divider #(
parameter DIV_RATIO = 8 // 默认8分频
)(
input wire sys_clk,
input wire sys_rst_n,
output reg div_clk
);
localparam CNT_WIDTH = $clog2(DIV_RATIO);
reg [CNT_WIDTH-1:0] cnt;
// 偶数分频逻辑
generate if (DIV_RATIO % 2 == 0) begin : even_div
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
cnt <= 0;
div_clk <= 0;
end else if (cnt == (DIV_RATIO/2 - 1)) begin
cnt <= 0;
div_clk <= ~div_clk;
end else begin
cnt <= cnt + 1;
end
end
end else begin : odd_div
// 奇数分频需要双沿处理
reg div_clk_pos, div_clk_neg;
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
cnt <= 0;
div_clk_pos <= 0;
end else if (cnt == (DIV_RATIO-1)) begin
cnt <= 0;
div_clk_pos <= ~div_clk_pos;
end else if (cnt == ((DIV_RATIO-1)/2)) begin
div_clk_pos <= ~div_clk_pos;
cnt <= cnt + 1;
end else begin
cnt <= cnt + 1;
end
end
always @(negedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
div_clk_neg <= 0;
end else if (cnt == ((DIV_RATIO-1)/2)) begin
div_clk_neg <= ~div_clk_neg;
end
end
assign div_clk = div_clk_pos | div_clk_neg;
end
endgenerate
endmodule
在Vivado中创建Testbench时,建议采用SystemVerilog的随机测试方法:
systemverilog复制module tb_divider;
logic clk = 0;
logic rst_n = 0;
logic div_clk;
universal_divider #(.DIV_RATIO(7)) uut (.*);
always #10 clk = ~clk;
initial begin
#100 rst_n = 1;
#2000 $finish;
end
// 自动检查分频比
property check_div_ratio;
int count;
@(posedge div_clk) disable iff(!rst_n)
(1, count=0) |=> (1, count=count+1) throughout
(##[0:$] $fell(div_clk)[->1], $display("Measured ratio: %0d", count+1));
endproperty
assert property(check_div_ratio);
endmodule
XDC约束示例:
tcl复制create_clock -period 20.000 -name sys_clk [get_ports sys_clk]
set_output_delay -clock sys_clk -max 2.000 [get_ports div_clk]
对于需要运行时调整分频比的场景,可采用寄存器接口方案:
verilog复制module dynamic_divider (
input wire sys_clk,
input wire sys_rst_n,
input wire [7:0] div_ratio,
input wire ratio_valid,
output wire div_clk
);
reg [7:0] current_ratio;
reg [7:0] cnt;
reg div_clk_reg;
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
current_ratio <= 8'd8;
cnt <= 0;
div_clk_reg <= 0;
end else begin
if (ratio_valid && div_ratio >= 8'd2)
current_ratio <= div_ratio;
if (cnt == current_ratio - 1) begin
cnt <= 0;
div_clk_reg <= ~div_clk_reg;
end else begin
cnt <= cnt + 1;
end
end
end
assign div_clk = div_clk_reg;
// 时钟切换保护
(* ASYNC_REG = "TRUE" *) reg [1:0] sync_ratio_valid;
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) sync_ratio_valid <= 0;
else sync_ratio_valid <= {sync_ratio_valid[0], ratio_valid};
end
endmodule
关键提示:动态分频时务必确保新分频比大于等于2,否则会导致计数器溢出。建议添加如下保护逻辑:
verilog复制if (div_ratio < 8'd2) div_ratio = 8'd2;
分频时钟使用时最常见的隐患是跨时钟域(CDC)问题。当分频时钟与系统时钟交互时,必须采用同步策略:
verilog复制module pulse_sync (
input wire src_clk,
input wire dst_clk,
input wire rst_n,
input wire pulse_in,
output wire pulse_out
);
(* ASYNC_REG = "TRUE" *) reg [2:0] sync_chain;
always @(posedge dst_clk or negedge rst_n) begin
if (!rst_n) sync_chain <= 0;
else sync_chain <= {sync_chain[1:0], pulse_in};
end
assign pulse_out = sync_chain[1] && !sync_chain[2];
endmodule
verilog复制module async_fifo #(
parameter DATA_WIDTH = 8,
parameter DEPTH = 16
)(
input wire wr_clk,
input wire rd_clk,
// ... 其他端口
);
// 使用XPM宏实现
xpm_cdc_gray #(
.DEST_SYNC_FF(4),
.WIDTH(DATA_WIDTH)
) cdc_inst (
.dest_clk(rd_clk),
.dest_out(fifo_wr_ptr_sync),
.src_clk(wr_clk),
.src_in(wr_ptr)
);
endmodule
verilog复制always @(posedge sys_clk or negedge rst_n) begin
if (!rst_n) begin
data_out <= 0;
en_div_clk <= 0;
end else begin
en_div_clk <= (cnt == DIV_RATIO - 1);
if (en_div_clk) data_out <= data_in;
end
end
在Intel Quartus的TimeQuest时序分析器中,对分频时钟需添加如下约束:
tcl复制create_generated_clock -name div_clk -source [get_pins pll_inst|clkout0] \
-divide_by 8 [get_ports div_clk]
set_clock_groups -asynchronous -group {sys_clk} -group {div_clk}
对于Xilinx Vivado,需特别注意跨时钟域路径的约束:
tcl复制set_clock_groups -name async_div_clk -asynchronous \
-group [get_clocks sys_clk] \
-group [get_clocks -of [get_pins div_clk_reg/Q]]