351 lines
11 KiB
Markdown
351 lines
11 KiB
Markdown
# 进阶-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的输入速率越高。
|
||
|
||
<div> <!--块级封装-->
|
||
<center> <!--将图片和文字居中-->
|
||
<img src="./images/1.png"
|
||
alt="无法显示图片时显示的文字"
|
||
style="zoom:30%"/>
|
||
<br> <!--换行-->
|
||
图1.ADDA模块示意图 <!--标题-->
|
||
</center>
|
||
</div>
|
||
|
||
### 3.2.2 数码管模块
|
||
|
||
数码管模块在前面基础实验3已经介绍过,这里不再赘述。
|
||
|
||
## 3.3 实战演练
|
||
|
||
### 3.3.1实验目标
|
||
|
||
能够驱动板载ADC模块,对ADC模块的输入数据进行测试,计算输入信号的频率值,并在数码管模块中显示。
|
||
|
||
### 3.3.2硬件资源
|
||
|
||
实验所需的信号源来自我们的实验平台,实验平台集成一个以FPGA为基础的dds信号发生器,该dds信号发生器可以输出频率可调的方波,正弦波,三角波,锯齿波等,用户可以在web平台使用并且改变输出波形和频率。
|
||
|
||
<div> <!--块级封装-->
|
||
<center> <!--将图片和文字居中-->
|
||
<img src="./images/2.png"
|
||
alt="无法显示图片时显示的文字"
|
||
style="zoom:30%"/>
|
||
<br> <!--换行-->
|
||
图2.ADDA实验示意图 <!--标题-->
|
||
</center>
|
||
</div>
|
||
|
||
用户接收到信号源之后需自行设计逻辑处理数据并显示。
|
||
|
||
### 3.3.3程序设计
|
||
|
||
#### pulse_gen.v
|
||
|
||
首先用户接收到的是8bit波形数据,要直接利用波形数据计算频率不是很方便,计算频率数据,我们只需要计算其脉冲的个数即可,所以我们设计一个模块,通过设计一个脉冲阈值trig_level,高于阈值的就计算为一次脉冲,输出一个周期的高电平方便后续模块计数,模块代码如下:
|
||
|
||
```verilog
|
||
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** | 最后用上述表达式计算频率输出。 |
|
||
|
||
代码设计如下:
|
||
|
||
```verilog
|
||
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码的算法不是特别简单,之后会在基础实验部分讲解。
|
||
|
||
顶层模块代码如下:
|
||
|
||
```verilog
|
||
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
|
||
```
|
||
|
||
### 3.3.4仿真验证
|
||
|
||
### 3.3.5上板验证
|
||
|
||
## 3.4 章末总结
|