FPGA 频率计实验
参考:正点原子开拓者 FPGA 开发指南
数字频率计是一种基本的测量仪器,被广泛应用于航天、电子、测控等领域。基于传统测频原理的频率计的测量精度将随被测信号频率的下降而降低,在使用中有较大的局限性,而等精度频率计不但具有较高的测量精度,而且在整个频率区域能保持恒定的测试精度。
一、等精度频率计简介
频率测量在电子设计和测量领域中经常用到,因此对频率测量方法的研究在实际工程应用中具有重要意义。常用的频率测量方法有两种:周期测量法和频率测量法。
- 周期测量法是先测量出被测信号的周期T,然后根据频率f = 1/T求出被测信号的频率。
- 频率测量法是在时间t内对被测信号的脉冲数N进行计数,然后求出单位时间内的脉冲数,即为被测信号的频率。
但是上述两种方法都会产生±1个被测脉冲的误差,在实际应用中有一定的局限性。根据测量原理,很容易发现周期测量法适合于低频信号测量,频率测量法适合于高频信号测量(因为周期越大或者频率越高那么所测得的周期或者脉冲数N的相对误差就会越小),但二者都不能兼顾高低频率同样精度的测量要求。
等精度测量的一个最大特点是测量的实际门控时间不是一个固定值,而是一个与被测信号有关的值,刚好是被测信号的整数倍。在计数允许时间内,同时对基准时钟和被测信号进行计数,再通过数学公式推导得到被测信号的频率。由于门控信号是被测信号的整数倍,就消除了对被测信号产生的±l周期误差,但是会产生对基准时钟±1周期的误差。 等精度测量原理如下图所示:
从以上叙述的等精度的测量原理可以很容易得出如下结论:首先,被测信号频率clk_fx的相对误差与被测信号的频率无关;其次,增大测量时间段“软件闸门”或提高“标频”clk_fs,可以减小相对误差,提高测量精度;最后,由于一般提供基准时钟clk_fs的石英晶振稳定性很高,所以基准时钟的相对误差很小,可忽略。假设基准时钟的频率为100MHz,只要实际闸门时间大于或等于1s,就可使测量的最大相对误差小于或等于10^(-8),即精度达到1/100MHz。等精度测量的核心思想在于如何保证在实际测量门闸内被测信号为整数个周期,这就需要在设计中让实际测量门闸信号与被测信号建立一定的关系。基于这种思想,设计中以被测信号的上升沿作为开启门闸和关闭门闸的驱动信号,只有在被测信号的上升沿才将上图中预置的“软件闸门”的状态锁存,因此在“实际闸门”Tx内被测信号的个数就能保证整数个周期,这样就避免普通测量方法中被测信号的±1的误差,但会产生高频的基准时钟信号的±l周期误差,由于基准时钟频率远高于被测信号,因此它产生的±1周期误差对测量精度的影响十分有限,特别是在中低频测量的时候,相较于传统的频率测量和周期测量方法,可以大大提高测量精度。
等精度测频的原理图如下图所示。图中,预置软件闸门信号GATE是由FPGA的定时模块产生,GATE的时间宽度对测频精度的影响较小,故可以在较大的范围内选择,GATE信号经被测时钟clk_fx同步化(图中的D触发器)到被测时钟域下。另外,为了方便处理,这里选择预置闸门信号的长度由参数GATE_TIME设置。图中的fs_cnt和fx_cnt是2个可控的32位高速计数器,fs_cnt_en和fx_cnt_en分别是其计数使能端,由同步化后的GATE信号控制,基准时钟信号clk_fs从时钟输入端clk_fs输入,待测信号clk_fx从时钟输入端clk_fx输入。测量时,生成的GATE信号,在被测时钟同步化后用来控制启动和关闭2个计数器,2个计数器分别对被测信号和基准时钟计数。若在一次实际闸门时间GATE_TIME中,计数器对被测信号的计数值为fx_cnt,对基准时钟的计数值为fs_cnt,而基准时钟的频率为CLK_FS,则被测信号的频率为clk_fx,则由公式:
二、实验任务
板载50MHz的时钟通过分频产生某一频率的时钟信号,作为被测时钟,然后用Verilog HDL编写的等精度测量模块测量被测时钟,并通过数码管显示。
三、程序设计
根据实验任务,我们可以大致规划出系统的控制流程:首先我们设计一个测试时钟模块用于生成被测的时钟,然后用等精度频率计模块测量被测时钟的频率,并将测得的时钟频率值送入数码管显示模块进行显示。由此画出系统的功能框图如下所示:
由系统框图可知,FPGA部分包括四个模块:顶层模块(top_cymometer)、等精度频率计模块(cymometer)、时钟产生模块(clk_test)、以及数码管显示模块(seg_led)。各模块功能如下:
顶层模块(top_cymometer):顶层模块完成了对其它三个模块的例化,实现各模块之间的数据交互。时钟产生模块产生被测时钟输出,并从外部接入至等精度频率计模块,以进行频率测量,将测量的结果传输给数码管驱动模块进行显示。顶层模块的原理图如下图所示:
等精度频率计模块(cymometer):等精度频率计模块测量输入的被测时钟的频率。并将测得的频率结果输出。
时钟产生模块(clk_test):时钟产生模块产生被测的时钟。
数码管显示模块(seg_led):将等精度频率计测得的时钟频率值在数码管上显示出来。
1、顶层模块的代码如下:
module top_cymometer(
//system clock
input sys_clk , // 时钟信号
input sys_rst_n, // 复位信号
//cymometer interface
input clk_fx , // 被测时钟
output clk_out , // 输出时钟
//user interface
output [5:0] sel , // 数码管位选
output [7:0] seg_led // 数码管段选
);
//parameter define
parameter CLK_FS = 26'd50000000; // 基准时钟频率值
//wire define
wire [19:0] data_fx; // 被测信号测量值
//*****************************************************
//** main code
//*****************************************************
//例化等精度频率计模块
cymometer #(.CLK_FS(CLK_FS) // 基准时钟频率值
) u_cymometer(
//system clock
.clk_fs (sys_clk ), // 基准时钟信号
.rst_n (sys_rst_n), // 复位信号
//cymometer interface
.clk_fx (clk_fx ), // 被测时钟信号
.data_fx (data_fx ) // 被测时钟频率输出
);
//例化测试时钟模块,产生测试时钟
clk_test #(.DIV_N(7'd100) // 分频系数
) u_clk_test(
//源时钟
.clk_in (sys_clk ), // 输入时钟
.rst_n (sys_rst_n), // 复位信号
//分频后的时钟
.clk_out (clk_out ) // 测试时钟
);
//例化数码管显示模块
seg_led u_seg_led(
//module clock
.clk (sys_clk ), // 数码管驱动模块的驱动时钟
.rst_n (sys_rst_n), // 复位信号
//seg_led interface
.sel (sel ), // 数码管位选
.seg_led (seg_led ), // 数码管段选
//user interface
.data (data_fx ), // 被测频率值
.point (6'd0 ), // 数码管显示的点控制
.en (1'b1 ), // 数码管驱动使能信号
.sign (1'b0 ) // 控制符号位显示
);
endmodule
2、时钟产生模块的代码如下:
module clk_test #(parameter DIV_N = 7'd100) //分频系数
(
//源时钟
input clk_in , // 输入时钟
input rst_n , // 复位信号
//分频后的时钟
output reg clk_out // 输出时钟
);
//reg define
reg [9:0] cnt; // 时钟分频计数
//*****************************************************
//** main code
//*****************************************************
//时钟分频
always @(posedge clk_in or negedge rst_n) begin
if(rst_n == 1'b0) begin
cnt <= 0;
clk_out <= 0;
end
else begin
if(cnt == DIV_N/2 - 1'b1) begin
cnt <= 10'd0;
clk_out <= ~clk_out;
end
else
cnt <= cnt + 1'b1;
end
end
endmodule
时钟产生模块通过分频产生被测时钟,这里是用偶数分频方法产生,修改代码第一行的DIV_N分频参数,可得到不同频率的时钟信号,时钟频率为clk_in/DIV_N 。由于该模块在顶层例化时clk_in为系统时钟50MHz,分频参数为100,产生的时钟频率为50000000/100 = 500000Hz。
3、等精度频率计模块的代码如下:
module cymometer
#(parameter CLK_FS = 26'd50_000_000) // 基准时钟频率值
( //system clock
input clk_fs , // 基准时钟信号
input rst_n , // 复位信号
//cymometer interface
input clk_fx , // 被测时钟信号
output reg [19:0] data_fx // 被测时钟频率输出
);
//parameter define
localparam MAX = 6'd32; // 定义fs_cnt、fx_cnt的最大位宽
localparam GATE_TIME = 16'd5_000; // 门控时间设置
//reg define
reg gate ; // 门控信号
reg gate_fs ; // 同步到基准时钟的门控信号
reg gate_fs_r ; // 用于同步gate信号的寄存器
reg gate_fs_d0 ; // 用于采集基准时钟下gate下降沿
reg gate_fs_d1 ; //
reg gate_fx_d0 ; // 用于采集被测时钟下gate下降沿
reg gate_fx_d1 ; //
reg [ 15:0] gate_cnt ; // 门控计数
reg [MAX-1:0] fs_cnt ; // 门控时间内基准时钟的计数值
reg [MAX-1:0] fs_cnt_temp ; // fs_cnt 临时值
reg [MAX-1:0] fx_cnt ; // 门控时间内被测时钟的计数值
reg [MAX-1:0] fx_cnt_temp ; // fx_cnt 临时值
//wire define
wire neg_gate_fs; // 基准时钟下门控信号下降沿
wire neg_gate_fx; // 被测时钟下门控信号下降沿
//*****************************************************
//** main code
//*****************************************************
//边沿检测,捕获信号下降沿
assign neg_gate_fs = gate_fs_d1 & (~gate_fs_d0);
assign neg_gate_fx = gate_fx_d1 & (~gate_fx_d0);
//门控信号计数器,使用被测时钟计数
always @(posedge clk_fx or negedge rst_n) begin
if(!rst_n)
gate_cnt <= 16'd0;
else if(gate_cnt == GATE_TIME + 5'd20)
gate_cnt <= 16'd0;
else
gate_cnt <= gate_cnt + 1'b1;
end
//门控信号,拉高时间为GATE_TIME个实测时钟周期
always @(posedge clk_fx or negedge rst_n) begin
if(!rst_n)
gate <= 1'b0;
else if(gate_cnt < 4'd10)
gate <= 1'b0;
else if(gate_cnt < GATE_TIME + 4'd10)
gate <= 1'b1;
else if(gate_cnt <= GATE_TIME + 5'd20)
gate <= 1'b0;
else
gate <= 1'b0;
end
//将门控信号同步到基准时钟下
always @(posedge clk_fs or negedge rst_n) begin
if(!rst_n) begin
gate_fs_r <= 1'b0;
gate_fs <= 1'b0;
end
else begin
gate_fs_r <= gate;
gate_fs <= gate_fs_r;
end
end
//打拍采门控信号的下降沿(被测时钟下)
always @(posedge clk_fx or negedge rst_n) begin
if(!rst_n) begin
gate_fx_d0 <= 1'b0;
gate_fx_d1 <= 1'b0;
end
else begin
gate_fx_d0 <= gate;
gate_fx_d1 <= gate_fx_d0;
end
end
//打拍采门控信号的下降沿(基准时钟下)
always @(posedge clk_fs or negedge rst_n) begin
if(!rst_n) begin
gate_fs_d0 <= 1'b0;
gate_fs_d1 <= 1'b0;
end
else begin
gate_fs_d0 <= gate_fs;
gate_fs_d1 <= gate_fs_d0;
end
end
//门控时间内对被测时钟计数
always @(posedge clk_fx or negedge rst_n) begin
if(!rst_n) begin
fx_cnt_temp <= 32'd0;
fx_cnt <= 32'd0;
end
else if(gate)
fx_cnt_temp <= fx_cnt_temp + 1'b1;
else if(neg_gate_fx) begin
fx_cnt_temp <= 32'd0;
fx_cnt <= fx_cnt_temp;
end
end
//门控时间内对基准时钟计数
always @(posedge clk_fs or negedge rst_n) begin
if(!rst_n) begin
fs_cnt_temp <= 32'd0;
fs_cnt <= 32'd0;
end
else if(gate_fs)
fs_cnt_temp <= fs_cnt_temp + 1'b1;
else if(neg_gate_fs) begin
fs_cnt_temp <= 32'd0;
fs_cnt <= fs_cnt_temp;
end
end
//计算被测信号频率
always @(posedge clk_fs or negedge rst_n) begin
if(!rst_n) begin
data_fx <= 20'd0;
end
else if(gate_fs == 1'b0)
data_fx <= (CLK_FS / fs_cnt) * fx_cnt ;
end
endmodule
在前面的等精度频率计简介中,我们知道在等精度测量中需要一个闸门信号(门控信号),并且该闸门信号需要同步化到被测时钟域下。这里我们为了方便处理,用被测时钟控制闸门信号的产生,这样就避免了同步化处理,当然了,完全可以用基准时钟控制闸门信号的产生,不过这时产生的闸门信号我们需要同步化到被测时钟域下,这样做的目的是为了不让被测时钟计数产生±1周期的误差。门控时间由参数GATE_TIME设置,此处设为5000,需要说明的是该值越大测得的被测时钟频率值越精确,但测量时间也会相应的变慢一些。另外因为闸门信号是由被测时钟产生的,当测量频率较高的信号或者说信号频率大于10KHz(此值跟门控时间有关)时是不会有什么问题的,但当测量低频信号像Hz级这种,如果门控时间设置的大的话,测量时间就会非常长,此时可修改门控时间的值,为被测时钟频率的5~10倍即可,对于几十KHz及以上的时钟信号,门控时间的大小对测量速度的影响较小,频率越高影响越小,但对测量精度影响较大,因而在测量频率较高的信号时,建议增大门控时间。
4、数码管显示模块的代码如下:
module seg_led(
//module clock
input clk , // 时钟信号
input rst_n , // 复位信号
//seg_led interface
output reg [5:0] sel , // 动态显示被选中的数码管
output reg [7:0] seg_led, // 一个数码管中亮的灯,包含小数点
//user interface
input [19:0] data , // 6个数码管要显示的数值
input [5:0] point , // 小数点具体显示的位置,从高到低,高电平有效
input en , // 数码管使能信号
input sign // 符号位(高电平显示"-"号)
);
//parameter define
localparam MAX_NUM = 13'd5000 ; // 1ms计数值
localparam CLK_DIVIDE = 4'd10 ; // 时钟分频
//reg define
reg [12:0] cnt0 ; // 1ms计数
reg flag ; // 1ms计满标志信号
reg [2:0] cnt ; // 切换显示数码管用
reg [3:0] num1 ; // 送给要显示的数码管,要亮的灯
reg point1 ; // 要显示的小数点
reg [23:0] num ; // 24位bcd码用寄存器
reg [ 3:0] clk_cnt ; // 时钟计数
reg dri_clk ; // 驱动数码管操作的驱动时钟
//wire define
wire [3:0] data0 ; // 十万位数
wire [3:0] data1 ; // 万位数
wire [3:0] data2 ; // 千位数
wire [3:0] data3 ; // 百位数
wire [3:0] data4 ; // 十位数
wire [3:0] data5 ; // 个位数
//*****************************************************
//** main code
//*****************************************************
assign data5 = data / 17'd100000; // 十万位数
assign data4 = data / 14'd10000 % 4'd10; // 万位数
assign data3 = data / 10'd1000 % 4'd10 ; // 千位数
assign data2 = data / 7'd100 % 4'd10 ; // 百位数
assign data1 = data / 4'd10 % 4'd10 ; // 十位数
assign data0 = data % 4'd10; // 个位数
//生成数码管的驱动时钟用于驱动数码管的操作
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
dri_clk <= 1'b1;
clk_cnt <= 4'd0;
end
else if(clk_cnt == CLK_DIVIDE/2 - 1'd1) begin
clk_cnt <= 4'd0;
dri_clk <= ~dri_clk;
end
else
clk_cnt <= clk_cnt + 1'b1;
end
//将20位2进制数转换为8421bcd码
always @ (posedge dri_clk or negedge rst_n) begin
if (!rst_n)
num <= 24'b0;
else begin
if (data5 || point[5]) begin
num[23:20] <= data5;
num[19:16] <= data4;
num[15:12] <= data3;
num[11:8] <= data2;
num[ 7:4] <= data1;
num[ 3:0] <= data0;
end
else begin
if (data4 || point[4]) begin
num[19:0] <= {data4,data3,data2,data1,data0};
if(sign)
num[23:20] <= 4'd11;
else
num[23:20] <= 4'd10;
end
else begin
if (data3 || point[3]) begin
num[15: 0] <= {data3,data2,data1,data0};
num[23:20] <= 4'd10;
if(sign)
num[19:16] <= 4'd11;
else
num[19:16] <= 4'd10;
end
else begin
if (data2 || point[2]) begin
num[11: 0] <= {data2,data1,data0};
num[23:16] <= {2{4'd10}};
if(sign)
num[15:12] <= 4'd11;
else
num[15:12] <= 4'd10;
end
else begin
if (data1 || point[1]) begin
num[ 7: 0] <= {data1,data0};
num[23:12] <= {3{4'd10}};
if(sign)
num[11:8] <= 4'd11;
else
num[11:8] <= 4'd10;
end
else begin
num[3:0] <= data0;
if(sign)
num[23:4] <= {{4{4'd10}},4'd11};
else
num[23:4] <= {5{4'd10}};
end
end
end
end
end
end
end
//计数1ms
always @ (posedge dri_clk or negedge rst_n) begin
if (rst_n == 1'b0) begin
flag <= 1'b0;
cnt0 <= 13'b0;
end
else if (cnt0 < MAX_NUM - 1'b1) begin
flag <= 1'b0;
cnt0 <= cnt0 + 1'b1;
end
else begin
flag <= 1'b1;
cnt0 <= 13'b0;
end
end
//计数器,用来计数6个状态(因为有6个灯)
always @ (posedge dri_clk or negedge rst_n) begin
if (rst_n == 1'b0)
cnt <= 3'b0;
else if(flag) begin
if(cnt < 3'd5)
cnt <= cnt + 1'b1;
else
cnt <= 3'b0;
end
end
//6个数码管轮流显示,完成刷新( 从右到左)
always @ (posedge dri_clk or negedge rst_n) begin
if(!rst_n) begin
sel <= 6'b000000;
num1 <= 4'b0;
end
else begin
if(en) begin
case (cnt)
3'd0 :begin
sel <= 6'b111110;
num1 <= num[3:0] ;
point1 <= ~point[0] ;
end
3'd1 :begin
sel <= 6'b111101;
num1 <= num[7:4] ;
point1 <= ~point[1] ;
end
3'd2 :begin
sel <= 6'b111011;
num1 <= num[11:8];
point1 <= ~point[2] ;
end
3'd3 :begin
sel <= 6'b110111;
num1 <= num[15:12];
point1 <= ~point[3] ;
end
3'd4 :begin
sel <= 6'b101111;
num1 <= num[19:16];
point1 <= ~point[4];
end
3'd5 :begin
sel <= 6'b011111;
num1 <= num[23:20];
point1 <= ~point[5];
end
default :begin
sel <= 6'b000000;
num1 <= 4'b0;
point1 <= 1'b1;
end
endcase
end
else
sel <= 6'b111111;
end
end
//数码管显示数据
always @ (posedge dri_clk or negedge rst_n) begin
if (!rst_n)
seg_led <= 7'h40;
else begin
case (num1)
4'd0 : seg_led <= {point1,7'b1000000};
4'd1 : seg_led <= {point1,7'b1111001};
4'd2 : seg_led <= {point1,7'b0100100};
4'd3 : seg_led <= {point1,7'b0110000};
4'd4 : seg_led <= {point1,7'b0011001};
4'd5 : seg_led <= {point1,7'b0010010};
4'd6 : seg_led <= {point1,7'b0000010};
4'd7 : seg_led <= {point1,7'b1111000};
4'd8 : seg_led <= {point1,7'b0000000};
4'd9 : seg_led <= {point1,7'b0010000};
4'd10: seg_led <= 8'b11111111;
4'd11: seg_led <= 8'b10111111;
default : seg_led <= {point1,7'b1000000};
endcase
end
end
endmodule
由上式可以看出,测量频率的相对误差与被测信号频率的大小无关,仅与闸门时间和基准时钟频率有关,即实现了整个测试频段的等精度测量。闸门时间越长,基准时钟频率越高,测频的相对误差就越小。基准时钟频率可由稳定度好、精度高的高频率晶体振荡器产生,在保证测量精度不变的前提下,提高基准时钟频率,可使闸门时间缩短,即提高测试速度。