1. 项目需求
· 通过小脚丫FPGA核心板上的2个数码管和轻触按键制作一个秒表,通过按键来控制秒表的功能,并在数码管上显示数值。
· 使用七段显示器作为输出设备,在小脚丫FPGA核心板上创建一个2位数秒表。
· 秒表应从 0.0 秒计数到 9.9秒,然后翻转,计数值每0.1秒精确更新一次。
· 秒表使用四个按钮输入:开始、停止、增量和清除(重置)。
· 开始输入使秒表开始以10Hz时钟速率递增(即每0.1秒计数一次)。
· 停止输入使计数器停止递增,但使数码管显示当前计数器值。
· 每次按下按钮时,增量输入都会导致显示值增加一次,无论按住增量按钮多长时间。
· 复位/清除输入强制计数器值为零。
2. 需求分析
1. 功能描述:
- 实现一个秒表,具备计时、开始、停止、增加计数、清零等功能。
- 使用两个数码管显示秒表的计时数值,精确到0.1秒。
2. 硬件平台:
- 使用小脚丫FPGA核心板作为硬件平台。
- 数码管作为输出设备。
3. 输入设备:
- 使用四个按钮作为输入设备,分别是:开始、停止、增量和清零按钮。
4. 计时精度:
- 计时精度为0.1秒,即每0.1秒更新一次计数值。
5. 功能操作:
- 开始按钮: 按下开始按钮后,秒表开始计时,以10Hz的时钟速率递增。
- 停止按钮: 按下停止按钮后,秒表停止计时,但数码管显示当前计数器的值。
- 增量按钮: 每次按下增量按钮,都会使秒表的显示值增加一次,无论按钮按住多长时间。
- 清零按钮: 按下清零按钮后,秒表的计数器值被强制重置为零。
6. 数码管显示:
- 使用两个数码管显示秒表的计数值。
- 计数范围为0.0秒到9.9秒,超过9.9秒后重0.0开始计算。
7. 时钟频率:
- 时钟频率应满足秒表精度的要求,即每0.1秒更新一次计数值。
8. 设计思路:
- 使用FPGA的逻辑实现秒表的计时和控制功能。
- 设计不同按钮的输入和对应的功能操作。
- 使用计数器来生成0.1秒的时间基准。
- 将计数值转换为数码管的显示格式,并输出到数码管。
9. 实现方法:
- 使用硬件描述语言(如Verilog或VHDL)编写FPGA的逻辑代码。
- 设计适当的模块,包括计时模块、状态机模块、数码管驱动模块等。
- 进行仿真验证,并在FPGA上进行硬件验证和调试。
10.性能要求:
- 确保秒表的计时精度和稳定性。
- 响应按钮操作的延迟应尽可能小,用户体验良好。
- 按钮操作反馈应及时准确,确保用户操作流畅。
通过以上分析,我们可以制定出实现该秒表项目的具体计划和步骤。
3. 实现的方式
按照需求分析,我们可以把秒表的主要功能分成三个部分,其中包括主模块(main)、分频器模块(divide)和数码管模块(LED)。我们所使用的硬件描述语言是Verilog,下面将会讲解各个模块大体的逻辑。
1. Main
这个是主模块,负责了秒表的整体逻辑,也负责连接分频器模块和数码管模块。这段Verilog硬件描述语言代码实现了一个基本的秒表功能,使用FPGA来控制数码管显示计时。
输入信号包括时钟信号 clk,开始信号 start,暂停信号 pause,增加信号 increase,重置信号 reset。输出信号为两个数码管的显示输出 seg_out_1 和 seg_out_2。使用两个4位寄存器 num_1 和 num_2 分别存储数码管显示的前后两位数字。使用一个单比特寄存器 sign_start 来表示秒表开始信号。使用一个单比特寄存器 sign 来表示是否能增加的信号。
LED模块的连接是将数码管的输入输出连接到 LED 模块,通过 seg_data_1 和 seg_data_2 输入数码管的数字,通过 seg_led_1 和 seg_led_2 输出到数码管。时钟分频的连接则是使用了一个分频器模块 divide 将输入时钟信号分频为更低的频率,这里分频为 1MHz 的时钟信号 clk1h。
代码里有使用 always 对按钮输入和时钟信号进行响应。它会根据不同的按钮输入和秒表状态,控制数码管显示的数字。对于状态控制,代码也实现了秒表的开始、暂停、增加和重置功能。当开始按钮被按下时,秒表开始计时,显示的数字会逐渐增加;当暂停按钮被按下时,秒表暂停,显示当前的计时值;当增加按钮被按下时,显示的数字会增加;当重置按钮被按下时,秒表的计时值被重置为零。
数码管的显示里是使用了两个数码管显示秒表的计时值,范围为0.0秒到9.9秒。在初始化块中对寄存器进行了初始化,将 num_1 和 num_2 初始化为 0,sign_start 初始化为 1,sign 初始化为 1。我们也使用了循环控制,即同时使用了多个 if-else 语句来控制秒表在不同状态下数码管显示的数字。
这段代码提供了一个基本框架,实现了基本的秒表功能,可以作为秒表功能的起点,但在实际应用中可能需要根据具体需求进行进一步的修改和扩展,也应该会有进一步的优化空间。
2. Divide
这段 Verilog 代码实现了一个可分频的时钟模块,也就是分频器模块。它可以将输入时钟信号分频得到一个较低频率的时钟输出。
输入信号包括时钟信号 clk 和复位信号 rst_n。输出信号为分频后的时钟信号 clkout。WIDTH 定义了计数器的位数,影响了计数的最大值。N 定义了分频系数,必须确保 N < 2^WIDTH - 1,否则计数会溢出。cnt_p 和 cnt_n 分别为上升沿和下降沿触发时的计数器。clk_p 和 clk_n 分别为上升沿和下降沿触发时的分频时钟。
我们通过 always 块实现对计数器的更新,根据时钟信号和复位信号进行触发和清零操作。我们也分别使用 always 块生成上升沿和下降沿触发时的分频时钟输出。上升沿触发时的分频时钟为 clk_p,下降沿触发时的分频时钟为 clk_n。由于分频系数的奇偶性不同,分频时钟的生成方式也不同。我们使用条件判断表达式 assign 选择时钟输出方式,当分频系数 N 为 1 时直接输出输入时钟信号 clk,否则根据分频系数奇偶性选择输出。
该模块可以方便地实现任意整数倍的时钟分频功能,根据输入时钟和分频系数的不同,可以得到不同频率的输出时钟信号。代码使用了寄存器来记录计数,通过复位信号进行清零,保证了稳定的时钟分频功能。该模块可以在 FPGA 中应用于需要特定频率时钟信号的场景,如时序逻辑、状态机等设计中。
3. LED
这段 Verilog 代码实现了一个简单的数码管驱动模块,用于控制两个数码管的显示。
输入信号包括两个数码管的数字输入 seg_data_1 和 seg_data_2。输出信号为两个数码管的LED控制信号 seg_led_1 和 seg_led_2。
代码使用了两个数组 seg_1 和 seg_2 分别存储了数码管显示的数字的LED控制信号。每个数字对应一个9位的LED控制信号,根据共阴极数码管的特性定义了不同数字的LED亮灭状态。
在 initial 块中对两个数组进行了初始化,为每个数字赋予了对应的LED控制信号。对于LED控制的信号输出则是使用 assign 语句将输入的数码管数字映射到对应的LED控制信号上。这样,当输入不同的四位数字时,就可以输出对应的九位LED控制信号,驱动数码管显示相应的数字。
该模块实现了简单的数码管驱动功能,可以方便地控制两个数码管显示不同的数字。通过初始化数组来定义数码管显示的数字与LED控制信号的映射关系,使得代码更加清晰易懂。此外,该模块也使用了外部逻辑,也就是main模块来控制输入的数码管数字,以实现具体的显示功能。
4. 代码(内嵌到报告中)及说明
Main.v
module main(
input clk, //定义时钟信号
input start, //定义开始信号
input pause, //定义暂停信号
input increase, //定义增加信号
input reset, //定义重置信号
output [8:0] seg_out_1, //定义前一个数码管输出
output [8:0] seg_out_2 //定义后一个数码管输出
);
reg[3:0] num_1; //定义第一个寄存器
reg[3:0] num_2; //定义第二个寄存器
reg sign_start; //定义秒表开始信号
reg sign; //定义是否能增加的信号
initial begin //初始化
num_1 = 4'd0; //数码管第一个数字为0
num_2 = 4'd0; //数码管第二个数字为0
sign_start = 1; //定义开始信号为1
sign = 1; //定义能否增加的信号为1
end
LED display(
.seg_data_1(num_1), //LED模块前寄存器的输入
.seg_data_2(num_2), //LED模块后寄存器的输入
.seg_led_1(seg_out_1), //LED模块前数码管的输出
.seg_led_2(seg_out_2) //LED模块后数码管的输出
);
wire clk1h;
divide #(.WIDTH(32), .N(1200000))u2(
.clk(clk), //输入时钟信号
.rst_n(reset), //重置时钟信号
.clkout(clk1h) //输出时钟信号
);
always @(posedge clk1h or negedge reset or negedge start or negedge pause) begin
if(!reset) begin //如果按了重置按钮,则进入该循环
num_1 <= 4'd0; //把前数码管的数字变回0
num_2 <= 4'd0; //把后数码管的数字变回0
sign_start <= 1'd0; //把开始的信号定义为0
end
else if (!start) begin //如果按了开始按钮,则进入该循环
sign_start <= 1'd1; //把开始的信号定义为1
end
else if (!pause) begin //如果按了暂停按钮,则进入该循环
sign_start <= 1'd0; //把开始的信号定义为0
end
else if (!increase && sign_start == 0) begin //如果按了增加按钮且秒表暂停了就进入以下循环
if (sign) begin
sign <= 1'd0; //把能否开始的信号定义为0
if (num_2 == 4'd9) begin //如果后数码管为9,则进入该循环
num_2 <= 4'd0; //把后数码管的数字变回0
if (num_1 == 4'd9) begin //如果前数码管为9,则进入该循环
num_1 <= 4'd0; //把前数码管的数字变回0
end
else begin
num_1 <= num_1 + 4'd1; //如果前数码管不为9,则加1
end
end
else begin
num_2 <= num_2 + 4'd1; //如果后数码管不为9,则加1
end
end
end
else if (increase) begin
sign <= 1'd1; //把能否开始的信号定义回到1,这样按一次按钮只会加一次数字,不会无限增加
end
else if (sign_start == 1) begin //如果开始信号为1,则进入该循环
if (num_2 == 4'd9) begin //如果后数码管为9,则进入该循环
num_2 <= 4'd0; //把后数码管的数字变回0
if (num_1 == 4'd9) begin //如果前数码管为9,则进入该循环
num_1 <= 4'd0; //把前数码管的数字变回0
end
else begin
num_1 <= num_1 + 4'd1; //如果前数码管不为9,则加1
end
end
else begin
num_2 <= num_2 + 4'd1; //如果后数码管不为9,则加1
end
end
end
endmodule
Divide.v
module divide (clk,rst_n,clkout); // Module Function:任意整数时钟分频
input clk, rst_n; //输入信号,其中clk连接到FPGA的C1脚,频率为12MHz
output clkout; //输出信号,可以连接到LED观察分频的时钟
//parameter是verilog里常数语句
parameter WIDTH = 24; //计数器的位数,计数的最大值为 2**WIDTH-1
parameter N = 12_000_000;
//分频系数,请确保 N < 2**WIDTH-1,否则计数会溢出
reg [WIDTH-1:0] cnt_p, cnt_n;
//cnt_p为上升沿触发时的计数器,cnt_n为下降沿触发时的计数器
reg clk_p, clk_n;
//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;
//计数器一直计数,当计数到N-1的时候清零,这是一个模N的计数器
end
//上升沿触发的分频时钟输出,如果N为奇数得到的时钟占空比不是50%;
//如果N为偶数得到的时钟占空比为50%
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
clk_p <= 0;
else if (cnt_p < (N >>1 )) //N>>1表示右移一位,相当于除以2去掉余数
clk_p <= 0;
else
clk_p <= 1; //得到的分频时钟正周期比负周期多一个clk时钟
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
//下降沿触发的分频时钟输出,和clk_p相差半个时钟
always @(negedge clk) begin
if(!rst_n)
clk_n <= 0;
else if (cnt_n < (N >>1))
clk_n <= 0;
else
clk_n <= 1; //得到的分频时钟正周期比负周期多一个clk时钟
end
assign clkout = (N == 1) ? clk : (N[0]) ? (clk_p & clk_n) : clk_p;
//条件判断表达式
//当N=1时,直接输出clk
//当N为偶数也就是N的最低位为0,N(0)=0,输出clk_p
//当N为奇数也就是N最低位为1,N(0)=1,输出clk_p&clk_n。正周期多所以是相与
endmodule
LED.v
module LED(seg_data_1,seg_data_2,seg_led_1,seg_led_2);
input [3:0] seg_data_1;
//数码管需要显示0~9十个数字,所以最少需要4位输入做译码
input [3:0] seg_data_2;
//小脚丫上第二个数码管,也就是后面的数码管,
//数码管需要显示0~9十个数字,所以最少需要4位输入做译码
output [8:0] seg_led_1;
//在小脚丫上控制一个数码管需要9个信号 MSB~LSB=DIG、DP、G、F、E、D、C、B、A
output [8:0] seg_led_2;
//在小脚丫上第二个数码管的控制信号 MSB~LSB=DIG、DP、G、F、E、D、C、B、A
reg [8:0] seg_1 [9:0];
//定义了一个reg型的数组变量,
//相当于一个10*9的存储器,存储器一共有10个数,每个数有9位宽
reg [8:0] seg_2 [9:0];
//定义了第二个reg型的数组变量,
//相当于一个10*9的存储器,存储器一共有10个数,每个数有9位宽
initial //在过程块中只能给reg型变量赋值,
//Verilog中有两种过程块always和initial
//initial和always不同,其中语句只执行一次
begin
seg_1[0] = 9'hbf; //对存储器中第一个数赋值9'b00_1011_1111,
//相当于共阴极接地,DP点变高亮起,7段显示数字 0.
seg_1[1] = 9'h86; //7段显示数字 1.
seg_1[2] = 9'hdb; //7段显示数字 2.
seg_1[3] = 9'hcf; //7段显示数字 3.
seg_1[4] = 9'he6; //7段显示数字 4.
seg_1[5] = 9'hed; //7段显示数字 5.
seg_1[6] = 9'hfd; //7段显示数字 6.
seg_1[7] = 9'h87; //7段显示数字 7.
seg_1[8] = 9'hff; //7段显示数字 8.
seg_1[9] = 9'hef; //7段显示数字 9.
seg_2[0] = 9'h3f; //对存储器中第一个数赋值9'b00_0011_1111,
//相当于共阴极接地,DP点变低不亮,7段显示数字 0
seg_2[1] = 9'h06; //7段显示数字 1
seg_2[2] = 9'h5b; //7段显示数字 2
seg_2[3] = 9'h4f; //7段显示数字 3
seg_2[4] = 9'h66; //7段显示数字 4
seg_2[5] = 9'h6d; //7段显示数字 5
seg_2[6] = 9'h7d; //7段显示数字 6
seg_2[7] = 9'h07; //7段显示数字 7
seg_2[8] = 9'h7f; //7段显示数字 8
seg_2[9] = 9'h6f; //7段显示数字 9
end
assign seg_led_1 = seg_1[seg_data_1];
//对第一个数码管连续赋值,这样输入不同四位数,就能输出对于译码的9位输出
assign seg_led_2 = seg_2[seg_data_2];
//对第二个数码管连续赋值,这样输入不同四位数,就能输出对于译码的9位输出
endmodule
5. FPGA的资源利用说明
6. 实现结果
7. 总结
这次的活动是一个很不错的体验,我学到了很多东西。从零开始学习如何设计秒表是一个很困难的事,幸好有小脚丫平台上提供的援助。在一开始我毫无头绪时,它帮助我实现了时钟频率和数码管显示的问题。未来我可能会加入防抖动的功能,解决因按钮输入时出现抖动而导致系统误判按钮操作。当然我也可能参加别的项目,努力进步自己。