1 使能时钟分频
FPGA 内部时钟使用逻辑计数分频产生的时钟,一般不推荐直接将其用于内部逻辑。若不希望使用 PLL 资源,可以考虑使用使能时钟进行分频。
使能时钟进行分频的好处:
(1)避免时钟过多,会造成不稳定;
(2)保持一个时钟,减少跨时钟域;
(3)时序设计可以使用 “ 多周期约束 ” 。
2 举例
(1)设计要求
输入时钟 100MHz ,产生 5MHz 的使能时钟信号,并利用此使能时钟信号进行 0 到 15 的周期计数。
(2)设计思路
输入时钟 100MHz ,使能时钟信号 5MHz ,100MHz/5MHz = 20 ,说明利用输入时钟进行 20 分频产生使能时钟信号。
需要注意,使能时钟信号与普通的低频信号不同。以同样是 10 分频举例,普通的低频信号设置为前 10 个时钟内是高电平,后 10 个时钟内是低电平;使能信号设置为前 19 个时钟内是低电平,最后 1 个时钟内是高电平。具体如图:
3 Verilog 代码
`timescale 1ns / 1ps
// 使能时钟分频
// 需求说明:
// 1 输入时钟 100 MHz ,输出使能时钟信号 5 MHz 。
// 2 使用使能信号进行 0 ~ 15 的周期计数。
// 设计思路:
// 1 100MHz/5MHz = 20 ,即输入信号做 20 分频。
// 2 设置计数器 cnt_div ,对输入信号做 0 到 19 的计数实现分频。
// 3 使能信号与普通的分频不同,只有在 20 分频的最后一个输入时钟是高电平,其余时刻是低电平。即 cnt_div 是 0 到 18 时 clk_en 是低电平,cnt_div 是 19 时 clk_en 是高电平。
module divider(
input clk, // 100MHz
input rst_n,
output reg clk_en,
output reg [3:0] cnt_16
);
reg [4:0] cnt_div; // 分频计数器
localparam cnt_div_max = 5'd19; // 分频计数器的最大值
localparam cnt_16_max = 4'd15; // 利用使能信号进行计数的最大值
// 分频计数器 cnt_div 的初始化和赋值
always@(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt_div = 4'd0;
end
else begin
if(cnt_div < cnt_div_max)
cnt_div = cnt_div + 1'b1;
else
cnt_div = 5'd0;
end
end
// 使能时钟信号 clk_en 的产生
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
clk_en = 1'b0;
end
else begin
if(cnt_div == cnt_div_max) // 使能时钟信号只有在 20 分频时钟周期的最后一个时钟是高电平
clk_en = 1'b1;
else
clk_en = 1'b0;
end
end
// 计数器 cnt_16 的初始化和赋值
// 利用使能时钟进行 0 到 15 的计数
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt_16 = 4'd0;
end
else begin
if(cnt_16 == cnt_16_max + 1 ) // 使能时钟信号只有在 20 分频时钟周期的最后一个时钟是高电平
cnt_16 = 4'd0;
else begin
if(clk_en)
cnt_16 = cnt_16 + 1'b1;
else
cnt_16 = cnt_16;
end
end
end
endmodule
4 Testbench 代码
`timescale 1ns / 1ps
module divider_tb;
parameter CLK_PERIOD = 10; // 输入信号频率是 100MHz ,所以输入信号的时钟周期是 10ns
reg clk;
reg rst_n;
wire clk_en;
wire [3:0]cnt_16;
// 例化待测文件的顶层模块,其端口与 tb 中的激励信号连接
divider divider_0( .clk(clk), .rst_n(rst_n), .clk_en(clk_en), .cnt_16(cnt_16) );
// 时钟信号和复位信号的初始化
initial begin
#1;
clk = 0; // 初始时没有时钟信号
rst_n = 1; // 初始时复位信号为高电平
#1; // 经过 1 X 1ns = 1ns 后,复位信号为低电平
rst_n = 0;
#(CLK_PERIOD*2); // 经过 2 个 CLK_PERIOD 后,复位信号为高电平
rst_n = 1;
end
always #(CLK_PERIOD/2) clk = ~clk; // 时钟信号每 5 ns 跳变一次
initial begin
@(posedge clk); // 等待复位信号结束的第一个输入信号的上升沿开始产生激励信号
@(posedge rst_n);
// 从 0 到 15 计数需要 16 个使能信号,1 个使能信号需要 20 个时钟信号,所以使能信号计数 1 轮需要 20*16 个时钟信号。
// 想要观察到 6 轮计数,所以需要产生 20*16*6 个时钟信号的上升沿作为激励
repeat(20*16*6) begin
@(posedge clk);
end
#10_000; // 仿真持续 10_000 ns 后停止
$stop;
end
endmodule
5 仿真波形
使能信号周期 200000 ps ,即 5MHz 。
6 存在的问题
设计的思路是在使能信号为高电平时,计数器 cnt_16 加 1 。但根据波形可以看出在 clk_en 高电平时 clk_16 没有加 1 ,产生了滞后。
改进的思路是:改变 cnt_16 加 1 的触发条件,同时改变 cnt_16 的复位条件,如下:
// 计数器 cnt_16 的初始化和赋值
// 利用使能时钟进行 0 到 15 的计数
always @(posedge clk_en or negedge rst_n) begin
if(!rst_n) begin
cnt_16 = 4'd0;
end
else begin
if(cnt_16 == cnt_16_max)
cnt_16 = 4'd0;
else
cnt_16 = cnt_16 + 1'b1;
end
end
(注意与前述的 verilog 代码的 cnt_16 赋值块进行对比)
新的波形:
可以看出,在 clk_en 为高电平时,cnt_16 进行了加 1 操作。