2024年寒假练 - 基于Lattice MXO2的小脚丫FPGA核心板 实现具有启动、停止、递增和清除功能的秒表
1.项目需求
通过小脚丫FPGA核心板上的2个数码管和轻触按键制作一个秒表,通过按键来控制秒表的功能,并在数码管上显示数值。使用七段显示器作为输出设备,在小脚丫FPGA核心板上创建一个2位数秒表。 秒表应从0.0秒计数到9.9秒,然后翻转,计数值每0.1秒精确更新一次。秒表使用三个按钮输入:开始、停止、增量和清除(重置)。 开始输入使秒表开始以10Hz时钟速率递增(即每0.1秒计数一次); 停止输入使计数器停止递增,但使数码管显示当前计数器值; 每次按下按钮时,增量输入都会导致显示值增加一次,无论按住增量按钮多长时间; 复位/清除输入强制计数器值为零。
2.完成的功能及达到的性能
3.实现思路
功能框图
秒表功能需要使用以下硬件模块:
- 分频器模块(Divide):用于生成1Hz的时钟信号,以便将秒表的计时精度控制在0.1秒。这个模块会将输入时钟信号分频为1Hz。
- 数码管显示模块(Display):这个模块用于控制七段数码管的显示,根据输入的数据信号来确定每个数码管的显示内容。
- 秒表控制模块(状态机)(Timer Control):这个模块将处理按钮输入信号,并根据按钮的状态控制秒表的功能,如开始、停止、增量和清除,
- 计时器模块(Timer):这个模块用于实际计时功能,它会根据1Hz的时钟信号进行计数,并根据按钮输入信号的控制进行计数增加或停止。
- 按键输入检测模块(Button Input Detector):用于检测按钮输入的状态,以便触发相应的功能。
在 Verilog 中,需要用到以下语法和结构:
- 模块声明:使用
module
关键字声明模块。 - 端口声明:在模块中声明输入和输出端口,使用
input
和output
关键字。 - 参数声明:使用
parameter
关键字声明参数,用于定义模块的常量或配置选项。 - 寄存器声明:使用
reg
关键字声明寄存器,用于在时序逻辑中存储数据。 - 初值块:使用
initial
关键字定义初值块,用于在仿真开始时初始化变量。 - 过程块:使用
always
关键字定义过程块,用于描述组合逻辑或时序逻辑。 - 时钟边沿敏感块:在过程块中使用
posedge
或negedge
关键字定义时钟边沿敏感块,用于描述时钟触发的行为。 - 分配语句:使用
assign
关键字进行连续赋值,用于将表达式赋值给线。 - 实例化模块:使用模块名和端口列表实例化模块,将模块连接到设计中的其他部分。
- 延迟:使用
#
符号指定延迟,例如#10
表示 10 个时间单位的延迟。 - 连接运算符:使用
.
符号进行连接,将模块的端口与线或寄存器连接起来。
本片中我讲详细介绍如何实现 分频器模块 数码管显示模块 时钟边沿敏感块与状态机逻辑
分频器模块
以下内容引用自小脚丫fpga官方论坛
时钟信号的处理是FPGA的特色之一,因此分频器也是FPGA设计中使用频率非常高的基本设计之一。一般在FPGA中都有集成的锁相环可以实现各种时钟的分频和倍频设计,但是通过语言设计进行时钟分频是最基本的训练,在对时钟要求不高的设计时也能节省锁相环资源。在本实验中我们将实现任意整数的分频器,分频的时钟保持50%占空比。
1,偶数分频:偶数倍分频相对简单,比较容易理解。通过计数器计数是完全可以实现的。如进行N倍偶数分频,那么通过时钟触发计数器计数,当计数器从0计数到N/2-1时,输出时钟进行翻转,以此循环下去。
2,奇数分频: 如果要实现占空比为50%的奇数倍分频,不能同偶数分频一样计数器记到一半的时候输出时钟翻转,那样得不到占空比50%的时钟。以待分频时钟CLK为例,如果以偶数分频的方法来做奇数分频,在CLK上升沿触发,将得到不是50%占空比的一个时钟信号(正周期比负周期多一个时钟或者少一个时钟);但是如果在CLK下降沿也触发,又得到另外一个不是50%占空比的时钟信号,这两个时钟相位正好相差半个CLK时钟周期。通过这两个时钟信号进行逻辑运算我们可以巧妙的得到50%占空比的时钟。
总结如下:对于实现占空比为50%的N倍奇数分频,首先进行上升沿触发进行模N计数,计数选定到某一个值进行输出时钟翻转,然后经过(N-1)/2再次进行翻转得到一个占空比非50%奇数n分频时钟。再者同时进行下降沿触发的模N计数,到和上升沿触发输出时钟翻转选定值相同值时,进行输出时钟时钟翻转,同样经过(N-1)/2时,输出时钟再次翻转生成占空比非50%的奇数n分频时钟。两个占空比非50%的n分频时钟进行逻辑运算(正周期多的相与,负周期多的相或),得到占空比为50%的奇数n分频时钟。
引用结束
通过上述文档我们不难看出,对于偶数倍分频来说,实现相对简单,通过计数器进行计数即可。例如,需要进行进行4倍偶数分频时,可以在计数器从0计数到4时输出一个时钟脉冲,然后循环执行这个过程。
但对于奇数倍分频,情况稍微复杂一些。为了保持50%的占空比,不能简单地在计数器达到一半时翻转时钟信号。具体来说,可以通过在时钟信号的上升沿和下降沿触发计数器,利用它们的相位差来实现50%的占空比。通过适当设置计数器的阈值,在两个时钟信号的相位差达到半个时钟周期时,进行时钟翻转,从而实现占空比为50%的奇数倍分频。
本案例中分频器代码如下(对应模块divide)
module divide (
input clk, rst_n,
output reg clkout
);
parameter WIDTH = 25; // Counter width
parameter N = 1200000; // Division factor, ensure N < 2**WIDTH-1 to avoid overflow
reg [WIDTH-1:0] cnt_p, cnt_n;
reg clk_p, clk_n;
always @ (posedge clk or negedge rst_n)
begin
if (!rst_n)
cnt_p <= 0;
else if (cnt_p == (N-1))
cnt_p <= 0;
else
cnt_p <= cnt_p + 1;
end
always @ (posedge clk or negedge rst_n)
begin
if (!rst_n)
clk_p <= 0;
else if (cnt_p < (N>>1))
clk_p <= 0;
else
clk_p <= 1;
end
always @ (negedge clk or negedge rst_n)
begin
if (!rst_n)
cnt_n <= 0;
else if (cnt_n == (N-1))
cnt_n <= 0;
else
cnt_n <= cnt_n + 1;
end
always @ (negedge clk)
begin
if (!rst_n)
clk_n <= 0;
else if (cnt_n < (N>>1))
clk_n <= 0;
else
clk_n <= 1;
end
assign clkout = (N == 1) ? clk : (N[0]) ? (clk_p & clk_n) : clk_p;
endmodule
调用该模块所用到的代码
// Module instantiation format
divide #(.WIDTH(4), .N(11)) u1 (
.clk(clk), // Input-output signal connections. .clk represents the signal name defined in the module itself; (clk) represents the stimulus signal we define here.
.rst_n(rst_n), // The signal names defined in the testbench may differ from the port signal names of the module being instantiated.
.clkout(clkout)
);
数码管显示模块
小脚丫FPGA核心板上所连接的数码管如下图所示共阴8段数码管的信号端低电平有效,而共阳端接高电平有效。当共阳端接高电平时只要在各个位段上加上相应的低电平信号就可以使相应的位段发光。比如:要使a段发光,则在a段信号端加上低电平即可。共阴极的数码管则相反。 可以看到数码管的控制和LED的控制有相似之处
数码管所有的信号都连接到FPGA的管脚,作为输出信号控制。FPGA只要输出这些信号就能够控制数码管的那一段LED亮或者灭。这样我们可以通过开关来控制FPGA的输出,下面是数码管显示的表格
通过上表我们不难看出可以通过二维数组来存储这些数据一下为实现代码对应模块(Display)
module Display
(
input [3:0] seg_data_1, // Input data for 7-segment display 1
input [3:0] seg_data_2, // Input data for 7-segment display 2
output [8:0] seg_led_1, // Output for 7-segment display 1
output [8:0] seg_led_2 // Output for 7-segment display 2
);
reg [8:0] seg1 [9:0]; // Register array for 7-segment display 1
reg [8:0] seg2 [9:0]; // Register array for 7-segment display 2
// Initialize segment patterns for display 1 and display 2
initial
begin
seg1[0] = 9'hbf;
seg1[1] = 9'h86;
seg1[2] = 9'hdb;
seg1[3] = 9'hcf;
seg1[4] = 9'he6;
seg1[5] = 9'hed;
seg1[6] = 9'hfd;
seg1[7] = 9'h87;
seg1[8] = 9'hff;
seg1[9] = 9'hef;
seg2[0]= 9'h3f;
seg2[1]= 9'h06;
seg2[2]= 9'h5b;
seg2[3]= 9'h4f;
seg2[4]= 9'h66;
seg2[5]= 9'h6d;
seg2[6]= 9'h7d;
seg2[7]= 9'h07;
seg2[8]= 9'h7f;
seg2[9]= 9'h6f;
end
// Assign segment patterns to the respective 7-segment displays based on input data
assign seg_led_1 = seg1[seg_data_1]; // Display 7-segment pattern based on input data for display 1
assign seg_led_2 = seg2[seg_data_2]; // Display 7-segment pattern based on input data for display 2
endmodule
需要提醒的是小脚丫FPGA核心板上的数码管含有一位DP 小数点 DIG 背光控制 本文在管脚分配章节在做介绍
状态机逻辑
接下来我们来分析如何实现秒表计数功能
Start condition:当开始信号 start_key为低电平时,秒表开始运行,显示状态被设置为运行状态。
Stop condition:当停止信号 stop_key 为低电平时,秒表停止运行,显示状态被设置为停止状态。
Reset condition:当复位信号 rst_key为低电平时,秒表被重置,所有显示数据被清零,显示状态被设置为停止状态。
Increase condition:如果增加信号 increase 为低电平,并且显示状态为停止状态,则秒表的显示数据递增。如果当前秒数显示数据不是 9,则仅递增秒数;如果秒数显示数据已经是 9,则秒数归零并且分钟显示数据递增。
Overflow condition:当显示状态为运行状态且分钟和秒钟显示数据都为 9 时,表示秒表已经达到最大值,此时秒表停止,并且所有显示数据被清零。
Increment carry condition:当显示状态为运行状态且秒钟显示数据已经是 9 时,表示秒钟已经到达 9,需要进位到分钟。此时秒钟归零,分钟递增。
Regular increment condition:当显示状态为运行状态时,秒表正常运行,秒钟显示数据每次递增 1。
为实现以上所诉状态机逻辑 需要用到以下代码:
// 控制逻辑
always @ (posedge clock_1hz or negedge reset or negedge start or negedge stop)
begin
if (!reset)
begin
data_display1 <= 4'd0;
data_display2 <= 4'd0;
display_status <= 1'd0;
display_increase <= 1'd0;
end
else if(!start)
begin
display_status <= 1'd1;
end
else if(!stop)
begin
display_status <= 1'd0;
end
else if(!increase)
begin
display_increase <= 1'd1;
end
else if(display_increase == 1 && display_status == 0)
begin
if(data_display2 != 9)
begin
data_display2 <= data_display2 + 1;
display_increase <= 1'd0;
end
else
begin
data_display2 <= 0;
data_display1 <= data_display1 + 1;
display_increase <= 1'd0;
end
end
else if(data_display1 == 9 && data_display2 == 9)
begin
display_status = 0;
data_display1 = 0;
data_display2 = 0;
display_increase <= 1'd0;
end
else if(display_status == 1 && data_display2 == 9)
begin
data_display2 <= 4'd0;
data_display1 <= data_display1 + 1;
end
else if(display_status == 1)
begin
data_display2 <= data_display2 + 1;
end
end
时钟边沿敏感块
Verilog 中的时钟边沿敏感块通常使用 always @(posedge clk) 或 always @(negedge clk) 语句来实现。接下来我将会详细介绍always@块如何使用
always @ (sensitivity_list)
begin
// 在这里编写逻辑代码
end
sensitivity_list:敏感性列表,包含了在这个 always @ 块中会检测变化的信号。当列表中的信号发生变化时,always @ 块中的逻辑将会执行。可以是时钟信号,也可以是其他信号,取决于逻辑的需要。在 always @ 块中,如果没有指定敏感性列表,则表示该块内的逻辑与任何信号的变化都无关,会在任何信号变化时都执行。因此如果我想要实现本次历程需要设置一下敏感信号
posedge_clk1h
:检测时钟信号clk1h
的上升沿。negedge_rst
:检测复位信号rst
的下降沿。negedge_start
:检测开始信号start
的下降沿。negedge_stop
:检测停止信号stop
的下降沿。
always @ (posedge clk1h or negedge rst or negedge start or negedge stop)
我们在时钟边沿敏感块中写入逻辑代码即可实现上诉所有功能
4.硬件引脚分配
这里推荐一个网站可以实现fpga在线编程下载与引脚分配
5.GPT资源利用情况
经过GPT优化后的代码资源利用很低
Total number of LUT4s=(Number of logic LUT4s)+2×(Number of distributed RAMs)+2×(Number of ripple logic)