使用FPGA实现频率检测(频率鉴别)
使用FPGA实现频率检测通常由两种方式,周期测量法和频率测量法。周期测量法是测量信号一个周期内基准时钟的个数,频率测量法是测量固定时间内有多少个信号周期。
虽然频率测量法测量高频信号时精度更高,但其需要一个闸门时间,响应速度不够快,另外我想是实现的是可以区分1khz、10khz、高电平和低电平的功能,可以说是一个鉴频器,而不是一个频率计,所以根据我的需求说说我的思路和实现方式。
我的基本思路是利用采样时钟对输入信号的周期和一个周期内的高电平进行计数,将测量值和不同频率的阈值范围进行比较,从而鉴别该输入信号的频率。这个过程不得不涉及到两个问题,一是信号频率识别范围,一是信号占空比的范围。信号传输过程中尤其是远距离传输必然会发生一些失真,所以需要对频率和占空比设计一个合理值,防止误识别。根据需求,我将频率识别范围设置为±10%,占空比设置为±5%,因为我设计的是频率区分,所以范围设计的稍宽,好处就是对不同的环境、温度漂移等容忍性更高。
在实际使用代码时,遇到一些问题,比如当信号出现高频干扰,代码有时会将1khz误识别为10khz,又进行了一些优化,现记录一下目前的逻辑处理流程。
1、信号预处理。
我使用了20mhz的外部晶振,系统时钟也选用了该时钟频率。一般情况下高频干扰是ns级别,几ns到几百ns,我将干扰持续时间设置为2us,足够消除高频干扰。具体做法是在每个时钟上升沿,将当前的输入电平与内部已锁存的信号进行比对。如果输入电平发生变化,启动滤波计数器。如果输入电平在 40个时钟周期(2µs) 内发生抖动,计数器清零,变化被忽略。代码如下:
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
glitch_filter_cnt <= 0;
signal_filtered <= 1'b0;
signal_in_d1 <= 1'b0;
end else begin
signal_in_d1 <= signal_in; // 同步打拍
if (signal_in_d1 == signal_filtered) begin
// 输入与当前输出一致,复位计数器
glitch_filter_cnt <= 0;
end else begin
// 输入电平发生变化,开始计时
if (glitch_filter_cnt < GLITCH_FILTER_CYCLES) begin
glitch_filter_cnt <= glitch_filter_cnt + 1;
end else begin
// 当电平连续稳定维持超过2µs,允许翻转
signal_filtered <= signal_in_d1;
glitch_filter_cnt <= 0;
end
end
end
end
2、测量计数。
对经过滤波的信号进行处理,实时检测信号的上升沿和下降沿,通过两个相邻上升沿完成周期测量,通过上升沿和紧邻的下降沿完成脉宽测量,如果输入信号一直没变化,则设置超时时间,让计数器一直累加,达到超时时间后判断是高电平还是低电平。
边沿检测代码:
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
edge_detect_reg <= 2'b00;
else
edge_detect_reg <= {edge_detect_reg[0], signal_filtered};
end
// 01 -> 上升沿, 10 -> 下降沿
assign is_rising_edge = (edge_detect_reg == 2'b01);
assign is_falling_edge = (edge_detect_reg == 2'b10);
计数器逻辑:
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
period_cnt <= 0;
high_width_cnt <= 0;
watchdog_cnt <= 0;
dc_level_cnt <= 0;
end else begin
// 看门狗计数器
// 只要有边沿会清零看门狗,否则一直累加直到超时时间
if (is_rising_edge || is_falling_edge || is_watchdog_timeout) begin
watchdog_cnt <= 0;
end else begin
watchdog_cnt <= watchdog_cnt + 1;
end
// 周期计数器
// 上升沿清零,重新开始测量
if (is_rising_edge) begin
period_cnt <= 0;
end else if (period_cnt < COUNTER_MAX) begin
period_cnt <= period_cnt + 1;
end
// 高电平脉宽计数器
// 上升沿清零,仅在高电平期间累加
if (is_rising_edge) begin
high_width_cnt <= 0;
end else if (signal_filtered) begin
if (high_width_cnt < COUNTER_MAX) begin
high_width_cnt <= high_width_cnt + 1;
end
end
// 直流电平统计 (DC Level Statistic)
// 用于超时后判断高电平还是低电平
if (is_watchdog_timeout || is_rising_edge || is_falling_edge) begin
dc_level_cnt <= 0;
end else if (signal_filtered) begin
dc_level_cnt <= dc_level_cnt + 1;
end
end
end
assign is_watchdog_timeout = (watchdog_cnt == WATCHDOG_LIMIT_CYCLES);
3、状态输出。
最终的状态我定义了4种状态,分别用00表示低电平,11表示高电平,01表示10khz,10表示1khz,高低电平属于故障状态,应具有较高优先级,也就是上文提到的看门狗超时时间,我定义的是1.5ms,也就是比1khz的周期再多半个。然后在再次检测到上升沿时判断频率信号的情况,如果周期在0.9ms~1.1ms 之间,且脉宽在0.45ms~0.55ms之间就输出10,如果周期在 90µs~110µs 之间,且脉宽在45µs~55µs之间就输出01,否则输出11,如果既没有发生超时,也不是上升沿时刻,那么输出信号保持上一状态不变。
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
freq_status_out <= STATUS_DC_LOW;
end else begin
if (is_watchdog_timeout) begin
// 高优先级
if (dc_level_cnt < DC_DETECT_THRESHOLD)
freq_status_out <= STATUS_DC_LOW; // 低电平
else
freq_status_out <= STATUS_UNKNOWN; // 高电平
end
else if (is_rising_edge) begin
// 低优先级,检测1khz
if ((period_cnt > PERIOD_MIN_1K) && (period_cnt < PERIOD_MAX_1K) &&
(high_width_cnt > WIDTH_MIN_1K) && (high_width_cnt < WIDTH_MAX_1K)) begin
freq_status_out <= STATUS_1K;
end
// 检测10khz
else if ((period_cnt > PERIOD_MIN_10K) && (period_cnt < PERIOD_MAX_10K) &&
(high_width_cnt > WIDTH_MIN_10K) && (high_width_cnt < WIDTH_MAX_10K)) begin
freq_status_out <= STATUS_10K;
end
// 其他情况输出11
else begin
freq_status_out <= STATUS_UNKNOWN;
end
end
// 如果没有超时,也没有检测到上升沿,锁存
end
end
上面是我优化后的代码的核心内容,但这份代码还存在一些问题,就是仅仅测量一次就输出结果,没有经过滤波处理,后续应该加上几级滤波处理再输出更好一些。另外,目前的代码只能检测两种频率信号,后续可以增加多频检测功能,比如1kHz,2kHz,5kHz,10kHz,修改相对来说简单,因为逻辑和基本框架已经搭建好了,但如果检测更高频率的信号,比如1mhz,需要注意高频干扰的2us不合适,会把1mhz信号当作干扰滤掉,需要更小滤波深度,比如200ns。
附上一张流程图:

链接:我的代码文件https://download.ZEEKLOG.net/download/xinzhong123456/92555674