2026寒假练 - 用小脚丫FPGA高速ADC制作数字频率计与幅度测量仪
该项目使用了小脚丫FPGA,实现了数字频率计与幅度测量仪的设计,它的主要功能为:使用ADC 模块(3PA1030),FPGA产生采样时钟 25MHZ,采样外部输入信号(正弦或方波)。   在 100 Hz–1 MHz 范围内测量信号频率,方法用过零检测。  计算峰峰值,对方波额外测量占空比。  七段数码管显示频率,OLED 显示幅度数值与条形图;按键切换量程与显示模式。  用 10 kHz 标准信号校准,给出标定步骤与测量误差说明。。
标签
嵌入式系统
FPGA
数字逻辑
ADC
huahuali
更新2026-03-24
19

所选任务介绍

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

项目介绍

  1. 使用ADC 模块(3PA1030),FPGA产生采样时钟 25MHZ,采样外部输入信号(正弦或方波)。  
  2. 在 100 Hz–1 MHz 范围内测量信号频率,方法用过零检测。 
  3. 计算峰峰值,对方波额外测量占空比。 
  4. 七段数码管显示频率,OLED 显示幅度数值与条形图;按键切换量程与显示模式。 
  5. 用 10 kHz 标准信号校准给出标定步骤与测量误差说明。  

项目硬件介绍

小脚丫FPGA:

Altera MAX10系列10M02SCM芯片

2000个LE资源,12kB用户闪存,108KBIT RAM;

16路硬件乘法器;

112个用户GPIO;

12Mhz时钟

image.png

image.png

3PA1030 ADC模块

3PA1030 是一款单电压芯片、10 位、50 MSPS模数转换器, 在时钟(CLK)的驱动下工作。

运放输入、输出电压关系:Vo  = -0.1Vi + 0.511V

ADC芯片输出的计数/511 * 0.5 = Vo  运放输出的电压

ADC芯片的输入电压被配置为0~1V,所以模块的输入电压范围是-5~+5V

image.png

七段数码管

核心板上有两位共阳极数码管。共阳端接高电平,在各个位段加上低电平信号就可以使相应的位段发光。

image.png

OLED

SPI总线(CLK、DATA、RES、DC),分辨率为128 * 64

image.png

按键

当按键未被按下时,连接到FPGA的管脚是高电平;当按键被按下时,连接到FPGA的管脚是低电平。

image.png

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

image.png

本项目基于ADC(3PA1030)与FPGA构建一个数字信号测量系统。FPGA 提供25MHz采样时钟驱动ADC,对100Hz-1MHz的输入信号(正弦波/方波)进行数字化。FPGA采用过零检测法计算信号频率,并实时计算信号峰峰值,对方波信号额外测量占空比。七段数码管显示待测信号频率的高两位,OLED屏幕显示待测信号的峰峰值、峰峰值柱形图、测量信号类型(正弦信号or方波信号)、测量信号的频率与占空比。用户可通过按键切换量程与待测信号的类型。

系统使用10kHz信号进行校准,通过对比测量值与标准值来修正系统误差,确保测量精度。

软件流程图及关键代码介绍

调试软件:Quartus 17.1

编程语言:verilog

image.png

峰峰值、频率计算模块:

通过25MHz采样时钟驱动ADC采集10位数据。内部状态机依次执行空闲、准备、测量、计算和显示五个状态。对于方波信号统计过零点数量计算其频率,对于正弦波信号统计其在最大值、最小值间的跳变次数计算其频率。同时捕获信号最大值和最小值以计算峰峰值,并通过高电平计数确定方波信号的占空比。用户可通过按键切换测量信号(正弦信号、方波信号)和两种闸门时间量程,最终输出频率、峰峰值和占空比结果供显示模块使用。

always @(posedge adc_clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
gate_counter <= 32'd0;
zero_cross_count <= 32'd0;
freq_result <= 32'd0;

max_value <= 10'd0;
min_value <= 10'd1023;
adc_data_last <= 10'd0;

sum <= 20'd0;
sample_count <= 22'd0;
vpp_result <= 32'd0;

high_cnt <= 32'd0;
duty_result <= 10'd0;

dynamic_threshold_high <= 10'd0;
dynamic_threshold_low <= 10'd1023;
edge_detected <= 1'b0;
last_max_value <= 10'd0;
last_min_value <= 10'd1023;

stable_counter <= 4'd0;
temp_max <= 10'd0;
temp_min <= 10'd1023;

end else begin
case (state)
IDLE: begin
// 初始化测量参数
if (clk_1hz) begin
state <= PREPARE;
end
end

PREPARE: begin
// 准备测量,清零计数器
gate_counter <= 32'd0;
zero_cross_count <= 32'd0;

max_value <= 10'd0;
min_value <= 10'd511;

sum <= 20'd0;
sample_count <= 22'd0;
high_cnt <= 32'd0;

//正弦波测量初始化
dynamic_threshold_high <= 10'd0;
dynamic_threshold_low <= 10'd1023;
edge_detected <= 1'b0;
last_max_value <= 10'd0;
last_min_value <= 10'd1023;
stable_counter <= 4'd0;

state <= MEASURE;
end

MEASURE: begin
gate_counter <= gate_counter + 32'd1;

// 幅度测量:更新最大值和最小值
if (adc_data > max_value) begin
max_value <= adc_data;
end
if (adc_data < min_value) begin
min_value <= adc_data;
end


if (sample_count < 32'd2_097_152) begin //18

if (adc_data < 10'd354) begin //1.65V作为中间值
high_cnt <= high_cnt + 32'd1;
end

sample_count <= sample_count + 32'd1;
end




if(measure_mode == 1'd0)begin //测量方波的频率
adc_data_last <= adc_data;

if((adc_data >= 10'd354 && adc_data_last < 10'd354) ||(adc_data <= 10'd354 && adc_data_last > 10'd354))

begin
zero_cross_count <= zero_cross_count + 16'd1; //一个闸门时间里的过零个数
end

end
else begin //测量正弦的频率


// 检查最大最小值是否稳定
if (((max_value >= temp_max - 10'd5) &&(max_value <= temp_max + 10'd5)) && ((min_value >= temp_min- 10'd5) && (min_value <= temp_min+ 10'd5))) begin
if (stable_counter == 4'd5) begin // 连续15次相同认为稳定
last_max_value <= max_value;
last_min_value <= min_value;

// 动态计算阈值(使用最大最小值的百分比)
dynamic_threshold_high <= last_max_value - ((last_max_value - last_min_value) >> 2); // 75%位置
dynamic_threshold_low <= last_min_value + ((last_max_value - last_min_value) >> 2); // 25%位置

// 检测上升沿(从低于低阈值到高于高阈值)
if (!edge_detected && adc_data >= dynamic_threshold_high) begin
edge_detected <= 1'b1;
zero_cross_count <= zero_cross_count + 1'd1;
end

// 检测下降沿(从高于高阈值到低于低阈值)
if (edge_detected && adc_data <= dynamic_threshold_low) begin
edge_detected <= 1'b0;
zero_cross_count <= zero_cross_count + 1'd1;
end

end
else begin
stable_counter <= stable_counter + 1;
temp_max <= max_value;
temp_min <= min_value;
end
end
else begin
stable_counter <= 4'd0;
temp_max <= max_value;
temp_min <= min_value;
end

end


case (range_sel)
1'b0: begin // 1秒闸门,适合测量低频
if (gate_counter >= 32'd25_000_000) begin

state <= CALCULATE;
end
end
1'b1: begin // 0.5秒闸门
if (gate_counter >= 32'd12_500_000) begin
state <= CALCULATE;
end
end
endcase
end

CALCULATE: begin
// 计算频率(单位:Hz)
// 每个周期有2个过零点
if(~measure_mode) begin
case (range_sel)
1'b0: freq_result <= zero_cross_count >> 1; // 除以2
1'b1: freq_result <= zero_cross_count; // 0.5秒闸门,乘以2
endcase
end else begin
case (range_sel)
1'b0: freq_result <= zero_cross_count >> 1; // 除以2
1'b1: freq_result <= zero_cross_count; // 0.5秒闸门,乘以2
endcase

end


// vpp_result <= (((max_value - min_value) * 32'd40080) >> 12) ;
vpp_result <= (((max_value - min_value) * 32'd38586) >> 12) - 32'd207;
// 计算占空比(单位:0.1%)
duty_result <= ((high_cnt*1000) >> 21);

state <= DISPLAY;
end


DISPLAY: begin

// 等待下一次测量
if (!clk_1hz) begin
state <= IDLE;
end
end

default: state <= IDLE;
endcase
end
end


OLED显示模块

待显示数据:12位幅度值(mv)、20位频率数据(hz)、8位占空比数据(%)、测量信号类型和量程选择。

  input    [11:0]     voltage_adc,  //幅度值输入,mv
input [19:0] Freq_data,
input [9:0] Duty_data,

input wire [0:0] measure_mode, //
input wire[0:0] range_sel , // 量程选择

通过组合逻辑将二进制测量数据转换为十进制数字并格式化为显示字符串。

reg [127:0] voltage_str;  // 电压字符串
reg [15:0] voltage_mv; // 电压值(毫伏)
reg [3:0] volt_int; // 电压整数部分
reg [3:0] volt_dec1; // 电压小数第1位
reg [3:0] volt_dec2; // 电压小数第2位
reg [3:0] volt_dec3; // 电压小数第3位


reg [15:0] remainder;
reg [15:0] temp_remainder;


always @(*) begin
voltage_mv = voltage_adc;
volt_int = voltage_mv / 1000; // 整数部分(0-3)
remainder = voltage_mv - (volt_int * 1000);

if (remainder >= 900) begin volt_dec1 = 4'd9; end
else if (remainder >= 800) begin volt_dec1 = 4'd8;end
else if (remainder >= 700) begin volt_dec1 = 4'd7;end
else if (remainder >= 600) begin volt_dec1 = 4'd6;end
else if (remainder >= 500) begin volt_dec1 = 4'd5;end
else if (remainder >= 400) begin volt_dec1 = 4'd4;end
else if (remainder >= 300) begin volt_dec1 = 4'd3;end
else if (remainder >= 200) begin volt_dec1 = 4'd2;end
else if (remainder >= 100) begin volt_dec1 = 4'd1;end
else volt_dec1 = 4'd0;

temp_remainder = remainder - (volt_dec1 * 100);
if (temp_remainder >= 90) begin volt_dec2 = 4'd9;end
else if (temp_remainder >= 80) begin volt_dec2 = 4'd8;end
else if (temp_remainder >= 70) begin volt_dec2 = 4'd7;end
else if (temp_remainder >= 60) begin volt_dec2 = 4'd6;end
else if (temp_remainder >= 50) begin volt_dec2 = 4'd5;end
else if (temp_remainder >= 40) begin volt_dec2 = 4'd4;end
else if (temp_remainder >= 30) begin volt_dec2 = 4'd3;end
else if (temp_remainder >= 20) begin volt_dec2 = 4'd2;end
else if (temp_remainder >= 10) begin volt_dec2 = 4'd1;end
else volt_dec2 = 4'd0;

volt_dec3 = temp_remainder - (volt_dec2 * 10);

voltage_str = {"volt:",
8'h30 + volt_int, // 整数部分
8'h30 + volt_dec1, // 小数第1位
8'h30 + volt_dec2, // 小数第2位
8'h30 + volt_dec3, // 小数第3位
"mv "}; // 空格填充
end




reg [127:0] data1_str; // 16字符
reg [127:0] data2_str; // 16字符
reg [31:0] temp;
reg [3:0] d5, d4, d3, d2, d1, d0;
reg [3:0] dd2,dd1, dd0;

always @(*) begin

d5 = 4'd0; d4 = 4'd0; d3 = 4'd0; d2 = 4'd0; d1 = 4'd0; d0 = 4'd0;
dd1 = 4'd0; dd0 = 4'd0;

temp = Freq_data;

// 十万位
if (temp >= 900000) d5 = 4'd9;
else if (temp >= 800000) d5 = 4'd8;
else if (temp >= 700000) d5 = 4'd7;
else if (temp >= 600000) d5 = 4'd6;
else if (temp >= 500000) d5 = 4'd5;
else if (temp >= 400000) d5 = 4'd4;
else if (temp >= 300000) d5 = 4'd3;
else if (temp >= 200000) d5 = 4'd2;
else if (temp >= 100000) d5 = 4'd1;

if (temp >= 100000) temp = temp - (d5 * 100000);

// 万位
if (temp >= 90000) d4 = 4'd9;
else if (temp >= 80000) d4 = 4'd8;
else if (temp >= 70000) d4 = 4'd7;
else if (temp >= 60000) d4 = 4'd6;
else if (temp >= 50000) d4 = 4'd5;
else if (temp >= 40000) d4 = 4'd4;
else if (temp >= 30000) d4 = 4'd3;
else if (temp >= 20000) d4 = 4'd2;
else if (temp >= 10000) d4 = 4'd1;

if (temp >= 10000) temp = temp - (d4 * 10000);

// 千位
if (temp >= 9000) d3 = 4'd9;
else if (temp >= 8000) d3 = 4'd8;
else if (temp >= 7000) d3 = 4'd7;
else if (temp >= 6000) d3 = 4'd6;
else if (temp >= 5000) d3 = 4'd5;
else if (temp >= 4000) d3 = 4'd4;
else if (temp >= 3000) d3 = 4'd3;
else if (temp >= 2000) d3 = 4'd2;
else if (temp >= 1000) d3 = 4'd1;

if (temp >= 1000) temp = temp - (d3 * 1000);

// 百位
if (temp >= 900) d2 = 4'd9;
else if (temp >= 800) d2 = 4'd8;
else if (temp >= 700) d2 = 4'd7;
else if (temp >= 600) d2 = 4'd6;
else if (temp >= 500) d2 = 4'd5;
else if (temp >= 400) d2 = 4'd4;
else if (temp >= 300) d2 = 4'd3;
else if (temp >= 200) d2 = 4'd2;
else if (temp >= 100) d2 = 4'd1;

if (temp >= 100) temp = temp - (d2 * 100);

// 十位
if (temp >= 90) d1 = 4'd9;
else if (temp >= 80) d1 = 4'd8;
else if (temp >= 70) d1 = 4'd7;
else if (temp >= 60) d1 = 4'd6;
else if (temp >= 50) d1 = 4'd5;
else if (temp >= 40) d1 = 4'd4;
else if (temp >= 30) d1 = 4'd3;
else if (temp >= 20) d1 = 4'd2;
else if (temp >= 10) d1 = 4'd1;

if (temp >= 10) temp = temp - (d1 * 10);

// 个位
d0 = temp;

// 处理数据2
temp = Duty_data;

if (temp >= 900) dd2 = 4'd9;
else if (temp >= 800) dd2 = 4'd8;
else if (temp >= 700) dd2 = 4'd7;
else if (temp >= 600) dd2 = 4'd6;
else if (temp >= 500) dd2 = 4'd5;
else if (temp >= 400) dd2 = 4'd4;
else if (temp >= 300) dd2 = 4'd3;
else if (temp >= 200) dd2 = 4'd2;
else if (temp >= 100) dd2 = 4'd1;

if (temp >= 100) temp = temp - (dd2 * 100);
// 十位
if (temp >= 90) dd1 = 4'd9;
else if (temp >= 80) dd1 = 4'd8;
else if (temp >= 70) dd1 = 4'd7;
else if (temp >= 60) dd1 = 4'd6;
else if (temp >= 50) dd1 = 4'd5;
else if (temp >= 40) dd1 = 4'd4;
else if (temp >= 30) dd1 = 4'd3;
else if (temp >= 20) dd1 = 4'd2;
else if (temp >= 10) dd1 = 4'd1;

if (temp >= 10) temp = temp - (dd1 * 10);

// 个位
dd0 = temp;

// 构建字符串
data1_str = {"freq:",
8'h30 + d5,
8'h30 + d4,
8'h30 + d3,
8'h30 + d2,
8'h30 + d1,
8'h30 + d0,
"hz "};

data2_str = {"duty:",
8'h30 + dd2,
8'h30 + dd1,
".",
8'h30 + dd0,
"% "};
end

第一行显示标题、第二显示电压数值、第三根据电压值动态显示条形图、第四显示刻度,后续分别显示测量模式、量程选择、频率和占空比等字符串。

						case(cnt_main)	//MAIN状态

5'd0: begin state <= INIT; end
5'd1: begin
y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00;
num <= 5'd16; char <= " frequency "; state <= SCAN;
end
5'd2: begin
y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00;
num <= 5'd16; char <= voltage_str;

state <= SCAN;
end
5'd3: begin
// 显示条形图
y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00;
num <= 5'd16;
if(voltage_adc >= 12'd3000) char <= "----------------";
else if(voltage_adc >= 12'd2800) char <= "-------------- ";
else if(voltage_adc >= 12'd2500) char <= "------------ ";
else if(voltage_adc >= 12'd2000) char <= "---------- ";
else if(voltage_adc >= 12'd1500) char <= "-------- ";
else if(voltage_adc >= 12'd1000) char <= "------- ";
else if(voltage_adc >= 12'd800) char <= "----- ";
else if(voltage_adc >= 12'd600) char <= "--- ";
else if(voltage_adc >= 12'd300) char <= "- ";
else char <= " ";
state <= SCAN;
end
5'd4: begin
y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00;
num <= 5'd16;
char <= "0 50 100 ";
state <= SCAN;
end


5'd5: //measure_mode
begin
y_p <= 8'hb4; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16;
if(measure_mode == 1'd0) char <= "square wave ";
else char <= "sine wave ";
state <= SCAN;
end

5'd6: //range_sel
begin
y_p <= 8'hb5; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16;

if(range_sel == 1'd0) char <= "100hz-10khz ";
else char <= "1khz-1mhz ";

state <= SCAN;
end

5'd7: begin y_p <= 8'hb6; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= data1_str;state <= SCAN; end
5'd8: begin y_p <= 8'hb7; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= data2_str;state <= SCAN; end


default: state <= IDLE;
endcase

按键消抖模块

该模块参考小脚丫社区代码,删去不必要的代码,减少资源占用按键消抖模块对输入按键进行同步、消抖和边沿检测,通过约10ms的稳定计时消除抖动,最终输出按键按下时的单周期脉冲信号以及可翻转的按键状态信号

module Debounce(clk,rst_n,key_n,key_pulse); 

input clk;
input rst_n;
input [1:0] key_n;
output [1:0] key_pulse;

reg [1:0] key_rst;
always @(posedge clk or negedge rst_n)
if (!rst_n) key_rst <= 2'b11;
else key_rst <= key_n;

wire key_an = (key_rst == key_n) ? 0 : 1;

reg [18:0] cnt;
always @ (posedge clk or negedge rst_n)
if (!rst_n) cnt <= 19'd0;
else if(key_an) cnt <= 19'd0;
else cnt <= cnt + 1'b1;

reg [1:0] low_sw;
always @(posedge clk or negedge rst_n)
if (!rst_n) low_sw <= 2'b11;
else if (cnt == 19'd500000)
low_sw <= key_n;

reg [1:0] low_sw_r;
always @ ( posedge clk or negedge rst_n )
if (!rst_n) low_sw_r <= 2'b11;
else low_sw_r <= low_sw;

wire [1:0] key_pulse;
assign key_pulse = low_sw_r & (~low_sw);

endmodule

数码管显示模块

该模块参考小脚丫社区代码。数码管显示模块存储了0~9十六进制数对应7段数码管编码值的查找表,将输入的两组4位二进制数据分别转换为驱动两个数码管各段亮灭的9位控制信号。

module Segment_led
(
input wire[3:0] seg_data_1, //四位输入数据信号
input wire[3:0]seg_data_2, //四位输入数据信号
output [8:0] Segment_led_1, //数码管1, MSB~LSB = SPGFEDCBA
output [8:0] Segment_led_2 //数码管2, MSB~LSB = SPGFEDCBA
);

reg[8:0] mem [9:0]; //存储7段数码管译码数据
initial
begin
mem[0] = 9'h3f; // 0
mem[1] = 9'h06; // 1
mem[2] = 9'h5b; // 2
mem[3] = 9'h4f; // 3
mem[4] = 9'h66; // 4
mem[5] = 9'h6d; // 5
mem[6] = 9'h7d; // 6
mem[7] = 9'h07; // 7
mem[8] = 9'h7f; // 8
mem[9] = 9'h6f; // 9
end

assign Segment_led_1 = mem[seg_data_1];
assign Segment_led_2 = mem[seg_data_2];

endmodule

功能展示图及说明

给ADC模拟端输入10khz,占空比50%,幅值3.3V的方波信号:

OLED实际显示占空比测量值49.9%,幅值测量值3342mv,频率为10khz,数码管显示频率的最高两位数值,按键key2可以设置频率范围。

image.png

将方波信号的幅值变为2.0v,幅值测量值为2142mv。

image.png

按下按键key1,切换为正弦波信号的测量。

给ADC模拟端输入10khz,幅值3.3V的正弦信号:

OLED实际显示幅值测量值3493mv频率为10002hz。(正弦波测量模式下,占空比数据无效)

image.png

将正弦信号的幅值变为2.0v,幅值测量值为2142mv。

image.png

通过以上步骤可知,本系统的频率和占空比测量值较为准确,下面通过两点标定法对峰峰值进行校准:

将峰峰值转换公式(((max_value - min_value) * 32'd40080) >> 12),改为(((max_value - min_value) * 32'd38544) >> 12) - 32'd61

标定后测试结果:

image.png

image.png

image.png

测量误差说明:10位ADC固有的量化误差导致最小分辨率约为3.2mV,信号噪声会影响最大值和最小值的捕获稳定性,而简单的常数减法校准方法无法补偿系统的非线性误差和温度漂移,因此电压测量结果不够准确。

FPGA资源占用报告:

image.png

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

fpga oled显示问题:fpga oled的驱动不同于mcu spi的编程逻辑,最终参照了小脚丫社区的oled显示代码,完成自己想要的界面显示。

adc电压值测量不准确问题:本工程没有对adc电压采集结果进行任何滤波,可能引入干扰。后续可以添加数字滤波抑制噪声,优化峰峰值计算公式以提高测量精度。

布线不通过问题:小脚丫这个fpga资源比较紧缺,代码稍微复杂一些,就会导致编译不通过,所以只能尽量精简逻辑。

心得体会

第一次接触Altera 的fpga,对编程环境不太熟悉,花了很大功夫去适应。同时也是第一次接触adc电压采集的项目,最终能完成项目要求很是开心。


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