11 KiB
进阶-3-频率计
3.1 章节导读
本实验将基于实验平台设计并实现一个简易频率计,用于测量输入信号的频率值,并通过数码管进行实时显示。实验核心是掌握ADC模块的使用方法,被测信号频率的获取方法及其在数字系统中的处理流程。
3.2 理论学习
3.2.1 ADC模块
实验平台有一块8bit高速ADDA模块,其中ADC模块使用AD9280芯片,支持最高32MSPS的速率,模拟电压输入范围为-5~+5V,ADC模块可以根据输入电压的大小将其转换为0~255(2的8次方)的数值。模块有一个clk管脚和8个data管脚,data的输入速率和驱动时钟有关,给clk管脚的驱动时钟越快,采样率越高,data的输入速率越高。

图1.ADDA模块示意图
3.2.2 数码管模块
数码管模块在前面基础实验3已经介绍过,这里不再赘述。
3.3 实战演练
3.3.1实验目标
能够驱动板载ADC模块,对ADC模块的输入数据进行测试,计算输入信号的频率值,并在数码管模块中显示。
3.3.2硬件资源
实验所需的信号源来自我们的实验平台,实验平台集成一个以FPGA为基础的dds信号发生器,该dds信号发生器可以输出频率可调的方波,正弦波,三角波,锯齿波等,用户可以在web平台使用并且改变输出波形和频率。

图2.ADDA实验示意图
用户接收到信号源之后需自行设计逻辑处理数据并显示。
3.3.3程序设计
pulse_gen.v
首先用户接收到的是8bit波形数据,要直接利用波形数据计算频率不是很方便,计算频率数据,我们只需要计算其脉冲的个数即可,所以我们设计一个模块,通过设计一个脉冲阈值trig_level,高于阈值的就计算为一次脉冲,输出一个周期的高电平方便后续模块计数,模块代码如下:
module pulse_gen(
input rstn, //系统复位,低电平有效
input [7:0] trig_level,
input ad_clk, //AD9280驱动时钟
input [7:0] ad_data, //AD输入数据
output ad_pulse //输出的脉冲信号
);
//因为可能会有抖动,设置一个范围值避免反复触发
parameter THR_DATA = 3;
//reg define
reg pulse;
reg pulse_delay;
//*****************************************************
//** main code
//*****************************************************
assign ad_pulse = pulse & pulse_delay;
//根据触发电平,将输入的AD采样值转换成高低电平
always @ (posedge ad_clk or negedge rstn)begin
if(!rstn)
pulse <= 1'b0;
else begin
if((trig_level >= THR_DATA) && (ad_data < trig_level - THR_DATA))
pulse <= 1'b0;
else if(ad_data > trig_level + THR_DATA)
pulse <= 1'b1;
end
end
//延时一个时钟周期,用于消除抖动
always @ (posedge ad_clk or negedge rstn)begin
if(!rstn)
pulse_delay <= 1'b0;
else
pulse_delay <= pulse;
end
endmodule
cymometer.v
下面根据pulse_gen信号生成的脉冲数据进行计数,计算其频率。我们这里采用门控时钟法,用 clk_fs
(参考时钟)作为时间基准,测量 clk_fx
(被测信号)的频率。
门控时钟法的原理很简单,也就是在一个固定时间窗内(即门控时间 GATE_TIME
),计数被测时钟 clk_fx 的上升沿次数,再结合参考时钟 clk_fs
的计数值,就可以算出频率:
\text{频率} = \frac{\text{被测脉冲数量}}{\text{门控时间(秒)}} = \frac{fx\_cnt}{fs\_cnt / \text{CLK\_FS}} = \frac{\text{CLK\_FS} \times fx\_cnt}{fs\_cnt}
步骤 | 描述 |
---|---|
1 | 使用 clk_fx 作为计数时钟,控制一个门控时间 gate 信号 |
2 | 当 gate 为高电平时,fx_cnt_temp 开始统计 clk_fx 的脉冲个数 |
3 | 同时将 gate 同步到参考时钟 clk_fs ,并计数 fs_cnt_temp ,记录 gate 高电平持续期间 clk_fs 的个数 |
4 | 一旦 gate 下降沿到来(通过打拍检测),将计数值冻结到 fx_cnt 和 fs_cnt 中 |
5 | 最后用上述表达式计算频率输出。 |
代码设计如下:
module cymometer
#(parameter CLK_FS = 26'd50_000_000) // 基准时钟频率值
( //system clock
input clk_fs , // 基准时钟信号
input rstn , // 复位信号
//cymometer interface
input clk_fx , // 被测时钟信号
output reg [19:0] data_fx // 被测时钟频率输出
);
//parameter define
localparam MAX = 30; // 定义fs_cnt、fx_cnt的最大位宽
localparam GATE_TIME = 16'd2_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 [ 58:0] data_fx_t ; //
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 rstn) begin
if(!rstn)
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 rstn) begin
if(!rstn)
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 rstn) begin
if(!rstn) 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 rstn) begin
if(!rstn) 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 rstn) begin
if(!rstn) 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 rstn) begin
if(!rstn) 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 rstn) begin
if(!rstn) 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 rstn) begin
if(!rstn) begin
data_fx_t <= 1'b0;
end
else if(gate_fs == 1'b0)
data_fx_t <= CLK_FS * fx_cnt ;
end
always @(posedge clk_fs or negedge rstn) begin
if(!rstn) begin
data_fx <= 20'd0;
end
else if(gate_fs == 1'b0)
data_fx <= data_fx_t / fs_cnt ;
end
endmodule
frequency_meter.v
由于之前基础实验设计过数码管显示模块,本次实验不在赘述,但因为数码管模块是输入ascii码进行显示的,而现在输出频率数据是一个20bit的二进制数,所以我们应该先想办法将二进制转成ascii码再连接数码管模块进行显示。BCD转ascii码通过查表的方式即可完成。但二进制转BCD码的算法不是特别简单,之后会在基础实验部分讲解。
顶层模块代码如下:
module frequency_meter(
input clk,
input rstn, // 复位信号
output ad_clk, // AD时钟
input [7:0] ad_data, // AD输入数据
output [7:0] led_display_seg,
output wire [7:0] led_display_sel
);
wire ad_pulse;
wire [19:0] data_fx;
wire [25:0] bcd;
wire [31:0] data_bcd;
wire [63:0] asciidata;
assign data_bcd = {6'b00,bcd};
//生成ad驱动时钟,由于使用杜邦线连接,ad_clk不要超过10M
PLL PLLinst(
.clkout0(ad_clk), // output 10M
.lock(),
.clkin1(clk) // input
);
pulse_gen pulse_gen_inst (
.rstn(rstn),
.trig_level(8'd128),
.ad_clk(ad_clk),
.ad_data(ad_data),
.ad_pulse(ad_pulse)
);
cymometer # (
.CLK_FS(32'd27_000_000)
)
cymometer_inst (
.clk_fs(clk),
.rstn(rstn),
.clk_fx(ad_pulse),
.data_fx(data_fx)
);
//二进制转bcd码模块
bin2bcd # (
.W(20)
)
bin2bcd_inst (
.bin(data_fx),
.bcd(bcd)
);
//4位BCD码转ascii模块,例化8次使8个bcd同时输出ascii
genvar i;
generate
for (i = 0; i < 8; i = i + 1) begin : generate_module
bcd2ascii bcd2ascii_inst (
.bcd(data_bcd[i*4 +:4]),
.asciidata(asciidata[i*8 +: 8])
);
end
endgenerate
//数码管显示模块
led_display_driver led_display_driver_inst (
.clk(clk),
.rstn(rstn),
.assic_seg(asciidata),
.seg_point(8'b00000000),
.led_display_seg(led_display_seg),
.led_display_sel(led_display_sel)
);
endmodule