第一次接触FPGA开发时,我被时钟信号搞得晕头转向。主板上的晶振只能提供一个固定频率,但我的设计需要多个不同频率的时钟信号,这该怎么办?导师当时只说了一句:"用分频器啊!"后来我才明白,分频器就是数字电路中的"时钟魔术师"。
分频器本质上是个计数器+状态机组合。举个例子,就像把24小时制转换成12小时制:每计数到12就归零,同时AM/PM标志翻转。在数字电路中,最常见的分频器分为三大类:偶分频(2、4、6...)、奇分频(3、5、7...)和小数分频(1.5、3.4等)。我在做VGA显示控制器时,就需要将100MHz主时钟分频出25MHz像素时钟,这就是典型的4分频应用。
实际项目中遇到过个坑:有次用开发板测试UART通信,直接用50MHz时钟驱动115200波特率发生器,结果数据全是乱码。后来才明白需要先分频出11.52MHz时钟(实际用了小数分频近似)。这个教训让我深刻理解到:正确的时钟分频是数字系统稳定的基石。
偶分频之所以简单,是因为它天生适合产生50%占空比的时钟。记得我初学时用示波器观察过一个8分频信号:每8个输入时钟周期,输出时钟完成一次高低电平切换,高电平持续4个周期,低电平也是4个周期,完美对称。
Verilog实现的核心在于计数器的设计。这里分享个实用技巧:对于N分频(N为偶数),我们只需要在计数器达到(N/2)-1和N-1时翻转输出时钟。比如下面这个可配置的偶分频模块:
verilog复制module even_divider #(parameter N=8) (
input clk,
input rst_n,
output reg clk_out
);
reg [7:0] cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= 0;
clk_out <= 0;
end else begin
cnt <= (cnt == N-1) ? 0 : cnt + 1;
if (cnt == (N/2)-1 || cnt == N-1)
clk_out <= ~clk_out;
end
end
endmodule
虽然50%占空比最常见,但有些特殊场景需要其他比例。比如驱动某些传感器时,需要高电平持续时间是低电平的三倍。这时可以通过调整翻转条件来实现:
verilog复制// 产生25%占空比的4分频时钟
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
clk_out <= 0;
end else begin
if (cnt == 0) clk_out <= 1; // 第0周期拉高
else if (cnt == 1) clk_out <= 0; // 第1周期拉低
// 保持低电平直到周期结束
end
end
实测发现,非对称分频时钟在通过时钟树传播时可能引入额外抖动,建议在时序要求严格的场合还是优先使用50%占空比设计。
第一次实现5分频时,我百思不得其解:5是奇数,怎么才能做到高电平2.5个周期?后来才明白需要同时利用上升沿和下降沿。具体做法是:
verilog复制module odd_divider #(parameter N=5) (
input clk,
input rst_n,
output clk_out
);
reg [3:0] cnt;
reg clk_pos, clk_neg;
// 计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) cnt <= 0;
else cnt <= (cnt == N-1) ? 0 : cnt + 1;
end
// 上升沿时钟
always @(posedge clk) begin
if (cnt == (N-1)/2 || cnt == N-1)
clk_pos <= ~clk_pos;
end
// 下降沿时钟
always @(negedge clk) begin
if (cnt == (N-1)/2 || cnt == N-1)
clk_neg <= ~clk_neg;
end
assign clk_out = clk_pos | clk_neg;
endmodule
在Xilinx FPGA上实测这个设计时,发现输出时钟有毛刺。后来通过添加时钟缓冲器(BUFG)解决了这个问题,这也提醒我:双沿触发的设计要特别注意时钟质量。
某些电机控制场景需要精确控制脉冲宽度。比如要产生3分频且高电平占2/3周期的时钟,可以这样实现:
verilog复制always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
clk_out <= 0;
end else begin
case(cnt)
0: clk_out <= 1; // 周期0拉高
1: ; // 周期1保持高
2: clk_out <= 0; // 周期2拉低
endcase
end
end
需要注意的是,这种非对称时钟可能引起保持时间违规。我在一次项目验收时就栽过跟头,后来通过寄存器打拍解决了时序问题。
小数分频的核心思想是用整数分频来逼近小数。比如要实现8.7分频:
verilog复制module frac_divider #(
parameter M=87,
parameter N=10
)(
input clk,
input rst_n,
output reg clk_out
);
reg [15:0] cycle_cnt;
reg [3:0] div_cnt;
reg [7:0] div_cycle;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cycle_cnt <= 0;
div_cnt <= 0;
clk_out <= 0;
div_cycle <= 8; // 初始使用8分频
end else begin
cycle_cnt <= (cycle_cnt == M-1) ? 0 : cycle_cnt + 1;
if (div_cnt == div_cycle-1) begin
div_cnt <= 0;
clk_out <= ~clk_out;
// 每10个输出周期切换分频系数
if (cycle_cnt == M-1) begin
div_cycle <= (div_cnt < 3) ? 8 : 9;
end
end else begin
div_cnt <= div_cnt + 1;
end
end
end
endmodule
小数分频最大的挑战是周期抖动。通过实验发现,交替使用不同分频系数比集中使用效果更好。例如8.7分频,采用"8-9-8-9-8-9-9-9-9-9"的模式比"8-8-8-9-9-9-9-9-9-9"的抖动更小。
在音频处理项目中,我需要生成44.1kHz时钟(基于100MHz主时钟,分频比约2267.57)。通过采用2267和2268交替分频,配合二阶Σ-Δ调制器,最终实现的时钟抖动小于200ps,完全满足CD音质要求。
分频时钟最大的风险是产生跨时钟域问题。有次调试I2C接口时,我用主时钟分频产生SCL,结果SDA数据经常出错。后来发现是分频时钟与主时钟的相位关系不确定导致的。解决方案是:
推荐的安全设计模式:
verilog复制// 使用时钟使能而非分频时钟
reg clk_en;
always @(posedge clk) begin
if (cnt == N-1) clk_en <= 1;
else clk_en <= 0;
end
always @(posedge clk) begin
if (clk_en) begin
// 需要分频时钟驱动的逻辑
end
end
在软件定义无线电项目中,我需要实时调整分频比。直接修改分频参数会导致时钟丢失。后来采用双计数器方案:当前计数器工作时,预加载新参数到影子寄存器,在当前分频周期结束时自动切换。
verilog复制reg [15:0] current_N, next_N;
always @(posedge clk) begin
if (load_new_N) next_N <= new_N;
if (cnt == current_N-1) begin
cnt <= 0;
if (next_N != current_N)
current_N <= next_N;
end else begin
cnt <= cnt + 1;
end
end
在电池供电设备中,我发现传统分频器会持续消耗功率。通过改进设计,让分频器在非活动期间自动关闭,功耗降低了40%。关键是在分频器空闲时关闭时钟树:
verilog复制always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
active <= 0;
end else if (start_condition) begin
active <= 1;
end else if (stop_condition) begin
active <= 0;
end
end
assign gated_clk = clk & active;
高速设计中最头疼的是分频时钟的时序收敛问题。经过多次尝试,总结出以下经验:
在28nm工艺的FPGA上,采用这些方法后,500MHz系统时钟的3分频信号能稳定达到166MHz,满足-1速度等级要求。