2026寒假练 - 基于小脚丫FPGA实现数字频率计与幅度测量仪
该项目使用了小脚丫FPGA,实现了数字频率计与幅度测量仪的设计,它的主要功能为:测量频率、占空比和幅度。
标签
FPGA
ADC
Wiki
更新2026-03-24
华南理工大学
18

项目总结报告

项目介绍

基于高速ADC的数字频率计与幅度测量仪:

1. 使用电赛板上的 ADC 模块,FPGA 产生稳定的采样时钟 `ADC_CLK`(使用 PLL),采样外部输入信号(正弦或方波)。

2. 在 100 Hz–1 MHz 范围内测量信号频率,方法可采用过零检测、边沿计数或自相关。

3. 计算峰峰值或有效值幅度;对方波额外测量占空比。

4. OLED 显示频率、幅度数值;按键切换外部输入信号。

使用到的硬件

1. 基于小脚丫FPGA的电赛训练平台

基于小脚丫FPGA的电赛训练平台专为全国大学生电子设计竞赛技能培训设计,帮助信号源、仪器仪表、控制以及信号处理类题目的训练,核心模块可以是基于全FPGA或MCU+FPGA混合架构的核心板,板上有通过两个16Pin的插座可以安装高速ADC(16Pin可再用模块/同时支持DIP和邮票孔)、高速DAC(16Pin可再用模块/支持DIP和邮票孔)、板上安装了高速比较器、姿态传感器、旋转编码器以及按键等。

_DSC5721-抠图-16-9.png

2. STEP-Altera-MAX10-02小脚丫FPGA核心板:

STEP-MAX10是小脚丫平台基于Altera公司芯片开发的FPGA开发板。核心FPGA芯片采用了Altera公司MAX10系列下的10M02SCM153/10M08SAM153,这些芯片管脚完全兼容,封装相同,区别是内部资源不同。小脚丫STEP MAX10开发板的尺寸同样也采用了DIP40封装,小巧携带方便。板载资源也是十分丰富,集成了USB-Blaster编程器和按键、拨码开关、数码管、LED等多种外设资源。板上的36个FPGA IO接口都通过2.54mm通孔焊盘引出,可以和面包板配合使用,快速搭建自己的硬件电路。板卡尺寸为52mm x 18mm,能够灵活的嵌入到插座或者其他的系统中。

STEP-MAX10 FPGA板上集成的编程器能够完美支持Intel-Altera工具Quartus系列开发软件,只需要一根Type-C USB数据线就能够完成FPGA的编程仿真和下载,使用更加方便。

图层 0 拷贝.png

3. 高速ADC模块

ScreenShot_2026-03-08_211834_928.png

方案框图和项目设计思路介绍

1.drawio.png

基于高速ADC的数字频率计与幅度测量仪采用模块化设计,以FPGA为核心控制单元。系统通过PLL生成稳定采样时钟驱动高速ADC模块采集外部信号,经数字测频模块(cymometer)实现100Hz–1MHz频率测量,同时通过占空比检测(duty_cycle)与峰值检测(peak_detector)模块完成波形参数提取。各测量数据经BCD码转换后,由OLED屏幕实时显示频率、占空比及峰峰值信息。

调试软件及使用的编程语言说明、软件流程图及关键代码介绍

1. 调试软件:quartus

Fk_Yy_PJnXZYpRiYjLG4rl-5Jogu.png

2. 编程语言:verilog

3. 软件流程图:

1.drawio.png

4. 关键代码介绍

oled:

5'd1:	begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "----------------";state <= SCAN; end
5'd2: begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " FPGA Training ";state <= SCAN; end
5'd3: begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= " www.eetree.cn ";state <= SCAN; end
5'd4: begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "----------------";state <= SCAN; end
5'd5: begin y_p <= 8'hb4; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "Freq: Hz";state <= SCAN; end
5'd6: begin y_p <= 8'hb5; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "Vpp: V";state <= SCAN; end
5'd7: begin y_p <= 8'hb6; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "Duty: %";state <= SCAN; end
5'd8: begin y_p <= 8'hb7; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "Have Fun! :) ";state <= SCAN; end
// 频率显示
5'd9: begin y_p <= 8'hb4; x_ph <= 8'h16; x_pl <= 8'h08; num <= 5'd 1; char <= data_freq0; state <= SCAN; end
5'd10: begin y_p <= 8'hb4; x_ph <= 8'h16; x_pl <= 8'h00; num <= 5'd 1; char <= data_freq1; state <= SCAN; end
5'd11: begin y_p <= 8'hb4; x_ph <= 8'h15; x_pl <= 8'h08; num <= 5'd 1; char <= data_freq2; state <= SCAN; end
5'd12: begin y_p <= 8'hb4; x_ph <= 8'h15; x_pl <= 8'h00; num <= 5'd 1; char <= data_freq3; state <= SCAN; end
5'd13: begin y_p <= 8'hb4; x_ph <= 8'h14; x_pl <= 8'h08; num <= 5'd 1; char <= data_freq4; state <= SCAN; end
5'd14: begin y_p <= 8'hb4; x_ph <= 8'h14; x_pl <= 8'h00; num <= 5'd 1; char <= data_freq5; state <= SCAN; end
5'd15: begin y_p <= 8'hb4; x_ph <= 8'h13; x_pl <= 8'h08; num <= 5'd 1; char <= data_freq6; state <= SCAN; end
// 占空比显示
5'd16: begin y_p <= 8'hb6; x_ph <= 8'h17; x_pl <= 8'h00; num <= 5'd 1; char <= data_duty0; state <= SCAN; end
5'd17: begin y_p <= 8'hb6; x_ph <= 8'h16; x_pl <= 8'h08; num <= 5'd 1; char <= data_duty1; state <= SCAN; end
5'd18: begin y_p <= 8'hb6; x_ph <= 8'h16; x_pl <= 8'h00; num <= 5'd 1; char <= data_duty2; state <= SCAN; end
// 峰峰值电压显示 (格式: XX.X V)
5'd19: begin y_p <= 8'hb5; x_ph <= 8'h17; x_pl <= 8'h00; num <= 5'd 1; char <= data_vpp0; state <= SCAN; end
5'd20: begin y_p <= 8'hb5; x_ph <= 8'h16; x_pl <= 8'h08; num <= 5'd 1; char <= "."; state <= SCAN; end
5'd21: begin y_p <= 8'hb5; x_ph <= 8'h16; x_pl <= 8'h00; num <= 5'd 1; char <= data_vpp1; state <= SCAN; end
5'd22: begin y_p <= 8'hb5; x_ph <= 8'h15; x_pl <= 8'h08; num <= 5'd 1; char <= data_vpp2; state <= SCAN; end


binary2bcd_freq:

module binary2bcd_freq(
input wire sys_clk,
input wire sys_rst_n,
input wire [29:0] data,

output reg [27:0] bcd_data //7位十进制数的值
);

parameter CNT_SHIFT_NUM = 7'd30; //由data的位宽决定这里是30

reg [6:0] cnt_shift; //移位判断计数器该值由data的位宽决定这里是6
reg [57:0] data_shift; //移位判断数据寄存器,由data和bcddata的位宽之和决定。
reg shift_flag; //移位判断标志信号

//cnt_shift计数
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
cnt_shift <= 7'd0;
else if((cnt_shift == CNT_SHIFT_NUM + 7'd1) && (shift_flag))
cnt_shift <= 7'd0;
else if(shift_flag)
cnt_shift <= cnt_shift + 7'd1;
else
cnt_shift <= cnt_shift;
end

//data_shift 计数器为0时赋初值,计数器为1~CNT_SHIFT_NUM时进行移位操作
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
data_shift <= 58'd0;
else if(cnt_shift == 7'd0)
data_shift <= {28'b0,data};
else if((cnt_shift <= CNT_SHIFT_NUM)&&(!shift_flag))begin
data_shift[33:30] <= (data_shift[33:30] > 4) ? (data_shift[33:30] + 2'd3):(data_shift[33:30]);
data_shift[37:34] <= (data_shift[37:34] > 4) ? (data_shift[37:34] + 2'd3):(data_shift[37:34]);
data_shift[41:38] <= (data_shift[41:38] > 4) ? (data_shift[41:38] + 2'd3):(data_shift[41:38]);
data_shift[45:42] <= (data_shift[45:42] > 4) ? (data_shift[45:42] + 2'd3):(data_shift[45:42]);
data_shift[49:46] <= (data_shift[49:46] > 4) ? (data_shift[49:46] + 2'd3):(data_shift[49:46]);
data_shift[53:50] <= (data_shift[53:50] > 4) ? (data_shift[53:50] + 2'd3):(data_shift[53:50]);
data_shift[57:54] <= (data_shift[57:54] > 4) ? (data_shift[57:54] + 2'd3):(data_shift[57:54]);
end
else if((cnt_shift <= CNT_SHIFT_NUM)&&(shift_flag))
data_shift <= data_shift << 1;
else
data_shift <= data_shift;
end

//shift_flag 移位判断标志信号,用于控制移位判断的先后顺序
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
shift_flag <= 1'b0;
else
shift_flag <= ~shift_flag;
end

//当计数器等于CNT_SHIFT_NUM时,移位判断操作完成,整体输出
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
bcd_data <= 28'd0;
else if(cnt_shift == CNT_SHIFT_NUM + 28'd1)
bcd_data <= data_shift[57:30];
else
bcd_data <= bcd_data;
end

endmodule


binary2bcd_duty:

module binary2bcd_duty(
input wire sys_clk,
input wire sys_rst_n,
input wire [6:0] data, // 占空比值 (0-100), 只需7位

output reg [11:0] bcd_data // 3位BCD: 百位[11:8], 十位[7:4], 个位[3:0]
);

// 简单的BCD转换:因为占空比范围只有0-100,可以用组合逻辑直接计算
wire [3:0] hundreds;
wire [3:0] tens;
wire [6:0] remainder; // 减去百位后的余数

// 百位:只能是0或1
assign hundreds = (data >= 7'd100) ? 4'd1 : 4'd0;

// 减去百位后的余数
assign remainder = (data >= 7'd100) ? (data - 7'd100) : data;

// 十位:0-9
assign tens = (remainder >= 7'd90) ? 4'd9 :
(remainder >= 7'd80) ? 4'd8 :
(remainder >= 7'd70) ? 4'd7 :
(remainder >= 7'd60) ? 4'd6 :
(remainder >= 7'd50) ? 4'd5 :
(remainder >= 7'd40) ? 4'd4 :
(remainder >= 7'd30) ? 4'd3 :
(remainder >= 7'd20) ? 4'd2 :
(remainder >= 7'd10) ? 4'd1 : 4'd0;

// 个位:余数减去十位*10
wire [6:0] ones_temp;
assign ones_temp = remainder - ({3'd0, tens} * 7'd10);
wire [3:0] ones;
assign ones = ones_temp[3:0];

// 寄存输出
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
bcd_data <= 12'd0;
else
bcd_data <= {hundreds, tens, ones};
end

endmodule


binary2bcd_vpp:

// 7位二进制转3位BCD (0-100) - 峰峰值电压x10 (0.0-10.0V)
module binary2bcd_vpp(
input wire sys_clk,
input wire sys_rst_n,
input wire [6:0] data,

output reg [11:0] bcd_data // 3位BCD: 百位、十位、个位
);

wire [3:0] hundreds;
wire [3:0] tens;
wire [3:0] ones;

// 百位 (0-1): 100对应的二进制是1100100,最高位是bit6
// 但为了正确判断>=100,需要用比较器
assign hundreds = (data >= 7'd100) ? 4'd1 : 4'd0;

// 去掉百位后的值 (0-99)
wire [6:0] rem_a;
assign rem_a = (data >= 7'd100) ? (data - 7'd100) : data;

// 十位 (0-9)
assign tens = (rem_a >= 7'd90) ? 4'd9 :
(rem_a >= 7'd80) ? 4'd8 :
(rem_a >= 7'd70) ? 4'd7 :
(rem_a >= 7'd60) ? 4'd6 :
(rem_a >= 7'd50) ? 4'd5 :
(rem_a >= 7'd40) ? 4'd4 :
(rem_a >= 7'd30) ? 4'd3 :
(rem_a >= 7'd20) ? 4'd2 :
(rem_a >= 7'd10) ? 4'd1 : 4'd0;

// 个位 (0-9)
wire [6:0] ones_full;
assign ones_full = rem_a - {tens, 2'b0} - {tens, 1'b0}; // rem_a - tens*10
assign ones = ones_full[3:0];

always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
bcd_data <= 12'd0;
else
bcd_data <= {hundreds, tens, ones};
end

endmodule


clk_test:

module clk_test(
input clk_in, // 12MHz
input rst_n,

output reg clk_out_100,
output reg clk_out_1K,
output reg clk_out_10K,
output reg clk_out_100K
);

parameter DIV_N_100 = 16'd60_000;
parameter DIV_N_1K = 13'd6_000;
parameter DIV_N_10K = 10'd600;
parameter DIV_N_100K = 6'd60;

reg [15:0] cnt_100;
reg [12:0] cnt_1K;
reg [9:0] cnt_10K;
reg [5:0] cnt_100K;

always @(posedge clk_in or negedge rst_n) begin
if(!rst_n) begin
cnt_100 <= 16'd0;
clk_out_100 <= 1'b0;
end
else if(cnt_100 == DIV_N_100 - 16'd1) begin
cnt_100 <= 16'd0;
clk_out_100 <= ~clk_out_100;
end
else
cnt_100 <= cnt_100 + 16'd1;
end

always @(posedge clk_in or negedge rst_n) begin
if(!rst_n) begin
cnt_1K <= 13'd0;
clk_out_1K <= 1'b0;
end
else if(cnt_1K == DIV_N_1K - 13'd1) begin
cnt_1K <= 13'd0;
clk_out_1K <= ~clk_out_1K;
end
else
cnt_1K <= cnt_1K + 13'd1;
end

always @(posedge clk_in or negedge rst_n) begin
if(!rst_n) begin
cnt_10K <= 10'd0;
clk_out_10K <= 1'b0;
end
else if(cnt_10K == DIV_N_10K - 10'd1) begin
cnt_10K <= 10'd0;
clk_out_10K <= ~clk_out_10K;
end
else
cnt_10K <= cnt_10K + 10'd1;
end

always @(posedge clk_in or negedge rst_n) begin
if(!rst_n) begin
cnt_100K <= 6'd0;
clk_out_100K <= 1'b0;
end
else if(cnt_100K == DIV_N_100K - 6'd1) begin
cnt_100K <= 6'd0;
clk_out_100K <= ~clk_out_100K;
end
else
cnt_100K <= cnt_100K + 6'd1;
end

endmodule


cymometer:

module cymometer#(
parameter CNT_GATE_MAX = 28'd18_000_000, // 测频周期时间为1.5s
parameter CNT_TIME_MAX = 28'd19_200_000, // 清零周期时间为1.6s
parameter CNT_GATE_LOW = 28'd3_000_000, // 闸门为低的时间0.25s
parameter CLK_FS_FREQ = 28'd50_000_000
) (
input sys_clk, // 系统时钟,12M
input clk_fs, // 基准时钟,50M
input sys_rst_n, // 系统复位,低电平有效
input clk_fx, // 被测时钟信号

output reg [29:0] data_fx, // 被测时钟频率值

input wire ready, // 值可被接受
input wire [45:0] quotient, // 商
input wire [45:0] remainder, // 余数
input wire vld_out, // 值有效

output reg [45:0] dividend, // 被除数
output reg [45:0] divisor, // 除数
output reg en // 开始信号
);

localparam TIME = 10'd150; // 数据稳定时间

reg gate_sclk;
reg [27:0] cnt_gate_fs;
reg gate_fx;
reg gate_fx_d0;
reg gate_fx_d1;
reg gate_fx_d2;
reg gate_fx_d3;
reg gate_fs;
reg gate_fs_d0;
reg gate_fs_d1;
reg [29:0] cnt_fx;
reg [29:0] cnt_fx_reg;
reg [29:0] cnt_fs;
reg [29:0] cnt_fs_reg;
reg [29:0] cnt_fs_reg_reg;
reg calc_flag;
reg [45:0] numer;
reg fx_flag;
reg [45:0] numer_reg;
reg [27:0] cnt_dely;
reg flag_dely;

wire gate_fx_pose; // 上升沿
wire gate_fx_nege; // 下降沿
wire gate_fs_nege; // 下降沿

// 计算公式 CLK_FX_FREQ = cnt_fx*CLK_FS_FREQ/cnt_fs
assign gate_fx_pose = ((gate_fx) && (!gate_fx_d3))? 1'b1:1'b0;//上升沿
assign gate_fx_nege = ((!gate_fx_d2) && gate_fx_d3)? 1'b1:1'b0;//下降沿
assign gate_fs_nege = ((!gate_fs_d0) && gate_fs_d1)? 1'b1:1'b0;//下降沿

// 产生软件闸门时间计数器 cnt_gate_fs 1.5s
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
cnt_gate_fs <= 28'd0;
else if(cnt_gate_fs == CNT_GATE_MAX - 28'd1 )
cnt_gate_fs <= 28'd0;
else
cnt_gate_fs <= cnt_gate_fs + 28'd1;
end

// 产生软件闸门 GATE_SCLK
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
gate_sclk <= 1'b0;
else if(cnt_gate_fs == CNT_GATE_LOW - 1'b1)
gate_sclk <= 1'b1;
else if(cnt_gate_fs == CNT_GATE_MAX - CNT_GATE_LOW - 1'b1)
gate_sclk <= 1'b0;
else
gate_sclk <= gate_sclk;
end

// 将软件闸门同步到被测时钟下得到实际闸门,并进行打拍获取上升沿和下降沿
always@(posedge clk_fx or negedge sys_rst_n)begin
if(!sys_rst_n)begin
gate_fx <= 1'b0;
gate_fx_d0 <= 1'b0;
gate_fx_d1 <= 1'b0;
gate_fx_d2 <= 1'b0;
gate_fx_d3 <= 1'b0;
end
else begin
gate_fx <= gate_sclk;
gate_fx_d0 <= gate_fx;
gate_fx_d1 <= gate_fx_d0;
gate_fx_d2 <= gate_fx_d1;
gate_fx_d3 <= gate_fx_d2;
end
end

// 获取实际闸门的下降沿 在基准时钟下获取下降沿
always@(posedge clk_fs or negedge sys_rst_n)begin
if(!sys_rst_n)begin
gate_fs <= 1'b0;
gate_fs_d0 <= 1'b0;
gate_fs_d1 <= 1'b0;
end
else begin
gate_fs <= gate_fx;
gate_fs_d0 <= gate_fs;
gate_fs_d1 <= gate_fs_d0;
end
end

// 在实际闸门下分别计算时钟周期数 cnt_fx(被测时钟) cnt_fs(基准时钟)
// 被测时钟下的周期个数 cnt_fx
always@(posedge clk_fx or negedge sys_rst_n)begin
if(!sys_rst_n)
cnt_fx <= 30'd0;
else if(gate_fx_d2)
cnt_fx <= cnt_fx + 30'd1;
else if(!gate_fx_d2)
cnt_fx <= 30'd0;
else
cnt_fx <= cnt_fx;
end

// 在下降沿将被测时钟的时钟周期数进行缓存
always@(posedge clk_fx or negedge sys_rst_n)begin
if(!sys_rst_n)
cnt_fx_reg <= 30'd0;
else if(gate_fx_nege)
cnt_fx_reg <= cnt_fx;
else
cnt_fx_reg <= cnt_fx_reg;
end

// 基准时钟下的周期个数 cnt_fs
always@(posedge clk_fs or negedge sys_rst_n)begin
if(!sys_rst_n)
cnt_fs <= 30'd0;
else if(gate_fx)
cnt_fs <= cnt_fs + 30'd1;
else if(gate_fs_nege)
cnt_fs <= 30'd0;
else
cnt_fs <= cnt_fs;
end

// 在下降沿将基准时钟的时钟周期数进行缓存
always@(posedge clk_fs or negedge sys_rst_n)begin
if(!sys_rst_n)
cnt_fs_reg <= 30'd0;
else if(gate_fs_nege)
cnt_fs_reg <= cnt_fs;
else
cnt_fs_reg <= cnt_fs_reg;
end

// CLK_FX_FREQ = cnt_fx*CLK_FS_FREQ/cnt_fs
// 先计算得到分子 cnt_fx*CLK_FS_FREQ
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
numer <= 46'd0;
else if(cnt_gate_fs == CNT_GATE_MAX - CNT_GATE_LOW + TIME)
numer <= cnt_fx_reg * CLK_FS_FREQ;
else
numer <= numer;
end

// 打一拍对计算得到的值 numer_reg(分子) 进行同步并寄存
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
numer_reg <= 46'd0;
else if(cnt_gate_fs == (CNT_GATE_MAX - (CNT_GATE_LOW / 2'd2) - TIME))
numer_reg <= numer;
else
numer_reg <= numer_reg;
end

// 打一拍对计算得到的值 cnt_fs_reg_reg(分母) 进行同步并寄存
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
cnt_fs_reg_reg <=30'd0;
else if(cnt_gate_fs == (CNT_GATE_MAX - (CNT_GATE_LOW / 2'd2)- TIME))
cnt_fs_reg_reg <= cnt_fs_reg;
else
cnt_fs_reg_reg <= cnt_fs_reg_reg;
end

// 产生计算标志信号calc_flag
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
calc_flag <= 1'b0;
else if(cnt_gate_fs == (CNT_GATE_MAX - CNT_GATE_LOW / 2'd2 - 28'd2))
calc_flag <= 1'b1;
else if(cnt_gate_fs == (CNT_GATE_MAX - CNT_GATE_LOW / 2'd2 - 28'd1))
calc_flag <= 1'b0;
else
calc_flag <= calc_flag;
end

// 被测时钟启动是否为零
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
fx_flag <= 1'b0;
else if(clk_fx && gate_fx)
fx_flag <= 1'b1;
else
fx_flag <= fx_flag;
end

// 闸门时间计数
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
cnt_dely <= 28'd0;
else if(gate_fx_pose)
cnt_dely <= 28'd0;
else if(cnt_dely == CNT_TIME_MAX)
cnt_dely <= CNT_TIME_MAX;
else
cnt_dely <= cnt_dely + 28'd1;
end

// 上升沿到来后2s拉高
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
flag_dely <= 1'b0;
else if(cnt_dely >= CNT_TIME_MAX)
flag_dely <= 1'b1;
else if(cnt_dely < CNT_TIME_MAX)
flag_dely <= 1'b0;
else
flag_dely <= flag_dely;
end

// 获得被测信号的频率值
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
data_fx <= 30'd0;
else if(!fx_flag) //启动清零
data_fx <= 30'd0;
else if(flag_dely) //被测时钟被拔掉
data_fx <= 30'd0;
else if(vld_out)
data_fx <= quotient;
else
data_fx <= data_fx;
end

always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
en <= 1'b0;
else if(cnt_gate_fs == (CNT_GATE_MAX - CNT_GATE_LOW / 2'd2))
en <= 1'b1;
else if(vld_out)
en <= 1'b0;
else
en <= en;
end

always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)begin
dividend <= 46'd0;
divisor <= 46'd1;
end
else if(calc_flag)begin
dividend <= numer_reg;
divisor <= cnt_fs_reg_reg;
end
else begin
dividend <= dividend;
divisor <= divisor;
end
end

endmodule


key_debounce:

module key_debounce #(
parameter N = 2
) (
input sys_clk,
input sys_rst_n,
input [N-1:0] key,
output [N-1:0] key_pulse
);

reg [N-1:0] key_rst_pre; //定义一个寄存器型变量存储上一个触发时的按键值
reg [N-1:0] key_rst; //定义一个寄存器变量储存储当前时刻触发的按键值

wire [N-1:0] key_edge; //检测到按键由高到低变化时产生一个高脉冲

//利用非阻塞赋值特点,将两个时钟触发时按键状态存储在两个寄存器变量中
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
key_rst <= {N{1'b1}}; //初始化时给key_rst赋值全为1,{}中表示N个1
key_rst_pre <= {N{1'b1}};
end
else begin
key_rst <= key; //第一个时钟上升沿触发之后key的值赋给key_rst,同时key_rst的值赋给key_rst_pre
key_rst_pre <= key_rst; //非阻塞赋值。相当于经过两个时钟触发,key_rst存储的是当前时刻key的值,key_rst_pre存储的是前一个时钟的key的值
end
end

//脉冲边沿检测。当key检测到下降沿时,key_edge产生一个时钟周期的高电平
assign key_edge = key_rst_pre & (~key_rst);

reg [17:0] cnt; //产生延时所用的计数器,系统时钟12MHz,要延时20ms左右时间,至少需要18位计数器

//产生20ms延时,当检测到key_edge有效是计数器清零开始计数
always @(posedge sys_clk or negedge sys_rst_n)
begin
if(!sys_rst_n)
cnt <= 18'h0;
else if(key_edge)
cnt <= 18'h0;
else
cnt <= cnt + 1'h1;
end

reg [N-1:0] key_sec_pre; //延时后检测电平寄存器变量
reg [N-1:0] key_sec;


//延时后检测key,如果按键状态变低产生一个时钟的高脉冲。如果按键状态是高的话说明按键无效
always @(posedge sys_clk or negedge sys_rst_n)
begin
if (!sys_rst_n)
key_sec <= {N{1'b1}};
else if (cnt==18'h3ffff)
key_sec <= key;
end
always @(posedge sys_clk or negedge sys_rst_n)
begin
if (!sys_rst_n)
key_sec_pre <= {N{1'b1}};
else
key_sec_pre <= key_sec;
end
assign key_pulse = key_sec_pre & (~key_sec);

endmodule


adc:

module adc#(
parameter CNT_GATE_MAX = 28'd18_000_000 , // 测频周期时间为1.5s
parameter CNT_GATE_LOW = 28'd3_000_000 , // 闸门为低的时间0.25s
parameter CNT_TIME_MAX = 28'd19_200_000 , // 清零周期时间为1.6s
parameter CLK_FS_FREQ = 28'd50_000_000 ,
parameter DATAWIDTH = 8'd46 , // 需要46位以支持1MHz测量
parameter N = 2
) (
input sys_clk,
input sys_rst_n,
input [N-1:0] key,
input [9:0] ad_data, // ADC数据输入

output oled_rst,
output oled_dcn,
output oled_clk,
output oled_dat,
output ad_clk, // ADC时钟输出 (50MHz)
output clk_out_pin // 被测时钟输出 (接到ADC输入)
);

wire [29:0] data_fx; // 被测信号频率测量值
wire [6:0] data_duty; // 占空比测量值 (0-100), 只需7位
wire [6:0] peak_pp; // 峰峰值电压x10 (0-100, 表示0.0-10.0V)
wire clk_fs;
wire clk_out_1M_25;
wire clk_out_1M_75;
wire locked;

// ADC时钟输出 (使用50MHz基准时钟)
assign ad_clk = clk_fs;

// 被测时钟输出 (接到ADC输入)
assign clk_out_pin = clk_out;

// 频率测量的除法器信号
wire en_freq;
wire [45:0] dividend_freq;
wire [45:0] divisor_freq;
wire ready_freq;
wire [45:0] quotient_freq;
wire [45:0] remainder_freq;
wire vld_out_freq;

wire [27:0] bcd_data_freq; // 频率BCD数据 (7位)
wire [11:0] bcd_data_duty; // 占空比BCD数据 (3位: 百位、十位、个位)
wire [11:0] bcd_data_vpp; // 峰峰值电压BCD数据 (3位: XX.X)

wire clk_out_100;
wire clk_out_1K;
wire clk_out_10K;
wire clk_out_100K;
wire clk_out_500K;

wire [N-1:0] key_pulse;

reg clk_out;
reg [2:0] freq_sel; // 频率选择状态: 0=100Hz, 1=1KHz, 2=10KHz, 3=100KHz, 4=1M_25, 5=1M_75

always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
freq_sel <= 3'd0;
else if (key_pulse[0]) begin // 按下按键0后回到上一个频率
if (freq_sel == 3'd0)
freq_sel <= 3'd5;
else
freq_sel <= freq_sel - 3'd1;
end
else if (key_pulse[1]) begin // 按下按键1后回到下一个频率
if (freq_sel == 3'd5)
freq_sel <= 3'd0;
else
freq_sel <= freq_sel + 3'd1;
end
end

always @(*) begin
case (freq_sel)
3'd0: clk_out = clk_out_100;
3'd1: clk_out = clk_out_1K;
3'd2: clk_out = clk_out_10K;
3'd3: clk_out = clk_out_100K;
3'd4: clk_out = clk_out_1M_25;
3'd5: clk_out = clk_out_1M_75;
default: clk_out = clk_out_100;
endcase
end

ip_pll u_ip_pll(
.areset (~sys_rst_n),
.inclk0 (sys_clk),
.c0 (clk_fs),
.c1 (clk_out_1M_25),
.c2 (clk_out_1M_75),
.locked (locked)
);

cymometer#(
.CNT_GATE_MAX (CNT_GATE_MAX), // 测频周期时间为1.5s
.CNT_GATE_LOW (CNT_GATE_LOW), // 闸门为低的时间0.25s
.CNT_TIME_MAX (CNT_TIME_MAX),
.CLK_FS_FREQ (CLK_FS_FREQ )
) u_cymometer(
.sys_clk (sys_clk ), // 系统时钟,12M
.clk_fs (clk_fs ), // 基准时钟,50M
.sys_rst_n (sys_rst_n ),
.clk_fx (clk_out ), // 被测时钟信号
.data_fx (data_fx ), // 被测时钟频率值
.dividend (dividend_freq),
.divisor (divisor_freq),
.en (en_freq ),
.ready (ready_freq ),
.quotient (quotient_freq),
.remainder (remainder_freq),
.vld_out (vld_out_freq)
);

// 占空比测量模块(内置移位除法器,无需外部除法器)
duty_cycle#(
.CNT_GATE_MAX (CNT_GATE_MAX),
.CNT_GATE_LOW (CNT_GATE_LOW)
) u_duty_cycle(
.sys_clk (sys_clk ),
.clk_fs (clk_fs ),
.sys_rst_n (sys_rst_n ),
.clk_fx (clk_out ),
.duty_data (data_duty )
);

div_fsm#(
.DATAWIDTH (DATAWIDTH)
) u_div_fsm_freq(
.clk (sys_clk ),
.rst_n (sys_rst_n ),
.en (en_freq ),
.dividend (dividend_freq),
.divisor (divisor_freq),
.ready (ready_freq ),
.quotient (quotient_freq),
.remainder (remainder_freq),
.vld_out (vld_out_freq)
);

binary2bcd_freq u_binary2bcd_freq(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n ),
.data (data_fx ),
.bcd_data (bcd_data_freq)
);

binary2bcd_duty u_binary2bcd_duty(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n ),
.data (data_duty ),
.bcd_data (bcd_data_duty)
);

oled u_oled(
.sys_clk (sys_clk),
.sys_rst_n (sys_rst_n),
.data_freq (bcd_data_freq),
.data_duty (bcd_data_duty),
.data_vpp (bcd_data_vpp),
.oled_rst (oled_rst),
.oled_dcn (oled_dcn),
.oled_clk (oled_clk),
.oled_dat (oled_dat)
);

clk_test u_clk_test(
.clk_in (sys_clk),
.rst_n (sys_rst_n),
.clk_out_100(clk_out_100),
.clk_out_1K (clk_out_1K),
.clk_out_10K(clk_out_10K),
.clk_out_100K(clk_out_100K)
);

key_debounce u_key_debounce (
.sys_clk (sys_clk),
.sys_rst_n (sys_rst_n),
.key (key),
.key_pulse (key_pulse)
);

// 峰峰值检测模块
peak_detector u_peak_detector (
.sys_clk (sys_clk),
.sys_rst_n (sys_rst_n),
.clk_fs (clk_fs),
.ad_data (ad_data),
.peak_pp (peak_pp)
);

// 峰峰值BCD转换
binary2bcd_vpp u_binary2bcd_vpp(
.sys_clk (sys_clk),
.sys_rst_n (sys_rst_n),
.data (peak_pp),
.bcd_data (bcd_data_vpp)
);

endmodule


功能展示图及说明

image.png

频率:100Hz

峰峰值:4.40V

占空比:50%

image.png

频率:1KHz

峰峰值:4.40V

占空比:50%

image.png

频率:10KHz

峰峰值:4.40V

占空比:50%

image.png

频率:100KHz

峰峰值:4.40V

占空比:50%

image.png

频率:1MHz

峰峰值:6.00V

占空比:24%(实际为25%)

image.png

频率:1MHz

峰峰值:6.10V

占空比:74%(实际为75%)


FPGA资源占用报告

image.png

项目中遇到的难题及解决方法

1. FPGA资源不足

本项目使用的STEP-MAX10-02核心板FPGA芯片资源有限(约2000个逻辑单元),而频率测量、除法运算、BCD转换、OLED驱动等模块都需要消耗大量资源。

解决方法:

  • 精准设置数据位宽,避免不必要的宽位宽开销。例如占空比值范围0-100,仅需7位而非32位
  • 优化BCD转换模块,对占空比和峰峰值这类小范围数据采用组合逻辑直接计算,而非移位加3算法,节省触发器资源
  • 复用除法器模块,减少重复逻辑

2. 缺少示波器和信号发生器等调试设备

开发过程中没有专业的测试仪器,难以验证频率测量和ADC采样的正确性。

解决方法:

  • 使用PLL IP核生成多种已知频率的测试信号(100Hz、1kHz、10kHz、100kHz、1MHz等),作为被测信号输入
  • 使用Quartus软件自带的Signal Tap Logic Analyzer在线逻辑分析仪观察内部信号波形
  • 通过Signal Tap实时监控计数器、状态机等关键信号,验证逻辑正确性

3. 多时钟域跨域同步问题

频率测量模块涉及多个时钟域:系统时钟(12MHz)、基准时钟(50MHz)、被测时钟(100Hz-1MHz),直接传递信号会产生亚稳态。

解决方法:

  • 使用多级寄存器打拍技术(如gate_fx信号打4拍)进行同步处理
  • 采用脉冲边沿检测电路检测闸门信号的上升沿和下降沿
  • 在跨时钟域传递数据时使用寄存器缓存,确保数据稳定后再使用
// 闸门信号打拍同步示例
always@(posedge clk_fx or negedge sys_rst_n)begin
if(!sys_rst_n)begin
gate_fx <= 1'b0;
gate_fx_d0 <= 1'b0;
gate_fx_d1 <= 1'b0;
gate_fx_d2 <= 1'b0;
gate_fx_d3 <= 1'b0;
end
else begin
gate_fx <= gate_sclk;
gate_fx_d0 <= gate_fx;
gate_fx_d1 <= gate_fx_d0;
gate_fx_d2 <= gate_fx_d1;
gate_fx_d3 <= gate_fx_d2;
end
end

4. 高精度除法运算的实现

频率计算公式为 `CLK_FX_FREQ = cnt_fx * CLK_FS_FREQ / cnt_fs`,需要进行46位宽的除法运算。

解决方法:

  • 采用移位减法除法器(div_fsm模块),通过状态机实现逐位计算
  • 使用流水线设计,将被除数和除数准备好后再启动计算
  • 设置合理的数据位宽(46位)以支持最高1MHz的频率测量

心得体会

通过本次基于FPGA的高速ADC数字频率计与幅度测量仪项目,我在技术能力和工程实践方面都有了显著的收获。

在项目开始之前,我对FPGA的理解主要停留在理论学习层面。通过本次实践,我深刻体会到FPGA设计与软件编程的本质区别:FPGA是并行执行的,需要充分考虑时序、资源占用和跨时钟域等问题。例如,在频率测量模块中,闸门信号需要在被测时钟域和基准时钟域之间传递,这让我真正理解了"亚稳态"的危害以及多级寄存器同步的重要性。

使用资源有限的MAX10芯片是一次宝贵的锻炼。在调试过程中,曾多次遇到"Fit Failed"错误,这迫使我重新审视代码中每一个信号的位宽设置。我学会了根据实际数据范围选择最小够用的位宽,例如占空比只有0-100,用7位即可;峰峰值乘以10后最大100,7位也足够。这种"精打细算"的思维方式对嵌入式和FPGA开发都非常重要。

在没有示波器和信号发生器的情况下完成项目,是一个很大的挑战。通过学习和使用Signal Tap Logic Analyzer,我掌握了在线逻辑分析仪的使用方法,学会了如何选取关键信号进行监控,如何分析时序波形定位问题。这种"软件替代硬件"的调试思路,在资源受限的开发环境中非常实用。

本项目采用了模块化设计,将频率测量、占空比检测、峰峰值检测、BCD转换、OLED显示等功能分别封装成独立模块。这种设计方式带来了几个好处:

1. 各模块可以独立开发和调试,降低了复杂度

2. 模块接口清晰,便于团队协作和代码复用

3. 出现问题时可以快速定位到具体模块

本项目涉及多个经典算法的硬件实现,如等精度测频法、移位加3的BCD转换、移位减法除法器等。我体会到将算法从软件思维转换为硬件实现需要考虑很多因素:时钟周期、流水线、资源消耗等。这是一个不断权衡和优化的过程。

尽管项目完成了基本功能,但仍有一些可以改进的地方:

1. 频率测量精度可以进一步提高,例如采用更长的闸门时间或更高精度的基准时钟

2. ADC采样的峰峰值检测可以加入数字滤波,提高抗干扰能力

3. 可以扩展支持更多波形类型(三角波、锯齿波等)的识别

总的来说,这次项目让我对FPGA开发有了更深入的理解,也培养了面对复杂工程问题时的分析和解决能力。这些经验和技能将对我今后的学习和工作产生深远的影响。

附件下载
adc.zip
团队介绍
华南理工大学
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号