FPGA:简易版数字频率计(Verilog)_简易频率计fpga
FPGA:简易版数字频率计(Verilog)
目录
- FPGA:简易版数字频率计(Verilog)
-
- 实验原理及要求
- 实验器材
- 实验思路
- 代码
-
- 分频器模块
- 计数器模块
-
- 思路
- 完整模块代码
- 数码管模块
- 顶层模块
实验原理及要求
所谓频率就是周期性信号在单位时间(1s)内变化的次数。若在一定时间间隔T(也称闸门时间)内测得这个周期性信号的重复变化次数为N,则其频率可表示为:f=N/T。由上面的表示式可以看到,若时间间隔T取1s,则f=N,但是这种频率计仅能测出频率大于或等于1Hz的情况,且频率越高,精度也越高。实际应用中,频率计的闸门时间是个可变量,当频率小于1Hz时,闸门时间就要适当放大。本实验中为了简化实验代码,闸门时间固定为1s,闸门信号是一个0.5Hz的方波,在闸门有效(高电平)期间,对输入的脉冲进行计数,在闸门信号的下降沿时刻,锁存当前的计数值(频率值),并且清零所有的频率计数器,显示的频率2s刷新一次。
实验器材
- DE2-115开发板:内置时钟是50MHz
- Quartus II :用于编写Verilog代码并进行仿真
实验思路
首先,根据试验要求,需要设计一个频率为0.5Hz的闸门信号,可以通过对50MHz时钟信号进行(100_000_000)分频得到。其次,我们需要一个输入,为了是实验内容更为简单,我们的输入信号也通过对50MHz时钟信号进行分频得到,可以通过设置不同的分频器数得到不同的输入信号。然后,我们需要一个计数器,用来统计输入信号的在闸门信号位于高电平时重复变化的次数即期频率,通过这个计数器,我们会得到一个数字。最后,输出我们需要接到数码管上,所以我们需要一个模块把得到数字显示在数码管上。综上,我们需要四个模块,包括分频器模块(得到输入信号和门闸信号)、计数器模块(核心模块)、数码管显示模块以及顶层模块。
代码
分频器模块
原理可参考FPGA分频器设计(支持奇偶数,空占比为50%) - CSDN App
module divider #( parameter DIVISOR = 50_000_000// 目标分频系数(支持奇偶数)) ( input wire clk, // 输入时钟 input wire rst, // 复位信号(高有效) output wire clk_div // 分频输出(50%占空比)); localparam IS_ODD = DIVISOR % 2; // 判断分频系数奇偶性 reg [31:0] cnt_pos, cnt_neg; // 上升沿/下降沿计数器 reg clk_pos, clk_neg; // 上升沿/下降沿分频时钟 // 奇数分频逻辑(占空比50%) generate if (IS_ODD) begin : odd_divider // 上升沿计数器(0到DIVISOR-1循环计数) always @(posedge clk or posedge rst) begin if (rst) begin cnt_pos <= 0; clk_pos <= 0; end else begin if (cnt_pos == DIVISOR - 1) begin // 计满后归零 cnt_pos <= 0; clk_pos <= ~clk_pos; // 同时翻转电平 end else begin if (cnt_pos == (DIVISOR - 1)/2) begin // 中间点翻转电平 clk_pos <= ~clk_pos; end cnt_pos <= cnt_pos + 1; // 计数器递增 end end end // 下降沿计数器(0到DIVISOR-1循环计数,独立于上升沿计数器) always @(negedge clk or posedge rst) begin if (rst) begin cnt_neg <= 0; clk_neg <= 0; end else begin if (cnt_neg == DIVISOR - 1) begin // 计满后归零 cnt_neg <= 0; clk_neg <= ~clk_neg; // 同时翻转电平 end else begin if (cnt_neg == (DIVISOR - 1)/2) begin // 中间点翻转电平 clk_neg <= ~clk_neg; end cnt_neg <= cnt_neg + 1; // 计数器递增 end end end assign clk_div = clk_pos | clk_neg; // 合并双沿产生的脉冲 end else begin : even_divider // 偶数分频逻辑(占空比50%) reg [31:0] even_counter; // 偶数分频计数器 reg even_clk; always @(posedge clk or posedge rst) begin if (rst) begin even_counter <= 0; even_clk <= 0; end else begin if (even_counter == DIVISOR/2-1) begin // 计满DIVISOR次后归零 even_counter <= 0; even_clk <= ~even_clk; // 翻转电平 end else begin even_counter <= even_counter + 1; // 计数器递增 end end end assign clk_div = even_clk; end endgenerateendmodule
计数器模块
思路
这段代码有三个输入,一个输出,三个输入分别是待测信号、闸门信号以及复位信号,输出是一个14位的二进制数字,用来表示待测信号的频率。关于这段代码,难点在于正确的更新输出,实验要求每2秒(其实就是在闸门信号的下降沿)更新数据。
起初我想的很简单,首先,定义一个中间变量reg [13:0] tempnum;
,然后这个变量对待测信号的上升沿敏感,在每次上升沿的时候检测闸门信号是否位于高电平,是则+1,不是则不进行操作。然后中间变量tempnum
和输出outnum
则对闸门信号的下降沿敏感,在检测到闸门信号的下降沿时将中间变量的值赋给输出outnum <= tempnum;
,然后清零计数器tempnum <= 14\'b0;
这样就实现了试验要求。但是这样做编译器会报错。Verilog 不允许在同一个 always 块中同时使用posedge和negedge检测不同信号,一个变量也不能同时对上升沿和下降沿敏感,这在硬件上时无法实现的。因此,我们需要采取迂回策略。
那么,问题来了,该怎样迂回,下降沿即信号从高电平跳变到低电平,那么,就有意思了,我们可以定义一个1位的reg类型变量(下文将其称为使能缓存)来储存闸门信号上一个时钟的状态,这时候,我们就要用到非阻塞赋值,它
会在当前时间步结束时同时更新所有赋值。基于此,我们就能实现间接检测下降沿信号了,首先,我们在待测信号的每一个上升沿更新使能缓存clken_prev <= clken;
,由于是非阻塞赋值,所以在当前语句块内,使能缓存仍是上一个周期的值,这时候我们可以检测闸门信号,如果闸门信号是高电平,那么中间变量tempnum加一,如果是低电平,就可以将将中间变量的值赋给输出,然后清零计数器,代码如下:
if (clken) begin tempnum <= tempnum + 1\'b1; // 计数值+1endelse if (clken_prev && !clken) begin outnum <= tempnum; tempnum <= 14\'b0;end
完整模块代码
module counter ( input wire clk, input wire clken, input wire rst, output reg [13:0] outnum );reg [13:0] tempnum;reg clken_prev;always @(posedge clk or posedge rst) begin if (rst) begin // 异步复位 tempnum <= 14\'b0; // 使用正确的位宽 outnum <= 14\'b0; clken_prev <= 1\'b0; end else begin clken_prev <= clken; // 先更新使能缓存 if (clken) begin tempnum <= tempnum + 1\'b1; // 使用1\'b1进行递增 end else if (clken_prev && !clken) begin outnum <= tempnum; tempnum <= 14\'b0; end endendendmodule
数码管模块
module seg_decoder( input wire [13:0] num, output reg [6:0] seg_tho, output reg [6:0] seg_hun, output reg [6:0] seg_ten, output reg [6:0] seg_one); always @(*) begin // 个位译码 case (num % 10) 4\'d0: seg_one = 7\'b1000000; 4\'d1: seg_one = 7\'b1111001; 4\'d2: seg_one = 7\'b0100100; 4\'d3: seg_one = 7\'b0110000; 4\'d4: seg_one = 7\'b0011001; 4\'d5: seg_one = 7\'b0010010; 4\'d6: seg_one = 7\'b0000010; 4\'d7: seg_one = 7\'b1111000; 4\'d8: seg_one = 7\'b0000000; 4\'d9: seg_one = 7\'b0010000; default: seg_one = 7\'b1111111; endcase // 十位译码 case ((num%100) / 10) 4\'d0: seg_ten = 7\'b1000000; 4\'d1: seg_ten = 7\'b1111001; 4\'d2: seg_ten = 7\'b0100100; 4\'d3: seg_ten = 7\'b0110000; 4\'d4: seg_ten = 7\'b0011001; 4\'d5: seg_ten = 7\'b0010010; 4\'d6: seg_ten = 7\'b0000010; 4\'d7: seg_ten = 7\'b1111000; 4\'d8: seg_ten = 7\'b0000000; 4\'d9: seg_ten = 7\'b0010000; default: seg_ten = 7\'b1111111; endcase // 百位译码 case ((num%1000) / 100) 4\'d0: seg_hun = 7\'b1000000; 4\'d1: seg_hun = 7\'b1111001; 4\'d2: seg_hun = 7\'b0100100; 4\'d3: seg_hun = 7\'b0110000; 4\'d4: seg_hun = 7\'b0011001; 4\'d5: seg_hun = 7\'b0010010; 4\'d6: seg_hun = 7\'b0000010; 4\'d7: seg_hun = 7\'b1111000; 4\'d8: seg_hun = 7\'b0000000; 4\'d9: seg_hun = 7\'b0010000; default: seg_hun = 7\'b1111111; endcase // 千位译码 case (num / 1000) 4\'d0: seg_tho = 7\'b1000000; 4\'d1: seg_tho = 7\'b1111001; 4\'d2: seg_tho = 7\'b0100100; 4\'d3: seg_tho = 7\'b0110000; 4\'d4: seg_tho = 7\'b0011001; 4\'d5: seg_tho = 7\'b0010010; 4\'d6: seg_tho = 7\'b0000010; 4\'d7: seg_tho = 7\'b1111000; 4\'d8: seg_tho = 7\'b0000000; 4\'d9: seg_tho = 7\'b0010000; default: seg_tho = 7\'b1111111; endcase endendmodule
顶层模块
module FreDivider( input wire clk, // 系统信号 input wire rst, output wire [6:0] seg_thos, output wire [6:0] seg_huns, output wire [6:0] seg_tens, output wire [6:0] seg_ones); // 这里添加分号结束模块端口列表声明 // 声明中间信号 wire clk_in; wire clken; wire [13:0] num; // 时钟分频模块实例化 divider #( .DIVISOR(50_000) ) clk_divin ( .clk(clk), .rst(rst), .clk_div(clk_in) ); // 这里添加分号结束实例化语句 divider #( .DIVISOR(100_000_000) ) clk_diven ( .clk(clk), .rst(rst), .clk_div(clken) ); // 计数器实例化 counter count ( .clk(clk_in), .clken(clken), .rst(rst), .outnum(num) ); // 这里添加分号结束实例化语句 seg_decoder seg_fre ( .num(num), .seg_tho(seg_thos), .seg_hun(seg_huns), .seg_ten(seg_tens), .seg_one(seg_ones) ); // 这里添加分号结束实例化语句endmodule